import { type SHA256Hash } from "./core/hash.js"; import type { RunOut } from "./eval.js"; import type { StubCall } from "./core/stub-rules.js"; /** Cache behaviour: never touch the cache / read-only / read-and-write. */ export type CacheMode = "off" | "read" | "readwrite"; /** Everything that determines a trial's model output (the cache key inputs). */ export interface CacheKeyInput { readonly task: string; readonly model: string; /** * Reasoning budget (`--effort`). Keyed for the same reason `model` is: it moves * the output distribution, so a replay across effort levels would serve a result * the caller did not ask for. `undefined` (the harness default) drops out of the * hash via JSON, so entries recorded before effort existed stay valid. */ readonly effort?: string | number; readonly tools: readonly string[]; /** The resolved fixture + arm + plugin files written before the run. */ readonly files: Record; /** The resolved `.claude/settings.json` for the arm (or undefined). */ readonly settings: unknown; /** * Per-run env that affects model behaviour (e.g. `VIGILES_INTERCEPT_TOOLS`). * Keyed because two intercept configs that share tool names — so produce * identical merged `settings` — still differ in their `when`/`denyReason`, which * lives only in the env. Omit when there's no model-affecting env. */ readonly env?: Record; /** * Content digest of a natively-installed plugin dir (`--plugin-dir`), or * undefined when there is none. Folded in so editing a skill INSIDE the dir * invalidates the entry — a path-only key would false-replay, since the dir's * files are NOT in `files` (that holds only the materialized fixture / `plugin` * arm, not a native install). See {@link hashDir}. */ readonly pluginDirHash?: string; /** * The run's tool stubs, each ENCODED (`encodeStub` — a RegExp token as its * source and flags, since `JSON.stringify(/x/)` is `{}`). A changed answer is * a changed input. Omit when there are none. */ readonly stubs?: readonly string[]; /** * Where the run executes: the env kind and an ephemeral run's seed — never the * per-trial HOME path. A seed file is part of what the agent sees. */ readonly runEnv?: unknown; /** * The harness BINARY version (e.g. `claude --version`). The harness evolves * fast — a CLI upgrade changes the system prompt + tool definitions, which steer * behaviour as much as the model does — so a cached result must invalidate when * the binary changes, or a replay silently serves a result from a different * harness. Resolved once per run; omit when unknown (then it doesn't partition). */ readonly harnessVersion?: string; /** Which trial this is — distinct trials are distinct samples, cached apart. */ readonly trialIndex: number; } /** A recorded trial: its raw output plus the post-run cwd snapshot. */ export interface CacheRecord { readonly out: RunOut; /** Text files present in the cwd after the run (relative path → contents). */ readonly files: Record; /** * The run's stub calls. The stub log lives beside the work dir, not in it, so * the cwd snapshot does not hold it; a replay must report the same calls — * unanswered ones included. Absent when the run had no stubs. */ readonly stubCalls?: readonly StubCall[]; } /** * Canonicalize a value so the key is stable regardless of object key order — * recursively sorts object keys. Arrays keep order (it's significant for tools). * Exported so the eval LOCK ({@link ./eval-lock}) hashes its inputs the same way. */ export declare function canonical(value: unknown): unknown; /** * Cache record-format version, SALTED into every key (Jest `CACHE_VERSION` / * webpack `cache.version` pattern). Bump when the `CacheRecord` shape — or how a * record is produced in a way the key can't otherwise see — changes, so old * entries become *unreachable* rather than deserializing into a stale shape (no * brittle read-time version gate needed). A major bump means orphaned files on * disk; reclaim them by deleting the cache dir. */ export declare const CACHE_FORMAT_VERSION = 3; /** Deterministic content hash of the key inputs (order-independent). */ export declare function cacheKey(input: CacheKeyInput): SHA256Hash; /** * Read a cached record by key. A MISS (no file) returns `null` — normal, the run * proceeds. A CORRUPT record (file present but not valid JSON) **throws** instead * of silently degrading to a re-run: a broken cassette is a real failure the CI * gate must surface, not mask. The message tells you how to recover. */ export declare function readCache(dir: string, key: SHA256Hash): CacheRecord | null; /** * Write a cached record by key (creating the cache dir as needed). When `dir` is * the default `.vigiles/eval-cache`, the recorded runs are per-checkout, so the * `.vigiles/.gitignore` is kept current; a `cacheDir` the spec chose elsewhere is * the author's to place and is left alone. */ export declare function writeCache(dir: string, key: SHA256Hash, record: CacheRecord): void; /** Snapshot the text files under `cwd` as `relativePath → contents` (bounded). */ export declare function snapshotDir(cwd: string): Record; /** Restore a snapshot into `cwd`, recreating directories as needed. */ export declare function restoreDir(cwd: string, files: Record): void; /** * Content digest of a directory: a lexicographically-sorted list of * `relativePath:contentHash` for every file, hashed to one value. Editing, * adding, removing, or moving any file changes the digest. It hashes file * CONTENT (not mtime — CI checkouts reset mtimes, the classic stale-cache * anti-pattern) and includes the relative path (so a rename invalidates and two * files can't swap contents undetected). A flat sorted list, NOT a Merkle tree — * sufficient at plugin-dir scale; the tree's incremental-recompute payoff isn't * worth the complexity here (cf. Bazel/Turborepo hash content per file). */ export declare function hashDir(dir: string): SHA256Hash; //# sourceMappingURL=eval-cache.d.ts.map