Files
pad/docs
xarmian d509d75308 fix(items): a move uses the item as it stands under the lock (BUG-3665) (#2165)
* fix(store): a move records its origin from the item as it stands under the lock (BUG-3665)

On the no-precheck path (a forced move, or a direct MoveItem call),
`existing` is the pre-lock pool read; preMove is the item re-read under the
workspace lock. The event decisions already used preMove, but the durable
item_collection_moves row still took its from_collection_id, and its
"crossed collections" test, from `existing`. A move another writer landed in
between therefore recorded A->C instead of B->C, and a client that cached the
item in B (and can see neither A nor C) never got the moved-out tombstone.
Both now use preMove.

TestBUG3665_MoveRecordsTheOriginUnderTheLock moves the item A->B through a
test-only seam (beforeMoveLock) between the forced move's pre-lock read and
its lock: records [A->B B->C]; on main [A->B A->C].

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(items): a move refuses an item changed since its payload was built (BUG-3665, codex r1 Medium)

The move handler migrates the item's fields from a read taken before the
move's lock and writes them under it, so a write landing in between was
overwritten, and the move's history and events described the older body.
Every move, a forced one included (force skips only the open-children
guard), now carries a precheck that compares the locked snapshot's
collection and fields with what the payload was built from, and answers
409 conflict when they differ. docs/api.md records the new 409.

The beforeMoveLock seam gains SetBeforeMoveLockForTesting.
TestBUG3665_MoveRefusesAnItemChangedSinceItsPayloadWasBuilt edits the item
between the handler's read and the lock on both the forced and unforced
routes: 409 and the edit kept; red (200, edit lost) with the check removed.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(items): a move re-reads every input of its payload under the lock (BUG-3665, codex r2 Medium)

r2 found the source collection's schema also shapes the payload and was not
re-checked. Rather than chase members one round at a time, the precheck now
covers the population of the payload's inputs, listed in the comment: the
item's collection and fields, the source and target collections' schema and
settings (the migration and the close-state check read them), and the
target's unique-field holders (re-queried on the lock's transaction). Any
change answers 409 conflict; the unique case keeps its own message.

The test is a table: one case per input, each run on the forced and
unforced routes, plus an unchanged-move control. Disabling each re-check
fails exactly its own two legs.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-14 11:08:56 -04:00
..