amalgm 0.1.238 → 0.1.240
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/shared-realtime-tunnel.js +22 -14
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Durable local authorship for one registered code folder.
|
|
5
|
+
*
|
|
6
|
+
* The official stream still has only two values: card and checkpoint. This
|
|
7
|
+
* file is not shared state. It records the exact local state a real file/Git
|
|
8
|
+
* observation saw, relative to the last verified official state on this
|
|
9
|
+
* machine. The reducer may preserve only this record; a physical mismatch by
|
|
10
|
+
* itself never creates authorship.
|
|
11
|
+
*
|
|
12
|
+
* Axiom 4: a reserved predecessor is immutable and later work occupies one
|
|
13
|
+
* successor pinned to its exact content. Proven by workspace-local-intent.test.js.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { AMALGM_DIR } = require('../config');
|
|
20
|
+
const durable = require('./durable');
|
|
21
|
+
const provenance = require('./provenance');
|
|
22
|
+
|
|
23
|
+
const INTENT_CONTRACT = 'repo-local-intent@4';
|
|
24
|
+
const LEGACY_INTENT_CONTRACTS = new Set([
|
|
25
|
+
'repo-local-intent@2',
|
|
26
|
+
'repo-local-intent@3',
|
|
27
|
+
]);
|
|
28
|
+
const SUCCESSOR_CONTRACT = 'repo-local-intent-successor@2';
|
|
29
|
+
const LEGACY_SUCCESSOR_CONTRACT = 'repo-local-intent-successor@1';
|
|
30
|
+
const INTENT_DIR = path.join(AMALGM_DIR, 'state', 'repo-local-intent');
|
|
31
|
+
const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
32
|
+
const MUTATION_ID = /^[A-Za-z0-9_-]{16,160}$/;
|
|
33
|
+
|
|
34
|
+
function newMutationId() {
|
|
35
|
+
return `mut_${crypto.randomBytes(18).toString('base64url')}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function intentFile(resourceId) {
|
|
39
|
+
const digest = crypto.createHash('sha256').update(resourceId).digest('hex').slice(0, 24);
|
|
40
|
+
return path.join(INTENT_DIR, `${digest}.json`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function canonicalPath(localPath) {
|
|
44
|
+
const resolved = path.resolve(localPath);
|
|
45
|
+
const tail = [];
|
|
46
|
+
let existing = resolved;
|
|
47
|
+
while (!fs.existsSync(existing)) {
|
|
48
|
+
const parent = path.dirname(existing);
|
|
49
|
+
if (parent === existing) return resolved;
|
|
50
|
+
tail.unshift(path.basename(existing));
|
|
51
|
+
existing = parent;
|
|
52
|
+
}
|
|
53
|
+
return path.join(fs.realpathSync.native(existing), ...tail);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function branchState(branches) {
|
|
57
|
+
return (branches || [])
|
|
58
|
+
.map((branch) => ({ name: branch.name, oid: branch.oid ?? null }))
|
|
59
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cardState(card) {
|
|
63
|
+
return {
|
|
64
|
+
head: {
|
|
65
|
+
branch: card?.head?.branch ?? null,
|
|
66
|
+
oid: card?.head?.oid ?? null,
|
|
67
|
+
detached: Boolean(card?.head?.detached),
|
|
68
|
+
},
|
|
69
|
+
branches: branchState(card?.branches || card?.refs),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function semanticState(value) {
|
|
74
|
+
return {
|
|
75
|
+
card: cardState(value?.local?.card),
|
|
76
|
+
checkpoint: {
|
|
77
|
+
baseOid: value?.local?.checkpoint?.baseOid ?? null,
|
|
78
|
+
branch: value?.local?.checkpoint?.branch ?? null,
|
|
79
|
+
revision: value?.local?.checkpoint?.value?.revision ?? null,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function semanticDigest(value) {
|
|
85
|
+
return crypto.createHash('sha256')
|
|
86
|
+
.update(JSON.stringify(semanticState(value)))
|
|
87
|
+
.digest('hex');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validTree(value) {
|
|
91
|
+
return SHA.test(String(value || ''));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validInitialBase(base) {
|
|
95
|
+
return Boolean(
|
|
96
|
+
base?.through === 0
|
|
97
|
+
&& base.card === null
|
|
98
|
+
&& validTree(base.indexTree)
|
|
99
|
+
&& base.indexTree === base.worktreeTree
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validLocal(local) {
|
|
104
|
+
return Boolean(
|
|
105
|
+
validTree(local?.card?.head?.oid)
|
|
106
|
+
&& Array.isArray(local?.card?.branches)
|
|
107
|
+
&& validTree(local?.indexTree)
|
|
108
|
+
&& validTree(local?.worktreeTree)
|
|
109
|
+
&& local?.checkpoint?.baseOid === local.card.head.oid
|
|
110
|
+
&& typeof local?.physicalDigest === 'string'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sameCard(left, right) {
|
|
115
|
+
return JSON.stringify(cardState(left)) === JSON.stringify(cardState(right));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validSuccessor(value, predecessor) {
|
|
119
|
+
const currentContract = value?.contract === SUCCESSOR_CONTRACT;
|
|
120
|
+
const legacyContract = value?.contract === LEGACY_SUCCESSOR_CONTRACT;
|
|
121
|
+
return Boolean(
|
|
122
|
+
value
|
|
123
|
+
&& (currentContract || legacyContract)
|
|
124
|
+
&& (!currentContract || MUTATION_ID.test(value.birthMutationId))
|
|
125
|
+
&& value.predecessorSemanticDigest === predecessor.semanticDigest
|
|
126
|
+
&& value.base?.semanticDigest === predecessor.semanticDigest
|
|
127
|
+
&& sameCard(value.base?.card, predecessor.local.card)
|
|
128
|
+
&& value.base?.indexTree === predecessor.local.indexTree
|
|
129
|
+
&& value.base?.worktreeTree === predecessor.local.worktreeTree
|
|
130
|
+
&& value.base?.physicalDigest === predecessor.local.physicalDigest
|
|
131
|
+
&& validLocal(value.local)
|
|
132
|
+
&& value.semanticDigest === semanticDigest(value)
|
|
133
|
+
&& typeof value.capturedAt === 'string'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validIntent(value, project) {
|
|
138
|
+
const initial = validInitialBase(value?.base);
|
|
139
|
+
const currentContract = value?.contract === INTENT_CONTRACT;
|
|
140
|
+
const legacyContract = LEGACY_INTENT_CONTRACTS.has(value?.contract)
|
|
141
|
+
&& (
|
|
142
|
+
value.contract !== 'repo-local-intent@2'
|
|
143
|
+
|| value.successor === undefined
|
|
144
|
+
|| value.successor === null
|
|
145
|
+
);
|
|
146
|
+
return Boolean(
|
|
147
|
+
value
|
|
148
|
+
&& (currentContract || legacyContract)
|
|
149
|
+
&& (!currentContract || MUTATION_ID.test(value.birthMutationId))
|
|
150
|
+
&& value.resourceId === project.resourceId
|
|
151
|
+
&& value.ownerUserId === project.ownerUserId
|
|
152
|
+
&& canonicalPath(value.path) === canonicalPath(project.path)
|
|
153
|
+
&& Number.isInteger(value.base?.through)
|
|
154
|
+
&& value.base.through >= 0
|
|
155
|
+
&& (
|
|
156
|
+
initial
|
|
157
|
+
|| (
|
|
158
|
+
validTree(value.base?.card?.head?.oid)
|
|
159
|
+
&& Array.isArray(value.base?.card?.branches)
|
|
160
|
+
&& validTree(value.base?.indexTree)
|
|
161
|
+
&& validTree(value.base?.worktreeTree)
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
&& validLocal(value.local)
|
|
165
|
+
&& value.semanticDigest === semanticDigest(value)
|
|
166
|
+
&& (
|
|
167
|
+
value.successor === undefined
|
|
168
|
+
|| value.successor === null
|
|
169
|
+
|| validSuccessor(value.successor, value)
|
|
170
|
+
)
|
|
171
|
+
&& (
|
|
172
|
+
value.publication === null
|
|
173
|
+
|| (
|
|
174
|
+
typeof value.publication?.mutationId === 'string'
|
|
175
|
+
&& (
|
|
176
|
+
!currentContract
|
|
177
|
+
|| value.publication.mutationId === value.birthMutationId
|
|
178
|
+
)
|
|
179
|
+
&& value.publication.semanticDigest === value.semanticDigest
|
|
180
|
+
&& value.publication.baseThrough === value.base.through
|
|
181
|
+
&& (
|
|
182
|
+
(
|
|
183
|
+
value.publication.numberedVersion === null
|
|
184
|
+
&& value.publication.numberedAt === null
|
|
185
|
+
&& value.publication.foldedAt === null
|
|
186
|
+
)
|
|
187
|
+
|| (
|
|
188
|
+
Number.isInteger(value.publication.numberedVersion)
|
|
189
|
+
&& value.publication.numberedVersion > 0
|
|
190
|
+
&& typeof value.publication.numberedAt === 'string'
|
|
191
|
+
&& (
|
|
192
|
+
value.publication.foldedAt === null
|
|
193
|
+
|| typeof value.publication.foldedAt === 'string'
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function persistIntent(project, value) {
|
|
203
|
+
if (!validIntent(value, project)) throw new Error('local code intent is invalid');
|
|
204
|
+
durable.writeJson(intentFile(project.resourceId), value, { pretty: true });
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readIntent(project) {
|
|
209
|
+
if (!project?.resourceId || !project?.path || !project?.ownerUserId) return null;
|
|
210
|
+
const file = intentFile(project.resourceId);
|
|
211
|
+
let raw;
|
|
212
|
+
try {
|
|
213
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error?.code === 'ENOENT') return null;
|
|
216
|
+
throw new Error(`cannot read local code intent ${file}`, { cause: error });
|
|
217
|
+
}
|
|
218
|
+
let value;
|
|
219
|
+
try {
|
|
220
|
+
value = JSON.parse(raw);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new Error(`local code intent ${file} is not valid JSON`, { cause: error });
|
|
223
|
+
}
|
|
224
|
+
if (!validIntent(value, project)) {
|
|
225
|
+
throw new Error(`local code intent ${file} has an invalid or unsupported contract`);
|
|
226
|
+
}
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writeIntent(project, input) {
|
|
231
|
+
const held = readIntent(project);
|
|
232
|
+
const nextSemanticDigest = semanticDigest({ local: input.local });
|
|
233
|
+
const samePredecessor = held
|
|
234
|
+
&& held.semanticDigest === nextSemanticDigest
|
|
235
|
+
&& JSON.stringify(held.base) === JSON.stringify(input.base);
|
|
236
|
+
let birthMutationId = MUTATION_ID.test(String(input.birthMutationId || ''))
|
|
237
|
+
? input.birthMutationId
|
|
238
|
+
: null;
|
|
239
|
+
if (
|
|
240
|
+
!birthMutationId
|
|
241
|
+
&& (
|
|
242
|
+
input.preserveBirthMutationId === true
|
|
243
|
+
|| samePredecessor
|
|
244
|
+
)
|
|
245
|
+
&& MUTATION_ID.test(String(held?.birthMutationId || ''))
|
|
246
|
+
) birthMutationId = held.birthMutationId;
|
|
247
|
+
if (
|
|
248
|
+
!birthMutationId
|
|
249
|
+
&& MUTATION_ID.test(String(input.publication?.mutationId || ''))
|
|
250
|
+
) birthMutationId = input.publication.mutationId;
|
|
251
|
+
if (!birthMutationId) birthMutationId = newMutationId();
|
|
252
|
+
const next = {
|
|
253
|
+
contract: INTENT_CONTRACT,
|
|
254
|
+
birthMutationId,
|
|
255
|
+
resourceId: project.resourceId,
|
|
256
|
+
ownerUserId: project.ownerUserId,
|
|
257
|
+
path: canonicalPath(project.path),
|
|
258
|
+
base: input.base,
|
|
259
|
+
local: input.local,
|
|
260
|
+
publication: input.publication || null,
|
|
261
|
+
terminal: input.terminal || null,
|
|
262
|
+
capturedAt: new Date().toISOString(),
|
|
263
|
+
};
|
|
264
|
+
next.semanticDigest = nextSemanticDigest;
|
|
265
|
+
next.successor = input.successor === undefined && samePredecessor
|
|
266
|
+
? held.successor || null
|
|
267
|
+
: input.successor || null;
|
|
268
|
+
return persistIntent(project, next);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Freeze the published intent and atomically replace only its open successor.
|
|
273
|
+
*
|
|
274
|
+
* A successor is a final desk state, not an edit log. Its base is the exact
|
|
275
|
+
* content identity of the frozen predecessor, so repeated saves coalesce
|
|
276
|
+
* without ever changing the bytes or publication identity already in flight.
|
|
277
|
+
*/
|
|
278
|
+
function writeSuccessor(project, predecessor, input) {
|
|
279
|
+
let held = readIntent(project);
|
|
280
|
+
if (!held || held.semanticDigest !== predecessor?.semanticDigest) return null;
|
|
281
|
+
if (
|
|
282
|
+
held.contract !== INTENT_CONTRACT
|
|
283
|
+
|| !MUTATION_ID.test(String(held.birthMutationId || ''))
|
|
284
|
+
) {
|
|
285
|
+
held = writeIntent(project, {
|
|
286
|
+
base: held.base,
|
|
287
|
+
local: held.local,
|
|
288
|
+
terminal: held.terminal,
|
|
289
|
+
successor: held.successor,
|
|
290
|
+
publication: held.publication,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const prior = held.successor || null;
|
|
294
|
+
const successor = {
|
|
295
|
+
contract: SUCCESSOR_CONTRACT,
|
|
296
|
+
birthMutationId: null,
|
|
297
|
+
predecessorSemanticDigest: held.semanticDigest,
|
|
298
|
+
base: {
|
|
299
|
+
semanticDigest: held.semanticDigest,
|
|
300
|
+
card: held.local.card,
|
|
301
|
+
indexTree: held.local.indexTree,
|
|
302
|
+
worktreeTree: held.local.worktreeTree,
|
|
303
|
+
physicalDigest: held.local.physicalDigest,
|
|
304
|
+
},
|
|
305
|
+
local: input.local,
|
|
306
|
+
terminal: input.terminal || null,
|
|
307
|
+
capturedAt: new Date().toISOString(),
|
|
308
|
+
};
|
|
309
|
+
successor.semanticDigest = semanticDigest(successor);
|
|
310
|
+
successor.birthMutationId = (
|
|
311
|
+
prior?.semanticDigest === successor.semanticDigest
|
|
312
|
+
&& MUTATION_ID.test(String(prior.birthMutationId || ''))
|
|
313
|
+
) ? prior.birthMutationId : newMutationId();
|
|
314
|
+
persistIntent(project, { ...held, contract: INTENT_CONTRACT, successor });
|
|
315
|
+
return successor;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function readSuccessor(project) {
|
|
319
|
+
return readIntent(project)?.successor || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function clearSuccessor(project, predecessor = null) {
|
|
323
|
+
let held = readIntent(project);
|
|
324
|
+
if (
|
|
325
|
+
!held?.successor
|
|
326
|
+
|| (
|
|
327
|
+
predecessor
|
|
328
|
+
&& held.semanticDigest !== predecessor.semanticDigest
|
|
329
|
+
)
|
|
330
|
+
) return false;
|
|
331
|
+
if (
|
|
332
|
+
held.contract !== INTENT_CONTRACT
|
|
333
|
+
|| !MUTATION_ID.test(String(held.birthMutationId || ''))
|
|
334
|
+
) {
|
|
335
|
+
held = writeIntent(project, {
|
|
336
|
+
base: held.base,
|
|
337
|
+
local: held.local,
|
|
338
|
+
terminal: held.terminal,
|
|
339
|
+
successor: held.successor,
|
|
340
|
+
publication: held.publication,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
persistIntent(project, { ...held, contract: INTENT_CONTRACT, successor: null });
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function clearIntent(project) {
|
|
348
|
+
if (!project?.resourceId) return false;
|
|
349
|
+
const file = intentFile(project.resourceId);
|
|
350
|
+
if (!fs.existsSync(file)) return false;
|
|
351
|
+
fs.rmSync(file, { force: true });
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function stampPublication(project, intent, mutationId) {
|
|
356
|
+
const held = readIntent(project);
|
|
357
|
+
if (
|
|
358
|
+
!held
|
|
359
|
+
|| held.semanticDigest !== intent?.semanticDigest
|
|
360
|
+
|| mutationId !== held.birthMutationId
|
|
361
|
+
) return null;
|
|
362
|
+
return writeIntent(project, {
|
|
363
|
+
birthMutationId: held.birthMutationId,
|
|
364
|
+
base: held.base,
|
|
365
|
+
local: held.local,
|
|
366
|
+
terminal: held.terminal,
|
|
367
|
+
publication: {
|
|
368
|
+
mutationId,
|
|
369
|
+
semanticDigest: held.semanticDigest,
|
|
370
|
+
baseThrough: held.base.through,
|
|
371
|
+
numberedVersion: null,
|
|
372
|
+
numberedAt: null,
|
|
373
|
+
foldedAt: null,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function reservePublication(project, intent) {
|
|
379
|
+
let held = readIntent(project);
|
|
380
|
+
if (!held || held.semanticDigest !== intent?.semanticDigest) return null;
|
|
381
|
+
if (!MUTATION_ID.test(String(held.birthMutationId || ''))) {
|
|
382
|
+
held = writeIntent(project, {
|
|
383
|
+
base: held.base,
|
|
384
|
+
local: held.local,
|
|
385
|
+
terminal: held.terminal,
|
|
386
|
+
successor: held.successor,
|
|
387
|
+
publication: held.publication,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (
|
|
391
|
+
held.publication
|
|
392
|
+
&& held.publication.semanticDigest === held.semanticDigest
|
|
393
|
+
&& held.publication.baseThrough === held.base.through
|
|
394
|
+
&& held.publication.numberedVersion === null
|
|
395
|
+
) return held;
|
|
396
|
+
provenance.requireTransition(
|
|
397
|
+
provenance.publicationState(held.publication),
|
|
398
|
+
provenance.ACTION.RESERVE,
|
|
399
|
+
provenance.STATE.RESERVED,
|
|
400
|
+
['intent-record', 'publication-id'],
|
|
401
|
+
);
|
|
402
|
+
return stampPublication(
|
|
403
|
+
project,
|
|
404
|
+
held,
|
|
405
|
+
held.birthMutationId,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function markPublicationNumbered(project, mutationId, version, numberedAt = null) {
|
|
410
|
+
const held = readIntent(project);
|
|
411
|
+
if (!held || held.publication?.mutationId !== mutationId) return null;
|
|
412
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
413
|
+
throw new Error('numbered publication version is invalid');
|
|
414
|
+
}
|
|
415
|
+
if (
|
|
416
|
+
held.publication.numberedVersion !== null
|
|
417
|
+
&& held.publication.numberedVersion !== version
|
|
418
|
+
) throw new Error('publication received a different official number');
|
|
419
|
+
if (held.publication.numberedVersion === version) return held;
|
|
420
|
+
provenance.requireTransition(
|
|
421
|
+
provenance.publicationState(held.publication),
|
|
422
|
+
provenance.ACTION.NUMBER,
|
|
423
|
+
provenance.STATE.NUMBERED,
|
|
424
|
+
['publication-id', 'official-version', 'server-receipt'],
|
|
425
|
+
);
|
|
426
|
+
return writeIntent(project, {
|
|
427
|
+
base: held.base,
|
|
428
|
+
local: held.local,
|
|
429
|
+
terminal: held.terminal,
|
|
430
|
+
publication: {
|
|
431
|
+
...held.publication,
|
|
432
|
+
numberedVersion: version,
|
|
433
|
+
numberedAt: numberedAt || new Date().toISOString(),
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function markPublicationFolded(project, mutationId, version, numberedAt = null) {
|
|
439
|
+
const numbered = markPublicationNumbered(project, mutationId, version, numberedAt);
|
|
440
|
+
if (!numbered || numbered.publication.foldedAt !== null) return numbered;
|
|
441
|
+
provenance.requireTransition(
|
|
442
|
+
provenance.publicationState(numbered.publication),
|
|
443
|
+
provenance.ACTION.FOLD,
|
|
444
|
+
provenance.STATE.FOLDED,
|
|
445
|
+
['predecessor-folded', 'official-event', 'official-snapshot'],
|
|
446
|
+
);
|
|
447
|
+
return writeIntent(project, {
|
|
448
|
+
base: numbered.base,
|
|
449
|
+
local: numbered.local,
|
|
450
|
+
terminal: numbered.terminal,
|
|
451
|
+
publication: {
|
|
452
|
+
...numbered.publication,
|
|
453
|
+
foldedAt: new Date().toISOString(),
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function officialAcknowledgement(project, intent) {
|
|
459
|
+
if (!intent?.publication?.mutationId) return null;
|
|
460
|
+
if (
|
|
461
|
+
Number.isInteger(intent.publication.numberedVersion)
|
|
462
|
+
&& intent.publication.numberedVersion > 0
|
|
463
|
+
&& typeof intent.publication.numberedAt === 'string'
|
|
464
|
+
) {
|
|
465
|
+
return {
|
|
466
|
+
mutationId: intent.publication.mutationId,
|
|
467
|
+
version: intent.publication.numberedVersion,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const row = require('../state/mutations').getMutation(
|
|
471
|
+
`resource:${project.resourceId}`,
|
|
472
|
+
intent.publication.mutationId,
|
|
473
|
+
{ revealOperation: true },
|
|
474
|
+
);
|
|
475
|
+
return (
|
|
476
|
+
row
|
|
477
|
+
&& row.state === 'synced'
|
|
478
|
+
&& Number.isInteger(row.officialCloudVersion)
|
|
479
|
+
&& row.officialCloudVersion > 0
|
|
480
|
+
&& intent.publication.semanticDigest === intent.semanticDigest
|
|
481
|
+
) ? {
|
|
482
|
+
mutationId: row.mutationId,
|
|
483
|
+
version: row.officialCloudVersion,
|
|
484
|
+
} : null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function publicationMayRebase(project, intent, through) {
|
|
488
|
+
if (
|
|
489
|
+
!intent?.publication?.mutationId
|
|
490
|
+
|| intent.publication.numberedVersion !== null
|
|
491
|
+
|| !Number.isInteger(through)
|
|
492
|
+
|| through <= intent.base.through
|
|
493
|
+
) return false;
|
|
494
|
+
const row = require('../state/mutations').getMutation(
|
|
495
|
+
`resource:${project.resourceId}`,
|
|
496
|
+
intent.publication.mutationId,
|
|
497
|
+
);
|
|
498
|
+
return !row || (
|
|
499
|
+
row.state === 'rejected'
|
|
500
|
+
&& row.failureCode === 'code_project_stale_base'
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function readOwnedIntent(project) {
|
|
505
|
+
const intent = readIntent(project);
|
|
506
|
+
if (!intent) return null;
|
|
507
|
+
return officialAcknowledgement(project, intent) ? null : intent;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = {
|
|
511
|
+
INTENT_CONTRACT,
|
|
512
|
+
SUCCESSOR_CONTRACT,
|
|
513
|
+
_test: {
|
|
514
|
+
canonicalPath,
|
|
515
|
+
cardState,
|
|
516
|
+
semanticDigest,
|
|
517
|
+
validInitialBase,
|
|
518
|
+
validIntent,
|
|
519
|
+
validSuccessor,
|
|
520
|
+
},
|
|
521
|
+
clearIntent,
|
|
522
|
+
clearSuccessor,
|
|
523
|
+
intentFile,
|
|
524
|
+
markPublicationFolded,
|
|
525
|
+
markPublicationNumbered,
|
|
526
|
+
officialAcknowledgement,
|
|
527
|
+
publicationMayRebase,
|
|
528
|
+
readIntent,
|
|
529
|
+
readOwnedIntent,
|
|
530
|
+
readSuccessor,
|
|
531
|
+
reservePublication,
|
|
532
|
+
stampPublication,
|
|
533
|
+
writeIntent,
|
|
534
|
+
writeSuccessor,
|
|
535
|
+
};
|
|
@@ -72,6 +72,15 @@ async function hasTree(worktreeRoot, treeOid) {
|
|
|
72
72
|
return result.exitCode === 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** Gitlinks are a different repository and are not a file state we can apply. */
|
|
76
|
+
async function gitlinksInTree(worktreeRoot, treeOid) {
|
|
77
|
+
const { stdout } = await run(['ls-tree', '-r', '-z', treeOid], worktreeRoot);
|
|
78
|
+
return stdout.split('\0').filter(Boolean).flatMap((entry) => {
|
|
79
|
+
const match = entry.match(/^160000 commit [0-9a-f]+\t(.+)$/);
|
|
80
|
+
return match ? [match[1]] : [];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
/**
|
|
76
85
|
* The tree a captured worktree patch describes: base commit's tree with
|
|
77
86
|
* the patch applied. Computed entirely in a scratch index — the worktree
|
|
@@ -135,7 +144,7 @@ async function readBlob(worktreeRoot, oid) {
|
|
|
135
144
|
|
|
136
145
|
/** The working file as bytes: { kind: 'file'|'symlink', content, exec } or null. */
|
|
137
146
|
function workFileState(worktreeRoot, relPath) {
|
|
138
|
-
const abs =
|
|
147
|
+
const abs = transition.confinedPath(worktreeRoot, relPath);
|
|
139
148
|
let stat;
|
|
140
149
|
try {
|
|
141
150
|
stat = fs.lstatSync(abs);
|
|
@@ -145,7 +154,7 @@ function workFileState(worktreeRoot, relPath) {
|
|
|
145
154
|
if (stat.isSymbolicLink()) {
|
|
146
155
|
return { kind: 'symlink', content: Buffer.from(fs.readlinkSync(abs)), exec: false };
|
|
147
156
|
}
|
|
148
|
-
if (!stat.isFile()) return
|
|
157
|
+
if (!stat.isFile()) return { kind: 'other' };
|
|
149
158
|
return { kind: 'file', content: fs.readFileSync(abs), exec: (stat.mode & 0o100) !== 0 };
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -168,7 +177,11 @@ async function reduceTree(worktreeRoot, currentTree, baseTree, targetTree) {
|
|
|
168
177
|
const updates = [];
|
|
169
178
|
for (const relPath of await changedPaths(worktreeRoot, baseTree, targetTree)) {
|
|
170
179
|
if (unsafePath(relPath)) throw new Error(`unsafe path: ${relPath}`);
|
|
171
|
-
|
|
180
|
+
const entry = await entryAt(worktreeRoot, targetTree, relPath);
|
|
181
|
+
if (entry && entry.type !== 'blob') {
|
|
182
|
+
throw new Error(`unsupported gitlink/submodule at ${relPath}`);
|
|
183
|
+
}
|
|
184
|
+
updates.push({ path: relPath, entry });
|
|
172
185
|
}
|
|
173
186
|
|
|
174
187
|
return {
|
|
@@ -176,6 +189,66 @@ async function reduceTree(worktreeRoot, currentTree, baseTree, targetTree) {
|
|
|
176
189
|
};
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
function sameEntry(left, right) {
|
|
193
|
+
return (
|
|
194
|
+
left === null
|
|
195
|
+
? right === null
|
|
196
|
+
: right !== null
|
|
197
|
+
&& left.mode === right.mode
|
|
198
|
+
&& left.type === right.type
|
|
199
|
+
&& left.oid === right.oid
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve a crash-time tree from an exact apply WAL.
|
|
205
|
+
*
|
|
206
|
+
* Values equal to WAL source or target are known official progress and land
|
|
207
|
+
* on target. A value equal to neither is a real write made after the WAL was
|
|
208
|
+
* sealed, so it remains on top of target.
|
|
209
|
+
*/
|
|
210
|
+
async function reconcileApplyingTree(
|
|
211
|
+
worktreeRoot,
|
|
212
|
+
currentTree,
|
|
213
|
+
sourceTree,
|
|
214
|
+
targetTree,
|
|
215
|
+
{ sourceCrossed = false, crossedPaths = null } = {},
|
|
216
|
+
) {
|
|
217
|
+
const paths = new Set([
|
|
218
|
+
...await changedPaths(worktreeRoot, sourceTree, currentTree),
|
|
219
|
+
...await changedPaths(worktreeRoot, targetTree, currentTree),
|
|
220
|
+
]);
|
|
221
|
+
const local = [];
|
|
222
|
+
const updates = [];
|
|
223
|
+
for (const relPath of [...paths].sort()) {
|
|
224
|
+
if (unsafePath(relPath)) throw new Error(`unsafe path: ${relPath}`);
|
|
225
|
+
const [current, source, target] = await Promise.all([
|
|
226
|
+
entryAt(worktreeRoot, currentTree, relPath),
|
|
227
|
+
entryAt(worktreeRoot, sourceTree, relPath),
|
|
228
|
+
entryAt(worktreeRoot, targetTree, relPath),
|
|
229
|
+
]);
|
|
230
|
+
for (const entry of [current, source, target]) {
|
|
231
|
+
if (entry && entry.type !== 'blob') {
|
|
232
|
+
throw new Error(`unsupported gitlink/submodule at ${relPath}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (sameEntry(current, target)) continue;
|
|
236
|
+
const crossed = sourceCrossed || crossedPaths?.has(relPath) === true;
|
|
237
|
+
// Endpoint resemblance alone is not provenance. If the apply receipt
|
|
238
|
+
// proves this path crossed, returning to the source value is a later
|
|
239
|
+
// local write (including deleting an official create) and must remain.
|
|
240
|
+
if (sameEntry(current, source) && !crossed) continue;
|
|
241
|
+
local.push(relPath);
|
|
242
|
+
updates.push({ path: relPath, entry: current });
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
tree: updates.length
|
|
246
|
+
? await updateHeldTree(worktreeRoot, targetTree, updates)
|
|
247
|
+
: targetTree,
|
|
248
|
+
local,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
179
252
|
/** heldTree with the given entries replaced/removed — via a scratch index. */
|
|
180
253
|
async function updateHeldTree(worktreeRoot, heldTree, applied) {
|
|
181
254
|
return withScratch(async (dir) => {
|
|
@@ -214,15 +287,22 @@ async function advance(worktreeRoot, heldTree, targetTree, { label } = {}) {
|
|
|
214
287
|
if (unsafePath(relPath)) throw new Error(`unsafe path: ${relPath}`);
|
|
215
288
|
const held = await entryAt(worktreeRoot, heldTree, relPath);
|
|
216
289
|
const target = await entryAt(worktreeRoot, targetTree, relPath);
|
|
217
|
-
if ((held && held.type !== 'blob') || (target && target.type !== 'blob'))
|
|
290
|
+
if ((held && held.type !== 'blob') || (target && target.type !== 'blob')) {
|
|
291
|
+
throw new Error(`unsupported gitlink/submodule at ${relPath}`);
|
|
292
|
+
}
|
|
218
293
|
const heldBytes = held ? await readBlob(worktreeRoot, held.oid) : null;
|
|
219
294
|
const targetBytes = target ? await readBlob(worktreeRoot, target.oid) : null;
|
|
220
295
|
const work = workFileState(worktreeRoot, relPath);
|
|
221
296
|
|
|
222
297
|
if (workMatches(work, held, heldBytes)) {
|
|
223
298
|
writes.push(target
|
|
224
|
-
? {
|
|
225
|
-
|
|
299
|
+
? {
|
|
300
|
+
path: relPath,
|
|
301
|
+
content: targetBytes,
|
|
302
|
+
mode: target.mode,
|
|
303
|
+
expected: work,
|
|
304
|
+
}
|
|
305
|
+
: { path: relPath, content: null, expected: work });
|
|
226
306
|
}
|
|
227
307
|
applied.push({ path: relPath, entry: target });
|
|
228
308
|
}
|
|
@@ -238,9 +318,12 @@ async function advance(worktreeRoot, heldTree, targetTree, { label } = {}) {
|
|
|
238
318
|
|
|
239
319
|
module.exports = {
|
|
240
320
|
advance,
|
|
321
|
+
changedPaths,
|
|
241
322
|
diffOfTree,
|
|
323
|
+
gitlinksInTree,
|
|
242
324
|
hasTree,
|
|
243
325
|
indexTree,
|
|
326
|
+
reconcileApplyingTree,
|
|
244
327
|
reduceTree,
|
|
245
328
|
treeOf,
|
|
246
329
|
treeOfPatch,
|