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