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
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { isUuid } = require('./identity');
4
4
  const { KINDS, normalizeNodeName } = require('./operations');
5
+ const { validateLiveSymlinkTargets } = require('./symlinks');
5
6
 
6
7
  function invalid(message) {
7
8
  throw Object.assign(new Error(message), { statusCode: 400 });
@@ -35,6 +36,12 @@ function normalizeTreeSnapshot(input) {
35
36
  }
36
37
  const parentId = value.parentId == null ? null : String(value.parentId).toLowerCase();
37
38
  if (parentId && !isUuid(parentId)) invalid('workspace tree parent identity is invalid');
39
+ const trashComponentId = value.trashComponentId == null
40
+ ? null
41
+ : String(value.trashComponentId).toLowerCase();
42
+ if (trashComponentId && !isUuid(trashComponentId)) {
43
+ invalid('workspace tree trash component identity is invalid');
44
+ }
38
45
  const createdAt = timestamp(value.createdAt, new Date().toISOString());
39
46
  const updatedAt = timestamp(value.updatedAt, createdAt);
40
47
  if (value.kind === 'symlink' && (typeof value.symlinkTarget !== 'string' || !value.symlinkTarget)) {
@@ -48,6 +55,7 @@ function normalizeTreeSnapshot(input) {
48
55
  name: normalizeNodeName(value.name),
49
56
  symlinkTarget: value.kind === 'symlink' ? value.symlinkTarget : null,
50
57
  trashedAt: value.trashedAt ? timestamp(value.trashedAt) : null,
58
+ trashComponentId,
51
59
  purgedAt: value.purgedAt ? timestamp(value.purgedAt) : null,
52
60
  createdAt,
53
61
  updatedAt,
@@ -58,17 +66,30 @@ function normalizeTreeSnapshot(input) {
58
66
  if (!root || root.parentId !== null || root.kind !== 'folder') {
59
67
  invalid('workspace tree requires one folder root whose id is the workspace id');
60
68
  }
69
+ const ancestry = buildAncestryIndex(nodes, workspaceId);
61
70
  const siblingNames = new Set();
62
71
  for (const node of nodes.values()) {
63
72
  if (node.id === workspaceId) continue;
64
- if (!node.parentId || !nodes.has(node.parentId)) invalid('workspace tree node parent is missing');
65
- const seen = new Set([node.id]);
66
- let current = node;
67
- while (current.parentId) {
68
- if (seen.has(current.parentId)) invalid('workspace tree contains a cycle');
69
- seen.add(current.parentId);
70
- current = nodes.get(current.parentId);
71
- if (!current) invalid('workspace tree node parent is missing');
73
+ if (node.purgedAt) {
74
+ if (node.trashComponentId) {
75
+ invalid('a purged workspace tree node cannot name a physical trash component');
76
+ }
77
+ } else if (node.trashedAt) {
78
+ if (!node.trashComponentId) {
79
+ invalid('an active workspace tree tombstone requires explicit trash provenance');
80
+ }
81
+ const component = nodes.get(node.trashComponentId);
82
+ if (
83
+ !component
84
+ || !component.trashedAt
85
+ || component.purgedAt
86
+ || component.trashComponentId !== component.id
87
+ || !isAncestor(ancestry, component.id, node.id)
88
+ ) {
89
+ invalid('workspace tree trash component is not an ancestor');
90
+ }
91
+ } else if (node.trashComponentId) {
92
+ invalid('a live workspace tree node cannot name a trash component');
72
93
  }
73
94
  if (!node.trashedAt && !node.purgedAt) {
74
95
  const key = `${node.parentId}\0${node.name}`;
@@ -76,7 +97,57 @@ function normalizeTreeSnapshot(input) {
76
97
  siblingNames.add(key);
77
98
  }
78
99
  }
100
+ validateLiveSymlinkTargets(nodes, workspaceId);
79
101
  return { schemaVersion: 1, workspaceId, version: input.version, nodes };
80
102
  }
81
103
 
104
+ function buildAncestryIndex(nodes, rootId) {
105
+ const children = new Map();
106
+ for (const node of nodes.values()) {
107
+ if (node.id === rootId) continue;
108
+ if (!node.parentId || !nodes.has(node.parentId)) invalid('workspace tree node parent is missing');
109
+ const siblings = children.get(node.parentId) || [];
110
+ siblings.push(node.id);
111
+ children.set(node.parentId, siblings);
112
+ }
113
+
114
+ const entered = new Map();
115
+ const exited = new Map();
116
+ const stack = [{ id: rootId, exit: false }];
117
+ let clock = 0;
118
+ while (stack.length) {
119
+ const frame = stack.pop();
120
+ if (frame.exit) {
121
+ exited.set(frame.id, clock);
122
+ clock += 1;
123
+ continue;
124
+ }
125
+ if (entered.has(frame.id)) invalid('workspace tree contains a cycle');
126
+ entered.set(frame.id, clock);
127
+ clock += 1;
128
+ stack.push({ id: frame.id, exit: true });
129
+ const descendants = children.get(frame.id) || [];
130
+ for (let index = descendants.length - 1; index >= 0; index -= 1) {
131
+ stack.push({ id: descendants[index], exit: false });
132
+ }
133
+ }
134
+ if (entered.size !== nodes.size) invalid('workspace tree contains a cycle');
135
+ return { entered, exited };
136
+ }
137
+
138
+ function isAncestor(index, ancestorId, nodeId) {
139
+ const ancestorEntered = index.entered.get(ancestorId);
140
+ const ancestorExited = index.exited.get(ancestorId);
141
+ const nodeEntered = index.entered.get(nodeId);
142
+ const nodeExited = index.exited.get(nodeId);
143
+ return (
144
+ ancestorEntered !== undefined
145
+ && ancestorExited !== undefined
146
+ && nodeEntered !== undefined
147
+ && nodeExited !== undefined
148
+ && ancestorEntered <= nodeEntered
149
+ && nodeExited <= ancestorExited
150
+ );
151
+ }
152
+
82
153
  module.exports = { normalizeTreeSnapshot };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ function invalidSymlink(node, message) {
4
+ throw Object.assign(new Error(message), {
5
+ code: 'WORKSPACE_TREE_SYMLINK_ESCAPE',
6
+ reason: 'symlink-escapes-workspace',
7
+ statusCode: 409,
8
+ nodeId: node?.id || null,
9
+ });
10
+ }
11
+
12
+ function portableTargetSegments(target, node) {
13
+ if (
14
+ typeof target !== 'string'
15
+ || !target
16
+ || target.includes('\0')
17
+ || /^[\\/]/.test(target)
18
+ || /^[A-Za-z]:/.test(target)
19
+ ) {
20
+ invalidSymlink(node, 'workspace tree symlink target must be relative');
21
+ }
22
+ return target.split(/[\\/]+/);
23
+ }
24
+
25
+ function liveNode(node) {
26
+ return Boolean(node && !node.trashedAt && !node.purgedAt);
27
+ }
28
+
29
+ function logicalParentSegments(nodes, rootId, node) {
30
+ const segments = [];
31
+ const seen = new Set([node.id]);
32
+ let currentId = node.parentId;
33
+ while (currentId && currentId !== rootId) {
34
+ if (seen.has(currentId)) {
35
+ invalidSymlink(node, 'workspace tree symlink ancestry contains a cycle');
36
+ }
37
+ seen.add(currentId);
38
+ const current = nodes.get(currentId);
39
+ if (!current) {
40
+ invalidSymlink(node, 'workspace tree symlink parent is missing');
41
+ }
42
+ segments.push(current.name);
43
+ currentId = current.parentId;
44
+ }
45
+ if (currentId !== rootId) {
46
+ invalidSymlink(node, 'workspace tree symlink is outside its workspace');
47
+ }
48
+ return segments.reverse();
49
+ }
50
+
51
+ function validateLiveSymlinkTargets(inputNodes, rootId) {
52
+ const nodes = inputNodes instanceof Map
53
+ ? inputNodes
54
+ : new Map((inputNodes || []).map((node) => [node.id, node]));
55
+ for (const node of nodes.values()) {
56
+ if (node.kind !== 'symlink' || !liveNode(node)) continue;
57
+ const resolved = logicalParentSegments(nodes, rootId, node);
58
+ for (const segment of portableTargetSegments(node.symlinkTarget, node)) {
59
+ if (!segment || segment === '.') continue;
60
+ if (segment === '..') {
61
+ if (!resolved.length) {
62
+ invalidSymlink(
63
+ node,
64
+ `workspace tree symlink ${node.id} escapes its workspace`,
65
+ );
66
+ }
67
+ resolved.pop();
68
+ } else {
69
+ resolved.push(segment);
70
+ }
71
+ }
72
+ }
73
+ return nodes;
74
+ }
75
+
76
+ module.exports = { validateLiveSymlinkTargets };