amalgm 0.1.239 → 0.1.241

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 (87) hide show
  1. package/lib/shared-realtime-tunnel.js +18 -4
  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/server/routes/state.js +13 -0
  6. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  7. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  8. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  9. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  10. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  11. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  12. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  13. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  14. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  15. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  16. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  17. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  18. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  19. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  20. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  21. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  22. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +98 -4
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +156 -8
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  41. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  42. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  43. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  44. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  45. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  46. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  48. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  49. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  50. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  67. package/runtime/scripts/amalgm-mcp/workspace/objects.js +185 -55
  68. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  69. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  70. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  72. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  73. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  74. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  83. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  86. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  87. 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 };