Skip to content

fix(plugin-form,fields): a superseded load or save commits nothing in the line-items panel; useRecordQuery resets when it can no longer query (objectui#10712) - #10773

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10712-superseded-read-guard-2
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10712-superseded-read-guard-2

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10712

Clause-②: no

PR 2 of 2 for objectui#10712. PR objectui#10723 (merged 4345558, Refs #10712) carried the six record forms and useRecordQuery's runQuery. This PR carries the six rows seat 5's release note 5852404775 listed for PR 2: LineItemsPanel's load and save (R1–R2), its dirty-reload hold (R3, per the seat's ruling 5855609171, option A, in round 2), and useRecordQuery's reset and debounce (R4–R6, with R6's sort / page sibling, found by the round-2 contract review 5856013809, fixed in round 3). No export, prop, schema key or registration moves; @object-ui/plugin-form and @object-ui/fields publish the same surface. Dispatched by the domain:ui seat 2, session https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN (mode:subagent), executing triage 5839162848, release note 5852404775 and ruling 5855609171.

What changed

  • packages/plugin-form/src/LineItemsPanel.tsx (+120 / −8 against base 25c7d584e4; round 2 added +76 / −4).
    • R1. The load's rows commit (setRows / setOriginal / setDirty / setRowsHeldFor) sits behind if (!isCurrent()) return;, taken right after the awaited find, and the finally releases loading only for the current run (if (isCurrent()) setLoading(false)). The setError(null) after the commit keeps its rule (cleared on a commit, objectui#10682) and no longer needs its own isCurrent().
    • R2. The save takes the parent it saves (savedParent) and a parentStillShown() predicate read from latestLoadRef (the panel's latest load and parentId, written from an effect keyed on both, never during render); after the batch it reloads through latestLoadRef.current.load('save') only while that parent is still on screen, and writes its failure only then. save's dependency list drops load, which it no longer reads.
    • R3 (round 2). load takes an origin ('inputs' from the fetch effect, 'save' from the post-save reload, the default). An 'inputs' read is HELD when the panel holds unsaved edits for the CURRENT parent: dirty, the rows held for parentId, and the last run's parent, adapter, child object and relationship field equal to the current ones. A held read is not numbered (it supersedes nothing) and sets heldReloadRef; every run that does start clears that flag, since every run reads the panel's current inputs. A same-parent read that was in flight at the hold replays it once on its commit through a heldReplay state bump the fetch effect is keyed on (the panel is clean after that commit, so the replay is not held). The values load reads to decide this live in refs, so an edit does not move load: heldEditsRef mirrors dirty / rowsHeldFor from an effect declared ahead of load; lastRunRef is written where a run is numbered. The precedent is the default form's bus hold, formDirtyRef / heldChangeRef in ObjectForm.tsx (unsaved input holds the re-read; one is replayed when the form is pristine again or its save lands), named in the comment.
    • The ruling's condition is the three-clause one (dirty, rowsHeldFor equals parentId, the last run's parent equals parentId). The adapter / child object / relationship-field identity is added to it so the hold covers exactly the inputs the ruling names (sort, limit, filter): rows loaded from another adapter or another object are not the same rows, so a change of either is read. Pinned as controls.
    • Every decline in load still releases loading before any await, so a declining run is never superseded at the finally.
  • packages/fields/src/widgets/useRecordQuery.ts (+52 / −30 against base; round 3 added +13 / −7). One reset callback (the consumer's reset() and the reset effect's body) bumps runSeqRef, clears the debounce timer, ends loading, and clears the query state. The reset effect is keyed on canQuery (enabled AND a data source with find AND an object name) instead of enabled. A clearDebounce callback replaces the three inline clearTimeout spellings. The debounce cleanup effect (formerly unmount only) is keyed on what the fetch effect is keyed on: runQuery (moved by enabled, the data source, the object, pageSize, paginate, the filter, expand and searchFields) and, since round 3, page and sort, which move without moving runQuery. So a pending debounced search is dropped whenever the fetch effect is about to run a query with the typed term, and on unmount. The header bullet about clearing is reworded.
    • Round 3, the R6 sibling. Keyed on runQuery alone (round 2), the cleanup let a setSearch timer armed before a toggleSort / setSort or setPage inside the window stay live: setSearch arms it with page 1 and the sort in scope, the fetch effect ran the query with the new sort or page and the typed term, and the timer then ran as the latest call with the older sort or page, so its answer won. The reviewer offered two fixes: clearDebounce() at the top of the fetch effect body, or page and sort in the existing cleanup's key. The second is one dependency-list edit on the effect that already exists and already covers unmount, so it is the fix. refetch() is not a clearing point (source reading, not pinned): called in the same event as a setSearch, its closure still holds the previous term, so dropping the armed timer there would drop the typed one.
  • Dependency lists that move, stated in full: in the panel, save drops load (it reads latestLoadRef), the latestLoadRef effect is keyed [load, parentId], the heldEditsRef mirror effect [dirty, rowsHeldFor], and the fetch effect goes from [load] to [load, heldReplay]; load's own list is unchanged. In the hook, the reset effect goes from [enabled] to [canQuery, reset], the debounce cleanup effect from [] to [runQuery, page, sort, clearDebounce] (round 2 had [runQuery, clearDebounce]), setSearch gains clearDebounce, and the two new callbacks are keyed [] (clearDebounce) and [clearDebounce] (reset). The hook's fetch effect ([canQuery, page, sort, runQuery]) and runQuery are unchanged.
  • Pins: packages/plugin-form/src/LineItemsPanel.supersededRun-10712.test.tsx (10 rows, R1–R2), packages/plugin-form/src/LineItemsPanel.dirtyReloadHold-10712.test.tsx (14 rows, R3, round 2), packages/fields/src/widgets/useRecordQuery.cannotQuery-10712.test.tsx (12 rows, R4–R6: 10 in round 1, the sort-in-window and page-in-window rows in round 3, whose header now names both triggers).
  • One merged pin edited (round 2, per the ruling): LineItemsPanel.heldForAnotherParent-10740.test.tsx, the row "a RE-load of the SAME parent that fails keeps the held edits: the edited line is drawn editable, Save is on, and the save carries l1 under p1". Exactly what changed: its TRIGGER. It re-read p1 through a host re-render with limit: 200 while p1's edit was held; under the hold that re-render issues no read, so the row would have waited forever for its second find. It now presses Save, lands the batch, and fails the post-save reload (the second find, still p1's). Every assertion is unchanged: the edited line drawn, inputs enabled, no placeholder, Save on, and the save (now the row's second save) carrying update l1 { label: 'p1 line edited', po: 'p1' }. The property it keeps — a same-parent re-read that fails leaves the author's lines drawn, editable and saveable under that parent — is the same, exercised through the one same-parent re-read that still runs while edits are held. The row's comment and the pin header's bullet say so. The other eight rows are byte-identical and green.
  • One round-1 pin row rewritten (round 2): supersededRun-10712's "the reload after a save reads the panel's CURRENT inputs, not the ones captured at the click" re-rendered with limit: 200 while the save was in flight and expected an immediate second find; under the hold it now asserts no read is issued for the change (find still 1), and that the post-save reload (now the second find) carries $top: 200. Ablation A5 (the captured load) still reddens it.
  • Two patch changesets, one per package, no new changeset in round 3. The plugin-form one gained an R3 paragraph in round 2; its frontmatter is byte-identical. The fields one is unchanged since round 1: its closing sentence 「Now a pending debounced search is dropped when the query changes.」, which the round-2 review measured false at bef026bdc5 for a sort or page change, is now true for every input runQuery reads plus page and sort (pinned for filter, sort and page; the other inputs move runQuery the same way the filter does).

The six rows, measured first

Every measurement is the new pin run on the UNTOUCHED base sources (25c7d584e4 for R1–R2 and R4–R6; the round-1 head 5cd02243bb for R3), before the source edit, through the real SchemaRenderer and record:line_items (panel) or renderHook (hook), every find and batchTransaction a deferred promise settled by hand.

row measurement on base change
R1 load: rows commit and loading scoped to the run TRUE. p1's read held, host moves to p2, p2 answers p2 line, then p1 answers late: drawn lines [] (the superseded rows were committed, then refused as p1's behind the objectui#10740 placeholder; p2's answer gone either way). Superseded read landing FIRST as a failure: loading paragraph gone and the grid drawn while p2's read was pending; landing first as a success: loading gone, placeholder drawn. the rows commit and the finally read isCurrent()
R2a save reloads the OLD parent TRUE. p1 edited, Save (batch held), host moves to p2, p2 answers; the batch lands: a THIRD find with po: 'p1' (find called 3 times, expected 2), and the save never ends. With p2's load having FAILED instead: the same third read, whose commit would clear p2's own failure banner. the reload runs only while savedParent is on screen, through the panel's latest load
R2b save failure written after a move TRUE. Same sequence, the batch rejects p1 save failed: the banner is drawn over p2's panel. the failure is written only while savedParent is on screen
R2c save reloads with the inputs captured at the click TRUE. Save on p1 (batch held), host re-renders with limit: 200; the batch lands: the save's reload carries $top: 500, the click's row cap. latestLoadRef.current.load('save')
R3 a sort / limit / filter change while rows are dirty discards the edits TRUE on all three triggers (round-1 probe, then the round-2 pin on 5cd02243bb): before, drawn ['p1 line edited'], Save on, 1 find; the re-render issues a second find for p1 (with $orderby, $top: 200, or the AND-merged filter) and the grid is replaced by the loading paragraph; after the answer, drawn ['p1 line'], Save off, no banner. Silent. the hold (round 2)
R4 enabled goes false with a read in flight TRUE. After the disable: records [], loading TRUE; the late answer then commits: records ['late'], loading false. reset() behaves the same. the reset bumps runSeqRef and ends loading
R5 objectName / dataSource going null TRUE. objectName to null after first committed and a refetch in flight: records stay ['first'], loading stays TRUE, then ['late'] commits. dataSource to null with a read in flight: loading stays TRUE, the answer commits. the reset effect is keyed on canQuery
R6 the debounced setSearch closure TRUE. setSearch('a'), then a filter change inside the window: the fetch effect issues the current query ($filter closed, $search 'a'), and the timer then issues a THIRD query from the older closure (find called 3 times). enabled going false inside the window: the pending search still ran. Sibling, found by the round-2 review, re-measured on bef026bdc5 by the round-3 base leg and StrictMode probe: setSearch('a') then toggleSort('name') inside the window: the fetch effect's call carries $orderby name asc and $search a, and the timer then issues a THIRD call with $search a and no $orderby; with paginate, setSearch('a') then setPage(2): the second call carries $skip 10, the third $skip 0. Answered last, the third wins (the review's reading: records UNSORTED while sort reads name asc, PAGE1 while page reads 2). the debounce is cleared where runQuery, page or sort moves (the fetch effect's own key, round 3) and in the reset

Pins and ablations, round 1 (R1–R2, R4–R6)

Base leg (untouched 25c7d584e4, pins written first), one locked run: VERDICT command-exit 1, Tests 13 failed | 9 passed (22) over the two round-1 pins plus the R3 probe; each red row failed on its intended assertion (quoted in the round-1 report on the card, 5855592309). Fix leg at 5cd02243bb: the two pins plus every sibling pin (LineItemsPanel* ×8, recordFormsErrorClears-10682, supersededReads-10712, useRecordQuery.supersededAnswer-10712, useRecordQuery.test.ts): VERDICT command-exit 0, Test Files 14 passed (14), Tests 178 passed (178) (with the 9-row hook pin, before its reset()-in-window control).

Ablations on committed 5cd02243bb, each through objectstack's scripts/ablation-replace.mjs (anchor declared to hit once; anchor 1 to 0, blob changed, then blob == HEAD and git diff HEAD empty), under a shell trap on EXIT INT TERM; panel legs over the R1–R2 pin plus recordFormsErrorClears-10682 and heldForAnotherParent-10740 (88 rows), hook legs over the R4–R6 pin plus supersededAnswer-10712 (18 rows). No build is on the resolution path (the pins import ./index / ./useRecordQuery relatively and @object-ui/react resolves to src through the root vitest alias).

leg mutation red
A1 the rows-commit guard if (!isCurrent()) return; deleted 2: 「values: a SUPERSEDED load that lands LAST」 and objectui#10682's 「a SUPERSEDED load that succeeds does not clear the failure the current load reported」
A2 finally releases loading unconditionally 2: both loading rows
A3 the save reloads regardless of parentStillShown() 2: 「LANDS after the swap to p2」 and 「swap to p2 whose load FAILED」
A4 the save's failure written regardless of parentStillShown() 1: 「FAILS after the swap to p2」
A5 the save reloads through the captured load 1: 「reads the panel's CURRENT inputs」
A6 runSeqRef.current += 1 deleted from reset 4: enabled, reset(), objectName, dataSource
A7 setLoading(false) deleted from reset 4: the same four, on their loading assertion
A8 the reset effect keyed on enabled again 2: objectName, dataSource
A9 the debounce cleanup keyed on unmount only 1: 「a filter change inside the debounce window」
A10 clearDebounce() deleted from reset 1: the reset()-in-window control

Pins and ablations, round 2 (R3)

LineItemsPanel.dirtyReloadHold-10712.test.tsx, 14 rows: for each of sort, limit and filter, a clean-change control (reloads with the change), the dirty row (no read, edits kept drawn and editable, Save on, no loading paragraph, no placeholder), and the post-save row (the reload after Save carries the changed input, exactly two find calls); plus five controls: a parent move while dirty is read and the held rows refused there (objectui#10740's shape), a swap back to the edited parent after a move is read (the last-run-parent clause), a change held while a same-parent read was in flight is replayed once on that read's commit (the fourth find carries $top: 200), a change of adapter is read, and a change of child object is read.

Base leg on the round-1 head 5cd02243bb (the R3 pin, the edited 10740 pin and the edited R1–R2 pin, written before the source edit), one locked run: VERDICT command-exit 1, Tests 9 failed | 24 passed (33). The red rows, each on its find count: the three dirty rows (「a same-parent reload ran over unsaved edits: called 1 times, but got 2 times」), the three post-save rows (1 expected, 2 before Save), the parent-move control (its pre-move hold assertion), the in-flight replay row (3 expected, 4), and the rewritten R2c row. Green on base: the three clean controls, the swap-back row, the adapter and child-object rows, and the re-judged 10740 row (its post-save trigger exercises the property at either source).

Fix leg at bef026bdc5, one locked run: the R3 pin, the R1–R2 pin, every sibling LineItemsPanel* pin (×8), recordFormsErrorClears-10682 and supersededReads-10712: VERDICT command-exit 0, Test Files 12 passed (12), Tests 165 passed (165).

Ablations on committed bef026bdc5 (panel blob bbeb098240f7), each through objectstack's scripts/ablation-replace.mjs (anchor declared to hit once; 「ok mutation landed: anchor 1 to 0」 with the blob change, then 「blob after restore」 == HEAD and git diff HEAD empty, git status clean before and after), under a shell trap on EXIT INT TERM, over the R3 pin, the 10740 pin and the R1–R2 pin (33 rows). Three legs (H2, H3, H7) were first spelled with a replacement that was a substring of its own anchor; the tool refused them as no-ops (「the replacement count moved N to N, a rise of 0」), nothing was written (restore proven), and they re-ran with a unique replacement token, readings below.

leg mutation red
H1 the hold condition neutralised (if (false && …)) 9: the three dirty rows, the three post-save rows, the parent-move control's pre-move assertion, the in-flight replay row, and the rewritten R2c row
H2 the last-run-parent clause dropped 3: 「a swap BACK to the edited parent after a move is read」, 10740's 「a swap BACK to p1 whose load fails again」, and the in-flight replay row (the swap back is held)
H3 the adapter / child-object / relationship-field identity clauses dropped 2: 「a change of ADAPTER while dirty is read」, 「a change of CHILD OBJECT while dirty is read」
H4 the dirty clause dropped 4: the three clean-change controls (a clean change is held), and the in-flight replay row (the replay is held)
H5 the rowsHeldFor equals parentId clause dropped 1: 10740's 「after a refused state, a load for the CURRENT parent that commits takes the grid back」 (the held rows are another parent's, and the reload for the current one is held)
H6 the replay on commit deleted 1: the in-flight replay row
H7 a run no longer clears the pending flag 4: the three post-save rows and the rewritten R2c row (an extra replay read after the post-save reload)
H8 the post-save reload marked 'inputs' (so it can be held) 6: the re-judged 10740 row, the R1–R2 control 「a save whose parent is still on screen reloads it」, the three post-save rows, and the rewritten R2c row

Pins and ablations, round 3 (the R6 sibling)

Two rows added to useRecordQuery.cannotQuery-10712.test.tsx: 「a sort change inside the debounce window: the fetch effect carries the typed term with the new sort, and the armed timer runs no older query」 (setSearch('a'), toggleSort('name'); the second find carries $orderby name asc and $search a; the window is outwaited; exactly two find calls; the answer commits with sort reading name asc) and 「a page change inside the debounce window (paginate): the fetch effect carries the typed term on the new page, and the armed timer runs no older query」 (pageSize 10, setSearch('a'), setPage(2); the second find carries $skip 10 and $search a; exactly two calls; page reads 2).

One locked chain at ad6629e704 (VERDICT command-exit 0, each leg's exit read from its own log; the hook swapped and restored by git checkout, restore proven by blob == HEAD 910e8d78a8e3 and git diff HEAD empty after every leg):

  • Base leg: the hook at bef026bdc5 (blob d010cbf927c7, the round-3 key spelled 0 times on disk, the round-2 key 1 time) under the three useRecordQuery.* pins: vitest exit 1, Test Files 1 failed | 2 passed (3), Tests 2 failed | 28 passed (30). The red rows are exactly the two, each on its find count: 「the debounced timer ran the query it was armed with, without the sort: expected "vi.fn()" to be called 2 times, but got 3 times」 and 「the debounced timer ran the query it was armed with, for page 1: expected "vi.fn()" to be called 2 times, but got 3 times」.
  • Fix leg: the three useRecordQuery.* pins plus the plugin-form panel set (LineItemsPanel* ×10, recordFormsErrorClears-10682, supersededReads-10712): vitest exit 0, Test Files 15 passed (15), Tests 195 passed (195).
  • Ablation A11, through objectstack's scripts/ablation-replace.mjs (WRAP mode, anchor declared to hit once): the cleanup key back to [runQuery, clearDebounce]. 「ok mutation landed: anchor 1 to 0, blob 910e8d78a8e3 to e1a59b997873」 (the tool's arrows spelled as a word); over the three useRecordQuery.* pins: vitest exit 1, Tests 2 failed | 28 passed (30), the red rows exactly the two round-3 rows; then 「blob after restore」 and 「blob at HEAD」 both 910e8d78a8e3, 「git diff HEAD empty」.
  • StrictMode, a throwaway renderHook probe under a React.StrictMode wrapper (deleted, never committed; readings written to a file), the term a armed and the change made inside the window, counting the find calls after the double-mounted initial reads: at bef026bdc5 sort 2 (the second $search a without $orderby), page 2 (the second $skip 0), filter 1, plain setSearch 1; at ad6629e704 sort 1, page 1, filter 1, plain 1, each carrying the term with the new sort, page or filter. A setSearch('a') from page 2 then setSearch('ab') reads the same at both heads: a at once through the fetch effect (the page moves to 1), then ab through the timer. StrictMode double-invokes effects on mount only, where no timer exists yet.

Gates

At 5cd02243bb (round 1): pnpm turbo run build --filter="@object-ui/plugin-form^..." --filter="@object-ui/fields^..." --concurrency=2 exit 0, Tasks: 11 successful, 11 total; pnpm turbo run type-check --filter @object-ui/plugin-form --filter @object-ui/fields --concurrency=2 exit 0, Tasks: 14 successful, 14 total; the union pnpm exec vitest run --maxWorkers=2 packages/plugin-form/ packages/fields/ exit 0, Test Files 346 passed | 1 skipped (347), Tests 5000 passed | 8 skipped (5008); the round-1 light gates and the narrowed eslint (recorded in the round-1 report 5855592309).

At bef026bdc5 (round 2), one locked chain, each step's exit read from its own log: the round-2 ablations above (the tree restored, panel blob == HEAD bbeb098240f7, git diff HEAD empty, git status clean); pnpm turbo run build --filter="@object-ui/plugin-form^..." --concurrency=2 exit 0, Tasks: 11 successful, 11 total (the recreated worktree had no dist/; the closure was rebuilt from turbo's cache, which the type-check then resolved against); pnpm turbo run type-check --filter @object-ui/plugin-form --concurrency=2 exit 0, Tasks: 13 successful, 13 total, tsc --noEmit && tsc -p tsconfig.test.json echoed for @object-ui/plugin-form. The fields package was byte-identical to round 1 at that head.

