/** * Conflict index — durable record of pending divergences awaiting resolution. * * Lives at `/.hq-conflicts/index.json` (inside HQ content, NOT in * `~/.hq/`). Two reasons it sits in HQ content rather than in the state dir: * 1. The `/resolve-conflicts` HQ skill discovers it relative to the user's * HQ folder — that's the user's mental anchor for "where my files are." * 2. The conflict-side files themselves live in HQ content, so the index * and the files it references stay co-located. If the user moves HQ, * the index moves with it. * * Excluded from cross-machine sync via `.hqignore` — each machine resolves * its own queue. We never propagate conflict files (they'd just create more * conflicts on the other side). * * Writes are atomic (tmp + rename). The resolution skill mutates this file * mid-walk; a torn write would corrupt the only record of pending conflicts * and could lose track of files we'd written to disk. Higher stakes than the * journal, which the next sync can rebuild. */ import type { ConflictIndex, ConflictIndexEntry } from "../types.js"; /** * Absolute path to the conflict index for a given HQ root. */ export declare function getConflictIndexPath(hqRoot: string): string; /** * Read the conflict index. Returns an empty index if the file doesn't exist * yet (first-conflict-ever case). * * Throws on corrupt JSON — we deliberately don't auto-repair, since the only * record of pending conflicts is too important to silently overwrite. The * `/resolve-conflicts` skill surfaces this case to the user with a manual * inspection prompt. */ export declare function readConflictIndex(hqRoot: string): ConflictIndex; /** * Atomically write the conflict index. Writes to `.tmp.` then * renames into place — `rename(2)` is atomic on POSIX, so a crash mid-write * leaves either the old file or the new one, never a half-written one. * * Always sorts conflicts by `detectedAt` ascending before writing — keeps * the file diff-friendly across runs and makes "oldest-first walk" the * natural read order in the resolution skill. */ export declare function writeConflictIndex(hqRoot: string, index: ConflictIndex): void; /** * Idempotent append. If an entry with the same `id` already exists (same * original path, same detection timestamp), update it in place rather than * duplicating. This matters because re-running sync after a conflict but * before resolution will re-detect the same divergence — without dedup the * index would grow unboundedly. * * The "update in place" path also covers the case where the cloud advanced * again between detections: we want the latest `remoteVersionId` and * `remoteHash` so the resolution skill shows the user the *current* cloud * state, not stale data from the first detection. */ export declare function appendConflictEntry(hqRoot: string, entry: ConflictIndexEntry): void; /** * Remove an entry by id. Used by the `/resolve-conflicts` skill after the * user picks a resolution and the conflict file is cleaned up. No-op if the * id isn't present (e.g. user manually removed the file then re-ran the * skill — we want that to be a clean exit, not an error). */ export declare function removeConflictEntry(hqRoot: string, id: string): void; /** Summary of what a {@link pruneConflictIndex} pass reclaimed. */ export interface PruneConflictIndexResult { /** Rows dropped because their `.conflict-*` mirror no longer exists. */ prunedOrphans: number; /** Rows dropped because the original file and its mirror are byte-identical. */ prunedIdentical: number; /** Byte-identical mirror files deleted from disk during the pass. */ removedMirrors: number; /** Rows kept (genuine divergence, or unprovable — fail-safe retained). */ kept: number; /** * Up to 10 ORIGINAL (non-mirror) paths of the kept rows, for a * post-sync reconcile surface (`conflicts-remaining`). Empty when * `kept === 0`. Lets a caller name the preserved conflict variants a human * still has to resolve without re-reading the index. feedback_d2082110. */ keptSamplePaths: string[]; } /** * Garbage-collect the conflict index so it self-heals instead of growing * without bound. Before this pass existed the ledger only ever shrank when a * human resolved a conflict via `/resolve-conflicts`; every false positive and * every orphaned row lingered forever, so the index over-reported the real * pending-conflict count (the menubar's journal-derived count stayed correct, * but `.hq-conflicts/index.json` did not). * * Two classes of entry are dropped — both provably not a pending conflict: * * 1. **Orphaned** — the `.conflict-*` mirror file is gone from disk. The * mirror is the only artifact a human resolves against; once it's missing * the row can never be acted on, so it's pure litter (the "missing-cloud" * rows operators reported climbing into the dozens). * * 2. **Byte-identical false positives** — the original file and its conflict * mirror both still exist and are byte-for-byte identical. The mirror * holds the remote bytes captured at detection time, so identical bytes * mean there was never a real divergence (this is exactly the manual * safe-purge operators have been doing by hand). The stale mirror file is * deleted and the row dropped. * * Conservative by construction — an entry is kept whenever it might be a real * conflict: the original file is missing (a genuine local-delete-vs-remote * divergence), the bytes differ, or either side can't be read. The index file * is only rewritten when at least one row is actually dropped, so a clean * ledger keeps its mtime untouched. */ export declare function pruneConflictIndex(hqRoot: string): PruneConflictIndexResult; //# sourceMappingURL=conflict-index.d.ts.map