amalgm 0.1.262 → 0.1.263

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.
@@ -95,12 +95,24 @@ function createFollower(options) {
95
95
  last = null, // resume seed: { cardId, checkpointId }
96
96
  // May return a promise (e.g. a capture worker): the follower awaits it,
97
97
  // and any doorbell that lands mid-pair re-arms a retry (`pendingWake`).
98
- captureFn = (repo) => capture(repo, { withBundle: false }),
98
+ // Called with { suspects }: the worktree paths rung since the last
99
+ // settled state, or null when everything is suspect.
100
+ captureFn = (repo, { suspects } = {}) => capture(repo, { withBundle: false, suspects }),
99
101
  } = options;
100
102
  if (!repoPath) throw new Error('createFollower requires { repoPath }');
101
103
  if (typeof emit !== 'function') throw new Error('createFollower requires { emit }');
102
104
 
103
105
  let known = last ? { cardId: last.cardId, checkpointId: last.checkpointId } : null;
106
+ // The warm-checkpoint scope: which worktree paths the doorbell has named
107
+ // since the last settled state. Each entry remembers the GENERATION of
108
+ // the ring that named it, so a path rung again mid-pair keeps its
109
+ // suspicion when the pair's success consumes the older hints. A nameless
110
+ // ring — or any entrance that may follow missed doorbells (birth, flush,
111
+ // a command ending) — makes EVERYTHING suspect until a pair settles.
112
+ const SCOPE_CAP = 512;
113
+ const scope = new Map(); // repo-relative path -> generation
114
+ let scopeGen = 0;
115
+ let scopeAllGen = 0; // >0: everything is suspect (set at birth, flush, command end, nameless ring)
104
116
  let owned = 0; // depth of amalgm-owned commands in flight
105
117
  let blocked = null; // lock name while the last capture attempt met a held lock
106
118
  let blockedPath = null;