At ad6629e704 (round 3, the fields package only; plugin-form is byte-identical to bef026bdc5), one locked chain, each step's exit read from its own log, VERDICT command-exit 0: pnpm turbo run type-check --filter @object-ui/fields --concurrency=2 exit 0, Tasks: 12 successful, 12 total, with 「@object-ui/fields:type-check: cache miss, executing」 (run fresh, not replayed) and tsc --noEmit && tsc -p tsconfig.test.json echoed; --listFilesOnly puts the pin in the tsconfig.test.json program and the hook in the tsconfig.json program (1 hit each). The fields package suite, pnpm exec vitest run --maxWorkers=2 packages/fields/: exit 0, Test Files 213 passed | 1 skipped (214), Tests 3439 passed | 7 skipped (3446) (the hook's three callers, LookupField, the people picker and RecordPickerDialog, live there).

Light gates at bef026bdc5 and again at ad6629e704, each exit 0 at both heads: check:control-bytes, check:new-line-citations (VERDICT new-cross-file-line-citations: 0 new citation(s)), 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:phantom-deps, check:unreferenced-sources, check:published-tsconfig-exclude, check:metadata-write-doors, lint:coverage, type-check:coverage, check:changeset-claims (「No pending changeset names a file this change touches」), check:pending-changeset-literals, check-changeset-presence (「6 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)」), check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed; check-governed-queue-guard --test over the 8 paths: NOT GOVERNED.

