/**
 * Literacy bridge — maps LiteracyTaskDefinition to the new compiler pipeline.
 *
 * This module bridges the task loading system (TaskSource →
 * GeneralizedTaskDefinition) and the compiler (TaskGraph →
 * PromptfooCompiler). It allows literacy tasks to run through
 * the compiler.
 *
 * Pipeline: LiteracyTaskDefinition[] → TaskGraphBuilder → topological order →
 *           LiteracyModeHandler (per task) → LiteracyBridgeResult
 *
 * Key behaviors:
 * - Tasks without explicit mode get mode: "literacy" (backward compat)
 * - LiteracyTaskDefinition fields map to compiler input fields
 * - Rubric config is loaded from config/rubrics
 * - Prompts from config/prompts are integrated
 * - TaskGraphBuilder validates the DAG, deduplicates, and orders tasks
 *
 * @see docs/archive/exec-plans/architecture-overhaul/phase-7-migrate-literacy.md
 */
import { RubricConfigSchema, formatZodErrors, } from "../../_vendor/ailf-core/index.js";
import { compileLiteracyTask, } from "./mode-handlers/literacy/index.js";
import { tryLoadConfigFile } from "./config-loader.js";
import { buildTaskGraph } from "./task-graph-builder.js";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Compile an array of LiteracyTaskDefinition through the literacy handler.
 *
 * This is the primary entry point for Phase 7 migration. It takes
 * LiteracyTaskDefinition[] and routes them through the compiler pipeline:
 *
 *   LiteracyTaskDefinition[] → TaskGraphBuilder → topological order →
 *   LiteracyModeHandler (per task) → LiteracyBridgeResult
 *
 * The TaskGraphBuilder provides:
 * - Duplicate task ID detection (warns on collisions)
 * - Status-based filtering (archived/paused/draft)
 * - Dependency edge discovery and DAG cycle validation
 * - Topological priority assignment (tasks with deps run in order)
 *
 * Note: The incoming tasks are typically pre-filtered by the pipeline
 * step (area/tag/taskId filters + release auto-scope). The graph
 * builder's own filtering is intentionally invoked WITHOUT a filter
 * argument to avoid double-filtering — it still applies status-based
 * rules (e.g., rejecting archived tasks that slipped through).
 */
export function compileLiteracyTasks(tasks, options) {
    const rubricConfig = loadRubricResolutionInput(options.rootDir);
    const warnings = [];
    const results = [];
    let totalTests = 0;
    // Build the task graph — validates DAG, deduplicates, assigns priority.
    // No filter passed: tasks are already pre-filtered by the pipeline step.
    const graphResult = buildTaskGraph({ tasks });
    warnings.push(...graphResult.warnings);
    if (graphResult.filteredOut.length > 0) {
        warnings.push(`TaskGraphBuilder filtered out ${graphResult.filteredOut.length} task(s) ` +
            `by status: ${graphResult.filteredOut.join(", ")}`);
    }
    // If all tasks were filtered out, return empty result
    if (!graphResult.graph) {
        return { tasks: [], warnings, totalTests: 0 };
    }
    // Extract tasks in topological order from the graph.
    // The graph nodes are keyed by taskId; we sort by priority (lower = first)
    // and look up the original LiteracyTaskDefinition for each node.
    const taskMap = new Map(tasks.map((t) => [t.id, t]));
    const orderedNodes = [...graphResult.graph.nodes.values()].sort((a, b) => a.priority - b.priority);
    const compileOptions = {
        graderProvider: options.graderProvider,
        rootDir: options.rootDir,
        evalMode: options.evalMode,
        models: options.models,
        rubricConfig,
        graderContext: options.graderContext,
        preflightContext: options.preflightContext,
    };
    for (const node of orderedNodes) {
        const task = taskMap.get(node.taskId);
        if (!task) {
            warnings.push(`TaskGraphBuilder produced node "${node.taskId}" with no matching LiteracyTaskDefinition — skipped`);
            continue;
        }
        const result = compileLiteracyTask(task, compileOptions);
        results.push({ taskId: task.id, result });
        warnings.push(...result.warnings);
        totalTests += result.tests.length;
    }
    return { tasks: results, warnings, totalTests };
}
/**
 * Compare old-style expanded entries with new-style compiled entries.
 *
 * This is the parallel comparison gate (task 7b). For each task, it
 * checks that the new compiler produces structurally equivalent output
 * to the legacy expand-tasks path.
 */
