/**
 * Post-emit feedback runner.
 *
 * After all per-owner emits complete, this orchestrator:
 *   1. Runs all applicable verifiers against the realized output
 *   2. Groups errors by source file → owning entity
 *   3. For each owner with errors, asks the caller to re-emit the
 *      file via the provided `reemit` callback (with errors + previous
 *      content as context — the caller builds the LLM prompt)
 *   4. Re-runs verifiers + records per-pass deltas
 *   5. Bounded retries (default 1 pass)
 *
 * The runner is intentionally framework-agnostic: it does NOT call any
 * specific LLM provider or import the per-owner-emit module. The
 * caller wires `reemit` to whatever re-emission path makes sense for
 * the project. This keeps the runner testable in isolation and
 * forward-compatible with future re-emit strategies.
 *
 * Rollback policy: if a re-emit INCREASES error count for that file,
 * the runner rolls back to the previous content. The LLM doesn't get
 * to make things worse.
 */
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { runAllVerifiers } from './verifier-manifest.js';
/**
 * Default file→owner mapping for the SpecVerse TypeScript layout:
 *   `backend/src/behaviors/<Owner>.ai.ts` → `<Owner>`
 *   `<anything>/<Owner>.ai.ts` → `<Owner>` (relaxed fallback)
 * Returns null for files that don't match.
 */
export function defaultMapFileToOwner(file) {
    const m = /(?:^|\/)([A-Za-z_][\w-]*)\.ai\.ts$/.exec(file);
    return m ? m[1] : null;
}
/**
 * Default treatment policy (Phase 2 of
 * 2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT):
 *
 *   - `STUB002` (LLM-throw stubs) → `surface-spec-gap`. The LLM already
 *     signaled "I lack context for this action" via the throw message;
 *     re-prompting either no-ops or hallucinates cross-file regressions.
 *     Better to tell the user about the gap so they can adjust the
 *     spec / manifest / provider choice.
 *
 *   - Everything else (tsc errors, STUB001 γ-fallback, STUB003 trivial-
 *     return, STUB004 empty-body, STUB005 misplaced-brace, future
 *     codes) → `auto-reemit`.
 *
 * Override at the call site for testing or for advanced policies.
 */
export function defaultTreatmentSelector(error) {
    if (error.code === 'STUB002')
        return 'surface-spec-gap';
    return 'auto-reemit';
}
/**
 * Group verifier errors by file (preserving error order within each
 * file). Files with no errors are absent from the map.
 */
function groupErrorsByFile(errors) {
    const out = new Map();
    for (const e of errors) {
        let bucket = out.get(e.file);
        if (!bucket) {
            bucket = [];
            out.set(e.file, bucket);
        }
        bucket.push(e);
    }
    return out;
}
/**
 * Sum the error count for a specific file across a result set.
 */
