/**
 * #91b — view-layer reachability gate.
 *
 * The TSI presentational gate (#91, `typescript-interfaces.ts::isPresentationalShape`)
 * drops the obvious view types — a `Props` name, a React-typed field, a
 * `components/ui` path. It deliberately leaves a conservative tail: plain-data
 * render / editor / config shapes that carry an `id` but aren't domain
 * entities, declared OUTSIDE `components/ui` (in `adapters/graph/`, `hooks/`,
 * an editor `store/`, …) and structurally indistinguishable from a real model
 * by name or field type alone. On idle-meta that tail is `GraphNode`,
 * `GameEdge`, `Toast`, `ApiEndpoint`, `ValidationMessage`, …
 *
 * This module closes that tail with a SIGNAL rather than a name/path guess:
 * **import reachability**. In a monorepo with a real frontend/backend split, a
 * persistent domain entity is referenced from the backend / shared-domain /
 * library layer (it is read or written somewhere off the screen); a
 * view-layer type is referenced ONLY from inside the frontend app subtrees.
 * So: a TSI-extracted entity whose every textual reference lands inside a
 * frontend-app package — and never in a non-frontend `.ts` file — is
 * view-layer, and we drop it.
 *
 * This cleanly separates siblings that share a file: idle-meta's `GameNode`
 * (referenced by `shared/services/*` → kept) from `GameEdge` (referenced only
 * within `apps/editor/*` → dropped), even though both live in
 * `apps/editor/src/store/types.ts`.
 *
 * Conservative guards (favour keeping an entity):
 *   - Runs ONLY when there is a genuine split — at least one frontend-app
 *     subtree that is a PROPER SUBSET of the source (non-frontend files
 *     exist). A single-package app (one root `package.json`, e.g. a lone
 *     Next.js app like the guesthouse case) yields no frontend subtree → the
 *     gate no-ops and every entity is preserved.
 *   - Only `typescript-interfaces`-sourced entities are eligible. Entities
 *     from schema-backed adapters (prisma / decorators) are never dropped.
 *   - An entity referenced by even ONE non-frontend file is kept.
 *   - Skips entirely on pathologically large trees (cost backstop).
 */
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
import { join } from 'path';
/** Dirs never walked. Mirrors structural-detector.ts. */
const IGNORE_DIRS = new Set([
    'node_modules', 'dist', 'build', 'out', '.git', '.next', '.turbo',
    '.cache', 'coverage', '__pycache__', '.pytest_cache', 'target',
    '.idea', '.vscode',
]);
/** UI-framework dependency names — presence marks a package as frontend. */
const UI_DEP = /^(?:react|react-dom|react-native|expo|next|vue|svelte|@angular\/core|solid-js|preact|@remix-run\/react|gatsby)$/;
/** Server-framework dependency names — presence marks a package as backend. */
const SERVER_DEP = /^(?:express|fastify|@nestjs\/core|koa|hapi|@hapi\/hapi|restify|polka|@trpc\/server|apollo-server|mongoose|prisma|@prisma\/client|pg|mysql2|typeorm|sequelize)$/;
const SOURCE_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
function isTestOrDecl(path) {
    return /\.d\.ts$/.test(path) || /[\\/]__tests__[\\/]|\.(?:test|spec)\.[tj]sx?$/.test(path);
}
/** Read deps + devDeps names from a package.json (best-effort). */
function depNames(pkgPath) {
    try {
        const json = JSON.parse(readFileSync(pkgPath, 'utf-8'));
        return [
            ...Object.keys(json.dependencies || {}),
            ...Object.keys(json.devDependencies || {}),
            ...Object.keys(json.peerDependencies || {}),
        ];
    }
    catch {
        return [];
    }
}
/**
 * Detect frontend-app subtrees: sub-package directories whose package.json
 * declares a UI framework and NO server framework. The ROOT package.json is
 * never classified — without a sub-package boundary there's no way to tell a
 * frontend file from a backend file within a single package, so a lone app
 * yields an empty result and the caller no-ops.
 *
 * Returns absolute directory prefixes (each ending in the platform separator
 * is NOT guaranteed — callers compare with `startsWith(dir + sep)` or exact).
 */
