"use strict";
/**
 * `dxkit guardrail check` orchestrator.
 *
 * The matcher (`gitAwareMatch`) and classifier (`classify`) are pure
 * modules that already exist. This file wires them together with the
 * baseline file format, the producer pipeline, and the per-pair
 * context lookups (severity, drift signals, changed-line overlap)
 * the classifier needs to make policy decisions.
 *
 * Pipeline:
 *
 *   1. Load the prior baseline file.
 *   2. Re-run every analyzer (via `gatherCurrentScan`) to produce the
 *      current side of the diff.
 *   3. Convert both sides to `LocatedIdentity[]` and run the
 *      git-aware matcher.
 *   4. Build per-pair classify context:
 *        - severity from the current security aggregate or per-kind
 *          defaults
 *        - kind from the matched BaselineEntry
 *        - scannerVersionDiffers from per-kind tool version compare
 *        - configDiffers from envelope hash compare
 *        - overlapsChangedLines from `git diff base..HEAD` hunks
 *          intersected with the finding's line
 *   5. Run the brownfield policy classifier over every pair.
 *   6. Optionally filter via `--changed-only`: drop pairs whose
 *      locator falls outside the diff. Non-locator pairs (dep-vuln,
 *      duplication, etc.) are always kept — their "semantic"
 *      identity doesn't map cleanly to changed lines.
 *   7. Compose a `GuardrailCheckResult` with a deterministic
 *      blocks/warns verdict so the CLI can pick exit code + render.
 *
 * Drift signals come from comparing the baseline's `analysis` /
 * `tools` envelope against the freshly-gathered envelope. Per-kind
 * tool attribution uses the current run's `SecurityAggregate.provenance`
 * — the cleaner alternative to a hardcoded kind→tool table.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.runGuardrailCheck = runGuardrailCheck;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const create_1 = require("./create");
const baseline_file_1 = require("./baseline-file");
const coverage_1 = require("./coverage");
const entry_to_located_1 = require("./entry-to-located");
const git_aware_match_1 = require("./git-aware-match");
const modes_1 = require("./modes");
const policy_1 = require("./policy");
const ref_baseline_1 = require("./ref-baseline");
const sanitize_1 = require("./sanitize");
const diff_1 = require("../allowlist/diff");
const KIND_DEFAULT_SEVERITY = Object.freeze({
    secret: 'high',
    code: 'medium',
    config: 'medium',
    'dep-vuln': 'medium',
    duplication: 'medium',
    'coverage-gap': 'medium',
    'test-gap': 'medium',
    hygiene: 'low',
    'test-file-degradation': 'medium',
    'god-file': 'medium',
    'stale-file': 'low',
    'large-file': 'medium',
    'secret-hmac': 'high',
    // Stale-allow is a self-detected dxkit hygiene finding (orphaned
    // allowlist annotation). Low severity — it's a maintenance signal,
    // not an active risk; the underlying suppressed finding is already
    // gone.
    'stale-allow': 'low',
});
/**
 * Run the guardrail-check pipeline. Pure-orchestrator: loads the
 * baseline, gathers current state, runs the matcher + classifier,
 * and returns a structured result. Renderers + CLI are downstream.
 */