function errorCountForFile(errors, file) {
    let n = 0;
    for (const e of errors)
        if (e.file === file)
            n++;
    return n;
}
export async function runPostEmitFeedback(ctx, reemit, options = {}) {
    const maxPasses = options.maxPasses ?? 1;
    const maxFilesPerPass = options.maxFilesPerPass ?? 20;
    const mapFileToOwner = options.mapFileToOwner ?? defaultMapFileToOwner;
    const enabledOverrides = options.enabledOverrides;
    const treatmentSelector = options.treatmentSelector ?? defaultTreatmentSelector;
    const passes = [];
    const verifierNotes = [];
    const specGaps = [];
    const seenSpecGaps = new Set();
    /** Pull out spec-gap classified errors into the result's specGaps[]
     *  while leaving auto-reemit errors in place for the loop. Dedups
     *  across passes (same error from re-running verifiers shouldn't
     *  appear twice in the sidecar). */
    function partitionErrors(errors) {
        const actionable = [];
        for (const e of errors) {
            const strategy = treatmentSelector(e);
            if (strategy === 'surface-spec-gap') {
                const key = `${e.file}:${e.line ?? 0}:${e.col ?? 0}:${e.code}`;
                if (!seenSpecGaps.has(key)) {
                    seenSpecGaps.add(key);
                    specGaps.push({
                        ownerName: mapFileToOwner(e.file),
                        file: e.file,
                        ...(e.line !== undefined ? { line: e.line } : {}),
                        ...(e.col !== undefined ? { col: e.col } : {}),
                        code: e.code,
                        message: e.message,
                    });
                }
                continue;
            }
            if (strategy === 'skip')
                continue;
            actionable.push(e);
        }
        return actionable;
    }
    // Initial verifier pass.
    let currentResults = await runAllVerifiers(ctx, enabledOverrides);
    for (const r of currentResults) {
        if (r.notes.length > 0)
            verifierNotes.push({ verifierId: r.verifierId, notes: r.notes });
    }
    const initialErrorsRaw = currentResults.flatMap((r) => r.errors);
    const initialErrors = partitionErrors(initialErrorsRaw);
    let currentErrors = initialErrors;
    for (let pass = 1; pass <= maxPasses; pass++) {
        const errorsBefore = currentErrors.length;
        if (errorsBefore === 0)
            break;
        const byFile = groupErrorsByFile(currentErrors);
        const passResult = {
            passNumber: pass,
            errorsBefore,
            errorsAfter: errorsBefore,
            ownersAttempted: 0,
            ownersImproved: 0,
            ownersUnchanged: 0,
            ownersRolledBack: 0,
            ownersUnmappable: 0,
            ownersReemitFailed: 0,
            ownersOverBudget: 0,
        };
        // Per-file backup so we can roll back if the LLM makes it worse.
        const backups = new Map();
        // Apply the per-pass file budget. Iteration order over Map mirrors
        // insertion order — which mirrors verifier emit order — so the
        // first N files are deterministic across runs.
        const fileEntries = [...byFile.entries()];
        const allowed = fileEntries.slice(0, maxFilesPerPass);
        const overBudget = fileEntries.slice(maxFilesPerPass);
        passResult.ownersOverBudget = overBudget.length;
        for (const [file, fileErrors] of allowed) {
            const ownerName = mapFileToOwner(file);
            if (!ownerName) {
                passResult.ownersUnmappable++;
                continue;
            }
            const absFile = join(ctx.outputDir, file);
            let previousContent;
            try {
                previousContent = readFileSync(absFile, 'utf8');
            }
            catch {
                passResult.ownersReemitFailed++;
                continue;
            }
            passResult.ownersAttempted++;
            const newContent = await reemit({
                ownerName,
                file,
                errors: fileErrors,
                previousContent,
            });
            if (!newContent) {
                passResult.ownersReemitFailed++;
                continue;
            }
            backups.set(file, previousContent);
            writeFileSync(absFile, newContent);
        }
        // Re-run verifiers, then compare per-file error counts.
        const newResults = await runAllVerifiers(ctx, enabledOverrides);
        let newErrors = partitionErrors(newResults.flatMap((r) => r.errors));
        // Rollback pass: any file whose error count INCREASED gets restored.
        // Only consider files we actually attempted (within the budget).
        const filesToRestore = [];
        for (const [file, fileErrors] of allowed) {
            const before = fileErrors.length;
            const after = errorCountForFile(newErrors, file);
            if (after > before && backups.has(file)) {
                filesToRestore.push(file);
                passResult.ownersRolledBack++;
            }
            else if (after < before) {
                passResult.ownersImproved++;
            }
            else if (after === before && after > 0) {
                passResult.ownersUnchanged++;
            }
        }
        if (filesToRestore.length > 0) {
            for (const file of filesToRestore) {
                const absFile = join(ctx.outputDir, file);
                writeFileSync(absFile, backups.get(file));
            }
            // Re-run verifiers one more time so the reported `errorsAfter`
            // reflects the post-rollback state.
            const finalResults = await runAllVerifiers(ctx, enabledOverrides);
            newErrors = partitionErrors(finalResults.flatMap((r) => r.errors));
        }
        passResult.errorsAfter = newErrors.length;
        passes.push(passResult);
        currentResults = newResults;
        currentErrors = newErrors;
        // Early exit: if a pass produced no improvement, more passes
        // unlikely to help.
        if (passResult.errorsAfter >= passResult.errorsBefore)
            break;
    }
    return {
        initialErrors,
        finalErrors: currentErrors,
        passes,
        verifierNotes,
        specGaps,
    };
}
//# sourceMappingURL=feedback-runner.js.map