// Quality Intelligence drift re-check + targeted regeneration routes (Epic #735, Issue #743).
//
// NOTE on "current sources": The original run's START request sources are NOT persisted in the
// manifest. Therefore re-check and regenerate-stale both require the sources to be re-supplied
// by the caller. Both routes are POST to carry the sources in the body; the GET verb is avoided
// because sources can be arbitrarily large.
//
//   POST /api/quality-intelligence/runs/:id/re-check
//     Body: { sources: QualityIntelligenceInlineSource[] }
//     Returns: QualityIntelligenceUiStalenessReport
//
//   POST /api/quality-intelligence/runs/:id/regenerate-stale
//     Body: { sources: QualityIntelligenceInlineSource[] }
//     Returns: { runId: string; regeneratedCount: number; preservedCount: number }
//
// Both routes go through the central CSRF guard in server.ts (all POSTs do).
import { randomUUID } from "node:crypto";
import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
import { ALL_POLICY_PROFILES, buildAtomCoverageStatuses, buildCoverageMap, compareStaleness, regressionDefault, validateCandidates, } from "@oscharko-dev/keiko-quality-intelligence";
import { assertValidRunId, sha256Hex } from "@oscharko-dev/keiko-security";
import { createInMemoryQualityIntelligenceLocalStore, loadQualityIntelligenceCandidates, loadQualityIntelligenceRun, recordQualityIntelligenceCandidates, recordQualityIntelligenceRun, } from "@oscharko-dev/keiko-evidence";
import { excerptsByAtomId, runQualityIntelligenceModelRoutedTestDesign, } from "@oscharko-dev/keiko-workflows";
import { makeCapsuleResolver } from "./capsuleAdapter.js";
import { makeFigmaSnapshotLoader, makeFigmaVisionHintProvider } from "./figmaSnapshotAdapter.js";
import { createQiGenerationPort, QiGenerationError } from "./generationPort.js";
import { createQiJudgePort } from "./judgePort.js";
import { resolveQiTestDesignSelection } from "./modelSelection.js";
import { ingestInlineSources, QiIngestionError } from "./runIngestion.js";
const MAX_BODY_BYTES = 2 * 1024 * 1024;
const REQUIREMENTS_ENVELOPE_PREFIX = "qi-src-req-";
const errorResult = (status, code, message) => ({
    status,
    body: { error: { code, message } },
});
// Validate the (already-non-empty) :id path param and map an invalid id to a 400 (mirrors the
// evidence read handlers) instead of letting it reach the store and surface as a generic 500 in the
// outer catch. assertValidRunId rejects separators / `..` / NUL, so a traversal-shaped id never
// reaches a filesystem path. Returns the error result to short-circuit, or null when the id is valid.
function invalidRunIdFormat(id) {
    try {
        assertValidRunId(id);
        return null;
    }
    catch {
        return errorResult(400, "QI_BAD_REQUEST", "Run id is invalid.");
    }
}
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
const readBody = (req) => new Promise((resolve, reject) => {
    const chunks = [];
    let total = 0;
    req.on("data", (chunk) => {
        total += chunk.length;
        if (total > MAX_BODY_BYTES) {
            reject(new Error("body too large"));
            req.resume();
            return;
        }
        chunks.push(chunk);
    });
    req.on("end", () => {
        resolve(Buffer.concat(chunks).toString("utf8"));
    });
    req.on("error", reject);
});
function validateCapsuleSource(label, raw) {
    if (typeof raw.capsuleId !== "string" || raw.capsuleId.trim().length === 0)
        return undefined;
    return { kind: "capsule", label, capsuleId: raw.capsuleId };
}
function validateCapsuleSetSource(label, raw) {
    if (typeof raw.capsuleSetId !== "string" || raw.capsuleSetId.trim().length === 0) {
        return undefined;
    }
    return { kind: "capsule-set", label, capsuleSetId: raw.capsuleSetId };
}
function validateFigmaSnapshotSource(label, raw) {
    if (typeof raw.snapshotRunId !== "string" || raw.snapshotRunId.trim().length === 0) {
        return undefined;
    }
    return { kind: "figma-snapshot", label, snapshotRunId: raw.snapshotRunId };
}
function validateConnectorSource(label, raw) {
    if (raw.kind === "capsule") {
        return validateCapsuleSource(label, raw);
    }
    if (raw.kind === "capsule-set") {
        return validateCapsuleSetSource(label, raw);
    }
    if (raw.kind === "figma-snapshot") {
        return validateFigmaSnapshotSource(label, raw);
    }
    return undefined;
}
function validateSource(raw) {
    if (!isObject(raw) || typeof raw.label !== "string")
        return undefined;
    const label = raw.label;
    if (raw.kind === "requirements" && typeof raw.text === "string") {
        return { kind: "requirements", label, text: raw.text };
    }
    if (raw.kind === "workspace" && typeof raw.path === "string") {
        return { kind: "workspace", label, path: raw.path };
    }
    if (raw.kind === "file" && typeof raw.path === "string") {
        return { kind: "file", label, path: raw.path };
    }
    return validateConnectorSource(label, raw);
}
async function parseSources(req) {
    let raw;
    try {
        raw = await readBody(req);
    }
    catch {
        return {
            ok: false,
            result: errorResult(413, "QI_BODY_TOO_LARGE", "Request body is too large."),
        };
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        return {
            ok: false,
            result: errorResult(400, "QI_BAD_REQUEST", "Request body is not valid JSON."),
        };
    }
    if (!isObject(parsed) || !Array.isArray(parsed.sources) || parsed.sources.length === 0) {
        return {
            ok: false,
            result: errorResult(400, "QI_BAD_REQUEST", "At least one source is required."),
        };
    }
    const sources = [];
    for (const raw_ of parsed.sources) {
        const source = validateSource(raw_);
        if (source === undefined) {
            return {
                ok: false,
                result: errorResult(400, "QI_BAD_SOURCE", "A source entry is malformed."),
            };
        }
        sources.push(source);
    }
    return { ok: true, sources };
}
function buildJudgePortIfAvailable(deps, modelId) {
    try {
        return createQiJudgePort(deps, modelId);
    }
    catch {
        return undefined;
    }
}
function resolveProfile(profileId) {
    if (profileId === undefined || profileId.trim().length === 0)
        return regressionDefault;
    return ALL_POLICY_PROFILES.find((profile) => profile.id === profileId) ?? regressionDefault;
}
function mapCurrentAtomFingerprints(ingestedAtoms) {
    return ingestedAtoms.map((entry) => ({
        atomId: String(entry.atom.id),
        envelopeId: String(entry.atom.sourceEnvelopeId),
        canonicalHashSha256Hex: entry.atom.canonicalHashSha256Hex,
    }));
}
function mapCurrentSourceFingerprints(ingestion) {
    return ingestion.envelopes.map((envelope) => ({
        envelopeId: String(envelope.id),
        integrityHashSha256Hex: envelope.provenance.integrityHashSha256Hex,
    }));
}
function toEvidenceRefs(ingestedAtoms) {
    return Object.freeze(ingestedAtoms.map((entry) => Object.freeze({
        envelopeId: String(entry.atom.sourceEnvelopeId),
        atomId: String(entry.atom.id),
        lifecycleStatus: entry.atom.lifecycleStatus,
    })));
}
function rowToCandidate(row, runId) {
    return {
        id: QualityIntelligence.asQualityIntelligenceTestCaseId(row.id),
        runId: QualityIntelligence.asQualityIntelligenceRunId(runId),
        derivedFromAtomIds: row.derivedFromAtomIds.map((atomId) => QualityIntelligence.asQualityIntelligenceEvidenceAtomId(atomId)),
        title: row.title,
        preconditions: row.preconditions,
        steps: row.steps,
        expectedResults: row.expectedResults,
        priority: row.priority,
        riskClass: row.riskClass,
        tags: row.tags,
        status: row.status,
    };
}
function toCandidateFindingRow(finding) {
    return {
        id: String(finding.id),
        kind: finding.kind,
        severity: finding.severity,
        summaryRedacted: finding.summary,
        ...(finding.candidateId !== undefined ? { candidateId: String(finding.candidateId) } : {}),
    };
}
function buildCoverageGapFindingRow(runId, atomStatus, ordinal, excerpt) {
    const payload = ["v1-cov-gap", String(runId), String(atomStatus.atomId), String(ordinal)].join("");
    // Mirror the initial-run severity model (modelRoutedTestDesign): a zero-coverage requirement is
    // the headline audit gap (high); a weakly-covered one is a softer "strengthen this" signal (low).
    const severity = atomStatus.status === "uncovered" ? "high" : "low";
    // Mirror the initial-run summary shape (#790): name the requirement via its redacted excerpt,
    // not just the opaque atom id, so the regenerated run's gap findings stay auditor-readable.
    const atomLabel = excerpt === undefined
        ? `Atom ${String(atomStatus.atomId)}`
        : `Atom ${String(atomStatus.atomId)} ("${excerpt}")`;
    const summaryRedacted = atomStatus.status === "uncovered"
        ? `${atomLabel} has no tracing test (uncovered).`
        : `${atomLabel} is only weakly covered (no dedicated test traces to it).`;
    return Object.freeze({
        id: `qi-finding-${sha256Hex(payload).slice(0, 32)}`,
        kind: "coverage-gap",
        severity,
        summaryRedacted,
    });
}
function toCoverageMatrix(statuses, excerptByAtomId) {
    return Object.freeze(statuses.map((status) => {
        const excerpt = excerptByAtomId.get(String(status.atomId));
        return Object.freeze({
            atomId: String(status.atomId),
            status: status.status,
            confidence: status.confidence,
            coveringCandidateIds: Object.freeze(status.coveringCandidateIds.map(String)),
            ...(excerpt !== undefined ? { requirementExcerptRedacted: excerpt } : {}),
        });
    }));
}
function filteredJudgeFindings(findings, candidateIds) {
    return findings.filter((finding) => finding.kind === "test-quality" &&
        finding.candidateId !== undefined &&
        candidateIds.has(finding.candidateId));
}
function selectedQualityScore(args) {
    const { oldManifest, regeneratedManifest, preservedCount, regeneratedCount } = args;
    if (preservedCount > 0 && regeneratedCount > 0)
        return null;
    if (regeneratedCount > 0)
        return regeneratedManifest?.qualityScore ?? null;
    if (preservedCount > 0)
        return oldManifest.qualityScore ?? null;
    return null;
}
function loadManifestForDrift(id, evidenceDir) {
    const manifest = loadQualityIntelligenceRun(id, { evidenceDir });
    return manifest === undefined
        ? {
            ok: false,
            result: errorResult(404, "QI_NOT_FOUND", "Quality Intelligence run not found."),
        }
        : { ok: true, manifest };
}
function ingestSourcesForDrift(sources, ingestRunId, deps) {
    try {
        return {
            ok: true,
            ingestion: ingestInlineSources({
                request: { sources },
                runId: ingestRunId,
                registeredAt: new Date().toISOString(),
                capsuleResolver: makeCapsuleResolver(deps),
                figmaSnapshotLoader: makeFigmaSnapshotLoader(deps),
                figmaVision: makeFigmaVisionHintProvider(deps),
            }),
        };
    }
    catch (error) {
        const code = error instanceof QiIngestionError ? error.code : "QI_INGESTION_FAILED";
        const message = error instanceof QiIngestionError ? error.message : "Source ingestion failed.";
        return { ok: false, result: errorResult(400, code, message) };
    }
}
function buildDriftStaleness(manifest, oldCandidates, ingestion) {
    return compareStaleness({
        oldFingerprints: manifest.sourceFingerprints ?? [],
        evidenceRefs: manifest.evidenceRefs.map((ref) => ({
            envelopeId: ref.envelopeId,
            atomId: ref.atomId,
        })),
        candidates: oldCandidates.map((candidate) => ({
            id: candidate.id,
            derivedFromAtomIds: candidate.derivedFromAtomIds,
        })),
        currentFingerprints: mapCurrentSourceFingerprints(ingestion),
        currentAtomFingerprints: mapCurrentAtomFingerprints(ingestion.ingestedAtoms),
        ...(manifest.atomFingerprints !== undefined
            ? { oldAtomFingerprints: manifest.atomFingerprints }
            : {}),
    });
}
function buildDriftContext(sources, manifest, ingestion, oldArtifact) {
    const oldCandidates = oldArtifact?.candidates ?? [];
    return {
        sources,
        manifest,
        ingestion,
        staleness: buildDriftStaleness(manifest, oldCandidates, ingestion),
        oldCandidates,
        oldEditedRevisions: oldArtifact?.editedRevisions ?? [],
    };
}
async function computeDrift(req, evidenceDir, id, ingestRunId, deps) {
    const parsed = await parseSources(req);
    if (!parsed.ok)
        return { ok: false, result: parsed.result };
    const loaded = loadManifestForDrift(id, evidenceDir);
    if (!loaded.ok)
        return { ok: false, result: loaded.result };
    const ingested = ingestSourcesForDrift(parsed.sources, ingestRunId, deps);
    if (!ingested.ok)
        return { ok: false, result: ingested.result };
    const oldArtifact = loadQualityIntelligenceCandidates(id, { evidenceDir });
    return {
        ok: true,
        value: buildDriftContext(parsed.sources, loaded.manifest, ingested.ingestion, oldArtifact),
    };
}
function collectStaleIds(staleness) {
    return new Set([
        ...staleness.changedStale.map((reason) => reason.candidateId),
        ...staleness.orphanedStale.map((reason) => reason.candidateId),
    ]);
}
function buildPreservedState(drift, staleIds) {
    const preservedCandidates = drift.oldCandidates.filter((candidate) => !staleIds.has(candidate.id));
    const preservedIds = new Set(preservedCandidates.map((candidate) => candidate.id));
    return {
        preservedCandidates,
        preservedEditedRevisions: drift.oldEditedRevisions.filter((revision) => preservedIds.has(revision.candidateId)),
    };
}
function looksLikeLegacyRequirementsFallback(drift, staleIds) {
    if (staleIds.size === 0 || drift.manifest.atomFingerprints !== undefined)
        return false;
    if (!(drift.sources.length > 0 && drift.sources.every((source) => source.kind === "requirements"))) {
        return false;
    }
    const evidenceRefMap = new Map(drift.manifest.evidenceRefs.map((ref) => [ref.atomId, ref.envelopeId]));
    return drift.oldCandidates.some((candidate) => staleIds.has(candidate.id) &&
        candidate.derivedFromAtomIds.some((atomId) => evidenceRefMap.get(atomId)?.startsWith("qi-src-")));
}
function buildCurrentAtomIndexes(ingestion) {
    const byId = new Map(ingestion.ingestedAtoms.map((entry) => [String(entry.atom.id), entry]));
    const byEnvelope = new Map();
    for (const entry of ingestion.ingestedAtoms) {
        const envelopeId = String(entry.atom.sourceEnvelopeId);
        const current = byEnvelope.get(envelopeId);
        if (current === undefined) {
            byEnvelope.set(envelopeId, [entry]);
        }
        else {
            current.push(entry);
        }
    }
    return {
        byId,
        byEnvelope,
        envelopeIds: new Set(ingestion.envelopes.map((envelope) => String(envelope.id))),
    };
}
function buildOldAtomIndexes(atomFingerprints) {
    const byId = new Map(atomFingerprints.map((fp) => [
        fp.atomId,
        { envelopeId: fp.envelopeId, canonicalHashSha256Hex: fp.canonicalHashSha256Hex },
    ]));
    const idsByEnvelope = new Map();
    for (const fp of atomFingerprints) {
        const ids = idsByEnvelope.get(fp.envelopeId);
        if (ids === undefined) {
            idsByEnvelope.set(fp.envelopeId, new Set([fp.atomId]));
        }
        else {
            ids.add(fp.atomId);
        }
    }
    return { byId, idsByEnvelope };
}
function addReplacementRequirementAtoms(envelopeId, current, old, atomIdsToRegenerate) {
    const oldIds = old.idsByEnvelope.get(envelopeId) ?? new Set();
    for (const replacement of current.byEnvelope.get(envelopeId) ?? []) {
        const replacementId = String(replacement.atom.id);
        if (!oldIds.has(replacementId))
            atomIdsToRegenerate.add(replacementId);
    }
}
function addRegenerationAtomsForCandidate(candidate, current, old, atomIdsToRegenerate) {
    for (const atomId of candidate.derivedFromAtomIds) {
        const oldAtom = old.byId.get(atomId);
        const currentAtom = current.byId.get(atomId);
        if (oldAtom !== undefined &&
            currentAtom !== undefined &&
            currentAtom.atom.canonicalHashSha256Hex !== oldAtom.canonicalHashSha256Hex) {
            atomIdsToRegenerate.add(String(currentAtom.atom.id));
            continue;
        }
        if (oldAtom === undefined || !current.envelopeIds.has(oldAtom.envelopeId))
            continue;
        if (!oldAtom.envelopeId.startsWith(REQUIREMENTS_ENVELOPE_PREFIX))
            continue;
        addReplacementRequirementAtoms(oldAtom.envelopeId, current, old, atomIdsToRegenerate);
    }
}
function collectAtomsToRegenerate(drift, staleIds) {
    const current = buildCurrentAtomIndexes(drift.ingestion);
    const old = buildOldAtomIndexes(drift.manifest.atomFingerprints ?? []);
    const atomIdsToRegenerate = new Set();
    for (const candidate of drift.oldCandidates) {
        if (!staleIds.has(candidate.id))
            continue;
        addRegenerationAtomsForCandidate(candidate, current, old, atomIdsToRegenerate);
    }
    return drift.ingestion.ingestedAtoms.filter((entry) => atomIdsToRegenerate.has(String(entry.atom.id)));
}
function narrowRegeneration(drift) {
    const staleIds = collectStaleIds(drift.staleness);
    const preserved = buildPreservedState(drift, staleIds);
    const legacyRequirementsFallback = looksLikeLegacyRequirementsFallback(drift, staleIds);
    if (legacyRequirementsFallback || staleIds.size === 0) {
        return {
            staleIds,
            atomsToRegenerate: Object.freeze([]),
            preservedCandidates: preserved.preservedCandidates,
            preservedEditedRevisions: preserved.preservedEditedRevisions,
            legacyRequirementsFallback,
        };
    }
    return {
        staleIds,
        atomsToRegenerate: collectAtomsToRegenerate(drift, staleIds),
        preservedCandidates: preserved.preservedCandidates,
        preservedEditedRevisions: preserved.preservedEditedRevisions,
        legacyRequirementsFallback: false,
    };
}
function regenWorkflowDeps(deps, target, evidenceStore, capture) {
    return {
        sink: { emit: () => undefined },
        evidenceStore,
        candidatesSink: {
            record: (cands, generatedAt) => {
                capture(cands, generatedAt);
            },
        },
        generate: createQiGenerationPort(deps, target),
        ...(target.kind === "model" ? { judge: buildJudgePortIfAvailable(deps, target.modelId) } : {}),
    };
}
function buildScopedRegenPlan(newRunId, requestedAt) {
    return {
        id: QualityIntelligence.asQualityIntelligenceRunId(newRunId),
        requestedAt,
        plannerKind: "model-routed",
        stages: [],
    };
}
async function executeScopedWorkflow(args) {
    const { deps, target, evidenceStore, capture, plan, ingestion, atomsToRegenerate, profile } = args;
    try {
        const summary = await runQualityIntelligenceModelRoutedTestDesign({
            plan,
            envelopes: ingestion.envelopes,
            ingestedAtoms: atomsToRegenerate,
            provenanceRefs: ingestion.provenanceRefs,
            profile,
        }, regenWorkflowDeps(deps, target, evidenceStore, capture));
        return summary.status === "succeeded"
            ? null
            : errorResult(500, "QI_REGEN_FAILED", "Scoped regeneration did not succeed.");
    }
    catch (error) {
        const code = error instanceof QiGenerationError ? error.code : "QI_REGEN_FAILED";
        const message = error instanceof QiGenerationError ? error.message : "Scoped regeneration failed.";
        return errorResult(500, code, message);
    }
}
function finalizeScopedWorkflow(evidenceStore, newRunId, generatedCandidates, generatedAt) {
    const manifest = evidenceStore.load(newRunId);
    if (manifest === undefined || generatedAt === undefined) {
        return {
            ok: false,
            result: errorResult(500, "QI_REGEN_FAILED", "Scoped regeneration did not persist in memory."),
        };
    }
    return {
        ok: true,
        value: { manifest, candidates: generatedCandidates, generatedAt },
    };
}
async function runScopedEphemeral(args) {
    const { deps, target, newRunId, requestedAt, ingestion, atomsToRegenerate, profile } = args;
    const evidenceStore = createInMemoryQualityIntelligenceLocalStore();
    let generatedCandidates = [];
    let generatedAt;
    const failure = await executeScopedWorkflow({
        deps,
        target,
        evidenceStore,
        capture: (cands, ts) => {
            generatedCandidates = [...cands];
            generatedAt = ts;
        },
        plan: buildScopedRegenPlan(newRunId, requestedAt),
        ingestion,
        atomsToRegenerate,
        profile,
    });
    if (failure !== null)
        return { ok: false, result: failure };
    return finalizeScopedWorkflow(evidenceStore, newRunId, generatedCandidates, generatedAt);
}
function buildMergedCandidates(newRunId, preservedCandidates, regeneratedCandidates) {
    return [
        ...preservedCandidates.map((candidate) => rowToCandidate(candidate, newRunId)),
        ...regeneratedCandidates,
    ];
}
function buildCoverageArtifacts(runId, ingestion, mergedCandidates) {
    const atoms = ingestion.ingestedAtoms.map((entry) => entry.atom);
    const coverageMap = buildCoverageMap({ runId, atoms, candidates: mergedCandidates });
    const atomStatuses = buildAtomCoverageStatuses(atoms, coverageMap);
    const excerptByAtomId = excerptsByAtomId(ingestion.ingestedAtoms);
    return {
        coverageMatrix: toCoverageMatrix(atomStatuses, excerptByAtomId),
        coverageGapRows: atomStatuses
            .map((status, index) => status.status === "covered"
            ? null
            : buildCoverageGapFindingRow(runId, status, index, excerptByAtomId.get(String(status.atomId))))
            .filter((row) => row !== null),
    };
}
function buildMergedFindings(args) {
    const preservedIds = new Set(args.preservedCandidates.map((candidate) => candidate.id));
    const regeneratedIds = new Set(args.regeneratedCandidates.map((candidate) => String(candidate.id)));
    const preservedJudgeRows = filteredJudgeFindings(args.oldManifest.findings, preservedIds);
    const regeneratedJudgeRows = args.regeneratedManifest === undefined
        ? []
        : filteredJudgeFindings(args.regeneratedManifest.findings, regeneratedIds);
    return Object.freeze([
        ...args.coverageGapRows,
        ...validateCandidates(args.runId, args.mergedCandidates).map(toCandidateFindingRow),
        ...preservedJudgeRows,
        ...regeneratedJudgeRows,
    ]);
}
function buildMergedRunRecord(args) {
    const { newRunId, requestedAt, profile, oldManifest, ingestion, preservedCandidates } = args;
    return {
        runId: newRunId,
        planAt: requestedAt,
        completedAt: args.completedAt,
        status: "succeeded",
        policyProfileIds: [profile.id],
        retentionPolicyId: oldManifest.retentionPolicyId,
        modelGatewayCallCount: args.regeneratedManifest?.modelGatewayCallCount ?? 0,
        totals: {
            candidates: args.preservedCandidates.length + args.regeneratedCandidates.length,
            findings: args.findings.length,
            exports: 0,
        },
        findings: args.findings,
        exports: Object.freeze([]),
        evidenceRefs: toEvidenceRefs(ingestion.ingestedAtoms),
        provenanceRefs: ingestion.provenanceRefs,
        coverageMatrix: args.coverageMatrix,
        qualityScore: selectedQualityScore({
            oldManifest,
            regeneratedManifest: args.regeneratedManifest,
            preservedCount: preservedCandidates.length,
            regeneratedCount: args.regeneratedCandidates.length,
        }),
        sourceFingerprints: mapCurrentSourceFingerprints(ingestion),
        atomFingerprints: mapCurrentAtomFingerprints(ingestion.ingestedAtoms),
        ...optionalModelFields(args.regeneratedManifest),
    };
}
// Carry forward the regenerated manifest's optional model provenance (modelId / modelParameters /
// seedUsed) only when present, so the merged record omits — rather than nulls — an absent field.
function optionalModelFields(regeneratedManifest) {
    if (regeneratedManifest === undefined)
        return {};
    return {
        ...(regeneratedManifest.modelId !== undefined ? { modelId: regeneratedManifest.modelId } : {}),
        ...(regeneratedManifest.modelParameters !== undefined
            ? { modelParameters: regeneratedManifest.modelParameters }
            : {}),
        ...(regeneratedManifest.seedUsed !== undefined
            ? { seedUsed: regeneratedManifest.seedUsed }
            : {}),
    };
}
function recordMergedManifest(evidenceDir, args) {
    recordQualityIntelligenceRun(buildMergedRunRecord(args), { evidenceDir });
}
function recordMergedCandidatesArtifact(args) {
    recordQualityIntelligenceCandidates({
        runId: args.newRunId,
        generatedAt: args.completedAt,
        candidates: args.mergedCandidates,
        editedRevisions: args.preservedEditedRevisions,
        evidenceDir: args.evidenceDir,
        redact: args.deps.redactor,
    });
}
function persistMergedRun(args) {
    const mergedCandidates = buildMergedCandidates(args.newRunId, args.preservedCandidates, args.regeneratedCandidates);
    const runId = QualityIntelligence.asQualityIntelligenceRunId(args.newRunId);
    const coverage = buildCoverageArtifacts(runId, args.ingestion, mergedCandidates);
    const findings = buildMergedFindings({
        runId,
        mergedCandidates,
        coverageGapRows: coverage.coverageGapRows,
        oldManifest: args.oldManifest,
        preservedCandidates: args.preservedCandidates,
        regeneratedCandidates: args.regeneratedCandidates,
        regeneratedManifest: args.regeneratedManifest,
    });
    recordMergedManifest(args.evidenceDir, {
        newRunId: args.newRunId,
        requestedAt: args.requestedAt,
        profile: args.profile,
        oldManifest: args.oldManifest,
        ingestion: args.ingestion,
        preservedCandidates: args.preservedCandidates,
        regeneratedCandidates: args.regeneratedCandidates,
        regeneratedManifest: args.regeneratedManifest,
        completedAt: args.completedAt,
        findings,
        coverageMatrix: coverage.coverageMatrix,
    });
    recordMergedCandidatesArtifact({
        deps: args.deps,
        evidenceDir: args.evidenceDir,
        newRunId: args.newRunId,
        completedAt: args.completedAt,
        mergedCandidates,
        preservedEditedRevisions: args.preservedEditedRevisions,
    });
}
function immediateRegenerationResult(id, drift, narrowed) {
    if (narrowed.staleIds.size === 0) {
        return {
            status: 200,
            body: { runId: id, regeneratedCount: 0, preservedCount: drift.oldCandidates.length },
        };
    }
    if (narrowed.legacyRequirementsFallback) {
        return errorResult(409, "QI_REGEN_LEGACY_REQUIREMENTS_UNSUPPORTED", "This run predates atom-level requirements drift metadata. Start a new QI run against the current requirements sources instead.");
    }
    // Data-loss guard: targeted regeneration must never turn a non-empty run into an empty one. If
    // every candidate is stale yet nothing maps to a regeneratable atom (preserved == 0 AND nothing to
    // regenerate), persisting the merge would silently drop the entire run. This is the catastrophic
    // shape an atom-id scheme drift would take; fail closed with an actionable error instead (Epic
    // #735 drift correctness). The legitimate "some tests orphaned, some preserved" case keeps
    // preserved > 0 and is unaffected; a no-drift run already returned above.
    if (narrowed.preservedCandidates.length === 0 && narrowed.atomsToRegenerate.length === 0) {
        return errorResult(409, "QI_REGEN_WOULD_EMPTY", "Regenerating the stale tests would remove every test in this run because the current source no longer maps to any of them. Start a fresh QI run against the current source instead.");
    }
    return null;
}
function resolveScopedRegenerationTarget(deps) {
    const selection = resolveQiTestDesignSelection(deps);
    return selection.kind === "model"
        ? { kind: "model", modelId: selection.modelId }
        : { kind: "baseline" };
}
async function regenerateCandidateSlice(args) {
    if (args.narrowed.atomsToRegenerate.length === 0) {
        return {
            ok: true,
            value: { manifest: undefined, candidates: [], completedAt: new Date().toISOString() },
        };
    }
    const outcome = await runScopedEphemeral({
        deps: args.deps,
        target: resolveScopedRegenerationTarget(args.deps),
        newRunId: args.newRunId,
        requestedAt: args.requestedAt,
        ingestion: args.drift.ingestion,
        atomsToRegenerate: args.narrowed.atomsToRegenerate,
        profile: args.profile,
    });
    return outcome.ok
        ? {
            ok: true,
            value: {
                manifest: outcome.value.manifest,
                candidates: outcome.value.candidates,
                completedAt: outcome.value.generatedAt,
            },
        }
        : outcome;
}
async function regenerateFromDrift(args) {
    const { deps, id, evidenceDir, newRunId, requestedAt, drift } = args;
    const narrowed = narrowRegeneration(drift);
    const immediate = immediateRegenerationResult(id, drift, narrowed);
    if (immediate !== null)
        return immediate;
    const profile = resolveProfile(drift.manifest.policyProfileIds[0]);
    const regenerated = await regenerateCandidateSlice({
        deps,
        newRunId,
        requestedAt,
        drift,
        narrowed,
        profile,
    });
    if (!regenerated.ok)
        return regenerated.result;
    persistMergedRun({
        deps,
        evidenceDir,
        newRunId,
        requestedAt,
        profile,
        oldManifest: drift.manifest,
        ingestion: drift.ingestion,
        preservedCandidates: narrowed.preservedCandidates,
        preservedEditedRevisions: narrowed.preservedEditedRevisions,
        regeneratedCandidates: regenerated.value.candidates,
        regeneratedManifest: regenerated.value.manifest,
        completedAt: regenerated.value.completedAt,
    });
    return {
        status: 200,
        body: {
            runId: newRunId,
            regeneratedCount: regenerated.value.candidates.length,
            preservedCount: narrowed.preservedCandidates.length,
        },
    };
}
export async function handleQiReCheck(ctx, deps) {
    const { id } = ctx.params;
    if (id === undefined || id.trim().length === 0) {
        return errorResult(400, "QI_BAD_REQUEST", "Run id is required.");
    }
    const invalidId = invalidRunIdFormat(id);
    if (invalidId !== null)
        return invalidId;
    const evidenceDir = deps.evidenceDir;
    if (evidenceDir === undefined) {
        return errorResult(500, "QI_NO_EVIDENCE_DIR", "The evidence directory is not configured.");
    }
    try {
        const drift = await computeDrift(ctx.req, evidenceDir, id, `qi-recheck-${id}`, deps);
        if (!drift.ok)
            return drift.result;
        const { staleness } = drift.value;
        return {
            status: 200,
            body: {
                runId: id,
                staleCount: staleness.changedStale.length + staleness.orphanedStale.length,
                fresh: staleness.fresh,
                changedStale: staleness.changedStale,
                orphanedStale: staleness.orphanedStale,
            },
        };
    }
    catch {
        return errorResult(500, "QI_RECHECK_FAILED", "Failed to inspect the current sources for drift.");
    }
}
export async function handleQiRegenerateStale(ctx, deps) {
    const { id } = ctx.params;
    if (id === undefined || id.trim().length === 0) {
        return errorResult(400, "QI_BAD_REQUEST", "Run id is required.");
    }
    const invalidId = invalidRunIdFormat(id);
    if (invalidId !== null)
        return invalidId;
    const evidenceDir = deps.evidenceDir;
    if (evidenceDir === undefined) {
        return errorResult(500, "QI_NO_EVIDENCE_DIR", "The evidence directory is not configured.");
    }
    const newRunId = `qi-run-${randomUUID()}`;
    const requestedAt = new Date().toISOString();
    try {
        const drift = await computeDrift(ctx.req, evidenceDir, id, newRunId, deps);
        if (!drift.ok)
            return drift.result;
        return await regenerateFromDrift({
            deps,
            id,
            evidenceDir,
            newRunId,
            requestedAt,
            drift: drift.value,
        });
    }
    catch {
        return errorResult(500, "QI_REGEN_FAILED", "Failed to regenerate stale candidates.");
    }
}
export const QI_RECHECK_ROUTE_GROUP = [
    {
        method: "POST",
        pattern: "/api/quality-intelligence/runs/:id/re-check",
        handler: handleQiReCheck,
    },
    {
        method: "POST",
        pattern: "/api/quality-intelligence/runs/:id/regenerate-stale",
        handler: handleQiRegenerateStale,
    },
];
