// engine/rules-reconciler.ts — Compare CLAUDE.md conventions against .claude/rules/ and auto-patch drift (SPEC-189)
import { readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, basename } from 'node:path';
import { parseConventions } from './convention-scanner/convention-parser.js';
import { assertEnglishOnlyArtifactText } from './spec-language/english-only.js';
// ── Helpers ───────────────────────────────────────────────────────────────────
function listRulesFiles(projectPath) {
    const rulesDir = join(projectPath, '.claude', 'rules');
    try {
        return readdirSync(rulesDir)
            .filter((f) => f.endsWith('.md'))
            .map((f) => join(rulesDir, f));
    }
    catch {
        return [];
    }
}
function readFileSafe(path) {
    try {
        return readFileSync(path, 'utf-8');
    }
    catch {
        return '';
    }
}
/** Unique convention categories detected in CLAUDE.md (source: 'claude-md' only). */
function categoriesInClaudeMd(rules) {
    const categories = new Set();
    for (const r of rules) {
        if (r.source === 'claude-md') {
            categories.add(r.category);
        }
    }
    return categories;
}
/**
 * Build a map: category → rules files that mention it (via keyword matching).
 * A rules file "covers" a category if it contains any line that would parse
 * as that category using the same keywords as convention-parser.
 */
function buildCoverageMap(rulesFiles, allRules) {
    const coverageMap = new Map();
    // Build per-file category sets from already-parsed rules
    for (const r of allRules) {
        if (r.source !== 'rules-file') {
            continue;
        }
        const existing = coverageMap.get(r.category) ?? [];
        if (!existing.includes(r.sourceFile)) {
            existing.push(r.sourceFile);
        }
        coverageMap.set(r.category, existing);
    }
    // Ensure every rules file appears in the map even with no matches
    for (const filePath of rulesFiles) {
        for (const [, files] of coverageMap) {
            if (!files.includes(filePath)) {
                // already handled above; this loop is a no-op but makes intent clear
            }
        }
    }
    return coverageMap;
}
/**
 * Find rules files whose content does NOT match any category still present
 * in CLAUDE.md — meaning they may be stale.
 *
 * A file is "stale" when it has NO categories in common with CLAUDE.md categories.
 */
function detectStaleRules(rulesFiles, allRules, claudeCategories) {
    // Build per-file category sets
    const fileCategories = new Map();
    for (const r of allRules) {
        if (r.source !== 'rules-file') {
            continue;
        }
        const cats = fileCategories.get(r.sourceFile) ?? new Set();
        cats.add(r.category);
        fileCategories.set(r.sourceFile, cats);
    }
    const stale = [];
    for (const filePath of rulesFiles) {
        const cats = fileCategories.get(filePath);
        if (!cats || cats.size === 0) {
            // No parseable conventions found — candidate for stale
            stale.push(basename(filePath));
            continue;
        }
        // Stale if none of its categories overlap with CLAUDE.md categories
        const hasOverlap = [...cats].some((c) => claudeCategories.has(c));
        if (!hasOverlap) {
            stale.push(basename(filePath));
        }
    }
    return stale;
}
/** Generate minimal markdown content for a new rules file covering a category. */
function generateRuleFileContent(category, conventions) {
    const relevant = conventions.filter((r) => r.source === 'claude-md' && r.category === category);
    const lines = [
        `# ${category.charAt(0).toUpperCase() + category.slice(1)} Conventions (auto-generated by reconcile_rules)`,
        '',
        `Rules for the **${category}** category extracted from CLAUDE.md.`,
        '',
        '## Conventions',
        '',
    ];
    for (const r of relevant) {
        const prefix = r.severity === 'critical'
            ? '**[MANDATORY]**'
            : r.severity === 'warning'
                ? '**[SHOULD]**'
                : '';
        lines.push(`- ${prefix} ${r.description}`);
    }
    lines.push('');
    return lines.join('\n');
}
/** Write a new rules file to .claude/rules/ for the given category. */
function writeNewRulesFile(projectPath, category, conventions) {
    const rulesDir = join(projectPath, '.claude', 'rules');
    mkdirSync(rulesDir, { recursive: true });
    const fileName = `${category}.md`;
    const filePath = join(rulesDir, fileName);
    const content = generateRuleFileContent(category, conventions);
    assertEnglishOnlyArtifactText(content, 'rule');
    writeFileSync(filePath, content, 'utf-8');
    return fileName;
}
// ── Public API ────────────────────────────────────────────────────────────────
/**
 * Compare CLAUDE.md conventions against existing .claude/rules/ files.
 * Auto-patches missing convention categories by generating new rules files.
 * Never overwrites existing rules files.
 *
 * Never throws — returns a zero-score reconciliation on unexpected errors.
 */
export function reconcileRules(projectPath) {
    try {
        // 1. Parse all conventions (CLAUDE.md + rules files)
        const allRules = parseConventions(projectPath);
        const rulesFiles = listRulesFiles(projectPath);
        // 2. Determine which categories are defined in CLAUDE.md
        const claudeCategories = categoriesInClaudeMd(allRules);
        if (claudeCategories.size === 0) {
            return {
                rulesInSync: rulesFiles.length === 0,
                newRulesNeeded: [],
                staleRules: rulesFiles.map((f) => basename(f)),
                updatedRules: [],
                healthScore: rulesFiles.length === 0 ? 100 : 0,
            };
        }
        // 3. Build coverage map: which categories are covered by rules files
        const coverageMap = buildCoverageMap(rulesFiles, allRules);
        // 4. Find categories in CLAUDE.md with no rules file coverage
        const newRulesNeeded = [];
        for (const category of claudeCategories) {
            const coveredBy = coverageMap.get(category) ?? [];
            if (coveredBy.length === 0) {
                newRulesNeeded.push(category);
            }
        }
        // 5. Detect stale rules files
        const staleRules = detectStaleRules(rulesFiles, allRules, claudeCategories);
        // 6. Auto-patch: generate new rules files for uncovered categories
        const updatedRules = [];
        for (const category of newRulesNeeded) {
            const fileName = writeNewRulesFile(projectPath, category, allRules);
            updatedRules.push(fileName);
        }
        // 7. Calculate health score
        const covered = claudeCategories.size - newRulesNeeded.length + updatedRules.length;
        const healthScore = Math.round((covered / claudeCategories.size) * 100);
        return {
            rulesInSync: newRulesNeeded.length === 0 && staleRules.length === 0,
            newRulesNeeded,
            staleRules,
            updatedRules,
            healthScore,
        };
    }
    catch {
        return {
            rulesInSync: false,
            newRulesNeeded: [],
            staleRules: [],
            updatedRules: [],
            healthScore: 0,
        };
    }
}
// Re-export for external use
export { readFileSafe };
//# sourceMappingURL=rules-reconciler.js.map