// tools/detect-drift.ts — CI-embedded continuous compliance checking
// Uses the validator engine's detectDrift() to compare spec vs code state
// and produce reports in multiple formats (human, CI JSON, GitHub annotations).
import { readFile } from 'node:fs/promises';
import { specStore, knowledgeStore } from '../storage/index.js';
import { detectDrift } from '../engine/validator.js';
import { t, ti } from '../i18n/index.js';
import { detectPIIInSpec } from '../engine/pii-detector.js';
import { detectEventSchemaDrift } from './detect-drift-event.js';
export async function handleDetectDrift(args) {
    const { specId, projectId, mode = 'quick', outputFormat = 'report', threshold = 10 } = args;
    // 1. Load spec
    const spec = await specStore.getSpec(projectId, specId);
    if (!spec) {
        return {
            content: [{ type: 'text', text: ti('spec.notFound', { id: specId }) }],
            isError: true,
        };
    }
    // 2. Load project knowledge
    const knowledge = await knowledgeStore.getKnowledge(projectId);
    if (!knowledge) {
        return {
            content: [{ type: 'text', text: t('project.notFound') }],
            isError: true,
        };
    }
    const projectPath = knowledge.projectPath;
    // 2b. Load Constitution for compliance context
    const constitution = await knowledgeStore.getConstitution(projectId);
    // 3. Run drift detection engine
    // The threshold is the minimum compliance score. Drift tolerance = 100 - threshold.
    const complianceThreshold = 100 - threshold;
    const report = await detectDrift(spec, projectPath, mode, complianceThreshold);
    // 3b. SPEC-030: Privacy drift detection
    let huContent = '';
    if (spec.huPath) {
        try {
            huContent = await readFile(spec.huPath, 'utf-8');
        }
        catch {
            /* file not available */
        }
    }
    const privacyDrifts = detectPrivacyDrift(spec.id, spec.title, huContent);
    // 3c. SPEC-031: Event schema drift detection
    const eventSchemaDrifts = await detectEventSchemaDrift(projectId, spec.id);
    // 4. Format output based on requested format
    let formattedOutput;
    switch (outputFormat) {
        case 'ci-json':
            formattedOutput = formatCiJson(report);
            break;
        case 'github-annotations':
            formattedOutput = formatGitHubAnnotations(report);
            break;
        case 'report':
        default:
            formattedOutput = formatHumanReport(report, spec.title);
            break;
    }
    // 5. Check PLAN.md progress if spec has a plan
    const planProgress = await loadPlanProgress(spec.planPath);
    const statusEmoji = report.isCompliant ? 'PASS' : 'FAIL';
    return {
        content: [
            {
                type: 'text',
                text: `[${statusEmoji}] ${ti('tools.detect_drift.success', { driftScore: String(100 - report.driftScore) })}`,
            },
            {
                type: 'text',
                text: formattedOutput,
            },
            ...(planProgress !== null
                ? [
                    {
                        type: 'text',
                        text: JSON.stringify({ planProgress }, null, 2),
                    },
                ]
                : []),
            ...(constitution
                ? [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            constitutionCompliance: {
                                totalPrinciples: constitution.principles.length,
                                strictCount: constitution.principles.filter((p) => p.enforceLevel === 'strict')
                                    .length,
                                checkedAt: new Date().toISOString(),
                            },
                        }, null, 2),
                    },
                ]
                : []),
            // SPEC-030: Privacy drift report
            ...(privacyDrifts.length > 0
                ? [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            privacyDrift: {
                                count: privacyDrifts.length,
                                criticalCount: privacyDrifts.filter((d) => d.severity === 'critical').length,
                                drifts: privacyDrifts,
                                message: 'Privacy drift detected. Review missing privacy criteria before merging.',
                            },
                        }, null, 2),
                    },
                ]
                : []),
            // SPEC-031: Event schema drift report
            ...(eventSchemaDrifts.length > 0
                ? [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            eventSchemaDrift: {
                                type: 'event-schema-mismatch',
                                count: eventSchemaDrifts.length,
                                /* v8 ignore next 2 */
                                breakingCount: eventSchemaDrifts.filter((d) => d.severity === 'breaking')
                                    .length,
                                drifts: eventSchemaDrifts,
                                message: 'Event schema drift detected. Implementation diverges from versioned contract.',
                            },
                        }, null, 2),
                    },
                ]
                : []),
        ],
    };
}
// --- Output formatters ---
function formatHumanReport(report, specTitle) {
    const output = {
        specId: report.specId,
        specTitle,
        complianceScore: report.driftScore,
        isCompliant: report.isCompliant,
        lastChecked: report.lastChecked,
        driftCount: report.drifts.length,
        drifts: report.drifts.map((d) => ({
            criterion: d.specCriterion,
            expected: d.expected,
            actual: d.actual,
            severity: d.severity,
            file: d.file || '(spec-level)',
            autoFixable: d.autoFixable,
            ...(d.suggestedFix ? { suggestedFix: d.suggestedFix } : {}),
        })),
        summary: buildDriftSummary(report),
    };
    return JSON.stringify(output, null, 2);
}
function formatCiJson(report) {
    const ciOutput = {
        status: report.isCompliant ? 'pass' : 'fail',
        score: report.driftScore,
        threshold: report.isCompliant ? 'met' : 'not-met',
        totalDrifts: report.drifts.length,
        critical: report.drifts.filter((d) => d.severity === 'critical').length,
        high: report.drifts.filter((d) => d.severity === 'high').length,
        medium: report.drifts.filter((d) => d.severity === 'medium').length,
        low: report.drifts.filter((d) => d.severity === 'low').length,
        drifts: report.drifts.map((d) => ({
            file: d.file,
            criterion: d.specCriterion,
            severity: d.severity,
            autoFixable: d.autoFixable,
        })),
        checkedAt: report.lastChecked,
    };
    return JSON.stringify(ciOutput, null, 2);
}
function formatGitHubAnnotations(report) {
    const annotations = report.drifts.map((d) => {
        const level = d.severity === 'critical' || d.severity === 'high' ? 'error' : 'warning';
        const file = d.file || 'spec';
        return `::${level} file=${file}::Drift detected: ${d.specCriterion} — expected "${d.expected}", found "${d.actual}"`;
    });
    if (annotations.length === 0) {
        annotations.push('::notice::No drift detected. Spec and implementation are aligned.');
    }
    return annotations.join('\n');
}
async function loadPlanProgress(planPath) {
    if (!planPath) {
        return null;
    }
    try {
        const content = await readFile(planPath, 'utf-8');
        const total = (content.match(/- \[[ x]\]/g) ?? []).length;
        const completed = (content.match(/- \[x\]/gi) ?? []).length;
        return { total, completed, percent: total > 0 ? Math.round((completed / total) * 100) : 0 };
    }
    catch {
        return null;
    }
}
function buildDriftSummary(report) {
    const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
    for (const d of report.drifts) {
        bySeverity[d.severity] = (bySeverity[d.severity] ?? 0) + 1;
    }
    const autoFixable = report.drifts.filter((d) => d.autoFixable).length;
    const requiresManual = report.drifts.length - autoFixable;
    return {
        bySeverity,
        autoFixable,
        requiresManual,
        recommendation: report.isCompliant
            ? 'Spec and implementation are aligned within tolerance.'
            : 'Implementation has drifted from spec. Review and reconcile before proceeding.',
    };
}
// SPEC-030: Privacy drift detection --------------------------------------------
/**
 * Checks if a spec that has PII fields is missing a privacy section.
 * Returns privacy-specific drift items to append to the standard drift report.
 */
