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 +1 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +29 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +54 -2
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +10 -1
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +40 -3
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +3 -14
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
&&
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
-
? (
|
|
436
|
+
? (sameTransition(last, currentCheckpoint)
|
|
448
437
|
? history
|
|
449
438
|
: [...history, { ...currentCheckpoint, mutationId }])
|
|
450
439
|
: history;
|