amalgm 0.1.238 → 0.1.240

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 (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -14,9 +14,9 @@ const {
14
14
  renameWorkspace,
15
15
  upsertWorkspace,
16
16
  } = require('./store');
17
- const { listRepoCards, refreshRepoCards, repoCardServiceStats } = require('./cards');
17
+ const { listRepoCards, refreshRepoCards } = require('./cards');
18
18
  const { machineId } = require('./identity');
19
- const { readWall, wallStats } = require('./wall');
19
+ const { readWall } = require('./wall');
20
20
 
21
21
  const execFileAsync = promisify(execFile);
22
22
  const MAX_BUFFER = 10 * 1024 * 1024;
@@ -66,6 +66,10 @@ function enrollWorkspaceTree(workspace) {
66
66
  return require('./tree-store').enrollWorkspace(workspace.id);
67
67
  }
68
68
 
69
+ function workspaceAfterTreeEnrollment(workspace) {
70
+ return listWorkspaces().find((entry) => entry.id === workspace.id) || workspace;
71
+ }
72
+
69
73
  async function uniqueProjectPath(workspaceRoot, requestedName) {
70
74
  const name = sanitizeProjectName(requestedName);
71
75
  let candidate = path.join(workspaceRoot, name);
@@ -216,15 +220,20 @@ async function handleProjectRegister(body, sendJson) {
216
220
  });
217
221
  ensureProjectMemory(workspace, 'workspace:register');
218
222
  const tree = enrollWorkspaceTree(workspace);
223
+ const enrolledWorkspace = workspaceAfterTreeEnrollment(workspace);
219
224
 
220
225
  sendJson(200, {
221
226
  success: true,
222
- workspace,
227
+ workspace: enrolledWorkspace,
223
228
  path: workspace.path,
224
229
  name: workspace.name,
225
230
  uri: workspace.uri,
226
231
  workspaceRoot,
227
- tree: { nodes: tree.nodes.length, created: tree.created },
232
+ tree: {
233
+ nodes: tree.nodes.length,
234
+ created: tree.created,
235
+ observation: enrolledWorkspace.treeObservation,
236
+ },
228
237
  });
229
238
  } catch (error) {
230
239
  sendJson(500, {
@@ -351,14 +360,19 @@ async function handleImport(body, sendJson) {
351
360
  });
352
361
  ensureProjectMemory(workspace, 'workspace:import');
353
362
  const tree = enrollWorkspaceTree(workspace);
363
+ const enrolledWorkspace = workspaceAfterTreeEnrollment(workspace);
354
364
  sendJson(200, {
355
365
  success: true,
356
366
  path: workspace.path,
357
367
  name: workspace.name,
358
- workspace,
368
+ workspace: enrolledWorkspace,
359
369
  workspaceRoot,
360
370
  alreadyInWorkspace: true,
361
- tree: { nodes: tree.nodes.length, created: tree.created },
371
+ tree: {
372
+ nodes: tree.nodes.length,
373
+ created: tree.created,
374
+ observation: enrolledWorkspace.treeObservation,
375
+ },
362
376
  });
363
377
  return;
364
378
  }
@@ -384,15 +398,20 @@ async function handleImport(body, sendJson) {
384
398
  });
385
399
  ensureProjectMemory(workspace, 'workspace:import');
386
400
  const tree = enrollWorkspaceTree(workspace);
401
+ const enrolledWorkspace = workspaceAfterTreeEnrollment(workspace);
387
402
 
388
403
  sendJson(200, {
389
404
  success: true,
390
405
  path: workspace.path,
391
406
  name: workspace.name,
392
- workspace,
407
+ workspace: enrolledWorkspace,
393
408
  workspaceRoot,
394
409
  alreadyInWorkspace: false,
395
- tree: { nodes: tree.nodes.length, created: tree.created },
410
+ tree: {
411
+ nodes: tree.nodes.length,
412
+ created: tree.created,
413
+ observation: enrolledWorkspace.treeObservation,
414
+ },
396
415
  });
397
416
  } catch (error) {
398
417
  sendJson(500, {
@@ -433,13 +452,18 @@ async function handleClone(body, sendJson) {
433
452
  });
434
453
  ensureProjectMemory(workspace, 'workspace:clone');
435
454
  const tree = enrollWorkspaceTree(workspace);
455
+ const enrolledWorkspace = workspaceAfterTreeEnrollment(workspace);
436
456
  sendJson(200, {
437
457
  success: true,
438
458
  path: workspace.path,
439
- workspace,
459
+ workspace: enrolledWorkspace,
440
460
  workspaceRoot,
441
461
  alreadyExists: true,
442
- tree: { nodes: tree.nodes.length, created: tree.created },
462
+ tree: {
463
+ nodes: tree.nodes.length,
464
+ created: tree.created,
465
+ observation: enrolledWorkspace.treeObservation,
466
+ },
443
467
  });
444
468
  return;
445
469
  }
@@ -477,14 +501,19 @@ async function handleClone(body, sendJson) {
477
501
  });
