@narumitw/pi-sync 0.54.2 → 0.54.3
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 +19 -11
- package/dist/chunks/{chunk-ORQRHEEO.ts → chunk-26PMXY7E.ts} +3 -3
- package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-2FETCGGT.ts} +2 -2
- package/dist/chunks/{chunk-MYPSMJHJ.ts → chunk-35GM6XKM.ts} +275 -151
- package/dist/chunks/chunk-35GM6XKM.ts.map +7 -0
- package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-A3TFXY4O.ts} +2 -2
- package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-AAZDBIBJ.ts} +2 -2
- package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-CAGJOKB7.ts} +5 -5
- package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-E6ONFG57.ts} +15 -2
- package/dist/chunks/{chunk-QE7NZZWT.ts.map → chunk-E6ONFG57.ts.map} +2 -2
- package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-GC3QOL6J.ts} +2 -2
- package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-HMXUKU43.ts} +3 -3
- package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-I4JYVFUS.ts} +5 -6
- package/dist/chunks/chunk-I4JYVFUS.ts.map +7 -0
- package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-ITFR6NNQ.ts} +14 -2
- package/dist/chunks/chunk-ITFR6NNQ.ts.map +7 -0
- package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-KATKRR3D.ts} +18 -11
- package/dist/chunks/chunk-KATKRR3D.ts.map +7 -0
- package/dist/chunks/{chunk-CL7BZRRQ.ts → chunk-KGE47GCZ.ts} +2 -2
- package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-R35N4CL3.ts} +2 -2
- package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
- package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
- package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-VCL7TZYW.ts} +2 -2
- package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-XU2VXGQV.ts} +14 -8
- package/dist/chunks/chunk-XU2VXGQV.ts.map +7 -0
- package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-YTHQEW35.ts} +3 -3
- package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-4CZFB7WI.ts} +3 -3
- package/dist/chunks/{conflict-review-4PLVLMRO.ts → conflict-review-ZOTOHL2E.ts} +11 -10
- package/dist/chunks/{conflict-review-4PLVLMRO.ts.map → conflict-review-ZOTOHL2E.ts.map} +1 -1
- package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-UNHBVVON.ts} +3 -3
- package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-HVRTPEVK.ts} +7 -7
- package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-ZIN4BUSF.ts} +297 -136
- package/dist/chunks/manager-ui-ZIN4BUSF.ts.map +7 -0
- package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-ICXVTCML.ts} +5 -3
- package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-H6AEVA3Q.ts} +6 -6
- package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y4P3O5B5.ts} +2 -2
- package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-ZMW5C4M7.ts} +4 -4
- package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-VW7F3JN4.ts} +5 -5
- package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-3EMS2J5B.ts} +5 -4
- package/dist/chunks/{sync-operations-47OUX76C.ts → sync-operations-2CWST333.ts} +140 -24
- package/dist/chunks/sync-operations-2CWST333.ts.map +7 -0
- package/dist/index.ts +103 -68
- package/dist/index.ts.map +2 -2
- package/docs/settings.md +47 -10
- package/docs/tui-contract.md +61 -0
- package/package.json +1 -1
- package/src/commands/command-execution.ts +2 -1
- package/src/commands/command-types.ts +4 -0
- package/src/commands/command.ts +3 -1
- package/src/settings/config.ts +13 -10
- package/src/settings/settings-store.ts +1 -0
- package/src/settings/settings-types.ts +2 -0
- package/src/settings/settings-validation.ts +3 -0
- package/src/state/merge-baseline-store.ts +18 -0
- package/src/sync/apply-review.ts +63 -0
- package/src/sync/automatic-sync.ts +45 -33
- package/src/sync/merged-sync.ts +202 -104
- package/src/sync/startup-check.ts +39 -21
- package/src/sync/startup-timings.ts +90 -0
- package/src/sync/sync-inspection.ts +8 -4
- package/src/sync/sync-mutations.ts +49 -11
- package/src/sync/sync-queries.ts +14 -3
- package/src/ui/manager-state.ts +10 -6
- package/src/ui/manager-ui.ts +57 -7
- package/src/ui/merge-review.ts +3 -3
- package/src/ui/merged-sync-summary.ts +2 -18
- package/src/ui/settings-frame.ts +55 -0
- package/src/ui/settings-ui.ts +141 -43
- package/src/ui/setup/setup-prompts.ts +4 -3
- package/src/ui/sync-behavior.ts +15 -0
- package/src/ui/sync-format.ts +69 -38
- package/src/ui/transfer-changes.ts +33 -0
- package/src/ui/transfer-review.ts +27 -0
- package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
- package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
- package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
- package/dist/chunks/chunk-MYPSMJHJ.ts.map +0 -7
- package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
- package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
- package/dist/chunks/sync-operations-47OUX76C.ts.map +0 -7
- /package/dist/chunks/{chunk-ORQRHEEO.ts.map → chunk-26PMXY7E.ts.map} +0 -0
- /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-2FETCGGT.ts.map} +0 -0
- /package/dist/chunks/{chunk-V6VZ5TLG.ts.map → chunk-A3TFXY4O.ts.map} +0 -0
- /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-AAZDBIBJ.ts.map} +0 -0
- /package/dist/chunks/{chunk-5ZWQ33XL.ts.map → chunk-CAGJOKB7.ts.map} +0 -0
- /package/dist/chunks/{chunk-IJ6VV6FN.ts.map → chunk-GC3QOL6J.ts.map} +0 -0
- /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-HMXUKU43.ts.map} +0 -0
- /package/dist/chunks/{chunk-CL7BZRRQ.ts.map → chunk-KGE47GCZ.ts.map} +0 -0
- /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-R35N4CL3.ts.map} +0 -0
- /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-VCL7TZYW.ts.map} +0 -0
- /package/dist/chunks/{chunk-UQ3ALBIB.ts.map → chunk-YTHQEW35.ts.map} +0 -0
- /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → conflict-artifacts-4CZFB7WI.ts.map} +0 -0
- /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-UNHBVVON.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → manager-result-dispatcher-HVRTPEVK.ts.map} +0 -0
- /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → merge-baseline-store-ICXVTCML.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → remote-selection-ui-H6AEVA3Q.ts.map} +0 -0
- /package/dist/chunks/{s3-backend-AQGV6LFT.ts.map → s3-backend-Y4P3O5B5.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → setup-switch-ZMW5C4M7.ts.map} +0 -0
- /package/dist/chunks/{setup-wizard-GSDPDFC4.ts.map → setup-wizard-VW7F3JN4.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-K45QVLHB.ts.map → sync-inspection-3EMS2J5B.ts.map} +0 -0
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
readMergeJournal,
|
|
11
11
|
saveConflictArtifact,
|
|
12
12
|
writeMergeJournal
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-26PMXY7E.ts";
|
|
14
14
|
import {
|
|
15
15
|
fileHashMap,
|
|
16
16
|
hasLocalChanges,
|
|
@@ -22,26 +22,30 @@ import {
|
|
|
22
22
|
formatRemoteSelectionStatus,
|
|
23
23
|
readSnapshotForHead,
|
|
24
24
|
requireCompatibleRemoteSelection
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-A3TFXY4O.ts";
|
|
26
26
|
import {
|
|
27
27
|
createSyncBackend
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-2FETCGGT.ts";
|
|
29
29
|
import {
|
|
30
30
|
applySnapshotTransaction,
|
|
31
31
|
recoverPendingSnapshotTransactions,
|
|
32
32
|
syncMutationParents
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-KGE47GCZ.ts";
|
|
34
34
|
import {
|
|
35
35
|
backupLocal,
|
|
36
36
|
captureMutationOwner,
|
|
37
37
|
protectedSessionPaths
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-GC3QOL6J.ts";
|
|
39
|
+
import {
|
|
40
|
+
createStartupTimings
|
|
41
|
+
} from "./chunk-UZPMDLTL.ts";
|
|
39
42
|
import {
|
|
40
43
|
safeTerminalText,
|
|
41
44
|
snapshotPathLabel
|
|
42
45
|
} from "./chunk-AMJRUXFT.ts";
|
|
43
46
|
import {
|
|
44
47
|
TEXT_MERGE_WORK_LIMIT,
|
|
48
|
+
hasCurrentMergeBaseline,
|
|
45
49
|
isMergeTextPath,
|
|
46
50
|
mergeSession,
|
|
47
51
|
mergeText,
|
|
@@ -49,7 +53,7 @@ import {
|
|
|
49
53
|
readMergeAncestor,
|
|
50
54
|
readMergeAncestors,
|
|
51
55
|
stageMergeBaseline
|
|
52
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-E6ONFG57.ts";
|
|
53
57
|
import {
|
|
54
58
|
mergePathIdentity,
|
|
55
59
|
planFileMerge
|
|
@@ -59,12 +63,12 @@ import {
|
|
|
59
63
|
statePathForConfig,
|
|
60
64
|
syncStateFingerprint,
|
|
61
65
|
writeStateForConfig
|
|
62
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-VCL7TZYW.ts";
|
|
63
67
|
import {
|
|
64
68
|
loadConfig,
|
|
65
69
|
syncCheckConfigFingerprint,
|
|
66
70
|
syncConfigReviewIdentity
|
|
67
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-KATKRR3D.ts";
|
|
68
72
|
import {
|
|
69
73
|
SyncBackendConflictError,
|
|
70
74
|
expectedRemoteHead
|
|
@@ -290,9 +294,9 @@ async function confirmMergeReview(ctx, title, content, signal, isCurrent, confir
|
|
|
290
294
|
}) : await runMenu(ctx, menu, { getState: () => void 0, signal, isCurrent, onError: () => {
|
|
291
295
|
} });
|
|
292
296
|
if (!isCurrent() || signal?.aborted) return false;
|
|
293
|
-
if (result.kind === "error") throw new Error("
|
|
297
|
+
if (result.kind === "error") throw new Error("Sync review failed; no transfer was performed.");
|
|
294
298
|
if (result.kind === "unsupported")
|
|
295
|
-
throw new Error("
|
|
299
|
+
throw new Error("Sync review requires observable TUI or RPC; review the plan before using --yes.");
|
|
296
300
|
if (result.kind === "completed") choice.value = result.value;
|
|
297
301
|
else if (result.kind !== "closed") return false;
|
|
298
302
|
if (choice.value === "approve") return true;
|
|
@@ -309,7 +313,7 @@ async function confirmMergeReview(ctx, title, content, signal, isCurrent, confir
|
|
|
309
313
|
}
|
|
310
314
|
});
|
|
311
315
|
if (!isCurrent() || signal?.aborted) return false;
|
|
312
|
-
if (detail.kind === "error") throw new Error("
|
|
316
|
+
if (detail.kind === "error") throw new Error("Sync review failed; no transfer was performed.");
|
|
313
317
|
if (detail.kind !== "cancelled" || detail.reason !== "back") return false;
|
|
314
318
|
}
|
|
315
319
|
return false;
|
|
@@ -317,9 +321,30 @@ async function confirmMergeReview(ctx, title, content, signal, isCurrent, confir
|
|
|
317
321
|
|
|
318
322
|
// src/ui/merged-sync-summary.ts
|
|
319
323
|
import { truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
|
|
324
|
+
|
|
325
|
+
// src/ui/transfer-changes.ts
|
|
326
|
+
function transferChanges(before, after, protectedPaths = /* @__PURE__ */ new Set()) {
|
|
327
|
+
const beforeMap = before ? fileHashMap(before) : {};
|
|
328
|
+
const afterMap = fileHashMap(after);
|
|
329
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort().flatMap((filePath) => {
|
|
330
|
+
if (protectedPaths.has(filePath)) return [];
|
|
331
|
+
if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
|
|
332
|
+
if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
|
|
333
|
+
return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function transferCounts(changes) {
|
|
337
|
+
return {
|
|
338
|
+
add: changes.filter((change) => change.action === "Add").length,
|
|
339
|
+
update: changes.filter((change) => change.action === "Update").length,
|
|
340
|
+
remove: changes.filter((change) => change.action === "Delete").length
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/ui/merged-sync-summary.ts
|
|
320
345
|
function mergedSyncSummary(input) {
|
|
321
|
-
const local =
|
|
322
|
-
const remote =
|
|
346
|
+
const local = transferChanges(input.localBefore, input.localAfter);
|
|
347
|
+
const remote = transferChanges(input.remoteBefore, input.remoteAfter);
|
|
323
348
|
const all = [...local, ...remote];
|
|
324
349
|
const updatesOnly = all.length > 0 && all.every((change) => change.action === "Update");
|
|
325
350
|
const verb = updatesOnly ? "Update" : "Apply changes to";
|
|
@@ -351,6 +376,7 @@ function mergedSyncSummary(input) {
|
|
|
351
376
|
const context = [`Setup: ${safeTerminalText(input.setupName)}`, `Remote: ${safeTerminalText(input.destination)}`];
|
|
352
377
|
const safety = [
|
|
353
378
|
`Remote snapshot publication: ${input.publish ? "yes" : "no"}`,
|
|
379
|
+
"Remote removals affect the active snapshot; retained history is not erased.",
|
|
354
380
|
`Sessions: ${input.sessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
355
381
|
input.groups.length ? `Unresolved groups: ${input.groups.length} \u2014 these paths stay unchanged on each side` : "Conflicts: none",
|
|
356
382
|
...input.publish && input.capability === "read-check-write-verify" ? ["Warning: simultaneous remote writes can still race; visible changes are rejected."] : [],
|
|
@@ -381,15 +407,6 @@ function mergedSyncSummary(input) {
|
|
|
381
407
|
].join("\n")
|
|
382
408
|
};
|
|
383
409
|
}
|
|
384
|
-
function changes(before, after) {
|
|
385
|
-
const beforeMap = fileHashMap(before);
|
|
386
|
-
const afterMap = fileHashMap(after);
|
|
387
|
-
return [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort().flatMap((filePath) => {
|
|
388
|
-
if (!Object.hasOwn(beforeMap, filePath)) return [{ path: filePath, action: "Add" }];
|
|
389
|
-
if (!Object.hasOwn(afterMap, filePath)) return [{ path: filePath, action: "Delete" }];
|
|
390
|
-
return beforeMap[filePath] !== afterMap[filePath] ? [{ path: filePath, action: "Update" }] : [];
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
410
|
|
|
394
411
|
// src/sync/conflict-artifact-ownership.ts
|
|
395
412
|
import fs from "node:fs/promises";
|
|
@@ -1351,7 +1368,11 @@ function formatDiff(local, remote) {
|
|
|
1351
1368
|
const localMap = fileHashMap(local);
|
|
1352
1369
|
const remoteMap = fileHashMap(remote);
|
|
1353
1370
|
const allPaths = [.../* @__PURE__ */ new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])].sort();
|
|
1354
|
-
const lines = [
|
|
1371
|
+
const lines = [
|
|
1372
|
+
`local: ${local.files.length} files`,
|
|
1373
|
+
`remote: ${safeTerminalText(remote.id)} (${remote.files.length} files)`,
|
|
1374
|
+
""
|
|
1375
|
+
];
|
|
1355
1376
|
let changed = 0;
|
|
1356
1377
|
for (const filePath of allPaths) {
|
|
1357
1378
|
if (!Object.hasOwn(localMap, filePath)) {
|
|
@@ -1369,76 +1390,87 @@ function formatDiff(local, remote) {
|
|
|
1369
1390
|
return lines.join("\n");
|
|
1370
1391
|
}
|
|
1371
1392
|
function formatSnapshotOnlyDiff(title, snapshot) {
|
|
1372
|
-
return [
|
|
1373
|
-
|
|
1374
|
-
|
|
1393
|
+
return [
|
|
1394
|
+
`${safeTerminalText(title)}: ${safeTerminalText(snapshot.id)}`,
|
|
1395
|
+
...snapshot.files.map((file) => `Add: ${snapshotPathLabel(file.path)}`)
|
|
1396
|
+
].join("\n");
|
|
1375
1397
|
}
|
|
1376
1398
|
function formatPushSummary(config, destination, upload, head, preservedRemoteFileCount = 0, remote) {
|
|
1377
1399
|
return [
|
|
1378
1400
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1379
1401
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
1380
|
-
`
|
|
1402
|
+
`Direction: Local \u2192 Remote. Source: ${safeTerminalText(agentDir())}.`,
|
|
1403
|
+
`Publish a snapshot containing ${upload.files.length} files; unchanged and preserved files are not counted as changes.`,
|
|
1381
1404
|
`Sessions: ${upload.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1382
|
-
head ? `Remote latest: ${head.snapshotId}` : "Remote latest: empty",
|
|
1383
|
-
"Publication effect: the backend's active head will reference the new immutable snapshot.",
|
|
1405
|
+
head ? `Remote latest: ${safeTerminalText(head.snapshotId)}` : "Remote latest: empty",
|
|
1406
|
+
"Publication effect: the backend's active head will reference the new immutable snapshot. Removed paths leave the active snapshot; retained history is not erased.",
|
|
1384
1407
|
...remote && inspectRemoteSelection(config.include, remote).kind === "different" ? ["Included-content policy effect: replace the differing remote selection with this setup's local selection."] : [],
|
|
1385
1408
|
formatPublicationPreview(remote, upload),
|
|
1386
|
-
preservedRemoteFileCount > 0 ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.` : "Possible secrets were scanned before this prompt."
|
|
1409
|
+
config.skipSecretScan ? "Warning: possible-secret scanning is disabled for this push." : preservedRemoteFileCount > 0 ? `Possible secrets in locally managed files were scanned before this prompt; ${preservedRemoteFileCount} preserved remote file(s) were not rescanned.` : "Possible secrets were scanned before this prompt."
|
|
1387
1410
|
].join("\n");
|
|
1388
1411
|
}
|
|
1389
|
-
function formatApplyPreview(local, remote) {
|
|
1390
|
-
return formatDirectionalChanges(
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1412
|
+
function formatApplyPreview(local, remote, protectedPaths = /* @__PURE__ */ new Set(), additionalRemovals = []) {
|
|
1413
|
+
return formatDirectionalChanges(
|
|
1414
|
+
local,
|
|
1415
|
+
remote,
|
|
1416
|
+
{
|
|
1417
|
+
add: "Add locally",
|
|
1418
|
+
update: "Update locally",
|
|
1419
|
+
remove: "Remove locally"
|
|
1420
|
+
},
|
|
1421
|
+
protectedPaths,
|
|
1422
|
+
additionalRemovals
|
|
1423
|
+
);
|
|
1395
1424
|
}
|
|
1396
|
-
function formatPullSummary(config, destination, local, remote,
|
|
1425
|
+
function formatPullSummary(config, destination, local, remote, protectedPaths, additionalRemovals = []) {
|
|
1397
1426
|
return [
|
|
1398
1427
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1399
1428
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
1429
|
+
"Direction: Remote \u2192 Local.",
|
|
1400
1430
|
`Snapshot: ${safeTerminalText(remote.id)}`,
|
|
1401
1431
|
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1402
|
-
`Protected live sessions: ${
|
|
1403
|
-
formatApplyPreview(local, remote),
|
|
1432
|
+
`Protected live sessions: ${protectedPaths.size || "none"}`,
|
|
1433
|
+
formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
|
|
1434
|
+
...additionalRemovals.length ? ["Case-variant aliases listed above are removed before writing canonical paths."] : [],
|
|
1404
1435
|
"A local backup is created before these writes/deletes. The remote active snapshot is unchanged."
|
|
1405
1436
|
].join("\n");
|
|
1406
1437
|
}
|
|
1407
|
-
function formatRollbackSummary(config, destination, local, remote, requestedSnapshot,
|
|
1438
|
+
function formatRollbackSummary(config, destination, local, remote, requestedSnapshot, protectedPaths, additionalRemovals = []) {
|
|
1408
1439
|
return [
|
|
1409
1440
|
`Sync setup: ${safeTerminalText(config.setupName)}`,
|
|
1410
1441
|
`Storage location: ${safeTerminalText(destination)}`,
|
|
1442
|
+
"Direction: Historical snapshot \u2192 Local and remote active head.",
|
|
1411
1443
|
`Snapshot: ${safeTerminalText(requestedSnapshot)}`,
|
|
1412
1444
|
`Sessions: ${remote.syncSessions ? "included \u2014 may contain private conversations" : "not included"}`,
|
|
1413
|
-
`Protected live sessions: ${
|
|
1414
|
-
formatApplyPreview(local, remote),
|
|
1445
|
+
`Protected live sessions: ${protectedPaths.size || "none"}`,
|
|
1446
|
+
formatApplyPreview(local, remote, protectedPaths, additionalRemovals),
|
|
1447
|
+
...additionalRemovals.length ? ["Case-variant aliases listed above are removed before writing canonical paths."] : [],
|
|
1415
1448
|
"A local backup is created before applying; the backend's active head will change."
|
|
1416
1449
|
].join("\n");
|
|
1417
1450
|
}
|
|
1418
1451
|
function formatPublicationPreview(remote, upload) {
|
|
1419
|
-
if (!remote) {
|
|
1420
|
-
return ["Remote is empty.", ...upload.files.map((file) => `Add remotely: ${snapshotPathLabel(file.path)}`)].join(
|
|
1421
|
-
"\n"
|
|
1422
|
-
);
|
|
1423
|
-
}
|
|
1424
1452
|
return formatDirectionalChanges(remote, upload, {
|
|
1425
1453
|
add: "Add remotely",
|
|
1426
1454
|
update: "Update remotely",
|
|
1427
1455
|
remove: "Remove remotely"
|
|
1428
1456
|
});
|
|
1429
1457
|
}
|
|
1430
|
-
function formatDirectionalChanges(before, after, labels) {
|
|
1431
|
-
const
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
else if (beforeMap[filePath] !== afterMap[filePath]) lines.push(`${labels.update}: ${snapshotPathLabel(filePath)}`);
|
|
1458
|
+
function formatDirectionalChanges(before, after, labels, protectedPaths = /* @__PURE__ */ new Set(), additionalRemovals = []) {
|
|
1459
|
+
const changes = transferChanges(before, after, protectedPaths);
|
|
1460
|
+
const known = new Set(changes.map((change) => change.path));
|
|
1461
|
+
for (const filePath of additionalRemovals) {
|
|
1462
|
+
if (!known.has(filePath) && !protectedPaths.has(filePath)) {
|
|
1463
|
+
changes.push({ path: filePath, action: "Delete" });
|
|
1464
|
+
known.add(filePath);
|
|
1465
|
+
}
|
|
1439
1466
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1467
|
+
const counts = transferCounts(changes);
|
|
1468
|
+
const label = { Add: labels.add, Update: labels.update, Delete: labels.remove };
|
|
1469
|
+
return [
|
|
1470
|
+
`Changes: ${counts.add} add \xB7 ${counts.update} update \xB7 ${counts.remove} remove`,
|
|
1471
|
+
...changes.map((change) => `${label[change.action]}: ${snapshotPathLabel(change.path)}`),
|
|
1472
|
+
...changes.length ? [] : ["No file changes."]
|
|
1473
|
+
].join("\n");
|
|
1442
1474
|
}
|
|
1443
1475
|
function countPreservedRemoteFiles(local, upload) {
|
|
1444
1476
|
const localPaths = new Set(local.files.map((file) => file.path));
|
|
@@ -1503,6 +1535,7 @@ function safeList(values) {
|
|
|
1503
1535
|
// src/sync/merged-sync.ts
|
|
1504
1536
|
var MAX_ATTEMPTS = 3;
|
|
1505
1537
|
async function mergeSync(ctx, options, factory = createSyncBackend, resolution) {
|
|
1538
|
+
const timings = options.startupTimings ?? createStartupTimings();
|
|
1506
1539
|
const validateOwner = captureMutationOwner(ctx, options.signal);
|
|
1507
1540
|
const isCurrent = () => {
|
|
1508
1541
|
try {
|
|
@@ -1521,7 +1554,7 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1521
1554
|
sessionDir: sessionRoot,
|
|
1522
1555
|
signal: options.signal
|
|
1523
1556
|
};
|
|
1524
|
-
const validate = async () => {
|
|
1557
|
+
const validate = () => timings.measure("transfer authorization", async () => {
|
|
1525
1558
|
validateOwner();
|
|
1526
1559
|
const latest = await loadConfig(options.setup);
|
|
1527
1560
|
validateOwner();
|
|
@@ -1535,19 +1568,22 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1535
1568
|
}
|
|
1536
1569
|
if (options.auto && (!latest.automaticTransfer || !ctx.isIdle()))
|
|
1537
1570
|
throw new Error("Automatic transfer is no longer authorized at this idle boundary.");
|
|
1538
|
-
};
|
|
1571
|
+
});
|
|
1539
1572
|
await validate();
|
|
1540
|
-
const backend = await factory(config);
|
|
1573
|
+
const backend = await timings.measure("transfer backend initialization", () => factory(config));
|
|
1541
1574
|
await validate();
|
|
1542
1575
|
if (options.auto && backend.capability === "read-check-write-verify") {
|
|
1543
1576
|
throw new Error(
|
|
1544
1577
|
"Automatic transfer requires conditional publication or a lease; this backend supports only read/check/write/verify. Use manual sync."
|
|
1545
1578
|
);
|
|
1546
1579
|
}
|
|
1547
|
-
const pending = await readMergeJournal(config);
|
|
1580
|
+
const pending = await timings.measure("transfer journal read", () => readMergeJournal(config));
|
|
1548
1581
|
await validate();
|
|
1549
1582
|
if (pending) {
|
|
1550
|
-
const recovered = await
|
|
1583
|
+
const recovered = await timings.measure(
|
|
1584
|
+
"transfer journal recovery",
|
|
1585
|
+
() => completeJournal(ctx, config, backend, pending, validate, timings, options.signal, options.auto)
|
|
1586
|
+
);
|
|
1551
1587
|
if (!recovered) {
|
|
1552
1588
|
if ((options.auto || !options.silent) && !options.signal?.aborted)
|
|
1553
1589
|
ctx.ui.notify(
|
|
@@ -1566,18 +1602,40 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1566
1602
|
try {
|
|
1567
1603
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1568
1604
|
await validate();
|
|
1569
|
-
const state = await readStateForConfig(config);
|
|
1605
|
+
const state = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1570
1606
|
if (state.unresolved?.length && !config.partialSync)
|
|
1571
1607
|
throw new Error(
|
|
1572
1608
|
"Partial progress remains unresolved; re-enable partial sync or review an explicit force direction."
|
|
1573
1609
|
);
|
|
1574
|
-
const localRaw = await
|
|
1610
|
+
const localRaw = await timings.measure(
|
|
1611
|
+
"transfer local snapshot",
|
|
1612
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1613
|
+
);
|
|
1575
1614
|
const local = portableSnapshot(localRaw, config.localFields);
|
|
1576
1615
|
if (!sameLocalFields(state.localFields, config.localFields))
|
|
1577
1616
|
throw new Error("Local-field rules changed; review and confirm a directional migration first.");
|
|
1578
|
-
const head = await backend.readHead(options.signal);
|
|
1617
|
+
const head = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1579
1618
|
await validate();
|
|
1580
|
-
|
|
1619
|
+
if (options.auto && !resolution && !config.partialSync && head && state.lastAppliedSnapshot && state.lastRemoteRevision && !state.unresolved?.length && head.snapshotId === state.lastAppliedSnapshot && backend.sameRevision(head.revision, state.lastRemoteRevision) && inspectRemoteSelection(config.include, { selection: head.selection, files: [] }).kind === "same" && matchesAcceptedState(state, config, head, local)) {
|
|
1620
|
+
const unchanged = await timings.measure("transfer unchanged verification", async () => {
|
|
1621
|
+
if (!await hasCurrentMergeBaseline(config, local, state)) return false;
|
|
1622
|
+
await validate();
|
|
1623
|
+
const currentLocal = await timings.measure(
|
|
1624
|
+
"transfer local snapshot",
|
|
1625
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1626
|
+
);
|
|
1627
|
+
await validate();
|
|
1628
|
+
const currentState = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1629
|
+
await validate();
|
|
1630
|
+
return sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) && syncStateFingerprint(currentState) === syncStateFingerprint(state);
|
|
1631
|
+
});
|
|
1632
|
+
await validate();
|
|
1633
|
+
if (unchanged) {
|
|
1634
|
+
timings.markUnchanged();
|
|
1635
|
+
return "applied";
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const rawRemote = head ? await timings.measure("transfer snapshot download", () => readSnapshotForHead(backend, head, options.signal)) : void 0;
|
|
1581
1639
|
await validate();
|
|
1582
1640
|
if (rawRemote) {
|
|
1583
1641
|
const files = [...local.files, ...rawRemote.files];
|
|
@@ -1590,12 +1648,16 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1590
1648
|
const remote = rawRemote ? portableSnapshot(filterSnapshotForConfigPolicy(rawRemote, config), rawRemote.localFields) : void 0;
|
|
1591
1649
|
if (rawRemote) {
|
|
1592
1650
|
requireCompatibleRemoteSelection(config, rawRemote);
|
|
1593
|
-
const validation =
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1651
|
+
const validation = await timings.measure(
|
|
1652
|
+
"transfer merge planning",
|
|
1653
|
+
() => planFileMerge({
|
|
1654
|
+
baseline: state.lastFileHashes,
|
|
1655
|
+
local: local.files,
|
|
1656
|
+
remote: rawRemote.files,
|
|
1657
|
+
selectionCompatible: true
|
|
1658
|
+
})
|
|
1659
|
+
);
|
|
1660
|
+
await validate();
|
|
1599
1661
|
const managedPaths = /* @__PURE__ */ new Set([
|
|
1600
1662
|
...Object.keys(state.lastFileHashes),
|
|
1601
1663
|
...[...local.files, ...remote?.files ?? []].map((file) => file.path)
|
|
@@ -1622,30 +1684,35 @@ async function mergeSync(ctx, options, factory = createSyncBackend, resolution)
|
|
|
1622
1684
|
throw review("The established remote is missing; choose an explicit recovery direction.", "remote-empty");
|
|
1623
1685
|
const selectionState = inspectRemoteSelection(config.include, rawRemote);
|
|
1624
1686
|
const selectionCompatible = selectionState.kind === "same";
|
|
1625
|
-
let plan =
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1687
|
+
let plan = await timings.measure(
|
|
1688
|
+
"transfer merge planning",
|
|
1689
|
+
() => planFileMerge({
|
|
1690
|
+
baseline: state.lastFileHashes,
|
|
1691
|
+
local: local.files,
|
|
1692
|
+
remote: remote.files,
|
|
1693
|
+
selectionCompatible,
|
|
1694
|
+
protectedPaths: protectedSessionPaths(ctx, sessionRoot)
|
|
1695
|
+
})
|
|
1696
|
+
);
|
|
1697
|
+
await validate();
|
|
1632
1698
|
let resolved = { plan, fields: [] };
|
|
1633
1699
|
if (config.mergeSettings) {
|
|
1634
1700
|
try {
|
|
1635
|
-
resolved = await
|
|
1701
|
+
resolved = await timings.measure(
|
|
1702
|
+
"transfer settings merge",
|
|
1703
|
+
() => resolveSettingsConflicts(config, state, local, remote, plan)
|
|
1704
|
+
);
|
|
1636
1705
|
} catch (error) {
|
|
1637
1706
|
if (!config.partialSync) throw error;
|
|
1638
1707
|
}
|
|
1639
1708
|
}
|
|
1640
1709
|
await validate();
|
|
1641
|
-
plan = await
|
|
1642
|
-
|
|
1643
|
-
state,
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
protectedSessionPaths(ctx, sessionRoot),
|
|
1648
|
-
{ textBudget, signal: options.signal }
|
|
1710
|
+
plan = await timings.measure(
|
|
1711
|
+
"transfer content merge",
|
|
1712
|
+
() => resolveContentConflicts(config, state, local, remote, resolved.plan, protectedSessionPaths(ctx, sessionRoot), {
|
|
1713
|
+
textBudget,
|
|
1714
|
+
signal: options.signal
|
|
1715
|
+
})
|
|
1649
1716
|
);
|
|
1650
1717
|
await validate();
|
|
1651
1718
|
if (resolution) {
|
|
@@ -1752,37 +1819,49 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1752
1819
|
const publish = !sameHashes(fileHashMap(publication), fileHashMap(remote)) || Boolean(config.partialSync && rawRemote.version !== 3);
|
|
1753
1820
|
const apply = !sameHashes(fileHashMap(localRaw), fileHashMap(after));
|
|
1754
1821
|
if (!publish && !apply) {
|
|
1755
|
-
const beforeEvidence = await backend.readHead(options.signal);
|
|
1822
|
+
const beforeEvidence = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1756
1823
|
await validate();
|
|
1757
1824
|
if (!beforeEvidence || !backend.sameRevision(beforeEvidence.revision, head.revision))
|
|
1758
1825
|
throw new Error("Remote changed before baseline acceptance; retry from a fresh observation.");
|
|
1759
1826
|
progress = await persistProgress();
|
|
1760
1827
|
await validate();
|
|
1761
|
-
const currentLocal = await
|
|
1828
|
+
const currentLocal = await timings.measure(
|
|
1829
|
+
"transfer local snapshot",
|
|
1830
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1831
|
+
);
|
|
1762
1832
|
await validate();
|
|
1763
|
-
if (!sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) || syncStateFingerprint(await readStateForConfig(config)) !== syncStateFingerprint(state))
|
|
1833
|
+
if (!sameHashes(fileHashMap(currentLocal), fileHashMap(localRaw)) || syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== syncStateFingerprint(state))
|
|
1764
1834
|
throw new Error(
|
|
1765
1835
|
"Local content or baseline changed before baseline acceptance; retry from a fresh observation."
|
|
1766
1836
|
);
|
|
1767
1837
|
await validate();
|
|
1768
|
-
const currentHead = await backend.readHead(options.signal);
|
|
1838
|
+
const currentHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1769
1839
|
await validate();
|
|
1770
1840
|
if (!currentHead || !backend.sameRevision(currentHead.revision, head.revision))
|
|
1771
1841
|
throw new Error("Remote changed before baseline acceptance; retry from a fresh observation.");
|
|
1772
|
-
await
|
|
1773
|
-
|
|
1774
|
-
config,
|
|
1775
|
-
|
|
1776
|
-
|
|
1842
|
+
await timings.measure(
|
|
1843
|
+
"transfer baseline write",
|
|
1844
|
+
() => writeAcceptedState(config, head, accepted, validate, progress)
|
|
1845
|
+
);
|
|
1846
|
+
await timings.measure(
|
|
1847
|
+
"transfer cleanup",
|
|
1848
|
+
() => pruneMergeBaselines(
|
|
1849
|
+
config,
|
|
1850
|
+
acceptedState(config, head, accepted, progress),
|
|
1851
|
+
captureMutationOwner(ctx, options.signal)
|
|
1852
|
+
)
|
|
1777
1853
|
);
|
|
1778
1854
|
await validate();
|
|
1779
|
-
await
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1855
|
+
await timings.measure(
|
|
1856
|
+
"transfer cleanup",
|
|
1857
|
+
() => pruneCompletedConflicts(
|
|
1858
|
+
config,
|
|
1859
|
+
backend.identity,
|
|
1860
|
+
acceptedState(config, head, accepted, progress),
|
|
1861
|
+
accepted,
|
|
1862
|
+
remote,
|
|
1863
|
+
captureMutationOwner(ctx, options.signal)
|
|
1864
|
+
)
|
|
1786
1865
|
);
|
|
1787
1866
|
await validate();
|
|
1788
1867
|
if (!options.silent)
|
|
@@ -1792,7 +1871,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1792
1871
|
);
|
|
1793
1872
|
return "applied";
|
|
1794
1873
|
}
|
|
1795
|
-
await
|
|
1874
|
+
await timings.measure(
|
|
1875
|
+
"transfer preflight",
|
|
1876
|
+
() => preflightMergedTargets(localRaw, after, snapshotOptions, ctx.sessionManager.getSessionFile?.())
|
|
1877
|
+
);
|
|
1796
1878
|
await validate();
|
|
1797
1879
|
if (!config.skipSecretScan && scanSnapshot(upload).length)
|
|
1798
1880
|
throw new Error("Refusing to merge possible secrets. Review managed content before syncing.");
|
|
@@ -1822,19 +1904,25 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1822
1904
|
return "cancelled";
|
|
1823
1905
|
}
|
|
1824
1906
|
await validate();
|
|
1825
|
-
const refreshed = await
|
|
1907
|
+
const refreshed = await timings.measure(
|
|
1908
|
+
"transfer local snapshot",
|
|
1909
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1910
|
+
);
|
|
1826
1911
|
if (!sameHashes(fileHashMap(refreshed), fileHashMap(localRaw)))
|
|
1827
1912
|
throw new Error("Local content changed during review; no merged transfer was performed.");
|
|
1828
|
-
if (syncStateFingerprint(await readStateForConfig(config)) !== syncStateFingerprint(state))
|
|
1913
|
+
if (syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== syncStateFingerprint(state))
|
|
1829
1914
|
throw new Error("Sync baseline changed during review; retry from a fresh observation.");
|
|
1830
1915
|
await validate();
|
|
1831
|
-
const refreshedHead = await backend.readHead(options.signal);
|
|
1916
|
+
const refreshedHead = await timings.measure("transfer remote head", () => backend.readHead(options.signal));
|
|
1832
1917
|
await validate();
|
|
1833
1918
|
if (!refreshedHead || !backend.sameRevision(refreshedHead.revision, head.revision))
|
|
1834
1919
|
throw new Error("Remote changed during review; no merged transfer was performed.");
|
|
1835
1920
|
progress = await persistProgress();
|
|
1836
1921
|
await validate();
|
|
1837
|
-
const backup = await
|
|
1922
|
+
const backup = await timings.measure(
|
|
1923
|
+
"transfer backup",
|
|
1924
|
+
() => backupLocal(config.snapshotIdentity, snapshotOptions, options.signal)
|
|
1925
|
+
);
|
|
1838
1926
|
await validate();
|
|
1839
1927
|
const journal = {
|
|
1840
1928
|
version: 1,
|
|
@@ -1850,10 +1938,15 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1850
1938
|
...sessionRoot !== void 0 ? { sessionRoot } : {},
|
|
1851
1939
|
...!publish ? { committedHead: head, applyOnly: true } : {}
|
|
1852
1940
|
};
|
|
1853
|
-
await writeMergeJournal(config, journal);
|
|
1854
|
-
const atCommitState = syncStateFingerprint(
|
|
1941
|
+
await timings.measure("transfer journal write", () => writeMergeJournal(config, journal));
|
|
1942
|
+
const atCommitState = syncStateFingerprint(
|
|
1943
|
+
await timings.measure("transfer state read", () => readStateForConfig(config))
|
|
1944
|
+
);
|
|
1855
1945
|
await validate();
|
|
1856
|
-
const atCommit = await
|
|
1946
|
+
const atCommit = await timings.measure(
|
|
1947
|
+
"transfer local snapshot",
|
|
1948
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
1949
|
+
);
|
|
1857
1950
|
await validate();
|
|
1858
1951
|
if (!sameHashes(fileHashMap(atCommit), fileHashMap(localRaw)) || atCommitState !== journal.stateIdentity) {
|
|
1859
1952
|
await validate();
|
|
@@ -1863,7 +1956,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1863
1956
|
);
|
|
1864
1957
|
}
|
|
1865
1958
|
try {
|
|
1866
|
-
await
|
|
1959
|
+
await timings.measure(
|
|
1960
|
+
"transfer preflight",
|
|
1961
|
+
() => preflightMergedTargets(atCommit, after, snapshotOptions, ctx.sessionManager.getSessionFile?.())
|
|
1962
|
+
);
|
|
1867
1963
|
await validate();
|
|
1868
1964
|
} catch (error) {
|
|
1869
1965
|
await validate();
|
|
@@ -1872,13 +1968,16 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1872
1968
|
}
|
|
1873
1969
|
try {
|
|
1874
1970
|
if (publish) {
|
|
1875
|
-
const result = await
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1971
|
+
const result = await timings.measure(
|
|
1972
|
+
"transfer remote publication",
|
|
1973
|
+
() => backend.publishSnapshot(upload, expectedRemoteHead(head), {
|
|
1974
|
+
signal: options.signal,
|
|
1975
|
+
onCommit: options.onCommit
|
|
1976
|
+
})
|
|
1977
|
+
);
|
|
1879
1978
|
journal.committedHead = result.head;
|
|
1880
1979
|
journal.warnings = result.warnings;
|
|
1881
|
-
await writeMergeJournal(config, journal);
|
|
1980
|
+
await timings.measure("transfer journal write", () => writeMergeJournal(config, journal));
|
|
1882
1981
|
} else options.onCommit?.();
|
|
1883
1982
|
} catch (error) {
|
|
1884
1983
|
if (error instanceof SyncBackendConflictError && error.phase === "before-commit" && !error.candidateMayHaveBeenActive) {
|
|
@@ -1888,7 +1987,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1888
1987
|
}
|
|
1889
1988
|
throw mergeTransferError(error, config.backend);
|
|
1890
1989
|
}
|
|
1891
|
-
const completed = await
|
|
1990
|
+
const completed = await timings.measure(
|
|
1991
|
+
"transfer journal recovery",
|
|
1992
|
+
() => completeJournal(ctx, config, backend, journal, validate, timings, options.signal, options.auto)
|
|
1993
|
+
);
|
|
1892
1994
|
if (!completed) {
|
|
1893
1995
|
if (!options.signal?.aborted && (options.auto || !options.silent))
|
|
1894
1996
|
ctx.ui.notify(
|
|
@@ -1911,10 +2013,10 @@ An explicit direction adopts this setup's included-content policy; no automatic
|
|
|
1911
2013
|
return "applied";
|
|
1912
2014
|
}
|
|
1913
2015
|
} finally {
|
|
1914
|
-
await releaseConflictArtifacts(config, ownedStatePath, createdArtifacts);
|
|
2016
|
+
await timings.measure("transfer cleanup", () => releaseConflictArtifacts(config, ownedStatePath, createdArtifacts));
|
|
1915
2017
|
}
|
|
1916
2018
|
}
|
|
1917
|
-
async function completeJournal(ctx, config, backend, journal, validate, signal, auto = false) {
|
|
2019
|
+
async function completeJournal(ctx, config, backend, journal, validate, timings, signal, auto = false) {
|
|
1918
2020
|
if (journal.identity !== mergeJournalIdentity(config, backend.identity) || !sameLocalFields(journal.upload.localFields, config.localFields))
|
|
1919
2021
|
throw new Error(
|
|
1920
2022
|
"Merge journal belongs to a different setup, selection or local-field policy; preserve it for reviewed recovery."
|
|
@@ -1928,11 +2030,14 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1928
2030
|
);
|
|
1929
2031
|
}
|
|
1930
2032
|
const snapshotOptions = { ...snapshotOptionsForContext(ctx, config), sessionDir: sessionRoot, signal };
|
|
1931
|
-
const head = await backend.readHead(signal);
|
|
2033
|
+
const head = await timings.measure("transfer remote head", () => backend.readHead(signal));
|
|
1932
2034
|
if (!journal.committedHead && head && (backend.sameRevision(head.revision, journal.expectedHead.revision) || backend.matchesUncommittedRecoveryHead?.(head, journal.expectedHead))) {
|
|
1933
2035
|
await validate();
|
|
1934
|
-
const local = await
|
|
1935
|
-
|
|
2036
|
+
const local = await timings.measure(
|
|
2037
|
+
"transfer local snapshot",
|
|
2038
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
2039
|
+
);
|
|
2040
|
+
if (!sameHashes(fileHashMap(local), fileHashMap(journal.before)) || syncStateFingerprint(await timings.measure("transfer state read", () => readStateForConfig(config))) !== journal.stateIdentity) {
|
|
1936
2041
|
throw new Error(
|
|
1937
2042
|
"Uncertain publication is not active, but local content or baseline changed; keep the journal for reviewed recovery."
|
|
1938
2043
|
);
|
|
@@ -1942,7 +2047,7 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1942
2047
|
}
|
|
1943
2048
|
await validate();
|
|
1944
2049
|
if (journal.committedHead && (!head || !backend.sameRevision(head.revision, journal.committedHead.revision))) {
|
|
1945
|
-
const state2 = await readStateForConfig(config);
|
|
2050
|
+
const state2 = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1946
2051
|
await validate();
|
|
1947
2052
|
if (matchesAcceptedState(state2, config, journal.committedHead, journal.accepted ?? journal.after, journal.progress)) {
|
|
1948
2053
|
await clearMergeJournal(config);
|
|
@@ -1953,8 +2058,11 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1953
2058
|
}
|
|
1954
2059
|
}
|
|
1955
2060
|
if (journal.applyOnly && (!head || !backend.sameRevision(head.revision, journal.expectedHead.revision))) {
|
|
1956
|
-
const local = await
|
|
1957
|
-
|
|
2061
|
+
const local = await timings.measure(
|
|
2062
|
+
"transfer local snapshot",
|
|
2063
|
+
() => createSnapshot(config.snapshotIdentity, snapshotOptions)
|
|
2064
|
+
);
|
|
2065
|
+
const state2 = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1958
2066
|
await validate();
|
|
1959
2067
|
if (sameHashes(fileHashMap(local), fileHashMap(journal.before)) && syncStateFingerprint(state2) === journal.stateIdentity) {
|
|
1960
2068
|
await clearMergeJournal(config);
|
|
@@ -1967,11 +2075,14 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1967
2075
|
"Publication outcome cannot be reconciled with the current remote head. Journal and backup retained; do not retry publication or restore old bytes blindly."
|
|
1968
2076
|
);
|
|
1969
2077
|
}
|
|
1970
|
-
const observed = await
|
|
2078
|
+
const observed = await timings.measure(
|
|
2079
|
+
"transfer snapshot download",
|
|
2080
|
+
() => readSnapshotForHead(backend, head, signal)
|
|
2081
|
+
);
|
|
1971
2082
|
requireCompatibleRemoteSelection(config, observed);
|
|
1972
2083
|
if (!sameHashes(fileHashMap(observed), fileHashMap(journal.upload)) || JSON.stringify(observed.selection) !== JSON.stringify(journal.upload.selection))
|
|
1973
2084
|
throw new Error("Remote snapshot does not match the recorded merge publication.");
|
|
1974
|
-
const state = await readStateForConfig(config);
|
|
2085
|
+
const state = await timings.measure("transfer state read", () => readStateForConfig(config));
|
|
1975
2086
|
if (matchesAcceptedState(state, config, head, journal.accepted ?? journal.after, journal.progress)) {
|
|
1976
2087
|
await validate();
|
|
1977
2088
|
await clearMergeJournal(config);
|
|
@@ -1994,36 +2105,48 @@ async function completeJournal(ctx, config, backend, journal, validate, signal,
|
|
|
1994
2105
|
validateOwner();
|
|
1995
2106
|
if (auto && !ctx.isIdle()) throw new Error("Automatic file apply is no longer idle; evidence retained.");
|
|
1996
2107
|
};
|
|
1997
|
-
await
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2108
|
+
await timings.measure(
|
|
2109
|
+
"transfer local apply",
|
|
2110
|
+
() => applyMergedSnapshot(
|
|
2111
|
+
journal.before,
|
|
2112
|
+
journal.after,
|
|
2113
|
+
protectedSessionPaths(ctx, sessionRoot),
|
|
2114
|
+
{ ...snapshotOptions, validateMutation },
|
|
2115
|
+
validate,
|
|
2116
|
+
ctx.sessionManager.getSessionFile?.()
|
|
2117
|
+
)
|
|
2004
2118
|
);
|
|
2005
2119
|
await validate();
|
|
2006
|
-
const headAtAcceptance = await backend.readHead(signal);
|
|
2120
|
+
const headAtAcceptance = await timings.measure("transfer remote head", () => backend.readHead(signal));
|
|
2007
2121
|
await validate();
|
|
2008
2122
|
if (!headAtAcceptance || !backend.sameRevision(headAtAcceptance.revision, head.revision))
|
|
2009
2123
|
throw new Error("Remote changed during merged apply; journal and backup retained for review.");
|
|
2010
|
-
await
|
|
2124
|
+
await timings.measure(
|
|
2125
|
+
"transfer baseline write",
|
|
2126
|
+
() => writeAcceptedState(config, head, journal.accepted ?? journal.after, validate, journal.progress)
|
|
2127
|
+
);
|
|
2011
2128
|
await validate();
|
|
2012
2129
|
await clearMergeJournal(config);
|
|
2013
2130
|
await validate();
|
|
2014
|
-
await
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2131
|
+
await timings.measure(
|
|
2132
|
+
"transfer cleanup",
|
|
2133
|
+
() => pruneMergeBaselines(
|
|
2134
|
+
config,
|
|
2135
|
+
acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
|
|
2136
|
+
captureMutationOwner(ctx, signal)
|
|
2137
|
+
)
|
|
2018
2138
|
);
|
|
2019
2139
|
await validate();
|
|
2020
|
-
await
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2140
|
+
await timings.measure(
|
|
2141
|
+
"transfer cleanup",
|
|
2142
|
+
() => pruneCompletedConflicts(
|
|
2143
|
+
config,
|
|
2144
|
+
backend.identity,
|
|
2145
|
+
acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
|
|
2146
|
+
journal.accepted ?? journal.after,
|
|
2147
|
+
journal.upload,
|
|
2148
|
+
captureMutationOwner(ctx, signal)
|
|
2149
|
+
)
|
|
2027
2150
|
);
|
|
2028
2151
|
await validate();
|
|
2029
2152
|
return true;
|
|
@@ -2057,6 +2180,7 @@ async function writeAcceptedState(config, head, snapshot, validate, progress) {
|
|
|
2057
2180
|
export {
|
|
2058
2181
|
confirmMergeReview,
|
|
2059
2182
|
applySnapshot,
|
|
2183
|
+
addTopLevelCaseVariantDeletes,
|
|
2060
2184
|
formatDiff,
|
|
2061
2185
|
formatSnapshotOnlyDiff,
|
|
2062
2186
|
formatPushSummary,
|
|
@@ -2067,4 +2191,4 @@ export {
|
|
|
2067
2191
|
createSyncDecision,
|
|
2068
2192
|
mergeSync
|
|
2069
2193
|
};
|
|
2070
|
-
//# sourceMappingURL=chunk-
|
|
2194
|
+
//# sourceMappingURL=chunk-35GM6XKM.ts.map
|