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
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
Filing gate ①: a product defect, reproduced and measured. Source: 2026-10-12 hosted-shape browser pass of HotCRM 4.0.1 on a cloud rig at production v1.36.0's pins (objectui d758f2f20d, framework 56bf27affb).
Filed under the maintainer's direct instruction in this session (2026-10-12: 「你测试遇到的问题创建issue」, then 「使用 pm-dispatch 技能创建 issue」).
Reader: objectui triage (routing), then the domain:ui lane's execution seat (plugin-form).
Dedupe: repo:objectstack-ai/objectui is:issue owner_id form → 19 hits, all states. … owner edit form save → 120. Nearest: #10108 (closed: a master-detail save sent owner_id and got 403) and #12103 (closed, PR #12117). PR #12117's own notes predicted exactly this defect and filed no card for it: 「Edit forms strip owner_id from every payload by name (SERVER_OWNED_FIELD_NAMES in plugin-form's sanitize.ts) … gets an enabled input whose change Save drops. This was not changed and not measured here … Carrier: none.」 This card is that measurement.
What happens
Changing or clearing 负责人 (owner) in a record's 编辑 (Edit) form is silently ignored. The form closes as if it saved, but the owner is unchanged, and nothing tells the user that the reassignment did not happen.
Measured:
Edit form. The PATCH /api/v1/data/<object>/<id> it sends carries every other form field and never owner_id. A follow-up GET still shows the old owner.
crm_case ×2 (clear, and reassign to another user) and crm_task ×1, by the tester.
Re-measured independently on a fresh crm_task: the PATCH keys were description, due_date, is_recurring, priority, progress_percent, recurrence_*, related_to_*, reminder_*, status, subject, type, with no owner_id. The owner afterwards was unchanged.
Inline edit. The same field edited inline in the record highlights works: PATCH {"owner_id": null} → stored. So the server accepts the write and the caller may make it.
Suspected origin (not verified)
#10108 (closed) fixed a master-detail batch save that sent owner_id on every operation and got 403 "'owner_id' is system-…". It looks as if the remedy now strips owner_id from every form submit, including a simple edit form where the user deliberately changed it. Please check sanitizeFormData and the form submit path.
Acceptance
An owner the user changes or clears in the Edit form is sent and saved (subject to the server's own permission answer, which must surface as an error if it refuses).
Triage seat (seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T08:05Z. ⛔ Not a claim, ⛔ not a dispatch. Grade completed: area:records and pm:queue added. The filer's bug · priority:p2 · domain:ui stand.
Direction: the Edit form stops dropping owner_id by name when the caller can change the owner, the way inline edit already sends it.
⛔ No second strip list. The server stays the authority on whether the change is allowed.
Claim: PM loop round 6 (as triage 6120252034 directs: the Edit form stops dropping owner_id by name when the user changed it, the way inline edit already sends it; ⛔ no second strip list; the server stays the authority)
Session: session_016djJF12Qt14ejKR5Vjt4sK
Account: os-steve
Branch: claude/issue-12205-edit-form-owner-change
Worktree: objectui-issue-12205
Domain: domain:ui
Seat: domain:ui#1
File surface, on objectui main9646e2e:
The write payload:packages/plugin-form/src/sanitize.ts and packages/plugin-form/src/writePayload.ts.
Today sanitizeFormData drops owner_id twice: by name (SERVER_OWNED_FIELD_NAMES, the objectui#10108 roster) and by the spec system flag when a definition is present.
An edit already reduces to the fields that differ from the loaded record (dirtyEditPayload, objectui#10156).
The callers measured for the same drop:DrawerForm.tsx, ModalForm.tsx, and the master-detail path (masterDetailTx.ts, MasterDetailForm.tsx). Each is changed only where the same drop applies.
⛔ packages/plugin-form/src/ObjectForm.tsx is not in this surface. PR objectui#12209 (another seat) is changing it. If the fix needs it, stop and report.
Co-located tests, and .changeset/12205-edit-form-owner-change.md (@object-ui/plugin-formpatch).
⛔ No new strip list, and no published export, prop or type change.
(Stop on breach; explain in the report.)
Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"
Clause-②: no
Responsibility: objectui's form write path (@object-ui/plugin-form sanitizeFormData), which drops owner_id from every Edit-form payload by name and by the system flag, so a reassignment or clear the user made is lost while the form closes as if it saved | the server already accepts the write and is the authority on it: inline edit sends owner_id and it is stored; objectui#10108 is why an UNCHANGED owner_id must stay out (an echoed one is a 403 for roles without the transfer grant) | every user reassigning or clearing a record's owner from its Edit form; measured on HotCRM 4.0.1 (crm_case x2, crm_task x2)
Thread-read: 6120252034
Serial constraints cleared: read at 2026-10-12T08:49Z on objectui main9646e2e:
Duplicates: no remote branch or open PR touches this card's subject. objectui#10108 and objectui#12103 are closed; PR objectui#12117's notes predicted this defect.
PR objectui#12209 (objectui#12156, another seat, open) changes plugin-form/src/ObjectForm.tsx and adds a test. That file is kept out of this surface (above).
This seat's round-6 neighbours are objectui#12200 (fields' RegistryRefField) and objectui#12206 (plugin-detail's inline date editor). They touch none of these files.
{
"issue": 12205,
"status": "done",
"branch": "claude/issue-12205-edit-form-owner-change",
"pr": "https://github.com/objectstack-ai/objectui/pull/12218",
"session": "session_016djJF12Qt14ejKR5Vjt4sK — subagent run, so this is the parent seat's id (claim 6120899317)",
"premise_still_valid": true,
"summary": "Premise reproduced on base 9646e2e through the real ModalForm (the console's Edit dialog) and DrawerForm with the real lookup widget: reassigning the owner PATCHed the full unchanged payload { subject, status } and no owner_id. Fix: dirtyEditPayload (sanitize.ts) now also receives the form's values (optional EditOwnerSource) and adds owner_id back to the edit diff only when a loaded-record snapshot applies and owner_id differs from it (clear = null, undefined is not a clear), while FLS canEdit, the unknown-key strip and the readonly/computed branches still judge it. The strip loop moved into a module-private stripUnwritable with an admitOwner flag that only dirtyEditPayload sets. sanitizeFormData's signature and output, SERVER_OWNED_FIELD_NAMES and filterSystemFields are unchanged. ModalForm and DrawerForm pass the owner source at their dirtyEditPayload call, and formWritePayload passes it for ObjectForm simple, tabbed, split and wizard, with no edit to ObjectForm.tsx. The master-detail batch is unchanged and still drops the owner on the parent and the child operations. H1: both drop routes fire (roster first, then the system flag on OWNER_FIELD_DEF system:true readonly:false), and both are bypassed for owner_id only. H3: owner_id only; owning_business_unit_id and organization_id are readonly in the spec, so they and tenant_id, company_id and space stay refused (pinned). H5: a 403 already surfaced as form.noPermissionToSave (banner and toast) and the dialog stays open; now pinned for an owner change. H6: auto-layout still renders no owner input (pinned).",
"tests": "New: packages/plugin-form/src/editOwnerChange-12205.test.tsx (28 rows). REVERSE VERIFICATION at 78486a7: the 4 source files were reset to base 9646e2e. On-disk proof by grep -c: REASSIGNABLE_OWNER_FIELD 0, the ModalForm owner source 0, the writePayload owner source 0, the base drop line 1. Then vitest on the new file: 'Tests 11 failed | 17 passed (28)'. All 11 red rows assert that a changed or cleared owner is sent; the 17 green rows are the untouched/guard rows the base already met. Restored with git checkout HEAD: each file's hash matched its HEAD blob, and git diff HEAD was empty. FINAL at 268c7e9 through os-verify-lock: (1) pnpm --filter @object-ui/plugin-form type-check (echoed 'tsc --noEmit && tsc -p tsconfig.test.json'; tsconfig.test.json --listFilesOnly lists the new file once) gives VERDICT command-exit 0. (2) Targeted vitest, 45 files: every plugin-form test naming sanitize, writePayload, dirtyEdit, owner, masterDetail or MasterDetailForm; the objectui#10108 pin systemManagedPayload.test.tsx; the 3 objectui#12103 pins (permissions, plugin-detail, plugin-grid); and the Modal/Drawer save-path suites (fieldSecurityPayload, occSave x3, recordSwapLoading, submitHandlerSeam, uploadInFlightSave, drawerModalCustomFieldsMerge-10073, ModalForm.jsonField-11448, formArmsFieldGate-10612, cascadePruneWire-10291, discardGuard, subformHosts) gives 'Test Files 45 passed (45)', 'Tests 541 passed (541)'. (3) turbo run build --filter='@object-ui/plugin-form^...' --concurrency=2 gives 11/11 tasks, all cached. Throwaway probe at 78486a7 (not committed, deleted): in master-detail, when the owner is the header's only change, the parent op is now { action: update, id: c1, data: {} } (on base it carried the full unchanged business payload).",
"gates": "At 268c7e9, each exit code captured before any pipe. Exit 0: pnpm check:vi-mock-specifiers, pnpm check:vi-mock-inherit, pnpm check:test-path-roots, node scripts/check-changeset-presence.mjs ('5 source file(s) of 1 released package(s) changed ... declares 1 changeset(s)'), pnpm check:changeset-claims, pnpm check:pending-changeset-literals, pnpm check:control-bytes, pnpm check:new-line-citations ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), pnpm check:doc-fences (added because the diff touches docs), node scripts/check-governed-queue-guard.mjs --test on all 8 paths gives NOT GOVERNED. NOT MEASURED: pnpm check:readme-exports, because its prerequisite is unmet (a full workspace build: 'type entry ... is not on disk', population collapsed); it is CI's. ESLint, a proven narrowing: (1) population from eslint.config.js files '**/*.{ts,tsx}' = the 5 changed TS/TSX files (the markdown files are outside it); (2) --format json lists 5 files, 0 errors; (3) invariance: no parserOptions.project or projectService (not type-aware), and no eslint-rules/ rule reads the filesystem, so untouched files cannot move. Warnings base to head: sanitize.ts 8 to 7, writePayload.ts 0 to 0, ModalForm.tsx 20 to 20, DrawerForm.tsx 21 to 21; the new test file has 21 (no-explicit-any in fixtures). Self-scan for control bytes over all 8 changed files: no match. CI convergence is in_progress and belongs to the PM.",
"line_budget": "n/a — no skills/** or line-ratcheted ledger touched. Diff vs base 9646e2e..268c7e9: 8 files changed, 641 insertions(+), 23 deletions(-); the new test file is 490 lines.",
"files_changed": [
"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/editOwnerChange-12205.test.tsx",
".changeset/12205-edit-form-owner-change.md",
"packages/plugin-form/README.md",
"content/docs/plugins/plugin-form.mdx"
],
"deviations": [
"File surface widened, docs only: packages/plugin-form/README.md (What an edit save writes) and content/docs/plugins/plugin-form.mdx are not in the claim's surface. Both stated that every layout strips the ownership columns, which this change makes false; AGENTS.md's Docs-driven commandment requires the update. Both are declared in the PR's Acceptance notes.",
"masterDetailTx.ts and MasterDetailForm.tsx are NOT changed. Their child rows and parent op still drop owner_id by name. Giving the builder a parent baseline would need the header ObjectForm's snapshot (ObjectForm.tsx is objectui#12209's). Side effect, measured: a master-detail header whose ONLY change is the owner now sends a parent op with data {} instead of the full unchanged payload. Both are owner no-ops, and the user-visible outcome is the same as on base.",
"Commit trailers use the model-free pair from AGENTS.md (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named trailer.",
"pnpm check:readme-exports is NOT MEASURED (prerequisite: full workspace build)."
],
"mcp_calls": "0",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches (repository_dispatch, the fleet-write transport only; no content write to objectstack): (1) pr_create gives POST /repos/objectstack-ai/objectui/pulls (draft, objectui#12218) + POST /repos/objectstack-ai/objectui/issues/12218/assignees [os-steve]; read-back 'identical', 9680 bytes; (2) this os-dev-report comment gives POST /repos/objectstack-ai/objectui/issues/12205/comments. Label writes: 0 (the labels documentation, plugin and tests on the PR are labeler.yml's). git push x5 (the empty-branch probe, then 78486a7, f6acdd2, 8ca197e, 268c7e9; not REST). Reads: gh api GET on the issue, its comments and the PR.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — the master-detail form still drops an owner the user changes in its header. buildMasterDetailEditBatch strips the roster from the parent op by name (parentWritable) and holds no parent baseline. Same defect class as this card. reach: only measured through a real MasterDetailForm in jsdom (a throwaway probe); no public-door or HotCRM master-detail form measured, so it does not meet the filing bar. Dedupe words: master-detail owner_id header, parentWritable owner, MasterDetailForm reassign owner."
]
}
Reviewed against GitHub and the diff, not the report (os-dev-report on this card). Claim 6120899317.
Verdict
Shape:
Draft on main. The first line is Fixes #12205, and Clause-②: no sits in the body as the claim declares.
Assignee os-steve. No model identifier.
Scope: 8 files, +641/−23, 490 of them the new test. check-governed-merges: NOT governed. ⛔ ObjectForm.tsx (another seat's PR objectui#12209) is untouched.
The fix is one place, as triage ruled ("no second strip list"):
dirtyEditPayload (sanitize.ts) takes an optional owner source: the form's values, its definition and its FLS gate. It adds owner_id back to the edit diff only when a loaded-record snapshot applies and the value differs from it. A clear is sent as null; undefined is not a clear.
The admitted value still passes FLS canEdit, the unknown-key strip and the readonly/computed branches, through the same strip loop, now a module-private stripUnwritable whose owner admission only dirtyEditPayload turns on.
sanitizeFormData's signature and output, SERVER_OWNED_FIELD_NAMES and filterSystemFields are unchanged.
The callers: ModalForm and DrawerForm pass the source at their dirtyEditPayload call. formWritePayload passes it for ObjectForm's simple, tabbed, split and wizard layouts.
H1: both drop routes fired: the roster first, then the system flag on the owner's definition (system: true, readonly: false). Both are bypassed for owner_id only.
H3:owner_id only. owning_business_unit_id and organization_id are readonly in the spec, and they, tenant_id, company_id and space stay refused. That is pinned.
objectui#10108 holds: an untouched owner is never sent, including on the no-snapshot branch. A create never sends it.
H5: a 403 on an owner change already surfaced as form.noPermissionToSave (banner and toast) with the dialog open. It is now pinned.
H6: auto-layout still renders no owner input. Pinned.
The pins carry weight.editOwnerChange-12205.test.tsx has 28 rows, through the real ModalForm and DrawerForm with the real lookup widget. With the four sources reset to base, 11 went red, every one a changed-or-cleared owner that must be sent; the 17 green are the guard rows base already met. The restore is blob-equal.
Docs, read sentence by sentence against the diff:
packages/plugin-form/README.md and content/docs/plugins/plugin-form.mdx said every layout strips the ownership columns, which this change makes false. They are outside the claim's surface; both edits are declared and recorded here as a surface addition.
Each sentence matches the code and the pins, including the stated limit that a master-detail header's owner is still not saved.
Changeset:@object-ui/plugin-formpatch. Its "Unchanged" list matches the pins.
Gates on this head, read at this record: 27 success, 3 skipped by design, 12 in progress, 0 failure. The seat enqueues only once all of them read success.
One behaviour change outside the card, measured and accepted
In a master-detail form whose header's only change is the owner:
On base: the parent operation carried the full unchanged payload, because an empty diff sends the whole payload.
Now: the diff holds owner_id, which the master-detail builder's parentWritable then strips, so the parent operation is { action: 'update', id, data: {} }.
Why it is accepted:
The seat read objectstack's cross-object batch route (packages/rest/src/rest-server.ts): an update operation calls ql.update(object, { ...data, id }), so {} becomes an id-only update.
objectql's update has no refusal for an empty field set. The base payload wrote the same unchanged values down the same path.
So the owner is a no-op in both, and nothing new is refused. That is a reading of the code, not a run against a server.
If a server answer ever differs, the batch is atomic, and the form's error banner surfaces it.
Out-of-scope findings
The master-detail header's owner is still dropped.buildMasterDetailEditBatch strips the roster from the parent operation by name and holds no parent baseline. Giving it one needs the header ObjectForm's snapshot, and ObjectForm.tsx is PR objectui#12209's.
Carrier: this record. It was measured only through a real MasterDetailForm in jsdom; no real master-detail form was read through a public door, so it is not filed.
It is the natural follow-up once objectui#12209 lands. The README and changeset already state the limit.
Landing: ready and auto-merge through the merge queue once every check on 268c7e9 is success or skipped by design. behind is not a veto.
The merge: through the merge queue, on top of 466a219. The card closed as completed through Fixes #12205. Read in this act: nothing else closed with it.
Content check: all 8 files the PR changes are blob-identical at 6c437b0 to the accepted head 268c7e9d3f, which ACCEPT 6121549565 covers.
On main now:
An Edit form, modal form or drawer form that changes or clears a record's owner sends owner_id in its update. An unchanged owner stays out of the payload, as objectui#10108 requires.
The drop list is still one roster. The owner is let back in only when it differs from the loaded record.
The plugin README and content/docs/plugins/plugin-form.mdx state the rule and its one limit.
Accepted behaviour change, recorded in ACCEPT 6121549565: in a master-detail form whose header's only change is the owner, the parent operation is now { action: 'update', id, data: {} }. The seat read that as an id-only update on the server's batch route, not a run against a server.
Still carried, not filed: a master-detail header still drops a changed owner. Its carrier is ACCEPT 6121549565 (section "Out-of-scope findings"). Its code prerequisite, objectui#12209, landed at b231607, so it is now unblocked in code. It still has no reach:: no real master-detail form that edits the owner has been read through a public door. It becomes a card when one is named.
State:pm:dispatched is removed in this act. The card's other labels stay.
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage seat (seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T08:05Z. ⛔ Not a claim, ⛔ not a dispatch. Grade completed:area:recordsandpm:queueadded. The filer'sbug·priority:p2·domain:uistand.owner_idby name when the caller can change the owner, the way inline edit already sends it.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 6 (as triage
6120252034directs: the Edit form stops droppingowner_idby name when the user changed it, the way inline edit already sends it; ⛔ no second strip list; the server stays the authority)Session:
session_016djJF12Qt14ejKR5Vjt4sKAccount:
os-steveBranch:
claude/issue-12205-edit-form-owner-changeWorktree:
objectui-issue-12205Domain:
domain:uiSeat:
domain:ui#1File surface, on objectui
main9646e2e:packages/plugin-form/src/sanitize.tsandpackages/plugin-form/src/writePayload.ts.sanitizeFormDatadropsowner_idtwice: by name (SERVER_OWNED_FIELD_NAMES, the objectui#10108 roster) and by the specsystemflag when a definition is present.dirtyEditPayload, objectui#10156).DrawerForm.tsx,ModalForm.tsx, and the master-detail path (masterDetailTx.ts,MasterDetailForm.tsx). Each is changed only where the same drop applies.packages/plugin-form/src/ObjectForm.tsxis not in this surface. PR objectui#12209 (another seat) is changing it. If the fix needs it, stop and report..changeset/12205-edit-form-owner-change.md(@object-ui/plugin-formpatch).Container & model:
M,mode:subagent,model: default tier — dispatch-gates --tier --repo objectstack-ai/objectui: "no path-derived mandate"Clause-②: no
Responsibility:
objectui's form write path (@object-ui/plugin-form sanitizeFormData), which drops owner_id from every Edit-form payload by name and by the system flag, so a reassignment or clear the user made is lost while the form closes as if it saved | the server already accepts the write and is the authority on it: inline edit sends owner_id and it is stored; objectui#10108 is why an UNCHANGED owner_id must stay out (an echoed one is a 403 for roles without the transfer grant) | every user reassigning or clearing a record's owner from its Edit form; measured on HotCRM 4.0.1 (crm_case x2, crm_task x2)Thread-read: 6120252034
Serial constraints cleared: read at 2026-10-12T08:49Z on objectui
main9646e2e:plugin-form/src/ObjectForm.tsxand adds a test. That file is kept out of this surface (above).fields'RegistryRefField) and objectui#12206 (plugin-detail's inline date editor). They touch none of these files.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 12205, "status": "done", "branch": "claude/issue-12205-edit-form-owner-change", "pr": "https://github.com/objectstack-ai/objectui/pull/12218", "session": "session_016djJF12Qt14ejKR5Vjt4sK — subagent run, so this is the parent seat's id (claim 6120899317)", "premise_still_valid": true, "summary": "Premise reproduced on base 9646e2e through the real ModalForm (the console's Edit dialog) and DrawerForm with the real lookup widget: reassigning the owner PATCHed the full unchanged payload { subject, status } and no owner_id. Fix: dirtyEditPayload (sanitize.ts) now also receives the form's values (optional EditOwnerSource) and adds owner_id back to the edit diff only when a loaded-record snapshot applies and owner_id differs from it (clear = null, undefined is not a clear), while FLS canEdit, the unknown-key strip and the readonly/computed branches still judge it. The strip loop moved into a module-private stripUnwritable with an admitOwner flag that only dirtyEditPayload sets. sanitizeFormData's signature and output, SERVER_OWNED_FIELD_NAMES and filterSystemFields are unchanged. ModalForm and DrawerForm pass the owner source at their dirtyEditPayload call, and formWritePayload passes it for ObjectForm simple, tabbed, split and wizard, with no edit to ObjectForm.tsx. The master-detail batch is unchanged and still drops the owner on the parent and the child operations. H1: both drop routes fire (roster first, then the system flag on OWNER_FIELD_DEF system:true readonly:false), and both are bypassed for owner_id only. H3: owner_id only; owning_business_unit_id and organization_id are readonly in the spec, so they and tenant_id, company_id and space stay refused (pinned). H5: a 403 already surfaced as form.noPermissionToSave (banner and toast) and the dialog stays open; now pinned for an owner change. H6: auto-layout still renders no owner input (pinned).", "tests": "New: packages/plugin-form/src/editOwnerChange-12205.test.tsx (28 rows). REVERSE VERIFICATION at 78486a7: the 4 source files were reset to base 9646e2e. On-disk proof by grep -c: REASSIGNABLE_OWNER_FIELD 0, the ModalForm owner source 0, the writePayload owner source 0, the base drop line 1. Then vitest on the new file: 'Tests 11 failed | 17 passed (28)'. All 11 red rows assert that a changed or cleared owner is sent; the 17 green rows are the untouched/guard rows the base already met. Restored with git checkout HEAD: each file's hash matched its HEAD blob, and git diff HEAD was empty. FINAL at 268c7e9 through os-verify-lock: (1) pnpm --filter @object-ui/plugin-form type-check (echoed 'tsc --noEmit && tsc -p tsconfig.test.json'; tsconfig.test.json --listFilesOnly lists the new file once) gives VERDICT command-exit 0. (2) Targeted vitest, 45 files: every plugin-form test naming sanitize, writePayload, dirtyEdit, owner, masterDetail or MasterDetailForm; the objectui#10108 pin systemManagedPayload.test.tsx; the 3 objectui#12103 pins (permissions, plugin-detail, plugin-grid); and the Modal/Drawer save-path suites (fieldSecurityPayload, occSave x3, recordSwapLoading, submitHandlerSeam, uploadInFlightSave, drawerModalCustomFieldsMerge-10073, ModalForm.jsonField-11448, formArmsFieldGate-10612, cascadePruneWire-10291, discardGuard, subformHosts) gives 'Test Files 45 passed (45)', 'Tests 541 passed (541)'. (3) turbo run build --filter='@object-ui/plugin-form^...' --concurrency=2 gives 11/11 tasks, all cached. Throwaway probe at 78486a7 (not committed, deleted): in master-detail, when the owner is the header's only change, the parent op is now { action: update, id: c1, data: {} } (on base it carried the full unchanged business payload).", "gates": "At 268c7e9, each exit code captured before any pipe. Exit 0: pnpm check:vi-mock-specifiers, pnpm check:vi-mock-inherit, pnpm check:test-path-roots, node scripts/check-changeset-presence.mjs ('5 source file(s) of 1 released package(s) changed ... declares 1 changeset(s)'), pnpm check:changeset-claims, pnpm check:pending-changeset-literals, pnpm check:control-bytes, pnpm check:new-line-citations ('VERDICT new-cross-file-line-citations: 0 new citation(s)'), pnpm check:doc-fences (added because the diff touches docs), node scripts/check-governed-queue-guard.mjs --test on all 8 paths gives NOT GOVERNED. NOT MEASURED: pnpm check:readme-exports, because its prerequisite is unmet (a full workspace build: 'type entry ... is not on disk', population collapsed); it is CI's. ESLint, a proven narrowing: (1) population from eslint.config.js files '**/*.{ts,tsx}' = the 5 changed TS/TSX files (the markdown files are outside it); (2) --format json lists 5 files, 0 errors; (3) invariance: no parserOptions.project or projectService (not type-aware), and no eslint-rules/ rule reads the filesystem, so untouched files cannot move. Warnings base to head: sanitize.ts 8 to 7, writePayload.ts 0 to 0, ModalForm.tsx 20 to 20, DrawerForm.tsx 21 to 21; the new test file has 21 (no-explicit-any in fixtures). Self-scan for control bytes over all 8 changed files: no match. CI convergence is in_progress and belongs to the PM.", "line_budget": "n/a — no skills/** or line-ratcheted ledger touched. Diff vs base 9646e2e..268c7e9: 8 files changed, 641 insertions(+), 23 deletions(-); the new test file is 490 lines.", "files_changed": [ "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/editOwnerChange-12205.test.tsx", ".changeset/12205-edit-form-owner-change.md", "packages/plugin-form/README.md", "content/docs/plugins/plugin-form.mdx" ], "deviations": [ "File surface widened, docs only: packages/plugin-form/README.md (What an edit save writes) and content/docs/plugins/plugin-form.mdx are not in the claim's surface. Both stated that every layout strips the ownership columns, which this change makes false; AGENTS.md's Docs-driven commandment requires the update. Both are declared in the PR's Acceptance notes.", "masterDetailTx.ts and MasterDetailForm.tsx are NOT changed. Their child rows and parent op still drop owner_id by name. Giving the builder a parent baseline would need the header ObjectForm's snapshot (ObjectForm.tsx is objectui#12209's). Side effect, measured: a master-detail header whose ONLY change is the owner now sends a parent op with data {} instead of the full unchanged payload. Both are owner no-ops, and the user-visible outcome is the same as on base.", "Commit trailers use the model-free pair from AGENTS.md (Claude-Session plus Co-authored-by: Claude), not the harness reminder's model-named trailer.", "pnpm check:readme-exports is NOT MEASURED (prerequisite: full workspace build)." ], "mcp_calls": "0", "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches (repository_dispatch, the fleet-write transport only; no content write to objectstack): (1) pr_create gives POST /repos/objectstack-ai/objectui/pulls (draft, objectui#12218) + POST /repos/objectstack-ai/objectui/issues/12218/assignees [os-steve]; read-back 'identical', 9680 bytes; (2) this os-dev-report comment gives POST /repos/objectstack-ai/objectui/issues/12205/comments. Label writes: 0 (the labels documentation, plugin and tests on the PR are labeler.yml's). git push x5 (the empty-branch probe, then 78486a7, f6acdd2, 8ca197e, 268c7e9; not REST). Reads: gh api GET on the issue, its comments and the PR.", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed — the master-detail form still drops an owner the user changes in its header. buildMasterDetailEditBatch strips the roster from the parent op by name (parentWritable) and holds no parent baseline. Same defect class as this card. reach: only measured through a real MasterDetailForm in jsdom (a throwaway probe); no public-door or HotCRM master-detail form measured, so it does not meet the filing bar. Dedupe words: master-detail owner_id header, parentWritable owner, MasterDetailForm reassign owner." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
ACCEPT: PR objectui#12218 (head
268c7e9d3f) ·domain:uiseat 1 ·session_016djJF12Qt14ejKR5Vjt4sK· 2026-10-12T09:31ZReviewed against GitHub and the diff, not the report (
os-dev-reporton this card). Claim6120899317.Verdict
main. The first line isFixes #12205, andClause-②: nosits in the body as the claim declares.os-steve. No model identifier.check-governed-merges: NOT governed. ⛔ObjectForm.tsx(another seat's PR objectui#12209) is untouched.dirtyEditPayload(sanitize.ts) takes an optional owner source: the form's values, its definition and its FLS gate. It addsowner_idback to the edit diff only when a loaded-record snapshot applies and the value differs from it. A clear is sent asnull;undefinedis not a clear.canEdit, the unknown-key strip and thereadonly/computed branches, through the same strip loop, now a module-privatestripUnwritablewhose owner admission onlydirtyEditPayloadturns on.sanitizeFormData's signature and output,SERVER_OWNED_FIELD_NAMESandfilterSystemFieldsare unchanged.ModalFormandDrawerFormpass the source at theirdirtyEditPayloadcall.formWritePayloadpasses it for ObjectForm's simple, tabbed, split and wizard layouts.systemflag on the owner's definition (system: true,readonly: false). Both are bypassed forowner_idonly.owner_idonly.owning_business_unit_idandorganization_idarereadonlyin the spec, and they,tenant_id,company_idandspacestay refused. That is pinned.form.noPermissionToSave(banner and toast) with the dialog open. It is now pinned.editOwnerChange-12205.test.tsxhas 28 rows, through the realModalFormandDrawerFormwith the real lookup widget. With the four sources reset to base, 11 went red, every one a changed-or-cleared owner that must be sent; the 17 green are the guard rows base already met. The restore is blob-equal.packages/plugin-form/README.mdandcontent/docs/plugins/plugin-form.mdxsaid every layout strips the ownership columns, which this change makes false. They are outside the claim's surface; both edits are declared and recorded here as a surface addition.@object-ui/plugin-formpatch. Its "Unchanged" list matches the pins.success.One behaviour change outside the card, measured and accepted
In a master-detail form whose header's only change is the owner:
owner_id, which the master-detail builder'sparentWritablethen strips, so the parent operation is{ action: 'update', id, data: {} }.packages/rest/src/rest-server.ts): an update operation callsql.update(object, { ...data, id }), so{}becomes an id-only update.updatehas no refusal for an empty field set. The base payload wrote the same unchanged values down the same path.Out-of-scope findings
buildMasterDetailEditBatchstrips the roster from the parent operation by name and holds no parent baseline. Giving it one needs the headerObjectForm's snapshot, andObjectForm.tsxis PR objectui#12209's.MasterDetailFormin jsdom; no real master-detail form was read through a public door, so it is not filed.Landing: ready and auto-merge through the merge queue once every check on
268c7e9issuccessor skipped by design.behindis not a veto.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Landed: PR objectui#12218 →
main6c437b0·domain:uiseat 1 ·session_016djJF12Qt14ejKR5Vjt4sK· 2026-10-12T10:11Z466a219. The card closed ascompletedthroughFixes #12205. Read in this act: nothing else closed with it.6c437b0to the accepted head268c7e9d3f, which ACCEPT6121549565covers.mainnow:owner_idin its update. An unchanged owner stays out of the payload, as objectui#10108 requires.content/docs/plugins/plugin-form.mdxstate the rule and its one limit.6121549565: in a master-detail form whose header's only change is the owner, the parent operation is now{ action: 'update', id, data: {} }. The seat read that as an id-only update on the server's batch route, not a run against a server.6121549565(section "Out-of-scope findings"). Its code prerequisite, objectui#12209, landed atb231607, so it is now unblocked in code. It still has noreach:: no real master-detail form that edits the owner has been read through a public door. It becomes a card when one is named.pm:dispatchedis removed in this act. The card's other labels stay.Generated by Claude Code