amalgm 0.1.245 → 0.1.246

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -23,7 +23,6 @@ const {
23
23
  claimCloudMutations,
24
24
  getMutation,
25
25
  } = require('../state/mutations');
26
- const { getReplicaByResourceId } = require('../state/replicas');
27
26
  const {
28
27
  COOKIE_CLOUD_CONTRACT,
29
28
  applyCloudCookieMutation,
@@ -90,21 +89,6 @@ test('official cookie mutations materialize in order and deletions stay tombston
90
89
  version: 1,
91
90
  committedAt: '2026-07-20T20:00:00.000Z',
92
91
  }, { database: db });
93
- assert.equal(
94
- getReplicaByResourceId(localClaim.envelope.resourceId, { database: db }).appliedVersion,
95
- 0,
96
- 'the acknowledgement records the number but does not apply it',
97
- );
98
- applyCloudCookieMutation({
99
- version: 1,
100
- committedAt: '2026-07-20T20:00:00.000Z',
101
- envelope: localClaim.envelope,
102
- }, { database: db });
103
- assert.equal(
104
- getReplicaByResourceId(localClaim.envelope.resourceId, { database: db }).appliedVersion,
105
- 1,
106
- 'the writer advances only when its committed event passes through the ordered inbox',
107
- );
108
92
 
109
93
  const remoteCookie = cookie('remote', 'remote-secret');
110
94
  const key = require('../browser/cookie-jar').cookieKey(remoteCookie);
