/** * Attachment store repair routine — janitor-safe, dry-run capable. * * Detects and resolves two classes of orphan that can arise from crashes or * prior ordering bugs (pre-T11997): * * (1) **row-without-file** — an `attachments` row exists but the blob file * on disk is absent. The row is MARKED (lifecycleStatus → 'archived', * summary updated) rather than deleted — metadata is preserved for * url-kind rows that are re-fetchable. Amendment 2 (adversarial review). * * (2) **file-without-row** — a blob file on disk has no corresponding row in * the `attachments` table AND is not referenced by any of the three doc * storage surfaces (tasks.db attachments, blobs manifest.db, and * docs-publications.json). Eligible for deletion only after the grace * period elapses. Amendment 3 (adversarial review). * * Amendment 1 (config restore): handled separately in config-repair.ts via * `repairConfigFile`. * * Output contract: * - Silent by default — NO console output; callers read the returned result. * - Writes one JSONL line per action to `.cleo/audit/attachment-repair.jsonl`. * - Returns a structured {@link RepairResult} so the janitor (T11995) can * report counts without parsing the audit log. * - Converges on repeated runs: a fully-healthy store produces zero actions. * * @task T11997 * @epic T11992 */ /** One action taken (or that would be taken in dry-run) by the repair routine. */ export interface RepairAction { /** What happened. */ readonly kind: 'mark-row-without-file' | 'delete-unreferenced-blob' | 'skip-grace-period' | 'skip-referenced-blob'; /** SHA-256 of the affected blob (64 hex chars, or '' when n/a). */ readonly sha256: string; /** Attachment row ID, when the row exists. */ readonly attachmentId?: string; /** Absolute path to the on-disk blob file, when applicable. */ readonly filePath?: string; /** Human-readable reason. */ readonly reason: string; } /** Structured result of {@link repairAttachmentStore}. */ export interface RepairResult { /** * Whether any mutations were (or would be) made. * `false` when the store is fully healthy or `dryRun` had nothing to fix. */ readonly mutated: boolean; /** `true` when this was a dry-run invocation (no writes performed). */ readonly dryRun: boolean; /** Rows-without-files found (and marked, or would-mark). */ readonly rowsWithoutFilesCount: number; /** Unreferenced blob files deleted (or would-delete). */ readonly unreferencedBlobsDeletedCount: number; /** Blobs skipped because they are still within the grace period. */ readonly gracePeriodSkipCount: number; /** Blobs skipped because they appear in a secondary reference surface. */ readonly referencedSkipCount: number; /** All actions, in order. */ readonly actions: readonly RepairAction[]; } /** Options for {@link repairAttachmentStore}. */ export interface RepairOptions { /** * When `true`, analyse the store and return what would happen but do NOT * write any changes. Default: `false`. */ readonly dryRun?: boolean; /** * Minimum age in milliseconds before an unreferenced on-disk blob is * eligible for deletion. Default: 5 minutes (300 000 ms). * * The grace period exists to avoid racing against an in-flight `put` that * has written the file but not yet committed the row. */ readonly gracePeriodMs?: number; /** * Working directory (project root) for resolving `.cleo/` paths. * Defaults to `process.cwd()`. */ readonly cwd?: string; } /** * Scan the attachment store for orphans and optionally repair them. * * This function is designed for janitor consumption (T11995). Call it with * `dryRun: true` first to preview what would change, then without `dryRun` to * apply. The function is idempotent: running it repeatedly on a healthy store * returns `{ mutated: false, actions: [] }`. * * @param opts - Repair options (see {@link RepairOptions}). * @returns Structured {@link RepairResult} — no console output. * * @example * ```ts * import { repairAttachmentStore } from '@cleocode/core/store/attachment-repair'; * * const result = await repairAttachmentStore({ dryRun: true }); * if (result.rowsWithoutFilesCount > 0) { * console.log(`${result.rowsWithoutFilesCount} rows would be marked`); * await repairAttachmentStore({ dryRun: false }); // apply * } * ``` * * @task T11997 */ export declare function repairAttachmentStore(opts?: RepairOptions): Promise; //# sourceMappingURL=attachment-repair.d.ts.map