@@ -210,12 +222,15 @@ function createFollower(options) {
210
222
 
211
223
  // The torn-read guard: two consecutive reads must agree before a state
212
224
  // may travel — a repo mid-mutation cannot produce the same ids twice.
225
+ // Both halves of the pair read the SAME scope: the paths rung so far.
226
+ const genNow = scopeGen;
227
+ const suspects = scopeAllGen > 0 ? null : [...scope.keys()];
213
228
  let first;
214
229
  let second;
215
230
  try {
216
- first = await captureFn(repoPath);
231
+ first = await captureFn(repoPath, { suspects });
217
232
  if (closed || owned > 0) return; // the pair is stale before it finished
218
- second = first && !first.busy ? await captureFn(repoPath) : null;
233
+ second = first && !first.busy ? await captureFn(repoPath, { suspects }) : null;
219
234
  } catch (error) {
220
235
  if (closed) return;
221
236
  // A read tripping over a mid-flight writer (a file vanishing between
@@ -234,11 +249,34 @@ function createFollower(options) {
234
249
  retry(settleMs);
235
250
  return;
236
251
  }
237
- if (!speak(second)) retry(capMs); // consumer failure: patient, loud, never lost
252
+ if (!speak(second)) {
253
+ retry(capMs); // consumer failure: patient, loud, never lost
254
+ return;
255
+ }
256
+ // The settled pair proved the ground the scope described: hints from
257
+ // BEFORE the pair began are consumed; anything rung mid-pair keeps its
258
+ // newer generation and stays suspect for the pendingWake retry.
259
+ if (scopeAllGen > 0 && scopeAllGen <= genNow) scopeAllGen = 0;
260
+ for (const [rel, gen] of scope) {
261
+ if (gen <= genNow) scope.delete(rel);
262
+ }
238
263
  }
239
264
 
240
- function ring() {
265
+ function ring(hint) {
241
266
  if (closed || owned > 0) return; // the command's end is the boundary
267
+ // Accumulate the warm-checkpoint scope. A .git-area hint never touches
268
+ // untracked ground, so it schedules a capture without widening the
269
+ // scope; a nameless hint (or an overflowing scope) suspects everything.
270
+ scopeGen += 1;
271
+ if (hint === null || hint === undefined) {
272
+ scopeAllGen = scopeGen;
273
+ } else if (hint !== '.git' && !hint.startsWith('.git/')) {
274
+ scope.set(hint, scopeGen);
275
+ if (scope.size > SCOPE_CAP) {
276
+ scope.clear();
277
+ scopeAllGen = scopeGen;
278
+ }
279
+ }
242
280
  if (settleTimer) clearTimeout(settleTimer);
243
281
  settleTimer = setTimeout(() => {
244
282
  settleTimer = null;
@@ -262,7 +300,13 @@ function createFollower(options) {
262
300
 
263
301
  function endCommand() {
264
302
  owned = Math.max(0, owned - 1);
265
- if (owned === 0 && !closed) fire();
303
+ // Rings were suppressed while the command ran: its writes are nobody's
304
+ // hints, so the capture at the boundary suspects everything.
305
+ if (owned === 0 && !closed) {
306
+ scopeGen += 1;
307
+ scopeAllGen = scopeGen;
308
+ fire();
309
+ }
266
310
  }
267
311
 
268
312
  /** Run an amalgm-owned command: no captures while it runs, one at its end. */
@@ -284,10 +328,21 @@ function createFollower(options) {
284
328
 
285
329
  return {
286
330
  ring,
287
- /** Birth: capture once, immediately. With a seeded `last`, an unchanged repo is silence. */
288
- start: () => fire(),
289
- /** Consumer-requested capture — the "scanNow" of this layer. */
290
- flush: () => fire(),
331
+ /** Birth: capture once, immediately, suspecting everything — rings
332
+ * before birth were nobody's. With a seeded `last`, an unchanged
333
+ * repo is silence. */
334
+ start: () => {
335
+ scopeGen += 1;
336
+ scopeAllGen = scopeGen;
337
+ return fire();
338
+ },
339
+ /** Consumer-requested capture — the "scanNow" of this layer. Its whole
340
+ * purpose is catching a LOST doorbell, so it never trusts the scope. */
341
+ flush: () => {
342
+ scopeGen += 1;
343
+ scopeAllGen = scopeGen;
344
+ return fire();
345
+ },
291
346
  beginCommand,
292
347
  endCommand,
293
348
  command,
@@ -371,3 +371,93 @@ test('add follows in-order mirrored mutations and refuses after a version gap un
371
371
  fs.rmSync(targetDir, { recursive: true, force: true });
372
372
  }
373
373
  });
374
+
375
+ // --- the repo travel law -----------------------------------------------------
376
+
377
+ function activeCloud({ content = { has: () => true, ensureUpload: () => {} }, repoTransportFor = null } = {}) {
378
+ const db = new Database(':memory:');
379
+ let registry = null;
380
+ const cloud = createEntityCloud({
381
+ db,
382
+ userId: 'user-1',
383
+ deviceId: 'device-1',
384
+ content,
385
+ repoTransportFor,
386
+ recordAt: (uuid) => {
387
+ try { return registry.syncRecord(uuid); } catch { return null; }
388
+ },
389
+ });
390
+ registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
391
+ const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
392
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
393
+ cloud.install({
394
+ registry,
395
+ resourceId: bootstrap.resourceId,
396
+ authorityEpoch: 1,
397
+ headVersion: 0,
398
+ snapshotBase64: bootstrap.snapshotBase64,
399
+ snapshotChecksum: bootstrap.snapshotChecksum,
400
+ });
401
+ return { db, cloud, registry, root, resourceId: bootstrap.resourceId };
402
+ }
403
+
404
+ test('a record enclosed by a repository never journals its own cloud mutation — the repo state parcel is the carrier', () => {
405
+ const transportHead = 'a'.repeat(64);
406
+ const { db, registry, root, resourceId } = activeCloud({
407
+ repoTransportFor: () => transportHead,
408
+ });
409
+
410
+ const repo = registry.create({ type: 'repo.git', parentUUID: root.uuid, name: 'engine', payloadVersion: 'state-1' });
411
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 1,
412
+ 'the repository record itself travels');
413
+
414
+ const folder = registry.create({ type: 'folder', parentUUID: repo.uuid, name: 'src' });
415
+ registry.create({ type: 'file.text', parentUUID: folder.uuid, name: 'index.js', payloadVersion: 'b'.repeat(64) });
416
+ registry.create({ type: 'link', parentUUID: repo.uuid, name: 'current', payloadVersion: 'src' });
417
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 1,
418
+ 'worktree children mail no postcards: a register or a rebase is one parcel on the repo entity');
419
+
420
+ const catalog = db.prepare('SELECT entity_uuid FROM entity_cloud_catalog WHERE resource_id = ? ORDER BY entity_uuid').all(resourceId)
421
+ .map((row) => row.entity_uuid).sort();
422
+ assert.deepEqual(catalog, [root.uuid, repo.uuid].sort(),
423
+ 'the cloud catalog holds the traveling graph only — no rows for repo-enclosed children');
424
+
425
+ // A later Detect correction on the child (type/payload) stays local too.
426
+ const inner = registry.activeAt(folder.uuid, 'index.js');
427
+ registry.attest(inner.uuid, { type: 'file.binary', payloadVersion: 'c'.repeat(64) });
428
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 1,
429
+ 'the census drip is dead: child corrections never reach the outbox');
430
+ });
431
+
432
+ test('an atomic snapshot replacement excludes repo-enclosed records and never demands their bytes', () => {
433
+ const transportHead = 'd'.repeat(64);
434
+ const childHead = 'e'.repeat(64);
435
+ const { db, cloud, registry, root, resourceId } = activeCloud({
436
+ repoTransportFor: () => transportHead,
437
+ content: {
438
+ ensureUpload: () => {},
439
+ has: ({ contentHash }) => {
440
+ assert.notEqual(contentHash, childHead, 'an enclosed child head must never be demanded as separate cloud cargo');
441
+ return true;
442
+ },
443
+ },
444
+ });
445
+
446
+ registry.withoutJournal(() => {
447
+ const repo = registry.create({ type: 'repo.git', parentUUID: root.uuid, name: 'engine', payloadVersion: 'state-1' });
448
+ registry.create({ type: 'file.text', parentUUID: repo.uuid, name: 'inner.md', payloadVersion: childHead });
449
+ registry.create({ type: 'file.text', parentUUID: root.uuid, name: 'loose.md', payloadVersion: 'f'.repeat(64) });
450
+ });
451
+
452
+ cloud.replaceSnapshot(registry.syncRecords());
453
+ const operation = JSON.parse(db.prepare(`
454
+ SELECT operation_json FROM entity_cloud_mutations
455
+ WHERE resource_id = ? ORDER BY sequence DESC LIMIT 1
456
+ `).get(resourceId).operation_json);
457
+ assert.equal(operation.kind, 'entity.snapshot.replace');
458
+ const types = operation.snapshot.records.map((record) => record.type).sort();
459
+ assert.deepEqual(types, ['file.text', 'repo.git', 'workspace'],
460
+ 'the atomic graph carries the workspace, the loose file, and ONE record per repository');
461
+ assert.ok(!operation.snapshot.records.some((record) => record.name === 'inner.md'),
462
+ 'repo children stay out of the published graph');
463
+ });
@@ -461,3 +461,239 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
461
461
  fs.rmSync(rootDir, { recursive: true, force: true });
462
462
  }
463
463
  });
