amalgm 0.1.211 → 0.1.213

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.211",
3
+ "version": "0.1.213",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -33,6 +33,7 @@ const {
33
33
  checkpointValueReady,
34
34
  readCheckpointValue,
35
35
  revisionOf,
36
+ sameTransition,
36
37
  } = require('../workspace/checkpoint');
37
38
 
38
39
  test.after(() => {
@@ -241,3 +242,31 @@ test('a clean losing commit travels as parked metadata on a no-op checkpoint', (
241
242
  assert.deepEqual(checkpoint.to, sharedValue);
242
243
  assert.deepEqual(checkpoint.parked, parked);
243
244
  });
245
+
246
+ test('a server rail number does not turn the same parked checkpoint into new work', () => {
247
+ const value = { revision: null, indexDiff: '', worktreeDiff: '' };
248
+ const checkpoint = {
249
+ branch: 'main',
250
+ baseOid: 'a'.repeat(40),
251
+ from: value,
252
+ to: value,
253
+ parked: [{
254
+ oid: 'b'.repeat(40),
255
+ ref: `refs/amalgm/parked/${'b'.repeat(40)}`,
256
+ objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
257
+ version: 12,
258
+ }],
259
+ };
260
+
261
+ assert.equal(sameTransition(checkpoint, {
262
+ ...checkpoint,
263
+ parked: checkpoint.parked.map((entry) => ({ ...entry, version: 37 })),
264
+ }), true);
265
+ assert.equal(sameTransition(checkpoint, {
266
+ ...checkpoint,
267
+ parked: checkpoint.parked.map((entry) => ({
268
+ ...entry,
269
+ objects: { ...entry.objects, checksum: 'd'.repeat(64) },
270
+ })),
271
+ }), false);
272
+ });
@@ -129,14 +129,22 @@ test('wall reduction chooses one rail winner and orders checkpoint transitions b
129
129
  const oldCard = card('/old', 'a'.repeat(40), 4);
130
130
  oldCard.checkpoints = [transition('machine-a', 9, oldCard.head.oid, '', 'a')];
131
131
  const newCard = card('/new', 'b'.repeat(40), 7);
132
- newCard.checkpoints = [transition('machine-b', 8, newCard.head.oid, '', 'b')];
132
+ const later = transition('machine-b', 11, newCard.head.oid, 'b', 'c');
133
+ later.parked = [{ oid: 'd'.repeat(40) }];
134
+ const earlier = transition('machine-b', 8, newCard.head.oid, '', 'b');
135
+ earlier.parked = [{ oid: 'c'.repeat(40) }];
136
+ newCard.checkpoints = [later, earlier];
133
137
  const plans = reduceWall([
134
138
  { machine: 'machine-a', cards: [oldCard] },
135
139
  { machine: 'machine-b', cards: [newCard] },
136
140
  ]);
137
141
  const plan = plans.get('root:test');
138
142
  assert.equal(plan.card.positionVersion, 7);
139
- assert.deepEqual(plan.transitions.map((item) => item.version), [8]);
143
+ assert.deepEqual(plan.transitions.map((item) => item.version), [8, 11]);
144
+ assert.deepEqual(plan.parked.map((item) => [item.oid, item.version]), [
145
+ ['c'.repeat(40), 8],
146
+ ['d'.repeat(40), 11],
147
+ ]);
140
148
  });
141
149
 
142
150
  test('a ready parked bundle never regresses to pending', () => {
@@ -501,6 +509,50 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
501
509
  assert.deepEqual(readRepoState('root:test').parked.map((entry) => entry.oid), [losingOid]);
502
510
  });
503
511
 
512
+ test('rail-parked peer commits become one staged checkpoint on every machine', async () => {
513
+ const { repo, oid: baseOid } = await makeRepo('peer-parked-commits');
514
+
515
+ await git(repo, 'switch', '-c', 'winner');
516
+ fs.writeFileSync(path.join(repo, 'winner.txt'), 'winning card\n');
517
+ await git(repo, 'add', '.');
518
+ await git(repo, 'commit', '-m', 'winning card');
519
+ const winningOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
520
+
521
+ await git(repo, 'switch', '-C', 'loser-a', baseOid);
522
+ fs.writeFileSync(path.join(repo, 'loser-a.txt'), 'first losing commit\n');
523
+ await git(repo, 'add', '.');
524
+ await git(repo, 'commit', '-m', 'first losing commit');
525
+ const losingAOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
526
+
527
+ await git(repo, 'switch', '-C', 'loser-b', baseOid);
528
+ fs.writeFileSync(path.join(repo, 'loser-b.txt'), 'second losing commit\n');
529
+ await git(repo, 'add', '.');
530
+ await git(repo, 'commit', '-m', 'second losing commit');
531
+ const losingBOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
532
+
533
+ await git(repo, 'switch', '-C', 'main', winningOid);
534
+ const winningCard = card(repo, winningOid, 4);
535
+ const outcome = await materializeRepo(winningCard, {
536
+ repoKey: 'root:test',
537
+ card: winningCard,
538
+ transitions: [],
539
+ parked: [
540
+ { oid: losingAOid, version: 2 },
541
+ { oid: losingBOid, version: 3 },
542
+ ],
543
+ });
544
+
545
+ assert.equal(outcome.state, 'in-sync');
546
+ assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), winningOid);
547
+ assert.deepEqual(
548
+ (await git(repo, 'diff', '--cached', '--name-only')).trim().split('\n'),
549
+ ['loser-a.txt', 'loser-b.txt'],
550
+ );
551
+ assert.equal((await git(repo, 'diff', '--name-only')).trim(), '');
552
+ assert.equal(fs.readFileSync(path.join(repo, 'loser-a.txt'), 'utf8'), 'first losing commit\n');
553
+ assert.equal(fs.readFileSync(path.join(repo, 'loser-b.txt'), 'utf8'), 'second losing commit\n');
554
+ });
555
+
504
556
  test('a local commit made after the verified card snapshot is never parked or overwritten', async () => {
505
557
  const { repo, oid: baseOid } = await makeRepo('card-snapshot-race');
506
558
  const verifiedCard = card(repo, baseOid, 1);
@@ -96,13 +96,21 @@ function valueIdentity(value) {
96
96
  });
97
97
  }