@@ -194,181 +178,4 @@ test('a stale cloud base never re-emits local cookies as bootstrap mutations', (
194
178
  assert.equal(result.bootstrapDeferred, true);
195
179
  assert.equal(result.bootstrappedMutations, 0);
196
180
  assert.equal(db.prepare('SELECT COUNT(*) AS count FROM mutation_journal').get().count, 0);
197
- assert.equal(
198
- readCookieJar().cookies.some((item) => item.name === 'already-synced'),
199
- true,
200
- 'a stale prefix cannot roll the physical jar backward',
201
- );
202
- });
203
-
204
- test('a private snapshot ahead of cloud head is rejected before changing the jar', () => {
205
- const before = JSON.stringify(readCookieJar());
206
- const resourceId = privateCookieResourceId(userId);
207
- const bytes = Buffer.from(JSON.stringify({
208
- schemaVersion: 1,
209
- version: 8,
210
- updatedAt: '2026-07-20T19:00:00.000Z',
211
- records: [],
212
- tombstones: [],
213
- }), 'utf8');
214
- assert.throws(() => installCookieCloudBootstrap({
215
- resourceId,
216
- authorityEpoch: 1,
217
- headVersion: 7,
218
- snapshotVersion: 8,
219
- snapshotChecksum: require('crypto').createHash('sha256').update(bytes).digest('hex'),
220
- snapshotBase64: bytes.toString('base64'),
221
- contract: COOKIE_CLOUD_CONTRACT,
222
- schemaVersion: 1,
223
- }), /cannot exceed headVersion/);
224
- assert.equal(JSON.stringify(readCookieJar()), before);
225
- });
226
-
227
- test('a stale cookie prefix ahead of the local cursor cannot claim application', () => {
228
- mergeCookieJar({ sourceId: 'machine-a', upserts: [cookie('local-only', 'keep-me')] });
229
- const db = openLocalDb();
230
- db.prepare('DELETE FROM mutation_journal').run();
231
- const resourceId = privateCookieResourceId(userId);
232
- const before = JSON.stringify(readCookieJar());
233
- const bytes = Buffer.from(JSON.stringify({
234
- schemaVersion: 1,
235
- version: 3,
236
- updatedAt: '2026-07-20T19:00:00.000Z',
237
- records: [],
238
- tombstones: [],
239
- }), 'utf8');
240
-
241
- const result = installCookieCloudBootstrap({
242
- resourceId,
243
- authorityEpoch: 1,
244
- headVersion: 7,
245
- snapshotVersion: 3,
246
- snapshotChecksum: require('crypto').createHash('sha256').update(bytes).digest('hex'),
247
- snapshotBase64: bytes.toString('base64'),
248
- contract: COOKIE_CLOUD_CONTRACT,
249
- schemaVersion: 1,
250
- }, { database: db });
251
-
252
- assert.equal(result.bootstrapDeferred, true);
253
- assert.equal(result.replica.appliedVersion, 0);
254
- assert.equal(JSON.stringify(readCookieJar()), before);
255
- });
256
-
257
- test('cookie bootstrap journals legacy local state before replacing the physical jar', () => {
258
- mergeCookieJar({
259
- sourceId: 'legacy-browser',
260
- upserts: [cookie('legacy-session', 'must-survive')],
261
- });
262
- const db = openLocalDb();
263
- db.prepare('DELETE FROM mutation_journal').run();
264
- const resourceId = privateCookieResourceId(userId);
265
- const bytes = Buffer.from(JSON.stringify({
266
- schemaVersion: 1,
267
- version: 0,
268
- updatedAt: '2026-07-20T19:00:00.000Z',
269
- records: [],
270
- tombstones: [],
271
- }), 'utf8');
272
- const input = {
273
- resourceId,
274
- authorityEpoch: 1,
275
- headVersion: 0,
276
- snapshotVersion: 0,
277
- snapshotChecksum: require('crypto').createHash('sha256').update(bytes).digest('hex'),
278
- snapshotBase64: bytes.toString('base64'),
279
- contract: COOKIE_CLOUD_CONTRACT,
280
- schemaVersion: 1,
281
- };
282
-
283
- assert.throws(
284
- () => installCookieCloudBootstrap(input, {
285
- database: db,
286
- testHooks: { crashAfterSnapshotPersist: true },
287
- }),
288
- /simulated cookie bootstrap crash/,
289
- );
290
- assert.equal(
291
- db.prepare(`
292
- SELECT COUNT(*) AS count FROM mutation_journal
293
- WHERE shared_resource_id = ? AND mutation_origin = 'local'
294
- `).get(resourceId).count,
295
- 1,
296
- 'the local cookie is durable before the jar is replaced',
297
- );
298
-
299
- installCookieCloudBootstrap(input, { database: db });
300
- assert.equal(
301
- readCookieJar().cookies.some((item) => (
302
- item.name === 'legacy-session' && item.value === 'must-survive'
303
- )),
304
- true,
305
- );
306
- assert.equal(
307
- db.prepare(`
308
- SELECT COUNT(*) AS count FROM mutation_journal
309
- WHERE shared_resource_id = ? AND mutation_origin = 'local'
310
- `).get(resourceId).count,
311
- 1,
312
- 'restart does not mint a second birth certificate',
313
- );
314
- });
315
-
316
- test('cookie bootstrap completes a crash-left saving-local birth instead of replaying forever', () => {
317
- mergeCookieJar({
318
- sourceId: 'crashing-browser',
319
- upserts: [cookie('crash-left', 'must-recover')],
320
- });
321
- const db = openLocalDb();
322
- const row = db.prepare(`
323
- SELECT * FROM mutation_journal
324
- WHERE operation_kind = 'cookie.upsert'
325
- ORDER BY journal_id DESC
326
- LIMIT 1
327
- `).get();
328
- db.prepare(`
329
- UPDATE mutation_journal
330
- SET mutation_state = 'saving-local',
331
- materialized_at = NULL,
332
- event_seq = NULL
333
- WHERE journal_id = ?
334
- `).run(row.journal_id);
335
- db.prepare('DELETE FROM event_log').run();
336
-
337
- const bytes = Buffer.from(JSON.stringify({
338
- schemaVersion: 1,
339
- version: 0,
340
- updatedAt: '2026-07-20T19:00:00.000Z',
341
- records: [],
342
- tombstones: [],
343
- }), 'utf8');
344
- installCookieCloudBootstrap({
345
- resourceId: row.shared_resource_id,
346
- authorityEpoch: 1,
347
- headVersion: 0,
348
- snapshotVersion: 0,
349
- snapshotChecksum: require('crypto').createHash('sha256').update(bytes).digest('hex'),
350
- snapshotBase64: bytes.toString('base64'),
351
- contract: COOKIE_CLOUD_CONTRACT,
352
- schemaVersion: 1,
353
- }, { database: db });
354
-
355
- const repaired = db.prepare(`
356
- SELECT mutation_state, materialized_at, event_seq
357
- FROM mutation_journal
358
- WHERE journal_id = ?
359
- `).get(row.journal_id);
360
- assert.equal(repaired.mutation_state, 'saved-local');
361
- assert.ok(repaired.materialized_at);
362
- assert.ok(repaired.event_seq);
363
- assert.equal(
364
- readCookieJar().cookies.some((item) => (
365
- item.name === 'crash-left' && item.value === 'must-recover'
366
- )),
367
- true,
368
- );
369
- assert.equal(
370
- db.prepare('SELECT COUNT(*) AS count FROM mutation_journal').get().count,
371
- 1,
372
- 'recovery completes the original birth instead of minting another',
373
- );
374
181
  });