478
502
  ensureProjectMemory(workspace, 'workspace:clone');
479
503
  const tree = enrollWorkspaceTree(workspace);
504
+ const enrolledWorkspace = workspaceAfterTreeEnrollment(workspace);
480
505
 
481
506
  sendJson(200, {
482
507
  success: true,
483
508
  path: workspace.path,
484
- workspace,
509
+ workspace: enrolledWorkspace,
485
510
  workspaceRoot,
486
511
  alreadyExists: false,
487
- tree: { nodes: tree.nodes.length, created: tree.created },
512
+ tree: {
513
+ nodes: tree.nodes.length,
514
+ created: tree.created,
515
+ observation: enrolledWorkspace.treeObservation,
516
+ },
488
517
  });
489
518
  }
490
519
 
@@ -714,17 +743,7 @@ async function handleRepoCardsRefresh(sendJson) {
714
743
 
715
744
  function handleWorkspaceSyncStats(sendJson) {
716
745
  try {
717
- const cards = listRepoCards();
718
- const health = { healthy: 0, missing: 0, broken: 0, inaccessible: 0 };
719
- for (const card of cards) health[card.health] = (health[card.health] || 0) + 1;
720
- sendJson(200, {
721
- machine: machineId(),
722
- cards: { total: cards.length, ...health },
723
- service: repoCardServiceStats(),
724
- wall: wallStats(),
725
- checkpoints: require('./checkpoint').checkpointStats(),
726
- reducer: require('./reducer').reducerStats(),
727
- });
746
+ sendJson(200, require('./sync-stats').workspaceSyncStats());
728
747
  } catch (error) {
729
748
  sendJson(500, { error: error instanceof Error ? error.message : 'Failed to read sync stats' });
730
749
  }
@@ -732,8 +751,14 @@ function handleWorkspaceSyncStats(sendJson) {
732
751
 
733
752
  function handleCodeProjectJoin(body, sendJson) {
734
753
  try {
754
+ const keys = Object.keys(body || {}).sort();
755
+ if (
756
+ keys.length !== 3
757
+ || keys[0] !== 'ownerUserId'
758
+ || keys[1] !== 'path'
759
+ || keys[2] !== 'resourceId'
760
+ ) throw new Error('code project join requires exactly resourceId, path, and ownerUserId');
735
761
  const project = require('./wall').bindProject(
736
- body?.repoKey,
737
762
  body?.resourceId,
738
763
  body?.ownerUserId,
739
764
  body?.path,
@@ -820,12 +845,8 @@ function handleTreeMutation(body, sendJson) {
820
845
  const actorUserId = require('./identity').userId();
821
846
  if (!actorUserId) throw Object.assign(new Error('this runtime is not enrolled to a user'), { statusCode: 401 });
822
847
  const operation = body?.operation || body;
823
- const accessNodeId = operation?.kind === 'tree.create' ? operation?.parentId : operation?.nodeId;
824
- const access = require('./access-store').resolveAccess(accessNodeId, actorUserId);
825
- if (!access?.role) throw Object.assign(new Error('tree operation is forbidden'), { statusCode: 403 });
826
848
  sendJson(200, require('./tree-store').commitTreeOperation(operation, {
827
849
  mutationId: body?.mutationId,
828
- role: access.role,
829
850
  source: 'workspace-tree:api',
830
851
  }));
831
852
  } catch (error) {
@@ -135,11 +135,17 @@ function ensureWorkspaceIdentityRows(database, workspace, options = {}) {
135
135
  }
136
136
  }
137
137
 
138
- function rowToWorkspace(row) {
138
+ function rowToWorkspace(row, database = openLocalDb()) {
139
139
  if (!row) return null;
140
140
  const parsed = parseJson(row.workspace_json, {});
141
141
  const resolvedPath = row.path ? canonicalProjectPath(row.path) : row.path;
142
142
  const legacyManagedPath = row.path ? isLegacyManagedWorkspacePath(row.path) : false;
143
+ const syncHolds = require('./tree/observation-holds').list(row.id, database);
144
+ const status = syncHolds.length
145
+ ? 'held'
146
+ : resolvedPath && fs.existsSync(resolvedPath)
147
+ ? 'ready'
148
+ : row.status;
143
149
  return {
144
150
  ...parsed,
145
151
  id: row.id,
@@ -151,7 +157,13 @@ function rowToWorkspace(row) {
151
157
  computerId: row.computer_id || parsed.computerId || null,
152
158
  source: legacyManagedPath ? 'computer' : row.source,
153
159
  managed: legacyManagedPath ? false : row.managed === 1,
154
- status: resolvedPath && fs.existsSync(resolvedPath) ? 'ready' : row.status,
160
+ status,
161
+ treeObservation: syncHolds.length ? {
162
+ state: 'held',
163
+ owner: 'workspace-tree',
164
+ retry: { kind: 'automatic' },
165
+ holds: syncHolds,
166
+ } : null,
155
167
  createdAt: row.created_at,
156
168
  updatedAt: row.updated_at,
157
169
  };
@@ -182,13 +194,71 @@ function publishEvents(events) {
182
194
  }
183
195
 
184
196
  function listWorkspaces() {
185
- return openLocalDb()
197
+ const database = openLocalDb();
198
+ return database
186
199
  .prepare('SELECT * FROM workspaces ORDER BY lower(name), path')
187
200
  .all()
188
- .map(rowToWorkspace)
201
+ .map((row) => rowToWorkspace(row, database))
189
202
  .filter(Boolean);
190
203
  }
191
204
 
205
+ function replaceTreeObservationInTransaction(
206
+ workspaceId,
207
+ holds,
208
+ options = {},
209
+ ) {
210
+ const database = options.database || openLocalDb();
211
+ const changed = require('./tree/observation-holds').replace(
212
+ workspaceId,
213
+ holds,
214
+ database,
215
+ {
216
+ ownerKind: options.ownerKind,
217
+ ownerId: options.ownerId,
218
+ },
219
+ );
220
+ if (!changed) return { changed, events: [] };
221
+ const row = workspaceRow(database, workspaceId);
222
+ if (!row) return { changed, events: [] };
223
+ const workspace = rowToWorkspace(row, database);
224
+ return {
225
+ changed,
226
+ events: publishWorkspaceEvents(
227
+ database,
228
+ 'update',
229
+ workspace,
230
+ options.source || 'workspace-tree:observation-hold',
231
+ ),
232
+ };
233
+ }
234
+
235
+ function publishTreeObservationCommit(result) {
236
+ publishEvents(result?.events || []);
237
+ }
238
+
239
+ function commitCompletedTreeObservation(workspaceId, holds, options = {}) {
240
+ const database = options.database || openLocalDb();
241
+ let result = { changed: false, events: [] };
242
+ database.transaction(() => {
243
+ if (typeof options.mutate === 'function') options.mutate();
244
+ result = replaceTreeObservationInTransaction(
245
+ workspaceId,
246
+ holds,
247
+ {
248
+ database,
249
+ ownerKind: options.ownerKind,
250
+ ownerId: options.ownerId,
251
+ source: options.source,
252
+ },
253
+ );
254
+ })();
255
+ publishTreeObservationCommit(result);
256
+ return {
257
+ changed: result.changed,
258
+ holds: require('./tree/observation-holds').list(workspaceId, database),
259
+ };
260
+ }
261
+
192
262
  function upsertWorkspace(input, options = {}) {
193
263
  const db = openLocalDb();
194
264
  let workspace;
@@ -313,6 +383,9 @@ function removeWorkspace(identifier, options = {}) {
313
383
  }
314
384
 
315
385
  module.exports = {
386
+ commitCompletedTreeObservation,
387
+ publishTreeObservationCommit,
388
+ replaceTreeObservationInTransaction,
316
389
  listWorkspaces,
317
390
  normalizeWorkspaceRecord,
318
391
  removeWorkspace,
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Referee projection for the floor, physical verification, and local overlay.
5
+ *
6
+ * Referee axiom: a gate must see every active or successor intent. Proven by
7
+ * workspace-sync-stats.test.js and workspace-sync-harness-core.test.mjs.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { listRepoCards, repoCardServiceStats } = require('./cards');
14
+ const { repoKeyOfCard } = require('./checkpoint');
15
+ const { machineId, userId } = require('./identity');
16
+ const projectStream = require('./project-stream');
17
+ const reducer = require('./reducer');
18
+ const { listWorkspaces } = require('./store');
19
+ const wall = require('./wall');
20
+ const runtimeVersion = require('../../../../package.json').version;
21
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
22
+ const RUNTIME_ROOT = path.resolve(__dirname, '..');
23
+
24
+ function filesUnder(root) {
25
+ return fs.readdirSync(root, { withFileTypes: true })
26
+ .flatMap((entry) => {
27
+ const file = path.join(root, entry.name);
28
+ if (entry.isDirectory()) return filesUnder(file);
29
+ return entry.isFile() ? [file] : [];
30
+ })
31
+ .sort();
32
+ }
33
+
34
+ // The sync identity follows the architecture tree, not a hand-maintained
35
+ // dependency list. New state/materialization modules and native workspace
36
+ // helpers therefore enter the release identity automatically.
37
+ const SYNC_FILES = [
38
+ path.join(RUNTIME_ROOT, 'state'),
39
+ path.join(RUNTIME_ROOT, 'workspace'),
40
+ ].flatMap(filesUnder);
41
+
42
+ function fileFingerprint(files) {
43
+ const hash = crypto.createHash('sha256');
44
+ for (const file of files.filter((candidate) => fs.existsSync(candidate)).sort()) {
45
+ hash.update(path.relative(PACKAGE_ROOT, file));
46
+ hash.update('\0');
47
+ hash.update(fs.readFileSync(file));
48
+ hash.update('\0');
49
+ }
50
+ return `sha256:${hash.digest('hex')}`;
51
+ }
52
+
53
+ function sharedTunnelFiles() {
54
+ return [
55
+ path.join(PACKAGE_ROOT, 'lib', 'shared-realtime-tunnel.js'),
56
+ path.join(PACKAGE_ROOT, 'packages', 'amalgm', 'lib', 'shared-realtime-tunnel.js'),
57
+ ].filter((file) => fs.existsSync(file));
58
+ }
59
+
60
+ const runtimeBuildFingerprint = fileFingerprint([
61
+ path.join(PACKAGE_ROOT, 'package.json'),
62
+ ...filesUnder(RUNTIME_ROOT),
63
+ ...sharedTunnelFiles(),
64
+ ]);
65
+ const runtimeSyncFingerprint = fileFingerprint([
66
+ ...SYNC_FILES,
67
+ ...sharedTunnelFiles(),
68
+ ]);
69
+
70
+ function canonical(value) {
71
+ if (Array.isArray(value)) return value.map(canonical);
72
+ if (!value || typeof value !== 'object') return value;
73
+ return Object.fromEntries(
74
+ Object.keys(value).sort().map((key) => [key, canonical(value[key])]),
75
+ );
76
+ }
77
+
78
+ function digest(value) {
79
+ return crypto.createHash('sha256')
80
+ .update(JSON.stringify(canonical(value)))
81
+ .digest('hex');
82
+ }
83
+
84
+ function samePath(left, right) {
85
+ return Boolean(left && right && path.resolve(left) === path.resolve(right));
86
+ }
87
+
88
+ function folderFor(project, cards, workspaces) {
89
+ const card = cards.find((entry) => (
90
+ entry.kind === 'primary'
91
+ && repoKeyOfCard(entry) === project.repoKey
92
+ ));
93
+ const folderPath = project.path || card?.path || null;
94
+ const workspace = workspaces.find((entry) => samePath(entry.path, folderPath));
95
+ return {
96
+ card,
97
+ folderPath,
98
+ folderUuid: workspace?.id || null,
99
+ };
100
+ }
101
+
102
+ function waitingFor(state, outcome) {
103
+ const waiting = [];
104
+ if (outcome?.state === 'waiting' || outcome?.state === 'error') {
105
+ waiting.push({
106
+ state: outcome.state,
107
+ reason: outcome.reason || null,
108
+ });
109
+ }
110
+ return waiting;
111
+ }
112
+
113
+ function verificationFor(state, outcome, pendingIntent = null) {
114
+ return {
115
+ contract: state?.verificationContract || null,
116
+ appliedThrough: Number.isSafeInteger(state?.appliedThrough)
117
+ ? state.appliedThrough
118
+ : null,
119
+ materialized: state?.materialized === true,
120
+ targetDigest: state?.targetDigest || null,
121
+ expectedPhysicalDigest: state?.expectedPhysicalDigest || null,
122
+ observedPhysicalDigest: state?.observedPhysicalDigest || null,
123
+ pendingIntent,
124
+ waiting: waitingFor(state, outcome),
125
+ };
126
+ }
127
+
128
+ function pendingIntentValue(value, owned) {
129
+ const successor = value?.successor || null;
130
+ if (!owned && !successor) return null;
131
+ const result = {
132
+ baseThrough: value.base.through,
133
+ semanticDigest: value.semanticDigest,
134
+ mutationId: value.birthMutationId || value.publication?.mutationId || null,
135
+ terminalCode: value.terminal?.code || null,
136
+ };
137
+ if (successor) {
138
+ result.successor = {
139
+ predecessorSemanticDigest: successor.predecessorSemanticDigest,
140
+ semanticDigest: successor.semanticDigest,
141
+ mutationId: successor.birthMutationId || null,
142
+ terminalCode: successor.terminal?.code || null,
143
+ };
144
+ }
145
+ return result;
146
+ }
147
+
148
+ function pendingIntentFor(project) {
149
+ const intents = require('./intent');
150
+ const value = intents.readIntent(project);
151
+ return pendingIntentValue(
152
+ value,
153
+ Boolean(value && !intents.officialAcknowledgement(project, value)),
154
+ );
155
+ }
156
+
157
+ function autoUpdateDisabled(env = process.env) {
158
+ const disabled = String(env.AMALGM_DISABLE_AUTO_UPDATE || '').trim().toLowerCase();
159
+ if (['1', 'true', 'yes', 'on'].includes(disabled)) return true;
160
+ const enabled = String(env.AMALGM_AUTO_UPDATE || '').trim().toLowerCase();
161
+ return ['0', 'false', 'no', 'off'].includes(enabled);
162
+ }
163
+
164
+ function projectObservation({
165
+ project,
166
+ cards,
167
+ workspaces,
168
+ reducerRepos,
169
+ }) {
170
+ const folder = folderFor(project, cards, workspaces);
171
+ const state = reducer.readRepoState(project.repoKey, project.resourceId);
172
+ const exactState = (
173
+ state
174
+ && folder.folderPath
175
+ && samePath(state.path, folder.folderPath)
176
+ && (!folder.card || state.replica === folder.card.replica)
177
+ ) ? state : null;
178
+ const snapshot = projectStream.readSnapshot(project.file, project.resourceId);
179
+ const publication = wall.publicationStatus(project.repoKey, exactState);
180
+ const outcome = reducerRepos.find((entry) => (
181
+ entry.repoKey === project.repoKey
182
+ && (!folder.folderPath || !entry.path || samePath(entry.path, folder.folderPath))
183
+ )) || null;
184
+ return {
185
+ resourceId: project.resourceId,
186
+ repoKey: project.repoKey,
187
+ folderPath: folder.folderPath,
188
+ folderUuid: folder.folderUuid,
189
+ receivedThrough: publication.receivedThrough,
190
+ snapshotThrough: snapshot.version,
191
+ snapshotDigest: digest(snapshot),
192
+ pendingOutbound: publication.pending,
193
+ verification: verificationFor(exactState, outcome, pendingIntentFor(project)),
194
+ };
195
+ }
196
+
197
+ function workspaceSyncStats() {
198
+ const cards = listRepoCards();
199
+ const wallState = wall.wallStats();
200
+ const reducerState = reducer.reducerStats();
201
+ const workspaces = listWorkspaces();
202
+ const projects = wallState.projects.map((entry) => projectObservation({
203
+ project: wall.projectForRepo(entry.repoKey),
204
+ cards,
205
+ workspaces,
206
+ reducerRepos: reducerState.repos,
207
+ }));
208
+ const health = { healthy: 0, missing: 0, broken: 0, inaccessible: 0 };
209
+ for (const card of cards) health[card.health] = (health[card.health] || 0) + 1;
210
+ return {
211
+ machine: machineId(),
212
+ user: userId(),
213
+ runtimeVersion,
214
+ runtime: {
215
+ buildFingerprint: runtimeBuildFingerprint,
216
+ syncFingerprint: runtimeSyncFingerprint,
217
+ autoUpdateDisabled: autoUpdateDisabled(),
218
+ },
219
+ projects,
220
+ cards: { total: cards.length, ...health },
221
+ service: repoCardServiceStats(),
222
+ wall: wallState,
223
+ checkpoints: require('./checkpoint').checkpointStats(),
224
+ reducer: reducerState,
225
+ };
226
+ }
227
+
228
+ module.exports = {
229
+ _test: {
230
+ digest,
231
+ folderFor,
232
+ autoUpdateDisabled,
233
+ fileFingerprint,
234
+ syncFingerprintFiles: SYNC_FILES.map((file) => path.relative(RUNTIME_ROOT, file)),
235
+ pendingIntentFor,
236
+ pendingIntentValue,
237
+ verificationFor,
238
+ },
239
+ workspaceSyncStats,
240
+ };