amalgm 0.1.245 → 0.1.246

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.
Files changed (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -6,7 +6,6 @@ const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
8
  const { execFile } = require('child_process');
9
- const { EventEmitter } = require('events');
10
9
  const { promisify } = require('util');
11
10
 
12
11
  const execFileAsync = promisify(execFile);
@@ -24,7 +23,6 @@ const {
24
23
  repoCardServiceStats,
25
24
  startRepoCardService,
26
25
  stopRepoCardService,
27
- _private,
28
26
  } = require('../workspace/cards');
29
27
 
30
28
  test.after(() => {
@@ -318,7 +316,7 @@ test('coverage shift and deletion inside a single pass still reads as missing',
318
316
  assert.equal(findCard(settled.cards, repo), undefined);
319
317
  });
320
318
 
321
- test('the live service hears a commit through the git-dir doorbell', async (t) => {
319
+ test('the live service hears a commit through the git-dir doorbell', async () => {
322
320
  const repo = await makeRepo('projects/belled');
323
321
  const projects = path.join(tempRoot, 'projects');
324
322
  upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
@@ -326,13 +324,6 @@ test('the live service hears a commit through the git-dir doorbell', async (t) =
326
324
  await refreshRepoCards();
327
325
  const before = findCard(listRepoCards(), repo);
328
326
  assert.equal(before.health, 'healthy');
329
- // fs.watch reports resource exhaustion asynchronously on macOS.
330
- await new Promise((resolve) => setTimeout(resolve, 50));
331
- if (!_private.watcherComplete(before.replica)) {
332
- stopRepoCardService();
333
- t.skip('the host refused part or all of the required fs.watch set');
334
- return;
335
- }
336
327
 
337
328
  fs.writeFileSync(path.join(repo, 'ding.txt'), 'ding\n');
338
329
  await run(repo, 'add', '.');
@@ -350,61 +341,7 @@ test('the live service hears a commit through the git-dir doorbell', async (t) =
350
341
  assert.ok(repoCardServiceStats().doorbellRings >= 1);
351
342
  });
352
343
 
353
- test('a doorbell rebuild releases its old watch set before opening the replacement', async () => {
354
- const repo = await makeRepo('projects/bounded-doorbell');
355
- const projects = path.join(tempRoot, 'projects');
356
- upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
357
- stopRepoCardService();
358
- await refreshRepoCards();
359
-
360
- const originalWatch = fs.watch;
361
- let openHandles = 0;
362
- let maximumOpenHandles = 0;
363
- let handleLimit = Number.POSITIVE_INFINITY;
364
- fs.watch = () => {
365
- if (openHandles >= handleLimit) {
366
- const error = new Error('simulated watch-handle ceiling');
367
- error.code = 'EMFILE';
368
- throw error;
369
- }
370
- openHandles += 1;
371
- maximumOpenHandles = Math.max(maximumOpenHandles, openHandles);
372
- const watcher = new EventEmitter();
373
- let closed = false;
374
- watcher.close = () => {
375
- if (closed) return;
376
- closed = true;
377
- openHandles -= 1;
378
- };
379
- watcher.unref = () => watcher;
380
- return watcher;
381
- };
382
-
383
- try {
384
- startRepoCardService();
385
- await refreshRepoCards();
386
- const card = findCard(listRepoCards(), repo);
387
- const initialHandles = _private.watcherCount(card.replica);
388
- const initialTotal = openHandles;
389
- assert.ok(initialHandles > 0);
390
- assert.equal(_private.watcherComplete(card.replica), true);
391
- assert.ok(initialTotal >= initialHandles);
392
-
393
- handleLimit = initialTotal;
394
- maximumOpenHandles = openHandles;
395
- _private.rebuildDoorbell(card.replica, card);
396
-
397
- assert.equal(_private.watcherComplete(card.replica), true);
398
- assert.equal(openHandles, initialTotal);
399
- assert.equal(maximumOpenHandles, initialTotal);
400
- assert.equal(repoCardServiceStats().incompleteDoorbells, 0);
401
- } finally {
402
- stopRepoCardService();
403
- fs.watch = originalWatch;
404
- }
405
- });
406
-
407
- test('the targeted doorbell path hears nested branch creation and deletion', async () => {
344
+ test('the live service hears nested branch creation and deletion', async () => {
408
345
  const repo = await makeRepo('projects/belled-refs');
409
346
  const projects = path.join(tempRoot, 'projects');
410
347
  upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
@@ -414,7 +351,6 @@ test('the targeted doorbell path hears nested branch creation and deletion', asy
414
351
  const before = findCard(listRepoCards(), repo);
415
352
 
416
353
  await run(repo, 'branch', branch);
417
- _private.ringDoorbell(before.replica);
418
354
  let deadline = Date.now() + 5_000;
419
355
  let created = findCard(listRepoCards(), repo);
420
356
  while (!created.refs.some((ref) => ref.name === branch) && Date.now() < deadline) {
@@ -424,7 +360,6 @@ test('the targeted doorbell path hears nested branch creation and deletion', asy
424
360
  assert.ok(created.refs.some((ref) => ref.name === branch));
425
361
 
426
362
  await run(repo, 'branch', '-D', branch);
427
- _private.ringDoorbell(before.replica);
428
363
  deadline = Date.now() + 5_000;
429
364
  let deleted = findCard(listRepoCards(), repo);
430
365
  while (deleted.refs.some((ref) => ref.name === branch) && Date.now() < deadline) {
@@ -437,7 +372,7 @@ test('the targeted doorbell path hears nested branch creation and deletion', asy
437
372
  assert.equal(deleted.generation, before.generation + 2);
438
373
  });
439
374
 
440
- test('the targeted doorbell path hears an uncommitted nested edit without a polling pass', async () => {
375
+ test('the live service hears an uncommitted nested edit without a polling pass', async () => {
441
376
  const repo = await makeRepo('projects/live-dirty');
442
377
  const projects = path.join(tempRoot, 'projects');
443
378
  upsertWorkspace({ path: projects, name: 'Projects', source: 'computer' });
@@ -448,8 +383,6 @@ test('the targeted doorbell path hears an uncommitted nested edit without a poll
448
383
  const nested = path.join(repo, 'src', 'nested');
449
384
  fs.mkdirSync(nested, { recursive: true });
450
385
  fs.writeFileSync(path.join(nested, 'live.txt'), 'live\n');
451
- const beforeCard = findCard(listRepoCards(), repo);
452
- _private.ringDoorbell(beforeCard.replica);
453
386
 
454
387
  const deadline = Date.now() + 5_000;
455
388
  let card = findCard(listRepoCards(), repo);
@@ -486,7 +419,6 @@ test('editing an already-dirty file rings the checkpoint directly', async () =>
486
419
  const card = findCard(listRepoCards(), repo);
487
420
  const file = path.join(repo, 'dirty.txt');
488
421
  fs.writeFileSync(file, 'first\n');
489
- _private.ringDoorbell(card.replica);
490
422
 
491
423
  let deadline = Date.now() + 5_000;
492
424
  while (published.length === 0 && Date.now() < deadline) {
@@ -495,7 +427,6 @@ test('editing an already-dirty file rings the checkpoint directly', async () =>
495
427
  published.length = 0;
496
428
 
497
429
  fs.writeFileSync(file, 'second\n');
498
- _private.ringDoorbell(card.replica);
499
430
  deadline = Date.now() + 5_000;
500
431
  while (published.length === 0 && Date.now() < deadline) {
501
432
  await new Promise((resolve) => setTimeout(resolve, 25));
@@ -13,18 +13,9 @@ const execFileAsync = promisify(execFile);
13
13
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-repo-cards-test-'));
14
14
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
15
15
  process.env.AMALGM_COMPUTER_ID = 'computer-test';
16
- fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
17
- fs.writeFileSync(
18
- path.join(process.env.AMALGM_DIR, 'computer.json'),
19
- JSON.stringify({
20
- computer_id: 'computer-test',
21
- user_id: '11111111-1111-4111-8111-111111111111',
22
- }),
23
- );
24
16
 
25
17
  const { closeLocalDb, openLocalDb } = require('../state/db');
26
18
  const {
27
- _test: cardTest,
28
19
  buildRepoCard,
29
20
  cardFingerprint,
30
21
  discoverWorktrees,
@@ -108,7 +99,7 @@ test('a clean repo cards with exact head, clean counts, and full identity', asyn
108
99
  await run(dir, 'remote', 'add', 'origin', 'git@github.com:amalgm/clean.git');
109
100
 
110
101
  const card = await buildRepoCard(dir);
111
- assert.equal(card.card, 'repo-card@3');
102
+ assert.equal(card.card, 'repo-card@2');
112
103
  assert.equal(card.machine, 'computer-test');
113
104
  assert.match(card.replica, /^rpl_[A-Za-z0-9_-]+$/);
114
105
  assert.equal(card.path, await fs.promises.realpath(dir));
@@ -408,82 +399,3 @@ test('the fingerprint holds across observations and moves on any real change', a
408
399
  const third = await buildRepoCard(dir);
409
400
  assert.notEqual(cardFingerprint(second), cardFingerprint(third));
410
401
  });
411
-
412
- test('unchanged commits reuse their card object source and one new oid is resolved once', async () => {
413
- const dir = await makeRepo('object-source-cache');
414
- await run(dir, 'branch', 'side');
415
- const local = await buildRepoCard(dir);
416
- await run(dir, 'remote', 'add', 'origin', 'https://github.com/amalgm/object-source-cache.git');
417
- const { resourceId } = require('../workspace/wall').bindProject(
418
- '018f0000-0000-7000-8000-00000000cace',
419
- '11111111-1111-4111-8111-111111111111',
420
- dir,
421
- );
422
- assert.equal(
423
- require('../workspace/wall').projectForPath(dir)?.resourceId,
424
- resourceId,
425
- );
426
- const ready = { kind: 'bundle', resourceId, checksum: 'ready' };
427
- const previous = {
428
- ...local,
429
- repo: { ...local.repo, remote: 'https://github.com/amalgm/object-source-cache' },
430
- objects: ready,
431
- refs: local.refs.map((ref) => ({ ...ref, objects: ready })),
432
- };
433
- const classified = await classifyDirectory(dir);
434
- const replica = require('../workspace/identity').replicaForGitDir(
435
- await fs.promises.realpath(classified.gitDir),
436
- );
437
- assert.equal(previous.replica, replica.replicaId);
438
- assert.equal(
439
- cardTest.reusableObjectSource(local.head.oid, local.head.oid, ready, resourceId),
440
- ready,
441
- );
442
- const objects = require('../workspace/objects');
443
- const original = objects.objectSourceFor;
444
- let calls = 0;
445
- objects.objectSourceFor = async () => {
446
- calls += 1;
447
- return { kind: 'pending' };
448
- };
449
- try {
450
- const unchanged = await buildRepoCard(dir, previous);
451
- assert.equal(calls, 0);
452
- assert.equal(unchanged.objects.kind, 'bundle');
453
- assert.ok(unchanged.refs.every((ref) => ref.objects.kind === 'bundle'));
454
-
455
- write(dir, 'next.txt', 'next\n');
456
- await run(dir, 'add', '.');
457
- await run(dir, 'commit', '-m', 'next');
458
- const changed = await buildRepoCard(dir, unchanged);
459
- assert.equal(calls, 1, 'HEAD and its branch share one content-addressed lookup');
460
- assert.equal(changed.objects.kind, 'pending');
461
- assert.equal(changed.refs.find((ref) => ref.name === 'main').objects.kind, 'pending');
462
- assert.equal(changed.refs.find((ref) => ref.name === 'side').objects.kind, 'bundle');
463
- } finally {
464
- objects.objectSourceFor = original;
465
- }
466
- });
467
-
468
- test('only complete object sources are reusable', () => {
469
- assert.deepEqual(
470
- cardTest.reusableObjectSource(
471
- 'a',
472
- 'a',
473
- { kind: 'bundle', resourceId: 'shr_project_a', checksum: 'ready' },
474
- 'shr_project_a',
475
- ),
476
- { kind: 'bundle', resourceId: 'shr_project_a', checksum: 'ready' },
477
- );
478
- assert.equal(
479
- cardTest.reusableObjectSource(
480
- 'a',
481
- 'a',
482
- { kind: 'bundle', resourceId: 'shr_project_old', checksum: 'ready' },
483
- 'shr_project_new',
484
- ),
485
- null,
486
- );
487
- assert.equal(cardTest.reusableObjectSource('a', 'b', { kind: 'remote' }), null);
488
- assert.equal(cardTest.reusableObjectSource('a', 'a', { kind: 'pending' }), null);
489
- });
@@ -10,8 +10,6 @@ const { promisify } = require('util');
10
10
 
11
11
  const execFileAsync = promisify(execFile);
12
12
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-checkpoint-reducer-'));
13
- process.env.AMALGM_DIR = path.join(root, '.amalgm');
14
- process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
15
13
  const merge = require('../workspace/merge');
16
14
 
17
15
  test.after(() => fs.rmSync(root, { recursive: true, force: true }));
@@ -59,44 +57,51 @@ test('rail-ordered checkpoint transitions combine and a later revert removes onl
59
57
  const treeA = await merge.treeOfPatch(repo, oid, patchA);
60
58
  const treeB = await merge.treeOfPatch(repo, oid, patchB);
61
59
 
62
- const afterA = await merge.reduceTree(repo, clean, clean, treeA);
63
- const afterB = await merge.reduceTree(repo, afterA.tree, clean, treeB);
60
+ const afterA = await merge.reduceTree(repo, clean, clean, treeA, { label: '1' });
61
+ const afterB = await merge.reduceTree(repo, afterA.tree, clean, treeB, { label: '2' });
64
62
  assert.equal(await treeText(repo, afterB.tree, 'a.txt'), 'a1\n');
65
63
  assert.equal(await treeText(repo, afterB.tree, 'b.txt'), 'b1\n');
64
+ assert.deepEqual(afterB.conflicts, []);
66
65
 
67
66
  // A's next operation says "from A+B to B": the reducer can remove A even
68
67
  // though B's latest checkpoint already incorporated A.
69
- const reverted = await merge.reduceTree(repo, afterB.tree, afterB.tree, treeB);
68
+ const reverted = await merge.reduceTree(repo, afterB.tree, afterB.tree, treeB, { label: '3' });
70
69
  assert.equal(await treeText(repo, reverted.tree, 'a.txt'), 'a0\n');
71
70
  assert.equal(await treeText(repo, reverted.tree, 'b.txt'), 'b1\n');
72
71
  });
73
72
 
74
- test('a later overlapping transition supplies the exact final bytes', async () => {
75
- const { repo, oid, tree: clean } = await makeRepo('later-wins');
73
+ test('an overlapping transition produces deterministic conflict bytes', async () => {
74
+ const { repo, oid, tree: clean } = await makeRepo('conflict');
76
75
  const leftPatch = await patchFor(repo, { 'same.txt': 'left\nline 2\n' });
77
76
  const rightPatch = await patchFor(repo, { 'same.txt': 'right\nline 2\n' });
78
77
  const left = await merge.treeOfPatch(repo, oid, leftPatch);
79
78
  const right = await merge.treeOfPatch(repo, oid, rightPatch);
80
79
 
81
- const current = await merge.reduceTree(repo, clean, clean, left);
82
- const first = await merge.reduceTree(repo, current.tree, clean, right);
83
- const second = await merge.reduceTree(repo, current.tree, clean, right);
80
+ const current = await merge.reduceTree(repo, clean, clean, left, { label: '10' });
81
+ const first = await merge.reduceTree(repo, current.tree, clean, right, { label: '11' });
82
+ const second = await merge.reduceTree(repo, current.tree, clean, right, { label: '11' });
84
83
 
85
84
  assert.equal(first.tree, second.tree);
86
- assert.equal(await treeText(repo, first.tree, 'same.txt'), 'right\nline 2\n');
85
+ assert.deepEqual(first.conflicts, second.conflicts);
86
+ assert.equal(first.conflicts[0].kind, 'content-overlap');
87
+ const content = await treeText(repo, first.tree, 'same.txt');
88
+ assert.match(content, /<<<<<<< current/);
89
+ assert.match(content, /left/);
90
+ assert.match(content, /right/);
87
91
  });
88
92
 
89
93
  test('an echoed transition that is already present reduces to a no-op', async () => {
90
94
  const { repo, oid, tree: clean } = await makeRepo('echo');
91
95
  const patch = await patchFor(repo, { 'a.txt': 'shared\n' });
92
96
  const shared = await merge.treeOfPatch(repo, oid, patch);
93
- const current = await merge.reduceTree(repo, clean, clean, shared);
94
- const echoed = await merge.reduceTree(repo, current.tree, clean, shared);
97
+ const current = await merge.reduceTree(repo, clean, clean, shared, { label: '20' });
98
+ const echoed = await merge.reduceTree(repo, current.tree, clean, shared, { label: '21' });
95
99
  assert.equal(echoed.tree, current.tree);
100
+ assert.deepEqual(echoed.conflicts, []);
96
101
  });
97
102
 
98
- test('a later deletion removes a file changed by an earlier writer', async () => {
99
- const { repo, oid, tree: clean } = await makeRepo('later-delete');
103
+ test('a deletion never removes a file another writer edited', async () => {
104
+ const { repo, oid, tree: clean } = await makeRepo('delete-vs-edit');
100
105
  const localPatch = await patchFor(repo, { 'a.txt': 'precious local edit\n' });
101
106
  const localTree = await merge.treeOfPatch(repo, oid, localPatch);
102
107
  fs.rmSync(path.join(repo, 'a.txt'));
@@ -104,44 +109,8 @@ test('a later deletion removes a file changed by an earlier writer', async () =>
104
109
  await git(repo, 'reset', '--hard', 'HEAD');
105
110
  const deletedTree = await merge.treeOfPatch(repo, oid, deletePatch);
106
111
 
107
- const current = await merge.reduceTree(repo, clean, clean, localTree);
108
- const outcome = await merge.reduceTree(repo, current.tree, clean, deletedTree);
109
- await assert.rejects(treeText(repo, outcome.tree, 'a.txt'));
110
- });
111
-
112
- test('an earlier inbound event advances underneath a pending local write', async () => {
113
- const { repo, oid, tree: clean } = await makeRepo('pending-local');
114
- const inboundPatch = await patchFor(repo, { 'same.txt': 'inbound\n' });
115
- const inbound = await merge.treeOfPatch(repo, oid, inboundPatch);
116
-
117
- fs.writeFileSync(path.join(repo, 'same.txt'), 'pending local\n');
118
- const outcome = await merge.advance(repo, clean, inbound);
119
-
120
- assert.equal(fs.readFileSync(path.join(repo, 'same.txt'), 'utf8'), 'pending local\n');
121
- assert.equal(await treeText(repo, outcome.heldTree, 'same.txt'), 'inbound\n');
122
- });
123
-
124
- test('an inbound file write cannot overwrite a local write that races its observation', async () => {
125
- const { repo, oid, tree: clean } = await makeRepo('pending-local-race');
126
- const inboundPatch = await patchFor(repo, { 'same.txt': 'inbound\n' });
127
- const inbound = await merge.treeOfPatch(repo, oid, inboundPatch);
128
- const transition = require('../workspace/transition');
129
- const applyFiles = transition.applyFiles;
130
- transition.applyFiles = async (...args) => {
131
- fs.writeFileSync(path.join(repo, 'same.txt'), 'local writer won the race\n');
132
- return applyFiles(...args);
133
- };
134
- try {
135
- await assert.rejects(
136
- merge.advance(repo, clean, inbound),
137
- /local file changed before ordered apply/,
138
- );
139
- } finally {
140
- transition.applyFiles = applyFiles;
141
- }
142
-
143
- assert.equal(
144
- fs.readFileSync(path.join(repo, 'same.txt'), 'utf8'),
145
- 'local writer won the race\n',
146
- );
112
+ const current = await merge.reduceTree(repo, clean, clean, localTree, { label: '30' });
113
+ const outcome = await merge.reduceTree(repo, current.tree, clean, deletedTree, { label: '31' });
114
+ assert.equal(outcome.conflicts[0].kind, 'delete-vs-change');
115
+ assert.equal(await treeText(repo, outcome.tree, 'a.txt'), 'precious local edit\n');
147
116
  });