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
@@ -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
- sharedChannelId,
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
- * Decide what an already-bound replica may do with a snapshot before the
258
- * caller touches its materialization.
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 inspectReplicaSnapshot(input, options = {}) {
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 current = getReplicaByResourceId(resourceId, { database });
277
- if (!current) throw invalid(`unknown shared replica ${resourceId}`, 404);
278
- const projectedAuthorityEpoch = options.projectedAuthorityEpoch == null
279
- ? current.authorityEpoch
280
- : integer(options.projectedAuthorityEpoch, 'projectedAuthorityEpoch', 1);
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 codeProject = body?.contract === 'code-project-stream@2';
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
- : contract === 'code-project-stream@2'
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
+ });