/** * Cost recorder: orchestrates capture from provider extraction to ledger * appends. Owns all attribution — providers stay platform-pure (DD3). * * Flow (design §4.1): validate input → acquire per-session lock → read * newest cursor (instance ledgers + unattributed) → provider.extract → * split segments by claim intervals × phase transitions → merge per * (instance, phase, model) → validate + append. The ledger append is the * atomic commit point (DD1); re-runs after a crash re-extract from the * last committed cursor. * * Failures append structured lines to .aidlc/cost/capture.log — never raw * transcript or ledger content (SR1). */ import { type ProviderEnv } from './providers/provider.js'; import type { RecordInput } from './types.js'; export declare function captureLogPath(projectRoot: string): string; /** Whether the capture path has ever executed, and when it last did. */ export interface CaptureLiveness { /** capture.log exists and holds at least one parseable line. */ everRan: boolean; /** Newest capture.log timestamp, or null when it has never run. */ lastAt: string | null; } /** * Liveness of the capture path (cost-capture-goes-dark/AC-10). * * "No cost data" has two very different causes and reports used to render them * identically: no usage occurred, or capture never ran. The second is the one * that cost this project its cost history — a hook whose frozen absolute paths * had gone stale could not execute, so it wrote nothing at all, not even an * error. **The absence of capture.log is therefore itself the signal**: every * capture attempt logs, including the failures, so a missing log means nothing * ever tried. * * Cheap by construction — one stat and one read of a file capped at 256 KB — so * it is safe to consult on every report. */ export declare function captureLiveness(projectRoot: string): CaptureLiveness; export declare function logCapture(projectRoot: string, stage: string, errorCode: string, counts?: Record): void; interface CaptureLogLine { timestamp: string; stage: string; error_code: string; } /** * The capture error worth showing in `aidlc cost`, or null when the pipeline * looks healthy (FR3.10). * * "Worth showing" is narrower than "most recent non-ok line", because a note * that never clears is a note users learn to ignore. Three filters apply: * * 1. Benign codes (`source-missing`, `ok`) are not failures at all. * 2. An error older than the newest `ok` line has been superseded — capture has * demonstrably worked since. * 3. An error older than the retention window is stale regardless, so a project * that simply stopped capturing doesn't carry a note forever. */ export declare function lastCaptureError(projectRoot: string, now?: Date): CaptureLogLine | null; /** * Capture-log lines older than the retention window, as line counts kept vs * dropped. Read-only half of pruneCaptureLog, shared with `aidlc doctor`. */ export declare function expiredCaptureLogLines(projectRoot: string, now?: Date): number; /** * Drop capture-log lines older than the retention window. Returns the number * removed. Unparseable lines are dropped too — they carry no usable timestamp * and cannot be surfaced (AC-13). */ export declare function pruneCaptureLog(projectRoot: string, now?: Date): number; /** * Lock files past the staleness threshold (unparseable ones count stale, * mirroring acquireLock's steal logic). Read-only half of * removeStaleLocks, shared with `aidlc doctor` (update-spring-clean AC-19). */ export declare function staleLockFiles(projectRoot: string, now?: Date): string[]; /** Remove stale lock files. Returns the removed filenames (AC-19). */ export declare function removeStaleLocks(projectRoot: string, now?: Date): string[]; /** Try to take the per-session lock. Returns a release fn, or null if held. */ export declare function acquireLock(projectRoot: string, sessionId: string): (() => void) | null; interface PhaseWindow { phase: string; from: number; /** Exclusive upper bound; Infinity for the current phase. */ to: number; } /** * Build phase windows for an instance from its transitions.log. * * This read was silently broken from the first commit: it took * `t as unknown as { timestamp: string; ... }` and skipped any record without * `rec.timestamp`, while `TransitionRecord` declares the field as `at`. Every * transition was therefore skipped, the loop body never ran, and the function * returned a single window covering all time — and since all four call sites seed * `initialPhase` with `'implementation'`, **every priced record in the project was * labelled `implementation`**. Measured: 18 of the 19 instances on the canonical * field had 100% of their priced usage mislabelled, and the only correct ledgers * were the three legacy instances the stale reader happened to match. The cast is * what silenced the compile error that would have caught this, so it is gone * (turn-scoped-cost/AC-1, AC-3). * * A record with `to: null` closes the preceding window without opening a new one: * five instances end that way, so counting it would produce a phantom trailing * phase (AC-41). `complete` is left as a legitimate label — it passes `PHASE_RE` * and already appears in ledgers, so normalising it here would quietly move money * between rows. */ export declare function phaseWindows(projectRoot: string, instance: string, initialPhase: string): PhaseWindow[]; /** All instances that have session events mentioning this session. */ export declare function instancesForSession(projectRoot: string, sessionId: string): string[]; export interface RecordResult { appended: number; unattributed: number; instances: string[]; } export interface RecordOptions { projectRoot: string; provider: string; input: RecordInput; claimTimeoutMinutes: number; env?: ProviderEnv; now?: Date; } /** * Run one capture pass for a session. Returns counts, or null when the * lock is held elsewhere (the holder re-checks before releasing, so a * firing lost to the lock is still captured — design §4.2). */ export declare function recordUsage(opts: RecordOptions): Promise; /** * Total milliseconds covered by a set of intervals, counting overlap once. * * A union, not a sum, and the difference is not academic: records that share a * parent produce overlapping spans, so summing them over-counts by 10.8% on this * repo's corpus (27.76h summed against a 25.05h true union). Summing per-bucket * unions still over-counts the global union by ~0.9%, because most residual overlap * is *across* model buckets rather than within one — that residual is accepted and * documented rather than hidden, since it is what keeps the stored value additive * across phases and instances (turn-scoped-cost/AC-19, AC-44). */ export declare function unionMs(intervals: Array<{ from: number; to: number; }>): number; export {}; //# sourceMappingURL=recorder.d.ts.map