// @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.
// Planu — project-dna/stack-detector.ts
// Detects project stack by reading manifest files.
import { readFile, access } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
import { join, dirname, resolve as pathResolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// ---------------------------------------------------------------------------
// Registry loader (readFileSync — not intercepted by test mocks of fs/promises)
// ---------------------------------------------------------------------------
let signalsCache = null;
function loadSignals() {
    if (signalsCache) {
        return signalsCache;
    }
    const here = fileURLToPath(import.meta.url);
    const configDir = pathResolve(dirname(here), '..', '..', 'config');
    const raw = readFileSync(join(configDir, 'detection-signals.json'), 'utf-8');
    signalsCache = JSON.parse(raw);
    return signalsCache;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function fileExists(filePath) {
    try {
        await access(filePath);
        return true;
    }
    catch {
        return false;
    }
}
async function readFileIfExists(filePath) {
    try {
        return await readFile(filePath, 'utf-8');
    }
    catch {
        return null;
    }
}
// ---------------------------------------------------------------------------
// Stack detection
// ---------------------------------------------------------------------------
/**
 * Detects the technology stack by reading common manifest files.
 * Returns normalized stack tokens (e.g. 'nextjs', 'supabase', 'vitest').
 */
export async function detectStack(projectPath) {
    const tokens = new Set();
    await detectFromPackageJson(projectPath, tokens);
    await detectFromRequirements(projectPath, tokens);
    await detectFromCargo(projectPath, tokens);
    await detectFromGoMod(projectPath, tokens);
    await detectFromTsConfig(projectPath, tokens);
    await detectFromCiCd(projectPath, tokens);
    return Array.from(tokens);
}
async function detectFromPackageJson(projectPath, tokens) {
    const content = await readFileIfExists(join(projectPath, 'package.json'));
    if (content === null) {
        return;
    }
    let pkg;
    try {
        pkg = JSON.parse(content);
    }
    catch {
        return;
    }
    if (typeof pkg !== 'object' || pkg === null) {
        return;
    }
    const record = pkg;
    const deps = {
        ...(typeof record.dependencies === 'object' && record.dependencies !== null
            ? record.dependencies
            : {}),
        ...(typeof record.devDependencies === 'object' && record.devDependencies !== null
            ? record.devDependencies
            : {}),
    };
    const signals = loadSignals();
    // Signals: pattern → frameworkId (npm dependencies)
    for (const s of signals.signals) {
        if (s.type === 'dependency' && s.manager === 'npm' && s.pattern in deps) {
            tokens.add(s.frameworkId);
        }
    }
    // Databases: pattern → id (npm dependencies)
    for (const db of signals.databases) {
        if (db.type === 'dependency' && db.manager === 'npm' && db.pattern in deps) {
            tokens.add(db.id);
        }
    }
}
async function detectFromRequirements(projectPath, tokens) {
    const content = await readFileIfExists(join(projectPath, 'requirements.txt'));
    if (content === null) {
        return;
    }
    const lines = content.toLowerCase();
    const signals = loadSignals();
    for (const s of signals.signals) {
        if (s.type === 'dependency' && s.manager === 'pip' && lines.includes(s.pattern.toLowerCase())) {
            tokens.add(s.frameworkId);
        }
    }
    for (const db of signals.databases) {
        if (db.type === 'dependency' &&
            db.manager === 'pip' &&
            lines.includes(db.pattern.toLowerCase())) {
            tokens.add(db.id);
        }
    }
}
async function detectFromCargo(projectPath, tokens) {
    const content = await readFileIfExists(join(projectPath, 'Cargo.toml'));
    if (content === null) {
        return;
    }
    const text = content.toLowerCase();
    const signals = loadSignals();
    for (const s of signals.signals) {
        if (s.type === 'dependency' &&
            s.manager === 'cargo' &&
            text.includes(s.pattern.toLowerCase())) {
            tokens.add(s.frameworkId);
        }
    }
}
async function detectFromGoMod(projectPath, tokens) {
    const content = await readFileIfExists(join(projectPath, 'go.mod'));
    if (content === null) {
        return;
    }
    const text = content.toLowerCase();
    const signals = loadSignals();
    for (const s of signals.signals) {
        if (s.type === 'dependency' && s.manager === 'go' && text.includes(s.pattern.toLowerCase())) {
            tokens.add(s.frameworkId);
        }
    }
}
async function detectFromTsConfig(projectPath, tokens) {
    const content = await readFileIfExists(join(projectPath, 'tsconfig.json'));
    if (content === null) {
        return;
    }
    try {
        const parsed = JSON.parse(content);
        if (typeof parsed !== 'object' || parsed === null) {
            return;
        }
        const record = parsed;
        const compilerOptions = record.compilerOptions;
        if (typeof compilerOptions === 'object' && compilerOptions !== null) {
            const opts = compilerOptions;
            if (opts.strict === true) {
                tokens.add('typescript-strict');
            }
        }
    }
    catch {
        // malformed tsconfig, skip
    }
}
async function detectFromCiCd(projectPath, tokens) {
    const githubWorkflows = join(projectPath, '.github', 'workflows');
    if (await fileExists(githubWorkflows)) {
        tokens.add('github-actions');
    }
    if (await fileExists(join(projectPath, '.gitlab-ci.yml'))) {
        tokens.add('gitlab-ci');
    }
}
/**
 * Detects the package manager by checking lock files and manifests.
 */
export async function detectPackageManager(projectPath) {
    const checks = [
        ['pnpm-lock.yaml', 'pnpm'],
        ['yarn.lock', 'yarn'],
        ['bun.lockb', 'bun'],
        ['bun.lock', 'bun'],
        ['package-lock.json', 'npm'],
        ['Cargo.lock', 'cargo'],
        ['go.sum', 'go'],
        ['requirements.txt', 'pip'],
        ['Pipfile.lock', 'pip'],
        ['poetry.lock', 'pip'],
    ];
    for (const [file, pm] of checks) {
        if (await fileExists(join(projectPath, file))) {
            return pm;
        }
    }
    if (await fileExists(join(projectPath, 'package.json'))) {
        return 'npm';
    }
    return 'unknown';
}
// ---------------------------------------------------------------------------
// Testing framework detection
// ---------------------------------------------------------------------------
/**
 * Detects the primary testing framework based on stack tokens and config files.
 */
export async function detectTestingFramework(projectPath, stack) {
    if (stack.includes('vitest')) {
        return 'vitest';
    }
    if (stack.includes('jest')) {
        return 'jest';
    }
    if (stack.includes('pytest')) {
        return 'pytest';
    }
    // Go test is always available if go.mod exists
    if (await fileExists(join(projectPath, 'go.mod'))) {
        return 'go-test';
    }
    return undefined;
}
// ---------------------------------------------------------------------------
// Linter detection
// ---------------------------------------------------------------------------
/**
 * Detects the primary linter based on config files and stack tokens.
 */
export async function detectLinter(projectPath, stack) {
    const eslintFiles = [
        'eslint.config.js',
        'eslint.config.mjs',
        'eslint.config.ts',
        '.eslintrc.js',
        '.eslintrc.json',
        '.eslintrc.yml',
        '.eslintrc.yaml',
    ];
    for (const f of eslintFiles) {
        if (await fileExists(join(projectPath, f))) {
            return 'eslint';
        }
    }
    if (stack.includes('fastapi') || stack.includes('django') || stack.includes('flask')) {
        if (await fileExists(join(projectPath, 'pyproject.toml'))) {
            const content = await readFileIfExists(join(projectPath, 'pyproject.toml'));
            if (content?.includes('ruff') === true) {
                return 'ruff';
            }
        }
        return 'ruff';
    }
    if (await fileExists(join(projectPath, '.golangci.yml'))) {
        return 'golangci-lint';
    }
    if (await fileExists(join(projectPath, 'Cargo.toml'))) {
        return 'clippy';
    }
    return undefined;
}
// ---------------------------------------------------------------------------
// Formatter detection
// ---------------------------------------------------------------------------
/**
 * Detects the primary code formatter based on config files and stack tokens.
 */
export async function detectFormatter(projectPath, stack) {
    const prettierFiles = [
        'prettier.config.js',
        'prettier.config.mjs',
        '.prettierrc',
        '.prettierrc.json',
        '.prettierrc.yml',
        '.prettierrc.yaml',
    ];
    for (const f of prettierFiles) {
        if (await fileExists(join(projectPath, f))) {
            return 'prettier';
        }
    }
    if (stack.includes('fastapi') || stack.includes('django') || stack.includes('flask')) {
        return 'black';
    }
    if (await fileExists(join(projectPath, 'go.mod'))) {
        return 'gofmt';
    }
    if (await fileExists(join(projectPath, 'Cargo.toml'))) {
        return 'rustfmt';
    }
    return undefined;
}
//# sourceMappingURL=stack-detector.js.map