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
@@ -7,9 +7,10 @@
7
7
  * - one winning card (highest official card version), and
8
8
  * - one checkpoint tree (checkpoint transitions applied in official order).
9
9
  *
10
- * The materializer has no peer branches. It obtains the winning commit,
11
- * moves to that exact card when safe, advances the one held checkpoint tree
12
- * to the newly reduced tree, and verifies on the next pass.
10
+ * The materializer advances one official event at a time. Before touching the
11
+ * checkout it durably records the observed source, the official state, and the
12
+ * target with any proven local intent preserved. Restart completes that one
13
+ * transition before a newer event may begin.
13
14
  */
14
15
 
15
16
  const crypto = require('crypto');
@@ -22,25 +23,27 @@ const {
22
23
  captureCheckpoint,
23
24
  hasCommit,
24
25
  headAndDirty,
25
- isAncestor,
26
26
  localBranches,
27
- mergeBase,
28
- operationState,
29
27
  } = require('./git');
30
28
  const {
31
29
  CHECKPOINT_CONTRACT,
30
+ captureIntentAgainst,
31
+ captureIntentForCard,
32
32
  checkpointValueReady,
33
- readCheckpoint,
34
33
  readCheckpointValue,
35
34
  repoKeyOfCard,
36
35
  revisionOf,
37
36
  } = require('./checkpoint');
38
- const { machineId } = require('./identity');
39
37
  const merge = require('./merge');
38
+ const provenance = require('./provenance');
40
39
  const transition = require('./transition');
41
40
 
42
41
  const STATE_DIR = path.join(AMALGM_DIR, 'state', 'repo-reducer');
42
+ const VERIFICATION_CONTRACT = 'verified-repo-state@1';
43
+ const APPLYING_CONTRACT = 'repo-apply@4';
44
+ const HYDRATING_CONTRACT = 'repo-hydrate@5';
43
45
  const REDUCE_QUIET_MS = 25;
46
+ const REPAIR_RETRY_MS = 1_000;
44
47
  const REDUCE_INTERVAL_MS = 30_000;
45
48
 
