// ---------------------------------------------------------------------------
// judge-cache.ts -- SHA-256 content-hash cache for judge results
// ---------------------------------------------------------------------------
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, appendFileSync } from "node:fs";
import { join, dirname } from "node:path";
export class JudgeCache {
    skillDir;
    data;
    dirty = false;
    cachePath;
    constructor(skillDir) {
        this.skillDir = skillDir;
        this.cachePath = join(skillDir, "evals", ".judge-cache.json");
        this.data = this.load();
    }
    load() {
        try {
            if (existsSync(this.cachePath)) {
                const raw = readFileSync(this.cachePath, "utf-8");
                const parsed = JSON.parse(raw);
                if (parsed && typeof parsed === "object" && parsed.version === 1 && typeof parsed.entries === "object") {
                    return parsed;
                }
            }
        }
        catch {
            // Corruption recovery: delete corrupted file and start fresh
            try {
                if (existsSync(this.cachePath)) {
                    unlinkSync(this.cachePath);
                    console.warn(`[judge-cache] Corrupted cache file deleted: ${this.cachePath}`);
                }
            }
            catch {
                // ignore deletion failure
            }
        }
        return { version: 1, entries: {} };
    }
    static computeKey(assertionText, output, judgeModel) {
        return createHash("sha256")
            .update(`${assertionText}||${output}||${judgeModel}`)
            .digest("hex");
    }
    async getOrCompute(assertionText, output, judgeModel, compute) {
        const key = JudgeCache.computeKey(assertionText, output, judgeModel);
        const cached = this.data.entries[key];
        if (cached) {
            // Return cached result, reconstructing the AssertionResult shape
            return {
                id: "", // caller overwrites this
                text: assertionText,
                pass: cached.pass,
                reasoning: cached.reasoning,
            };
        }
        const result = await compute();
        // Store in cache
        this.data.entries[key] = {
            pass: result.pass,
            reasoning: result.reasoning,
            cachedAt: new Date().toISOString(),
            judgeModel,
        };
        this.dirty = true;
        return result;
    }
    has(assertionText, output, judgeModel) {
        const key = JudgeCache.computeKey(assertionText, output, judgeModel);
        return key in this.data.entries;
    }
    get size() {
        return Object.keys(this.data.entries).length;
    }
    /**
     * Persist cache to disk. Call after all operations are complete.
     * Also ensures .judge-cache.json is in .gitignore.
     */
    flush() {
        if (!this.dirty)
            return;
        const dir = dirname(this.cachePath);
        mkdirSync(dir, { recursive: true });
        writeFileSync(this.cachePath, JSON.stringify(this.data, null, 2), "utf-8");
        this.dirty = false;
        // T-009: Ensure .judge-cache.json is in .gitignore
        this.ensureGitignore();
    }
    ensureGitignore() {
        const gitignorePath = join(this.skillDir, ".gitignore");
        const pattern = "evals/.judge-cache.json";
        try {
            if (existsSync(gitignorePath)) {
                const content = readFileSync(gitignorePath, "utf-8");
                if (content.includes(pattern))
                    return;
                appendFileSync(gitignorePath, `\n${pattern}\n`);
            }
            else {
                writeFileSync(gitignorePath, `${pattern}\n`, "utf-8");
            }
        }
        catch {
            // Non-critical — don't fail the run for gitignore issues
        }
    }
}
//# sourceMappingURL=judge-cache.js.map