feat(drift-trio): phase C — scenarios S2–S8 + re-resolve-on-fiber hooks

drift-trio-scenarios.spec.ts: disjoint ping-pong, same-item interleave (no
settle between bursts), conflict pairs (move-vs-delete / value-vs-value /
move-vs-move — winner is CRDT policy, asserted only as convergence + drift
silence), undo storm, 12-edit burst churn, late-joiner adopt, and Ctrl+S mid
peer burst; per-tool adapters, marker-waits before every sweep (finding #7).
S4 exposed finding #9: mutation hooks resolved item pointers at call time and
committed later on the fiber — a remote remove in between frees the pointer
(doApplyItems) and the commit resurrects the deleted item. Phase-B mutation
hooks now re-resolve by uuid ON the fiber; a vanished item makes the mutation
lose silently. applyDeltaToY's concurrent update/delete verified coherent
(full resurrect or full remove) — no shared change needed. trio.ts: TabSet
oracles + exported startV2 for duo/late-join composition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
This commit is contained in:
Gergő Törcsvári 2026-07-21 11:02:41 +02:00
commit 23b0f43e73
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
4 changed files with 456 additions and 31 deletions

View file

@ -1518,15 +1518,23 @@ bool schCollabTestMoveSchItem( std::string aId, int aDx, int aDy )
if( !fr )
return false;
SCH_SHEET_PATH path;
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
&path, /*allowNull*/ true );
if( !item )
SCH_SHEET_PATH probe;
if( !fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ), &probe,
/*allowNull*/ true ) )
return false;
SCH_SCREEN* screen = path.LastScreen();
fr->CallAfter( [fr, item, screen, aDx, aDy]() { collabTestMove( fr, item, screen, aDx, aDy ); } );
// Re-resolve when the deferred body runs: a remote remove can apply in
// between and the captured pointer would be dangling — the commit would
// resurrect a deleted item (drift-trio S4 move-vs-delete). Vanished =>
// the move loses, silently.
fr->CallAfter( [fr, aId, aDx, aDy]() {
SCH_SHEET_PATH path;
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
&path, /*allowNull*/ true );
if( item )
collabTestMove( fr, item, path.LastScreen(), aDx, aDy );
} );
return true;
}
@ -1544,16 +1552,21 @@ bool schCollabTestMirrorSchItem( std::string aId, bool aHorizontal )
if( !item )
return false;
SCH_SCREEN* screen = path.LastScreen();
pcbjam_collab::runOnFiber( fr, [fr, aId, aHorizontal]() { // re-resolve on the fiber (S4)
SCH_SHEET_PATH path;
SCH_ITEM* live = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
&path, /*allowNull*/ true );
if( !live )
return;
pcbjam_collab::runOnFiber( fr, [fr, item, screen, aHorizontal]() {
SCH_COMMIT commit( fr );
commit.Modify( item, screen );
commit.Modify( live, path.LastScreen() );
if( aHorizontal )
item->MirrorHorizontally( item->GetPosition().x );
live->MirrorHorizontally( live->GetPosition().x );
else
item->MirrorVertically( item->GetPosition().y );
live->MirrorVertically( live->GetPosition().y );
commit.Push( wxT( "Collab test mirror" ) );
} );

View file

@ -2111,12 +2111,16 @@ std::string pcbCollabTestAddZone( int aX1, int aY1, int aX2, int aY2, std::strin
bool pcbCollabTestFlipBoardItem( std::string aId )
{
PCB_EDIT_FRAME* fr = pcbFrame();
BOARD_ITEM* item = testResolve( fr, aId );
if( !item )
if( !testResolve( fr, aId ) )
return false;
pcbjam_collab::runOnFiber( fr, [fr, item]() {
pcbjam_collab::runOnFiber( fr, [fr, aId]() { // re-resolve on the fiber (S4)
BOARD_ITEM* item = testResolve( fr, aId );
if( !item )
return;
BOARD_COMMIT commit( fr );
commit.Modify( item );
item->Flip( item->GetPosition(), FLIP_DIRECTION::LEFT_RIGHT );
@ -2135,14 +2139,19 @@ bool pcbCollabTestSetFootprintField( std::string aId, std::string aField, std::s
if( !item || item->Type() != PCB_FOOTPRINT_T )
return false;
FOOTPRINT* fp = static_cast<FOOTPRINT*>( item );
wxString text = wxString::FromUTF8( aText.c_str() );
bool isRef = ( aField == "Reference" );
wxString text = wxString::FromUTF8( aText.c_str() );
bool isRef = ( aField == "Reference" );
if( !isRef && aField != "Value" )
return false;
pcbjam_collab::runOnFiber( fr, [fr, fp, text, isRef]() {
pcbjam_collab::runOnFiber( fr, [fr, aId, text, isRef]() { // re-resolve on the fiber (S4)
BOARD_ITEM* live = testResolve( fr, aId );
if( !live || live->Type() != PCB_FOOTPRINT_T )
return;
FOOTPRINT* fp = static_cast<FOOTPRINT*>( live );
BOARD_COMMIT commit( fr );
commit.Modify( fp );
@ -2160,12 +2169,16 @@ bool pcbCollabTestSetFootprintField( std::string aId, std::string aField, std::s
bool pcbCollabTestSetBoardItemLocked( std::string aId, bool aLocked )
{
PCB_EDIT_FRAME* fr = pcbFrame();
BOARD_ITEM* item = testResolve( fr, aId );
if( !item )
if( !testResolve( fr, aId ) )
return false;
pcbjam_collab::runOnFiber( fr, [fr, item, aLocked]() {
pcbjam_collab::runOnFiber( fr, [fr, aId, aLocked]() { // re-resolve on the fiber (S4)
BOARD_ITEM* item = testResolve( fr, aId );
if( !item )
return;
BOARD_COMMIT commit( fr );
commit.Modify( item );
item->SetLocked( aLocked );
@ -2179,12 +2192,18 @@ bool pcbCollabTestSetBoardItemLocked( std::string aId, bool aLocked )
bool pcbCollabTestMoveBoardItem( std::string aId, int aDx, int aDy )
{
PCB_EDIT_FRAME* fr = pcbFrame();
BOARD_ITEM* item = testResolve( fr, aId );
if( !item )
if( !testResolve( fr, aId ) )
return false;
pcbjam_collab::runOnFiber( fr, [fr, item, aDx, aDy]() { collabTestMove( fr, item, aDx, aDy ); } );
// Re-resolve ON the fiber: a remote remove can apply between scheduling
// and running, and doApplyItems FREES removed items — a captured pointer
// would be dangling and the commit would resurrect a deleted item
// (drift-trio S4 move-vs-delete). Vanished => the move loses, silently.
pcbjam_collab::runOnFiber( fr, [fr, aId, aDx, aDy]() {
if( BOARD_ITEM* item = testResolve( fr, aId ) )
collabTestMove( fr, item, aDx, aDy );
} );
return true;
}