// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/permissions-merger/stack-detectors.ts — Stack-aware deny rule injection (SPEC-594 + SPEC-597)
//
// Registry-driven: reads src/config/deny-rules-registry.json + src/config/detection-signals.json
// as the sources of truth. Zero hardcoded framework/deploy-target names in this file.
// Adding a new stack = edit the JSON registries, never this file.
import { access, readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve as pathResolve } from 'node:path';
import { detectStack } from '../project-dna/stack-detector.js';
let denyRegistryCache = null;
let signalsRegistryCache = null;
function registryDir() {
    // This file lives at src/engine/permissions-merger/stack-detectors.ts.
    // Config registries live at src/config/*. Resolve via import.meta.url for ESM.
    const here = fileURLToPath(import.meta.url);
    return pathResolve(dirname(here), '..', '..', 'config');
}
async function loadDenyRegistry() {
    if (denyRegistryCache) {
        return denyRegistryCache;
    }
    const raw = await readFile(join(registryDir(), 'deny-rules-registry.json'), 'utf-8');
    denyRegistryCache = JSON.parse(raw);
    return denyRegistryCache;
}
async function loadSignalsRegistry() {
    if (signalsRegistryCache) {
        return signalsRegistryCache;
    }
    const raw = await readFile(join(registryDir(), 'detection-signals.json'), 'utf-8');
    signalsRegistryCache = JSON.parse(raw);
    return signalsRegistryCache;
}
/** Test helper — clears both registry caches. Production never calls this. */
export function resetStackDetectorCaches() {
    denyRegistryCache = null;
    signalsRegistryCache = null;
}
async function fileExists(filePath) {
    try {
        await access(filePath);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Detect deploy-target / database / container tokens from filesystem markers,
 * driven by the signals registry. Does NOT know framework names — only follows
 * the `deployTargets` + `databases` entries loaded from JSON.
 */
async function detectStackFromFiles(projectPath) {
    const registry = await loadSignalsRegistry();
    const tokens = new Set();
    for (const target of [...registry.deployTargets, ...registry.databases]) {
        if (target.type !== 'file') {
            continue;
        }
        // Simple exact-match patterns. Glob characters in the pattern are taken
        // literally as filename candidates for now; fuller glob support lives in
        // scan-project's detectors. This keeps stack-detectors fast + dependency-free.
        const primary = target.pattern.replace(/\{[^}]+\}/g, '');
        const candidates = expandBracePattern(target.pattern);
        for (const candidate of [primary, ...candidates]) {
            if (candidate === '') {
                continue;
            }
            if (await fileExists(join(projectPath, candidate))) {
                tokens.add(target.id);
                break;
            }
        }
    }
    // Env-var based detection
    for (const target of [...registry.deployTargets, ...registry.databases]) {
        if (target.type === 'env' && process.env[target.pattern]) {
            tokens.add(target.id);
        }
    }
    return Array.from(tokens);
}
function expandBracePattern(pattern) {
    const match = /\{([^}]+)\}/.exec(pattern);
    if (!match) {
        return [pattern];
    }
    const alternatives = match[1]?.split(',') ?? [];
    return alternatives.map((alt) => pattern.replace(/\{[^}]+\}/, alt.trim()));
}
/**
 * Detect the project stack and return deny rules. Purely registry-driven —
 * this function knows nothing about specific frameworks or tools; it routes
 * detected tokens through the JSON registry.
 *
 * @param projectPath - Absolute path to the project root
 * @returns Array of StackDenyRule (stack token + deny pattern)
 */
export async function collectStackDenyRules(projectPath) {
    const [depTokens, fileTokens, registry] = await Promise.all([
        detectStack(projectPath),
        detectStackFromFiles(projectPath),
        loadDenyRegistry(),
    ]);
    const allTokens = [...new Set([...depTokens, ...fileTokens])];
    const rules = [];
    for (const token of allTokens) {
        const entry = registry.stackRules.find((r) => r.detectionId === token);
        if (!entry) {
            continue;
        }
        for (const rule of entry.patterns) {
            rules.push({ stack: token, rule });
        }
    }
    return rules;
}
/**
 * Returns the always-on destructive patterns that apply to every project,
 * regardless of detected stack. Sourced from `alwaysDeny` in the registry.
 */
export async function getAlwaysDenyRules() {
    const registry = await loadDenyRegistry();
    return [...registry.alwaysDeny];
}
/**
 * Returns the unique deny rule strings from a list of StackDenyRules.
 */
export function stackDenyRulesToStrings(rules) {
    return [...new Set(rules.map((r) => r.rule))];
}
//# sourceMappingURL=stack-detectors.js.map