amalgm 0.1.245 → 0.1.246

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