export function compareCompilerOutputs(legacyEntries, newResult) {
    const discrepancies = [];
    for (const { taskId, result } of newResult.tasks) {
        // Find matching legacy entries by task description
        const legacyForTask = legacyEntries.filter((e) => e.description?.includes(taskId) ||
            e.description?.includes("(gold)") ||
            e.description?.includes("(baseline)"));
        // Check test count matches
        const newTestCount = result.tests.length;
        const legacyGold = legacyForTask.filter((e) => e.description?.includes("(gold)"));
        const legacyBaseline = legacyForTask.filter((e) => e.description?.includes("(baseline)"));
        const legacyCount = legacyGold.length + legacyBaseline.length;
        if (legacyCount > 0 && newTestCount !== legacyCount) {
            discrepancies.push({
                taskId,
                field: "testCount",
                legacy: legacyCount,
                new_: newTestCount,
                message: `Test count mismatch: legacy=${legacyCount}, new=${newTestCount}`,
            });
        }
        // Check assertion count on gold entries
        for (const test of result.tests) {
            if (test.description.includes("(gold)")) {
                const assertCount = test.assert?.length ?? 0;
                const matchingLegacy = legacyGold[0];
                if (matchingLegacy?.assert) {
                    const legacyAssertCount = matchingLegacy.assert.length;
                    if (assertCount !== legacyAssertCount) {
                        discrepancies.push({
                            taskId,
                            field: "assertionCount",
                            legacy: legacyAssertCount,
                            new_: assertCount,
                            message: `Gold assertion count mismatch: legacy=${legacyAssertCount}, new=${assertCount}`,
                        });
                    }
                }
            }
        }
    }
    return {
        passed: discrepancies.length === 0,
        discrepancies,
        summary: discrepancies.length === 0
            ? "All tasks produce structurally equivalent output"
            : `${discrepancies.length} discrepancy(ies) found`,
    };
}
// ---------------------------------------------------------------------------
// Rubric config loading
// ---------------------------------------------------------------------------
function loadRubricResolutionInput(rootDir) {
    const result = tryLoadConfigFile("rubrics", rootDir);
    if (!result)
        return undefined;
    // Run the canonical RubricConfigSchema (Plan 03-01) at the trust
    // boundary. Hand-rolled .ailf/rubrics.{yaml,json} configs and test
    // fixtures previously bypassed validation: tryLoadConfigFile only
    // ran the format-specific parser, then per-field unsafe casts read
    // the result. With the strict GraderJudgmentSchema from Plan 03-04,
    // a missing footer silently became "" and every grader emission
    // fell through to the synthesized 'unclassified' fallback (CR-03).
    // Fail loudly with a Zod-formatted message instead so config drift
    // is caught at load time, not at every grader emission.
    const parseResult = RubricConfigSchema.safeParse(result.data);
    if (!parseResult.success) {
        const lines = formatZodErrors(parseResult.error);
        throw new Error(`Invalid rubric config at ${result.filePath}:\n${lines.join("\n")}`);
    }
    const parsed = parseResult.data;
    const templates = {};
    for (const [key, t] of Object.entries(parsed.templates)) {
        // Plan 03-02 — thread per-dimension failureModes through to the
        // runtime prompt assembler when the config stamped the field via
        // `failureModesForDimension(dimension)`.
        templates[key] = {
            header: t.header,
            scale: t.scale,
            ...(t.dimension !== undefined ? { dimension: t.dimension } : {}),
            ...(t.criteria_label !== undefined && t.criteria_label !== null
                ? { criteria_label: t.criteria_label }
                : {}),
            ...(t.failureModes && t.failureModes.length > 0
                ? { failureModes: t.failureModes }
                : {}),
        };
    }
    // Plan 03-01 — footer is min(1) per RubricConfigSchema, so safeParse
    // above guarantees a non-empty string here. No defensive empty-string
    // fall-back needed.
    return { templates, footer: parsed.footer };
}
