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
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* real local edit or revert is one new transition.
|
|
14
14
|
*
|
|
15
15
|
* Checkpoint files below are local capture cache only. Their operations ride
|
|
16
|
-
* inside the
|
|
16
|
+
* inside the project stream, so the entire repo has one ordered rail.
|
|
17
|
+
*
|
|
18
|
+
* Axiom 4: only an exact local observation may create or update intent.
|
|
19
|
+
* Proven by workspace-local-intent.test.js.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
const crypto = require('crypto');
|
|
@@ -23,9 +26,10 @@ const { AMALGM_DIR } = require('../config');
|
|
|
23
26
|
const { appendStateEvent, subscribeStateEvents } = require('../state/events');
|
|
24
27
|
const { machineId } = require('./identity');
|
|
25
28
|
const { listRepoCards } = require('./cards');
|
|
26
|
-
const
|
|
29
|
+
const durable = require('./durable');
|
|
30
|
+
const { describeBytes, manifestChecksum, sha256 } = require('./content');
|
|
27
31
|
|
|
28
|
-
const CHECKPOINT_CONTRACT = 'repo-checkpoint@
|
|
32
|
+
const CHECKPOINT_CONTRACT = 'repo-checkpoint@11';
|
|
29
33
|
const MAX_INLINE_DIFF_BYTES = 512 * 1024;
|
|
30
34
|
const PUBLISH_QUIET_MS = 25;
|
|
31
35
|
const RECONCILE_INTERVAL_MS = 30_000;
|
|
@@ -54,7 +58,7 @@ function checkpointFile(machine, repoKey) {
|
|
|
54
58
|
.update(`${CHECKPOINT_CONTRACT}|${machine}|${repoKey}`)
|
|
55
59
|
.digest('hex')
|
|
56
60
|
.slice(0, 24);
|
|
57
|
-
return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-
|
|
61
|
+
return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-v11', `${digest}.json`);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
@@ -85,15 +89,44 @@ function canonicalPatch(diff) {
|
|
|
85
89
|
return patchSections(diff).join('');
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
function sectionIdentity(text) {
|
|
93
|
+
return manifestChecksum(describeBytes(Buffer.from(text, 'utf8')));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function revisionOfSections(baseOid, indexSections, worktreeSections) {
|
|
97
|
+
const index = [...indexSections].sort();
|
|
98
|
+
const worktree = [...worktreeSections].sort();
|
|
99
|
+
if (index.length === 0 && worktree.length === 0) return null;
|
|
100
|
+
return sha256(Buffer.from(JSON.stringify({ baseOid, index, worktree }), 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
function revisionOf(baseOid, indexDiff, worktreeDiff = '') {
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
return revisionOfSections(
|
|
105
|
+
baseOid,
|
|
106
|
+
patchSections(indexDiff).map(sectionIdentity),
|
|
107
|
+
patchSections(worktreeDiff).map(sectionIdentity),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function revisionOfValue(baseOid, value) {
|
|
112
|
+
if (
|
|
113
|
+
typeof value?.indexDiff === 'string'
|
|
114
|
+
&& typeof value?.worktreeDiff === 'string'
|
|
115
|
+
) return revisionOf(baseOid, value.indexDiff, value.worktreeDiff);
|
|
116
|
+
if (!value?.sections) throw new Error('checkpoint value has no content');
|
|
117
|
+
const identities = (layer) => {
|
|
118
|
+
if (!Array.isArray(value.sections[layer])) throw new Error('checkpoint sections are invalid');
|
|
119
|
+
return value.sections[layer].map((section) => {
|
|
120
|
+
if (typeof section?.text === 'string' && section.text.length > 0) {
|
|
121
|
+
return sectionIdentity(section.text);
|
|
122
|
+
}
|
|
123
|
+
if (/^[0-9a-f]{64}$/.test(String(section?.payload?.checksum || ''))) {
|
|
124
|
+
return section.payload.checksum;
|
|
125
|
+
}
|
|
126
|
+
throw new Error('checkpoint section has no content identity');
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
return revisionOfSections(baseOid, identities('index'), identities('worktree'));
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
function parseCheckpoint(text) {
|
|
@@ -126,8 +159,7 @@ function stampCheckpointMutation(machine, repoKey, checkpoint, mutationId) {
|
|
|
126
159
|
if (!existing || !sameTransition(existing, checkpoint)) return false;
|
|
127
160
|
if (existing.mutationId === mutationId) return true;
|
|
128
161
|
const file = checkpointFile(machine, repoKey);
|
|
129
|
-
|
|
130
|
-
fs.writeFileSync(file, `${JSON.stringify({ ...existing, mutationId })}\n`);
|
|
162
|
+
durable.writeJson(file, { ...existing, mutationId });
|
|
131
163
|
return true;
|
|
132
164
|
}
|
|
133
165
|
|
|
@@ -138,21 +170,12 @@ function valueIdentity(value) {
|
|
|
138
170
|
});
|
|
139
171
|
}
|
|
140
172
|
|
|
141
|
-
function parkedIdentity(parked) {
|
|
142
|
-
return JSON.stringify((parked || []).map((entry) => {
|
|
143
|
-
const stable = { ...entry };
|
|
144
|
-
delete stable.version;
|
|
145
|
-
return stable;
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
173
|
function sameTransition(left, right) {
|
|
150
174
|
return left
|
|
151
175
|
&& left.baseOid === right.baseOid
|
|
152
176
|
&& left.branch === right.branch
|
|
153
177
|
&& valueIdentity(left.from) === valueIdentity(right.from)
|
|
154
|
-
&& valueIdentity(left.to) === valueIdentity(right.to)
|
|
155
|
-
&& parkedIdentity(left.parked) === parkedIdentity(right.parked);
|
|
178
|
+
&& valueIdentity(left.to) === valueIdentity(right.to);
|
|
156
179
|
}
|
|
157
180
|
|
|
158
181
|
function sectionEntry(machine, repoKey, baseOid, layer, text, external) {
|
|
@@ -248,41 +271,366 @@ function checkpointValueReady(value) {
|
|
|
248
271
|
return !value?.payload || value.payload.kind === 'chunks';
|
|
249
272
|
}
|
|
250
273
|
|
|
251
|
-
function
|
|
252
|
-
machine,
|
|
253
|
-
repoKey,
|
|
254
|
-
branch,
|
|
255
|
-
baseOid,
|
|
256
|
-
sharedValue,
|
|
257
|
-
parked,
|
|
258
|
-
capturedAt = new Date().toISOString(),
|
|
259
|
-
}) {
|
|
260
|
-
if (parked.length === 0) return null;
|
|
274
|
+
function exactCard(card) {
|
|
261
275
|
return {
|
|
276
|
+
head: {
|
|
277
|
+
branch: card.head.branch ?? null,
|
|
278
|
+
oid: card.head.oid,
|
|
279
|
+
detached: Boolean(card.head.detached),
|
|
280
|
+
},
|
|
281
|
+
objects: card.objects,
|
|
282
|
+
branches: (card.refs || card.branches || []).map((branch) => ({
|
|
283
|
+
name: branch.name,
|
|
284
|
+
oid: branch.oid,
|
|
285
|
+
objects: branch.objects,
|
|
286
|
+
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function intentBase(state) {
|
|
291
|
+
if (
|
|
292
|
+
state?.verificationContract !== 'verified-repo-state@1'
|
|
293
|
+
|| state.materialized !== true
|
|
294
|
+
|| !Number.isInteger(state.appliedThrough)
|
|
295
|
+
|| state.appliedThrough < 0
|
|
296
|
+
|| !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(state.targetCard?.oid || ''))
|
|
297
|
+
|| !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(state.heldIndexTree || ''))
|
|
298
|
+
|| !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(state.heldTree || ''))
|
|
299
|
+
|| !Array.isArray(state.branches)
|
|
300
|
+
) return null;
|
|
301
|
+
return {
|
|
302
|
+
through: state.appliedThrough,
|
|
303
|
+
card: {
|
|
304
|
+
head: {
|
|
305
|
+
branch: state.targetCard.branch ?? null,
|
|
306
|
+
oid: state.targetCard.oid,
|
|
307
|
+
detached: !state.targetCard.branch,
|
|
308
|
+
},
|
|
309
|
+
branches: state.branches.map((branch) => ({
|
|
310
|
+
name: branch.name,
|
|
311
|
+
oid: branch.oid,
|
|
312
|
+
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
313
|
+
},
|
|
314
|
+
indexTree: state.heldIndexTree,
|
|
315
|
+
worktreeTree: state.heldTree,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function hasIntentEffect(base, local) {
|
|
320
|
+
const card = require('./intent')._test.cardState;
|
|
321
|
+
return JSON.stringify(card(base.card)) !== JSON.stringify(card(local.card))
|
|
322
|
+
|| base.indexTree !== local.indexTree
|
|
323
|
+
|| base.worktreeTree !== local.worktreeTree;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function initialIntentBase(project, card) {
|
|
327
|
+
if (project.ownerUserId !== require('./identity').userId()) return null;
|
|
328
|
+
const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
|
|
329
|
+
if (!replica || replica.appliedVersion !== 0) return null;
|
|
330
|
+
const snapshot = require('./project-stream').readSnapshot(project.file, project.resourceId);
|
|
331
|
+
if (snapshot.version !== 0 || snapshot.card !== null || snapshot.checkpoint !== null) return null;
|
|
332
|
+
const emptyTree = await require('./transition').emptyTree(card.path);
|
|
333
|
+
return {
|
|
334
|
+
through: 0,
|
|
335
|
+
card: null,
|
|
336
|
+
indexTree: emptyTree,
|
|
337
|
+
worktreeTree: emptyTree,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function gitlinkPaths(card, observed) {
|
|
342
|
+
const merge = require('./merge');
|
|
343
|
+
const trees = new Set([
|
|
344
|
+
observed.value.indexTree,
|
|
345
|
+
observed.value.worktreeTree,
|
|
346
|
+
card.head.oid,
|
|
347
|
+
...(card.refs || card.branches || []).map((branch) => branch.oid),
|
|
348
|
+
].filter(Boolean));
|
|
349
|
+
const paths = new Set();
|
|
350
|
+
for (const tree of trees) {
|
|
351
|
+
for (const entry of await merge.gitlinksInTree(card.path, tree)) paths.add(entry);
|
|
352
|
+
}
|
|
353
|
+
return [...paths].sort();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Capture is the only place physical state may become local authorship.
|
|
358
|
+
* It requires a verified baseline (or an already-owned intent) and runs only
|
|
359
|
+
* outside the reducer's materialization section.
|
|
360
|
+
*/
|
|
361
|
+
async function recordObservedIntent(
|
|
362
|
+
project,
|
|
363
|
+
card,
|
|
364
|
+
checkpoint,
|
|
365
|
+
base,
|
|
366
|
+
observed,
|
|
367
|
+
options = {},
|
|
368
|
+
) {
|
|
369
|
+
const intents = require('./intent');
|
|
370
|
+
const local = {
|
|
371
|
+
card: exactCard(card),
|
|
372
|
+
checkpoint: {
|
|
373
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
374
|
+
baseOid: card.head.oid,
|
|
375
|
+
branch: card.head.branch ?? null,
|
|
376
|
+
value: checkpoint.to,
|
|
377
|
+
},
|
|
378
|
+
indexTree: observed.value.indexTree,
|
|
379
|
+
worktreeTree: observed.value.worktreeTree,
|
|
380
|
+
physicalDigest: observed.digest,
|
|
381
|
+
};
|
|
382
|
+
const links = await gitlinkPaths(card, observed);
|
|
383
|
+
const terminal = links.length > 0
|
|
384
|
+
? {
|
|
385
|
+
code: 'unsupported_gitlink',
|
|
386
|
+
message: `unsupported gitlink/submodule at ${links[0]}`,
|
|
387
|
+
}
|
|
388
|
+
: null;
|
|
389
|
+
const previous = intents.readIntent(project);
|
|
390
|
+
const previousSuccessor = previous?.successor || null;
|
|
391
|
+
if (previous?.terminal && observed.digest === previous.local.physicalDigest) {
|
|
392
|
+
return {
|
|
393
|
+
state: 'rejected',
|
|
394
|
+
reason: previous.terminal.code,
|
|
395
|
+
intent: previous,
|
|
396
|
+
unchanged: true,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (
|
|
400
|
+
previousSuccessor?.terminal
|
|
401
|
+
&& observed.digest === previousSuccessor.local.physicalDigest
|
|
402
|
+
) {
|
|
403
|
+
return {
|
|
404
|
+
state: 'rejected',
|
|
405
|
+
reason: previousSuccessor.terminal.code,
|
|
406
|
+
intent: previous,
|
|
407
|
+
successor: previousSuccessor,
|
|
408
|
+
unchanged: true,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const nextSemanticDigest = intents._test.semanticDigest({ local });
|
|
412
|
+
const acknowledgement = previous
|
|
413
|
+
? intents.officialAcknowledgement(project, previous)
|
|
414
|
+
: null;
|
|
415
|
+
if (
|
|
416
|
+
acknowledgement
|
|
417
|
+
&& previous.semanticDigest === nextSemanticDigest
|
|
418
|
+
&& base.through <= acknowledgement.version
|
|
419
|
+
) {
|
|
420
|
+
if (previousSuccessor) intents.clearSuccessor(project, previous);
|
|
421
|
+
return { state: 'silent', reason: 'officially-acknowledged' };
|
|
422
|
+
}
|
|
423
|
+
const mayReplaceAnchored = Boolean(
|
|
424
|
+
previous?.publication
|
|
425
|
+
&& (
|
|
426
|
+
previous.semanticDigest === options.replaceAnchoredIntent
|
|
427
|
+
|| (acknowledgement && acknowledgement.version <= base.through)
|
|
428
|
+
|| intents.publicationMayRebase(project, previous, base.through)
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
if (
|
|
432
|
+
previous?.publication
|
|
433
|
+
&& previous.semanticDigest !== nextSemanticDigest
|
|
434
|
+
&& !mayReplaceAnchored
|
|
435
|
+
) {
|
|
436
|
+
const successor = intents.writeSuccessor(project, previous, {
|
|
437
|
+
local,
|
|
438
|
+
terminal,
|
|
439
|
+
});
|
|
440
|
+
if (!successor) {
|
|
441
|
+
return {
|
|
442
|
+
state: 'waiting',
|
|
443
|
+
reason: 'local-intent-moving',
|
|
444
|
+
intent: previous,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
state: terminal ? 'rejected' : 'captured',
|
|
449
|
+
reason: terminal ? terminal.code : 'successor-captured',
|
|
450
|
+
intent: previous,
|
|
451
|
+
successor,
|
|
452
|
+
mayMaterialize: true,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (!terminal && !hasIntentEffect(base, local)) {
|
|
456
|
+
if (
|
|
457
|
+
options.preserveNoEffectIntent === true
|
|
458
|
+
&& previous
|
|
459
|
+
&& !previous.publication
|
|
460
|
+
) {
|
|
461
|
+
const saved = intents.writeIntent(project, {
|
|
462
|
+
base,
|
|
463
|
+
local,
|
|
464
|
+
terminal: null,
|
|
465
|
+
publication: null,
|
|
466
|
+
birthMutationId: previous.birthMutationId,
|
|
467
|
+
preserveBirthMutationId: true,
|
|
468
|
+
});
|
|
469
|
+
return {
|
|
470
|
+
state: 'captured',
|
|
471
|
+
reason: 'identity-confirmation',
|
|
472
|
+
intent: saved,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (!previous?.publication || mayReplaceAnchored) {
|
|
476
|
+
intents.clearIntent(project);
|
|
477
|
+
return { state: 'silent', reason: 'no-local-effect' };
|
|
478
|
+
}
|
|
479
|
+
if (acknowledgement && acknowledgement.version <= base.through) {
|
|
480
|
+
return { state: 'silent', reason: 'officially-acknowledged' };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const candidate = {
|
|
484
|
+
base,
|
|
485
|
+
local,
|
|
486
|
+
terminal,
|
|
487
|
+
successor: null,
|
|
488
|
+
publication: (
|
|
489
|
+
previous?.semanticDigest === nextSemanticDigest
|
|
490
|
+
&& previous.publication?.baseThrough === base.through
|
|
491
|
+
)
|
|
492
|
+
? previous.publication
|
|
493
|
+
: null,
|
|
494
|
+
birthMutationId: options.preserveBirthMutationId === true
|
|
495
|
+
? previous?.birthMutationId
|
|
496
|
+
: mayReplaceAnchored
|
|
497
|
+
? previousSuccessor?.birthMutationId
|
|
498
|
+
: null,
|
|
499
|
+
preserveBirthMutationId: options.preserveBirthMutationId === true,
|
|
500
|
+
};
|
|
501
|
+
const saved = intents.writeIntent(project, candidate);
|
|
502
|
+
return terminal
|
|
503
|
+
? { state: 'rejected', reason: terminal.code, intent: saved }
|
|
504
|
+
: { state: 'captured', intent: saved };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function captureIntentObservationWithinTurn(project, card, checkpoint) {
|
|
508
|
+
const reducer = require('./reducer');
|
|
509
|
+
const state = reducer.readRepoState(project.repoKey, project.resourceId);
|
|
510
|
+
const base = intentBase(state) || await initialIntentBase(project, card);
|
|
511
|
+
if (!base) {
|
|
512
|
+
return { state: 'silent', reason: 'no-verified-baseline' };
|
|
513
|
+
}
|
|
514
|
+
const observed = await reducer.observePhysicalRepo(card.path);
|
|
515
|
+
if (observed.state !== 'ready') return { state: 'waiting', reason: 'local-state-moving' };
|
|
516
|
+
const observedRevision = revisionOf(
|
|
517
|
+
observed.captured.baseOid,
|
|
518
|
+
observed.captured.indexDiff,
|
|
519
|
+
observed.captured.worktreeDiff,
|
|
520
|
+
);
|
|
521
|
+
const observedCard = {
|
|
522
|
+
head: {
|
|
523
|
+
branch: observed.value.head.branch ?? null,
|
|
524
|
+
oid: observed.value.head.oid,
|
|
525
|
+
detached: !observed.value.head.branch,
|
|
526
|
+
},
|
|
527
|
+
branches: observed.value.branches,
|
|
528
|
+
};
|
|
529
|
+
const intents = require('./intent');
|
|
530
|
+
if (
|
|
531
|
+
observed.captured.baseOid !== card.head.oid
|
|
532
|
+
|| (
|
|
533
|
+
checkpoint
|
|
534
|
+
&& (
|
|
535
|
+
checkpoint.to?.revision !== observedRevision
|
|
536
|
+
|| checkpoint.baseOid !== observed.captured.baseOid
|
|
537
|
+
|| (checkpoint.branch ?? null) !== (observed.value.head.branch ?? null)
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
|| JSON.stringify(intents._test.cardState(exactCard(card)))
|
|
541
|
+
!== JSON.stringify(intents._test.cardState(observedCard))
|
|
542
|
+
) {
|
|
543
|
+
return { state: 'waiting', reason: 'local-state-moving' };
|
|
544
|
+
}
|
|
545
|
+
const to = checkpointValue(
|
|
546
|
+
machineId(),
|
|
547
|
+
project.repoKey,
|
|
548
|
+
observed.captured.baseOid,
|
|
549
|
+
observed.captured.indexDiff,
|
|
550
|
+
observed.captured.worktreeDiff,
|
|
551
|
+
);
|
|
552
|
+
if (!checkpointValueReady(to)) {
|
|
553
|
+
return { state: 'waiting', reason: 'checkpoint-payload-pending' };
|
|
554
|
+
}
|
|
555
|
+
const capturedCheckpoint = {
|
|
262
556
|
checkpoint: CHECKPOINT_CONTRACT,
|
|
263
|
-
machine,
|
|
264
|
-
repoKey,
|
|
265
|
-
branch,
|
|
266
|
-
baseOid,
|
|
267
|
-
from:
|
|
268
|
-
to
|
|
269
|
-
|
|
270
|
-
capturedAt,
|
|
557
|
+
machine: machineId(),
|
|
558
|
+
repoKey: project.repoKey,
|
|
559
|
+
branch: observed.value.head.branch ?? null,
|
|
560
|
+
baseOid: observed.captured.baseOid,
|
|
561
|
+
from: checkpoint?.from || null,
|
|
562
|
+
to,
|
|
563
|
+
capturedAt: new Date().toISOString(),
|
|
271
564
|
};
|
|
565
|
+
if (!intents.readIntent(project) && observed.digest === state?.observedPhysicalDigest) {
|
|
566
|
+
return {
|
|
567
|
+
state: 'silent',
|
|
568
|
+
reason: 'verified-state',
|
|
569
|
+
checkpoint: capturedCheckpoint,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const result = await recordObservedIntent(
|
|
573
|
+
project,
|
|
574
|
+
card,
|
|
575
|
+
capturedCheckpoint,
|
|
576
|
+
base,
|
|
577
|
+
observed,
|
|
578
|
+
);
|
|
579
|
+
return { ...result, checkpoint: capturedCheckpoint };
|
|
272
580
|
}
|
|
273
581
|
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
582
|
+
function captureIntentObservation(project, card, checkpoint) {
|
|
583
|
+
return require('./reducer').withRepoTurn(
|
|
584
|
+
project.repoKey,
|
|
585
|
+
'capture',
|
|
586
|
+
() => captureIntentObservationWithinTurn(project, card, checkpoint),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function captureLocalIntent(project, card, checkpoint) {
|
|
591
|
+
const { checkpoint: capturedCheckpoint, ...outcome } = await captureIntentObservation(
|
|
592
|
+
project,
|
|
593
|
+
card,
|
|
594
|
+
checkpoint,
|
|
595
|
+
);
|
|
596
|
+
return outcome;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* A valid apply WAL is also an exact baseline. Recovery first resolves every
|
|
601
|
+
* source/target component to target while retaining values that equal
|
|
602
|
+
* neither. Only then does this function durably name the retained state as
|
|
603
|
+
* local intent over the official WAL target.
|
|
604
|
+
*/
|
|
605
|
+
async function captureIntentAgainst(project, card, base, observed, options = {}) {
|
|
606
|
+
if (!base || observed?.state !== 'ready' || !observed.captured) {
|
|
607
|
+
return { state: 'waiting', reason: 'local-state-moving' };
|
|
608
|
+
}
|
|
609
|
+
const to = checkpointValue(
|
|
610
|
+
machineId(),
|
|
611
|
+
project.repoKey,
|
|
612
|
+
card.head.oid,
|
|
613
|
+
observed.captured.indexDiff,
|
|
614
|
+
observed.captured.worktreeDiff,
|
|
615
|
+
);
|
|
616
|
+
if (!checkpointValueReady(to)) {
|
|
617
|
+
return { state: 'waiting', reason: 'checkpoint-payload-pending' };
|
|
618
|
+
}
|
|
619
|
+
return recordObservedIntent(project, card, { to }, base, observed, options);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Observe one card and turn a real local change into durable intent.
|
|
624
|
+
*
|
|
625
|
+
* The reducer calls this before ordered apply so an inbound event cannot beat
|
|
626
|
+
* the debounced filesystem watcher. It still obeys the same capture rule:
|
|
627
|
+
* without a verified baseline, the physical checkout is not authorship.
|
|
628
|
+
*/
|
|
629
|
+
async function captureIntentForCard(project, card) {
|
|
630
|
+
if (!project || !card?.head?.oid) {
|
|
631
|
+
return { state: 'silent', reason: 'no-registered-card' };
|
|
632
|
+
}
|
|
633
|
+
return captureLocalIntent(project, card, null);
|
|
286
634
|
}
|
|
287
635
|
|
|
288
636
|
function readCheckpointValue(baseOid, value) {
|
|
@@ -344,28 +692,6 @@ function readCheckpointValue(baseOid, value) {
|
|
|
344
692
|
return { state: 'ready', indexDiff, worktreeDiff };
|
|
345
693
|
}
|
|
346
694
|
|
|
347
|
-
function checkpointBaseReady(card, reduced, publication) {
|
|
348
|
-
return Boolean(
|
|
349
|
-
reduced
|
|
350
|
-
&& publication?.current
|
|
351
|
-
&& reduced.baseOid === card.head?.oid
|
|
352
|
-
&& reduced.branch === card.head?.branch
|
|
353
|
-
&& typeof reduced.indexDiff === 'string'
|
|
354
|
-
&& typeof reduced.worktreeDiff === 'string'
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function checkpointWasAcknowledged(checkpoint, card, localRevision, version) {
|
|
359
|
-
return Boolean(
|
|
360
|
-
checkpoint
|
|
361
|
-
&& Number.isInteger(version)
|
|
362
|
-
&& version > 0
|
|
363
|
-
&& checkpoint.baseOid === card.head?.oid
|
|
364
|
-
&& checkpoint.branch === card.head?.branch
|
|
365
|
-
&& checkpoint.to?.revision === localRevision
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
695
|
function checkpointMayPublish(repoKey) {
|
|
370
696
|
return !require('./reducer').isMaterializing(repoKey);
|
|
371
697
|
}
|
|
@@ -376,125 +702,62 @@ async function publishPass(replicaId = null) {
|
|
|
376
702
|
for (const card of listRepoCards()) {
|
|
377
703
|
if (replicaId && card.replica !== replicaId) continue;
|
|
378
704
|
if (card.health !== 'healthy' || card.kind !== 'primary' || card.operation !== 'none') continue;
|
|
379
|
-
const
|
|
705
|
+
const wall = require('./wall');
|
|
706
|
+
const project = wall.projectForPath(card.path);
|
|
707
|
+
const repoKey = project?.repoKey;
|
|
380
708
|
if (!repoKey || !card.head?.oid) continue;
|
|
381
709
|
if (!checkpointMayPublish(repoKey)) {
|
|
382
710
|
run.waiting += 1;
|
|
383
711
|
continue;
|
|
384
712
|
}
|
|
385
713
|
try {
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
repoKey,
|
|
389
|
-
wall.projectForRepo(repoKey)?.resourceId,
|
|
390
|
-
);
|
|
391
|
-
if (!checkpointBaseReady(card, reduced, wall.publicationStatus(repoKey, reduced))) {
|
|
714
|
+
const intent = await captureIntentObservation(project, card, null);
|
|
715
|
+
if (intent.state === 'waiting') {
|
|
392
716
|
run.waiting += 1;
|
|
393
717
|
continue;
|
|
394
718
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
run.waiting += 1;
|
|
719
|
+
if (intent.state === 'rejected') {
|
|
720
|
+
if (intent.unchanged) run.skips += 1;
|
|
721
|
+
else run.failures += 1;
|
|
399
722
|
continue;
|
|
400
723
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const file = checkpointFile(machine, repoKey);
|
|
404
|
-
const existing = readCheckpoint(machine, repoKey);
|
|
405
|
-
const parked = await Promise.all(localParkedEntries(existing, reduced, card).map(async (entry) => ({
|
|
406
|
-
...entry,
|
|
407
|
-
objects: await require('./objects').bundleSourceFor(card.path, repoKey, entry.oid),
|
|
408
|
-
})));
|
|
409
|
-
if (!wall.publicationStatus(repoKey, reduced).current) {
|
|
410
|
-
run.waiting += 1;
|
|
724
|
+
if (!intent.checkpoint) {
|
|
725
|
+
run.skips += 1;
|
|
411
726
|
continue;
|
|
412
727
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
parked,
|
|
431
|
-
});
|
|
432
|
-
if (!refreshed) {
|
|
433
|
-
const project = wall.projectForRepo(repoKey);
|
|
434
|
-
const version = wall.mutationVersion(
|
|
435
|
-
project?.resourceId,
|
|
436
|
-
existing?.mutationId,
|
|
437
|
-
repoKey,
|
|
438
|
-
);
|
|
439
|
-
if (checkpointWasAcknowledged(existing, card, localRevision, version)) {
|
|
440
|
-
run.skips += 1;
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
728
|
+
const file = checkpointFile(machine, repoKey);
|
|
729
|
+
const existing = readCheckpoint(machine, repoKey);
|
|
730
|
+
const to = intent.checkpoint.to;
|
|
731
|
+
const snapshot = project && fs.existsSync(project.file)
|
|
732
|
+
? require('./project-stream').readSnapshot(project.file, project.resourceId)
|
|
733
|
+
: null;
|
|
734
|
+
const onOfficialCard = snapshot?.card
|
|
735
|
+
&& wall._test.sameCardState(card, snapshot.card);
|
|
736
|
+
const from = onOfficialCard
|
|
737
|
+
? snapshot.checkpoint.value
|
|
738
|
+
: { revision: null, indexDiff: '', worktreeDiff: '' };
|
|
739
|
+
const envelope = {
|
|
740
|
+
...intent.checkpoint,
|
|
741
|
+
from,
|
|
742
|
+
};
|
|
743
|
+
if (onOfficialCard && valueIdentity(from) === valueIdentity(to)) {
|
|
744
|
+
if (existing) {
|
|
443
745
|
fs.rmSync(file, { force: true });
|
|
444
|
-
run.skips += 1;
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
if (refreshed && !sameTransition(existing, refreshed)) {
|
|
448
|
-
fs.writeFileSync(file, `${JSON.stringify(refreshed)}\n`);
|
|
449
746
|
appendStateEvent({
|
|
450
747
|
resource: 'repo-checkpoints',
|
|
451
|
-
op: '
|
|
748
|
+
op: 'delete',
|
|
452
749
|
id: card.replica,
|
|
453
|
-
|
|
454
|
-
source: 'repo-checkpoint:parked-objects',
|
|
750
|
+
source: 'repo-checkpoint:covered',
|
|
455
751
|
});
|
|
456
|
-
run.published += 1;
|
|
457
|
-
continue;
|
|
458
752
|
}
|
|
459
753
|
run.skips += 1;
|
|
460
754
|
continue;
|
|
461
755
|
}
|
|
462
|
-
|
|
463
|
-
const from = checkpointValue(
|
|
464
|
-
machine,
|
|
465
|
-
repoKey,
|
|
466
|
-
card.head.oid,
|
|
467
|
-
reduced.indexDiff,
|
|
468
|
-
reduced.worktreeDiff,
|
|
469
|
-
);
|
|
470
|
-
const to = checkpointValue(
|
|
471
|
-
machine,
|
|
472
|
-
repoKey,
|
|
473
|
-
card.head.oid,
|
|
474
|
-
captured.indexDiff,
|
|
475
|
-
captured.worktreeDiff,
|
|
476
|
-
);
|
|
477
|
-
if (!checkpointValueReady(from) || !checkpointValueReady(to)) {
|
|
478
|
-
run.waiting += 1;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
const envelope = {
|
|
482
|
-
checkpoint: CHECKPOINT_CONTRACT,
|
|
483
|
-
machine,
|
|
484
|
-
repoKey,
|
|
485
|
-
branch: card.head.branch,
|
|
486
|
-
baseOid: card.head.oid,
|
|
487
|
-
from,
|
|
488
|
-
to,
|
|
489
|
-
parked,
|
|
490
|
-
capturedAt: new Date().toISOString(),
|
|
491
|
-
};
|
|
492
756
|
if (sameTransition(existing, envelope)) {
|
|
493
757
|
run.skips += 1;
|
|
494
758
|
continue;
|
|
495
759
|
}
|
|
496
|
-
|
|
497
|
-
fs.writeFileSync(file, `${JSON.stringify(envelope)}\n`);
|
|
760
|
+
durable.writeJson(file, envelope);
|
|
498
761
|
appendStateEvent({
|
|
499
762
|
resource: 'repo-checkpoints',
|
|
500
763
|
op: existing ? 'update' : 'insert',
|
|
@@ -582,11 +845,7 @@ function checkpointStats() {
|
|
|
582
845
|
|
|
583
846
|
module.exports = {
|
|
584
847
|
_test: {
|
|
585
|
-
checkpointBaseReady,
|
|
586
|
-
checkpointForSharedState,
|
|
587
848
|
checkpointMayPublish,
|
|
588
|
-
checkpointWasAcknowledged,
|
|
589
|
-
localParkedEntries,
|
|
590
849
|
},
|
|
591
850
|
CHECKPOINT_CONTRACT,
|
|
592
851
|
MAX_INLINE_DIFF_BYTES,
|
|
@@ -594,12 +853,16 @@ module.exports = {
|
|
|
594
853
|
checkpointStats,
|
|
595
854
|
checkpointValue,
|
|
596
855
|
checkpointValueReady,
|
|
856
|
+
captureIntentAgainst,
|
|
857
|
+
captureIntentForCard,
|
|
858
|
+
captureLocalIntent,
|
|
597
859
|
parseCheckpoint,
|
|
598
860
|
publishCheckpoints,
|
|
599
861
|
readCheckpoint,
|
|
600
862
|
readCheckpointValue,
|
|
601
863
|
repoKeyOfCard,
|
|
602
864
|
revisionOf,
|
|
865
|
+
revisionOfValue,
|
|
603
866
|
sameTransition,
|
|
604
867
|
stampCheckpointMutation,
|
|
605
868
|
startCheckpointService,
|