export function detectFrontendAppDirs(sourceDir) {
    const frontend = [];
    const walk = (dir, depth) => {
        if (depth > 6)
            return;
        let entries;
        try {
            entries = readdirSync(dir);
        }
        catch {
            return;
        }
        for (const name of entries) {
            if (IGNORE_DIRS.has(name))
                continue;
            const full = join(dir, name);
            let isDir = false;
            try {
                isDir = statSync(full).isDirectory();
            }
            catch {
                continue;
            }
            if (!isDir)
                continue;
            const pkg = join(full, 'package.json');
            if (existsSync(pkg)) {
                const deps = depNames(pkg);
                const hasUi = deps.some((d) => UI_DEP.test(d));
                const hasServer = deps.some((d) => SERVER_DEP.test(d));
                if (hasUi && !hasServer)
                    frontend.push(full);
                // A package boundary (frontend or not) ends the descent for app
                // detection — nested packages are rare and the top boundary is the
                // architectural unit. Still descend to catch apps/* nested one level.
            }
            walk(full, depth + 1);
        }
    };
    walk(sourceDir, 0);
    return frontend;
}
/** Is `file` inside any of the frontend-app dirs? */
function underAny(file, dirs) {
    return dirs.some((d) => file === d || file.startsWith(d + '/') || file.startsWith(d + '\\'));
}
/** Collect every source file under sourceDir (sync fs walk). */
function listSourceFiles(sourceDir, cap) {
    const out = [];
    const walk = (dir) => {
        if (out.length > cap)
            return;
        let entries;
        try {
            entries = readdirSync(dir);
        }
        catch {
            return;
        }
        for (const name of entries) {
            if (IGNORE_DIRS.has(name))
                continue;
            const full = join(dir, name);
            let st;
            try {
                st = statSync(full);
            }
            catch {
                continue;
            }
            if (st.isDirectory()) {
                walk(full);
            }
            else if (SOURCE_EXT.test(name) && !isTestOrDecl(full)) {
                out.push(full);
            }
            if (out.length > cap)
                return;
        }
    };
    walk(sourceDir);
    return out;
}
/**
 * Drop TSI-extracted entities that are reachable ONLY from frontend-app
 * subtrees. Mutates `facts.entities` + `facts.relationships`. Returns the
 * drops for logging / tests. No-op (returns []) when there is no
 * frontend/backend split or no eligible entities.
 */
export function gateViewOnlyEntities(opts) {
    const { facts, sourceDir } = opts;
    const fileCap = opts.fileCap ?? 20000;
    const drops = [];
    if (!sourceDir || !existsSync(sourceDir))
        return drops;
    // Eligible candidates: TSI-sourced entities only.
    const candidates = facts.entities.filter((e) => e?.sourceFramework === 'typescript-interfaces' && typeof e?.name === 'string');
    if (candidates.length === 0)
        return drops;
    const frontendDirs = detectFrontendAppDirs(sourceDir);
    if (frontendDirs.length === 0)
        return drops; // no split → can't distinguish
    const files = listSourceFiles(sourceDir, fileCap);
    if (files.length > fileCap)
        return drops; // pathological tree → bail
    // Require a genuine split: at least one source file OUTSIDE the frontend
    // dirs (otherwise everything is frontend and the gate is meaningless).
    const hasNonFrontend = files.some((f) => !underAny(f, frontendDirs));
    if (!hasNonFrontend)
        return drops;
    // Build name → set-of-files-that-reference-it for the candidate names, in a
    // single pass over the source. Word-boundary match avoids substring hits
    // (`GameEdge` does not match `GameEdgeList`).
    const names = candidates.map((c) => c.name);
    const patterns = new Map(names.map((n) => [n, new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)]));
    const refFiles = new Map(names.map((n) => [n, new Set()]));
    for (const file of files) {
        let src;
        try {
            src = readFileSync(file, 'utf-8');
        }
        catch {
            continue;
        }
        for (const n of names) {
            // Cheap includes() pre-filter before the regex.
            if (src.includes(n) && patterns.get(n).test(src)) {
                refFiles.get(n).add(file);
            }
        }
    }
    // Decide drops: a candidate referenced at least once and ONLY inside
    // frontend-app subtrees is view-layer.
    const toDrop = new Set();
    for (const c of candidates) {
        const refs = refFiles.get(c.name);
        if (refs.size === 0)
            continue; // never referenced (incl. its own decl) → keep
        let allFrontend = true;
        for (const f of refs) {
            if (!underAny(f, frontendDirs)) {
                allFrontend = false;
                break;
            }
        }
        if (allFrontend) {
            toDrop.add(c.name);
            drops.push({ name: c.name, referencedOnlyIn: frontendDirs.filter((d) => [...refs].some((f) => underAny(f, [d]))) });
            console.warn(`   ⚠️  Prepass: entity "${c.name}" dropped — referenced only from frontend-app subtrees (view-layer, #91b)`);
        }
    }
    if (toDrop.size === 0)
        return drops;
    facts.entities = facts.entities.filter((e) => !toDrop.has(e?.name));
    facts.relationships = facts.relationships.filter((r) => !toDrop.has(r?.from) && !toDrop.has(r?.to));
    return drops;
}
//# sourceMappingURL=view-reachability.js.map