/**
 * pipeline/plan.ts
 *
 * Execution plan assembly for the `--explain` CLI flag.
 *
 * Computes a detailed preview of what a command will do without executing
 * anything. Calls existing pure functions (task expansion, model loading,
 * cache hashing, pricing) and composes them into an `ExecutionPlan`.
 *
 * @see docs/archive/exec-plans/execution-preview.md
 */
import { existsSync, readdirSync, statSync } from "fs";
import { resolve } from "path";
import { createLiteracyModeBase, modelMatchesLiteracyVariant, } from "./compiler/mode-bases/literacy.js";
import { lookupPricing } from "../agent-observer/pricing.js";
import { RepoTaskSource } from "../adapters/task-sources/repo-task-source.js";
import { loadAllTsTaskFiles } from "../adapters/task-sources/task-file-loader.js";
import { lookupCache } from "./cache.js";
import { compileLiteracyTasks } from "./compiler/literacy-bridge.js";
import { resolveVendoredSubdir, tryLoadConfigFile, } from "./compiler/config-loader.js";
import { LiteracyVariant } from "./normalize-mode.js";
import { validateConfiguration } from "./validate.js";
/**
 * Known promptfoo provider prefixes — stripped to get the raw model name.
 * Must be ordered longest-first to avoid partial matches.
 */
const PROVIDER_PREFIXES = [
    "anthropic:messages:",
    "openai:chat:",
    "openai:responses:",
    "openai:",
    "anthropic:",
    "google:",
];
function extractModelName(id) {
    for (const prefix of PROVIDER_PREFIXES) {
        if (id.startsWith(prefix)) {
            return id.slice(prefix.length);
        }
    }
    const parts = id.split(":");
    return parts.length > 1 ? parts.slice(1).join(":") : id;
}
function loadModelsFile(rootDir) {
    const result = tryLoadConfigFile("models", rootDir);
    return result?.data ?? null;
}
const _literacyBase = createLiteracyModeBase();
/**
 * Check whether a model participates in a given eval mode + optional variant.
 *
 * For literacy mode, checks both mode enrollment and variant participation
 * via the shared `modelMatchesLiteracyVariant` helper. For non-literacy
 * modes, checks mode enrollment only.
 */
function modeMatchesModel(mode, model, variant) {
    // Check basic mode enrollment
    if (model.modes &&
        model.modes.length > 0 &&
        !model.modes.includes(mode)) {
        return false;
    }
    // For literacy mode with a variant, check variant participation
    if (mode === "literacy" && variant) {
        switch (variant) {
            case LiteracyVariant.AGENTIC:
                return (modelMatchesLiteracyVariant(model, "agentic-naive") ||
                    modelMatchesLiteracyVariant(model, "agentic-optimized"));
            case LiteracyVariant.FULL:
                return (modelMatchesLiteracyVariant(model, "baseline") ||
                    modelMatchesLiteracyVariant(model, "agentic-naive") ||
                    modelMatchesLiteracyVariant(model, "agentic-optimized"));
            default:
                return modelMatchesLiteracyVariant(model, variant);
        }
    }
    return true;
}
// ---------------------------------------------------------------------------
// Cost estimation
// ---------------------------------------------------------------------------
/**
 * Average token estimates per evaluation call (conservative).
 * These are rough averages based on typical Sanity documentation tasks.
 */
const AVG_TOKENS = {
    /** Grader output per judgment */
    graderOutput: 500,
    /** Grader prompt per judgment (rubric + response) */
    graderPrompt: 2000,
    /** Model response per task */
    responseTokens: 1500,
    /** Task prompt + context tokens */
    taskPromptTokens: 2000,
    /** Higher estimate multiplier */
    upperMultiplier: 2.0,
};
// ---------------------------------------------------------------------------
// Cache prediction
// ---------------------------------------------------------------------------
/**
 * Build a complete execution plan for the `run` command.
 *
 * This is a read-only operation — it computes the plan by calling existing
 * pure functions (task expansion, model loading, cache lookup, pricing)
 * without executing any pipeline steps or writing to process.env.
 */
export async function buildPipelinePlan(opts, rootDir) {
    // 1. Validate configuration (same as Step 0)
    let validation;
    try {
        validation = validateConfiguration(rootDir);
    }
    catch {
        validation = { issues: [], valid: false };
    }
    const warnings = validation.issues
        .filter((i) => i.severity === "warning")
        .map((i) => `[${i.source}] ${i.message}`);
    const errors = validation.issues
        .filter((i) => i.severity === "error")
        .map((i) => `[${i.source}] ${i.message}`);
    // 2. Expand tasks with filters
    const filter = opts.areaOption || opts.taskOption || opts.tagOption?.length
        ? {
            areas: opts.areaOption
                ? opts.areaOption.split(",").map((a) => a.trim())
                : undefined,
            taskIds: opts.taskOption
                ? opts.taskOption.split(",").map((t) => t.trim())
                : undefined,
            tags: opts.tagOption,
        }
        : undefined;
    let totalTests = 0;
    let tasks = [];
    let repoTaskCount;
    // -----------------------------------------------------------------------
    // Load and compile tasks — unified path for all modes
    // -----------------------------------------------------------------------
    try {
        const modelsForCompile = loadModelsFile(rootDir);
        const graderProvider = modelsForCompile?.grader?.id ?? "openai:chat:gpt-4o";
        const modelEntries = (modelsForCompile?.models ?? []).map((m) => ({ id: m.id, label: m.label }));
        // Load *.task.ts files from tasks/<mode>/ (or dist/tasks/<mode>/ when vendored)
        const modeTasksDir = resolveVendoredSubdir(rootDir, `tasks/${opts.mode}`);
        if (existsSync(modeTasksDir)) {
            const rawTasks = await loadAllTsTaskFiles(modeTasksDir);
            if (rawTasks.length > 0) {
                // Dynamic import of the handler module
                const handlerModulePath = `./compiler/mode-handlers/${opts.mode}/index.js`;
                const mod = await import(handlerModulePath);
                const handler = mod.handler;
                const skippedByMode = new Map();
                for (const rawFile of rawTasks) {
                    for (const taskDef of rawFile.tasks) {
                        const task = taskDef;
                        // Filter to matching mode (skip tasks from other modes in same dir)
                        if ("mode" in task && task.mode !== opts.mode) {
                            const taskMode = task.mode ?? "unknown";
                            skippedByMode.set(taskMode, (skippedByMode.get(taskMode) ?? 0) + 1);
                            continue;
                        }
                        // Apply area/task/tag filter
                        if (filter) {
                            if (filter.areas?.length &&
                                !filter.areas
                                    .map((a) => a.toLowerCase())
                                    .includes((task.area ?? "").toLowerCase()))
                                continue;
                            if (filter.taskIds?.length && !filter.taskIds.includes(task.id))
                                continue;
                            if (filter.tags?.length &&
                                (!task.tags || !task.tags.some((t) => filter.tags.includes(t))))
                                continue;
                        }
                        const result = handler.compileTask(task, {
                            rootDir,
                            graderProvider,
                            models: modelEntries,
                            // For literacy mode, pass the variant as evalMode
                            ...(opts.mode === "literacy"
                                ? {
                                    evalMode: opts.variant === LiteracyVariant.AGENTIC
                                        ? LiteracyVariant.AGENTIC
                                        : LiteracyVariant.STANDARD,
                                }
                                : {}),
                        });
                        totalTests += result.tests.length;
                        for (const test of result.tests) {
                            const desc = typeof test.description === "string"
                                ? test.description
                                : (taskDef.id ?? "unknown");
                            const isBaseline = desc.includes("[Baseline]") || desc.endsWith("(baseline)");
                            tasks.push({
                                description: desc,
                                variant: isBaseline
                                    ? LiteracyVariant.STANDARD
                                    : "gold",
                            });
                        }
                    }
                }
                if (skippedByMode.size > 0) {
                    const summary = [...skippedByMode.entries()]
                        .map(([m, n]) => `${n} ${m}`)
                        .join(", ");
                    const total = [...skippedByMode.values()].reduce((a, b) => a + b, 0);
                    warnings.push(`Skipped ${total} task(s) with non-matching mode (${summary}). Current pipeline mode: ${opts.mode}. Run with --mode <mode> to include them.`);
                }
            }
        }
    }
    catch (err) {
        const detail = err instanceof Error ? err.message : String(err);
        errors.push(`Failed to compile tasks: ${detail}`);
    }
    // Scan repo tasks path for additional task count (preview only)
    if (opts.repoTasksPath) {
        try {
            const repoSource = new RepoTaskSource(opts.repoTasksPath);
            const allRepoTasks = await repoSource.loadTasks(filter);
            // Filter to current mode tasks
            const repoTasks = allRepoTasks.filter((t) => t.mode === opts.mode);
            const skippedRepoTasks = allRepoTasks.length - repoTasks.length;
            if (skippedRepoTasks > 0) {
                const skippedModes = new Map();
                for (const t of allRepoTasks) {
                    if (t.mode !== opts.mode) {
                        skippedModes.set(t.mode, (skippedModes.get(t.mode) ?? 0) + 1);
                    }
                }
                const summary = [...skippedModes.entries()]
                    .map(([m, n]) => `${n} ${m}`)
                    .join(", ");
                warnings.push(`Skipped ${skippedRepoTasks} repo task(s) with non-matching mode (${summary}). Current pipeline mode: ${opts.mode}. Run with --mode <mode> to include them.`);
            }
            repoTaskCount = repoTasks.length;
            if (repoTaskCount > 0 && opts.mode === "literacy") {
                // Literacy-specific compilation for repo tasks (detailed test expansion)
                const literacyRepoTasks = repoTasks.filter((t) => t.mode === "literacy");
                const modelsForCompile = loadModelsFile(rootDir);
                const graderProvider = modelsForCompile?.grader?.id ?? "openai:chat:gpt-4o";
                const compileResult = compileLiteracyTasks(literacyRepoTasks, {
                    rootDir,
                    evalMode: opts.variant === LiteracyVariant.AGENTIC
                        ? LiteracyVariant.AGENTIC
                        : LiteracyVariant.STANDARD,
                    graderProvider,
                    models: (modelsForCompile?.models ?? []).map((m) => ({ id: m.id, label: m.label })),
                });
                totalTests += compileResult.totalTests;
                for (const { taskId, result } of compileResult.tasks) {
                    for (const test of result.tests) {
                        const desc = typeof test.description === "string" ? test.description : taskId;
                        const isBaseline = desc.includes("[Baseline]") || desc.endsWith("(baseline)");
                        tasks.push({
                            description: desc,
                            variant: isBaseline
                                ? LiteracyVariant.STANDARD
                                : "gold",
                        });
                    }
                }
            }
            else if (repoTaskCount > 0) {
                // Non-literacy modes: approximate 1 test per task (compilation not
                // supported for non-literacy repo tasks in the explain preview yet)
                totalTests += repoTaskCount;
            }
        }
        catch {
            warnings.push(`Failed to scan repo tasks at ${opts.repoTasksPath} — count may be underestimated`);
        }
    }
    // 3. Apply debug filter simulation
    const debugPlan = simulateDebugFilter(totalTests, opts.debug);
    const effectiveTestCount = debugPlan?.filteredCount ?? totalTests;
    // 4. Load models for the selected mode
    const modelsFile = loadModelsFile(rootDir);
    const models = [];
    let graderModelName = "";
    if (modelsFile) {
        const activeModels = modelsFile.models.filter((m) => modeMatchesModel(opts.mode, m, opts.variant));
        // For agentic mode, each model appears twice (naive + optimized)
        for (const m of activeModels) {
            const modelName = extractModelName(m.id);
            if (opts.variant === LiteracyVariant.AGENTIC) {
                if (modelMatchesLiteracyVariant(m, "agentic-naive")) {
                    models.push({
                        id: m.id,
                        label: `${m.label} (Naive)`,
                        modelName,
                    });
                }
                if (modelMatchesLiteracyVariant(m, "agentic-optimized")) {
                    models.push({
                        id: m.id,
                        label: `${m.label} (Optimized)`,
                        modelName,
                    });
                }
            }
            else {
                models.push({ id: m.id, label: m.label, modelName });
            }
        }
        graderModelName = extractModelName(modelsFile.grader.id);
    }
    // 5. Estimate cost
    const rubricCount = estimateRubricAssertionsPerTask(rootDir);
    const costEstimate = models.length > 0 && effectiveTestCount > 0
        ? estimateCost(effectiveTestCount, models, graderModelName, rubricCount)
        : undefined;
    // 6. Cache prediction
    const cachePrediction = predictCacheStatus(rootDir, {
        noCache: opts.noCache,
        skipEval: opts.skipEval,
        skipFetch: opts.skipFetch,
    });
    // 7. Build step plan
    const steps = buildStepPlan({
        compareEnabled: opts.compareEnabled,
        dryRun: opts.dryRun,
        gapAnalysisEnabled: opts.gapAnalysisEnabled,
        graderReplications: opts.graderReplications,
        noCache: opts.noCache,
        publishEnabled: opts.publishEnabled,
        skipEval: opts.skipEval,
        skipFetch: opts.skipFetch,
    }, cachePrediction);
    // 8. Comparison plan
    const comparison = buildComparisonPlan(rootDir, opts.compareEnabled, opts.compareBaseline, opts.beforeOption, opts.compareThreshold);
    // 9. File lists
    const filesRead = collectFilesRead(rootDir, opts.mode);
    const filesCreated = collectFilesCreated({
        compareEnabled: opts.compareEnabled,
        gapAnalysisEnabled: opts.gapAnalysisEnabled,
        publishEnabled: opts.publishEnabled,
    });
    return {
        cacheStatus: cachePrediction.predictions,
        command: "run",
        comparison,
        costEstimate,
        debug: debugPlan,
        description: `Run the full evaluation pipeline in ${opts.mode} mode`,
        errors,
        filesCreated,
        filesRead,
        mode: opts.mode,
        models: models.length > 0 ? models : undefined,
        repoTaskCount,
        source: opts.source ?? "production",
        steps,
        tasks: tasks.length > 0 ? tasks : undefined,
        totalCacheSavingsMs: cachePrediction.estimatedSavedMs,
        totalTests: effectiveTestCount,
        warnings,
    };
}
/**
 * Build a minimal plan for commands that don't have complex step graphs.
 * Used by compare, validate, fetch-docs, baseline, etc.
 */
