import type { SqliteDatabase } from '../db/driver.js'; import type { UsageFilter, UsageRepository } from '../db/repositories/usage-repository.js'; import type { UsageRecord } from '../models/usage-record.js'; /** * Keeping the database from growing forever, and merging two machines into one. * * Three deliberate shapes here: * * 1. **Pruning is a command, not a policy.** A retention setting that silently * deleted last quarter on some future run is a worse tool than one that * never deletes: the data is gone and nothing asked. `prune` is explicit, * dry-run by default, and can be put in a cron by someone who wants a policy. * 2. **Dry run is the default.** The only irreversible operation in this * package reports what it would remove and changes nothing until told twice. * 3. **Import merges rather than replaces.** Record ids are derived * deterministically from source identifiers, so the same turn imported twice * -- or collected on both machines -- upserts to one row. That is what makes * "laptop plus desktop" answerable at all. */ export interface PruneResult { cutoff: string; /** Records matched. With `apply: false` this is what WOULD be removed. */ matched: number; removed: number; applied: boolean; remaining: number; } export interface VacuumResult { /** * Total on-disk footprint before and after: the database file PLUS its `-wal` * and `-shm` companions. * * Measuring only the `.db` is actively misleading under WAL, which is the mode * this package always opens in. Freshly written data lives in the `-wal` until * a checkpoint folds it back, so a 1.8MB database can show a 4KB `.db` -- and a * vacuum would report reclaiming nothing while most of the bytes sat next door. * * Absent when the database is in memory, which has no files to measure. */ bytesBefore?: number; bytesAfter?: number; reclaimedBytes?: number; } export interface ImportResult { /** Lines read from the file, excluding blanks. */ read: number; /** Rows that parsed and validated. */ accepted: number; /** Rows written. Equal to `accepted`; re-importing the same rows changes no total. */ written: number; /** Rows rejected, with the line number and why. Never silently dropped. */ rejected: { line: number; reason: string; }[]; /** Records in the database before and after, so a merge's effect is visible. */ recordsBefore: number; recordsAfter: number; } /** * Turns one exported row back into a record, or says why it cannot. * * Strict on purpose: a row that half-parses would import a turn with invented * zeroes, and a merged database that quietly under-counts is worse than a failed * import. Rejections carry a line number so the offending row can be found. */ export declare function parseExportedRecord(row: unknown): { record: UsageRecord; } | { reason: string; }; export declare class LifecycleService { private readonly repo; private readonly db; private readonly dbPath; constructor(repo: UsageRepository, db: SqliteDatabase, dbPath: string); /** * @param apply Must be explicitly true to delete anything. The default reports * what would go and touches nothing. */ prune(cutoff: string, options?: { apply?: boolean; filter?: UsageFilter; }): PruneResult; /** * Compacts the file. Deleting rows frees pages inside the database but does * not shrink it, so a prune that reclaims no disk is a prune that looks like * it did nothing. */ vacuum(): VacuumResult; /** Merges JSON Lines produced by `ai-usage export --format jsonl`. */ import(lines: Iterable): ImportResult; /** The database's real footprint: the file plus its WAL companions. */ private fileSize; }