amalgm 0.1.175 → 0.1.176

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 (34) hide show
  1. package/lib/shared-realtime-tunnel.js +28 -12
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/index.js +1 -1
  4. package/runtime/scripts/amalgm-mcp/server/routes/state.js +5 -1
  5. package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +4 -0
  6. package/runtime/scripts/amalgm-mcp/state/docs.js +13 -6
  7. package/runtime/scripts/amalgm-mcp/state/migrations.js +0 -4
  8. package/runtime/scripts/amalgm-mcp/state/mutations.js +13 -1
  9. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +25 -1
  10. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +9 -0
  11. package/runtime/scripts/amalgm-mcp/tests/state-promotion-ownership.test.js +30 -3
  12. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +4 -5
  13. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +91 -0
  14. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +37 -11
  15. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +10 -2
  16. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +14 -6
  17. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +324 -0
  18. package/runtime/scripts/amalgm-mcp/tests/workspace-store.test.js +36 -1
  19. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +81 -48
  20. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -1
  21. package/runtime/scripts/amalgm-mcp/workspace/cards.js +9 -21
  22. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +187 -122
  23. package/runtime/scripts/amalgm-mcp/workspace/git.js +71 -0
  24. package/runtime/scripts/amalgm-mcp/workspace/merge.js +119 -2
  25. package/runtime/scripts/amalgm-mcp/workspace/objects.js +49 -17
  26. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +582 -0
  27. package/runtime/scripts/amalgm-mcp/workspace/rest.js +15 -1
  28. package/runtime/scripts/amalgm-mcp/workspace/store.js +8 -18
  29. package/runtime/scripts/amalgm-mcp/workspace/transition.js +67 -0
  30. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +3 -4
  31. package/runtime/scripts/amalgm-mcp/workspace/wall.js +331 -180
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-reconcile.test.js +0 -359
  33. package/runtime/scripts/amalgm-mcp/workspace/ledger.js +0 -96
  34. package/runtime/scripts/amalgm-mcp/workspace/reconcile.js +0 -452
@@ -13,6 +13,14 @@ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-repo-objects-test
13
13
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
14
14
  process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
15
15
  process.env.AMALGM_COMPUTER_ID = 'machine-objects-test';
16
+ fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
17
+ fs.writeFileSync(
18
+ path.join(process.env.AMALGM_DIR, 'computer.json'),
19
+ JSON.stringify({
20
+ computer_id: 'machine-objects-test',
21
+ user_id: '11111111-1111-4111-8111-111111111111',
22
+ }),
23
+ );
16
24
 
17
25
  const { closeLocalDb } = require('../state/db');
18
26
  const { hasCommit, headOid } = require('../workspace/git');
@@ -69,7 +77,7 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
69
77
  continue;
70
78
  }
71
79
  uploaded.set(upload.sha256, upload.dataBase64);
72
- acknowledgeUpload(upload.sha256);
80
+ acknowledgeUpload(upload.resourceId, upload.sha256);
73
81
  const sourceState = await objectSourceFor(source, 'repo:test', unpushedOid);
74
82
  if (sourceState.kind === 'bundle') break;
75
83
  }
@@ -86,7 +94,7 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
86
94
  while (true) {
87
95
  const download = claimDownload();
88
96
  if (!download) break;
89
- receiveDownload(download.sha256, uploaded.get(download.sha256));
97
+ receiveDownload(download.resourceId, download.sha256, uploaded.get(download.sha256));
90
98
  }
91
99
  assert.equal((await importFromLocator(destination, locator)).state, 'imported');
92
100
  assert.equal(await hasCommit(destination, unpushedOid), true);
@@ -95,16 +103,16 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
95
103
 
