amalgm 0.1.221 → 0.1.234
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/state/attachments.js +7 -1
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +39 -2
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +180 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +47 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +28 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +152 -12
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +6 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +105 -67
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +183 -23
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +99 -206
- package/runtime/scripts/amalgm-mcp/workspace/card.js +30 -9
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +3 -2
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +194 -20
- package/runtime/scripts/amalgm-mcp/workspace/git.js +23 -5
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +24 -186
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +13 -1
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +323 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +115 -101
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +106 -137
|
@@ -28,16 +28,16 @@ const { appendStateEvent } = require('../state/events');
|
|
|
28
28
|
const {
|
|
29
29
|
acknowledgeCloudMutation,
|
|
30
30
|
listPendingCloudMutations,
|
|
31
|
+
pruneMutationJournal,
|
|
31
32
|
} = require('../state/mutations');
|
|
32
33
|
const { registerReplica } = require('../state/replicas');
|
|
33
|
-
const { openDoc, getDocText, writeDocText } = require('../state/docs');
|
|
34
34
|
const { upsertWorkspace } = require('../workspace/store');
|
|
35
35
|
const { refreshRepoCards } = require('../workspace/cards');
|
|
36
36
|
const { checkpointFile, repoKeyOfCard } = require('../workspace/checkpoint');
|
|
37
|
+
const projectStream = require('../workspace/project-stream');
|
|
37
38
|
const {
|
|
38
39
|
_test: wallTest,
|
|
39
40
|
bindProject,
|
|
40
|
-
compactWallText,
|
|
41
41
|
hasLocalContribution,
|
|
42
42
|
isCommittedCard,
|
|
43
43
|
projectForRepo,
|
|
@@ -105,6 +105,24 @@ test('no commit means no shared card', () => {
|
|
|
105
105
|
assert.equal(isCommittedCard({ head: { branch: 'main', oid: 'a'.repeat(40) } }), true);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
test('transport metadata alone does not make a new code record', () => {
|
|
109
|
+
const base = {
|
|
110
|
+
wall: 'code-project@6',
|
|
111
|
+
project: 'root:ordered',
|
|
112
|
+
machine: 'machine-a',
|
|
113
|
+
mutationId: 'mut_older_transport',
|
|
114
|
+
version: 10,
|
|
115
|
+
publishedAt: '2026-07-26T00:00:00.000Z',
|
|
116
|
+
cards: [{ marker: 'same-content' }],
|
|
117
|
+
};
|
|
118
|
+
assert.equal(wallTest.samePublishedRecord(base, {
|
|
119
|
+
...base,
|
|
120
|
+
mutationId: 'mut_newer_transport',
|
|
121
|
+
version: 11,
|
|
122
|
+
publishedAt: '2026-07-26T00:01:00.000Z',
|
|
123
|
+
}), true);
|
|
124
|
+
});
|
|
125
|
+
|
|
108
126
|
test('only a value different from both the materialized baseline and the rail is new work', () => {
|
|
109
127
|
const oidA = 'a'.repeat(40);
|
|
110
128
|
const oidB = 'b'.repeat(40);
|
|
@@ -158,7 +176,7 @@ test('only a value different from both the materialized baseline and the rail is
|
|
|
158
176
|
pendingCard: true,
|
|
159
177
|
}), true);
|
|
160
178
|
assert.equal(hasLocalContribution(local, remoteAdvance, baseline, {
|
|
161
|
-
checkpoint: 'repo-checkpoint@
|
|
179
|
+
checkpoint: 'repo-checkpoint@9',
|
|
162
180
|
machine: 'machine-a',
|
|
163
181
|
branch: 'main',
|
|
164
182
|
baseOid: oidA,
|
|
@@ -228,7 +246,7 @@ test('only a value different from both the materialized baseline and the rail is
|
|
|
228
246
|
},
|
|
229
247
|
), true);
|
|
230
248
|
assert.equal(hasLocalContribution(local, plan, baseline, {
|
|
231
|
-
checkpoint: 'repo-checkpoint@
|
|
249
|
+
checkpoint: 'repo-checkpoint@9',
|
|
232
250
|
machine: 'machine-a',
|
|
233
251
|
branch: 'main',
|
|
234
252
|
baseOid: oidA,
|
|
@@ -322,14 +340,12 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
322
340
|
repoKey = repoKeyOfCard(refreshed.cards[0]);
|
|
323
341
|
project = projectForRepo(repoKey);
|
|
324
342
|
|
|
325
|
-
|
|
326
|
-
fs.writeFileSync(project.file, '');
|
|
327
|
-
const opened = openDoc(project.file);
|
|
343
|
+
projectStream.writeSnapshot(project.file, projectStream._test.emptySnapshot(repoKey));
|
|
328
344
|
registerReplica({
|
|
329
345
|
resourceId: project.resourceId,
|
|
330
|
-
localResource:
|
|
346
|
+
localResource: `${projectStream.LOCAL_RESOURCE_PREFIX}${project.resourceId}`,
|
|
331
347
|
localPath: project.file,
|
|
332
|
-
contract:
|
|
348
|
+
contract: projectStream.STREAM_CONTRACT,
|
|
333
349
|
schemaVersion: 1,
|
|
334
350
|
authorityEpoch: 1,
|
|
335
351
|
});
|
|
@@ -338,7 +354,7 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
338
354
|
assert.equal(first.published, true);
|
|
339
355
|
const entries = readWall(repoKey);
|
|
340
356
|
assert.equal(entries.length, 1);
|
|
341
|
-
assert.equal(entries[0].wall, 'code-project@
|
|
357
|
+
assert.equal(entries[0].wall, 'code-project@6');
|
|
342
358
|
assert.equal(entries[0].project, repoKey);
|
|
343
359
|
assert.equal(entries[0].machine, 'machine-a');
|
|
344
360
|
assert.equal(entries[0].cards.length, 1);
|
|
@@ -361,12 +377,10 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
361
377
|
const sealedCard = readWall(repoKey)[0].cards[0];
|
|
362
378
|
assert.equal(sealedCard.positionMutationId, first.mutationId);
|
|
363
379
|
assert.equal(sealedCard.positionVersion, 1);
|
|
364
|
-
const
|
|
365
|
-
assert.equal(
|
|
366
|
-
assert.equal(
|
|
367
|
-
|
|
368
|
-
null,
|
|
369
|
-
);
|
|
380
|
+
const sealedSnapshot = projectStream.readSnapshot(project.file, repoKey);
|
|
381
|
+
assert.equal(sealedSnapshot.records.length, 1);
|
|
382
|
+
assert.equal(sealedSnapshot.records[0].version, 1);
|
|
383
|
+
assert.equal(sealedSnapshot.records[0].cards[0].positionVersion ?? null, null);
|
|
370
384
|
assert.equal(publishedCardFor(refreshed.cards[0]).positionVersion, 1);
|
|
371
385
|
const readyObjects = {
|
|
372
386
|
...refreshed.cards[0],
|
|
@@ -376,7 +390,11 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
376
390
|
objects: { kind: 'bundle' },
|
|
377
391
|
})),
|
|
378
392
|
};
|
|
379
|
-
assert.equal(
|
|
393
|
+
assert.equal(
|
|
394
|
+
publishedCardFor(readyObjects).positionVersion,
|
|
395
|
+
1,
|
|
396
|
+
'transport readiness does not turn an accepted card into new Git work',
|
|
397
|
+
);
|
|
380
398
|
|
|
381
399
|
const again = publishWall(repoKey);
|
|
382
400
|
assert.equal(again.published, false);
|
|
@@ -391,7 +409,7 @@ test('a repo project appends immutable machine records', async () => {
|
|
|
391
409
|
assert.equal(Object.hasOwn(after[0].cards[0], 'dirty'), false);
|
|
392
410
|
assert.equal(after[0].cards[0].positionMutationId, first.mutationId);
|
|
393
411
|
assert.equal(after[0].cards[0].positionVersion, 1);
|
|
394
|
-
assert.equal(
|
|
412
|
+
assert.equal(projectStream.readSnapshot(project.file, repoKey).records.length, 1);
|
|
395
413
|
});
|
|
396
414
|
|
|
397
415
|
test('a checkpoint base replaces history already covered by the applied rail', async () => {
|
|
@@ -399,7 +417,7 @@ test('a checkpoint base replaces history already covered by the applied rail', a
|
|
|
399
417
|
const file = checkpointFile('machine-a', repoKey);
|
|
400
418
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
401
419
|
const firstCheckpoint = {
|
|
402
|
-
checkpoint: 'repo-checkpoint@
|
|
420
|
+
checkpoint: 'repo-checkpoint@9',
|
|
403
421
|
machine: 'machine-a',
|
|
404
422
|
repoKey,
|
|
405
423
|
branch: card.head.branch,
|
|
@@ -418,9 +436,10 @@ test('a checkpoint base replaces history already covered by the applied rail', a
|
|
|
418
436
|
assert.deepEqual(settleProject(), [2]);
|
|
419
437
|
assert.equal(readWall(repoKey)[0].cards[0].checkpoints[0].version, 2);
|
|
420
438
|
assert.equal(publishWall(repoKey).published, false);
|
|
421
|
-
const
|
|
439
|
+
const checkpointSnapshot = projectStream.readSnapshot(project.file, repoKey);
|
|
440
|
+
assert.equal(checkpointSnapshot.records[0].version, 2);
|
|
422
441
|
assert.equal(
|
|
423
|
-
|
|
442
|
+
checkpointSnapshot.records[0].cards[0].checkpoints[0].version ?? null,
|
|
424
443
|
null,
|
|
425
444
|
);
|
|
426
445
|
const reducerFile = path.join(
|
|
@@ -487,14 +506,14 @@ test('side branches keep one rail operation while the checked-out card advances'
|
|
|
487
506
|
|
|
488
507
|
await run(repo, 'branch', branch);
|
|
489
508
|
await refreshRepoCards();
|
|
490
|
-
const beforeMaterialization =
|
|
509
|
+
const beforeMaterialization = fs.readFileSync(project.file, 'utf8');
|
|
491
510
|
const blocked = await require('../workspace/reducer').withMaterialization(
|
|
492
511
|
repoKey,
|
|
493
512
|
async () => publishWall(repoKey),
|
|
494
513
|
);
|
|
495
514
|
assert.equal(blocked.published, false);
|
|
496
515
|
assert.equal(blocked.reason, 'repo-materializing');
|
|
497
|
-
assert.equal(
|
|
516
|
+
assert.equal(fs.readFileSync(project.file, 'utf8'), beforeMaterialization);
|
|
498
517
|
|
|
499
518
|
const created = publishWall(repoKey);
|
|
500
519
|
assert.equal(created.published, true);
|
|
@@ -524,18 +543,39 @@ test('side branches keep one rail operation while the checked-out card advances'
|
|
|
524
543
|
assert.deepEqual(settleProject(), [6]);
|
|
525
544
|
});
|
|
526
545
|
|
|
527
|
-
test('
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
546
|
+
test('an inbound numbered record and a later local publish keep one record per machine', async () => {
|
|
547
|
+
const peerMutationId = 'mut_project_peer_0001';
|
|
548
|
+
projectStream.applyCloudMutation({
|
|
549
|
+
version: 7,
|
|
550
|
+
committedAt: '2026-07-26T12:00:07.000Z',
|
|
551
|
+
envelope: {
|
|
552
|
+
resourceId: project.resourceId,
|
|
553
|
+
authorityEpoch: 1,
|
|
554
|
+
mutationId: peerMutationId,
|
|
555
|
+
deviceId: 'machine-b',
|
|
556
|
+
baseVersion: 6,
|
|
557
|
+
contract: projectStream.STREAM_CONTRACT,
|
|
558
|
+
schemaVersion: 1,
|
|
559
|
+
operationKind: 'code.publish',
|
|
560
|
+
operation: {
|
|
561
|
+
kind: 'code.publish',
|
|
562
|
+
record: {
|
|
563
|
+
wall: 'code-project@6',
|
|
564
|
+
project: repoKey,
|
|
565
|
+
machine: 'machine-b',
|
|
566
|
+
mutationId: peerMutationId,
|
|
567
|
+
publishedAt: '2026-07-26T12:00:07.000Z',
|
|
568
|
+
cards: [],
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
536
572
|
});
|
|
537
|
-
|
|
538
|
-
|
|
573
|
+
nextCloudVersion = 8;
|
|
574
|
+
markReducerApplied(7);
|
|
575
|
+
assert.deepEqual(
|
|
576
|
+
readWall(repoKey).map((entry) => [entry.machine, entry.version]),
|
|
577
|
+
[['machine-a', 6], ['machine-b', 7]],
|
|
578
|
+
);
|
|
539
579
|
|
|
540
580
|
const repo = path.join(tempRoot, 'projects/app');
|
|
541
581
|
fs.writeFileSync(path.join(repo, 'another.txt'), 'y\n');
|
|
@@ -544,197 +584,50 @@ test('peer records and earlier local history survive later publishes', async ()
|
|
|
544
584
|
await refreshRepoCards();
|
|
545
585
|
const result = publishWall(repoKey);
|
|
546
586
|
assert.equal(result.published, true);
|
|
547
|
-
|
|
548
|
-
const lines = getDocText(project.file).trim().split('\n');
|
|
549
|
-
assert.equal(lines.length, beforeLines.length + 2);
|
|
550
|
-
for (const line of beforeLines) assert.ok(lines.includes(line));
|
|
551
|
-
assert.ok(lines.includes(peerLine));
|
|
552
587
|
assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
|
|
553
588
|
assert.deepEqual(settleProject(), [8]);
|
|
589
|
+
assert.equal(projectStream.readSnapshot(project.file, repoKey).records.length, 2);
|
|
554
590
|
});
|
|
555
591
|
|
|
556
|
-
test('
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const baseUpdate = Y.encodeStateAsUpdate(base);
|
|
561
|
-
const updates = ['machine-a', 'machine-b', 'machine-c'].map((machine) => {
|
|
562
|
-
const writer = new Y.Doc();
|
|
563
|
-
Y.applyUpdate(writer, baseUpdate);
|
|
564
|
-
let update = null;
|
|
565
|
-
writer.on('update', (value) => {
|
|
566
|
-
update = value;
|
|
567
|
-
});
|
|
568
|
-
const text = writer.getText('content');
|
|
569
|
-
text.insert(text.length, `${JSON.stringify({ machine })}\n`);
|
|
570
|
-
return update;
|
|
571
|
-
});
|
|
572
|
-
const replicas = [updates, [...updates].reverse()].map((order) => {
|
|
573
|
-
const replica = new Y.Doc();
|
|
574
|
-
Y.applyUpdate(replica, baseUpdate);
|
|
575
|
-
for (const update of order) Y.applyUpdate(replica, update);
|
|
576
|
-
return replica.getText('content').toString();
|
|
577
|
-
});
|
|
578
|
-
assert.equal(replicas[0], replicas[1]);
|
|
579
|
-
assert.deepEqual(
|
|
580
|
-
replicas[0].trim().split('\n').map((line) => JSON.parse(line).machine).sort(),
|
|
581
|
-
['existing', 'machine-a', 'machine-b', 'machine-c'],
|
|
582
|
-
);
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
test('wall compaction deletes only records superseded for every machine', () => {
|
|
586
|
-
const line = (machine, value) => JSON.stringify({
|
|
587
|
-
wall: 'code-project@4',
|
|
588
|
-
project: repoKey,
|
|
589
|
-
machine,
|
|
590
|
-
mutationId: `mut_project_${machine}_${value}`,
|
|
591
|
-
cards: [{ value }],
|
|
592
|
-
});
|
|
593
|
-
const oldA = line('machine-a', 'old-a');
|
|
594
|
-
const oldB = line('machine-b', 'old-b');
|
|
595
|
-
const latestA = line('machine-a', 'latest-a');
|
|
596
|
-
const torn = '{"wall":"code-project@4","machine":"torn"';
|
|
597
|
-
const latestB = line('machine-b', 'latest-b');
|
|
598
|
-
const current = `${oldA}\n${oldB}\n${latestA}\n${torn}\n${latestB}\n`;
|
|
599
|
-
const compacted = compactWallText(current, repoKey);
|
|
600
|
-
|
|
601
|
-
assert.equal(compacted, `${latestA}\n${torn}\n${latestB}\n`);
|
|
602
|
-
assert.equal(compacted.includes(oldA), false);
|
|
603
|
-
assert.equal(compacted.includes(oldB), false);
|
|
604
|
-
});
|
|
592
|
+
test('official code order survives mutation-journal pruning', () => {
|
|
593
|
+
const recordBeforePrune = readWall(repoKey).find((entry) => entry.machine === 'machine-a');
|
|
594
|
+
assert.equal(recordBeforePrune.version, 8);
|
|
595
|
+
assert.equal(recordBeforePrune.cards[0].positionVersion, 8);
|
|
605
596
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
project: repoKey,
|
|
610
|
-
machine: '16.612Z',
|
|
611
|
-
cards: [{ value: 'corrupted-legacy-record' }],
|
|
612
|
-
});
|
|
613
|
-
const ordered = (machine) => JSON.stringify({
|
|
614
|
-
wall: 'code-project@4',
|
|
615
|
-
project: repoKey,
|
|
616
|
-
machine,
|
|
617
|
-
mutationId: `mut_project_${machine}`,
|
|
618
|
-
cards: [{ value: machine }],
|
|
597
|
+
const pruned = pruneMutationJournal({
|
|
598
|
+
cutoffIso: '2100-01-01T00:00:00.000Z',
|
|
599
|
+
batchSize: 10_000,
|
|
619
600
|
});
|
|
620
|
-
|
|
621
|
-
const latestB = ordered('machine-b');
|
|
622
|
-
|
|
601
|
+
assert.ok(pruned.deleted >= 8);
|
|
623
602
|
assert.equal(
|
|
624
|
-
|
|
625
|
-
|
|
603
|
+
require('../state/mutations').getMutation(
|
|
604
|
+
`resource:${project.resourceId}`,
|
|
605
|
+
recordBeforePrune.mutationId,
|
|
606
|
+
),
|
|
607
|
+
null,
|
|
626
608
|
);
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
test('retired machine values stay represented without pinning obsolete active history', () => {
|
|
630
|
-
const line = (machine, value) => JSON.stringify({
|
|
631
|
-
wall: 'code-project@4',
|
|
632
|
-
project: repoKey,
|
|
633
|
-
machine,
|
|
634
|
-
mutationId: `mut_project_${machine}_${value}`,
|
|
635
|
-
cards: [{ value }],
|
|
636
|
-
});
|
|
637
|
-
const retiredA = line('retired-a', 'latest-retired-a');
|
|
638
|
-
const retiredB = line('retired-b', 'latest-retired-b');
|
|
639
|
-
const oldA = line('machine-a', 'old-a');
|
|
640
|
-
const oldB = line('machine-b', 'old-b');
|
|
641
|
-
const latestA = line('machine-a', 'latest-a');
|
|
642
|
-
const latestB = line('machine-b', 'latest-b');
|
|
643
609
|
|
|
610
|
+
const durable = readWall(repoKey).find((entry) => entry.machine === 'machine-a');
|
|
611
|
+
assert.equal(durable.version, 8);
|
|
612
|
+
assert.equal(durable.cards[0].positionVersion, 8);
|
|
644
613
|
assert.equal(
|
|
645
|
-
|
|
646
|
-
|
|
614
|
+
require('../workspace/wall').mutationVersion(
|
|
615
|
+
project.resourceId,
|
|
616
|
+
durable.mutationId,
|
|
647
617
|
repoKey,
|
|
648
618
|
),
|
|
649
|
-
|
|
650
|
-
);
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test('a concurrent deletion-only compaction and append commute in Yjs', () => {
|
|
654
|
-
const Y = require('yjs');
|
|
655
|
-
const line = (machine, value) => JSON.stringify({
|
|
656
|
-
wall: 'code-project@4',
|
|
657
|
-
project: repoKey,
|
|
658
|
-
machine,
|
|
659
|
-
mutationId: `mut_project_${machine}_${value}`,
|
|
660
|
-
cards: [{ value }],
|
|
661
|
-
});
|
|
662
|
-
const oldA = line('machine-a', 'old-a');
|
|
663
|
-
const oldB = line('machine-b', 'old-b');
|
|
664
|
-
const latestA = line('machine-a', 'latest-a');
|
|
665
|
-
const latestB = line('machine-b', 'latest-b');
|
|
666
|
-
const baseText = `${oldA}\n${oldB}\n${latestA}\n${latestB}\n`;
|
|
667
|
-
const compacted = compactWallText(baseText, repoKey);
|
|
668
|
-
const base = new Y.Doc();
|
|
669
|
-
base.getText('content').insert(0, baseText);
|
|
670
|
-
const baseUpdate = Y.encodeStateAsUpdate(base);
|
|
671
|
-
|
|
672
|
-
const writerUpdate = (change) => {
|
|
673
|
-
const writer = new Y.Doc();
|
|
674
|
-
Y.applyUpdate(writer, baseUpdate);
|
|
675
|
-
let update = null;
|
|
676
|
-
writer.on('update', (value) => {
|
|
677
|
-
update = value;
|
|
678
|
-
});
|
|
679
|
-
change(writer.getText('content'));
|
|
680
|
-
return update;
|
|
681
|
-
};
|
|
682
|
-
const compactUpdate = writerUpdate((text) => {
|
|
683
|
-
text.delete(0, baseText.length - compacted.length);
|
|
684
|
-
});
|
|
685
|
-
const appendLine = `${line('machine-c', 'latest-c')}\n`;
|
|
686
|
-
const appendUpdate = writerUpdate((text) => {
|
|
687
|
-
text.insert(text.length, appendLine);
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
const replicas = [
|
|
691
|
-
[compactUpdate, appendUpdate],
|
|
692
|
-
[appendUpdate, compactUpdate],
|
|
693
|
-
].map((order) => {
|
|
694
|
-
const replica = new Y.Doc();
|
|
695
|
-
Y.applyUpdate(replica, baseUpdate);
|
|
696
|
-
for (const update of order) Y.applyUpdate(replica, update);
|
|
697
|
-
return replica.getText('content').toString();
|
|
698
|
-
});
|
|
699
|
-
assert.equal(replicas[0], replicas[1]);
|
|
700
|
-
assert.deepEqual(
|
|
701
|
-
replicas[0].trim().split('\n').map((entry) => JSON.parse(entry).cards[0].value),
|
|
702
|
-
['latest-a', 'latest-b', 'latest-c'],
|
|
619
|
+
8,
|
|
703
620
|
);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
writeDocText(
|
|
708
|
-
project.file,
|
|
709
|
-
`${getDocText(project.file)}{"wall":"code-project@4","project":"${repoKey}","machine":"machine-c","car\n`,
|
|
621
|
+
assert.equal(
|
|
622
|
+
require('../workspace/reducer').reduceWall(readWall(repoKey)).get(repoKey).card.positionVersion,
|
|
623
|
+
8,
|
|
710
624
|
);
|
|
711
|
-
settleProject();
|
|
712
|
-
assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
test('publishing compacts an oversized history before appending the new record', async () => {
|
|
716
|
-
const before = getDocText(project.file);
|
|
717
|
-
writeDocText(project.file, `${'obsolete\n'.repeat(132_000)}${before}`);
|
|
718
|
-
settleProject();
|
|
719
|
-
assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') > 1024 * 1024);
|
|
720
|
-
|
|
721
|
-
const repo = path.join(tempRoot, 'projects/app');
|
|
722
|
-
fs.writeFileSync(path.join(repo, 'after-compaction.txt'), 'bounded\n');
|
|
723
|
-
await run(repo, 'add', '.');
|
|
724
|
-
await run(repo, 'commit', '-m', 'publish after compaction');
|
|
725
|
-
await refreshRepoCards();
|
|
726
|
-
const result = publishWall(repoKey);
|
|
727
|
-
|
|
728
|
-
assert.equal(result.published, true);
|
|
729
|
-
assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') < 1024 * 1024);
|
|
730
|
-
assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
|
|
731
|
-
settleProject();
|
|
732
625
|
});
|
|
733
626
|
|
|
734
627
|
test('project stats name every project resource, binding, and publish counter', () => {
|
|
735
628
|
const stats = wallStats();
|
|
736
629
|
const row = stats.projects.find((entry) => entry.repoKey === repoKey);
|
|
737
|
-
assert.equal(stats.contract, 'code-project@
|
|
630
|
+
assert.equal(stats.contract, 'code-project@6');
|
|
738
631
|
assert.equal(row.resourceId, project.resourceId);
|
|
739
632
|
assert.equal(row.replica.appliedVersion >= 0, true);
|
|
740
633
|
assert.equal(row.machines, 2);
|
|
@@ -60,7 +60,13 @@ function cardKind(classified) {
|
|
|
60
60
|
return classified.linked ? 'linked-worktree' : 'primary';
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
function reusableObjectSource(previousOid, currentOid, source) {
|
|
64
|
+
return previousOid === currentOid && ['remote', 'bundle'].includes(source?.kind)
|
|
65
|
+
? source
|
|
66
|
+
: null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function observeWorktree(classified, previousCard = null) {
|
|
64
70
|
const worktreeRoot = classified.toplevel;
|
|
65
71
|
const gitDir = await fs.promises.realpath(classified.gitDir);
|
|
66
72
|
const replica = replicaForGitDir(gitDir);
|
|
@@ -77,14 +83,28 @@ async function observeWorktree(classified) {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
const repoIdentity = remote === null ? null : remoteIdentity(remote);
|
|
80
|
-
const
|
|
81
|
-
|
|
86
|
+
const previous = previousCard?.replica === replica.replicaId
|
|
87
|
+
&& previousCard.repo?.remote === repoIdentity
|
|
88
|
+
? previousCard
|
|
82
89
|
: null;
|
|
90
|
+
const previousRefs = new Map((previous?.refs || []).map((ref) => [ref.name, ref]));
|
|
91
|
+
const objectSources = new Map();
|
|
92
|
+
const sourceFor = (oid, priorOid, priorSource) => {
|
|
93
|
+
if (!oid || !repoIdentity) return null;
|
|
94
|
+
if (objectSources.has(oid)) return objectSources.get(oid);
|
|
95
|
+
const source = reusableObjectSource(priorOid, oid, priorSource)
|
|
96
|
+
|| require('./objects').objectSourceFor(worktreeRoot, repoIdentity, oid);
|
|
97
|
+
objectSources.set(oid, source);
|
|
98
|
+
return source;
|
|
99
|
+
};
|
|
100
|
+
const objects = await sourceFor(head.oid, previous?.head?.oid, previous?.objects);
|
|
83
101
|
const refs = await Promise.all(branches.map(async (branch) => ({
|
|
84
102
|
...branch,
|
|
85
|
-
objects:
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
objects: await sourceFor(
|
|
104
|
+
branch.oid,
|
|
105
|
+
previousRefs.get(branch.name)?.oid,
|
|
106
|
+
previousRefs.get(branch.name)?.objects,
|
|
107
|
+
),
|
|
88
108
|
})));
|
|
89
109
|
return {
|
|
90
110
|
card: CARD_CONTRACT,
|
|
@@ -117,14 +137,14 @@ async function observeWorktree(classified) {
|
|
|
117
137
|
* moved. A repo still in motion after that simply cards again on the next
|
|
118
138
|
* doorbell; every card is a true observation of some moment.
|
|
119
139
|
*/
|
|
120
|
-
async function buildRepoCard(dirPath) {
|
|
140
|
+
async function buildRepoCard(dirPath, previousCard = null) {
|
|
121
141
|
const classified = await classifyDirectory(dirPath);
|
|
122
142
|
if (classified.kind !== 'worktree') {
|
|
123
143
|
throw new Error(`cannot card ${classified.path}: ${classified.kind}`);
|
|
124
144
|
}
|
|
125
|
-
const observed = await observeWorktree(classified);
|
|
145
|
+
const observed = await observeWorktree(classified, previousCard);
|
|
126
146
|
if (observed.head.oid === await headOid(classified.toplevel)) return observed;
|
|
127
|
-
return observeWorktree(classified);
|
|
147
|
+
return observeWorktree(classified, observed);
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
/**
|
|
@@ -206,6 +226,7 @@ async function discoverWorktrees(rootPath) {
|
|
|
206
226
|
}
|
|
207
227
|
|
|
208
228
|
module.exports = {
|
|
229
|
+
_test: { reusableObjectSource },
|
|
209
230
|
CARD_CONTRACT,
|
|
210
231
|
buildRepoCard,
|
|
211
232
|
cardFingerprint,
|
|
@@ -171,6 +171,7 @@ async function runRefresh() {
|
|
|
171
171
|
service.counters.refreshes += 1;
|
|
172
172
|
|
|
173
173
|
const roots = trackedRoots();
|
|
174
|
+
const previousByPath = new Map(listRepoCards().map((card) => [card.path, card]));
|
|
174
175
|
const targets = new Map(); // worktree root → registry root that reached it
|
|
175
176
|
const skipped = [];
|
|
176
177
|
const rootErrors = [];
|
|
@@ -189,7 +190,7 @@ async function runRefresh() {
|
|
|
189
190
|
const built = [];
|
|
190
191
|
for (const [target, root] of targets) {
|
|
191
192
|
try {
|
|
192
|
-
built.push({ card: await buildRepoCard(target), root });
|
|
193
|
+
built.push({ card: await buildRepoCard(target, previousByPath.get(target)), root });
|
|
193
194
|
} catch (error) {
|
|
194
195
|
skipped.push({ path: target, reason: 'observation-failed', error: error.message });
|
|
195
196
|
}
|
|
@@ -321,7 +322,7 @@ async function refreshRepoCard(replicaId) {
|
|
|
321
322
|
const row = db.prepare('SELECT * FROM repo_cards WHERE replica_id = ?').get(replicaId);
|
|
322
323
|
if (!row) return null;
|
|
323
324
|
|
|
324
|
-
const observedCard = await buildRepoCard(row.path);
|
|
325
|
+
const observedCard = await buildRepoCard(row.path, rowToCard(row));
|
|
325
326
|
if (observedCard.replica !== replicaId) return refreshRepoCards();
|
|
326
327
|
const card = observedCard;
|
|
327
328
|
const fingerprint = cardFingerprint(card);
|