amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -57,13 +57,7 @@ function isPrivateFile(name) {
57
57
  // enrolled as leaf nodes and are never followed by the scanner.
58
58
  function shouldEnroll(relativePath) {
59
59
  const parts = partsFor(relativePath);
60
- if (
61
- parts.length === 0
62
- || parts.some((part) => (
63
- EXCLUDED_DIRECTORIES.has(part)
64
- || part.startsWith('.amalgm-tree-apply-')
65
- ))
66
- ) return false;
60
+ if (parts.length === 0 || parts.some((part) => EXCLUDED_DIRECTORIES.has(part))) return false;
67
61
  const name = parts.at(-1);
68
62
  if (EXCLUDED_FILES.has(name) || isPrivateFile(name)) return false;
69
63
  return !name.startsWith('.#') && !name.endsWith('~') && !/\.(?:swp|swo)$/i.test(name);
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { AMALGM_DIR } = require('../../config');
6
6
  const { openLocalDb } = require('../../state/db');
7
+ const { descendantsInclude } = require('./reducer');
7
8
 
8
9
  function invalid(message, statusCode = 400) {
9
10
  return Object.assign(new Error(message), { statusCode });
@@ -22,7 +23,6 @@ function rowToNode(row) {
22
23
  deviceId: row.device_id || null,
23
24
  inode: row.inode || null,
24
25
  trashedAt: row.trashed_at || null,
25
- trashComponentId: row.trash_component_id || null,
26
26
  purgedAt: row.purged_at || null,
27
27
  missingAt: row.missing_at || null,
28
28
  createdAt: row.created_at,
@@ -53,24 +53,58 @@ function relativePathForNode(nodes, nodeId, memo = new Map()) {
53
53
  return relativePath;
54
54
  }
55
55
 
56
- function trashPath(workspaceId, nodeId, workspacePath = null) {
57
- if (workspacePath) {
58
- // Tree trash participates in the same rename-only WAL as the visible
59
- // workspace. Keep it beside that workspace so arbitrary registered
60
- // folders (including external volumes) never turn trash/restore into an
61
- // impossible cross-device rename.
62
- return path.join(
63
- path.dirname(path.resolve(workspacePath)),
64
- `.amalgm-tree-trash-${workspaceId}`,
65
- nodeId,
66
- );
67
- }
68
- // Compatibility for callers that only need the historical locator. New
69
- // materialization plans always pass workspacePath and persist the exact
70
- // same-filesystem location in their action.
56
+ function trashPath(workspaceId, nodeId) {
71
57
  return path.join(AMALGM_DIR, 'trash', 'workspaces', workspaceId, nodeId);
72
58
  }
73
59
 
60
+ function requireMissing(targetPath, message) {
61
+ try {
62
+ fs.lstatSync(targetPath);
63
+ throw invalid(message, 409);
64
+ } catch (error) {
65
+ if (error?.code !== 'ENOENT') throw error;
66
+ }
67
+ }
68
+
69
+ function materializeOperation(workspace, before, after, operation) {
70
+ const oldNode = before.get(operation.nodeId) || null;
71
+ const newNode = after.get(operation.nodeId) || null;
72
+ const oldRelative = oldNode ? relativePathForNode(before, oldNode.id) : null;
73
+ const newRelative = newNode ? relativePathForNode(after, newNode.id) : null;
74
+ const oldPath = oldRelative ? path.join(workspace.path, ...oldRelative.split('/')) : null;
75
+ const newPath = newRelative ? path.join(workspace.path, ...newRelative.split('/')) : null;
76
+
77
+ if (operation.kind === 'tree.create') {
78
+ requireMissing(newPath, 'create destination already exists');
79
+ fs.mkdirSync(path.dirname(newPath), { recursive: true });
80
+ if (operation.nodeKind === 'folder') fs.mkdirSync(newPath);
81
+ else if (operation.nodeKind === 'symlink') fs.symlinkSync(operation.symlinkTarget, newPath);
82
+ else fs.writeFileSync(newPath, '');
83
+ } else if (operation.kind === 'tree.rename' || operation.kind === 'tree.move') {
84
+ requireMissing(newPath, 'move destination already exists');
85
+ fs.mkdirSync(path.dirname(newPath), { recursive: true });
86
+ fs.renameSync(oldPath, newPath);
87
+ } else if (operation.kind === 'tree.trash') {
88
+ const destination = trashPath(workspace.id, operation.nodeId);
89
+ requireMissing(destination, 'trash destination already exists');
90
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
91
+ fs.renameSync(oldPath, destination);
92
+ } else if (operation.kind === 'tree.restore') {
93
+ const source = trashPath(workspace.id, operation.nodeId);
94
+ requireMissing(newPath, 'restore destination already exists');
95
+ fs.mkdirSync(path.dirname(newPath), { recursive: true });
96
+ fs.renameSync(source, newPath);
97
+ } else if (operation.kind === 'tree.purge') {
98
+ for (const node of before.values()) {
99
+ if (node.id === operation.nodeId || descendantsInclude(before, operation.nodeId, node.id)) {
100
+ fs.rmSync(trashPath(workspace.id, node.id), { recursive: true, force: true });
101
+ }
102
+ }
103
+ }
104
+
105
+ return { oldPath, newPath };
106
+ }
107
+
74
108
  function absolutePathForWorkspace(workspaceId, relativePath, database) {
75
109
  const workspace = database.prepare('SELECT path FROM workspaces WHERE id = ?').get(workspaceId);
76
110
  if (!workspace) throw invalid('workspace not found', 404);
@@ -81,78 +115,32 @@ function persistNodes(database, before, after, workspaceId) {
81
115
  const now = new Date().toISOString();
82
116
  const pathMemo = new Map();
83
117
  const changedPaths = [];
84
- const persisted = new Map(
85
- database.prepare(
86
- 'SELECT * FROM workspace_nodes WHERE workspace_id = ?',
87
- ).all(workspaceId)
88
- .map(rowToNode)
89
- .map((node) => [node.id, node]),
90
- );
91
118
  for (const node of after.values()) {
92
119
  const relativePath = relativePathForNode(after, node.id, pathMemo);
93
- const previousFromBefore = before.get(node.id);
94
- const previous = previousFromBefore || persisted.get(node.id);
95
- const previousRelative = previousFromBefore
96
- ? relativePathForNode(before, previousFromBefore.id)
97
- : previous?.relativePath || null;
98
- if (!persisted.has(node.id)) {
120
+ const previous = before.get(node.id);
121
+ const previousRelative = previous ? relativePathForNode(before, previous.id) : null;
122
+ if (!previous) {
99
123
  const absolutePath = relativePath ? absolutePathForWorkspace(workspaceId, relativePath, database) : null;
100
- const stats = absolutePath && !node.trashedAt && !node.purgedAt
101
- ? fs.lstatSync(absolutePath)
102
- : null;
124
+ const stats = absolutePath ? fs.lstatSync(absolutePath) : null;
103
125
  database.prepare(`
104
126
  INSERT INTO workspace_nodes (
105
127
  id, workspace_id, parent_id, kind, name, relative_path, symlink_target,
106
- device_id, inode, trashed_at, trash_component_id, purged_at,
107
- missing_at, created_at, updated_at
108
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
109
- ON CONFLICT(id) DO UPDATE SET
110
- workspace_id = excluded.workspace_id,
111
- parent_id = excluded.parent_id,
112
- kind = excluded.kind,
113
- name = excluded.name,
114
- relative_path = excluded.relative_path,
115
- symlink_target = excluded.symlink_target,
116
- device_id = excluded.device_id,
117
- inode = excluded.inode,
118
- trashed_at = excluded.trashed_at,
119
- trash_component_id = excluded.trash_component_id,
120
- purged_at = excluded.purged_at,
121
- missing_at = NULL,
122
- updated_at = excluded.updated_at
128
+ device_id, inode, trashed_at, purged_at, missing_at, created_at, updated_at
129
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
123
130
  `).run(
124
131
  node.id, node.workspaceId, node.parentId, node.kind, node.name,
125
132
  relativePath, node.symlinkTarget, stats ? String(stats.dev) : null,
126
- stats ? String(stats.ino) : null, node.trashedAt,
127
- node.trashComponentId, node.purgedAt,
133
+ stats ? String(stats.ino) : null, node.trashedAt, node.purgedAt,
128
134
  node.createdAt, now,
129
135
  );
130
136
  } else {
131
137
  database.prepare(`
132
138
  UPDATE workspace_nodes
133
139
  SET parent_id = ?, name = ?, relative_path = ?, trashed_at = ?,
134
- trash_component_id = ?, purged_at = ?, missing_at = NULL,
135
- updated_at = ?
140
+ purged_at = ?, missing_at = NULL, updated_at = ?
136
141
  WHERE id = ?
137
- `).run(
138
- node.parentId,
139
- node.name,
140
- relativePath,
141
- node.trashedAt,
142
- node.trashComponentId,
143
- node.purgedAt,
144
- now,
145
- node.id,
146
- );
147
- // Trashed and purged rows are UUID tombstones, not live document
148
- // locations. Multiple tombstones may legally retain the same logical
149
- // path, so only a resulting live node may participate in document path
150
- // rebinding.
151
- if (
152
- previousRelative !== relativePath
153
- && !node.trashedAt
154
- && !node.purgedAt
155
- ) {
142
+ `).run(node.parentId, node.name, relativePath, node.trashedAt, node.purgedAt, now, node.id);
143
+ if (previousRelative !== relativePath) {
156
144
  changedPaths.push({
157
145
  nodeId: node.id,
158
146
  oldPath: absolutePathForWorkspace(workspaceId, previousRelative, database),
@@ -177,9 +165,9 @@ function hasAncestorInSet(node, nodesById, ids) {
177
165
 
178
166
  module.exports = {
179
167
  hasAncestorInSet,
168
+ materializeOperation,
180
169
  persistNodes,
181
170
  relativePathForNode,
182
171
  rowToNode,
183
- trashPath,
184
172
  workspaceRow,
185
173
  };
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const { normalizeTreeOperation } = require('./operations');
4
- const { validateLiveSymlinkTargets } = require('./symlinks');
5
4
 
6
5
  function toMap(nodes) {
7
6
  if (nodes instanceof Map) return new Map(nodes);
@@ -20,25 +19,6 @@ function descendantsInclude(nodes, ancestorId, candidateId) {
20
19
  return false;
21
20
  }
22
21
 
23
- function descendantIds(nodes, ancestorId) {
24
- const children = new Map();
25
- for (const node of nodes.values()) {
26
- if (!node.parentId) continue;
27
- const siblings = children.get(node.parentId) || [];
28
- siblings.push(node.id);
29
- children.set(node.parentId, siblings);
30
- }
31
- const descendants = [];
32
- const queue = [ancestorId];
33
- for (let cursor = 0; cursor < queue.length; cursor += 1) {
34
- for (const childId of children.get(queue[cursor]) || []) {
35
- descendants.push(childId);
36
- queue.push(childId);
37
- }
38
- }
39
- return descendants;
40
- }
41
-
42
22
  function ensureAvailableName(nodes, workspaceId, parentId, name, exceptId = null) {
43
23
  const duplicate = Array.from(nodes.values()).find((node) => (
44
24
  node.workspaceId === workspaceId
@@ -71,12 +51,10 @@ function applyTreeOperation(inputNodes, inputOperation) {
71
51
  name: operation.name,
72
52
  symlinkTarget: operation.symlinkTarget || null,
73
53
  trashedAt: null,
74
- trashComponentId: null,
75
54
  purgedAt: null,
76
55
  createdAt: operation.createdAt,
77
56
  updatedAt: operation.createdAt,
78
57
  });
79
- validateLiveSymlinkTargets(nodes, operation.workspaceId);
80
58
  return nodes;
81
59
  }
82
60
 
@@ -94,7 +72,6 @@ function applyTreeOperation(inputNodes, inputOperation) {
94
72
  }
95
73
 
96
74
  const next = { ...existing, updatedAt: operation.changedAt };
97
- const existingTrashComponentId = existing.trashComponentId || existing.id;
98
75
  if (operation.kind === 'tree.rename') {
99
76
  ensureAvailableName(nodes, operation.workspaceId, existing.parentId, operation.name, existing.id);
100
77
  next.name = operation.name;
@@ -115,7 +92,6 @@ function applyTreeOperation(inputNodes, inputOperation) {
115
92
  throw Object.assign(new Error('a node is already trashed'), { statusCode: 409 });
116
93
  }
117
94
  next.trashedAt = operation.trashedAt;
118
- next.trashComponentId = operation.nodeId;
119
95
  } else if (operation.kind === 'tree.restore') {
120
96
  if (!existing.trashedAt) {
121
97
  throw Object.assign(new Error('a node must be trashed before it can be restored'), { statusCode: 409 });
@@ -133,47 +109,26 @@ function applyTreeOperation(inputNodes, inputOperation) {
133
109
  next.parentId = parentId;
134
110
  next.name = name;
135
111
  next.trashedAt = null;
136
- next.trashComponentId = null;
137
112
  } else if (operation.kind === 'tree.purge') {
138
113
  if (!existing.trashedAt) {
139
114
  throw Object.assign(new Error('a node must be trashed before permanent removal'), { statusCode: 409 });
140
115
  }
141
116
  next.purgedAt = operation.purgedAt;
142
- next.trashComponentId = null;
143
117
  }
144
118
 
145
119
  nodes.set(next.id, next);
146
120
  if (operation.kind === 'tree.trash' || operation.kind === 'tree.restore' || operation.kind === 'tree.purge') {
147
- for (const childId of descendantIds(nodes, next.id)) {
148
- const child = nodes.get(childId);
121
+ for (const child of nodes.values()) {
122
+ if (child.id === next.id || !descendantsInclude(nodes, next.id, child.id)) continue;
149
123
  if (operation.kind === 'tree.trash' && !child.trashedAt) {
150
- nodes.set(child.id, {
151
- ...child,
152
- trashedAt: operation.trashedAt,
153
- trashComponentId: operation.nodeId,
154
- updatedAt: operation.changedAt,
155
- });
156
- } else if (
157
- operation.kind === 'tree.restore'
158
- && (child.trashComponentId || child.id) === existingTrashComponentId
159
- ) {
160
- nodes.set(child.id, {
161
- ...child,
162
- trashedAt: null,
163
- trashComponentId: null,
164
- updatedAt: operation.changedAt,
165
- });
124
+ nodes.set(child.id, { ...child, trashedAt: operation.trashedAt, updatedAt: operation.changedAt });
125
+ } else if (operation.kind === 'tree.restore' && child.trashedAt === existing.trashedAt) {
126
+ nodes.set(child.id, { ...child, trashedAt: null, updatedAt: operation.changedAt });
166
127
  } else if (operation.kind === 'tree.purge') {
167
- nodes.set(child.id, {
168
- ...child,
169
- trashComponentId: null,
170
- purgedAt: operation.purgedAt,
171
- updatedAt: operation.changedAt,
172
- });
128
+ nodes.set(child.id, { ...child, purgedAt: operation.purgedAt, updatedAt: operation.changedAt });
173
129
  }
174
130
  }
175
131
  }
176
- validateLiveSymlinkTargets(nodes, operation.workspaceId);
177
132
  return nodes;
178
133
  }
179
134
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { isUuid } = require('./identity');
4
4
  const { KINDS, normalizeNodeName } = require('./operations');
5
- const { validateLiveSymlinkTargets } = require('./symlinks');
6
5
 
7
6
  function invalid(message) {
8
7
  throw Object.assign(new Error(message), { statusCode: 400 });
@@ -36,12 +35,6 @@ function normalizeTreeSnapshot(input) {
36
35
  }
37
36
  const parentId = value.parentId == null ? null : String(value.parentId).toLowerCase();
38
37
  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
- }
45
38
  const createdAt = timestamp(value.createdAt, new Date().toISOString());
46
39
  const updatedAt = timestamp(value.updatedAt, createdAt);
47
40
  if (value.kind === 'symlink' && (typeof value.symlinkTarget !== 'string' || !value.symlinkTarget)) {
@@ -55,7 +48,6 @@ function normalizeTreeSnapshot(input) {
55
48
  name: normalizeNodeName(value.name),
56
49
  symlinkTarget: value.kind === 'symlink' ? value.symlinkTarget : null,
57
50
  trashedAt: value.trashedAt ? timestamp(value.trashedAt) : null,
58
- trashComponentId,
59
51
  purgedAt: value.purgedAt ? timestamp(value.purgedAt) : null,
60
52
  createdAt,
61
53
  updatedAt,
@@ -66,30 +58,17 @@ function normalizeTreeSnapshot(input) {
66
58
  if (!root || root.parentId !== null || root.kind !== 'folder') {
67
59
  invalid('workspace tree requires one folder root whose id is the workspace id');
68
60
  }
69
- const ancestry = buildAncestryIndex(nodes, workspaceId);
70
61
  const siblingNames = new Set();
71
62
  for (const node of nodes.values()) {
72
63
  if (node.id === workspaceId) continue;
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');
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');
93
72
  }
94
73
  if (!node.trashedAt && !node.purgedAt) {
95
74
  const key = `${node.parentId}\0${node.name}`;
@@ -97,57 +76,7 @@ function normalizeTreeSnapshot(input) {
97
76
  siblingNames.add(key);
98
77
  }
99
78
  }
100
- validateLiveSymlinkTargets(nodes, workspaceId);
101
79
  return { schemaVersion: 1, workspaceId, version: input.version, nodes };
102
80
  }
103
81
 
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
-
153
82
  module.exports = { normalizeTreeSnapshot };