amalgm 0.1.265 → 0.1.266
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/observer/README.md +4 -1
- package/runtime/scripts/amalgm-mcp/observer/index.js +30 -5
- package/runtime/scripts/amalgm-mcp/registration/index.js +73 -19
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +14 -4
- package/runtime/scripts/amalgm-mcp/registration/tree.js +4 -1
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +14 -1
- package/runtime/scripts/amalgm-mcp/registry/index.js +7 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +5 -1
package/package.json
CHANGED
|
@@ -94,7 +94,10 @@ never merges, never decides truth — it emits facts for the layers above.
|
|
|
94
94
|
(catch-up for the window we were dead), watcher death (rebuild the
|
|
95
95
|
doorbell, reconcile the gap once), and whenever a consumer asks — a sync
|
|
96
96
|
session opening, a document opening, an app regaining focus are events
|
|
97
|
-
too, and they belong to the layer above.
|
|
97
|
+
too, and they belong to the layer above. A freshly registered repo is the
|
|
98
|
+
one adapter-defined birth exception: its Card + Checkpoint is already the
|
|
99
|
+
complete first observation, so it arms Watch without scanning every child
|
|
100
|
+
again. Restart still performs the ordinary conservative catch-up scan.
|
|
98
101
|
4. **Echo suppression is content, not bookkeeping.** A writer records its own
|
|
99
102
|
write (`noteWrite`); a scan that finds the recorded hash finds nothing.
|
|
100
103
|
5. **Identity has two axes, never mixed — and KIND decides which axis
|
|
@@ -1229,6 +1229,22 @@ function createObserver(options) {
|
|
|
1229
1229
|
}
|
|
1230
1230
|
scanFilesRoot(root, { allowRootRepository: true });
|
|
1231
1231
|
},
|
|
1232
|
+
/**
|
|
1233
|
+
* Structural registration already knows its nested Git boundaries from
|
|
1234
|
+
* the same state parcel that names them. Refresh only that in-memory
|
|
1235
|
+
* forwarding map after it installs those observer roots; this is not a
|
|
1236
|
+
* filesystem census and it neither reads nor records worktree files.
|
|
1237
|
+
*/
|
|
1238
|
+
refreshRepoTerritories(rootId) {
|
|
1239
|
+
const root = store.listRoots().find((row) => row.rootId === rootId);
|
|
1240
|
+
if (!root || root.kind !== 'repo') {
|
|
1241
|
+
throw new Error(`refreshRepoTerritories requires a live repo root: ${rootId}`);
|
|
1242
|
+
}
|
|
1243
|
+
const state = live.get(root.rootId);
|
|
1244
|
+
if (!state) return;
|
|
1245
|
+
state.territories = new Set(childRepoRoots(root)
|
|
1246
|
+
.map((child) => child.path.slice(root.path.length + 1)));
|
|
1247
|
+
},
|
|
1232
1248
|
scanNow,
|
|
1233
1249
|
|
|
1234
1250
|
// The referee (verify.js): re-hash every enrolled byte, report every
|
|
@@ -1313,7 +1329,7 @@ function createObserver(options) {
|
|
|
1313
1329
|
return statusOf(root);
|
|
1314
1330
|
},
|
|
1315
1331
|
|
|
1316
|
-
start(rootId = null) {
|
|
1332
|
+
start(rootId = null, { census = true } = {}) {
|
|
1317
1333
|
const firstStart = !started;
|
|
1318
1334
|
if (firstStart) started = true;
|
|
1319
1335
|
if (!firstStart && rootId === null) return;
|
|
@@ -1328,10 +1344,19 @@ function createObserver(options) {
|
|
|
1328
1344
|
const state = attach(root, { watch: true });
|
|
1329
1345
|
state.held = false;
|
|
1330
1346
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1347
|
+
// A normal root needs its first observed census before its structural
|
|
1348
|
+
// registration can become evidence. A freshly registered Git root is
|
|
1349
|
+
// different: its already-committed Card + Checkpoint is that complete
|
|
1350
|
+
// initial observation. Re-walking its worktree here would duplicate
|
|
1351
|
+
// the adapter's state (and can block the runtime for a large repo).
|
|
1352
|
+
// `census: false` is therefore only the repo adapter's birth path;
|
|
1353
|
+
// later recoveries and runtime start retain the conservative census.
|
|
1354
|
+
if (census) {
|
|
1355
|
+
try {
|
|
1356
|
+
for (const root of roots) scanNow(root.rootId);
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
console.warn('[Observer] initial scan failed:', error?.message || error);
|
|
1359
|
+
}
|
|
1335
1360
|
}
|
|
1336
1361
|
},
|
|
1337
1362
|
|
|
@@ -172,13 +172,14 @@ function createRegistration(options) {
|
|
|
172
172
|
for (const { scopeId } of memory.allBindings()) {
|
|
173
173
|
if (!liveIds.has(scopeId)) memory.dropScope(scopeId);
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
// Retirement is decided from durable witness bindings, not from the
|
|
176
|
+
// in-memory lens map. A fresh runtime has no lenses yet, but it may have
|
|
177
|
+
// restarted halfway through retiring an adopted tree; letting that old
|
|
178
|
+
// observer root survive would make its next start look like a second
|
|
179
|
+
// tree. The binding is the evidence that survives the crash, so it is
|
|
180
|
+
// the only honest starting point.
|
|
181
|
+
for (const row of observer.listRoots()) {
|
|
182
|
+
const record = rootRecordOf(row.rootId);
|
|
182
183
|
const nestedRepoScope = row.kind === 'repo' && record?.type === 'repo.git'
|
|
183
184
|
&& observer.listRoots().some((other) => other.rootId !== row.rootId
|
|
184
185
|
&& row.path.startsWith(`${other.path}/`));
|
|
@@ -190,15 +191,18 @@ function createRegistration(options) {
|
|
|
190
191
|
// sweep above collects. The reverse order once minted a duplicate
|
|
191
192
|
// tree: an unbound living root looks like fresh ground.
|
|
192
193
|
const family = observer.listRoots()
|
|
193
|
-
.filter((member) => member.rootId === rootId || member.path.startsWith(`${row.path}/`));
|
|
194
|
+
.filter((member) => member.rootId === row.rootId || member.path.startsWith(`${row.path}/`));
|
|
194
195
|
for (const member of family) {
|
|
195
|
-
if (member.rootId !== rootId) observer.removeRoot(member.rootId);
|
|
196
|
+
if (member.rootId !== row.rootId) observer.removeRoot(member.rootId);
|
|
196
197
|
}
|
|
197
|
-
observer.removeRoot(rootId);
|
|
198
|
-
memory.dropScope(rootId);
|
|
199
|
-
lenses.delete(rootId);
|
|
198
|
+
observer.removeRoot(row.rootId);
|
|
199
|
+
memory.dropScope(row.rootId);
|
|
200
|
+
lenses.delete(row.rootId);
|
|
200
201
|
}
|
|
201
202
|
}
|
|
203
|
+
for (const [rootId] of [...lenses]) {
|
|
204
|
+
if (!observer.listRoots().some((row) => row.rootId === rootId)) lenses.delete(rootId);
|
|
205
|
+
}
|
|
202
206
|
for (const root of treeRoots()) {
|
|
203
207
|
if (lenses.has(root.rootId)) continue;
|
|
204
208
|
const lens = createEvidence({ observer, registry, memory, rootId: root.rootId, classify });
|
|
@@ -508,18 +512,67 @@ function createRegistration(options) {
|
|
|
508
512
|
throw error;
|
|
509
513
|
}
|
|
510
514
|
|
|
511
|
-
const startWatch = () =>
|
|
512
|
-
observer.start(root.rootId);
|
|
513
|
-
ensureTrees();
|
|
514
|
-
reconcileInitialRepoWorktrees(root.path);
|
|
515
|
-
flushIfDirty();
|
|
516
|
-
requireLiveWatchers(root.path);
|
|
517
|
-
};
|
|
515
|
+
const startWatch = () => startRepositoryFromBaseline(root);
|
|
518
516
|
if (suppressJournal) registry.withoutJournal(startWatch);
|
|
519
517
|
else startWatch();
|
|
520
518
|
return { rootPath: root.path, record: rootRecordOf(root.rootId) };
|
|
521
519
|
}
|
|
522
520
|
|
|
521
|
+
/**
|
|
522
|
+
* A repo's initial Card + Checkpoint is not preliminary data awaiting a
|
|
523
|
+
* generic filesystem census: it *is* the first complete state. Start its
|
|
524
|
+
* follower while the watcher is held, then release the watcher without
|
|
525
|
+
* scanning every worktree child a second time. The next raw ding reaches
|
|
526
|
+
* the follower and exactly one settled state authorizes the ordinary child
|
|
527
|
+
* reconciliation. This keeps Register synchronous without duplicating
|
|
528
|
+
* either its cargo capture or its initial Detect pass.
|
|
529
|
+
*/
|
|
530
|
+
function startRepositoryFromBaseline(root) {
|
|
531
|
+
// Structural registration already knows every nested repo boundary from
|
|
532
|
+
// the same assignment that packed its state. Install those territories
|
|
533
|
+
// directly from that identity graph; discovering them again by walking
|
|
534
|
+
// files would reintroduce the duplicate initial census this adapter
|
|
535
|
+
// exists to avoid.
|
|
536
|
+
const territories = [];
|
|
537
|
+
for (const record of registry.activeByType('repo.git')) {
|
|
538
|
+
const repoPath = addressOf(record.uuid);
|
|
539
|
+
if (repoPath === null || (repoPath !== root.path && !repoPath.startsWith(`${root.path}/`))) continue;
|
|
540
|
+
let territory = observer.listRoots().find((candidate) => candidate.path === repoPath) ?? null;
|
|
541
|
+
if (!territory) territory = observer.enrollRoot(repoPath, { scan: false, watch: false });
|
|
542
|
+
if (territory.kind !== 'repo') {
|
|
543
|
+
throw new Error(`registered repository ${repoPath} is no longer Git ground before Watch began`);
|
|
544
|
+
}
|
|
545
|
+
memory.bind(territory.rootId, territory.rootId, record.uuid);
|
|
546
|
+
if (!lenses.has(territory.rootId)) {
|
|
547
|
+
// A repo baseline is already authoritative state. The lens exists so
|
|
548
|
+
// callers can address the tree, but does not reconcile absent
|
|
549
|
+
// observer child rows as if that state had never been captured.
|
|
550
|
+
lenses.set(territory.rootId, createEvidence({
|
|
551
|
+
observer, registry, memory, rootId: territory.rootId, classify,
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
territories.push(territory);
|
|
555
|
+
}
|
|
556
|
+
territories.sort((left, right) => left.path.length - right.path.length);
|
|
557
|
+
// The outer raw watcher must forward a nested repo's first later ding
|
|
558
|
+
// to that nested follower. Its territory set is normally filled by a
|
|
559
|
+
// census; structural registration already supplied the same boundaries,
|
|
560
|
+
// so refresh the forwarding map without performing that census.
|
|
561
|
+
for (const territory of territories) observer.refreshRepoTerritories(territory.rootId);
|
|
562
|
+
repoFollowers.sync({ startOnBirth: false });
|
|
563
|
+
for (const territory of territories) observer.start(territory.rootId, { census: false });
|
|
564
|
+
requireLiveWatchers(root.path);
|
|
565
|
+
|
|
566
|
+
// Edge discovery is a distinct registration promise. It only reads link
|
|
567
|
+
// surfaces (repo roots answer that with a link census), never converts
|
|
568
|
+
// the just-packed worktree into generic per-file evidence. The newly
|
|
569
|
+
// reached roots then receive their usual identity/lens setup.
|
|
570
|
+
edgesDirty = true;
|
|
571
|
+
tryFollow();
|
|
572
|
+
ensureTrees();
|
|
573
|
+
repoFollowers.sync({ startOnBirth: false });
|
|
574
|
+
}
|
|
575
|
+
|
|
523
576
|
/**
|
|
524
577
|
* Plan all repository cargo under one selected root before its identity
|
|
525
578
|
* transaction begins. This work can be slow (a Card contains a Git bundle
|
|
@@ -876,6 +929,7 @@ function createRegistration(options) {
|
|
|
876
929
|
// mutation. Keep the whole first Watch/Detect transition inside the
|
|
877
930
|
// existing journal boundary used for the seeded structural bind.
|
|
878
931
|
const startWatch = () => {
|
|
932
|
+
if (root.kind === 'repo') return startRepositoryFromBaseline(root);
|
|
879
933
|
observer.start(root.rootId);
|
|
880
934
|
ensureTrees();
|
|
881
935
|
reconcileInitialRepoWorktrees(root.path);
|
|
@@ -76,7 +76,16 @@ function createRepoFollowers({
|
|
|
76
76
|
* move, be adopted, or cease being a repository; in every case the old
|
|
77
77
|
* follower is closed before a replacement can speak.
|
|
78
78
|
*/
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* A freshly registered repo already has a Card + Checkpoint baseline and
|
|
81
|
+
* its watcher is still held. Its follower must exist before that hold is
|
|
82
|
+
* released, but it must not immediately spend a second pair of full Git
|
|
83
|
+
* captures just to rediscover the state it was born with. Any ding heard
|
|
84
|
+
* during the release rings this follower normally. A cold runtime restart
|
|
85
|
+
* keeps the default (`startOnBirth: true`) and independently proves the
|
|
86
|
+
* persisted baseline against disk.
|
|
87
|
+
*/
|
|
88
|
+
function sync({ startOnBirth = true } = {}) {
|
|
80
89
|
if (stopped) return;
|
|
81
90
|
const roots = observer.listRoots().filter((root) => root.kind === 'repo');
|
|
82
91
|
const live = new Set(roots.map((root) => root.rootId));
|
|
@@ -143,9 +152,10 @@ function createRepoFollowers({
|
|
|
143
152
|
},
|
|
144
153
|
});
|
|
145
154
|
followers.set(root.rootId, { path: root.path, uuid: record.uuid, follower });
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
155
|
+
// A restart catches up from disk; a fresh Register has already
|
|
156
|
+
// captured its exact baseline and will receive held doorbells when
|
|
157
|
+
// Watch releases, so it does not duplicate the expensive birth pair.
|
|
158
|
+
if (startOnBirth) void follower.start();
|
|
149
159
|
}
|
|
150
160
|
}
|
|
151
161
|
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
* although they have identical behavior today. A structural walk therefore
|
|
24
24
|
* uses file.text as the content-free ordinary-file placeholder. The first
|
|
25
25
|
* Detect pass immediately attests the actual bytes and corrects that
|
|
26
|
-
* representation through the
|
|
26
|
+
* representation through the first later settled repo reconciliation. The
|
|
27
|
+
* initial Card + Checkpoint is authoritative content state; repeating a
|
|
28
|
+
* generic per-file scan merely to refine this local physical label would
|
|
29
|
+
* duplicate that state and can freeze large registration.
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
32
|
const fs = require('fs');
|
|
@@ -622,7 +622,20 @@ function createEvidence(options) {
|
|
|
622
622
|
const parentUuid = ensureDir(path.dirname(territory) === '.' ? '' : path.dirname(territory), root);
|
|
623
623
|
const name = path.basename(territory);
|
|
624
624
|
const at = registry.activeAt(parentUuid, name);
|
|
625
|
-
if (at?.type
|
|
625
|
+
if (at?.type === 'repo.git') continue;
|
|
626
|
+
const territoryRoot = observer.listRoots()
|
|
627
|
+
.find((row) => row.kind === 'repo' && row.path === path.join(groundPath, territory));
|
|
628
|
+
const arrivedRecord = arrivedTree(territoryRoot?.device, territoryRoot?.inode, REPO_TREE);
|
|
629
|
+
if (!arrivedRecord) continue;
|
|
630
|
+
// A Git territory is the repo-shaped form of the same adoption law
|
|
631
|
+
// directories use above. Its initial Card + Checkpoint already
|
|
632
|
+
// travels with the existing repo UUID, so the new observer root must
|
|
633
|
+
// adopt that identity — never mint or ignore a half-repository.
|
|
634
|
+
if (at) {
|
|
635
|
+
nowDivergent.set(territory, null); // two identity claims: decide neither
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
registry.move(arrivedRecord.uuid, { parentUUID: parentUuid, name });
|
|
626
639
|
}
|
|
627
640
|
// A lifted lens is the reverse transition on the same record.
|
|
628
641
|
for (const { record, rel } of walkActive(root.uuid, '', [])) {
|
|
@@ -750,6 +750,13 @@ function createRegistry(options) {
|
|
|
750
750
|
/** Full logical graph for cloud snapshots. Machine bindings and evidence
|
|
751
751
|
* are intentionally absent. */
|
|
752
752
|
syncRecords: () => store.allEntities().map(syncRecord),
|
|
753
|
+
/** Active records of one physical type. This is a query primitive for
|
|
754
|
+
* type adapters; it does not expose storage writes or weaken the single
|
|
755
|
+
* registry boundary. */
|
|
756
|
+
activeByType: (type) => {
|
|
757
|
+
if (!TYPES.has(type)) throw new Error(`unknown entity type ${JSON.stringify(type)}`);
|
|
758
|
+
return store.activeByType(type).map(record);
|
|
759
|
+
},
|
|
753
760
|
/** Every registered tree's top record. */
|
|
754
761
|
roots: () => store.activeRoots().map(record),
|
|
755
762
|
/** Every active managed reference with the identity it targets. The
|
|
@@ -608,13 +608,17 @@ test('adding a repository registers its allowed tree and saves its initial Repoc
|
|
|
608
608
|
assert.ok(initialState?.stateId, 'registration saved the initial Card + Checkpoint');
|
|
609
609
|
assert.ok(initialState.checkpoint.untracked.some((file) => file.path === 'readme.md'),
|
|
610
610
|
'the persisted baseline contains the actual working checkpoint, not only its hash');
|
|
611
|
+
const observerRoot = service.observer.listRoots().find((root) => root.path === repo);
|
|
612
|
+
assert.deepEqual(service.observer.filesForRoot(observerRoot.rootId), [],
|
|
613
|
+
'repo birth does not perform a second per-file census after its Card + Checkpoint is saved');
|
|
611
614
|
assert.equal(tree.root().version, registered.version,
|
|
612
615
|
'add returned only after the repo entity pointed at its saved baseline');
|
|
613
616
|
const readme = tree.registry.activeAt(registered.uuid, 'readme.md');
|
|
614
617
|
const src = tree.registry.activeAt(registered.uuid, 'src');
|
|
615
618
|
assert.equal(readme.type, 'file.text');
|
|
616
619
|
assert.equal(src.type, 'folder');
|
|
617
|
-
assert.equal(tree.registry.activeAt(src.uuid, 'blob.bin').type, 'file.
|
|
620
|
+
assert.equal(tree.registry.activeAt(src.uuid, 'blob.bin').type, 'file.text',
|
|
621
|
+
'repo birth records structural leaf shape; the Card + Checkpoint owns its actual bytes without a second read');
|
|
618
622
|
assert.equal(tree.registry.activeAt(registered.uuid, 'node_modules'), null,
|
|
619
623
|
'the same enrollment policy excludes dependency trees inside repos');
|
|
620
624
|
const initial = tree.root();
|