464
+
465
+ test('the repo travel law end to end: one traveling record, child UUIDs ride the transport map, and add binds the exact same identities', async () => {
466
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-repo-travel-law-'));
467
+ const sourceState = path.join(rootDir, 'source-state');
468
+ const targetState = path.join(rootDir, 'target-state');
469
+ const sourceRepo = path.join(rootDir, 'engine');
470
+ const targetParent = path.join(rootDir, 'checkout');
471
+ fs.mkdirSync(path.join(sourceRepo, 'src'), { recursive: true });
472
+ fs.mkdirSync(targetParent, { recursive: true });
473
+ git(sourceRepo, ['init', '--quiet']);
474
+ git(sourceRepo, ['config', 'user.email', 'test@example.com']);
475
+ git(sourceRepo, ['config', 'user.name', 'Amalgm Test']);
476
+ fs.writeFileSync(path.join(sourceRepo, 'tracked.md'), '# committed\n');
477
+ fs.writeFileSync(path.join(sourceRepo, 'src', 'index.js'), 'module.exports = 1;\n');
478
+ git(sourceRepo, ['add', '.']);
479
+ git(sourceRepo, ['commit', '--quiet', '-m', 'initial']);
480
+ fs.writeFileSync(path.join(sourceRepo, 'untracked.txt'), 'not committed\n');
481
+
482
+ const { unpack } = require('../registration/repo-states');
483
+ const source = createRegistrationService({
484
+ dir: sourceState, homeDir: path.join(rootDir, 'source-home'),
485
+ cloudUserId: 'user-1', cloudDeviceId: 'source-device',
486
+ });
487
+ const target = createRegistrationService({
488
+ dir: targetState, homeDir: path.join(rootDir, 'target-home'),
489
+ cloudUserId: 'user-1', cloudDeviceId: 'target-device',
490
+ });
491
+ const mutationOps = (stateDir) => {
492
+ const handle = new Database(path.join(stateDir, 'identity.db'), { readonly: true });
493
+ try {
494
+ return handle.prepare('SELECT operation_json FROM entity_cloud_mutations ORDER BY sequence').all()
495
+ .map((row) => JSON.parse(row.operation_json));
496
+ } finally {
497
+ handle.close();
498
+ }
499
+ };
500
+ try {
501
+ const bootstrap = source.entityCloud.bootstrap(source.registration.registry.syncRecords());
502
+ source.entityCloud.install({
503
+ registry: source.registration.registry, resourceId: bootstrap.resourceId,
504
+ authorityEpoch: 1, headVersion: 0, snapshotBase64: bootstrap.snapshotBase64,
505
+ snapshotChecksum: bootstrap.snapshotChecksum,
506
+ });
507
+ const registered = await source.register(sourceRepo);
508
+
509
+ // ONE traveling record: the repository. No child postcards, ever.
510
+ const ops = mutationOps(sourceState);
511
+ assert.equal(ops.length, 1, `a repository register is one cloud mutation, got ${JSON.stringify(ops.map((op) => op.record?.name))}`);
512
+ assert.equal(ops[0].kind, 'entity.upsert');
513
+ assert.equal(ops[0].record.uuid, registered.record.uuid);
514
+ assert.ok(ops[0].record.transportVersion, 'the one record names its Repocard transport');
515
+
516
+ // The transport parcel carries the worktree identity map, and the map
517
+ // names the exact UUIDs the local registry minted.
518
+ const transport = unpack(source.entityContent.bytes({ contentHash: ops[0].record.transportVersion }).toString('utf8'));
519
+ assert.ok(Array.isArray(transport.identity), 'the transport carries the identity map');
520
+ const mapByPath = new Map(transport.identity.map((entry) => [entry.path, entry]));
521
+ const localByPath = new Map();
522
+ const walkLocal = (parentUuid, base) => {
523
+ for (const child of source.registration.registry.children(parentUuid)) {
524
+ if (child.status !== 'active') continue;
525
+ const childPath = base ? `${base}/${child.name}` : child.name;
526
+ localByPath.set(childPath, child.uuid);
527
+ walkLocal(child.uuid, childPath);
528
+ }
529
+ };
530
+ walkLocal(registered.record.uuid, '');
531
+ for (const relPath of ['tracked.md', 'src', 'src/index.js', 'untracked.txt']) {
532
+ assert.equal(mapByPath.get(relPath)?.uuid, localByPath.get(relPath),
533
+ `the transport map and the local registry agree on ${relPath}`);
534
+ }
535
+
536
+ // The gateway would fold that one mutation into the active head: the
537
+ // authoritative snapshot contains exactly the traveling graph.
538
+ const snapshot = snapshotFromRecords([ops[0].record]);
539
+ const snapshotBytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
540
+ target.entityCloud.install({
541
+ registry: target.registration.registry, resourceId: bootstrap.resourceId,
542
+ authorityEpoch: 1, headVersion: 1, snapshotBase64: snapshotBytes.toString('base64'),
543
+ snapshotChecksum: sha256(snapshotBytes),
544
+ });
545
+ const uploaded = [];
546
+ while (true) {
547
+ const part = source.entityCloud.claimContent();
548
+ if (!part) break;
549
+ uploaded.push(part);
550
+ source.entityCloud.acknowledgeContent({
551
+ resourceId: bootstrap.resourceId, contentHash: part.contentHash,
552
+ kind: part.kind, partIndex: part.partIndex,
553
+ });
554
+ }
555
+ for (const part of [...uploaded.filter((entry) => entry.kind === 'manifest'), ...uploaded.filter((entry) => entry.kind === 'chunk')]) {
556
+ target.receiveCloudContent({
557
+ resourceId: bootstrap.resourceId, contentHash: part.contentHash, kind: part.kind,
558
+ partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
559
+ sha256: part.sha256, dataBase64: part.dataBase64,
560
+ });
561
+ }
562
+ const job = await target.add({ uuid: registered.record.uuid, targetPath: targetParent });
563
+ assert.equal(job.state, 'complete', job.error);
564
+ const destination = path.join(targetParent, 'engine');
565
+ assert.equal(verify(destination, source.registration.repoState(registered.record.uuid)).ok, true);
566
+
567
+ // The receiving machine wears the SAME child UUIDs — bound from the
568
+ // transport map, never minted as competitors.
569
+ for (const [relPath, uuid] of localByPath) {
570
+ const bound = target.registration.registry.syncRecord(uuid);
571
+ assert.equal(bound?.name, path.basename(relPath), `${relPath} keeps its permanent UUID across machines`);
572
+ }
573
+ assert.equal(mutationOps(targetState).length, 0, 'the add binding echoes nothing upstream');
574
+
575
+ // A live edit settles as the NEXT parcel: still one mutation on the
576
+ // repo entity, still no child postcards, map still riding along.
577
+ await new Promise((resolve) => setTimeout(resolve, 250));
578
+ fs.writeFileSync(path.join(sourceRepo, 'later.md'), 'a later change\n');
579
+ const deadline = Date.now() + 30_000;
580
+ while (mutationOps(sourceState).length < 2) {
581
+ if (Date.now() > deadline) assert.fail('timed out waiting for the settled repo parcel');
582
+ await new Promise((resolve) => setTimeout(resolve, 50));
583
+ }
584
+ await new Promise((resolve) => setTimeout(resolve, 500));
585
+ const settledOps = mutationOps(sourceState);
586
+ assert.ok(settledOps.every((op) => op.record?.uuid === registered.record.uuid),
587
+ `every traveling mutation is the repo entity itself, got ${JSON.stringify(settledOps.map((op) => op.record?.name))}`);
588
+ const settled = settledOps[settledOps.length - 1];
589
+ const settledTransport = unpack(source.entityContent.bytes({ contentHash: settled.record.transportVersion }).toString('utf8'));
590
+ assert.equal(
591
+ new Map(settledTransport.identity.map((entry) => [entry.path, entry])).get('tracked.md')?.uuid,
592
+ localByPath.get('tracked.md'),
593
+ 'a settled parcel repacks the registry-held identity map with the same permanent UUIDs',
594
+ );
595
+ } finally {
596
+ source.close();
597
+ target.close();
598
+ fs.rmSync(rootDir, { recursive: true, force: true });
599
+ }
600
+ });
601
+
602
+ test('a workspace containing a repository travels as its loose records plus ONE repo record, and add binds repo children from the map', async () => {
603
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-workspace-repo-travel-'));
604
+ const sourceState = path.join(rootDir, 'source-state');
605
+ const targetState = path.join(rootDir, 'target-state');
606
+ const workspace = path.join(rootDir, 'ambient');
607
+ const repo = path.join(workspace, 'engine');
608
+ const targetParent = path.join(rootDir, 'checkout');
609
+ fs.mkdirSync(repo, { recursive: true });
610
+ fs.mkdirSync(targetParent, { recursive: true });
611
+ fs.writeFileSync(path.join(workspace, 'notes.md'), '# loose ground\n');
612
+ git(repo, ['init', '--quiet']);
613
+ git(repo, ['config', 'user.email', 'test@example.com']);
614
+ git(repo, ['config', 'user.name', 'Amalgm Test']);
615
+ fs.writeFileSync(path.join(repo, 'tracked.md'), '# committed\n');
616
+ git(repo, ['add', '.']);
617
+ git(repo, ['commit', '--quiet', '-m', 'initial']);
618
+
619
+ const { unpack } = require('../registration/repo-states');
620
+ const source = createRegistrationService({
621
+ dir: sourceState, homeDir: path.join(rootDir, 'source-home'),
622
+ cloudUserId: 'user-1', cloudDeviceId: 'source-device',
623
+ });
624
+ const target = createRegistrationService({
625
+ dir: targetState, homeDir: path.join(rootDir, 'target-home'),
626
+ cloudUserId: 'user-1', cloudDeviceId: 'target-device',
627
+ });
628
+ try {
629
+ const bootstrap = source.entityCloud.bootstrap(source.registration.registry.syncRecords());
630
+ source.entityCloud.install({
631
+ registry: source.registration.registry, resourceId: bootstrap.resourceId,
632
+ authorityEpoch: 1, headVersion: 0, snapshotBase64: bootstrap.snapshotBase64,
633
+ snapshotChecksum: bootstrap.snapshotChecksum,
634
+ });
635
+ const registered = await source.register(workspace);
636
+
637
+ const sourceDb = new Database(path.join(sourceState, 'identity.db'), { readonly: true });
638
+ const ops = sourceDb.prepare('SELECT operation_json FROM entity_cloud_mutations ORDER BY sequence').all()
639
+ .map((row) => JSON.parse(row.operation_json));
640
+ sourceDb.close();
641
+ const traveled = ops.map((op) => op.record);
642
+ // A loose file lawfully journals twice (structural create, then its
643
+ // first Detect attest); what matters is WHICH entities travel at all.
644
+ assert.deepEqual(
645
+ [...new Set(traveled.map((record) => record.name))].sort(),
646
+ ['ambient', 'engine', 'notes.md'],
647
+ 'the workspace, its loose file, and ONE record per repository travel — repo children do not',
648
+ );
649
+ const repoRecord = traveled.find((record) => record.type === 'repo.git');
650
+ const trackedUuid = (() => {
651
+ const children = source.registration.registry.children(repoRecord.uuid);
652
+ return children.find((child) => child.name === 'tracked.md').uuid;
653
+ })();
654
+
655
+ // The gateway's fold keeps the LAST record per UUID; mirror that here.
656
+ const snapshot = snapshotFromRecords([...new Map(traveled.map((record) => [record.uuid, record])).values()]);
657
+ const snapshotBytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
658
+ target.entityCloud.install({
659
+ registry: target.registration.registry, resourceId: bootstrap.resourceId,
660
+ authorityEpoch: 1, headVersion: 3, snapshotBase64: snapshotBytes.toString('base64'),
661
+ snapshotChecksum: sha256(snapshotBytes),
662
+ });
663
+ const uploaded = [];
664
+ while (true) {
665
+ const part = source.entityCloud.claimContent();
666
+ if (!part) break;
667
+ uploaded.push(part);
668
+ source.entityCloud.acknowledgeContent({
669
+ resourceId: bootstrap.resourceId, contentHash: part.contentHash,
670
+ kind: part.kind, partIndex: part.partIndex,
671
+ });
672
+ }
673
+ for (const part of [...uploaded.filter((entry) => entry.kind === 'manifest'), ...uploaded.filter((entry) => entry.kind === 'chunk')]) {
674
+ target.receiveCloudContent({
675
+ resourceId: bootstrap.resourceId, contentHash: part.contentHash, kind: part.kind,
676
+ partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
677
+ sha256: part.sha256, dataBase64: part.dataBase64,
678
+ });
679
+ }
680
+
681
+ const job = await target.add({ uuid: registered.record.uuid, targetPath: targetParent });
682
+ assert.equal(job.state, 'complete', job.error);
683
+ const destination = path.join(targetParent, 'ambient');
684
+ assert.equal(fs.readFileSync(path.join(destination, 'engine', 'tracked.md'), 'utf8'), '# committed\n');
685
+ assert.equal(target.registration.registry.syncRecord(trackedUuid)?.name, 'tracked.md',
686
+ 'a file inside the nested repository keeps its permanent UUID across machines via the transport map');
687
+ const targetDb = new Database(path.join(targetState, 'identity.db'), { readonly: true });
688
+ try {
689
+ assert.equal(targetDb.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
690
+ 'binding the workspace echoes nothing upstream');
691
+ } finally {
692
+ targetDb.close();
693
+ }
694
+ } finally {
695
+ source.close();
696
+ target.close();
697
+ fs.rmSync(rootDir, { recursive: true, force: true });
698
+ }
699
+ });
@@ -0,0 +1,197 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The coverage law under exam: one OS watcher per outermost tree.
5
+ *
6
+ * Claims, each proven against a REAL fs.watch or the deterministic kit:
7
+ * 1. A nested repo opens ZERO watchers of its own — the encloser's
8
+ * doorbell is its doorbell, and dings still reach its follower raw.
9
+ * 2. Adoption demotes: a self-owned root that becomes enclosed closes
10
+ * its handle. Departure promotes: losing the encloser reopens it.
11
+ * 3. Held doorbells (watchNow) prove the watcher without concluding
12
+ * anything; start() releases them into the ordinary catch-up.
13
+ * 4. An owner's death reconciles the whole covered family, not just
14
+ * the owner.
15
+ * 5. status() tells the truth for covered roots: their health IS the
16
+ * owner's health.
17
+ */
18
+
19
+ const assert = require('node:assert/strict');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const test = require('node:test');
23
+
24
+ const {
25
+ cleanup, eventsOf, fakeWatchKit, makeDir, makeObserver, nudgeUntil, realRepo,
26
+ } = require('./observer.rig');
27
+
28
+ test.after(() => cleanup());
29
+
30
+ /** Watcher handles the kit has open, by watched path. */
31
+ function kitWithBells() {
32
+ const kit = fakeWatchKit();
33
+ const open = new Set();
34
+ const inner = kit.watch;
35
+ kit.watch = (rootPath, onDirty, onDead) => {
36
+ open.add(rootPath);
37
+ const handle = inner(rootPath, onDirty, onDead);
38
+ const close = handle.close;
39
+ handle.close = () => { open.delete(rootPath); close(); };
40
+ return handle;
41
+ };
42
+ kit.open = open;
43
+ return kit;
44
+ }
45
+
46
+ test('a nested repo owns no watcher: the encloser holds the only handle', () => {
47
+ const dir = makeDir('coverage-dedup');
48
+ const repo = path.join(dir, 'project');
49
+ fs.mkdirSync(repo);
50
+ fs.mkdirSync(path.join(repo, '.git'));
51
+ fs.writeFileSync(path.join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n');
52
+ fs.writeFileSync(path.join(repo, 'app.js'), 'code');
53
+
54
+ const kit = kitWithBells();
55
+ const { observer } = makeObserver({ watch: kit.watch });
56
+ observer.enrollRoot(dir);
57
+ observer.start();
58
+
59
+ const roots = observer.listRoots();
60
+ assert.equal(roots.length, 2, 'workspace and nested repo are both roots');
61
+ assert.deepEqual([...kit.open], [dir],
62
+ 'exactly ONE recursive handle exists, on the outermost tree');
63
+
64
+ const repoRoot = roots.find((root) => root.kind === 'repo');
65
+ const covered = observer.status().find((row) => row.rootId === repoRoot.rootId);
66
+ assert.equal(covered.coveredBy, roots.find((root) => root.kind === 'files').rootId);
67
+ assert.ok(covered.watching, 'covered ground reports watching through its owner');
68
+ observer.stop();
69
+ });
70
+
71
+ test('a covered repo still hears raw dings: the encloser forwards, rebased', () => {
72
+ const dir = makeDir('coverage-forward');
73
+ const repo = path.join(dir, 'project');
74
+ fs.mkdirSync(repo);
75
+ fs.mkdirSync(path.join(repo, '.git'));
76
+ fs.writeFileSync(path.join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n');
77
+ fs.writeFileSync(path.join(repo, 'app.js'), 'code');
78
+
79
+ const kit = kitWithBells();
80
+ const { observer, events } = makeObserver({ watch: kit.watch });
81
+ observer.enrollRoot(dir);
82
+ observer.start();
83
+ const repoRoot = observer.listRoots().find((root) => root.kind === 'repo');
84
+ const mark = events.length;
85
+
86
+ // The ding lands on the WORKSPACE watcher, named from the workspace.
87
+ kit.ring(dir, 'project/app.js');
88
+ const spoken = events.slice(mark).filter((event) => event.type === 'repo.changed');
89
+ assert.deepEqual(spoken, [{
90
+ type: 'repo.changed', rootId: repoRoot.rootId, areas: ['worktree'], hint: 'app.js',
91
+ }], 'one raw repo doorbell, instantly, rebased to the repo\'s own ground');
92
+
93
+ // A git-area ding forwards with its area intact.
94
+ kit.ring(dir, 'project/.git/HEAD');
95
+ const gitSpoken = events.filter((event) => event.type === 'repo.changed'
96
+ && event.areas.length === 1 && event.areas[0] === 'git');
97
+ assert.equal(gitSpoken.length, 1, 'the .git ding forwards as git-area news');
98
+ observer.stop();
99
+ });
100
+
101
+ test('adoption demotes and departure promotes: handles follow the outermost tree', () => {
102
+ const parent = makeDir('coverage-parent');
103
+ const repoDir = path.join(parent, 'standalone');
104
+ fs.mkdirSync(repoDir);
105
+ fs.mkdirSync(path.join(repoDir, '.git'));
106
+ fs.writeFileSync(path.join(repoDir, '.git', 'HEAD'), 'ref: refs/heads/main\n');
107
+ fs.writeFileSync(path.join(repoDir, 'main.js'), 'code');
108
+
109
+ const kit = kitWithBells();
110
+ const { observer } = makeObserver({ watch: kit.watch });
111
+ observer.enrollRoot(repoDir);
112
+ observer.start();
113
+ assert.deepEqual([...kit.open], [repoDir], 'standalone: the repo owns its handle');
114
+
115
+ // A workspace registered ABOVE it adopts the coverage.
116
+ observer.enrollRoot(parent);
117
+ assert.deepEqual([...kit.open], [parent],
118
+ 'enclosed by watched ground: the repo handle closes, the workspace handle is the doorbell');
119
+
120
+ // The workspace leaving promotes the repo back to self-owned.
121
+ const workspaceRoot = observer.listRoots().find((root) => root.path === parent);
122
+ observer.removeRoot(workspaceRoot.rootId);
123
+ assert.deepEqual([...kit.open], [repoDir], 'departure promotes: the repo reopens its own handle');
124
+ observer.stop();
125
+ });
126
+
127
+ test('watchNow proves the doorbell and holds it; start() releases into catch-up', () => {
128
+ const dir = makeDir('coverage-held');
129
+ fs.writeFileSync(path.join(dir, 'note.md'), 'v1');
130
+ const kit = kitWithBells();
131
+ const { observer, events } = makeObserver({ watch: kit.watch });
132
+ const root = observer.enrollRoot(dir, { scan: false, watch: false });
133
+
134
+ const health = observer.watchNow(root.rootId);
135
+ assert.ok(health.watching, 'the watcher is born and provable before anything commits');
136
+ assert.deepEqual([...kit.open], [dir], 'the handle exists during the hold');
137
+
138
+ kit.ring(dir, 'note.md');
139
+ assert.deepEqual(events, [], 'held dings conclude nothing — no scan, no event');
140
+
141
+ observer.start(root.rootId);
142
+ assert.ok(eventsOf(events, 'file.created').some((event) => event.path === 'note.md'),
143
+ 'start() releases the hold and its scan is the catch-up');
144
+ observer.stop();
145
+ });
146
+
147
+ test('an owner death reconciles the covered family', async () => {
148
+ const dir = makeDir('coverage-death');
149
+ const repo = path.join(dir, 'project');
150
+ fs.mkdirSync(repo);
151
+ fs.mkdirSync(path.join(repo, '.git'));
152
+ fs.writeFileSync(path.join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n');
153
+
154
+ let dieNow = null;
155
+ const kit = kitWithBells();
156
+ const inner = kit.watch;
157
+ kit.watch = (rootPath, onDirty, onDead) => {
158
+ const handle = inner(rootPath, onDirty, onDead);
159
+ if (rootPath === dir) dieNow = () => { handle.degraded = true; onDead?.(); };
160
+ return handle;
161
+ };
162
+ const { observer, events } = makeObserver({ watch: kit.watch });
163
+ observer.enrollRoot(dir);
164
+ observer.start();
165
+ const repoRoot = observer.listRoots().find((root) => root.kind === 'repo');
166
+ const mark = events.length;
167
+
168
+ dieNow();
169
+ const caughtUp = events.slice(mark).filter((event) => event.type === 'repo.changed'
170
+ && event.rootId === repoRoot.rootId && event.hint === null);
171
+ assert.equal(caughtUp.length, 1,
172
+ 'the revived owner reconciles the covered repo too: one unconditional catch-up doorbell');
173
+ observer.stop();
174
+ });
175
+
176
+ test('live ground truth: a real nested repo change reaches its follower through the shared watcher', async () => {
177
+ const dir = makeDir('coverage-live');
178
+ const repo = path.join(dir, 'project');
179
+ fs.mkdirSync(repo);
180
+ fs.mkdirSync(path.join(repo, '.git'));
181
+ fs.writeFileSync(path.join(repo, '.git', 'HEAD'), 'ref: refs/heads/main\n');
182
+ fs.writeFileSync(path.join(repo, 'app.js'), 'v1');
183
+
184
+ const { observer, events } = makeObserver(); // REAL fs.watch
185
+ observer.enrollRoot(dir);
186
+ observer.start();
187
+ const repoRoot = observer.listRoots().find((root) => root.kind === 'repo');
188
+ const mark = events.length;
189
+
190
+ const arrived = await nudgeUntil(
191
+ () => fs.writeFileSync(path.join(repo, 'app.js'), 'v2'),
192
+ () => events.slice(mark).some((event) => event.type === 'repo.changed'
193
+ && event.rootId === repoRoot.rootId),
194
+ );
195
+ assert.ok(arrived, 'a real OS event on covered ground reaches the repo through the one shared watcher');
196
+ observer.stop();
197
+ });