amalgm 0.1.179 → 0.1.181
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 +20 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +59 -1
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +28 -14
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +45 -10
package/package.json
CHANGED
|
@@ -26,7 +26,12 @@ const { closeLocalDb } = require('../state/db');
|
|
|
26
26
|
const { captureCheckpoint, captureWorktreeDiff } = require('../workspace/git');
|
|
27
27
|
const merge = require('../workspace/merge');
|
|
28
28
|
const { applyPatch } = require('../workspace/transition');
|
|
29
|
-
const {
|
|
29
|
+
const {
|
|
30
|
+
checkpointFile,
|
|
31
|
+
checkpointValueReady,
|
|
32
|
+
readCheckpointValue,
|
|
33
|
+
revisionOf,
|
|
34
|
+
} = require('../workspace/checkpoint');
|
|
30
35
|
|
|
31
36
|
test.after(() => {
|
|
32
37
|
closeLocalDb();
|
|
@@ -161,3 +166,17 @@ test('checkpoint captures are local cache; their operations travel on the one wa
|
|
|
161
166
|
assert.notEqual(first, checkpointFile('m1', 'https://github.com/a/c'));
|
|
162
167
|
assert.match(first, /repo-checkpoints-v5/);
|
|
163
168
|
});
|
|
169
|
+
|
|
170
|
+
test('an uploading checkpoint payload is not published as a transition', () => {
|
|
171
|
+
const pending = {
|
|
172
|
+
revision: 'a'.repeat(64),
|
|
173
|
+
payload: {
|
|
174
|
+
kind: 'pending',
|
|
175
|
+
contract: 'opaque-chunks@1',
|
|
176
|
+
checksum: 'b'.repeat(64),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
assert.equal(checkpointValueReady(pending), false);
|
|
180
|
+
assert.equal(readCheckpointValue('c'.repeat(40), pending).state, 'payload-pending');
|
|
181
|
+
assert.equal(checkpointValueReady({ revision: null, indexDiff: '', worktreeDiff: '' }), true);
|
|
182
|
+
});
|
|
@@ -22,7 +22,14 @@ fs.writeFileSync(
|
|
|
22
22
|
|
|
23
23
|
const { closeLocalDb } = require('../state/db');
|
|
24
24
|
const { revisionOf } = require('../workspace/checkpoint');
|
|
25
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
captureIsOnRail,
|
|
27
|
+
hydrateRepo,
|
|
28
|
+
materializeRepo,
|
|
29
|
+
readRepoState,
|
|
30
|
+
reduceCheckpoint,
|
|
31
|
+
reduceWall,
|
|
32
|
+
} = require('../workspace/reducer');
|
|
26
33
|
|
|
27
34
|
test.after(() => {
|
|
28
35
|
closeLocalDb();
|
|
@@ -123,6 +130,24 @@ test('a stale lower rail card cannot roll back a newer branch and commit', () =>
|
|
|
123
130
|
assert.equal(plan.card.positionVersion, 12);
|
|
124
131
|
});
|
|
125
132
|
|
|
133
|
+
test('an old pending payload does not block the later complete checkpoint transition', async () => {
|
|
134
|
+
const { repo, oid } = await makeRepo('pending-payload');
|
|
135
|
+
const diff = await patchFor(repo, { 'a.txt': 'a1\n' });
|
|
136
|
+
const pending = transition('machine-a', 2, oid, '', '');
|
|
137
|
+
pending.from = {
|
|
138
|
+
revision: 'a'.repeat(64),
|
|
139
|
+
payload: { kind: 'pending', contract: 'opaque-chunks@1', checksum: 'b'.repeat(64) },
|
|
140
|
+
};
|
|
141
|
+
pending.to = pending.from;
|
|
142
|
+
const complete = transition('machine-a', 3, oid, '', diff);
|
|
143
|
+
const reduced = await reduceCheckpoint(
|
|
144
|
+
{ path: repo },
|
|
145
|
+
{ card: card(repo, oid, 1), transitions: [pending, complete] },
|
|
146
|
+
);
|
|
147
|
+
assert.equal(reduced.state, 'ready');
|
|
148
|
+
assert.match(reduced.indexDiff, /a1/);
|
|
149
|
+
});
|
|
150
|
+
|
|
126
151
|
test('an empty project folder hydrates from the winning card bundle without cloning', async () => {
|
|
127
152
|
const sourceRepo = await makeRepo('hydrate-source');
|
|
128
153
|
const source = sourceRepo.repo;
|
|
@@ -272,6 +297,39 @@ test('one materializer converges to the reduced checkpoint and later removes a r
|
|
|
272
297
|
assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'b1\n');
|
|
273
298
|
});
|
|
274
299
|
|
|
300
|
+
test('once local work is on the rail, a checkpoint conflict materializes exactly without echoing', async () => {
|
|
301
|
+
const { repo, oid } = await makeRepo('materialize-conflict');
|
|
302
|
+
const localPatch = await patchFor(repo, { 'a.txt': 'local answer\n' });
|
|
303
|
+
const remotePatch = await patchFor(repo, { 'a.txt': 'remote answer\n' });
|
|
304
|
+
const localTransition = transition('machine-local', 2, oid, '', localPatch);
|
|
305
|
+
const plan = {
|
|
306
|
+
repoKey: 'root:test',
|
|
307
|
+
card: card(repo, oid, 1),
|
|
308
|
+
transitions: [
|
|
309
|
+
localTransition,
|
|
310
|
+
transition('machine-remote', 3, oid, '', remotePatch),
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
fs.writeFileSync(path.join(repo, 'a.txt'), 'local answer\n');
|
|
315
|
+
await git(repo, 'add', 'a.txt');
|
|
316
|
+
assert.equal(captureIsOnRail(plan, {
|
|
317
|
+
baseOid: oid,
|
|
318
|
+
indexDiff: localPatch,
|
|
319
|
+
worktreeDiff: '',
|
|
320
|
+
}), true);
|
|
321
|
+
|
|
322
|
+
const outcome = await materializeRepo(plan.card, plan);
|
|
323
|
+
assert.equal(outcome.state, 'conflict');
|
|
324
|
+
const first = fs.readFileSync(path.join(repo, 'a.txt'), 'utf8');
|
|
325
|
+
assert.match(first, /local answer/);
|
|
326
|
+
assert.match(first, /remote answer/);
|
|
327
|
+
assert.match(first, /<<<<<<< current/);
|
|
328
|
+
|
|
329
|
+
await materializeRepo(plan.card, plan);
|
|
330
|
+
assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), first);
|
|
331
|
+
});
|
|
332
|
+
|
|
275
333
|
test('a writer publishing again cannot erase its earlier uncommitted work', async () => {
|
|
276
334
|
const { repo, oid } = await makeRepo('checkpoint-history');
|
|
277
335
|
const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
|
|
@@ -119,10 +119,17 @@ function checkpointValue(machine, repoKey, baseOid, indexDiff, worktreeDiff) {
|
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function checkpointValueReady(value) {
|
|
123
|
+
return !value?.payload || value.payload.kind === 'chunks';
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
function readCheckpointValue(baseOid, value) {
|
|
123
127
|
let indexDiff = typeof value?.indexDiff === 'string' ? value.indexDiff : '';
|
|
124
128
|
let worktreeDiff = typeof value?.worktreeDiff === 'string' ? value.worktreeDiff : '';
|
|
125
129
|
if (value?.payload) {
|
|
130
|
+
if (value.payload.kind === 'pending') {
|
|
131
|
+
return { state: 'payload-pending', indexDiff: null, worktreeDiff: null };
|
|
132
|
+
}
|
|
126
133
|
const payload = require('./objects').readPayload(value.payload);
|
|
127
134
|
if (payload.state !== 'ready') {
|
|
128
135
|
return { state: payload.state, indexDiff: null, worktreeDiff: null };
|
|
@@ -203,26 +210,32 @@ async function publishPass(replicaId = null) {
|
|
|
203
210
|
continue;
|
|
204
211
|
}
|
|
205
212
|
|
|
213
|
+
const from = checkpointValue(
|
|
214
|
+
machine,
|
|
215
|
+
repoKey,
|
|
216
|
+
card.head.oid,
|
|
217
|
+
reduced.indexDiff,
|
|
218
|
+
reduced.worktreeDiff,
|
|
219
|
+
);
|
|
220
|
+
const to = checkpointValue(
|
|
221
|
+
machine,
|
|
222
|
+
repoKey,
|
|
223
|
+
card.head.oid,
|
|
224
|
+
captured.indexDiff,
|
|
225
|
+
captured.worktreeDiff,
|
|
226
|
+
);
|
|
227
|
+
if (!checkpointValueReady(from) || !checkpointValueReady(to)) {
|
|
228
|
+
run.waiting += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
206
231
|
const envelope = {
|
|
207
232
|
checkpoint: CHECKPOINT_CONTRACT,
|
|
208
233
|
machine,
|
|
209
234
|
repoKey,
|
|
210
235
|
branch: card.head.branch,
|
|
211
236
|
baseOid: card.head.oid,
|
|
212
|
-
from
|
|
213
|
-
|
|
214
|
-
repoKey,
|
|
215
|
-
card.head.oid,
|
|
216
|
-
reduced.indexDiff,
|
|
217
|
-
reduced.worktreeDiff,
|
|
218
|
-
),
|
|
219
|
-
to: checkpointValue(
|
|
220
|
-
machine,
|
|
221
|
-
repoKey,
|
|
222
|
-
card.head.oid,
|
|
223
|
-
captured.indexDiff,
|
|
224
|
-
captured.worktreeDiff,
|
|
225
|
-
),
|
|
237
|
+
from,
|
|
238
|
+
to,
|
|
226
239
|
parked,
|
|
227
240
|
capturedAt: new Date().toISOString(),
|
|
228
241
|
};
|
|
@@ -322,6 +335,7 @@ module.exports = {
|
|
|
322
335
|
MAX_INLINE_DIFF_BYTES,
|
|
323
336
|
checkpointFile,
|
|
324
337
|
checkpointStats,
|
|
338
|
+
checkpointValueReady,
|
|
325
339
|
parseCheckpoint,
|
|
326
340
|
publishCheckpoints,
|
|
327
341
|
readCheckpoint,
|
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
repoKeyOfCard,
|
|
33
33
|
revisionOf,
|
|
34
34
|
} = require('./checkpoint');
|
|
35
|
+
const { machineId } = require('./identity');
|
|
35
36
|
const merge = require('./merge');
|
|
36
37
|
const transition = require('./transition');
|
|
37
38
|
|
|
@@ -378,6 +379,7 @@ async function reduceCheckpoint(localCard, plan) {
|
|
|
378
379
|
for (const operation of plan.transitions) {
|
|
379
380
|
const from = readCheckpointValue(baseOid, operation.from);
|
|
380
381
|
const to = readCheckpointValue(baseOid, operation.to);
|
|
382
|
+
if (from.state === 'payload-pending' || to.state === 'payload-pending') continue;
|
|
381
383
|
if (from.state !== 'ready' || to.state !== 'ready') {
|
|
382
384
|
return { state: 'waiting', reason: `checkpoint-${from.state !== 'ready' ? from.state : to.state}` };
|
|
383
385
|
}
|
|
@@ -415,6 +417,25 @@ async function reduceCheckpoint(localCard, plan) {
|
|
|
415
417
|
};
|
|
416
418
|
}
|
|
417
419
|
|
|
420
|
+
function captureIsOnRail(plan, captured, localMachine = machineId()) {
|
|
421
|
+
if (
|
|
422
|
+
!captured
|
|
423
|
+
|| captured.indexDiff === null
|
|
424
|
+
|| captured.baseOid !== plan.card.head.oid
|
|
425
|
+
) return false;
|
|
426
|
+
const latest = [...plan.transitions]
|
|
427
|
+
.reverse()
|
|
428
|
+
.find((operation) => operation.machine === localMachine);
|
|
429
|
+
return Boolean(
|
|
430
|
+
latest
|
|
431
|
+
&& latest.to?.revision === revisionOf(
|
|
432
|
+
captured.baseOid,
|
|
433
|
+
captured.indexDiff,
|
|
434
|
+
captured.worktreeDiff,
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
418
439
|
async function materializeRepo(localCardInput, plan) {
|
|
419
440
|
const refreshed = await refreshRepoCard(localCardInput.replica) || localCardInput;
|
|
420
441
|
const cardResult = await moveToWinningCard(refreshed, plan.card);
|
|
@@ -435,16 +456,28 @@ async function materializeRepo(localCardInput, plan) {
|
|
|
435
456
|
if (reduced.state !== 'ready') return reduced;
|
|
436
457
|
|
|
437
458
|
const baseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
?
|
|
442
|
-
:
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
459
|
+
const captured = await captureCheckpoint(localCard.path);
|
|
460
|
+
const represented = captureIsOnRail(plan, captured);
|
|
461
|
+
const capturedIndex = represented
|
|
462
|
+
? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
|
|
463
|
+
: null;
|
|
464
|
+
const capturedWorktree = represented
|
|
465
|
+
? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
|
|
466
|
+
: null;
|
|
467
|
+
const heldWorktree = capturedWorktree || (
|
|
468
|
+
previous?.baseOid === plan.card.head.oid
|
|
469
|
+
&& previous.heldTree
|
|
470
|
+
&& await merge.hasTree(localCard.path, previous.heldTree)
|
|
471
|
+
? previous.heldTree
|
|
472
|
+
: baseTree
|
|
473
|
+
);
|
|
474
|
+
const heldIndex = capturedIndex || (
|
|
475
|
+
previous?.baseOid === plan.card.head.oid
|
|
476
|
+
&& previous.heldIndexTree
|
|
477
|
+
&& await merge.hasTree(localCard.path, previous.heldIndexTree)
|
|
478
|
+
? previous.heldIndexTree
|
|
479
|
+
: baseTree
|
|
480
|
+
);
|
|
448
481
|
const currentIndex = await merge.indexTree(localCard.path);
|
|
449
482
|
const reducedLocalIndex = await merge.reduceTree(
|
|
450
483
|
localCard.path,
|
|
@@ -641,9 +674,11 @@ function reducerStats() {
|
|
|
641
674
|
}
|
|
642
675
|
|
|
643
676
|
module.exports = {
|
|
677
|
+
captureIsOnRail,
|
|
644
678
|
hydrateRepo,
|
|
645
679
|
materializeRepo,
|
|
646
680
|
readRepoState,
|
|
681
|
+
reduceCheckpoint,
|
|
647
682
|
reduceNow,
|
|
648
683
|
reduceWall,
|
|
649
684
|
reducerStats,
|