// engine/hooks/handlers/on-impl-change.ts — on-impl-change hook handler (SPEC-137)
// Triggered when source implementation files (src/**/*.ts) change.
// Analyzes related specs, updates completion percentage, auto-reconciles if all criteria met.
import { hashProjectPath } from '../../../storage/base-store.js';
const UPDATE_STATUS_MODULE = '../../../tools/update-status/index.js';
// ---------------------------------------------------------------------------
// Rate limiter (per spec, 5 min cooldown)
// ---------------------------------------------------------------------------
const RATE_LIMIT_MS = 5 * 60 * 1000; // 5 minutes
// Key format: `${projectPath}:${specId}` for per-project isolation
const lastAnalysisTimestamps = new Map();
/**
 * Build the composite key for per-project rate limiting.
 */
function makeRateLimitKey(projectPath, specId) {
    return `${projectPath}:${specId}`;
}
/**
 * Check if the spec is within rate limit window for the given project.
 * Returns true if rate-limited (should skip), false if should proceed.
 */
export function isRateLimited(specId, nowMs = Date.now(), projectPath = '') {
    const key = makeRateLimitKey(projectPath, specId);
    const last = lastAnalysisTimestamps.get(key);
    if (last === undefined) {
        return false;
    }
    return nowMs - last < RATE_LIMIT_MS;
}
/**
 * Record analysis timestamp for a spec in the given project.
 */
export function recordAnalysisTimestamp(specId, nowMs = Date.now(), projectPath = '') {
    lastAnalysisTimestamps.set(makeRateLimitKey(projectPath, specId), nowMs);
}
/**
 * Clear all rate limit timestamps (for testing).
 */
export function clearRateLimitTimestamps() {
    lastAnalysisTimestamps.clear();
}
// ---------------------------------------------------------------------------
// Pattern matching
// ---------------------------------------------------------------------------
/**
 * Returns true for src/**\/*.ts files, excluding test files.
 * Test files are identified by the .test.ts or .spec.ts suffix.
 */
export function isSourceFile(filePath) {
    if (!filePath.startsWith('src/')) {
        return false;
    }
    if (!filePath.endsWith('.ts')) {
        return false;
    }
    if (filePath.endsWith('.test.ts') || filePath.endsWith('.spec.ts')) {
        return false;
    }
    return true;
}
/**
 * Find specs related to a changed file by matching file path patterns.
 * Skips specs in terminal states (done, discarded).
 */
export function findRelatedSpecs(filePath, specs) {
    const related = [];
    const filePathLower = filePath.toLowerCase();
    for (const spec of specs) {
        // Skip specs in terminal states
        if (spec.status === 'done' || spec.status === 'discarded') {
            continue;
        }
        // Match by criterion paths (glob patterns declared in the spec)
        if (spec.criterionPaths && spec.criterionPaths.length > 0) {
            const matches = spec.criterionPaths.some((pattern) => {
                const normalized = pattern.toLowerCase().replace(/\*+/g, '');
                return filePathLower.includes(normalized) || normalized.includes(filePathLower);
            });
            if (matches) {
                related.push(spec);
                continue;
            }
        }
        // Match by spec slug/id fragments in the file path
        const specSlug = spec.id
            .toLowerCase()
            .replace('spec-', '')
            .replace(/[^a-z0-9]/g, '-');
        if (specSlug.length > 2 && filePathLower.includes(specSlug)) {
            related.push(spec);
        }
    }
    return related;
}
// ---------------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------------
/**
 * Handle an implementation file change event.
 * Fire-and-forget — all errors are caught internally.
 * When specs array and analyzeSpec function are provided, analyzes related specs.
 * When autoMarkDone is true (default), specs reaching 100% are automatically
 * transitioned to 'done' status via the spec store.
 */
export async function handleOnImplChange(event, projectPath, specs, analyzeSpec, autoMarkDone = true) {
    try {
        if (!isSourceFile(event.path)) {
            return;
        }
        console.warn(`[hooks] Implementation change detected: ${event.path} (${event.type}) in ${projectPath}`);
        if (!specs || specs.length === 0 || !analyzeSpec) {
            return;
        }
        const relatedSpecs = findRelatedSpecs(event.path, specs);
        if (relatedSpecs.length === 0) {
            return;
        }
        const now = Date.now();
        const specsToAnalyze = relatedSpecs.filter((s) => !isRateLimited(s.id, now, projectPath));
        if (specsToAnalyze.length === 0) {
            return;
        }
        const results = [];
        let autoReconciled = false;
        for (const spec of specsToAnalyze) {
            recordAnalysisTimestamp(spec.id, now, projectPath);
            const result = await analyzeSpec(spec, projectPath);
            results.push({
                specId: spec.id,
                completionPct: result.completionPct,
                progressUpdated: result.progressUpdated,
                criterionResults: result.criterionResults,
            });
            if (result.completionPct === 100) {
                autoReconciled = true;
                console.warn(`[hooks] Auto-reconcile triggered for ${spec.id} (100% completion)`);
                if (autoMarkDone) {
                    await markSpecDone(spec.id, projectPath);
                }
            }
        }
        return {
            changedFile: event.path,
            specsAnalyzed: results,
            autoReconciled,
        };
    }
    catch (err) {
        console.warn(`[hooks] on-impl-change error for ${event.path}:`, err);
        return undefined;
    }
}
// ---------------------------------------------------------------------------
// Auto-done helper
// ---------------------------------------------------------------------------
/**
 * Mark a spec as 'done' in the spec store.
 * Errors are logged but not re-thrown so the hook pipeline continues.
 */
async function markSpecDone(specId, projectPath) {
    try {
        const projectId = hashProjectPath(projectPath);
        // SPEC-720: Route through handleUpdateStatus so all gates (DoD/validate/QA) run.
        const updateStatusModule = (await import(UPDATE_STATUS_MODULE));
        const result = await updateStatusModule.handleUpdateStatus({
            specId,
            status: 'done',
            projectId,
            projectPath,
            trigger: 'fs-hook',
            actor: 'system',
        });
        if (result.isError) {
            const reason = result.content[0]?.type === 'text' ? result.content[0].text : 'gate blocked';
            console.warn(`[hooks] Auto-done blocked for ${specId} (gates): ${reason}`);
        }
        else {
            console.warn(`[hooks] Auto-done: spec ${specId} marked as 'done' via update_status gates`);
        }
    }
    catch (err) {
        console.warn(`[hooks] Auto-done failed for ${specId}:`, err);
    }
}
//# sourceMappingURL=on-impl-change.js.map