98
98
 
99
+ function parkedIdentity(parked) {
100
+ return JSON.stringify((parked || []).map((entry) => {
101
+ const stable = { ...entry };
102
+ delete stable.version;
103
+ return stable;
104
+ }));
105
+ }
106
+
99
107
  function sameTransition(left, right) {
100
108
  return left
101
109
  && left.baseOid === right.baseOid
102
110
  && left.branch === right.branch
103
111
  && valueIdentity(left.from) === valueIdentity(right.from)
104
112
  && valueIdentity(left.to) === valueIdentity(right.to)
105
- && JSON.stringify(left.parked || []) === JSON.stringify(right.parked || []);
113
+ && parkedIdentity(left.parked) === parkedIdentity(right.parked);
106
114
  }
107
115
 
108
116
  function checkpointValue(machine, repoKey, baseOid, indexDiff, worktreeDiff) {
@@ -397,6 +405,7 @@ module.exports = {
397
405
  readCheckpointValue,
398
406
  repoKeyOfCard,
399
407
  revisionOf,
408
+ sameTransition,
400
409
  startCheckpointService,
401
410
  stopCheckpointService,
402
411
  };
@@ -126,7 +126,6 @@ function reduceWall(entries) {
126
126
  && checkpoint.machine === wallEntry.machine
127
127
  ) {
128
128
  repo.transitions.push(checkpoint);
129
- repo.parked.push(...(checkpoint.parked || []));
130
129
  }
131
130
  }
132
131
  }
@@ -138,7 +137,12 @@ function reduceWall(entries) {
138
137
  && entry.branch === repo.card.head?.branch
139
138
  ))
140
139
  .sort((left, right) => left.version - right.version);