async function runGuardrailCheck(options) {
    const cwd = path.resolve(options.cwd);
    const policy = (0, policy_1.resolvePolicy)(options.policyPath, cwd);
    const mode = options.resolvedMode ??
        (0, modes_1.resolveBaselineMode)({
            cwd,
            cliMode: options.cliMode,
            cliRef: options.cliRef,
            policyMode: policy.baseline?.mode,
            policyRef: policy.baseline?.ref,
        });
    // Load the prior side. Committed modes read from the baseline
    // file on disk; ref-based mode recomputes prior state by checking
    // out a git ref into a temporary worktree. Both paths produce a
    // `BaselineFile`-shaped value so the matcher / classifier
    // downstream stay mode-agnostic.
    const { baseline, baselinePath } = await loadPriorSide(cwd, mode, options);
    const current = await (0, create_1.gatherCurrentScan)({ cwd, verbose: options.verbose });
    const priorLocated = (0, entry_to_located_1.entriesToLocated)(baseline.findings);
    const currentLocated = (0, entry_to_located_1.entriesToLocated)(current.findings);
    // The matcher needs the baseline's anchor commit to drive `git
    // diff`. Empty string is the canonical "not a git repo at capture
    // time" value; the matcher's reachability check handles it by
    // falling back to plain set-diff (passes 1 + 1.5 are skipped).
    const matchResult = (0, git_aware_match_1.gitAwareMatch)(priorLocated, currentLocated, {
        cwd,
        baseSha: baseline.repo.commitSha || 'HEAD',
        headSha: 'HEAD',
    });
    const priorById = indexById(baseline.findings);
    const currentById = indexById(current.findings);
    const severityByCurrentId = buildSeverityIndex(current.aggregate);
    const envelopeDrift = diffEnvelopes(baseline, current);
    // Per-kind tool attribution drives the per-pair
    // scannerVersionDiffers signal. A pair is in drift only when the
    // tools that produced its kind actually changed version between
    // runs — narrower than "any tool drifted globally," which would
    // overstate the drift signal for unrelated kinds.
    const toolsByKind = buildToolsByKind(current.aggregate);
    const changedLineCache = new Map();
    const headSha = readHeadSha(cwd);
    const baseSha = baseline.repo.commitSha;
    const linesChangedFor = (file) => {
        if (!baseSha || !headSha)
            return undefined;
        let cached = changedLineCache.get(file);
        if (cached)
            return cached;
        cached = readChangedLineSet(cwd, baseSha, headSha, file);
        changedLineCache.set(file, cached);
        return cached;
    };
    const classifiedPairs = [];
    let blocks = false;
    let warns = false;
    for (const pair of matchResult.pairs) {
        const anchorEntry = (pair.currentId ? currentById.get(pair.currentId) : undefined) ??
            (pair.priorId ? priorById.get(pair.priorId) : undefined);
        if (!anchorEntry)
            continue;
        const severity = (pair.currentId ? severityByCurrentId.get(pair.currentId) : undefined) ??
            KIND_DEFAULT_SEVERITY[anchorEntry.kind];
        const file = locatorFile(anchorEntry);
        const line = locatorLine(anchorEntry);
        const overlapsChangedLines = file !== undefined && line !== undefined && line > 0
            ? (linesChangedFor(file)?.has(line) ?? false)
            : undefined;
        const scannerVersionDiffers = pair.status === 'added' && kindHasDriftingTool(anchorEntry.kind, toolsByKind, envelopeDrift);
        const configDiffers = pair.status === 'added' &&
            (envelopeDrift.configHashChanged ||
                envelopeDrift.ignoreHashChanged ||
                envelopeDrift.policyHashChanged);
        const context = {
            severity,
            kind: anchorEntry.kind,
            ...(scannerVersionDiffers ? { scannerVersionDiffers: true } : {}),
            ...(configDiffers ? { configDiffers: true } : {}),
            ...(overlapsChangedLines !== undefined ? { overlapsChangedLines } : {}),
        };
        const classification = (0, policy_1.classify)(pair, policy, context);
        if (classification.blocks)
            blocks = true;
        if (classification.warns)
            warns = true;
        classifiedPairs.push({
            pair,
            classification,
            severity,
            kind: anchorEntry.kind,
            ...(file !== undefined ? { file } : {}),
            ...(line !== undefined ? { line } : {}),
            ...(overlapsChangedLines !== undefined ? { overlapsChangedLines } : {}),
        });
    }
    const filteredPairs = options.changedOnly
        ? classifiedPairs.filter((p) => keepUnderChangedOnly(p))
        : classifiedPairs;
    // Re-derive the verdict after filtering — a --changed-only run
    // shouldn't be blocked by a pair that the filter just dropped.
    let filteredBlocks = false;
    let filteredWarns = false;
    for (const p of filteredPairs) {
        if (p.classification.blocks)
            filteredBlocks = true;
        if (p.classification.warns)
            filteredWarns = true;
    }
    // Allowlist delta between the baseline's anchor SHA and the
    // current working tree. Surfaced in the markdown renderer so
    // PR reviewers see new suppressions being introduced; absent /
    // degenerate when the SHA isn't reachable (shallow clone, etc.)
    // and the renderer treats that as "delta unavailable."
    const allowlistDelta = (0, diff_1.computeAllowlistDelta)(cwd, baseline.repo.commitSha);
    return {
        mode,
        ...(baselinePath !== undefined ? { baselinePath } : {}),
        baseline,
        current,
        matchResult,
        pairs: filteredPairs,
        envelopeDrift,
        policy,
        blocks: options.changedOnly ? filteredBlocks : blocks,
        warns: options.changedOnly ? filteredWarns : warns,
        allowlistDelta,
    };
}
// `resolvePolicy` moved to `./policy.ts` so `createBaseline` and
// `runGuardrailCheck` share one canonical loader.
function indexById(entries) {
    const out = new Map();
    for (const e of entries)
        out.set(e.id, e);
    return out;
}
/**
 * Severity-by-fingerprint index built from the current run's
 * security aggregate. CodeFindings carry `fingerprint` (computed via
 * `computeCodeFingerprint` — the same hash `identityFor` produces
 * for secret/code/config kinds), and DepVulnFindings carry
 * `fingerprint` (computed via `computeFingerprint` — same as
 * identityFor for dep-vulns). For other kinds the lookup misses and
 * the caller falls back to `KIND_DEFAULT_SEVERITY`.
 */