eslint at bef026bdc5, narrowed to the panel and its three pins (eslint --no-inline-config --format json): exit 0, 4 files, 0 errors. LineItemsPanel.tsx carries 18 warnings with the same rule multiset as its base blob (14 no-explicit-any, 2 set-state-in-effect, 2 exhaustive-deps; the two set-state-in-effect sites and the two exhaustive-deps sites are the base's own). The pins carry only no-explicit-any (14, 12, 10). eslint at ad6629e704, narrowed to the hook and its R4–R6 pin: exit 0, 2 files, 0 errors; useRecordQuery.ts carries 8 warnings with the same rule multiset and the same four non-any sites as its base blob linted through --stdin --stdin-filename (4 no-explicit-any, 2 exhaustive-deps at runQuery and the fetch effect, 2 set-state-in-effect at the fetch and reset effects; the debounce cleanup's new key draws none), and the pin carries 8 no-explicit-any. The narrowing evidence is the round-1 one: the population is eslint.config.js's files: ['**/*.{ts,tsx}'], --print-config shows parserOptions: {} and no project / projectService, so no rule is type-aware and this diff cannot move an untouched file's verdict.

NOT MEASURED locally, declared to CI: the repo-wide pnpm lint and the rest of the lint and test farm.

Serial

At claim time (2026-09-27T10:58Z) no open PR touched either file. origin/main is fetched into a private ref (refs/os/10712-main) before each push; the round-3 fetch, right before the round-3 push, reads 17b323e5ca: five commits past the base 25c7d584e4 (63835d998f, 35eb2f0ee3, b5b6f37531, 805abe240c, 17b323e5ca; 35 files), none a file this branch touches (overlap 0 of 8). git merge-tree --write-tree refs/os/10712-main HEAD at ad6629e704 exits 0 with a clean tree (ca89b681fe). No merge commit was needed. The head is ad6629e704 (four commits on the branch: 5cd02243bb round 1, bef026bdc5 round 2, 8451dc9709 and ad6629e704 round 3, the second a comment-only pin header), and the remote branch is at the same sha.

Acceptance notes

  • R3's cost, stated (the ruling's own sentence). While a change is held, the grid shows the rows under the previous sort, filter or limit until the panel saves, with no signal. The next read (the post-save reload, a parent move, or the commit of a same-parent read that was in flight) carries the change.
  • A failed save keeps the hold. The batch rejects, dirty stays true, the failure banner is drawn over the edited lines (unchanged), and the held change stays pending until a read runs.
  • A refused-filter change while dirty is held like any other input change; the malformed-filter notice is drawn ahead of the grid as before, and no find is issued either way (at base the load declined before the wire), so nothing observable moves.
  • reset() fixed in place (round 1): a consumer's reset() with a read in flight had the R4 gap. No caller in this repository calls reset(); it is the exported API's own guard.
  • total is not reset when the hook clears (base behaviour kept). Observation, not filed.
  • A save in flight across a parent move keeps saving true until its batch lands (base behaviour, disabled={saving}, objectui#10631); a save whose parent left the screen ends when the batch does, without a reload.
  • Attribution. Commit trailers are the model-free pair (Co-authored-by: Claude and Claude-Session:), and this body ends with the role file's session-URL footer; the harness reminder asked for a model-named trailer and another footer.
  • latestLoadRef is a passive-effect mirror, left as is (the round-2 review's observation, not a regression): in the microtask between a non-discrete host re-render's commit and its passive flush a settling save reads the previous parentId, its re-read is superseded by the fetch effect's read in the same flush, and only its failure banner could land in that window, which base wrote unconditionally; useLayoutEffect would close it.
  • One class (a) finding beyond the six rows, found by the round-2 contract review (5856013809) and fixed here in round 3: R6's defect by a sibling trigger in the same hook, a setSearch timer armed before a sort or page change running last with the older sort or page (measurement in the R6 row, pins and ablation in the round-3 section). Fixed in-PR as the same defect class in the same file, a mechanical key change, and the fields changeset's closing sentence already claimed the property. No other class (a), (b) or (c) finding.

Generated by Claude Code

… the line-items panel; `useRecordQuery` resets when it can no longer query (objectui#10712)

`LineItemsPanel`: a load a newer one has superseded commits neither its rows
nor the end of the loading state (the run number `loadSeqRef` now guards the
rows commit and the `finally`). The save reloads through the panel's latest
`load` only while the parent it saved is still on screen, and writes its
failure only then; a save whose parent has left the screen re-reads nothing
and reports nothing.

`useRecordQuery`: the reset is keyed on `canQuery` (`enabled`, the data
source and the object together) and is the same `reset()` a consumer calls.
It moves the run number, so a read in flight commits nothing, ends `loading`,
drops a pending debounced search and clears the query state. A pending
debounced search is also dropped whenever `runQuery` changes, since the fetch
effect re-runs the current query with the typed term then.

Pins: `LineItemsPanel.supersededRun-10712.test.tsx` and
`useRecordQuery.cannotQuery-10712.test.tsx`; one `patch` changeset per package.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3067.8 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-Cxr5-zRR.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 554.94KB 132.67KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.27KB 65.67KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 157.64KB 40.28KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…le it has unsaved edits (objectui#10712 R3)

A change to a load input other than the parent (sort, limit, filter) is held
while the panel holds unsaved edits for the current parent: dirty, the rows
held for `parentId`, and the last run's parent (and adapter, child object,
relationship field) equal to the current ones. A held change issues no read
and is not numbered. The next read that runs carries it: the post-save
reload, a parent move, or the commit of a same-parent read that was in flight
at the hold, which replays one read. The precedent is the default form's bus
hold (`formDirtyRef` / `heldChangeRef` in ObjectForm.tsx).

The objectui#10740 control row "a RE-load of the SAME parent that fails keeps
the held edits" keeps its assertions and changes its trigger to a post-save
reload that fails, since a limit change no longer reads while edits are held.

Pin: `LineItemsPanel.dirtyReloadHold-10712.test.tsx`; the plugin-form
changeset covers R3, frontmatter unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3068.0 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-UksYeih6.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 554.94KB 132.67KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.27KB 65.67KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bef026bdc5c910aed4fbaf073804422daa29c587

Setup: refs/review/pr-10773 (head bef026bdc5) and refs/review/main (now 805abe240c, one commit past the body's b5b6f37531); merge-base 25c7d584e4. Detached worktree at the head with an offline pnpm install --frozen-lockfile (workspace links resolve into the worktree, checked by readlink). Every vitest and tsc run under os-verify-lock (fix leg VERDICT command-exit 0, held 46 s; legs script command-exit 0, held 148 s; the two ;-chained runs echo each part's exit, all 0 except the probe written to red). Throwaway probes written into the worktree only and deleted; worktree removed; /home/user/objectui untouched (HEAD f905090a14, status clean). Blobs: panel base 42ebd9123f equals main, head bbeb098240, round 1 9681ab8e6a; hook base edbc46ff27 equals main, head d010cbf927 equals round 1; 10740 pin base 5b57a3b691 equals main, head 41dda5b81c.

① Derived judgments

R1–R6 base readings reproduced. Pure-base leg (base panel and hook blobs under the head pins): Tests 21 failed | 22 passed (43); red: supersededRun 7 (values, both loading rows, LANDS, FAILS, FAILED-swap, R2c), cannotQuery 6 (enabled, reset(), objectName, dataSource, filter-in-window, enabled-in-window) — the body's 13 — and dirtyReloadHold 8 (three dirty, three post-save, parent-move control, in-flight replay); the 10740 pin's nine rows green including the re-judged one. Round-1-head leg (panel 9681ab8e6a, the three head pins): Tests 9 failed | 24 passed (33), the nine the body names. Head fix leg, the body's 12 panel files plus the three fields pins plus my 18 probes: Test Files 17 passed (17), Tests 211 passed (211); 211 minus 18 minus (10 plus 8 plus 10) is the body's 165.

Interleavings tried at head (throwaway probes), all as the fix predicts: a save landing after a hold with the parent moved meanwhile (p2 read once with the new input, no third read, no replay); two quick input changes while dirty (one post-save read carrying both); a held flag across a parent swap (p2 read once, its commit replays nothing); an in-flight same-parent read, a hold, a parent move before its commit (stale p1 commit refused, no replay, four reads); unmount with a held flag and a read in flight (no throw, no read after); held change, failed save, landed save (one read carrying the change); refused filter while dirty (no read, notice drawn, Save stays ON over the hidden edited rows exactly as at base, and the later fix of the filter is held too); the in-flight read FAILS after a hold (no replay, edits kept, the flag survives, the post-save reload carries it); StrictMode mount (double read pre-existing, the last commits, hold and carry unchanged); relationshipField change while dirty is read; parentId to undefined and back is read. Hook: canQuery flapping with a read in flight (stale answer commits nothing), reset() mid-debounce then a new term (only the new term fires), unmount mid-debounce (nothing fires), StrictMode filter change in the window (exactly one more read), pageSize change in the window (one read, carries the term), refetch while disabled (nothing), total not reset (body observation confirmed).

R3 against the ruling. The three clauses are present verbatim (held.dirty, held.rowsHeldFor === parentId, last.parentId === parentId). The added adapter, childObject and relationshipField identity clauses only turn a would-be hold into a read, so they narrow the hold to exactly the inputs the ruling names; without them a childObject change while dirty would be held and Save would write the held po_line rows as object: schema.childObject, the objectui#10740 hazard by another route. Inside the ruling, and necessary. "The next read replays it" holds on the post-save path (pins), the parent-move path, the in-flight-commit path, and the in-flight-failure path (flag survives, next run carries it). No path leaves the panel clean with a held change never replayed: setDirty(false) is written only by a commit, every numbered run clears the flag and reads current inputs, and a commit with the flag set bumps heldReplay. Cost: the ruling's sentence is the cost; two instances of it worth knowing: a filter change produced by a resolved context token (objectui#10666) and the fix of a refused filter are filter changes, so both are held until save.

objectui#10740 row edit. Trigger moved from a limit: 200 re-render to a landed save whose reload fails, the ruling's own example. Its five expect assertions are byte-identical; nth(saves, 1) became nth(saves, 2), stated in the body. Flavour change worth naming: at the failed reload the lines are already persisted, so the "unsaved edits" are the still-dirty flag over saved rows; the row still pins that a failed same-parent re-read leaves rows drawn, editable and saveable, and it is the one row that reds when the post-save reload is made holdable (H8). The other eight rows are byte-identical against main (two hunks in the diff: header bullet and this row). All nine green at head and at both base legs.

Hooks and closures. No ref written during render; heldEditsRef mirrored from an effect declared ahead of load; lastRunRef written where a run is numbered; latestLoadRef from an effect keyed [load, parentId]. load's list is unchanged and covers everything it reads; save dropped load and reads latestLoadRef. Observation, not a regression: latestLoadRef is a passive-effect mirror, so a batch settling in the microtask between a non-discrete host re-render's commit and its passive flush reads the previous parentId; the p1 read it would issue is superseded by the fetch effect's p2 read in the same flush (no wrong commit), but the failure banner could be written in that window; base wrote both unconditionally; useLayoutEffect would close it. eslint --no-inline-config at head: panel 18 (14 no-explicit-any, 2 exhaustive-deps at the two useMemo sites, 2 set-state-in-effect at the child-schema and fetch effects), hook 8 (4, 2, 2), each equal to its base blob through --stdin; the three plugin-form pins 14, 12, 10 no-explicit-any; --print-config shows parserOptions: {} and no project or projectService.

Pins and ablations. No pin tautological: every non-control row of the three new pins is red on its base leg on the asserted find count or drawn lines. Ablations at head, one-anchor replacements, restore proven after every leg by blob equal to HEAD and empty git diff HEAD: A1 gives 2 red (values row, objectui#10682's superseded-success row), A6 gives 4, H1 gives 9, H2 gives 3 (both swap-back rows, in-flight replay), H6 gives 1, H7 gives 4, H8 gives 6 (re-judged 10740 row, save control, three post-save rows, R2c). All match the body's tables row for row.

Type-check. pnpm turbo run type-check for both packages exited 0, but @object-ui/plugin-form#type-check was a REPLAY from the shared worktree cache (its log names the dev's removed worktree path); fields executed fresh. Re-derived directly: tsc --noEmit and tsc -p tsconfig.test.json in both packages, four programs, each exit 0.

Finding, class (a), R6's defect by a sibling trigger, at head. The cleanup effect is keyed on runQuery, whose identity does not move on a sort or page change, so a setSearch timer armed before such a change stays live. Measured through renderHook at head: setSearch('a') then toggleSort('name') inside the window gives three find calls, the third with $search 'a' and no $orderby; answered last it wins: records ["UNSORTED"] while sort state reads name asc. setSearch('a') then setPage(2) (paginate): third call $skip 0; records ["PAGE1"] while page state reads 2. The release note's R6 names filter, object and data-source changes, which this PR closes; the fields changeset's closing sentence claims the general property, which is false as written. Smallest in-PR fix: clear the debounce wherever a query run starts (a clearDebounce() at the top of the fetch effect body, or the cleanup effect keyed on [runQuery, page, sort, clearDebounce]), plus two pin rows.

② Semver level

  • patch × 2, one per package, both in the one fixed group; at head check-changeset-presence reads 6 source files of 2 released packages with 2 changesets, check-changeset-fixed, check-changeset-no-major, check:changeset-claims (no pending changeset names a touched file) and check:new-line-citations (0 new) all exit 0. Clause-② no holds: neither index file is touched, useRecordQuery is package-internal (three widget callers), its two interfaces unchanged, load's origin parameter component-internal, no prop, registration or schema key moves.
  • Plugin-form changeset: every sentence true at head, including the R3 paragraph and the cost sentence.
  • Fields changeset: true through the reset paragraph; the debounce paragraph's last sentence "Now a pending debounced search is dropped when the query changes." is FALSE for a sort or page change (measured above). Replacement, if the residual is not fixed in-PR: "Now a pending debounced search is dropped when the filter, object, data source or page size changes; a sort or page change inside the window still lets the armed search run (objectui#10712 follow-up)."
  • Replacement body against head: six-row table true (each base reading reproduced); ablation tables true row for row (seven re-run); dependency-list statement true in full; 10740 description true; Acceptance notes true except "No class (a), (b) or (c) finding beyond the six rows" (the R6 sibling above). Counts: "LineItemsPanel.dirtyReloadHold-10712.test.tsx (15 rows, R3, round 2)" and "15 rows: for each of sort, limit and filter ... plus six controls" are wrong; the pin has 14 rows (three inputs × three rows plus five controls), as the body's own 33 (14 + 9 + 10) and my 43 (10 + 10 + 14 + 9) confirm. Replacement: "(14 rows, R3, round 2)" and "14 rows: ... plus five controls:". Minor: the round-1 "Tests 178 passed (178)" was taken with the 9-row hook pin before its reset()-in-window control (the live body says so; the replacement dropped that clause; at head those 14 files count 179), so add "(with the 9-row hook pin, before its reset()-in-window control)". Serial is written against main b5b6f37531; main is now 805abe240c (app-shell only).

③ Boundary flags

  • CI at head: 43 check runs, all completed, 40 success, 3 skipped (matrix placeholders Test (coverage shard ${{ matrix.shard }}/4), Test (coverage), dependabot), 0 failure; Vercel commit status success. Lint, Type Check, eight test shards, dist pins, Build & E2E, Line Citation Gate, the five Changeset checks and Governed Surface Queue Guard all green.
  • git merge-tree --write-tree against current main 805abe240c: exit 0, clean tree e372567f14; main's four commits past the base touch none of the 8 files.
  • Open PRs: 10; none touches any of the 8 files (chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired #8941 touches both packages' package.json only; chore: release packages #5400 is the release PR).
  • Governed predicate on objectstack-ai/objectui#10773: 0 of 8 paths governed, 1331 changed lines under the threshold, exit 0.
  • Draft: yes; assignee os-elon-musk; author objectstack-fleet[bot]; mergeable_state: clean; body first line Fixes #10712, third Clause-②: no.

Implemented-by: claude/issue-10712-superseded-read-guard-2
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: FAIL

What stops the landing:

  1. Published text over-claims: the fields changeset's "Now a pending debounced search is dropped when the query changes." is false at head for a sort or page change inside the window (the armed timer runs the older sort or page as the latest call and its answer is shown). Either take the two-line fix in-PR (clear the debounce where any query run starts, keyed on page and sort as well as runQuery, with two pin rows), after which the sentence stands, or replace it with the narrowed sentence in ② and file the residual as a class (a) finding; the body's "No class (a), (b) or (c) finding beyond the six rows" must then name it.
  2. Body counts: "15 rows" (twice) and "six controls" for dirtyReloadHold-10712; replace with "14 rows" and "five controls".
    Everything else in ①–③ passes; with those changes the PR lands.

…ort or page change too (objectui#10712)

The debounce cleanup effect was keyed on `runQuery`, whose identity does not
move on a `sort` or `page` change, so a `setSearch` timer armed before such a
change stayed live and ran last with the older sort or page: the records shown
were unsorted while `sort` read `name asc`, or page 1 while `page` read 2.
The cleanup is now keyed on what the fetch effect is keyed on: `runQuery`,
`page` and `sort`. Two pin rows (sort-in-window, page-in-window), each red at
the previous head on its `find` count.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…ectui#10712)

The pin's header described the debounce defect through a filter, object or
data-source change only, and the fix as keyed on `runQuery`. It now names the
sort and page triggers the two round-3 rows pin, and the fix as keyed on what
the fetch effect is keyed on. Comment only.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3068.4 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-BbglXf_7.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 554.94KB 132.67KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.29KB 65.66KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ad6629e7043b9e4891856742b17a469795933072

A narrow round-3 review, run in-seat by the dispatching seat, of the delta bef026bdc5..ad6629e704 (two commits) against the round-2 FAIL record 5856013809. Every other judgment in that record stands. Its full reading of R1–R6, R3's hold against the ruling, the objectui#10740 row edit, the hook rules and the pins is unchanged, because LineItemsPanel.tsx and the plugin-form pins are byte-identical since bef026bdc5.

① Derived judgments

  • The R6 sibling is fixed, and the fix is the review's second option. The hook's debounce cleanup effect was keyed [runQuery, clearDebounce]; it is now keyed [runQuery, page, sort, clearDebounce], which is the fetch effect's own key ([canQuery, page, sort, runQuery]) minus canQuery. The reset effect covers canQuery.
    • A sort or page change inside the window therefore runs this cleanup in the same commit, ahead of the fetch effect's body, so the armed timer is gone before the new query is numbered.
    • Unmount is still covered.
    • A search change alone does not move the key, so the typed term still reaches the wire only through the timer.
    • setSearch moving page to 1 clears its own just-armed timer, and the fetch effect then carries the term at once. There is one read, not two. The dev's StrictMode probe reads that case identically at both heads.
  • The two new pin rows assert the property, not the mechanism.
    • Sort-in-window: exactly two find calls, the second carrying $orderby name asc and $search a; the committed records are the sorted answer, with sort reading name asc.
    • Page-in-window (paginate): the second call carries $skip 10 and $search a; exactly two calls; page reads 2.
    • The quoted base leg on bef026bdc5 reddens exactly these two rows, on their find count (3 against 2). Ablation A11 (the key back to [runQuery, clearDebounce]) reddens exactly these two, with blob-proven restore.
  • The fields changeset's sentence "Now a pending debounced search is dropped when the query changes." is true at head for every input runQuery reads, plus page and sort. It is pinned for filter, sort and page.
  • Body counts are corrected in the replacement:
    • dirtyReloadHold-10712 has 14 rows, as three triggers times three rows plus five controls;
    • the round-1 "178" line carries its qualifier;
    • Serial is against 17b323e5ca;
    • Acceptance notes name the sibling as the one class-(a) finding beyond the six rows, found in review and fixed here, plus the latestLoadRef observation.
  • The widening is in scope. The dev ran the whole packages/fields/ suite (213 files, 3439 tests, green) beyond the named pin sets, because the hook's three callers live there. That is in scope for the role file. It is noted against the maintainer's spend flag.

② Semver level

patch times 2, no new changeset, Clause-②: no. The round-3 change is a dependency-list edit inside a package-internal hook; no export, prop or type moves.

③ Boundary flags

  • CI on ad6629e704: 43 check runs; 40 success, 3 skipped (Test (coverage), the coverage-shard matrix entry, dependabot), 0 failure, 0 pending.
  • git merge-tree --write-tree against main 17b323e5ca is clean (tree ca89b681fe, the dev's reading). Main's five commits past base touch none of the 8 files.
  • Governed: NOT GOVERNED.
  • Draft; assignee os-elon-musk.
  • The round-3 dev served at the default judgment tier after the fable-quota takeover; see the report's takeover field.

Implemented-by: claude/issue-10712-superseded-read-guard-2
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 13:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit bf14b64 Sep 27, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10712-superseded-read-guard-2 branch September 27, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment