@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/dist/cli/rescue-core.js
CHANGED
|
@@ -616,6 +616,7 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
616
616
|
unchangedList,
|
|
617
617
|
appendLog,
|
|
618
618
|
out,
|
|
619
|
+
decisions: [],
|
|
619
620
|
actions: [],
|
|
620
621
|
};
|
|
621
622
|
// --- Pre-operation safety snapshot (BEFORE any destructive op) ---
|
|
@@ -645,16 +646,19 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
645
646
|
out(` snapshot complete (restore any file: cp "${backupDir}/<relpath>" "${hqRoot}/<relpath>")\n`);
|
|
646
647
|
}
|
|
647
648
|
// --- Walk + classify (PHASE 1: read-only) ---
|
|
648
|
-
// Classification mutates nothing on disk: every
|
|
649
|
-
//
|
|
650
|
-
//
|
|
649
|
+
// Classification mutates nothing on disk: every outcome is recorded as a
|
|
650
|
+
// typed RescueDecision. Destructive ops (delete, rescue-move,
|
|
651
|
+
// conflict-quarantine, symlink-drop) are built from those decisions and
|
|
652
|
+
// executed later (PHASE 2). This is the
|
|
651
653
|
// ordering-safety invariant: if classification throws partway through the
|
|
652
654
|
// wipe set (e.g. an unreadable entry, a future unhandled file shape), it
|
|
653
|
-
// aborts HERE — before a single file has been touched
|
|
654
|
-
// half-applied wipe like the
|
|
655
|
+
// aborts HERE — before a single file has been touched or even queued as a
|
|
656
|
+
// filesystem closure — instead of leaving a half-applied wipe like the
|
|
657
|
+
// pre-port bash classifier did (DEV-1767:
|
|
655
658
|
// `Error: Path is a directory` on a nested dir-symlink, thrown after ~10 core
|
|
656
|
-
// files were already deleted). Dry-run
|
|
657
|
-
//
|
|
659
|
+
// files were already deleted). Dry-run renders each typed decision inline as
|
|
660
|
+
// the walk records it, preserving the legacy progress/fault timing while
|
|
661
|
+
// still sharing the same classify path as the live pass.
|
|
658
662
|
out("\n");
|
|
659
663
|
if (wipeToplevel.length === 0) {
|
|
660
664
|
out("==> Wipe set is empty; nothing to process or overlay.\n");
|
|
@@ -734,6 +738,7 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
734
738
|
// Actions run in classification (walk) order — the same order the old
|
|
735
739
|
// interleaved walk mutated in — so per-file output and on-disk results are
|
|
736
740
|
// unchanged versus before the two-phase split.
|
|
741
|
+
ctx.actions = buildRescueActions(ctx);
|
|
737
742
|
applyClassifiedActions(ctx, backupDir);
|
|
738
743
|
// --- Back up preserve-subpaths to a mktemp shuttle ---
|
|
739
744
|
const shuttle = path.join(tmpdir, "preserve");
|
|
@@ -965,8 +970,30 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
965
970
|
}
|
|
966
971
|
return { status: 0 };
|
|
967
972
|
}
|
|
968
|
-
function
|
|
969
|
-
ctx.
|
|
973
|
+
function recordDecision(ctx, decision) {
|
|
974
|
+
ctx.decisions.push(decision);
|
|
975
|
+
if (ctx.cfg.dryRun)
|
|
976
|
+
renderDryRunDecision(ctx, decision);
|
|
977
|
+
switch (decision.kind) {
|
|
978
|
+
case "drop-reindex-symlink":
|
|
979
|
+
ctx.counts.symlinkDropped += 1;
|
|
980
|
+
return;
|
|
981
|
+
case "user-only":
|
|
982
|
+
ctx.counts.userOnly += 1;
|
|
983
|
+
return;
|
|
984
|
+
case "cloud-symlink-reconciled":
|
|
985
|
+
ctx.counts.cloudSymlinkReconciled += 1;
|
|
986
|
+
return;
|
|
987
|
+
case "drift-reconciled":
|
|
988
|
+
ctx.counts.driftReconciled += 1;
|
|
989
|
+
return;
|
|
990
|
+
case "unchanged-preserve":
|
|
991
|
+
case "unchanged-delete":
|
|
992
|
+
ctx.counts.unchanged += 1;
|
|
993
|
+
return;
|
|
994
|
+
default:
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
970
997
|
}
|
|
971
998
|
function applyClassifiedActions(ctx, backupDir) {
|
|
972
999
|
const completed = [];
|
|
@@ -1117,18 +1144,18 @@ function isOverwriteSafe(rel) {
|
|
|
1117
1144
|
rel === "core/docs/hq/USER-GUIDE.md" ||
|
|
1118
1145
|
rel === "core/policies/_digest.md");
|
|
1119
1146
|
}
|
|
1120
|
-
function
|
|
1147
|
+
function masterSyncSymlinkTarget(localPath) {
|
|
1121
1148
|
const st = lstatOrNull(localPath);
|
|
1122
1149
|
if (!st || !st.isSymbolicLink())
|
|
1123
|
-
return
|
|
1150
|
+
return null;
|
|
1124
1151
|
let tgt = "";
|
|
1125
1152
|
try {
|
|
1126
1153
|
tgt = fs.readlinkSync(localPath);
|
|
1127
1154
|
}
|
|
1128
1155
|
catch {
|
|
1129
|
-
return
|
|
1156
|
+
return null;
|
|
1130
1157
|
}
|
|
1131
|
-
return tgt.includes("/personal/") || tgt.startsWith("personal/");
|
|
1158
|
+
return tgt.includes("/personal/") || tgt.startsWith("personal/") ? tgt : null;
|
|
1132
1159
|
}
|
|
1133
1160
|
function isUnderPreserve(cfg, rel) {
|
|
1134
1161
|
for (const sp of cfg.preserveSubpaths) {
|
|
@@ -1380,14 +1407,13 @@ function conflictOne(ctx, rel) {
|
|
|
1380
1407
|
ctx.out(` conflicted: ${rel} -> ${destRel}\n`);
|
|
1381
1408
|
ctx.appendLog(`conflicted\t${rel}\t-> ${destRel}\n`);
|
|
1382
1409
|
}
|
|
1383
|
-
// --- classify
|
|
1410
|
+
// --- classify one file (the per-file workhorse) ---
|
|
1384
1411
|
//
|
|
1385
|
-
// Read-only by contract: this
|
|
1386
|
-
//
|
|
1387
|
-
//
|
|
1388
|
-
// set has classified without throwing. Keep it that way
|
|
1389
|
-
|
|
1390
|
-
function processOne(ctx, rel) {
|
|
1412
|
+
// Read-only by contract: this returns a RescueDecision only. It does not render
|
|
1413
|
+
// dry-run text, update action queues, or mutate the filesystem. Dry-run text is
|
|
1414
|
+
// rendered by recordDecision, and PHASE 2 action building happens only once the
|
|
1415
|
+
// whole wipe set has classified without throwing. Keep it that way.
|
|
1416
|
+
function classifyOne(ctx, rel) {
|
|
1391
1417
|
const { cfg } = ctx;
|
|
1392
1418
|
const localPath = path.join(ctx.hqRoot, rel);
|
|
1393
1419
|
const srcPath = path.join(ctx.srcDir, rel);
|
|
@@ -1397,51 +1423,24 @@ function processOne(ctx, rel) {
|
|
|
1397
1423
|
throw new Error(`rescue classifier fault injected at ${rel} (HQ_RESCUE_FAULT_AT_REL)`);
|
|
1398
1424
|
}
|
|
1399
1425
|
if (isUnderPreserve(cfg, rel))
|
|
1400
|
-
return;
|
|
1426
|
+
return { kind: "skip", rel };
|
|
1401
1427
|
// Conflict-resolution artifacts (`<name>.conflict-<ts>-<peer>.<ext>`).
|
|
1402
1428
|
const base = rel.includes("/") ? rel.slice(rel.lastIndexOf("/") + 1) : rel;
|
|
1403
1429
|
if (/\.conflict-/.test(base)) {
|
|
1404
|
-
|
|
1405
|
-
ctx.out(` drop conflict artifact: ${rel}\n`);
|
|
1406
|
-
}
|
|
1407
|
-
else {
|
|
1408
|
-
queueAction(ctx, `drop conflict artifact: ${rel}`, [rel], () => fs.rmSync(localPath, { force: true }));
|
|
1409
|
-
}
|
|
1410
|
-
return;
|
|
1430
|
+
return { kind: "drop-conflict-artifact", rel };
|
|
1411
1431
|
}
|
|
1412
1432
|
// Script-managed files: core/core.yaml + legacy core.yaml.
|
|
1413
1433
|
if (rel === "core/core.yaml" || rel === "core.yaml") {
|
|
1414
|
-
|
|
1415
|
-
ctx.out(` skip script-managed (rewrites at stamp step): ${rel}\n`);
|
|
1416
|
-
}
|
|
1417
|
-
else {
|
|
1418
|
-
queueAction(ctx, `drop script-managed: ${rel}`, [rel], () => fs.rmSync(localPath, { force: true }));
|
|
1419
|
-
}
|
|
1420
|
-
return;
|
|
1434
|
+
return { kind: "drop-script-managed", rel };
|
|
1421
1435
|
}
|
|
1422
1436
|
// Symlinks (mid-tree).
|
|
1423
1437
|
const lst = lstatOrNull(localPath);
|
|
1424
1438
|
if (lst && lst.isSymbolicLink()) {
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
let tgt = "";
|
|
1429
|
-
try {
|
|
1430
|
-
tgt = fs.readlinkSync(localPath);
|
|
1431
|
-
}
|
|
1432
|
-
catch {
|
|
1433
|
-
/* ignore */
|
|
1434
|
-
}
|
|
1435
|
-
ctx.out(` drop reindex symlink: ${rel} -> ${tgt}\n`);
|
|
1436
|
-
}
|
|
1437
|
-
else {
|
|
1438
|
-
queueAction(ctx, `drop reindex symlink: ${rel}`, [rel], () => {
|
|
1439
|
-
fs.rmSync(localPath, { force: true });
|
|
1440
|
-
ctx.appendLog(`symlink-dropped\t${rel}\t(reindex regenerable)\n`);
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1439
|
+
const target = masterSyncSymlinkTarget(localPath);
|
|
1440
|
+
if (target !== null) {
|
|
1441
|
+
return { kind: "drop-reindex-symlink", rel, target };
|
|
1443
1442
|
}
|
|
1444
|
-
return;
|
|
1443
|
+
return { kind: "skip", rel };
|
|
1445
1444
|
}
|
|
1446
1445
|
// Is path in upstream HEAD?
|
|
1447
1446
|
const inHead = existsFollow(srcPath) ? 1 : 0;
|
|
@@ -1468,10 +1467,7 @@ function processOne(ctx, rel) {
|
|
|
1468
1467
|
}
|
|
1469
1468
|
// USER-ONLY: unknown to upstream (HEAD AND floor both lack it).
|
|
1470
1469
|
if (inHead === 0 && inFloor === 0) {
|
|
1471
|
-
|
|
1472
|
-
if (cfg.dryRun)
|
|
1473
|
-
ctx.out(` user-only (leave in place): ${rel}\n`);
|
|
1474
|
-
return;
|
|
1470
|
+
return { kind: "user-only", rel };
|
|
1475
1471
|
}
|
|
1476
1472
|
// Path is/was in upstream. Determine if user edited it.
|
|
1477
1473
|
let userEdited = 0;
|
|
@@ -1496,99 +1492,154 @@ function processOne(ctx, rel) {
|
|
|
1496
1492
|
}
|
|
1497
1493
|
// Cloud-update reclassification.
|
|
1498
1494
|
if (userEdited === 1 && isCloudFlattenedSymlinkEquiv(ctx, rel, localPath)) {
|
|
1499
|
-
|
|
1500
|
-
if (cfg.dryRun) {
|
|
1501
|
-
ctx.out(` cloud-symlink reconciled (unchanged): ${rel} (hq-symlink: marker matches upstream target)\n`);
|
|
1502
|
-
}
|
|
1503
|
-
else {
|
|
1504
|
-
queueAction(ctx, `cloud-symlink reconciled: ${rel}`, [rel], () => {
|
|
1505
|
-
fs.rmSync(localPath, { force: true });
|
|
1506
|
-
ctx.appendLog(`cloud-symlink-reconciled\t${rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`);
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
return;
|
|
1495
|
+
return { kind: "cloud-symlink-reconciled", rel };
|
|
1510
1496
|
}
|
|
1511
1497
|
// Convergence guard: drifted from floor but identical to upstream HEAD.
|
|
1512
1498
|
if (userEdited === 1 && inHead === 1 && bytesEqual(localPath, srcPath)) {
|
|
1513
|
-
|
|
1514
|
-
if (cfg.dryRun) {
|
|
1515
|
-
ctx.out(` drift reconciled (identical to upstream HEAD; no rescue): ${rel}\n`);
|
|
1516
|
-
}
|
|
1517
|
-
else {
|
|
1518
|
-
queueAction(ctx, `drift reconciled: ${rel}`, [rel], () => {
|
|
1519
|
-
fs.rmSync(localPath, { force: true });
|
|
1520
|
-
ctx.appendLog(`drift-reconciled\t${rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`);
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
return;
|
|
1499
|
+
return { kind: "drift-reconciled", rel };
|
|
1524
1500
|
}
|
|
1525
1501
|
if (userEdited === 1) {
|
|
1526
|
-
if (
|
|
1527
|
-
|
|
1528
|
-
ctx.out(` user-edit (diff-append): ${rel} -> personal/CLAUDE.md\n`);
|
|
1529
|
-
ctx.counts.userEdit += 1;
|
|
1530
|
-
}
|
|
1531
|
-
else if (isOverwriteSafe(rel)) {
|
|
1532
|
-
ctx.out(` user-edit (overwrite-safe): ${rel} -> upstream wins (no copy preserved)\n`);
|
|
1533
|
-
ctx.counts.userOverwrite += 1;
|
|
1534
|
-
}
|
|
1535
|
-
else if (isConflictClass(rel)) {
|
|
1536
|
-
ctx.out(` user-edit (conflict): ${rel} -> .hq-conflicts/rescue-${ctx.runTs}/${rel}\n`);
|
|
1537
|
-
ctx.counts.userConflict += 1;
|
|
1538
|
-
}
|
|
1539
|
-
else {
|
|
1540
|
-
ctx.out(` user-edit (rescue): ${rel} -> ${mapRescueTarget(rel)}\n`);
|
|
1541
|
-
ctx.counts.userEdit += 1;
|
|
1542
|
-
}
|
|
1502
|
+
if (rel === ".claude/CLAUDE.md") {
|
|
1503
|
+
return { kind: "user-edit-diff-append", rel };
|
|
1543
1504
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1505
|
+
if (isOverwriteSafe(rel)) {
|
|
1506
|
+
return { kind: "user-edit-overwrite-safe", rel };
|
|
1507
|
+
}
|
|
1508
|
+
if (isConflictClass(rel)) {
|
|
1509
|
+
return { kind: "user-edit-conflict", rel };
|
|
1510
|
+
}
|
|
1511
|
+
return { kind: "user-edit-rescue", rel, target: mapRescueTarget(rel) };
|
|
1512
|
+
}
|
|
1513
|
+
// user_edited=0: matches floor baseline. Split on byte-equality to HEAD.
|
|
1514
|
+
if (bytesEqual(localPath, path.join(ctx.srcDir, rel))) {
|
|
1515
|
+
return { kind: "unchanged-preserve", rel };
|
|
1516
|
+
}
|
|
1517
|
+
return { kind: "unchanged-delete", rel };
|
|
1518
|
+
}
|
|
1519
|
+
function renderDryRunDecision(ctx, decision) {
|
|
1520
|
+
switch (decision.kind) {
|
|
1521
|
+
case "skip":
|
|
1522
|
+
break;
|
|
1523
|
+
case "drop-conflict-artifact":
|
|
1524
|
+
ctx.out(` drop conflict artifact: ${decision.rel}\n`);
|
|
1525
|
+
break;
|
|
1526
|
+
case "drop-script-managed":
|
|
1527
|
+
ctx.out(` skip script-managed (rewrites at stamp step): ${decision.rel}\n`);
|
|
1528
|
+
break;
|
|
1529
|
+
case "drop-reindex-symlink":
|
|
1530
|
+
ctx.out(` drop reindex symlink: ${decision.rel} -> ${decision.target}\n`);
|
|
1531
|
+
break;
|
|
1532
|
+
case "user-only":
|
|
1533
|
+
ctx.out(` user-only (leave in place): ${decision.rel}\n`);
|
|
1534
|
+
break;
|
|
1535
|
+
case "cloud-symlink-reconciled":
|
|
1536
|
+
ctx.out(` cloud-symlink reconciled (unchanged): ${decision.rel} (hq-symlink: marker matches upstream target)\n`);
|
|
1537
|
+
break;
|
|
1538
|
+
case "drift-reconciled":
|
|
1539
|
+
ctx.out(` drift reconciled (identical to upstream HEAD; no rescue): ${decision.rel}\n`);
|
|
1540
|
+
break;
|
|
1541
|
+
case "user-edit-diff-append":
|
|
1542
|
+
ctx.out(` user-edit (diff-append): ${decision.rel} -> personal/CLAUDE.md\n`);
|
|
1543
|
+
ctx.counts.userEdit += 1;
|
|
1544
|
+
break;
|
|
1545
|
+
case "user-edit-overwrite-safe":
|
|
1546
|
+
ctx.out(` user-edit (overwrite-safe): ${decision.rel} -> upstream wins (no copy preserved)\n`);
|
|
1547
|
+
ctx.counts.userOverwrite += 1;
|
|
1548
|
+
break;
|
|
1549
|
+
case "user-edit-conflict":
|
|
1550
|
+
ctx.out(` user-edit (conflict): ${decision.rel} -> .hq-conflicts/rescue-${ctx.runTs}/${decision.rel}\n`);
|
|
1551
|
+
ctx.counts.userConflict += 1;
|
|
1552
|
+
break;
|
|
1553
|
+
case "user-edit-rescue":
|
|
1554
|
+
ctx.out(` user-edit (rescue): ${decision.rel} -> ${decision.target}\n`);
|
|
1555
|
+
ctx.counts.userEdit += 1;
|
|
1556
|
+
break;
|
|
1557
|
+
case "unchanged-preserve":
|
|
1558
|
+
ctx.out(` unchanged (preserved in place): ${decision.rel}\n`);
|
|
1559
|
+
break;
|
|
1560
|
+
case "unchanged-delete":
|
|
1561
|
+
ctx.out(` unchanged (delete + replace): ${decision.rel}\n`);
|
|
1562
|
+
break;
|
|
1563
|
+
case "wholesale-replace-template":
|
|
1564
|
+
ctx.out(" wholesale-replace: companies/_template (template carve-out)\n");
|
|
1565
|
+
break;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
function buildRescueActions(ctx) {
|
|
1569
|
+
const actions = [];
|
|
1570
|
+
const add = (label, affectedRels, runAction) => {
|
|
1571
|
+
actions.push({ label, affectedRels, run: runAction });
|
|
1572
|
+
};
|
|
1573
|
+
for (const decision of ctx.decisions) {
|
|
1574
|
+
const localPath = path.join(ctx.hqRoot, decision.rel);
|
|
1575
|
+
switch (decision.kind) {
|
|
1576
|
+
case "skip":
|
|
1577
|
+
case "user-only":
|
|
1578
|
+
break;
|
|
1579
|
+
case "drop-conflict-artifact":
|
|
1580
|
+
add(`drop conflict artifact: ${decision.rel}`, [decision.rel], () => fs.rmSync(localPath, { force: true }));
|
|
1581
|
+
break;
|
|
1582
|
+
case "drop-script-managed":
|
|
1583
|
+
add(`drop script-managed: ${decision.rel}`, [decision.rel], () => fs.rmSync(localPath, { force: true }));
|
|
1584
|
+
break;
|
|
1585
|
+
case "drop-reindex-symlink":
|
|
1586
|
+
add(`drop reindex symlink: ${decision.rel}`, [decision.rel], () => {
|
|
1587
|
+
fs.rmSync(localPath, { force: true });
|
|
1588
|
+
ctx.appendLog(`symlink-dropped\t${decision.rel}\t(reindex regenerable)\n`);
|
|
1589
|
+
});
|
|
1590
|
+
break;
|
|
1591
|
+
case "cloud-symlink-reconciled":
|
|
1592
|
+
add(`cloud-symlink reconciled: ${decision.rel}`, [decision.rel], () => {
|
|
1593
|
+
fs.rmSync(localPath, { force: true });
|
|
1594
|
+
ctx.appendLog(`cloud-symlink-reconciled\t${decision.rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`);
|
|
1595
|
+
});
|
|
1596
|
+
break;
|
|
1597
|
+
case "drift-reconciled":
|
|
1598
|
+
add(`drift reconciled: ${decision.rel}`, [decision.rel], () => {
|
|
1599
|
+
fs.rmSync(localPath, { force: true });
|
|
1600
|
+
ctx.appendLog(`drift-reconciled\t${decision.rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`);
|
|
1601
|
+
});
|
|
1602
|
+
break;
|
|
1603
|
+
case "user-edit-diff-append":
|
|
1604
|
+
add(`rescue diff-append: ${decision.rel}`, [decision.rel], () => rescueOne(ctx, decision.rel));
|
|
1605
|
+
break;
|
|
1606
|
+
case "user-edit-overwrite-safe":
|
|
1607
|
+
add(`overwrite-safe: ${decision.rel}`, [decision.rel], () => {
|
|
1550
1608
|
fs.rmSync(localPath, { force: true });
|
|
1551
1609
|
ctx.counts.userOverwrite += 1;
|
|
1552
|
-
ctx.appendLog(`overwritten\t${rel}\t(overwrite-safe; upstream wins, no copy preserved)\n`);
|
|
1610
|
+
ctx.appendLog(`overwritten\t${decision.rel}\t(overwrite-safe; upstream wins, no copy preserved)\n`);
|
|
1553
1611
|
});
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
ctx.counts.unchanged += 1;
|
|
1566
|
-
if (bytesEqual(localPath, path.join(ctx.srcDir, rel))) {
|
|
1567
|
-
if (cfg.dryRun) {
|
|
1568
|
-
ctx.out(` unchanged (preserved in place): ${rel}\n`);
|
|
1569
|
-
}
|
|
1570
|
-
else {
|
|
1571
|
-
queueAction(ctx, `preserve unchanged: ${rel}`, [], () => {
|
|
1572
|
-
fs.appendFileSync(ctx.unchangedList, `/${rel}\n`);
|
|
1573
|
-
ctx.appendLog(`unchanged\t${rel}\t(identical to upstream; left in place, mtime preserved)\n`);
|
|
1612
|
+
break;
|
|
1613
|
+
case "user-edit-conflict":
|
|
1614
|
+
add(`conflict quarantine: ${decision.rel}`, [decision.rel], () => conflictOne(ctx, decision.rel));
|
|
1615
|
+
break;
|
|
1616
|
+
case "user-edit-rescue":
|
|
1617
|
+
add(`rescue: ${decision.rel}`, [decision.rel], () => rescueOne(ctx, decision.rel));
|
|
1618
|
+
break;
|
|
1619
|
+
case "unchanged-preserve":
|
|
1620
|
+
add(`preserve unchanged: ${decision.rel}`, [], () => {
|
|
1621
|
+
fs.appendFileSync(ctx.unchangedList, `/${decision.rel}\n`);
|
|
1622
|
+
ctx.appendLog(`unchanged\t${decision.rel}\t(identical to upstream; left in place, mtime preserved)\n`);
|
|
1574
1623
|
});
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
if (cfg.dryRun) {
|
|
1579
|
-
ctx.out(` unchanged (delete + replace): ${rel}\n`);
|
|
1580
|
-
}
|
|
1581
|
-
else {
|
|
1582
|
-
queueAction(ctx, `delete unchanged: ${rel}`, [rel], () => {
|
|
1624
|
+
break;
|
|
1625
|
+
case "unchanged-delete":
|
|
1626
|
+
add(`delete unchanged: ${decision.rel}`, [decision.rel], () => {
|
|
1583
1627
|
fs.rmSync(localPath, { force: true });
|
|
1584
|
-
ctx.appendLog(`deleted\t${rel}\t(unchanged vs baseline; re-laid by overlay if still upstream)\n`);
|
|
1628
|
+
ctx.appendLog(`deleted\t${decision.rel}\t(unchanged vs baseline; re-laid by overlay if still upstream)\n`);
|
|
1585
1629
|
});
|
|
1586
|
-
|
|
1630
|
+
break;
|
|
1631
|
+
case "wholesale-replace-template":
|
|
1632
|
+
add("wholesale-replace: companies/_template", [decision.rel], () => fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
|
|
1633
|
+
recursive: true,
|
|
1634
|
+
force: true,
|
|
1635
|
+
}));
|
|
1636
|
+
break;
|
|
1587
1637
|
}
|
|
1588
1638
|
}
|
|
1639
|
+
return actions;
|
|
1589
1640
|
}
|
|
1590
1641
|
// --- walk a wipe-set root ---
|
|
1591
|
-
// Precompute the per-file git facts
|
|
1642
|
+
// Precompute the per-file git facts classifyOne needs, in two batched passes
|
|
1592
1643
|
// instead of ~3 `git` spawns per file. Process-startup overhead (~15-20ms per
|
|
1593
1644
|
// spawn) dominates the per-file path, so on a large wipe set this turns a
|
|
1594
1645
|
// minutes-long classify into a seconds-long one. Behaviour is identical: the
|
|
@@ -1596,10 +1647,10 @@ function processOne(ctx, rel) {
|
|
|
1596
1647
|
// rel not represented in a map falls back to its original per-file spawn.
|
|
1597
1648
|
//
|
|
1598
1649
|
// Mirrors walkAndProcess's enumeration so the batched set matches what actually
|
|
1599
|
-
// reaches
|
|
1650
|
+
// reaches classifyOne: it skips `companies/_template` (wholesale-replaced, never
|
|
1600
1651
|
// classified per-file) and top-level symlinks (dropped, never classified).
|
|
1601
1652
|
function precomputeGitMaps(ctx, wipeToplevel) {
|
|
1602
|
-
const allRels = []; // every rel that reaches
|
|
1653
|
+
const allRels = []; // every rel that reaches classifyOne (files + symlinks)
|
|
1603
1654
|
const fileRels = []; // subset that are regular files (hashable)
|
|
1604
1655
|
const fileAbs = []; // parallel to fileRels
|
|
1605
1656
|
for (const rootRel of wipeToplevel) {
|
|
@@ -1616,7 +1667,7 @@ function precomputeGitMaps(ctx, wipeToplevel) {
|
|
|
1616
1667
|
: [];
|
|
1617
1668
|
for (const rel of rels) {
|
|
1618
1669
|
// A path containing a newline can't survive line-delimited batch stdin;
|
|
1619
|
-
// leave it out of the maps and let
|
|
1670
|
+
// leave it out of the maps and let classifyOne spawn per-file for it.
|
|
1620
1671
|
if (rel.includes("\n"))
|
|
1621
1672
|
continue;
|
|
1622
1673
|
allRels.push(rel);
|
|
@@ -1627,7 +1678,7 @@ function precomputeGitMaps(ctx, wipeToplevel) {
|
|
|
1627
1678
|
}
|
|
1628
1679
|
}
|
|
1629
1680
|
}
|
|
1630
|
-
// In head_compare mode
|
|
1681
|
+
// In head_compare mode classifyOne never consults these maps (it compares
|
|
1631
1682
|
// bytes against the upstream working tree directly), so skip both git passes.
|
|
1632
1683
|
if (ctx.baselineMode !== "history_floor") {
|
|
1633
1684
|
ctx.floorShas = new Map();
|
|
@@ -1663,7 +1714,7 @@ function precomputeGitMaps(ctx, wipeToplevel) {
|
|
|
1663
1714
|
}
|
|
1664
1715
|
ctx.floorShas = floorShas;
|
|
1665
1716
|
// Pass 2: local blob SHA, but ONLY for regular files that are in the floor.
|
|
1666
|
-
//
|
|
1717
|
+
// classifyOne reads localShas exclusively in the `inFloor === 1` branch, so a
|
|
1667
1718
|
// file absent from the floor (e.g. the transient `.claude/worktrees/` trees,
|
|
1668
1719
|
// which classify as user-only) never needs a hash — and hashing it would mean
|
|
1669
1720
|
// reading its bytes for nothing. Restricting the hash set to floor members is
|
|
@@ -1694,55 +1745,31 @@ function precomputeGitMaps(ctx, wipeToplevel) {
|
|
|
1694
1745
|
ctx.localShas = localShas;
|
|
1695
1746
|
}
|
|
1696
1747
|
function walkAndProcess(ctx, rootRel) {
|
|
1697
|
-
const { cfg } = ctx;
|
|
1698
1748
|
const rootAbs = path.join(ctx.hqRoot, rootRel);
|
|
1699
1749
|
if (!lexists(rootAbs))
|
|
1700
1750
|
return;
|
|
1701
1751
|
// companies/_template — wholesale-replace.
|
|
1702
1752
|
if (rootRel === "companies/_template") {
|
|
1703
|
-
|
|
1704
|
-
ctx.out(" wholesale-replace: companies/_template (template carve-out)\n");
|
|
1705
|
-
}
|
|
1706
|
-
else {
|
|
1707
|
-
queueAction(ctx, "wholesale-replace: companies/_template", [rootRel], () => fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
|
|
1708
|
-
recursive: true,
|
|
1709
|
-
force: true,
|
|
1710
|
-
}));
|
|
1711
|
-
}
|
|
1753
|
+
recordDecision(ctx, { kind: "wholesale-replace-template", rel: "companies/_template" });
|
|
1712
1754
|
return;
|
|
1713
1755
|
}
|
|
1714
1756
|
const lst = lstatOrNull(rootAbs);
|
|
1715
1757
|
// Top-level symlink.
|
|
1716
1758
|
if (lst && lst.isSymbolicLink()) {
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
let tgt = "";
|
|
1721
|
-
try {
|
|
1722
|
-
tgt = fs.readlinkSync(rootAbs);
|
|
1723
|
-
}
|
|
1724
|
-
catch {
|
|
1725
|
-
/* ignore */
|
|
1726
|
-
}
|
|
1727
|
-
ctx.out(` drop reindex symlink: ${rootRel} -> ${tgt}\n`);
|
|
1728
|
-
}
|
|
1729
|
-
else {
|
|
1730
|
-
queueAction(ctx, `drop reindex symlink: ${rootRel}`, [rootRel], () => {
|
|
1731
|
-
fs.rmSync(rootAbs, { force: true });
|
|
1732
|
-
ctx.appendLog(`symlink-dropped\t${rootRel}\t(reindex regenerable)\n`);
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1759
|
+
const target = masterSyncSymlinkTarget(rootAbs);
|
|
1760
|
+
if (target !== null) {
|
|
1761
|
+
recordDecision(ctx, { kind: "drop-reindex-symlink", rel: rootRel, target });
|
|
1735
1762
|
}
|
|
1736
1763
|
return;
|
|
1737
1764
|
}
|
|
1738
1765
|
// Top-level regular file.
|
|
1739
1766
|
if (lst && lst.isFile()) {
|
|
1740
|
-
|
|
1767
|
+
recordDecision(ctx, classifyOne(ctx, rootRel));
|
|
1741
1768
|
return;
|
|
1742
1769
|
}
|
|
1743
1770
|
// Top-level directory: recursive walk, pruning node_modules + nested .git.
|
|
1744
1771
|
for (const rel of findFilesAndSymlinks(rootAbs, ctx.hqRoot)) {
|
|
1745
|
-
|
|
1772
|
+
recordDecision(ctx, classifyOne(ctx, rel));
|
|
1746
1773
|
}
|
|
1747
1774
|
}
|
|
1748
1775
|
/**
|