import { type SHA256Hash } from "./core/hash.js"; /** Lock mode: never touch the lock / verify-only (CI) / record-and-write (local). */ export type LockMode = "off" | "check" | "update"; /** * Per-spec lock overrides (additive on `EvalSpec`/`TriggerRateSpec`). Normally the * mode comes from the CLI (`eval --check`/`--update` → `VIGILES_EVAL_LOCK`) and * the dir/epoch from defaults/config; set these to drive the lock programmatically * (or to point a test at a throwaway dir). Each field falls back to its env/default. */ export interface EvalLockOptions { /** Override the lock mode (else `VIGILES_EVAL_LOCK`, else `off`). */ readonly mode?: LockMode; /** Override the lock directory (else `/.vigiles/eval-locks`). */ readonly dir?: string; /** Override the behavior epoch (else `VIGILES_EVAL_API_VERSION`, else 1). */ readonly evalApiVersion?: number; } /** * On-disk lock-format version, salted into nothing (the lock is keyed by name, * not by hash) but VALIDATED on read so an incompatible shape fails loud rather * than deserializing into a stale structure. Bump on a breaking shape change. */ export declare const LOCK_VERSION = 1; /** Default directory for committed eval locks (tracked, NOT gitignored). */ export declare const DEFAULT_LOCK_DIR = ".vigiles/eval-locks"; /** * The model-affecting inputs hashed into a lock's `inputsHash`. Everything here * is something that, if it changes, means the recorded model behavior is stale * and you MUST re-drive the model (→ subscription → local). Deliberately EXCLUDED: * the scoring `measure`/assertions (re-run live against the replayed report), the * trial count (a sample-size knob, not a behavior input), and per-run env noise. */ export interface EvalLockInputs { /** Model id used (folded in; a floating alias can't detect weight drift — warned). */ readonly model: string; /** * Reasoning budget (`--effort`) the run was pinned to, or undefined for the * harness default. Hashed because it steers the model — the criterion this * interface already states — so a committed report recorded at one effort is * STALE for a run at another. `undefined` is dropped by `JSON.stringify`, so * locks committed before effort existed keep their hash and still replay. * * Caveat kept honest: "omitted" means the harness's own default, which is * per-model and can move between builds — reproducible only modulo that, the * same class of provenance caveat as `harnessVersion` below. */ readonly effort?: string | number; /** * A hand-bumped behavior epoch the project owns (`.vigilesrc.json` * `eval.apiVersion`), bumped when a harness-side change YOU made (a CLAUDE.md * edit, a global hook) would shift eval outputs but isn't otherwise in the * inputs. The escape hatch for "force a re-eval." */ readonly evalApiVersion: number; /** * The seam-specific canonical input object — the tasks/prompts/files/settings/ * sorted-tools/pluginDirHash/serialized-checks that steer the model. Assembled * by each entry point (it knows its own shape) and hashed opaquely here. */ readonly inputs: unknown; } /** * Why the harness binary version is **NOT** hashed (only recorded as provenance): * `--check` runs in CI where `claude` is PINNED to a fixed version, while a dev's * local `claude` is whatever they have — folding the version into the hash would * false-trip `--check` on every PR where those differ. It is also the lock's * honest scope: the gate verifies your committed results match your current * *author-controlled inputs*, not current model/harness behavior (there is no * automated live run). Harness/model drift is caught when YOU re-run `--update` * locally and review the moved numbers in the git diff. Keeping the version out * of the hash is what lets `--check` stay binary-free + deterministic in CI. * (The eval CACHE still keys on it — that's local replay soundness, a different * axis.) See research/cache-invalidation.md. */ /** Deterministic content hash of a lock's model-affecting inputs. */ export declare function evalInputsHash(input: EvalLockInputs): SHA256Hash; /** A committed eval lock: the integrity stamp + the replayable recorded report. */ export interface EvalLock { readonly version: number; /** The eval's report name (human-facing; also the lock filename slug source). */ readonly name: string; /** Hash of the model-affecting inputs ({@link evalInputsHash}). */ readonly inputsHash: string; /** The model id the report was produced against (for the drift warning). */ readonly model: string; /** * The effort the report was produced at, or undefined for the harness default * (provenance; already in the hash). Recorded because the complaint that * motivated effort support was not only that it could not be SET — it was that * nothing in the run record said which effort produced the numbers. */ readonly effort?: string | number; /** The harness version token at record time (provenance; already in the hash). */ readonly harnessVersionKey: string; /** The behavior epoch at record time (provenance; already in the hash). */ readonly evalApiVersion: number; /** ISO-8601 timestamp the lock was recorded (provenance; NOT in the hash). */ readonly builtAt: string; /** * The entry point's recorded report — the model's observed behavior, REPLAYED * verbatim on `--check` so the script's own assertions judge it. Stored as the * exact return type of the entry point (`EvalReport` / `TriggerRateReport` / * `CheckReport`) so replay is transparent to the caller. */ readonly report: unknown; } /** Filesystem-safe slug for a report name (the lock filename). */ export declare function lockSlug(name: string): string; /** Path to a named eval's lock file under `dir`. */ export declare function lockPath(dir: string, name: string): string; /** * Read a named eval's lock. A MISS (no file) returns `null`. A CORRUPT or * wrong-version file **throws** — a broken lock is a real failure the CI gate * must surface, not silently treat as "no lock" (which would let a stale eval * pass). The message says how to recover. */ export declare function readLock(dir: string, name: string): EvalLock | null; /** * Whether ANY lock has been committed under `dir`. `eval --check` fails when none * is (#197): with no lock there is no recorded result to verify the current inputs * against, so a pass would mean nothing was checked. Once the first lock is * committed, every named eval is held to having a fresh one (a new unlocked eval * then reads as stale). The edit-time nudge also stays silent until then. */ export declare function anyLocksCommitted(dir: string): boolean; /** * How many locks are committed under `dir` — 0 when the dir does not exist. * * No try/catch around the read: `anyLocksCommitted` never had one, and adding a * branch here that no test can reach would trade a real coverage line for a * defensive case the caller already survives (an unreadable `.vigiles/` is a * broken checkout, not a state this counter should paper over). */ export declare function countLocks(dir: string): number; /** * Does an edited path plausibly change an eval's INPUTS — so a committed lock may * now be stale? Two surfaces feed the hash: a skill's trigger surface (`SKILL.md`) * and the eval script that holds the prompts/spec (`*.eval.{mjs,cjs,js,mts,cts,ts}`). * Pure (string-only) so the nudge hook stays cheap and never runs an eval script. */ export declare function isEvalInputFile(path: string): boolean; /** * The NON-BLOCKING nudge to emit after an eval-input edit when committed locks * exist, or `null` for no nudge. Self-gating: it stays silent until you've opted * into the lock (committed one), so it can't annoy a repo that doesn't use evals. * It deliberately does NOT recompute staleness (that needs the eval script + is * the job of `eval --check`) — a reminder, not a gate. The honest harness-neutral * reminder; how it reaches the agent (both CC and Codex inject `additionalContext` * on `PostToolUse`) is the caller's concern. See docs/harness-testing-*.md. */ export declare function evalLockNudge(filePath: string, lockDir: string): string | null; /** Write a named eval's lock (pretty JSON for a reviewable git diff). */ export declare function writeLock(dir: string, lock: EvalLock): void; /** * Build a fresh lock envelope from a just-recorded report (the `--update` write). * `builtAt` is passed in (never read from the clock here) so the module stays * pure + deterministically testable; the CLI stamps the real timestamp. */ export declare function buildLock(args: { readonly name: string; readonly inputsHash: string; readonly model: string; readonly effort?: string | number; readonly harnessVersionKey: string; readonly evalApiVersion: number; readonly builtAt: string; readonly report: unknown; }): EvalLock; /** What the lock layer decides an entry point should do for this run. */ export type LockDecision = /** Drive the model normally (mode `off`, or `update`, or `check` with no lock-skip). */ { readonly kind: "run"; } /** `check` + a matching fresh lock → return the recorded report, NO model call. */ | { readonly kind: "replay"; readonly report: unknown; } /** `check` + a missing/stale lock → fail; the caller throws `reason`. */ | { readonly kind: "stale"; readonly reason: string; }; /** * Decide what `check` mode should do given the current input hash and the * committed lock. `off`/`update` always `run` (update records afterwards). `check` * replays a matching lock (no model) and is `stale` on a missing lock or a hash * mismatch — the deterministic CI gate. */ export declare function decideLock(mode: LockMode, name: string, currentHash: string, existing: EvalLock | null): LockDecision; /** A single numeric leaf that moved between the prior lock and a fresh `--update`. */ export interface NumberDelta { readonly path: string; readonly before: number; readonly after: number; } /** * Collect the numeric leaves that changed between two recorded reports — the * human-facing delta printed at `--update` time (e.g. `rate: 0.900 → 0.650`). * Generic over any report shape (walks numbers by dotted path), so it works for * `EvalReport`, `TriggerRateReport`, and `CheckReport` without per-type code. The * committed git diff is the primary review surface; this is the at-a-glance echo. */ export declare function diffReportNumbers(before: unknown, after: unknown): NumberDelta[]; /** Render the `--update` result for a human: NEW lock, or the per-number deltas. */ export declare function formatLockUpdate(name: string, deltas: readonly NumberDelta[], isNew: boolean): string; /** * Read the lock mode from the environment (`VIGILES_EVAL_LOCK`), set by the CLI's * `eval --check` / `--update` flags. A run knob (like `VIGILES_TRIALS`): the CLI * is the only place that should set it. Anything unrecognized → `off`. */ export declare function lockModeFromEnv(env?: NodeJS.ProcessEnv): LockMode; /** * The behavior epoch (`evalApiVersion`) for this run, read from the env the CLI * populates from `.vigilesrc.json` `eval.apiVersion`. Default 1. A malformed * value falls back to 1 (never throws) — the lock stays usable. */ export declare function evalApiVersionFromEnv(env?: NodeJS.ProcessEnv): number; //# sourceMappingURL=eval-lock.d.ts.map