amalgm 0.1.217 → 0.1.219
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 +1 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +95 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +55 -17
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +3 -2
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +135 -26
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +89 -13
package/package.json
CHANGED
|
@@ -180,7 +180,7 @@ test('checkpoint captures are local cache; their operations travel on the one wa
|
|
|
180
180
|
assert.equal(first, checkpointFile('m1', 'https://github.com/a/b'));
|
|
181
181
|
assert.notEqual(first, checkpointFile('m2', 'https://github.com/a/b'));
|
|
182
182
|
assert.notEqual(first, checkpointFile('m1', 'https://github.com/a/c'));
|
|
183
|
-
assert.match(first, /repo-checkpoints-
|
|
183
|
+
assert.match(first, /repo-checkpoints-v8/);
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
test('a materialized peer card is immediately a valid checkpoint base', () => {
|
|
@@ -258,7 +258,7 @@ test('two concurrent compacting publishers converge without losing either latest
|
|
|
258
258
|
const Y = require('yjs');
|
|
259
259
|
const repoKey = 'root:compaction-collision';
|
|
260
260
|
const line = (machine, value) => JSON.stringify({
|
|
261
|
-
wall: 'code-project@
|
|
261
|
+
wall: 'code-project@4',
|
|
262
262
|
project: repoKey,
|
|
263
263
|
machine,
|
|
264
264
|
mutationId: `mut_${machine}_${value}`,
|
|
@@ -152,6 +152,56 @@ test('wall reduction chooses one rail winner and orders checkpoint transitions b
|
|
|
152
152
|
]);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
test('a checkpoint base replaces covered history and keeps only newer mutations', async () => {
|
|
156
|
+
const { repo, oid } = await makeRepo('checkpoint-base');
|
|
157
|
+
const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
|
|
158
|
+
const patchAB = await patchFor(repo, { 'a.txt': 'a1\n', 'b.txt': 'b1\n' });
|
|
159
|
+
const current = card(repo, oid, 1);
|
|
160
|
+
current.checkpointBase = {
|
|
161
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
162
|
+
baseOid: oid,
|
|
163
|
+
branch: 'main',
|
|
164
|
+
through: 4,
|
|
165
|
+
value: value(oid, patchA),
|
|
166
|
+
};
|
|
167
|
+
current.checkpoints = [
|
|
168
|
+
transition('machine-a', 2, oid, '', patchA),
|
|
169
|
+
transition('machine-a', 5, oid, patchA, patchAB),
|
|
170
|
+
];
|
|
171
|
+
const plan = reduceWall([{ machine: 'machine-a', cards: [current] }]).get('root:test');
|
|
172
|
+
assert.equal(plan.checkpointBase.through, 4);
|
|
173
|
+
assert.deepEqual(plan.transitions.map((entry) => entry.version), [5]);
|
|
174
|
+
const reduced = await reduceCheckpoint(current, plan);
|
|
175
|
+
assert.equal(reduced.state, 'ready');
|
|
176
|
+
assert.equal(reduced.checkpointRevision, revisionOf(oid, patchAB, ''));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('different snapshots claiming the same rail position stop instead of choosing a winner', () => {
|
|
180
|
+
const oid = 'a'.repeat(40);
|
|
181
|
+
const first = card('/first', oid, 1);
|
|
182
|
+
first.checkpointBase = {
|
|
183
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
184
|
+
baseOid: oid,
|
|
185
|
+
branch: 'main',
|
|
186
|
+
through: 4,
|
|
187
|
+
value: value(oid, 'patch-a'),
|
|
188
|
+
};
|
|
189
|
+
const second = card('/second', oid, 1);
|
|
190
|
+
second.checkpointBase = {
|
|
191
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
192
|
+
baseOid: oid,
|
|
193
|
+
branch: 'main',
|
|
194
|
+
through: 4,
|
|
195
|
+
value: value(oid, 'patch-b'),
|
|
196
|
+
};
|
|
197
|
+
const plan = reduceWall([
|
|
198
|
+
{ machine: 'machine-a', cards: [first] },
|
|
199
|
+
{ machine: 'machine-b', cards: [second] },
|
|
200
|
+
]).get('root:test');
|
|
201
|
+
assert.equal(plan.checkpointBase, null);
|
|
202
|
+
assert.equal(plan.checkpointBaseConflict, true);
|
|
203
|
+
});
|
|
204
|
+
|
|
155
205
|
test('a ready parked bundle never regresses to pending', () => {
|
|
156
206
|
const oid = 'c'.repeat(40);
|
|
157
207
|
const ready = card('/ready', 'a'.repeat(40), 1);
|
|
@@ -635,6 +685,51 @@ test('a local commit made after the verified card snapshot is never parked or ov
|
|
|
635
685
|
);
|
|
636
686
|
});
|
|
637
687
|
|
|
688
|
+
test('a pending descendant commit catches up without becoming a losing commit', async () => {
|
|
689
|
+
const { repo, oid: baseOid } = await makeRepo('pending-descendant-card');
|
|
690
|
+
const firstPatch = await patchFor(repo, { 'a.txt': 'before commit\n' });
|
|
691
|
+
const secondPatch = await patchFor(repo, {
|
|
692
|
+
'a.txt': 'before commit\n',
|
|
693
|
+
'b.txt': 'after commit\n',
|
|
694
|
+
});
|
|
695
|
+
const winningCard = card(repo, baseOid, 1);
|
|
696
|
+
const before = {
|
|
697
|
+
repoKey: 'root:test',
|
|
698
|
+
card: winningCard,
|
|
699
|
+
transitions: [transition('machine-peer', 2, baseOid, '', firstPatch)],
|
|
700
|
+
parked: [],
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
await materializeRepo(winningCard, before, 2);
|
|
704
|
+
await git(repo, 'add', '.');
|
|
705
|
+
await git(repo, 'commit', '-m', 'local pending card');
|
|
706
|
+
const pendingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
|
|
707
|
+
|
|
708
|
+
const pendingCard = card(repo, pendingOid, null);
|
|
709
|
+
const caughtUp = await materializeRepo(pendingCard, {
|
|
710
|
+
...before,
|
|
711
|
+
transitions: [
|
|
712
|
+
...before.transitions,
|
|
713
|
+
transition('machine-peer', 3, baseOid, firstPatch, secondPatch),
|
|
714
|
+
],
|
|
715
|
+
}, 3);
|
|
716
|
+
|
|
717
|
+
assert.equal(caughtUp.state, 'in-sync');
|
|
718
|
+
assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), pendingOid);
|
|
719
|
+
assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'before commit\n');
|
|
720
|
+
assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'after commit\n');
|
|
721
|
+
const staged = (await git(repo, 'diff', '--cached', '--name-only')).trim();
|
|
722
|
+
const status = (await git(repo, 'status', '--short')).trim();
|
|
723
|
+
assert.equal(staged, 'b.txt', status);
|
|
724
|
+
await assert.rejects(
|
|
725
|
+
git(repo, 'rev-parse', `refs/amalgm/parked/${pendingOid}`),
|
|
726
|
+
);
|
|
727
|
+
const state = readRepoState('root:test');
|
|
728
|
+
assert.equal(state.baseOid, pendingOid);
|
|
729
|
+
assert.equal(state.appliedThrough, 3);
|
|
730
|
+
assert.equal(state.cardVersion, null);
|
|
731
|
+
});
|
|
732
|
+
|
|
638
733
|
test('an empty index advance cannot overwrite a commit that just finished', async () => {
|
|
639
734
|
const { repo } = await makeRepo('index-commit-race');
|
|
640
735
|
const observedIndex = (await git(repo, 'write-tree')).trim();
|
|
@@ -35,6 +35,7 @@ const { upsertWorkspace } = require('../workspace/store');
|
|
|
35
35
|
const { refreshRepoCards } = require('../workspace/cards');
|
|
36
36
|
const { checkpointFile, repoKeyOfCard } = require('../workspace/checkpoint');
|
|
37
37
|
const {
|
|
38
|
+
_test: wallTest,
|
|
38
39
|
bindProject,
|
|
39
40
|
compactWallText,
|
|
40
41
|
hasLocalContribution,
|
|
@@ -147,8 +148,17 @@ test('only a value different from both the materialized baseline and the rail is
|
|
|
147
148
|
branches: [{ name: 'main', oid: oidC }],
|
|
148
149
|
};
|
|
149
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);
|
|
150
160
|
assert.equal(hasLocalContribution(local, remoteAdvance, baseline, {
|
|
151
|
-
checkpoint: 'repo-checkpoint@
|
|
161
|
+
checkpoint: 'repo-checkpoint@8',
|
|
152
162
|
machine: 'machine-a',
|
|
153
163
|
branch: 'main',
|
|
154
164
|
baseOid: oidA,
|
|
@@ -218,7 +228,7 @@ test('only a value different from both the materialized baseline and the rail is
|
|
|
218
228
|
},
|
|
219
229
|
), true);
|
|
220
230
|
assert.equal(hasLocalContribution(local, plan, baseline, {
|
|
221
|
-
checkpoint: 'repo-checkpoint@
|
|
231
|
+
checkpoint: 'repo-checkpoint@8',
|
|
222
232
|
machine: 'machine-a',
|
|
223
233
|
branch: 'main',
|
|
224
234
|
baseOid: oidA,
|
|
@@ -328,7 +338,7 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
328
338
|
assert.equal(first.published, true);
|
|
329
339
|
const entries = readWall(repoKey);
|
|
330
340
|
assert.equal(entries.length, 1);
|
|
331
|
-
assert.equal(entries[0].wall, 'code-project@
|
|
341
|
+
assert.equal(entries[0].wall, 'code-project@4');
|
|
332
342
|
assert.equal(entries[0].project, repoKey);
|
|
333
343
|
assert.equal(entries[0].machine, 'machine-a');
|
|
334
344
|
assert.equal(entries[0].cards.length, 1);
|
|
@@ -384,12 +394,12 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
384
394
|
assert.equal(getDocText(project.file).trim().split('\n').length, 1);
|
|
385
395
|
});
|
|
386
396
|
|
|
387
|
-
test('checkpoint
|
|
397
|
+
test('a checkpoint base replaces history already covered by the applied rail', async () => {
|
|
388
398
|
const card = readWall(repoKey)[0].cards[0];
|
|
389
399
|
const file = checkpointFile('machine-a', repoKey);
|
|
390
400
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
391
401
|
const firstCheckpoint = {
|
|
392
|
-
checkpoint: 'repo-checkpoint@
|
|
402
|
+
checkpoint: 'repo-checkpoint@8',
|
|
393
403
|
machine: 'machine-a',
|
|
394
404
|
repoKey,
|
|
395
405
|
branch: card.head.branch,
|
|
@@ -413,6 +423,20 @@ test('checkpoint transitions append until the next card and carry their official
|
|
|
413
423
|
JSON.parse(checkpointLines.at(-1)).cards[0].checkpoints[0].version ?? null,
|
|
414
424
|
null,
|
|
415
425
|
);
|
|
426
|
+
const reducerFile = path.join(
|
|
427
|
+
process.env.AMALGM_DIR,
|
|
428
|
+
'state',
|
|
429
|
+
'repo-reducer',
|
|
430
|
+
`${crypto.createHash('sha256').update(repoKey).digest('hex').slice(0, 24)}.json`,
|
|
431
|
+
);
|
|
432
|
+
fs.writeFileSync(reducerFile, `${JSON.stringify({
|
|
433
|
+
repoKey,
|
|
434
|
+
baseOid: card.head.oid,
|
|
435
|
+
branch: card.head.branch,
|
|
436
|
+
appliedThrough: 2,
|
|
437
|
+
indexDiff: '',
|
|
438
|
+
worktreeDiff: 'patch-a',
|
|
439
|
+
})}\n`);
|
|
416
440
|
|
|
417
441
|
fs.writeFileSync(file, `${JSON.stringify({
|
|
418
442
|
...firstCheckpoint,
|
|
@@ -422,12 +446,26 @@ test('checkpoint transitions append until the next card and carry their official
|
|
|
422
446
|
})}\n`);
|
|
423
447
|
const second = publishWall(repoKey);
|
|
424
448
|
assert.equal(second.published, true);
|
|
425
|
-
const
|
|
426
|
-
assert.equal(
|
|
427
|
-
assert.
|
|
449
|
+
const latestCard = readWall(repoKey)[0].cards[0];
|
|
450
|
+
assert.equal(latestCard.checkpointBase.through, 2);
|
|
451
|
+
assert.equal(latestCard.checkpoints.length, 1);
|
|
452
|
+
assert.deepEqual(latestCard.checkpoints.map((entry) => entry.mutationId), [second.mutationId]);
|
|
428
453
|
assert.deepEqual(settleProject(), [3]);
|
|
429
454
|
});
|
|
430
455
|
|
|
456
|
+
test('one failed project publisher cannot block the next repo', () => {
|
|
457
|
+
const selected = [{ repoKey: 'root:first' }, { repoKey: 'root:second' }];
|
|
458
|
+
const calls = [];
|
|
459
|
+
const results = wallTest.publishSelected(selected, [], (selectedProject) => {
|
|
460
|
+
calls.push(selectedProject.repoKey);
|
|
461
|
+
if (selectedProject.repoKey === 'root:first') throw new Error('too large');
|
|
462
|
+
return { published: true, repoKey: selectedProject.repoKey };
|
|
463
|
+
});
|
|
464
|
+
assert.deepEqual(calls, ['root:first', 'root:second']);
|
|
465
|
+
assert.equal(results[0].reason, 'publish-failed');
|
|
466
|
+
assert.equal(results[1].published, true);
|
|
467
|
+
});
|
|
468
|
+
|
|
431
469
|
test('side branches keep one rail operation while the checked-out card advances', async () => {
|
|
432
470
|
const repo = path.join(tempRoot, 'projects/app');
|
|
433
471
|
const before = readWall(repoKey)[0].cards[0];
|
|
@@ -475,7 +513,7 @@ test('side branches keep one rail operation while the checked-out card advances'
|
|
|
475
513
|
test('peer records and earlier local history survive later publishes', async () => {
|
|
476
514
|
const beforeLines = getDocText(project.file).trim().split('\n');
|
|
477
515
|
const peerLine = JSON.stringify({
|
|
478
|
-
wall: 'code-project@
|
|
516
|
+
wall: 'code-project@4',
|
|
479
517
|
project: repoKey,
|
|
480
518
|
machine: 'machine-b',
|
|
481
519
|
mutationId: 'mut_project_peer',
|
|
@@ -532,7 +570,7 @@ test('three concurrent append records stay whole under Yjs ordering', () => {
|
|
|
532
570
|
|
|
533
571
|
test('wall compaction deletes only records superseded for every machine', () => {
|
|
534
572
|
const line = (machine, value) => JSON.stringify({
|
|
535
|
-
wall: 'code-project@
|
|
573
|
+
wall: 'code-project@4',
|
|
536
574
|
project: repoKey,
|
|
537
575
|
machine,
|
|
538
576
|
mutationId: `mut_project_${machine}_${value}`,
|
|
@@ -541,7 +579,7 @@ test('wall compaction deletes only records superseded for every machine', () =>
|
|
|
541
579
|
const oldA = line('machine-a', 'old-a');
|
|
542
580
|
const oldB = line('machine-b', 'old-b');
|
|
543
581
|
const latestA = line('machine-a', 'latest-a');
|
|
544
|
-
const torn = '{"wall":"code-project@
|
|
582
|
+
const torn = '{"wall":"code-project@4","machine":"torn"';
|
|
545
583
|
const latestB = line('machine-b', 'latest-b');
|
|
546
584
|
const current = `${oldA}\n${oldB}\n${latestA}\n${torn}\n${latestB}\n`;
|
|
547
585
|
const compacted = compactWallText(current, repoKey);
|
|
@@ -553,13 +591,13 @@ test('wall compaction deletes only records superseded for every machine', () =>
|
|
|
553
591
|
|
|
554
592
|
test('an unordered legacy record cannot pin wall compaction', () => {
|
|
555
593
|
const unordered = JSON.stringify({
|
|
556
|
-
wall: 'code-project@
|
|
594
|
+
wall: 'code-project@4',
|
|
557
595
|
project: repoKey,
|
|
558
596
|
machine: '16.612Z',
|
|
559
597
|
cards: [{ value: 'corrupted-legacy-record' }],
|
|
560
598
|
});
|
|
561
599
|
const ordered = (machine) => JSON.stringify({
|
|
562
|
-
wall: 'code-project@
|
|
600
|
+
wall: 'code-project@4',
|
|
563
601
|
project: repoKey,
|
|
564
602
|
machine,
|
|
565
603
|
mutationId: `mut_project_${machine}`,
|
|
@@ -576,7 +614,7 @@ test('an unordered legacy record cannot pin wall compaction', () => {
|
|
|
576
614
|
|
|
577
615
|
test('retired machine values stay represented without pinning obsolete active history', () => {
|
|
578
616
|
const line = (machine, value) => JSON.stringify({
|
|
579
|
-
wall: 'code-project@
|
|
617
|
+
wall: 'code-project@4',
|
|
580
618
|
project: repoKey,
|
|
581
619
|
machine,
|
|
582
620
|
mutationId: `mut_project_${machine}_${value}`,
|
|
@@ -601,7 +639,7 @@ test('retired machine values stay represented without pinning obsolete active hi
|
|
|
601
639
|
test('a concurrent deletion-only compaction and append commute in Yjs', () => {
|
|
602
640
|
const Y = require('yjs');
|
|
603
641
|
const line = (machine, value) => JSON.stringify({
|
|
604
|
-
wall: 'code-project@
|
|
642
|
+
wall: 'code-project@4',
|
|
605
643
|
project: repoKey,
|
|
606
644
|
machine,
|
|
607
645
|
mutationId: `mut_project_${machine}_${value}`,
|
|
@@ -654,7 +692,7 @@ test('a concurrent deletion-only compaction and append commute in Yjs', () => {
|
|
|
654
692
|
test('a torn line is ignored by readers, never fatal', () => {
|
|
655
693
|
writeDocText(
|
|
656
694
|
project.file,
|
|
657
|
-
`${getDocText(project.file)}{"wall":"code-project@
|
|
695
|
+
`${getDocText(project.file)}{"wall":"code-project@4","project":"${repoKey}","machine":"machine-c","car\n`,
|
|
658
696
|
);
|
|
659
697
|
settleProject();
|
|
660
698
|
assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
|
|
@@ -682,7 +720,7 @@ test('publishing compacts an oversized history before appending the new record',
|
|
|
682
720
|
test('project stats name every project resource, binding, and publish counter', () => {
|
|
683
721
|
const stats = wallStats();
|
|
684
722
|
const row = stats.projects.find((entry) => entry.repoKey === repoKey);
|
|
685
|
-
assert.equal(stats.contract, 'code-project@
|
|
723
|
+
assert.equal(stats.contract, 'code-project@4');
|
|
686
724
|
assert.equal(row.resourceId, project.resourceId);
|
|
687
725
|
assert.equal(row.replica.appliedVersion >= 0, true);
|
|
688
726
|
assert.equal(row.machines, 2);
|
|
@@ -25,7 +25,7 @@ const { machineId } = require('./identity');
|
|
|
25
25
|
const { listRepoCards } = require('./cards');
|
|
26
26
|
const { captureCheckpoint } = require('./git');
|
|
27
27
|
|
|
28
|
-
const CHECKPOINT_CONTRACT = 'repo-checkpoint@
|
|
28
|
+
const CHECKPOINT_CONTRACT = 'repo-checkpoint@8';
|
|
29
29
|
const MAX_INLINE_DIFF_BYTES = 512 * 1024;
|
|
30
30
|
const PUBLISH_QUIET_MS = 25;
|
|
31
31
|
const RECONCILE_INTERVAL_MS = 30_000;
|
|
@@ -54,7 +54,7 @@ function checkpointFile(machine, repoKey) {
|
|
|
54
54
|
.update(`${CHECKPOINT_CONTRACT}|${machine}|${repoKey}`)
|
|
55
55
|
.digest('hex')
|
|
56
56
|
.slice(0, 24);
|
|
57
|
-
return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-
|
|
57
|
+
return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-v8', `${digest}.json`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function revisionOf(baseOid, indexDiff, worktreeDiff = '') {
|
|
@@ -416,6 +416,7 @@ module.exports = {
|
|
|
416
416
|
MAX_INLINE_DIFF_BYTES,
|
|
417
417
|
checkpointFile,
|
|
418
418
|
checkpointStats,
|
|
419
|
+
checkpointValue,
|
|
419
420
|
checkpointValueReady,
|
|
420
421
|
parseCheckpoint,
|
|
421
422
|
publishCheckpoints,
|
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
} = require('./git');
|
|
30
30
|
const {
|
|
31
31
|
CHECKPOINT_CONTRACT,
|
|
32
|
+
checkpointValueReady,
|
|
32
33
|
readCheckpointValue,
|
|
33
34
|
repoKeyOfCard,
|
|
34
35
|
revisionOf,
|
|
@@ -108,7 +109,14 @@ function reduceWall(entries) {
|
|
|
108
109
|
) continue;
|
|
109
110
|
let repo = repos.get(repoKey);
|
|
110
111
|
if (!repo) {
|
|
111
|
-
repo = {
|
|
112
|
+
repo = {
|
|
113
|
+
repoKey,
|
|
114
|
+
card: null,
|
|
115
|
+
checkpointBases: [],
|
|
116
|
+
transitions: [],
|
|
117
|
+
parked: [],
|
|
118
|
+
branchMap: new Map(),
|
|
119
|
+
};
|
|
112
120
|
repos.set(repoKey, repo);
|
|
113
121
|
}
|
|
114
122
|
if (!repo.card || card.positionVersion > repo.card.positionVersion) {
|
|
@@ -128,13 +136,39 @@ function reduceWall(entries) {
|
|
|
128
136
|
repo.transitions.push(checkpoint);
|
|
129
137
|
}
|
|
130
138
|
}
|
|
139
|
+
const checkpointBase = card.checkpointBase;
|
|
140
|
+
if (
|
|
141
|
+
checkpointBase?.checkpoint === CHECKPOINT_CONTRACT
|
|
142
|
+
&& Number.isInteger(checkpointBase.through)
|
|
143
|
+
&& checkpointBase.through >= 0
|
|
144
|
+
&& checkpointBase.baseOid === card.head?.oid
|
|
145
|
+
&& checkpointBase.branch === card.head?.branch
|
|
146
|
+
) {
|
|
147
|
+
repo.checkpointBases.push(checkpointBase);
|
|
148
|
+
}
|
|
131
149
|
}
|
|
132
150
|
}
|
|
133
151
|
for (const repo of repos.values()) {
|
|
152
|
+
const matchingBases = repo.checkpointBases
|
|
153
|
+
.filter((entry) => (
|
|
154
|
+
entry.baseOid === repo.card.head?.oid
|
|
155
|
+
&& entry.branch === repo.card.head?.branch
|
|
156
|
+
))
|
|
157
|
+
.sort((left, right) => right.through - left.through);
|
|
158
|
+
const newestThrough = matchingBases[0]?.through ?? null;
|
|
159
|
+
const newestBases = matchingBases
|
|
160
|
+
.filter((entry) => entry.through === newestThrough)
|
|
161
|
+
.sort((left, right) => (
|
|
162
|
+
Number(checkpointValueReady(right.value)) - Number(checkpointValueReady(left.value))
|
|
163
|
+
));
|
|
164
|
+
const baseRevisions = new Set(newestBases.map((entry) => entry.value?.revision ?? null));
|
|
165
|
+
repo.checkpointBaseConflict = baseRevisions.size > 1;
|
|
166
|
+
repo.checkpointBase = repo.checkpointBaseConflict ? null : newestBases[0] || null;
|
|
134
167
|
repo.transitions = repo.transitions
|
|
135
168
|
.filter((entry) => (
|
|
136
169
|
entry.baseOid === repo.card.head?.oid
|
|
137
170
|
&& entry.branch === repo.card.head?.branch
|
|
171
|
+
&& (!repo.checkpointBase || entry.version > repo.checkpointBase.through)
|
|
138
172
|
))
|
|
139
173
|
.sort((left, right) => left.version - right.version);
|
|
140
174
|
repo.parked = uniqueParked(repo.transitions.flatMap((checkpoint) => (
|
|
@@ -157,6 +191,7 @@ function reduceWall(entries) {
|
|
|
157
191
|
}
|
|
158
192
|
repo.branches = [...repo.branchMap.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
159
193
|
delete repo.branchMap;
|
|
194
|
+
delete repo.checkpointBases;
|
|
160
195
|
}
|
|
161
196
|
return repos;
|
|
162
197
|
}
|
|
@@ -253,6 +288,14 @@ async function moveToWinningCard(localCard, targetCard) {
|
|
|
253
288
|
&& current.head.branch === targetCard.head?.branch
|
|
254
289
|
) return { state: 'ready', moved: false };
|
|
255
290
|
|
|
291
|
+
const pendingCard = current.head.oid
|
|
292
|
+
&& targetCard.head?.oid
|
|
293
|
+
&& current.head.branch === targetCard.head?.branch
|
|
294
|
+
&& await isAncestor(localCard.path, targetCard.head.oid, current.head.oid);
|
|
295
|
+
if (pendingCard) {
|
|
296
|
+
return { state: 'ready', moved: false, pendingCard: true, conflicts: [] };
|
|
297
|
+
}
|
|
298
|
+
|
|
256
299
|
const objects = await obtainCardObjects(localCard, targetCard);
|
|
257
300
|
if (objects.state !== 'ready') return { state: 'waiting', reason: objects.state };
|
|
258
301
|
|
|
@@ -433,6 +476,21 @@ async function reduceCheckpoint(localCard, plan) {
|
|
|
433
476
|
let indexTree = baseTree;
|
|
434
477
|
let worktreeTree = baseTree;
|
|
435
478
|
const conflicts = [];
|
|
479
|
+
if (plan.checkpointBaseConflict) {
|
|
480
|
+
return { state: 'waiting', reason: 'checkpoint-base-disagrees' };
|
|
481
|
+
}
|
|
482
|
+
if (plan.checkpointBase) {
|
|
483
|
+
const snapshot = readCheckpointValue(baseOid, plan.checkpointBase.value);
|
|
484
|
+
if (snapshot.state !== 'ready') {
|
|
485
|
+
return { state: 'waiting', reason: `checkpoint-base-${snapshot.state}` };
|
|
486
|
+
}
|
|
487
|
+
indexTree = await merge.treeOfPatch(localCard.path, baseOid, snapshot.indexDiff);
|
|
488
|
+
worktreeTree = await merge.treeOfPatch(
|
|
489
|
+
localCard.path,
|
|
490
|
+
indexTree,
|
|
491
|
+
snapshot.worktreeDiff,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
436
494
|
|
|
437
495
|
for (const entry of plan.parked || []) {
|
|
438
496
|
const commonOid = await mergeBase(localCard.path, baseOid, entry.oid);
|
|
@@ -544,38 +602,87 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
|
|
|
544
602
|
const reduced = await reduceCheckpoint(localCard, plan);
|
|
545
603
|
if (reduced.state !== 'ready') return reduced;
|
|
546
604
|
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
const represented = captureIsOnRail(plan, captured);
|
|
550
|
-
const capturedIndex = represented
|
|
551
|
-
? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
|
|
552
|
-
: null;
|
|
553
|
-
const capturedWorktree = represented
|
|
554
|
-
? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
|
|
555
|
-
: null;
|
|
556
|
-
const heldWorktree = capturedWorktree || (
|
|
605
|
+
const sharedBaseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
|
|
606
|
+
const previousHeldWorktree = (
|
|
557
607
|
previous?.baseOid === plan.card.head.oid
|
|
558
608
|
&& previous.heldTree
|
|
559
609
|
&& await merge.hasTree(localCard.path, previous.heldTree)
|
|
560
610
|
? previous.heldTree
|
|
561
|
-
:
|
|
611
|
+
: sharedBaseTree
|
|
562
612
|
);
|
|
563
|
-
const
|
|
613
|
+
const previousHeldIndex = (
|
|
564
614
|
previous?.baseOid === plan.card.head.oid
|
|
565
615
|
&& previous.heldIndexTree
|
|
566
616
|
&& await merge.hasTree(localCard.path, previous.heldIndexTree)
|
|
567
617
|
? previous.heldIndexTree
|
|
568
|
-
:
|
|
618
|
+
: sharedBaseTree
|
|
569
619
|
);
|
|
620
|
+
let stateBaseOid = plan.card.head.oid;
|
|
621
|
+
let stateBranch = plan.card.head.branch;
|
|
622
|
+
let stateCardVersion = plan.card.positionVersion;
|
|
623
|
+
let stateCardMutationId = plan.card.positionMutationId;
|
|
624
|
+
let stateIndexDiff = reduced.indexDiff;
|
|
625
|
+
let stateWorktreeDiff = reduced.worktreeDiff;
|
|
626
|
+
let heldWorktree;
|
|
627
|
+
let heldIndex;
|
|
628
|
+
let targetWorktree;
|
|
629
|
+
let targetIndex;
|
|
630
|
+
const pendingConflicts = [];
|
|
631
|
+
|
|
632
|
+
if (cardResult.pendingCard) {
|
|
633
|
+
const pendingBaseTree = await merge.treeOf(localCard.path, localCard.head.oid);
|
|
634
|
+
const rebasedIndex = await merge.reduceTree(
|
|
635
|
+
localCard.path,
|
|
636
|
+
pendingBaseTree,
|
|
637
|
+
previousHeldIndex,
|
|
638
|
+
reduced.indexTree,
|
|
639
|
+
{ label: `pending-card-index-${appliedThrough ?? 'local'}` },
|
|
640
|
+
);
|
|
641
|
+
const rebasedWorktree = await merge.reduceTree(
|
|
642
|
+
localCard.path,
|
|
643
|
+
pendingBaseTree,
|
|
644
|
+
previousHeldWorktree,
|
|
645
|
+
reduced.worktreeTree,
|
|
646
|
+
{ label: `pending-card-worktree-${appliedThrough ?? 'local'}` },
|
|
647
|
+
);
|
|
648
|
+
heldIndex = pendingBaseTree;
|
|
649
|
+
heldWorktree = pendingBaseTree;
|
|
650
|
+
targetIndex = rebasedIndex.tree;
|
|
651
|
+
targetWorktree = rebasedWorktree.tree;
|
|
652
|
+
stateBaseOid = localCard.head.oid;
|
|
653
|
+
stateBranch = localCard.head.branch;
|
|
654
|
+
stateCardVersion = null;
|
|
655
|
+
stateCardMutationId = null;
|
|
656
|
+
stateIndexDiff = await merge.diffOfTree(localCard.path, stateBaseOid, targetIndex);
|
|
657
|
+
stateWorktreeDiff = await merge.diffOfTree(localCard.path, targetIndex, targetWorktree);
|
|
658
|
+
pendingConflicts.push(
|
|
659
|
+
...rebasedIndex.conflicts.map((item) => ({ ...item, layer: 'pending-card-index' })),
|
|
660
|
+
...rebasedWorktree.conflicts.map((item) => ({ ...item, layer: 'pending-card-worktree' })),
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
const captured = await captureCheckpoint(localCard.path);
|
|
664
|
+
const represented = captureIsOnRail(plan, captured);
|
|
665
|
+
const capturedIndex = represented
|
|
666
|
+
? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
|
|
667
|
+
: null;
|
|
668
|
+
const capturedWorktree = represented
|
|
669
|
+
? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
|
|
670
|
+
: null;
|
|
671
|
+
heldWorktree = capturedWorktree || previousHeldWorktree;
|
|
672
|
+
heldIndex = capturedIndex || previousHeldIndex;
|
|
673
|
+
targetWorktree = reduced.worktreeTree;
|
|
674
|
+
targetIndex = reduced.indexTree;
|
|
675
|
+
}
|
|
676
|
+
|
|
570
677
|
const currentIndex = await merge.indexTree(localCard.path);
|
|
571
678
|
const reducedLocalIndex = await merge.reduceTree(
|
|
572
679
|
localCard.path,
|
|
573
680
|
currentIndex,
|
|
574
681
|
heldIndex,
|
|
575
|
-
|
|
682
|
+
targetIndex,
|
|
576
683
|
{ label: `index-${reduced.checkpointRevision || 'clean'}` },
|
|
577
684
|
);
|
|
578
|
-
const applied = await merge.advance(localCard.path, heldWorktree,
|
|
685
|
+
const applied = await merge.advance(localCard.path, heldWorktree, targetWorktree, {
|
|
579
686
|
label: `checkpoint-${reduced.checkpointRevision || 'clean'}`,
|
|
580
687
|
});
|
|
581
688
|
const indexResult = await advanceIndex(localCard.path, currentIndex, reducedLocalIndex.tree);
|
|
@@ -583,28 +690,30 @@ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
|
|
|
583
690
|
const state = writeRepoState(plan.repoKey, {
|
|
584
691
|
path: localCard.path,
|
|
585
692
|
replica: localCard.replica,
|
|
586
|
-
branch:
|
|
587
|
-
baseOid:
|
|
588
|
-
cardVersion:
|
|
589
|
-
cardMutationId:
|
|
693
|
+
branch: stateBranch,
|
|
694
|
+
baseOid: stateBaseOid,
|
|
695
|
+
cardVersion: stateCardVersion,
|
|
696
|
+
cardMutationId: stateCardMutationId,
|
|
697
|
+
pendingCard: Boolean(cardResult.pendingCard),
|
|
590
698
|
appliedThrough,
|
|
591
|
-
checkpointRevision:
|
|
592
|
-
indexDiff:
|
|
593
|
-
worktreeDiff:
|
|
699
|
+
checkpointRevision: revisionOf(stateBaseOid, stateIndexDiff, stateWorktreeDiff),
|
|
700
|
+
indexDiff: stateIndexDiff,
|
|
701
|
+
worktreeDiff: stateWorktreeDiff,
|
|
594
702
|
checkpointVersions: plan.transitions.map((entry) => entry.version),
|
|
595
703
|
branches: plan.branches || [],
|
|
596
704
|
heldTree: applied.heldTree,
|
|
597
|
-
heldIndexTree:
|
|
705
|
+
heldIndexTree: targetIndex,
|
|
598
706
|
parked,
|
|
599
707
|
parkedWaiting: [...parkedWaiting, ...branchResult.waiting],
|
|
600
708
|
conflicts: [
|
|
601
709
|
...(cardResult.conflicts || []),
|
|
602
710
|
...reduced.conflicts,
|
|
711
|
+
...pendingConflicts,
|
|
603
712
|
...reducedLocalIndex.conflicts.map((item) => ({ ...item, layer: 'index-materialize' })),
|
|
604
713
|
...applied.conflicts,
|
|
605
714
|
],
|
|
606
|
-
materialized: applied.heldTree ===
|
|
607
|
-
&& reducedLocalIndex.tree ===
|
|
715
|
+
materialized: applied.heldTree === targetWorktree
|
|
716
|
+
&& reducedLocalIndex.tree === targetIndex,
|
|
608
717
|
});
|
|
609
718
|
appendStateEvent({
|
|
610
719
|
resource: 'workspace-reducer',
|
|
@@ -17,9 +17,15 @@ 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 {
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const {
|
|
21
|
+
CHECKPOINT_CONTRACT,
|
|
22
|
+
checkpointValue,
|
|
23
|
+
readCheckpoint,
|
|
24
|
+
repoKeyOfCard,
|
|
25
|
+
sameTransition,
|
|
26
|
+
} = require('./checkpoint');
|
|
27
|
+
|
|
28
|
+
const WALL_CONTRACT = 'code-project@4';
|
|
23
29
|
const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects');
|
|
24
30
|
const REGISTRY_FILE = path.join(PROJECT_DIR, 'projects.json');
|
|
25
31
|
const PUBLISH_QUIET_MS = 25;
|
|
@@ -246,7 +252,8 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousC
|
|
|
246
252
|
const onRail = samePosition(card, plan.card);
|
|
247
253
|
const atBaseline = baseline
|
|
248
254
|
&& card.head?.branch === baseline.branch
|
|
249
|
-
&& card.head?.oid === baseline.baseOid
|
|
255
|
+
&& card.head?.oid === baseline.baseOid
|
|
256
|
+
&& !baseline.pendingCard;
|
|
250
257
|
const alreadyPublished = validVersion(previousCard?.positionVersion)
|
|
251
258
|
&& samePosition(card, previousCard);
|
|
252
259
|
const reversesPublishedSharedPosition = previousCard
|
|
@@ -309,6 +316,46 @@ function hasObjectUpgrade(card, plan) {
|
|
|
309
316
|
});
|
|
310
317
|
}
|
|
311
318
|
|
|
319
|
+
function checkpointBaseFor(card, baseline, project, shared = null) {
|
|
320
|
+
if (
|
|
321
|
+
!baseline
|
|
322
|
+
|| baseline.baseOid !== card.head?.oid
|
|
323
|
+
|| baseline.branch !== card.head?.branch
|
|
324
|
+
|| !Number.isInteger(baseline.appliedThrough)
|
|
325
|
+
|| baseline.appliedThrough < 0
|
|
326
|
+
) return null;
|
|
327
|
+
const value = checkpointValue(
|
|
328
|
+
machineId(),
|
|
329
|
+
project.repoKey,
|
|
330
|
+
card.head.oid,
|
|
331
|
+
baseline.indexDiff || '',
|
|
332
|
+
baseline.worktreeDiff || '',
|
|
333
|
+
);
|
|
334
|
+
const reusableThrough = shared
|
|
335
|
+
&& shared.baseOid === card.head.oid
|
|
336
|
+
&& shared.branch === card.head.branch
|
|
337
|
+
&& shared.value?.revision === value.revision
|
|
338
|
+
&& Number.isInteger(shared.through)
|
|
339
|
+
&& shared.through >= 0;
|
|
340
|
+
return {
|
|
341
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
342
|
+
baseOid: card.head.oid,
|
|
343
|
+
branch: card.head.branch,
|
|
344
|
+
through: reusableThrough ? shared.through : baseline.appliedThrough,
|
|
345
|
+
value,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function hasCheckpointBaseUpgrade(card, plan, baseline, project) {
|
|
350
|
+
const shared = plan?.card?.checkpointBase;
|
|
351
|
+
const candidate = checkpointBaseFor(card, baseline, project, shared);
|
|
352
|
+
return Boolean(shared && candidate) && (
|
|
353
|
+
candidate.through === shared.through
|
|
354
|
+
&& candidate.value?.revision === shared.value?.revision
|
|
355
|
+
&& JSON.stringify(candidate.value) !== JSON.stringify(shared.value)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
312
359
|
function validVersion(value) {
|
|
313
360
|
return Number.isInteger(value) && value > 0;
|
|
314
361
|
}
|
|
@@ -443,6 +490,12 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
|
|
|
443
490
|
&& checkpoint.branch === card.head?.branch
|
|
444
491
|
? checkpoint
|
|
445
492
|
: null;
|
|
493
|
+
const checkpointBase = checkpointBaseFor(
|
|
494
|
+
card,
|
|
495
|
+
baseline,
|
|
496
|
+
project,
|
|
497
|
+
sharedPlan?.card?.checkpointBase,
|
|
498
|
+
);
|
|
446
499
|
const sameBase = previous?.head?.oid === card.head?.oid
|
|
447
500
|
&& previous?.head?.branch === card.head?.branch;
|
|
448
501
|
const history = sameBase && Array.isArray(previous?.checkpoints)
|
|
@@ -453,7 +506,10 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
|
|
|
453
506
|
entry.mutationId,
|
|
454
507
|
entry.version,
|
|
455
508
|
),
|
|
456
|
-
}))
|
|
509
|
+
})).filter((entry) => (
|
|
510
|
+
validVersion(entry.version)
|
|
511
|
+
&& (!checkpointBase || entry.version > checkpointBase.through)
|
|
512
|
+
))
|
|
457
513
|
: [];
|
|
458
514
|
const last = history.at(-1);
|
|
459
515
|
const checkpoints = currentCheckpoint
|
|
@@ -473,6 +529,7 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
|
|
|
473
529
|
mutationId,
|
|
474
530
|
project.resourceId,
|
|
475
531
|
),
|
|
532
|
+
...(checkpointBase ? { checkpointBase } : {}),
|
|
476
533
|
...(checkpoints.length ? { checkpoints } : {}),
|
|
477
534
|
};
|
|
478
535
|
});
|
|
@@ -549,7 +606,8 @@ function publishProject(project, rawCards) {
|
|
|
549
606
|
const checkpoint = readCheckpoint(machineId(), project.repoKey);
|
|
550
607
|
const previousCard = previousCards.find((entry) => entry.replica === card.replica);
|
|
551
608
|
return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard)
|
|
552
|
-
|| hasObjectUpgrade(card, sharedPlan)
|
|
609
|
+
|| hasObjectUpgrade(card, sharedPlan)
|
|
610
|
+
|| hasCheckpointBaseUpgrade(card, sharedPlan, baseline, project);
|
|
553
611
|
});
|
|
554
612
|
if (!removed && !contributes) {
|
|
555
613
|
service.counters.publishSkips += 1;
|
|
@@ -618,16 +676,33 @@ function publishProject(project, rawCards) {
|
|
|
618
676
|
};
|
|
619
677
|
}
|
|
620
678
|
|
|
679
|
+
function publishSelected(selected, allCards, publisher = publishProject) {
|
|
680
|
+
return selected.map((project) => {
|
|
681
|
+
try {
|
|
682
|
+
return publisher(
|
|
683
|
+
project,
|
|
684
|
+
allCards.filter((card) => (
|
|
685
|
+
repoKeyOfCard(card) === project.repoKey
|
|
686
|
+
&& isCommittedCard(card)
|
|
687
|
+
)),
|
|
688
|
+
);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
service.counters.publishFailures += 1;
|
|
691
|
+
console.warn(`[CodeProject] ${project.repoKey} publish failed:`, error.message);
|
|
692
|
+
return {
|
|
693
|
+
published: false,
|
|
694
|
+
reason: 'publish-failed',
|
|
695
|
+
repoKey: project.repoKey,
|
|
696
|
+
error: error.message,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
621
702
|
function publishWall(repoKey = null) {
|
|
622
703
|
const allCards = listRepoCards();
|
|
623
704
|
const selected = repoKey ? [projectForRepo(repoKey)].filter(Boolean) : projects();
|
|
624
|
-
const results = selected
|
|
625
|
-
project,
|
|
626
|
-
allCards.filter((card) => (
|
|
627
|
-
repoKeyOfCard(card) === project.repoKey
|
|
628
|
-
&& isCommittedCard(card)
|
|
629
|
-
)),
|
|
630
|
-
));
|
|
705
|
+
const results = publishSelected(selected, allCards);
|
|
631
706
|
if (repoKey) return results[0] || { published: false, reason: 'project-missing', repoKey };
|
|
632
707
|
return {
|
|
633
708
|
published: results.some((result) => result.published),
|
|
@@ -788,6 +863,7 @@ function wallStats() {
|
|
|
788
863
|
}
|
|
789
864
|
|
|
790
865
|
module.exports = {
|
|
866
|
+
_test: { publishSelected },
|
|
791
867
|
WALL_CONTRACT,
|
|
792
868
|
bindProject,
|
|
793
869
|
compactWallText,
|