@narumitw/pi-sync 0.54.1 → 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.
Files changed (102) hide show
  1. package/README.md +19 -11
  2. package/dist/chunks/{chunk-ORQRHEEO.ts → chunk-26PMXY7E.ts} +3 -3
  3. package/dist/chunks/{chunk-SDZCTCYQ.ts → chunk-2FETCGGT.ts} +2 -2
  4. package/dist/chunks/{chunk-HUTYAPFP.ts → chunk-35GM6XKM.ts} +275 -151
  5. package/dist/chunks/chunk-35GM6XKM.ts.map +7 -0
  6. package/dist/chunks/{chunk-V6VZ5TLG.ts → chunk-A3TFXY4O.ts} +2 -2
  7. package/dist/chunks/{chunk-5RZPINUQ.ts → chunk-AAZDBIBJ.ts} +2 -2
  8. package/dist/chunks/{chunk-5ZWQ33XL.ts → chunk-CAGJOKB7.ts} +5 -5
  9. package/dist/chunks/{chunk-QE7NZZWT.ts → chunk-E6ONFG57.ts} +15 -2
  10. package/dist/chunks/{chunk-QE7NZZWT.ts.map → chunk-E6ONFG57.ts.map} +2 -2
  11. package/dist/chunks/{chunk-IJ6VV6FN.ts → chunk-GC3QOL6J.ts} +2 -2
  12. package/dist/chunks/{chunk-VHXBCEVF.ts → chunk-HMXUKU43.ts} +3 -3
  13. package/dist/chunks/{chunk-4SBTAVFL.ts → chunk-I4JYVFUS.ts} +5 -6
  14. package/dist/chunks/chunk-I4JYVFUS.ts.map +7 -0
  15. package/dist/chunks/{chunk-2DI6PDZV.ts → chunk-ITFR6NNQ.ts} +14 -2
  16. package/dist/chunks/chunk-ITFR6NNQ.ts.map +7 -0
  17. package/dist/chunks/{chunk-3B4Z2SKV.ts → chunk-KATKRR3D.ts} +18 -11
  18. package/dist/chunks/chunk-KATKRR3D.ts.map +7 -0
  19. package/dist/chunks/{chunk-ANWNQIR7.ts → chunk-KGE47GCZ.ts} +4 -4
  20. package/dist/chunks/chunk-KGE47GCZ.ts.map +7 -0
  21. package/dist/chunks/{chunk-CHSIAC4N.ts → chunk-R35N4CL3.ts} +2 -2
  22. package/dist/chunks/chunk-UZPMDLTL.ts +54 -0
  23. package/dist/chunks/chunk-UZPMDLTL.ts.map +7 -0
  24. package/dist/chunks/{chunk-4QVPTIZB.ts → chunk-VCL7TZYW.ts} +2 -2
  25. package/dist/chunks/{chunk-WFKUKVCP.ts → chunk-XU2VXGQV.ts} +14 -8
  26. package/dist/chunks/chunk-XU2VXGQV.ts.map +7 -0
  27. package/dist/chunks/{chunk-UQ3ALBIB.ts → chunk-YTHQEW35.ts} +3 -3
  28. package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts → conflict-artifacts-4CZFB7WI.ts} +3 -3
  29. package/dist/chunks/{conflict-review-JRFJIBEN.ts → conflict-review-ZOTOHL2E.ts} +11 -10
  30. package/dist/chunks/{conflict-review-JRFJIBEN.ts.map → conflict-review-ZOTOHL2E.ts.map} +1 -1
  31. package/dist/chunks/{file-selection-WZX7HAY2.ts → file-selection-UNHBVVON.ts} +3 -3
  32. package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts → manager-result-dispatcher-HVRTPEVK.ts} +7 -7
  33. package/dist/chunks/{manager-ui-IURS4WXY.ts → manager-ui-ZIN4BUSF.ts} +297 -136
  34. package/dist/chunks/manager-ui-ZIN4BUSF.ts.map +7 -0
  35. package/dist/chunks/{merge-baseline-store-EVQAAML6.ts → merge-baseline-store-ICXVTCML.ts} +5 -3
  36. package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts → remote-selection-ui-H6AEVA3Q.ts} +6 -6
  37. package/dist/chunks/{s3-backend-AQGV6LFT.ts → s3-backend-Y4P3O5B5.ts} +2 -2
  38. package/dist/chunks/{setup-switch-KTUWL6DE.ts → setup-switch-ZMW5C4M7.ts} +4 -4
  39. package/dist/chunks/{setup-wizard-GSDPDFC4.ts → setup-wizard-VW7F3JN4.ts} +5 -5
  40. package/dist/chunks/{sync-inspection-K45QVLHB.ts → sync-inspection-3EMS2J5B.ts} +5 -4
  41. package/dist/chunks/{sync-operations-QQ2GXKVC.ts → sync-operations-2CWST333.ts} +140 -24
  42. package/dist/chunks/sync-operations-2CWST333.ts.map +7 -0
  43. package/dist/index.ts +103 -68
  44. package/dist/index.ts.map +2 -2
  45. package/docs/settings.md +47 -10
  46. package/docs/tui-contract.md +61 -0
  47. package/package.json +1 -1
  48. package/src/commands/command-execution.ts +2 -1
  49. package/src/commands/command-types.ts +4 -0
  50. package/src/commands/command.ts +3 -1
  51. package/src/settings/config.ts +13 -10
  52. package/src/settings/settings-store.ts +1 -0
  53. package/src/settings/settings-types.ts +2 -0
  54. package/src/settings/settings-validation.ts +3 -0
  55. package/src/snapshot/snapshot-transaction.ts +4 -2
  56. package/src/state/merge-baseline-store.ts +18 -0
  57. package/src/sync/apply-review.ts +63 -0
  58. package/src/sync/automatic-sync.ts +45 -33
  59. package/src/sync/merged-sync.ts +202 -104
  60. package/src/sync/startup-check.ts +39 -21
  61. package/src/sync/startup-timings.ts +90 -0
  62. package/src/sync/sync-inspection.ts +8 -4
  63. package/src/sync/sync-mutations.ts +49 -11
  64. package/src/sync/sync-queries.ts +14 -3
  65. package/src/ui/manager-state.ts +10 -6
  66. package/src/ui/manager-ui.ts +57 -7
  67. package/src/ui/merge-review.ts +3 -3
  68. package/src/ui/merged-sync-summary.ts +2 -18
  69. package/src/ui/settings-frame.ts +55 -0
  70. package/src/ui/settings-ui.ts +141 -43
  71. package/src/ui/setup/setup-prompts.ts +4 -3
  72. package/src/ui/sync-behavior.ts +15 -0
  73. package/src/ui/sync-format.ts +69 -38
  74. package/src/ui/transfer-changes.ts +33 -0
  75. package/src/ui/transfer-review.ts +27 -0
  76. package/dist/chunks/chunk-2DI6PDZV.ts.map +0 -7
  77. package/dist/chunks/chunk-3B4Z2SKV.ts.map +0 -7
  78. package/dist/chunks/chunk-4SBTAVFL.ts.map +0 -7
  79. package/dist/chunks/chunk-ANWNQIR7.ts.map +0 -7
  80. package/dist/chunks/chunk-HUTYAPFP.ts.map +0 -7
  81. package/dist/chunks/chunk-WFKUKVCP.ts.map +0 -7
  82. package/dist/chunks/manager-ui-IURS4WXY.ts.map +0 -7
  83. package/dist/chunks/sync-operations-QQ2GXKVC.ts.map +0 -7
  84. /package/dist/chunks/{chunk-ORQRHEEO.ts.map → chunk-26PMXY7E.ts.map} +0 -0
  85. /package/dist/chunks/{chunk-SDZCTCYQ.ts.map → chunk-2FETCGGT.ts.map} +0 -0
  86. /package/dist/chunks/{chunk-V6VZ5TLG.ts.map → chunk-A3TFXY4O.ts.map} +0 -0
  87. /package/dist/chunks/{chunk-5RZPINUQ.ts.map → chunk-AAZDBIBJ.ts.map} +0 -0
  88. /package/dist/chunks/{chunk-5ZWQ33XL.ts.map → chunk-CAGJOKB7.ts.map} +0 -0
  89. /package/dist/chunks/{chunk-IJ6VV6FN.ts.map → chunk-GC3QOL6J.ts.map} +0 -0
  90. /package/dist/chunks/{chunk-VHXBCEVF.ts.map → chunk-HMXUKU43.ts.map} +0 -0
  91. /package/dist/chunks/{chunk-CHSIAC4N.ts.map → chunk-R35N4CL3.ts.map} +0 -0
  92. /package/dist/chunks/{chunk-4QVPTIZB.ts.map → chunk-VCL7TZYW.ts.map} +0 -0
  93. /package/dist/chunks/{chunk-UQ3ALBIB.ts.map → chunk-YTHQEW35.ts.map} +0 -0
  94. /package/dist/chunks/{conflict-artifacts-BB2FZMIE.ts.map → conflict-artifacts-4CZFB7WI.ts.map} +0 -0
  95. /package/dist/chunks/{file-selection-WZX7HAY2.ts.map → file-selection-UNHBVVON.ts.map} +0 -0
  96. /package/dist/chunks/{manager-result-dispatcher-6MQKFCWB.ts.map → manager-result-dispatcher-HVRTPEVK.ts.map} +0 -0
  97. /package/dist/chunks/{merge-baseline-store-EVQAAML6.ts.map → merge-baseline-store-ICXVTCML.ts.map} +0 -0
  98. /package/dist/chunks/{remote-selection-ui-6LJJ4IMQ.ts.map → remote-selection-ui-H6AEVA3Q.ts.map} +0 -0
  99. /package/dist/chunks/{s3-backend-AQGV6LFT.ts.map → s3-backend-Y4P3O5B5.ts.map} +0 -0
  100. /package/dist/chunks/{setup-switch-KTUWL6DE.ts.map → setup-switch-ZMW5C4M7.ts.map} +0 -0
  101. /package/dist/chunks/{setup-wizard-GSDPDFC4.ts.map → setup-wizard-VW7F3JN4.ts.map} +0 -0
  102. /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-ORQRHEEO.ts";
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-V6VZ5TLG.ts";
25
+ } from "./chunk-A3TFXY4O.ts";
26
26
  import {
27
27
  createSyncBackend
28
- } from "./chunk-SDZCTCYQ.ts";
28
+ } from "./chunk-2FETCGGT.ts";
29
29
  import {
30
30
  applySnapshotTransaction,
31
31
  recoverPendingSnapshotTransactions,
32
32
  syncMutationParents
33
- } from "./chunk-ANWNQIR7.ts";
33
+ } from "./chunk-KGE47GCZ.ts";
34
34
  import {
35
35
  backupLocal,
36
36
  captureMutationOwner,
37
37
  protectedSessionPaths
38
- } from "./chunk-IJ6VV6FN.ts";
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-QE7NZZWT.ts";
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-4QVPTIZB.ts";
66
+ } from "./chunk-VCL7TZYW.ts";
63
67
  import {
64
68
  loadConfig,
65
69
  syncCheckConfigFingerprint,
66
70
  syncConfigReviewIdentity
67
- } from "./chunk-3B4Z2SKV.ts";
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("Merge review failed; no transfer was performed.");
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("Merge review requires observable TUI or RPC; review the plan before using --yes.");
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("Merge review failed; no transfer was performed.");
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 = changes(input.localBefore, input.localAfter);
322
- const remote = changes(input.remoteBefore, input.remoteAfter);
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 = [`local: ${local.files.length} files`, `remote: ${remote.id} (${remote.files.length} files)`, ""];
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 [`${title}: ${snapshot.id}`, ...snapshot.files.map((file) => `Add: ${snapshotPathLabel(file.path)}`)].join(
1373
- "\n"
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
- `Upload ${upload.files.length} files from ${safeTerminalText(agentDir())}.`,
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(local, remote, {
1391
- add: "Add locally",
1392
- update: "Update locally",
1393
- remove: "Remove locally"
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, protectedSessionCount) {
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: ${protectedSessionCount || "none"}`,
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, protectedSessionCount) {
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: ${protectedSessionCount || "none"}`,
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 beforeMap = fileHashMap(before);
1432
- const afterMap = fileHashMap(after);
1433
- const paths = [.../* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)])].sort();
1434
- const lines = [];
1435
- for (const filePath of paths) {
1436
- if (!Object.hasOwn(beforeMap, filePath)) lines.push(`${labels.add}: ${snapshotPathLabel(filePath)}`);
1437
- else if (!Object.hasOwn(afterMap, filePath)) lines.push(`${labels.remove}: ${snapshotPathLabel(filePath)}`);
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
- if (lines.length === 0) lines.push("No file changes.");
1441
- return lines.join("\n");
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 completeJournal(ctx, config, backend, pending, validate, options.signal, options.auto);
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 createSnapshot(config.snapshotIdentity, snapshotOptions);
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
- const rawRemote = head ? await readSnapshotForHead(backend, head, options.signal) : void 0;
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 = planFileMerge({
1594
- baseline: state.lastFileHashes,
1595
- local: local.files,
1596
- remote: rawRemote.files,
1597
- selectionCompatible: true
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 = planFileMerge({
1626
- baseline: state.lastFileHashes,
1627
- local: local.files,
1628
- remote: remote.files,
1629
- selectionCompatible,
1630
- protectedPaths: protectedSessionPaths(ctx, sessionRoot)
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 resolveSettingsConflicts(config, state, local, remote, plan);
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 resolveContentConflicts(
1642
- config,
1643
- state,
1644
- local,
1645
- remote,
1646
- resolved.plan,
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 createSnapshot(config.snapshotIdentity, snapshotOptions);
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 writeAcceptedState(config, head, accepted, validate, progress);
1773
- await pruneMergeBaselines(
1774
- config,
1775
- acceptedState(config, head, accepted, progress),
1776
- captureMutationOwner(ctx, options.signal)
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 pruneCompletedConflicts(
1780
- config,
1781
- backend.identity,
1782
- acceptedState(config, head, accepted, progress),
1783
- accepted,
1784
- remote,
1785
- captureMutationOwner(ctx, options.signal)
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 preflightMergedTargets(localRaw, after, snapshotOptions, ctx.sessionManager.getSessionFile?.());
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 createSnapshot(config.snapshotIdentity, snapshotOptions);
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 backupLocal(config.snapshotIdentity, snapshotOptions, options.signal);
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(await readStateForConfig(config));
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 createSnapshot(config.snapshotIdentity, snapshotOptions);
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 preflightMergedTargets(atCommit, after, snapshotOptions, ctx.sessionManager.getSessionFile?.());
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 backend.publishSnapshot(upload, expectedRemoteHead(head), {
1876
- signal: options.signal,
1877
- onCommit: options.onCommit
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 completeJournal(ctx, config, backend, journal, validate, options.signal, options.auto);
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 createSnapshot(config.snapshotIdentity, snapshotOptions);
1935
- if (!sameHashes(fileHashMap(local), fileHashMap(journal.before)) || syncStateFingerprint(await readStateForConfig(config)) !== journal.stateIdentity) {
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 createSnapshot(config.snapshotIdentity, snapshotOptions);
1957
- const state2 = await readStateForConfig(config);
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 readSnapshotForHead(backend, head, signal);
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 applyMergedSnapshot(
1998
- journal.before,
1999
- journal.after,
2000
- protectedSessionPaths(ctx, sessionRoot),
2001
- { ...snapshotOptions, validateMutation },
2002
- validate,
2003
- ctx.sessionManager.getSessionFile?.()
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 writeAcceptedState(config, head, journal.accepted ?? journal.after, validate, journal.progress);
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 pruneMergeBaselines(
2015
- config,
2016
- acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
2017
- captureMutationOwner(ctx, signal)
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 pruneCompletedConflicts(
2021
- config,
2022
- backend.identity,
2023
- acceptedState(config, head, journal.accepted ?? journal.after, journal.progress),
2024
- journal.accepted ?? journal.after,
2025
- journal.upload,
2026
- captureMutationOwner(ctx, signal)
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-HUTYAPFP.ts.map
2194
+ //# sourceMappingURL=chunk-35GM6XKM.ts.map