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
@@ -6,6 +6,7 @@ const assert = require('node:assert/strict');
6
6
  const { can, resolveEffectiveRole } = require('../workspace/tree/access');
7
7
  const { isUuidV7, uuidV7, uuidV7Timestamp } = require('../workspace/tree/identity');
8
8
  const { applyTreeOperation } = require('../workspace/tree/reducer');
9
+ const { normalizeTreeSnapshot } = require('../workspace/tree/snapshot');
9
10
 
10
11
  test('UUIDv7 identity is standard, timestamped, and independent of path', () => {
11
12
  const now = 1_782_000_123_456;
@@ -70,6 +71,82 @@ test('tree operations preserve identity across rename and reject cycles', () =>
70
71
  }), /destination folder does not exist|inside itself/);
71
72
  });
72
73
 
74
+ test('symlink legality is a portable invariant of the complete live tree', () => {
75
+ const workspaceId = uuidV7();
76
+ const folderId = uuidV7();
77
+ const nestedId = uuidV7();
78
+ const linkId = uuidV7();
79
+ const root = {
80
+ id: workspaceId,
81
+ workspaceId,
82
+ parentId: null,
83
+ kind: 'folder',
84
+ name: 'Workspace',
85
+ };
86
+ let nodes = new Map([[workspaceId, root]]);
87
+ nodes = applyTreeOperation(nodes, {
88
+ kind: 'tree.create',
89
+ workspaceId,
90
+ nodeId: folderId,
91
+ parentId: workspaceId,
92
+ nodeKind: 'folder',
93
+ name: 'folder',
94
+ });
95
+ nodes = applyTreeOperation(nodes, {
96
+ kind: 'tree.create',
97
+ workspaceId,
98
+ nodeId: nestedId,
99
+ parentId: folderId,
100
+ nodeKind: 'folder',
101
+ name: 'nested',
102
+ });
103
+ nodes = applyTreeOperation(nodes, {
104
+ kind: 'tree.create',
105
+ workspaceId,
106
+ nodeId: linkId,
107
+ parentId: nestedId,
108
+ nodeKind: 'symlink',
109
+ name: 'entry',
110
+ symlinkTarget: '../../folder/file.txt',
111
+ });
112
+
113
+ assert.throws(() => applyTreeOperation(nodes, {
114
+ kind: 'tree.move',
115
+ workspaceId,
116
+ nodeId: nestedId,
117
+ parentId: workspaceId,
118
+ }), /escapes its workspace/);
119
+ for (const [index, target] of [
120
+ '../../../outside',
121
+ '/tmp/outside',
122
+ '\\\\server\\share',
123
+ 'C:\\outside',
124
+ 'C:outside',
125
+ ].entries()) {
126
+ assert.throws(() => applyTreeOperation(nodes, {
127
+ kind: 'tree.create',
128
+ workspaceId,
129
+ nodeId: uuidV7(Date.now() + index),
130
+ parentId: workspaceId,
131
+ nodeKind: 'symlink',
132
+ name: `unsafe-${index}`,
133
+ symlinkTarget: target,
134
+ }), /relative|escapes its workspace/);
135
+ }
136
+
137
+ const snapshot = {
138
+ schemaVersion: 1,
139
+ workspaceId,
140
+ version: 0,
141
+ nodes: Array.from(nodes.values()).map((node) => ({ ...node })),
142
+ };
143
+ snapshot.nodes.find((node) => node.id === linkId).symlinkTarget = '../../../outside';
144
+ assert.throws(
145
+ () => normalizeTreeSnapshot(snapshot),
146
+ /escapes its workspace/,
147
+ );
148
+ });
149
+
73
150
  test('trash cascades preserve independently trashed children', () => {
74
151
  const workspaceId = uuidV7();
75
152
  const folderId = uuidV7();
@@ -99,9 +176,11 @@ test('trash cascades preserve independently trashed children', () => {
99
176
  kind: 'tree.trash', workspaceId, nodeId: fileId, trashedAt: '2026-07-21T00:01:00.000Z',
100
177
  });
101
178
  nodes = applyTreeOperation(nodes, {
102
- kind: 'tree.trash', workspaceId, nodeId: folderId, trashedAt: '2026-07-21T00:02:00.000Z',
179
+ kind: 'tree.trash', workspaceId, nodeId: folderId, trashedAt: '2026-07-21T00:01:00.000Z',
103
180
  });
104
181
  assert.equal(nodes.get(fileId).trashedAt, '2026-07-21T00:01:00.000Z');
182
+ assert.equal(nodes.get(fileId).trashComponentId, fileId);
183
+ assert.equal(nodes.get(folderId).trashComponentId, folderId);
105
184
 
106
185
  nodes = applyTreeOperation(nodes, {
107
186
  kind: 'tree.restore', workspaceId, nodeId: folderId,
@@ -111,4 +190,115 @@ test('trash cascades preserve independently trashed children', () => {
111
190
  assert.throws(() => applyTreeOperation(nodes, {
112
191
  kind: 'tree.restore', workspaceId, nodeId: folderId,
113
192
  }), /must be trashed/);
193
+
194
+ nodes = applyTreeOperation(nodes, {
195
+ kind: 'tree.purge', workspaceId, nodeId: fileId,
196
+ });
197
+ assert.ok(nodes.get(fileId).purgedAt);
198
+ assert.equal(nodes.get(fileId).trashComponentId, null);
199
+ });
200
+
201
+ test('snapshots require active trash provenance and retire it on purge', () => {
202
+ const workspaceId = uuidV7();
203
+ const fileId = uuidV7();
204
+ const base = {
205
+ schemaVersion: 1,
206
+ workspaceId,
207
+ version: 1,
208
+ nodes: [
209
+ {
210
+ id: workspaceId,
211
+ workspaceId,
212
+ parentId: null,
213
+ kind: 'folder',
214
+ name: 'Workspace',
215
+ },
216
+ {
217
+ id: fileId,
218
+ workspaceId,
219
+ parentId: workspaceId,
220
+ kind: 'file',
221
+ name: 'note.txt',
222
+ trashedAt: '2026-07-21T00:01:00.000Z',
223
+ },
224
+ ],
225
+ };
226
+ assert.throws(
227
+ () => normalizeTreeSnapshot(base),
228
+ /requires explicit trash provenance/,
229
+ );
230
+ const cyclic = structuredClone(base);
231
+ cyclic.nodes[1].trashedAt = null;
232
+ cyclic.nodes[1].parentId = fileId;
233
+ assert.throws(() => normalizeTreeSnapshot(cyclic), /contains a cycle/);
234
+
235
+ const siblingId = uuidV7();
236
+ const nonAncestor = structuredClone(base);
237
+ nonAncestor.nodes.push({
238
+ id: siblingId,
239
+ workspaceId,
240
+ parentId: workspaceId,
241
+ kind: 'folder',
242
+ name: 'other-trash',
243
+ trashedAt: '2026-07-21T00:01:00.000Z',
244
+ trashComponentId: siblingId,
245
+ });
246
+ nonAncestor.nodes[1].trashComponentId = siblingId;
247
+ assert.throws(() => normalizeTreeSnapshot(nonAncestor), /is not an ancestor/);
248
+
249
+ const purged = structuredClone(base);
250
+ purged.nodes[1].purgedAt = '2026-07-21T00:02:00.000Z';
251
+ const normalized = normalizeTreeSnapshot(purged);
252
+ assert.equal(normalized.nodes.get(fileId).trashComponentId, null);
253
+ });
254
+
255
+ test('snapshot ancestry validation stays bounded on a deep tree', { timeout: 10_000 }, () => {
256
+ const workspaceId = uuidV7();
257
+ const componentId = uuidV7();
258
+ const changedAt = '2026-07-21T00:01:00.000Z';
259
+ const nodes = [{
260
+ id: workspaceId,
261
+ workspaceId,
262
+ parentId: null,
263
+ kind: 'folder',
264
+ name: 'Workspace',
265
+ }, {
266
+ id: componentId,
267
+ workspaceId,
268
+ parentId: workspaceId,
269
+ kind: 'folder',
270
+ name: 'trash-root',
271
+ }];
272
+ let parentId = componentId;
273
+ for (let index = 0; index < 20_000; index += 1) {
274
+ const id = uuidV7();
275
+ nodes.push({
276
+ id,
277
+ workspaceId,
278
+ parentId,
279
+ kind: 'folder',
280
+ name: `level-${index}`,
281
+ });
282
+ parentId = id;
283
+ }
284
+ const live = normalizeTreeSnapshot({
285
+ schemaVersion: 1,
286
+ workspaceId,
287
+ version: 1,
288
+ nodes,
289
+ });
290
+ const trashed = applyTreeOperation(live.nodes, {
291
+ kind: 'tree.trash',
292
+ workspaceId,
293
+ nodeId: componentId,
294
+ trashedAt: changedAt,
295
+ });
296
+ const normalized = normalizeTreeSnapshot({
297
+ schemaVersion: 1,
298
+ workspaceId,
299
+ version: 2,
300
+ nodes: Array.from(trashed.values()),
301
+ });
302
+ assert.equal(normalized.nodes.size, nodes.length);
303
+ assert.equal(normalized.nodes.get(parentId).trashComponentId, componentId);
114
304
  });