// tools/update-status-convention-gate.ts — SPEC-190
// Convention and constitution compliance gates for update_status transitions.
import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import { knowledgeStore } from '../storage/index.js';
import { parseConventions, scanConventions } from '../engine/convention-scanner/index.js';
import { checkConstitutionOnTransition } from './update-status-actions.js';
const execFile = promisify(execFileCb);
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
function isSafeCommand(cmd) {
    return SAFE_COMMAND_RE.test(cmd);
}
async function execGate(cmd, cwd, timeoutMs) {
    const [bin, ...args] = cmd.split(/\s+/);
    try {
        const { stdout } = await execFile(bin ?? 'sh', args, {
            cwd,
            timeout: timeoutMs,
            maxBuffer: 4 * 1024 * 1024,
        });
        return { output: stdout.trim(), ok: true };
    }
    catch (err) {
        const e = err;
        const raw = Buffer.isBuffer(e.stdout)
            ? e.stdout.toString('utf-8')
            : typeof e.stdout === 'string'
                ? e.stdout
                : '';
        return { output: raw.trim(), ok: false };
    }
}
function countLintIssues(output) {
    // Handle eslint, tsc, and generic "N problem(s)" patterns
    const problemMatch = /(\d+)\s+problem/i.exec(output);
    if (problemMatch) {
        return parseInt(problemMatch[1] ?? '0', 10);
    }
    // Count non-empty lines as a fallback for unknown lint formats
    return output.split('\n').filter((l) => l.trim().length > 0).length;
}
// ---------------------------------------------------------------------------
// Individual gates
// ---------------------------------------------------------------------------
async function checkCompileGate(projectPath, lintCommand) {
    // Prefer tsc --noEmit; fall back to checking if lint command already covers it
    const hasTsc = lintCommand?.includes('tsc') ?? false;
    if (hasTsc) {
        return []; // lint will cover compile
    }
    const { output, ok } = await execGate('npx tsc --noEmit', projectPath, 15_000);
    if (ok) {
        return [];
    }
    const lines = output.split('\n').filter((l) => l.includes('error TS'));
    if (lines.length === 0) {
        return [];
    }
    const preview = lines.slice(0, 3).join('; ');
    return [`TypeScript compile: ${lines.length} error(s). ${preview}`];
}
async function checkLintGate(projectPath, lintCmd) {
    if (lintCmd !== null && !isSafeCommand(lintCmd)) {
        console.warn(`[Planu] convention-gate: lintCommand contains unsafe characters — skipping`);
        return [];
    }
    const cmd = lintCmd ?? 'npx eslint src --max-warnings=0 --format=compact';
    const { output, ok } = await execGate(cmd, projectPath, 10_000);
    if (ok) {
        return [];
    }
    const issueCount = countLintIssues(output);
    if (issueCount === 0) {
        return [];
    }
    const preview = output.split('\n').find((l) => l.trim().length > 0) ?? '';
    return [`Lint: ${issueCount} issue(s). ${preview}`];
}
async function checkTestGate(projectPath, testCmd) {
    if (testCmd !== null && !isSafeCommand(testCmd)) {
        console.warn(`[Planu] convention-gate: testCommand contains unsafe characters — skipping`);
        return [];
    }
    // Run tests with a reasonable timeout — only a warning if they fail
    const cmd = testCmd ?? 'npx vitest run --reporter=verbose';
    const { output, ok } = await execGate(cmd, projectPath, 20_000);
    if (ok) {
        return [];
    }
    // Extract summary line (e.g. "5 failed" or "FAIL src/foo.test.ts")
    const failLine = output
        .split('\n')
        .reverse()
        .find((l) => /fail|error/i.test(l) && l.trim().length > 0);
    const summary = failLine ??
        output
            .split('\n')
            .filter((l) => l.trim())
            .slice(-1)[0] ??
        '';
    return [`Tests: failed. ${summary.trim()}`];
}
// ---------------------------------------------------------------------------
// Convention gate (existing logic, unchanged)
// ---------------------------------------------------------------------------
/**
 * Checks for critical convention violations before allowing a spec to be marked as done.
 * Returns warning messages for critical violations (non-blocking, best-effort).
 */
export async function checkConventionGate(projectId) {
    try {
        const knowledge = await knowledgeStore.getKnowledge(projectId);
        if (!knowledge?.projectPath) {
            return [];
        }
        const rules = parseConventions(knowledge.projectPath);
        if (rules.length === 0) {
            return [];
        }
        const violations = scanConventions(knowledge.projectPath, rules);
        const critical = violations.filter((v) => v.severity === 'critical');
        if (critical.length === 0) {
            return [];
        }
        return critical.slice(0, 5).map((v) => `${v.category}: ${v.message} (${v.file})`);
    }
    catch {
        return [];
    }
}
// ---------------------------------------------------------------------------
// Main gate runner
// ---------------------------------------------------------------------------
/**
 * Runs all non-blocking compliance gates (convention + constitution + compile + lint + test)
 * in parallel. Extracts complexity from handleUpdateStatus.
 */
export async function runComplianceGates(projectId, specTitle, specTags, newStatus) {
    const isDone = newStatus === 'done';
    const isApprovedOrDone = newStatus === 'approved' || isDone;
    // Load knowledge once — needed for project path and commands
    const knowledge = isDone ? await knowledgeStore.getKnowledge(projectId) : null;
    const projectPath = knowledge?.projectPath ?? null;
    const [conventionWarnings, constitutionWarnings, compileWarnings, lintWarnings, testWarnings] = await Promise.all([
        isDone ? checkConventionGate(projectId) : Promise.resolve([]),
        isApprovedOrDone
            ? checkConstitutionOnTransition(projectId, specTitle, specTags)
            : Promise.resolve([]),
        isDone && projectPath
            ? checkCompileGate(projectPath, knowledge?.lintCommand ?? null)
            : Promise.resolve([]),
        isDone && projectPath
            ? checkLintGate(projectPath, knowledge?.lintCommand ?? null)
            : Promise.resolve([]),
        isDone && projectPath
            ? checkTestGate(projectPath, knowledge?.testCommand ?? null)
            : Promise.resolve([]),
    ]);
    return { conventionWarnings, constitutionWarnings, compileWarnings, lintWarnings, testWarnings };
}
//# sourceMappingURL=update-status-convention-gate.js.map