/**
 * scan-limits.ts — hard resource caps for the customer-side `ai-governance scan`.
 *
 * The scan walks a customer's repository and reads + analyses every source
 * file. Without bounds, pointing it at a pathological tree (millions of files,
 * a multi-GB generated file, a single minified line) could spike CPU / memory
 * on the customer's machine. That violates the product's core promise that
 * nothing we ship can burden client infrastructure (Locked Invariants 2 + 10;
 * Thomas 2026-05-30: "cant allow that for clients").
 *
 * Every walk shares one ScanBudget. It enforces:
 *   - a wall-clock deadline (the scan is bounded in time, never runs away),
 *   - a max visited-file count,
 *   - a max recursion depth,
 *   - a per-file byte ceiling (oversize files are skipped, not read — this also
 *     neutralises any ReDoS-on-one-giant-line risk in the signature matcher).
 *
 * Caps are NEVER silent: when one trips, the budget records it and the scan
 * surfaces it in the JSON summary and on stderr (no silent truncation).
 *
 * All limits are overridable via env for power users / tests.
 */
function envInt(name, fallback) {
    const raw = process.env[name];
    if (!raw)
        return fallback;
    const n = Number.parseInt(raw, 10);
    return Number.isFinite(n) && n > 0 ? n : fallback;
}
export function defaultScanLimits() {
    return {
        maxFiles: envInt('CXNI_SCAN_MAX_FILES', 100_000),
        maxDepth: envInt('CXNI_SCAN_MAX_DEPTH', 50),
        maxFileBytes: envInt('CXNI_SCAN_MAX_FILE_BYTES', 5_000_000),
        timeBudgetMs: envInt('CXNI_SCAN_TIME_BUDGET_MS', 90_000),
    };
}
/**
 * Mutable budget shared across a single scan invocation. Cheap, synchronous,
 * no timers — `expired()` is checked against `Date.now()` at each step so the
 * walk self-terminates without any background handle.
 */
export class ScanBudget {
    limits;
    deadline;
    filesVisited = 0;
    filesSkippedOversize = 0;
    hitFileCap = false;
    hitTimeBudget = false;
    hitDepthCap = false;
    constructor(limits = defaultScanLimits(), now = Date.now()) {
        this.limits = limits;
        this.deadline = now + limits.timeBudgetMs;
    }
    /** True once the wall-clock budget is exhausted. */
    expired(now = Date.now()) {
        if (now >= this.deadline) {
            this.hitTimeBudget = true;
            return true;
        }
        return false;
    }
    /**
     * Returns true while the walk may keep descending/visiting. Returns false
     * (and latches the matching cap flag) once the file-count cap or the time
     * budget is hit. Callers must stop walking when this returns false.
     */
    canContinue(now = Date.now()) {
        if (this.filesVisited >= this.limits.maxFiles) {
            this.hitFileCap = true;
            return false;
        }
        if (this.expired(now))
            return false;
        return true;
    }
    /** True if `depth` is within the recursion cap; records the cap otherwise. */
    withinDepth(depth) {
        if (depth > this.limits.maxDepth) {
            this.hitDepthCap = true;
            return false;
        }
        return true;
    }
    /**
     * Returns true if a file of `bytes` is small enough to read. Records an
     * oversize skip and returns false otherwise.
     */
    allowFileSize(bytes) {
        if (bytes > this.limits.maxFileBytes) {
            this.filesSkippedOversize++;
            return false;
        }
        return true;
    }
    /** Count one visited (read/analysed) file. */
    countVisit() {
        this.filesVisited++;
    }
    report() {
        const capped = this.hitFileCap || this.hitTimeBudget || this.hitDepthCap || this.filesSkippedOversize > 0;
        return {
            filesVisited: this.filesVisited,
            filesSkippedOversize: this.filesSkippedOversize,
            truncatedByFileCap: this.hitFileCap,
            truncatedByTimeBudget: this.hitTimeBudget,
            truncatedByDepthCap: this.hitDepthCap,
            capped,
            limits: this.limits,
        };
    }
}
//# sourceMappingURL=scan-limits.js.map