amalgm 0.1.246 → 0.1.247

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 (49) hide show
  1. package/README.md +5 -3
  2. package/lib/cli.js +92 -4
  3. package/lib/shared-realtime-tunnel.js +82 -19
  4. package/package.json +1 -1
  5. package/runtime/scripts/amalgm-mcp/automations/deliveries.js +276 -0
  6. package/runtime/scripts/amalgm-mcp/automations/rest.js +51 -0
  7. package/runtime/scripts/amalgm-mcp/automations/scheduler.js +11 -67
  8. package/runtime/scripts/amalgm-mcp/automations/store.js +67 -20
  9. package/runtime/scripts/amalgm-mcp/events/ingress.js +69 -215
  10. package/runtime/scripts/amalgm-mcp/events/processor.js +189 -0
  11. package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +41 -0
  12. package/runtime/scripts/amalgm-mcp/lib/email-md.js +107 -1
  13. package/runtime/scripts/amalgm-mcp/lib/generated/email-render.json +10 -2
  14. package/runtime/scripts/amalgm-mcp/observer/README.md +8 -6
  15. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +262 -22
  16. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +86 -0
  17. package/runtime/scripts/amalgm-mcp/registration/index.js +4 -1
  18. package/runtime/scripts/amalgm-mcp/registration/service.js +102 -0
  19. package/runtime/scripts/amalgm-mcp/registration/tree.js +19 -5
  20. package/runtime/scripts/amalgm-mcp/registry/index.js +16 -1
  21. package/runtime/scripts/amalgm-mcp/registry/store.js +58 -7
  22. package/runtime/scripts/amalgm-mcp/server/routes/automations.js +8 -0
  23. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +56 -0
  24. package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +14 -0
  25. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +1 -1
  26. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +2 -2
  27. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/citations.html +5 -5
  28. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/links.html +3 -3
  29. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/notification-shell.html +1 -1
  30. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/share-invite.html +13 -0
  31. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/text.html +1 -1
  32. package/runtime/scripts/amalgm-mcp/tests/email-render.test.js +17 -1
  33. package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +189 -0
  34. package/runtime/scripts/amalgm-mcp/tests/entity-content.test.js +126 -0
  35. package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +118 -0
  36. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +126 -11
  37. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +20 -6
  38. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +8 -0
  39. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +8 -1
  40. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +630 -102
  41. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +45 -10
  42. package/runtime/scripts/amalgm-mcp/tests/workspace-materialize.test.js +54 -0
  43. package/runtime/scripts/amalgm-mcp/workspace/materialize.js +292 -0
  44. package/runtime/scripts/chat-core/contract.js +21 -3
  45. package/runtime/scripts/chat-core/engine.js +8 -0
  46. package/runtime/scripts/chat-core/server.js +11 -1
  47. package/runtime/scripts/chat-core/tests/workspace-preflight.test.js +116 -0
  48. package/runtime/scripts/chat-core/workspace-preflight.js +89 -0
  49. package/runtime/scripts/local-gateway.js +0 -1
@@ -3,38 +3,72 @@
3
3
  /**
4
4
  * The production-owned vertical slice:
5
5
  *
6
- * amalgm add [path] → POST /entities/add → registration.add(path)
6
+ * amalgm register [path] → POST /entities/register → registration.register(path)
7
7
  * → Watch + Detect + Record
8
8
  *
9
9
  * The wall already proves detection. What is unproven here is the
10
10
  * WIRING — the parts that only exist because production owns the stack:
11
- * which policies are actually injected, where state lands, that the
12
- * observer's events genuinely reach the boundary, that identity
13
- * survives a process restart over the same directory, and that the
14
- * endpoint reports refusal as refusal.
11
+ * which policies are actually injected, where state lands, that a real
12
+ * doorbell reaches the registry with nobody poking the machinery, that
13
+ * identity survives a genuine close-and-reopen, that the composition
14
+ * root gives back everything it took, and that the endpoint tells a
15
+ * refusal from a fault over a real authenticated socket.
16
+ *
17
+ * Two rules this file holds itself to:
18
+ *
19
+ * NO ASSISTED PASSES. Nothing here calls `scanNow` or `flushIfDirty`
20
+ * to make a rail appear to work. A rail is proven through its own
21
+ * front door or it is not proven — so the timings are production's
22
+ * own (a 200ms settle), and the exams wait for them.
23
+ *
24
+ * NO FAKE TRANSPORT. The endpoint exams go over real HTTP through the
25
+ * real authorizing handler and the real dispatcher. A route proven
26
+ * against a hand-made context object proves the function, not the
27
+ * journey.
15
28
  *
16
29
  * Everything runs over a temporary AMALGM_DIR, set before config loads,
17
30
  * so the singleton this route uses can never touch a real installation.
18
31
  */
19
32
 
33
+ const crypto = require('crypto');
34
+ const { execFileSync } = require('child_process');
20
35
  const fs = require('fs');
36
+ const http = require('http');
21
37
  const os = require('os');
22
38
  const path = require('path');
23
39
  const test = require('node:test');
24
40
  const assert = require('node:assert');
25
41
 
26
- const tempRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-')));
42
+ const tempRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-register-')));
27
43
  process.env.AMALGM_DIR = path.join(tempRoot, 'home');
44
+ // The runtime reads its token from the environment; the CLI reads the
45
+ // same secret from disk and sends it. Setting it here makes the server
46
+ // below demand authorization exactly as production does.
47
+ process.env.AMALGM_RUNTIME_TOKEN = crypto.randomUUID();
48
+
49
+ const Database = require('better-sqlite3');
28
50
 
29
51
  const { classifyFile } = require('../registration/classify');
52
+ const { isRefusal } = require('../registration/refusal');
30
53
  const { ENTITIES_DIR, createRegistrationService, registrationService } = require('../registration/service');
31
- const { handleEntityRoutes } = require('../server/routes/entities');
54
+ const { adapterFor } = require('../adapters');
55
+ const { createRuntimeHttpServer } = require('../server/http-service');
32
56
  const { createLocalServiceRouter } = require('../server/local-service-router');
