amalgm 0.1.263 → 0.1.264

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.263",
3
+ "version": "0.1.264",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -15,8 +15,10 @@
15
15
  * (its own future head and rail), never an identity boundary. Every
16
16
  * top-level enrolled ground — a workspace, a repository, a single file,
17
17
  * or an explicitly added link — is such a tree, with one evidence LENS
18
- * over its observer root (nested repo territories live INSIDE their
19
- * tree as repo.git records; they are never trees of their own). A
18
+ * over its observer root. A Git territory noticed inside another watched
19
+ * tree is observer-only until an explicit registration admits it; that
20
+ * selected repo is then a tree of its own even if the outer watcher covers
21
+ * its local address. A
20
22
  * TREE'S IDENTITY IS ITS UUID, PERMANENT; its root path is only the
21
23
  * current local address — lenses are keyed by the observer's rootId
22
24
  * (the witness), the observer rescues moved ground by physical
@@ -66,6 +68,7 @@ const { refusal } = require('./refusal');
66
68
  const { createRepoFollowers } = require('./repo-followers');
67
69
  const { createRepoStates } = require('./repo-states');
68
70
  const { registerTree } = require('./tree');
71
+ const { gitDirTruth } = require('../observer/scan');
69
72
 
70
73
  // Events that can change what the recorded edges reach: a new or
71
74
  // retargeted pointer, repo ground moving under its card (a worktree
@@ -112,12 +115,27 @@ function createRegistration(options) {
112
115
  onSettled: reconcileSettledRepo,
113
116
  });
114
117
 
115
- /** Tree roots are the non-enclosed observer roots: a nested repo
116
- * territory belongs to the tree that encloses it. */
118
+ /**
119
+ * A semantic tree is defined by its parentless identity record, never by
120
+ * directory containment. `amalgm add` may materialize a cloud repo inside
121
+ * the local `workspaces/` directory, and `amalgm register` may select a
122
+ * repo already sitting there: both remain their own cloud root. The outer
123
+ * workspace watcher supplies coverage, but it does not own the repo's
124
+ * Card + Checkpoint or identity tree.
125
+ *
126
+ * An unbound observer root is a tree candidate only when no other observer
127
+ * root physically encloses it. That preserves the admission rule for a
128
+ * newly discovered nested repo: Watch can notice it, but only explicit
129
+ * registration can give it a cloud identity and state.
130
+ */
117
131
  function treeRoots() {
118
132
  const roots = observer.listRoots();
119
- return roots.filter((root) => !roots.some((other) =>
120
- other.rootId !== root.rootId && root.path.startsWith(`${other.path}/`)));
133
+ return roots.filter((root) => {
134
+ const record = rootRecordOf(root.rootId);
135
+ if (record) return record.parentUUID === null;
136
+ return !roots.some((other) => other.rootId !== root.rootId
137
+ && root.path.startsWith(`${other.path}/`));
138
+ });
121
139
  }
122
140
 
123
141
  function treeRootCovering(absPath) {
@@ -413,6 +431,75 @@ function createRegistration(options) {
413
431
  }
414
432
  }
415
433
 
434
+ /**
435
+ * A repository selected inside already-watched ground is still a new
436
+ * registered TREE, not a child record of the outer workspace. The outer
437
+ * watcher already knows its territory (or this enrollment establishes that
438
+ * same territory), so the only missing fact is the repo's atomic
439
+ * Card+Checkpoint-backed identity birth.
440
+ */
441
+ function registerRepositoryInsideCoveredGround(ground, { identityAt = null, suppressJournal = false } = {}) {
442
+ let root = observer.listRoots().find((candidate) => candidate.path === ground) || null;
443
+ if (!root) root = observer.enrollRoot(ground, { scan: false, watch: false });
444
+ if (root.kind !== 'repo') {
445
+ throw new Error(`expected Git repository ground at ${ground}, found ${root.kind}`);
446
+ }
447
+
448
+ const bound = memory.boundTo(root.rootId);
449
+ if (bound !== undefined) {
450
+ const record = registry.record(bound);
451
+ if (record?.type !== 'repo.git') {
452
+ throw new Error(`repository watcher ${ground} is bound to non-repository entity ${bound}`);
453
+ }
454
+ if (record.parentUUID !== null) {
455
+ // This is the explicit nested-boundary case created as part of an
456
+ // already-registered enclosing repo. It already has a complete
457
+ // Card+Checkpoint state and cannot be duplicated at one address.
458
+ requireLiveWatchers(ground);
459
+ return { rootPath: ground, record };
460
+ }
461
+ requireLiveWatchers(ground);
462
+ return { rootPath: ground, record };
463
+ }
464
+
465
+ try {
466
+ requireWatchBirth(root);
467
+ assertWitnessed({ observer, registry, memory });
468
+ const register = () => registry.transaction(() => {
469
+ registerTree({
470
+ root,
471
+ registry,
472
+ memory,
473
+ shouldEnroll: observer.shouldEnroll,
474
+ captureRepo,
475
+ repoStates,
476
+ repoStateAt,
477
+ identityAt,
478
+ });
479
+ assertWatchedThroughCommit(root.path);
480
+ });
481
+ if (suppressJournal) registry.withoutJournal(register);
482
+ else register();
483
+ } catch (error) {
484
+ // A root that existed only because the outer watcher discovered its
485
+ // territory must remain coverage after a refused explicit admission;
486
+ // do not remove it (the parent still owns the OS watcher). A root this
487
+ // call itself enrolled has no durable identity and can safely retire.
488
+ throw error;
489
+ }
490
+
491
+ const startWatch = () => {
492
+ observer.start(root.rootId);
493
+ ensureTrees();
494
+ reconcileInitialRepoWorktrees(root.path);
495
+ flushIfDirty();
496
+ requireLiveWatchers(root.path);
497
+ };
498
+ if (suppressJournal) registry.withoutJournal(startWatch);
499
+ else startWatch();
500
+ return { rootPath: root.path, record: rootRecordOf(root.rootId) };
501
+ }
502
+
416
503
  /** Real ground -> the UUID of the entity that owns it, across trees. */
