/**
 * Project Profile Analysis Tools
 *
 * Deterministic extraction of project stack, file classifications, and
 * framework-specific conventions. Produces a JSON profile conforming to
 * the zuvo project-profile schema v1.0.
 */
import { readFile, writeFile, access, readdir, mkdir } from "node:fs/promises";
import { readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
import { getCodeIndex } from "./index-tools.js";
import { extractAstroConventions } from "./astro-config.js";
// ---------------------------------------------------------------------------
// Versioning — used by get_extractor_versions
// ---------------------------------------------------------------------------
export const EXTRACTOR_VERSIONS = {
    stack_detector: "1.0.0",
    file_classifier: "1.0.0",
    hono: "2.0.0",
    nestjs: "1.0.0",
    nextjs: "1.0.0",
    express: "1.0.0",
    react: "2.0.0", // Wave 1-4 + Tier 1-4: component/hook kinds, classifyReactKind, React class heritage — forces cache invalidation
    typescript: "3.0.0", // v3: extends/implements heritage, generics in signatures, enum members as constants, is_async flag, modifiers + accessor_kind in meta, namespace + ambient module declarations, anonymous default export synth, RangeError guard, AST-based import-graph branch with type_only flag + tsconfig paths resolution + find_circular_deps type-only filter (was v2.1: field_definition, CommonJS exports, object-literal methods, class_static_block, generator_function_declaration)
    javascript: "1.0.0", // independent JS version — bump for JS-only extractor changes (CJS, .jsx-specific) without forcing TS reindex
    python: "1.0.0",
    php: "2.0.0", // v2: extends/implements/uses_traits/modifiers/attributes on classes; typed properties + readonly + @var fallback on properties; promoted constructor params as synthetic field symbols; backed enum types; method-level modifiers + attributes; Codeception base classes (Unit/Cest/Cept) classify as test_suite — forces full reindex of PHP files
    astro: "1.0.0",
    kotlin: "1.0.0",
    monorepo: "1.0.0", // workspace resolver — bump forces reindex when workspace schema changes
};
// ---------------------------------------------------------------------------
// Stack Detector
// ---------------------------------------------------------------------------
async function fileExists(path) {
    try {
        await access(path);
        return true;
    }
    catch {
        return false;
    }
}
async function readJson(path) {
    try {
        return JSON.parse(await readFile(path, "utf-8"));
    }
    catch {
        return null;
    }
}
export async function detectStack(projectRoot) {
    const detected_from = [];
    const pkg = await readJson(join(projectRoot, "package.json"));
    // Framework detection
    let framework = null;
    let framework_version = null;
    if (pkg) {
        const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
        const frameworkMap = [
            ["hono", "hono"],
            ["@nestjs/core", "nestjs"],
            ["next", "nextjs"],
            ["nuxt", "nuxt"],
            ["@remix-run/node", "remix"],
            ["astro", "astro"],
            ["express", "express"],
            ["fastify", "fastify"],
            ["@angular/core", "angular"],
            ["vue", "vue"],
            ["svelte", "svelte"],
        ];
        for (const [dep, name] of frameworkMap) {
            if (allDeps?.[dep]) {
                framework = name;
                framework_version = allDeps[dep]?.replace(/^[\^~>=<]/, "") ?? null;
                detected_from.push(`package.json:dependencies.${dep}`);
                break;
            }
        }
        // React detection (only if no framework found — React is often a sub-dep)
        if (!framework && allDeps?.["react"]) {
            framework = "react";
            framework_version = allDeps["react"]?.replace(/^[\^~>=<]/, "") ?? null;
            detected_from.push("package.json:dependencies.react");
        }
    }
    // Python framework detection (if no JS framework found)
    if (!framework) {
        const pyproject = await readJson(join(projectRoot, "pyproject.toml")) ?? null;
        const requirements = await readFile(join(projectRoot, "requirements.txt"), "utf-8").catch(() => "");
        const pipfile = await readFile(join(projectRoot, "Pipfile"), "utf-8").catch(() => "");
        const pyDeps = requirements + pipfile + (pyproject ? JSON.stringify(pyproject) : "");
        if (pyDeps.includes("fastapi")) {
            framework = "fastapi";
            detected_from.push("python:fastapi");
        }
        else if (pyDeps.includes("django")) {
            framework = "django";
            detected_from.push("python:django");
        }
        else if (pyDeps.includes("flask")) {
            framework = "flask";
            detected_from.push("python:flask");
        }
    }
    // PHP framework detection
    if (!framework) {
        const composer = await readJson(join(projectRoot, "composer.json"));
        if (composer) {
            const phpDeps = { ...composer.require, ...composer["require-dev"] };
            if (phpDeps?.["laravel/framework"]) {
                framework = "laravel";
                framework_version = phpDeps["laravel/framework"]?.replace(/^[\^~>=<]/, "") ?? null;
                detected_from.push("composer.json:require.laravel/framework");
            }
            else if (phpDeps?.["yiisoft/yii2"]) {
                framework = "yii2";
                framework_version = phpDeps["yiisoft/yii2"]?.replace(/^[\^~>=<]/, "") ?? null;
                detected_from.push("composer.json:require.yiisoft/yii2");
            }
            else if (phpDeps?.["symfony/framework-bundle"]) {
                framework = "symfony";
                detected_from.push("composer.json:require.symfony/framework-bundle");
            }
        }
    }
    // Language detection
    let language = "javascript";
    let language_version = null;
    // Check for Python first
    if (["fastapi", "django", "flask"].includes(framework ?? "")) {
        language = "python";
        detected_from.push("framework implies python");
    }
    else if (await fileExists(join(projectRoot, "pyproject.toml")) || await fileExists(join(projectRoot, "requirements.txt"))) {
        language = "python";
        detected_from.push("pyproject.toml or requirements.txt");
    }
    // Check for PHP
    if (["laravel", "symfony", "yii2"].includes(framework ?? "")) {
        language = "php";
        detected_from.push("framework implies php");
    }
    // TypeScript/JavaScript (only if not already Python/PHP)
    if (language === "javascript") {
        const tsconfig = await readJson(join(projectRoot, "tsconfig.json"));
        if (tsconfig) {
            language = "typescript";
            language_version = tsconfig?.compilerOptions?.target ?? null;
            detected_from.push("tsconfig.json");
        }
    }
    // Test runner detection
    let test_runner = null;
    if (language === "python") {
        if (await fileExists(join(projectRoot, "pytest.ini")) || await fileExists(join(projectRoot, "conftest.py"))) {
            test_runner = "pytest";
            detected_from.push("pytest.ini or conftest.py");
        }
    }
    else if (language === "php") {
        if (await fileExists(join(projectRoot, "phpunit.xml")) || await fileExists(join(projectRoot, "phpunit.xml.dist"))) {
            test_runner = "phpunit";
            detected_from.push("phpunit.xml");
        }
    }
    else if (pkg) {
        const devDeps = pkg.devDependencies ?? {};
        if (devDeps["vitest"]) {
            test_runner = "vitest";
            detected_from.push("package.json:devDependencies.vitest");
        }
        else if (devDeps["jest"]) {
            test_runner = "jest";
            detected_from.push("package.json:devDependencies.jest");
        }
        else if (devDeps["mocha"]) {
            test_runner = "mocha";
            detected_from.push("package.json:devDependencies.mocha");
        }
    }
    // Package manager detection
    let package_manager = null;
    if (await fileExists(join(projectRoot, "pnpm-lock.yaml"))) {
        package_manager = "pnpm";
        detected_from.push("pnpm-lock.yaml");
    }
    else if (await fileExists(join(projectRoot, "yarn.lock"))) {
        package_manager = "yarn";
        detected_from.push("yarn.lock");
    }
    else if (await fileExists(join(projectRoot, "package-lock.json"))) {
        package_manager = "npm";
        detected_from.push("package-lock.json");
    }
    else if (await fileExists(join(projectRoot, "bun.lockb"))) {
        package_manager = "bun";
        detected_from.push("bun.lockb");
    }
    // Monorepo detection — resolveWorkspaces() (Task 4) is the primary path.
    // Falls back to the legacy regex YAML parser when resolveWorkspaces returns
    // null (covers @manypkg's edge cases such as malformed configs).
    let monorepo = null;
    // Read raw workspace globs from manifest first — they are the canonical
    // shape exposed by `monorepo.workspaces` (string[] of patterns).
    let manifestWorkspaces = null;
    if (pkg?.workspaces) {
        manifestWorkspaces = Array.isArray(pkg.workspaces)
            ? pkg.workspaces
            : pkg.workspaces.packages ?? [];
        detected_from.push("package.json:workspaces");
    }
    else if (await fileExists(join(projectRoot, "pnpm-workspace.yaml"))) {
        try {
            const content = await readFile(join(projectRoot, "pnpm-workspace.yaml"), "utf-8");
            manifestWorkspaces =
                content.match(/- ['"]?([^'"]+)['"]?/g)?.map((m) => m.replace(/- ['"]?/, "").replace(/['"]$/, "")) ?? [];
            detected_from.push("pnpm-workspace.yaml");
        }
        catch {
            /* ignore parse errors */
        }
    }
    if (manifestWorkspaces) {
        const turboExists = await fileExists(join(projectRoot, "turbo.json"));
        const nxExists = await fileExists(join(projectRoot, "nx.json"));
        const tool = turboExists
            ? "turborepo"
            : nxExists
                ? "nx"
                : pkg?.workspaces
                    ? "workspaces"
                    : "pnpm-workspaces";
        // Try the rich resolver (Task 4) — graceful degradation if it returns null
        let workspace_details;
        try {
            const { resolveWorkspaces } = await import("../storage/workspace-resolver.js");
            const resolved = await resolveWorkspaces(projectRoot);
            if (resolved) {
                workspace_details = resolved.workspaces;
                detected_from.push("workspace-resolver");
            }
        }
        catch {
            /* fall back to manifest-only data — graceful */
        }
        monorepo = {
            tool,
            workspaces: manifestWorkspaces,
            ...(workspace_details ? { workspace_details } : {}),
        };
    }
    // Monorepo workspace scanning — if root has no framework/test_runner, scan workspaces
    if (monorepo && (!framework || !test_runner || language === "javascript")) {
        const workspacePatterns = monorepo.workspaces;
        const workspaceDirs = [];
        for (const pattern of workspacePatterns) {
            // Expand simple glob patterns like "apps/*" or "packages/*"
            const base = pattern.replace(/\/?\*$/, "");
            const baseDir = join(projectRoot, base);
            try {
                const entries = await readdir(baseDir, { withFileTypes: true });
                for (const entry of entries) {
                    if (entry.isDirectory()) {
                        workspaceDirs.push(join(baseDir, entry.name));
                    }
                }
            }
            catch { /* directory doesn't exist */ }
        }
        const frameworkMap = [
            ["hono", "hono"],
            ["@nestjs/core", "nestjs"],
            ["next", "nextjs"],
            ["nuxt", "nuxt"],
            ["@remix-run/node", "remix"],
            ["astro", "astro"],
            ["express", "express"],
            ["fastify", "fastify"],
        ];
        for (const wsDir of workspaceDirs) {
            const wsPkg = await readJson(join(wsDir, "package.json"));
            if (!wsPkg)
                continue;
            const wsDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
            const wsName = relative(projectRoot, wsDir);
            // Framework from workspace (prefer backend frameworks: hono, nestjs, express)
            if (!framework) {
                for (const [dep, name] of frameworkMap) {
                    if (wsDeps?.[dep]) {
                        framework = name;
                        framework_version = wsDeps[dep]?.replace(/^[\^~>=<]/, "") ?? null;
                        detected_from.push(`${wsName}/package.json:dependencies.${dep}`);
                        break;
                    }
                }
            }
            // Test runner from workspace
            if (!test_runner) {
                if (wsDeps?.["vitest"]) {
                    test_runner = "vitest";
                    detected_from.push(`${wsName}/package.json:devDependencies.vitest`);
                }
                else if (wsDeps?.["jest"]) {
                    test_runner = "jest";
                    detected_from.push(`${wsName}/package.json:devDependencies.jest`);
                }
            }
            // TypeScript from workspace
            if (language === "javascript") {
                const wsTsconfig = await readJson(join(wsDir, "tsconfig.json"));
                if (wsTsconfig) {
                    language = "typescript";
                    language_version = wsTsconfig?.compilerOptions?.target ?? null;
                    detected_from.push(`${wsName}/tsconfig.json`);
                }
            }
        }
    }
    // Also check root tsconfig.base.json for monorepos that use base config
    if (language === "javascript") {
        const baseTsconfig = await readJson(join(projectRoot, "tsconfig.base.json"));
        if (baseTsconfig) {
            language = "typescript";
            language_version = baseTsconfig?.compilerOptions?.target ?? null;
            detected_from.push("tsconfig.base.json");
        }
    }
    // Build tool detection (Vite, CRA, webpack, Parcel, esbuild, Rspack, Turbopack)
    // Order matters: check more specific/modern tools first.
    let build_tool = null;
    if (pkg) {
        const devDeps = pkg.devDependencies ?? {};
        const deps = pkg.dependencies ?? {};
        const allDeps = { ...deps, ...devDeps };
        if (allDeps["vite"]) {
            build_tool = "vite";
            detected_from.push("package.json:vite");
        }
        else if (allDeps["react-scripts"]) {
            build_tool = "cra";
            detected_from.push("package.json:react-scripts");
        }
        else if (allDeps["@rsbuild/core"]) {
            build_tool = "rsbuild";
            detected_from.push("package.json:@rsbuild/core");
        }
        else if (allDeps["@rspack/cli"] || allDeps["@rspack/core"]) {
            build_tool = "rspack";
            detected_from.push("package.json:@rspack/*");
        }
        else if (allDeps["parcel"] || allDeps["parcel-bundler"]) {
            build_tool = "parcel";
            detected_from.push("package.json:parcel");
        }
        else if (allDeps["webpack"] || allDeps["webpack-cli"]) {
            build_tool = "webpack";
            detected_from.push("package.json:webpack");
        }
        else if (allDeps["esbuild"]) {
            build_tool = "esbuild";
            detected_from.push("package.json:esbuild");
        }
        else if (allDeps["turbopack"]) {
            build_tool = "turbopack";
            detected_from.push("package.json:turbopack");
        }
    }
    // Fallback: look for config files if no dep match
    if (!build_tool) {
        const configChecks = [
            ["vite.config.ts", "vite"],
            ["vite.config.js", "vite"],
            ["vite.config.mjs", "vite"],
            ["webpack.config.js", "webpack"],
            ["webpack.config.ts", "webpack"],
            ["rspack.config.js", "rspack"],
            ["rsbuild.config.ts", "rsbuild"],
            [".parcelrc", "parcel"],
        ];
        for (const [file, tool] of configChecks) {
            if (await fileExists(join(projectRoot, file))) {
                build_tool = tool;
                detected_from.push(file);
                break;
            }
        }
    }
    return {
        framework,
        framework_version,
        language,
        language_version,
        test_runner,
        package_manager,
        build_tool,
        monorepo,
        detected_from,
    };
}
// ---------------------------------------------------------------------------
// File Classifier
// ---------------------------------------------------------------------------
const CRITICAL_PATH_PATTERNS = [
    /\/(app|main|server)\.(ts|js|tsx|jsx)$/,
    /\/middleware\//,
    /\/security\//,
    /\/auth\//,
    /\/crypto\//,
];
/** index.ts is critical ONLY at shallow depth (src/index.ts, apps/api/src/index.ts) — not barrel re-exports */
function isEntryPointIndex(path) {
    if (!/\/index\.(ts|js|tsx|jsx)$/.test(path))
        return false;
    // Count path depth — barrel files are deep (components/Foo/index.ts = 3+ segments after src)
    const parts = path.split("/");
    const srcIdx = parts.indexOf("src");
    if (srcIdx === -1)
        return parts.length <= 3; // no src dir — shallow if <=3 segments
    const depthAfterSrc = parts.length - srcIdx - 2; // segments between src/ and index.ts
    return depthAfterSrc <= 1; // src/index.ts (0) or src/app/index.ts (1) = critical
}
const IMPORTANT_PATH_PATTERNS = [
    /\/services?\//,
    /\/controllers?\//,
    /\/routes?\//,
    /\/handlers?\//,
    /\/resolvers?\//,
];
const ROUTINE_PATH_PATTERNS = [
    /\/utils?\//,
    /\/helpers?\//,
    /\/constants?\//,
    /\/types?\//,
    /\/interfaces?\//,
    /\/config\//,
    /\/dto\//,
    /\/schemas?\//,
];
function isTestFile(path) {
    return /\.(test|spec)\.(ts|js|tsx|jsx)$/.test(path);
}
function classifyCodeType(path, _symbol_count) {
    if (/\/(app|main|server)\.(ts|js)$/.test(path))
        return "ORCHESTRATOR";
    if (/\/middleware\//.test(path))
        return "GUARD";
    if (/\/auth\//.test(path))
        return "GUARD";
    if (/\.(service|repository)\.(ts|js)$/.test(path))
        return "SERVICE";
    if (/\.(controller|handler)\.(ts|js)$/.test(path))
        return "CONTROLLER";
    if (/\.(component|page)\.(tsx|jsx)$/.test(path))
        return "COMPONENT";
    if (/\/hooks?\//.test(path) || /\.hook\.(ts|js)$/.test(path))
        return "HOOK";
    if (/\/utils?\//.test(path) || /\/helpers?\//.test(path))
        return "PURE";
    if (/\/types?\//.test(path) || /\.d\.ts$/.test(path))
        return "TYPE_DEF";
    if (/\/constants?\//.test(path))
        return "CONSTANT";
    return "PURE";
}
export function classifyFiles(index) {
    const critical = [];
    const important = [];
    const routineCounts = {};
    let routineCount = 0;
    // Build importer count map
    const importerCount = new Map();
    for (const sym of index.symbols) {
        // Count how many unique files import each file
        if (sym.source?.includes("import ") || sym.source?.includes("require(")) {
            // Simplified — in production this would use the actual import graph
        }
    }
    // Build test file set for has_tests detection
    const testFiles = new Set(index.files
        .filter((f) => isTestFile(f.path))
        .map((f) => f.path));
    function hasTests(filePath) {
        const base = filePath.replace(/\.(ts|js|tsx|jsx)$/, "");
        return (testFiles.has(`${base}.test.ts`) ||
            testFiles.has(`${base}.test.tsx`) ||
            testFiles.has(`${base}.spec.ts`) ||
            testFiles.has(`${base}.spec.tsx`) ||
            testFiles.has(`${base}.test.js`) ||
            testFiles.has(`${base}.spec.js`));
    }
    for (const file of index.files) {
        if (isTestFile(file.path))
            continue; // skip test files
        if (/node_modules|\.d\.ts$|\.json$|\.md$|\.css$|\.scss$/.test(file.path))
            continue;
        const code_type = classifyCodeType(file.path, file.symbol_count);
        const dependents = importerCount.get(file.path) ?? 0;
        // Tier assignment
        const isCritical = CRITICAL_PATH_PATTERNS.some((p) => p.test(file.path)) || isEntryPointIndex(file.path) || dependents > 5;
        const isImportant = IMPORTANT_PATH_PATTERNS.some((p) => p.test(file.path)) || file.symbol_count > 3;
        const isRoutine = ROUTINE_PATH_PATTERNS.some((p) => p.test(file.path));
        if (isCritical) {
            const reason = dependents > 5
                ? `Hub module (${dependents} importers)`
                : CRITICAL_PATH_PATTERNS.find((p) => p.test(file.path))?.source.replace(/\\\//g, "/") ?? "entry point";
            critical.push({
                path: file.path,
                code_type,
                reason,
                dependents_count: dependents,
                has_tests: hasTests(file.path),
            });
        }
        else if (isImportant && !isRoutine) {
            important.push({
                path: file.path,
                code_type,
                dependents_count: dependents,
                has_tests: hasTests(file.path),
            });
        }
        else {
            routineCount++;
            routineCounts[code_type] = (routineCounts[code_type] ?? 0) + 1;
        }
    }
    // Compact important tier: aggregate by type + keep only top 30 by dependents
    const importantCounts = {};
    for (const f of important) {
        importantCounts[f.code_type] = (importantCounts[f.code_type] ?? 0) + 1;
    }
    const topImportant = important
        .sort((a, b) => b.dependents_count - a.dependents_count)
        .slice(0, 30);
    return {
        critical,
        important: { count: important.length, by_type: importantCounts, top: topImportant },
        routine: { count: routineCount, by_type: routineCounts },
    };
}
// ---------------------------------------------------------------------------
// Hono Extractor
// ---------------------------------------------------------------------------
let _honoFallbackCount = 0;
export function getHonoFallbackCount() { return _honoFallbackCount; }
/**
 * Extract Hono conventions from an orchestrator file.
 *
 * Uses the tree-sitter AST extractor (HonoExtractor) and adapts the result
 * to the legacy Conventions shape. Falls back to the regex-based legacy
 * implementation on failure (with counter tracking for observability).
 *
 * Kill switch: set CODESIFT_LEGACY_HONO=1 to force legacy extractor.
 */
export async function extractHonoConventions(source, filePath) {
    if (process.env.CODESIFT_LEGACY_HONO === "1") {
        const { legacyExtractHonoConventions } = await import("./legacy-hono-conventions.js");
        return legacyExtractHonoConventions(source, filePath);
    }
    // If called with a source string and a file path that doesn't exist on disk,
    // this is the legacy string-fixture API — use legacy extractor directly.
    // The AST extractor needs a real file to parse.
    const { existsSync } = await import("node:fs");
    const { isAbsolute, resolve: pathResolve } = await import("node:path");
    const resolved = isAbsolute(filePath) ? filePath : pathResolve(filePath);
    if (!existsSync(resolved)) {
        const { legacyExtractHonoConventions } = await import("./legacy-hono-conventions.js");
        return legacyExtractHonoConventions(source, filePath);
    }
    try {
        return await honoConventionsAdapter(resolved);
    }
    catch (err) {
        _honoFallbackCount++;
        const msg = err instanceof Error ? err.message : String(err);
        console.warn(`[codesift] Hono AST extractor failed (fallback #${_honoFallbackCount}): ${msg}`);
        if (process.env.NODE_ENV !== "production" && process.env.CODESIFT_SILENT_FALLBACK !== "1") {
            throw err;
        }
        const { legacyExtractHonoConventions } = await import("./legacy-hono-conventions.js");
        return legacyExtractHonoConventions(source, filePath);
    }
}
/**
 * Adapter: run HonoExtractor.parse() and map HonoAppModel → Conventions.
 */
async function honoConventionsAdapter(filePath) {
    const { HonoExtractor } = await import("../parser/extractors/hono.js");
    const extractor = new HonoExtractor();
    const model = await extractor.parse(filePath);
    // Map middleware_chains
    const middleware_chains = model.middleware_chains.map((mc) => ({
        scope: mc.scope === "*" ? "global" : mc.scope.replace(/\/\*$/, "").split("/").filter(Boolean)[1] ?? mc.scope,
        file: mc.entries[0]?.file ?? filePath,
        chain: mc.entries.map((e) => ({
            name: e.name,
            line: e.line,
            order: e.order,
        })),
    }));
    // Map route_mounts
    const route_mounts = model.mounts
        .filter((m) => m.mount_type === "hono_route")
        .map((m) => ({
        file: filePath,
        line: 0, // line not tracked in new model at mount level
        mount_path: m.mount_path,
        imported_from: m.child_file || null,
        exported_as: m.child_var,
    }));
    // Map auth_patterns from middleware names
    const authGroups = {};
    let auth_middleware = null;
    for (const mc of model.middleware_chains) {
        const scope = mc.scope === "*" ? "global" : mc.scope.replace(/\/\*$/, "").split("/").filter(Boolean)[1] ?? mc.scope;
        for (const entry of mc.entries) {
            if (/auth|clerk|jwt|session|passport/i.test(entry.name)) {
                auth_middleware = entry.name;
                if (!authGroups[scope])
                    authGroups[scope] = { requires_auth: false, middleware: [] };
                authGroups[scope].requires_auth = true;
                if (!authGroups[scope].middleware.includes(entry.name)) {
                    authGroups[scope].middleware.push(entry.name);
                }
            }
        }
    }
    // Ensure all route groups represented
    for (const mount of model.mounts) {
        const group = mount.mount_path.split("/").filter(Boolean)[1] ?? "root";
        if (!authGroups[group])
            authGroups[group] = { requires_auth: false, middleware: [] };
    }
    return {
        middleware_chains,
        rate_limits: [], // rate limits extracted from AST in future; empty for now
        route_mounts,
        auth_patterns: { auth_middleware, groups: authGroups },
    };
}
// ---------------------------------------------------------------------------
// NestJS Extractor
// ---------------------------------------------------------------------------
/** Count net `[` vs `]` on a line — positive means more open brackets. */
function countBracketBalance(text) {
    let depth = 0;
    for (const ch of text) {
        if (ch === "[")
            depth++;
        else if (ch === "]")
            depth--;
    }
    return depth;
}
export function extractNestConventions(source, filePath) {
    const lines = source.split("\n");
    // Build import map
    const importMap = new Map();
    for (const line of lines) {
        const defaultImport = line.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
        if (defaultImport) {
            const names = defaultImport[1].split(",").map((n) => n.trim().split(/\s+as\s+/).pop().trim());
            for (const name of names) {
                importMap.set(name, defaultImport[2]);
            }
        }
    }
    const modules = [];
    const global_guards = [];
    const global_filters = [];
    const global_pipes = [];
    const global_interceptors = [];
    const controllers = [];
    let throttler = null;
    let inImports = false;
    let importsBracketDepth = 0;
    let inProviders = false;
    let providersBracketDepth = 0;
    let inControllers = false;
    let controllersBracketDepth = 0;
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const lineNum = i + 1;
        // Track @Module sections via bracket depth — handles both multi-line arrays
        // (imports: [\n  A,\n  B,\n]) and single-line arrays (imports: [A, B]).
        // Critical: mark inImports BEFORE scanning the current line so the scan
        // block runs on single-line `imports: [...]` arrays.
        let closeInImportsAfterLine = false;
        if (/imports:\s*\[/.test(line)) {
            inImports = true;
            importsBracketDepth += countBracketBalance(line.slice(line.indexOf("[")));
            if (importsBracketDepth <= 0) {
                closeInImportsAfterLine = true;
                importsBracketDepth = 0;
            }
        }
        else if (inImports) {
            importsBracketDepth += countBracketBalance(line);
            if (importsBracketDepth <= 0) {
                closeInImportsAfterLine = true;
                importsBracketDepth = 0;
            }
        }
        // R-1 fix: use bracket-depth tracking for providers/controllers too
        // (same approach as imports) so single-line arrays close correctly.
        let closeProvidersAfterLine = false;
        if (/providers:\s*\[/.test(line)) {
            inProviders = true;
            providersBracketDepth += countBracketBalance(line.slice(line.indexOf("[", line.indexOf("providers"))));
            if (providersBracketDepth <= 0) {
                closeProvidersAfterLine = true;
                providersBracketDepth = 0;
            }
        }
        else if (inProviders) {
            providersBracketDepth += countBracketBalance(line);
            if (providersBracketDepth <= 0) {
                closeProvidersAfterLine = true;
                providersBracketDepth = 0;
            }
        }
        let closeControllersAfterLine = false;
        if (/controllers:\s*\[/.test(line)) {
            inControllers = true;
            controllersBracketDepth += countBracketBalance(line.slice(line.indexOf("[", line.indexOf("controllers"))));
            if (controllersBracketDepth <= 0) {
                closeControllersAfterLine = true;
                controllersBracketDepth = 0;
            }
        }
        else if (inControllers) {
            controllersBracketDepth += countBracketBalance(line);
            if (controllersBracketDepth <= 0) {
                closeControllersAfterLine = true;
                controllersBracketDepth = 0;
            }
        }
        // Extract module imports — scan for all module names on the line (not just
        // the first indented one). Handles single-line `imports: [A, B.forFeature([...]), C]`
        // where multiple modules share one line.
        if (inImports) {
            const moduleRe = /(\w+Module)(?:\.for(Root|Feature)(?:Async)?\s*\()?/g;
            let moduleMatch;
            const matchedThisLine = new Set();
            while ((moduleMatch = moduleRe.exec(line)) !== null) {
                // Avoid duplicates within the same line
                const key = `${moduleMatch[1]}:${moduleMatch.index}`;
                if (matchedThisLine.has(key))
                    continue;
                matchedThisLine.add(key);
                const name = moduleMatch[1];
                const dynamicKind = moduleMatch[2]; // "Root" | "Feature" | undefined
                const isGlobal = /isGlobal:\s*true/.test(line) || /ConfigModule|SentryModule/.test(name);
                const entry = {
                    name,
                    file: filePath,
                    line: lineNum,
                    imported_from: importMap.get(name) ?? null,
                    is_global: isGlobal,
                };
                // G2: forFeature([...]) — extract entity class names (scan ahead up to 15 lines)
                if (dynamicKind === "Feature") {
                    const entities = [];
                    let bracketDepth = 0;
                    let started = false;
                    for (let j = i; j < Math.min(i + 15, lines.length); j++) {
                        for (const ch of lines[j]) {
                            if (ch === "[") {
                                bracketDepth++;
                                started = true;
                            }
                            else if (ch === "]") {
                                bracketDepth--;
                                if (started && bracketDepth === 0)
                                    break;
                            }
                        }
                        // Capture entity names after the opening [
                        const featureMatch = lines[j].match(/forFeature\s*\(\s*\[([^\]]*)\]?/);
                        if (featureMatch) {
                            const inner = featureMatch[1];
                            for (const m of inner.matchAll(/\b([A-Z]\w*)\b/g))
                                entities.push(m[1]);
                            // Continue to next lines if the array spans multiple
                            for (let k = j + 1; k < Math.min(j + 10, lines.length) && !/\]/.test(lines[k - 1]); k++) {
                                for (const m of lines[k].matchAll(/\b([A-Z]\w*)\b/g))
                                    entities.push(m[1]);
                            }
                            break;
                        }
                        // Multi-line case: forFeature([ on one line, entities on next
                        if (/forFeature\s*\(\s*\[\s*$/.test(lines[j])) {
                            for (let k = j + 1; k < Math.min(j + 10, lines.length); k++) {
                                if (/^\s*\]/.test(lines[k]))
                                    break;
                                for (const m of lines[k].matchAll(/\b([A-Z]\w*)\b/g))
                                    entities.push(m[1]);
                            }
                            break;
                        }
                    }
                    if (entities.length > 0)
                        entry.entities = entities;
                }
                // G2: forRoot({...}) — extract top-level config keys
                if (dynamicKind === "Root") {
                    const keys = [];
                    let braceDepth = 0;
                    for (let j = i; j < Math.min(i + 15, lines.length); j++) {
                        for (const ch of lines[j]) {
                            if (ch === "{")
                                braceDepth++;
                            else if (ch === "}")
                                braceDepth--;
                        }
                        // Only capture keys at the top-level of the forRoot config object
                        if (braceDepth >= 1) {
                            // Match indented key: value pattern at top-level (heuristic: 1-level indent)
                            const keyMatch = lines[j].match(/^\s{6,10}(\w+):\s*/);
                            if (keyMatch && !keys.includes(keyMatch[1]))
                                keys.push(keyMatch[1]);
                        }
                        if (braceDepth === 0 && j > i)
                            break;
                    }
                    if (keys.length > 0)
                        entry.dynamic_config_keys = keys;
                }
                modules.push(entry);
            }
            // Extract ThrottlerModule config
            if (/ThrottlerModule/.test(line)) {
                // Scan ahead for ttl and limit
                for (let j = i; j < Math.min(i + 15, lines.length); j++) {
                    const ttlMatch = lines[j].match(/ttl:\s*(\d+)/);
                    const limitMatch = lines[j].match(/limit:\s*(?:.*?:\s*)?(\d+)/);
                    if (ttlMatch && !throttler) {
                        throttler = { ttl: parseInt(ttlMatch[1]), limit: 60 };
                    }
                    if (limitMatch && throttler) {
                        // Take the production value (non-development)
                        const allLimits = lines[j].match(/(\d+)/g);
                        if (allLimits && allLimits.length > 0) {
                            throttler.limit = parseInt(allLimits[allLimits.length - 1]);
                        }
                    }
                }
            }
        }
        // Extract controllers
        if (inControllers) {
            const ctrlMatch = line.match(/(\w+Controller)\b/);
            if (ctrlMatch)
                controllers.push(ctrlMatch[1]);
        }
        // Extract global providers (APP_GUARD, APP_FILTER, APP_PIPE)
        if (inProviders) {
            if (/provide:\s*APP_GUARD/.test(line)) {
                // Scan for useClass on next lines
                for (let j = i; j < Math.min(i + 5, lines.length); j++) {
                    const useClassMatch = lines[j].match(/useClass:\s*(\w+)/);
                    if (useClassMatch) {
                        global_guards.push({
                            name: useClassMatch[1],
                            token: "APP_GUARD",
                            file: filePath,
                            line: j + 1,
                            imported_from: importMap.get(useClassMatch[1]) ?? null,
                        });
                        break;
                    }
                }
            }
            if (/provide:\s*APP_FILTER/.test(line)) {
                for (let j = i; j < Math.min(i + 5, lines.length); j++) {
                    const useClassMatch = lines[j].match(/useClass:\s*(\w+)/);
                    if (useClassMatch) {
                        global_filters.push({
                            name: useClassMatch[1],
                            token: "APP_FILTER",
                            file: filePath,
                            line: j + 1,
                            imported_from: importMap.get(useClassMatch[1]) ?? null,
                        });
                        break;
                    }
                }
            }
            if (/provide:\s*APP_PIPE/.test(line)) {
                for (let j = i; j < Math.min(i + 5, lines.length); j++) {
                    const useClassMatch = lines[j].match(/useClass:\s*(\w+)/);
                    if (useClassMatch) {
                        global_pipes.push({
                            name: useClassMatch[1],
                            token: "APP_PIPE",
                            file: filePath,
                            line: j + 1,
                            imported_from: importMap.get(useClassMatch[1]) ?? null,
                        });
                        break;
                    }
                }
            }
            if (/provide:\s*APP_INTERCEPTOR/.test(line)) {
                for (let j = i; j < Math.min(i + 5, lines.length); j++) {
                    const useClassMatch = lines[j].match(/useClass:\s*(\w+)/);
                    if (useClassMatch) {
                        global_interceptors.push({
                            name: useClassMatch[1],
                            token: "APP_INTERCEPTOR",
                            file: filePath,
                            line: j + 1,
                            imported_from: importMap.get(useClassMatch[1]) ?? null,
                        });
                        break;
                    }
                }
            }
        }
        // Close inImports at end of line iteration if brackets balanced
        if (closeInImportsAfterLine)
            inImports = false;
        if (closeProvidersAfterLine)
            inProviders = false;
        if (closeControllersAfterLine)
            inControllers = false;
    }
    // G1: parse middleware.configure(consumer) chains
    const middleware_chains = parseMiddlewareChains(source, filePath);
    return { modules, global_guards, global_filters, global_pipes, global_interceptors, controllers, throttler, middleware_chains };
}
/**
 * G1: Parse `configure(consumer: MiddlewareConsumer) { ... }` blocks.
 * Extracts consumer.apply(Middleware).forRoutes(...) chains.
 */
export function parseMiddlewareChains(source, filePath) {
    const results = [];
    // Find configure( method — can be `configure(consumer` or `configure(consumer: MiddlewareConsumer)`
    const configureStart = source.search(/\bconfigure\s*\(\s*\w+\s*(?::\s*\w+)?\s*\)\s*\{/);
    if (configureStart === -1)
        return results;
    // Extract body of configure method via brace counting
    const bodyStart = source.indexOf("{", configureStart);
    if (bodyStart === -1)
        return results;
    let depth = 1;
    let i = bodyStart + 1;
    while (i < source.length && depth > 0) {
        if (source[i] === "{")
            depth++;
        else if (source[i] === "}")
            depth--;
        i++;
    }
    const body = source.slice(bodyStart + 1, i - 1);
    // Match: consumer.apply(Middleware).forRoutes(<args>) — may chain multiple middlewares
    // Use a tolerant pattern that captures the apply(...) arg and forRoutes(...) arg separately.
    const applyRe = /\.apply\s*\(\s*([\w,\s]+)\s*\)\s*\.forRoutes\s*\(([\s\S]*?)\)\s*[;}]/g;
    let m;
    while ((m = applyRe.exec(body)) !== null) {
        const middlewareNames = m[1].split(",").map((s) => s.trim()).filter(Boolean);
        const routesArg = m[2];
        const routes = [];
        // Parse routesArg — supports:
        //   '*'
        //   'users/*'
        //   { path: 'users', method: RequestMethod.GET }
        //   ControllerClass
        const stringPathRe = /['"`]([^'"`]+)['"`]/g;
        let sm;
        while ((sm = stringPathRe.exec(routesArg)) !== null) {
            // Check if this string is part of a { path: '...', method: ... } object literal
            const before = routesArg.slice(Math.max(0, sm.index - 30), sm.index);
            if (/path:\s*$/.test(before)) {
                // Object form — capture path AND method
                const objRe = /path:\s*['"`]([^'"`]+)['"`]\s*,\s*method:\s*(?:RequestMethod\.)?(\w+)/;
                const afterContext = routesArg.slice(Math.max(0, sm.index - 20), sm.index + 200);
                const objMatch = objRe.exec(afterContext);
                if (objMatch) {
                    routes.push({ path: objMatch[1], method: objMatch[2] });
                    continue;
                }
            }
            routes.push({ path: sm[1] });
        }
        // Also capture bare ControllerClass (PascalCase identifier) if present
        for (const name of middlewareNames) {
            // R-3 fix: guard indexOf returning -1 (would produce wrong line via slice(0,-1))
            const namePos = source.indexOf(name, configureStart);
            const line = namePos >= 0 ? source.slice(0, namePos).split("\n").length : 0;
            results.push({ middleware: name, routes, file: filePath, line });
        }
    }
    return results;
}
export function extractNextConventions(_projectRoot, files) {
    const pages = [];
    const api_routes = [];
    const inngest_functions = [];
    const webhooks = [];
    let services_count = 0;
    let client_component_count = 0;
    let server_action_count = 0;
    let middleware = null;
    const hasAppDir = files.some((f) => f.path.includes("app/"));
    const hasSrcDir = files.some((f) => f.path.startsWith("src/"));
    const hasI18n = files.some((f) => f.path.includes("[locale]") || f.path.includes("i18n"));
    for (const file of files) {
        const p = file.path;
        // Middleware
        if (/^(src\/)?middleware\.(ts|js)$/.test(p)) {
            middleware = { file: p, matchers: [] };
        }
        // App Router pages (paths from index have no leading /)
        if (/app\/.*\/page\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "page" });
        }
        if (/app\/.*\/layout\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "layout" });
        }
        if (/app\/.*\/loading\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "loading" });
        }
        if (/app\/.*\/error\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "error" });
        }
        if (/app\/.*\/not-found\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "not-found" });
        }
        if (/app\/.*\/global-error\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "global-error" });
        }
        if (/app\/.*\/default\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "default" });
        }
        if (/app\/.*\/template\.(tsx|jsx|ts|js)$/.test(p)) {
            pages.push({ path: p, type: "template" });
        }
        // API routes (App Router — route.ts files under app/api/)
        if (/app\/api\/.*route\.(ts|js)$/.test(p)) {
            api_routes.push({ path: p, methods: [], file: p });
        }
        // Pages Router API routes
        if (/pages\/api\//.test(p)) {
            api_routes.push({ path: p, methods: [], file: p });
        }
        // Inngest functions
        if (/inngest\/.*\.(ts|js)$/.test(p) && !/\.test\./.test(p) && !/\.spec\./.test(p) && !/index\./.test(p)) {
            inngest_functions.push(p);
        }
        // Services
        if (/services?\/[^/]+\.(ts|js)$/.test(p) && !/\.test\./.test(p) && !/\.spec\./.test(p) && !/\.d\.ts$/.test(p) && !/index\./.test(p)) {
            services_count++;
        }
        // Webhooks
        if (/webhook/.test(p) && /route\.(ts|js)$/.test(p)) {
            webhooks.push(p);
        }
        // Directive scanning — check first line for "use client" / "use server"
        if (/\.(tsx|ts|jsx|js)$/.test(p) && /app\//.test(p)) {
            try {
                const head = readFileSync(join(_projectRoot, p), { encoding: "utf8", flag: "r" }).slice(0, 80);
                if (/['"]use client['"]/.test(head))
                    client_component_count++;
                if (/['"]use server['"]/.test(head))
                    server_action_count++;
            }
            catch {
                // file may have been deleted since indexing
            }
        }
    }
    return {
        pages,
        middleware,
        api_routes,
        services_count,
        client_component_count,
        server_action_count,
        inngest_functions,
        webhooks,
        config: { app_router: hasAppDir, src_dir: hasSrcDir, i18n: hasI18n },
    };
}
export function extractExpressConventions(source, filePath) {
    const lines = source.split("\n");
    const middleware = [];
    const routers = [];
    const error_handlers = [];
    // Import map
    const importMap = new Map();
    for (const line of lines) {
        const req = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
        if (req)
            importMap.set(req[1], req[2]);
        const imp = line.match(/import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/);
        if (imp) {
            const names = imp[1] ? imp[1].split(",").map((n) => n.trim()) : [imp[2]];
            for (const n of names)
                importMap.set(n, imp[3]);
        }
    }
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const lineNum = i + 1;
        // app.use(middleware)
        const useMatch = line.match(/app\.use\s*\(\s*(\w+)\s*\)/);
        if (useMatch) {
            middleware.push({ name: useMatch[1], file: filePath, line: lineNum });
            continue;
        }
        // app.use("/path", router)
        const routeMatch = line.match(/app\.use\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)\s*\)/);
        if (routeMatch) {
            routers.push({
                mount_path: routeMatch[1],
                file: filePath,
                line: lineNum,
                imported_from: importMap.get(routeMatch[2]) ?? null,
            });
            continue;
        }
        // Error handler: (err, req, res, next) => ...
        if (/app\.use\s*\(\s*(?:function\s*)?\(\s*err\s*,/.test(line) || /app\.use\s*\(\s*\(\s*err\s*:/.test(line)) {
            error_handlers.push({ file: filePath, line: lineNum });
        }
    }
    return { middleware, routers, error_handlers };
}
export function extractReactConventions(files, deps, symbols) {
    // State management
    let state_management = null;
    if (deps["@reduxjs/toolkit"] || deps["redux"])
        state_management = "redux";
    else if (deps["zustand"])
        state_management = "zustand";
    else if (deps["jotai"])
        state_management = "jotai";
    else if (deps["recoil"])
        state_management = "recoil";
    else if (deps["mobx"])
        state_management = "mobx";
    // Routing
    let routing = null;
    if (deps["react-router-dom"] || deps["react-router"])
        routing = "react-router";
    else if (deps["@tanstack/react-router"])
        routing = "tanstack-router";
    else if (deps["wouter"])
        routing = "wouter";
    // UI library
    // shadcn/ui detection: canonical path pattern is components/ui/*.tsx — checked
    // FIRST so it takes precedence over generic radix dep (shadcn re-exports radix).
    const hasShadcnFiles = files.some((f) => /(^|\/)components\/ui\/[a-z-]+\.(tsx|jsx)$/.test(f.path));
    let ui_library = null;
    if (hasShadcnFiles)
        ui_library = "shadcn";
    else if (deps["@mui/material"])
        ui_library = "mui";
    else if (deps["@chakra-ui/react"])
        ui_library = "chakra";
    else if (deps["antd"])
        ui_library = "antd";
    else if (deps["@radix-ui/react-dialog"] || deps["@radix-ui/themes"])
        ui_library = "radix";
    else if (deps["tailwindcss"])
        ui_library = "tailwind";
    // Form library detection (Item 7)
    let form_library = null;
    if (deps["react-hook-form"])
        form_library = "react-hook-form";
    else if (deps["formik"])
        form_library = "formik";
    else if (deps["final-form"] || deps["react-final-form"])
        form_library = "final-form";
    // File-path-based component counts (legacy, coarse)
    let pages = 0, components = 0, hooks = 0;
    for (const f of files) {
        if (/\/pages?\//.test(f.path) && /\.(tsx|jsx)$/.test(f.path))
            pages++;
        else if (/\/components?\//.test(f.path) && /\.(tsx|jsx)$/.test(f.path))
            components++;
        if (/\/hooks?\//.test(f.path) || /\.hook\.(ts|js)$/.test(f.path))
            hooks++;
    }
    // Symbol-based semantic counts (requires Wave 1 extractor)
    let actual_component_count = 0;
    let actual_hook_count = 0;
    const hookUsageMap = new Map();
    const component_patterns = { memo: 0, forwardRef: 0, lazy: 0 };
    if (symbols) {
        // Set of stdlib hooks to exclude from "hook usage" tracking — we want
        // to highlight which library/custom hooks components consume.
        for (const sym of symbols) {
            if (sym.kind === "component") {
                actual_component_count++;
                if (sym.source) {
                    // Detect wrapper patterns in component source
                    // Generic-aware patterns: memo<Props>(...), forwardRef<T, P>(...), lazy<T>(...) — Item 9
                    if (/\b(?:React\.)?memo\s*(?:<[^>]+>)?\s*\(/.test(sym.source))
                        component_patterns.memo++;
                    if (/\b(?:React\.)?forwardRef\s*(?:<[^>]+>)?\s*\(/.test(sym.source))
                        component_patterns.forwardRef++;
                    if (/\b(?:React\.)?lazy\s*(?:<[^>]+>)?\s*\(/.test(sym.source))
                        component_patterns.lazy++;
                    // Count hook calls inside this component
                    const hookCalls = sym.source.matchAll(/\b(use[A-Z]\w*)\s*\(/g);
                    for (const m of hookCalls) {
                        const hookName = m[1];
                        hookUsageMap.set(hookName, (hookUsageMap.get(hookName) ?? 0) + 1);
                    }
                }
            }
            else if (sym.kind === "hook") {
                actual_hook_count++;
            }
        }
    }
    const hook_usage = [...hookUsageMap.entries()]
        .sort((a, b) => b[1] - a[1])
        .slice(0, 10)
        .map(([name, count]) => ({ name, count }));
    return {
        state_management,
        routing,
        ui_library,
        form_library,
        component_count: { pages, components, hooks },
        actual_component_count,
        actual_hook_count,
        hook_usage,
        component_patterns,
    };
}
export function extractPythonConventions(files) {
    const routers = [];
    const middlewareSet = new Set();
    let models_dir = null;
    let framework_type = null;
    for (const f of files) {
        // FastAPI routers
        if (/router\.py$|routes?\.py$/.test(f.path)) {
            routers.push({ path: f.path, file: f.path });
            if (!framework_type)
                framework_type = "fastapi";
        }
        // Django views/urls
        if (/views\.py$|urls\.py$/.test(f.path)) {
            routers.push({ path: f.path, file: f.path });
            if (!framework_type)
                framework_type = "django";
        }
        // Flask blueprints
        if (/blueprint/.test(f.path)) {
            routers.push({ path: f.path, file: f.path });
            if (!framework_type)
                framework_type = "flask";
        }
        // Middleware
        if (/middleware/.test(f.path) && f.path.endsWith(".py")) {
            middlewareSet.add(f.path);
        }
        // Models
        if (/models?\.py$/.test(f.path) && !models_dir) {
            const dir = f.path.split("/").slice(0, -1).join("/");
            models_dir = dir || null;
        }
    }
    // Test framework detection
    let test_framework = null;
    if (files.some((f) => /conftest\.py$/.test(f.path) || /test_.*\.py$/.test(f.path))) {
        test_framework = "pytest";
    }
    else if (files.some((f) => /tests?\.py$/.test(f.path))) {
        test_framework = "unittest";
    }
    return {
        framework_type,
        routers,
        middleware: [...middlewareSet],
        models_dir,
        test_framework,
    };
}
export function extractPhpConventions(files) {
    const controllers = [];
    const middleware = [];
    const models = [];
    const routes_files = [];
    let migrations_count = 0;
    for (const f of files) {
        const name = f.path.split("/").pop()?.replace(/\.php$/, "") ?? "";
        if (/Controller\.php$/.test(f.path)) {
            controllers.push({ name, path: f.path });
        }
        if (/(^|\/)[Mm]iddleware\//.test(f.path) && f.path.endsWith(".php")) {
            middleware.push({ name, path: f.path });
        }
        if (/(^|\/)[Mm]odels?\//.test(f.path) && f.path.endsWith(".php")) {
            models.push({ name, path: f.path });
        }
        if (/(^|\/)routes\//.test(f.path) && f.path.endsWith(".php")) {
            routes_files.push(f.path);
        }
        if (/(^|\/)migrations?\//.test(f.path)) {
            migrations_count++;
        }
    }
    return { controllers, middleware, models, routes_files, migrations_count };
}
export function extractYii2Conventions(files) {
    const base = extractPhpConventions(files);
    const modules = [];
    const widgets = [];
    const behaviors = [];
    const components = [];
    const assets = [];
    const config_files = [];
    for (const f of files) {
        const name = f.path.split("/").pop()?.replace(/\.php$/, "") ?? "";
        // Modules: Module.php in modules/*/ directories
        if (/(^|\/)modules\/[^/]+\/Module\.php$/.test(f.path)) {
            modules.push({ name, path: f.path });
        }
        // Widgets: files in widgets/ directories or named *Widget.php
        if ((/(^|\/)widgets\//.test(f.path) || /Widget\.php$/.test(f.path)) && f.path.endsWith(".php")) {
            widgets.push({ name, path: f.path });
        }
        // Behaviors: files in behaviors/ directories or named *Behavior.php
        if ((/(^|\/)behaviors\//.test(f.path) || /Behavior\.php$/.test(f.path)) && f.path.endsWith(".php")) {
            behaviors.push({ name, path: f.path });
        }
        // Components: files in components/ directory
        if (/(^|\/)components\//.test(f.path) && f.path.endsWith(".php")) {
            components.push({ name, path: f.path });
        }
        // Assets: files named *Asset.php in assets/ directory
        if (/(^|\/)assets\//.test(f.path) && /Asset\.php$/.test(f.path)) {
            assets.push({ name, path: f.path });
        }
        // Config files
        if (/config\/(web|console|db|params|main|test)\.php$/.test(f.path)) {
            config_files.push(f.path);
        }
    }
    return {
        ...base,
        framework_type: "yii2",
        modules,
        widgets,
        behaviors,
        components,
        assets,
        config_files,
    };
}
// ---------------------------------------------------------------------------
// Identity Extractor
// ---------------------------------------------------------------------------
async function extractIdentity(projectRoot) {
    const pkg = await readJson(join(projectRoot, "package.json"));
    const projectName = pkg?.name ?? projectRoot.split("/").pop() ?? "unknown";
    // Detect monorepo
    const isMonorepo = !!(pkg?.workspaces || await fileExists(join(projectRoot, "pnpm-workspace.yaml")));
    // Git remote
    let gitRemote = null;
    try {
        const { stdout } = await execFileAsync("git", ["config", "--get", "remote.origin.url"], {
            cwd: projectRoot, timeout: 3000,
        });
        gitRemote = stdout.toString().trim().replace(/\.git$/, "").replace(/^git@github\.com:/, "github.com/") || null;
    }
    catch { /* not a git repo or no remote */ }
    return {
        project_name: projectName,
        project_type: isMonorepo ? "monorepo" : "single",
        workspace_root: projectRoot,
        git_remote: gitRemote,
    };
}
// ---------------------------------------------------------------------------
// Dependency Graph Extractor
// ---------------------------------------------------------------------------
function extractDependencyGraph(index) {
    // Entry points: files matching app/main/server/index at shallow depth
    const entry_points = [];
    const importCount = new Map();
    // Count imports per file from symbols
    for (const sym of index.symbols) {
        if (sym.source) {
            const importMatches = sym.source.match(/from\s+['"]([^'"]+)['"]/g);
            if (importMatches) {
                for (const m of importMatches) {
                    const path = m.replace(/from\s+['"]/, "").replace(/['"]$/, "");
                    // Resolve relative imports to file paths
                    if (path.startsWith(".")) {
                        const resolved = join(sym.file.replace(/\/[^/]+$/, ""), path).replace(/\.(js|ts|tsx|jsx)$/, "");
                        importCount.set(resolved, (importCount.get(resolved) ?? 0) + 1);
                    }
                }
            }
        }
    }
    // Find entry points
    for (const f of index.files) {
        if (/\/(app|main|server)\.(ts|js|tsx)$/.test(f.path))
            entry_points.push(f.path);
        if (/^(src\/)?index\.(ts|js)$/.test(f.path))
            entry_points.push(f.path);
    }
    // Hub modules: files imported by many others
    const hub_modules = [...importCount.entries()]
        .filter(([, count]) => count >= 5)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 20)
        .map(([path, count]) => ({ path, imported_by_count: count }));
    // Leaf modules: files that import others but are not imported themselves
    const importedFiles = new Set(importCount.keys());
    const leaf_modules = index.files
        .filter((f) => !importedFiles.has(f.path.replace(/\.(ts|js|tsx|jsx)$/, "")) && !/(test|spec)\.(ts|js)$/.test(f.path))
        .slice(0, 30)
        .map((f) => f.path);
    // Orphan files: files with no imports AND not imported
    const orphan_files = index.files
        .filter((f) => {
        const base = f.path.replace(/\.(ts|js|tsx|jsx)$/, "");
        return !importedFiles.has(base) && f.symbol_count === 0 && !/(test|spec)\.(ts|js)$/.test(f.path);
    })
        .slice(0, 20)
        .map((f) => f.path);
    return { entry_points, hub_modules, leaf_modules, orphan_files };
}
// ---------------------------------------------------------------------------
// Test Conventions Extractor
// ---------------------------------------------------------------------------
async function extractTestConventions(projectRoot, index) {
    const testFiles = index.files.filter((f) => /(test|spec)\.(ts|js|tsx|jsx)$/.test(f.path));
    const file_patterns = [...new Set(testFiles.map((f) => {
            if (f.path.includes(".test."))
                return "*.test.*";
            if (f.path.includes(".spec."))
                return "*.spec.*";
            return "*.test.*";
        }))];
    // Find setup files
    const setup_files = [];
    for (const f of index.files) {
        if (/setup\.(ts|js)$/.test(f.path) && !/(node_modules|dist|\.next)/.test(f.path)) {
            setup_files.push(f.path);
        }
        if (/vitest\.setup\.(ts|js)$/.test(f.path))
            setup_files.push(f.path);
        if (/jest\.setup\.(ts|js)$/.test(f.path))
            setup_files.push(f.path);
    }
    // Detect mock style and common patterns by reading a few test files
    let mock_style = null;
    const mock_patterns = [];
    const common_mocks_set = new Set();
    // Read up to 5 test files to detect patterns
    const sampleTests = testFiles
        .filter((f) => f.path.includes("service") || f.path.includes("controller") || f.path.includes("guard"))
        .slice(0, 5);
    for (const tf of sampleTests) {
        try {
            const content = await readFile(join(projectRoot, tf.path), "utf-8");
            // Mock style
            if (!mock_style) {
                if (content.includes("vi.mock"))
                    mock_style = "vi.mock";
                else if (content.includes("jest.mock"))
                    mock_style = "jest.mock";
                else if (content.includes("sinon"))
                    mock_style = "sinon";
            }
            // Common mock patterns — extract vi.mock/jest.mock calls
            const mockCalls = content.match(/(?:vi|jest)\.mock\s*\(\s*['"]([^'"]+)['"]/g);
            if (mockCalls) {
                for (const mc of mockCalls) {
                    const path = mc.match(/['"]([^'"]+)['"]/)?.[1];
                    if (path)
                        common_mocks_set.add(path);
                }
            }
            // Detect specific patterns
            if (content.includes("mockPrismaClient") || content.includes("prismaMock")) {
                if (!mock_patterns.some((p) => p.name === "prisma")) {
                    mock_patterns.push({ name: "prisma", import_from: "setup or inline", usage: "mockPrismaClient / prismaMock" });
                }
            }
            if (content.includes("mockDeep") || content.includes("DeepMockProxy")) {
                if (!mock_patterns.some((p) => p.name === "deep-mock")) {
                    mock_patterns.push({ name: "deep-mock", import_from: "vitest-mock-extended or jest-mock-extended", usage: "mockDeep<Type>()" });
                }
            }
            if (content.includes("$transaction") && content.includes("mock")) {
                if (!mock_patterns.some((p) => p.name === "transaction")) {
                    mock_patterns.push({ name: "transaction", import_from: "prisma mock", usage: "$transaction mock for DB operations" });
                }
            }
        }
        catch { /* skip unreadable files */ }
    }
    // Also check setup files for shared patterns
    for (const sf of setup_files) {
        try {
            const content = await readFile(join(projectRoot, sf), "utf-8");
            const exports = content.match(/export\s+(?:const|function|class)\s+(\w+)/g);
            if (exports) {
                for (const exp of exports) {
                    const name = exp.match(/(\w+)$/)?.[1];
                    if (name && /mock|stub|fake|fixture|factory/i.test(name)) {
                        mock_patterns.push({ name, import_from: sf, usage: "shared test helper" });
                    }
                }
            }
        }
        catch { /* skip */ }
    }
    // Determine assertion library from stack
    const pkg = await readJson(join(projectRoot, "package.json"));
    const devDeps = pkg?.devDependencies ?? {};
    let assertion_library = "expect"; // default
    if (devDeps["vitest"])
        assertion_library = "vitest/expect";
    else if (devDeps["jest"])
        assertion_library = "jest/expect";
    else if (devDeps["chai"])
        assertion_library = "chai";
    return {
        mock_style,
        setup_files,
        mock_patterns: mock_patterns.slice(0, 10),
        assertion_library,
        file_patterns,
        common_mocks: [...common_mocks_set].slice(0, 20),
    };
}
// ---------------------------------------------------------------------------
// Known Gotchas Extractor
// ---------------------------------------------------------------------------
function extractKnownGotchas(index) {
    const gotchas = [];
    // Check for common gotcha patterns in symbols
    for (const sym of index.symbols) {
        if (!sym.source)
            continue;
        // as any casts in production code (not tests)
        if (/(test|spec)\.(ts|js)$/.test(sym.file))
            continue;
        // Detect patterns that are known gotchas
        if (/process\.env\.\w+/.test(sym.source) && !/config|env\.schema|validate/.test(sym.file)) {
            if (!gotchas.some((g) => g.gotcha.includes("scattered process.env"))) {
                gotchas.push({
                    gotcha: "scattered process.env access outside config module",
                    evidence: [sym.file],
                    severity: "medium",
                });
            }
        }
    }
    // Check for common project-level gotchas
    const hasEslintIgnore = index.files.some((f) => f.path.includes(".eslintignore"));
    if (hasEslintIgnore) {
        gotchas.push({
            gotcha: ".eslintignore present — some files bypass linting",
            evidence: [".eslintignore"],
            severity: "low",
        });
    }
    return { auto_detected: gotchas.slice(0, 10) };
}
// ---------------------------------------------------------------------------
// Dependency Health
// ---------------------------------------------------------------------------
async function extractDependencyHealth(projectRoot) {
    const pkg = await readJson(join(projectRoot, "package.json"));
    if (!pkg) {
        // Try Python
        const pyproject = await readJson(join(projectRoot, "pyproject.toml"));
        if (!pyproject)
            return null;
        return { total: 0, prod: 0, dev: 0, key_versions: {} };
    }
    const prod = Object.keys(pkg.dependencies ?? {});
    const dev = Object.keys(pkg.devDependencies ?? {});
    const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
    // Extract key versions — frameworks, runtimes, major tools
    const keyPackages = [
        "react", "next", "hono", "@nestjs/core", "express", "vue", "angular",
        "typescript", "vitest", "jest", "prisma", "@prisma/client",
        "tailwindcss", "@anthropic-ai/sdk", "openai",
        "stripe", "inngest", "@clerk/nextjs", "@clerk/backend",
        "@sentry/nextjs", "@sentry/nestjs", "drizzle-orm",
    ];
    const key_versions = {};
    for (const k of keyPackages) {
        if (allDeps[k])
            key_versions[k] = allDeps[k];
    }
    return {
        total: prod.length + dev.length,
        prod: prod.length,
        dev: dev.length,
        key_versions,
    };
}
// ---------------------------------------------------------------------------
// Git Health
// ---------------------------------------------------------------------------
async function extractGitHealth(projectRoot) {
    try {
        const totalRes = await execFileAsync("git", ["rev-list", "--count", "HEAD"], {
            cwd: projectRoot, timeout: 5000,
        });
        const totalStr = totalRes.stdout.toString().trim();
        const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
        const recentRes = await execFileAsync("git", ["rev-list", "--count", `--since=${thirtyDaysAgo}`, "HEAD"], {
            cwd: projectRoot, timeout: 5000,
        });
        const recentStr = recentRes.stdout.toString().trim();
        const lastRes = await execFileAsync("git", ["log", "-1", "--format=%aI"], {
            cwd: projectRoot, timeout: 5000,
        });
        const lastCommitDate = lastRes.stdout.toString().trim();
        const contribRes = await execFileAsync("git", ["shortlog", "-sn", "--no-merges", "HEAD"], {
            cwd: projectRoot, timeout: 10000,
            maxBuffer: 5 * 1024 * 1024, // 5MB — many contributors on long-lived repos
        });
        const contributorsStr = contribRes.stdout.toString().trim();
        const contributors = contributorsStr.split("\n").filter(Boolean).length;
        return {
            total_commits: parseInt(totalStr) || 0,
            recent_commits_30d: parseInt(recentStr) || 0,
            last_commit_date: lastCommitDate || null,
            contributors,
        };
    }
    catch {
        return null;
    }
}
const analyzeProjectCache = new Map();
/** Test-only — clear the analyzeProject cache. */
export function resetAnalyzeProjectCacheForTesting() {
    analyzeProjectCache.clear();
}
export async function analyzeProject(repoName, options = {}) {
    const startTime = Date.now();
    let files_analyzed = 0;
    let files_skipped = 0;
    const skip_reasons = {};
    const index = await getCodeIndex(repoName);
    if (!index) {
        const failedProfile = {
            version: "1.0",
            generated_at: new Date().toISOString(),
            generated_by: {
                tool: "codesift",
                tool_version: "1.0.0",
                extractor_versions: { ...EXTRACTOR_VERSIONS },
            },
            compatible_with: ">=1.0, <2.0",
            status: "failed",
            generation_metadata: {
                files_analyzed: 0,
                files_skipped: 0,
                skip_reasons: { no_index: 1 },
                duration_ms: Date.now() - startTime,
            },
        };
        return buildSummary(failedProfile, "(not written — no index)");
    }
    // Cache hit: reuse previous profile when the underlying index hasn't been
    // touched since we last computed. force=true bypasses (callers needing
    // fresh analysis even on an unchanged index, e.g. wiki regeneration).
    if (!options.force) {
        const cached = analyzeProjectCache.get(repoName);
        if (cached && cached.updatedAt === index.updated_at) {
            return cached.profile;
        }
    }
    // Prefer real project root over conversation index root (~/.claude/projects/...)
    // The conversation index root points to Claude's project dir, not the actual git repo
    let projectRoot = index.root;
    if (projectRoot.includes("/.claude/projects/")) {
        // Fall back to CWD which is the actual project directory
        projectRoot = process.cwd();
    }
    files_analyzed = index.file_count;
    // Step 0: Identity
    const identity = await extractIdentity(projectRoot);
    // Step 1: Stack detection
    const stack = await detectStack(projectRoot);
    // Step 2: File classification
    const file_classifications = classifyFiles(index);
    // Step 2b: Dependency graph
    const dependency_graph = extractDependencyGraph(index);
    // Step 2c: Test conventions
    const test_conventions = await extractTestConventions(projectRoot, index);
    // Step 2d: Known gotchas
    const known_gotchas = extractKnownGotchas(index);
    // Step 3: Framework-specific convention extraction
    let conventions;
    let nestConventions;
    let nextConventions;
    let expressConventions;
    let reactConventions;
    let pythonConventions;
    let phpConventions;
    let astroConventions;
    let status = "complete";
    const fw = stack.framework;
    try {
        if (fw === "hono") {
            const orchestratorFile = file_classifications.critical.find((f) => f.code_type === "ORCHESTRATOR");
            if (orchestratorFile) {
                const appSource = await readFile(join(projectRoot, orchestratorFile.path), "utf-8");
                conventions = await extractHonoConventions(appSource, orchestratorFile.path);
            }
            else {
                status = "partial";
                skip_reasons["no_orchestrator_file"] = 1;
            }
        }
        else if (fw === "nestjs") {
            const moduleFile = index.files.find((f) => f.path.endsWith("app.module.ts"));
            if (moduleFile) {
                const moduleSource = await readFile(join(projectRoot, moduleFile.path), "utf-8");
                nestConventions = extractNestConventions(moduleSource, moduleFile.path);
            }
            else {
                status = "partial";
                skip_reasons["no_app_module_file"] = 1;
            }
        }
        else if (fw === "nextjs") {
            nextConventions = extractNextConventions(projectRoot, index.files);
        }
        else if (fw === "express") {
            const entryFile = file_classifications.critical.find((f) => f.code_type === "ORCHESTRATOR")
                ?? index.files.find((f) => /\/(app|server|index)\.(ts|js)$/.test(f.path));
            if (entryFile) {
                const appSource = await readFile(join(projectRoot, entryFile.path), "utf-8");
                expressConventions = extractExpressConventions(appSource, entryFile.path);
            }
            else {
                status = "partial";
                skip_reasons["no_entry_file"] = 1;
            }
        }
        else if (fw === "react") {
            const pkg = await readJson(join(projectRoot, "package.json"));
            const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
            reactConventions = extractReactConventions(index.files, allDeps, index.symbols);
        }
        else if (fw === "fastapi" || fw === "django" || fw === "flask") {
            pythonConventions = extractPythonConventions(index.files);
        }
        else if (fw === "yii2") {
            phpConventions = extractYii2Conventions(index.files);
        }
        else if (fw === "laravel" || fw === "symfony") {
            phpConventions = extractPhpConventions(index.files);
        }
        else if (fw === "astro") {
            const astroResult = await extractAstroConventions(index.files.map((f) => f.path), projectRoot);
            astroConventions = astroResult.conventions;
            status = "complete";
        }
        else {
            status = "partial";
        }
    }
    catch {
        status = "partial";
        skip_reasons[`${fw ?? "unknown"}_extractor_error`] = 1;
    }
    const profile = {
        version: "1.0",
        generated_at: new Date().toISOString(),
        generated_by: {
            tool: "codesift",
            tool_version: "1.0.0",
            extractor_versions: { ...EXTRACTOR_VERSIONS },
        },
        compatible_with: ">=1.0, <2.0",
        status,
        identity,
        stack,
        file_classifications,
        dependency_graph,
        test_conventions,
        known_gotchas,
        ...(conventions ? { conventions } : {}),
        ...(nestConventions ? { nest_conventions: nestConventions } : {}),
        ...(nextConventions ? { next_conventions: nextConventions } : {}),
        ...(expressConventions ? { express_conventions: expressConventions } : {}),
        ...(reactConventions ? { react_conventions: reactConventions } : {}),
        ...(pythonConventions ? { python_conventions: pythonConventions } : {}),
        ...(phpConventions ? { php_conventions: phpConventions } : {}),
        ...(astroConventions ? { astro_conventions: astroConventions } : {}),
        dependency_health: await extractDependencyHealth(projectRoot) ?? undefined,
        git_health: (await extractGitHealth(projectRoot)) ?? undefined,
        generation_metadata: {
            files_analyzed,
            files_skipped,
            skip_reasons,
            duration_ms: Date.now() - startTime,
        },
    };
    // Write full profile to disk — MCP returns only summary
    const profilePath = await writeProfileToDisk(projectRoot, profile);
    const summary = buildSummary(profile, profilePath);
    analyzeProjectCache.set(repoName, { updatedAt: index.updated_at, profile: summary });
    return summary;
}
// ---------------------------------------------------------------------------
// Disk persistence — write profile to .zuvo/project-profile.json
// ---------------------------------------------------------------------------
async function writeProfileToDisk(projectRoot, profile) {
    const zuvoDir = join(projectRoot, ".zuvo");
    await mkdir(zuvoDir, { recursive: true });
    const profilePath = join(zuvoDir, "project-profile.json");
    await writeFile(profilePath, JSON.stringify(profile, null, 2), "utf-8");
    return profilePath;
}
export function buildConventionsSummary(profile) {
    const p = profile;
    if (p.conventions)
        return {
            middleware_chains: p.conventions.middleware_chains.length,
            rate_limits: p.conventions.rate_limits.length,
            route_mounts: p.conventions.route_mounts.length,
            auth_groups: Object.keys(p.conventions.auth_patterns.groups).length,
        };
    if (p.nest_conventions)
        return {
            type: "nestjs",
            modules: p.nest_conventions.modules.length,
            global_guards: p.nest_conventions.global_guards.length,
            global_filters: p.nest_conventions.global_filters.length,
            global_interceptors: p.nest_conventions.global_interceptors.length,
            controllers: p.nest_conventions.controllers.length,
            has_throttler: !!p.nest_conventions.throttler,
        };
    if (p.next_conventions)
        return {
            type: "nextjs",
            pages: p.next_conventions.pages.length,
            api_routes: p.next_conventions.api_routes.length,
            services: p.next_conventions.services_count,
            inngest_functions: p.next_conventions.inngest_functions.length,
            webhooks: p.next_conventions.webhooks.length,
            has_middleware: !!p.next_conventions.middleware,
            app_router: p.next_conventions.config.app_router,
            i18n: p.next_conventions.config.i18n,
        };
    if (p.express_conventions)
        return {
            type: "express",
            middleware: p.express_conventions.middleware.length,
            routers: p.express_conventions.routers.length,
            error_handlers: p.express_conventions.error_handlers.length,
        };
    if (p.react_conventions)
        return {
            type: "react",
            ...p.react_conventions.component_count,
            state_management: p.react_conventions.state_management,
            ui_library: p.react_conventions.ui_library,
        };
    if (p.python_conventions)
        return {
            type: "python",
            routers: p.python_conventions.routers.length,
            middleware: p.python_conventions.middleware.length,
            framework_type: p.python_conventions.framework_type,
        };
    if (p.php_conventions)
        return {
            type: "php",
            controllers: p.php_conventions.controllers.length,
            middleware: p.php_conventions.middleware.length,
            models: p.php_conventions.models.length,
            migrations: p.php_conventions.migrations_count,
        };
    if (p.astro_conventions)
        return {
            type: "astro",
            output_mode: p.astro_conventions.output_mode,
            adapter: p.astro_conventions.adapter,
            integrations: p.astro_conventions.integrations.length,
            has_i18n: !!p.astro_conventions.i18n,
            config_resolution: p.astro_conventions.config_resolution,
        };
    return null;
}
function buildSummary(profile, profilePath) {
    return {
        status: profile.status,
        profile_path: profilePath,
        stack: {
            framework: profile.stack?.framework ?? null,
            language: profile.stack?.language ?? "unknown",
            test_runner: profile.stack?.test_runner ?? null,
            package_manager: profile.stack?.package_manager ?? null,
            monorepo: !!profile.stack?.monorepo,
        },
        file_counts: {
            critical: profile.file_classifications?.critical.length ?? 0,
            important: profile.file_classifications?.important.count ?? 0,
            routine: profile.file_classifications?.routine.count ?? 0,
            total_analyzed: profile.generation_metadata.files_analyzed,
        },
        conventions_summary: buildConventionsSummary(profile),
        dependency_health: profile.dependency_health ? {
            total: profile.dependency_health.total,
            prod: profile.dependency_health.prod,
            dev: profile.dependency_health.dev,
            key_count: Object.keys(profile.dependency_health.key_versions).length,
        } : null,
        git_health: profile.git_health ?? null,
        duration_ms: profile.generation_metadata.duration_ms,
    };
}
// ---------------------------------------------------------------------------
// get_extractor_versions — fast metadata call
// ---------------------------------------------------------------------------
/**
 * Languages with a tree-sitter parser extractor — these get symbol-level
 * indexing (search_symbols, get_file_outline, get_symbol, find_references, etc.).
 */
export const PARSER_LANGUAGES = [
    "typescript",
    "javascript",
    "python",
    "go",
    "rust",
    "php",
    "kotlin",
    "prisma",
    "markdown",
    "astro",
    "sql",
    "sql-jinja",
];
/**
 * Languages indexed as FileEntry (so get_file_tree and search_text with
 * file_pattern work) but WITHOUT symbol extraction. Ripgrep-backed search_text
 * and scan_secrets work on these via filesystem. Upgrade path: add a
 * tree-sitter grammar .wasm and extractor to move a language to PARSER_LANGUAGES.
 */
export const TEXT_STUB_LANGUAGES = [
    "swift", "dart", "scala", "clojure",
    "elixir", "lua", "zig", "nim", "gradle", "sbt",
];
export function getExtractorVersions() {
    return {
        parser_languages: PARSER_LANGUAGES,
        text_stub_languages: TEXT_STUB_LANGUAGES,
        profile_frameworks: { ...EXTRACTOR_VERSIONS },
        note: "search_text, get_file_tree, search_conversations, and scan_secrets work on ALL indexed files " +
            "(both parser_languages AND text_stub_languages). Only symbol-based tools (search_symbols, " +
            "get_file_outline, get_symbol, find_references, trace_call_chain) require a full parser " +
            "extractor, so they return nothing for text_stub_languages. profile_frameworks is the list " +
            "of framework detectors used by analyze_project — NOT a list of supported languages.",
        versions: { ...EXTRACTOR_VERSIONS },
    };
}
//# sourceMappingURL=project-tools.js.map