amalgm 0.1.221 → 0.1.235

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
 
@@ -446,6 +461,7 @@ async function materializeBranches(localCard, plan, heldBranches = []) {
446
461
  pending.push({ name: ref.name, oid: currentOid });
447
462
  continue;
448
463
  }
464
+ if (currentOid === ref.oid) continue;
449
465
  if (ref.oid === null) {
450
466
  if (current.has(ref.name)) {
451
467
  await transition.deleteBranchRef(localCard.path, ref.name);
@@ -484,7 +500,6 @@ async function reduceCheckpoint(localCard, plan) {
484
500
  const baseTree = await merge.treeOf(localCard.path, baseOid);
485
501
  let indexTree = baseTree;
486
502
  let worktreeTree = baseTree;
487
- const conflicts = [];
488
503
  if (plan.checkpointBaseConflict) {
489
504
  return { state: 'waiting', reason: 'checkpoint-base-disagrees' };
490
505
  }
@@ -501,32 +516,6 @@ async function reduceCheckpoint(localCard, plan) {
501
516
  );
502
517
  }
503
518
 
504
- for (const entry of plan.parked || []) {
505
- const commonOid = await mergeBase(localCard.path, baseOid, entry.oid);
506
- if (!commonOid) return { state: 'waiting', reason: `parked-unrelated-${entry.oid}` };
507
- const commonTree = await merge.treeOf(localCard.path, commonOid);
508
- const parkedTree = await merge.treeOf(localCard.path, entry.oid);
509
- const label = `parked-${entry.version || entry.oid.slice(0, 12)}`;
510
- const reducedIndex = await merge.reduceTree(
511
- localCard.path,
512
- indexTree,
513
- commonTree,
514
- parkedTree,
515
- { label },
516
- );
517
- indexTree = reducedIndex.tree;
518
- conflicts.push(...reducedIndex.conflicts.map((item) => ({ ...item, layer: 'index' })));
519
- const reducedWorktree = await merge.reduceTree(
520
- localCard.path,
521
- worktreeTree,
522
- commonTree,
523
- parkedTree,
524
- { label },
525
- );
526
- worktreeTree = reducedWorktree.tree;
527
- conflicts.push(...reducedWorktree.conflicts.map((item) => ({ ...item, layer: 'worktree' })));
528
- }
529
-
530
519
  for (const operation of plan.transitions) {
531
520
  const from = readCheckpointValue(baseOid, operation.from);
532
521
  const to = readCheckpointValue(baseOid, operation.to);
@@ -536,11 +525,13 @@ async function reduceCheckpoint(localCard, plan) {
536
525
  }
537
526
  const fromIndexTree = await merge.treeOfPatch(localCard.path, baseOid, from.indexDiff);
538
527
  const toIndexTree = await merge.treeOfPatch(localCard.path, baseOid, to.indexDiff);
539
- const reducedIndex = await merge.reduceTree(localCard.path, indexTree, fromIndexTree, toIndexTree, {
540
- label: String(operation.version),
541
- });
528
+ const reducedIndex = await merge.reduceTree(
529
+ localCard.path,
530
+ indexTree,
531
+ fromIndexTree,
532
+ toIndexTree,
533
+ );
542
534
  indexTree = reducedIndex.tree;
543
- conflicts.push(...reducedIndex.conflicts.map((item) => ({ ...item, layer: 'index' })));
544
535
 
545
536
  const fromWorktreeTree = await merge.treeOfPatch(localCard.path, fromIndexTree, from.worktreeDiff);
546
537
  const toWorktreeTree = await merge.treeOfPatch(localCard.path, toIndexTree, to.worktreeDiff);
@@ -549,10 +540,8 @@ async function reduceCheckpoint(localCard, plan) {
549
540
  worktreeTree,
550
541
  fromWorktreeTree,
551
542
  toWorktreeTree,
552
- { label: String(operation.version) },
553
543
  );
554
544
  worktreeTree = reducedWorktree.tree;
555
- conflicts.push(...reducedWorktree.conflicts.map((item) => ({ ...item, layer: 'worktree' })));
556
545
  }
557
546
 
558
547
  const indexDiff = await merge.diffOfTree(localCard.path, baseOid, indexTree);
@@ -564,26 +553,32 @@ async function reduceCheckpoint(localCard, plan) {
564
553
  indexDiff,
565
554
  worktreeDiff,
566
555
  checkpointRevision: revisionOf(baseOid, indexDiff, worktreeDiff),
567
- conflicts,
568
556
  };
569
557
  }
570
558
 
571
- function captureIsOnRail(plan, captured, localMachine = machineId()) {
559
+ function captureIsOnRail(plan, captured, localMachine = machineId(), acknowledged = null) {
572
560
  if (
573
561
  !captured
574
562
  || captured.indexDiff === null
575
563
  || captured.baseOid !== plan.card.head.oid
576
564
  ) return false;
565
+ const revision = revisionOf(
566
+ captured.baseOid,
567
+ captured.indexDiff,
568
+ captured.worktreeDiff,
569
+ );
577
570
  const latest = [...plan.transitions]
578
571
  .reverse()
579
572
  .find((operation) => operation.machine === localMachine);
580
573
  return Boolean(
581
- latest
582
- && latest.to?.revision === revisionOf(
583
- captured.baseOid,
584
- captured.indexDiff,
585
- captured.worktreeDiff,
586
- ),
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
+ )
587
582
  );
588
583
  }
589
584
 
@@ -605,9 +600,31 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
605
600
  const planParked = new Set((plan.parked || []).map((entry) => entry.oid));
606
601
  const requiredParkedWaiting = parkedWaiting.filter((entry) => planParked.has(entry.oid));
607
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
+ }
608
622
  return { state: 'waiting', reason: `parked-${requiredParkedWaiting[0].state}` };
