"use strict";
/**
 * Brownfield policy + status classifier.
 *
 * The matcher in `git-aware-match.ts` emits raw `MatchPair`s with one
 * of four statuses (persisted / relocated / added / removed) plus a
 * confidence score and structured reasons. The guardrail check needs
 * a richer taxonomy — the difference between "developer introduced
 * a new finding" and "a scanner update surfaced a finding that was
 * always there" matters enormously for whether to block a PR.
 *
 * This module is the bridge. It takes a `MatchPair` plus optional
 * context (severity, scanner-version diff, config diff) and a
 * `BrownfieldPolicy`, then emits a `ClassifyResult` carrying the
 * post-policy `FindingStatus`, the block/warn verdict, and the
 * composed reason chain.
 *
 * Pure module — no I/O, deterministic over its inputs.
 *
 * Producer wiring note: today's classifier emits a subset of the full
 * `FindingStatus` taxonomy. Reservations for `probable_existing`,
 * `newly_detected`, and `fixed` are declared in the type space so
 * Phase 3's baseline-metadata work can light them up incrementally
 * without re-shaping consumer code.
 */
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.DEFAULT_POLICY_FILENAME = exports.DEFAULT_BROWNFIELD_POLICY = void 0;
exports.classify = classify;
exports.classifyAll = classifyAll;
exports.resolvePolicy = resolvePolicy;
exports.loadPolicyFromCwd = loadPolicyFromCwd;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
 * Default brownfield policy. Captures the conservative posture from
 * the agentic-brownfield strategy: block only on high-confidence new
 * regressions; warn on the categories that suggest a problem might
 * be real but might also be drift; legacy debt is permitted.
 *
 * Confidence thresholds: secrets + critical security demand a tight
 * confidence threshold (a low-confidence persisted secret pairing
 * gets demoted to uncertain and warned, not blocked). Lower-severity
 * findings can pair on weaker signal because the cost of a false
 * "secret is new" event is much higher than a false "TODO is new."
 */
exports.DEFAULT_BROWNFIELD_POLICY = Object.freeze({
    mode: 'brownfield',
    block: Object.freeze(['added']),
    warn: Object.freeze([
        'probable_existing',
        'newly_detected',
        'tooling_drift',
        'config_drift',
        'uncertain',
    ]),
    confidence: Object.freeze({
        critical: 0.75,
        high: 0.8,
        medium: 0.85,
        low: 0.9,
    }),
    blockRules: Object.freeze({
        newSecret: true,
        newCriticalSecurity: true,
        newHighSecurity: true,
        newCriticalDependencyVulnerability: true,
        newHighReachableDependencyVulnerability: true,
        newUntestedChangedSource: true,
        newSevereQualityIssueInChangedFiles: true,
    }),
    addedRequiresChangedLines: Object.freeze(['code', 'hygiene']),
});
/**
 * Classify one match pair against a brownfield policy.
 *
 * Pipeline:
 *   1. Start with the matcher's `pair.status` as the candidate
 *      `FindingStatus`.
 *   2. For `added`: check drift context. Scanner-version drift wins
 *      (more specific signal) over config drift; both demote to
 *      drift-bucket statuses regardless of severity.
 *   3. For `persisted` / `relocated`: check confidence against the
 *      per-severity threshold. Below threshold demotes to
 *      `'uncertain'`.
 *   4. Apply block-rule overrides: if a block-rule fires for this
 *      kind+severity combination AND the candidate status is
 *      `'added'`, the result blocks even if `'added'` weren't in the
 *      policy's block list.
 *   5. Apply the policy's `block` / `warn` membership to the final
 *      status to produce the booleans.
 */
function classify(pair, policy = exports.DEFAULT_BROWNFIELD_POLICY, context = {}) {
    let status = pair.status;
    const reasons = [...pair.reasons];
    // Step 2: drift context can reclassify 'added'.
    if (status === 'added') {
        if (context.scannerVersionDiffers) {
            status = 'tooling_drift';
            reasons.push({
                code: 'tooling-drift',
                detail: 'scanner or advisory-db version changed between runs',
            });
        }
        else if (context.configDiffers) {
            status = 'config_drift';
            reasons.push({
                code: 'config-drift',
                detail: 'suppression or policy config changed between runs',
            });
        }
        else if (context.kind &&
            policy.addedRequiresChangedLines.includes(context.kind) &&
            context.overlapsChangedLines === false) {
            // Scanner-wobble demotion: an `added` finding from a high-
            // wobble scanner (semgrep code, grep-based hygiene) that
            // sits outside the diff's changed lines is more likely a
            // baseline gap than a real regression. Demote to `uncertain`
            // (warn). The block-rules below still fire for findings the
            // diff actually touched.
            status = 'uncertain';
            reasons.push({
                code: 'unchanged-lines',
                detail: `${context.kind} finding outside diff hunks — demoted from added to uncertain (likely scanner wobble, not a developer-introduced regression)`,
            });
        }
    }
    // Step 3: confidence demotion for persisted/relocated pairs.
    if (status === 'persisted' || status === 'relocated') {
        const threshold = context.severity
            ? policy.confidence[context.severity]
            : Math.min(...Object.values(policy.confidence));
        if (pair.confidence < threshold) {
            reasons.push({
                code: 'low-confidence',
                detail: `match confidence ${pair.confidence.toFixed(2)} below threshold ${threshold.toFixed(2)}` +
                    (context.severity ? ` for severity ${context.severity}` : ''),
            });
            status = 'uncertain';
        }
    }
    // Step 4: block-rule overrides for newly-added findings.
    const blockRuleHit = evaluateBlockRules(status, policy.blockRules, context);
    if (blockRuleHit) {
        reasons.push({
            code: 'block-rule',
            detail: `policy block-rule fired: ${blockRuleHit}`,
        });
    }
    // Step 5: policy block/warn membership.
    const blocks = blockRuleHit !== null || policy.block.includes(status);
    const warns = policy.warn.includes(status);
    return { status, blocks, warns, reasons };
}
/**
 * Check whether any block-rule fires for the given classified pair.
 * Returns the matching rule's name (for reason rendering) or null
 * when no rule fires.
 *
 * Block-rules only apply to `added` status — they exist to escalate
 * specific kinds of new findings beyond the generic policy. A
 * `tooling_drift` reclassification means the `added` status is gone
 * and the block-rule no longer applies.
 */
