/**
 * bootstrap/detect-mode.mjs — greenfield-vs-brownfield heuristic for apt:bootstrap (C47).
 *
 * Implements D-03 compound heuristic:
 *   GREENFIELD ⇔ (source_file_count < 10)  AND
 *                (no README.md at project root)  AND
 *                (git commit count ≤ 2 OR no .git)
 *
 * Everything else is BROWNFIELD. The tripled-gate design is what makes this
 * robust — a fresh repo with a stub README stays greenfield UNTIL meaningful
 * code lands; an existing repo with no README but lots of code is correctly
 * classified brownfield and will infer rather than interrogate.
 *
 * Ties to detect-project.mjs's source-file counter (bounded at 500). We
 * replicate the extension set + ignore-dirs here so the bootstrap module
 * stays independent — detect-project.mjs is a CLI command, not a helper
 * library, and its surface is shaped for a different output envelope.
 *
 * No LLM calls. No network. Deterministic, testable.
 */
import { execFileSync } from 'node:child_process';
import { existsSync, readdirSync } from 'node:fs';
import { extname, join, resolve } from 'node:path';
const SOURCE_EXTENSIONS = new Set([
    '.js',
    '.mjs',
    '.cjs',
    '.ts',
    '.tsx',
    '.jsx',
    '.py',
    '.rs',
    '.go',
    '.java',
    '.rb',
    '.vue',
    '.svelte',
    '.php',
    '.cs',
    '.kt',
    '.swift',
]);
const IGNORE_DIRS = new Set([
    'node_modules',
    '.git',
    '.aperant',
    'dist',
    'build',
    '.next',
    '.turbo',
    '.vercel',
    '.cache',
    'target',
    'vendor',
    '__pycache__',
    'coverage',
    'out',
]);
/** Cap scanning at this value — the heuristic only needs to know if we're
 * above 10, not how far above. Keeps the walk cheap on monorepos. */
const SCAN_CAP = 50;
const MAX_DEPTH = 5;
/** Greenfield threshold for source file count (D-03). */
const GREENFIELD_SOURCE_MAX = 10;
/** Greenfield threshold for git commit count (D-03). */
const GREENFIELD_COMMIT_MAX = 2;
/**
 * @typedef {Object} DetectSignals
 * @property {number} sourceFileCount     Actual count, capped at SCAN_CAP.
 * @property {boolean} sourceCountCapped  True when we stopped at SCAN_CAP.
 * @property {boolean} hasReadme          Root-level README.md presence.
 * @property {boolean} hasGit             .git directory presence.
 * @property {number} commitCount         0 when no git or empty history.
 */
/**
 * @typedef {Object} DetectResult
 * @property {'greenfield' | 'brownfield'} mode
 * @property {DetectSignals} signals
 * @property {string[]} rationale   One bullet per D-03 gate the classifier evaluated.
 */
/**
 * Count source files up to SCAN_CAP in a bounded walk. Returns early once the
 * cap is hit (a count of 50 is as informative as 500 for the greenfield gate).
 *
 * @param {string} rootDir
 * @returns {{ count: number, capped: boolean }}
 */
function countSourceFiles(rootDir) {
    let count = 0;
    let capped = false;
    /** @param {string} dir @param {number} depth */
    function walk(dir, depth) {
        if (capped || depth > MAX_DEPTH)
            return;
        let entries;
        try {
            entries = readdirSync(dir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const e of entries) {
            if (capped)
                return;
            if (e.isDirectory()) {
                if (IGNORE_DIRS.has(e.name))
                    continue;
                // Skip ALL hidden directories (names starting with `.`). This
                // intentionally excludes .claude/, .cursor/, .github/, .husky/,
                // .aperant/, etc. — framework and tooling config dirs should not
                // inflate the source-file count used to classify a project as
                // brownfield. NOTE: if your project stores significant source files
                // under a hidden directory (unusual), run apt-tools bootstrap with
                // --force-brownfield to override the heuristic. (LOG-003)
                if (e.name.startsWith('.'))
                    continue;
                walk(join(dir, e.name), depth + 1);
            }
            else if (e.isFile()) {
                if (SOURCE_EXTENSIONS.has(extname(e.name))) {
                    count += 1;
                    if (count >= SCAN_CAP) {
                        capped = true;
                        return;
                    }
                }
            }
        }
    }
    walk(rootDir, 0);
    return { count, capped };
}
/**
 * Best-effort commit count. 0 on any failure — D-03 treats missing git as
 * "low history" which favors greenfield, matching the intent.
 *
 * @param {string} projectDir
 * @returns {number}
 */
function readCommitCount(projectDir) {
    try {
        const out = execFileSync('git', ['rev-list', '--count', 'HEAD'], {
            cwd: projectDir,
            encoding: 'utf-8',
            stdio: ['ignore', 'pipe', 'ignore'],
        }).trim();
        const n = Number.parseInt(out, 10);
        return Number.isFinite(n) && n >= 0 ? n : 0;
    }
    catch {
        return 0;
    }
}
/**
 * Classify a project as greenfield or brownfield per D-03.
 *
 * @param {string} projectDir
 * @returns {DetectResult}
 */
export function detectMode(projectDir) {
    const resolved = resolve(projectDir);
    const { count: sourceFileCount, capped: sourceCountCapped } = countSourceFiles(resolved);
    const hasReadme = existsSync(join(resolved, 'README.md'));
    const hasGit = existsSync(join(resolved, '.git'));
    const commitCount = hasGit ? readCommitCount(resolved) : 0;
    const lowSourceCount = sourceFileCount < GREENFIELD_SOURCE_MAX;
    const lowCommitGate = !hasGit || commitCount <= GREENFIELD_COMMIT_MAX;
    const isGreenfield = lowSourceCount && !hasReadme && lowCommitGate;
    const mode = isGreenfield ? 'greenfield' : 'brownfield';
    const rationale = [
        `source_file_count=${sourceFileCount}${sourceCountCapped ? ' (capped)' : ''} ${lowSourceCount ? '< ' : '≥ '}${GREENFIELD_SOURCE_MAX} (greenfield gate)`,
        `README.md ${hasReadme ? 'present' : 'missing'} (${hasReadme ? 'brownfield' : 'greenfield'} signal)`,
        hasGit
            ? `git commit_count=${commitCount} ${commitCount <= GREENFIELD_COMMIT_MAX ? '≤ ' : '> '}${GREENFIELD_COMMIT_MAX} (greenfield gate)`
            : 'no .git directory (treated as greenfield signal)',
        `compound → ${mode}`,
    ];
    return {
        mode,
        signals: { sourceFileCount, sourceCountCapped, hasReadme, hasGit, commitCount },
        rationale,
    };
}
export { GREENFIELD_COMMIT_MAX, GREENFIELD_SOURCE_MAX, SCAN_CAP };
//# sourceMappingURL=detect-mode.mjs.map