// engine/workers/handlers/auto-drift.ts — Auto drift detection on file changes (SPEC-225)
import { relative, extname } from 'node:path';
import { readdir, stat } from 'node:fs/promises';
import { specStore } from '../../../storage/index.js';
import { hashProjectPath } from '../../../storage/base-store.js';
import { DriftCacheStore } from '../../../storage/drift-cache-store.js';
import { emitAutopilotEvent } from '../../autopilot/event-bus.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const WORKER_NAME = 'auto-drift';
/** Default drift threshold: flag specs with score >= 0.3 */
const DEFAULT_DRIFT_THRESHOLD = 0.3;
/** Source file extensions tracked for heuristic scoring */
const SOURCE_EXTENSIONS = new Set([
    '.ts',
    '.js',
    '.tsx',
    '.jsx',
    '.py',
    '.go',
    '.rs',
    '.java',
    '.kt',
]);
/** Directories to skip when counting total source files */
const IGNORE_DIRS = new Set([
    'node_modules',
    '.git',
    'dist',
    'build',
    '__pycache__',
    'target',
    '.venv',
    'vendor',
    '.next',
    '.nuxt',
    'coverage',
    '.turbo',
    '.cache',
    'tests',
    '__tests__',
]);
// ---------------------------------------------------------------------------
// Heuristic drift scorer
// ---------------------------------------------------------------------------
/**
 * Lightweight drift heuristic: ratio of changedFiles that are source files
 * relative to an estimated total source file count for the project.
 *
 * Returns a score in [0, 1] where 1 means maximum estimated drift.
 */
async function computeHeuristicDriftScore(projectDir, changedFiles) {
    const relevantChanged = changedFiles.filter((f) => SOURCE_EXTENSIONS.has(extname(f)));
    if (relevantChanged.length === 0) {
        return 0;
    }
    const totalFiles = await countSourceFiles(projectDir);
    if (totalFiles === 0) {
        return 0;
    }
    // Score = changed source files / total source files, capped at 1
    return Math.min(relevantChanged.length / totalFiles, 1);
}
async function countSourceFiles(dir) {
    let count = 0;
    let entries;
    try {
        entries = await readdir(dir);
    }
    catch {
        return 0;
    }
    for (const entry of entries) {
        if (IGNORE_DIRS.has(entry)) {
            continue;
        }
        const fullPath = `${dir}/${entry}`;
        let st;
        try {
            st = await stat(fullPath);
        }
        catch {
            continue;
        }
        if (st.isDirectory()) {
            count += await countSourceFiles(fullPath);
        }
        else if (st.isFile() && SOURCE_EXTENSIONS.has(extname(entry))) {
            count++;
        }
    }
    return count;
}
// ---------------------------------------------------------------------------
// Public handler
// ---------------------------------------------------------------------------
/**
 * Worker handler for auto-drift detection.
 *
 * Triggered on source file changes. For each spec in 'implementing' or 'done'
 * state, computes a lightweight drift score. If score >= PLANU_DRIFT_THRESHOLD,
 * persists the result to DriftCacheStore with shown=false so the next tool call
 * can display a banner warning.
 *
 * Respects PLANU_AUTO_DRIFT=false to disable entirely.
 */
export async function runAutoDrift(projectDir, changedFiles) {
    const startedAt = new Date().toISOString();
    const start = Date.now();
    if (process.env.PLANU_AUTO_DRIFT === 'false') {
        return makeResult(WORKER_NAME, startedAt, start, 'skipped', [], 0);
    }
    const effectiveChanged = changedFiles ?? [];
    const projectId = hashProjectPath(projectDir);
    let specs;
    try {
        specs = await specStore.listSpecs(projectId);
    }
    catch {
        return makeResult(WORKER_NAME, startedAt, start, 'skipped', [], 0);
    }
    const activeSpecs = specs.filter((s) => s.status === 'implementing' || s.status === 'done');
    if (activeSpecs.length === 0) {
        return makeResult(WORKER_NAME, startedAt, start, 'success', [], effectiveChanged.length);
    }
    const findings = await buildDriftFindings(projectDir, effectiveChanged, activeSpecs, projectId);
    return makeResult(WORKER_NAME, startedAt, start, 'success', findings, effectiveChanged.length);
}
function makeResult(workerName, startedAt, start, status, findings, analyzedFiles) {
    return {
        workerName,
        startedAt,
        completedAt: new Date().toISOString(),
        durationMs: Date.now() - start,
        status,
        findings,
        analyzedFiles,
    };
}
async function buildDriftFindings(projectDir, effectiveChanged, activeSpecs, projectId) {
    const threshold = parseDriftThreshold();
    const cacheStore = new DriftCacheStore(projectId);
    const driftScore = await computeHeuristicDriftScore(projectDir, effectiveChanged);
    const driftedFiles = effectiveChanged
        .filter((f) => SOURCE_EXTENSIONS.has(extname(f)))
        .map((f) => relative(projectDir, f));
    const findings = [];
    for (const spec of activeSpecs) {
        if (driftScore >= threshold) {
            await cacheStore.upsert({
                specId: spec.id,
                driftScore,
                driftedFiles,
                cachedAt: new Date().toISOString(),
            });
            // Wire drift:detected event so autopilot trigger-rules can react
            emitAutopilotEvent({
                name: 'drift:detected',
                projectPath: projectDir,
                projectId,
                specId: spec.id,
                payload: { driftScore, driftLevel: driftScore >= 0.7 ? 'high' : 'warning' },
            });
            findings.push({
                severity: driftScore >= 0.7 ? 'high' : 'warning',
                file: '',
                message: `Spec ${spec.id} may have drifted (score: ${driftScore.toFixed(2)}, threshold: ${threshold.toFixed(2)})`,
                suggestion: `Run detect_drift for spec ${spec.id} to see full drift report`,
                category: 'auto-drift',
            });
        }
    }
    return findings;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function parseDriftThreshold() {
    const env = process.env.PLANU_DRIFT_THRESHOLD;
    if (env === undefined || env === '') {
        return DEFAULT_DRIFT_THRESHOLD;
    }
    const parsed = parseFloat(env);
    return Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : DEFAULT_DRIFT_THRESHOLD;
}
//# sourceMappingURL=auto-drift.js.map