amalgm 0.1.214 → 0.1.216

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.214",
3
+ "version": "0.1.216",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -243,6 +243,37 @@ test('a clean losing commit travels as parked metadata on a no-op checkpoint', (
243
243
  assert.deepEqual(checkpoint.parked, parked);
244
244
  });
245
245
 
246
+ test('only this machine keeps announcing its parked commits', () => {
247
+ const baseOid = 'a'.repeat(40);
248
+ const localOid = 'b'.repeat(40);
249
+ const peerOid = 'c'.repeat(40);
250
+ const pendingOid = 'd'.repeat(40);
251
+ const existing = {
252
+ branch: 'main',
253
+ baseOid,
254
+ parked: [{ oid: localOid, objects: { kind: 'pending' } }],
255
+ };
256
+ const reduced = {
257
+ parked: [
258
+ { oid: localOid, version: 10, objects: { kind: 'pending' } },
259
+ { oid: peerOid, version: 11, objects: { kind: 'bundle' } },
260
+ { oid: pendingOid, objects: { kind: 'pending' } },
261
+ ],
262
+ };
263
+ const card = { head: { branch: 'main', oid: baseOid } };
264
+
265
+ assert.deepEqual(
266
+ _test.localParkedEntries(existing, reduced, card).map((entry) => entry.oid),
267
+ [localOid, pendingOid],
268
+ );
269
+ assert.deepEqual(
270
+ _test.localParkedEntries(existing, reduced, {
271
+ head: { branch: 'main', oid: 'e'.repeat(40) },
272
+ }).map((entry) => entry.oid),
273
+ [pendingOid],
274
+ );
275
+ });
276
+
246
277
  test('a server rail number does not turn the same parked checkpoint into new work', () => {
247
278
  const value = { revision: null, indexDiff: '', worktreeDiff: '' };
248
279
  const checkpoint = {
@@ -0,0 +1,270 @@
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
+
9
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-publication-test-'));
10
+ process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
11
+ process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
12
+ process.env.AMALGM_COMPUTER_ID = 'machine-a';
13
+ process.env.AMALGM_DEVICE_ID = 'device-a';
14
+ fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
15
+ fs.writeFileSync(
16
+ path.join(process.env.AMALGM_DIR, 'computer.json'),
17
+ JSON.stringify({
18
+ computer_id: 'machine-a',
19
+ user_id: '11111111-1111-4111-8111-111111111111',
20
+ }),
21
+ );
22
+
23
+ const { closeLocalDb } = require('../state/db');
24
+ const {
25
+ _test: checkpointTest,
26
+ CHECKPOINT_CONTRACT,
27
+ checkpointFile,
28
+ sameTransition,
29
+ } = require('../workspace/checkpoint');
30
+ const { parkedForState } = require('../workspace/reducer');
31
+ const { compactWallText, hasLocalContribution, stampCards } = require('../workspace/wall');
32
+
33
+ test.after(() => {
34
+ closeLocalDb();
35
+ fs.rmSync(tempRoot, { recursive: true, force: true });
36
+ });
37
+
38
+ const rules = {
39
+ card: {
40
+ identity: 'branch-and-commit-content',
41
+ ownership: 'originator-publishes',
42
+ liveness: 'shared-wall-outbox',
43
+ gating: 'defines-winning-card',
44
+ },
45
+ branch: {
46
+ identity: 'ref-name-and-commit-content',
47
+ ownership: 'originator-publishes',
48
+ liveness: 'shared-wall-outbox',
49
+ gating: 'per-ref-rail-order',
50
+ },
51
+ checkpoint: {
52
+ identity: 'from-and-to-content',
53
+ ownership: 'originator-publishes',
54
+ liveness: 'shared-wall-outbox',
55
+ gating: 'winning-card-only',
56
+ },
57
+ parked: {
58
+ identity: 'commit-and-object-content',
59
+ ownership: 'holder-publishes-until-acknowledged',
60
+ liveness: 'shared-wall-outbox',
61
+ gating: 'inside-winning-card-checkpoint',
62
+ },
63
+ compaction: {
64
+ identity: 'derived-deletion',
65
+ ownership: 'any-machine-may-compact',
66
+ liveness: 'not-semantic-state',
67
+ gating: 'commutative-with-writers',
68
+ },
69
+ };
70
+
71
+ test('the publication model has one explicit rule for every value and property', () => {
72
+ assert.deepEqual(Object.keys(rules), ['card', 'branch', 'checkpoint', 'parked', 'compaction']);
73
+ for (const rule of Object.values(rules)) {
74
+ assert.deepEqual(Object.keys(rule), ['identity', 'ownership', 'liveness', 'gating']);
75
+ assert.equal(Object.values(rule).every(Boolean), true);
76
+ }
77
+ assert.deepEqual(
78
+ new Set(['card', 'branch', 'checkpoint', 'parked'].map((name) => rules[name].liveness)),
79
+ new Set(['shared-wall-outbox']),
80
+ );
81
+ });
82
+
83
+ test('server rail numbers do not change checkpoint or parked identity', () => {
84
+ const oid = 'a'.repeat(40);
85
+ const transition = {
86
+ checkpoint: CHECKPOINT_CONTRACT,
87
+ machine: 'machine-a',
88
+ repoKey: 'root:test',
89
+ branch: 'main',
90
+ baseOid: oid,
91
+ from: { revision: null, indexDiff: '', worktreeDiff: '' },
92
+ to: { revision: 'work-a', indexDiff: '', worktreeDiff: 'patch-a' },
93
+ parked: [{
94
+ oid: 'b'.repeat(40),
95
+ ref: `refs/amalgm/parked/${'b'.repeat(40)}`,
96
+ objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
97
+ }],
98
+ };
99
+ assert.equal(sameTransition(transition, {
100
+ ...transition,
101
+ mutationId: 'mut_server_metadata',
102
+ version: 41,
103
+ parked: transition.parked.map((entry) => ({ ...entry, version: 41 })),
104
+ }), true);
105
+ assert.equal(sameTransition(transition, {
106
+ ...transition,
107
+ to: { ...transition.to, revision: 'different-work' },
108
+ }), false);
109
+ assert.equal(sameTransition(transition, {
110
+ ...transition,
111
+ parked: transition.parked.map((entry) => ({
112
+ ...entry,
113
+ objects: { ...entry.objects, checksum: 'd'.repeat(64) },
114
+ })),
115
+ }), false);
116
+ });
117
+
118
+ test('receivers stay silent and only unacknowledged local parked work republishes', () => {
119
+ const head = 'a'.repeat(40);
120
+ const local = {
121
+ head: { branch: 'main', oid: head, detached: false },
122
+ refs: [{ name: 'main', oid: head }],
123
+ };
124
+ const transition = {
125
+ checkpoint: CHECKPOINT_CONTRACT,
126
+ machine: 'machine-a',
127
+ repoKey: 'root:test',
128
+ branch: 'main',
129
+ baseOid: head,
130
+ from: { revision: null, indexDiff: '', worktreeDiff: '' },
131
+ to: { revision: 'shared-work', indexDiff: '', worktreeDiff: 'patch' },
132
+ parked: [],
133
+ };
134
+ const plan = {
135
+ card: { head: { branch: 'main', oid: head, detached: false } },
136
+ branches: [{ name: 'main', oid: head }],
137
+ transitions: [{ ...transition, version: 9 }],
138
+ };
139
+ const baseline = {
140
+ branch: 'main',
141
+ baseOid: head,
142
+ branches: [{ name: 'main', oid: head }],
143
+ };
144
+ assert.equal(hasLocalContribution(local, plan, baseline, transition), false);
145
+
146
+ const localParked = { oid: 'b'.repeat(40), objects: { kind: 'bundle', checksum: 'local' } };
147
+ const unsentParked = { oid: 'c'.repeat(40), objects: { kind: 'bundle', checksum: 'unsent' } };
148
+ const peerParked = {
149
+ oid: 'd'.repeat(40),
150
+ objects: { kind: 'bundle', checksum: 'peer' },
151
+ version: 9,
152
+ };
153
+ assert.deepEqual(
154
+ checkpointTest.localParkedEntries(
155
+ { ...transition, parked: [localParked] },
156
+ { parked: [peerParked, unsentParked] },
157
+ local,
158
+ ),
159
+ [localParked, unsentParked],
160
+ );
161
+ assert.deepEqual(parkedForState([localParked], [], []), [localParked]);
162
+ assert.deepEqual(
163
+ parkedForState([localParked], [{ ...localParked, version: 10 }], []),
164
+ [{ ...localParked, version: 10 }],
165
+ );
166
+ assert.deepEqual(parkedForState([{ ...localParked, version: 10 }], [], []), []);
167
+ });
168
+
169
+ test('card, branch, checkpoint, and parked work ride one wall mutation', () => {
170
+ const repoKey = 'https://github.com/amalgm-inc/publication-invariant-test';
171
+ const head = 'a'.repeat(40);
172
+ const parkedOid = 'b'.repeat(40);
173
+ const checkpoint = {
174
+ checkpoint: CHECKPOINT_CONTRACT,
175
+ machine: 'machine-a',
176
+ repoKey,
177
+ branch: 'main',
178
+ baseOid: head,
179
+ from: { revision: null, indexDiff: '', worktreeDiff: '' },
180
+ to: { revision: 'work-a', indexDiff: '', worktreeDiff: 'patch-a' },
181
+ parked: [{
182
+ oid: parkedOid,
183
+ ref: `refs/amalgm/parked/${parkedOid}`,
184
+ objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
185
+ }],
186
+ };
187
+ const file = checkpointFile('machine-a', repoKey);
188
+ fs.mkdirSync(path.dirname(file), { recursive: true });
189
+ fs.writeFileSync(file, `${JSON.stringify(checkpoint)}\n`);
190
+
191
+ const mutationId = 'mut_one_shared_wall_outbox';
192
+ const stamped = stampCards([{
193
+ card: 'repo-card@2',
194
+ machine: 'machine-a',
195
+ replica: 'rpl_publication_test',
196
+ kind: 'primary',
197
+ repo: { remote: repoKey },
198
+ head: { branch: 'main', oid: head, detached: false },
199
+ refs: [
200
+ { name: 'main', oid: head },
201
+ { name: 'feature', oid: head },
202
+ ],
203
+ health: 'healthy',
204
+ }], [], null, null, mutationId, {
205
+ repoKey,
206
+ resourceId: 'shr_publication_invariant_test',
207
+ })[0];
208
+
209
+ assert.equal(stamped.positionMutationId, mutationId);
210
+ assert.equal(stamped.branches.every((ref) => ref.mutationId === mutationId), true);
211
+ assert.equal(stamped.checkpoints[0].mutationId, mutationId);
212
+ assert.equal(stamped.checkpoints[0].parked[0].oid, parkedOid);
213
+ });
214
+
215
+ test('two concurrent compacting publishers converge without losing either latest value', () => {
216
+ const Y = require('yjs');
217
+ const repoKey = 'root:compaction-collision';
218
+ const line = (machine, value) => JSON.stringify({
219
+ wall: 'code-project@3',
220
+ project: repoKey,
221
+ machine,
222
+ mutationId: `mut_${machine}_${value}`,
223
+ cards: [{ value }],
224
+ });
225
+ const baseText = [
226
+ line('machine-a', 'old-a'),
227
+ line('machine-b', 'old-b'),
228
+ line('machine-a', 'current-a'),
229
+ line('machine-b', 'current-b'),
230
+ '',
231
+ ].join('\n');
232
+ const compacted = compactWallText(baseText, repoKey);
233
+ const removedBytes = baseText.length - compacted.length;
234
+ assert.ok(removedBytes > 0);
235
+
236
+ const base = new Y.Doc();
237
+ base.getText('content').insert(0, baseText);
238
+ const baseUpdate = Y.encodeStateAsUpdate(base);
239
+ const baseVector = Y.encodeStateVector(base);
240
+ const publisherUpdate = (machine, value) => {
241
+ const writer = new Y.Doc();
242
+ Y.applyUpdate(writer, baseUpdate);
243
+ const text = writer.getText('content');
244
+ text.delete(0, removedBytes);
245
+ text.insert(text.length, `${line(machine, value)}\n`);
246
+ return Y.encodeStateAsUpdate(writer, baseVector);
247
+ };
248
+ const updateA = publisherUpdate('machine-a', 'latest-a');
249
+ const updateB = publisherUpdate('machine-b', 'latest-b');
250
+ const replicas = [
251
+ [updateA, updateB],
252
+ [updateB, updateA],
253
+ ].map((order) => {
254
+ const replica = new Y.Doc();
255
+ Y.applyUpdate(replica, baseUpdate);
256
+ for (const update of order) Y.applyUpdate(replica, update);
257
+ return replica.getText('content').toString();
258
+ });
259
+
260
+ assert.equal(replicas[0], replicas[1]);
261
+ const latest = new Map();
262
+ for (const raw of replicas[0].trim().split('\n')) {
263
+ const entry = JSON.parse(raw);
264
+ latest.set(entry.machine, entry.cards[0].value);
265
+ }
266
+ assert.deepEqual(Object.fromEntries(latest), {
267
+ 'machine-a': 'latest-a',
268
+ 'machine-b': 'latest-b',
269
+ });
270
+ });
@@ -28,6 +28,7 @@ const {
28
28
  hydrateRepo,
29
29
  localCardMayMaterialize,
30
30
  materializeRepo,
31
+ parkedForState,
31
32
  pathForRepo,
32
33
  readRepoState,
33
34
  reduceCheckpoint,
@@ -175,6 +176,17 @@ test('a ready parked bundle never regresses to pending', () => {
175
176
  assert.equal(plan.parked[0].objects.kind, 'bundle');
176
177
  });
177
178
 
179
+ test('an unpublished local parked commit stays active until the rail records it', () => {
180
+ const oid = 'f'.repeat(40);
181
+ const local = { oid, objects: { kind: 'bundle', checksum: 'local' } };
182
+ assert.deepEqual(parkedForState([local], [], []), [local]);
183
+ assert.deepEqual(parkedForState([local], [{ ...local, version: 7 }], []), [{
184
+ ...local,
185
+ version: 7,
186
+ }]);
187
+ assert.deepEqual(parkedForState([{ ...local, version: 7 }], [], []), []);
188
+ });
189
+
178
190
  test('a stale lower rail card cannot roll back a newer branch and commit', () => {
179
191
  const newer = card('/newer', 'b'.repeat(40), 12);
180
192
  newer.head.branch = 'feature';
@@ -508,6 +520,17 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
508
520
  assert.equal((await git(repo, 'status', '--porcelain')).trim().length > 0, true);
509
521
  assert.deepEqual(readRepoState('root:test').parked.map((entry) => entry.oid), [losingOid]);
510
522
 
523
+ const sharedParked = readRepoState('root:test').parked.map((entry) => ({
524
+ ...entry,
525
+ version: 2,
526
+ }));
527
+ await materializeRepo(card(repo, winningOid, 2), {
528
+ repoKey: 'root:test',
529
+ card: winningCard,
530
+ transitions: [],
531
+ parked: sharedParked,
532
+ });
533
+
511
534
  await git(repo, 'add', '.');
512
535
  await git(repo, 'commit', '-m', 'fold parked work');
513
536
  const foldedOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
@@ -521,7 +544,9 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
521
544
 
522
545
  assert.equal(folded.state, 'in-sync');
523
546
  assert.deepEqual(readRepoState('root:test').parked, []);
524
- assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingOid}`)).trim(), losingOid);
547
+ await assert.rejects(
548
+ git(repo, 'rev-parse', '--verify', `refs/amalgm/parked/${losingOid}`),
549
+ );
525
550
  });
526
551
 
527
552
  test('rail-parked peer commits become one staged checkpoint on every machine', async () => {
@@ -157,6 +157,17 @@ function checkpointForSharedState(existing, {
157
157
  };
158
158
  }
159
159
 
160
+ function localParkedEntries(existing, reduced, card) {
161
+ const held = existing?.baseOid === card.head?.oid
162
+ && existing.branch === card.head?.branch
163
+ ? existing.parked || []
164
+ : [];
165
+ const unpublished = (reduced?.parked || []).filter((entry) => (
166
+ !Number.isInteger(entry.version) || entry.version < 1
167
+ ));
168
+ return [...new Map([...held, ...unpublished].map((entry) => [entry.oid, entry])).values()];
169
+ }
170
+
160
171
  function readCheckpointValue(baseOid, value) {
161
172
  let indexDiff = typeof value?.indexDiff === 'string' ? value.indexDiff : '';
162
173
  let worktreeDiff = typeof value?.worktreeDiff === 'string' ? value.worktreeDiff : '';
@@ -228,7 +239,7 @@ async function publishPass(replicaId = null) {
228
239
  const localRevision = revisionOf(card.head.oid, captured.indexDiff, captured.worktreeDiff);
229
240
  const file = checkpointFile(machine, repoKey);
230
241
  const existing = readCheckpoint(machine, repoKey);
231
- const parked = await Promise.all((reduced.parked || []).map(async (entry) => ({
242
+ const parked = await Promise.all(localParkedEntries(existing, reduced, card).map(async (entry) => ({
232
243
  ...entry,
233
244
  objects: await require('./objects').bundleSourceFor(card.path, repoKey, entry.oid),
234
245
  })));
@@ -393,6 +404,7 @@ module.exports = {
393
404
  checkpointBaseReady,
394
405
  checkpointForSharedState,
395
406
  checkpointMayPublish,
407
+ localParkedEntries,
396
408
  },
397
409
  CHECKPOINT_CONTRACT,
398
410
  MAX_INLINE_DIFF_BYTES,
@@ -176,6 +176,18 @@ function uniqueParked(entries) {
176
176
  return order.map((oid) => byOid.get(oid));
177
177
  }
178
178
 
179
+ function parkedForState(previous, shared, discovered) {
180
+ const sharedOids = new Set((shared || []).map((entry) => entry.oid));
181
+ const unpublished = (previous || []).filter((entry) => (
182
+ !validVersion(entry.version) && !sharedOids.has(entry.oid)
183
+ ));
184
+ return uniqueParked([
185
+ ...(shared || []),
186
+ ...unpublished,
187
+ ...(discovered || []),
188
+ ]);
189
+ }
190
+
179
191
  async function obtainCardObjects(localCard, targetCard) {
180
192
  const oid = targetCard.head?.oid;
181
193
  if (!oid || await hasCommit(localCard.path, oid)) return { state: 'ready' };
@@ -348,7 +360,8 @@ async function moveToWinningCard(localCard, targetCard) {
348
360
 
349
361
  async function materializeParked(localCard, entries) {
350
362
  const waiting = [];
351
- for (const entry of uniqueParked(entries)) {
363
+ const active = uniqueParked(entries);
364
+ for (const entry of active) {
352
365
  if (!(await hasCommit(localCard.path, entry.oid))) {
353
366
  if (entry.objects?.kind === 'bundle') {
354
367
  const imported = await require('./objects').importFromLocator(localCard.path, entry.objects);
@@ -363,6 +376,10 @@ async function materializeParked(localCard, entries) {
363
376
  }
364
377
  await transition.parkCommit(localCard.path, entry.oid);
365
378
  }
379
+ await transition.pruneParkedCommits(
380
+ localCard.path,
381
+ new Set(active.map((entry) => entry.oid)),
382
+ );
366
383
  return waiting;
367
384
  }
368
385
 
@@ -512,10 +529,11 @@ async function materializeRepo(localCardInput, plan) {
512
529
  ? await refreshRepoCard(refreshed.replica) || refreshed
513
530
  : refreshed;
514
531
  const previous = readRepoState(plan.repoKey);
515
- const parked = uniqueParked([
516
- ...(plan.parked || []),
517
- ...(cardResult.parked || []),
518
- ]);
532
+ const parked = parkedForState(
533
+ previous?.parked,
534
+ plan.parked,
535
+ cardResult.parked,
536
+ );
519
537
  const parkedWaiting = await materializeParked(localCard, parked);
520
538
  const planParked = new Set((plan.parked || []).map((entry) => entry.oid));
521
539
  const requiredParkedWaiting = parkedWaiting.filter((entry) => planParked.has(entry.oid));
@@ -784,6 +802,7 @@ module.exports = {
784
802
  localCardMayMaterialize,
785
803
  materializeRepo,
786
804
  pathForRepo,
805
+ parkedForState,
787
806
  readRepoState,
788
807
  reduceCheckpoint,
789
808
  reduceNow,
@@ -236,6 +236,20 @@ function parkCommit(worktreeRoot, oid) {
236
236
  ]).then(() => ref);
237
237
  }
238
238
 
239
+ /** Delete internal parked refs that are no longer part of shared checkpoint state. */
240
+ async function pruneParkedCommits(worktreeRoot, keepOids) {
241
+ const output = await run('list-parked-commits', worktreeRoot, [
242
+ '-C', worktreeRoot, 'for-each-ref', '--format=%(refname)', 'refs/amalgm/parked',
243
+ ], { journalResult: false });
244
+ const prefix = 'refs/amalgm/parked/';
245
+ for (const ref of output.trim().split('\n').filter(Boolean)) {
246
+ if (!ref.startsWith(prefix) || keepOids.has(ref.slice(prefix.length))) continue;
247
+ await run('remove-parked-commit', worktreeRoot, [
248
+ '-C', worktreeRoot, 'update-ref', '-d', ref,
249
+ ]);
250
+ }
251
+ }
252
+
239
253
  function setBranchRef(worktreeRoot, branch, oid) {
240
254
  return run('set-branch-ref', worktreeRoot, [
241
255
  '-C', worktreeRoot, 'update-ref', `refs/heads/${branch}`, oid,
@@ -380,6 +394,7 @@ module.exports = {
380
394
  initializeRepo,
381
395
  moveToCard,
382
396
  parkCommit,
397
+ pruneParkedCommits,
383
398
  restoreInitialFiles,
384
399
  setBranchRef,
385
400
  setIndexTree,