@indigoai-us/hq-cloud 6.11.11 → 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 +5 -54
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +76 -978
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +265 -11
- package/dist/bin/sync-runner.test.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 +75 -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 +320 -170
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +2 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +276 -660
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +30 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +28 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +541 -748
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +382 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +55 -10
- package/dist/cognito-auth.js.map +1 -1
- package/dist/cognito-auth.test.js +61 -0
- package/dist/cognito-auth.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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +93 -6
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +59 -0
- package/dist/journal.test.js.map +1 -1
- package/dist/machine-auth.test.js +60 -2
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/object-io.d.ts +37 -1
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +149 -30
- package/dist/object-io.js.map +1 -1
- package/dist/object-io.test.js +121 -0
- package/dist/object-io.test.js.map +1 -1
- package/dist/operation-lock.d.ts +8 -8
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +99 -32
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +51 -4
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +8 -2
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +34 -0
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/prefix-coalesce.d.ts +20 -9
- package/dist/prefix-coalesce.d.ts.map +1 -1
- package/dist/prefix-coalesce.js +124 -28
- package/dist/prefix-coalesce.js.map +1 -1
- package/dist/prefix-coalesce.test.js +57 -2
- package/dist/prefix-coalesce.test.js.map +1 -1
- package/dist/remote-pull.d.ts +8 -3
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +85 -16
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +213 -2
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +2 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +197 -116
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +109 -0
- package/dist/s3.test.js.map +1 -1
- package/dist/scope-shrink.d.ts +3 -2
- package/dist/scope-shrink.d.ts.map +1 -1
- package/dist/scope-shrink.js +1 -1
- package/dist/scope-shrink.js.map +1 -1
- package/dist/skill-telemetry.d.ts +1 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +69 -9
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +86 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/event-sync.d.ts +6 -0
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +34 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +73 -0
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/metrics.d.ts +17 -1
- package/dist/sync/metrics.d.ts.map +1 -1
- package/dist/sync/metrics.js +32 -1
- package/dist/sync/metrics.js.map +1 -1
- package/dist/sync/metrics.test.js +74 -1
- package/dist/sync/metrics.test.js.map +1 -1
- package/dist/sync/pull-scope.d.ts.map +1 -1
- package/dist/sync/pull-scope.js +15 -7
- package/dist/sync/pull-scope.js.map +1 -1
- package/dist/sync/push-receiver.d.ts +12 -5
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +45 -17
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +67 -1
- 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/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +59 -6
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +74 -0
- package/dist/telemetry.test.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- 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 +38 -20
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +155 -143
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +103 -0
- package/dist/watcher.test.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.test.ts +298 -11
- package/src/bin/sync-runner.ts +99 -1054
- package/src/cli/reindex.test.ts +41 -3
- package/src/cli/reindex.ts +35 -19
- package/src/cli/rescue-classify-ordering.test.ts +81 -0
- package/src/cli/rescue-core.ts +400 -165
- package/src/cli/share.test.ts +38 -0
- package/src/cli/share.ts +420 -693
- package/src/cli/sync.test.ts +460 -1
- package/src/cli/sync.ts +788 -825
- package/src/cognito-auth.test.ts +77 -0
- package/src/cognito-auth.ts +73 -11
- package/src/daemon-worker.ts +3 -3
- package/src/index.ts +8 -0
- package/src/journal.test.ts +72 -0
- package/src/journal.ts +95 -8
- package/src/machine-auth.test.ts +64 -2
- package/src/object-io.test.ts +142 -0
- package/src/object-io.ts +183 -31
- package/src/operation-lock.test.ts +63 -4
- package/src/operation-lock.ts +99 -31
- package/src/personal-vault.test.ts +42 -0
- package/src/personal-vault.ts +8 -2
- package/src/prefix-coalesce.test.ts +71 -1
- package/src/prefix-coalesce.ts +155 -30
- package/src/remote-pull.test.ts +235 -1
- package/src/remote-pull.ts +106 -18
- package/src/s3.test.ts +126 -0
- package/src/s3.ts +237 -122
- package/src/scope-shrink.ts +6 -3
- package/src/skill-telemetry.test.ts +109 -0
- package/src/skill-telemetry.ts +82 -14
- package/src/sync/event-sync.test.ts +75 -0
- package/src/sync/event-sync.ts +54 -1
- package/src/sync/metrics.test.ts +81 -0
- package/src/sync/metrics.ts +59 -4
- package/src/sync/pull-scope.ts +23 -7
- package/src/sync/push-receiver.test.ts +73 -1
- package/src/sync/push-receiver.ts +56 -20
- package/src/sync-core.ts +58 -0
- package/src/telemetry.test.ts +85 -0
- package/src/telemetry.ts +69 -6
- package/src/types.ts +8 -0
- package/src/vault-client.test.ts +74 -0
- package/src/vault-client.ts +395 -43
- package/src/watcher.test.ts +117 -0
- package/src/watcher.ts +215 -174
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,7 +817,8 @@ 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.
|
|
816
|
-
|
|
820
|
+
ctx.actions = buildRescueActions(ctx);
|
|
821
|
+
applyClassifiedActions(ctx, backupDir);
|
|
817
822
|
|
|
818
823
|
// --- Back up preserve-subpaths to a mktemp shuttle ---
|
|
819
824
|
const shuttle = path.join(tmpdir, "preserve");
|
|
@@ -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,13 +1094,18 @@ interface WalkCtx {
|
|
|
1089
1094
|
floorShas?: Map<string, string | null>;
|
|
1090
1095
|
runTs: string;
|
|
1091
1096
|
srcSha: string;
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
|
|
1097
|
-
//
|
|
1098
|
-
|
|
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.
|
|
1108
|
+
actions: RescueAction[];
|
|
1099
1109
|
counts: {
|
|
1100
1110
|
userOnly: number;
|
|
1101
1111
|
unchanged: number;
|
|
@@ -1112,6 +1122,189 @@ interface WalkCtx {
|
|
|
1112
1122
|
out: (s: string) => void;
|
|
1113
1123
|
}
|
|
1114
1124
|
|
|
1125
|
+
interface RescueAction {
|
|
1126
|
+
label: string;
|
|
1127
|
+
affectedRels: string[];
|
|
1128
|
+
run: () => void;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
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
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function applyClassifiedActions(ctx: WalkCtx, backupDir: string): void {
|
|
1173
|
+
const completed: RescueAction[] = [];
|
|
1174
|
+
for (const action of ctx.actions) {
|
|
1175
|
+
try {
|
|
1176
|
+
action.run();
|
|
1177
|
+
completed.push(action);
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
const restored = rollbackCompletedActions(ctx, completed, backupDir);
|
|
1180
|
+
const manifest = writeApplyFailureManifest(
|
|
1181
|
+
ctx,
|
|
1182
|
+
backupDir,
|
|
1183
|
+
action,
|
|
1184
|
+
completed,
|
|
1185
|
+
restored,
|
|
1186
|
+
err,
|
|
1187
|
+
);
|
|
1188
|
+
ctx.out("\n");
|
|
1189
|
+
ctx.out(`error: rescue apply failed during '${action.label}'.\n`);
|
|
1190
|
+
if (restored.length > 0) {
|
|
1191
|
+
ctx.out(`==> Rolled back ${restored.length} prior action path(s) from the safety snapshot.\n`);
|
|
1192
|
+
}
|
|
1193
|
+
if (manifest) {
|
|
1194
|
+
ctx.out(`==> Recovery manifest: ${manifest}\n`);
|
|
1195
|
+
}
|
|
1196
|
+
throw err;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function rollbackCompletedActions(
|
|
1202
|
+
ctx: WalkCtx,
|
|
1203
|
+
completed: RescueAction[],
|
|
1204
|
+
backupDir: string,
|
|
1205
|
+
): string[] {
|
|
1206
|
+
if (!backupDir || !isDir(backupDir)) return [];
|
|
1207
|
+
|
|
1208
|
+
const rels: string[] = [];
|
|
1209
|
+
const seen = new Set<string>();
|
|
1210
|
+
for (let i = completed.length - 1; i >= 0; i--) {
|
|
1211
|
+
const action = completed[i];
|
|
1212
|
+
for (let j = action.affectedRels.length - 1; j >= 0; j--) {
|
|
1213
|
+
const rel = action.affectedRels[j];
|
|
1214
|
+
if (rel && !seen.has(rel)) {
|
|
1215
|
+
seen.add(rel);
|
|
1216
|
+
rels.push(rel);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const restored: string[] = [];
|
|
1222
|
+
for (const rel of rels) {
|
|
1223
|
+
const snapshotPath = path.join(backupDir, rel);
|
|
1224
|
+
if (!lexists(snapshotPath)) continue;
|
|
1225
|
+
const dest = path.join(ctx.hqRoot, rel);
|
|
1226
|
+
try {
|
|
1227
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
1228
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
1229
|
+
cpATo(snapshotPath, dest);
|
|
1230
|
+
restored.push(rel);
|
|
1231
|
+
} catch {
|
|
1232
|
+
// Recovery manifest below records any path that could not be restored.
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return restored;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function writeApplyFailureManifest(
|
|
1239
|
+
ctx: WalkCtx,
|
|
1240
|
+
backupDir: string,
|
|
1241
|
+
failedAction: RescueAction,
|
|
1242
|
+
completed: RescueAction[],
|
|
1243
|
+
restored: string[],
|
|
1244
|
+
err: unknown,
|
|
1245
|
+
): string | null {
|
|
1246
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1247
|
+
const completedLines =
|
|
1248
|
+
completed.length === 0
|
|
1249
|
+
? [" (none)"]
|
|
1250
|
+
: completed.map((action) => {
|
|
1251
|
+
const paths = action.affectedRels.length > 0 ? action.affectedRels.join(", ") : "(no filesystem path)";
|
|
1252
|
+
return ` - ${action.label}: ${paths}`;
|
|
1253
|
+
});
|
|
1254
|
+
const restoredSet = new Set(restored);
|
|
1255
|
+
const unrestored = completed
|
|
1256
|
+
.flatMap((action) => action.affectedRels)
|
|
1257
|
+
.filter((rel, idx, arr) => rel && arr.indexOf(rel) === idx && !restoredSet.has(rel));
|
|
1258
|
+
|
|
1259
|
+
const lines: string[] = [];
|
|
1260
|
+
lines.push("# HQ rescue apply recovery", "");
|
|
1261
|
+
lines.push(`Created: ${ctx.runTs}`);
|
|
1262
|
+
lines.push(`HQ root: ${ctx.hqRoot}`);
|
|
1263
|
+
lines.push(`Source: ${ctx.cfg.sourceRepo}@${ctx.cfg.ref} (${ctx.srcSha})`);
|
|
1264
|
+
lines.push(`Failed action: ${failedAction.label}`);
|
|
1265
|
+
lines.push(`Error: ${message}`);
|
|
1266
|
+
lines.push("");
|
|
1267
|
+
lines.push("## Completed Before Failure");
|
|
1268
|
+
lines.push(...completedLines);
|
|
1269
|
+
lines.push("");
|
|
1270
|
+
lines.push("## Failed Action Paths");
|
|
1271
|
+
if (failedAction.affectedRels.length > 0) {
|
|
1272
|
+
for (const rel of failedAction.affectedRels) lines.push(` - ${rel}`);
|
|
1273
|
+
} else {
|
|
1274
|
+
lines.push(" (no filesystem path)");
|
|
1275
|
+
}
|
|
1276
|
+
lines.push("");
|
|
1277
|
+
lines.push("## Rollback");
|
|
1278
|
+
if (restored.length > 0) {
|
|
1279
|
+
lines.push("Restored from the pre-op safety snapshot:");
|
|
1280
|
+
for (const rel of restored) lines.push(` - ${rel}`);
|
|
1281
|
+
} else {
|
|
1282
|
+
lines.push("No completed action paths were restored automatically.");
|
|
1283
|
+
}
|
|
1284
|
+
if (unrestored.length > 0) {
|
|
1285
|
+
lines.push("", "Completed action paths still requiring review:");
|
|
1286
|
+
for (const rel of unrestored) lines.push(` - ${rel}`);
|
|
1287
|
+
}
|
|
1288
|
+
if (backupDir && isDir(backupDir)) {
|
|
1289
|
+
lines.push("");
|
|
1290
|
+
lines.push("Manual restore examples:");
|
|
1291
|
+
lines.push(` cp "${backupDir}/<relpath>" "${ctx.hqRoot}/<relpath>"`);
|
|
1292
|
+
lines.push(` rsync -a "${backupDir}/" "${ctx.hqRoot}/"`);
|
|
1293
|
+
} else {
|
|
1294
|
+
lines.push("", "No pre-op safety snapshot was available for automatic restore.");
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const manifestDir = path.join(ctx.hqRoot, ".hq", "rescue-recovery");
|
|
1298
|
+
const manifestPath = path.join(manifestDir, `rescue-apply-failure-${ctx.runTs}.md`);
|
|
1299
|
+
try {
|
|
1300
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
1301
|
+
fs.writeFileSync(manifestPath, lines.join("\n") + "\n");
|
|
1302
|
+
return manifestPath;
|
|
1303
|
+
} catch {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1115
1308
|
// --- Rescue-target mapping ---
|
|
1116
1309
|
function mapRescueTarget(rel: string): string {
|
|
1117
1310
|
if (rel === ".claude/CLAUDE.md") return "personal/CLAUDE.md";
|
|
@@ -1143,16 +1336,16 @@ function isOverwriteSafe(rel: string): boolean {
|
|
|
1143
1336
|
);
|
|
1144
1337
|
}
|
|
1145
1338
|
|
|
1146
|
-
function
|
|
1339
|
+
function masterSyncSymlinkTarget(localPath: string): string | null {
|
|
1147
1340
|
const st = lstatOrNull(localPath);
|
|
1148
|
-
if (!st || !st.isSymbolicLink()) return
|
|
1341
|
+
if (!st || !st.isSymbolicLink()) return null;
|
|
1149
1342
|
let tgt = "";
|
|
1150
1343
|
try {
|
|
1151
1344
|
tgt = fs.readlinkSync(localPath);
|
|
1152
1345
|
} catch {
|
|
1153
|
-
return
|
|
1346
|
+
return null;
|
|
1154
1347
|
}
|
|
1155
|
-
return tgt.includes("/personal/") || tgt.startsWith("personal/");
|
|
1348
|
+
return tgt.includes("/personal/") || tgt.startsWith("personal/") ? tgt : null;
|
|
1156
1349
|
}
|
|
1157
1350
|
|
|
1158
1351
|
function isUnderPreserve(cfg: Config, rel: string): boolean {
|
|
@@ -1405,14 +1598,13 @@ function conflictOne(ctx: WalkCtx, rel: string): void {
|
|
|
1405
1598
|
ctx.appendLog(`conflicted\t${rel}\t-> ${destRel}\n`);
|
|
1406
1599
|
}
|
|
1407
1600
|
|
|
1408
|
-
// --- classify
|
|
1601
|
+
// --- classify one file (the per-file workhorse) ---
|
|
1409
1602
|
//
|
|
1410
|
-
// Read-only by contract: this
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
// set has classified without throwing. Keep it that way
|
|
1414
|
-
|
|
1415
|
-
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 {
|
|
1416
1608
|
const { cfg } = ctx;
|
|
1417
1609
|
const localPath = path.join(ctx.hqRoot, rel);
|
|
1418
1610
|
const srcPath = path.join(ctx.srcDir, rel);
|
|
@@ -1423,50 +1615,27 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1423
1615
|
throw new Error(`rescue classifier fault injected at ${rel} (HQ_RESCUE_FAULT_AT_REL)`);
|
|
1424
1616
|
}
|
|
1425
1617
|
|
|
1426
|
-
if (isUnderPreserve(cfg, rel)) return;
|
|
1618
|
+
if (isUnderPreserve(cfg, rel)) return { kind: "skip", rel };
|
|
1427
1619
|
|
|
1428
1620
|
// Conflict-resolution artifacts (`<name>.conflict-<ts>-<peer>.<ext>`).
|
|
1429
1621
|
const base = rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
1430
1622
|
if (/\.conflict-/.test(base)) {
|
|
1431
|
-
|
|
1432
|
-
ctx.out(` drop conflict artifact: ${rel}\n`);
|
|
1433
|
-
} else {
|
|
1434
|
-
ctx.actions.push(() => fs.rmSync(localPath, { force: true }));
|
|
1435
|
-
}
|
|
1436
|
-
return;
|
|
1623
|
+
return { kind: "drop-conflict-artifact", rel };
|
|
1437
1624
|
}
|
|
1438
1625
|
|
|
1439
1626
|
// Script-managed files: core/core.yaml + legacy core.yaml.
|
|
1440
1627
|
if (rel === "core/core.yaml" || rel === "core.yaml") {
|
|
1441
|
-
|
|
1442
|
-
ctx.out(` skip script-managed (rewrites at stamp step): ${rel}\n`);
|
|
1443
|
-
} else {
|
|
1444
|
-
ctx.actions.push(() => fs.rmSync(localPath, { force: true }));
|
|
1445
|
-
}
|
|
1446
|
-
return;
|
|
1628
|
+
return { kind: "drop-script-managed", rel };
|
|
1447
1629
|
}
|
|
1448
1630
|
|
|
1449
1631
|
// Symlinks (mid-tree).
|
|
1450
1632
|
const lst = lstatOrNull(localPath);
|
|
1451
1633
|
if (lst && lst.isSymbolicLink()) {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
let tgt = "";
|
|
1456
|
-
try {
|
|
1457
|
-
tgt = fs.readlinkSync(localPath);
|
|
1458
|
-
} catch {
|
|
1459
|
-
/* ignore */
|
|
1460
|
-
}
|
|
1461
|
-
ctx.out(` drop reindex symlink: ${rel} -> ${tgt}\n`);
|
|
1462
|
-
} else {
|
|
1463
|
-
ctx.actions.push(() => {
|
|
1464
|
-
fs.rmSync(localPath, { force: true });
|
|
1465
|
-
ctx.appendLog(`symlink-dropped\t${rel}\t(reindex regenerable)\n`);
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1634
|
+
const target = masterSyncSymlinkTarget(localPath);
|
|
1635
|
+
if (target !== null) {
|
|
1636
|
+
return { kind: "drop-reindex-symlink", rel, target };
|
|
1468
1637
|
}
|
|
1469
|
-
return;
|
|
1638
|
+
return { kind: "skip", rel };
|
|
1470
1639
|
}
|
|
1471
1640
|
|
|
1472
1641
|
// Is path in upstream HEAD?
|
|
@@ -1497,9 +1666,7 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1497
1666
|
|
|
1498
1667
|
// USER-ONLY: unknown to upstream (HEAD AND floor both lack it).
|
|
1499
1668
|
if (inHead === 0 && inFloor === 0) {
|
|
1500
|
-
|
|
1501
|
-
if (cfg.dryRun) ctx.out(` user-only (leave in place): ${rel}\n`);
|
|
1502
|
-
return;
|
|
1669
|
+
return { kind: "user-only", rel };
|
|
1503
1670
|
}
|
|
1504
1671
|
|
|
1505
1672
|
// Path is/was in upstream. Determine if user edited it.
|
|
@@ -1524,95 +1691,186 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1524
1691
|
|
|
1525
1692
|
// Cloud-update reclassification.
|
|
1526
1693
|
if (userEdited === 1 && isCloudFlattenedSymlinkEquiv(ctx, rel, localPath)) {
|
|
1527
|
-
|
|
1528
|
-
if (cfg.dryRun) {
|
|
1529
|
-
ctx.out(
|
|
1530
|
-
` cloud-symlink reconciled (unchanged): ${rel} (hq-symlink: marker matches upstream target)\n`,
|
|
1531
|
-
);
|
|
1532
|
-
} else {
|
|
1533
|
-
ctx.actions.push(() => {
|
|
1534
|
-
fs.rmSync(localPath, { force: true });
|
|
1535
|
-
ctx.appendLog(
|
|
1536
|
-
`cloud-symlink-reconciled\t${rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`,
|
|
1537
|
-
);
|
|
1538
|
-
});
|
|
1539
|
-
}
|
|
1540
|
-
return;
|
|
1694
|
+
return { kind: "cloud-symlink-reconciled", rel };
|
|
1541
1695
|
}
|
|
1542
1696
|
|
|
1543
1697
|
// Convergence guard: drifted from floor but identical to upstream HEAD.
|
|
1544
1698
|
if (userEdited === 1 && inHead === 1 && bytesEqual(localPath, srcPath)) {
|
|
1545
|
-
|
|
1546
|
-
if (cfg.dryRun) {
|
|
1547
|
-
ctx.out(` drift reconciled (identical to upstream HEAD; no rescue): ${rel}\n`);
|
|
1548
|
-
} else {
|
|
1549
|
-
ctx.actions.push(() => {
|
|
1550
|
-
fs.rmSync(localPath, { force: true });
|
|
1551
|
-
ctx.appendLog(
|
|
1552
|
-
`drift-reconciled\t${rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`,
|
|
1553
|
-
);
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
return;
|
|
1699
|
+
return { kind: "drift-reconciled", rel };
|
|
1557
1700
|
}
|
|
1558
1701
|
|
|
1559
1702
|
if (userEdited === 1) {
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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 }),
|
|
1791
|
+
);
|
|
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], () => {
|
|
1579
1827
|
fs.rmSync(localPath, { force: true });
|
|
1580
1828
|
ctx.counts.userOverwrite += 1;
|
|
1581
|
-
ctx.appendLog(
|
|
1829
|
+
ctx.appendLog(
|
|
1830
|
+
`overwritten\t${decision.rel}\t(overwrite-safe; upstream wins, no copy preserved)\n`,
|
|
1831
|
+
);
|
|
1582
1832
|
});
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
ctx.appendLog(`unchanged\t${rel}\t(identical to upstream; left in place, mtime preserved)\n`);
|
|
1833
|
+
break;
|
|
1834
|
+
case "user-edit-conflict":
|
|
1835
|
+
add(`conflict quarantine: ${decision.rel}`, [decision.rel], () =>
|
|
1836
|
+
conflictOne(ctx, decision.rel),
|
|
1837
|
+
);
|
|
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
|
+
);
|
|
1599
1848
|
});
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
ctx.out(` unchanged (delete + replace): ${rel}\n`);
|
|
1604
|
-
} else {
|
|
1605
|
-
ctx.actions.push(() => {
|
|
1849
|
+
break;
|
|
1850
|
+
case "unchanged-delete":
|
|
1851
|
+
add(`delete unchanged: ${decision.rel}`, [decision.rel], () => {
|
|
1606
1852
|
fs.rmSync(localPath, { force: true });
|
|
1607
|
-
ctx.appendLog(
|
|
1853
|
+
ctx.appendLog(
|
|
1854
|
+
`deleted\t${decision.rel}\t(unchanged vs baseline; re-laid by overlay if still upstream)\n`,
|
|
1855
|
+
);
|
|
1608
1856
|
});
|
|
1609
|
-
|
|
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;
|
|
1610
1866
|
}
|
|
1611
1867
|
}
|
|
1868
|
+
|
|
1869
|
+
return actions;
|
|
1612
1870
|
}
|
|
1613
1871
|
|
|
1614
1872
|
// --- walk a wipe-set root ---
|
|
1615
|
-
// Precompute the per-file git facts
|
|
1873
|
+
// Precompute the per-file git facts classifyOne needs, in two batched passes
|
|
1616
1874
|
// instead of ~3 `git` spawns per file. Process-startup overhead (~15-20ms per
|
|
1617
1875
|
// spawn) dominates the per-file path, so on a large wipe set this turns a
|
|
1618
1876
|
// minutes-long classify into a seconds-long one. Behaviour is identical: the
|
|
@@ -1620,10 +1878,10 @@ function processOne(ctx: WalkCtx, rel: string): void {
|
|
|
1620
1878
|
// rel not represented in a map falls back to its original per-file spawn.
|
|
1621
1879
|
//
|
|
1622
1880
|
// Mirrors walkAndProcess's enumeration so the batched set matches what actually
|
|
1623
|
-
// reaches
|
|
1881
|
+
// reaches classifyOne: it skips `companies/_template` (wholesale-replaced, never
|
|
1624
1882
|
// classified per-file) and top-level symlinks (dropped, never classified).
|
|
1625
1883
|
function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
1626
|
-
const allRels: string[] = []; // every rel that reaches
|
|
1884
|
+
const allRels: string[] = []; // every rel that reaches classifyOne (files + symlinks)
|
|
1627
1885
|
const fileRels: string[] = []; // subset that are regular files (hashable)
|
|
1628
1886
|
const fileAbs: string[] = []; // parallel to fileRels
|
|
1629
1887
|
|
|
@@ -1639,7 +1897,7 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1639
1897
|
: [];
|
|
1640
1898
|
for (const rel of rels) {
|
|
1641
1899
|
// A path containing a newline can't survive line-delimited batch stdin;
|
|
1642
|
-
// leave it out of the maps and let
|
|
1900
|
+
// leave it out of the maps and let classifyOne spawn per-file for it.
|
|
1643
1901
|
if (rel.includes("\n")) continue;
|
|
1644
1902
|
allRels.push(rel);
|
|
1645
1903
|
const st = lstatOrNull(path.join(ctx.hqRoot, rel));
|
|
@@ -1650,7 +1908,7 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1650
1908
|
}
|
|
1651
1909
|
}
|
|
1652
1910
|
|
|
1653
|
-
// In head_compare mode
|
|
1911
|
+
// In head_compare mode classifyOne never consults these maps (it compares
|
|
1654
1912
|
// bytes against the upstream working tree directly), so skip both git passes.
|
|
1655
1913
|
if (ctx.baselineMode !== "history_floor") {
|
|
1656
1914
|
ctx.floorShas = new Map();
|
|
@@ -1687,7 +1945,7 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1687
1945
|
ctx.floorShas = floorShas;
|
|
1688
1946
|
|
|
1689
1947
|
// Pass 2: local blob SHA, but ONLY for regular files that are in the floor.
|
|
1690
|
-
//
|
|
1948
|
+
// classifyOne reads localShas exclusively in the `inFloor === 1` branch, so a
|
|
1691
1949
|
// file absent from the floor (e.g. the transient `.claude/worktrees/` trees,
|
|
1692
1950
|
// which classify as user-only) never needs a hash — and hashing it would mean
|
|
1693
1951
|
// reading its bytes for nothing. Restricting the hash set to floor members is
|
|
@@ -1718,22 +1976,12 @@ function precomputeGitMaps(ctx: WalkCtx, wipeToplevel: string[]): void {
|
|
|
1718
1976
|
}
|
|
1719
1977
|
|
|
1720
1978
|
function walkAndProcess(ctx: WalkCtx, rootRel: string): void {
|
|
1721
|
-
const { cfg } = ctx;
|
|
1722
1979
|
const rootAbs = path.join(ctx.hqRoot, rootRel);
|
|
1723
1980
|
if (!lexists(rootAbs)) return;
|
|
1724
1981
|
|
|
1725
1982
|
// companies/_template — wholesale-replace.
|
|
1726
1983
|
if (rootRel === "companies/_template") {
|
|
1727
|
-
|
|
1728
|
-
ctx.out(" wholesale-replace: companies/_template (template carve-out)\n");
|
|
1729
|
-
} else {
|
|
1730
|
-
ctx.actions.push(() =>
|
|
1731
|
-
fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
|
|
1732
|
-
recursive: true,
|
|
1733
|
-
force: true,
|
|
1734
|
-
}),
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1984
|
+
recordDecision(ctx, { kind: "wholesale-replace-template", rel: "companies/_template" });
|
|
1737
1985
|
return;
|
|
1738
1986
|
}
|
|
1739
1987
|
|
|
@@ -1741,35 +1989,22 @@ function walkAndProcess(ctx: WalkCtx, rootRel: string): void {
|
|
|
1741
1989
|
|
|
1742
1990
|
// Top-level symlink.
|
|
1743
1991
|
if (lst && lst.isSymbolicLink()) {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
let tgt = "";
|
|
1748
|
-
try {
|
|
1749
|
-
tgt = fs.readlinkSync(rootAbs);
|
|
1750
|
-
} catch {
|
|
1751
|
-
/* ignore */
|
|
1752
|
-
}
|
|
1753
|
-
ctx.out(` drop reindex symlink: ${rootRel} -> ${tgt}\n`);
|
|
1754
|
-
} else {
|
|
1755
|
-
ctx.actions.push(() => {
|
|
1756
|
-
fs.rmSync(rootAbs, { force: true });
|
|
1757
|
-
ctx.appendLog(`symlink-dropped\t${rootRel}\t(reindex regenerable)\n`);
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1992
|
+
const target = masterSyncSymlinkTarget(rootAbs);
|
|
1993
|
+
if (target !== null) {
|
|
1994
|
+
recordDecision(ctx, { kind: "drop-reindex-symlink", rel: rootRel, target });
|
|
1760
1995
|
}
|
|
1761
1996
|
return;
|
|
1762
1997
|
}
|
|
1763
1998
|
|
|
1764
1999
|
// Top-level regular file.
|
|
1765
2000
|
if (lst && lst.isFile()) {
|
|
1766
|
-
|
|
2001
|
+
recordDecision(ctx, classifyOne(ctx, rootRel));
|
|
1767
2002
|
return;
|
|
1768
2003
|
}
|
|
1769
2004
|
|
|
1770
2005
|
// Top-level directory: recursive walk, pruning node_modules + nested .git.
|
|
1771
2006
|
for (const rel of findFilesAndSymlinks(rootAbs, ctx.hqRoot)) {
|
|
1772
|
-
|
|
2007
|
+
recordDecision(ctx, classifyOne(ctx, rel));
|
|
1773
2008
|
}
|
|
1774
2009
|
}
|
|
1775
2010
|
|