export function buildSimpleCommandPlan(opts) {
    let validation;
    try {
        validation = validateConfiguration(opts.rootDir);
    }
    catch {
        validation = { issues: [], valid: true };
    }
    return {
        cacheStatus: {},
        command: opts.command,
        description: opts.description,
        errors: validation.issues
            .filter((i) => i.severity === "error")
            .map((i) => `[${i.source}] ${i.message}`),
        filesCreated: opts.filesCreated ?? [],
        filesRead: opts.filesRead ?? [],
        steps: opts.steps ?? [],
        totalCacheSavingsMs: 0,
        warnings: validation.issues
            .filter((i) => i.severity === "warning")
            .map((i) => `[${i.source}] ${i.message}`),
    };
}
// ---------------------------------------------------------------------------
// Comparison plan builder
// ---------------------------------------------------------------------------
function buildComparisonPlan(rootDir, compareEnabled, compareBaseline, beforeOption, threshold) {
    if (!compareEnabled)
        return undefined;
    let baselinePath;
    if (compareBaseline) {
        baselinePath = resolve(rootDir, compareBaseline);
    }
    else {
        // Find latest baseline
        const baselinesDir = resolve(rootDir, "results", "baselines");
        if (existsSync(baselinesDir)) {
            const files = readdirSync(baselinesDir)
                .filter((f) => f.endsWith(".json"))
                .sort()
                .reverse();
            baselinePath =
                files.length > 0
                    ? resolve(baselinesDir, files[0])
                    : resolve(baselinesDir, "latest.json");
        }
        else {
            baselinePath = resolve(baselinesDir, "latest.json");
        }
    }
    const exists = existsSync(baselinePath);
    let baselineAge;
    if (exists) {
        try {
            const stat = statSync(baselinePath);
            const ageMs = Date.now() - stat.mtimeMs;
            const ageHours = Math.floor(ageMs / 3600000);
            if (ageHours < 1) {
                baselineAge = "< 1 hour";
            }
            else if (ageHours < 24) {
                baselineAge = `${ageHours} hour${ageHours === 1 ? "" : "s"}`;
            }
            else {
                const ageDays = Math.floor(ageHours / 24);
                baselineAge = `${ageDays} day${ageDays === 1 ? "" : "s"}`;
            }
        }
        catch {
            // stat failed — not critical
        }
    }
    return {
        baselineAge,
        baselinePath,
        exists,
        threshold: threshold ?? 2,
    };
}
// ---------------------------------------------------------------------------
// File list builders
// ---------------------------------------------------------------------------
function buildStepPlan(opts, cachePrediction) {
    const steps = [];
    // Step 0: Validate
    steps.push({
        cacheStatus: "miss",
        name: "Validate configuration",
        reason: "Parse and check all YAML configs, schemas, reference solutions",
        willRun: true,
    });
    if (opts.dryRun) {
        // Mark remaining steps as skipped
        const remaining = [
            "Fetch documentation",
            "Generate configs",
            "Run evaluation",
            "Calculate scores",
            "Generate report",
        ];
        for (const name of remaining) {
            steps.push({
                cacheStatus: "skipped",
                name,
                reason: "--dry-run: no execution",
                willRun: false,
            });
        }
        return steps;
    }
    // Step 1: Fetch docs
    const fetchStatus = cachePrediction.predictions["fetch-docs"] ?? "unknown";
    steps.push({
        cacheStatus: fetchStatus,
        estimatedSavedMs: fetchStatus === "hit" ? cachePrediction.estimatedSavedMs : undefined,
        name: "Fetch documentation",
        reason: fetchStatus === "skipped"
            ? "--no-fetch: reuse cached contexts"
            : fetchStatus === "hit"
                ? "CACHED (inputs unchanged)"
                : "Fetch from Sanity Content Lake",
        willRun: fetchStatus !== "skipped" && fetchStatus !== "hit",
    });
    // Step 2: Generate configs
    const genStatus = cachePrediction.predictions["generate-configs"] ?? "unknown";
    steps.push({
        cacheStatus: genStatus,
        name: "Generate configs",
        reason: genStatus === "hit"
            ? "CACHED (inputs unchanged)"
            : "Expand tasks → Promptfoo config files",
        willRun: genStatus !== "hit",
    });
    // Step 3: Run evaluation
    const evalStatus = cachePrediction.predictions["eval"] ?? "unknown";
    steps.push({
        cacheStatus: evalStatus,
        name: "Run evaluation",
        reason: opts.skipEval
            ? "--no-eval: reuse existing results"
            : evalStatus === "hit"
                ? "CACHED (inputs unchanged)"
                : "Execute Promptfoo evaluation against all models",
        willRun: !opts.skipEval && evalStatus !== "hit",
    });
    // Step 3c: Grader consistency (optional)
    if (opts.graderReplications) {
        steps.push({
            cacheStatus: "miss",
            name: "Grader consistency analysis",
            reason: `${opts.graderReplications} replications`,
            willRun: true,
        });
    }
    // Step 4: Calculate scores
    const scoreStatus = cachePrediction.predictions["calculate-scores"] ?? "unknown";
    steps.push({
        cacheStatus: scoreStatus,
        name: "Calculate scores",
        reason: scoreStatus === "hit"
            ? "CACHED (inputs unchanged)"
            : "Compute AI Literacy Scores from eval results",
        willRun: scoreStatus !== "hit",
    });
    // Step 4b: Gap analysis (optional)
    if (opts.gapAnalysisEnabled) {
        steps.push({
            cacheStatus: "miss",
            name: "Gap analysis",
            reason: "Classify failure modes + build remediation plan",
            willRun: true,
        });
    }
    // Step 4c: Publish report (optional)
    if (opts.publishEnabled) {
        steps.push({
            cacheStatus: "miss",
            name: "Publish report",
            reason: "Write to Sanity Content Lake + fan out to sinks",
            willRun: true,
        });
    }
    // Step 5: Generate report
    steps.push({
        cacheStatus: "miss",
        name: "Generate report",
        reason: "Build PR comment markdown + score summary",
        willRun: true,
    });
    // Step 5b: Compare (optional)
    if (opts.compareEnabled) {
        steps.push({
            cacheStatus: "miss",
            name: "Compare against baseline",
            reason: "Structured score comparison with delta analysis",
            willRun: true,
        });
    }
    return steps;
}
function collectFilesCreated(opts) {
    const files = [
        "results/latest/eval-results.json",
        "results/latest/score-summary.json",
        "results/latest/grader-judgments.json",
        "results/latest/pipeline-result.json",
    ];
    if (opts.compareEnabled) {
        files.push("results/latest/comparison-report.json");
    }
    if (opts.gapAnalysisEnabled) {
        files.push("results/latest/failure-modes.json");
        files.push("results/latest/gap-analysis.json");
    }
    return files.sort();
}
// ---------------------------------------------------------------------------
// Step plan builder
// ---------------------------------------------------------------------------
function collectFilesRead(rootDir, _mode) {
    const files = [
        "config/models.ts",
        "config/rubrics.ts",
        "config/prompts.ts",
        "config/sources.ts",
    ];
    // Task files
    const tasksDir = resolve(rootDir, "tasks");
    if (existsSync(tasksDir)) {
        const taskFiles = readdirSync(tasksDir)
            .filter((f) => /\.(yaml|yml|task\.ts|task\.js)$/.test(f) && !f.startsWith("."))
            .sort();
        for (const f of taskFiles)
            files.push(`tasks/${f}`);
    }
    // Context files (canonical)
    const canonicalDir = resolve(rootDir, "contexts", "canonical");
    if (existsSync(canonicalDir)) {
        const contextFiles = readdirSync(canonicalDir)
            .filter((f) => f.endsWith(".md"))
            .sort();
        for (const f of contextFiles)
            files.push(`contexts/canonical/${f}`);
    }
    // Reference solutions
    const refDir = resolve(rootDir, "canonical", "reference-solutions");
    if (existsSync(refDir)) {
        const refFiles = readdirSync(refDir)
            .filter((f) => statSync(resolve(refDir, f)).isFile() && !f.startsWith("."))
            .sort();
        for (const f of refFiles)
            files.push(`canonical/reference-solutions/${f}`);
    }
    // Thresholds (if readiness is involved)
    if (existsSync(resolve(rootDir, "config", "thresholds.ts"))) {
        files.push("config/thresholds.ts");
    }
    if (existsSync(resolve(rootDir, "config", "features.ts"))) {
        files.push("config/features.ts");
    }
    return [...new Set(files)].sort();
}
// ---------------------------------------------------------------------------
// Rubric assertion counting
// ---------------------------------------------------------------------------
function estimateCost(testCount, models, graderModelName, rubricAssertionsPerTask) {
    const perModel = [];
    let totalMin = 0;
    let totalMax = 0;
    for (const model of models) {
        const pricing = lookupPricing(model.modelName);
        if (!pricing) {
            perModel.push({
                cost: { max: 0, min: 0 },
                label: model.label,
                modelName: model.modelName,
            });
            continue;
        }
        const minCost = testCount *
            (pricing.input * AVG_TOKENS.taskPromptTokens +
                pricing.output * AVG_TOKENS.responseTokens);
        const maxCost = minCost * AVG_TOKENS.upperMultiplier;
        perModel.push({
            cost: { max: maxCost, min: minCost },
            label: model.label,
            modelName: model.modelName,
        });
        totalMin += minCost;
        totalMax += maxCost;
    }
    // Grading cost: each test × each model × rubric assertions per task
    const gradingCalls = testCount * models.length * rubricAssertionsPerTask;
    const graderPricing = lookupPricing(graderModelName);
    let gradingMin = 0;
    let gradingMax = 0;
    if (graderPricing) {
        gradingMin =
            gradingCalls *
                (graderPricing.input * AVG_TOKENS.graderPrompt +
                    graderPricing.output * AVG_TOKENS.graderOutput);
        gradingMax = gradingMin * AVG_TOKENS.upperMultiplier;
    }
    return {
        grading: { max: gradingMax, min: gradingMin },
        gradingCalls,
        perModel,
        totalApiCalls: testCount * models.length,
        totalCost: {
            max: totalMax + gradingMax,
            min: totalMin + gradingMin,
        },
    };
}
// ---------------------------------------------------------------------------
// Resolved options subset (matches pipeline-action.ts ResolvedOptions shape)
// Used by the plan builder without importing the full type to avoid circular deps.
// ---------------------------------------------------------------------------
function estimateRubricAssertionsPerTask(rootDir) {
    // Load rubrics config and count the default template set.
    // In practice, most tasks have 2-4 rubric assertions.
    const result = tryLoadConfigFile("rubrics", rootDir);
    if (!result)
        return 2; // conservative default
    try {
        const templateCount = result.data?.templates
            ? Object.keys(result.data.templates).length
            : 2;
        // Most tasks use 2-3 of the available templates
        return Math.min(templateCount, 3);
    }
    catch {
        return 2;
    }
}
// ---------------------------------------------------------------------------
// Pipeline plan builder
// ---------------------------------------------------------------------------
function predictCacheStatus(rootDir, opts) {
    const predictions = {};
    let estimatedSavedMs = 0;
    const steps = [
        "fetch-docs",
        "generate-configs",
        "eval",
        "calculate-scores",
        "report",
    ];
    for (const step of steps) {
        // Check for skip flags
        if (step === "fetch-docs" && opts.skipFetch) {
            predictions[step] = "skipped";
            continue;
        }
        if (step === "eval" && opts.skipEval) {
            predictions[step] = "skipped";
            continue;
        }
        if (opts.noCache) {
            predictions[step] = "miss";
            continue;
        }
        try {
            const result = lookupCache(rootDir, step);
            if (result.hit) {
                predictions[step] = "hit";
                estimatedSavedMs += result.entry.durationMs;
            }
            else {
                predictions[step] = "miss";
            }
        }
        catch {
            predictions[step] = "unknown";
        }
    }
    return { estimatedSavedMs, predictions };
}
// ---------------------------------------------------------------------------
// Simple command plan builders (non-pipeline)
// ---------------------------------------------------------------------------
function simulateDebugFilter(totalEntries, debug) {
    if (!debug?.enabled)
        return undefined;
    let filteredCount = totalEntries;
    let filterDescription = "";
    if (debug.pattern) {
        // We can't know exact match count without loading task descriptions,
        // but we can indicate the filter is applied
        filterDescription = `pattern "${debug.pattern}"`;
        // Conservative: assume ~30% survive a pattern filter
        filteredCount = Math.max(1, Math.ceil(totalEntries * 0.3));
    }
    else if (debug.sample) {
        filteredCount = Math.min(debug.sample, totalEntries);
        filterDescription = `random sample of ${filteredCount}`;
    }
    else {
        const firstN = debug.firstN ?? 2;
        filteredCount = Math.min(firstN * 2, totalEntries); // ×2 for gold+baseline
        filterDescription = `first ${Math.min(firstN, Math.ceil(totalEntries / 2))} tasks (${filteredCount} test entries)`;
    }
    return {
        filterDescription,
        filteredCount,
        totalCount: totalEntries,
    };
}
