/** * Legacy conflict-twin reconciliation (`hq sync doctor --reconcile-conflicts`). * * Until the version-aware conflict engine landed, every unresolved sync * conflict parked one side of the file as a sibling "twin" — * `.conflict--.` — next to the live path, and the * winner was whichever side the caller's `--on-conflict` flag named, never * the newer body. Vaults therefore hold twins whose bytes are strictly newer * than the live file (a policy at frontmatter `version: 10` parked beside a * live `version: 8`). * * This pass walks the HQ root for those legacy twins, groups them per live * path, and decides ONCE per live file: * * - a twin is promoted to the live path ONLY when its frontmatter * `version:` is strictly higher than the live file's — and only the * highest-versioned such twin. Every other twin (lower/equal version, no * version at all, byte-identical) is moved OUT of the tree into * `.hq/conflict-backups/` (sync-ignored) so nothing is destroyed, and no * twin ever remains beside the live file. A twin's disk mtime is its * DETECTION time, not its content's age, so mtime is never evidence for * promotion: a no-`version:` twin (prd.json, board.json, .jsonl, …) can * never overwrite the live body; * - with no live sibling, the twin is NOT restored: the usual cause is a * live file that was deliberately deleted (locally or via a synced * tombstone) while the sync-ignored twin was left behind, and restoring * it would resurrect the deletion fleet-wide on the next push. The twin * is parked under `.hq/conflict-backups/` and listed for manual review; * - drops every `.hq-conflicts/index.json` row that referenced the twin. * * Backups never overwrite each other: each is named from the twin's own * detection timestamp + machine token and suffixed `-1`, `-2`, … when that * name is already taken. * * Dry-run is the DEFAULT: the plan is returned (and optionally logged) and * nothing is written until `yes: true`. Symlink twins and unreadable files are * surfaced for manual review, never touched. Per-item failures are warnings, * never fatal. */ import { type ConflictDecision } from "../cli/conflict.js"; export type ReconcileTwinAction = /** Twin body wins (strictly higher version): it replaces the live file; the old live body is backed up. */ "promote-twin" /** Live body wins: the twin is moved out of the tree into the backup dir. */ | "keep-live" /** No live sibling exists: the twin is parked in the backup dir and listed for manual review, never restored. */ | "park-orphan" /** Left untouched (symlink, unreadable, …) — surfaced for a human. */ | "manual-review"; export interface ReconcileTwinPlanItem { /** hq-root-relative canonical key of the twin file. */ twinPath: string; /** hq-root-relative canonical key of the live file the twin shadows. */ livePath: string; action: ReconcileTwinAction; /** Winner decision for the two-sided cases (`local` = live, `remote` = twin). */ decision?: ConflictDecision; reason: string; } export interface ReconcileManualReviewItem { twinPath: string; livePath: string; action: ReconcileTwinAction; reason: string; /** Where the body now sits (set once a `park-orphan` has been applied). */ backupPath?: string; } export interface ReconcileTwinsResult { plan: ReconcileTwinPlanItem[]; /** False on dry-run (the default). */ applied: boolean; /** Twins whose body now sits at the live path (strictly higher version). */ promoted: number; /** Twins moved out of the tree because the live body won. */ removed: number; /** Orphan twins (no live sibling) parked under `.hq/conflict-backups/`. */ orphansParked: number; /** Bodies parked under `.hq/conflict-backups/` (displaced live bodies, losing twins, orphans). */ backedUp: number; /** `.hq-conflicts/index.json` rows dropped because their twin was reconciled. */ indexRowsDropped: number; /** Non-fatal per-item warnings emitted during plan + apply. */ warnings: number; /** Items an operator must look at: untouched twins and parked orphans. */ manualReview: ReconcileManualReviewItem[]; } export interface ReconcileTwinsOptions { hqRoot: string; /** Apply the plan. Default false = dry-run. */ yes?: boolean; /** Plan/progress sink; defaults to silent. */ log?: (line: string) => void; /** Warning sink; defaults to `log`. */ warn?: (line: string) => void; } /** * Build the reconcile plan. Pure planning — nothing on disk is touched. * Twins are grouped per live path so at most ONE twin per live file is * promoted (the highest strictly-greater frontmatter version). */ export declare function planLegacyConflictTwins(hqRoot: string, warn?: (message: string) => void): ReconcileTwinPlanItem[]; /** * Plan and (with `yes`) apply legacy-twin reconciliation. See module doc. */ export declare function reconcileLegacyConflictTwins(options: ReconcileTwinsOptions): ReconcileTwinsResult; //# sourceMappingURL=conflict-reconcile.d.ts.map