function detectPrivacyDrift(specId, specTitle, huContent) {
    const drifts = [];
    const piiResult = detectPIIInSpec(specTitle, huContent, huContent);
    if (!piiResult.hasPII) {
        return drifts;
    }
    const lower = huContent.toLowerCase();
    const hasPrivacySection = lower.includes('privacidad y datos') ||
        lower.includes('## privacy') ||
        lower.includes('gdpr') ||
        lower.includes('legal basis') ||
        lower.includes('retention');
    if (!hasPrivacySection) {
        drifts.push({
            specId,
            driftType: 'new-pii-without-criteria',
            description: `Spec has ${piiResult.fields.length} PII field(s) (${piiResult.fields.map((f) => f.fieldName).join(', ')}) ` +
                'but is missing a "Privacidad y Datos" section with privacy criteria.',
            severity: 'critical',
            detectedAt: new Date().toISOString(),
        });
    }
    const hasAuditTrail = lower.includes('audit trail') ||
        lower.includes('audit log') ||
        lower.includes('acceso') ||
        lower.includes('registro de acceso');
    if (piiResult.requiresAuditTrail && !hasAuditTrail) {
        drifts.push({
            specId,
            driftType: 'missing-audit-trail',
            description: 'Spec accesses PII but does not include an audit trail criterion (who accessed, when, from where, for what purpose).',
            severity: 'high',
            detectedAt: new Date().toISOString(),
        });
    }
    const hasRetentionPolicy = lower.includes('retention') ||
        lower.includes('retenci') ||
        lower.includes('dias') ||
        lower.includes('days') ||
        lower.includes('eliminaci');
    if (!hasRetentionPolicy) {
        drifts.push({
            specId,
            driftType: 'retention-policy-missing',
            description: 'Spec stores PII but does not define a retention policy (how long, what triggers deletion, deletion type).',
            severity: 'high',
            detectedAt: new Date().toISOString(),
        });
    }
    return drifts;
}
//# sourceMappingURL=detect-drift.js.map