function buildSeverityIndex(aggregate) {
    const out = new Map();
    for (const f of aggregate.findingsByCategory.secret) {
        if (f.fingerprint)
            out.set(f.fingerprint, f.severity);
    }
    for (const f of aggregate.findingsByCategory.code) {
        if (f.fingerprint)
            out.set(f.fingerprint, f.severity);
    }
    for (const f of aggregate.findingsByCategory.config) {
        if (f.fingerprint)
            out.set(f.fingerprint, f.severity);
    }
    for (const f of aggregate.findingsByCategory.dependency) {
        if (f.fingerprint)
            out.set(f.fingerprint, f.severity);
    }
    return out;
}
/**
 * Build a per-kind map of "tools that produced this kind in the
 * current run." Used by the `scannerVersionDiffers` per-pair
 * computation: a pair is in tool drift only when one of the tools
 * that produced its kind has actually drifted version.
 */
function buildToolsByKind(aggregate) {
    const secretTool = aggregate.provenance.secrets.tool ?? undefined;
    const codeTool = aggregate.provenance.codePatterns.tool ?? undefined;
    const depTool = aggregate.provenance.depVulns.tool ?? undefined;
    const tlsBypassRan = aggregate.provenance.tlsBypass.ran;
    const codeTools = new Set();
    if (codeTool)
        codeTools.add(codeTool);
    if (tlsBypassRan)
        codeTools.add('tls-bypass-registry');
    const secretTools = new Set();
    if (secretTool)
        secretTools.add(secretTool);
    const depTools = new Set();
    if (depTool)
        depTools.add(depTool);
    return {
        secret: secretTools,
        code: codeTools,
        config: secretTools, // .env-in-git + private-key files come from the secrets/file pass
        'dep-vuln': depTools,
        'secret-hmac': secretTools,
    };
}
function kindHasDriftingTool(kind, toolsByKind, drift) {
    const tools = toolsByKind[kind];
    if (!tools || tools.size === 0)
        return false;
    for (const diff of drift.toolVersionDiffs) {
        if (tools.has(diff.tool))
            return true;
    }
    return false;
}
function diffEnvelopes(baseline, current) {
    const toolVersionDiffs = [];
    const names = new Set([...Object.keys(baseline.tools), ...Object.keys(current.tools)]);
    for (const tool of [...names].sort()) {
        const baselineVersion = baseline.tools[tool];
        const currentVersion = current.tools[tool];
        if (baselineVersion !== currentVersion) {
            toolVersionDiffs.push({ tool, baselineVersion, currentVersion });
        }
    }
    return {
        toolchainHashChanged: baseline.analysis.toolchainHash !== current.analysisMeta.toolchainHash,
        policyHashChanged: baseline.analysis.policyHash !== current.analysisMeta.policyHash,
        ignoreHashChanged: baseline.analysis.ignoreHash !== current.analysisMeta.ignoreHash,
        configHashChanged: baseline.analysis.configHash !== current.analysisMeta.configHash,
        dxkitVersionChanged: baseline.analysis.dxkitVersion !== current.analysisMeta.dxkitVersion,
        toolVersionDiffs,
        coverageDrift: (0, coverage_1.diffCoverage)(baseline.coverage, current.coverage),
    };
}
function locatorFile(entry) {
    if ((0, sanitize_1.isSanitized)(entry))
        return undefined;
    switch (entry.kind) {
        case 'secret':
        case 'code':
        case 'config':
        case 'hygiene':
        case 'test-gap':
        case 'test-file-degradation':
        case 'god-file':
        case 'stale-file':
        case 'large-file':
            return entry.file;
        case 'coverage-gap':
            return entry.file;
        case 'duplication':
            return entry.fileA;
        case 'dep-vuln':
        case 'secret-hmac':
            return undefined;
    }
}
function locatorLine(entry) {
    if ((0, sanitize_1.isSanitized)(entry))
        return undefined;
    switch (entry.kind) {
        case 'secret':
        case 'code':
        case 'config':
        case 'hygiene':
            return entry.line;
        case 'duplication':
            return entry.startLineA;
        case 'coverage-gap':
            return entry.lineRange?.[0];
        default:
            return undefined;
    }
}
/**
 * `--changed-only` filter predicate. Keeps:
 *   - pairs without a line locator (dep-vuln, duplication, etc.) —
 *     their identity isn't line-bound, so changed-line overlap
 *     doesn't apply
 *   - prior-side pairs (persisted / relocated / removed) — they
 *     represent existing state, not newly-introduced findings, so
 *     they pass regardless of where they live in the diff
 *   - new-side pairs whose anchor line is inside the diff
 *
 * Drops new-side pairs (added / tooling_drift / config_drift /
 * newly_detected) whose locator IS known but doesn't overlap any
 * changed line. That's the exact scope a pre-commit / pre-push hook
 * wants — "only flag what this developer just touched."
 */
