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.
- package/lib/shared-realtime-tunnel.js +22 -14
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- 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 +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- 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
|
|
17
|
+
const { listRepoCards, refreshRepoCards } = require('./cards');
|
|
18
18
|
const { machineId } = require('./identity');
|
|
19
|
-
const { readWall
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|