import type { ResolvedConfig, ShotRecord } from "../types.js"; import type { VerifiedFinding } from "./verify.js"; export interface LedgerEntry { verdict: "clean" | "findings"; /** * Post-verification findings, whole. `verified` is stored rather than assumed: * a `--no-verify` run records findings the refuter never saw, and medium and * low findings are never refuted at all, so a cache hit that stamped them * verified would report a check that did not happen. */ findings?: VerifiedFinding[]; /** Which panel's verdict this is, so a stale entry is readable when debugging. */ panel: string; /** Member shot ids, same purpose. */ shotIds: string[]; judgedAt: string; runId: string; /** * The panel's reply, whole, as a file under the capture workspace * (relative to it). A pointer rather than the text: the ledger is committed * by projects that want cheap re-runs, and a transcript is page text. */ reply?: string; } export interface Ledger { note: string; entries: Record; } export declare function ledgerPath(resolved: ResolvedConfig): string; /** * Hash of a whole view group: every member's pixel hash, sorted by shot id so * capture order cannot perturb it. One member changing changes the group hash. * * A member carrying a design reference contributes the hand-off image's hash * too: the judge compares the build against that image, so swapping the file * is a changed input even when the app's pixels held still. Conditional on * purpose, so groups without designs keep the hashes they have always had. * * The accessibility tree is the same argument for the two panels given it: a * cached anatomy or content verdict must not outlive the evidence it was formed * with. It is conditional twice over, on the panel asking and on the shot * having one, so a group hashes exactly as it always did for every panel that * is not shown a tree. */ export declare function groupHash(shots: ShotRecord[], opts?: { aria?: boolean; }): string; /** * Everything except the pixels that can decide a verdict. * * The key used to carry the judge skill's version alone, which left three ways * for a cached verdict to outlive the rules that produced it. A project rubric * edited without bumping past the shipped skill's version changed the prompt and * not the key. `config.neverFile` never touched a version at all. And the * refuting skill's version was never in the key even though what the ledger * stores IS that skill's output, so amending the refuter left every stale * verdict standing. * * `promptHash` closes all three by covering the composed instruction text * itself. The version stays in the key because somebody reading ledger.json * should be able to see it without recomputing anything. */ export interface PanelIdentity { /** The judge panel whose verdicts this identity keys. */ panel: string; version: number; /** Short sha256 over every instruction text that can change a verdict. */ promptHash: string; model: string; /** * Whether this panel is shown the accessibility tree, and so whether the * tree belongs in its groups' hashes. Part of the identity rather than a * loose argument, because every call site that keys a verdict must agree * with the one that reads it back. */ aria?: boolean; } export declare function panelIdentity(opts: { panel: string; version: number; /** The panel's composed rubric text, core and vocabulary and extensions. */ panelText: string; refuteText: string; /** * handoff.md as composed for this run. Injected into the prompt only for * design-bearing batches, but hashed for every key: a split identity would * complicate the key for the rare event of a lookout release editing it, * and that release arguably owes a broad re-judge anyway. */ handoffText: string; model: string; /** * The challenge instructions as composed for this run, or "" when one AI * judges. Hashed on the refuter's own argument: what a ledger entry stores * is partly this pass's output, so amending it has to invalidate the * verdicts it produced. */ challengeText?: string; /** * Every AI that judged, as `:`. * * A verdict two judges reached is not a verdict one reached, and serving one * for the other is exactly the stale-verdict failure this hash exists to * prevent. It goes into the HASH rather than the key because the key's five * "@"-separated segments are load-bearing: `pruneLedger` deletes anything * with a different count, so a sixth segment would make every existing entry * unreachable in the one way that also deletes it. * * SORTED, so the roster is a set rather than an order. Which AI proposed can * alternate between runs, and encoding that here would make every alternating * run a cache miss and hold cost at first-run prices forever. The order that * actually ran is recorded on the entry instead, where it is readable when * debugging without fragmenting the cache. */ oracles?: readonly string[]; /** Whether this panel is shown the accessibility tree. */ aria?: boolean; }): PanelIdentity; export declare function ledgerKey(hash: string, id: PanelIdentity): string; export declare function loadLedger(resolved: ResolvedConfig): Promise; export declare function saveLedger(resolved: ResolvedConfig, ledger: Ledger): Promise; export declare function updateLedger(resolved: ResolvedConfig, mutate: (ledger: Ledger) => T | Promise): Promise<{ ledger: Ledger; result: T; }>; /** * Record one entry per judged view group. Findings are stored whole: on a * cache hit the group's findings come back together, which is what keeps a * comparative finding attached to the view it was made about. */ export declare function recordVerdicts(ledger: Ledger, runId: string, id: PanelIdentity, judged: { shots: ShotRecord[]; findings: VerifiedFinding[]; reply?: string; }[]): void; /** * Drop entries no current capture can ever hit again. * * A hit requires the key's exact group hash, so an entry whose hash matches * no group computable from the current report is unreachable: the pixels * moved, the route left the config, or the identity changed under it. The * committed ledger otherwise grows monotonically with every pixel change. * Entries for OTHER identities of a still-live hash are kept (alternating * --model keeps both caches), and the worst case of pruning wrongly is one * re-judge after a byte-identical revert, which is the failure this cache * prefers. Callers only run this on full-scope checks: a scoped run cannot * see every live group. */ export declare function pruneLedger(ledger: Ledger, liveGroupHashes: ReadonlySet): number;