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
plugin-form: a New form offers Owner, shows the picked user, and then drops the pick — the create payload strips owner_id, so the record is saved under the creator #12359
Filing gate ①: a user-visible defect, reproduced in a browser. reach: the console, measured on objectstack-ai/hotcrm (@objectstack/console 18.0.0-next.0) by the dev of objectstack-ai/hotcrm#2097 (report on that card, PR objectstack-ai/hotcrm#2099): Leads → New as sales.manager (a position holding allowTransfer and modifyAllRecords on crm_lead). Pick "Nina Reyes" in Lead Owner: the chip "Remove Nina Reyes" renders, no error shows. Click Create: POST /api/v1/data/crm_lead answers 201, its body carries no owner_id key, and the saved row's owner_id is the manager. Identical with the form entry required (hotcrm 2f3d898d) and not required (PR objectstack-ai/hotcrm#2099).
Filed by the repo:hotcrm seat (session_01P8mZuzuyZAHSrgQKmv6ASv). ⛔ Not a claim.
Reader: objectui triage, then the lane that owns packages/plugin-form.
Dedupe (MCP search_issues, objectui, open + closed): "create form owner_id picked owner dropped stripped sanitizeFormData new record saved under creator" → 3 hits: objectui#12205 (closed: the EDIT form drops a changed owner, fixed for edit only), objectui#12082 (open: create-only grants disable fields) and objectui#10563 (closed: wizard edit arms skip the sanitizer). None is the create path.
packages/plugin-form/src/sanitize.ts: owner_id is in SERVER_OWNED_FIELD_NAMES (L77), and stripUnwritable admits it back only when admitOwner is true (L178), which only dirtyEditPayload passes (REASSIGNABLE_OWNER_FIELD, L156, objectui#12205). So every create drops it, whatever the user picked.
The dev's reading, not re-verified here: DrawerForm.tsx and writePayload.ts have the same shape, so every New form that shows owner_id (Account, Opportunity, Contact, Lead in hotcrm) drops the pick. Only Lead was measured.
Why it matters
The form offers an editable Owner on create and confirms the pick on screen, then silently saves something else. A manager creating a lead for a rep gets a lead they own, with no error, and has to find and reassign it.
Fix direction (for the implementer to measure, ⛔ not a ruling)
On create, send owner_id when the user set it, the same admission objectui#12205 gave the edit path, and let the server's transfer check judge it (a caller without the grant gets the server's refusal rather than a silent swap). objectui#10108's rule, that an untouched owner is never sent, stays as is.
Or, if a create should never carry an owner, the form does not offer the field as editable on create. ⛔ Never offered and then dropped.
Acceptance
On a New form, an owner the user picks is either saved as that owner or refused loudly; an untouched Owner is still not sent.
Read fresh. On objectui main2312bd9a6b, sanitize.ts keeps owner_id in SERVER_OWNED_FIELD_NAMES (:67) and admits it only through admitOwner (:159), which only the edit path passes (objectui#12205). Every create drops a picked owner.
priority:p2: the form confirms the pick on screen, then silently saves the record under the creator. A manager creating for a rep has to find the lead and reassign it. Nothing leaks, and the server rules still apply.
Claim: PM loop round 1
Session: session_01XVp19E1MYUTFM3dnkwz8gB
Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-12359-create-owner-pick
Worktree: objectui-issue-12359
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/plugin-form/src/sanitize.ts, packages/plugin-form/src/ModalForm.tsx, packages/plugin-form/src/DrawerForm.tsx, packages/plugin-form/src/writePayload.ts (the create branches), and, revised 2026-10-14T17:10Z after the dev measured that writePayload.ts cannot fix its callers alone, the create callers ObjectForm.tsx, TabbedForm.tsx, SplitForm.tsx and WizardForm.tsx in packages/plugin-form/src/, and their tests (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus (default tier; dispatch-gates --tier --repo objectstack-ai/objectui reads no path-derived mandate)
Clause-②: no
Responsibility: objectui's plugin-form produces it (the create payload strips a picked owner_id) | the platform's allowTransfer check already judges an owner on a write; it never sees this one because the client drops it | every user with a New form that shows Owner; measured on HotCRM Leads as sales.manager
Thread-read: 6164838956
Serial constraints cleared: objectui#12205 (edit-path owner admission) is closed and on main. No in-flight domain:ui claim names sanitize.ts, ModalForm.tsx, DrawerForm.tsx or writePayload.ts (read 2026-10-14T15:49Z).
{
"issue": 12359,
"status": "rework",
"branch": "claude/issue-12359-create-owner-pick",
"pr": "https://github.com/objectstack-ai/objectui/pull/12378",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB (subagent; the parent's id)",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main df05784: a create drops the owner_id the user picked, because sanitizeFormData strips it and only dirtyEditPayload admits it back. New createOwnerPayload in sanitize.ts reuses the edit path's changedOwner and stripUnwritable(admitOwner) and judges the owner against the values the create form OPENED with, which is the seedCreateValues result. ModalForm (the console's New dialog) and DrawerForm pass formData, which on a create is the seed alone; a pick is sent, and a seeded owner left alone (the acting user from a current_user default, or a caller prefill) is not. formWritePayload gains an optional createBaseline source and puts a picked owner into payload, because ObjectForm's create writes payload rather than writePayload. Status is rework, and the PR says 'Part of #12359' instead of 'Fixes': A1/A2 measured that writePayload.ts cannot fix its callers by itself. ObjectForm, TabbedForm, SplitForm and WizardForm must each pass createBaseline, and those four files are outside the claimed file surface, so I stopped there as the order requires. WizardForm's formData also accumulates step data (mergedData) and needs a separate seed. Until those callers pass a baseline they behave exactly as before.",
"tests": "All at ae10afa. (1) Targeted vitest under the lock, 8 files (createOwnerPick-12359, editOwnerChange-12205, systemManagedPayload, sanitize, currentUserDefault, createDefaults, formArmsWritePayload-10563, editDirtyPayload-10156): 'Test Files 8 passed (8) / Tests 207 passed (207)'; lock VERDICT command-exit 0. (2) Under the same lock: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build exited 0, then pnpm --filter @object-ui/plugin-form type-check exited 0 ('tsc --noEmit && tsc -p tsconfig.test.json' echoed); tsconfig.test.json --listFilesOnly lists the new test (count 1). (3) Narrowing, unlocked and declared: 53 plugin-form test files that name ModalForm, DrawerForm, formWritePayload, sanitize, createOwnerPayload or a modal/drawer formType, run with --maxWorkers=2: 'Test Files 53 passed (53) / Tests 1036 passed (1036)'. NOT MEASURED: the full plugin-form test project, because the lock gave queue-timeout 99 four times while issue-12370 shard4 and issue-12372 full.sh held it. The new pin is a .test.tsx file in the dom project and uses no vi.mock. (4) eslint --no-inline-config --format json over the 5 changed source and test files: 5 files, 0 errors, 60 warnings, all no-explicit-any or existing react-hooks warnings. The population is the files changed from merge-base; eslint.config.js has no projectService, so lint is not type-aware and the diff cannot move the verdict on untouched files. (5) Exit 0 for the pre-tsc battery (phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity), check:new-line-citations, check-changeset-fixed and check-changeset-presence. check:spec-floors EXIT=1 with 'The scan collapsed: 0 package(s) declare @objectstack/spec', the known objectui#12334, recorded and not fixed. (6) Two ablations through objectstack scripts/ablation-replace.mjs; tests read source, so no dist step. Each mutation landed (anchor 1 to 0, blob changed) and each restore checked blob == HEAD ff49f0b81ac7 with git diff HEAD empty. Ablation (a): createOwnerPayload never adds the pick gives 8 failed / 13 passed, every 'is sent' row red. Ablation (b): changedOwner(owner, {}) ignores the seed gives 4 failed / 17 passed, every 'seeded owner not sent' row red (the A3 pin). A4: a mocked 403 FORBIDDEN on create shows 'You don't have permission to save this record.', onSuccess is not called and the dialog stays open.",
"mcp_calls": "0",
"api_writes": "4. Two branch pushes: the empty-branch probe and ae10afa. One POST /repos/objectstack-ai/objectui/pulls (draft PR #12378). One POST /repos/objectstack-ai/objectui/issues/12378/assignees (baozhoutao). This os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/12359/comments, is the fifth write. No label writes: objectui has no gate that reads labels and the order named none.",
"gates": {"targeted-vitest-8": 0, "closure-build": 0, "type-check": 0, "narrowed-plugin-form-53": 0, "full-plugin-form-project": "NOT MEASURED (lock queue-timeout x4)", "eslint-changed-files": 0, "pre-tsc-battery-14": 0, "check:new-line-citations": 0, "check:spec-floors": 1, "check-changeset-fixed": 0, "check-changeset-presence": 0},
"files_changed": [".changeset/12359-create-owner-pick.md", "packages/plugin-form/src/sanitize.ts", "packages/plugin-form/src/ModalForm.tsx", "packages/plugin-form/src/DrawerForm.tsx", "packages/plugin-form/src/writePayload.ts", "packages/plugin-form/src/createOwnerPick-12359.test.tsx"],
"deviations": [
"PR first line is 'Part of #12359', not the 'Fixes #12359' the order asked for. Merging it should not close the card while the ObjectForm, Tabbed, Split and Wizard create paths still drop a pick.",
"A2 assumption disproved: fixing writePayload.ts does not fix its four callers, because each must hand its opening values over. Those caller files are outside the file surface; stopped and reported rather than editing them.",
"The full plugin-form project was not run before push (lock never acquired); I ran the declared 53-file narrowing instead.",
"Clause-②: no, confirmed. createOwnerPayload and FormWriteSources are not exported from the plugin-form entry (index.tsx), and no prop or accept set moves."
],
"open_questions": [
{
"question": "Who lands the remaining half of #12359, which is passing createBaseline from ObjectForm (finalDefaultValues), TabbedForm and SplitForm (formData), and WizardForm (a seed ref held apart from mergedData)?",
"options": [
"A: extend this claim's file surface to those four files and send a patch round on this branch, so the PR can become 'Fixes #12359'",
"B: land this PR as Part of #12359 and dispatch the four callers as a new round on the same card"
],
"recommendation": "A. The change is mechanical, one source field per caller plus a seed ref in WizardForm, pinned the same way. It closes the card in one PR and lets createBaseline become required so a future caller cannot forget it."
}
],
"out_of_scope_findings": [
"carrier: whoever takes the remaining half · ModalForm's write branch tests schema.mode === 'create', while its load effect and formWritePayload use isCreateFormMode (mode create, or no recordId). A modal with no mode and no recordId seeds like a create but writes through dirtyEditPayload, so it skips omitServerResolvedDefaults (#4069). Not exercised; noted, not filed."
]
}
{
"issue": 12359,
"status": "done",
"branch": "claude/issue-12359-create-owner-pick",
"head": "609a32b",
"pr": "https://github.com/objectstack-ai/objectui/pull/12378",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB (subagent; the parent's id)",
"premise_still_valid": true,
"summary": "Patch round, option A. I merged origin/main into the branch (merge commit 46fcae4, no rebase, no force-push), then committed 609a32b. All six create writers now send a picked owner and leave a seeded owner off the wire. ModalForm and DrawerForm pass formData. ObjectForm passes formOpeningValues(schemaDefaults, initialData): the defaults block moved above handleSubmit, finalDefaultValues now goes through the same helper, and the callback deps gain only the memoised schemaDefaults. TabbedForm and SplitForm pass their seeded formData. WizardForm passes createSeedRef, set by a module-level holdCreateSeed at each of its three seed sites and held apart from the step-merged formData. FormWriteSources.createBaseline is now REQUIRED as Record or null. The only ripple was editOwnerChange-12205.test.tsx, a test inside the surface, which now passes createBaseline: null on its edit calls. A flat fields list (simple or mobile stepper) never renders the owner, because auto-layout filters system fields, so it has nothing to pick. I did not PATCH the PR body; the revised body, which starts 'Fixes #12359', is in the report comment for the seat to write (see deviations).",
"tests": "All at 609a32b. (1) Full plugin-form project under the lock: 'Test Files 183 passed (183) / Tests 2244 passed | 1 skipped (2245)'; VERDICT command-exit 0, held 414s. (2) Targeted run under the lock: the 8 files from round 1 plus WizardForm.regression, WizardForm.successBehavior, ObjectForm and subformHosts: 'Test Files 12 passed (12) / Tests 238 passed (238)'. (3) Under the same lock: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build exited 0, then pnpm --filter @object-ui/plugin-form type-check (tsc --noEmit plus tsconfig.test.json) exited 0. This is the compile proof that every formWritePayload caller passes the now-required createBaseline. (4) createOwnerPick-12359 has 33 tests. The new rows are a describe.each over ObjectForm simple, tabbed, split and wizard: a pick is sent, a seeded current_user creator is not sent, and a pick over the seed is sent. Together with editOwnerChange-12205 that file pair passes 61 of 61. (5) Ablations through objectstack scripts/ablation-replace.mjs. Every mutation landed (anchor 1 to 0) and every restore checked blob == HEAD with git diff HEAD empty. WizardForm 'createBaseline: createSeedRef.current' replaced by null: 2 failed (the wizard rows 'a picked owner is sent' and 'a pick over the seeded creator is sent'). ObjectForm's formOpeningValues baseline replaced by null: 2 failed (the matching simple rows). WizardForm baseline replaced by mergedData: 2 failed, which shows the step-merged formData cannot serve as the seed. (6) eslint --no-inline-config --format json over the 10 changed .ts/.tsx files: 10 files, 0 errors. Every react-hooks/exhaustive-deps warning in it is on an effect or callback this diff does not touch; the handleSubmit callbacks show none. eslint.config.js has no projectService, so lint is not type-aware. (7) Pre-tsc battery (14 named checks), check:new-line-citations, check-changeset-fixed and check-changeset-presence: all EXIT=0. check:spec-floors EXIT=1, the known objectui#12334.",
"mcp_calls": "0",
"api_writes": "This round: 1 git push (609a32b, which carries merge 46fcae4) and this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/12359/comments. Round 1 total was 5 (2 pushes, POST /pulls, POST /issues/12378/assignees, os-dev-report comment 6174036224). No PR body PATCH and no label writes.",
"gates": {"full-plugin-form-project": 0, "targeted-vitest-12": 0, "closure-build": 0, "type-check": 0, "eslint-changed-files-10": 0, "pre-tsc-battery-14": 0, "check:new-line-citations": 0, "check:spec-floors": 1, "check-changeset-fixed": 0, "check-changeset-presence": 0},
"files_changed": [".changeset/12359-create-owner-pick.md", "packages/plugin-form/src/sanitize.ts", "packages/plugin-form/src/writePayload.ts", "packages/plugin-form/src/ModalForm.tsx", "packages/plugin-form/src/DrawerForm.tsx", "packages/plugin-form/src/ObjectForm.tsx", "packages/plugin-form/src/TabbedForm.tsx", "packages/plugin-form/src/SplitForm.tsx", "packages/plugin-form/src/WizardForm.tsx", "packages/plugin-form/src/createOwnerPick-12359.test.tsx", "packages/plugin-form/src/editOwnerChange-12205.test.tsx"],
"deviations": [
"PR body not updated by me. The order asked for the update, but the role file forbids a dev PATCH of the PR body ('PR 正文 dev 只写一次 … ⛔ 不 PATCH;事后要改的报告点名改法,席位代写'), and the role file wins on conflict. The revised body, which starts 'Fixes #12359' then 'Clause-②: no' and names all six writers, is the second fenced block of this report comment. It is ready for the seat to write with PATCH /repos/objectstack-ai/objectui/pulls/12378 {body}. The live body still starts 'Part of #12359'.",
"The changeset now names all six writers. It is a patch for @object-ui/plugin-form.",
"Clause-②: no, re-confirmed. formOpeningValues and holdCreateSeed are module-private; createOwnerPayload and FormWriteSources are not exported from the plugin-form entry."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: whoever next edits ModalForm's write branch · that branch tests schema.mode === 'create', while its load effect uses isCreateFormMode. Noted, not filed, as the PM ruled."
]
}
Fixes #12359
Clause-②: no
## What was wrong`sanitizeFormData` drops `owner_id` from every payload, and only the edit path added a changed owner back (objectui#12205, `dirtyEditPayload`). A New form whose layout renders Owner offered a live lookup and showed the picked user's chip. Then it created the record with no `owner_id` key, so the server stamped the creator, answered 201 and showed no error.
## What changed-`sanitize.ts`: new `createOwnerPayload(payload, seed, owner)`. It judges the owner against the values the create form OPENED with (`seed`), using the same `changedOwner` judgement and the same `stripUnwritable(..., admitOwner)` door as the edit path. No second strip list and no second equality rule.
- Sent: a picked owner that differs from the seed.
- Not sent: an owner the form was seeded with and the user left alone (the acting user from a `current_user` default, or a caller prefill), an `undefined` owner, or any owner when the baseline is `null`. The rule from objectui#10108 is unchanged.
- Still refused: the caller's field-level security and an owner the object declares `readonly`.
-`writePayload.ts`: `FormWriteSources.createBaseline` is now REQUIRED (`Record | null`), so a layout cannot forget it. On a create, `formWritePayload` adds a picked owner to `payload`, because a create in `ObjectForm` writes `payload`, and then runs `omitServerResolvedDefaults`.
- The client does not decide whether the caller may transfer ownership. On insert, plugin-security admits `owner_id` equal to the acting user, auto-stamps an empty owner, and refuses another user without `allowTransfer` / `modifyAllRecords`. That refusal reaches the form through its normal error path, which a test pins.
## The six create writers in plugin-form| Writer | Baseline it passes ||---|---||`ModalForm` (the console's New dialog) |`formData`, which on a create is the seed alone ||`DrawerForm`|`formData`, which on a create is the seed alone ||`ObjectForm` simple |`formOpeningValues(schemaDefaults, initialData)`. `finalDefaultValues` now goes through the same helper, and the defaults block moved above `handleSubmit`||`TabbedForm`| its seeded `formData`||`SplitForm`| its seeded `formData`||`WizardForm`|`createSeedRef`, set by `holdCreateSeed` at each of its three seed sites. It is held apart from `formData`, which merges every step's answers |`MasterDetailForm`'s parent goes through `ObjectForm`'s `submitHandler` seam. A flat `fields` list (simple or mobile stepper) never renders the owner, because auto-layout filters system fields, so it has nothing to pick.
## Tests`createOwnerPick-12359.test.tsx` (dom project) covers:
- unit rows for `createOwnerPayload` and `formWritePayload`;
-`ModalForm` and `DrawerForm`: a pick is sent, an empty Owner is not sent, a seeded creator is not sent, and a pick over the seed is sent;
-`ObjectForm` simple, tabbed, split and wizard: a pick is sent, a seeded creator is not sent, and a pick over the seed is sent;
-`ModalForm` create refused with a 403: the error shows, `onSuccess` is not called, and the dialog stays open.
`editOwnerChange-12205.test.tsx` now passes `createBaseline: null` on its edit calls, since the field is required.
Ablations, all through `ablation-replace.mjs`. Every mutation landed, and every restore was checked with blob == HEAD and an empty `git diff HEAD`.
- At `ae10afa`:
-`createOwnerPayload` never adds the pick: 8 failed. Every "is sent" row went red.
-`changedOwner(owner, {})` ignores the seed: 4 failed. Every "seeded owner is not sent" row went red.
- At `609a32b`:
-`WizardForm``createBaseline: null`: 2 failed (wizard pick rows).
-`ObjectForm``createBaseline: null`: 2 failed (simple pick rows).
-`WizardForm``createBaseline: mergedData`: 2 failed. Using the step-merged values as the baseline drops the pick, which is why the seed ref exists.
Gates at `609a32b`:
- Full plugin-form project under the lock: 183 files passed, 2244 tests passed, 1 skipped.
- Targeted run, 12 files: 238 passed.
-`pnpm --filter '@object-ui/plugin-form^...' build`, then `pnpm --filter @object-ui/plugin-form type-check`: exit 0.
- eslint narrowed to the 10 changed files: 0 errors. The config has no type-aware linting.
- Pre-tsc battery, `check:new-line-citations`, `check-changeset-fixed` and `check-changeset-presence`: exit 0.
-`check:spec-floors`: exit 1, the known objectui#12334.
## Acceptance notes- An owner a caller passes through `initialData` counts as part of the seed, not as a pick. It is not sent unless the user changes it.
- Not fixed here, because it is a different defect: `ModalForm`'s write branch tests `schema.mode === 'create'`, while its load effect uses `isCreateFormMode`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB---_Generated by [Claude Code](https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB)_
Reviewed on GitHub at head 609a32b, against triage's scope (6164838956) and this card's Acceptance, not against the dev's report.
Shape: the PR is a draft against main. Its body opens Fixes #12359, the only closing keyword, followed by Clause-②: no. The seat wrote that body from the dev's report, because the role file has the dev write the PR body once and never PATCH it. The governed sweep reads NOT governed.
No contract review is owed.createOwnerPayload, FormWriteSources, formOpeningValues and holdCreateSeed are not on the plugin-form entry. No published export, prop or accept set moves.
Triage's scope is met, through the one admission objectui#12205 gave the edit path.createOwnerPayload reuses changedOwner and stripUnwritable(…, admitOwner), with no second strip list and no second equality rule. It judges the owner against the values the create form opened with:
a pick is sent;
a seeded owner left alone is not, which keeps objectui#10108's rule (the acting user from a current_user default, or a caller's initialData);
field-level security and a readonly owner still refuse it.
All six create writers are covered, after one patch round that widened the claim's file surface in place:ModalForm, DrawerForm, and the simple, tabbed, split and wizard layouts of ObjectForm.
FormWriteSources.createBaseline is now required, so a future layout cannot forget it, and type-check proves every caller passes it.
WizardForm holds its seed apart from the step-merged data. An ablation that feeds mergedData as the baseline turns the wizard rows red.
The client does not judge permission. The server's insert owner_id guard decides, as allowTransfer's spec description states ("ENFORCED via the insert/update owner_id guard"). A mocked 403 surfaces as the form's error, the dialog stays open, and onSuccess is not called.
Prose, checked sentence by sentence: the changeset (@object-ui/plugin-form: patch) matches the diff, including the six writers, the seeded-owner rule and the server's transfer judgement.
Evidence at 609a32b:
the full plugin-form project under the lock: 183 files, 2244 tests;
type-check after the closure build;
three ablations, each turning its rows red.
Out-of-scope finding (noted, not filed):ModalForm's write branch tests schema.mode === 'create' while its load effect uses isCreateFormMode. That is a different defect, not exercised here.
CI at head: 30 success, 3 skipped, 0 failures (filter=latest), all 12 test shards included.
Activity
objectstack-fleet commentedon Oct 14, 2026
Triage: first grade ·
bug·priority:p2·domain:ui·area:records·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T09:05ZRead fresh. On objectui
main2312bd9a6b,sanitize.tskeepsowner_idinSERVER_OWNED_FIELD_NAMES(:67) and admits it only throughadmitOwner(:159), which only the edit path passes (objectui#12205). Every create drops a picked owner.priority:p2: the form confirms the pick on screen, then silently saves the record under the creator. A manager creating for a rep has to find the lead and reassign it. Nothing leaks, and the server rules still apply.owner_idrequired, which blocks the insert-time stamp that already makes the creator the owner hotcrm#2097 (p2) waits on it.owner_idwhen the user set it, through the same admission console: changing or clearing the owner in a record's Edit form is silently dropped — the PATCH omits owner_id entirely (inline edit of the same field works) #12205 gave edit (the server'sallowTransferjudgement stays the gate). Re-measureDrawerForm.tsxandwritePayload.tsfor the same shape, and pin a create with and without a pick.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-12359-create-owner-pickWorktree:
objectui-issue-12359Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/plugin-form/src/sanitize.ts,packages/plugin-form/src/ModalForm.tsx,packages/plugin-form/src/DrawerForm.tsx,packages/plugin-form/src/writePayload.ts(the create branches), and, revised 2026-10-14T17:10Z after the dev measured thatwritePayload.tscannot fix its callers alone, the create callersObjectForm.tsx,TabbedForm.tsx,SplitForm.tsxandWizardForm.tsxinpackages/plugin-form/src/, and their tests (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: no
Responsibility:
objectui's plugin-form produces it (the create payload strips a picked owner_id) | the platform's allowTransfer check already judges an owner on a write; it never sees this one because the client drops it | every user with a New form that shows Owner; measured on HotCRM Leads as sales.managerThread-read: 6164838956
Serial constraints cleared: objectui#12205 (edit-path owner admission) is closed and on
main. No in-flightdomain:uiclaim namessanitize.ts,ModalForm.tsx,DrawerForm.tsxorwritePayload.ts(read 2026-10-14T15:49Z).Generated by Claude Code
baozhoutao commentedon Oct 14, 2026
os-dev-report
{ "issue": 12359, "status": "rework", "branch": "claude/issue-12359-create-owner-pick", "pr": "https://github.com/objectstack-ai/objectui/pull/12378", "session": "session_01XVp19E1MYUTFM3dnkwz8gB (subagent; the parent's id)", "premise_still_valid": true, "summary": "Premise confirmed on origin/main df05784: a create drops the owner_id the user picked, because sanitizeFormData strips it and only dirtyEditPayload admits it back. New createOwnerPayload in sanitize.ts reuses the edit path's changedOwner and stripUnwritable(admitOwner) and judges the owner against the values the create form OPENED with, which is the seedCreateValues result. ModalForm (the console's New dialog) and DrawerForm pass formData, which on a create is the seed alone; a pick is sent, and a seeded owner left alone (the acting user from a current_user default, or a caller prefill) is not. formWritePayload gains an optional createBaseline source and puts a picked owner into payload, because ObjectForm's create writes payload rather than writePayload. Status is rework, and the PR says 'Part of #12359' instead of 'Fixes': A1/A2 measured that writePayload.ts cannot fix its callers by itself. ObjectForm, TabbedForm, SplitForm and WizardForm must each pass createBaseline, and those four files are outside the claimed file surface, so I stopped there as the order requires. WizardForm's formData also accumulates step data (mergedData) and needs a separate seed. Until those callers pass a baseline they behave exactly as before.", "tests": "All at ae10afa. (1) Targeted vitest under the lock, 8 files (createOwnerPick-12359, editOwnerChange-12205, systemManagedPayload, sanitize, currentUserDefault, createDefaults, formArmsWritePayload-10563, editDirtyPayload-10156): 'Test Files 8 passed (8) / Tests 207 passed (207)'; lock VERDICT command-exit 0. (2) Under the same lock: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build exited 0, then pnpm --filter @object-ui/plugin-form type-check exited 0 ('tsc --noEmit && tsc -p tsconfig.test.json' echoed); tsconfig.test.json --listFilesOnly lists the new test (count 1). (3) Narrowing, unlocked and declared: 53 plugin-form test files that name ModalForm, DrawerForm, formWritePayload, sanitize, createOwnerPayload or a modal/drawer formType, run with --maxWorkers=2: 'Test Files 53 passed (53) / Tests 1036 passed (1036)'. NOT MEASURED: the full plugin-form test project, because the lock gave queue-timeout 99 four times while issue-12370 shard4 and issue-12372 full.sh held it. The new pin is a .test.tsx file in the dom project and uses no vi.mock. (4) eslint --no-inline-config --format json over the 5 changed source and test files: 5 files, 0 errors, 60 warnings, all no-explicit-any or existing react-hooks warnings. The population is the files changed from merge-base; eslint.config.js has no projectService, so lint is not type-aware and the diff cannot move the verdict on untouched files. (5) Exit 0 for the pre-tsc battery (phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity), check:new-line-citations, check-changeset-fixed and check-changeset-presence. check:spec-floors EXIT=1 with 'The scan collapsed: 0 package(s) declare @objectstack/spec', the known objectui#12334, recorded and not fixed. (6) Two ablations through objectstack scripts/ablation-replace.mjs; tests read source, so no dist step. Each mutation landed (anchor 1 to 0, blob changed) and each restore checked blob == HEAD ff49f0b81ac7 with git diff HEAD empty. Ablation (a): createOwnerPayload never adds the pick gives 8 failed / 13 passed, every 'is sent' row red. Ablation (b): changedOwner(owner, {}) ignores the seed gives 4 failed / 17 passed, every 'seeded owner not sent' row red (the A3 pin). A4: a mocked 403 FORBIDDEN on create shows 'You don't have permission to save this record.', onSuccess is not called and the dialog stays open.", "mcp_calls": "0", "api_writes": "4. Two branch pushes: the empty-branch probe and ae10afa. One POST /repos/objectstack-ai/objectui/pulls (draft PR #12378). One POST /repos/objectstack-ai/objectui/issues/12378/assignees (baozhoutao). This os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/12359/comments, is the fifth write. No label writes: objectui has no gate that reads labels and the order named none.", "gates": {"targeted-vitest-8": 0, "closure-build": 0, "type-check": 0, "narrowed-plugin-form-53": 0, "full-plugin-form-project": "NOT MEASURED (lock queue-timeout x4)", "eslint-changed-files": 0, "pre-tsc-battery-14": 0, "check:new-line-citations": 0, "check:spec-floors": 1, "check-changeset-fixed": 0, "check-changeset-presence": 0}, "files_changed": [".changeset/12359-create-owner-pick.md", "packages/plugin-form/src/sanitize.ts", "packages/plugin-form/src/ModalForm.tsx", "packages/plugin-form/src/DrawerForm.tsx", "packages/plugin-form/src/writePayload.ts", "packages/plugin-form/src/createOwnerPick-12359.test.tsx"], "deviations": [ "PR first line is 'Part of #12359', not the 'Fixes #12359' the order asked for. Merging it should not close the card while the ObjectForm, Tabbed, Split and Wizard create paths still drop a pick.", "A2 assumption disproved: fixing writePayload.ts does not fix its four callers, because each must hand its opening values over. Those caller files are outside the file surface; stopped and reported rather than editing them.", "The full plugin-form project was not run before push (lock never acquired); I ran the declared 53-file narrowing instead.", "Clause-②: no, confirmed. createOwnerPayload and FormWriteSources are not exported from the plugin-form entry (index.tsx), and no prop or accept set moves." ], "open_questions": [ { "question": "Who lands the remaining half of #12359, which is passing createBaseline from ObjectForm (finalDefaultValues), TabbedForm and SplitForm (formData), and WizardForm (a seed ref held apart from mergedData)?", "options": [ "A: extend this claim's file surface to those four files and send a patch round on this branch, so the PR can become 'Fixes #12359'", "B: land this PR as Part of #12359 and dispatch the four callers as a new round on the same card" ], "recommendation": "A. The change is mechanical, one source field per caller plus a seed ref in WizardForm, pinned the same way. It closes the card in one PR and lets createBaseline become required so a future caller cannot forget it." } ], "out_of_scope_findings": [ "carrier: whoever takes the remaining half · ModalForm's write branch tests schema.mode === 'create', while its load effect and formWritePayload use isCreateFormMode (mode create, or no recordId). A modal with no mode and no recordId seeds like a create but writes through dirtyEditPayload, so it skips omitServerResolvedDefaults (#4069). Not exercised; noted, not filed." ] }Generated by Claude Code
baozhoutao commentedon Oct 14, 2026
os-dev-report
{ "issue": 12359, "status": "done", "branch": "claude/issue-12359-create-owner-pick", "head": "609a32b", "pr": "https://github.com/objectstack-ai/objectui/pull/12378", "session": "session_01XVp19E1MYUTFM3dnkwz8gB (subagent; the parent's id)", "premise_still_valid": true, "summary": "Patch round, option A. I merged origin/main into the branch (merge commit 46fcae4, no rebase, no force-push), then committed 609a32b. All six create writers now send a picked owner and leave a seeded owner off the wire. ModalForm and DrawerForm pass formData. ObjectForm passes formOpeningValues(schemaDefaults, initialData): the defaults block moved above handleSubmit, finalDefaultValues now goes through the same helper, and the callback deps gain only the memoised schemaDefaults. TabbedForm and SplitForm pass their seeded formData. WizardForm passes createSeedRef, set by a module-level holdCreateSeed at each of its three seed sites and held apart from the step-merged formData. FormWriteSources.createBaseline is now REQUIRED as Record or null. The only ripple was editOwnerChange-12205.test.tsx, a test inside the surface, which now passes createBaseline: null on its edit calls. A flat fields list (simple or mobile stepper) never renders the owner, because auto-layout filters system fields, so it has nothing to pick. I did not PATCH the PR body; the revised body, which starts 'Fixes #12359', is in the report comment for the seat to write (see deviations).", "tests": "All at 609a32b. (1) Full plugin-form project under the lock: 'Test Files 183 passed (183) / Tests 2244 passed | 1 skipped (2245)'; VERDICT command-exit 0, held 414s. (2) Targeted run under the lock: the 8 files from round 1 plus WizardForm.regression, WizardForm.successBehavior, ObjectForm and subformHosts: 'Test Files 12 passed (12) / Tests 238 passed (238)'. (3) Under the same lock: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build exited 0, then pnpm --filter @object-ui/plugin-form type-check (tsc --noEmit plus tsconfig.test.json) exited 0. This is the compile proof that every formWritePayload caller passes the now-required createBaseline. (4) createOwnerPick-12359 has 33 tests. The new rows are a describe.each over ObjectForm simple, tabbed, split and wizard: a pick is sent, a seeded current_user creator is not sent, and a pick over the seed is sent. Together with editOwnerChange-12205 that file pair passes 61 of 61. (5) Ablations through objectstack scripts/ablation-replace.mjs. Every mutation landed (anchor 1 to 0) and every restore checked blob == HEAD with git diff HEAD empty. WizardForm 'createBaseline: createSeedRef.current' replaced by null: 2 failed (the wizard rows 'a picked owner is sent' and 'a pick over the seeded creator is sent'). ObjectForm's formOpeningValues baseline replaced by null: 2 failed (the matching simple rows). WizardForm baseline replaced by mergedData: 2 failed, which shows the step-merged formData cannot serve as the seed. (6) eslint --no-inline-config --format json over the 10 changed .ts/.tsx files: 10 files, 0 errors. Every react-hooks/exhaustive-deps warning in it is on an effect or callback this diff does not touch; the handleSubmit callbacks show none. eslint.config.js has no projectService, so lint is not type-aware. (7) Pre-tsc battery (14 named checks), check:new-line-citations, check-changeset-fixed and check-changeset-presence: all EXIT=0. check:spec-floors EXIT=1, the known objectui#12334.", "mcp_calls": "0", "api_writes": "This round: 1 git push (609a32b, which carries merge 46fcae4) and this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/12359/comments. Round 1 total was 5 (2 pushes, POST /pulls, POST /issues/12378/assignees, os-dev-report comment 6174036224). No PR body PATCH and no label writes.", "gates": {"full-plugin-form-project": 0, "targeted-vitest-12": 0, "closure-build": 0, "type-check": 0, "eslint-changed-files-10": 0, "pre-tsc-battery-14": 0, "check:new-line-citations": 0, "check:spec-floors": 1, "check-changeset-fixed": 0, "check-changeset-presence": 0}, "files_changed": [".changeset/12359-create-owner-pick.md", "packages/plugin-form/src/sanitize.ts", "packages/plugin-form/src/writePayload.ts", "packages/plugin-form/src/ModalForm.tsx", "packages/plugin-form/src/DrawerForm.tsx", "packages/plugin-form/src/ObjectForm.tsx", "packages/plugin-form/src/TabbedForm.tsx", "packages/plugin-form/src/SplitForm.tsx", "packages/plugin-form/src/WizardForm.tsx", "packages/plugin-form/src/createOwnerPick-12359.test.tsx", "packages/plugin-form/src/editOwnerChange-12205.test.tsx"], "deviations": [ "PR body not updated by me. The order asked for the update, but the role file forbids a dev PATCH of the PR body ('PR 正文 dev 只写一次 … ⛔ 不 PATCH;事后要改的报告点名改法,席位代写'), and the role file wins on conflict. The revised body, which starts 'Fixes #12359' then 'Clause-②: no' and names all six writers, is the second fenced block of this report comment. It is ready for the seat to write with PATCH /repos/objectstack-ai/objectui/pulls/12378 {body}. The live body still starts 'Part of #12359'.", "The changeset now names all six writers. It is a patch for @object-ui/plugin-form.", "Clause-②: no, re-confirmed. formOpeningValues and holdCreateSeed are module-private; createOwnerPayload and FormWriteSources are not exported from the plugin-form entry." ], "open_questions": [], "out_of_scope_findings": [ "carrier: whoever next edits ModalForm's write branch · that branch tests schema.mode === 'create', while its load effect uses isCreateFormMode. Noted, not filed, as the PM ruled." ] }Revised PR #12378 body, for the seat to write:
Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Review: ACCEPT — objectui PR #12378 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-14T18:01ZReviewed on GitHub at head
609a32b, against triage's scope (6164838956) and this card's Acceptance, not against the dev's report.main. Its body opensFixes #12359, the only closing keyword, followed byClause-②: no. The seat wrote that body from the dev's report, because the role file has the dev write the PR body once and never PATCH it. The governed sweep reads NOT governed.createOwnerPayload,FormWriteSources,formOpeningValuesandholdCreateSeedare not on the plugin-form entry. No published export, prop or accept set moves.createOwnerPayloadreuseschangedOwnerandstripUnwritable(…, admitOwner), with no second strip list and no second equality rule. It judges the owner against the values the create form opened with:current_userdefault, or a caller'sinitialData);readonlyowner still refuse it.ModalForm,DrawerForm, and the simple, tabbed, split and wizard layouts ofObjectForm.FormWriteSources.createBaselineis now required, so a future layout cannot forget it, and type-check proves every caller passes it.mergedDataas the baseline turns the wizard rows red.owner_idguard decides, asallowTransfer's spec description states ("ENFORCED via the insert/update owner_id guard"). A mocked 403 surfaces as the form's error, the dialog stays open, andonSuccessis not called.@object-ui/plugin-form: patch) matches the diff, including the six writers, the seeded-owner rule and the server's transfer judgement.609a32b:ModalForm's write branch testsschema.mode === 'create'while its load effect usesisCreateFormMode. That is a different defect, not exercised here.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-14T18:17Zmainfc211e8. The card closedcompletedby the PR'sFixes #12359, which is the only closing keyword. No other card closed with it.origin/main, all 11 files the PR touches are blob-identical to the accepted head609a32b. They include:sanitize.tsandwritePayload.ts;ModalForm,DrawerForm,ObjectForm,TabbedForm,SplitForm,WizardForm);.changeset/12359-create-owner-pick.md.pm:dispatchedis removed in the same stroke as this comment.Generated by Claude Code