amalgm 0.1.217 → 0.1.218
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 +50 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +46 -17
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +3 -2
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +51 -1
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +87 -12
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);
|
|
@@ -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,
|
|
@@ -148,7 +149,7 @@ test('only a value different from both the materialized baseline and the rail is
|
|
|
148
149
|
};
|
|
149
150
|
assert.equal(hasLocalContribution(local, remoteAdvance, baseline), false);
|
|
150
151
|
assert.equal(hasLocalContribution(local, remoteAdvance, baseline, {
|
|
151
|
-
checkpoint: 'repo-checkpoint@
|
|
152
|
+
checkpoint: 'repo-checkpoint@8',
|
|
152
153
|
machine: 'machine-a',
|
|
153
154
|
branch: 'main',
|
|
154
155
|
baseOid: oidA,
|
|
@@ -218,7 +219,7 @@ test('only a value different from both the materialized baseline and the rail is
|
|
|
218
219
|
},
|
|
219
220
|
), true);
|
|
220
221
|
assert.equal(hasLocalContribution(local, plan, baseline, {
|
|
221
|
-
checkpoint: 'repo-checkpoint@
|
|
222
|
+
checkpoint: 'repo-checkpoint@8',
|
|
222
223
|
machine: 'machine-a',
|
|
223
224
|
branch: 'main',
|
|
224
225
|
baseOid: oidA,
|
|
@@ -328,7 +329,7 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
328
329
|
assert.equal(first.published, true);
|
|
329
330
|
const entries = readWall(repoKey);
|
|
330
331
|
assert.equal(entries.length, 1);
|
|
331
|
-
assert.equal(entries[0].wall, 'code-project@
|
|
332
|
+
assert.equal(entries[0].wall, 'code-project@4');
|
|
332
333
|
assert.equal(entries[0].project, repoKey);
|
|
333
334
|
assert.equal(entries[0].machine, 'machine-a');
|
|
334
335
|
assert.equal(entries[0].cards.length, 1);
|
|
@@ -384,12 +385,12 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
384
385
|
assert.equal(getDocText(project.file).trim().split('\n').length, 1);
|
|
385
386
|
});
|
|
386
387
|
|
|
387
|
-
test('checkpoint
|
|
388
|
+
test('a checkpoint base replaces history already covered by the applied rail', async () => {
|
|
388
389
|
const card = readWall(repoKey)[0].cards[0];
|
|
389
390
|
const file = checkpointFile('machine-a', repoKey);
|
|
390
391
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
391
392
|
const firstCheckpoint = {
|
|
392
|
-
checkpoint: 'repo-checkpoint@
|
|
393
|
+
checkpoint: 'repo-checkpoint@8',
|
|
393
394
|
machine: 'machine-a',
|
|
394
395
|
repoKey,
|
|
395
396
|
branch: card.head.branch,
|
|
@@ -413,6 +414,20 @@ test('checkpoint transitions append until the next card and carry their official
|
|
|
413
414
|
JSON.parse(checkpointLines.at(-1)).cards[0].checkpoints[0].version ?? null,
|
|
414
415
|
null,
|
|
415
416
|
);
|
|
417
|
+
const reducerFile = path.join(
|
|
418
|
+
process.env.AMALGM_DIR,
|
|
419
|
+
'state',
|
|
420
|
+
'repo-reducer',
|
|
421
|
+
`${crypto.createHash('sha256').update(repoKey).digest('hex').slice(0, 24)}.json`,
|
|
422
|
+
);
|
|
423
|
+
fs.writeFileSync(reducerFile, `${JSON.stringify({
|
|
424
|
+
repoKey,
|
|
425
|
+
baseOid: card.head.oid,
|
|
426
|
+
branch: card.head.branch,
|
|
427
|
+
appliedThrough: 2,
|
|
428
|
+
indexDiff: '',
|
|
429
|
+
worktreeDiff: 'patch-a',
|
|
430
|
+
})}\n`);
|
|
416
431
|
|
|
417
432
|
fs.writeFileSync(file, `${JSON.stringify({
|
|
418
433
|
...firstCheckpoint,
|
|
@@ -422,12 +437,26 @@ test('checkpoint transitions append until the next card and carry their official
|
|
|
422
437
|
})}\n`);
|
|
423
438
|
const second = publishWall(repoKey);
|
|
424
439
|
assert.equal(second.published, true);
|
|
425
|
-
const
|
|
426
|
-
assert.equal(
|
|
427
|
-
assert.
|
|
440
|
+
const latestCard = readWall(repoKey)[0].cards[0];
|
|
441
|
+
assert.equal(latestCard.checkpointBase.through, 2);
|
|
442
|
+
assert.equal(latestCard.checkpoints.length, 1);
|
|
443
|
+
assert.deepEqual(latestCard.checkpoints.map((entry) => entry.mutationId), [second.mutationId]);
|
|
428
444
|
assert.deepEqual(settleProject(), [3]);
|
|
429
445
|
});
|
|
430
446
|
|
|
447
|
+
test('one failed project publisher cannot block the next repo', () => {
|
|
448
|
+
const selected = [{ repoKey: 'root:first' }, { repoKey: 'root:second' }];
|
|
449
|
+
const calls = [];
|
|
450
|
+
const results = wallTest.publishSelected(selected, [], (selectedProject) => {
|
|
451
|
+
calls.push(selectedProject.repoKey);
|
|
452
|
+
if (selectedProject.repoKey === 'root:first') throw new Error('too large');
|
|
453
|
+
return { published: true, repoKey: selectedProject.repoKey };
|
|
454
|
+
});
|
|
455
|
+
assert.deepEqual(calls, ['root:first', 'root:second']);
|
|
456
|
+
assert.equal(results[0].reason, 'publish-failed');
|
|
457
|
+
assert.equal(results[1].published, true);
|
|
458
|
+
});
|
|
459
|
+
|
|
431
460
|
test('side branches keep one rail operation while the checked-out card advances', async () => {
|
|
432
461
|
const repo = path.join(tempRoot, 'projects/app');
|
|
433
462
|
const before = readWall(repoKey)[0].cards[0];
|
|
@@ -475,7 +504,7 @@ test('side branches keep one rail operation while the checked-out card advances'
|
|
|
475
504
|
test('peer records and earlier local history survive later publishes', async () => {
|
|
476
505
|
const beforeLines = getDocText(project.file).trim().split('\n');
|
|
477
506
|
const peerLine = JSON.stringify({
|
|
478
|
-
wall: 'code-project@
|
|
507
|
+
wall: 'code-project@4',
|
|
479
508
|
project: repoKey,
|
|
480
509
|
machine: 'machine-b',
|
|
481
510
|
mutationId: 'mut_project_peer',
|
|
@@ -532,7 +561,7 @@ test('three concurrent append records stay whole under Yjs ordering', () => {
|
|
|
532
561
|
|
|
533
562
|
test('wall compaction deletes only records superseded for every machine', () => {
|
|
534
563
|
const line = (machine, value) => JSON.stringify({
|
|
535
|
-
wall: 'code-project@
|
|
564
|
+
wall: 'code-project@4',
|
|
536
565
|
project: repoKey,
|
|
537
566
|
machine,
|
|
538
567
|
mutationId: `mut_project_${machine}_${value}`,
|
|
@@ -541,7 +570,7 @@ test('wall compaction deletes only records superseded for every machine', () =>
|
|
|
541
570
|
const oldA = line('machine-a', 'old-a');
|
|
542
571
|
const oldB = line('machine-b', 'old-b');
|
|
543
572
|
const latestA = line('machine-a', 'latest-a');
|
|
544
|
-
const torn = '{"wall":"code-project@
|
|
573
|
+
const torn = '{"wall":"code-project@4","machine":"torn"';
|
|
545
574
|
const latestB = line('machine-b', 'latest-b');
|
|
546
575
|
const current = `${oldA}\n${oldB}\n${latestA}\n${torn}\n${latestB}\n`;
|
|
547
576
|
const compacted = compactWallText(current, repoKey);
|
|
@@ -553,13 +582,13 @@ test('wall compaction deletes only records superseded for every machine', () =>
|
|
|
553
582
|
|
|
554
583
|
test('an unordered legacy record cannot pin wall compaction', () => {
|
|
555
584
|
const unordered = JSON.stringify({
|
|
556
|
-
wall: 'code-project@
|
|
585
|
+
wall: 'code-project@4',
|
|
557
586
|
project: repoKey,
|
|
558
587
|
machine: '16.612Z',
|
|
559
588
|
cards: [{ value: 'corrupted-legacy-record' }],
|
|
560
589
|
});
|
|
561
590
|
const ordered = (machine) => JSON.stringify({
|
|
562
|
-
wall: 'code-project@
|
|
591
|
+
wall: 'code-project@4',
|
|
563
592
|
project: repoKey,
|
|
564
593
|
machine,
|
|
565
594
|
mutationId: `mut_project_${machine}`,
|
|
@@ -576,7 +605,7 @@ test('an unordered legacy record cannot pin wall compaction', () => {
|
|
|
576
605
|
|
|
577
606
|
test('retired machine values stay represented without pinning obsolete active history', () => {
|
|
578
607
|
const line = (machine, value) => JSON.stringify({
|
|
579
|
-
wall: 'code-project@
|
|
608
|
+
wall: 'code-project@4',
|
|
580
609
|
project: repoKey,
|
|
581
610
|
machine,
|
|
582
611
|
mutationId: `mut_project_${machine}_${value}`,
|
|
@@ -601,7 +630,7 @@ test('retired machine values stay represented without pinning obsolete active hi
|
|
|
601
630
|
test('a concurrent deletion-only compaction and append commute in Yjs', () => {
|
|
602
631
|
const Y = require('yjs');
|
|
603
632
|
const line = (machine, value) => JSON.stringify({
|
|
604
|
-
wall: 'code-project@
|
|
633
|
+
wall: 'code-project@4',
|
|
605
634
|
project: repoKey,
|
|
606
635
|
machine,
|
|
607
636
|
mutationId: `mut_project_${machine}_${value}`,
|
|
@@ -654,7 +683,7 @@ test('a concurrent deletion-only compaction and append commute in Yjs', () => {
|
|
|
654
683
|
test('a torn line is ignored by readers, never fatal', () => {
|
|
655
684
|
writeDocText(
|
|
656
685
|
project.file,
|
|
657
|
-
`${getDocText(project.file)}{"wall":"code-project@
|
|
686
|
+
`${getDocText(project.file)}{"wall":"code-project@4","project":"${repoKey}","machine":"machine-c","car\n`,
|
|
658
687
|
);
|
|
659
688
|
settleProject();
|
|
660
689
|
assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
|
|
@@ -682,7 +711,7 @@ test('publishing compacts an oversized history before appending the new record',
|
|
|
682
711
|
test('project stats name every project resource, binding, and publish counter', () => {
|
|
683
712
|
const stats = wallStats();
|
|
684
713
|
const row = stats.projects.find((entry) => entry.repoKey === repoKey);
|
|
685
|
-
assert.equal(stats.contract, 'code-project@
|
|
714
|
+
assert.equal(stats.contract, 'code-project@4');
|
|
686
715
|
assert.equal(row.resourceId, project.resourceId);
|
|
687
716
|
assert.equal(row.replica.appliedVersion >= 0, true);
|
|
688
717
|
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
|
}
|
|
@@ -433,6 +468,21 @@ async function reduceCheckpoint(localCard, plan) {
|
|
|
433
468
|
let indexTree = baseTree;
|
|
434
469
|
let worktreeTree = baseTree;
|
|
435
470
|
const conflicts = [];
|
|
471
|
+
if (plan.checkpointBaseConflict) {
|
|
472
|
+
return { state: 'waiting', reason: 'checkpoint-base-disagrees' };
|
|
473
|
+
}
|
|
474
|
+
if (plan.checkpointBase) {
|
|
475
|
+
const snapshot = readCheckpointValue(baseOid, plan.checkpointBase.value);
|
|
476
|
+
if (snapshot.state !== 'ready') {
|
|
477
|
+
return { state: 'waiting', reason: `checkpoint-base-${snapshot.state}` };
|
|
478
|
+
}
|
|
479
|
+
indexTree = await merge.treeOfPatch(localCard.path, baseOid, snapshot.indexDiff);
|
|
480
|
+
worktreeTree = await merge.treeOfPatch(
|
|
481
|
+
localCard.path,
|
|
482
|
+
indexTree,
|
|
483
|
+
snapshot.worktreeDiff,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
436
486
|
|
|
437
487
|
for (const entry of plan.parked || []) {
|
|
438
488
|
const commonOid = await mergeBase(localCard.path, baseOid, entry.oid);
|
|
@@ -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;
|
|
@@ -309,6 +315,46 @@ function hasObjectUpgrade(card, plan) {
|
|
|
309
315
|
});
|
|
310
316
|
}
|
|
311
317
|
|
|
318
|
+
function checkpointBaseFor(card, baseline, project, shared = null) {
|
|
319
|
+
if (
|
|
320
|
+
!baseline
|
|
321
|
+
|| baseline.baseOid !== card.head?.oid
|
|
322
|
+
|| baseline.branch !== card.head?.branch
|
|
323
|
+
|| !Number.isInteger(baseline.appliedThrough)
|
|
324
|
+
|| baseline.appliedThrough < 0
|
|
325
|
+
) return null;
|
|
326
|
+
const value = checkpointValue(
|
|
327
|
+
machineId(),
|
|
328
|
+
project.repoKey,
|
|
329
|
+
card.head.oid,
|
|
330
|
+
baseline.indexDiff || '',
|
|
331
|
+
baseline.worktreeDiff || '',
|
|
332
|
+
);
|
|
333
|
+
const reusableThrough = shared
|
|
334
|
+
&& shared.baseOid === card.head.oid
|
|
335
|
+
&& shared.branch === card.head.branch
|
|
336
|
+
&& shared.value?.revision === value.revision
|
|
337
|
+
&& Number.isInteger(shared.through)
|
|
338
|
+
&& shared.through >= 0;
|
|
339
|
+
return {
|
|
340
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
341
|
+
baseOid: card.head.oid,
|
|
342
|
+
branch: card.head.branch,
|
|
343
|
+
through: reusableThrough ? shared.through : baseline.appliedThrough,
|
|
344
|
+
value,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function hasCheckpointBaseUpgrade(card, plan, baseline, project) {
|
|
349
|
+
const shared = plan?.card?.checkpointBase;
|
|
350
|
+
const candidate = checkpointBaseFor(card, baseline, project, shared);
|
|
351
|
+
return Boolean(shared && candidate) && (
|
|
352
|
+
candidate.through === shared.through
|
|
353
|
+
&& candidate.value?.revision === shared.value?.revision
|
|
354
|
+
&& JSON.stringify(candidate.value) !== JSON.stringify(shared.value)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
312
358
|
function validVersion(value) {
|
|
313
359
|
return Number.isInteger(value) && value > 0;
|
|
314
360
|
}
|
|
@@ -443,6 +489,12 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
|
|
|
443
489
|
&& checkpoint.branch === card.head?.branch
|
|
444
490
|
? checkpoint
|
|
445
491
|
: null;
|
|
492
|
+
const checkpointBase = checkpointBaseFor(
|
|
493
|
+
card,
|
|
494
|
+
baseline,
|
|
495
|
+
project,
|
|
496
|
+
sharedPlan?.card?.checkpointBase,
|
|
497
|
+
);
|
|
446
498
|
const sameBase = previous?.head?.oid === card.head?.oid
|
|
447
499
|
&& previous?.head?.branch === card.head?.branch;
|
|
448
500
|
const history = sameBase && Array.isArray(previous?.checkpoints)
|
|
@@ -453,7 +505,10 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
|
|
|
453
505
|
entry.mutationId,
|
|
454
506
|
entry.version,
|
|
455
507
|
),
|
|
456
|
-
}))
|
|
508
|
+
})).filter((entry) => (
|
|
509
|
+
validVersion(entry.version)
|
|
510
|
+
&& (!checkpointBase || entry.version > checkpointBase.through)
|
|
511
|
+
))
|
|
457
512
|
: [];
|
|
458
513
|
const last = history.at(-1);
|
|
459
514
|
const checkpoints = currentCheckpoint
|
|
@@ -473,6 +528,7 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
|
|
|
473
528
|
mutationId,
|
|
474
529
|
project.resourceId,
|
|
475
530
|
),
|
|
531
|
+
...(checkpointBase ? { checkpointBase } : {}),
|
|
476
532
|
...(checkpoints.length ? { checkpoints } : {}),
|
|
477
533
|
};
|
|
478
534
|
});
|
|
@@ -549,7 +605,8 @@ function publishProject(project, rawCards) {
|
|
|
549
605
|
const checkpoint = readCheckpoint(machineId(), project.repoKey);
|
|
550
606
|
const previousCard = previousCards.find((entry) => entry.replica === card.replica);
|
|
551
607
|
return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard)
|
|
552
|
-
|| hasObjectUpgrade(card, sharedPlan)
|
|
608
|
+
|| hasObjectUpgrade(card, sharedPlan)
|
|
609
|
+
|| hasCheckpointBaseUpgrade(card, sharedPlan, baseline, project);
|
|
553
610
|
});
|
|
554
611
|
if (!removed && !contributes) {
|
|
555
612
|
service.counters.publishSkips += 1;
|
|
@@ -618,16 +675,33 @@ function publishProject(project, rawCards) {
|
|
|
618
675
|
};
|
|
619
676
|
}
|
|
620
677
|
|
|
678
|
+
function publishSelected(selected, allCards, publisher = publishProject) {
|
|
679
|
+
return selected.map((project) => {
|
|
680
|
+
try {
|
|
681
|
+
return publisher(
|
|
682
|
+
project,
|
|
683
|
+
allCards.filter((card) => (
|
|
684
|
+
repoKeyOfCard(card) === project.repoKey
|
|
685
|
+
&& isCommittedCard(card)
|
|
686
|
+
)),
|
|
687
|
+
);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
service.counters.publishFailures += 1;
|
|
690
|
+
console.warn(`[CodeProject] ${project.repoKey} publish failed:`, error.message);
|
|
691
|
+
return {
|
|
692
|
+
published: false,
|
|
693
|
+
reason: 'publish-failed',
|
|
694
|
+
repoKey: project.repoKey,
|
|
695
|
+
error: error.message,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
621
701
|
function publishWall(repoKey = null) {
|
|
622
702
|
const allCards = listRepoCards();
|
|
623
703
|
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
|
-
));
|
|
704
|
+
const results = publishSelected(selected, allCards);
|
|
631
705
|
if (repoKey) return results[0] || { published: false, reason: 'project-missing', repoKey };
|
|
632
706
|
return {
|
|
633
707
|
published: results.some((result) => result.published),
|
|
@@ -788,6 +862,7 @@ function wallStats() {
|
|
|
788
862
|
}
|
|
789
863
|
|
|
790
864
|
module.exports = {
|
|
865
|
+
_test: { publishSelected },
|
|
791
866
|
WALL_CONTRACT,
|
|
792
867
|
bindProject,
|
|
793
868
|
compactWallText,
|