141
- repo.parked = uniqueParked(repo.parked);
140
+ repo.parked = uniqueParked(repo.transitions.flatMap((checkpoint) => (
141
+ (checkpoint.parked || []).map((entry) => ({
142
+ ...entry,
143
+ version: checkpoint.version,
144
+ }))
145
+ )));
142
146
  if (repo.card.head?.branch) {
143
147
  const held = repo.branchMap.get(repo.card.head.branch);
144
148
  if (!held || repo.card.positionVersion > held.version) {
@@ -159,15 +163,17 @@ function reduceWall(entries) {
159
163
 
160
164
  function uniqueParked(entries) {
161
165
  const byOid = new Map();
166
+ const order = [];
162
167
  for (const entry of entries || []) {
163
168
  if (typeof entry?.oid === 'string' && /^[0-9a-f]{40,64}$/.test(entry.oid)) {
164
169
  const held = byOid.get(entry.oid);
170
+ if (!held) order.push(entry.oid);
165
171
  if (!held || held.objects?.kind !== 'bundle' || entry.objects?.kind === 'bundle') {
166
172
  byOid.set(entry.oid, entry);
167
173
  }
168
174
  }
169
175
  }
170
- return [...byOid.values()].sort((left, right) => left.oid.localeCompare(right.oid));
176
+ return order.map((oid) => byOid.get(oid));
171
177
  }
172
178
 
173
179
  async function obtainCardObjects(localCard, targetCard) {
@@ -411,6 +417,32 @@ async function reduceCheckpoint(localCard, plan) {
411
417
  let worktreeTree = baseTree;
412
418
  const conflicts = [];
413
419
 
420
+ for (const entry of plan.parked || []) {
421
+ const commonOid = await mergeBase(localCard.path, baseOid, entry.oid);
422
+ if (!commonOid) return { state: 'waiting', reason: `parked-unrelated-${entry.oid}` };
423
+ const commonTree = await merge.treeOf(localCard.path, commonOid);
424
+ const parkedTree = await merge.treeOf(localCard.path, entry.oid);
425
+ const label = `parked-${entry.version || entry.oid.slice(0, 12)}`;
426
+ const reducedIndex = await merge.reduceTree(
427
+ localCard.path,
428
+ indexTree,
429
+ commonTree,
430
+ parkedTree,
431
+ { label },
432
+ );
433
+ indexTree = reducedIndex.tree;
434
+ conflicts.push(...reducedIndex.conflicts.map((item) => ({ ...item, layer: 'index' })));
435
+ const reducedWorktree = await merge.reduceTree(
436
+ localCard.path,
437
+ worktreeTree,
438
+ commonTree,
439
+ parkedTree,
440
+ { label },
441
+ );
442
+ worktreeTree = reducedWorktree.tree;
443
+ conflicts.push(...reducedWorktree.conflicts.map((item) => ({ ...item, layer: 'worktree' })));
444
+ }
445
+
414
446
  for (const operation of plan.transitions) {
415
447
  const from = readCheckpointValue(baseOid, operation.from);
416
448
  const to = readCheckpointValue(baseOid, operation.to);
@@ -486,6 +518,11 @@ async function materializeRepo(localCardInput, plan) {
486
518
  ...(cardResult.parked || []),
487
519
  ]);
488
520
  const parkedWaiting = await materializeParked(localCard, parked);
521
+ const planParked = new Set((plan.parked || []).map((entry) => entry.oid));
522
+ const requiredParkedWaiting = parkedWaiting.filter((entry) => planParked.has(entry.oid));
523
+ if (requiredParkedWaiting.length > 0) {
524
+ return { state: 'waiting', reason: `parked-${requiredParkedWaiting[0].state}` };
525
+ }
489
526
  const branchResult = await materializeBranches(localCard, plan);
490
527
  const reduced = await reduceCheckpoint(localCard, plan);
491
528
  if (reduced.state !== 'ready') return reduced;
@@ -17,7 +17,7 @@ const { AMALGM_DIR } = require('../config');
17
17
  const { subscribeStateEvents } = require('../state/events');
18
18
  const { machineId, userId } = require('./identity');
19
19
  const { listRepoCards } = require('./cards');
20
- const { readCheckpoint, repoKeyOfCard } = require('./checkpoint');
20
+ const { readCheckpoint, repoKeyOfCard, sameTransition } = require('./checkpoint');
21
21
 
22
22
  const WALL_CONTRACT = 'code-project@3';
23
23
  const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects');
@@ -209,17 +209,6 @@ function samePosition(left, right) {
209
209
  && Boolean(left?.head?.detached) === Boolean(right?.head?.detached);
210
210
  }
211
211
 
212
- function checkpointIdentity(checkpoint) {
213
- if (!checkpoint) return null;
214
- return JSON.stringify({
215
- baseOid: checkpoint.baseOid,
216
- branch: checkpoint.branch,
217
- from: checkpoint.from,
218
- to: checkpoint.to,
219
- parked: checkpoint.parked || [],
220
- });
221
- }
222
-
223
212
  function oidMap(refs) {
224
213
  return new Map((refs || []).map((ref) => [ref.name, ref.oid ?? null]));
225
214
  }
@@ -278,7 +267,7 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousC
278
267
  const latest = [...(plan.transitions || [])]
279
268
  .reverse()
280
269
  .find((entry) => entry.machine === checkpoint.machine);
281
- if (checkpointIdentity(latest) !== checkpointIdentity(checkpoint)) return true;
270
+ if (!sameTransition(latest, checkpoint)) return true;
282
271
  }
283
272
  return false;
284
273
  }
@@ -444,7 +433,7 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
444
433
  : [];
445
434
  const last = history.at(-1);
446
435
  const checkpoints = currentCheckpoint
447
- ? (checkpointIdentity(last) === checkpointIdentity(currentCheckpoint)
436
+ ? (sameTransition(last, currentCheckpoint)
448
437
  ? history
449
438
  : [...history, { ...currentCheckpoint, mutationId }])
450
439
  : history;