Commit 0896838
fix(plugin-form): the line-items panel neither draws nor saves the lines it holds for another parent record (objectui#10740) (#10751)
Fixes #10740
Clause-②: no
A component's internal state and its Save guard; no declared surface
moves. `@object-ui/plugin-form` publishes `.` only, and no export, type,
prop or registration changes. Dispatched by the `domain:ui` seat 2,
session `https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`
(`mode:subagent`), executing triage `5852239622`. Round 3 closes one
residual write path (R7) the contract review found; the Round 3 section
at the foot carries its head and evidence.
## What changed
- **`packages/plugin-form/src/LineItemsPanel.tsx`** (+67 / −4 against
base). The panel now records which parent its held `rows` / `original`
belong to: one `useState`, `rowsHeldFor`, written wherever the rows are
written — beside a load's commit (`setRowsHeldFor(parentId)` next to
`setRows` / `setOriginal` / `setDirty(false)`), inside the CURRENT
load's `catch` that fails while nothing is held (an updater that keeps a
value already held and otherwise adopts `parentId`, inside
`isCurrent()`), and in `onChange` for an edit made before any load has
settled (the same keep-or-adopt updater, `parentId` narrowed to a string
first). One predicate, `heldForAnotherParent = rowsHeldFor !== undefined
&& rowsHeldFor !== parentId`, is read at three sites: the Save button's
`disabled`, the first statement of `save` after its existing
`dataSource` / `parentId` / `childObject` guard (it returns before
`setSaving`, before any batch is built), and the render, where a
placeholder (`data-testid="line-items-held-for-another-parent"`) stands
in for the grid ahead of `LineItemsField`. `save`'s and `onChange`'s
dependency lists gain what they read (`heldForAnotherParent`,
`parentId`).
- Unchanged: the rows commit's three existing statements (`setRows` /
`setOriginal` / `setDirty(false)`) and the `loading` release
(objectui#10712's PR 2 surface), the `loadSeqRef` / `isCurrent()` error
rule from PR objectui#10738, `save`'s two writes to `error`, every other
dependency list, every hook call, prop, export and registration. The
commit block gained the `setRowsHeldFor(parentId)` line and the `catch`
was re-braced to add the adoption; the three commit statements
themselves did not move.
-
**`packages/plugin-form/src/LineItemsPanel.heldForAnotherParent-10740.test.tsx`**
(new, +389 / −0): nine rows through the real `SchemaRenderer` and
`record:line_items`, every `find` and `batchTransaction` a deferred
promise settled by hand.
- **`.changeset/10740-line-items-held-for-another-parent.md`** (new, +24
/ −0, `'@object-ui/plugin-form': patch`).
## H1 — the reproduction on base `5ad3b88861`, and its controls
A throwaway probe (deleted, never committed) through the real
`SchemaRenderer` and `record:line_items`, ids explicit. The column is
`label` (aria-label `Line`), the child object `po_line`, the
relationship field `po`. Readings at base, then at the current head
`854e0922cc` (whose only change since `ef16b93486` is prose: the
readings are the pin's, re-run green there):
| sequence | base `5ad3b88861` | head `854e0922cc` |
|:--|:--|:--|
| `p1` answers `l1`, edited to `p1 line edited`; the host re-renders
with `parentId: 'p2'`; `p2`'s load rejects `p2 load failed`; then Save |
banner `p2 load failed`; drawn lines `['p1 line edited']`, inputs
enabled; **Save enabled**; Save ⇒ `batchTransaction` ops `[{object:
'po_line', action: 'update', id: 'l1', data: {label: 'p1 line edited',
po: 'p2'}}]` (`find` 2 calls, `batchTransaction` 1) | banner `p2 load
failed`; drawn lines `[]`, no grid, the placeholder drawn; **Save
disabled**; no batch (`find` 2, `batchTransaction` 0) |
| **R7** (the round-3 route): mount `p1` with NO adapter (`load` returns
at `!dataSource`, the grid is drawn, `rowsHeldFor` stays undefined);
type `typed under p1`; the host moves to `p2`, still no adapter; the
adapter arrives and `p2`'s load fails; then Save | grid drawn with
`['typed under p1']`, Save enabled; Save ⇒ `[{object: 'po_line', action:
'create', data: {label: 'typed under p1', po: 'p2'}}]` — a p1-typed line
created under p2 | placeholder drawn, `[]`, **Save disabled**, no batch
|
| lit control: the first row, but `p2`'s load answers `l2` `p2 line` |
drawn `['p2 line']`, no banner, Save disabled (nothing dirty); edit `l2`
+ Save ⇒ `[{object: 'po_line', action: 'update', id: 'l2', data: {label:
'p2 line edited', po: 'p2'}}]` | identical |
| same-parent failed RE-load: `p1`'s edit held, a host re-render with
`limit: 200` re-runs `p1`'s load, which rejects `p1 reload failed` |
banner; drawn `['p1 line edited']`, inputs enabled; Save enabled; Save ⇒
`[{object: 'po_line', action: 'update', id: 'l1', data: {label: 'p1 line
edited', po: 'p1'}}]` | identical (the edits are kept under their own
parent) |
| the DECLINE path: `p1`'s edit held, a re-render with `parentId: 'p2'`
and a refused filter (`{note: {$regex: 'a.c'}}`); the load declines
before any fetch | malformed-filter notice drawn, no grid; **Save
enabled** over the held (off-screen) rows; Save ⇒ `[{object: 'po_line',
action: 'update', id: 'l1', data: {label: 'p1 line edited', po: 'p2'}}]`
(`find` 1 call: the load never ran) | Save disabled; no batch |
The first row is the card's reading, op for op. R7 and the DECLINE row
are two routes the card did not name: R7 is a line typed under a grid
drawn with no adapter, adopted by the first parent whose load later
fails; the DECLINE row reaches the guard through the load declining
before its `catch`. Both are closed by binding the owner to the rows
wherever the rows are written.
## H2 — the shape: refuse by `rowsHeldFor`; nothing is cleared
The triage offered two shapes, "clear `rows` / `original`" or "refuse
Save". This PR refuses, on a value written wherever the rows are
written, and clears nothing. Why, measured:
1. **The decline path.** The DECLINE row above: a swap to `p2` with a
refused filter (and, by the same code path, a lost `dataSource`)
declines the load before its `try`, so a clear in the `catch` leaves
`p1`'s held rows saveable under `p2`. The guard reads the rows' own
parent and closes this path too (pinned; red at base).
2. **One writer of the owner per writer of the rows.** `rowsHeldFor`
moves where `rows` move (the load's commit, `onChange`) and once more
where a CURRENT load's failure leaves the empty rows on screen, so the
two cannot disagree. This also covers the one-render window between the
host re-render that moves `parentId` and the effect that starts the new
load: `rows` are still `p1`'s, `rowsHeldFor` still says `p1`, Save is
off in that frame too. R7 is the case where this failed at the round-2
head: `onChange` moved the rows without moving `rowsHeldFor` while it
was unset, so a line typed under an adapter-less mount stayed ownerless
until the next failure adopted it. `onChange` now writes the owner too.
3. **The same-parent rule.** A transient failure on a refresh must not
discard the author's unsaved edits (the RE-load row, and the "RE-load of
the SAME parent" pin). Held rows are never cleared, so a swap `p1` →
`p2` (failed) → `p1` (failed) draws `p1`'s held edit again under `p1`
(pinned as a control, "a swap BACK to p1"). A clear on the `p2` failure
would have discarded that edit.
4. **Adoption before a settle.** With nothing held yet, the parent on
screen is adopted the moment the rows first take a value: an edit made
in an unsettled grid (`onChange`, R7's fix), or the empty rows a CURRENT
load's failure leaves on screen (the `catch`, inside `isCurrent()`).
Either way a line the panel then holds belongs to the parent it was
entered under, not to a parent the panel later moves to. Measured at
base: a first load for `p1` fails, a line is added, the panel moves to
`p2` whose load fails ⇒ the added line is drawn under `p2` with Save
enabled (the defect, as a `create`). Pinned; red at base. A superseded
failure adopts nothing (the `catch` is inside `isCurrent()`).
5. **What the refused state draws.** After a failed load, the failure's
banner (the adapter's message, as before) and a muted placeholder where
the grid would be; after a refused-filter decline, that filter's own
notice, as before (only Save moves on that route); after a lost-adapter
decline, the placeholder alone. No line, editable or otherwise. A first
load that fails with nothing held is unchanged: banner plus the empty
grid with its entry row (pinned as a control). A load for the current
parent that commits takes the grid back (pinned).
The function-level guard in `save` is the write contract's own check, on
the function every caller reaches, in the same spirit as the
`childObject` guard beside it. It is redundant on the UI path by design:
the button and the function read the same render's predicate, so no
click through the rendered UI reaches `save` while the button is off.
See A2 below.
## H3 — every path from the held rows to a write
1. **`save`** → `buildMasterDetailEditBatch(parentObject ?? '',
parentId, parentPatch, [{rows, original, childSchema, …}])` →
`runBatchTransaction`. Guarded at both readers: the button's `disabled`
and the function's first statement. The rollup `sumRows(rows, …)` is
computed inside the same function, behind the same guard.
2. **`onChange`** → `setRows(next)`, `setDirty(true)`, and the owner
adoption. While the grid is drawn, `rowsHeldFor` is the current parent —
set by the load that offered the grid, or, when the grid was offered
before any load settled, by this edit itself — so an edit belongs to the
parent on screen, never to a parent the panel later moves to. In the
refused state the grid is not drawn (ablation A3 re-opens exactly this
and the "no p1 line is drawn" rows go red).
3. **The load's commit** → `setRows` / `setOriginal` / `setDirty(false)`
/ `setRowsHeldFor(parentId)`: the writer that defines whose the rows are
on a successful load.
4. **`LineItemsField` (`GridField` in `@object-ui/fields`)**: `grep -c
useEffect` on `GridField.tsx` at this base = 0; every `emit` /
`onChange` call sits in a user handler (`setCellValue`, `addRow`, the
remove handler, the reorder handler), all routed through the panel's
`onChange` in (2). Nothing changes rows without user input, the reading
PR objectui#10650 made and this PR re-took.
5. **No values bubble up.** The panel takes `schema` only; the
`record:line_items` registration (`LineItemsPanelRenderer` inside
`ElementDataSourceGate`) passes nothing else, and the post-save `load()`
reads no rows. No `forwardRef` / `useImperativeHandle` / key handler;
`save` is referenced once, at `onClick={save}`.
So there is one write path (`save`), guarded twice, and one edit path
(`onChange`), which brands every row it writes with the parent on
screen. **R7 is closed:** a row can no longer reach a write bearing a
parent it was not entered under, whether it was typed before a load
settled or held from a previous parent — in the refused state the grid
is not drawn, and in every drawn state the owner is unset or equals
`parentId`, and the first edit sets it to `parentId`.
## Pins and ablations
`LineItemsPanel.heldForAnotherParent-10740.test.tsx`, nine rows. Base
and ablation legs ran on COMMITTED source (`ef16b93486`, panel blob
`5884928c7a`) under `os-verify-lock`; the one later commit,
`854e0922cc`, moves prose only (no code token, proof in the Round 3
section), and the pin is re-run green there. Each mutation went through
objectstack's `scripts/ablation-replace.mjs` in WRAP mode (anchor
declared to hit once, counts and blob hashes read back from disk,
restore spelled `git checkout HEAD --` on the absolute path and proven
by blob == HEAD and an empty `git diff HEAD`). Two whole-file base legs,
each swapped in by hash under a shell `trap` and restored the same way:
the card base blob `0c498ce007` (`held ?? parentId` occurrences 0), and
the round-2 head blob `47696a5819` (occurrences 1 — the fix before R7
was closed). No build is on the resolution path: the pin imports
`./index` relatively and every `@object-ui/*` specifier resolves to
`src` through the root vitest alias, so `ablation-dist-preflight` does
not apply.
| row | base `0c498ce007` | round-2 head `47696a5819` | head
`854e0922cc` | red under |
|:--|:--|:--|:--|:--|
| p1 edited, swap to p2 whose load FAILS: no p1 line drawn, Save off, no
batch | **red** | green | green | A1, A3 |
| the same through a load that DECLINES (refused filter): Save off,
nothing sent | **red** | green | green | A1 |
| a line added under a FIRST load that failed, then a swap to p2 whose
load fails: not drawn under p2, Save off | **red** | green | green | A1,
A3 |
| **R7**: a line typed under an ADAPTER-LESS mount on p1, a swap to p2
with no adapter, then the adapter arrives and p2's load fails: no line
drawn under p2, Save off, no batch | **red** | **red** | green | A1, A3,
A6 |
| control: a swap to p2 whose load SUCCEEDS replaces p1's lines; a save
after editing p2's line carries `l2` under `p2` and nothing of `l1` |
green | green | green | A4 |
| control: a RE-load of the SAME parent that fails keeps the held edit
drawn, editable, Save on; the save carries `l1` under `p1` | green |
green | green | — |
| after a refused state, a load for the CURRENT parent that commits
takes the grid back | **red** | green | green | A3, A4 |
| control: a swap BACK to p1 whose load fails again draws p1's held edit
again | green | green | green | — |
| control: a FIRST load that fails draws the banner and the empty grid,
as before | green | green | green | — |
- **Base leg, card base blob `0c498ce007`**: `Failed Tests 5` (the four
refusal rows plus the recovery row), 4 green. Restore proven (blob ==
HEAD `5884928c7a`, `git diff HEAD` empty).
- **Base leg, round-2 head blob `47696a5819`**: `Failed Tests 1` —
exactly the R7 row. This is the one the round-3 close fixes; every other
row was already green at the round-2 head. Restore proven.
- **A1**, the button's `|| heldForAnotherParent` removed: 4 failed (the
three original "Save off" rows and R7).
- **A2**, the `save` guard neutralised (`if (false &&
heldForAnotherParent) return;`): **0 failed, 9 passed.** As under H2:
with the button guard in place no UI click reaches `save`, so this hunk
is observable only together with A1. Kept as the write contract's own
guard, not claimed as pinned.
- **A3**, the render branch made unreachable (`false &&
heldForAnotherParent ?`): 4 failed (the two "not drawn" rows, R7 and the
recovery row).
- **A4**, `setRowsHeldFor(parentId)` deleted from the load commit: 2
failed — the "swap to p2 SUCCEEDS" control and the recovery row, both at
`expected [] to deeply equal ['p2 line']`: with the commit no longer
branding the rows, `rowsHeldFor` stays at the value the p1 edit adopted,
so p2's own freshly loaded lines are refused behind the placeholder.
- **A5**, the `catch` failure-adoption deleted: **0 failed, 9 passed.**
Since Round 3, `onChange` also adopts, so every row whose owner the
`catch` used to set now has it set by the typed edit (or is an empty
grid, never saveable). The `catch` adoption is retained as the
documented adopt-on-failure behavior; like A2 it is kept and not
independently pinned. (At the round-2 head, before `onChange` adopted,
A5 reddened the "line added under a FIRST load that failed" row.) Its
one remaining observable effect is the B5 Acceptance note below.
- **A6**, the `onChange` owner adoption deleted (`setRowsHeldFor((held)
=> held ?? owner);`): **1 failed — exactly the R7 row.** This is the
ablation of the round-3 close.
- Each ablation leg printed `ok mutation landed: anchor 1 to 0` with the
blob change, and `ok restored: blob == HEAD (5884928…) and git diff
HEAD is empty`; the tree was clean before and after (`git status
--short` empty).
## Gates at head `854e0922cc`
Locked runs (`os-verify-lock`, `VERDICT command-exit 0` on each):
- `turbo run build --filter='@object-ui/plugin-form^...'
--concurrency=2`: `Tasks: 11 successful, 11 total`.
- `pnpm --filter @object-ui/plugin-form run type-check`: exit 0 with
`tsc --noEmit && tsc -p tsconfig.test.json` echoed; `--listFilesOnly`
counts the pin once in the `tsconfig.test.json` program and
`LineItemsPanel.tsx` once in the build program.
- `pnpm --filter @object-ui/plugin-form build`: exit 0, `[unplugin:dts]
Declaration files built`, `dist/index.js` emitted — the dts plugin fails
on any type error and did not. The three built artifacts are
byte-identical before and after the wording commit: `dist/index.js`
sha256 `9e08b5589db5…`, `dist/index.d.ts` `08fe75fa41b4…`,
`dist/index.umd.cjs` `19fddff1b34f…`, built at `ef16b93486` and again at
`854e0922cc`.
- the pin: `Tests 9 passed (9)`. The seven existing `LineItemsPanel*`
pins plus `recordFormsErrorClears-10682.test.tsx` at `ef16b93486`: `Test
Files 8 passed (8)`, `Tests 102 passed (102)` (the later commit moves no
code token).
- the whole package, `pnpm exec vitest run --maxWorkers=2
packages/plugin-form/`, at `7352890ae4` (round 1): `Test Files 131
passed (131)`, `Tests 1507 passed | 1 skipped (1508)`. The later commits
touch only `LineItemsPanel.tsx`, its pin and the changeset, covered by
the pin and sibling runs above.
Light gates, each exit 0 at `854e0922cc`: `check:new-line-citations`
(`VERDICT new-cross-file-line-citations: 0 new citation(s)`),
`check:control-bytes`; and at `ef16b93486` (no source token moved
since): `check:test-path-roots`, `check:vi-mock-specifiers`,
`check:vi-mock-inherit`, `check:vi-mock-override-shape`,
`check:shell-escape-residue`, `check:i18n-keys`,
`check:handler-key-reads`, `check:element-data-source-declaration`,
`check:self-import`, `check:unreferenced-sources`,
`type-check:coverage`, `lint:coverage`, `check:phantom-deps`,
`check:published-tsconfig-exclude`, `check:metadata-write-doors`.
Changeset gates at `854e0922cc`: `check-changeset-presence` (`2 source
file(s) of 1 released package(s) changed, and this change declares 1
changeset(s)`), `check-changeset-claims` (`No pending changeset names a
file this change touches`), `check-changeset-overwrite` (`No
pre-existing changeset was modified or deleted`),
`check-pending-changeset-literals`; `check-changeset-no-major` and
`check-changeset-fixed` at `ef16b93486`. `check-governed-merges.mjs
--test` over the three paths: NOT governed (0 of 3 paths hit the
register).
**eslint, narrowed to the 2 changed ts/tsx files** (`eslint
--no-inline-config --format json`): exit 0, the JSON counts 2 files, 0
errors. Narrowing evidence: the population is `eslint.config.js`'s
`files: ['**/*.{ts,tsx}']`; `--print-config` on `LineItemsPanel.tsx`
shows `parserOptions: {}` and no `project` / `projectService`, so no
rule is type-aware and this diff cannot move the verdict of a file it
does not touch. `LineItemsPanel.tsx` carries 18 warnings, the same count
and rule multiset as its base blob (14 `no-explicit-any`, 2
`set-state-in-effect`, 2 `exhaustive-deps`); the pin carries 12
`no-explicit-any`.
**NOT MEASURED locally, declared to CI**: the repo-wide `pnpm lint` and
the rest of the farm.
## Serial
At claim time no open PR touched `LineItemsPanel.tsx` (claim
`5852470959`). Before the round-1 pushes `origin/main` was `01700dcd7d`
(four commits past the base); before the round-4 push it is
`704e05b09d`, nine commits past the base. `git merge-tree --write-tree`
against it exits 0 with a clean tree (`be8cdeefbd`); at the round-5
review `origin/main` is `f308a655b8` (#10708, eleven commits past the
base), against which the tree is `01802fbba7`, also clean. None of the
eleven touches a file this branch touches (#10708 touched
`skills/objectui/` only); #10752 (`704e05b09d`) touched
`packages/plugin-form` elsewhere (six form arms,
`recordInvalidation.ts`, `formArmsInvalidation-10715.test.tsx`, and the
header comment of the sibling pin
`recordFormsErrorClears-10682.test.tsx`), and #10749 moved
`pnpm-lock.yaml`. objectui#10712's PR 2 is serial behind this PR, as the
claim says.
## H5 — pending changesets
`git grep` over `.changeset/` for `LineItemsPanel`, `line-items`, `line
items`: 14 pending files. The sentences that describe this panel's load,
rows or save were re-read against this change:
`10682-form-error-clears-2.md` (only the current load writes the banner;
a superseded load neither raises nor clears it — still true),
`10631-master-detail-save-in-flight.md` (the panel's grid is disabled
while its own save is in flight — still true),
`6194-line-items-row-fetch-decline.md` (the `childObject` declines and
`save`'s guard — still true), `6188`, `9050`, `9925`, `10163`, `8221`,
`9333` (each about another aspect of the panel — untouched). None
describes what a failed or declined load for another parent leaves on
screen, so no sentence is falsified and no dated note is needed; the new
changeset declares this change.
## objectui#10712, measured, not widened
The same probe took the two sequences objectui#10712's release comment
carries for its PR 2, at base and at this head:
- a late `p1` answer landing after `p2` committed: base draws `['p1
line']` under `p2`; head draws the placeholder (`rowsHeldFor` moved to
`p1` with the superseded rows, so they are refused), Save off. The
current answer, `p2`'s lines, is not kept: the run guard on the rows
commit is still that PR's to add.
- a save started on `p1` that lands after the swap to `p2`: its captured
`load` issues `find` with `po: 'p1'`; base draws `['p1 line edited']`
under `p2`; head draws the placeholder. The same partial mitigation, the
same remaining item.
objectui#10712 remains open; nothing here is scoped to it.
## Round 3
The round-2 head `11bf099f11` FAILED the contract review (`5852845813`)
on R7 — a line typed into a grid drawn with no adapter, adopted by the
first parent whose load later failed, then saved under it — and on four
wording items. Every other round-2 claim was reproduced by the review.
- **Round-3 range `11bf099f11..ef16b93`**, name-status / numstat: `M
packages/plugin-form/src/LineItemsPanel.tsx` (+34 / −14), `M
packages/plugin-form/src/LineItemsPanel.heldForAnotherParent-10740.test.tsx`
(+42 / −0), `M .changeset/10740-line-items-held-for-another-parent.md`
(+11 / −10). Two commits: `23f6bbc0` closed R7 in `onChange` and fixed
the wording; `ef16b93486` gave the `onChange` owner updater a type-safe
spelling (see Deviations).
- **The close.** `onChange` now writes the rows' owner beside the rows
when nothing is held yet — the seat's ruling permits the `onChange` form
as "strictly safer" because it covers any way a grid can be offered
before a load settles (the adapter-less decline, and a superseded run
releasing `loading`, objectui#10712's surface), not only the two the
ruling enumerated. `parentId` is narrowed in an `if (parentId)` block
before the updater closes over it.
- **The pin.** The new R7 row is red on the card base blob AND on the
round-2 head blob `47696a5819`, green at head, and reddened by A6 (the
`onChange` owner adoption deleted). Table above.
- **Round-3 wording follow-up, `854e0922cc`** (prose and comment only;
the round-3 contract review `5853196779` passed the code and failed the
published text). One commit, `M
.changeset/10740-line-items-held-for-another-parent.md` (+3 / −2), `M
packages/plugin-form/src/LineItemsPanel.tsx` (+4 / −4). The changeset
parenthetical now reads "(after a failed load a placeholder stands where
the grid would be, under the failure's banner; after a refused-filter
decline that filter's own notice stands there, as before; after a
lost-adapter decline the placeholder stands alone)"; its frontmatter is
byte-identical (md5 of the three frontmatter lines
`21fc281303c02040481bc4b368c13334` before and after). The `onChange`
comment's "The only grid offered before a load has settled…" became "The
grid this was found on is the one drawn when the load declined for want
of an adapter", which stays true against the superseded-release window
(the empty grid drawn while a later read is pending). No code token
moved: the commit's `.tsx` hunk has zero non-comment lines (`git diff
ef16b93 854e092 -- packages/plugin-form/src/LineItemsPanel.tsx`,
filtered to changed lines that do not start with `//`, is empty);
type-check and the dts build exit 0; and the three `dist/` artifacts
hash-equal before and after (Gates above).
- **Gates.** The plugin-form dependency-closure build, `type-check`
(both `tsc` programs) and `build` (the dts plugin) all exit 0 at
`854e0922cc`; the pin (9) is green there; the seven sibling pins plus
`recordFormsErrorClears-10682` (8 files / 102) and the light gates are
green at `ef16b93486`; the changeset gates, `check:control-bytes` and
`check:new-line-citations` are green at `854e0922cc`, listed above.
## Acceptance notes
- **The function-level guard is unpinned.** A2 moves no row, for the
structural reason under H2 and in the pin's header. Kept as the write
contract's own check; dropping it is a one-line change if the seat
prefers pins-only code.
- **The `catch` failure-adoption is unpinned since Round 3.** A5 now
moves no row: the `onChange` adoption covers the same typed-line rows,
and an empty grid left by a failed load is never saveable. Kept as the
documented adopt-on-failure behavior, parallel to A2.
- **B5.** After a FIRST load that fails with nothing typed, a swap to
another parent whose load also fails draws the placeholder, not the
empty grid (base drew the empty editable grid); the catch adoption's one
observable effect now that `onChange` adopts. Unpinned.
- **The refused state's placeholder** is hard-coded English, like this
panel's other placeholders (`Loading…`, `Save the record first to add
line items.`, the `childObject` hint); only the malformed-filter state
goes through `createSafeTranslation`. Observation, not filed.
- **A superseded save's failure** is still written unguarded (PR
objectui#10738's note); not re-measured here.
- **A first load that fails** still draws an empty editable grid under
the banner, as before this card; a line typed there now belongs to that
parent and stays with it (H2 item 4).
- No class (a), (b) or (c) finding beyond the objectui#10712 rows its
release comment already carries.
## Deviations
- **Attribution.** Commit trailers are the dispatch's model-free pair,
and this body ends with the role file's session-URL footer. The harness
attribution reminder asked for other lines.
- **The seat's prescribed close needed a type-safe spelling.** The
ruling's line `setRowsHeldFor((held) => held ?? parentId)` (and
equivalently the `onChange` form) does not type-check: in `onChange`,
`parentId` is `string | null | undefined` (`RecordContextValue.recordId`
is `string | null | undefined`) with no `load` guard above it to narrow
it, and the setter's state is `string | undefined`, so the updater's
`held ?? parentId` widens to include `null` — CI job 108556174339 red at
`src/LineItemsPanel.tsx` `error TS2345` on commit `23f6bbc0`, breaking
the plugin-form build and the six gates behind it. Fixed in `ef16b93486`
by capturing the narrowed value: `if (parentId) { const owner =
parentId; setRowsHeldFor((held) => held ?? owner); }`. Semantics
unchanged (a grid is only offered with a parent bound). The `catch`'s
copy of the line type-checks because `load`'s early `if (!dataSource ||
!parentId) return;` narrows `parentId` to a string before it.
- **The ablation legs run from inside the worktree**, because
`ablation-replace` resolves the repository from its cwd; run through the
lock wrapper's default cwd it exits 2 at its own prerequisite (nothing
written, nothing measured).
- **A removed pin row (Round 1).** The pin's first commit carried a
"defence in depth" row that cleared the Save button's `disabled` on the
DOM node and clicked. React filters a click by the fiber's `disabled`
prop, not the DOM property, so the click never reached `save` and the
row passed vacuously (A2 showed it). It was removed in `11bf099f11`
rather than kept as a false pin; the pin header states what it would
have pinned and why nothing can.
- **Round-4 body corrections (published text only).** The round-3 review
found four false evidence statements in the returned body and two
overstatements; all are replaced here with the record's text: the pin
table's "red under" column (A4 reddens only the p2-SUCCEEDS control and
the recovery row), the A4 bullet's cause, the Serial paragraph (main
re-read before the push, the merge-tree re-run at the head), the round
numbering (`11bf099f11` was round 2; `23f6bbc0` and `ef16b93486` are
round 3), H3's closing and H2 item 2.
---
_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f308a65 commit 0896838
3 files changed
Lines changed: 480 additions & 4 deletions
File tree
- .changeset
- packages/plugin-form/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
0 commit comments