amalgm 0.1.220 → 0.1.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * The reducer reads the single rail-ordered wall and derives exactly:
7
7
  * - one winning card (highest official card version), and
8
- * - one checkpoint tree (checkpoint transitions folded in official order).
8
+ * - one checkpoint tree (checkpoint transitions applied in official order).
9
9
  *
10
10
  * The materializer has no peer branches. It obtains the winning commit,
11
11
  * moves to that exact card when safe, advances the one held checkpoint tree
@@ -30,6 +30,7 @@ const {
30
30
  const {
31
31
  CHECKPOINT_CONTRACT,
32
32
  checkpointValueReady,
33
+ readCheckpoint,
33
34
  readCheckpointValue,
34
35
  repoKeyOfCard,
35
36
  revisionOf,
@@ -80,20 +81,40 @@ function readRepoState(repoKey) {
80
81
  }
81
82
  }
82
83
 
83
- function writeRepoState(repoKey, state) {
84
+ function repoStateIdentity(state) {
85
+ if (!state) return null;
86
+ const { indexDiff, worktreeDiff, updatedAt, ...identity } = state;
87
+ return JSON.stringify(identity);
88
+ }
89
+
90
+ function persistRepoState(repoKey, state) {
84
91
  fs.mkdirSync(STATE_DIR, { recursive: true });
85
92
  const next = { repoKey, ...state, updatedAt: new Date().toISOString() };
93
+ const previous = readRepoState(repoKey);
94
+ if (repoStateIdentity(previous) === repoStateIdentity(next)) {
95
+ return { state: previous, changed: false };
96
+ }
86
97
  fs.writeFileSync(stateFile(repoKey), `${JSON.stringify(next, null, 2)}\n`);
87
- return next;
98
+ return { state: next, changed: true };
99
+ }
100
+
101
+ function repoStateEventValue(state) {
102
+ const { indexDiff, worktreeDiff, ...value } = state;
103
+ return value;
88
104
  }
89
105
 
90
106
  function validVersion(value) {
91
107
  return Number.isInteger(value) && value > 0;
92
108
  }
93
109
 
110
+ function preferBundleObjects(held, candidate) {
111
+ if (held?.objects?.kind === 'bundle' || candidate?.objects?.kind !== 'bundle') return held;
112
+ return { ...held, objects: candidate.objects };
113
+ }
114
+
94
115
  /**
95
116
  * Pure wall reduction. Machine identity never chooses a winner. Checkpoint
96
- * transitions remain in the wall until the next card folds them into a
117
+ * transitions remain in the wall until the next card includes them in a
97
118
  * commit, so reducing current state never forgets an earlier edit.
98
119
  */
99
120
  function reduceWall(entries) {
@@ -121,11 +142,25 @@ function reduceWall(entries) {
121
142
  }
122
143
  if (!repo.card || card.positionVersion > repo.card.positionVersion) {
123
144
  repo.card = { ...card, writer: wallEntry.machine };
145
+ } else if (
146
+ card.positionVersion === repo.card.positionVersion
147
+ && card.positionMutationId === repo.card.positionMutationId
148
+ && card.head?.oid === repo.card.head?.oid
149
+ ) {
150
+ repo.card = preferBundleObjects(repo.card, card);
124
151
  }
125
152
  for (const ref of card.branches || []) {
126
153
  if (!ref?.name || !validVersion(ref.version)) continue;
127
154
  const held = repo.branchMap.get(ref.name);
128
- if (!held || ref.version > held.version) repo.branchMap.set(ref.name, ref);
155
+ if (!held || ref.version > held.version) {
156
+ repo.branchMap.set(ref.name, ref);
157
+ } else if (
158
+ ref.version === held.version
159
+ && ref.mutationId === held.mutationId
160
+ && ref.oid === held.oid
161
+ ) {
162
+ repo.branchMap.set(ref.name, preferBundleObjects(held, ref));
163
+ }
129
164
  }
130
165
  for (const checkpoint of card.checkpoints || []) {
131
166
  if (
@@ -293,9 +328,7 @@ async function moveToWinningCard(localCard, targetCard) {
293
328
  && targetCard.head?.oid
294
329
  && current.head.branch === targetCard.head?.branch
295
330
  && await isAncestor(localCard.path, targetCard.head.oid, current.head.oid);
296
- if (pendingCard) {
297
- return { state: 'ready', moved: false, pendingCard: true, conflicts: [] };
298
- }
331
+ if (pendingCard) return { state: 'ready', moved: false, pendingCard: true };
299
332
 
300
333
  const objects = await obtainCardObjects(localCard, targetCard);
301
334
  if (objects.state !== 'ready') return { state: 'waiting', reason: objects.state };
@@ -340,25 +373,14 @@ async function moveToWinningCard(localCard, targetCard) {
340
373
  });
341
374
  }
342
375
 
343
- const commonTree = await merge.treeOf(localCard.path, commonOid);
344
376
  const winningTree = await merge.treeOf(localCard.path, targetCard.head.oid);
345
377
  const oldTree = await merge.treeOf(localCard.path, current.head.oid);
346
- const committed = forward || current.head.oid === targetCard.head.oid
347
- ? { tree: winningTree, conflicts: [] }
348
- : await merge.reduceTree(
349
- localCard.path,
350
- winningTree,
351
- commonTree,
352
- oldTree,
353
- { label: `parked-${current.head.oid.slice(0, 12)}` },
354
- );
355
378
  const oldIndexTree = await merge.treeOfPatch(localCard.path, current.head.oid, movingIndexDiff);
356
379
  const staged = await merge.reduceTree(
357
380
  localCard.path,
358
- committed.tree,
381
+ winningTree,
359
382
  oldTree,
360
383
  oldIndexTree,
361
- { label: `staged-${current.head.oid.slice(0, 12)}` },
362
384
  );
363
385
  const oldWorktree = await merge.treeOfPatch(localCard.path, oldIndexTree, movingWorktreeDiff);
364
386
  const working = await merge.reduceTree(
@@ -366,13 +388,7 @@ async function moveToWinningCard(localCard, targetCard) {
366
388
  staged.tree,
367
389
  oldIndexTree,
368
390
  oldWorktree,
369
- { label: `working-${current.head.oid.slice(0, 12)}` },
370
391
  );
371
- const conflicts = [
372
- ...committed.conflicts,
373
- ...staged.conflicts,
374
- ...working.conflicts,
375
- ];
376
392
 
377
393
  const rollbackDir = path.join(STATE_DIR, 'card-moves');
378
394
  fs.mkdirSync(rollbackDir, { recursive: true });
@@ -398,7 +414,6 @@ async function moveToWinningCard(localCard, targetCard) {
398
414
  state: 'ready',
399
415
  moved: true,
400
416
  parked,
401
- conflicts: [...conflicts, ...applied.conflicts],
402
417
  };
403
418
  }
404
419
 
@@ -427,9 +442,11 @@ async function materializeParked(localCard, entries) {
427
442
  return waiting;
428
443
  }
429
444
 
430
- async function materializeBranches(localCard, plan) {
445
+ async function materializeBranches(localCard, plan, heldBranches = []) {
431
446
  const current = new Map((await localBranches(localCard.path)).map((ref) => [ref.name, ref.oid]));
447
+ const held = new Map(heldBranches.map((ref) => [ref.name, ref.oid]));
432
448
  const waiting = [];
449
+ const pending = [];
433
450
  let moved = false;
434
451
  for (const ref of plan.branches || []) {
435
452
  if (ref.name === plan.card.head?.branch) {
@@ -438,6 +455,13 @@ async function materializeBranches(localCard, plan) {
438
455
  }
439
456
  continue;
440
457
  }
458
+ const currentOid = current.get(ref.name) || null;
459
+ const heldOid = held.get(ref.name) || null;
460
+ if (currentOid !== heldOid && currentOid !== ref.oid) {
461
+ pending.push({ name: ref.name, oid: currentOid });
462
+ continue;
463
+ }
464
+ if (currentOid === ref.oid) continue;
441
465
  if (ref.oid === null) {
442
466
  if (current.has(ref.name)) {
443
467
  await transition.deleteBranchRef(localCard.path, ref.name);
@@ -468,7 +492,7 @@ async function materializeBranches(localCard, plan) {
468
492
  moved = true;
469
493
  }
470
494
  }
471
- return { moved, waiting };
495
+ return { moved, waiting, pending };
472
496
  }
473
497
 
474
498
  async function reduceCheckpoint(localCard, plan) {
@@ -476,7 +500,6 @@ async function reduceCheckpoint(localCard, plan) {
476
500
  const baseTree = await merge.treeOf(localCard.path, baseOid);
477
501
  let indexTree = baseTree;
478
502
  let worktreeTree = baseTree;
479
- const conflicts = [];
480
503
  if (plan.checkpointBaseConflict) {
481
504
  return { state: 'waiting', reason: 'checkpoint-base-disagrees' };
482
505
  }
@@ -493,32 +516,6 @@ async function reduceCheckpoint(localCard, plan) {
493
516
  );
494
517
  }
495
518
 
496
- for (const entry of plan.parked || []) {
497
- const commonOid = await mergeBase(localCard.path, baseOid, entry.oid);
498
- if (!commonOid) return { state: 'waiting', reason: `parked-unrelated-${entry.oid}` };
499
- const commonTree = await merge.treeOf(localCard.path, commonOid);
500
- const parkedTree = await merge.treeOf(localCard.path, entry.oid);
501
- const label = `parked-${entry.version || entry.oid.slice(0, 12)}`;
502
- const reducedIndex = await merge.reduceTree(
503
- localCard.path,
504
- indexTree,
505
- commonTree,
506
- parkedTree,
507
- { label },
508
- );
509
- indexTree = reducedIndex.tree;
510
- conflicts.push(...reducedIndex.conflicts.map((item) => ({ ...item, layer: 'index' })));
511
- const reducedWorktree = await merge.reduceTree(
512
- localCard.path,
513
- worktreeTree,
514
- commonTree,
515
- parkedTree,
516
- { label },
517
- );
518
- worktreeTree = reducedWorktree.tree;
519
- conflicts.push(...reducedWorktree.conflicts.map((item) => ({ ...item, layer: 'worktree' })));
520
- }
521
-
522
519
  for (const operation of plan.transitions) {
523
520
  const from = readCheckpointValue(baseOid, operation.from);
524
521
  const to = readCheckpointValue(baseOid, operation.to);
@@ -528,11 +525,13 @@ async function reduceCheckpoint(localCard, plan) {
528
525
  }
529
526
  const fromIndexTree = await merge.treeOfPatch(localCard.path, baseOid, from.indexDiff);
530
527
  const toIndexTree = await merge.treeOfPatch(localCard.path, baseOid, to.indexDiff);
531
- const reducedIndex = await merge.reduceTree(localCard.path, indexTree, fromIndexTree, toIndexTree, {
532
- label: String(operation.version),
533
- });
528
+ const reducedIndex = await merge.reduceTree(
529
+ localCard.path,
530
+ indexTree,
531
+ fromIndexTree,
532
+ toIndexTree,
533
+ );
534
534
  indexTree = reducedIndex.tree;
535
- conflicts.push(...reducedIndex.conflicts.map((item) => ({ ...item, layer: 'index' })));
536
535
 
537
536
  const fromWorktreeTree = await merge.treeOfPatch(localCard.path, fromIndexTree, from.worktreeDiff);
538
537
  const toWorktreeTree = await merge.treeOfPatch(localCard.path, toIndexTree, to.worktreeDiff);
@@ -541,10 +540,8 @@ async function reduceCheckpoint(localCard, plan) {
541
540
  worktreeTree,
542
541
  fromWorktreeTree,
543
542
  toWorktreeTree,
544
- { label: String(operation.version) },
545
543
  );
546
544
  worktreeTree = reducedWorktree.tree;
547
- conflicts.push(...reducedWorktree.conflicts.map((item) => ({ ...item, layer: 'worktree' })));
548
545
  }
549
546
 
550
547
  const indexDiff = await merge.diffOfTree(localCard.path, baseOid, indexTree);
@@ -556,26 +553,32 @@ async function reduceCheckpoint(localCard, plan) {
556
553
  indexDiff,
557
554
  worktreeDiff,
558
555
  checkpointRevision: revisionOf(baseOid, indexDiff, worktreeDiff),
559
- conflicts,
560
556
  };
561
557
  }
562
558
 
563
- function captureIsOnRail(plan, captured, localMachine = machineId()) {
559
+ function captureIsOnRail(plan, captured, localMachine = machineId(), acknowledged = null) {
564
560
  if (
565
561
  !captured
566
562
  || captured.indexDiff === null
567
563
  || captured.baseOid !== plan.card.head.oid
568
564
  ) return false;
565
+ const revision = revisionOf(
566
+ captured.baseOid,
567
+ captured.indexDiff,
568
+ captured.worktreeDiff,
569
+ );
569
570
  const latest = [...plan.transitions]
570
571
  .reverse()
571
572
  .find((operation) => operation.machine === localMachine);
572
573
  return Boolean(
573
- latest
574
- && latest.to?.revision === revisionOf(
575
- captured.baseOid,
576
- captured.indexDiff,
577
- captured.worktreeDiff,
578
- ),
574
+ (latest && latest.to?.revision === revision)
575
+ || (
576
+ Number.isInteger(acknowledged?.version)
577
+ && acknowledged.version > 0
578
+ && acknowledged.baseOid === captured.baseOid
579
+ && acknowledged.branch === plan.card.head.branch
580
+ && acknowledged.to?.revision === revision
581
+ )
579
582
  );
580
583
  }
581
584
 
@@ -597,9 +600,31 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
597
600
  const planParked = new Set((plan.parked || []).map((entry) => entry.oid));
598
601
  const requiredParkedWaiting = parkedWaiting.filter((entry) => planParked.has(entry.oid));
599
602
  if (requiredParkedWaiting.length > 0) {
603
+ // Parking is a local write. Remember it before waiting on an inbound
604
+ // bundle, so the next pass cannot replace unnumbered local work.
605
+ const persisted = persistRepoState(plan.repoKey, {
606
+ ...(previous || {}),
607
+ path: localCard.path,
608
+ replica: localCard.replica,
609
+ parked,
610
+ parkedWaiting,
611
+ materialized: false,
612
+ });
613
+ if (persisted.changed) {
614
+ appendStateEvent({
615
+ resource: 'workspace-reducer',
616
+ op: previous ? 'update' : 'insert',
617
+ id: localCard.replica,
618
+ value: repoStateEventValue(persisted.state),
619
+ source: 'workspace-reducer',
620
+ });
621
+ }
600
622
  return { state: 'waiting', reason: `parked-${requiredParkedWaiting[0].state}` };
601
623
  }
602
- const branchResult = await materializeBranches(localCard, plan);
624
+ const branchResult = await materializeBranches(localCard, plan, previous?.branches);
625
+ if (branchResult.waiting.length > 0) {
626
+ return { state: 'waiting', reason: `branch-${branchResult.waiting[0].state}` };
627
+ }
603
628
  const reduced = await reduceCheckpoint(localCard, plan);
604
629
  if (reduced.state !== 'ready') return reduced;
605
630
 
@@ -628,23 +653,19 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
628
653
  let heldIndex;
629
654
  let targetWorktree;
630
655
  let targetIndex;
631
- const pendingConflicts = [];
632
-
633
656
  if (cardResult.pendingCard) {
634
657
  const pendingBaseTree = await merge.treeOf(localCard.path, localCard.head.oid);
635
658
  const rebasedIndex = await merge.reduceTree(
636
659
  localCard.path,
637
- pendingBaseTree,
638
- previousHeldIndex,
639
660
  reduced.indexTree,
640
- { label: `pending-card-index-${appliedThrough ?? 'local'}` },
661
+ previousHeldIndex,
662
+ pendingBaseTree,
641
663
  );
642
664
  const rebasedWorktree = await merge.reduceTree(
643
665
  localCard.path,
644
- pendingBaseTree,
645
- previousHeldWorktree,
646
666
  reduced.worktreeTree,
647
- { label: `pending-card-worktree-${appliedThrough ?? 'local'}` },
667
+ previousHeldWorktree,
668
+ pendingBaseTree,
648
669
  );
649
670
  heldIndex = pendingBaseTree;
650
671
  heldWorktree = pendingBaseTree;
@@ -656,13 +677,20 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
656
677
  stateCardMutationId = null;
657
678
  stateIndexDiff = await merge.diffOfTree(localCard.path, stateBaseOid, targetIndex);
658
679
  stateWorktreeDiff = await merge.diffOfTree(localCard.path, targetIndex, targetWorktree);
659
- pendingConflicts.push(
660
- ...rebasedIndex.conflicts.map((item) => ({ ...item, layer: 'pending-card-index' })),
661
- ...rebasedWorktree.conflicts.map((item) => ({ ...item, layer: 'pending-card-worktree' })),
662
- );
663
680
  } else {
664
681
  const captured = await captureCheckpoint(localCard.path);
665
- const represented = captureIsOnRail(plan, captured);
682
+ const wall = require('./wall');
683
+ const localCheckpoint = readCheckpoint(machineId(), plan.repoKey);
684
+ const project = wall.projectForRepo(plan.repoKey);
685
+ const acknowledged = localCheckpoint && {
686
+ ...localCheckpoint,
687
+ version: wall.mutationVersion(
688
+ project?.resourceId,
689
+ localCheckpoint.mutationId,
690
+ plan.repoKey,
691
+ ),
692
+ };
693
+ const represented = captureIsOnRail(plan, captured, machineId(), acknowledged);
666
694
  const capturedIndex = represented
667
695
  ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
668
696
  : null;
@@ -678,17 +706,16 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
678
706
  const currentIndex = await merge.indexTree(localCard.path);
679
707
  const reducedLocalIndex = await merge.reduceTree(
680
708
  localCard.path,
681
- currentIndex,
682
- heldIndex,
683
709
  targetIndex,
684
- { label: `index-${reduced.checkpointRevision || 'clean'}` },
710
+ heldIndex,
711
+ currentIndex,
685
712
  );
686
713
  const applied = await merge.advance(localCard.path, heldWorktree, targetWorktree, {
687
714
  label: `checkpoint-${reduced.checkpointRevision || 'clean'}`,
688
715
  });
689
716
  const indexResult = await advanceIndex(localCard.path, currentIndex, reducedLocalIndex.tree);
690
717
  if (indexResult.state !== 'ready') return indexResult;
691
- const state = writeRepoState(plan.repoKey, {
718
+ const persisted = persistRepoState(plan.repoKey, {
692
719
  path: localCard.path,
693
720
  replica: localCard.replica,
694
721
  branch: stateBranch,
@@ -702,30 +729,26 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
702
729
  worktreeDiff: stateWorktreeDiff,
703
730
  checkpointVersions: plan.transitions.map((entry) => entry.version),
704
731
  branches: plan.branches || [],
732
+ pendingRefs: branchResult.pending,
705
733
  heldTree: applied.heldTree,
706
734
  heldIndexTree: targetIndex,
707
735
  parked,
708
736
  parkedWaiting: [...parkedWaiting, ...branchResult.waiting],
709
- conflicts: [
710
- ...(cardResult.conflicts || []),
711
- ...reduced.conflicts,
712
- ...pendingConflicts,
713
- ...reducedLocalIndex.conflicts.map((item) => ({ ...item, layer: 'index-materialize' })),
714
- ...applied.conflicts,
715
- ],
716
737
  materialized: applied.heldTree === targetWorktree
717
738
  && reducedLocalIndex.tree === targetIndex,
718
739
  });
719
- appendStateEvent({
720
- resource: 'workspace-reducer',
721
- op: previous ? 'update' : 'insert',
722
- id: localCard.replica,
723
- value: state,
724
- source: 'workspace-reducer',
725
- });
740
+ if (persisted.changed) {
741
+ appendStateEvent({
742
+ resource: 'workspace-reducer',
743
+ op: previous ? 'update' : 'insert',
744
+ id: localCard.replica,
745
+ value: repoStateEventValue(persisted.state),
746
+ source: 'workspace-reducer',
747
+ });
748
+ }
726
749
  return {
727
- state: state.conflicts.length ? 'conflict' : 'in-sync',
728
- reason: state.conflicts.length ? state.conflicts.map((item) => `${item.kind}:${item.path}`).join(', ') : null,
750
+ state: 'in-sync',
751
+ reason: null,
729
752
  moved: cardResult.moved || branchResult.moved || applied.applied.length > 0,
730
753
  };
731
754
  }
@@ -337,13 +337,13 @@ function setIndexTree(worktreeRoot, treeOid) {
337
337
  }
338
338
 
339
339
  /**
340
- * Land a merged write set in a working tree — the coexistence engine's
341
- * only mutation path. Each entry is { path, content, mode } (content a
340
+ * Apply one ordered file write set to a working tree. Each entry is
341
+ * { path, content, mode } (content a
342
342
  * Buffer, mode '100644' | '100755' | '120000') or { path, content: null }
343
343
  * for a deletion. Paths are confined to the worktree; the whole set lands
344
344
  * as one journal line.
345
345
  */
346
- function applyMergedFiles(worktreeRoot, writes, label) {
346
+ function applyFiles(worktreeRoot, writes, label) {
347
347
  const root = path.resolve(worktreeRoot);
348
348
  const written = [];
349
349
  const removed = [];
@@ -368,20 +368,20 @@ function applyMergedFiles(worktreeRoot, writes, label) {
368
368
  }
369
369
  written.push(write.path);
370
370
  }
371
- journal({ action: 'merge-files', worktree: root, label, written, removed, ok: true });
371
+ journal({ action: 'apply-files', worktree: root, label, written, removed, ok: true });
372
372
  } catch (error) {
373
373
  journal({
374
- action: 'merge-files', worktree: root, label, written, removed, ok: false,
374
+ action: 'apply-files', worktree: root, label, written, removed, ok: false,
375
375
  error: String(error.message).slice(0, 500),
376
376
  });
377
- throw new TransitionError('merge-files', root, String(error.message));
377
+ throw new TransitionError('apply-files', root, String(error.message));
378
378
  }
379
379
  }
380
380
 
381
381
  module.exports = {
382
382
  TransitionError,
383
383
  applyIndexPatch,
384
- applyMergedFiles,
384
+ applyFiles,
385
385
  applyPatch,
386
386
  checkoutInitialCard,
387
387
  createBundle,