417
504
  function resolveGround(real) {
418
505
  const home = treeRootCovering(real);
@@ -542,6 +629,23 @@ function createRegistration(options) {
542
629
  }
543
630
  const covering = treeRootCovering(ground);
544
631
  if (covering) {
632
+ // A repository has a different state adapter from the workspace that
633
+ // happens to contain its local address. Its observer territory may
634
+ // already exist, but it is not registered until this explicit command
635
+ // creates the parentless repo tree with its initial Card + Checkpoint.
636
+ // The same branch is used by cloud `add`: materialized repos commonly
637
+ // land under the default `workspaces/` directory while retaining their
638
+ // cloud root UUID and parentless graph position.
639
+ const isRepository = fs.lstatSync(ground).isDirectory() && gitDirTruth(ground) === 'present';
640
+ if (isRepository) {
641
+ const exact = observer.listRoots().find((root) => root.path === ground);
642
+ const known = exact && memory.boundTo(exact.rootId) !== undefined
643
+ ? registry.record(memory.boundTo(exact.rootId))
644
+ : null;
645
+ if (!known || known.type !== 'repo.git' || known.parentUUID === null) {
646
+ return registerRepositoryInsideCoveredGround(ground, { identityAt, suppressJournal });
647
+ }
648
+ }
545
649
  // This address is already registered ground. Its prior Register →
546
650
  // Watch transition remains the proof of its identity; repeating a
547
651
  // harmless "ensure the safe home exists" request must not rescan every
@@ -80,7 +80,8 @@
80
80
  * the folder record and every descendant's derived address follows,
81
81
  * which is why folders reconcile before files, parents before
82
82
  * children;
83
- * - repo territories as repo.git type transitions on the slot's record;
83
+ * - repo territories as observer truth, with an explicitly registered
84
+ * repository boundary supplying the corresponding repo.git record;
84
85
  * - ADOPTION, the channel law completing across trees: unclaimed
85
86
  * ground wearing the physical identity of an orphaned top-level tree
86
87
  * is that tree's arrival — one lawful parent update on its
@@ -604,23 +605,24 @@ function createEvidence(options) {
604
605
  };
605
606
  settleClaims(claimsOf(desiredDirs, 'dir'), 'dir');
606
607
 
607
- // Territories after folder continuity, before file ground: a
608
- // boundary is a repo.git type transition on the slot's record, and
609
- // the slot must already be where its folder channel says it is.
608
+ // Territories after folder continuity, before file ground. A Git
609
+ // boundary is NOT ordinary filesystem evidence: every repo.git record
610
+ // needs a Card + Checkpoint state and immutable transport before it can
611
+ // travel. This generic lens has neither, so it may only recognize a
612
+ // boundary that the registration adapter already made whole. An
613
+ // unregistered repository remains a local observer territory until
614
+ // `amalgm register <repo>` admits it through that adapter.
615
+ //
616
+ // This is deliberately not a convenience gate. Letting this layer
617
+ // mint or promote repo.git would create an identity record whose cloud
618
+ // head names no state — the exact invalid half-repository the cloud
619
+ // contract rejects. Folders and files are evidence-shaped; Git is a
620
+ // type-specific state carrier.
610
621
  for (const territory of [...territories].sort()) {
611
622
  const parentUuid = ensureDir(path.dirname(territory) === '.' ? '' : path.dirname(territory), root);
612
623
  const name = path.basename(territory);
613
624
  const at = registry.activeAt(parentUuid, name);
614
- if (!at) {
615
- // Adoption before birth here too: a registered top-level repo
616
- // TREE that moved in arrives as its own record, never a fresh
617
- // territory record. The enclosed observer root's identity is
618
- // the evidence — the same physical dir the orphaned tree owns.
619
- const enclosed = observer.listRoots().find((row) => row.path === path.join(groundPath, territory));
620
- const arrivedRecord = enclosed ? arrivedTree(enclosed.device, enclosed.inode, REPO_TREE) : null;
621
- if (arrivedRecord) registry.move(arrivedRecord.uuid, { parentUUID: parentUuid, name });
622
- else registry.create({ type: 'repo.git', parentUUID: parentUuid, name });
623
- } else if (at.type !== 'repo.git') registry.changeType(at.uuid, 'repo.git');
625
+ if (at?.type !== 'repo.git') continue;
624
626
  }
625
627
  // A lifted lens is the reverse transition on the same record.
626
628
  for (const { record, rel } of walkActive(root.uuid, '', [])) {
@@ -369,6 +369,11 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
369
369
  snapshotChecksum: sha256(snapshotBytes),
370
370
  });
371
371
 
372
+ // This is the real user destination: a repo lands below an already-live
373
+ // local workspaces-like container. Physical containment supplies watcher
374
+ // coverage only; the cloud repo must retain its parentless root identity.
375
+ target.registration.add(targetParent, { suppressJournal: true });
376
+
372
377
  const planned = source.entityCloud.contentHeads(records);
373
378
  assert.deepEqual(
374
379
  planned.map(({ contentHash }) => contentHash),
@@ -406,6 +411,8 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
406
411
  const expected = source.registration.repoState(registered.record.uuid);
407
412
  assert.equal(verify(destination, expected).ok, true, 'the downloaded Git state reproduces its Card + Checkpoint IDs');
408
413
  assert.equal(target.registration.registry.syncRecord(registered.record.uuid).uuid, registered.record.uuid);
414
+ assert.equal(target.registration.registry.syncRecord(registered.record.uuid).parentUUID, null,
415
+ 'a repo materialized beneath an existing local workspace keeps its cloud root position');
409
416
  assert.equal(target.observer.status().find((entry) => entry.path === fs.realpathSync(destination))?.watching, true,
410
417
  'the same successful add starts the destination repository watcher');
411
418
 
@@ -724,6 +724,45 @@ test('a nested repository has its own baseline and watcher, while its tree remai
724
724
  assert.equal(tree.registry.activeAt(src.uuid, 'live.js').type, 'file.text');
725
725
  });
726
726
 
727
+ test('a repo discovered inside watched workspace ground stays unregistered until explicit register gives it one complete repo root', async () => {
728
+ const service = makeService('workspace-repo-admission');
729
+ const workspace = makeGround('workspace-repo-admission');
730
+ const { record: workspaceRecord } = await service.register(workspace);
731
+ const repo = path.join(workspace, 't3code');
732
+ fs.mkdirSync(repo);
733
+ execFileSync('git', ['init', '-q'], { cwd: repo });
734
+ fs.writeFileSync(path.join(repo, 'readme.md'), '# T3\n');
735
+
736
+ // This is the production ordering that convicted the live Fly run: the
737
+ // outer workspace watcher discovers a newly cloned repository first.
738
+ const workspaceRoot = service.observer.listRoots().find((root) => root.path === workspace);
739
+ service.observer.scanNow(workspaceRoot.rootId);
740
+ await new Promise((resolve) => setImmediate(resolve));
741
+ service.registration.flushIfDirty();
742
+
743
+ const territory = service.observer.listRoots().find((root) => root.path === repo);
744
+ assert.equal(territory?.kind, 'repo', 'Watch knows the local Git territory');
745
+ assert.notEqual(
746
+ service.registration.tree(workspace).registry.activeAt(workspaceRecord.uuid, 't3code')?.type,
747
+ 'repo.git',
748
+ 'generic filesystem evidence never mints a half-repository with no Card + Checkpoint',
749
+ );
750
+
751
+ const { record } = await service.register(repo);
752
+ assert.equal(record.type, 'repo.git');
753
+ assert.equal(record.parentUUID, null,
754
+ 'selected repo is its own cloud tree even though its local address is inside workspaces');
755
+ assert.equal(service.registration.tree(repo).root().uuid, record.uuid,
756
+ 'the nested observer root now witnesses that selected tree');
757
+ assert.ok(service.registration.repoState(record.uuid)?.stateId,
758
+ 'its identity and initial Card + Checkpoint were committed together');
759
+ assert.equal(
760
+ service.observer.status().some((row) => row.path === repo && row.watching && !row.degraded),
761
+ true,
762
+ 'the selected repo is live after the atomic registration',
763
+ );
764
+ });
765
+
727
766
  test('a reopened repo watcher resumes from the saved Card + Checkpoint baseline', async () => {
728
767
  const service = makeService('repo-reopen');
729
768
  const repo = makeGround('repo-reopen');