46
49
  const service = {
@@ -52,21 +55,34 @@ const service = {
52
55
  repos: new Map(),
53
56
  lastPass: null,
54
57
  };
55
- const materializing = new Set();
58
+ const repoTurnTails = new Map();
59
+ const activeRepoTurns = new Map();
56
60
 
57
61
  function isMaterializing(repoKey) {
58
- return materializing.has(repoKey);
62
+ return activeRepoTurns.get(repoKey) === 'materialize';
59
63
  }
60
64
 
61
- async function withMaterialization(repoKey, work) {
62
- materializing.add(repoKey);
65
+ async function withRepoTurn(repoKey, kind, work) {
66
+ const previous = repoTurnTails.get(repoKey) || Promise.resolve();
67
+ let release;
68
+ const gate = new Promise((resolve) => { release = resolve; });
69
+ const tail = previous.then(() => gate);
70
+ repoTurnTails.set(repoKey, tail);
71
+ await previous;
72
+ activeRepoTurns.set(repoKey, kind);
63
73
  try {
64
74
  return await work();
65
75
  } finally {
66
- materializing.delete(repoKey);
76
+ activeRepoTurns.delete(repoKey);
77
+ release();
78
+ if (repoTurnTails.get(repoKey) === tail) repoTurnTails.delete(repoKey);
67
79
  }
68
80
  }
69
81
 
82
+ function withMaterialization(repoKey, work) {
83
+ return withRepoTurn(repoKey, 'materialize', work);
84
+ }
85
+
70
86
  function stateFile(repoKey) {
71
87
  const digest = crypto.createHash('sha256').update(repoKey).digest('hex').slice(0, 24);
72
88
  return path.join(STATE_DIR, `${digest}.json`);
@@ -96,7 +112,25 @@ function persistRepoState(repoKey, state) {
96
112
  if (repoStateIdentity(previous) === repoStateIdentity(next)) {
97
113
  return { state: previous, changed: false };
98
114
  }
99
- fs.writeFileSync(stateFile(repoKey), `${JSON.stringify(next, null, 2)}\n`);
115
+ const file = stateFile(repoKey);
116
+ const temporary = `${file}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`;
117
+ let descriptor = fs.openSync(temporary, 'wx', 0o600);
118
+ try {
119
+ fs.writeFileSync(descriptor, `${JSON.stringify(next, null, 2)}\n`);
120
+ fs.fsyncSync(descriptor);
121
+ fs.closeSync(descriptor);
122
+ descriptor = null;
123
+ fs.renameSync(temporary, file);
124
+ const directory = fs.openSync(STATE_DIR, 'r');
125
+ try {
126
+ fs.fsyncSync(directory);
127
+ } finally {
128
+ fs.closeSync(directory);
129
+ }
130
+ } finally {
131
+ if (descriptor !== null) fs.closeSync(descriptor);
132
+ if (fs.existsSync(temporary)) fs.rmSync(temporary, { force: true });
133
+ }
100
134
  return { state: next, changed: true };
101
135
  }
102
136
 
@@ -105,6 +139,37 @@ function repoStateEventValue(state) {
105
139
  return value;
106
140
  }
107
141
 
142
+ function saveRepoState(repoKey, localCard, previous, state) {
143
+ const persisted = persistRepoState(repoKey, state);
144
+ if (persisted.changed) {
145
+ appendStateEvent({
146
+ resource: 'workspace-reducer',
147
+ op: previous ? 'update' : 'insert',
148
+ id: localCard.replica,
149
+ value: repoStateEventValue(persisted.state),
150
+ source: 'workspace-reducer',
151
+ });
152
+ }
153
+ return persisted.state;
154
+ }
155
+
156
+ async function releaseVerifiedReceipts(repoKey, state) {
157
+ const operationId = state?.receiptOperationId;
158
+ if (!operationId) return state;
159
+ if (
160
+ state.materialized !== true
161
+ || typeof state.path !== 'string'
162
+ || !/^[0-9a-f]{64}$/.test(operationId)
163
+ ) {
164
+ throw new Error('invalid verified receipt release');
165
+ }
166
+ transition.releaseFileReceipts(state.path, operationId);
167
+ transition.releaseGitMetadataReceipts(state.path, operationId);
168
+ await transition.releaseBranchReceipt(state.path, operationId);
169
+ const { receiptOperationId, ...released } = state;
170
+ return persistRepoState(repoKey, released).state;
171
+ }
172
+
108
173
  function validVersion(value) {
109
174
  return Number.isInteger(value) && value > 0;
110
175
  }
@@ -135,15 +200,13 @@ function reduceWall(entries) {
135
200
  repo = {
136
201
  repoKey,
137
202
  card: null,
138
- checkpointBases: [],
139
203
  transitions: [],
140
- parked: [],
141
204
  branchMap: new Map(),
142
205
  };
143
206
  repos.set(repoKey, repo);
144
207
  }
145
208
  if (!repo.card || card.positionVersion > repo.card.positionVersion) {
146
- repo.card = { ...card, writer: wallEntry.machine };
209
+ repo.card = card;
147
210
  } else if (
148
211
  card.positionVersion === repo.card.positionVersion
149
212
  && card.positionMutationId === repo.card.positionMutationId
@@ -168,53 +231,19 @@ function reduceWall(entries) {
168
231
  if (
169
232
  checkpoint.checkpoint === CHECKPOINT_CONTRACT
170
233
  && validVersion(checkpoint.version)
171
- && checkpoint.machine === wallEntry.machine
172
234
  ) {
173
235
  repo.transitions.push(checkpoint);
174
236
  }
175
237
  }
176
- const checkpointBase = card.checkpointBase;
177
- if (
178
- checkpointBase?.checkpoint === CHECKPOINT_CONTRACT
179
- && Number.isInteger(checkpointBase.through)
180
- && checkpointBase.through >= 0
181
- && checkpointBase.baseOid === card.head?.oid
182
- && checkpointBase.branch === card.head?.branch
183
- ) {
184
- repo.checkpointBases.push(checkpointBase);
185
- }
186
238
  }
187
239
  }
188
240
  for (const repo of repos.values()) {
189
- const matchingBases = repo.checkpointBases
190
- .filter((entry) => (
191
- entry.baseOid === repo.card.head?.oid
192
- && entry.branch === repo.card.head?.branch
193
- && checkpointValueReady(entry.value)
194
- ))
195
- .sort((left, right) => right.through - left.through);
196
- const newestThrough = matchingBases[0]?.through ?? null;
197
- const newestBases = matchingBases
198
- .filter((entry) => entry.through === newestThrough)
199
- .sort((left, right) => (
200
- Number(checkpointValueReady(right.value)) - Number(checkpointValueReady(left.value))
201
- ));
202
- const baseRevisions = new Set(newestBases.map((entry) => entry.value?.revision ?? null));
203
- repo.checkpointBaseConflict = baseRevisions.size > 1;
204
- repo.checkpointBase = repo.checkpointBaseConflict ? null : newestBases[0] || null;
205
241
  repo.transitions = repo.transitions
206
242
  .filter((entry) => (
207
243
  entry.baseOid === repo.card.head?.oid
208
244
  && entry.branch === repo.card.head?.branch
209
- && (!repo.checkpointBase || entry.version > repo.checkpointBase.through)
210
245
  ))
211
246
  .sort((left, right) => left.version - right.version);
212
- repo.parked = uniqueParked(repo.transitions.flatMap((checkpoint) => (
213
- (checkpoint.parked || []).map((entry) => ({
214
- ...entry,
215
- version: checkpoint.version,
216
- }))
217
- )));
218
247
  if (repo.card.head?.branch) {
219
248
  const held = repo.branchMap.get(repo.card.head.branch);
220
249
  if (!held || repo.card.positionVersion > held.version) {
@@ -229,581 +258,1926 @@ function reduceWall(entries) {
229
258
  }
230
259
  repo.branches = [...repo.branchMap.values()].sort((left, right) => left.name.localeCompare(right.name));
231
260
  delete repo.branchMap;
232
- delete repo.checkpointBases;
233
261
  }
234
262
  return repos;
235
263
  }
236
264
 
237
- function uniqueParked(entries) {
238
- const byOid = new Map();
239
- const order = [];
240
- for (const entry of entries || []) {
241
- if (typeof entry?.oid === 'string' && /^[0-9a-f]{40,64}$/.test(entry.oid)) {
242
- const held = byOid.get(entry.oid);
243
- if (!held) order.push(entry.oid);
244
- if (!held || held.objects?.kind !== 'bundle' || entry.objects?.kind === 'bundle') {
245
- byOid.set(entry.oid, entry);
246
- }
247
- }
248
- }
249
- return order.map((oid) => byOid.get(oid));
250
- }
251
-
252
- function parkedForState(previous, shared, discovered) {
253
- const sharedOids = new Set((shared || []).map((entry) => entry.oid));
254
- const unpublished = (previous || []).filter((entry) => (
255
- !validVersion(entry.version) && !sharedOids.has(entry.oid)
256
- ));
257
- return uniqueParked([
258
- ...(shared || []),
259
- ...unpublished,
260
- ...(discovered || []),
261
- ]);
262
- }
263
-
264
265
  async function obtainCardObjects(localCard, targetCard) {
265
266
  const oid = targetCard.head?.oid;
266
267
  if (!oid || await hasCommit(localCard.path, oid)) return { state: 'ready' };
267
- if (targetCard.objects?.kind === 'bundle') {
268
- const imported = await require('./objects').importFromLocator(localCard.path, targetCard.objects);
269
- if (imported.state !== 'imported') return { state: `objects-${imported.state}` };
270
- } else if (targetCard.objects?.kind === 'pending') {
271
- return { state: 'objects-uploading' };
272
- } else {
273
- const approved = localCard.repo?.remote
274
- && localCard.repo.remote === targetCard.repo?.remote
275
- && targetCard.head?.branch;
276
- if (!approved) return { state: 'missing-objects-no-approved-source' };
277
- await transition.fetchBranch(localCard.path, 'origin', targetCard.head.branch);
278
- }
279
- return await hasCommit(localCard.path, oid)
280
- ? { state: 'ready' }
281
- : { state: 'missing-objects-after-fetch' };
282
- }
283
-
284
- async function hydrateRepo(project, plan) {
285
- const targetCard = plan.card;
286
- if (!project?.path) return { state: 'waiting', reason: 'local-project-path-missing' };
287
- if (!targetCard?.head?.oid) return { state: 'waiting', reason: 'winning-card-unborn' };
288
268
  if (targetCard.objects?.kind !== 'bundle') {
289
- return { state: 'waiting', reason: targetCard.objects?.kind === 'pending'
290
- ? 'objects-uploading'
291
- : 'winning-card-bundle-missing' };
269
+ throw new Error('official card has no complete bundle');
292
270
  }
293
- const prepared = require('./objects').prepareBundleImport(targetCard.objects);
294
- if (prepared.state !== 'ready') {
295
- return { state: 'waiting', reason: `objects-${prepared.state}` };
296
- }
297
- await transition.initializeRepo(project.path, targetCard.repo?.remote || null);
298
- await transition.importBundle(project.path, prepared.file);
299
- const holding = transition.holdInitialFiles(project.path);
300
- try {
301
- await transition.checkoutInitialCard(project.path, {
302
- branch: targetCard.head.branch,
303
- oid: targetCard.head.oid,
304
- });
305
- } catch (error) {
306
- transition.restoreInitialFiles(project.path, holding);
307
- throw error;
271
+ const imported = await require('./objects').importFromLocator(localCard.path, targetCard.objects);
272
+ if (imported.state !== 'imported') return { state: `objects-${imported.state}` };
273
+ if (!await hasCommit(localCard.path, oid)) {
274
+ throw new Error(`official card bundle does not contain ${oid}`);
308
275
  }
309
- transition.restoreInitialFiles(project.path, holding);
310
- await refreshRepoCards();
311
- return { state: 'hydrated', moved: true };
276
+ return { state: 'ready' };
312
277
  }
313
278
 
314
- async function moveToWinningCard(localCard, targetCard) {
315
- const current = await headAndDirty(localCard.path);
316
- const operation = await operationState(localCard.path);
317
- if (operation !== 'none') return { state: 'waiting', reason: 'local-operation-in-flight' };
318
- if (
319
- current.head.oid !== localCard.head?.oid
320
- || current.head.branch !== localCard.head?.branch
321
- ) {
322
- return { state: 'waiting', reason: 'local-card-changed-after-verification' };
323
- }
324
- if (
325
- current.head.oid === targetCard.head?.oid
326
- && current.head.branch === targetCard.head?.branch
327
- ) return { state: 'ready', moved: false };
328
-
329
- const pendingCard = current.head.oid
330
- && targetCard.head?.oid
331
- && current.head.branch === targetCard.head?.branch
332
- && await isAncestor(localCard.path, targetCard.head.oid, current.head.oid);
333
- if (pendingCard) return { state: 'ready', moved: false, pendingCard: true };
334
-
335
- const objects = await obtainCardObjects(localCard, targetCard);
336
- if (objects.state !== 'ready') return { state: 'waiting', reason: objects.state };
337
-
338
- const forward = current.head.oid
339
- && targetCard.head?.oid
340
- && await isAncestor(localCard.path, current.head.oid, targetCard.head.oid);
341
- if (!current.head.oid || !targetCard.head?.oid) {
342
- return { state: 'waiting', reason: 'unborn-card-movement' };
343
- }
344
- const commonOid = forward
345
- ? current.head.oid
346
- : await mergeBase(localCard.path, current.head.oid, targetCard.head.oid);
347
- if (!commonOid) return { state: 'waiting', reason: 'unrelated-card-histories' };
348
-
349
- const captured = await captureCheckpoint(localCard.path);
350
- if (captured.indexDiff === null || captured.baseOid !== current.head.oid) {
351
- return { state: 'waiting', reason: 'local-state-moving' };
352
- }
353
- const repoKey = repoKeyOfCard(localCard);
354
- const resourceId = require('./wall').projectForRepo(repoKey)?.resourceId;
355
- const reducedState = readRepoState(repoKey, resourceId);
356
- const capturedOnRail = reducedState?.baseOid === current.head.oid
357
- && reducedState.checkpointRevision === revisionOf(
358
- current.head.oid,
359
- captured.indexDiff,
360
- captured.worktreeDiff,
361
- );
362
- const movingIndexDiff = capturedOnRail ? '' : captured.indexDiff;
363
- const movingWorktreeDiff = capturedOnRail ? '' : captured.worktreeDiff;
364
- const parked = [];
365
- if (!forward && current.head.oid !== targetCard.head.oid) {
366
- const parkedRef = await transition.parkCommit(localCard.path, current.head.oid);
367
- parked.push({
368
- oid: current.head.oid,
369
- ref: parkedRef,
370
- sourceBranch: current.head.branch,
371
- objects: await require('./objects').bundleSourceFor(
372
- localCard.path,
373
- repoKey,
374
- current.head.oid,
375
- ),
376
- });
377
- }
279
+ function planThrough(plan) {
280
+ return Math.max(
281
+ Number(plan?.card?.positionVersion) || 0,
282
+ ...(plan?.branches || []).map((branch) => Number(branch.version) || 0),
283
+ ...(plan?.transitions || []).map((operation) => Number(operation.version) || 0),
284
+ );
285
+ }
378
286
 
379
- const winningTree = await merge.treeOf(localCard.path, targetCard.head.oid);
380
- const oldTree = await merge.treeOf(localCard.path, current.head.oid);
381
- const oldIndexTree = await merge.treeOfPatch(localCard.path, current.head.oid, movingIndexDiff);
382
- const staged = await merge.reduceTree(
383
- localCard.path,
384
- winningTree,
385
- oldTree,
386
- oldIndexTree,
287
+ function validHydrating(value, repoKey, projectPath) {
288
+ return Boolean(
289
+ value
290
+ && value.contract === HYDRATING_CONTRACT
291
+ && value.repoKey === repoKey
292
+ && value.path === projectPath
293
+ && value.plan?.repoKey === repoKey
294
+ && validTree(value.plan.card?.head?.oid)
295
+ && Number.isInteger(value.through)
296
+ && value.through > 0
297
+ && validTree(value.emptyTree)
298
+ && value.reduced?.state === 'ready'
299
+ && validTree(value.reduced.indexTree)
300
+ && validTree(value.reduced.worktreeTree)
301
+ && value.requestDigest === applyingRequestDigest(value.through, value.plan)
302
+ && value.operationId === hydratingId(value.requestDigest)
303
+ && (value.bootstrapSealed === undefined || value.bootstrapSealed === true)
304
+ && (value.bootstrapReleased === undefined || value.bootstrapReleased === true)
305
+ && value.bootstrap
306
+ && validBootstrapMetadata(value.bootstrap.source, true)
307
+ && validBootstrapMetadata(value.bootstrap.target, false)
308
+ && (
309
+ value.execution === undefined
310
+ || (
311
+ validPhysicalState(value.execution.source)
312
+ && validPhysicalState(value.execution.target)
313
+ && validPhysicalState(value.execution.official)
314
+ && value.execution.operationId === value.operationId
315
+ && validSourceReceipts(value.execution.sourceReceipts)
316
+ )
317
+ )
387
318
  );
388
- const oldWorktree = await merge.treeOfPatch(localCard.path, oldIndexTree, movingWorktreeDiff);
389
- const working = await merge.reduceTree(
390
- localCard.path,
391
- staged.tree,
392
- oldIndexTree,
393
- oldWorktree,
394
- );
395
-
396
- const rollbackDir = path.join(STATE_DIR, 'card-moves');
397
- fs.mkdirSync(rollbackDir, { recursive: true });
398
- fs.writeFileSync(
399
- path.join(rollbackDir, `${current.head.oid}-to-${targetCard.head.oid}.json`),
400
- `${JSON.stringify({
401
- baseOid: current.head.oid,
402
- indexDiff: captured.indexDiff,
403
- worktreeDiff: captured.worktreeDiff,
404
- })}\n`,
405
- );
406
- await transition.forceMoveToCard(localCard.path, {
407
- expectedBranch: current.head.branch,
408
- expectedOid: current.head.oid,
409
- targetBranch: targetCard.head.branch,
410
- targetOid: targetCard.head.oid,
411
- });
412
- const applied = await merge.advance(localCard.path, winningTree, working.tree, {
413
- label: `card-${targetCard.positionVersion}`,
414
- });
415
- await transition.setIndexTree(localCard.path, staged.tree);
319
+ }
320
+
321
+ function hydrationBootstrapSealed(hydrating) {
322
+ return Boolean(
323
+ hydrating?.bootstrapSealed === true
324
+ || hydrating?.bootstrapReleased === true
325
+ || hydrating?.execution,
326
+ );
327
+ }
328
+
329
+ function validBootstrapMetadata(value, allowUnborn) {
330
+ return Boolean(
331
+ value
332
+ && value.head
333
+ && (typeof value.head.branch === 'string' || value.head.branch === null)
334
+ && (validTree(value.head.oid) || (allowUnborn && value.head.oid === null))
335
+ && Array.isArray(value.branches)
336
+ && value.branches.every((branch) => (
337
+ typeof branch?.name === 'string'
338
+ && branch.name.length > 0
339
+ && validTree(branch.oid)
340
+ )),
341
+ );
342
+ }
343
+
344
+ function bootstrapBranchNames(bootstrap) {
345
+ const source = new Map(branchState(bootstrap.source.branches).map((ref) => [ref.name, ref.oid]));
346
+ const target = new Map(branchState(bootstrap.target.branches).map((ref) => [ref.name, ref.oid]));
347
+ return [...new Set([...source.keys(), ...target.keys()])]
348
+ .filter((name) => !sameOid(source.get(name), target.get(name)))
349
+ .sort();
350
+ }
351
+
352
+ async function hydrationBootstrapCrossed(worktreeRoot, hydrating) {
353
+ const names = bootstrapBranchNames(hydrating.bootstrap);
354
+ const crossed = await Promise.all(names.map(async (name) => (
355
+ await transition.branchReceiptCrossed(worktreeRoot, hydrating.operationId, name)
356
+ ? name
357
+ : null
358
+ )));
416
359
  return {
417
- state: 'ready',
418
- moved: true,
419
- parked,
360
+ branches: new Set(crossed.filter(Boolean)),
361
+ head: transition.gitMetadataReceiptCrossed(
362
+ worktreeRoot,
363
+ hydrating.operationId,
364
+ 'HEAD',
365
+ ),
420
366
  };
421
367
  }
422
368
 
423
- async function materializeParked(localCard, entries) {
424
- const waiting = [];
425
- const active = uniqueParked(entries);
426
- for (const entry of active) {
427
- if (!(await hasCommit(localCard.path, entry.oid))) {
428
- if (entry.objects?.kind === 'bundle') {
429
- const imported = await require('./objects').importFromLocator(localCard.path, entry.objects);
430
- if (imported.state !== 'imported') {
431
- waiting.push({ ...entry, state: imported.state });
432
- continue;
433
- }
434
- } else {
435
- waiting.push({ ...entry, state: entry.objects?.kind || 'objects-missing' });
436
- continue;
437
- }
438
- }
439
- await transition.parkCommit(localCard.path, entry.oid);
440
- }
441
- await transition.pruneParkedCommits(
442
- localCard.path,
443
- new Set(active.map((entry) => entry.oid)),
369
+ async function applyHydrationBootstrap(worktreeRoot, hydrating) {
370
+ transition.recoverGitMetadataTransactions(worktreeRoot);
371
+ const currentHead = (await headAndDirty(worktreeRoot)).head;
372
+ const currentBranches = await localBranches(worktreeRoot);
373
+ const crossed = await hydrationBootstrapCrossed(worktreeRoot, hydrating);
374
+ const branches = reconcileApplyingBranches(
375
+ currentBranches,
376
+ hydrating.bootstrap.source.branches,
377
+ hydrating.bootstrap.target.branches,
378
+ crossed.branches,
379
+ receiptProvenHeadReturn(currentHead, hydrating.bootstrap.source.head, crossed.head),
380
+ );
381
+ const initialRefPlan = branchRefApplyPlan(
382
+ currentHead,
383
+ currentBranches,
384
+ branches,
385
+ );
386
+ const initialRefs = await advanceBranchRefChanges(
387
+ worktreeRoot,
388
+ initialRefPlan.beforeHead,
389
+ hydrating.operationId,
390
+ );
391
+ if (initialRefs.state !== 'ready') return initialRefs;
392
+ const afterRefs = (await headAndDirty(worktreeRoot)).head;
393
+ const afterBranches = await localBranches(worktreeRoot);
394
+ const refreshedCrossed = await hydrationBootstrapCrossed(
395
+ worktreeRoot,
396
+ hydrating,
397
+ );
398
+ const resolvedBranches = reconcileApplyingBranches(
399
+ afterBranches,
400
+ hydrating.bootstrap.source.branches,
401
+ hydrating.bootstrap.target.branches,
402
+ refreshedCrossed.branches,
403
+ receiptProvenHeadReturn(
404
+ afterRefs,
405
+ hydrating.bootstrap.source.head,
406
+ refreshedCrossed.head,
407
+ ),
408
+ );
409
+ const head = reconcileApplyingHead(
410
+ afterRefs,
411
+ hydrating.bootstrap.source.head,
412
+ hydrating.bootstrap.target.head,
413
+ resolvedBranches,
414
+ {
415
+ headCrossed: refreshedCrossed.head,
416
+ branchCrossed: refreshedCrossed.branches,
417
+ plannedPostRefBranches: hydrating.bootstrap.target.branches,
418
+ },
419
+ );
420
+ return advanceBranchesAndHead(
421
+ worktreeRoot,
422
+ afterRefs,
423
+ afterBranches,
424
+ resolvedBranches,
425
+ head,
426
+ hydrating.operationId,
444
427
  );
445
- return waiting;
446
428
  }
447
429
 
448
- async function materializeBranches(localCard, plan, heldBranches = []) {
449
- const current = new Map((await localBranches(localCard.path)).map((ref) => [ref.name, ref.oid]));
450
- const held = new Map(heldBranches.map((ref) => [ref.name, ref.oid]));
451
- const waiting = [];
452
- const pending = [];
453
- let moved = false;
454
- for (const ref of plan.branches || []) {
455
- if (ref.name === plan.card.head?.branch) {
456
- if (ref.oid !== plan.card.head.oid) {
457
- waiting.push({ name: ref.name, state: 'checked-out-ref-disagrees-with-card' });
458
- }
459
- continue;
460
- }
461
- const currentOid = current.get(ref.name) || null;
462
- const heldOid = held.get(ref.name) || null;
463
- if (currentOid !== heldOid && currentOid !== ref.oid) {
464
- pending.push({ name: ref.name, oid: currentOid });
465
- continue;
430
+ async function hydrateRepo(project, plan) {
431
+ if (!project?.path) throw new Error('code project has no registered local path');
432
+ let stored = readRepoState(plan.repoKey, project.resourceId);
433
+ if (stored?.receiptOperationId) {
434
+ stored = await releaseVerifiedReceipts(plan.repoKey, stored);
435
+ }
436
+ const pending = validHydrating(stored?.hydrating, plan.repoKey, project.path)
437
+ ? stored.hydrating
438
+ : null;
439
+ const activePlan = pending?.plan || recoveryPlan(plan);
440
+ const targetCard = activePlan.card;
441
+ if (!targetCard?.head?.oid) throw new Error('official card has no commit');
442
+ await transition.initializeRepo(project.path, plan.card.repo?.remote || null);
443
+ let reduced = pending?.reduced || null;
444
+ let through = pending?.through || null;
445
+ if (!pending) {
446
+ if (plan.card.objects?.kind !== 'bundle') {
447
+ throw new Error('official card has no complete bundle');
466
448
  }
467
- if (currentOid === ref.oid) continue;
468
- if (ref.oid === null) {
469
- if (current.has(ref.name)) {
470
- await transition.deleteBranchRef(localCard.path, ref.name);
471
- moved = true;
472
- }
473
- continue;
449
+ const bundle = require('./objects').prepareBundleImport(plan.card.objects);
450
+ if (bundle.state !== 'ready') {
451
+ return { state: 'waiting', reason: `objects-${bundle.state}` };
474
452
  }
475
- if (!(await hasCommit(localCard.path, ref.oid))) {
476
- if (ref.objects?.kind === 'bundle') {
477
- const imported = await require('./objects').importFromLocator(localCard.path, ref.objects);
478
- if (imported.state !== 'imported') {
479
- waiting.push({ name: ref.name, state: imported.state });
480
- continue;
481
- }
482
- } else if (ref.objects?.kind === 'remote' && localCard.repo?.remote) {
483
- await transition.fetchBranch(localCard.path, 'origin', ref.name);
484
- } else {
485
- waiting.push({ name: ref.name, state: ref.objects?.kind || 'objects-missing' });
486
- continue;
453
+ await transition.importBundle(project.path, bundle.file);
454
+ reduced = await preparePlan({ path: project.path }, plan);
455
+ if (reduced.state !== 'ready') return reduced;
456
+ through = planThrough(plan);
457
+ }
458
+ const requestDigest = applyingRequestDigest(through, activePlan);
459
+ const official = targetRepoState(activePlan, through, reduced);
460
+ const initialHead = pending ? null : await headAndDirty(project.path);
461
+ const initialBranches = pending ? null : await localBranches(project.path);
462
+ let hydrating = pending || {
463
+ contract: HYDRATING_CONTRACT,
464
+ repoKey: plan.repoKey,
465
+ path: project.path,
466
+ through,
467
+ plan: activePlan,
468
+ reduced: {
469
+ state: 'ready',
470
+ indexTree: reduced.indexTree,
471
+ worktreeTree: reduced.worktreeTree,
472
+ checkpointRevision: reduced.checkpointRevision,
473
+ },
474
+ requestDigest,
475
+ operationId: hydratingId(requestDigest),
476
+ emptyTree: await transition.emptyTree(project.path),
477
+ bootstrap: {
478
+ source: {
479
+ head: {
480
+ branch: initialHead.head.branch,
481
+ oid: initialHead.head.oid,
482
+ },
483
+ branches: branchState(initialBranches),
484
+ },
485
+ target: {
486
+ head: official.head,
487
+ branches: official.branches,
488
+ },
489
+ },
490
+ };
491
+ if (!pending) {
492
+ persistRepoState(plan.repoKey, {
493
+ ...(stored || {}),
494
+ resourceId: project.resourceId || null,
495
+ path: project.path,
496
+ replica: null,
497
+ hydrating,
498
+ materialized: false,
499
+ });
500
+ }
501
+ if (!hydrationBootstrapSealed(hydrating)) {
502
+ try {
503
+ await transition.ensureEmptyIndex(
504
+ project.path,
505
+ hydrating.operationId,
506
+ { retainReceipt: true },
507
+ );
508
+ } catch (error) {
509
+ if (error instanceof transition.TransitionError) {
510
+ return { state: 'waiting', reason: 'local-index-moving' };
487
511
  }
512
+ throw error;
488
513
  }
489
- if (!(await hasCommit(localCard.path, ref.oid))) {
490
- waiting.push({ name: ref.name, state: 'objects-missing-after-import' });
491
- continue;
514
+ const bootstrapped = await applyHydrationBootstrap(project.path, hydrating);
515
+ if (bootstrapped.state !== 'ready') return bootstrapped;
516
+ }
517
+ let observed = await observePhysicalRepo(project.path);
518
+ if (observed.state !== 'ready') {
519
+ return { state: 'waiting', reason: 'local-state-moving' };
520
+ }
521
+ let execution = hydrating.execution || null;
522
+ if (!execution) {
523
+ const bootstrapCrossed = await hydrationBootstrapCrossed(project.path, hydrating);
524
+ const index = await merge.reconcileApplyingTree(
525
+ project.path,
526
+ observed.value.indexTree,
527
+ hydrating.emptyTree,
528
+ official.indexTree,
529
+ );
530
+ const worktree = await merge.reconcileApplyingTree(
531
+ project.path,
532
+ observed.value.worktreeTree,
533
+ hydrating.emptyTree,
534
+ official.worktreeTree,
535
+ );
536
+ const branches = reconcileApplyingBranches(
537
+ observed.value.branches,
538
+ hydrating.bootstrap.source.branches,
539
+ official.branches,
540
+ bootstrapCrossed.branches,
541
+ receiptProvenHeadReturn(
542
+ observed.value.head,
543
+ hydrating.bootstrap.source.head,
544
+ bootstrapCrossed.head,
545
+ ),
546
+ );
547
+ const head = sameCardPosition(observed.value, { head: official.head })
548
+ ? official.head
549
+ : reconcileApplyingHead(
550
+ observed.value.head,
551
+ hydrating.bootstrap.source.head,
552
+ hydrating.bootstrap.target.head,
553
+ branches,
554
+ {
555
+ headCrossed: bootstrapCrossed.head,
556
+ branchCrossed: bootstrapCrossed.branches,
557
+ plannedPostRefBranches: hydrating.bootstrap.target.branches,
558
+ },
559
+ );
560
+ if (head.branch) {
561
+ const attached = branches.find((branch) => branch.name === head.branch);
562
+ if (attached) attached.oid = head.oid;
563
+ else branches.push({ name: head.branch, oid: head.oid });
564
+ branches.sort((left, right) => left.name.localeCompare(right.name));
492
565
  }
493
- if (current.get(ref.name) !== ref.oid) {
494
- await transition.setBranchRef(localCard.path, ref.name, ref.oid);
495
- moved = true;
566
+ const source = observed.value;
567
+ const target = {
568
+ head,
569
+ branches,
570
+ indexTree: index.tree,
571
+ worktreeTree: worktree.tree,
572
+ };
573
+ const receipts = await sourceReceipts(project.path, source, target);
574
+ const sealed = await observePhysicalRepo(project.path);
575
+ const sealedReceipts = await sourceReceipts(project.path, source, target);
576
+ if (
577
+ sealed.state !== 'ready'
578
+ || sealed.digest !== stateDigest(source)
579
+ || !sameReceiptPlan(receipts, sealedReceipts)
580
+ ) {
581
+ return { state: 'waiting', reason: 'local-state-moving' };
496
582
  }
583
+ execution = {
584
+ source,
585
+ target,
586
+ official: officialPhysicalState(official),
587
+ operationId: hydrating.operationId,
588
+ sourceReceipts: sealedReceipts,
589
+ };
497
590
  }
498
- return { moved, waiting, pending };
499
- }
500
-
501
- async function reduceCheckpoint(localCard, plan) {
502
- const baseOid = plan.card.head.oid;
503
- const baseTree = await merge.treeOf(localCard.path, baseOid);
504
- let indexTree = baseTree;
505
- let worktreeTree = baseTree;
506
- if (plan.checkpointBaseConflict) {
507
- return { state: 'waiting', reason: 'checkpoint-base-disagrees' };
591
+ if (hydrating.bootstrapSealed !== true) {
592
+ hydrating = {
593
+ ...hydrating,
594
+ execution,
595
+ bootstrapSealed: true,
596
+ };
597
+ persistRepoState(plan.repoKey, {
598
+ ...(stored || {}),
599
+ resourceId: project.resourceId || null,
600
+ path: project.path,
601
+ replica: null,
602
+ hydrating,
603
+ materialized: false,
604
+ });
508
605
  }
509
- if (plan.checkpointBase) {
510
- const snapshot = readCheckpointValue(baseOid, plan.checkpointBase.value);
511
- if (snapshot.state !== 'ready') {
512
- return { state: 'waiting', reason: `checkpoint-base-${snapshot.state}` };
513
- }
514
- indexTree = await merge.treeOfPatch(localCard.path, baseOid, snapshot.indexDiff);
515
- worktreeTree = await merge.treeOfPatch(
516
- localCard.path,
517
- indexTree,
518
- snapshot.worktreeDiff,
606
+ if (hydrating.bootstrapReleased !== true) {
607
+ transition.releaseGitMetadataReceipts(
608
+ project.path,
609
+ hydrating.operationId,
519
610
  );
611
+ await transition.releaseBranchReceipt(project.path, hydrating.operationId);
612
+ const latest = readRepoState(plan.repoKey, project.resourceId);
613
+ hydrating = {
614
+ ...hydrating,
615
+ execution,
616
+ bootstrapReleased: true,
617
+ };
618
+ persistRepoState(plan.repoKey, {
619
+ ...(latest || stored || {}),
620
+ resourceId: project.resourceId || null,
621
+ path: project.path,
622
+ replica: null,
623
+ hydrating,
624
+ materialized: false,
625
+ });
520
626
  }
521
-
522
- for (const operation of plan.transitions) {
523
- const from = readCheckpointValue(baseOid, operation.from);
524
- const to = readCheckpointValue(baseOid, operation.to);
525
- if (from.state === 'payload-pending' || to.state === 'payload-pending') continue;
526
- if (from.state !== 'ready' || to.state !== 'ready') {
527
- return { state: 'waiting', reason: `checkpoint-${from.state !== 'ready' ? from.state : to.state}` };
528
- }
529
- const fromIndexTree = await merge.treeOfPatch(localCard.path, baseOid, from.indexDiff);
530
- const toIndexTree = await merge.treeOfPatch(localCard.path, baseOid, to.indexDiff);
531
- const reducedIndex = await merge.reduceTree(
532
- localCard.path,
533
- indexTree,
534
- fromIndexTree,
535
- toIndexTree,
536
- );
537
- indexTree = reducedIndex.tree;
538
-
539
- const fromWorktreeTree = await merge.treeOfPatch(localCard.path, fromIndexTree, from.worktreeDiff);
540
- const toWorktreeTree = await merge.treeOfPatch(localCard.path, toIndexTree, to.worktreeDiff);
541
- const reducedWorktree = await merge.reduceTree(
542
- localCard.path,
543
- worktreeTree,
544
- fromWorktreeTree,
545
- toWorktreeTree,
627
+ const crossed = await crossedSourceReceipts(
628
+ project.path,
629
+ execution.operationId,
630
+ execution.sourceReceipts,
631
+ );
632
+ const index = await merge.reconcileApplyingTree(
633
+ project.path,
634
+ observed.value.indexTree,
635
+ execution.source.indexTree,
636
+ execution.target.indexTree,
637
+ { sourceCrossed: crossed.index },
638
+ );
639
+ const worktree = await merge.reconcileApplyingTree(
640
+ project.path,
641
+ observed.value.worktreeTree,
642
+ execution.source.worktreeTree,
643
+ execution.target.worktreeTree,
644
+ { crossedPaths: crossed.worktree },
645
+ );
646
+ const branches = reconcileApplyingBranches(
647
+ observed.value.branches,
648
+ execution.source.branches,
649
+ execution.target.branches,
650
+ crossed.branches,
651
+ receiptProvenHeadReturn(
652
+ observed.value.head,
653
+ execution.source.head,
654
+ crossed.head,
655
+ ),
656
+ );
657
+ const head = reconcileApplyingHead(
658
+ observed.value.head,
659
+ execution.source.head,
660
+ execution.target.head,
661
+ branches,
662
+ {
663
+ headCrossed: crossed.head,
664
+ branchCrossed: crossed.branches,
665
+ plannedPostRefBranches: execution.target.branches,
666
+ },
667
+ );
668
+ const headResult = await advanceBranchesAndHead(
669
+ project.path,
670
+ observed.value.head,
671
+ observed.value.branches,
672
+ branches,
673
+ head,
674
+ execution.operationId,
675
+ );
676
+ if (headResult.state !== 'ready') return headResult;
677
+ const worktreeResult = await advanceWorktree(
678
+ project.path,
679
+ observed.value.worktreeTree,
680
+ worktree.tree,
681
+ { label: execution.operationId },
682
+ );
683
+ if (worktreeResult.state !== 'ready') return worktreeResult;
684
+ const indexResult = await advanceIndex(
685
+ project.path,
686
+ observed.value.indexTree,
687
+ index.tree,
688
+ execution.operationId,
689
+ );
690
+ if (indexResult.state !== 'ready') return indexResult;
691
+ const final = await observePhysicalRepo(project.path);
692
+ if (final.state !== 'ready') {
693
+ return { state: 'waiting', reason: 'local-state-moving' };
694
+ }
695
+ const localCard = await require('./card').buildRepoCard(project.path);
696
+ const captured = await captureIntentAgainst(
697
+ project,
698
+ localCard,
699
+ {
700
+ through,
701
+ card: {
702
+ head: {
703
+ branch: official.head.branch,
704
+ oid: official.head.oid,
705
+ detached: !official.head.branch,
706
+ },
707
+ branches: official.branches,
708
+ },
709
+ indexTree: official.indexTree,
710
+ worktreeTree: official.worktreeTree,
711
+ },
712
+ final,
713
+ );
714
+ if (captured.state === 'waiting') return captured;
715
+ if (captured.state === 'rejected') {
716
+ throw new Error(
717
+ captured.intent?.terminal?.message
718
+ || `local code intent rejected: ${captured.reason}`,
546
719
  );
547
- worktreeTree = reducedWorktree.tree;
548
720
  }
549
-
550
- const indexDiff = await merge.diffOfTree(localCard.path, baseOid, indexTree);
551
- const worktreeDiff = await merge.diffOfTree(localCard.path, indexTree, worktreeTree);
552
- return {
553
- state: 'ready',
554
- indexTree,
555
- worktreeTree,
721
+ const indexDiff = await merge.diffOfTree(
722
+ project.path,
723
+ activePlan.card.head.oid,
724
+ reduced.indexTree,
725
+ );
726
+ const worktreeDiff = await merge.diffOfTree(
727
+ project.path,
728
+ reduced.indexTree,
729
+ reduced.worktreeTree,
730
+ );
731
+ const verified = persistRepoState(plan.repoKey, {
732
+ verificationContract: VERIFICATION_CONTRACT,
733
+ resourceId: project.resourceId || null,
734
+ path: localCard.path,
735
+ replica: localCard.replica,
736
+ branch: activePlan.card.head.branch,
737
+ baseOid: activePlan.card.head.oid,
738
+ cardVersion: activePlan.card.positionVersion,
739
+ cardMutationId: activePlan.card.positionMutationId,
740
+ targetCard: {
741
+ branch: activePlan.card.head.branch,
742
+ oid: activePlan.card.head.oid,
743
+ version: activePlan.card.positionVersion,
744
+ mutationId: activePlan.card.positionMutationId,
745
+ },
746
+ appliedThrough: through,
747
+ checkpointRevision: reduced.checkpointRevision,
556
748
  indexDiff,
557
749
  worktreeDiff,
558
- checkpointRevision: revisionOf(baseOid, indexDiff, worktreeDiff),
750
+ checkpointVersions: (activePlan.transitions || []).map((entry) => entry.version),
751
+ branches: activePlan.branches || [],
752
+ heldTree: reduced.worktreeTree,
753
+ heldIndexTree: reduced.indexTree,
754
+ targetDigest: stateDigest(official),
755
+ expectedPhysicalDigest: final.digest,
756
+ observedPhysicalDigest: final.digest,
757
+ materialized: true,
758
+ receiptOperationId: execution.operationId,
759
+ }).state;
760
+ await releaseVerifiedReceipts(plan.repoKey, verified);
761
+ await refreshRepoCards();
762
+ return { state: 'hydrated', moved: true };
763
+ }
764
+
765
+ function sameCardPosition(left, right) {
766
+ return left?.head?.oid === right?.head?.oid
767
+ && left?.head?.branch === right?.head?.branch;
768
+ }
769
+
770
+ function validTree(oid) {
771
+ return typeof oid === 'string' && /^[0-9a-f]{40,64}$/.test(oid);
772
+ }
773
+
774
+ function cardState(card) {
775
+ return {
776
+ branch: card?.head?.branch ?? null,
777
+ oid: card?.head?.oid ?? null,
559
778
  };
560
779
  }
561
780
 
562
- function captureIsOnRail(plan, captured, localMachine = machineId(), acknowledged = null) {
563
- if (
564
- !captured
565
- || captured.indexDiff === null
566
- || captured.baseOid !== plan.card.head.oid
567
- ) return false;
568
- const revision = revisionOf(
569
- captured.baseOid,
570
- captured.indexDiff,
571
- captured.worktreeDiff,
572
- );
573
- const latest = [...plan.transitions]
574
- .reverse()
575
- .find((operation) => operation.machine === localMachine);
781
+ function intentOwnsHead(intent) {
576
782
  return Boolean(
577
- (latest && latest.to?.revision === revision)
578
- || (
579
- Number.isInteger(acknowledged?.version)
580
- && acknowledged.version > 0
581
- && acknowledged.baseOid === captured.baseOid
582
- && acknowledged.branch === plan.card.head.branch
583
- && acknowledged.to?.revision === revision
783
+ intent
784
+ && !sameCardPosition(
785
+ { head: cardState(intent.base.card) },
786
+ { head: cardState(intent.local.card) },
584
787
  )
585
788
  );
586
789
  }
587
790
 
588
- async function materializeRepo(localCardInput, plan, appliedThrough = null) {
589
- const refreshed = localCardInput;
590
- const cardResult = await moveToWinningCard(refreshed, plan.card);
591
- if (cardResult.state !== 'ready') return cardResult;
592
-
593
- const localCard = cardResult.moved
594
- ? await refreshRepoCard(refreshed.replica) || refreshed
595
- : refreshed;
596
- const project = require('./wall').projectForRepo(plan.repoKey);
597
- const previous = readRepoState(plan.repoKey, project?.resourceId);
598
- const parked = parkedForState(
599
- previous?.parked,
600
- plan.parked,
601
- cardResult.parked,
602
- );
603
- const parkedWaiting = await materializeParked(localCard, parked);
604
- const planParked = new Set((plan.parked || []).map((entry) => entry.oid));
605
- const requiredParkedWaiting = parkedWaiting.filter((entry) => planParked.has(entry.oid));
606
- if (requiredParkedWaiting.length > 0) {
607
- // Parking is a local write. Remember it before waiting on an inbound
608
- // bundle, so the next pass cannot replace unnumbered local work.
609
- const persisted = persistRepoState(plan.repoKey, {
610
- ...(previous || {}),
611
- resourceId: project?.resourceId || null,
612
- path: localCard.path,
613
- replica: localCard.replica,
614
- parked,
615
- parkedWaiting,
616
- materialized: false,
617
- });
618
- if (persisted.changed) {
619
- appendStateEvent({
620
- resource: 'workspace-reducer',
621
- op: previous ? 'update' : 'insert',
622
- id: localCard.replica,
623
- value: repoStateEventValue(persisted.state),
624
- source: 'workspace-reducer',
625
- });
791
+ async function usableIntent(project, previous, localCard) {
792
+ const intents = require('./intent');
793
+ const intent = intents.readIntent(project);
794
+ if (!intent) return { state: 'none', intent: null, successor: null };
795
+ const successor = intent.successor || null;
796
+ if (
797
+ previous?.verificationContract === VERIFICATION_CONTRACT
798
+ && previous.materialized === true
799
+ && Number.isInteger(previous.appliedThrough)
800
+ && intent.publication?.foldedAt
801
+ && Number.isInteger(intent.publication.numberedVersion)
802
+ && intent.publication.numberedVersion <= previous.appliedThrough
803
+ && !successor
804
+ ) {
805
+ intents.clearIntent(project);
806
+ return { state: 'none', intent: null, successor: null };
807
+ }
808
+ const initial = intents._test.validInitialBase(intent.base);
809
+ if (initial) {
810
+ const emptyTree = await transition.emptyTree(localCard.path);
811
+ if (
812
+ intent.base.indexTree !== emptyTree
813
+ || intent.base.worktreeTree !== emptyTree
814
+ ) {
815
+ return { state: 'blocked', intent };
626
816
  }
627
- return { state: 'waiting', reason: `parked-${requiredParkedWaiting[0].state}` };
628
- }
629
- const branchResult = await materializeBranches(localCard, plan, previous?.branches);
630
- if (branchResult.waiting.length > 0) {
631
- return { state: 'waiting', reason: `branch-${branchResult.waiting[0].state}` };
632
- }
633
- const reduced = await reduceCheckpoint(localCard, plan);
634
- if (reduced.state !== 'ready') return reduced;
635
-
636
- const sharedBaseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
637
- const previousHeldWorktree = (
638
- previous?.baseOid === plan.card.head.oid
639
- && previous.heldTree
640
- && await merge.hasTree(localCard.path, previous.heldTree)
641
- ? previous.heldTree
642
- : sharedBaseTree
643
- );
644
- const previousHeldIndex = (
645
- previous?.baseOid === plan.card.head.oid
646
- && previous.heldIndexTree
647
- && await merge.hasTree(localCard.path, previous.heldIndexTree)
648
- ? previous.heldIndexTree
649
- : sharedBaseTree
650
- );
651
- let stateBaseOid = plan.card.head.oid;
652
- let stateBranch = plan.card.head.branch;
653
- let stateCardVersion = plan.card.positionVersion;
654
- let stateCardMutationId = plan.card.positionMutationId;
655
- let stateIndexDiff = reduced.indexDiff;
656
- let stateWorktreeDiff = reduced.worktreeDiff;
657
- let heldWorktree;
658
- let heldIndex;
659
- let targetWorktree;
660
- let targetIndex;
661
- if (cardResult.pendingCard) {
662
- const pendingBaseTree = await merge.treeOf(localCard.path, localCard.head.oid);
663
- const rebasedIndex = await merge.reduceTree(
664
- localCard.path,
665
- reduced.indexTree,
666
- previousHeldIndex,
667
- pendingBaseTree,
668
- );
669
- const rebasedWorktree = await merge.reduceTree(
670
- localCard.path,
671
- reduced.worktreeTree,
672
- previousHeldWorktree,
673
- pendingBaseTree,
674
- );
675
- heldIndex = pendingBaseTree;
676
- heldWorktree = pendingBaseTree;
677
- targetIndex = rebasedIndex.tree;
678
- targetWorktree = rebasedWorktree.tree;
679
- stateBaseOid = localCard.head.oid;
680
- stateBranch = localCard.head.branch;
681
- stateCardVersion = null;
682
- stateCardMutationId = null;
683
- stateIndexDiff = await merge.diffOfTree(localCard.path, stateBaseOid, targetIndex);
684
- stateWorktreeDiff = await merge.diffOfTree(localCard.path, targetIndex, targetWorktree);
685
- } else {
686
- const captured = await captureCheckpoint(localCard.path);
687
- const wall = require('./wall');
688
- const localCheckpoint = readCheckpoint(machineId(), plan.repoKey);
689
- const acknowledged = localCheckpoint && {
690
- ...localCheckpoint,
691
- version: wall.mutationVersion(
692
- project?.resourceId,
693
- localCheckpoint.mutationId,
694
- plan.repoKey,
695
- ),
696
- };
697
- const represented = captureIsOnRail(plan, captured, machineId(), acknowledged);
698
- const capturedIndex = represented
699
- ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
700
- : null;
701
- const capturedWorktree = represented
702
- ? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
703
- : null;
704
- heldWorktree = capturedWorktree || previousHeldWorktree;
705
- heldIndex = capturedIndex || previousHeldIndex;
706
- targetWorktree = reduced.worktreeTree;
707
- targetIndex = reduced.indexTree;
708
- }
709
-
710
- const currentIndex = await merge.indexTree(localCard.path);
711
- const reducedLocalIndex = await merge.reduceTree(
712
- localCard.path,
713
- targetIndex,
714
- heldIndex,
715
- currentIndex,
716
- );
717
- const applied = await merge.advance(localCard.path, heldWorktree, targetWorktree, {
718
- label: `checkpoint-${reduced.checkpointRevision || 'clean'}`,
719
- });
720
- const indexResult = await advanceIndex(localCard.path, currentIndex, reducedLocalIndex.tree);
721
- if (indexResult.state !== 'ready') return indexResult;
722
- const persisted = persistRepoState(plan.repoKey, {
723
- resourceId: project?.resourceId || null,
724
- path: localCard.path,
725
- replica: localCard.replica,
726
- branch: stateBranch,
727
- baseOid: stateBaseOid,
728
- cardVersion: stateCardVersion,
729
- cardMutationId: stateCardMutationId,
730
- pendingCard: Boolean(cardResult.pendingCard),
731
- appliedThrough,
732
- checkpointRevision: revisionOf(stateBaseOid, stateIndexDiff, stateWorktreeDiff),
733
- indexDiff: stateIndexDiff,
734
- worktreeDiff: stateWorktreeDiff,
735
- checkpointVersions: plan.transitions.map((entry) => entry.version),
736
- branches: plan.branches || [],
737
- pendingRefs: branchResult.pending,
738
- heldTree: applied.heldTree,
739
- heldIndexTree: targetIndex,
740
- parked,
741
- parkedWaiting: [...parkedWaiting, ...branchResult.waiting],
742
- materialized: applied.heldTree === targetWorktree
743
- && reducedLocalIndex.tree === targetIndex,
744
- });
745
- if (persisted.changed) {
746
- appendStateEvent({
747
- resource: 'workspace-reducer',
748
- op: previous ? 'update' : 'insert',
749
- id: localCard.replica,
750
- value: repoStateEventValue(persisted.state),
751
- source: 'workspace-reducer',
752
- });
753
817
  }
754
- return {
755
- state: 'in-sync',
756
- reason: null,
757
- moved: cardResult.moved || branchResult.moved || applied.applied.length > 0,
758
- };
818
+ const applying = validApplying(previous?.applying)
819
+ ? previous.applying
820
+ : null;
821
+ const verified = previous?.verificationContract === VERIFICATION_CONTRACT
822
+ && previous.materialized === true
823
+ && Number.isInteger(previous.appliedThrough);
824
+ const verifiedThrough = applying?.through ?? previous?.appliedThrough;
825
+ if (
826
+ !initial
827
+ && (
828
+ (!applying && !verified)
829
+ || intent.base.through > verifiedThrough
830
+ )
831
+ ) {
832
+ return { state: 'blocked', intent };
833
+ }
834
+ for (const tree of [
835
+ intent.base.indexTree,
836
+ intent.base.worktreeTree,
837
+ intent.local.indexTree,
838
+ intent.local.worktreeTree,
839
+ successor?.base?.indexTree,
840
+ successor?.base?.worktreeTree,
841
+ successor?.local?.indexTree,
842
+ successor?.local?.worktreeTree,
843
+ ]) {
844
+ if (tree == null) continue;
845
+ if (!validTree(tree) || !await merge.hasTree(localCard.path, tree)) {
846
+ return { state: 'blocked', intent, successor };
847
+ }
848
+ }
849
+ return { state: 'ready', intent, successor };
759
850
  }
760
851
 
761
- async function advanceIndex(worktreeRoot, observedTree, targetTree) {
762
- if (observedTree === targetTree) return { state: 'ready', moved: false };
763
- const patch = await merge.diffOfTree(worktreeRoot, observedTree, targetTree);
852
+ async function advanceWorktree(worktreeRoot, heldTree, targetTree, options) {
764
853
  try {
765
- await transition.applyIndexPatch(worktreeRoot, patch);
766
- return { state: 'ready', moved: true };
854
+ return {
855
+ state: 'ready',
856
+ value: await merge.advance(worktreeRoot, heldTree, targetTree, options),
857
+ };
767
858
  } catch (error) {
768
859
  if (error instanceof transition.TransitionError) {
769
- return { state: 'waiting', reason: 'local-index-moving', moved: false };
860
+ return { state: 'waiting', reason: 'local-state-moving' };
770
861
  }
771
862
  throw error;
772
863
  }
773
864
  }
774
865
 
775
- function localCardMayMaterialize(publishedCard, publication) {
776
- return Boolean(publishedCard)
777
- || publication?.reason === 'shared-state-covered'
778
- || publication?.reason === 'project-not-current';
866
+ function recoveryPlan(plan) {
867
+ return {
868
+ repoKey: plan.repoKey,
869
+ card: {
870
+ head: {
871
+ branch: plan.card.head?.branch ?? null,
872
+ oid: plan.card.head?.oid ?? null,
873
+ },
874
+ positionVersion: plan.card.positionVersion,
875
+ positionMutationId: plan.card.positionMutationId,
876
+ },
877
+ branches: (plan.branches || []).map((ref) => ({
878
+ name: ref.name,
879
+ oid: ref.oid ?? null,
880
+ version: ref.version,
881
+ mutationId: ref.mutationId,
882
+ })),
883
+ transitions: (plan.transitions || []).map((operation) => ({
884
+ machine: operation.machine,
885
+ version: operation.version,
886
+ mutationId: operation.mutationId,
887
+ to: { revision: operation.to?.revision ?? null },
888
+ })),
889
+ };
779
890
  }
780
891
 
781
- function pathForRepo(localCard, project) {
782
- return localCard?.path || project?.path || null;
892
+ function applyingRequestDigest(through, plan) {
893
+ return stateDigest({ through, plan: recoveryPlan(plan) });
783
894
  }
784
895
 
785
- async function runReducer() {
786
- service.counters.passes += 1;
896
+ function applyingId(requestDigest, source, target) {
897
+ return stateDigest({
898
+ requestDigest,
899
+ source: stateDigest(source),
900
+ target: stateDigest(target),
901
+ });
902
+ }
903
+
904
+ function hydratingId(requestDigest) {
905
+ return stateDigest({
906
+ contract: HYDRATING_CONTRACT,
907
+ requestDigest,
908
+ });
909
+ }
910
+
911
+ function validSourceReceipts(value) {
912
+ return Boolean(
913
+ value
914
+ && Array.isArray(value.worktree)
915
+ && value.worktree.every((name) => typeof name === 'string' && name.length > 0)
916
+ && typeof value.index === 'boolean'
917
+ && typeof value.head === 'boolean'
918
+ && Array.isArray(value.branches)
919
+ && value.branches.every((name) => typeof name === 'string' && name.length > 0),
920
+ );
921
+ }
922
+
923
+ function validApplying(value) {
924
+ if (!(
925
+ value
926
+ && value.contract === APPLYING_CONTRACT
927
+ && value.plan?.repoKey
928
+ && value.reduced?.state === 'ready'
929
+ && Number.isInteger(value.through)
930
+ && value.through >= 0
931
+ && value.requestDigest === applyingRequestDigest(value.through, value.plan)
932
+ && validPhysicalState(value.source)
933
+ && validPhysicalState(value.target)
934
+ && validPhysicalState(value.official)
935
+ && /^[0-9a-f]{64}$/.test(value.operationId)
936
+ && validSourceReceipts(value.sourceReceipts)
937
+ )) return false;
938
+ if (
939
+ value.predecessorIntentDigest !== undefined
940
+ && !(
941
+ /^[0-9a-f]{64}$/.test(value.predecessorIntentDigest)
942
+ && validPhysicalState(value.provenanceBase)
943
+ )
944
+ ) return false;
945
+ return true;
946
+ }
947
+
948
+ function validPhysicalState(value) {
949
+ return Boolean(
950
+ value
951
+ && validTree(value.head?.oid)
952
+ && Array.isArray(value.branches)
953
+ && validTree(value.indexTree)
954
+ && validTree(value.worktreeTree)
955
+ );
956
+ }
957
+
958
+ function sameOid(left, right) {
959
+ return (left ?? null) === (right ?? null);
960
+ }
961
+
962
+ function sameReceiptPlan(left, right) {
963
+ return JSON.stringify(left) === JSON.stringify(right);
964
+ }
965
+
966
+ function headAttachment(head) {
967
+ return head?.branch
968
+ ? { branch: head.branch }
969
+ : { branch: null, oid: head?.oid ?? null };
970
+ }
971
+
972
+ function sameHeadAttachment(left, right) {
973
+ return JSON.stringify(headAttachment(left)) === JSON.stringify(headAttachment(right));
974
+ }
975
+
976
+ async function sourceReceipts(worktreeRoot, source, target) {
977
+ const worktreePaths = source.worktreeTree === target.worktreeTree
978
+ ? []
979
+ : await merge.changedPaths(worktreeRoot, source.worktreeTree, target.worktreeTree);
980
+ const sourceRefs = new Map(branchState(source.branches).map((ref) => [ref.name, ref.oid]));
981
+ const targetRefs = new Map(branchState(target.branches).map((ref) => [ref.name, ref.oid]));
982
+ const branchNames = [...new Set([...sourceRefs.keys(), ...targetRefs.keys()])]
983
+ .filter((name) => !sameOid(sourceRefs.get(name), targetRefs.get(name)))
984
+ .sort();
985
+ return {
986
+ worktree: worktreePaths,
987
+ index: source.indexTree !== target.indexTree,
988
+ head: !sameHeadAttachment(source.head, target.head),
989
+ branches: branchNames,
990
+ };
991
+ }
992
+
993
+ async function crossedSourceReceipts(worktreeRoot, operationId, receipts) {
994
+ const worktree = transition.crossedFileReceiptPaths(
995
+ worktreeRoot,
996
+ operationId,
997
+ receipts.worktree,
998
+ );
999
+ const crossedBranches = await Promise.all(
1000
+ receipts.branches.map(async (name) => (
1001
+ await transition.branchReceiptCrossed(worktreeRoot, operationId, name)
1002
+ ? name
1003
+ : null
1004
+ )),
1005
+ );
1006
+ const branches = new Set(crossedBranches.filter(Boolean));
1007
+ return {
1008
+ worktree,
1009
+ index: receipts.index
1010
+ && transition.gitMetadataReceiptCrossed(worktreeRoot, operationId, 'index'),
1011
+ head: receipts.head
1012
+ && transition.gitMetadataReceiptCrossed(worktreeRoot, operationId, 'HEAD'),
1013
+ branches,
1014
+ };
1015
+ }
1016
+
1017
+ function receiptProvenHeadReturn(current, source, headCrossed) {
1018
+ return (
1019
+ headCrossed
1020
+ && source?.branch
1021
+ && sameCardPosition({ head: current }, { head: source })
1022
+ ) ? source.branch : null;
1023
+ }
1024
+
1025
+ function reconcileApplyingBranches(
1026
+ currentRefs,
1027
+ sourceRefs,
1028
+ targetRefs,
1029
+ crossed = new Set(),
1030
+ retainedHeadBranch = null,
1031
+ ) {
1032
+ const current = new Map(branchState(currentRefs).map((ref) => [ref.name, ref.oid]));
1033
+ const source = new Map(branchState(sourceRefs).map((ref) => [ref.name, ref.oid]));
1034
+ const target = new Map(branchState(targetRefs).map((ref) => [ref.name, ref.oid]));
1035
+ const names = new Set([...current.keys(), ...source.keys(), ...target.keys()]);
1036
+ return [...names].sort().flatMap((name) => {
1037
+ const currentOid = current.get(name) ?? null;
1038
+ const sourceOid = source.get(name) ?? null;
1039
+ const targetOid = target.get(name) ?? null;
1040
+ const oid = name === retainedHeadBranch
1041
+ ? currentOid
1042
+ : sameOid(currentOid, targetOid)
1043
+ ? targetOid
1044
+ : sameOid(currentOid, sourceOid) && !crossed.has(name)
1045
+ ? targetOid
1046
+ : currentOid;
1047
+ return oid === null ? [] : [{ name, oid }];
1048
+ });
1049
+ }
1050
+
1051
+ function headAfterRefPlan(sealedHead, postRefBranches) {
1052
+ const postRefMap = new Map(
1053
+ branchState(postRefBranches).map((ref) => [ref.name, ref.oid]),
1054
+ );
1055
+ return sealedHead.branch
1056
+ ? {
1057
+ branch: sealedHead.branch,
1058
+ oid: postRefMap.get(sealedHead.branch) ?? null,
1059
+ }
1060
+ : headAttachment(sealedHead);
1061
+ }
1062
+
1063
+ function branchRefApplyPlan(sealedHead, currentBranches, targetBranches) {
1064
+ const current = new Map(
1065
+ branchState(currentBranches)
1066
+ .filter((ref) => ref.oid !== null)
1067
+ .map((ref) => [ref.name, ref.oid]),
1068
+ );
1069
+ const target = new Map(
1070
+ branchState(targetBranches)
1071
+ .filter((ref) => ref.oid !== null)
1072
+ .map((ref) => [ref.name, ref.oid]),
1073
+ );
1074
+ const changes = [...new Set([...current.keys(), ...target.keys()])]
1075
+ .sort()
1076
+ .flatMap((name) => {
1077
+ const expectedOid = current.get(name) ?? null;
1078
+ const oid = target.get(name) ?? null;
1079
+ return expectedOid === oid ? [] : [{ branch: name, expectedOid, oid }];
1080
+ });
1081
+ const deferred = sealedHead?.branch
1082
+ ? changes.find((change) => (
1083
+ change.branch === sealedHead.branch
1084
+ && change.oid === null
1085
+ )) || null
1086
+ : null;
1087
+ if (!deferred) {
1088
+ return {
1089
+ beforeHead: changes,
1090
+ headBranches: branchMapState(target),
1091
+ afterHead: [],
1092
+ };
1093
+ }
1094
+ const headBranches = new Map(target);
1095
+ headBranches.set(deferred.branch, deferred.expectedOid);
1096
+ return {
1097
+ beforeHead: changes.filter((change) => change !== deferred),
1098
+ headBranches: branchMapState(headBranches),
1099
+ afterHead: [deferred],
1100
+ };
1101
+ }
1102
+
1103
+ async function advanceBranchRefChanges(worktreeRoot, changes, operationId) {
1104
+ if (changes.length === 0) return { state: 'ready' };
1105
+ try {
1106
+ await transition.updateBranchRefs(worktreeRoot, changes, operationId);
1107
+ return { state: 'ready' };
1108
+ } catch (error) {
1109
+ if (error instanceof transition.TransitionError) {
1110
+ return { state: 'waiting', reason: 'local-refs-moving' };
1111
+ }
1112
+ throw error;
1113
+ }
1114
+ }
1115
+
1116
+ async function advanceBranchesAndHead(
1117
+ worktreeRoot,
1118
+ sealedHead,
1119
+ currentBranches,
1120
+ targetBranches,
1121
+ targetHead,
1122
+ operationId,
1123
+ ) {
1124
+ const plan = branchRefApplyPlan(
1125
+ sealedHead,
1126
+ currentBranches,
1127
+ targetBranches,
1128
+ );
1129
+ const beforeHead = await advanceBranchRefChanges(
1130
+ worktreeRoot,
1131
+ plan.beforeHead,
1132
+ operationId,
1133
+ );
1134
+ if (beforeHead.state !== 'ready') return beforeHead;
1135
+ const head = await advanceHeadAfterSealedRefPlan(
1136
+ worktreeRoot,
1137
+ sealedHead,
1138
+ plan.headBranches,
1139
+ targetHead,
1140
+ operationId,
1141
+ );
1142
+ if (head.state !== 'ready') return head;
1143
+ return advanceBranchRefChanges(
1144
+ worktreeRoot,
1145
+ plan.afterHead,
1146
+ operationId,
1147
+ );
1148
+ }
1149
+
1150
+ function reconcileApplyingHead(
1151
+ current,
1152
+ source,
1153
+ target,
1154
+ branches,
1155
+ {
1156
+ headCrossed = false,
1157
+ branchCrossed = new Set(),
1158
+ plannedPostRefBranches = [],
1159
+ } = {},
1160
+ ) {
1161
+ const postRefSource = headAfterRefPlan(source, plannedPostRefBranches);
1162
+ const sourceBranchCrossed = source?.branch
1163
+ ? branchCrossed.has(source.branch)
1164
+ : false;
1165
+ const selected = sameCardPosition({ head: current }, { head: target })
1166
+ ? headAttachment(target)
1167
+ : sameCardPosition({ head: current }, { head: postRefSource }) && !headCrossed
1168
+ ? headAttachment(target)
1169
+ : sameCardPosition({ head: current }, { head: source })
1170
+ && !headCrossed
1171
+ && !sourceBranchCrossed
1172
+ ? headAttachment(target)
1173
+ : headAttachment(current);
1174
+ if (!selected.branch) return selected;
1175
+ const attached = branches.find((branch) => branch.name === selected.branch);
1176
+ if (!attached) {
1177
+ return {
1178
+ branch: null,
1179
+ oid: current?.oid ?? target?.oid ?? null,
1180
+ };
1181
+ }
1182
+ return {
1183
+ branch: selected.branch,
1184
+ oid: attached.oid,
1185
+ };
1186
+ }
1187
+
1188
+ async function advanceHeadAfterSealedRefPlan(
1189
+ worktreeRoot,
1190
+ sealedHead,
1191
+ postRefBranches,
1192
+ targetHead,
1193
+ operationId,
1194
+ ) {
1195
+ const postRefMap = new Map(
1196
+ branchState(postRefBranches).map((ref) => [ref.name, ref.oid]),
1197
+ );
1198
+ const expectedHead = headAfterRefPlan(sealedHead, postRefBranches);
1199
+ if (sameCardPosition({ head: expectedHead }, { head: targetHead })) {
1200
+ return { state: 'ready' };
1201
+ }
1202
+ try {
1203
+ await transition.moveHeadToCard(worktreeRoot, {
1204
+ expectedBranch: expectedHead.branch,
1205
+ expectedOid: expectedHead.oid,
1206
+ expectedTargetOid: targetHead.branch
1207
+ ? postRefMap.get(targetHead.branch) ?? null
1208
+ : null,
1209
+ targetBranch: targetHead.branch,
1210
+ targetOid: targetHead.oid,
1211
+ operation: operationId,
1212
+ });
1213
+ return { state: 'ready' };
1214
+ } catch (error) {
1215
+ if (error instanceof transition.TransitionError) {
1216
+ return { state: 'waiting', reason: 'local-head-moving' };
1217
+ }
1218
+ throw error;
1219
+ }
1220
+ }
1221
+
1222
+ async function applyingAnchors(
1223
+ localCard,
1224
+ plan,
1225
+ prepared,
1226
+ intent,
1227
+ successor,
1228
+ observed,
1229
+ through,
1230
+ ) {
1231
+ const official = targetRepoState(plan, through, prepared);
1232
+ const covered = intent && intentFoldedThrough(intent, through);
1233
+ if (covered) {
1234
+ const predecessor = successor?.base || intent.local;
1235
+ const retained = successor?.local || observed.value;
1236
+ const staged = await merge.reduceTree(
1237
+ localCard.path,
1238
+ official.indexTree,
1239
+ predecessor.indexTree,
1240
+ retained.indexTree,
1241
+ );
1242
+ const working = await merge.reduceTree(
1243
+ localCard.path,
1244
+ official.worktreeTree,
1245
+ predecessor.worktreeTree,
1246
+ retained.worktreeTree,
1247
+ );
1248
+ const sourceHead = cardState(predecessor.card);
1249
+ const currentHead = retained.card?.head || retained.head;
1250
+ const head = (
1251
+ sameCardPosition({ head: currentHead }, { head: sourceHead })
1252
+ || sameCardPosition({ head: currentHead }, { head: official.head })
1253
+ ) ? official.head : currentHead;
1254
+ const branches = reconcileApplyingBranches(
1255
+ retained.card?.branches || retained.branches,
1256
+ predecessor.card.branches,
1257
+ official.branches,
1258
+ );
1259
+ if (head.branch) {
1260
+ const attached = branches.find((branch) => branch.name === head.branch);
1261
+ if (attached) attached.oid = head.oid;
1262
+ else branches.push({ name: head.branch, oid: head.oid });
1263
+ branches.sort((left, right) => left.name.localeCompare(right.name));
1264
+ }
1265
+ return {
1266
+ source: observed.value,
1267
+ target: {
1268
+ head,
1269
+ branches,
1270
+ indexTree: staged.tree,
1271
+ worktreeTree: working.tree,
1272
+ },
1273
+ official,
1274
+ provenanceBase: {
1275
+ head: sourceHead,
1276
+ branches: branchState(intent.local.card.branches),
1277
+ indexTree: intent.local.indexTree,
1278
+ worktreeTree: intent.local.worktreeTree,
1279
+ },
1280
+ predecessorIntentDigest: intent.semanticDigest,
1281
+ };
1282
+ }
1283
+ const retainedIntent = successor
1284
+ ? { ...intent, local: successor.local, terminal: successor.terminal }
1285
+ : intent;
1286
+ const staged = intent
1287
+ ? await merge.reduceTree(
1288
+ localCard.path,
1289
+ prepared.indexTree,
1290
+ retainedIntent.base.indexTree,
1291
+ retainedIntent.local.indexTree,
1292
+ )
1293
+ : { tree: prepared.indexTree };
1294
+ const working = intent
1295
+ ? await merge.reduceTree(
1296
+ localCard.path,
1297
+ prepared.worktreeTree,
1298
+ retainedIntent.base.worktreeTree,
1299
+ retainedIntent.local.worktreeTree,
1300
+ )
1301
+ : { tree: prepared.worktreeTree };
1302
+ const refs = classifyBranchRefs(plan, observed.value.branches, retainedIntent);
1303
+ const pendingCard = intentOwnsHead(retainedIntent)
1304
+ ? cardState(retainedIntent.local.card)
1305
+ : null;
1306
+ return {
1307
+ source: observed.value,
1308
+ target: physicalRepoState({
1309
+ plan,
1310
+ pendingCard,
1311
+ expectedBranches: refs.expected,
1312
+ indexTree: staged.tree,
1313
+ worktreeTree: working.tree,
1314
+ }),
1315
+ official,
1316
+ };
1317
+ }
1318
+
1319
+ function applyingIntentBase(applying) {
1320
+ return {
1321
+ through: applying.through,
1322
+ card: {
1323
+ head: {
1324
+ branch: applying.official.head.branch,
1325
+ oid: applying.official.head.oid,
1326
+ detached: !applying.official.head.branch,
1327
+ },
1328
+ branches: branchState(applying.official.branches),
1329
+ },
1330
+ indexTree: applying.official.indexTree,
1331
+ worktreeTree: applying.official.worktreeTree,
1332
+ };
1333
+ }
1334
+
1335
+ /**
1336
+ * Recover one interrupted ordered apply without guessing.
1337
+ *
1338
+ * Source is the observed checkout before apply; target is the official state
1339
+ * with proven local intent preserved. An uncrossed source or the planned
1340
+ * post-ref HEAD resolves to target. A receipt-proven return to source and any
1341
+ * third value are retained and durably captured as successor intent over the
1342
+ * official target.
1343
+ */
1344
+ async function recoverApplyingIntent(project, localCard, previous) {
1345
+ const applying = previous?.applying;
1346
+ if (!validApplying(applying)) return { state: 'ready', card: localCard };
1347
+ const observed = await observePhysicalRepo(localCard.path);
1348
+ if (observed.state !== 'ready') {
1349
+ return { state: 'waiting', reason: 'local-state-moving' };
1350
+ }
1351
+ const crossed = await crossedSourceReceipts(
1352
+ localCard.path,
1353
+ applying.operationId,
1354
+ applying.sourceReceipts,
1355
+ );
1356
+ const index = await merge.reconcileApplyingTree(
1357
+ localCard.path,
1358
+ observed.value.indexTree,
1359
+ applying.source.indexTree,
1360
+ applying.target.indexTree,
1361
+ { sourceCrossed: crossed.index },
1362
+ );
1363
+ const worktree = await merge.reconcileApplyingTree(
1364
+ localCard.path,
1365
+ observed.value.worktreeTree,
1366
+ applying.source.worktreeTree,
1367
+ applying.target.worktreeTree,
1368
+ { crossedPaths: crossed.worktree },
1369
+ );
1370
+ const branches = reconcileApplyingBranches(
1371
+ observed.value.branches,
1372
+ applying.source.branches,
1373
+ applying.target.branches,
1374
+ crossed.branches,
1375
+ receiptProvenHeadReturn(
1376
+ observed.value.head,
1377
+ applying.source.head,
1378
+ crossed.head,
1379
+ ),
1380
+ );
1381
+ const head = reconcileApplyingHead(
1382
+ observed.value.head,
1383
+ applying.source.head,
1384
+ applying.target.head,
1385
+ branches,
1386
+ {
1387
+ headCrossed: crossed.head,
1388
+ branchCrossed: crossed.branches,
1389
+ plannedPostRefBranches: applying.target.branches,
1390
+ },
1391
+ );
1392
+ const resolved = {
1393
+ head,
1394
+ branches,
1395
+ indexTree: index.tree,
1396
+ worktreeTree: worktree.tree,
1397
+ };
1398
+
1399
+ const headResult = await advanceBranchesAndHead(
1400
+ localCard.path,
1401
+ observed.value.head,
1402
+ observed.value.branches,
1403
+ resolved.branches,
1404
+ head,
1405
+ applying.operationId,
1406
+ );
1407
+ if (headResult.state !== 'ready') return headResult;
1408
+
1409
+ const worktreeResult = await advanceWorktree(
1410
+ localCard.path,
1411
+ observed.value.worktreeTree,
1412
+ resolved.worktreeTree,
1413
+ { label: applying.operationId },
1414
+ );
1415
+ if (worktreeResult.state !== 'ready') return worktreeResult;
1416
+ const indexResult = await advanceIndex(
1417
+ localCard.path,
1418
+ observed.value.indexTree,
1419
+ resolved.indexTree,
1420
+ applying.operationId,
1421
+ );
1422
+ if (indexResult.state !== 'ready') return indexResult;
1423
+
1424
+ const recovered = await observePhysicalRepo(localCard.path);
1425
+ if (recovered.state !== 'ready' || recovered.digest !== stateDigest(resolved)) {
1426
+ return { state: 'waiting', reason: 'local-state-moving' };
1427
+ }
1428
+ const refreshed = await refreshRepoCard(localCard.replica) || {
1429
+ ...localCard,
1430
+ head: {
1431
+ branch: resolved.head.branch,
1432
+ oid: resolved.head.oid,
1433
+ detached: !resolved.head.branch,
1434
+ },
1435
+ refs: resolved.branches,
1436
+ };
1437
+ const retainedThirdValue = stateDigest(resolved) !== stateDigest(applying.target);
1438
+ const heldIntent = project
1439
+ ? require('./intent').readOwnedIntent(project)
1440
+ : null;
1441
+ const rebaseUnnumberedIntent = Boolean(
1442
+ heldIntent
1443
+ && !heldIntent.publication
1444
+ && heldIntent.base.through < applying.through
1445
+ );
1446
+ if (
1447
+ applying.predecessorIntentDigest
1448
+ || retainedThirdValue
1449
+ || rebaseUnnumberedIntent
1450
+ ) {
1451
+ if (!project) return { state: 'waiting', reason: 'unknown-local-state' };
1452
+ const captured = await captureIntentAgainst(
1453
+ project,
1454
+ {
1455
+ ...refreshed,
1456
+ head: {
1457
+ branch: resolved.head.branch,
1458
+ oid: resolved.head.oid,
1459
+ detached: !resolved.head.branch,
1460
+ },
1461
+ refs: resolved.branches.map((branch) => {
1462
+ const source = [
1463
+ ...(refreshed.refs || refreshed.branches || []),
1464
+ ...(localCard.refs || localCard.branches || []),
1465
+ ].find((ref) => ref.name === branch.name && ref.oid === branch.oid);
1466
+ return source ? { ...source, ...branch } : branch;
1467
+ }),
1468
+ },
1469
+ applyingIntentBase(applying),
1470
+ recovered,
1471
+ {
1472
+ replaceAnchoredIntent: applying.predecessorIntentDigest,
1473
+ preserveNoEffectIntent: rebaseUnnumberedIntent,
1474
+ preserveBirthMutationId: rebaseUnnumberedIntent,
1475
+ },
1476
+ );
1477
+ if (captured.state === 'waiting' || captured.state === 'rejected') return captured;
1478
+ }
1479
+
1480
+ const settledReceipts = await sourceReceipts(
1481
+ localCard.path,
1482
+ resolved,
1483
+ resolved,
1484
+ );
1485
+ saveRepoState(previous.repoKey, refreshed, previous, {
1486
+ ...previous,
1487
+ applying: {
1488
+ ...applying,
1489
+ source: resolved,
1490
+ target: resolved,
1491
+ sourceReceipts: settledReceipts,
1492
+ },
1493
+ materialized: false,
1494
+ });
1495
+ return { state: 'ready', card: refreshed };
1496
+ }
1497
+
1498
+ async function finishApplying(project, localCard, previous) {
1499
+ const applying = previous?.applying;
1500
+ if (!validApplying(applying)) {
1501
+ return { state: 'waiting', reason: 'apply-wal-missing' };
1502
+ }
1503
+ const observed = await observePhysicalRepo(localCard.path);
1504
+ if (
1505
+ observed.state !== 'ready'
1506
+ || observed.digest !== stateDigest(applying.target)
1507
+ ) {
1508
+ return {
1509
+ state: 'waiting',
1510
+ reason: observed.state === 'ready'
1511
+ ? 'local-intent-capture-required'
1512
+ : 'local-state-moving',
1513
+ };
1514
+ }
1515
+ const reduced = applying.reduced;
1516
+ const official = targetRepoState(applying.plan, applying.through, reduced);
1517
+ const indexDiff = await merge.diffOfTree(
1518
+ localCard.path,
1519
+ applying.plan.card.head.oid,
1520
+ reduced.indexTree,
1521
+ );
1522
+ const worktreeDiff = await merge.diffOfTree(
1523
+ localCard.path,
1524
+ reduced.indexTree,
1525
+ reduced.worktreeTree,
1526
+ );
1527
+ const verified = saveRepoState(previous.repoKey, localCard, previous, {
1528
+ verificationContract: VERIFICATION_CONTRACT,
1529
+ resourceId: project?.resourceId || null,
1530
+ path: localCard.path,
1531
+ replica: localCard.replica,
1532
+ branch: applying.plan.card.head.branch,
1533
+ baseOid: applying.plan.card.head.oid,
1534
+ cardVersion: applying.plan.card.positionVersion,
1535
+ cardMutationId: applying.plan.card.positionMutationId,
1536
+ targetCard: {
1537
+ branch: applying.plan.card.head.branch,
1538
+ oid: applying.plan.card.head.oid,
1539
+ version: applying.plan.card.positionVersion,
1540
+ mutationId: applying.plan.card.positionMutationId,
1541
+ },
1542
+ appliedThrough: applying.through,
1543
+ checkpointRevision: reduced.checkpointRevision,
1544
+ indexDiff,
1545
+ worktreeDiff,
1546
+ checkpointVersions: applying.plan.transitions.map((entry) => entry.version),
1547
+ branches: applying.plan.branches || [],
1548
+ heldTree: reduced.worktreeTree,
1549
+ heldIndexTree: reduced.indexTree,
1550
+ targetDigest: stateDigest(official),
1551
+ expectedPhysicalDigest: observed.digest,
1552
+ observedPhysicalDigest: observed.digest,
1553
+ materialized: true,
1554
+ receiptOperationId: applying.operationId,
1555
+ });
1556
+ await releaseVerifiedReceipts(previous.repoKey, verified);
1557
+ const foldedIntent = require('./intent').readIntent(project);
1558
+ if (
1559
+ foldedIntent?.publication?.foldedAt
1560
+ && foldedIntent.publication.numberedVersion <= applying.through
1561
+ ) {
1562
+ provenance.requireTransition(
1563
+ provenance.STATE.FOLDED,
1564
+ provenance.ACTION.APPLY,
1565
+ provenance.STATE.OFFICIAL,
1566
+ [
1567
+ 'official-target',
1568
+ 'apply-wal',
1569
+ 'physical-verification',
1570
+ 'applied-cursor',
1571
+ 'intent-retired',
1572
+ ],
1573
+ );
1574
+ require('./intent').clearIntent(project);
1575
+ }
1576
+ return { state: 'in-sync', reason: null, moved: true };
1577
+ }
1578
+
1579
+ function refMap(refs) {
1580
+ return new Map((refs || []).filter((ref) => ref?.name).map((ref) => [ref.name, ref]));
1581
+ }
1582
+
1583
+ /**
1584
+ * A ref differs for only two reasons:
1585
+ *
1586
+ * - it still equals the last verified official value, so it must advance;
1587
+ * - it is an explicit local intent created after that verified value.
1588
+ *
1589
+ * Acknowledgement ends local ownership. From then on, a later official value
1590
+ * wins like any other ordered write.
1591
+ */
1592
+ function classifyBranchRefs(
1593
+ plan,
1594
+ currentRefs,
1595
+ intent = null,
1596
+ ) {
1597
+ const current = refMap(currentRefs);
1598
+ const target = refMap(plan.branches);
1599
+ const base = refMap(intent?.base?.card?.branches);
1600
+ const local = refMap(intent?.local?.card?.branches);
1601
+ const ownedHeadBranch = intentOwnsHead(intent)
1602
+ ? intent.local.card.head?.branch ?? null
1603
+ : null;
1604
+ const checked = plan.card.head?.branch || null;
1605
+ if (checked) {
1606
+ target.set(checked, {
1607
+ name: checked,
1608
+ oid: plan.card.head.oid,
1609
+ mutationId: plan.card.positionMutationId,
1610
+ version: plan.card.positionVersion,
1611
+ });
1612
+ }
1613
+ const names = [...new Set([
1614
+ ...current.keys(),
1615
+ ...target.keys(),
1616
+ ...base.keys(),
1617
+ ...local.keys(),
1618
+ ])].sort();
1619
+ const intents = [];
1620
+ const changes = [];
1621
+
1622
+ for (const name of names) {
1623
+ const currentOid = current.get(name)?.oid ?? null;
1624
+ const targetOid = target.get(name)?.oid ?? null;
1625
+ const baseOid = base.get(name)?.oid ?? null;
1626
+ const localOid = local.get(name)?.oid ?? null;
1627
+ const owned = Boolean(
1628
+ intent
1629
+ && (
1630
+ localOid !== baseOid
1631
+ || name === ownedHeadBranch
1632
+ )
1633
+ );
1634
+ const desiredOid = owned ? localOid : targetOid;
1635
+ if (owned) intents.push({ name, oid: localOid });
1636
+ if (currentOid !== desiredOid) {
1637
+ changes.push({ branch: name, expectedOid: currentOid, oid: desiredOid });
1638
+ }
1639
+ }
1640
+
1641
+ const expected = new Map([...target].map(([name, ref]) => [name, ref.oid ?? null]));
1642
+ for (const intent of intents) expected.set(intent.name, intent.oid);
1643
+ return { changes, expected, pending: intents };
1644
+ }
1645
+
1646
+ async function obtainBranchObjects(localCard, ref) {
1647
+ if (ref.oid === null || await hasCommit(localCard.path, ref.oid)) return null;
1648
+ if (ref.objects?.kind === 'bundle') {
1649
+ const imported = await require('./objects').importFromLocator(localCard.path, ref.objects);
1650
+ if (imported.state !== 'imported') return imported.state;
1651
+ } else {
1652
+ throw new Error(`official branch ${ref.name} has no complete bundle`);
1653
+ }
1654
+ if (!await hasCommit(localCard.path, ref.oid)) {
1655
+ throw new Error(`official branch bundle does not contain ${ref.oid}`);
1656
+ }
1657
+ return null;
1658
+ }
1659
+
1660
+ async function reduceCheckpoint(localCard, plan) {
1661
+ const baseOid = plan.card.head.oid;
1662
+ const baseTree = await merge.treeOf(localCard.path, baseOid);
1663
+ let indexTree = baseTree;
1664
+ let worktreeTree = baseTree;
1665
+
1666
+ for (const operation of plan.transitions) {
1667
+ const from = readCheckpointValue(baseOid, operation.from);
1668
+ const to = readCheckpointValue(baseOid, operation.to);
1669
+ if (from.state !== 'ready' || to.state !== 'ready') {
1670
+ const incomplete = from.state !== 'ready' ? from.state : to.state;
1671
+ if (incomplete === 'downloading') {
1672
+ return { state: 'waiting', reason: 'checkpoint-downloading' };
1673
+ }
1674
+ throw new Error(`official checkpoint is ${incomplete}`);
1675
+ }
1676
+ const fromIndexTree = await merge.treeOfPatch(localCard.path, baseOid, from.indexDiff);
1677
+ const toIndexTree = await merge.treeOfPatch(localCard.path, baseOid, to.indexDiff);
1678
+ const reducedIndex = await merge.reduceTree(
1679
+ localCard.path,
1680
+ indexTree,
1681
+ fromIndexTree,
1682
+ toIndexTree,
1683
+ );
1684
+ indexTree = reducedIndex.tree;
1685
+
1686
+ const fromWorktreeTree = await merge.treeOfPatch(localCard.path, fromIndexTree, from.worktreeDiff);
1687
+ const toWorktreeTree = await merge.treeOfPatch(localCard.path, toIndexTree, to.worktreeDiff);
1688
+ const reducedWorktree = await merge.reduceTree(
1689
+ localCard.path,
1690
+ worktreeTree,
1691
+ fromWorktreeTree,
1692
+ toWorktreeTree,
1693
+ );
1694
+ worktreeTree = reducedWorktree.tree;
1695
+ }
1696
+
1697
+ const indexDiff = await merge.diffOfTree(localCard.path, baseOid, indexTree);
1698
+ const worktreeDiff = await merge.diffOfTree(localCard.path, indexTree, worktreeTree);
1699
+ return {
1700
+ state: 'ready',
1701
+ indexTree,
1702
+ worktreeTree,
1703
+ indexDiff,
1704
+ worktreeDiff,
1705
+ checkpointRevision: revisionOf(baseOid, indexDiff, worktreeDiff),
1706
+ };
1707
+ }
1708
+
1709
+ /**
1710
+ * Official state is all-or-nothing. Download every referenced object and
1711
+ * reduce every checkpoint payload before changing HEAD, refs, files, or the
1712
+ * verified cursor.
1713
+ */
1714
+ async function preparePlan(localCard, plan) {
1715
+ const card = await obtainCardObjects(localCard, plan.card);
1716
+ if (card.state !== 'ready') {
1717
+ return { state: 'waiting', reason: card.state };
1718
+ }
1719
+ for (const ref of plan.branches || []) {
1720
+ if (ref.oid === null) continue;
1721
+ const reason = await obtainBranchObjects(localCard, ref);
1722
+ if (reason) return { state: 'waiting', reason: `branch-${reason}` };
1723
+ }
1724
+ return reduceCheckpoint(localCard, plan);
1725
+ }
1726
+
1727
+ function canonicalValue(value) {
1728
+ if (Array.isArray(value)) return value.map(canonicalValue);
1729
+ if (!value || typeof value !== 'object') return value;
1730
+ return Object.fromEntries(
1731
+ Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]),
1732
+ );
1733
+ }
1734
+
1735
+ function stateDigest(value) {
1736
+ return crypto.createHash('sha256')
1737
+ .update(JSON.stringify(canonicalValue(value)))
1738
+ .digest('hex');
1739
+ }
1740
+
1741
+ function branchState(refs) {
1742
+ return (refs || [])
1743
+ .map((ref) => ({ name: ref.name, oid: ref.oid ?? null }))
1744
+ .sort((left, right) => left.name.localeCompare(right.name));
1745
+ }
1746
+
1747
+ function branchMapState(refs) {
1748
+ return [...refs.entries()]
1749
+ .map(([name, oid]) => ({ name, oid }))
1750
+ .sort((left, right) => left.name.localeCompare(right.name));
1751
+ }
1752
+
1753
+ async function observePhysicalRepo(worktreeRoot) {
1754
+ transition.recoverFileTransactions(worktreeRoot);
1755
+ transition.recoverGitMetadataTransactions(worktreeRoot);
1756
+ const beforeHead = await headAndDirty(worktreeRoot);
1757
+ const beforeBranches = await localBranches(worktreeRoot);
1758
+ const captured = await captureCheckpoint(worktreeRoot);
1759
+ if (captured.indexDiff === null || captured.baseOid !== beforeHead.head.oid) {
1760
+ return { state: 'moving' };
1761
+ }
1762
+ const indexTree = await merge.treeOfPatch(
1763
+ worktreeRoot,
1764
+ captured.baseOid,
1765
+ captured.indexDiff,
1766
+ );
1767
+ const worktreeTree = await merge.treeOfPatch(
1768
+ worktreeRoot,
1769
+ indexTree,
1770
+ captured.worktreeDiff,
1771
+ );
1772
+ const afterHead = await headAndDirty(worktreeRoot);
1773
+ const afterBranches = await localBranches(worktreeRoot);
1774
+ if (
1775
+ !sameCardPosition(beforeHead, afterHead)
1776
+ || JSON.stringify(branchState(beforeBranches)) !== JSON.stringify(branchState(afterBranches))
1777
+ ) return { state: 'moving' };
1778
+ const value = {
1779
+ head: {
1780
+ branch: afterHead.head.branch,
1781
+ oid: afterHead.head.oid,
1782
+ },
1783
+ branches: branchState(afterBranches),
1784
+ indexTree,
1785
+ worktreeTree,
1786
+ };
1787
+ return { state: 'ready', value, digest: stateDigest(value), captured };
1788
+ }
1789
+
1790
+ /**
1791
+ * Close the only gap between local observation and ordered apply.
1792
+ *
1793
+ * A mismatch is only a stop signal here. It never becomes authorship. Once
1794
+ * the lock is released, checkpoint capture may record the change, but only
1795
+ * against the same verified baseline that was already durable before apply.
1796
+ */
1797
+ async function verifyApplyBoundary(project, localCard, receivedThrough) {
1798
+ const previous = readRepoState(project.repoKey, project.resourceId);
1799
+ if (
1800
+ previous?.path !== localCard.path
1801
+ || previous.replica !== localCard.replica
1802
+ ) return { state: 'ready' };
1803
+
1804
+ const observed = await observePhysicalRepo(localCard.path);
1805
+ if (observed.state !== 'ready') {
1806
+ return { state: 'waiting', reason: 'local-state-moving' };
1807
+ }
1808
+
1809
+ const intents = require('./intent');
1810
+ const held = intents.readIntent(project);
1811
+ const successor = held?.successor || null;
1812
+ const owned = intents.readOwnedIntent(project);
1813
+ if (successor) {
1814
+ return observed.digest === successor.local.physicalDigest
1815
+ ? { state: 'ready' }
1816
+ : { state: 'waiting', reason: 'local-intent-capture-required' };
1817
+ }
1818
+ if (owned) {
1819
+ return observed.digest === owned.local.physicalDigest
1820
+ ? { state: 'ready' }
1821
+ : { state: 'waiting', reason: 'local-intent-capture-required' };
1822
+ }
1823
+
1824
+ const acknowledgement = held
1825
+ ? intents.officialAcknowledgement(project, held)
1826
+ : null;
1827
+ if (acknowledgement) {
1828
+ if (acknowledgement.version > receivedThrough) {
1829
+ return { state: 'waiting', reason: 'official-ack-settling' };
1830
+ }
1831
+ return { state: 'ready' };
1832
+ }
1833
+
1834
+ if (
1835
+ previous.verificationContract === VERIFICATION_CONTRACT
1836
+ && previous.materialized === true
1837
+ && observed.digest !== previous.observedPhysicalDigest
1838
+ ) {
1839
+ return { state: 'waiting', reason: 'local-intent-capture-required' };
1840
+ }
1841
+ return { state: 'ready' };
1842
+ }
1843
+
1844
+ function targetRepoState(plan, through, reduced) {
1845
+ const branches = refMap(plan.branches);
1846
+ if (plan.card.head?.branch) {
1847
+ branches.set(plan.card.head.branch, {
1848
+ name: plan.card.head.branch,
1849
+ oid: plan.card.head.oid,
1850
+ });
1851
+ }
1852
+ return {
1853
+ through,
1854
+ head: {
1855
+ branch: plan.card.head?.branch ?? null,
1856
+ oid: plan.card.head?.oid ?? null,
1857
+ },
1858
+ branches: branchState([...branches.values()]),
1859
+ indexTree: reduced.indexTree,
1860
+ worktreeTree: reduced.worktreeTree,
1861
+ };
1862
+ }
1863
+
1864
+ function intentFoldedThrough(intent, through) {
1865
+ return Boolean(
1866
+ intent?.publication
1867
+ && Number.isInteger(intent.publication.numberedVersion)
1868
+ && intent.publication.numberedVersion <= through
1869
+ && typeof intent.publication.foldedAt === 'string'
1870
+ );
1871
+ }
1872
+
1873
+ function physicalRepoState({
1874
+ plan,
1875
+ pendingCard,
1876
+ expectedBranches,
1877
+ indexTree,
1878
+ worktreeTree,
1879
+ }) {
1880
+ const branches = new Map(expectedBranches);
1881
+ const head = pendingCard || {
1882
+ branch: plan.card.head?.branch ?? null,
1883
+ oid: plan.card.head?.oid ?? null,
1884
+ };
1885
+ if (head.branch) branches.set(head.branch, head.oid);
1886
+ return {
1887
+ head: { branch: head.branch, oid: head.oid },
1888
+ branches: branchMapState(branches),
1889
+ indexTree,
1890
+ worktreeTree,
1891
+ };
1892
+ }
1893
+
1894
+ function officialPhysicalState(official) {
1895
+ return {
1896
+ head: official.head,
1897
+ branches: official.branches,
1898
+ indexTree: official.indexTree,
1899
+ worktreeTree: official.worktreeTree,
1900
+ };
1901
+ }
1902
+
1903
+ async function materializeRepoOnce(localCardInput, plan, appliedThrough = null) {
1904
+ const refreshed = localCardInput;
1905
+ const project = require('./wall').projectForRepo(plan.repoKey);
1906
+ const stored = readRepoState(plan.repoKey, project?.resourceId);
1907
+ let previous = stored?.path === refreshed.path
1908
+ && stored.replica === refreshed.replica
1909
+ ? stored
1910
+ : null;
1911
+ const resolvedIntent = await usableIntent(project, previous, refreshed);
1912
+ if (resolvedIntent.state === 'blocked') {
1913
+ return { state: 'waiting', reason: 'local-intent-provenance-unverified' };
1914
+ }
1915
+ const intent = resolvedIntent.intent;
1916
+ const successor = resolvedIntent.successor;
1917
+ const requestedThrough = Number.isInteger(appliedThrough) && appliedThrough >= 0
1918
+ ? appliedThrough
1919
+ : planThrough(plan);
1920
+ const requestedDigest = applyingRequestDigest(requestedThrough, plan);
1921
+ const prepared = await preparePlan(refreshed, plan);
1922
+ if (prepared.state !== 'ready') return prepared;
1923
+ const expectedTargetDigest = stateDigest(
1924
+ targetRepoState(plan, requestedThrough, prepared),
1925
+ );
1926
+ const official = targetRepoState(plan, requestedThrough, prepared);
1927
+ const alreadyVerified = previous?.verificationContract === VERIFICATION_CONTRACT
1928
+ && previous.materialized === true
1929
+ && previous.appliedThrough === requestedThrough
1930
+ && previous.targetDigest === expectedTargetDigest;
1931
+ if (alreadyVerified) {
1932
+ const observed = await observePhysicalRepo(refreshed.path);
1933
+ if (observed.state !== 'ready') {
1934
+ return { state: 'waiting', reason: 'local-state-moving' };
1935
+ }
1936
+ const provenDigests = new Set([
1937
+ previous.observedPhysicalDigest,
1938
+ successor?.local?.physicalDigest,
1939
+ intent?.base?.through === requestedThrough
1940
+ ? intent.local.physicalDigest
1941
+ : null,
1942
+ ].filter(Boolean));
1943
+ if (!provenDigests.has(observed.digest)) {
1944
+ return { state: 'waiting', reason: 'local-intent-capture-required' };
1945
+ }
1946
+ return { state: 'in-sync', reason: null, moved: false };
1947
+ }
1948
+ const observed = await observePhysicalRepo(refreshed.path);
1949
+ if (observed.state !== 'ready') {
1950
+ return { state: 'waiting', reason: 'local-state-moving' };
1951
+ }
1952
+ const retainedDigest = successor?.local?.physicalDigest
1953
+ || intent?.local?.physicalDigest;
1954
+ if (intent && observed.digest !== retainedDigest) {
1955
+ return { state: 'waiting', reason: 'local-intent-capture-required' };
1956
+ }
1957
+ if (
1958
+ !intent
1959
+ && previous?.verificationContract === VERIFICATION_CONTRACT
1960
+ && previous.materialized === true
1961
+ && observed.digest !== previous.observedPhysicalDigest
1962
+ ) {
1963
+ return { state: 'waiting', reason: 'local-intent-capture-required' };
1964
+ }
1965
+ if (
1966
+ !intent
1967
+ && !(
1968
+ previous?.verificationContract === VERIFICATION_CONTRACT
1969
+ && previous.materialized === true
1970
+ )
1971
+ && observed.digest !== stateDigest(officialPhysicalState(official))
1972
+ ) {
1973
+ return { state: 'waiting', reason: 'unknown-local-state' };
1974
+ }
1975
+ const anchors = await applyingAnchors(
1976
+ refreshed,
1977
+ plan,
1978
+ prepared,
1979
+ intent,
1980
+ successor,
1981
+ observed,
1982
+ requestedThrough,
1983
+ );
1984
+ const receipts = await sourceReceipts(
1985
+ refreshed.path,
1986
+ anchors.source,
1987
+ anchors.target,
1988
+ );
1989
+ const sealed = await observePhysicalRepo(refreshed.path);
1990
+ const sealedReceipts = await sourceReceipts(
1991
+ refreshed.path,
1992
+ anchors.source,
1993
+ anchors.target,
1994
+ );
1995
+ if (
1996
+ sealed.state !== 'ready'
1997
+ || sealed.digest !== stateDigest(anchors.source)
1998
+ || !sameReceiptPlan(receipts, sealedReceipts)
1999
+ ) {
2000
+ return { state: 'waiting', reason: 'local-state-moving' };
2001
+ }
2002
+ const operationId = applyingId(requestedDigest, anchors.source, anchors.target);
2003
+ saveRepoState(plan.repoKey, refreshed, previous, {
2004
+ ...(previous || {}),
2005
+ resourceId: project?.resourceId || null,
2006
+ path: refreshed.path,
2007
+ replica: refreshed.replica,
2008
+ applying: {
2009
+ contract: APPLYING_CONTRACT,
2010
+ through: requestedThrough,
2011
+ requestDigest: requestedDigest,
2012
+ plan: recoveryPlan(plan),
2013
+ reduced: {
2014
+ state: 'ready',
2015
+ indexTree: prepared.indexTree,
2016
+ worktreeTree: prepared.worktreeTree,
2017
+ checkpointRevision: prepared.checkpointRevision,
2018
+ },
2019
+ ...anchors,
2020
+ operationId,
2021
+ sourceReceipts: sealedReceipts,
2022
+ },
2023
+ materialized: false,
2024
+ });
2025
+ return materializeRepo(refreshed, plan, requestedThrough);
2026
+ }
2027
+
2028
+ /**
2029
+ * One durable apply is finished before any newer rail target. Its stored
2030
+ * source, official state, and local-preserving target let restart finish the
2031
+ * same transition without reclassifying partial output as a new local write.
2032
+ */
2033
+ async function materializeRepo(localCardInput, plan, appliedThrough = null) {
2034
+ const project = require('./wall').projectForRepo(plan.repoKey);
2035
+ let stored = readRepoState(plan.repoKey, project?.resourceId);
2036
+ if (stored?.receiptOperationId) {
2037
+ stored = await releaseVerifiedReceipts(plan.repoKey, stored);
2038
+ }
2039
+ if (validHydrating(stored?.hydrating, plan.repoKey, stored?.path)) {
2040
+ return hydrateRepo({ ...project, path: stored.path }, plan);
2041
+ }
2042
+ let previous = stored?.path === localCardInput.path
2043
+ && stored.replica === localCardInput.replica
2044
+ ? stored
2045
+ : null;
2046
+ if (!validApplying(previous?.applying)) {
2047
+ return materializeRepoOnce(localCardInput, plan, appliedThrough);
2048
+ }
2049
+ const interruptedRequest = previous.applying;
2050
+ let recoveryCard = localCardInput;
2051
+ const recovery = await recoverApplyingIntent(project, recoveryCard, previous);
2052
+ if (recovery.state !== 'ready') return recovery;
2053
+ recoveryCard = recovery.card;
2054
+ previous = readRepoState(plan.repoKey, project?.resourceId) || previous;
2055
+
2056
+ const recovered = await finishApplying(project, recoveryCard, previous);
2057
+ if (recovered.state !== 'in-sync') return recovered;
2058
+
2059
+ const requestedThrough = Number.isInteger(appliedThrough) && appliedThrough >= 0
2060
+ ? appliedThrough
2061
+ : interruptedRequest.through;
2062
+ if (
2063
+ interruptedRequest.requestDigest
2064
+ === applyingRequestDigest(requestedThrough, plan)
2065
+ ) return recovered;
2066
+
2067
+ const refreshed = await refreshRepoCard(recoveryCard.replica) || recoveryCard;
2068
+ return materializeRepoOnce(refreshed, plan, appliedThrough);
2069
+ }
2070
+
2071
+ async function advanceIndex(worktreeRoot, observedTree, targetTree, operationId = null) {
2072
+ if (observedTree === targetTree) return { state: 'ready', moved: false };
2073
+ const patch = await merge.diffOfTree(worktreeRoot, observedTree, targetTree);
2074
+ try {
2075
+ await transition.applyIndexPatch(
2076
+ worktreeRoot,
2077
+ patch,
2078
+ operationId,
2079
+ {
2080
+ expectedTree: observedTree,
2081
+ targetTree,
2082
+ },
2083
+ );
2084
+ return { state: 'ready', moved: true };
2085
+ } catch (error) {
2086
+ if (error instanceof transition.TransitionError) {
2087
+ return { state: 'waiting', reason: 'local-index-moving', moved: false };
2088
+ }
2089
+ throw error;
2090
+ }
2091
+ }
2092
+
2093
+ function localCardMayMaterialize(publishedCard, publication) {
2094
+ return Boolean(publishedCard)
2095
+ || publication?.mayMaterialize === true;
2096
+ }
2097
+
2098
+ function pathForRepo(localCard, project) {
2099
+ return localCard?.path || project?.path || null;
2100
+ }
2101
+
2102
+ function snapshotIsCurrent(receivedThrough, snapshotThrough) {
2103
+ return Number.isInteger(receivedThrough)
2104
+ && receivedThrough >= 0
2105
+ && snapshotThrough === receivedThrough;
2106
+ }
2107
+
2108
+ /**
2109
+ * A pass either proved the repo current, exposed terminal corruption, or has
2110
+ * more ordinary work to do. Every non-terminal wait uses the same bounded
2111
+ * retry; reasons are diagnostics, never control flow.
2112
+ */
2113
+ function repairDelayForPass(outcomes, moved = false) {
2114
+ return moved || outcomes.some((outcome) => outcome?.state === 'waiting')
2115
+ ? REPAIR_RETRY_MS
2116
+ : null;
2117
+ }
2118
+
2119
+ function readOfficialPlan(repoKey, project, wall) {
2120
+ const receivedThrough = wall.publicationStatus(repoKey).receivedThrough;
2121
+ const snapshot = require('./project-stream').readSnapshot(
2122
+ project.file,
2123
+ project.resourceId,
2124
+ );
2125
+ const snapshotThrough = snapshot.version;
2126
+ if (!snapshotIsCurrent(receivedThrough, snapshotThrough)) {
2127
+ return {
2128
+ state: 'waiting',
2129
+ reason: 'official-snapshot-behind',
2130
+ receivedThrough,
2131
+ snapshotThrough,
2132
+ };
2133
+ }
2134
+ const plan = reduceWall(wall.readWall(repoKey)).get(repoKey);
2135
+ if (!plan?.card) {
2136
+ return {
2137
+ state: 'waiting',
2138
+ reason: 'official-card-settling',
2139
+ receivedThrough,
2140
+ snapshotThrough,
2141
+ };
2142
+ }
2143
+ return { state: 'ready', receivedThrough, snapshotThrough, plan };
2144
+ }
2145
+
2146
+ async function runReducer() {
2147
+ service.counters.passes += 1;
787
2148
  const startedAt = Date.now();
788
2149
  const wall = require('./wall');
789
2150
  const plans = reduceWall(wall.readWall());
790
2151
  const locals = new Map();
2152
+ const visited = new Set();
791
2153
  for (const card of listRepoCards()) {
792
2154
  if (card.kind !== 'primary' || card.health !== 'healthy') continue;
793
- const repoKey = repoKeyOfCard(card);
2155
+ const repoKey = wall.projectForPath(card.path)?.repoKey;
794
2156
  if (repoKey && !locals.has(repoKey)) locals.set(repoKey, card);
795
2157
  }
796
2158
 
797
2159
  let moved = false;
798
- for (const [repoKey, plan] of plans) {
2160
+ for (const [repoKey] of plans) {
2161
+ visited.add(repoKey);
799
2162
  let local = locals.get(repoKey);
800
2163
  const project = wall.projectForRepo(repoKey);
801
- if (!plan.card) continue;
802
2164
  try {
2165
+ const official = readOfficialPlan(repoKey, project, wall);
2166
+ if (official.state !== 'ready') {
2167
+ const outcome = { ...official, moved: false };
2168
+ service.repos.set(repoKey, {
2169
+ repoKey,
2170
+ path: pathForRepo(local, project),
2171
+ ...outcome,
2172
+ at: new Date().toISOString(),
2173
+ });
2174
+ service.counters.waiting += 1;
2175
+ continue;
2176
+ }
803
2177
  if (!local?.head?.oid) {
804
2178
  const hydrated = await withMaterialization(
805
2179
  repoKey,
806
- () => hydrateRepo(project, plan),
2180
+ () => hydrateRepo(project, official.plan),
807
2181
  );
808
2182
  service.repos.set(repoKey, {
809
2183
  repoKey,
@@ -819,10 +2193,9 @@ async function runReducer() {
819
2193
  continue;
820
2194
  }
821
2195
  const refreshed = await refreshRepoCard(local.replica) || local;
822
- const publishedCard = wall.publishedCardFor(refreshed);
823
- const publication = publishedCard ? null : wall.publishWall(repoKey);
824
- if (!localCardMayMaterialize(publishedCard, publication)) {
825
- const outcome = { state: 'waiting', reason: 'local-card-publishing', moved: false };
2196
+ const capture = await captureIntentForCard(project, refreshed);
2197
+ if (capture.state === 'waiting' && capture.mayMaterialize !== true) {
2198
+ const outcome = { state: 'waiting', reason: capture.reason, moved: false };
826
2199
  service.repos.set(repoKey, {
827
2200
  repoKey,
828
2201
  path: refreshed.path,
@@ -832,10 +2205,19 @@ async function runReducer() {
832
2205
  service.counters.waiting += 1;
833
2206
  continue;
834
2207
  }
835
- const receivedThrough = wall.publicationStatus(repoKey).receivedThrough;
836
- const currentPlan = reduceWall(wall.readWall(repoKey)).get(repoKey);
837
- if (!currentPlan?.card) {
838
- const outcome = { state: 'waiting', reason: 'official-card-settling', moved: false };
2208
+ if (capture.state === 'rejected') {
2209
+ throw new Error(
2210
+ capture.intent?.terminal?.message
2211
+ || `local code intent rejected: ${capture.reason}`,
2212
+ );
2213
+ }
2214
+ const publishedCard = wall.publishedCardFor(refreshed);
2215
+ const publication = publishedCard ? null : wall.publishWall(repoKey);
2216
+ if (publication?.terminalError) {
2217
+ throw new Error(`code publication failed: ${publication.terminalError}`);
2218
+ }
2219
+ if (!localCardMayMaterialize(publishedCard, publication)) {
2220
+ const outcome = { state: 'waiting', reason: 'local-card-publishing', moved: false };
839
2221
  service.repos.set(repoKey, {
840
2222
  repoKey,
841
2223
  path: refreshed.path,
@@ -845,10 +2227,25 @@ async function runReducer() {
845
2227
  service.counters.waiting += 1;
846
2228
  continue;
847
2229
  }
848
- const outcome = await withMaterialization(
849
- repoKey,
850
- () => materializeRepo(refreshed, currentPlan, receivedThrough),
851
- );
2230
+ const outcome = await withMaterialization(repoKey, async () => {
2231
+ const boundary = await verifyApplyBoundary(
2232
+ project,
2233
+ refreshed,
2234
+ official.receivedThrough,
2235
+ );
2236
+ if (boundary.state !== 'ready') return { ...boundary, moved: false };
2237
+ return materializeRepo(refreshed, official.plan, official.receivedThrough);
2238
+ });
2239
+ if (outcome.reason === 'local-intent-capture-required') {
2240
+ const latest = await refreshRepoCard(refreshed.replica) || refreshed;
2241
+ const retryCapture = await captureIntentForCard(project, latest);
2242
+ if (retryCapture.state === 'rejected') {
2243
+ throw new Error(
2244
+ retryCapture.intent?.terminal?.message
2245
+ || `local code intent rejected: ${retryCapture.reason}`,
2246
+ );
2247
+ }
2248
+ }
852
2249
  service.repos.set(repoKey, { repoKey, path: local.path, ...outcome, at: new Date().toISOString() });
853
2250
  if (outcome.state === 'waiting') service.counters.waiting += 1;
854
2251
  if (outcome.moved) {
@@ -870,6 +2267,10 @@ async function runReducer() {
870
2267
  }
871
2268
  service.lastPass = { at: new Date().toISOString(), durationMs: Date.now() - startedAt, moved };
872
2269
  if (moved) refreshRepoCards().catch(() => {});
2270
+ return repairDelayForPass(
2271
+ [...visited].map((repoKey) => service.repos.get(repoKey)),
2272
+ moved,
2273
+ );
873
2274
  }
874
2275
 
875
2276
  let running = null;
@@ -880,23 +2281,25 @@ function reduceNow() {
880
2281
  queued = true;
881
2282
  return running;
882
2283
  }
2284
+ let repairDelay = null;
883
2285
  running = (async () => {
884
2286
  do {
885
2287
  queued = false;
886
- await runReducer();
2288
+ repairDelay = await runReducer();
887
2289
  } while (queued);
888
2290
  })().finally(() => {
889
2291
  running = null;
2292
+ if (repairDelay !== null) scheduleReduce(repairDelay);
890
2293
  });
891
2294
  return running;
892
2295
  }
893
2296
 
894
- function scheduleReduce() {
2297
+ function scheduleReduce(delayMs = REDUCE_QUIET_MS) {
895
2298
  if (!service.started || service.timer) return;
896
2299
  service.timer = setTimeout(() => {
897
2300
  service.timer = null;
898
2301
  reduceNow().catch((error) => console.warn('[WorkspaceReducer] pass failed:', error.message));
899
- }, REDUCE_QUIET_MS);
2302
+ }, delayMs);
900
2303
  service.timer.unref?.();
901
2304
  }
902
2305
 
@@ -909,8 +2312,7 @@ function startWorkspaceReducer() {
909
2312
  const acknowledged = event.resource === 'shared-mutations'
910
2313
  && wallState.projects.some((project) => project.resourceId === event.sharedResourceId);
911
2314
  if (
912
- event.resource === 'repo-cards'
913
- || event.resource === 'repo-checkpoints'
2315
+ event.resource === 'repo-checkpoints'
914
2316
  || wallPaths.includes(event.id)
915
2317
  || acknowledged
916
2318
  ) scheduleReduce();
@@ -942,19 +2344,26 @@ function reducerStats() {
942
2344
 
943
2345
  module.exports = {
944
2346
  advanceIndex,
945
- captureIsOnRail,
2347
+ applyHydrationBootstrap,
2348
+ classifyBranchRefs,
946
2349
  hydrateRepo,
947
2350
  isMaterializing,
948
2351
  localCardMayMaterialize,
949
2352
  materializeRepo,
2353
+ observePhysicalRepo,
950
2354
  pathForRepo,
951
- parkedForState,
952
2355
  readRepoState,
953
2356
  reduceCheckpoint,
954
2357
  reduceNow,
955
2358
  reduceWall,
2359
+ readOfficialPlan,
2360
+ repairDelayForPass,
956
2361
  reducerStats,
2362
+ snapshotIsCurrent,
2363
+ stateDigest,
957
2364
  startWorkspaceReducer,
958
2365
  stopWorkspaceReducer,
2366
+ verifyApplyBoundary,
959
2367
  withMaterialization,
2368
+ withRepoTurn,
960
2369
  };