@indigoai-us/hq-cloud 6.11.12 → 6.11.13
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/dist/bin/sync-runner-company.d.ts +35 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -0
- package/dist/bin/sync-runner-company.js +290 -0
- package/dist/bin/sync-runner-company.js.map +1 -0
- package/dist/bin/sync-runner-events.d.ts +12 -0
- package/dist/bin/sync-runner-events.d.ts.map +1 -0
- package/dist/bin/sync-runner-events.js +12 -0
- package/dist/bin/sync-runner-events.js.map +1 -0
- package/dist/bin/sync-runner-planning.d.ts +53 -0
- package/dist/bin/sync-runner-planning.d.ts.map +1 -0
- package/dist/bin/sync-runner-planning.js +59 -0
- package/dist/bin/sync-runner-planning.js.map +1 -0
- package/dist/bin/sync-runner-rollup.d.ts +24 -0
- package/dist/bin/sync-runner-rollup.d.ts.map +1 -0
- package/dist/bin/sync-runner-rollup.js +46 -0
- package/dist/bin/sync-runner-rollup.js.map +1 -0
- package/dist/bin/sync-runner-telemetry.d.ts +5 -0
- package/dist/bin/sync-runner-telemetry.d.ts.map +1 -0
- package/dist/bin/sync-runner-telemetry.js +5 -0
- package/dist/bin/sync-runner-telemetry.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts +17 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -0
- package/dist/bin/sync-runner-watch-loop.js +372 -0
- package/dist/bin/sync-runner-watch-loop.js.map +1 -0
- package/dist/bin/sync-runner-watch-routes.d.ts +25 -0
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -0
- package/dist/bin/sync-runner-watch-routes.js +74 -0
- package/dist/bin/sync-runner-watch-routes.js.map +1 -0
- package/dist/bin/sync-runner.d.ts +3 -54
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +73 -1154
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +34 -17
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +39 -5
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/rescue-classify-ordering.test.js +17 -0
- package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +45 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +197 -170
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +224 -676
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +399 -726
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +20 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/daemon-worker.d.ts +2 -2
- package/dist/daemon-worker.js +3 -3
- package/dist/daemon-worker.js.map +1 -1
- package/dist/object-io.js +1 -1
- package/dist/object-io.js.map +1 -1
- package/dist/remote-pull.d.ts +2 -2
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +23 -3
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +24 -2
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/sync/push-receiver.d.ts +6 -0
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +32 -2
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +31 -0
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync-core.d.ts +27 -0
- package/dist/sync-core.d.ts.map +1 -0
- package/dist/sync-core.js +54 -0
- package/dist/sync-core.js.map +1 -0
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +284 -36
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +59 -0
- package/dist/vault-client.test.js.map +1 -1
- package/dist/watcher.d.ts +2 -20
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +3 -113
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +350 -0
- package/src/bin/sync-runner-events.ts +25 -0
- package/src/bin/sync-runner-planning.ts +121 -0
- package/src/bin/sync-runner-rollup.ts +72 -0
- package/src/bin/sync-runner-telemetry.ts +8 -0
- package/src/bin/sync-runner-watch-loop.ts +443 -0
- package/src/bin/sync-runner-watch-routes.ts +86 -0
- package/src/bin/sync-runner.ts +96 -1253
- package/src/cli/reindex.test.ts +41 -3
- package/src/cli/reindex.ts +35 -19
- package/src/cli/rescue-classify-ordering.test.ts +20 -0
- package/src/cli/rescue-core.ts +252 -176
- package/src/cli/share.ts +363 -705
- package/src/cli/sync.test.ts +25 -0
- package/src/cli/sync.ts +612 -802
- package/src/daemon-worker.ts +3 -3
- package/src/object-io.ts +1 -1
- package/src/remote-pull.test.ts +30 -1
- package/src/remote-pull.ts +29 -4
- package/src/sync/push-receiver.test.ts +35 -0
- package/src/sync/push-receiver.ts +41 -2
- package/src/sync-core.ts +58 -0
- package/src/vault-client.test.ts +74 -0
- package/src/vault-client.ts +395 -43
- package/src/watcher.ts +6 -141
package/src/cli/rescue-core.ts
CHANGED
|
@@ -694,6 +694,7 @@ function doRescue(
|
|
|
694
694
|
unchangedList,
|
|
695
695
|
appendLog,
|
|
696
696
|
out,
|
|
697
|
+
decisions: [],
|
|
697
698
|
actions: [],
|
|
698
699
|
};
|
|
699
700
|
|
|
@@ -728,16 +729,19 @@ function doRescue(
|
|
|
728
729
|
}
|
|
729
730
|
|
|
730
731
|
// --- Walk + classify (PHASE 1: read-only) ---
|
|
731
|
-
// Classification mutates nothing on disk: every
|
|
732
|
-
//
|
|
733
|
-
//
|
|
732
|
+
// Classification mutates nothing on disk: every outcome is recorded as a
|
|
733
|
+
// typed RescueDecision. Destructive ops (delete, rescue-move,
|
|
734
|
+
// conflict-quarantine, symlink-drop) are built from those decisions and
|
|
735
|
+
// executed later (PHASE 2). This is the
|
|
734
736
|
// ordering-safety invariant: if classification throws partway through the
|
|
735
737
|
// wipe set (e.g. an unreadable entry, a future unhandled file shape), it
|
|
736
|
-
// aborts HERE — before a single file has been touched
|
|
737
|
-
// half-applied wipe like the
|
|
738
|
+
// aborts HERE — before a single file has been touched or even queued as a
|
|
739
|
+
// filesystem closure — instead of leaving a half-applied wipe like the
|
|
740
|
+
// pre-port bash classifier did (DEV-1767:
|
|
738
741
|
// `Error: Path is a directory` on a nested dir-symlink, thrown after ~10 core
|
|
739
|
-
// files were already deleted). Dry-run
|
|
740
|
-
//
|
|
742
|
+
// files were already deleted). Dry-run renders each typed decision inline as
|
|
743
|
+
// the walk records it, preserving the legacy progress/fault timing while
|
|
744
|
+
// still sharing the same classify path as the live pass.
|
|
741
745
|
out("\n");
|
|
742
746
|
if (wipeToplevel.length === 0) {
|
|
743
747
|
out("==> Wipe set is empty; nothing to process or overlay.\n");
|
|
@@ -813,6 +817,7 @@ function doRescue(
|
|
|
813
817
|
// Actions run in classification (walk) order — the same order the old
|
|
814
818
|
// interleaved walk mutated in — so per-file output and on-disk results are
|
|
815
819
|
// unchanged versus before the two-phase split.
|
|
820
|
+
ctx.actions = buildRescueActions(ctx);
|
|
816
821
|
applyClassifiedActions(ctx, backupDir);
|
|
817
822
|
|
|
818
823
|
// --- Back up preserve-subpaths to a mktemp shuttle ---
|
|
@@ -1076,7 +1081,7 @@ interface WalkCtx {
|
|
|
1076
1081
|
historyFloor: string;
|
|
1077
1082
|
baselineMode: "history_floor" | "head_compare";
|
|
1078
1083
|
// Batched git results, precomputed once before the classify walk so
|
|
1079
|
-
//
|
|
1084
|
+
// classifyOne does map lookups instead of spawning git ~3x per file. On a
|
|
1080
1085
|
// large wipe set, process-startup overhead dominates and this is the
|
|
1081
1086
|
// difference between minutes and seconds. A rel absent from a map falls back
|
|
1082
1087
|
// to a per-file spawn (safety for paths that can't be batched line-delimited,
|
|
@@ -1089,12 +1094,17 @@ interface WalkCtx {
|
|
|
1089
1094
|
floorShas?: Map<string, string | null>;
|
|
1090
1095
|
runTs: string;
|
|
1091
1096
|
srcSha: string;
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
|
|
1097
|
-
//
|
|
1097
|
+
// Read-only classifier output, collected in walk order. Dry-run rendering is
|
|
1098
|
+
// emitted inline as each decision is recorded; live filesystem action
|
|
1099
|
+
// construction is derived from this same list only after the full classify
|
|
1100
|
+
// walk completes.
|
|
1101
|
+
decisions: RescueDecision[];
|
|
1102
|
+
// Deferred destructive ops, built from RescueDecision only AFTER the entire
|
|
1103
|
+
// wipe set has been classified without error (PHASE 2 in doRescue). This is
|
|
1104
|
+
// what makes a classifier crash fail-safe — a throw mid-classify aborts before
|
|
1105
|
+
// a single file is mutated, so the wipe is never left half-applied
|
|
1106
|
+
// (DEV-1767). Dry-run never fills this (it returns after classification), so
|
|
1107
|
+
// live + dry-run share one classify path.
|
|
1098
1108
|
actions: RescueAction[];
|
|
1099
1109
|
counts: {
|
|
1100
1110
|
userOnly: number;
|
|
@@ -1118,13 +1128,45 @@ interface RescueAction {
|
|
|
1118
1128
|
run: () => void;
|
|
1119
1129
|
}
|
|
1120
1130
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1131
|
+
export type RescueDecision =
|
|
1132
|
+
| { kind: "skip"; rel: string }
|
|
1133
|
+
| { kind: "drop-conflict-artifact"; rel: string }
|
|
1134
|
+
| { kind: "drop-script-managed"; rel: string }
|
|
1135
|
+
| { kind: "drop-reindex-symlink"; rel: string; target: string }
|
|
1136
|
+
| { kind: "user-only"; rel: string }
|
|
1137
|
+
| { kind: "cloud-symlink-reconciled"; rel: string }
|
|
1138
|
+
| { kind: "drift-reconciled"; rel: string }
|
|
1139
|
+
| { kind: "user-edit-diff-append"; rel: string }
|
|
1140
|
+
| { kind: "user-edit-overwrite-safe"; rel: string }
|
|
1141
|
+
| { kind: "user-edit-conflict"; rel: string }
|
|
1142
|
+
| { kind: "user-edit-rescue"; rel: string; target: string }
|
|
1143
|
+
| { kind: "unchanged-preserve"; rel: string }
|
|
1144
|
+
| { kind: "unchanged-delete"; rel: string }
|
|
1145
|
+
| { kind: "wholesale-replace-template"; rel: "companies/_template" };
|
|
1146
|
+
|
|
1147
|
+
function recordDecision(ctx: WalkCtx, decision: RescueDecision): void {
|
|
1148
|
+
ctx.decisions.push(decision);
|
|
1149
|
+
if (ctx.cfg.dryRun) renderDryRunDecision(ctx, decision);
|
|
1150
|
+
switch (decision.kind) {
|
|
1151
|
+
case "drop-reindex-symlink":
|
|
1152
|
+
ctx.counts.symlinkDropped += 1;
|
|
1153
|
+
return;
|
|
1154
|
+
case "user-only":
|
|
1155
|
+
ctx.counts.userOnly += 1;
|
|
1156
|
+
return;
|
|
1157
|
+
case "cloud-symlink-reconciled":
|
|
1158
|
+
ctx.counts.cloudSymlinkReconciled += 1;
|
|
1159
|
+
return;
|
|
1160
|
+
case "drift-reconciled":
|
|
1161
|
+
ctx.counts.driftReconciled += 1;
|
|
1162
|
+
return;
|
|
1163
|
+
case "unchanged-preserve":
|
|
1164
|
+
case "unchanged-delete":
|
|
1165
|
+
ctx.counts.unchanged += 1;
|
|
1166
|
+
return;
|
|
1167
|
+
default:
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1128
1170
|
}
|
|
1129
1171
|
|
|
1130
1172
|
function applyClassifiedActions(ctx: WalkCtx, backupDir: string): void {
|
|
@@ -1294,16 +1336,16 @@ function isOverwriteSafe(rel: string): boolean {
|
|
|
1294
1336
|
);
|
|
1295
1337
|
}
|
|
1296
1338
|
|
|
1297
|
-
function
|
|
1339
|
+
function masterSyncSymlinkTarget(localPath: string): string | null {
|
|
1298
1340
|
const st = lstatOrNull(localPath);
|
|
1299
|
-
if (!st || !st.isSymbolicLink()) return
|
|
1341
|
+
if (!st || !st.isSymbolicLink()) return null;
|
|
1300
1342
|
let tgt = "";
|
|
1301
1343
|
try {
|
|
1302
1344
|
tgt = fs.readlinkSync(localPath);
|
|
1303
1345
|
} catch {
|
|
1304
|
-
return
|
|
1346
|
+
return null;
|
|
1305
1347
|
}
|
|
1306
|
-
return tgt.includes("/personal/") || tgt.startsWith("personal/");
|
|
1348
|
+
return tgt.includes("/personal/") || tgt.startsWith("personal/") ? tgt : null;
|
|
1307
1349
|
}
|
|
1308
1350
|
|
|
1309
1351
|
function isUnderPreserve(cfg: Config, rel: string): boolean {
|
|
@@ -1556,14 +1598,13 @@ function conflictOne(ctx: WalkCtx, rel: string): void {
|
|
|
1556
1598
|
ctx.appendLog(`conflicted\t${rel}\t-> ${destRel}\n`);
|
|
1557
1599
|
}
|
|
1558
1600
|
|
|
1559
|
-
// --- classify
|
|
1601
|
+
// --- classify one file (the per-file workhorse) ---
|
|
1560
1602
|
//
|
|
1561
|
-
// Read-only by contract: this
|
|
1562
|
-
//
|
|
1563
|
-
//
|
|
1564
|
-
// set has classified without throwing. Keep it that way
|
|
1565
|
-
|
|
1566
|
-
function processOne(ctx: WalkCtx, rel: string): void {
|
|
1603
|
+
// Read-only by contract: this returns a RescueDecision only. It does not render
|
|
1604
|
+
// dry-run text, update action queues, or mutate the filesystem. Dry-run text is
|
|
1605
|
+
// rendered by recordDecision, and PHASE 2 action building happens only once the
|
|
1606
|
+
// whole wipe set has classified without throwing. Keep it that way.
|
|
1607
|
+
function classifyOne(ctx: WalkCtx, rel: string): RescueDecision {
|
|
1567
1608
|
const { cfg } = ctx;
|
|
1568
1609
|
const localPath = path.join(ctx.hqRoot, rel);
|
|
1569
1610
|
const srcPath = path.join(ctx.srcDir, rel);
|
|
@@ -1574,54 +1615,27 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1574
1615
|
throw new Error(`rescue classifier fault injected at ${rel} (HQ_RESCUE_FAULT_AT_REL)`);
|
|
1575
1616
|
}
|
|
1576
1617
|
|
|
1577
|
-
if (isUnderPreserve(cfg, rel)) return;
|
|
1618
|
+
if (isUnderPreserve(cfg, rel)) return { kind: "skip", rel };
|
|
1578
1619
|
|
|
1579
1620
|
// Conflict-resolution artifacts (`<name>.conflict-<ts>-<peer>.<ext>`).
|
|
1580
1621
|
const base = rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
1581
1622
|
if (/\.conflict-/.test(base)) {
|
|
1582
|
-
|
|
1583
|
-
ctx.out(` drop conflict artifact: ${rel}\n`);
|
|
1584
|
-
} else {
|
|
1585
|
-
queueAction(ctx, `drop conflict artifact: ${rel}`, [rel], () =>
|
|
1586
|
-
fs.rmSync(localPath, { force: true }),
|
|
1587
|
-
);
|
|
1588
|
-
}
|
|
1589
|
-
return;
|
|
1623
|
+
return { kind: "drop-conflict-artifact", rel };
|
|
1590
1624
|
}
|
|
1591
1625
|
|
|
1592
1626
|
// Script-managed files: core/core.yaml + legacy core.yaml.
|
|
1593
1627
|
if (rel === "core/core.yaml" || rel === "core.yaml") {
|
|
1594
|
-
|
|
1595
|
-
ctx.out(` skip script-managed (rewrites at stamp step): ${rel}\n`);
|
|
1596
|
-
} else {
|
|
1597
|
-
queueAction(ctx, `drop script-managed: ${rel}`, [rel], () =>
|
|
1598
|
-
fs.rmSync(localPath, { force: true }),
|
|
1599
|
-
);
|
|
1600
|
-
}
|
|
1601
|
-
return;
|
|
1628
|
+
return { kind: "drop-script-managed", rel };
|
|
1602
1629
|
}
|
|
1603
1630
|
|
|
1604
1631
|
// Symlinks (mid-tree).
|
|
1605
1632
|
const lst = lstatOrNull(localPath);
|
|
1606
1633
|
if (lst && lst.isSymbolicLink()) {
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
let tgt = "";
|
|
1611
|
-
try {
|
|
1612
|
-
tgt = fs.readlinkSync(localPath);
|
|
1613
|
-
} catch {
|
|
1614
|
-
/* ignore */
|
|
1615
|
-
}
|
|
1616
|
-
ctx.out(` drop reindex symlink: ${rel} -> ${tgt}\n`);
|
|
1617
|
-
} else {
|
|
1618
|
-
queueAction(ctx, `drop reindex symlink: ${rel}`, [rel], () => {
|
|
1619
|
-
fs.rmSync(localPath, { force: true });
|
|
1620
|
-
ctx.appendLog(`symlink-dropped\t${rel}\t(reindex regenerable)\n`);
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1634
|
+
const target = masterSyncSymlinkTarget(localPath);
|
|
1635
|
+
if (target !== null) {
|
|
1636
|
+
return { kind: "drop-reindex-symlink", rel, target };
|
|
1623
1637
|
}
|
|
1624
|
-
return;
|
|
1638
|
+
return { kind: "skip", rel };
|
|
1625
1639
|
}
|
|
1626
1640
|
|
|
1627
1641
|
// Is path in upstream HEAD?
|
|
@@ -1652,9 +1666,7 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1652
1666
|
|
|
1653
1667
|
// USER-ONLY: unknown to upstream (HEAD AND floor both lack it).
|
|
1654
1668
|
if (inHead === 0 && inFloor === 0) {
|
|
1655
|
-
|
|
1656
|
-
if (cfg.dryRun) ctx.out(` user-only (leave in place): ${rel}\n`);
|
|
1657
|
-
return;
|
|
1669
|
+
return { kind: "user-only", rel };
|
|
1658
1670
|
}
|
|
1659
1671
|
|
|
1660
1672
|
// Path is/was in upstream. Determine if user edited it.
|
|
@@ -1679,99 +1691,186 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1679
1691
|
|
|
1680
1692
|
// Cloud-update reclassification.
|
|
1681
1693
|
if (userEdited === 1 && isCloudFlattenedSymlinkEquiv(ctx, rel, localPath)) {
|
|
1682
|
-
|
|
1683
|
-
if (cfg.dryRun) {
|
|
1684
|
-
ctx.out(
|
|
1685
|
-
` cloud-symlink reconciled (unchanged): ${rel} (hq-symlink: marker matches upstream target)\n`,
|
|
1686
|
-
);
|
|
1687
|
-
} else {
|
|
1688
|
-
queueAction(ctx, `cloud-symlink reconciled: ${rel}`, [rel], () => {
|
|
1689
|
-
fs.rmSync(localPath, { force: true });
|
|
1690
|
-
ctx.appendLog(
|
|
1691
|
-
`cloud-symlink-reconciled\t${rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`,
|
|
1692
|
-
);
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
return;
|
|
1694
|
+
return { kind: "cloud-symlink-reconciled", rel };
|
|
1696
1695
|
}
|
|
1697
1696
|
|
|
1698
1697
|
// Convergence guard: drifted from floor but identical to upstream HEAD.
|
|
1699
1698
|
if (userEdited === 1 && inHead === 1 && bytesEqual(localPath, srcPath)) {
|
|
1700
|
-
|
|
1701
|
-
if (cfg.dryRun) {
|
|
1702
|
-
ctx.out(` drift reconciled (identical to upstream HEAD; no rescue): ${rel}\n`);
|
|
1703
|
-
} else {
|
|
1704
|
-
queueAction(ctx, `drift reconciled: ${rel}`, [rel], () => {
|
|
1705
|
-
fs.rmSync(localPath, { force: true });
|
|
1706
|
-
ctx.appendLog(
|
|
1707
|
-
`drift-reconciled\t${rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`,
|
|
1708
|
-
);
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
return;
|
|
1699
|
+
return { kind: "drift-reconciled", rel };
|
|
1712
1700
|
}
|
|
1713
1701
|
|
|
1714
1702
|
if (userEdited === 1) {
|
|
1715
|
-
if (
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1703
|
+
if (rel === ".claude/CLAUDE.md") {
|
|
1704
|
+
return { kind: "user-edit-diff-append", rel };
|
|
1705
|
+
}
|
|
1706
|
+
if (isOverwriteSafe(rel)) {
|
|
1707
|
+
return { kind: "user-edit-overwrite-safe", rel };
|
|
1708
|
+
}
|
|
1709
|
+
if (isConflictClass(rel)) {
|
|
1710
|
+
return { kind: "user-edit-conflict", rel };
|
|
1711
|
+
}
|
|
1712
|
+
return { kind: "user-edit-rescue", rel, target: mapRescueTarget(rel) };
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// user_edited=0: matches floor baseline. Split on byte-equality to HEAD.
|
|
1716
|
+
if (bytesEqual(localPath, path.join(ctx.srcDir, rel))) {
|
|
1717
|
+
return { kind: "unchanged-preserve", rel };
|
|
1718
|
+
}
|
|
1719
|
+
return { kind: "unchanged-delete", rel };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function renderDryRunDecision(ctx: WalkCtx, decision: RescueDecision): void {
|
|
1723
|
+
switch (decision.kind) {
|
|
1724
|
+
case "skip":
|
|
1725
|
+
break;
|
|
1726
|
+
case "drop-conflict-artifact":
|
|
1727
|
+
ctx.out(` drop conflict artifact: ${decision.rel}\n`);
|
|
1728
|
+
break;
|
|
1729
|
+
case "drop-script-managed":
|
|
1730
|
+
ctx.out(` skip script-managed (rewrites at stamp step): ${decision.rel}\n`);
|
|
1731
|
+
break;
|
|
1732
|
+
case "drop-reindex-symlink":
|
|
1733
|
+
ctx.out(` drop reindex symlink: ${decision.rel} -> ${decision.target}\n`);
|
|
1734
|
+
break;
|
|
1735
|
+
case "user-only":
|
|
1736
|
+
ctx.out(` user-only (leave in place): ${decision.rel}\n`);
|
|
1737
|
+
break;
|
|
1738
|
+
case "cloud-symlink-reconciled":
|
|
1739
|
+
ctx.out(
|
|
1740
|
+
` cloud-symlink reconciled (unchanged): ${decision.rel} (hq-symlink: marker matches upstream target)\n`,
|
|
1741
|
+
);
|
|
1742
|
+
break;
|
|
1743
|
+
case "drift-reconciled":
|
|
1744
|
+
ctx.out(` drift reconciled (identical to upstream HEAD; no rescue): ${decision.rel}\n`);
|
|
1745
|
+
break;
|
|
1746
|
+
case "user-edit-diff-append":
|
|
1747
|
+
ctx.out(` user-edit (diff-append): ${decision.rel} -> personal/CLAUDE.md\n`);
|
|
1748
|
+
ctx.counts.userEdit += 1;
|
|
1749
|
+
break;
|
|
1750
|
+
case "user-edit-overwrite-safe":
|
|
1751
|
+
ctx.out(` user-edit (overwrite-safe): ${decision.rel} -> upstream wins (no copy preserved)\n`);
|
|
1752
|
+
ctx.counts.userOverwrite += 1;
|
|
1753
|
+
break;
|
|
1754
|
+
case "user-edit-conflict":
|
|
1755
|
+
ctx.out(
|
|
1756
|
+
` user-edit (conflict): ${decision.rel} -> .hq-conflicts/rescue-${ctx.runTs}/${decision.rel}\n`,
|
|
1757
|
+
);
|
|
1758
|
+
ctx.counts.userConflict += 1;
|
|
1759
|
+
break;
|
|
1760
|
+
case "user-edit-rescue":
|
|
1761
|
+
ctx.out(` user-edit (rescue): ${decision.rel} -> ${decision.target}\n`);
|
|
1762
|
+
ctx.counts.userEdit += 1;
|
|
1763
|
+
break;
|
|
1764
|
+
case "unchanged-preserve":
|
|
1765
|
+
ctx.out(` unchanged (preserved in place): ${decision.rel}\n`);
|
|
1766
|
+
break;
|
|
1767
|
+
case "unchanged-delete":
|
|
1768
|
+
ctx.out(` unchanged (delete + replace): ${decision.rel}\n`);
|
|
1769
|
+
break;
|
|
1770
|
+
case "wholesale-replace-template":
|
|
1771
|
+
ctx.out(" wholesale-replace: companies/_template (template carve-out)\n");
|
|
1772
|
+
break;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function buildRescueActions(ctx: WalkCtx): RescueAction[] {
|
|
1777
|
+
const actions: RescueAction[] = [];
|
|
1778
|
+
const add = (label: string, affectedRels: string[], runAction: () => void) => {
|
|
1779
|
+
actions.push({ label, affectedRels, run: runAction });
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
for (const decision of ctx.decisions) {
|
|
1783
|
+
const localPath = path.join(ctx.hqRoot, decision.rel);
|
|
1784
|
+
switch (decision.kind) {
|
|
1785
|
+
case "skip":
|
|
1786
|
+
case "user-only":
|
|
1787
|
+
break;
|
|
1788
|
+
case "drop-conflict-artifact":
|
|
1789
|
+
add(`drop conflict artifact: ${decision.rel}`, [decision.rel], () =>
|
|
1790
|
+
fs.rmSync(localPath, { force: true }),
|
|
1733
1791
|
);
|
|
1734
|
-
|
|
1735
|
-
|
|
1792
|
+
break;
|
|
1793
|
+
case "drop-script-managed":
|
|
1794
|
+
add(`drop script-managed: ${decision.rel}`, [decision.rel], () =>
|
|
1795
|
+
fs.rmSync(localPath, { force: true }),
|
|
1796
|
+
);
|
|
1797
|
+
break;
|
|
1798
|
+
case "drop-reindex-symlink":
|
|
1799
|
+
add(`drop reindex symlink: ${decision.rel}`, [decision.rel], () => {
|
|
1800
|
+
fs.rmSync(localPath, { force: true });
|
|
1801
|
+
ctx.appendLog(`symlink-dropped\t${decision.rel}\t(reindex regenerable)\n`);
|
|
1802
|
+
});
|
|
1803
|
+
break;
|
|
1804
|
+
case "cloud-symlink-reconciled":
|
|
1805
|
+
add(`cloud-symlink reconciled: ${decision.rel}`, [decision.rel], () => {
|
|
1806
|
+
fs.rmSync(localPath, { force: true });
|
|
1807
|
+
ctx.appendLog(
|
|
1808
|
+
`cloud-symlink-reconciled\t${decision.rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`,
|
|
1809
|
+
);
|
|
1810
|
+
});
|
|
1811
|
+
break;
|
|
1812
|
+
case "drift-reconciled":
|
|
1813
|
+
add(`drift reconciled: ${decision.rel}`, [decision.rel], () => {
|
|
1814
|
+
fs.rmSync(localPath, { force: true });
|
|
1815
|
+
ctx.appendLog(
|
|
1816
|
+
`drift-reconciled\t${decision.rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`,
|
|
1817
|
+
);
|
|
1818
|
+
});
|
|
1819
|
+
break;
|
|
1820
|
+
case "user-edit-diff-append":
|
|
1821
|
+
add(`rescue diff-append: ${decision.rel}`, [decision.rel], () =>
|
|
1822
|
+
rescueOne(ctx, decision.rel),
|
|
1823
|
+
);
|
|
1824
|
+
break;
|
|
1825
|
+
case "user-edit-overwrite-safe":
|
|
1826
|
+
add(`overwrite-safe: ${decision.rel}`, [decision.rel], () => {
|
|
1736
1827
|
fs.rmSync(localPath, { force: true });
|
|
1737
1828
|
ctx.counts.userOverwrite += 1;
|
|
1738
|
-
ctx.appendLog(
|
|
1829
|
+
ctx.appendLog(
|
|
1830
|
+
`overwritten\t${decision.rel}\t(overwrite-safe; upstream wins, no copy preserved)\n`,
|
|
1831
|
+
);
|
|
1739
1832
|
});
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1833
|
+
break;
|
|
1834
|
+
case "user-edit-conflict":
|
|
1835
|
+
add(`conflict quarantine: ${decision.rel}`, [decision.rel], () =>
|
|
1836
|
+
conflictOne(ctx, decision.rel),
|
|
1743
1837
|
);
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
} else {
|
|
1755
|
-
queueAction(ctx, `preserve unchanged: ${rel}`, [], () => {
|
|
1756
|
-
fs.appendFileSync(ctx.unchangedList, `/${rel}\n`);
|
|
1757
|
-
ctx.appendLog(`unchanged\t${rel}\t(identical to upstream; left in place, mtime preserved)\n`);
|
|
1838
|
+
break;
|
|
1839
|
+
case "user-edit-rescue":
|
|
1840
|
+
add(`rescue: ${decision.rel}`, [decision.rel], () => rescueOne(ctx, decision.rel));
|
|
1841
|
+
break;
|
|
1842
|
+
case "unchanged-preserve":
|
|
1843
|
+
add(`preserve unchanged: ${decision.rel}`, [], () => {
|
|
1844
|
+
fs.appendFileSync(ctx.unchangedList, `/${decision.rel}\n`);
|
|
1845
|
+
ctx.appendLog(
|
|
1846
|
+
`unchanged\t${decision.rel}\t(identical to upstream; left in place, mtime preserved)\n`,
|
|
1847
|
+
);
|
|
1758
1848
|
});
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
ctx.out(` unchanged (delete + replace): ${rel}\n`);
|
|
1763
|
-
} else {
|
|
1764
|
-
queueAction(ctx, `delete unchanged: ${rel}`, [rel], () => {
|
|
1849
|
+
break;
|
|
1850
|
+
case "unchanged-delete":
|
|
1851
|
+
add(`delete unchanged: ${decision.rel}`, [decision.rel], () => {
|
|
1765
1852
|
fs.rmSync(localPath, { force: true });
|
|
1766
|
-
ctx.appendLog(
|
|
1853
|
+
ctx.appendLog(
|
|
1854
|
+
`deleted\t${decision.rel}\t(unchanged vs baseline; re-laid by overlay if still upstream)\n`,
|
|
1855
|
+
);
|
|
1767
1856
|
});
|
|
1768
|
-
|
|
1857
|
+
break;
|
|
1858
|
+
case "wholesale-replace-template":
|
|
1859
|
+
add("wholesale-replace: companies/_template", [decision.rel], () =>
|
|
1860
|
+
fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
|
|
1861
|
+
recursive: true,
|
|
1862
|
+
force: true,
|
|
1863
|
+
}),
|
|
1864
|
+
);
|
|
1865
|
+
break;
|
|
1769
1866
|
}
|
|
1770
1867
|
}
|
|
1868
|
+
|
|
1869
|
+
return actions;
|
|
1771
1870
|
}
|
|
1772
1871
|
|
|
1773
1872
|
// --- walk a wipe-set root ---
|
|
1774
|
-
// Precompute the per-file git facts
|
|
1873
|
+
// Precompute the per-file git facts classifyOne needs, in two batched passes
|
|
1775
1874
|
// instead of ~3 `git` spawns per file. Process-startup overhead (~15-20ms per
|
|
1776
1875
|
// spawn) dominates the per-file path, so on a large wipe set this turns a
|
|
1777
1876
|
// minutes-long classify into a seconds-long one. Behaviour is identical: the
|
|
@@ -1779,10 +1878,10 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1779
1878
|
// rel not represented in a map falls back to its original per-file spawn.
|
|
1780
1879
|
//
|
|
1781
1880
|
// Mirrors walkAndProcess's enumeration so the batched set matches what actually
|
|
1782
|
-
// reaches
|
|
1881
|
+
// reaches classifyOne: it skips `companies/_template` (wholesale-replaced, never
|
|
1783
1882
|
// classified per-file) and top-level symlinks (dropped, never classified).
|
|
1784
1883
|
function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
1785
|
-
const allRels: string[] = []; // every rel that reaches
|
|
1884
|
+
const allRels: string[] = []; // every rel that reaches classifyOne (files + symlinks)
|
|
1786
1885
|
const fileRels: string[] = []; // subset that are regular files (hashable)
|
|
1787
1886
|
const fileAbs: string[] = []; // parallel to fileRels
|
|
1788
1887
|
|
|
@@ -1798,7 +1897,7 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1798
1897
|
: [];
|
|
1799
1898
|
for (const rel of rels) {
|
|
1800
1899
|
// A path containing a newline can't survive line-delimited batch stdin;
|
|
1801
|
-
// leave it out of the maps and let
|
|
1900
|
+
// leave it out of the maps and let classifyOne spawn per-file for it.
|
|
1802
1901
|
if (rel.includes("\n")) continue;
|
|
1803
1902
|
allRels.push(rel);
|
|
1804
1903
|
const st = lstatOrNull(path.join(ctx.hqRoot, rel));
|
|
@@ -1809,7 +1908,7 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1809
1908
|
}
|
|
1810
1909
|
}
|
|
1811
1910
|
|
|
1812
|
-
// In head_compare mode
|
|
1911
|
+
// In head_compare mode classifyOne never consults these maps (it compares
|
|
1813
1912
|
// bytes against the upstream working tree directly), so skip both git passes.
|
|
1814
1913
|
if (ctx.baselineMode !== "history_floor") {
|
|
1815
1914
|
ctx.floorShas = new Map();
|
|
@@ -1846,7 +1945,7 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1846
1945
|
ctx.floorShas = floorShas;
|
|
1847
1946
|
|
|
1848
1947
|
// Pass 2: local blob SHA, but ONLY for regular files that are in the floor.
|
|
1849
|
-
//
|
|
1948
|
+
// classifyOne reads localShas exclusively in the `inFloor === 1` branch, so a
|
|
1850
1949
|
// file absent from the floor (e.g. the transient `.claude/worktrees/` trees,
|
|
1851
1950
|
// which classify as user-only) never needs a hash — and hashing it would mean
|
|
1852
1951
|
// reading its bytes for nothing. Restricting the hash set to floor members is
|
|
@@ -1877,22 +1976,12 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1877
1976
|
}
|
|
1878
1977
|
|
|
1879
1978
|
function walkAndProcess(ctx: WalkCtx, rootRel: string): void {
|
|
1880
|
-
const { cfg } = ctx;
|
|
1881
1979
|
const rootAbs = path.join(ctx.hqRoot, rootRel);
|
|
1882
1980
|
if (!lexists(rootAbs)) return;
|
|
1883
1981
|
|
|
1884
1982
|
// companies/_template — wholesale-replace.
|
|
1885
1983
|
if (rootRel === "companies/_template") {
|
|
1886
|
-
|
|
1887
|
-
ctx.out(" wholesale-replace: companies/_template (template carve-out)\n");
|
|
1888
|
-
} else {
|
|
1889
|
-
queueAction(ctx, "wholesale-replace: companies/_template", [rootRel], () =>
|
|
1890
|
-
fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
|
|
1891
|
-
recursive: true,
|
|
1892
|
-
force: true,
|
|
1893
|
-
}),
|
|
1894
|
-
);
|
|
1895
|
-
}
|
|
1984
|
+
recordDecision(ctx, { kind: "wholesale-replace-template", rel: "companies/_template" });
|
|
1896
1985
|
return;
|
|
1897
1986
|
}
|
|
1898
1987
|
|
|
@@ -1900,35 +1989,22 @@ function walkAndProcess(ctx: WalkCtx, rootRel: string): void {
|
|
|
1900
1989
|
|
|
1901
1990
|
// Top-level symlink.
|
|
1902
1991
|
if (lst && lst.isSymbolicLink()) {
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
let tgt = "";
|
|
1907
|
-
try {
|
|
1908
|
-
tgt = fs.readlinkSync(rootAbs);
|
|
1909
|
-
} catch {
|
|
1910
|
-
/* ignore */
|
|
1911
|
-
}
|
|
1912
|
-
ctx.out(` drop reindex symlink: ${rootRel} -> ${tgt}\n`);
|
|
1913
|
-
} else {
|
|
1914
|
-
queueAction(ctx, `drop reindex symlink: ${rootRel}`, [rootRel], () => {
|
|
1915
|
-
fs.rmSync(rootAbs, { force: true });
|
|
1916
|
-
ctx.appendLog(`symlink-dropped\t${rootRel}\t(reindex regenerable)\n`);
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1992
|
+
const target = masterSyncSymlinkTarget(rootAbs);
|
|
1993
|
+
if (target !== null) {
|
|
1994
|
+
recordDecision(ctx, { kind: "drop-reindex-symlink", rel: rootRel, target });
|
|
1919
1995
|
}
|
|
1920
1996
|
return;
|
|
1921
1997
|
}
|
|
1922
1998
|
|
|
1923
1999
|
// Top-level regular file.
|
|
1924
2000
|
if (lst && lst.isFile()) {
|
|
1925
|
-
|
|
2001
|
+
recordDecision(ctx, classifyOne(ctx, rootRel));
|
|
1926
2002
|
return;
|
|
1927
2003
|
}
|
|
1928
2004
|
|
|
1929
2005
|
// Top-level directory: recursive walk, pruning node_modules + nested .git.
|
|
1930
2006
|
for (const rel of findFilesAndSymlinks(rootAbs, ctx.hqRoot)) {
|
|
1931
|
-
|
|
2007
|
+
recordDecision(ctx, classifyOne(ctx, rel));
|
|
1932
2008
|
}
|
|
1933
2009
|
}
|
|
1934
2010
|
|