@@ -0,0 +1,449 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * THE LIFE GRID — half of the doorbell grammar matrix: every physical
5
+ * kind Detect can see × every life event ground can undergo (create,
6
+ * change, rename, move, disappear, restart). One exam per box, proven
7
+ * TOTAL by construction: the rows are DERIVED from the adapter
8
+ * contract — exactly the alphabet types whose adapter binds the
9
+ * ratified detect socket — so a new type, or a blind type gaining its
10
+ * socket, convicts this suite until it takes its row. The one blind
11
+ * type today is asserted by name: `reference` has no detect socket by
12
+ * its own constitution (its truth is registry identity; the rendered
13
+ * symlink is derived state, proven in the registration suite) — a
14
+ * Detect row for it would invent semantics ahead of the socket.
15
+ *
16
+ * These cells prove DETECTION grammar at the scan entrance; live
17
+ * doorbell delivery is the other half's business
18
+ * (doorbell.matrix.watch.test.js).
19
+ *
20
+ * Grammar ratified here (each line observed on real ground first):
21
+ * - Both file kinds share ONE row definition: the grammar cannot
22
+ * care what the bytes are, because the same code speaks for both.
23
+ * - A folder rename re-addresses every member at this layer
24
+ * (dir.renamed + member file.renamed); "descendants silent" is the
25
+ * REGISTRY's record-level law, proven in its own suites.
26
+ * - Repository territory is path-keyed at this layer: a moved repo
27
+ * is burial + discovery; identity continuity is the registry's
28
+ * rescue, not the doorbell's.
29
+ * - Recreation after a witnessed deletion is a new birth.
30
+ * - A repo-kind root speaks once (repo.changed) for any member churn
31
+ * — the RAW repo doorbell; the follower, a layer above, turns it
32
+ * into card, checkpoint, or silence.
33
+ * - Constructs (automations, agents, skills…) are plain ground: they
34
+ * move through every box above with no voice of their own.
35
+ */
36
+
37
+ const test = require('node:test');
38
+ const assert = require('node:assert/strict');
39
+ const fs = require('fs');
40
+ const path = require('path');
41
+
42
+ const { adapterFor } = require('../adapters');
43
+ const { ENTITY_TYPES } = require('../registry');
44
+ const {
45
+ afterRestart, clean, cleanup, filesRoot, gitInit, makeDir, makeObserver, speaks,
46
+ } = require('./doorbell.matrix.rig');
47
+
48
+ test.after(cleanup);
49
+
50
+ const LIFE_EVENTS = Object.freeze(['create', 'change', 'rename', 'move', 'disappear', 'restart']);
51
+
52
+ /**
53
+ * The six file cells, defined ONCE and instantiated for both file
54
+ * kinds: byte-indifference is proven by construction, not by claim.
55
+ */
56
+ function fileRow(name, versionOne, versionTwo) {
57
+ const at = (dir, rel) => path.join(dir, rel);
58
+ return {
59
+ create: () => {
60
+ const rig = filesRoot();
61
+ fs.writeFileSync(at(rig.dir, name), versionOne);
62
+ rig.observer.scanNow(rig.root.rootId);
63
+ speaks(rig, [['file.created', { path: name }]]);
64
+ assert.match(rig.events[rig.mark].hash, /^[0-9a-f]{64}$/, 'born with its content hash');
65
+ clean(rig);
66
+ },
67
+ change: () => {
68
+ const rig = filesRoot((dir) => fs.writeFileSync(at(dir, name), versionOne));
69
+ fs.writeFileSync(at(rig.dir, name), versionTwo);
70
+ rig.observer.scanNow(rig.root.rootId);
71
+ speaks(rig, [['file.changed', { path: name }]]);
72
+ assert.notEqual(rig.events[rig.mark].hash, rig.events[rig.mark].previousHash,
73
+ 'the version moved with the bytes');
74
+ clean(rig);
75
+ },
76
+ rename: () => {
77
+ const rig = filesRoot((dir) => {
78
+ fs.mkdirSync(at(dir, 'a'));
79
+ fs.writeFileSync(at(dir, `a/${name}`), versionOne);
80
+ });
81
+ fs.renameSync(at(rig.dir, `a/${name}`), at(rig.dir, `a/kept-${name}`));
82
+ rig.observer.scanNow(rig.root.rootId);
83
+ speaks(rig, [['file.renamed', { from: `a/${name}`, to: `a/kept-${name}` }]],
84
+ 'one rename: no delete, no create');
85
+ clean(rig);
86
+ },
87
+ move: () => {
88
+ const rig = filesRoot((dir) => {
89
+ fs.mkdirSync(at(dir, 'a'));
90
+ fs.mkdirSync(at(dir, 'b'));
91
+ fs.writeFileSync(at(dir, `a/${name}`), versionOne);
92
+ });
93
+ fs.renameSync(at(rig.dir, `a/${name}`), at(rig.dir, `b/${name}`));
94
+ rig.observer.scanNow(rig.root.rootId);
95
+ speaks(rig, [['file.renamed', { from: `a/${name}`, to: `b/${name}` }]],
96
+ 'a move is a rename across directories: same channel, new address');
97
+ clean(rig);
98
+ },
99
+ disappear: () => {
100
+ const rig = filesRoot((dir) => fs.writeFileSync(at(dir, name), versionOne));
101
+ fs.rmSync(at(rig.dir, name));
102
+ rig.observer.scanNow(rig.root.rootId);
103
+ speaks(rig, [['file.deleted', { path: name }]]);
104
+ clean(rig);
105
+ },
106
+ restart: () => {
107
+ const rig = afterRestart(
108
+ (dir) => fs.writeFileSync(at(dir, name), versionOne),
109
+ (dir) => fs.writeFileSync(at(dir, name), versionTwo));
110
+ speaks(rig, [['file.changed', { path: name }]],
111
+ 'an edit made while nothing ran is one change on the same channel');
112
+ clean(rig);
113
+ },
114
+ };
115
+ }
116
+
117
+ const PNG = (last) => Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, last]);
118
+
119
+ const GRID = {
120
+ 'file.binary': fileRow('img.png', PNG(1), PNG(2)),
121
+ 'file.text': fileRow('notes.md', 'a quiet first draft', 'a louder second draft'),
122
+
123
+ folder: {
124
+ create: () => {
125
+ const rig = filesRoot();
126
+ fs.mkdirSync(path.join(rig.dir, 'docs'));
127
+ rig.observer.scanNow(rig.root.rootId);
128
+ speaks(rig, [['dir.created', { path: 'docs' }]]);
129
+ clean(rig);
130
+ },
131
+ change: () => {
132
+ const rig = filesRoot((dir) => fs.mkdirSync(path.join(dir, 'docs')));
133
+ fs.writeFileSync(path.join(rig.dir, 'docs', 'new.md'), 'n');
134
+ rig.observer.scanNow(rig.root.rootId);
135
+ speaks(rig, [['file.created', { path: 'docs/new.md' }]],
136
+ 'a folder\'s only content is membership: the member speaks, the folder is silent');
137
+ clean(rig);
138
+ },
139
+ rename: () => {
140
+ const rig = filesRoot((dir) => {
141
+ fs.mkdirSync(path.join(dir, 'docs'));
142
+ fs.writeFileSync(path.join(dir, 'docs', 'one.md'), '1');
143
+ });
144
+ fs.renameSync(path.join(rig.dir, 'docs'), path.join(rig.dir, 'papers'));
145
+ rig.observer.scanNow(rig.root.rootId);
146
+ speaks(rig, [
147
+ ['dir.renamed', { from: 'docs', to: 'papers' }],
148
+ ['file.renamed', { from: 'docs/one.md', to: 'papers/one.md' }],
149
+ ], 'the address layer re-addresses every member; record-level descendant silence is the registry\'s law');
150
+ clean(rig);
151
+ },
152
+ move: () => {
153
+ const rig = filesRoot((dir) => {
154
+ fs.mkdirSync(path.join(dir, 'docs'));
155
+ fs.writeFileSync(path.join(dir, 'docs', 'one.md'), '1');
156
+ fs.mkdirSync(path.join(dir, 'attic'));
157
+ });
158
+ fs.renameSync(path.join(rig.dir, 'docs'), path.join(rig.dir, 'attic', 'docs'));
159
+ rig.observer.scanNow(rig.root.rootId);
160
+ speaks(rig, [
161
+ ['dir.renamed', { from: 'docs', to: 'attic/docs' }],
162
+ ['file.renamed', { from: 'docs/one.md', to: 'attic/docs/one.md' }],
163
+ ]);
164
+ clean(rig);
165
+ },
166
+ disappear: () => {
167
+ const rig = filesRoot((dir) => {
168
+ fs.mkdirSync(path.join(dir, 'docs'));
169
+ fs.writeFileSync(path.join(dir, 'docs', 'one.md'), '1');
170
+ fs.writeFileSync(path.join(dir, 'docs', 'two.md'), '2');
171
+ });
172
+ fs.rmSync(path.join(rig.dir, 'docs'), { recursive: true });
173
+ rig.observer.scanNow(rig.root.rootId);
174
+ speaks(rig, [
175
+ ['dir.deleted', { path: 'docs' }],
176
+ ['file.deleted', { path: 'docs/one.md' }],
177
+ ['file.deleted', { path: 'docs/two.md' }],
178
+ ], 'death speaks for the folder AND every member: no silent burials');
179
+ clean(rig);
180
+ },
181
+ restart: () => {
182
+ const rig = afterRestart(() => {}, (dir) => fs.mkdirSync(path.join(dir, 'docs')));
183
+ speaks(rig, [['dir.created', { path: 'docs' }]]);
184
+ clean(rig);
185
+ },
186
+ },
187
+
188
+ link: {
189
+ create: () => {
190
+ const rig = filesRoot((dir) => fs.writeFileSync(path.join(dir, 'target.md'), 't'));
191
+ fs.symlinkSync('target.md', path.join(rig.dir, 'ln'));
192
+ rig.observer.scanNow(rig.root.rootId);
193
+ speaks(rig, [['link.created', { path: 'ln' }]]);
194
+ clean(rig);
195
+ },
196
+ change: () => {
197
+ const rig = filesRoot((dir) => {
198
+ fs.writeFileSync(path.join(dir, 'one.md'), '1');
199
+ fs.writeFileSync(path.join(dir, 'two.md'), '2');
200
+ fs.symlinkSync('one.md', path.join(dir, 'ln'));
201
+ });
202
+ fs.unlinkSync(path.join(rig.dir, 'ln'));
203
+ fs.symlinkSync('two.md', path.join(rig.dir, 'ln'));
204
+ rig.observer.scanNow(rig.root.rootId);
205
+ speaks(rig, [['link.changed', { path: 'ln' }]],
206
+ 'a retarget is the same channel: the address absorbs the inode churn');
207
+ clean(rig);
208
+ },
209
+ rename: () => {
210
+ const rig = filesRoot((dir) => {
211
+ fs.writeFileSync(path.join(dir, 'target.md'), 't');
212
+ fs.symlinkSync('target.md', path.join(dir, 'ln'));
213
+ });
214
+ fs.renameSync(path.join(rig.dir, 'ln'), path.join(rig.dir, 'ln2'));
215
+ rig.observer.scanNow(rig.root.rootId);
216
+ speaks(rig, [['link.renamed', { from: 'ln', to: 'ln2' }]]);
217
+ clean(rig);
218
+ },
219
+ move: () => {
220
+ const rig = filesRoot((dir) => {
221
+ fs.mkdirSync(path.join(dir, 'a'));
222
+ fs.mkdirSync(path.join(dir, 'b'));
223
+ fs.writeFileSync(path.join(dir, 'target.md'), 't');
224
+ fs.symlinkSync('../target.md', path.join(dir, 'a', 'ln'));
225
+ });
226
+ fs.renameSync(path.join(rig.dir, 'a', 'ln'), path.join(rig.dir, 'b', 'ln'));
227
+ rig.observer.scanNow(rig.root.rootId);
228
+ speaks(rig, [['link.renamed', { from: 'a/ln', to: 'b/ln' }]]);
229
+ clean(rig);
230
+ },
231
+ disappear: () => {
232
+ const rig = filesRoot((dir) => {
233
+ fs.writeFileSync(path.join(dir, 'target.md'), 't');
234
+ fs.symlinkSync('target.md', path.join(dir, 'ln'));
235
+ });
236
+ fs.unlinkSync(path.join(rig.dir, 'ln'));
237
+ rig.observer.scanNow(rig.root.rootId);
238
+ speaks(rig, [['link.deleted', { path: 'ln' }]],
239
+ 'only the pointer died: its target is untouched and says nothing');
240
+ clean(rig);
241
+ },
242
+ restart: () => {
243
+ const rig = afterRestart(
244
+ (dir) => {
245
+ fs.writeFileSync(path.join(dir, 'one.md'), '1');
246
+ fs.writeFileSync(path.join(dir, 'two.md'), '2');
247
+ fs.symlinkSync('one.md', path.join(dir, 'ln'));
248
+ },
249
+ (dir) => {
250
+ fs.unlinkSync(path.join(dir, 'ln'));
251
+ fs.symlinkSync('two.md', path.join(dir, 'ln'));
252
+ });
253
+ speaks(rig, [['link.changed', { path: 'ln' }]]);
254
+ clean(rig);
255
+ },
256
+ },
257
+
258
+ 'repo.git': {
259
+ create: () => {
260
+ const rig = filesRoot();
261
+ fs.mkdirSync(path.join(rig.dir, 'lib'));
262
+ gitInit(path.join(rig.dir, 'lib'));
263
+ rig.observer.scanNow(rig.root.rootId);
264
+ speaks(rig, [['repo.discovered', { path: path.join(rig.dir, 'lib') }]],
265
+ 'a .git claims its subtree: territory is born, not files');
266
+ clean(rig);
267
+ },
268
+ change: () => {
269
+ const dir = makeDir('cell');
270
+ gitInit(dir);
271
+ fs.writeFileSync(path.join(dir, 'a.md'), 'a');
272
+ const { observer, events } = makeObserver();
273
+ const root = observer.enrollRoot(dir);
274
+ assert.equal(root.kind, 'repo', 'territory is decided by disk');
275
+ const rig = { dir, events, observer, root, mark: events.length };
276
+ fs.writeFileSync(path.join(dir, 'b.md'), 'b');
277
+ observer.scanNow(root.rootId);
278
+ speaks(rig, [['repo.changed', {}]],
279
+ 'a repository speaks once for any member churn: one raw repo doorbell, zero file events — the follower turns it into card, checkpoint, or silence');
280
+ clean(rig);
281
+ },
282
+ rename: () => {
283
+ const rig = filesRoot((dir) => {
284
+ fs.mkdirSync(path.join(dir, 'lib'));
285
+ gitInit(path.join(dir, 'lib'));
286
+ });
287
+ fs.renameSync(path.join(rig.dir, 'lib'), path.join(rig.dir, 'lib2'));
288
+ rig.observer.scanNow(rig.root.rootId);
289
+ speaks(rig, [
290
+ ['repo.discovered', { path: path.join(rig.dir, 'lib2') }],
291
+ ['repo.removed', { path: path.join(rig.dir, 'lib') }],
292
+ ], 'territory is path-keyed here: burial + discovery; identity continuity is the registry\'s rescue');
293
+ clean(rig);
294
+ },
295
+ move: () => {
296
+ const rig = filesRoot((dir) => {
297
+ fs.mkdirSync(path.join(dir, 'lib'));
298
+ gitInit(path.join(dir, 'lib'));
299
+ fs.mkdirSync(path.join(dir, 'vendor'));
300
+ });
301
+ fs.renameSync(path.join(rig.dir, 'lib'), path.join(rig.dir, 'vendor', 'lib'));
302
+ rig.observer.scanNow(rig.root.rootId);
303
+ speaks(rig, [
304
+ ['repo.discovered', { path: path.join(rig.dir, 'vendor', 'lib') }],
305
+ ['repo.removed', { path: path.join(rig.dir, 'lib') }],
306
+ ]);
307
+ clean(rig);
308
+ },
309
+ disappear: () => {
310
+ const rig = filesRoot((dir) => {
311
+ fs.mkdirSync(path.join(dir, 'lib'));
312
+ gitInit(path.join(dir, 'lib'));
313
+ });
314
+ fs.rmSync(path.join(rig.dir, 'lib'), { recursive: true, force: true });
315
+ rig.observer.scanNow(rig.root.rootId);
316
+ speaks(rig, [['repo.removed', { path: path.join(rig.dir, 'lib') }]]);
317
+ clean(rig);
318
+ },
319
+ restart: () => {
320
+ const rig = afterRestart(() => {}, (dir) => {
321
+ fs.mkdirSync(path.join(dir, 'lib'));
322
+ gitInit(path.join(dir, 'lib'));
323
+ });
324
+ speaks(rig, [['repo.discovered', { path: path.join(rig.dir, 'lib') }]],
325
+ 'a repository born while nothing ran is discovered by the catch-up census');
326
+ clean(rig);
327
+ },
328
+ },
329
+
330
+ workspace: {
331
+ create: () => {
332
+ const rig = filesRoot((dir) => {
333
+ fs.writeFileSync(path.join(dir, 'notes.md'), 'n');
334
+ fs.mkdirSync(path.join(dir, 'sub'));
335
+ fs.writeFileSync(path.join(dir, 'sub', 'plan.md'), 'p');
336
+ });
337
+ assert.equal(rig.root.kind, 'files', 'no .git, so the ground is a workspace');
338
+ rig.mark = 0;
339
+ speaks(rig, [
340
+ ['dir.created', { path: 'sub' }],
341
+ ['file.created', { path: 'notes.md' }],
342
+ ['file.created', { path: 'sub/plan.md' }],
343
+ ], 'enrollment is the birth of the members');
344
+ clean(rig);
345
+ },
346
+ change: () => {
347
+ const rig = filesRoot((dir) => fs.writeFileSync(path.join(dir, 'notes.md'), 'v1'));
348
+ fs.writeFileSync(path.join(rig.dir, 'notes.md'), 'v2');
349
+ rig.observer.scanNow(rig.root.rootId);
350
+ speaks(rig, [['file.changed', { path: 'notes.md' }]],
351
+ 'the workspace has no voice of its own: its members speak');
352
+ clean(rig);
353
+ },
354
+ rename: () => {
355
+ const rig = filesRoot((dir) => fs.writeFileSync(path.join(dir, 'notes.md'), 'n'));
356
+ const renamed = `${rig.dir}-v2`;
357
+ fs.renameSync(rig.dir, renamed);
358
+ rig.observer.scanNow(rig.root.rootId);
359
+ speaks(rig, [['root.moved', { from: rig.dir, to: renamed }]],
360
+ 'a same-parent rename is followed: same identity, new address');
361
+ assert.equal(rig.observer.listRoots()[0].path, renamed);
362
+ clean(rig);
363
+ },
364
+ move: () => {
365
+ const rig = filesRoot((dir) => fs.writeFileSync(path.join(dir, 'notes.md'), 'n'));
366
+ const elsewhere = makeDir('elsewhere');
367
+ fs.renameSync(rig.dir, path.join(elsewhere, 'taken'));
368
+ rig.observer.scanNow(rig.root.rootId);
369
+ speaks(rig, [['root.unavailable', { path: rig.dir }]],
370
+ 'beyond the parent is unwatched ground: honest absence, never a guess');
371
+ assert.equal(rig.observer.listRoots()[0].path, rig.dir,
372
+ 'belief is preserved: the root stays enrolled at its last known address');
373
+ },
374
+ disappear: () => {
375
+ const rig = filesRoot((dir) => fs.writeFileSync(path.join(dir, 'notes.md'), 'n'));
376
+ fs.rmSync(rig.dir, { recursive: true });
377
+ rig.observer.scanNow(rig.root.rootId);
378
+ speaks(rig, [['root.unavailable', { path: rig.dir }]],
379
+ 'presence is evidence, never lifecycle: no trash, no delete, no burial');
380
+ assert.equal(rig.observer.listRoots().length, 1, 'the root stays enrolled');
381
+ },
382
+ restart: () => {
383
+ let renamed;
384
+ const rig = afterRestart(
385
+ (dir) => fs.writeFileSync(path.join(dir, 'notes.md'), 'n'),
386
+ (dir) => {
387
+ renamed = `${dir}-r2`;
388
+ fs.renameSync(dir, renamed);
389
+ });
390
+ speaks(rig, [['root.moved', { from: rig.dir, to: renamed }]],
391
+ 'a rename made while nothing ran is followed at the startup entrance');
392
+ assert.equal(rig.observer.listRoots()[0].path, renamed);
393
+ clean(rig);
394
+ },
395
+ },
396
+ };
397
+
398
+ test('the life grid is total: every kind Detect can see faces every life event — a missing box convicts', () => {
399
+ const sighted = ENTITY_TYPES.filter((type) => adapterFor(type).rails.detect);
400
+ const blind = ENTITY_TYPES.filter((type) => !adapterFor(type).rails.detect);
401
+ assert.deepEqual(Object.keys(GRID).sort(), sighted.sort(),
402
+ 'the rows are exactly the alphabet types whose adapter binds the detect socket — a new type, or a blind one gaining its socket, must take its row here');
403
+ assert.deepEqual(blind, ['reference'],
404
+ 'the one type Detect cannot see today: its truth is registry identity, its rendering derived state proven in the registration suite — a row here would invent semantics ahead of the socket');
405
+ for (const type of Object.keys(GRID)) {
406
+ assert.deepEqual(Object.keys(GRID[type]), LIFE_EVENTS,
407
+ `the ${type} row must face exactly the six life events`);
408
+ }
409
+ });
410
+
411
+ for (const type of Object.keys(GRID)) {
412
+ for (const [event, cell] of Object.entries(GRID[type])) {
413
+ test(`life grid — ${type} × ${event}`, cell);
414
+ }
415
+ }
416
+
417
+ // --- coda: constructs are plain ground ------------------------------------
418
+
419
+ test('constructs are plain ground: automation, agent, and skill shapes speak only the physical grammar', () => {
420
+ const rig = filesRoot((dir) => {
421
+ fs.mkdirSync(path.join(dir, 'automations', 'daily-digest'), { recursive: true });
422
+ fs.writeFileSync(path.join(dir, 'automations', 'daily-digest', 'automation.md'), '# daily digest');
423
+ fs.writeFileSync(path.join(dir, 'automations', 'daily-digest', 'run.js'), 'export {}');
424
+ fs.mkdirSync(path.join(dir, 'agents', 'scribe'), { recursive: true });
425
+ fs.writeFileSync(path.join(dir, 'agents', 'scribe', 'agent.md'), '# scribe');
426
+ fs.mkdirSync(path.join(dir, 'skills', 'summarize'), { recursive: true });
427
+ fs.writeFileSync(path.join(dir, 'skills', 'summarize', 'SKILL.md'), '# summarize');
428
+ });
429
+ const birthVoice = new Set(rig.events.map((event) => event.type));
430
+ assert.deepEqual([...birthVoice].sort(), ['dir.created', 'file.created'],
431
+ 'birth is folders and files — a construct has no voice of its own');
432
+
433
+ // A construct rename is a folder rename; a construct death is a
434
+ // folder death. Meaning is layered above; the grammar never changes.
435
+ rig.mark = rig.events.length;
436
+ fs.renameSync(path.join(rig.dir, 'automations', 'daily-digest'),
437
+ path.join(rig.dir, 'automations', 'weekly-digest'));
438
+ fs.rmSync(path.join(rig.dir, 'agents'), { recursive: true });
439
+ rig.observer.scanNow(rig.root.rootId);
440
+ speaks(rig, [
441
+ ['dir.renamed', { from: 'automations/daily-digest', to: 'automations/weekly-digest' }],
442
+ ['file.renamed', { from: 'automations/daily-digest/automation.md', to: 'automations/weekly-digest/automation.md' }],
443
+ ['file.renamed', { from: 'automations/daily-digest/run.js', to: 'automations/weekly-digest/run.js' }],
444
+ ['dir.deleted', { path: 'agents' }],
445
+ ['dir.deleted', { path: 'agents/scribe' }],
446
+ ['file.deleted', { path: 'agents/scribe/agent.md' }],
447
+ ]);
448
+ clean(rig);
449
+ });