609
623
  }
610
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
+ }
611
628
  const reduced = await reduceCheckpoint(localCard, plan);
612
629
  if (reduced.state !== 'ready') return reduced;
613
630
 
@@ -636,23 +653,19 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
636
653
  let heldIndex;
637
654
  let targetWorktree;
638
655
  let targetIndex;
639
- const pendingConflicts = [];
640
-
641
656
  if (cardResult.pendingCard) {
642
657
  const pendingBaseTree = await merge.treeOf(localCard.path, localCard.head.oid);
643
658
  const rebasedIndex = await merge.reduceTree(
644
659
  localCard.path,
645
- pendingBaseTree,
646
- previousHeldIndex,
647
660
  reduced.indexTree,
648
- { label: `pending-card-index-${appliedThrough ?? 'local'}` },
661
+ previousHeldIndex,
662
+ pendingBaseTree,
649
663
  );
650
664
  const rebasedWorktree = await merge.reduceTree(
651
665
  localCard.path,
652
- pendingBaseTree,
653
- previousHeldWorktree,
654
666
  reduced.worktreeTree,
655
- { label: `pending-card-worktree-${appliedThrough ?? 'local'}` },
667
+ previousHeldWorktree,
668
+ pendingBaseTree,
656
669
  );
657
670
  heldIndex = pendingBaseTree;
658
671
  heldWorktree = pendingBaseTree;
@@ -664,13 +677,20 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
664
677
  stateCardMutationId = null;
665
678
  stateIndexDiff = await merge.diffOfTree(localCard.path, stateBaseOid, targetIndex);
666
679
  stateWorktreeDiff = await merge.diffOfTree(localCard.path, targetIndex, targetWorktree);
667
- pendingConflicts.push(
668
- ...rebasedIndex.conflicts.map((item) => ({ ...item, layer: 'pending-card-index' })),
669
- ...rebasedWorktree.conflicts.map((item) => ({ ...item, layer: 'pending-card-worktree' })),
670
- );
671
680
  } else {
672
681
  const captured = await captureCheckpoint(localCard.path);
673
- 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);
674
694
  const capturedIndex = represented
675
695
  ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
676
696
  : null;
@@ -686,17 +706,16 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
686
706
  const currentIndex = await merge.indexTree(localCard.path);
687
707
  const reducedLocalIndex = await merge.reduceTree(
688
708
  localCard.path,
689
- currentIndex,
690
- heldIndex,
691
709
  targetIndex,
692
- { label: `index-${reduced.checkpointRevision || 'clean'}` },
710
+ heldIndex,
711
+ currentIndex,
693
712
  );
694
713
  const applied = await merge.advance(localCard.path, heldWorktree, targetWorktree, {
695
714
  label: `checkpoint-${reduced.checkpointRevision || 'clean'}`,
696
715
  });
697
716
  const indexResult = await advanceIndex(localCard.path, currentIndex, reducedLocalIndex.tree);
698
717
  if (indexResult.state !== 'ready') return indexResult;
699
- const state = writeRepoState(plan.repoKey, {
718
+ const persisted = persistRepoState(plan.repoKey, {
700
719
  path: localCard.path,
701
720
  replica: localCard.replica,
702
721
  branch: stateBranch,
@@ -715,26 +734,21 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
715
734
  heldIndexTree: targetIndex,
716
735
  parked,
717
736
  parkedWaiting: [...parkedWaiting, ...branchResult.waiting],
718
- conflicts: [
719
- ...(cardResult.conflicts || []),
720
- ...reduced.conflicts,
721
- ...pendingConflicts,
722
- ...reducedLocalIndex.conflicts.map((item) => ({ ...item, layer: 'index-materialize' })),
723
- ...applied.conflicts,
724
- ],
725
737
  materialized: applied.heldTree === targetWorktree
726
738
  && reducedLocalIndex.tree === targetIndex,
727
739
  });
728
- appendStateEvent({
729
- resource: 'workspace-reducer',
730
- op: previous ? 'update' : 'insert',
731
- id: localCard.replica,
732
- value: state,
733
- source: 'workspace-reducer',
734
- });
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
+ }
735
749
  return {
736
- state: state.conflicts.length ? 'conflict' : 'in-sync',
737
- reason: state.conflicts.length ? state.conflicts.map((item) => `${item.kind}:${item.path}`).join(', ') : null,
750
+ state: 'in-sync',
751
+ reason: null,
738
752
  moved: cardResult.moved || branchResult.moved || applied.applied.length > 0,
739
753
  };
740
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,