/**
 * Structural component detector — fallback when ORM adapters can't extract
 * an entity graph for community detection.
 *
 * **Why this exists**: `community-detection.ts` runs Louvain on the
 * relationship graph extracted by per-framework adapters (typescript-prisma,
 * typescript-decorators). When the source uses raw drivers, custom ORMs, or
 * unannotated NoSQL, no adapter fires and there's no graph to cluster on —
 * the LLM falls back to its own judgement, which is high-variance (idle-meta:
 * 5 components one run, 1 component the next, on identical source).
 *
 * This detector uses STRUCTURAL signals always present in the source:
 *   1. Sub-directory `package.json` files (each = a deployable unit)
 *   2. Conventional sub-directories (apps/, services/, modules/, packages/)
 *
 * Both signals reflect AUTHOR-STATED architectural boundaries, no LLM
 * judgement required. Particularly strong for monorepos (workspace-style
 * projects, Lerna/Nx/Turborepo, npm workspaces) where each package.json
 * IS the component boundary by design.
 *
 * Signal precedence (configurable in future):
 *   - sub-package.json files (highest signal — author committed to a deployable boundary)
 *   - conventional directory grouping (apps/* / services/* / modules/* / packages/*)
 *   - (future) file-import-graph Louvain (universal fallback)
 *
 * Returns ComponentSuggestion[] in the same shape `community-detection.ts`
 * emits, so `formatSuggestionsAsMarkdown()` and the analyse prompt's
 * COMPONENT-BOUNDARY DISCIPLINE section consume it identically.
 */
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
import { join, relative, basename } from 'path';
/** Skip these dirs entirely when walking. */
const IGNORE_DIRS = new Set([
    'node_modules', 'dist', 'build', 'out', '.git', '.next', '.turbo',
    '.cache', 'coverage', '__pycache__', '.pytest_cache', 'target',
    '.idea', '.vscode',
]);
/** Conventional architectural-boundary directories. */
const APP_DIRS = ['apps', 'services', 'modules', 'packages', 'projects'];
/**
 * Walk a source dir for package.json files (excluding root + IGNORE_DIRS).
 * Returns one entry per non-root package.json.
 */
function findSubPackageJsonFiles(sourceDir) {
    const results = [];
    function walk(dir, depth = 0) {
        if (depth > 6)
            return; // sanity limit on tree depth
        let entries;
        try {
            entries = readdirSync(dir);
        }
        catch {
            return;
        }
        // First check for package.json AT THIS LEVEL (skip the source root itself)
        if (depth > 0 && entries.includes('package.json')) {
            const pkgPath = join(dir, 'package.json');
            let name = null;
            try {
                const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
                name = typeof pkg.name === 'string' ? pkg.name : null;
            }
            catch { /* malformed package.json — skip name extraction */ }
            results.push({
                path: pkgPath,
                relPath: relative(sourceDir, pkgPath),
                name,
                dir,
            });
            // Don't descend further into this package — it's a boundary.
            return;
        }
        // Otherwise descend into subdirs
        for (const entry of entries) {
            if (IGNORE_DIRS.has(entry))
                continue;
            if (entry.startsWith('.') && !entry.startsWith('._'))
                continue; // skip dotfiles
            const full = join(dir, entry);
            let stats;
            try {
                stats = statSync(full);
            }
            catch {
                continue;
            }
            if (stats.isDirectory()) {
                walk(full, depth + 1);
            }
        }
    }
    walk(sourceDir);
    return results;
}
/**
 * Look for conventional architectural-boundary subdirectories (apps/,
 * services/, modules/, packages/, projects/) that contain multiple sub-dirs.
 * Each child sub-dir is a candidate component.
 *
 * Used when no sub-package.json files are found but the project has a
 * structural convention.
 */
function findConventionalDirs(sourceDir) {
    const results = [];
    for (const conv of APP_DIRS) {
        const parent = join(sourceDir, conv);
        if (!existsSync(parent))
            continue;
        let stats;
        try {
            stats = statSync(parent);
        }
        catch {
            continue;
        }
        if (!stats.isDirectory())
            continue;
        let entries;
        try {
            entries = readdirSync(parent);
        }
        catch {
            continue;
        }
        const childDirs = entries.filter((entry) => {
            if (IGNORE_DIRS.has(entry))
                return false;
            if (entry.startsWith('.'))
                return false;
            try {
                return statSync(join(parent, entry)).isDirectory();
            }
            catch {
                return false;
            }
        });
        if (childDirs.length >= 2) {
            results.push({ parentDir: conv, childDirs });
        }
    }
    return results;
}
/**
 * PascalCase-ify a name. Tolerates kebab-case ("idle-clicker-mobile" →
 * "IdleClickerMobile"), snake_case, scoped npm names ("@scope/foo-bar" →
 * "FooBar"), and already-PascalCase strings.
 */
function pascalCase(name) {
    // Strip npm scope
    let s = name.replace(/^@[^/]+\//, '');
    // Split on -, _, /, .
    return s.split(/[-_/.]+/)
        .filter(Boolean)
        .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
        .join('');
}
/**
 * Run structural component detection. Returns ComponentSuggestion[] suitable
 * for plugging into the same facts.suggestedComponents field that
 * community-detection.ts populates.
 *
 * Each suggestion has empty `entities[]` and `crossEdges[]` — the LLM still
 * has to read source to populate those, but the COMPONENT BOUNDARIES are
 * suggested by author intent (package.json or apps/* / services/* / etc).
 */
export function detectStructuralComponents(sourceDir) {
    // Strategy 1: sub-package.json files
    const subPackages = findSubPackageJsonFiles(sourceDir);
    if (subPackages.length >= 2) {
        return subPackages.map((pkg) => ({
            suggestedName: pkg.name
                ? pascalCase(pkg.name) + 'Component'
                : pascalCase(basename(pkg.dir)) + 'Component',
            entities: [], // LLM reads source for entity content
            crossEdges: [],
            internalEdgeCount: 0,
            structural: {
                type: 'package',
                sourceDir: pkg.dir,
                relPath: pkg.relPath,
            },
        }));
    }
    // Strategy 2: apps/ / services/ / modules/ / packages/ subdirs
    const convDirs = findConventionalDirs(sourceDir);
    if (convDirs.length > 0) {
        // Pick the convention dir with the most children
        const winning = convDirs.sort((a, b) => b.childDirs.length - a.childDirs.length)[0];
        return winning.childDirs.map((child) => ({
            suggestedName: pascalCase(child) + 'Component',
            entities: [],
            crossEdges: [],
            internalEdgeCount: 0,
            structural: {
                type: 'directory',
                sourceDir: join(sourceDir, winning.parentDir, child),
                relPath: join(winning.parentDir, child),
            },
        }));
    }
    // No structural signal found
    return [];
}
//# sourceMappingURL=structural-detector.js.map