function keepUnderChangedOnly(p) {
    if (p.file === undefined || p.line === undefined)
        return true;
    const isNewSide = p.classification.status === 'added' ||
        p.classification.status === 'tooling_drift' ||
        p.classification.status === 'config_drift' ||
        p.classification.status === 'newly_detected';
    if (!isNewSide)
        return true;
    return p.overlapsChangedLines === true;
}
function readHeadSha(cwd) {
    try {
        return (0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8' }).trim();
    }
    catch {
        return '';
    }
}
/**
 * Compute the set of HEAD-side line numbers modified between
 * `baseSha` and `headSha` for `file`. Used by the per-pair
 * `overlapsChangedLines` signal: a current-side finding at line N
 * overlaps the diff iff N is in this set.
 *
 * Walks `git diff --unified=0` hunks. Returns an empty set on any
 * failure (file missing in either revision, git unavailable, etc.).
 */
function readChangedLineSet(cwd, baseSha, headSha, file) {
    const out = new Set();
    let diff;
    try {
        diff = (0, child_process_1.execFileSync)('git', ['diff', '--unified=0', '--no-color', '--find-renames', baseSha, headSha, '--', file], { cwd, encoding: 'utf8' });
    }
    catch {
        return out;
    }
    if (!diff.trim())
        return out;
    const hunkRe = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm;
    let match;
    while ((match = hunkRe.exec(diff)) !== null) {
        const newStart = parseInt(match[1], 10);
        const newCount = match[2] !== undefined ? parseInt(match[2], 10) : 1;
        if (newCount === 0) {
            // Pure-deletion hunk on the new side — no new-side lines.
            continue;
        }
        for (let i = 0; i < newCount; i++)
            out.add(newStart + i);
    }
    return out;
}
/**
 * Load the prior side of the guardrail diff. Dispatches on
 * `mode.mode`:
 *
 *   - `committed-full` / `committed-sanitized` → read the on-disk
 *     baseline file. The path is `options.baselinePath` when
 *     supplied, otherwise the conventional
 *     `.dxkit/baselines/<name>.json`.
 *   - `ref-based` → run the full gather pipeline against a git
 *     worktree of `mode.ref`, then project the resulting
 *     `CurrentScan` into a synthetic `BaselineFile`. The matcher
 *     downstream doesn't care which path produced the value.
 *
 * The synthetic `BaselineFile` for ref-based mode carries the ref-
 * scan's envelope unchanged — including its `repo.commitSha`,
 * `tools`, `analysis` hashes, and `saltMode`. That's exactly what
 * the matcher needs to compute git-aware diffs + envelope drift
 * against the current scan.
 */
async function loadPriorSide(cwd, mode, options) {
    if (mode.mode !== 'ref-based') {
        const baselinePath = options.baselinePath ?? (0, baseline_file_1.pathForBaseline)(cwd, options.name ?? baseline_file_1.DEFAULT_BASELINE_NAME);
        if (!fs.existsSync(baselinePath)) {
            throw new Error(`baseline file not found: ${baselinePath}. ` +
                `Run \`vyuh-dxkit baseline create\` first to capture today's state.`);
        }
        return { baseline: (0, baseline_file_1.readBaselineFile)(baselinePath), baselinePath };
    }
    if (!mode.ref) {
        // Defensive: the resolver always populates `ref` for ref-based
        // mode. A missing ref here would be a programming error.
        throw new Error('ref-based baseline mode requires a resolved ref; got undefined.');
    }
    const refScan = await (0, ref_baseline_1.gatherFromRef)({ cwd, ref: mode.ref, verbose: options.verbose });
    const baseline = {
        schemaVersion: baseline_file_1.BASELINE_SCHEMA_VERSION,
        name: options.name ?? baseline_file_1.DEFAULT_BASELINE_NAME,
        createdAt: new Date().toISOString(),
        repo: refScan.repoState,
        analysis: refScan.analysisMeta,
        tools: refScan.tools,
        saltMode: refScan.saltMode,
        findings: refScan.findings,
    };
    return { baseline };
}
//# sourceMappingURL=check.js.map