// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// SPEC-597-EXEMPT: canonical mapping (npm-package↔framework, keyword catalogue)
// — must enumerate framework names by design; no detection logic to migrate.
// engine/project-health-checker.ts — SPEC-180 + SPEC-192 + SPEC-177: Project health check with convention scanning
import { execSync } from 'node:child_process';
import { readFile, access } from 'node:fs/promises';
import { join } from 'node:path';
import { parseConventions, scanConventions } from './convention-scanner/index.js';
// ── helpers ──────────────────────────────────────────────────────────────────
function fileExists(path) {
    return access(path)
        .then(() => true)
        .catch(() => false);
}
function execSafe(cmd, cwd) {
    try {
        return execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
    }
    catch {
        return '';
    }
}
function daysSince(iso) {
    return Math.floor((Date.now() - new Date(iso).getTime()) / 86_400_000);
}
// ── helpers: serverless detection (SPEC-177) ──────────────────────────────
const SERVERLESS_PLATFORM_KEYWORDS = [
    'vercel',
    'netlify',
    'lambda',
    'cloudflare-workers',
    'deno-deploy',
];
function isServerlessByKnowledge(knowledge) {
    if (knowledge.stack.some((s) => /serverless|vercel|netlify|edge/i.test(s))) {
        return true;
    }
    const services = knowledge.externalServices.map((s) => s.toLowerCase());
    const deployPlatform = (knowledge.conventions['deploy-platform'] ?? '').toLowerCase();
    return SERVERLESS_PLATFORM_KEYWORDS.some((p) => services.some((s) => s.includes(p)) || deployPlatform.includes(p));
}
// ── check: conventions (25 %) ─────────────────────────────────────────────
async function checkConventions(projectPath) {
    const items = [];
    let score = 100;
    // Check CLAUDE.md for planu markers
    const claudeMdPath = join(projectPath, 'CLAUDE.md');
    const hasClaude = await fileExists(claudeMdPath);
    if (!hasClaude) {
        score -= 50;
        items.push({
            severity: 'warning',
            category: 'conventions',
            message: 'CLAUDE.md not found — coding conventions not defined',
            fix: 'Run init_project to generate CLAUDE.md with planu markers',
        });
    }
    else {
        const content = await readFile(claudeMdPath, 'utf-8').catch(() => '');
        const hasMarkers = content.includes('<!-- planu:') || content.includes('## Project Overview');
        if (hasMarkers) {
            items.push({ severity: 'good', category: 'conventions', message: 'CLAUDE.md complete ✓' });
        }
        else {
            score -= 25;
            items.push({
                severity: 'warning',
                category: 'conventions',
                message: 'CLAUDE.md exists but lacks planu markers',
                fix: 'Run init_project to inject SDD workflow markers',
            });
        }
    }
    // Scan src/ lib/ app/ for non-ASCII identifiers (non-English variable names)
    const srcDirs = ['src', 'lib', 'app'];
    const nonAsciiPattern = /(?:function|const|let|var|export)\s+([À-ÿ\w]*[À-ÿ][À-ÿ\w]*)/;
    let nonEnglishFiles = 0;
    for (const dir of srcDirs) {
        const result = execSafe(`grep -rl --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" -P "[À-ÿ]" ${dir} 2>/dev/null | head -5`, projectPath);
        const files = result.trim().split('\n').filter(Boolean);
        for (const file of files) {
            const content = await readFile(join(projectPath, file), 'utf-8').catch(() => '');
            if (nonAsciiPattern.test(content)) {
                nonEnglishFiles++;
                score -= 25;
                items.push({
                    severity: 'warning',
                    category: 'conventions',
                    message: `Non-English identifiers found in ${file}`,
                    fix: 'Rename identifiers to English to improve cross-team readability',
                });
                if (nonEnglishFiles >= 2) {
                    break;
                }
            }
        }
        if (nonEnglishFiles >= 2) {
            break;
        }
    }
    if (nonEnglishFiles === 0 && hasClaude) {
        items.push({ severity: 'good', category: 'conventions', message: 'English identifiers ✓' });
    }
    return { score: Math.max(0, score), items };
}
// ── check: code quality (20 %) ───────────────────────────────────────────
async function checkCodeQuality(projectPath) {
    const items = [];
    let score = 100;
    // ESLint config
    const eslintFiles = [
        'eslint.config.js',
        'eslint.config.mjs',
        'eslint.config.cjs',
        '.eslintrc.js',
        '.eslintrc.cjs',
        '.eslintrc.json',
        '.eslintrc.yaml',
        '.eslintrc.yml',
    ];
    const hasEslint = (await Promise.all(eslintFiles.map((f) => fileExists(join(projectPath, f))))).some(Boolean);
    if (hasEslint) {
        items.push({ severity: 'good', category: 'code-quality', message: 'ESLint configured ✓' });
    }
    else {
        score -= 50;
        items.push({
            severity: 'warning',
            category: 'code-quality',
            message: 'No ESLint config found',
            fix: 'Add eslint.config.js to enforce consistent code style',
        });
    }
    // TypeScript strict mode
    const tsconfigPath = join(projectPath, 'tsconfig.json');
    const hasTsconfig = await fileExists(tsconfigPath);
    if (hasTsconfig) {
        try {
            const raw = await readFile(tsconfigPath, 'utf-8');
            // Strip comments before parsing
            const cleaned = raw.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
            const tsconfig = JSON.parse(cleaned);
            if (tsconfig.compilerOptions?.strict === true) {
                items.push({
                    severity: 'good',
                    category: 'code-quality',
                    message: 'TypeScript strict ✓',
                });
            }
            else {
                score -= 50;
                items.push({
                    severity: 'warning',
                    category: 'code-quality',
                    message: 'TypeScript strict mode not enabled',
                    fix: 'Add "strict": true to tsconfig.json compilerOptions',
                });
            }
        }
        catch {
            score -= 25;
            items.push({
                severity: 'warning',
                category: 'code-quality',
                message: 'tsconfig.json could not be parsed',
                fix: 'Ensure tsconfig.json is valid JSON with comments removed',
            });
        }
    }
    else {
        score -= 50;
        items.push({
            severity: 'warning',
            category: 'code-quality',
            message: 'No tsconfig.json found',
            fix: 'Add tsconfig.json with strict: true to enable type safety',
        });
    }
    return { score: Math.max(0, score), items };
}
// ── check: spec lifecycle (20 %) ─────────────────────────────────────────
function checkSpecLifecycle(projectPath, specs) {
    const items = [];
    let score = 100;
    const mergedOutput = execSafe('git branch --merged develop', projectPath);
    const mergedBranches = new Set(mergedOutput.split('\n').map((b) => b.replace(/^\*?\s+/, '').trim()));
    const implementing = specs.filter((s) => s.status === 'implementing');
    for (const spec of implementing) {
        const days = spec.updatedAt ? daysSince(spec.updatedAt) : 0;
        if (days > 7) {
            score -= 20;
            items.push({
                severity: 'warning',
                category: 'spec-lifecycle',
                message: `${spec.specId} implementing for ${days} days`,
                fix: `Review ${spec.specId} — consider breaking it down or marking it done`,
            });
        }
        if (spec.gitBranch && mergedBranches.size > 1 && !mergedBranches.has(spec.gitBranch)) {
            items.push({
                severity: 'warning',
                category: 'spec-lifecycle',
                message: `${spec.specId} branch '${spec.gitBranch}' not merged to develop`,
                fix: 'Merge or close the feature branch to clean up lifecycle',
            });
        }
    }
    if (implementing.length === 0) {
        items.push({
            severity: 'good',
            category: 'spec-lifecycle',
            message: 'No stale implementing specs ✓',
        });
    }
    else if (score === 100) {
        items.push({
            severity: 'good',
            category: 'spec-lifecycle',
            message: 'All implementing specs are recent ✓',
        });
    }
    return { score: Math.max(0, score), items };
}
// ── check: git hygiene (15 %) ─────────────────────────────────────────────
async function checkGitHygiene(projectPath) {
    const items = [];
    let score = 100;
    // Check .gitignore for planu/
    const gitignorePath = join(projectPath, '.gitignore');
    const gitignoreContent = await readFile(gitignorePath, 'utf-8').catch(() => '');
    const planuIgnored = gitignoreContent
        .split('\n')
        .some((l) => ['planu/', 'planu/*', 'planu'].includes(l.trim()));
    if (planuIgnored) {
        score = 0;
        items.push({
            severity: 'critical',
            category: 'git-hygiene',
            message: 'planu/ found in .gitignore — specs will not be tracked in git',
            fix: 'Remove planu/ from .gitignore. Specs must be committed to the repository.',
        });
    }
    else {
        items.push({ severity: 'good', category: 'git-hygiene', message: 'planu/ not ignored ✓' });
    }
    // Uncommitted files
    const statusOutput = execSafe('git status --porcelain', projectPath);
    const uncommitted = statusOutput.trim().split('\n').filter(Boolean).length;
    if (uncommitted > 0 && score > 0) {
        score = Math.max(50, score - uncommitted * 5);
        items.push({
            severity: 'warning',
            category: 'git-hygiene',
            message: `${uncommitted} uncommitted file(s) in working directory`,
            fix: 'Commit or stash changes to keep the working tree clean',
        });
    }
    else if (uncommitted === 0) {
        items.push({ severity: 'good', category: 'git-hygiene', message: 'Working tree clean ✓' });
    }
    return { score: Math.max(0, score), items };
}
// ── check: platform fitness (10 %) ────────────────────────────────────────
async function checkPlatformFitness(projectPath, knowledge) {
    const items = [];
    let score = 100;
    const isServerless = isServerlessByKnowledge(knowledge) ||
        (await fileExists(join(projectPath, 'serverless.yml'))) ||
        (await fileExists(join(projectPath, 'serverless.yaml')));
    if (isServerless) {
        // Crude check: module-level `let` in src/lib files (mutable state = bad for serverless)
        const result = execSafe(`grep -rl --include="*.ts" --include="*.js" -P "^let\\s+\\w+" src lib 2>/dev/null | head -3`, projectPath);
        const files = result.trim().split('\n').filter(Boolean);
        if (files.length > 0) {
            score = 50;
            items.push({
                severity: 'warning',
                category: 'platform-fitness',
                message: `Module-level mutable state detected in serverless context (${files.slice(0, 2).join(', ')})`,
                fix: 'Avoid module-level let in serverless functions — use Redis or database persistence for cross-request data (cold starts reset module state)',
            });
        }
        else {
            items.push({
                severity: 'good',
                category: 'platform-fitness',
                message: 'Serverless state usage OK ✓',
            });
        }
    }
    else {
        items.push({
            severity: 'good',
            category: 'platform-fitness',
            message: 'Platform fitness check passed ✓',
        });
    }
    return { score, items };
}
// ── check: dependencies (10 %) ───────────────────────────────────────────
async function checkDependencies(projectPath) {
    const items = [];
    const lockFiles = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb'];
    const hasLock = (await Promise.all(lockFiles.map((f) => fileExists(join(projectPath, f))))).some(Boolean);
    if (hasLock) {
        items.push({ severity: 'good', category: 'dependencies', message: 'Lock file present ✓' });
        return { score: 100, items };
    }
    items.push({
        severity: 'warning',
        category: 'dependencies',
        message: 'No lock file found (package-lock.json, pnpm-lock.yaml, yarn.lock)',
        fix: 'Commit your lock file to ensure reproducible builds',
    });
    return { score: 50, items };
}
// ── check: deep convention scan (SPEC-192) ────────────────────────────────
function checkDeepConventions(projectPath) {
    const items = [];
    try {
        const rules = parseConventions(projectPath);
        if (rules.length === 0) {
            items.push({
                severity: 'warning',
                category: 'conventions',
                message: 'No convention rules found in CLAUDE.md or .claude/rules/',
                fix: 'Add coding conventions to CLAUDE.md for automated enforcement',
            });
            return { score: 50, items, scanResult: null };
        }
        const violations = scanConventions(projectPath, rules);
        // Build structured scan result
        const bySeverity = {};
        const byCategory = {};
        for (const v of violations) {
            bySeverity[v.severity] = (bySeverity[v.severity] ?? 0) + 1;
            byCategory[v.category] = (byCategory[v.category] ?? 0) + 1;
        }
        const scanResult = {
            rules,
            violations,
            summary: {
                totalRules: rules.length,
                totalViolations: violations.length,
                bySeverity,
                byCategory,
            },
            scannedAt: new Date().toISOString(),
        };
        if (violations.length === 0) {
            items.push({
                severity: 'good',
                category: 'conventions',
                message: `All ${rules.length} convention rules pass ✓`,
            });
            return { score: 100, items, scanResult };
        }
        // Score: start at 100, deduct per violation (capped)
        const criticalCount = bySeverity.critical ?? 0;
        const warningCount = bySeverity.warning ?? 0;
        const deduction = Math.min(80, criticalCount * 15 + warningCount * 5);
        const score = Math.max(20, 100 - deduction);
        // Group violations by category for concise reporting
        for (const [category, count] of Object.entries(byCategory)) {
            items.push({
                severity: 'warning',
                category: 'conventions',
                message: `${count} ${category} violation(s) found`,
                fix: `Run convention scan for details — fix ${category} issues to improve score`,
            });
        }
        return { score, items, scanResult };
    }
    catch {
        return { score: 75, items, scanResult: null };
    }
}
// ── formatter ─────────────────────────────────────────────────────────────
function formatSummary(score, items) {
    const emoji = score >= 80 ? '✅' : score >= 60 ? '⚠️' : '❌';
    const lines = [`Project Health: ${score}/100 ${emoji}`, ''];
    const critical = items.filter((i) => i.severity === 'critical');
    const warnings = items.filter((i) => i.severity === 'warning');
    const good = items.filter((i) => i.severity === 'good');
    if (critical.length > 0) {
        lines.push('🔴 Critical (fix now):');
        for (const item of critical) {
            lines.push(`  - ${item.message}`);
        }
        lines.push('');
    }
    if (warnings.length > 0) {
        lines.push('🟡 Warnings (fix soon):');
        for (const item of warnings) {
            lines.push(`  - ${item.message}`);
        }
        lines.push('');
    }
    if (good.length > 0) {
        lines.push('🟢 Good:');
        for (const item of good) {
            lines.push(`  - ${item.message}`);
        }
    }
    return lines.join('\n').trimEnd();
}
// ── orchestrator ──────────────────────────────────────────────────────────
export async function checkProjectHealth(projectPath, knowledge, specs) {
    const deepConventions = checkDeepConventions(projectPath);
    const [conventions, codeQuality, gitHygiene, platformFitness, dependencies] = await Promise.all([
        checkConventions(projectPath),
        checkCodeQuality(projectPath),
        checkGitHygiene(projectPath),
        checkPlatformFitness(projectPath, knowledge),
        checkDependencies(projectPath),
    ]);
    const specLifecycle = checkSpecLifecycle(projectPath, specs);
    // SPEC-192: Convention compliance weighted at 30%
    // Merge basic + deep convention scores (average of both)
    const conventionScore = Math.round((conventions.score + deepConventions.score) / 2);
    // Weighted average — conventions 30%, code-quality 20%, spec-lifecycle 15%,
    // git-hygiene 15%, platform-fitness 10%, dependencies 10%
    const score = Math.round(conventionScore * 0.3 +
        codeQuality.score * 0.2 +
        specLifecycle.score * 0.15 +
        gitHygiene.score * 0.15 +
        platformFitness.score * 0.1 +
        dependencies.score * 0.1);
    const items = [
        ...conventions.items,
        ...deepConventions.items,
        ...codeQuality.items,
        ...specLifecycle.items,
        ...gitHygiene.items,
        ...platformFitness.items,
        ...dependencies.items,
    ];
    return {
        score,
        items,
        summary: formatSummary(score, items),
        conventionScan: deepConventions.scanResult,
    };
}
//# sourceMappingURL=project-health-checker.js.map