function evaluateBlockRules(status, rules, context) {
    if (status !== 'added')
        return null;
    if (rules.newSecret && context.kind === 'secret')
        return 'newSecret';
    if (rules.newCriticalSecurity && context.kind === 'code' && context.severity === 'critical') {
        return 'newCriticalSecurity';
    }
    if (rules.newHighSecurity && context.kind === 'code' && context.severity === 'high') {
        return 'newHighSecurity';
    }
    if (rules.newCriticalDependencyVulnerability &&
        context.kind === 'dep-vuln' &&
        context.severity === 'critical') {
        return 'newCriticalDependencyVulnerability';
    }
    if (rules.newHighReachableDependencyVulnerability &&
        context.kind === 'dep-vuln' &&
        context.severity === 'high' &&
        context.reachable === true) {
        return 'newHighReachableDependencyVulnerability';
    }
    if (rules.newUntestedChangedSource &&
        context.kind === 'test-gap' &&
        context.overlapsChangedLines === true) {
        return 'newUntestedChangedSource';
    }
    if (rules.newSevereQualityIssueInChangedFiles &&
        (context.kind === 'code' || context.kind === 'hygiene') &&
        (context.severity === 'critical' || context.severity === 'high') &&
        context.overlapsChangedLines === true) {
        return 'newSevereQualityIssueInChangedFiles';
    }
    return null;
}
/**
 * Convenience: classify every pair in a match result against the
 * same policy. Returns an array aligned with the input pair order
 * so callers can render side-by-side. Per-pair context (severity,
 * kind, drift flags) is supplied via the optional `contextFor`
 * callback — callers map their `FindingId` back to the producer
 * envelope to fill in the fields the classifier reads.
 */
function classifyAll(pairs, policy = exports.DEFAULT_BROWNFIELD_POLICY, contextFor = () => ({})) {
    return pairs.map((pair) => classify(pair, policy, contextFor(pair)));
}
/** Conventional location for a per-repo brownfield policy. Loaded
 *  automatically by `resolvePolicy` when present. */
exports.DEFAULT_POLICY_FILENAME = path.join('.dxkit', 'policy.json');
/**
 * Load a brownfield policy with the three-step resolution order
 * shared by `createBaseline` and `runGuardrailCheck`:
 *
 *   1. `policyPath` (explicit `--policy <p>` flag). Errors if the
 *      path is supplied but unreadable / malformed.
 *   2. `<cwd>/.dxkit/policy.json` (conventional). Silently skipped
 *      when absent so consumers without a policy get the defaults.
 *   3. `DEFAULT_BROWNFIELD_POLICY` (compiled-in fallback).
 *
 * Customer fields shallow-merge over the default. The
 * `confidence` / `blockRules` blocks deep-merge by key. Unknown
 * fields are preserved — the classifier ignores what it doesn't
 * know, so forward-compatible policy files don't break old dxkit.
 */
function resolvePolicy(policyPath, cwd) {
    let resolvedPath = policyPath;
    if (!resolvedPath) {
        const conventional = path.join(cwd, exports.DEFAULT_POLICY_FILENAME);
        if (fs.existsSync(conventional))
            resolvedPath = conventional;
    }
    if (!resolvedPath)
        return exports.DEFAULT_BROWNFIELD_POLICY;
    let raw;
    try {
        raw = fs.readFileSync(resolvedPath, 'utf8');
    }
    catch (err) {
        throw new Error(`policy file not readable: ${resolvedPath} (${err.message})`);
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch (err) {
        throw new Error(`policy file is not valid JSON: ${resolvedPath} (${err.message})`);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error(`policy file root is not an object: ${resolvedPath}`);
    }
    const obj = parsed;
    return {
        ...exports.DEFAULT_BROWNFIELD_POLICY,
        ...obj,
        confidence: { ...exports.DEFAULT_BROWNFIELD_POLICY.confidence, ...(obj.confidence ?? {}) },
        blockRules: { ...exports.DEFAULT_BROWNFIELD_POLICY.blockRules, ...(obj.blockRules ?? {}) },
        block: obj.block ?? exports.DEFAULT_BROWNFIELD_POLICY.block,
        warn: obj.warn ?? exports.DEFAULT_BROWNFIELD_POLICY.warn,
        addedRequiresChangedLines: obj.addedRequiresChangedLines ?? exports.DEFAULT_BROWNFIELD_POLICY.addedRequiresChangedLines,
        mode: 'brownfield',
    };
}
/**
 * Convenience wrapper for callers that don't take a `--policy`
 * override (e.g., `createBaseline`). Loads the conventional file if
 * present; returns defaults otherwise.
 */
function loadPolicyFromCwd(cwd) {
    return resolvePolicy(undefined, cwd);
}
//# sourceMappingURL=policy.js.map