amalgm 0.1.218 → 0.1.220

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.218",
3
+ "version": "0.1.220",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -176,6 +176,46 @@ test('a checkpoint base replaces covered history and keeps only newer mutations'
176
176
  assert.equal(reduced.checkpointRevision, revisionOf(oid, patchAB, ''));
177
177
  });
178
178
 
179
+ test('an unfinished checkpoint base cannot replace the last complete base', () => {
180
+ const oid = 'a'.repeat(40);
181
+ const complete = card('/complete', oid, 1);
182
+ complete.checkpointBase = {
183
+ checkpoint: CHECKPOINT_CONTRACT,
184
+ baseOid: oid,
185
+ branch: 'main',
186
+ through: 4,
187
+ value: value(oid, 'complete-patch'),
188
+ };
189
+ complete.checkpoints = [
190
+ transition('machine-a', 5, oid, 'complete-patch', 'later-patch'),
191
+ ];
192
+
193
+ const unfinished = card('/unfinished', oid, 1);
194
+ unfinished.checkpointBase = {
195
+ checkpoint: CHECKPOINT_CONTRACT,
196
+ baseOid: oid,
197
+ branch: 'main',
198
+ through: 6,
199
+ value: {
200
+ revision: 'b'.repeat(64),
201
+ payload: {
202
+ kind: 'pending',
203
+ contract: 'opaque-chunks@1',
204
+ checksum: 'c'.repeat(64),
205
+ },
206
+ },
207
+ };
208
+
209
+ const plan = reduceWall([
210
+ { machine: 'machine-a', cards: [complete] },
211
+ { machine: 'machine-b', cards: [unfinished] },
212
+ ]).get('root:test');
213
+
214
+ assert.equal(plan.checkpointBase.through, 4);
215
+ assert.equal(plan.checkpointBaseConflict, false);
216
+ assert.deepEqual(plan.transitions.map((entry) => entry.version), [5]);
217
+ });
218
+
179
219
  test('different snapshots claiming the same rail position stop instead of choosing a winner', () => {
180
220
  const oid = 'a'.repeat(40);
181
221
  const first = card('/first', oid, 1);
@@ -685,6 +725,51 @@ test('a local commit made after the verified card snapshot is never parked or ov
685
725
  );
686
726
  });
687
727
 
