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.
- package/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- 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
|
+
});
|