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
|
@@ -233,26 +233,6 @@ function acknowledgePromotion(promotionIdInput, acknowledgement, options = {}) {
|
|
|
233
233
|
// the shared channel — their base is the promoted snapshot — so the
|
|
234
234
|
// normal outbox claim delivers them. Their journal identity, order, and
|
|
235
235
|
// already-projected local events are untouched.
|
|
236
|
-
const pendingRehomes = cachedStatement(database, `
|
|
237
|
-
SELECT channel_id, mutation_id
|
|
238
|
-
FROM mutation_journal
|
|
239
|
-
WHERE local_resource = ?
|
|
240
|
-
AND shared_resource_id IS NULL
|
|
241
|
-
AND mutation_origin = 'local'
|
|
242
|
-
AND journal_id > ?
|
|
243
|
-
ORDER BY journal_id ASC
|
|
244
|
-
`).all(promotion.localResource, promotion.journalCursor);
|
|
245
|
-
const sharedChannelId = `resource:${replica.resourceId}`;
|
|
246
|
-
for (const mutation of pendingRehomes) {
|
|
247
|
-
// The permanent mutation ID joins semantic and physical durability.
|
|
248
|
-
// Rekey both halves in this transaction; otherwise restart can see the
|
|
249
|
-
// shared journal row and strand its still-local native file receipt.
|
|
250
|
-
require('./doc-disk').rehomeMutation(database, {
|
|
251
|
-
mutationId: mutation.mutation_id,
|
|
252
|
-
oldChannelId: mutation.channel_id,
|
|
253
|
-
newChannelId: sharedChannelId,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
236
|
const rehomed = cachedStatement(database, `
|
|
257
237
|
UPDATE mutation_journal
|
|
258
238
|
SET channel_id = ?,
|
|
@@ -266,7 +246,7 @@ function acknowledgePromotion(promotionIdInput, acknowledgement, options = {}) {
|
|
|
266
246
|
AND mutation_origin = 'local'
|
|
267
247
|
AND journal_id > ?
|
|
268
248
|
`).run(
|
|
269
|
-
|
|
249
|
+
`resource:${replica.resourceId}`,
|
|
270
250
|
replica.resourceId,
|
|
271
251
|
require('../workspace/identity').machineId(),
|
|
272
252
|
replica.authorityEpoch,
|
|
@@ -35,41 +35,6 @@ function integer(value, name, minimum) {
|
|
|
35
35
|
return number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* A snapshot is an official prefix, never an independently numbered value.
|
|
40
|
-
* Validate the complete authority envelope before any caller touches disk,
|
|
41
|
-
* memory, or SQLite so every snapshot contract shares one ordering boundary.
|
|
42
|
-
*/
|
|
43
|
-
function validateSnapshotEnvelope(input, expected = {}) {
|
|
44
|
-
const resourceId = requiredText(input?.resourceId, 'resourceId', 132);
|
|
45
|
-
if (!isCloudResourceId(resourceId)) {
|
|
46
|
-
throw invalid('resourceId must be a node UUID or legacy cloud id');
|
|
47
|
-
}
|
|
48
|
-
const contract = requiredText(input?.contract, 'contract', 128);
|
|
49
|
-
if (!CONTRACT.test(contract)) throw invalid('contract has an invalid format');
|
|
50
|
-
const schemaVersion = integer(input?.schemaVersion, 'schemaVersion', 1);
|
|
51
|
-
const authorityEpoch = integer(input?.authorityEpoch, 'authorityEpoch', 1);
|
|
52
|
-
const snapshotVersion = integer(input?.snapshotVersion, 'snapshotVersion', 0);
|
|
53
|
-
const headVersion = integer(input?.headVersion, 'headVersion', 0);
|
|
54
|
-
if (expected.contract && contract !== expected.contract) {
|
|
55
|
-
throw invalid(`snapshot contract must be ${expected.contract}`, 409);
|
|
56
|
-
}
|
|
57
|
-
if (expected.schemaVersion && schemaVersion !== expected.schemaVersion) {
|
|
58
|
-
throw invalid(`snapshot schemaVersion must be ${expected.schemaVersion}`, 409);
|
|
59
|
-
}
|
|
60
|
-
if (snapshotVersion > headVersion) {
|
|
61
|
-
throw invalid('snapshotVersion cannot exceed headVersion', 409);
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
resourceId,
|
|
65
|
-
contract,
|
|
66
|
-
schemaVersion,
|
|
67
|
-
authorityEpoch,
|
|
68
|
-
snapshotVersion,
|
|
69
|
-
headVersion,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
38
|
function normalizeReplica(row) {
|
|
74
39
|
if (!row) return null;
|
|
75
40
|
return {
|
|
@@ -254,17 +219,11 @@ function markReplicaRevoked(input, options = {}) {
|
|
|
254
219
|
}
|
|
255
220
|
|
|
256
221
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* Rail version and authority epoch are independent coordinates. Most
|
|
261
|
-
* contracts have one canonical value at a rail version, while an
|
|
262
|
-
* authority-projected contract (the workspace tree) may legitimately have
|
|
263
|
-
* different visible bytes at the same rail version after an access change.
|
|
264
|
-
* Older authority is never allowed to install, even when it carries a larger
|
|
265
|
-
* rail number.
|
|
222
|
+
* A freshly attached official snapshot subsumes every version at or before
|
|
223
|
+
* it, so the replica's cursors jump forward to the snapshot — never backward:
|
|
224
|
+
* a replica that already applied past the snapshot keeps its position.
|
|
266
225
|
*/
|
|
267
|
-
function
|
|
226
|
+
function advanceReplicaSnapshot(input, options = {}) {
|
|
268
227
|
const resourceId = requiredText(input?.resourceId, 'resourceId', 132);
|
|
269
228
|
const snapshotVersion = integer(input?.snapshotVersion, 'snapshotVersion', 0);
|
|
270
229
|
const snapshotChecksum = requiredText(input?.snapshotChecksum, 'snapshotChecksum', 64);
|
|
@@ -273,101 +232,23 @@ function inspectReplicaSnapshot(input, options = {}) {
|
|
|
273
232
|
}
|
|
274
233
|
const authorityEpoch = integer(input?.authorityEpoch, 'authorityEpoch', 1);
|
|
275
234
|
const database = options.database || openLocalDb();
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
authorityEpoch < current.authorityEpoch
|
|
283
|
-
|| authorityEpoch < projectedAuthorityEpoch
|
|
284
|
-
) {
|
|
285
|
-
throw invalid(
|
|
286
|
-
`shared resource ${resourceId} snapshot has stale authority epoch ${authorityEpoch}`,
|
|
287
|
-
409,
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
// `authorityEpoch` on the replica is the newest authority advertised by
|
|
291
|
-
// the tunnel. `projectedAuthorityEpoch` is the authority whose bytes and
|
|
292
|
-
// permissions are physically installed. A reconnect may advance the first
|
|
293
|
-
// coordinate before the same-version projection arrives, so projection
|
|
294
|
-
// eligibility must compare with the second.
|
|
295
|
-
const authorityChanged = authorityEpoch > projectedAuthorityEpoch;
|
|
296
|
-
const authorityProjection = options.allowAuthorityProjection === true && authorityChanged;
|
|
297
|
-
if (
|
|
298
|
-
current.snapshotVersion === snapshotVersion
|
|
299
|
-
&& current.snapshotChecksum !== null
|
|
300
|
-
&& current.snapshotChecksum !== snapshotChecksum
|
|
301
|
-
&& !authorityProjection
|
|
302
|
-
) {
|
|
303
|
-
throw invalid(
|
|
304
|
-
`shared resource ${resourceId} has unequal state at version ${snapshotVersion}`,
|
|
305
|
-
409,
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
if (snapshotVersion < current.appliedVersion) {
|
|
309
|
-
return { action: 'stale', authorityChanged, replica: current };
|
|
310
|
-
}
|
|
311
|
-
if (authorityProjection) {
|
|
312
|
-
return { action: 'install', authorityChanged, replica: current };
|
|
313
|
-
}
|
|
314
|
-
if (snapshotVersion === current.appliedVersion) {
|
|
315
|
-
if (
|
|
316
|
-
current.snapshotVersion === snapshotVersion
|
|
317
|
-
&& current.snapshotChecksum === null
|
|
318
|
-
) {
|
|
319
|
-
return { action: 'metadata', authorityChanged, replica: current };
|
|
320
|
-
}
|
|
321
|
-
if (
|
|
322
|
-
current.snapshotVersion === snapshotVersion
|
|
323
|
-
&& current.snapshotChecksum === snapshotChecksum
|
|
324
|
-
) {
|
|
325
|
-
return { action: authorityChanged ? 'metadata' : 'duplicate', authorityChanged, replica: current };
|
|
326
|
-
}
|
|
327
|
-
if (current.snapshotVersion < snapshotVersion) {
|
|
328
|
-
return { action: 'metadata', authorityChanged, replica: current };
|
|
329
|
-
}
|
|
330
|
-
throw invalid(
|
|
331
|
-
`shared resource ${resourceId} has unequal state at version ${snapshotVersion}`,
|
|
332
|
-
409,
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
return { action: 'install', authorityChanged, replica: current };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* A freshly attached official snapshot subsumes every version at or before
|
|
340
|
-
* it, so the replica's cursors jump forward to the snapshot — never backward:
|
|
341
|
-
* a replica that already applied past the snapshot keeps its position.
|
|
342
|
-
*/
|
|
343
|
-
function advanceReplicaSnapshot(input, options = {}) {
|
|
344
|
-
const database = options.database || openLocalDb();
|
|
345
|
-
const disposition = inspectReplicaSnapshot(input, options);
|
|
346
|
-
const {
|
|
347
|
-
resourceId,
|
|
348
|
-
snapshotVersion,
|
|
349
|
-
snapshotChecksum,
|
|
350
|
-
authorityEpoch,
|
|
351
|
-
} = {
|
|
352
|
-
resourceId: disposition.replica.resourceId,
|
|
353
|
-
snapshotVersion: integer(input?.snapshotVersion, 'snapshotVersion', 0),
|
|
354
|
-
snapshotChecksum: requiredText(input?.snapshotChecksum, 'snapshotChecksum', 64),
|
|
355
|
-
authorityEpoch: integer(input?.authorityEpoch, 'authorityEpoch', 1),
|
|
356
|
-
};
|
|
357
|
-
if (disposition.action === 'stale' || disposition.action === 'duplicate') {
|
|
358
|
-
return disposition.replica;
|
|
359
|
-
}
|
|
235
|
+
const row = cachedStatement(
|
|
236
|
+
database,
|
|
237
|
+
'SELECT * FROM shared_resource_replicas WHERE resource_id = ?',
|
|
238
|
+
).get(resourceId);
|
|
239
|
+
if (!row) throw invalid(`unknown shared replica ${resourceId}`, 404);
|
|
360
240
|
cachedStatement(database, `
|
|
361
241
|
UPDATE shared_resource_replicas
|
|
362
|
-
SET applied_version =
|
|
363
|
-
snapshot_version =
|
|
364
|
-
snapshot_checksum =
|
|
242
|
+
SET applied_version = MAX(applied_version, ?),
|
|
243
|
+
snapshot_version = MAX(snapshot_version, ?),
|
|
244
|
+
snapshot_checksum = CASE WHEN snapshot_version <= ? THEN ? ELSE snapshot_checksum END,
|
|
365
245
|
authority_epoch = MAX(authority_epoch, ?),
|
|
366
246
|
sync_status = 'synced',
|
|
367
247
|
last_error = NULL,
|
|
368
248
|
updated_at = ?
|
|
369
249
|
WHERE resource_id = ?
|
|
370
250
|
`).run(
|
|
251
|
+
snapshotVersion,
|
|
371
252
|
snapshotVersion,
|
|
372
253
|
snapshotVersion,
|
|
373
254
|
snapshotChecksum,
|
|
@@ -387,6 +268,4 @@ module.exports = {
|
|
|
387
268
|
markReplicaRevoked,
|
|
388
269
|
refreshReplicaAuthority,
|
|
389
270
|
registerReplica,
|
|
390
|
-
inspectReplicaSnapshot,
|
|
391
|
-
validateSnapshotEnvelope,
|
|
392
271
|
};
|
|
@@ -118,35 +118,9 @@ async function handleWorkspaceContentDiscover(body, sendJson) {
|
|
|
118
118
|
|
|
119
119
|
async function handlePromotionStart(body, sendJson) {
|
|
120
120
|
try {
|
|
121
|
-
const codeProject = body?.contract === 'code-project-stream@2';
|
|
122
121
|
const requestedResourceId = typeof body?.resourceId === 'string' && body.resourceId.trim()
|
|
123
122
|
? body.resourceId.trim()
|
|
124
123
|
: null;
|
|
125
|
-
if (codeProject) {
|
|
126
|
-
if (!requestedResourceId) {
|
|
127
|
-
const error = new Error('code project promotion requires a resourceId');
|
|
128
|
-
error.statusCode = 409;
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
const projectStream = require('../workspace/project-stream');
|
|
132
|
-
const localPath = require('path').resolve(body?.path || '');
|
|
133
|
-
const snapshot = projectStream.readSnapshot(localPath, requestedResourceId);
|
|
134
|
-
const bytes = Buffer.from(`${JSON.stringify(snapshot)}\n`, 'utf8');
|
|
135
|
-
const result = require('./promotions').enqueuePromotion({
|
|
136
|
-
promotionId: body?.promotionId || `prm_${crypto.randomBytes(16).toString('base64url')}`,
|
|
137
|
-
resourceId: requestedResourceId,
|
|
138
|
-
localResource: `${projectStream.LOCAL_RESOURCE_PREFIX}${requestedResourceId}`,
|
|
139
|
-
localPath,
|
|
140
|
-
contract: projectStream.STREAM_CONTRACT,
|
|
141
|
-
schemaVersion: 2,
|
|
142
|
-
snapshotBase64: bytes.toString('base64'),
|
|
143
|
-
contentBytes: bytes.length,
|
|
144
|
-
name: body?.name,
|
|
145
|
-
mediaType: 'application/vnd.amalgm.code-project+json',
|
|
146
|
-
});
|
|
147
|
-
sendJson(result.duplicate ? 200 : 202, result);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
124
|
const requestedNodeId = requestedResourceId
|
|
151
125
|
&& require('../workspace/tree/identity').isUuid(requestedResourceId)
|
|
152
126
|
? requestedResourceId.toLowerCase()
|
|
@@ -240,23 +214,13 @@ async function handlePromotionFail(body, sendJson) {
|
|
|
240
214
|
async function handleAttachmentStart(body, sendJson) {
|
|
241
215
|
try {
|
|
242
216
|
const tree = body?.contract === 'workspace-tree@1';
|
|
243
|
-
const
|
|
244
|
-
const localPath = tree || codeProject
|
|
245
|
-
? require('path').resolve(body?.path || '')
|
|
246
|
-
: require('./docs').openDoc(body?.path).path;
|
|
217
|
+
const localPath = tree ? require('path').resolve(body?.path || '') : require('./docs').openDoc(body?.path).path;
|
|
247
218
|
if (tree) {
|
|
248
219
|
const stats = fs.statSync(localPath);
|
|
249
220
|
if (!stats.isDirectory() || fs.readdirSync(localPath).length > 0) {
|
|
250
221
|
sendJson(409, { error: 'cloud workspace attachment requires an empty folder' });
|
|
251
222
|
return;
|
|
252
223
|
}
|
|
253
|
-
} else if (codeProject) {
|
|
254
|
-
const stream = require('../workspace/project-stream');
|
|
255
|
-
const snapshot = stream.readSnapshot(localPath, body?.resourceId);
|
|
256
|
-
if (snapshot.version !== 0 || snapshot.card !== null || snapshot.checkpoint !== null) {
|
|
257
|
-
sendJson(409, { error: 'code project attachment requires an empty local projection' });
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
224
|
} else if (require('./docs').getDocText(localPath).length > 0) {
|
|
261
225
|
sendJson(409, { error: 'cloud attachment requires an empty local text file' });
|
|
262
226
|
return;
|
|
@@ -353,12 +317,7 @@ async function handleMutationFail(body, sendJson) {
|
|
|
353
317
|
sendJson(200, require('./mutations').failCloudMutation(
|
|
354
318
|
body?.resourceId,
|
|
355
319
|
body?.mutationId,
|
|
356
|
-
{
|
|
357
|
-
code: body?.code,
|
|
358
|
-
message: body?.message,
|
|
359
|
-
retryable: body?.retryable,
|
|
360
|
-
retryAt: body?.retryAt,
|
|
361
|
-
},
|
|
320
|
+
{ message: body?.message, retryable: body?.retryable, retryAt: body?.retryAt },
|
|
362
321
|
));
|
|
363
322
|
} catch (error) {
|
|
364
323
|
sendHandlerError(error, sendJson);
|
|
@@ -372,9 +331,7 @@ async function handleMutationIngest(body, sendJson) {
|
|
|
372
331
|
? require('../browser/cookie-jar').applyCloudCookieMutation(body)
|
|
373
332
|
: contract === 'workspace-tree@1'
|
|
374
333
|
? require('../workspace/tree-store').applyCloudTreeMutation(body)
|
|
375
|
-
:
|
|
376
|
-
? require('../workspace/project-stream').applyCloudMutation(body)
|
|
377
|
-
: require('./docs').applyCloudDocMutation(body);
|
|
334
|
+
: require('./docs').applyCloudDocMutation(body);
|
|
378
335
|
sendJson(200, result);
|
|
379
336
|
} catch (error) {
|
|
380
337
|
sendHandlerError(error, sendJson);
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The adapter contract exams (adapters/): the ratified three-adapter
|
|
5
|
+
* map over the registry's physical alphabet, the socket law proven the
|
|
6
|
+
* only way that counts — ONE GENERIC CALLER reading, applying, and
|
|
7
|
+
* verifying real ground of every kind through identical calls, zero
|
|
8
|
+
* type branching — and the contract's convictions: each stated law
|
|
9
|
+
* shown to refuse its violation, not just to accept lawful shape.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const test = require('node:test');
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execFileSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const { ENTITY_TYPES } = require('../registry');
|
|
20
|
+
const { ADAPTERS, RAILS, SOCKETS, adapterFor, assertAdapter, buildTypeMap } = require('../adapters');
|
|
21
|
+
const filesystem = require('../adapters/filesystem');
|
|
22
|
+
const git = require('../adapters/git');
|
|
23
|
+
const reference = require('../adapters/reference');
|
|
24
|
+
|
|
25
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-adapters-'));
|
|
26
|
+
|
|
27
|
+
test.after(() => {
|
|
28
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function ground(name) {
|
|
32
|
+
const dir = path.join(tempRoot, name);
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeRepo(name) {
|
|
38
|
+
const dir = fs.realpathSync(ground(name));
|
|
39
|
+
const run = (...args) => execFileSync('git', args, { cwd: dir, stdio: 'pipe' });
|
|
40
|
+
run('init', '--quiet');
|
|
41
|
+
run('config', 'user.email', 'adapters@amalgm.test');
|
|
42
|
+
run('config', 'user.name', 'Adapters');
|
|
43
|
+
run('config', 'commit.gpgsign', 'false');
|
|
44
|
+
fs.writeFileSync(path.join(dir, 'seed.txt'), 'seed\n');
|
|
45
|
+
run('add', '-A');
|
|
46
|
+
run('commit', '--quiet', '-m', 'seed');
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A minimal lawful descriptor; each conviction exam breaks exactly one law. */
|
|
51
|
+
function lawfulAdapter(overrides = {}) {
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
name: 'exam',
|
|
54
|
+
types: Object.freeze({ 'file.text': 'its bytes' }),
|
|
55
|
+
rails: Object.freeze({ detect: Object.freeze({ truth: () => null }) }),
|
|
56
|
+
...overrides,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- The physical alphabet and the ratified map ----------------------
|
|
61
|
+
|
|
62
|
+
test('the physical alphabet is exactly the ratified seven', () => {
|
|
63
|
+
assert.deepEqual([...ENTITY_TYPES],
|
|
64
|
+
['file.binary', 'file.text', 'folder', 'link', 'reference', 'repo.git', 'workspace']);
|
|
65
|
+
assert.ok(Object.isFrozen(ENTITY_TYPES));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('constructs are not physical types: meaning is layered above, never encoded as a kind', () => {
|
|
69
|
+
for (const construct of ['automation', 'agent', 'app', 'tool', 'skill']) {
|
|
70
|
+
assert.ok(!ENTITY_TYPES.includes(construct), `${construct} is not in the physical alphabet`);
|
|
71
|
+
assert.throws(() => adapterFor(construct), /no adapter serves entity type/);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the seven rails are declared once, frozen, in journey order', () => {
|
|
76
|
+
assert.deepEqual([...RAILS], ['watch', 'detect', 'record', 'send', 'receive', 'apply', 'verify']);
|
|
77
|
+
assert.ok(Object.isFrozen(RAILS));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('every type in the registry alphabet is assigned exactly one adapter', () => {
|
|
81
|
+
for (const type of ENTITY_TYPES) {
|
|
82
|
+
const claimants = ADAPTERS.filter((adapter) => type in adapter.types);
|
|
83
|
+
assert.equal(claimants.length, 1, `${type} must have exactly one adapter`);
|
|
84
|
+
assert.equal(adapterFor(type), claimants[0]);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('the adapter set is exactly the ratified three', () => {
|
|
89
|
+
assert.deepEqual(ADAPTERS.map((adapter) => adapter.name).sort(), ['filesystem', 'git', 'reference']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('same-ground transition kinds share ONE adapter, so a transition never changes hands', () => {
|
|
93
|
+
// file ↔ link on one slot, text ↔ binary, workspace shedding to
|
|
94
|
+
// folder on adoption: all inside the filesystem adapter by design.
|
|
95
|
+
for (const type of ['workspace', 'folder', 'file.text', 'file.binary', 'link']) {
|
|
96
|
+
assert.equal(adapterFor(type), filesystem, `${type} rides the filesystem adapter`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('a transition may cross adapters as a handoff: folder and repo.git are served apart', () => {
|
|
101
|
+
// becomeRepo/becomeFiles moves a container between these two on the
|
|
102
|
+
// SAME UUID — identity is the registry's, never the adapter's.
|
|
103
|
+
assert.equal(adapterFor('repo.git'), git);
|
|
104
|
+
assert.notEqual(adapterFor('folder'), adapterFor('repo.git'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('an unknown type convicts and names the alphabet', () => {
|
|
108
|
+
assert.throws(() => adapterFor('hologram'), /no adapter serves entity type "hologram"/);
|
|
109
|
+
assert.throws(() => adapterFor('hologram'), new RegExp(ENTITY_TYPES.join(', ')));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('every adapter descriptor satisfies the contract it shipped under', () => {
|
|
113
|
+
for (const adapter of ADAPTERS) assert.equal(assertAdapter(adapter), adapter);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('the reference adapter is assigned, not yet operational — absence is honest, a stub is not', () => {
|
|
117
|
+
assert.deepEqual(Object.keys(reference.rails), []);
|
|
118
|
+
assert.ok(Object.isFrozen(reference.rails));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('every declared truth is one non-empty sentence', () => {
|
|
122
|
+
for (const adapter of ADAPTERS) {
|
|
123
|
+
for (const [type, truth] of Object.entries(adapter.types)) {
|
|
124
|
+
assert.ok(typeof truth === 'string' && truth.length > 0, `${adapter.name}/${type} declares truth`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---- The socket law, proven by a generic caller ----------------------
|
|
130
|
+
|
|
131
|
+
test('PROOF: one generic caller reads truth for every kind through one socket — zero type branching', () => {
|
|
132
|
+
const folderPath = ground('proof-folder');
|
|
133
|
+
fs.writeFileSync(path.join(folderPath, 'member.txt'), 'hello');
|
|
134
|
+
const filePath = path.join(ground('proof-file'), 'notes.md');
|
|
135
|
+
fs.writeFileSync(filePath, 'first line\n');
|
|
136
|
+
const linkPath = path.join(ground('proof-link'), 'pointer');
|
|
137
|
+
fs.symlinkSync('../somewhere', linkPath);
|
|
138
|
+
const repoPath = makeRepo('proof-repo');
|
|
139
|
+
|
|
140
|
+
// The fixtures know their ground; the CALLER below is one loop with
|
|
141
|
+
// one call shape — it never asks "which kind is this?".
|
|
142
|
+
const fixtures = [
|
|
143
|
+
{ type: 'folder', path: folderPath, change: () => fs.writeFileSync(path.join(folderPath, 'newcomer.txt'), 'x') },
|
|
144
|
+
{ type: 'file.text', path: filePath, change: () => fs.appendFileSync(filePath, 'second line\n') },
|
|
145
|
+
{ type: 'link', path: linkPath, change: () => { fs.unlinkSync(linkPath); fs.symlinkSync('../elsewhere', linkPath); } },
|
|
146
|
+
{ type: 'repo.git', path: repoPath, change: () => fs.writeFileSync(path.join(repoPath, 'untracked.txt'), 'new work') },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
for (const fixture of fixtures) {
|
|
150
|
+
const read = adapterFor(fixture.type).rails.detect.truth(fixture.path);
|
|
151
|
+
assert.equal(read.truth, 'present', `${fixture.type} truth is present`);
|
|
152
|
+
assert.ok(typeof read.version === 'string' && read.version.length > 0, `${fixture.type} truth carries a version`);
|
|
153
|
+
|
|
154
|
+
const again = adapterFor(fixture.type).rails.detect.truth(fixture.path);
|
|
155
|
+
assert.equal(again.version, read.version, `${fixture.type}: equal truth is silence`);
|
|
156
|
+
|
|
157
|
+
fixture.change();
|
|
158
|
+
const changed = adapterFor(fixture.type).rails.detect.truth(fixture.path);
|
|
159
|
+
assert.equal(changed.truth, 'present');
|
|
160
|
+
assert.notEqual(changed.version, read.version, `${fixture.type}: changed truth changes the version`);
|
|
161
|
+
|
|
162
|
+
fs.rmSync(fixture.path, { recursive: true, force: true });
|
|
163
|
+
const gone = adapterFor(fixture.type).rails.detect.truth(fixture.path);
|
|
164
|
+
assert.equal(gone.truth, 'absent', `${fixture.type}: vanished ground is honest absence, never an error`);
|
|
165
|
+
assert.ok(!('version' in gone), `${fixture.type}: absence claims no version`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('a repo mid-operation is honestly unsettled — truth unreadable is a state, never an error', () => {
|
|
170
|
+
const repoPath = makeRepo('busy-repo');
|
|
171
|
+
fs.writeFileSync(path.join(repoPath, '.git', 'MERGE_HEAD'), '0'.repeat(40));
|
|
172
|
+
const read = adapterFor('repo.git').rails.detect.truth(repoPath);
|
|
173
|
+
assert.equal(read.truth, 'unsettled');
|
|
174
|
+
assert.equal(read.reason, 'MERGE_HEAD');
|
|
175
|
+
assert.ok(!('version' in read), 'unsettled truth claims no version');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('ground without a repository is an absent repo.git, whatever else stands there', () => {
|
|
179
|
+
const plain = ground('no-repo');
|
|
180
|
+
fs.writeFileSync(path.join(plain, 'just-a-file.txt'), 'not a repo');
|
|
181
|
+
assert.equal(adapterFor('repo.git').rails.detect.truth(plain).truth, 'absent');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('a repository that cannot be inspected is unsettled — unreadable is never absent', () => {
|
|
185
|
+
// Absence withdraws territory; unsettledness preserves the last known
|
|
186
|
+
// truth and reads again. Mistaking one for the other is the lie the
|
|
187
|
+
// axiom forbids.
|
|
188
|
+
const repoPath = makeRepo('sealed-repo');
|
|
189
|
+
fs.chmodSync(repoPath, 0o000);
|
|
190
|
+
try {
|
|
191
|
+
const read = adapterFor('repo.git').rails.detect.truth(repoPath);
|
|
192
|
+
assert.equal(read.truth, 'unsettled');
|
|
193
|
+
assert.equal(read.reason, 'EACCES');
|
|
194
|
+
assert.ok(!('version' in read), 'unsettled truth claims no version');
|
|
195
|
+
} finally {
|
|
196
|
+
fs.chmodSync(repoPath, 0o755);
|
|
197
|
+
}
|
|
198
|
+
assert.equal(adapterFor('repo.git').rails.detect.truth(repoPath).truth, 'present',
|
|
199
|
+
'the same ground, readable again, settles to presence');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('a git command failing mid-capture is unsettled — never a fourth answer', () => {
|
|
203
|
+
// The repo and its .git are visible, but git itself cannot read HEAD:
|
|
204
|
+
// capture fails AFTER the .git look succeeded. The contract promises
|
|
205
|
+
// exactly three answers — a throw is not one of them.
|
|
206
|
+
const repoPath = makeRepo('headless-repo');
|
|
207
|
+
fs.chmodSync(path.join(repoPath, '.git', 'HEAD'), 0o000);
|
|
208
|
+
try {
|
|
209
|
+
const read = adapterFor('repo.git').rails.detect.truth(repoPath);
|
|
210
|
+
assert.equal(read.truth, 'unsettled');
|
|
211
|
+
assert.match(read.reason, /^git /, 'the reason names the git command that failed');
|
|
212
|
+
} finally {
|
|
213
|
+
fs.chmodSync(path.join(repoPath, '.git', 'HEAD'), 0o644);
|
|
214
|
+
}
|
|
215
|
+
assert.equal(adapterFor('repo.git').rails.detect.truth(repoPath).truth, 'present',
|
|
216
|
+
'the same repository, readable again, settles to presence');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('a membership that cannot be read completely is unsettled — never quietly partial', () => {
|
|
220
|
+
const dir = ground('sealed');
|
|
221
|
+
fs.writeFileSync(path.join(dir, 'inside.txt'), 'x');
|
|
222
|
+
fs.chmodSync(dir, 0o000);
|
|
223
|
+
try {
|
|
224
|
+
const read = adapterFor('folder').rails.detect.truth(dir);
|
|
225
|
+
assert.equal(read.truth, 'unsettled');
|
|
226
|
+
assert.equal(read.reason, 'EACCES');
|
|
227
|
+
assert.ok(!('state' in read), 'unsettled truth claims no state');
|
|
228
|
+
} finally {
|
|
229
|
+
fs.chmodSync(dir, 0o755);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('STITCH: production detection and the adapter speak one truth — no parallel detector', () => {
|
|
234
|
+
// The observer's content decision (safeHash) now reads through the
|
|
235
|
+
// adapter socket. Prove agreement on real ground: the hash the
|
|
236
|
+
// production observer records for a file is byte-identical to the
|
|
237
|
+
// truth the adapter answers for the same ground.
|
|
238
|
+
const Database = require('better-sqlite3');
|
|
239
|
+
const { createObserver } = require('../observer');
|
|
240
|
+
const dir = fs.realpathSync(ground('stitch'));
|
|
241
|
+
fs.writeFileSync(path.join(dir, 'watched.txt'), 'one truth\n');
|
|
242
|
+
const events = [];
|
|
243
|
+
const observer = createObserver({ db: new Database(':memory:'), emit: (event) => events.push(event) });
|
|
244
|
+
try {
|
|
245
|
+
const root = observer.enrollRoot(dir);
|
|
246
|
+
observer.scanNow(root.rootId);
|
|
247
|
+
const born = events.find((event) => event.type === 'file.created' && event.path === 'watched.txt');
|
|
248
|
+
assert.ok(born, 'the observer detected the file');
|
|
249
|
+
const read = adapterFor('file.text').rails.detect.truth(path.join(dir, 'watched.txt'));
|
|
250
|
+
assert.equal(read.truth, 'present');
|
|
251
|
+
assert.equal(born.hash, read.state.sha256, 'observer-recorded content IS adapter truth');
|
|
252
|
+
} finally {
|
|
253
|
+
observer.stop?.();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('filesystem truth follows the ground, not the label: a slot swapping file for link swaps kinds', () => {
|
|
258
|
+
const slot = path.join(ground('swap'), 'occupant');
|
|
259
|
+
fs.writeFileSync(slot, 'bytes');
|
|
260
|
+
const asFile = adapterFor('file.text').rails.detect.truth(slot);
|
|
261
|
+
assert.equal(asFile.state.kind, 'file');
|
|
262
|
+
fs.unlinkSync(slot);
|
|
263
|
+
fs.symlinkSync('over-there', slot);
|
|
264
|
+
const asLink = adapterFor('link').rails.detect.truth(slot);
|
|
265
|
+
assert.equal(asLink.state.kind, 'link');
|
|
266
|
+
assert.equal(asLink.state.target, 'over-there');
|
|
267
|
+
assert.notEqual(asLink.version, asFile.version);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('folder truth is direct membership, never member content', () => {
|
|
271
|
+
const dir = ground('membership');
|
|
272
|
+
fs.writeFileSync(path.join(dir, 'a.txt'), 'one');
|
|
273
|
+
const before = adapterFor('folder').rails.detect.truth(dir);
|
|
274
|
+
assert.deepEqual(before.state.members, [{ name: 'a.txt', kind: 'file' }]);
|
|
275
|
+
fs.writeFileSync(path.join(dir, 'a.txt'), 'rewritten entirely');
|
|
276
|
+
const after = adapterFor('folder').rails.detect.truth(dir);
|
|
277
|
+
assert.equal(after.version, before.version, "a member's content is the member's truth, not the folder's");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---- The contract convicts -------------------------------------------
|
|
281
|
+
|
|
282
|
+
test('an unfrozen descriptor convicts: the contract is frozen ground', () => {
|
|
283
|
+
const thawed = { name: 'exam', types: Object.freeze({ link: 'its written target text' }), rails: Object.freeze({}) };
|
|
284
|
+
assert.throws(() => assertAdapter(thawed), /frozen object/);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('a nameless adapter convicts', () => {
|
|
288
|
+
assert.throws(() => assertAdapter(lawfulAdapter({ name: '' })), /name is a non-empty string/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('an adapter serving no types convicts', () => {
|
|
292
|
+
assert.throws(() => assertAdapter(lawfulAdapter({ types: Object.freeze({}) })), /non-empty map of entity type/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('an undeclared truth convicts: every type names its source of truth', () => {
|
|
296
|
+
assert.throws(
|
|
297
|
+
() => assertAdapter(lawfulAdapter({ types: Object.freeze({ 'file.text': '' }) })),
|
|
298
|
+
/must declare its source of truth/,
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('an invented rail convicts and names the seven', () => {
|
|
303
|
+
assert.throws(
|
|
304
|
+
() => assertAdapter(lawfulAdapter({ rails: Object.freeze({ teleport: Object.freeze({ go: () => null }) }) })),
|
|
305
|
+
/"teleport" is not a rail/,
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('binding a rail with no ratified socket convicts: the socket comes first', () => {
|
|
310
|
+
// apply and verify are real rails with real repocard implementations —
|
|
311
|
+
// and STILL unbindable, because no multi-type consumer has ratified
|
|
312
|
+
// their socket. Deferral is enforced, not just documented.
|
|
313
|
+
for (const rail of ['watch', 'record', 'send', 'receive', 'apply', 'verify']) {
|
|
314
|
+
assert.throws(
|
|
315
|
+
() => assertAdapter(lawfulAdapter({ rails: Object.freeze({ [rail]: Object.freeze({ go: () => null }) }) })),
|
|
316
|
+
/no ratified socket yet/,
|
|
317
|
+
`${rail} has no socket to bind`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('an adapter-specific surface name convicts: the socket is the same for everyone', () => {
|
|
323
|
+
assert.throws(
|
|
324
|
+
() => assertAdapter(lawfulAdapter({ rails: Object.freeze({ detect: Object.freeze({ walkRoot: () => null }) }) })),
|
|
325
|
+
/whole socket or binds the rail not at all/,
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('a partial or padded socket convicts: whole socket or nothing', () => {
|
|
330
|
+
assert.throws(
|
|
331
|
+
() => assertAdapter(lawfulAdapter({ rails: Object.freeze({ detect: Object.freeze({}) }) })),
|
|
332
|
+
/whole socket or binds the rail not at all/,
|
|
333
|
+
);
|
|
334
|
+
assert.throws(
|
|
335
|
+
() => assertAdapter(lawfulAdapter({ rails: Object.freeze({ detect: Object.freeze({ truth: () => null, extra: () => null }) }) })),
|
|
336
|
+
/whole socket or binds the rail not at all/,
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('a stubbed surface convicts: unbuilt means absent, never a placeholder', () => {
|
|
341
|
+
assert.throws(
|
|
342
|
+
() => assertAdapter(lawfulAdapter({ rails: Object.freeze({ detect: Object.freeze({ truth: null }) }) })),
|
|
343
|
+
/never stubbed/,
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('thawed rails or surfaces convict: freezing is per level, not just the top', () => {
|
|
348
|
+
assert.throws(
|
|
349
|
+
() => assertAdapter(lawfulAdapter({ rails: { detect: Object.freeze({ truth: () => null }) } })),
|
|
350
|
+
/rails is a frozen map/,
|
|
351
|
+
);
|
|
352
|
+
assert.throws(
|
|
353
|
+
() => assertAdapter(lawfulAdapter({ rails: Object.freeze({ detect: { truth: () => null } }) })),
|
|
354
|
+
/frozen map of its socket's surfaces/,
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// ---- The map builder convicts ----------------------------------------
|
|
359
|
+
|
|
360
|
+
test('two adapters claiming one type convicts and names both claimants', () => {
|
|
361
|
+
const a = lawfulAdapter({ name: 'first' });
|
|
362
|
+
const b = lawfulAdapter({ name: 'second' });
|
|
363
|
+
assert.throws(() => buildTypeMap([a, b], ['file.text']), /claimed by both first and second/);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('claiming a type outside the alphabet convicts', () => {
|
|
367
|
+
const a = lawfulAdapter({ types: Object.freeze({ hologram: 'projected light' }) });
|
|
368
|
+
assert.throws(() => buildTypeMap([a], ['file.text']), /not in the registry's alphabet/);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('an unassigned alphabet type convicts: every type is assigned', () => {
|
|
372
|
+
assert.throws(() => buildTypeMap([lawfulAdapter()], ['file.text', 'file.binary']), /"file.binary" has no adapter/);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('detect is the ONLY ratified socket: a socket is derived from a real consumer or not ratified at all', () => {
|
|
376
|
+
assert.ok(Object.isFrozen(SOCKETS));
|
|
377
|
+
assert.deepEqual(Object.keys(SOCKETS), ['detect']);
|
|
378
|
+
assert.deepEqual([...SOCKETS.detect], ['truth']);
|
|
379
|
+
});
|