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