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.
@@ -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@8',
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@8',
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
- fs.mkdirSync(path.dirname(project.file), { recursive: true });
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: opened.resource,
346
+ localResource: `${projectStream.LOCAL_RESOURCE_PREFIX}${project.resourceId}`,
331
347
  localPath: project.file,
332
- contract: 'text-yjs@1',
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@4');
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 sealedLines = getDocText(project.file).trim().split('\n');
365
- assert.equal(sealedLines.length, 1);
366
- assert.equal(
367
- JSON.parse(sealedLines.at(-1)).cards[0].positionVersion,
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(publishedCardFor(readyObjects), null);
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(getDocText(project.file).trim().split('\n').length, 1);
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@8',
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 checkpointLines = getDocText(project.file).trim().split('\n');
439
+ const checkpointSnapshot = projectStream.readSnapshot(project.file, repoKey);
440
+ assert.equal(checkpointSnapshot.records[0].version, 2);
422
441
  assert.equal(
423
- JSON.parse(checkpointLines.at(-1)).cards[0].checkpoints[0].version ?? null,
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 = getDocText(project.file);
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(getDocText(project.file), beforeMaterialization);
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('peer records and earlier local history survive later publishes', async () => {
528
- const beforeLines = getDocText(project.file).trim().split('\n');
529
- const peerLine = JSON.stringify({
530
- wall: 'code-project@4',
531
- project: repoKey,
532
- machine: 'machine-b',
533
- mutationId: 'mut_project_peer',
534
- publishedAt: '2026-07-19T00:00:00.000Z',
535
- cards: [{ card: 'repo-card@2', machine: 'machine-b', replica: 'rpl_peer', path: '/work/app', head: { branch: 'preview' } }],
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
- writeDocText(project.file, `${getDocText(project.file)}${peerLine}\n`);
538
- assert.deepEqual(settleProject(), [7]);
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('three concurrent append records stay whole under Yjs ordering', () => {
557
- const Y = require('yjs');
558
- const base = new Y.Doc();
559
- base.getText('content').insert(0, '{"machine":"existing"}\n');
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
- test('an unordered legacy record cannot pin wall compaction', () => {
607
- const unordered = JSON.stringify({
608
- wall: 'code-project@4',
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
- const latestA = ordered('machine-a');
621
- const latestB = ordered('machine-b');
622
-
601
+ assert.ok(pruned.deleted >= 8);
623
602
  assert.equal(
624
- compactWallText(`${unordered}\n${latestA}\n${latestB}\n`, repoKey),
625
- `${latestA}\n${latestB}\n`,
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
- compactWallText(
646
- `${retiredA}\n${retiredB}\n${oldA}\n${oldB}\n${latestA}\n${latestB}\n`,
614
+ require('../workspace/wall').mutationVersion(
615
+ project.resourceId,
616
+ durable.mutationId,
647
617
  repoKey,
648
618
  ),
649
- `${retiredA}\n${retiredB}\n${latestA}\n${latestB}\n`,
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
- test('a torn line is ignored by readers, never fatal', () => {
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@4');
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
- async function observeWorktree(classified) {
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 objects = head.oid && repoIdentity
81
- ? await require('./objects').objectSourceFor(worktreeRoot, repoIdentity, head.oid)
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: repoIdentity
86
- ? await require('./objects').objectSourceFor(worktreeRoot, repoIdentity, branch.oid)
87
- : null,
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);