728
+ test('a pending descendant commit catches up without becoming a losing commit', async () => {
729
+ const { repo, oid: baseOid } = await makeRepo('pending-descendant-card');
730
+ const firstPatch = await patchFor(repo, { 'a.txt': 'before commit\n' });
731
+ const secondPatch = await patchFor(repo, {
732
+ 'a.txt': 'before commit\n',
733
+ 'b.txt': 'after commit\n',
734
+ });
735
+ const winningCard = card(repo, baseOid, 1);
736
+ const before = {
737
+ repoKey: 'root:test',
738
+ card: winningCard,
739
+ transitions: [transition('machine-peer', 2, baseOid, '', firstPatch)],
740
+ parked: [],
741
+ };
742
+
743
+ await materializeRepo(winningCard, before, 2);
744
+ await git(repo, 'add', '.');
745
+ await git(repo, 'commit', '-m', 'local pending card');
746
+ const pendingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
747
+
748
+ const pendingCard = card(repo, pendingOid, null);
749
+ const caughtUp = await materializeRepo(pendingCard, {
750
+ ...before,
751
+ transitions: [
752
+ ...before.transitions,
753
+ transition('machine-peer', 3, baseOid, firstPatch, secondPatch),
754
+ ],
755
+ }, 3);
756
+
757
+ assert.equal(caughtUp.state, 'in-sync');
758
+ assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), pendingOid);
759
+ assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'before commit\n');
760
+ assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'after commit\n');
761
+ const staged = (await git(repo, 'diff', '--cached', '--name-only')).trim();
762
+ const status = (await git(repo, 'status', '--short')).trim();
763
+ assert.equal(staged, 'b.txt', status);
764
+ await assert.rejects(
765
+ git(repo, 'rev-parse', `refs/amalgm/parked/${pendingOid}`),
766
+ );
767
+ const state = readRepoState('root:test');
768
+ assert.equal(state.baseOid, pendingOid);
769
+ assert.equal(state.appliedThrough, 3);
770
+ assert.equal(state.cardVersion, null);
771
+ });
772
+
688
773
  test('an empty index advance cannot overwrite a commit that just finished', async () => {
689
774
  const { repo } = await makeRepo('index-commit-race');
690
775
  const observedIndex = (await git(repo, 'write-tree')).trim();
@@ -148,6 +148,15 @@ test('only a value different from both the materialized baseline and the rail is
148
148
  branches: [{ name: 'main', oid: oidC }],
149
149
  };
150
150
  assert.equal(hasLocalContribution(local, remoteAdvance, baseline), false);
151
+ assert.equal(hasLocalContribution({
152
+ ...local,
153
+ head: { branch: 'main', oid: oidB, detached: false },
154
+ refs: [{ name: 'main', oid: oidB }],
155
+ }, remoteAdvance, {
156
+ ...baseline,
157
+ baseOid: oidB,
158
+ pendingCard: true,
159
+ }), true);
151
160
  assert.equal(hasLocalContribution(local, remoteAdvance, baseline, {
152
161
  checkpoint: 'repo-checkpoint@8',
153
162
  machine: 'machine-a',
@@ -444,6 +453,20 @@ test('a checkpoint base replaces history already covered by the applied rail', a
444
453
  assert.deepEqual(settleProject(), [3]);
445
454
  });
446
455
 
456
+ test('an uploading checkpoint base is not announced as complete', () => {
457
+ const card = readWall(repoKey)[0].cards[0];
458
+ const [stamped] = stampCards([card], [], { card, branches: [], transitions: [] }, {
459
+ repoKey,
460
+ baseOid: card.head.oid,
461
+ branch: card.head.branch,
462
+ appliedThrough: 3,
463
+ indexDiff: 'x'.repeat(600 * 1024),
464
+ worktreeDiff: '',
465
+ }, 'mut_project_pending_base', project);
466
+
467
+ assert.equal(stamped.checkpointBase, undefined);
468
+ });
469
+
447
470
  test('one failed project publisher cannot block the next repo', () => {
448
471
  const selected = [{ repoKey: 'root:first' }, { repoKey: 'root:second' }];
449
472
  const calls = [];
@@ -153,6 +153,7 @@ function reduceWall(entries) {
153
153
  .filter((entry) => (
154
154
  entry.baseOid === repo.card.head?.oid
155
155
  && entry.branch === repo.card.head?.branch
156
+ && checkpointValueReady(entry.value)
156
157
  ))
157
158
  .sort((left, right) => right.through - left.through);
158
159
  const newestThrough = matchingBases[0]?.through ?? null;
@@ -288,6 +289,14 @@ async function moveToWinningCard(localCard, targetCard) {
288
289
  && current.head.branch === targetCard.head?.branch
289
290
  ) return { state: 'ready', moved: false };
290
291
 
292
+ const pendingCard = current.head.oid
293
+ && targetCard.head?.oid
294
+ && current.head.branch === targetCard.head?.branch
295
+ && await isAncestor(localCard.path, targetCard.head.oid, current.head.oid);
296
+ if (pendingCard) {
297
+ return { state: 'ready', moved: false, pendingCard: true, conflicts: [] };
298
+ }
299
+
291
300
  const objects = await obtainCardObjects(localCard, targetCard);
292
301
  if (objects.state !== 'ready') return { state: 'waiting', reason: objects.state };
293
302
 
@@ -594,38 +603,87 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
594
603
  const reduced = await reduceCheckpoint(localCard, plan);
595
604
  if (reduced.state !== 'ready') return reduced;
596
605
 
597
- const baseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
598
- const captured = await captureCheckpoint(localCard.path);
599
- const represented = captureIsOnRail(plan, captured);
600
- const capturedIndex = represented
601
- ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
602
- : null;
603
- const capturedWorktree = represented
604
- ? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
605
- : null;
606
- const heldWorktree = capturedWorktree || (
606
+ const sharedBaseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
607
+ const previousHeldWorktree = (
607
608
  previous?.baseOid === plan.card.head.oid
608
609
  && previous.heldTree
609
610
  && await merge.hasTree(localCard.path, previous.heldTree)
610
611
  ? previous.heldTree
611
- : baseTree
612
+ : sharedBaseTree
612
613
  );
613
- const heldIndex = capturedIndex || (
614
+ const previousHeldIndex = (
614
615
  previous?.baseOid === plan.card.head.oid
615
616
  && previous.heldIndexTree
616
617
  && await merge.hasTree(localCard.path, previous.heldIndexTree)
617
618
  ? previous.heldIndexTree
618
- : baseTree
619
+ : sharedBaseTree
619
620
  );
621
+ let stateBaseOid = plan.card.head.oid;
622
+ let stateBranch = plan.card.head.branch;
623
+ let stateCardVersion = plan.card.positionVersion;
624
+ let stateCardMutationId = plan.card.positionMutationId;
625
+ let stateIndexDiff = reduced.indexDiff;
626
+ let stateWorktreeDiff = reduced.worktreeDiff;
627
+ let heldWorktree;
628
+ let heldIndex;
629
+ let targetWorktree;
630
+ let targetIndex;
631
+ const pendingConflicts = [];
632
+
633
+ if (cardResult.pendingCard) {
634
+ const pendingBaseTree = await merge.treeOf(localCard.path, localCard.head.oid);
635
+ const rebasedIndex = await merge.reduceTree(
636
+ localCard.path,
637
+ pendingBaseTree,
638
+ previousHeldIndex,
639
+ reduced.indexTree,
640
+ { label: `pending-card-index-${appliedThrough ?? 'local'}` },
641
+ );
642
+ const rebasedWorktree = await merge.reduceTree(
643
+ localCard.path,
644
+ pendingBaseTree,
645
+ previousHeldWorktree,
646
+ reduced.worktreeTree,
647
+ { label: `pending-card-worktree-${appliedThrough ?? 'local'}` },
648
+ );
649
+ heldIndex = pendingBaseTree;
650
+ heldWorktree = pendingBaseTree;
651
+ targetIndex = rebasedIndex.tree;
652
+ targetWorktree = rebasedWorktree.tree;
653
+ stateBaseOid = localCard.head.oid;
654
+ stateBranch = localCard.head.branch;
655
+ stateCardVersion = null;
656
+ stateCardMutationId = null;
657
+ stateIndexDiff = await merge.diffOfTree(localCard.path, stateBaseOid, targetIndex);
658
+ 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
+ } else {
664
+ const captured = await captureCheckpoint(localCard.path);
665
+ const represented = captureIsOnRail(plan, captured);
666
+ const capturedIndex = represented
667
+ ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
668
+ : null;
669
+ const capturedWorktree = represented
670
+ ? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
671
+ : null;
672
+ heldWorktree = capturedWorktree || previousHeldWorktree;
673
+ heldIndex = capturedIndex || previousHeldIndex;
674
+ targetWorktree = reduced.worktreeTree;
675
+ targetIndex = reduced.indexTree;
676
+ }
677
+
620
678
  const currentIndex = await merge.indexTree(localCard.path);
621
679
  const reducedLocalIndex = await merge.reduceTree(
622
680
  localCard.path,
623
681
  currentIndex,
624
682
  heldIndex,
625
- reduced.indexTree,
683
+ targetIndex,
626
684
  { label: `index-${reduced.checkpointRevision || 'clean'}` },
627
685
  );
628
- const applied = await merge.advance(localCard.path, heldWorktree, reduced.worktreeTree, {
686
+ const applied = await merge.advance(localCard.path, heldWorktree, targetWorktree, {
629
687
  label: `checkpoint-${reduced.checkpointRevision || 'clean'}`,
630
688
  });
631
689
  const indexResult = await advanceIndex(localCard.path, currentIndex, reducedLocalIndex.tree);
@@ -633,28 +691,30 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
633
691
  const state = writeRepoState(plan.repoKey, {
634
692
  path: localCard.path,
635
693
  replica: localCard.replica,
636
- branch: plan.card.head.branch,
637
- baseOid: plan.card.head.oid,
638
- cardVersion: plan.card.positionVersion,
639
- cardMutationId: plan.card.positionMutationId,
694
+ branch: stateBranch,
695
+ baseOid: stateBaseOid,
696
+ cardVersion: stateCardVersion,
697
+ cardMutationId: stateCardMutationId,
698
+ pendingCard: Boolean(cardResult.pendingCard),
640
699
  appliedThrough,
641
- checkpointRevision: reduced.checkpointRevision,
642
- indexDiff: reduced.indexDiff,
643
- worktreeDiff: reduced.worktreeDiff,
700
+ checkpointRevision: revisionOf(stateBaseOid, stateIndexDiff, stateWorktreeDiff),
701
+ indexDiff: stateIndexDiff,
702
+ worktreeDiff: stateWorktreeDiff,
644
703
  checkpointVersions: plan.transitions.map((entry) => entry.version),
645
704
  branches: plan.branches || [],
646
705
  heldTree: applied.heldTree,
647
- heldIndexTree: reduced.indexTree,
706
+ heldIndexTree: targetIndex,
648
707
  parked,
649
708
  parkedWaiting: [...parkedWaiting, ...branchResult.waiting],
650
709
  conflicts: [
651
710
  ...(cardResult.conflicts || []),
652
711
  ...reduced.conflicts,
712
+ ...pendingConflicts,
653
713
  ...reducedLocalIndex.conflicts.map((item) => ({ ...item, layer: 'index-materialize' })),
654
714
  ...applied.conflicts,
655
715
  ],
656
- materialized: applied.heldTree === reduced.worktreeTree
657
- && reducedLocalIndex.tree === reduced.indexTree,
716
+ materialized: applied.heldTree === targetWorktree
717
+ && reducedLocalIndex.tree === targetIndex,
658
718
  });
659
719
  appendStateEvent({
660
720
  resource: 'workspace-reducer',
@@ -20,6 +20,7 @@ const { listRepoCards } = require('./cards');
20
20
  const {
21
21
  CHECKPOINT_CONTRACT,
22
22
  checkpointValue,
23
+ checkpointValueReady,
23
24
  readCheckpoint,
24
25
  repoKeyOfCard,
25
26
  sameTransition,
@@ -252,7 +253,8 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousC
252
253
  const onRail = samePosition(card, plan.card);
253
254
  const atBaseline = baseline
254
255
  && card.head?.branch === baseline.branch
255
- && card.head?.oid === baseline.baseOid;
256
+ && card.head?.oid === baseline.baseOid
257
+ && !baseline.pendingCard;
256
258
  const alreadyPublished = validVersion(previousCard?.positionVersion)
257
259
  && samePosition(card, previousCard);
258
260
  const reversesPublishedSharedPosition = previousCard
@@ -330,6 +332,7 @@ function checkpointBaseFor(card, baseline, project, shared = null) {
330
332
  baseline.indexDiff || '',
331
333
  baseline.worktreeDiff || '',
332
334
  );
335
+ if (!checkpointValueReady(value)) return null;
333
336
  const reusableThrough = shared
334
337
  && shared.baseOid === card.head.oid
335
338
  && shared.branch === card.head.branch