33
57
 
34
58
  const services = [];
59
+ let server;
60
+ let port;
61
+
62
+ test.before(async () => {
63
+ const router = createLocalServiceRouter();
64
+ server = createRuntimeHttpServer((ctx) => router.handle(ctx), { serviceName: 'AmalgmMCP-test' });
65
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
66
+ port = server.address().port;
67
+ });
35
68
 
36
- test.after(() => {
37
- for (const service of services) service.stop();
69
+ test.after(async () => {
70
+ for (const service of services) service.close();
71
+ if (server) await new Promise((resolve) => server.close(resolve));
38
72
  fs.rmSync(tempRoot, { recursive: true, force: true });
39
73
  });
40
74
 
@@ -51,32 +85,83 @@ function makeGround(name) {
51
85
  return fs.realpathSync(dir);
52
86
  }
53
87
 
54
- // --- the injected type policy ---------------------------------------------
88
+ /** Exactly the request the CLI makes: JSON body, token in its header. */
89
+ function post(requestPath, body, { token = process.env.AMALGM_RUNTIME_TOKEN } = {}) {
90
+ const payload = JSON.stringify(body);
91
+ return new Promise((resolve, reject) => {
92
+ const req = http.request({
93
+ host: '127.0.0.1',
94
+ port,
95
+ path: requestPath,
96
+ method: 'POST',
97
+ headers: {
98
+ 'content-type': 'application/json',
99
+ 'content-length': Buffer.byteLength(payload),
100
+ ...(token ? { 'x-amalgm-runtime-token': token } : {}),
101
+ },
102
+ }, (res) => {
103
+ let text = '';
104
+ res.setEncoding('utf8');
105
+ res.on('data', (chunk) => { text += chunk; });
106
+ res.on('end', () => {
107
+ let parsed = null;
108
+ try { parsed = JSON.parse(text); } catch { parsed = null; }
109
+ resolve({ status: res.statusCode, payload: parsed });
110
+ });
111
+ });
112
+ req.on('error', reject);
113
+ req.end(payload);
114
+ });
115
+ }
55
116
 
56
- test('classify: ordinary bytes are text', () => {
57
- const file = path.join(makeGround('classify-text'), 'notes.md');
58
- fs.writeFileSync(file, '# hello\nplain words\n');
59
- assert.equal(classifyFile(file), 'file.text');
60
- });
117
+ /**
118
+ * Make ONE change through the front door and wait for the registry to
119
+ * show it on its own.
120
+ *
121
+ * One change, never repeated, because that is the user: they save once.
122
+ * Nothing here scans, flushes, or repairs; a rail that needs help fails,
123
+ * loudly, at the deadline, which is generous only because the whole wall
124
+ * shares one machine. A converging rail returns the moment it converges.
125
+ */
126
+ async function arrives(act, seen, what, timeoutMs = 30_000) {
127
+ act();
128
+ const deadline = Date.now() + timeoutMs;
129
+ while (!seen()) {
130
+ if (Date.now() > deadline) assert.fail(`timed out waiting for ${what}`);
131
+ await new Promise((resolve) => setTimeout(resolve, 20));
132
+ }
133
+ }
134
+
135
+ // --- one read, one answer ---------------------------------------------------
136
+
137
+ const fileTruth = adapterFor('file.text').rails.detect.truth;
61
138
 
62
- test('classify: a NUL in the opening bytes means bytes, never text', () => {
63
- const file = path.join(makeGround('classify-binary'), 'blob.bin');
64
- fs.writeFileSync(file, Buffer.from([0x89, 0x50, 0x00, 0x4e, 0x47]));
65
- assert.equal(classifyFile(file), 'file.binary');
139
+ test('Detect reports a file\'s type from the same read that fingerprints it', () => {
140
+ const dir = makeGround('one-read');
141
+ const text = path.join(dir, 'notes.md');
142
+ const bytes = path.join(dir, 'blob.bin');
143
+ fs.writeFileSync(text, '# hello\nplain words\n');
144
+ fs.writeFileSync(bytes, Buffer.from([0x89, 0x50, 0x00, 0x4e, 0x47]));
145
+
146
+ const read = fileTruth(text);
147
+ assert.equal(read.truth, 'present');
148
+ assert.ok(read.state.sha256, 'the fingerprint');
149
+ assert.equal(read.state.binary, false, 'and the kind, from the same bytes');
150
+ assert.equal(fileTruth(bytes).state.binary, true);
66
151
  });
67
152
 
68
- test('classify: ground that cannot be read supports no structural claim', () => {
69
- // The weaker of the two answers, never a throw: the caller is
70
- // mid-transaction over a hash it already holds.
71
- assert.equal(classifyFile(path.join(tempRoot, 'never-existed')), 'file.binary');
153
+ test('a NUL anywhere is seen, not only in an opening window', () => {
154
+ // The whole file is streamed for the hash, so the kind is decided over
155
+ // the whole file too. A sniff window would have called this text and
156
+ // disagreed with the referee, which reads every byte.
157
+ const file = path.join(makeGround('late-nul'), 'long.bin');
158
+ fs.writeFileSync(file, Buffer.concat([Buffer.alloc(200_000, 0x61), Buffer.from([0])]));
159
+ assert.equal(fileTruth(file).state.binary, true);
72
160
  });
73
161
 
74
- test('classify: a NUL past the sniff window does not retroactively unmake text', () => {
75
- // The law is about the OPENING bytes, stated exactly so a huge text
76
- // file is never re-read whole to answer a hint.
77
- const file = path.join(makeGround('classify-late'), 'long.txt');
78
- fs.writeFileSync(file, Buffer.concat([Buffer.alloc(9000, 0x61), Buffer.from([0])]));
79
- assert.equal(classifyFile(file), 'file.text');
162
+ test('the type policy is total: it names a type for either report, and cannot fail', () => {
163
+ assert.equal(classifyFile({ binary: false }), 'file.text');
164
+ assert.equal(classifyFile({ binary: true }), 'file.binary');
80
165
  });
81
166
 
82
167
  // --- composition: what production actually builds --------------------------
@@ -94,14 +179,130 @@ test('the singleton lives under the Amalgm home, and is one', () => {
94
179
  services.push(registrationService());
95
180
  });
96
181
 
97
- test('add registers the named ground and every file beneath it', () => {
182
+ test('user-home bootstrap creates only declared roots and registers them through the shared registration boundary', async () => {
183
+ const home = path.join(tempRoot, 'user-home');
184
+ const service = createRegistrationService({
185
+ dir: path.join(tempRoot, 'state-user-home'),
186
+ homeDir: home,
187
+ });
188
+ services.push(service);
189
+
190
+ const bootstrapped = await service.bootstrapUserHome();
191
+ assert.equal(bootstrapped.homeDir, home);
192
+ assert.deepEqual(bootstrapped.roots.map((entry) => entry.key), [
193
+ 'agents', 'apps', 'automations', 'toolbox', 'workspaces', 'system',
194
+ ]);
195
+ for (const entry of bootstrapped.roots) {
196
+ assert.equal(entry.record.parentUUID, null, `${entry.key} is its own registered tree`);
197
+ assert.ok(entry.record.uuid, `${entry.key} received a UUID through registration`);
198
+ assert.equal(service.registration.tree(entry.path).root().uuid, entry.record.uuid);
199
+ }
200
+ assert.deepEqual(
201
+ bootstrapped.exactManaged.map((entry) => [entry.key, entry.scope]),
202
+ [
203
+ ['agents-root', 'exact'], ['apps-root', 'exact'], ['automations-root', 'exact'],
204
+ ['toolbox-root', 'exact'], ['workspaces-root', 'exact'], ['system-root', 'exact'],
205
+ ['home-manifest', 'exact'], ['defaults', 'exact'], ['policy', 'exact'],
206
+ ],
207
+ );
208
+ for (const entry of bootstrapped.exactManaged) {
209
+ assert.ok(entry.record.uuid, `${entry.key} is registered before cloud governance`);
210
+ const root = bootstrapped.roots.find((candidate) => candidate.key === entry.rootKey);
211
+ if (entry.relativePath === entry.rootKey) {
212
+ assert.equal(entry.record.uuid, root.record.uuid,
213
+ 'a managed root is marked by its own exact entity identity');
214
+ } else {
215
+ assert.notEqual(entry.record.uuid, root.record.uuid,
216
+ 'a managed child has its own exact entity identity, never inherited management');
217
+ }
218
+ }
219
+
220
+ const repeat = await service.bootstrapUserHome();
221
+ assert.deepEqual(
222
+ repeat.roots.map((entry) => entry.record.uuid),
223
+ bootstrapped.roots.map((entry) => entry.record.uuid),
224
+ 'bootstrap is idempotent: existing declared ground is covered by the same trees',
225
+ );
226
+ });
227
+
228
+ test('a fresh machine binds existing user-home UUIDs before it starts watching', async () => {
229
+ const first = createRegistrationService({
230
+ dir: path.join(tempRoot, 'state-user-home-origin'),
231
+ homeDir: path.join(tempRoot, 'user-home-origin'),
232
+ });
233
+ services.push(first);
234
+ const origin = await first.bootstrapUserHome();
235
+
236
+ const recipient = createRegistrationService({
237
+ dir: path.join(tempRoot, 'state-user-home-recipient'),
238
+ homeDir: path.join(tempRoot, 'user-home-recipient'),
239
+ });
240
+ services.push(recipient);
241
+ const restored = await recipient.bootstrapUserHome({ identitySnapshot: origin.cloudSnapshot });
242
+
243
+ assert.deepEqual(
244
+ restored.roots.map((entry) => entry.record.uuid),
245
+ origin.roots.map((entry) => entry.record.uuid),
246
+ 'the cloud graph supplies root UUIDs instead of a new machine minting replacements',
247
+ );
248
+ assert.deepEqual(
249
+ restored.exactManaged.map((entry) => entry.record.uuid),
250
+ origin.exactManaged.map((entry) => entry.record.uuid),
251
+ 'exact platform-managed targets retain their own identities too',
252
+ );
253
+ assert.equal(recipient.observer.status().every((root) => root.watching && !root.degraded), true,
254
+ 'only the bound identities enter Watch');
255
+ });
256
+
257
+ test('a signed-in register call writes every entity delivery intent in identity.db before Watch begins', async () => {
258
+ const service = createRegistrationService({
259
+ dir: path.join(tempRoot, 'state-cloud-register'),
260
+ homeDir: path.join(tempRoot, 'cloud-user-home'),
261
+ cloudUserId: 'user-cloud-register',
262
+ cloudDeviceId: 'computer-cloud-register',
263
+ });
264
+ const home = await service.bootstrapUserHome();
265
+ const bootstrap = service.entityCloud.bootstrap(service.registration.registry);
266
+ service.entityCloud.install({
267
+ registry: service.registration.registry,
268
+ resourceId: bootstrap.resourceId,
269
+ authorityEpoch: 1,
270
+ headVersion: 0,
271
+ snapshotBase64: bootstrap.snapshotBase64,
272
+ snapshotChecksum: bootstrap.snapshotChecksum,
273
+ });
274
+ const ground = makeGround('cloud-register');
275
+ fs.writeFileSync(path.join(ground, 'one.md'), 'one');
276
+
277
+ try {
278
+ const registered = await service.register(ground);
279
+ const db = new Database(path.join(service.dir, 'identity.db'), { readonly: true });
280
+ const rows = db.prepare('SELECT operation_json, mutation_state FROM entity_cloud_mutations ORDER BY sequence').all();
281
+ db.close();
282
+ const emitted = rows.map((row) => JSON.parse(row.operation_json).record.uuid);
283
+ const childUuid = service.registration.tree(ground).registry.activeAt(registered.record.uuid, 'one.md').uuid;
284
+ assert.ok(rows.length >= 2, 'the root and its child each have a durable cloud operation');
285
+ assert.ok(emitted.includes(registered.record.uuid));
286
+ assert.ok(emitted.includes(childUuid));
287
+ assert.equal(rows.every((row) => row.mutation_state === 'pending'), true);
288
+ const childHead = service.registration.tree(ground).registry.syncRecord(childUuid).payloadVersion;
289
+ const captured = JSON.parse(fs.readFileSync(path.join(service.dir, 'content', 'manifests', `${childHead}.json`), 'utf8'));
290
+ assert.equal(captured.bytes, 3, 'Register captures the child bytes before it emits their cloud head');
291
+ assert.equal(captured.chunks.length, 1);
292
+ assert.ok(home.roots.length, 'the bootstrap graph existed only in the initial entity snapshot');
293
+ } finally {
294
+ service.close();
295
+ }
296
+ });
297
+
298
+ test('register registers the named ground and every file beneath it', async () => {
98
299
  const service = makeService('add');
99
300
  const ground = makeGround('add');
100
301
  fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
101
302
  fs.mkdirSync(path.join(ground, 'nested'));
102
303
  fs.writeFileSync(path.join(ground, 'nested', 'b.bin'), Buffer.from([0, 1, 2]));
103
304
 
104
- const { rootPath, record } = service.add(ground);
305
+ const { rootPath, record } = await service.register(ground);
105
306
  assert.equal(rootPath, ground);
106
307
  assert.ok(record.uuid, 'the tree has a permanent identity');
107
308
  assert.equal(record.parentUUID, null, 'a tree is a parentless record');
@@ -116,11 +317,25 @@ test('add registers the named ground and every file beneath it', () => {
116
317
  assert.equal(nested[0].type, 'file.binary', 'and it distinguishes bytes from text');
117
318
  });
118
319
 
119
- test('watching is armed: add begins the Watch rail, not just the walk', () => {
320
+ test('registerMany registers every selected file or folder through the one registration boundary', async () => {
321
+ const service = makeService('add-many');
322
+ const folder = makeGround('add-many-folder');
323
+ const file = path.join(makeGround('add-many-file-parent'), 'solo.md');
324
+ fs.writeFileSync(path.join(folder, 'inside.md'), 'inside');
325
+ fs.writeFileSync(file, 'solo');
326
+
327
+ const results = await service.registerMany([folder, file]);
328
+ assert.deepEqual(results.map((entry) => entry.rootPath), [folder, file]);
329
+ assert.deepEqual(results.map((entry) => entry.record.parentUUID), [null, null]);
330
+ assert.equal(service.observer.status().filter((root) => root.watching && !root.degraded).length >= 2, true,
331
+ 'each selected root passed through Register → Watch');
332
+ });
333
+
334
+ test('watching is armed: register begins the Watch rail, not just the walk', async () => {
120
335
  const service = makeService('watching');
121
336
  const ground = makeGround('watching');
122
337
  fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
123
- service.add(ground);
338
+ await service.register(ground);
124
339
 
125
340
  const [root] = service.observer.status();
126
341
  assert.equal(root.path, ground);
@@ -128,31 +343,322 @@ test('watching is armed: add begins the Watch rail, not just the walk', () => {
128
343
  assert.equal(root.degraded, false);
129
344
  });
130
345
 
131
- test('the Detect → Record wire is live: a later change reaches the registry', () => {
132
- const service = makeService('wire');
133
- const ground = makeGround('wire');
346
+ test('adding a repository registers its allowed tree and saves its initial Repocard baseline', async () => {
347
+ const service = makeService('repo-card');
348
+ const repo = makeGround('repo-card');
349
+ execFileSync('git', ['init', '-q'], { cwd: repo });
350
+ fs.writeFileSync(path.join(repo, 'readme.md'), '# initial checkpoint\n');
351
+ fs.mkdirSync(path.join(repo, 'src'));
352
+ fs.writeFileSync(path.join(repo, 'src', 'blob.bin'), Buffer.from([0, 1, 2]));
353
+ fs.mkdirSync(path.join(repo, 'node_modules'));
354
+ fs.writeFileSync(path.join(repo, 'node_modules', 'ignored.js'), 'never register me');
355
+
356
+ const { record: registered } = await service.register(repo);
357
+ const tree = service.registration.tree(repo);
358
+ assert.equal(registered.type, 'repo.git');
359
+ const initialState = service.registration.repoState(registered.uuid);
360
+ assert.ok(initialState?.stateId, 'registration saved the initial Card + Checkpoint');
361
+ assert.ok(initialState.checkpoint.untracked.some((file) => file.path === 'readme.md'),
362
+ 'the persisted baseline contains the actual working checkpoint, not only its hash');
363
+ assert.equal(tree.root().version, registered.version,
364
+ 'add returned only after the repo entity pointed at its saved baseline');
365
+ const readme = tree.registry.activeAt(registered.uuid, 'readme.md');
366
+ const src = tree.registry.activeAt(registered.uuid, 'src');
367
+ assert.equal(readme.type, 'file.text');
368
+ assert.equal(src.type, 'folder');
369
+ assert.equal(tree.registry.activeAt(src.uuid, 'blob.bin').type, 'file.binary');
370
+ assert.equal(tree.registry.activeAt(registered.uuid, 'node_modules'), null,
371
+ 'the same enrollment policy excludes dependency trees inside repos');
372
+ const initial = tree.root();
373
+
374
+ // Only a settled new checkpoint may reconcile child ground. The raw
375
+ // filesystem event itself never writes a repo or child record.
376
+ await arrives(
377
+ () => fs.writeFileSync(path.join(repo, 'later.md'), 'untracked checkpoint change\n'),
378
+ () => tree.root().version !== initial.version
379
+ && Boolean(tree.registry.activeAt(registered.uuid, 'later.md')),
380
+ 'the worktree change to become one new repo state and child entity',
381
+ );
382
+ const after = tree.root();
383
+ assert.equal(after.uuid, initial.uuid, 'the codebase remains one entity');
384
+ assert.equal(after.type, 'repo.git');
385
+ const later = tree.registry.activeAt(registered.uuid, 'later.md');
386
+ assert.equal(later.type, 'file.text');
387
+
388
+ await arrives(
389
+ () => fs.renameSync(path.join(repo, 'later.md'), path.join(repo, 'moved.md')),
390
+ () => tree.registry.activeAt(registered.uuid, 'moved.md')?.uuid === later.uuid,
391
+ 'the settled repo diff to preserve a moved child UUID',
392
+ );
393
+
394
+ // Repocard's own wall proves that this is `card.changed`; this composition
395
+ // exam proves the semantic result reaches the repo UUID through the real
396
+ // watcher after Git turns the checkpoint into committed history.
397
+ await arrives(
398
+ () => {
399
+ execFileSync('git', ['add', 'moved.md'], { cwd: repo });
400
+ execFileSync('git', [
401
+ '-c', 'user.name=Amalgm Test',
402
+ '-c', 'user.email=amalgm-test@example.invalid',
403
+ 'commit', '-qm', 'record checkpoint',
404
+ ], { cwd: repo });
405
+ },
406
+ () => tree.root().version !== after.version,
407
+ 'the committed Card state to be recorded on the same repo entity',
408
+ );
409
+ assert.equal(tree.root().uuid, after.uuid);
410
+ });
411
+
412
+ test('an edge-reached repository gets the same complete registration as a directly added repo', async () => {
413
+ const service = makeService('edge-repo-card');
414
+ const holder = makeGround('edge-repo-holder');
415
+ const target = makeGround('edge-repo-target');
416
+ execFileSync('git', ['init', '-q'], { cwd: holder });
417
+ execFileSync('git', ['init', '-q'], { cwd: target });
418
+ fs.mkdirSync(path.join(target, 'src'));
419
+ fs.writeFileSync(path.join(target, 'src', 'entry.js'), 'export const ready = true;\n');
420
+ fs.symlinkSync(target, path.join(holder, 'linked-repo'));
421
+
422
+ await service.register(holder);
423
+
424
+ const targetTree = service.registration.tree(target);
425
+ assert.ok(targetTree, 'the reachable repo is a registered tree, not merely an observer root');
426
+ const targetRoot = targetTree.root();
427
+ assert.equal(targetRoot.type, 'repo.git');
428
+ assert.ok(service.registration.repoState(targetRoot.uuid)?.stateId,
429
+ 'the repo has its Card + Checkpoint baseline before add returns');
430
+
431
+ const src = targetTree.registry.activeAt(targetRoot.uuid, 'src');
432
+ assert.equal(src?.type, 'folder', 'the repo worktree shape was structurally registered');
433
+ assert.equal(targetTree.registry.activeAt(src.uuid, 'entry.js')?.type, 'file.text');
434
+
435
+ const observerRoot = service.observer.status().find((row) => row.path === target);
436
+ assert.equal(observerRoot?.watching, true, 'its Watch rail is live too');
437
+ assert.equal(observerRoot?.degraded, false);
438
+ });
439
+
440
+ test('a nested repository has its own baseline and watcher, while its tree remains registered', async () => {
441
+ const service = makeService('nested-repo-card');
442
+ const repo = makeGround('nested-repo-card');
443
+ execFileSync('git', ['init', '-q'], { cwd: repo });
444
+ const nested = path.join(repo, 'vendor', 'library');
445
+ fs.mkdirSync(path.join(nested, 'src'), { recursive: true });
446
+ execFileSync('git', ['init', '-q'], { cwd: nested });
447
+ fs.writeFileSync(path.join(nested, 'src', 'entry.js'), 'export const answer = 42;\n');
448
+
449
+ const { record: root } = await service.register(repo);
450
+ const tree = service.registration.tree(repo);
451
+ const vendor = tree.registry.activeAt(root.uuid, 'vendor');
452
+ const library = tree.registry.activeAt(vendor.uuid, 'library');
453
+ assert.equal(library.type, 'repo.git');
454
+ const baseline = service.registration.repoState(library.uuid);
455
+ assert.ok(baseline?.stateId,
456
+ 'nested repo registration captured its own Card + Checkpoint');
457
+ const src = tree.registry.activeAt(library.uuid, 'src');
458
+ assert.equal(tree.registry.activeAt(src.uuid, 'entry.js').type, 'file.text');
459
+ const identity = new Database(path.join(service.dir, 'identity.db'), { readonly: true });
460
+ const nestedRow = identity.prepare('SELECT payload_version FROM entities WHERE uuid = ?').get(library.uuid);
461
+ identity.close();
462
+ assert.equal(nestedRow.payload_version, baseline.stateId,
463
+ 'the nested repo entity points at the baseline it just saved');
464
+ assert.equal(
465
+ service.observer.status().some((row) => row.path === nested && row.watching && !row.degraded),
466
+ true,
467
+ 'the nested repo has a live semantic watcher of its own',
468
+ );
469
+
470
+ await arrives(
471
+ () => fs.writeFileSync(path.join(nested, 'src', 'live.js'), 'export const live = true;\n'),
472
+ () => Boolean(tree.registry.activeAt(src.uuid, 'live.js'))
473
+ && service.registration.repoState(library.uuid).stateId !== baseline.stateId,
474
+ 'a nested repo change to record the new child under its own repo boundary',
475
+ );
476
+ assert.equal(tree.registry.activeAt(src.uuid, 'live.js').type, 'file.text');
477
+ });
478
+
479
+ test('a reopened repo watcher resumes from the saved Card + Checkpoint baseline', async () => {
480
+ const service = makeService('repo-reopen');
481
+ const repo = makeGround('repo-reopen');
482
+ execFileSync('git', ['init', '-q'], { cwd: repo });
483
+ fs.writeFileSync(path.join(repo, 'readme.md'), '# stable\n');
484
+
485
+ const { record: first } = await service.register(repo);
486
+ const before = service.registration.repoState(first.uuid);
487
+ service.close();
488
+
489
+ const reopened = createRegistrationService({ dir: service.dir });
490
+ services.push(reopened);
491
+ const { record: again } = await reopened.register(repo);
492
+ assert.equal(again.uuid, first.uuid, 'the repo identity is durable');
493
+ assert.equal(reopened.registration.repoState(again.uuid).stateId, before.stateId,
494
+ 'Watch resumed from the registered Card + Checkpoint, not a new birth capture');
495
+ await new Promise((resolve) => setTimeout(resolve, 300));
496
+ assert.equal(reopened.registration.tree(repo).root().version, again.version,
497
+ 'an unchanged repo remains silent after reopen');
498
+ });
499
+
500
+ test('register registers every entity before it starts Watch', async () => {
501
+ const service = makeService('ordered');
502
+ const ground = makeGround('ordered');
503
+ fs.mkdirSync(path.join(ground, 'nested'));
504
+ fs.writeFileSync(path.join(ground, 'nested', 'entry.txt'), 'alpha');
505
+
506
+ const start = service.observer.start.bind(service.observer);
507
+ let checked = false;
508
+ service.observer.start = (rootId) => {
509
+ // Register is durable identity, not the later-created Record lens.
510
+ // The observer's first census now happens before evidence reconciliation
511
+ // so a pre-registered nested repo is never mistaken for absent ground.
512
+ const identity = new Database(path.join(service.dir, 'identity.db'), { readonly: true });
513
+ const root = identity.prepare(
514
+ "SELECT uuid FROM entities WHERE parent_uuid IS NULL AND status = 'active'",
515
+ ).get();
516
+ const nested = identity.prepare(
517
+ "SELECT uuid FROM entities WHERE parent_uuid = ? AND name = 'nested' AND status = 'active'",
518
+ ).get(root.uuid);
519
+ const file = identity.prepare(
520
+ "SELECT uuid FROM entities WHERE parent_uuid = ? AND name = 'entry.txt' AND status = 'active'",
521
+ ).get(nested.uuid);
522
+ identity.close();
523
+ assert.ok(nested, 'the folder already has a durable identity');
524
+ assert.ok(file, 'so does the file within it');
525
+ assert.equal(service.observer.status()[0].watching, false,
526
+ 'the observer has not begun watching while registration is in progress');
527
+ checked = true;
528
+ return start(rootId);
529
+ };
530
+
531
+ const { rootPath } = await service.register(ground);
532
+ assert.equal(rootPath, ground);
533
+ assert.equal(checked, true, 'Watch begins only after the structural walk commits');
534
+ });
535
+
536
+ test('register never reserves or mutates a user filename for watching', async () => {
537
+ const service = makeService('ordinary-name');
538
+ const ground = makeGround('ordinary-name');
539
+ const name = '.amalgm-doorbell';
540
+ const file = path.join(ground, name);
541
+ fs.writeFileSync(file, 'this is user ground');
542
+
543
+ const { record } = await service.register(ground);
544
+ const tree = service.registration.tree(ground);
545
+ assert.equal(fs.readFileSync(file, 'utf8'), 'this is user ground');
546
+ assert.ok(tree.registry.activeAt(record.uuid, name),
547
+ 'it remains an ordinary enrolled file, not Amalgm control state');
548
+ });
549
+
550
+ // --- the rail, through its own front door ----------------------------------
551
+
552
+ test('a real doorbell reaches the registry with nobody poking the machinery', async () => {
553
+ const service = makeService('doorbell');
554
+ const ground = makeGround('doorbell');
134
555
  fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
135
- const { record } = service.add(ground);
556
+ const { record } = await service.register(ground);
557
+ const tree = service.registration.tree(ground);
558
+
559
+ // Ground that appears after registration. From here on NOTHING in this
560
+ // test touches the observer or the boundary: the watcher notices, the
561
+ // scan concludes, the boundary records. Cut the emit wire in
562
+ // service.js, or never start the observer, and this never converges.
563
+ await arrives(
564
+ () => fs.writeFileSync(path.join(ground, 'later.md'), 'arrived after'),
565
+ () => tree.registry.children(record.uuid).some((row) => row.name === 'later.md'),
566
+ 'the new file to reach the registry on its own',
567
+ );
568
+
569
+ const arrived = tree.registry.children(record.uuid).find((row) => row.name === 'later.md');
570
+ assert.equal(arrived.type, 'file.text', 'typed by the read that detected it');
571
+ });
572
+
573
+ test('a later change reaches the registry as a new version on the same UUID', async () => {
574
+ const service = makeService('change');
575
+ const ground = makeGround('change');
576
+ const file = path.join(ground, 'a.txt');
577
+ fs.writeFileSync(file, 'alpha');
578
+ const { record } = await service.register(ground);
136
579
  const tree = service.registration.tree(ground);
580
+ const before = tree.registry.children(record.uuid).find((row) => row.name === 'a.txt');
137
581
 
138
- // Ground that appeared after registration. The scan publishes, the
139
- // boundary records — cut the emit wire in service.js and this fails.
140
- fs.writeFileSync(path.join(ground, 'later.txt'), 'arrived after');
141
- service.observer.scanNow(service.observer.listRoots()[0].rootId);
142
- service.registration.flushIfDirty();
582
+ await arrives(
583
+ () => fs.writeFileSync(file, 'beta — different bytes entirely'),
584
+ () => tree.registry.children(record.uuid)
585
+ .find((row) => row.name === 'a.txt').version !== before.version,
586
+ 'the edit to reach the registry on its own',
587
+ );
143
588
 
144
- const names = tree.registry.children(record.uuid).map((row) => row.name).sort();
145
- assert.deepEqual(names, ['a.txt', 'later.txt']);
589
+ const after = tree.registry.children(record.uuid).find((row) => row.name === 'a.txt');
590
+ assert.equal(after.uuid, before.uuid, 'the same thing, changed — never a new identity');
146
591
  });
147
592
 
148
- test('identity survives a restart over the same directory', () => {
593
+ test('bytes replacing text re-type the same UUID, through the same one doorbell', async () => {
594
+ const service = makeService('retype');
595
+ const ground = makeGround('retype');
596
+ const file = path.join(ground, 'a.txt');
597
+ fs.writeFileSync(file, 'plain words');
598
+ const { record } = await service.register(ground);
599
+ const tree = service.registration.tree(ground);
600
+ const born = tree.registry.children(record.uuid).find((row) => row.name === 'a.txt');
601
+ assert.equal(born.type, 'file.text');
602
+
603
+ await arrives(
604
+ () => fs.writeFileSync(file, Buffer.from([0x89, 0x50, 0x00, 0x4e, 0x47])),
605
+ () => tree.registry.children(record.uuid)
606
+ .find((row) => row.name === 'a.txt').type === 'file.binary',
607
+ 'the new bytes to re-type the record on their own',
608
+ );
609
+ const after = tree.registry.children(record.uuid).find((row) => row.name === 'a.txt');
610
+ assert.equal(after.uuid, born.uuid, 'the same thing, now bytes — never a new identity');
611
+ });
612
+
613
+ // --- lifetime ---------------------------------------------------------------
614
+
615
+ test('close() is silent: no queued pass runs after the databases shut', async () => {
616
+ // A shutdown signal can land between a doorbell and the pass it
617
+ // scheduled. Every stage that owns a pending pass — the boundary AND
618
+ // each of its lenses — must be stopped before the handles close, or a
619
+ // shutdown that went exactly right complains about itself.
620
+ const service = createRegistrationService({ dir: path.join(tempRoot, 'state-quiet') });
621
+ const ground = makeGround('quiet');
622
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
623
+ await service.register(ground);
624
+
625
+ const warnings = [];
626
+ const realWarn = console.warn;
627
+ console.warn = (...args) => warnings.push(args.join(' '));
628
+ try {
629
+ const [root] = service.observer.listRoots();
630
+ service.registration.onEvent({ type: 'file.changed', rootId: root.rootId, path: ground });
631
+ service.close();
632
+ // Drain the immediates the doorbell scheduled, and then some.
633
+ await new Promise((resolve) => setTimeout(resolve, 50));
634
+ } finally {
635
+ console.warn = realWarn;
636
+ }
637
+ assert.deepEqual(warnings, [], 'a clean shutdown says nothing');
638
+ });
639
+
640
+ test('close() gives back everything the root took, and twice is not an error', async () => {
641
+ const service = createRegistrationService({ dir: path.join(tempRoot, 'state-close') });
642
+ const ground = makeGround('close');
643
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
644
+ await service.register(ground);
645
+
646
+ service.close();
647
+ service.close(); // shutdown is reached from more than one direction
648
+
649
+ // A closed handle refuses to answer — the honest proof that the
650
+ // databases are shut, not merely unwatched.
651
+ assert.throws(() => service.observer.listRoots(), /database connection is not open/i);
652
+ });
653
+
654
+ test('identity survives a real close-and-reopen over the same directory', async () => {
149
655
  const dir = path.join(tempRoot, 'state-restart');
150
656
  const ground = makeGround('restart');
151
657
  fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
152
658
 
153
659
  const first = createRegistrationService({ dir });
154
- const before = first.add(ground).record.uuid;
155
- first.stop();
660
+ const before = (await first.register(ground)).record.uuid;
661
+ first.close(); // every handle released: the next stack is genuinely alone
156
662
 
157
663
  const second = createRegistrationService({ dir });
158
664
  services.push(second);
@@ -162,95 +668,117 @@ test('identity survives a restart over the same directory', () => {
162
668
 
163
669
  // --- the policies are the real ones ---------------------------------------
164
670
 
165
- test("amalgm's enrollment policy is in force, and refuses loudly", () => {
671
+ test("amalgm's enrollment policy is in force, and refuses loudly", async () => {
166
672
  const service = makeService('policy');
167
673
  const excluded = path.join(makeGround('policy'), 'node_modules');
168
674
  fs.mkdirSync(excluded);
169
- assert.throws(() => service.add(excluded), /enrollment policy excludes it/);
675
+ await assert.rejects(
676
+ () => service.register(excluded),
677
+ (error) => isRefusal(error) && /enrollment policy excludes it/.test(error.message),
678
+ 'a refusal, named as one',
679
+ );
680
+ });
681
+
682
+ test('ground that is not there is a refusal, not a fault', async () => {
683
+ const service = makeService('missing');
684
+ await assert.rejects(
685
+ () => service.register(path.join(tempRoot, 'no-such-ground')),
686
+ (error) => isRefusal(error) && /no ground at that address/.test(error.message),
687
+ );
170
688
  });
171
689
 
172
- test('adding ground already inside a registered tree returns that tree', () => {
690
+ test('a refusal is what the boundary RAISED, never what an error says', () => {
691
+ // The whole point of naming refusals: an unlucky message must not
692
+ // promote a fault into "your path is bad".
693
+ assert.equal(isRefusal(new Error('refusing to add /x: enrollment policy excludes it')), false);
694
+ assert.equal(isRefusal(null), false);
695
+ });
696
+
697
+ test('adding ground already inside a registered tree returns that tree', async () => {
173
698
  const service = makeService('covered');
174
699
  const ground = makeGround('covered');
175
700
  fs.mkdirSync(path.join(ground, 'inner'));
176
701
  fs.writeFileSync(path.join(ground, 'inner', 'a.txt'), 'alpha');
177
702
 
178
- const tree = service.add(ground);
179
- const inner = service.add(path.join(ground, 'inner'));
703
+ const tree = await service.register(ground);
704
+ const inner = await service.register(path.join(ground, 'inner'));
180
705
  assert.equal(inner.rootPath, ground, 'one owner per path — no second tree is born');
181
706
  assert.equal(inner.record.uuid, tree.record.uuid);
182
707
  });
183
708
 
184
- // --- the endpoint ----------------------------------------------------------
709
+ // --- the endpoint, over real authenticated HTTP ----------------------------
185
710
 
186
- function fakeCtx(pathname, method, body) {
187
- const sent = {};
188
- return {
189
- ctx: {
190
- pathname,
191
- method,
192
- readJsonBody: async () => body,
193
- sendJson: (status, payload) => Object.assign(sent, { status, payload }),
194
- },
195
- sent,
196
- };
197
- }
711
+ test('an unauthorized request never reaches the boundary', async () => {
712
+ const ground = makeGround('unauthorized');
713
+ const answer = await post('/entities/register', { path: ground }, { token: null });
714
+ assert.equal(answer.status, 401);
715
+ });
198
716
 
199
- test('POST /entities/add registers and answers with the tree', async () => {
717
+ test('POST /entities/register registers and answers with the tree', async () => {
200
718
  const ground = makeGround('route');
201
719
  fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
202
720
 
203
- const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: ground });
204
- assert.equal(await handleEntityRoutes(ctx), true);
205
- assert.equal(sent.status, 200);
206
- assert.equal(sent.payload.rootPath, ground);
207
- assert.ok(sent.payload.record.uuid);
721
+ const answer = await post('/entities/register', { path: ground });
722
+ assert.equal(answer.status, 200);
723
+ assert.equal(answer.payload.rootPath, ground);
724
+ assert.ok(answer.payload.record.uuid);
725
+ });
726
+
727
+ test('POST /entities/register accepts paths for one batch of selected ground', async () => {
728
+ const first = makeGround('route-many-first');
729
+ const second = path.join(makeGround('route-many-second-parent'), 'second.txt');
730
+ fs.writeFileSync(second, 'second');
731
+
732
+ const answer = await post('/entities/register', { paths: [first, second] });
733
+ assert.equal(answer.status, 200);
734
+ assert.deepEqual(answer.payload.results.map((entry) => entry.rootPath), [first, second]);
735
+ assert.deepEqual(answer.payload.results.map((entry) => entry.record.parentUUID), [null, null]);
208
736
  });
209
737
 
210
738
  test('the endpoint reports a refusal as a refusal, with the reason', async () => {
211
739
  const excluded = path.join(makeGround('route-refused'), '.ssh');
212
740
  fs.mkdirSync(excluded);
213
741
 
214
- const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: excluded });
215
- assert.equal(await handleEntityRoutes(ctx), true);
216
- assert.equal(sent.status, 400, 'the request named ground the boundary refuses');
217
- assert.match(sent.payload.error, /enrollment policy excludes it/);
742
+ const answer = await post('/entities/register', { path: excluded });
743
+ assert.equal(answer.status, 400, 'the request named ground the boundary refuses');
744
+ assert.match(answer.payload.error, /enrollment policy excludes it/);
218
745
  });
219
746
 
220
747
  test('a missing path is answered, not registered', async () => {
221
- const { ctx, sent } = fakeCtx('/entities/add', 'POST', {});
222
- assert.equal(await handleEntityRoutes(ctx), true);
223
- assert.equal(sent.status, 400);
224
- assert.match(sent.payload.error, /path is required/);
748
+ const answer = await post('/entities/register', {});
749
+ assert.equal(answer.status, 400);
750
+ assert.match(answer.payload.error, /path.*required/);
225
751
  });
226
752
 
227
753
  test('ground that is not there is a refusal, never a crash', async () => {
228
- const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: path.join(tempRoot, 'no-such-ground') });
229
- assert.equal(await handleEntityRoutes(ctx), true);
230
- assert.equal(sent.status, 400);
231
- assert.ok(sent.payload.error);
754
+ const answer = await post('/entities/register', { path: path.join(tempRoot, 'no-such-ground') });
755
+ assert.equal(answer.status, 400);
756
+ assert.match(answer.payload.error, /no ground at that address/);
757
+ });
758
+
759
+ test('the route group claims only its own request', async () => {
760
+ assert.equal((await post('/entities', {})).status, 404);
232
761
  });
233
762
 
234
- test('the route is MOUNTED: the runtime dispatcher reaches it', async () => {
235
- // A handler nothing routes to is a handler that does not exist. This
236
- // is the local dispatcher production actually serves, unmodified.
237
- const ground = makeGround('mounted');
763
+ // --- the fault, last: it convicts the installation's singleton -------------
764
+
765
+ test('a torn identity store is a FAULT, never the caller\'s bad path', async () => {
766
+ // The sharpest case the split exists for. The tree is registered and
767
+ // its identity is intact; the EVIDENCE that witnesses it is destroyed
768
+ // underneath the runtime. The loss law convicts — loudly, by design —
769
+ // and that conviction must never reach the user as "your path is bad".
770
+ //
771
+ // (The server's fault handler logs the error; the noise below is that
772
+ // handler doing its job.)
773
+ const ground = makeGround('torn');
238
774
  fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
775
+ assert.equal((await post('/entities/register', { path: ground })).status, 200);
239
776
 
240
- const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: ground });
241
- assert.equal(await createLocalServiceRouter().handle(ctx), true);
242
- assert.equal(sent.status, 200);
243
- assert.equal(sent.payload.rootPath, ground);
244
- });
777
+ const evidence = new Database(path.join(ENTITIES_DIR, 'observer.db'));
778
+ evidence.prepare('DELETE FROM observer_roots').run();
779
+ evidence.close();
245
780
 
246
- test('the route group claims only its own request', async () => {
247
- const others = [
248
- fakeCtx('/entities/add', 'GET', {}),
249
- fakeCtx('/workspace/projects', 'POST', {}),
250
- fakeCtx('/entities', 'POST', {}),
251
- ];
252
- for (const { ctx, sent } of others) {
253
- assert.equal(await handleEntityRoutes(ctx), false);
254
- assert.equal(sent.status, undefined, 'an unclaimed request is never answered');
255
- }
781
+ const answer = await post('/entities/register', { path: makeGround('torn-next') });
782
+ assert.equal(answer.status, 500, 'an installation fault, reported as one');
783
+ assert.doesNotMatch(String(answer.payload?.error), /refusing to add/);
256
784
  });