96
104
  test('a large checkpoint uses the same verified chunk lane', () => {
97
105
  const content = Buffer.alloc(1_300_000, 0x61);
98
- assert.equal(publishPayload('large-checkpoint', content).kind, 'pending');
106
+ assert.equal(publishPayload('large-checkpoint', content, 'repo:test').kind, 'pending');
99
107
 
100
108
  const uploaded = new Map();
101
109
  while (true) {
102
110
  const upload = claimUpload();
103
111
  if (!upload) break;
104
112
  uploaded.set(upload.sha256, upload.dataBase64);
105
- acknowledgeUpload(upload.sha256);
113
+ acknowledgeUpload(upload.resourceId, upload.sha256);
106
114
  }
107
- const locator = publishPayload('large-checkpoint', content);
115
+ const locator = publishPayload('large-checkpoint', content, 'repo:test');
108
116
  assert.equal(locator.kind, 'chunks');
109
117
  assert.ok(locator.chunks.length >= 3);
110
118
 
@@ -116,7 +124,7 @@ test('a large checkpoint uses the same verified chunk lane', () => {
116
124
  while (true) {
117
125
  const download = claimDownload();
118
126
  if (!download) break;
119
- receiveDownload(download.sha256, uploaded.get(download.sha256));
127
+ receiveDownload(download.resourceId, download.sha256, uploaded.get(download.sha256));
120
128
  }
121
129
  const received = readPayload(locator);
122
130
  assert.equal(received.state, 'ready');
@@ -0,0 +1,324 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { execFile } = require('child_process');
9
+ const { promisify } = require('util');
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-reducer-test-'));
13
+ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
14
+ process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
15
+ process.env.AMALGM_COMPUTER_ID = 'machine-local';
16
+ process.env.AMALGM_DEVICE_ID = 'device-local';
17
+ fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
18
+ fs.writeFileSync(
19
+ path.join(process.env.AMALGM_DIR, 'computer.json'),
20
+ JSON.stringify({ computer_id: 'machine-local', user_id: '11111111-1111-4111-8111-111111111111' }),
21
+ );
22
+
23
+ const { closeLocalDb } = require('../state/db');
24
+ const { revisionOf } = require('../workspace/checkpoint');
25
+ const { materializeRepo, readRepoState, reduceWall } = require('../workspace/reducer');
26
+
27
+ test.after(() => {
28
+ closeLocalDb();
29
+ fs.rmSync(tempRoot, { recursive: true, force: true });
30
+ });
31
+
32
+ async function git(cwd, ...args) {
33
+ const { stdout } = await execFileAsync(
34
+ 'git',
35
+ ['-c', 'user.email=test@amalgm.test', '-c', 'user.name=Amalgm Test', '-c', 'commit.gpgsign=false', ...args],
36
+ { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', LC_ALL: 'C' } },
37
+ );
38
+ return stdout;
39
+ }
40
+
41
+ async function makeRepo(name) {
42
+ const repo = path.join(tempRoot, name);
43
+ fs.mkdirSync(repo, { recursive: true });
44
+ await git(repo, 'init', '-b', 'main');
45
+ fs.writeFileSync(path.join(repo, 'a.txt'), 'a0\n');
46
+ fs.writeFileSync(path.join(repo, 'b.txt'), 'b0\n');
47
+ await git(repo, 'add', '.');
48
+ await git(repo, 'commit', '-m', 'base');
49
+ return { repo, oid: (await git(repo, 'rev-parse', 'HEAD')).trim() };
50
+ }
51
+
52
+ async function patchFor(repo, edits) {
53
+ for (const [name, content] of Object.entries(edits)) fs.writeFileSync(path.join(repo, name), content);
54
+ const diff = await git(repo, 'diff', 'HEAD', '--binary', '--full-index');
55
+ await git(repo, 'reset', '--hard', 'HEAD');
56
+ return diff;
57
+ }
58
+
59
+ function value(baseOid, diff) {
60
+ return {
61
+ revision: revisionOf(baseOid, diff, ''),
62
+ indexDiff: diff,
63
+ worktreeDiff: '',
64
+ };
65
+ }
66
+
67
+ function transition(machine, version, baseOid, from, to) {
68
+ return {
69
+ checkpoint: 'repo-checkpoint@4',
70
+ machine,
71
+ repoKey: 'root:test',
72
+ branch: 'main',
73
+ baseOid,
74
+ from: value(baseOid, from),
75
+ to: value(baseOid, to),
76
+ version,
77
+ mutationId: `mut_${machine}_${version}`.padEnd(16, 'x'),
78
+ };
79
+ }
80
+
81
+ function card(repo, oid, version) {
82
+ return {
83
+ card: 'repo-card@2',
84
+ path: repo,
85
+ replica: 'rpl_local',
86
+ machine: 'machine-local',
87
+ kind: 'primary',
88
+ health: 'healthy',
89
+ operation: 'none',
90
+ head: { branch: 'main', oid, detached: false },
91
+ repo: { rootCommit: 'test', remote: null },
92
+ objects: { kind: 'local' },
93
+ positionVersion: version,
94
+ positionMutationId: `mut_card_${version}`.padEnd(16, 'x'),
95
+ };
96
+ }
97
+
98
+ test('wall reduction chooses one rail winner and orders checkpoint transitions by rail', () => {
99
+ const oldCard = { ...card('/old', 'a'.repeat(40), 4), checkpointVersion: 9 };
100
+ oldCard.checkpoint = transition('machine-a', 9, oldCard.head.oid, '', 'a');
101
+ const newCard = { ...card('/new', 'b'.repeat(40), 7), checkpointVersion: 8 };
102
+ newCard.checkpoint = transition('machine-b', 8, newCard.head.oid, '', 'b');
103
+ const plans = reduceWall([
104
+ { machine: 'machine-a', cards: [oldCard] },
105
+ { machine: 'machine-b', cards: [newCard] },
106
+ ]);
107
+ const plan = plans.get('root:test');
108
+ assert.equal(plan.card.positionVersion, 7);
109
+ assert.deepEqual(plan.transitions.map((item) => item.version), [8]);
110
+ });
111
+
112
+ test('branch refs reduce per name; an absent stale list cannot delete another writer branch', () => {
113
+ const first = card('/a', 'a'.repeat(40), 5);
114
+ first.branches = [
115
+ { name: 'main', oid: first.head.oid, version: 5, mutationId: 'mut_main_00000001' },
116
+ { name: 'feature', oid: 'c'.repeat(40), version: 4, mutationId: 'mut_feature_0001' },
117
+ ];
118
+ const stale = card('/b', 'a'.repeat(40), 6);
119
+ stale.branches = [
120
+ { name: 'main', oid: stale.head.oid, version: 6, mutationId: 'mut_main_00000002' },
121
+ ];
122
+ let plan = reduceWall([
123
+ { machine: 'machine-a', cards: [first] },
124
+ { machine: 'machine-b', cards: [stale] },
125
+ ]).get('root:test');
126
+ assert.equal(plan.branches.find((ref) => ref.name === 'feature').oid, 'c'.repeat(40));
127
+
128
+ stale.branches.push({
129
+ name: 'feature',
130
+ oid: null,
131
+ version: 7,
132
+ mutationId: 'mut_feature_del01',
133
+ });
134
+ plan = reduceWall([
135
+ { machine: 'machine-a', cards: [first] },
136
+ { machine: 'machine-b', cards: [stale] },
137
+ ]).get('root:test');
138
+ assert.equal(plan.branches.find((ref) => ref.name === 'feature').oid, null);
139
+ });
140
+
141
+ test('a multi-user storm reduces identically regardless of network arrival order', () => {
142
+ const oid = 'a'.repeat(40);
143
+ const entries = Array.from({ length: 12 }, (_, index) => {
144
+ const machine = `machine-${index}`;
145
+ const observed = card(`/machine-${index}`, oid, index + 1);
146
+ observed.checkpoint = transition(machine, 100 + index, oid, '', `patch-${index}`);
147
+ observed.checkpointVersion = 100 + index;
148
+ observed.checkpointMutationId = `mut_checkpoint_${index}`.padEnd(16, 'x');
149
+ observed.branches = [
150
+ {
151
+ name: `feature-${index % 4}`,
152
+ oid: index >= 8 && index % 4 === 0 ? null : String(index % 10).repeat(40),
153
+ version: 200 + index,
154
+ mutationId: `mut_branch_${index}`.padEnd(16, 'x'),
155
+ },
156
+ ];
157
+ return { machine, cards: [observed] };
158
+ });
159
+ const normalized = (wallEntries) => {
160
+ const plan = reduceWall(wallEntries).get('root:test');
161
+ return {
162
+ card: [plan.card.positionVersion, plan.card.writer],
163
+ checkpoints: plan.transitions.map((item) => [item.version, item.machine]),
164
+ branches: plan.branches.map((ref) => [ref.name, ref.oid, ref.version]),
165
+ };
166
+ };
167
+ const expected = normalized(entries);
168
+ let seed = 0x5eed1234;
169
+ const random = () => {
170
+ seed ^= seed << 13;
171
+ seed ^= seed >>> 17;
172
+ seed ^= seed << 5;
173
+ return (seed >>> 0) / 0x100000000;
174
+ };
175
+ for (let pass = 0; pass < 250; pass += 1) {
176
+ const shuffled = [...entries];
177
+ for (let index = shuffled.length - 1; index > 0; index -= 1) {
178
+ const other = Math.floor(random() * (index + 1));
179
+ [shuffled[index], shuffled[other]] = [shuffled[other], shuffled[index]];
180
+ }
181
+ assert.deepEqual(normalized(shuffled), expected);
182
+ }
183
+ });
184
+
185
+ test('one materializer converges to the reduced checkpoint and later removes a reverted edit', async () => {
186
+ const { repo, oid } = await makeRepo('materialize');
187
+ const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
188
+ const patchB = await patchFor(repo, { 'b.txt': 'b1\n' });
189
+ const localCard = card(repo, oid, 1);
190
+
191
+ const first = {
192
+ repoKey: 'root:test',
193
+ card: localCard,
194
+ transitions: [
195
+ transition('machine-a', 2, oid, '', patchA),
196
+ transition('machine-b', 3, oid, patchA, `${patchA}${patchB}`),
197
+ ],
198
+ };
199
+ const applied = await materializeRepo(localCard, first);
200
+ assert.equal(applied.state, 'in-sync');
201
+ assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'a1\n');
202
+ assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'b1\n');
203
+
204
+ const combined = readRepoState('root:test').indexDiff;
205
+ const second = {
206
+ repoKey: 'root:test',
207
+ card: localCard,
208
+ transitions: [
209
+ transition('machine-b', 3, oid, patchA, combined),
210
+ transition('machine-a', 4, oid, combined, patchB),
211
+ ],
212
+ };
213
+ const reverted = await materializeRepo(localCard, second);
214
+ assert.equal(reverted.state, 'in-sync');
215
+ assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'a0\n');
216
+ assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'b1\n');
217
+ });
218
+
219
+ test('a losing commit is parked, the winning card lands exactly, and losing work becomes uncommitted', async () => {
220
+ const { repo, oid: baseOid } = await makeRepo('simultaneous-commit');
221
+ fs.writeFileSync(path.join(repo, 'local-commit.txt'), 'losing commit work\n');
222
+ await git(repo, 'add', '.');
223
+ await git(repo, 'commit', '-m', 'local losing commit');
224
+ const losingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
225
+ fs.writeFileSync(path.join(repo, 'local-dirty.txt'), 'also uncommitted\n');
226
+
227
+ await git(repo, 'switch', '--detach', baseOid);
228
+ fs.writeFileSync(path.join(repo, 'winner.txt'), 'winning commit work\n');
229
+ await git(repo, 'add', '.');
230
+ await git(repo, 'commit', '-m', 'remote winning commit');
231
+ const winningOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
232
+ await git(repo, 'switch', '-C', 'main', losingOid);
233
+
234
+ const localCard = card(repo, losingOid, 1);
235
+ const winningCard = {
236
+ ...localCard,
237
+ head: { branch: 'main', oid: winningOid, detached: false },
238
+ positionVersion: 2,
239
+ positionMutationId: 'mut_card_winner_0001',
240
+ };
241
+ const outcome = await materializeRepo(localCard, {
242
+ repoKey: 'root:test',
243
+ card: winningCard,
244
+ transitions: [],
245
+ parked: [],
246
+ });
247
+
248
+ assert.equal(outcome.state, 'in-sync');
249
+ assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), winningOid);
250
+ assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingOid}`)).trim(), losingOid);
251
+ assert.equal(fs.readFileSync(path.join(repo, 'winner.txt'), 'utf8'), 'winning commit work\n');
252
+ assert.equal(fs.readFileSync(path.join(repo, 'local-commit.txt'), 'utf8'), 'losing commit work\n');
253
+ assert.equal(fs.readFileSync(path.join(repo, 'local-dirty.txt'), 'utf8'), 'also uncommitted\n');
254
+ assert.equal((await git(repo, 'status', '--porcelain')).trim().length > 0, true);
255
+ assert.deepEqual(readRepoState('root:test').parked.map((entry) => entry.oid), [losingOid]);
256
+ });
257
+
258
+ test('checkpoint then commit folds through the same card movement and unrelated local work stays', async () => {
259
+ const { repo, oid: baseOid } = await makeRepo('checkpoint-commit-fold');
260
+ await git(repo, 'switch', '-c', 'target');
261
+ fs.writeFileSync(path.join(repo, 'shared.txt'), 'already checkpointed\n');
262
+ await git(repo, 'add', '.');
263
+ await git(repo, 'commit', '-m', 'commit checkpointed work');
264
+ const targetOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
265
+ await git(repo, 'switch', '-C', 'main', baseOid);
266
+
267
+ fs.writeFileSync(path.join(repo, 'shared.txt'), 'already checkpointed\n');
268
+ fs.writeFileSync(path.join(repo, 'local-only.txt'), 'keep me dirty\n');
269
+ const localCard = card(repo, baseOid, 1);
270
+ const outcome = await materializeRepo(localCard, {
271
+ repoKey: 'root:test',
272
+ card: {
273
+ ...localCard,
274
+ head: { branch: 'main', oid: targetOid, detached: false },
275
+ positionVersion: 2,
276
+ positionMutationId: 'mut_card_fold_00001',
277
+ },
278
+ transitions: [],
279
+ parked: [],
280
+ branches: [{ name: 'main', oid: targetOid, version: 2 }],
281
+ });
282
+
283
+ assert.equal(outcome.state, 'in-sync');
284
+ assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), targetOid);
285
+ assert.equal(fs.readFileSync(path.join(repo, 'shared.txt'), 'utf8'), 'already checkpointed\n');
286
+ assert.equal(fs.readFileSync(path.join(repo, 'local-only.txt'), 'utf8'), 'keep me dirty\n');
287
+ assert.equal((await git(repo, 'status', '--porcelain')).trim(), '?? local-only.txt');
288
+ });
289
+
290
+ test('the materializer creates, moves, and deletes branch refs without checking them out', async () => {
291
+ const { repo, oid } = await makeRepo('branches');
292
+ await git(repo, 'switch', '-c', 'feature');
293
+ fs.writeFileSync(path.join(repo, 'feature.txt'), 'feature\n');
294
+ await git(repo, 'add', '.');
295
+ await git(repo, 'commit', '-m', 'feature');
296
+ const featureOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
297
+ await git(repo, 'switch', 'main');
298
+ await git(repo, 'branch', '-D', 'feature');
299
+
300
+ const localCard = card(repo, oid, 1);
301
+ const createdPlan = {
302
+ repoKey: 'root:test',
303
+ card: localCard,
304
+ transitions: [],
305
+ parked: [],
306
+ branches: [
307
+ { name: 'main', oid, version: 1 },
308
+ { name: 'feature', oid: featureOid, version: 2 },
309
+ ],
310
+ };
311
+ await materializeRepo(localCard, createdPlan);
312
+ assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureOid);
313
+ assert.equal((await git(repo, 'branch', '--show-current')).trim(), 'main');
314
+
315
+ await materializeRepo(localCard, {
316
+ ...createdPlan,
317
+ branches: [
318
+ { name: 'main', oid, version: 1 },
319
+ { name: 'feature', oid: null, version: 3 },
320
+ ],
321
+ });
322
+ await assert.rejects(() => git(repo, 'rev-parse', 'refs/heads/feature'));
323
+ assert.equal((await git(repo, 'branch', '--show-current')).trim(), 'main');
324
+ });
@@ -11,7 +11,8 @@ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
11
11
  process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
12
12
  process.env.AMALGM_COMPUTER_ID = 'computer-test';
13
13
 
14
- const { closeLocalDb } = require('../state/db');
14
+ const { closeLocalDb, openLocalDb } = require('../state/db');
15
+ const { isUuidV7 } = require('../workspace/tree/identity');
15
16
  const {
16
17
  listWorkspaces,
17
18
  removeWorkspace,
@@ -82,3 +83,37 @@ test('workspace registry does not auto-discover managed folders', () => {
82
83
  assert.equal(fs.existsSync(projectPath), true);
83
84
  assert.equal(listWorkspaces().some((item) => item.path === projectPath), false);
84
85
  });
86
+
87
+ test('legacy workspace ids are replaced once and never retained as aliases', () => {
88
+ const projectPath = path.join(tempRoot, 'legacy-id-project');
89
+ fs.mkdirSync(projectPath, { recursive: true });
90
+
91
+ const workspace = upsertWorkspace({
92
+ id: 'legacy-workspace-id',
93
+ path: projectPath,
94
+ name: 'Legacy id project',
95
+ source: 'computer',
96
+ });
97
+
98
+ assert.equal(isUuidV7(workspace.id), true);
99
+ assert.notEqual(workspace.id, 'legacy-workspace-id');
100
+ assert.equal(
101
+ openLocalDb().prepare('SELECT count(*) AS count FROM workspace_id_aliases').get().count,
102
+ 0,
103
+ 'the historical alias table stays inert',
104
+ );
105
+ assert.throws(
106
+ () => renameWorkspace('legacy-workspace-id', 'Should not resolve'),
107
+ /Workspace not found/,
108
+ );
109
+ assert.equal(
110
+ upsertWorkspace({
111
+ id: 'another-legacy-id',
112
+ path: projectPath,
113
+ name: 'Same workspace',
114
+ source: 'computer',
115
+ }).id,
116
+ workspace.id,
117
+ 'stable path lookup returns the one canonical workspace UUID',
118
+ );
119
+ });
@@ -23,11 +23,23 @@ fs.writeFileSync(
23
23
  );
24
24
 
25
25
  const { closeLocalDb } = require('../state/db');
26
+ const { acknowledgeCloudMutation } = require('../state/mutations');
26
27
  const { registerReplica } = require('../state/replicas');
27
28
  const { openDoc, getDocText, writeDocText } = require('../state/docs');
28
29
  const { upsertWorkspace } = require('../workspace/store');
29
30
  const { refreshRepoCards } = require('../workspace/cards');
30
- const { publishWall, readWall, wallResourceId, wallStats } = require('../workspace/wall');
31
+ const { repoKeyOfCard } = require('../workspace/checkpoint');
32
+ const {
33
+ bindProject,
34
+ projectForRepo,
35
+ projectResourceId,
36
+ publishWall,
37
+ readWall,
38
+ wallStats,
39
+ } = require('../workspace/wall');
40
+
41
+ let repoKey;
42
+ let project;
31
43
 
32
44
  test.after(() => {
33
45
  closeLocalDb();
@@ -42,25 +54,29 @@ async function run(cwd, ...args) {
42
54
  );
43
55
  }
44
56
 
45
- const wallFile = path.join(process.env.AMALGM_DIR, 'state', 'machine-cards-v4.jsonl');
46
-
47
- test('the wall resource id is derived from the account: stable per user, distinct across users, valid on the rail', () => {
57
+ test('a project id is stable for one owner and repo, and distinct for another owner or repo', () => {
48
58
  const userA = '11111111-1111-4111-8111-111111111111';
49
59
  const userB = '22222222-2222-4222-8222-222222222222';
50
- assert.equal(wallResourceId(userA), wallResourceId(userA));
51
- assert.notEqual(wallResourceId(userA), wallResourceId(userB));
52
- assert.match(wallResourceId(userA), /^shr_[A-Za-z0-9_-]{16,128}$/);
53
- assert.equal(wallResourceId(null), null);
60
+ assert.equal(projectResourceId('repo-a', userA), projectResourceId('repo-a', userA));
61
+ assert.notEqual(projectResourceId('repo-a', userA), projectResourceId('repo-a', userB));
62
+ assert.notEqual(projectResourceId('repo-a', userA), projectResourceId('repo-b', userA));
63
+ assert.match(projectResourceId('repo-a', userA), /^shr_[A-Za-z0-9_-]{16,128}$/);
64
+ assert.equal(projectResourceId(null, userA), null);
54
65
  });
55
66
 
56
- test('publishing without a bound wall is a visible skip, never a write', () => {
57
- const result = publishWall();
58
- assert.equal(result.published, false);
59
- assert.equal(result.reason, 'wall-not-bound');
60
- assert.equal(fs.existsSync(wallFile), false);
67
+ test('a collaborator binds the same repo to the owner project, not a parallel project', () => {
68
+ const shared = projectResourceId('repo-shared', '22222222-2222-4222-8222-222222222222');
69
+ const bound = bindProject(
70
+ 'repo-shared',
71
+ shared,
72
+ '22222222-2222-4222-8222-222222222222',
73
+ );
74
+ assert.equal(bound.resourceId, shared);
75
+ assert.equal(projectForRepo('repo-shared').resourceId, shared);
76
+ assert.equal(projectForRepo('repo-shared').ownerUserId, '22222222-2222-4222-8222-222222222222');
61
77
  });
62
78
 
63
- test('a bound wall carries exactly one line per machine, replaced in place', async () => {
79
+ test('a repo project carries exactly one line per machine, replaced in place', async () => {
64
80
  const repo = path.join(tempRoot, 'projects/app');
65
81
  fs.mkdirSync(repo, { recursive: true });
66
82
  await run(repo, 'init', '-b', 'main');
@@ -68,81 +84,98 @@ test('a bound wall carries exactly one line per machine, replaced in place', asy
68
84
  await run(repo, 'add', '.');
69
85
  await run(repo, 'commit', '-m', 'initial');
70
86
  upsertWorkspace({ path: path.join(tempRoot, 'projects'), name: 'Projects', source: 'computer' });
71
- await refreshRepoCards();
87
+ const refreshed = await refreshRepoCards();
88
+ repoKey = repoKeyOfCard(refreshed.cards[0]);
89
+ project = projectForRepo(repoKey);
72
90
 
73
- // Bind the wall the way a completed attachment would: an empty file with
74
- // a registered replica for the account's derived resource id.
75
- fs.mkdirSync(path.dirname(wallFile), { recursive: true });
76
- fs.writeFileSync(wallFile, '');
77
- const opened = openDoc(wallFile);
91
+ fs.mkdirSync(path.dirname(project.file), { recursive: true });
92
+ fs.writeFileSync(project.file, '');
93
+ const opened = openDoc(project.file);
78
94
  registerReplica({
79
- resourceId: wallResourceId(),
95
+ resourceId: project.resourceId,
80
96
  localResource: opened.resource,
81
- localPath: wallFile,
97
+ localPath: project.file,
82
98
  contract: 'text-yjs@1',
83
99
  schemaVersion: 1,
84
100
  authorityEpoch: 1,
85
101
  });
86
102
 
87
- const first = publishWall();
103
+ const first = publishWall(repoKey);
88
104
  assert.equal(first.published, true);
89
- const entries = readWall();
105
+ const entries = readWall(repoKey);
90
106
  assert.equal(entries.length, 1);
107
+ assert.equal(entries[0].wall, 'code-project@1');
108
+ assert.equal(entries[0].project, repoKey);
91
109
  assert.equal(entries[0].machine, 'machine-a');
92
110
  assert.equal(entries[0].cards.length, 1);
93
111
  assert.equal(entries[0].cards[0].head.branch, 'main');
112
+ assert.equal(entries[0].cards[0].positionMutationId, first.mutationId);
113
+ assert.equal(entries[0].cards[0].positionVersion, null);
94
114
 
95
- // Same cards again: no new version, no doc write.
96
- const again = publishWall();
115
+ acknowledgeCloudMutation(project.resourceId, first.mutationId, {
116
+ version: 1,
117
+ committedAt: '2026-07-23T00:00:00.000Z',
118
+ });
119
+ const sealed = publishWall(repoKey);
120
+ assert.equal(sealed.published, true);
121
+ const sealedCard = readWall(repoKey)[0].cards[0];
122
+ assert.equal(sealedCard.positionMutationId, first.mutationId);
123
+ assert.equal(sealedCard.positionVersion, 1);
124
+
125
+ const again = publishWall(repoKey);
97
126
  assert.equal(again.published, false);
98
127
  assert.equal(again.reason, 'unchanged');
99
128
 
100
- // A real change replaces the line — never appends a second one.
101
129
  fs.writeFileSync(path.join(repo, 'new.txt'), 'x\n');
102
130
  await refreshRepoCards();
103
- const republished = publishWall();
131
+ const republished = publishWall(repoKey);
104
132
  assert.equal(republished.published, true);
105
- const after = readWall();
133
+ const after = readWall(repoKey);
106
134
  assert.equal(after.length, 1);
107
135
  assert.equal(after[0].cards[0].dirty.untracked, 1);
108
- assert.equal(getDocText(wallFile).trim().split('\n').length, 1);
136
+ assert.equal(after[0].cards[0].positionMutationId, first.mutationId);
137
+ assert.equal(after[0].cards[0].positionVersion, 1);
138
+ assert.equal(getDocText(project.file).trim().split('\n').length, 1);
109
139
  });
110
140
 
111
- test('a peer machine\'s line is preserved byte-for-byte through this machine\'s publishes', async () => {
141
+ test('a peer machine line is preserved byte-for-byte through local publishes', async () => {
112
142
  const peerLine = JSON.stringify({
113
- wall: 'machine-cards@4',
143
+ wall: 'code-project@1',
144
+ project: repoKey,
114
145
  machine: 'machine-b',
115
146
  publishedAt: '2026-07-19T00:00:00.000Z',
116
147
  cards: [{ card: 'repo-card@2', machine: 'machine-b', replica: 'rpl_peer', path: '/work/app', head: { branch: 'preview' } }],
117
148
  });
118
- // Land the peer's line the way fanout would: a doc edit from elsewhere.
119
- writeDocText(wallFile, `${getDocText(wallFile)}${peerLine}\n`);
149
+ writeDocText(project.file, `${getDocText(project.file)}${peerLine}\n`);
120
150
 
121
151
  const repo = path.join(tempRoot, 'projects/app');
122
152
  fs.writeFileSync(path.join(repo, 'another.txt'), 'y\n');
123
153
  await refreshRepoCards();
124
- const result = publishWall();
154
+ const result = publishWall(repoKey);
125
155
  assert.equal(result.published, true);
126
156
 
127
- const lines = getDocText(wallFile).trim().split('\n');
157
+ const lines = getDocText(project.file).trim().split('\n');
128
158
  assert.equal(lines.length, 2);
129
- assert.ok(lines.includes(peerLine), 'the peer line must survive untouched');
130
- const entries = readWall();
131
- assert.deepEqual(entries.map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
159
+ assert.ok(lines.includes(peerLine));
160
+ assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
132
161
  });
133
162
 
134
- test('a torn line is ignored by readers and healed by its owner, never fatal', () => {
135
- writeDocText(wallFile, `${getDocText(wallFile)}{"wall":"machine-cards@4","machine":"machine-c","car\n`);
136
- const entries = readWall();
137
- assert.deepEqual(entries.map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
163
+ test('a torn line is ignored by readers, never fatal', () => {
164
+ writeDocText(
165
+ project.file,
166
+ `${getDocText(project.file)}{"wall":"code-project@1","project":"${repoKey}","machine":"machine-c","car\n`,
167
+ );
168
+ assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
138
169
  });
139
170
 
140
- test('wall stats name the resource, the binding, and the publish counters', () => {
171
+ test('project stats name every project resource, binding, and publish counter', () => {
141
172
  const stats = wallStats();
142
- assert.equal(stats.resourceId, wallResourceId());
143
- assert.equal(stats.contract, 'machine-cards@4');
144
- assert.equal(stats.replica.appliedVersion >= 0, true);
145
- assert.equal(stats.machines, 2);
173
+ const row = stats.projects.find((entry) => entry.repoKey === repoKey);
174
+ assert.equal(stats.contract, 'code-project@1');
175
+ assert.equal(row.resourceId, project.resourceId);
176
+ assert.equal(row.replica.appliedVersion >= 0, true);
177
+ assert.equal(row.machines, 2);
146
178
  assert.ok(stats.publishes >= 3);
147
179
  assert.ok(stats.publishSkips >= 1);
180
+ assert.ok(stats.paths.includes(project.file));
148
181
  });