amalgm 0.1.221 → 0.1.235
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/state/attachments.js +7 -1
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +39 -2
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +180 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +61 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +28 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +152 -12
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +6 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +105 -67
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +183 -23
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +99 -206
- package/runtime/scripts/amalgm-mcp/workspace/card.js +33 -9
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +3 -2
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +194 -20
- package/runtime/scripts/amalgm-mcp/workspace/git.js +23 -5
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +24 -186
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +13 -1
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +323 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +115 -101
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +106 -137
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { appendStateEvent } = require('../state/events');
|
|
6
|
+
const { openLocalDb } = require('../state/db');
|
|
7
|
+
|
|
8
|
+
const STREAM_CONTRACT = 'code-project-stream@1';
|
|
9
|
+
const WALL_CONTRACT = 'code-project@6';
|
|
10
|
+
const LOCAL_RESOURCE_PREFIX = 'code-project:';
|
|
11
|
+
|
|
12
|
+
function invalid(message, statusCode = 400) {
|
|
13
|
+
return Object.assign(new Error(message), { statusCode });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function emptySnapshot(project) {
|
|
17
|
+
return { schemaVersion: 1, project, version: 0, records: [] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function localResource(resourceId) {
|
|
21
|
+
return `${LOCAL_RESOURCE_PREFIX}${resourceId}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeRecord(input, project, official = false) {
|
|
25
|
+
if (
|
|
26
|
+
!input
|
|
27
|
+
|| input.wall !== WALL_CONTRACT
|
|
28
|
+
|| input.project !== project
|
|
29
|
+
|| typeof input.machine !== 'string'
|
|
30
|
+
|| !input.machine
|
|
31
|
+
|| typeof input.mutationId !== 'string'
|
|
32
|
+
|| !input.mutationId
|
|
33
|
+
|| typeof input.publishedAt !== 'string'
|
|
34
|
+
|| !Number.isFinite(Date.parse(input.publishedAt))
|
|
35
|
+
|| !Array.isArray(input.cards)
|
|
36
|
+
) throw invalid('code project record is invalid', 409);
|
|
37
|
+
const version = Number(input.version);
|
|
38
|
+
if (official && (!Number.isSafeInteger(version) || version < 1)) {
|
|
39
|
+
throw invalid('code project record has no official version', 409);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
...input,
|
|
43
|
+
...(Number.isSafeInteger(version) && version > 0 ? { version } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeSnapshot(input, project = null, official = false) {
|
|
48
|
+
if (
|
|
49
|
+
!input
|
|
50
|
+
|| input.schemaVersion !== 1
|
|
51
|
+
|| typeof input.project !== 'string'
|
|
52
|
+
|| !input.project
|
|
53
|
+
|| !Number.isSafeInteger(Number(input.version))
|
|
54
|
+
|| Number(input.version) < 0
|
|
55
|
+
|| !Array.isArray(input.records)
|
|
56
|
+
|| (project && input.project !== project)
|
|
57
|
+
) throw invalid('code project snapshot is invalid', 409);
|
|
58
|
+
const machines = new Set();
|
|
59
|
+
const records = input.records.map((record) => {
|
|
60
|
+
const normalized = normalizeRecord(record, input.project, official);
|
|
61
|
+
if (official && normalized.version > Number(input.version)) {
|
|
62
|
+
throw invalid('code project record is newer than its snapshot', 409);
|
|
63
|
+
}
|
|
64
|
+
if (machines.has(normalized.machine)) {
|
|
65
|
+
throw invalid('code project snapshot repeats a machine', 409);
|
|
66
|
+
}
|
|
67
|
+
machines.add(normalized.machine);
|
|
68
|
+
return normalized;
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
schemaVersion: 1,
|
|
72
|
+
project: input.project,
|
|
73
|
+
version: Number(input.version),
|
|
74
|
+
records,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readSnapshot(file, project) {
|
|
79
|
+
if (!fs.existsSync(file) || fs.statSync(file).size === 0) return emptySnapshot(project);
|
|
80
|
+
try {
|
|
81
|
+
return normalizeSnapshot(JSON.parse(fs.readFileSync(file, 'utf8')), project);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error?.statusCode) throw error;
|
|
84
|
+
throw invalid('code project snapshot is not JSON', 409);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeSnapshot(file, snapshot) {
|
|
89
|
+
fs.mkdirSync(require('path').dirname(file), { recursive: true });
|
|
90
|
+
fs.writeFileSync(file, `${JSON.stringify(snapshot)}\n`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function applyRecord(snapshot, record, version = null) {
|
|
94
|
+
const next = normalizeRecord(
|
|
95
|
+
version ? { ...record, version } : record,
|
|
96
|
+
snapshot.project,
|
|
97
|
+
false,
|
|
98
|
+
);
|
|
99
|
+
const records = new Map(snapshot.records.map((entry) => [entry.machine, entry]));
|
|
100
|
+
records.set(next.machine, next);
|
|
101
|
+
return {
|
|
102
|
+
...snapshot,
|
|
103
|
+
version: Number.isSafeInteger(version)
|
|
104
|
+
? Math.max(snapshot.version, version)
|
|
105
|
+
: snapshot.version,
|
|
106
|
+
records: [...records.values()].sort((left, right) => (
|
|
107
|
+
left.machine.localeCompare(right.machine)
|
|
108
|
+
)),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function records(project) {
|
|
113
|
+
const snapshot = readSnapshot(project.file, project.repoKey);
|
|
114
|
+
let changed = false;
|
|
115
|
+
const sealed = snapshot.records.map((record) => {
|
|
116
|
+
if (record.version) return record;
|
|
117
|
+
const mutation = require('../state/mutations').getMutation(
|
|
118
|
+
`resource:${project.resourceId}`,
|
|
119
|
+
record.mutationId,
|
|
120
|
+
);
|
|
121
|
+
const version = mutation?.officialCloudVersion;
|
|
122
|
+
if (!Number.isSafeInteger(version) || version < 1) return record;
|
|
123
|
+
changed = true;
|
|
124
|
+
return { ...record, version };
|
|
125
|
+
});
|
|
126
|
+
if (changed) writeSnapshot(project.file, {
|
|
127
|
+
...snapshot,
|
|
128
|
+
version: Math.max(snapshot.version, ...sealed.map((record) => record.version || 0)),
|
|
129
|
+
records: sealed,
|
|
130
|
+
});
|
|
131
|
+
return sealed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function publish(project, record) {
|
|
135
|
+
const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
|
|
136
|
+
if (!replica || replica.contract !== STREAM_CONTRACT) {
|
|
137
|
+
throw invalid('code project stream is not attached', 409);
|
|
138
|
+
}
|
|
139
|
+
return require('../state/mutations').commitLocalMutation({
|
|
140
|
+
channelId: `resource:${project.resourceId}`,
|
|
141
|
+
localResource: localResource(project.resourceId),
|
|
142
|
+
sharedResourceId: project.resourceId,
|
|
143
|
+
mutationId: record.mutationId,
|
|
144
|
+
deviceId: require('./identity').machineId(),
|
|
145
|
+
authorityEpoch: replica.authorityEpoch,
|
|
146
|
+
baseCloudVersion: replica.appliedVersion,
|
|
147
|
+
contract: STREAM_CONTRACT,
|
|
148
|
+
schemaVersion: 1,
|
|
149
|
+
operationKind: 'code.publish',
|
|
150
|
+
operation: { kind: 'code.publish', record },
|
|
151
|
+
}, {
|
|
152
|
+
materialize() {
|
|
153
|
+
writeSnapshot(
|
|
154
|
+
project.file,
|
|
155
|
+
applyRecord(readSnapshot(project.file, project.repoKey), record),
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
event: {
|
|
159
|
+
resource: localResource(project.resourceId),
|
|
160
|
+
op: 'update',
|
|
161
|
+
id: project.file,
|
|
162
|
+
sharedResourceId: project.resourceId,
|
|
163
|
+
value: record,
|
|
164
|
+
source: 'code-project:local',
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function seal(project, mutationId, version) {
|
|
170
|
+
if (!Number.isSafeInteger(Number(version)) || Number(version) < 1) return false;
|
|
171
|
+
const snapshot = readSnapshot(project.file, project.repoKey);
|
|
172
|
+
let changed = false;
|
|
173
|
+
const records = snapshot.records.map((record) => {
|
|
174
|
+
if (record.mutationId !== mutationId || record.version === Number(version)) return record;
|
|
175
|
+
changed = true;
|
|
176
|
+
return { ...record, version: Number(version) };
|
|
177
|
+
});
|
|
178
|
+
if (!changed) return false;
|
|
179
|
+
writeSnapshot(project.file, {
|
|
180
|
+
...snapshot,
|
|
181
|
+
version: Math.max(snapshot.version, Number(version)),
|
|
182
|
+
records,
|
|
183
|
+
});
|
|
184
|
+
appendStateEvent({
|
|
185
|
+
resource: localResource(project.resourceId),
|
|
186
|
+
op: 'update',
|
|
187
|
+
id: project.file,
|
|
188
|
+
sharedResourceId: project.resourceId,
|
|
189
|
+
resourceVersion: Number(version),
|
|
190
|
+
source: 'code-project:ack',
|
|
191
|
+
});
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function decodeSnapshot(input) {
|
|
196
|
+
if (input?.contract !== STREAM_CONTRACT || Number(input?.schemaVersion) !== 1) {
|
|
197
|
+
throw invalid('unsupported code project stream', 409);
|
|
198
|
+
}
|
|
199
|
+
const bytes = Buffer.from(String(input.snapshotBase64 || ''), 'base64');
|
|
200
|
+
if (
|
|
201
|
+
!bytes.length
|
|
202
|
+
|| crypto.createHash('sha256').update(bytes).digest('hex') !== input.snapshotChecksum
|
|
203
|
+
) throw invalid('code project snapshot checksum mismatch', 409);
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(bytes.toString('utf8'));
|
|
207
|
+
} catch {
|
|
208
|
+
throw invalid('code project snapshot must be JSON', 409);
|
|
209
|
+
}
|
|
210
|
+
const snapshot = normalizeSnapshot(parsed, null, true);
|
|
211
|
+
if (snapshot.version !== Number(input.snapshotVersion)) {
|
|
212
|
+
throw invalid('code project snapshot version mismatch', 409);
|
|
213
|
+
}
|
|
214
|
+
return { bytes, snapshot };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function installSnapshot(file, input, options = {}) {
|
|
218
|
+
const database = options.database || openLocalDb();
|
|
219
|
+
const { snapshot } = decodeSnapshot(input);
|
|
220
|
+
const replicas = require('../state/replicas');
|
|
221
|
+
const existing = replicas.getReplicaByResourceId(input.resourceId, { database });
|
|
222
|
+
if (existing && existing.localPath !== file) {
|
|
223
|
+
throw invalid('code project stream is bound to another local file', 409);
|
|
224
|
+
}
|
|
225
|
+
let installed = snapshot;
|
|
226
|
+
if (existing) {
|
|
227
|
+
for (const mutation of require('../state/mutations').listPendingCloudMutations(
|
|
228
|
+
input.resourceId,
|
|
229
|
+
{ database },
|
|
230
|
+
)) {
|
|
231
|
+
if (mutation.contract !== STREAM_CONTRACT || mutation.operationKind !== 'code.publish') continue;
|
|
232
|
+
installed = applyRecord(installed, mutation.operation?.record);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
writeSnapshot(file, installed);
|
|
236
|
+
const registered = existing
|
|
237
|
+
? {
|
|
238
|
+
replica: replicas.advanceReplicaSnapshot({
|
|
239
|
+
resourceId: input.resourceId,
|
|
240
|
+
snapshotVersion: input.snapshotVersion,
|
|
241
|
+
snapshotChecksum: input.snapshotChecksum,
|
|
242
|
+
authorityEpoch: input.authorityEpoch,
|
|
243
|
+
}, { database }),
|
|
244
|
+
}
|
|
245
|
+
: replicas.registerReplica({
|
|
246
|
+
resourceId: input.resourceId,
|
|
247
|
+
localResource: localResource(input.resourceId),
|
|
248
|
+
localPath: file,
|
|
249
|
+
contract: STREAM_CONTRACT,
|
|
250
|
+
schemaVersion: 1,
|
|
251
|
+
authorityEpoch: input.authorityEpoch,
|
|
252
|
+
appliedVersion: input.snapshotVersion,
|
|
253
|
+
snapshotVersion: input.snapshotVersion,
|
|
254
|
+
snapshotChecksum: input.snapshotChecksum,
|
|
255
|
+
}, { database });
|
|
256
|
+
appendStateEvent({
|
|
257
|
+
resource: localResource(input.resourceId),
|
|
258
|
+
op: 'update',
|
|
259
|
+
id: file,
|
|
260
|
+
sharedResourceId: input.resourceId,
|
|
261
|
+
resourceVersion: Number(input.snapshotVersion),
|
|
262
|
+
source: 'code-project:snapshot',
|
|
263
|
+
});
|
|
264
|
+
return registered;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function applyCloudMutation(input, options = {}) {
|
|
268
|
+
const envelope = input?.envelope || {};
|
|
269
|
+
const record = envelope.operation?.record;
|
|
270
|
+
if (
|
|
271
|
+
envelope.contract !== STREAM_CONTRACT
|
|
272
|
+
|| envelope.operationKind !== 'code.publish'
|
|
273
|
+
|| envelope.operation?.kind !== 'code.publish'
|
|
274
|
+
|| record?.mutationId !== envelope.mutationId
|
|
275
|
+
) throw invalid('invalid code project cloud mutation', 409);
|
|
276
|
+
const database = options.database || openLocalDb();
|
|
277
|
+
const replica = require('../state/replicas').getReplicaByResourceId(
|
|
278
|
+
envelope.resourceId,
|
|
279
|
+
{ database },
|
|
280
|
+
);
|
|
281
|
+
if (!replica || replica.contract !== STREAM_CONTRACT) {
|
|
282
|
+
throw invalid('unknown code project stream', 404);
|
|
283
|
+
}
|
|
284
|
+
return require('../state/mutations').ingestCloudMutation({
|
|
285
|
+
...input,
|
|
286
|
+
localResource: localResource(envelope.resourceId),
|
|
287
|
+
}, {
|
|
288
|
+
database,
|
|
289
|
+
materialize() {
|
|
290
|
+
writeSnapshot(
|
|
291
|
+
replica.localPath,
|
|
292
|
+
applyRecord(
|
|
293
|
+
readSnapshot(replica.localPath, record.project),
|
|
294
|
+
record,
|
|
295
|
+
Number(input.version),
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
},
|
|
299
|
+
event: {
|
|
300
|
+
resource: localResource(envelope.resourceId),
|
|
301
|
+
op: 'update',
|
|
302
|
+
id: replica.localPath,
|
|
303
|
+
sharedResourceId: envelope.resourceId,
|
|
304
|
+
resourceVersion: Number(input.version),
|
|
305
|
+
value: record,
|
|
306
|
+
source: 'code-project:cloud',
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
LOCAL_RESOURCE_PREFIX,
|
|
313
|
+
STREAM_CONTRACT,
|
|
314
|
+
WALL_CONTRACT,
|
|
315
|
+
_test: { applyRecord, emptySnapshot, normalizeSnapshot },
|
|
316
|
+
applyCloudMutation,
|
|
317
|
+
installSnapshot,
|
|
318
|
+
publish,
|
|
319
|
+
readSnapshot,
|
|
320
|
+
records,
|
|
321
|
+
seal,
|
|
322
|
+
writeSnapshot,
|
|
323
|
+
};
|