Repository navigation
feat(plugin-form): an edit save writes only the fields that changed - #10546
Conversation
An edit form now diffs its sanitized payload against the record it read with findOne and writes only the fields that differ, on both write routes: the host-owned submitHandler seam (how a master-detail form builds its parent operation) and the plain OCC-guarded update. ObjectForm, ModalForm and DrawerForm each hold the record they read in a ref tagged with the object and record it was read for; the save path is its only reader. The comparison is the one master-detail child rows already used; it moves from masterDetailTx.ts into sanitize.ts unchanged so there is one rule. Everything it cannot settle is sent: '1' and 1, null and '', a lookup id and its expanded object all read as changed. An empty diff sends the full sanitized payload, as a save with no changes always has. After a save succeeds the baseline advances by what was written, so a form that stays mounted cannot drop a change back to the first-read value. Create, the OCC token and the 409 dialog are untouched. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…vacuous
New pins for objectui#10156: the comparison rule pair by pair, and on real
components the plain edit, the modal and drawer edits and the master-detail
parent operation each send only the changed field; type drift ('5' then a
retyped 5, null then an emptied textarea) and a cascade clear are sent; an
unchanged save sends the full sanitized payload; the 409 Overwrite resends the
changed fields only; a second save from a still-mounted form diffs against
what the first wrote; create is unchanged.
Rows that asserted a column ABSENT after a real edit would now stay green with
their filter deleted, because the dirty diff alone keeps an unchanged column
off the wire. The sanitizer, roster and field-level-security rows now submit
with nothing changed, the one edit route whose payload is the full sanitized
set. Rows that asserted an untouched field PRESENT now assert it absent.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
The README and the plugin-form docs page say what an edit save now writes, the comparison rule, the saves that still send every field, the unchanged OCC guard, and what a host submitHandler receives in edit mode. The tabbed, wizard and split variants are named as not covered. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…pending changeset true The new snapshot and dirty-payload helpers take Record<string, unknown>, so sanitize.ts carries only the explicit-any warnings it had plus the four the moved comparison brought with it. The pending objectui#10120 changeset said ObjectForm and ModalForm "produce the same payloads they did before". Released beside this change that is no longer true of the whole payload; it now says what it meant, that both already withheld the refused field and still do. Body only; front matter byte-identical. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…covered A simple form whose mobile stepper option is on renders through WizardForm, whose save path is its own, so it still sends every value it holds. The README, the docs page and the changeset now say so beside the tabbed, wizard and split variants. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
ObjectFormSchema.submitHandler's JSDoc said the form hands the collected values to the handler. Since objectui#10156, in edit mode, for a record the form read itself, the simple, modal and drawer layouts hand over what the form would have written: the fields that differ from that read, or the full sanitized payload when nothing changed. A field whose sameness cannot be settled counts as changed. The tabbed, wizard and split layouts still hand over every collected value. The JSDoc now says so, and the copies on ModalFormSchema and DrawerFormSchema gain the same clause so they keep matching the key they are declared from. The changeset declares @object-ui/types and names the correction. JSDoc only; no type moves. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Rendered by an isolated review subagent spawned by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… storage key (objectui#8522) (objectstack-ai#10549) Fixes objectstack-ai#8522 Clause-②: yes — a published export (`useColumnWidths` and its two types) is removed. Executes ruling `5810224713` (remove: reading 2 of the card, superseded and left behind). Dispatched by the `domain:ui` seat 2 PM; implemented by its `os-dev` in session `https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`. ## What changed - `packages/plugin-kanban/src/useColumnWidths.ts` deleted: the hook, `UseColumnWidthsOptions`, `UseColumnWidthsReturn`, and its storage key `objectui:kanban-column-widths`. - `packages/plugin-kanban/src/index.tsx`: the three entry exports removed (the `useColumnWidths` value export and the two-type `export type` line). - `@object-ui/types` docblocks, comment-only hunks: - `complex.ts`, the bare-`kanban` retirement docblock: the "NOT retired" sentence no longer names the hook as a consumer. `KanbanCard` / `KanbanColumn` / `CardTemplate` keep the renderer and `CardTemplates` as consumers, and `ColumnWidthConfig` is recorded as having lost its one `plugin-kanban` reader to this card, which did not rule on the type. - `objectql.ts`, the `ObjectKanbanSchema` lane "member set is the READ set" paragraph: the hook is dropped from the list of read sites. The hook destructured `columns` as `_columns` and read no lane member, so the counted member set does not move. - `kanban-plugin-dialect-authoritative-7664.test.ts`: the pin-1 comment stops naming the hook. No assertion changed. - New pin `packages/plugin-kanban/src/__tests__/useColumnWidthsRetired-8522.test.ts`, following the `plugin-form` `entrySurface-6059` barrel-pin pattern (`import * as entry` + `Object.keys(entry)`) and the `spec-symbol-4650` compile-time `import('..').X` + `@ts-expect-error` pattern. - `.changeset/8522-retire-use-column-widths.md`: `'@object-ui/plugin-kanban': minor`, with a BREAKING body in migration terms. There is no `@object-ui/types` entry: `check-changeset-presence` asks for presence only, and the types hunks are comment-only. Its last migration bullet names objectui#7742. - `.changeset/7742-kanban-arm-batch70.md`, a pending changeset that this change falsifies, is reconciled here in prose only: - The sentences naming the `useColumnWidths` widths channel, and saying the prop and the hook consume the types, are scoped to "at this change". - A dated note (2026-09-25) records that the hook is retired (objectui#8522), that `ColumnWidthConfig` has no `plugin-kanban` reader, and that `CardTemplate` is still consumed by the `CardTemplates` component's `templates` prop. - The frontmatter is byte-identical: md5 of the frontmatter block is `b62313df8fb2af929fbe2c21c5ae3873` before and after. No tests were deleted. No test existed only to test the hook: the tracked tree had zero test importers of it. ## Premise, measured on `origin/main` `d461b0de8` - Readers. `git grep` over `packages apps examples content skills` found `useColumnWidths` 7 hits in 5 files, `UseColumnWidths` 4 hits in 2 files, and `kanban-column-widths` 1 hit in 1 file. Every hit was the hook's own file, the entry, the two docblocks, or the 7664 comment. On the same instrument, the control `useCrossSwimlaneMove` fired with 5 hits in 3 files. Zero readers holds. - Published surface. H3 answers yes. `@object-ui/plugin-kanban@17.6.0` from the npm registry exports the hook in its `dist/index.js` export list as `Ue as useColumnWidths`, and `dist/index.d.ts` re-exports it and both types. The storage-key literal is present in `dist/index.js` and `dist/index.umd.cjs`. That is why the changeset is written as a migration. - A tree-wide grep found one more carrier outside the file surface: the pending changeset `.changeset/7742-kanban-arm-batch70.md`. It names "a `useColumnWidths` hook option for widths" and says "the prop and the hook still consume the types". Both go false when this merges, and both would publish into the same release, so this PR reconciles it (see above). - A re-grep of every pending `.changeset/*.md` for `useColumnWidths`, `UseColumnWidths` and `kanban-column-widths` hits only 7742 and this PR's own changeset. - The control `CardTemplate` hits 3 files on the same instrument. - The two `7664-*` changesets that name `ColumnWidthConfig` claim only that it is declared and re-exported, which remains true. ## Verification The code legs below ran on `e62e390f4`. The only later commit, `8be30d7fc`, edits changeset prose only; the changeset gates were re-run on it. - Build, under the verify lock: `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-kanban...' build` printed `VERDICT command-exit 0`. The rebuilt `plugin-kanban/dist` has `useColumnWidths` 0 and `kanban-column-widths` 0 in `index.js`, `index.d.ts` and `index.umd.cjs`, and no `useColumnWidths.d.ts`. The control `useCrossSwimlaneMove` is 2 / 2 / 1. - `pnpm --filter @object-ui/plugin-kanban type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`) exits 0. `tsc -p tsconfig.test.json --listFilesOnly` lists the new pin, so the compile-time half is compiled. - `pnpm --filter @object-ui/types type-check` (including `tsconfig.examples.json` and `tsconfig.test.json`) exits 0. - `pnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/` reports `Test Files 61 passed (61)` and `Tests 412 passed (412)`. That is 61 tracked test files: 60 on base plus the new pin. - `pnpm exec vitest run --maxWorkers=2 packages/types/` reports `Test Files 233 passed (233)` and `Tests 5184 passed (5184)`. - Red on base. The base `index.tsx` and `useColumnWidths.ts` were written back onto disk. The landing was checked on disk: the entry had `useColumnWidths` twice and the hook file was present with its key. Under a `trap` restore: - vitest: `Tests 1 failed | 1 passed (2)`, with `AssertionError: expected [ 'KANBAN_UNCOLUMNED_ID', …(10) ] to not include 'useColumnWidths'`. The failure is the runtime pin; the type-only test is erased at runtime. - `tsc -p tsconfig.test.json` exits 2 with `error TS2578: Unused '@ts-expect-error' directive.` twice, once on each retired type query. - After restore, the `index.tsx` blob matches HEAD `7e09f2d92`, the hook file is absent, and `git diff HEAD` and `git status --porcelain` are both empty. - Changeset and prose gates, on `8be30d7fc`: - `node scripts/check-changeset-presence.mjs` printed `✅ 6 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)`. - `node scripts/check-changeset-overwrite.mjs` exited 0 (report-only). It printed `1 changeset(s) added, 1 modified, 0 deleted`. For 7742 it printed `declared at base: @object-ui/types: minor, @object-ui/plugin-kanban: minor` and `declares now: @object-ui/types: minor, @object-ui/plugin-kanban: minor`. - `node scripts/check-changeset-claims.mjs` exited 0 (report-only), reading `Corpus: 2 changeset(s) this change adds or modifies` with no self-contradiction. It listed 27 pending changesets that name `complex.ts` or `objectql.ts`; none of them names the hook. - `node scripts/check-pending-changeset-literals.mjs` printed `✅ No test source names a pending changeset`. - `node scripts/check-changeset-no-major.mjs` printed `✅ No changeset declares a major bump`. - `node scripts/check-new-cross-file-line-citations.mjs` printed `VERDICT new-cross-file-line-citations: 0 new citation(s)`. - `node scripts/check-control-bytes.mjs` passed. - Source gates, on `e62e390f4`. These exited 0: `check:unreferenced-sources`, `check:self-import`, `check:side-effects-array`, `check:handler-key-reads`, `check:entry-guard`, `check:phantom-deps`, `check:unused-deps`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:spec-symbols` and `check:esm-specifiers`. - `check:readme-exports` passed after the build. No README names any of the three removed exports. - ESLint on the 5 touched text files (`--no-inline-config --format json`): 0 errors. The 66 warnings are pre-existing `no-explicit-any` / `react-refresh` warnings in untouched lines, and there are none in the new pin. - NOT MEASURED: - `check:sdui-registration-pins`: it needs an `apps/console` build and exits 2 without one. No registration changed here. - `check:published-dist`: its turbo build of the whole workspace, `apps/console` included, was killed by my 300s timeout (exit 124). It was run outside the verify lock by mistake. That is declared to CI. ## Overlap The one `types/src/objectql.ts` hunk here is the one-line docblock edit in the `ObjectKanbanSchema` lane paragraph. objectui#10533 and objectstack-ai#10522, which also edited that file while this PR was being written, have since merged, and this hunk still applies on `main`. At landing, the open PRs that edit `objectql.ts` are objectui#10550, objectstack-ai#10546 and objectstack-ai#8941, and all three are hunk-disjoint from this edit. ## Acceptance notes - After this change, `ColumnWidthConfig` (in `@object-ui/types`, re-exported by `plugin-kanban`, and mirrored by `ColumnWidthConfigSchema`) has no reader in `plugin-kanban`. The ruling named the hook, its exports and its key, not the type, so the type stays. Recorded here only: there is no reproducible defect, and nobody is known to own it next. - The 7664 pin-1 comment still says `KanbanColumn` / `KanbanCard` are consumed by `CardTemplates`. The `CardTemplates` component consumes `CardTemplate`, not those two. That wording predates this change and is kept, because the dispatch limits this file to removing the hook's name. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #10156
Clause-②: no (conditional on the stop above)
The stop did not fire. No exported type, prop or signature changed.⚠️ One published behaviour does move: in edit mode, a host
submitHandlerkeeps its declared type, and@object-ui/plugin-formpublishes.only, fromindex.tsx, which re-exports neithersanitizenormasterDetailTx. Afterpnpm --filter @object-ui/plugin-form run buildon this branch,grep -coverdist/index.d.tsprints0for each ofdirtyEditPayload,snapshotLoadedRecord,advanceLoadedRecord,LoadedRecordSnapshot,EditSaveTarget,isSameStoredValue,changedFieldsandsanitizeFormData. The positive control,ObjectForm, prints6.submitHandlernow receives the payload the form would have written (see "What shipped"). The seat confirmedClause-②: noin comment5829229738on objectui#10156, and ruled that the published JSDoc describing the seam is corrected in this PR. Round 2 below does that.What shipped
An edit form now diffs its sanitized payload against the record it read with
findOne, and writes only the fields that differ.SimpleObjectForm(inObjectForm.tsx),ModalFormandDrawerFormeach hold the record in a ref, set by their ownfindOneand read only by the save path. The ref is tagged with the object and record it was read for. A save for any other record therefore finds no baseline and sends everything.initialData/formDataare left alone, because they seed the form and supply the OCC token. No exported type or prop was needed.SimpleObjectForm, the diff runs once, before the route is chosen. It covers the host-ownedsubmitHandlerseam, which is how a master-detail form builds its parent operation, and the plain OCC-guardedupdate.ModalFormandDrawerFormcompute it in the samewritePayloadtheir create path already used.isSameStoredValueandchangedFieldsmoved frommasterDetailTx.tsintosanitize.ts, and both callers import that copy. The function bodies are byte-identical to the originals: adiffof each body, extracted from the base blob and from this branch, is empty. The master-detail child rows and the edit form use the same rule.onSuccess. Skipping the request instead would report success for a save no server saw. That is the one outcome a wrong baseline must never produce.a second save from the same mounted form diffs against what the first save wrotepins it.occSave.tsx; the OCC token (baseRecordis stillinitialData/formData); the 409 dialog; the create path; and thesectionsbranch ofObjectForm.tsx. The save handler is shared by the flat and sectioned layouts, so a sectioned simple form also gets the diff. No line of thesectionsbranch changed.A one-field edit, on a record that also carries a numeric string, a null, an ownership column and
updated_at. The "full payload" line is the same pin under ablation A1 below, which puts the pre-branch full payload back:Round 2 (patch): the published
submitHandlerdoc says what an edit-mode handler receivesThe seat asked for this in comment
5829229738on objectui#10156. It is one commit,13ac7751e, fast-forward, and JSDoc plus changeset only: 20 lines added, 0 removed, no type moves.packages/types/src/objectql.ts: theObjectFormSchema.submitHandlerJSDoc. Its first paragraph is unchanged, so "hands the collected values" stays true for create. A new paragraph says that ineditmode, for a record the form read itself, the simple,modalanddrawerlayouts hand over what the form would have written: the fields that differ from that read, or the full sanitized payload when nothing changed. It also says a field whose sameness cannot be settled counts as changed, and that thetabbed,wizardandsplitlayouts, and a simple form the mobilestepperroutes through the wizard, still hand over every collected value. TheBulkActionDeficon example that draft PR objectui#8941 edits is untouched.ModalForm.tsxandDrawerForm.tsx: their exportedModalFormSchema/DrawerFormSchemacarry their own copy of the same "hands the collected values" sentence, under a comment that says the copy is declared from the canonical key so the two "can never drift apart". Each gains the same edit-mode clause..changeset/10156-edit-form-writes-only-changed-fields.md: declares@object-ui/typesas a patch, and one bullet names the correction.packages/types/src/zod/objectql.zod.ts, held by objectui#9618) carries nosubmitHandlerat all: 0 hits in the zod directory, against 2 files there that carryonSuccessas the control. There was nothing to report or to leave alone.The comparison rule
A field counts as clean only when every case below says "same". Anything this cannot settle is SENT.
===)null/undefinednull/'',undefined/''1/'1'(number vs numeric string)NaN/NaNEach row is pinned both ways round in
the one comparison — every pair the rule states.Premise checks (the dispatch's mechanism assumptions, measured)
SimpleObjectForm's fetch effect, and inModalForm/DrawerForm's fetch effects, and was already kept in component state (initialData/formData). A ref beside it was enough. Correction to the dispatch: the child-row comparison lived inmasterDetailTx.ts, not insanitize.ts. It now lives insanitize.ts.ifMatchis still theupdated_atthe form read. Overwrite resends the dirty fields only, pinned in"Overwrite" after a 409 re-sends the changed fields only, re-keyed to the server version.PATCHcarrying every sanitized field and reported the server's answer. It still does. Pinned for the plain edit and for the master-detail parent operation.region, the form emptiestier, andtier: nullis on the payload.updateandfindOneare never called.Pins (
editDirtyPayload-10156.test.tsx, real components, realsanitizeFormData)ifMatch;ops[0]is exactly{ref: 'PO-2'};5into a number input whose stored value is the string'5', and typing into then emptying a textarea whose stored value isnull. A unit row covers the lookup id against the expanded object;Reverse verification (head
cb00385aa)The fix was committed first. Each mutation went through
ablation-replace.mjs: the anchor had to hit exactly once, the blob change was verified on disk, and the file was trap-restored fromHEADon an absolute path. After every leg, thesanitize.tsblob equalsHEAD(9c115af88c2a) andgit diff HEADis empty. No rebuild was needed, because every consumer imports./sanitizerelatively inside the package.dirtyEditPayloadreturns the payload)String(a ?? '') === String(b ?? ''))sanitizeFormDatareturns its input (over the three retriaged files)an edit writes the edited field alone — never the FLS-refused onerowsFixtures retriaged, and why
A row that asserts a column ABSENT after a real edit now stays green with its filter deleted, because the dirty diff alone keeps an unchanged column off the wire. The new FLS edit rows show exactly this: they stayed green under A6. So each row whose job is the sanitizer, the roster or the field-level filter now submits with nothing changed. That is the one edit route whose payload is the filter's whole output, and A6 turns every one of them red.
ObjectForm.test.tsxstrips computed and server-managed fields from the edit-mode update payload: now submits unchanged, and its control is the storedname/budget.systemManagedPayload.test.tsxsends no server-owned column, from a form that declares none of them: same move. This row was still passing, but vacuously.fieldSecurityPayload.test.tsx: the filter row and the CONTROL row submit unchanged in all three containers. A new row per container pins that an edit writes the edited field alone. The file header says why.cascadePruneWire-10291.test.tsx, two EDIT rows: the negative-control select is now asserted ABSENT from the wire body, not present with its stored value. A wrongful null would still show, because it differs from the stored'gold'.drawerFirstLoadWindow-10190.test.tsxCONTROL: nowtoEqual({title: 'typed'}). That is stronger than before:noteis absent only because the form still holds the value the record landed with.Checks at the round-2 head (
13ac7751e)turbo run buildover the plugin-form dependency closure,--concurrency=2(includes@object-ui/types)@object-ui/typestype-checkpnpm --filter @object-ui/types run type-check(tsc --noEmit,tsconfig.examples.json,tsconfig.test.json)@object-ui/typessuites, including every suite that readsobjectql.tstextpnpm exec vitest run packages/types/pnpm exec vitest run packages/plugin-form/pnpm --filter @object-ui/plugin-form run type-checkeslintonobjectql.ts,ModalForm.tsx,DrawerForm.tsxcheck-changeset-*,check:pending-changeset-literalscheck:changeset-claimsobjectql.ts. Those paragraphs concern other faces or otherObjectFormSchemakeys, and none describes thesubmitHandlerdoccheck:*check-governed-queue-guard.mjs --testover the round-2 pathsRound 2 changed no executable line. So the round-1 reverse verification below, measured at
cb00385aa, still describes this code.Checks on the round-1 head (
cb00385aa)turbo run build --filter='@object-ui/plugin-form^...' --concurrency=2pnpm --filter @object-ui/plugin-form run buildpnpm exec vitest run packages/plugin-form/sanitizeFormData,ObjectForm,ModalForm,DrawerForm,saveWithOcc,occSaveormasterDetailTx(99 files, enumerated withgit grep -l)pnpm exec vitest runover them in two batchese111bfd68; everything after it touches only the changeset, README and docs pagepnpm --filter @object-ui/plugin-form run type-check(tsc --noEmit && tsc -p tsconfig.test.json)--listFileson the test project lists all six touched test fileseslint .inpackages/plugin-form(its ownlintscript),--format jsonno-explicit-anymoved with the comparison frommasterDetailTx.tstosanitize.tscheck-changeset-*check:changeset-claimscheck:*check:*/scripts/check-*scripts/check-*check-governed-queue-guard.mjs --testover the changed pathsNOT MEASURED:
check:readme-exports, which answered exit 1 with "population collapsed", andcheck:doc-snippets, which answered exit 2 PRECONDITION NOT MET. Both need packages outside this closure built, and neither answer is a verdict. Both read fenced code blocks, and this branch's README and docs-page additions contain 0 fences (git diffover the two files, counting added lines that open a fence). Remote CI on13ac7751e: 43 check-runs completed, 40 success and 3 skipped by design (dependabot,Test (coverage)and the coverage-shard matrix), 0 red.Declared narrowing on lint.
pnpm lintisturbo run lint, i.e. each package's owneslint .. The run above is that command for the one package this branch changes, over its whole tree.eslint.config.jssets neitherparserOptions.projectnorprojectService, so type-aware linting is not enabled, and a change inside this package cannot move the verdict on a file it does not touch. The.changeset, README and docs-page files are not eslint inputs.File surface
Round 2 added
packages/types/src/objectql.ts(thesubmitHandlerJSDoc only) through the seat's amendment, plus the JSDoc copies onModalFormSchema/DrawerFormSchema(see Round 2). In round 1, the claim namedsanitize.ts,ObjectForm.tsx,ModalForm.tsx,DrawerForm.tsx, the tests beside them and one changeset. This branch also touches:masterDetailTx.ts: the comparison's old home. It loses its private copy and imports the shared one; the function bodies are byte-identical;packages/plugin-form/README.mdandcontent/docs/plugins/plugin-form.mdx: the docs AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 requires;.changeset/10120-form-omits-fls-denied-fields.md: body only, frontmatter byte-identical. Its sentence "ObjectFormandModalFormproduce the same payloads they did before" would be false beside this change in the same release. It now says both already withheld the refused field and still do.Acceptance notes
tabbed,splitandwizardedit arms never callsanitizeFormData. Their editPATCHcarries the whole record:id,owner_id,created_by,updated_atand aformulacolumn, on this branch and onmainalike. This is objectui#10108's 403 and the "unknown field" rejection, on the arms that fix did not reach. The same arms also skip the field-level filter. A simple form withmobile.stepperroutes through the wizard arm too. The docs and changeset name all of these as not covered.createagain. It also drops a child cell changed back to its first-read value, so the second batch carries no child operation and still reports success. The row state'soriginalnever advances, and created rows never learn their ids. This is the child-row twin of the baseline advance above. The fix shape (refetch the children, or map the batch results) is not pinned, so it is not fixed in place here.updated_atfirst read asifMatch, so against a real server it would 409 on the user's own first save. OCC was out of scope and unchanged; this was not measured against a server.MasterDetailForm's fallback echo{...parentData, id}is used only when the batch returns noresults. It now carries the changed parent fields, not all of them.objectql.tswere already false before this PR, and are left as they are: thegroupBysentence in the pending changeset for the kanban group-by control, and theKanbanConditionalFormattingRulecomment in the one-authority-per-exported-name script test. Atcb00385aa, the changeset's address lands on a map-config comment and the script test's on aView typecomment. Round 2's inserted lines sit before both, but neither was true to begin with.sectionsbranch ofObjectForm.tsx, objectui#10475 and objectui#10476 were not touched.Session:
https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33CGenerated by Claude Code