"use strict";
/**
 * `dxkit baseline create` orchestrator.
 *
 * Builds the shared producer context (runs every analyzer once),
 * dispatches through the canonical producer registry (CLAUDE.md
 * Rule 10), captures repo + analysis-environment metadata, and
 * writes the result to `.dxkit/baselines/<name>.json`. The on-disk
 * file is the durable record subsequent `guardrail check` runs diff
 * against.
 *
 * This module is the producer side; the matcher + classifier on the
 * consumer side already exist in `git-aware-match.ts` and
 * `policy.ts`. The two are connected by the file format defined in
 * `baseline-file.ts`.
 *
 * Per-kind producer coverage + deferral rationale live in the
 * registry index (`./producers/index.ts`) — the single discovery
 * surface. Adding a new identity kind or analyzer means
 * registering a producer there, never an edit here.
 */
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.clearToolVersionCache = clearToolVersionCache;
exports.gatherCurrentScan = gatherCurrentScan;
exports.gatherScanCoverage = gatherScanCoverage;
exports.createBaseline = createBaseline;
const child_process_1 = require("child_process");
const crypto_1 = require("crypto");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const health_1 = require("../analyzers/health");
const cache_1 = require("../analyzers/cache");
const gather_1 = require("../analyzers/quality/gather");
const tests_1 = require("../analyzers/tests");
const gitleaks_1 = require("../analyzers/tools/gitleaks");
const tool_registry_1 = require("../analyzers/tools/tool-registry");
const detect_1 = require("../detect");
const coverage_1 = require("./coverage");
const constants_1 = require("../constants");
const baseline_file_1 = require("./baseline-file");
const modes_1 = require("./modes");
const policy_1 = require("./policy");
const producers_1 = require("./producers");
const salt_1 = require("./salt");
const sanitize_1 = require("./sanitize");
const gather_2 = require("../allowlist/gather");
/** Hash used for baseline-envelope metadata fields (policy, ignore,
 *  toolchain, config). Distinct concern from finding-identity
 *  fingerprints — these never enter the matcher's identity space. */
function hashContent(content) {
    return (0, crypto_1.createHash)('sha1').update(content).digest('hex').slice(0, 16); // fingerprint-helper-ok: envelope-metadata hash, not finding identity
}
/**
 * Read a small file's text content with the canonical "absent → ''"
 * convention. Treating absent files as the empty string keeps the
 * downstream metadata hash stable across runs where the file is
 * still missing.
 */
function readOptionalFile(filePath) {
    try {
        return fs.readFileSync(filePath, 'utf8');
    }
    catch {
        return '';
    }
}
/** Resolve the absolute commit SHA + branch name of the working tree.
 *  Empty strings when the directory isn't a git repo — the rest of
 *  the orchestrator works fine, only the git-aware matcher loses its
 *  diff anchor on a future check. */
function readRepoState(cwd) {
    const run = (...args) => {
        try {
            return (0, child_process_1.execFileSync)('git', args, {
                cwd,
                encoding: 'utf8',
                stdio: ['ignore', 'pipe', 'pipe'],
            }).trim();
        }
        catch {
            return '';
        }
    };
    return {
        commitSha: run('rev-parse', 'HEAD'),
        branch: run('rev-parse', '--abbrev-ref', 'HEAD'),
    };
}
/** Build the analysis-environment hash bundle from the live repo. */
function buildAnalysisMeta(cwd) {
    const policyHash = hashContent(JSON.stringify(policy_1.DEFAULT_BROWNFIELD_POLICY));
    const ignoreHash = hashContent(readOptionalFile(path.join(cwd, '.dxkit-ignore')));
    const configHash = hashContent(readOptionalFile(path.join(cwd, '.vyuh-dxkit.json')));
    // toolchainHash is filled in by `createBaseline` once the
    // per-tool version map has been resolved (depends on the gather).
    return { dxkitVersion: constants_1.VERSION, policyHash, ignoreHash, toolchainHash: '', configHash };
}
/** Build the per-tool name → version map from the security
 *  aggregate's provenance. Sparse; only the tools that actually
 *  ran appear. Versions come from each tool's registered
 *  `versionCheck` invocation via `findTool`, so the resulting
 *  `toolchainHash` actually differs when a tool is upgraded —
 *  closing the drift-detection gap that placeholder values left
 *  open. In-process scanners (no external binary) are tagged with
 *  the dxkit version so a dxkit upgrade invalidates the toolchain
 *  hash even when no external tool changed — see
 *  `IN_PROCESS_TOOLS`.
 *
 *  Compound tool names like `'osv-scanner-nuget-direct'` (the
 *  per-pack synthetic names the dep-vuln providers emit) are
 *  resolved by progressively shortening on `-` boundaries until a
 *  matching TOOL_DEFS key is found — so
 *  `'osv-scanner-nuget-direct'` → `'osv-scanner-nuget'` →
 *  `'osv-scanner'` (the canonical key). */
function buildToolsMap(toolNames, cwd) {
    const out = {};
    for (const name of toolNames) {
        if (!name)
            continue;
        out[name] = resolveToolVersion(name, cwd);
    }
    return out;
}
/**
 * Scanner names that don't correspond to an external binary — their
 * "version" tracks the dxkit version. Adding a new in-process
 * scanner (e.g. a future regex-based dependency checker) means
 * appending its name here, never special-casing inside
 * `resolveToolVersion`.
 *
 * Drives the `provenance.{secrets,codePatterns,...}.tool` values
 * that surface when external tools are unavailable — gitleaks
 * absent → `grep-secrets` runs the in-process fallback; the
 * TLS-bypass registry is always in-process.
 */
const IN_PROCESS_TOOLS = new Set(['tls-bypass-registry', 'grep-secrets']);
/**
 * Per-process cache of resolved tool versions, keyed by `${name}::${cwd}`.
 *
 * Why this exists: `findTool` spawns an `execFileSync` subprocess to
 * run each tool's `versionCheck` command. Under heavy concurrent
 * load (parallel vitest workers, large suites running side-by-side),
 * that subprocess can occasionally complete with empty stdout —
 * `resolveToolVersion`'s `if (status.version) return status.version`
 * branch is skipped, the `return 'present'` fallback fires, and the
 * resulting toolchainHash drifts between two back-to-back gathers
 * within the same process. The matcher's `tooling_drift` gate then
 * fires spuriously.
 *
 * Tool versions don't change mid-process — once we've resolved
 * `gitleaks → 8.24.0` for `cwd`, every subsequent ask in the same
 * process should return the same answer. The cache locks the first
 * probe's outcome and skips later subprocess spawns entirely; same
 * answer always, with the side benefit of faster repeated gathers.
 *
 * NOT applied to `findTool` itself: `tools-cli.ts` runs an install
 * command then immediately re-probes (the install just created the
 * binary, we need fresh state). That callsite must keep getting
 * uncached results. The cache stays local to the toolchain-version
 * resolver here.
 */
const VERSION_CACHE = new Map();
function resolveToolVersion(name, cwd) {
    const cacheKey = `${name}::${cwd}`;
    const cached = VERSION_CACHE.get(cacheKey);
    if (cached !== undefined)
        return cached;
    const resolved = resolveToolVersionUncached(name, cwd);
    VERSION_CACHE.set(cacheKey, resolved);
    return resolved;
}
function resolveToolVersionUncached(name, cwd) {
    if (IN_PROCESS_TOOLS.has(name))
        return `dxkit-${constants_1.VERSION}`;
    const parts = name.split('-');
    for (let i = parts.length; i > 0; i--) {
        const candidate = parts.slice(0, i).join('-');
        const def = tool_registry_1.TOOL_DEFS[candidate];
        if (!def)
            continue;
        // Probe the version a few times — under heavy CPU load (parallel
        // test pools, concurrent scanner runs) the underlying `execSync`
        // subprocess can occasionally return before its `--version`
        // output streams back, leaving us with a bare `'present'` even
        // though the tool itself is fully functional. The per-process
        // VERSION_CACHE then locks that empty result for the lifetime of
        // the run, which is what we want for byte-stable toolchainHashes
        // but is wrong when the empty result was a transient artifact.
        // Three attempts absorb the hiccup without slowing the common
        // path (first probe succeeds → exit immediately).
        for (let attempt = 0; attempt < 3; attempt++) {
            const status = (0, tool_registry_1.findTool)(def, cwd);
            if (status.version)
                return status.version;
        }
        return 'present';
    }
    return 'unknown';
}
/**
 * Test seam: clear the version cache between test runs so per-test
 * fixtures don't leak resolutions into one another. Production
 * callers never use this — the cache lives for the entire CLI
 * invocation and dies with the process.
 */
function clearToolVersionCache() {
    VERSION_CACHE.clear();
}
/**
 * Run every analyzer once, dispatch through the producer registry,
 * and return the assembled `CurrentScan`. Used by `createBaseline`
 * to capture today's state and by `runGuardrailCheck` to gather the
 * current side of the cross-run diff.
 *
 * Pure-orchestrator: each step has a single responsibility (analyze
 * → produce entries → resolve envelope metadata).
 */
async function gatherCurrentScan(options) {
    const cwd = path.resolve(options.cwd);
    const analysisResult = await (0, cache_1.readOrBuildAnalysisResult)({
        cwd,
        build: (innerCwd) => (0, health_1.gatherAnalysisResultBody)(innerCwd, { verbose: !!options.verbose }),
    });
    const aggregate = analysisResult.capabilities.securityAggregate;
    if (!aggregate) {
        throw new Error('baseline scan: cached AnalysisResult missing securityAggregate ' +
            '(expected to be populated by gatherAnalysisResultBody).');
    }
    const repoState = {
        ...readRepoState(cwd),
        root: cwd,
    };
    // Salt resolves once; threaded into every producer that needs to
    // compute HMACs. The mode lands on the baseline file so the
    // matcher can re-derive the same salt at check time (or warn when
    // it can't).
    const { mode: saltMode, salt } = (0, salt_1.resolveSalt)(cwd);
    // Build the producer context once. Every analyzer's gather runs
    // here (or earlier inside readOrBuildAnalysisResult) so producers
    // can be pure or near-pure consumers — adding a new producer
    // means extending this context with one more input, never
    // adding another producer-specific block in this function.
    const testGapsReport = await (0, tests_1.analyzeTestGaps)(cwd, { verbose: !!options.verbose });
    const hygieneMarkers = (0, gather_1.gatherHygieneMarkers)(cwd);
    const gitleaksOutcome = (0, gitleaks_1.gatherGitleaksResult)(cwd);
    const rawSecrets = gitleaksOutcome.kind === 'success' ? gitleaksOutcome.rawSecrets : [];
    // Inline `dxkit-allow:` annotations gathered from source so the
    // stale-allow producer can flag orphans whose underlying findings
    // are no longer present.
    const inlineAllowlistAnnotations = (0, gather_2.gatherInlineAllowlistAnnotations)(cwd);
    const producerCtx = {
        cwd,
        commitSha: repoState.commitSha,
        salt,
        analysisResult,
        testGapsReport,
        hygiene: hygieneMarkers,
        rawSecrets,
        inlineAllowlistAnnotations,
    };
    // Dispatch through the canonical producer registry (CLAUDE.md
    // Rule 10). Adding a new identity kind means registering a
    // producer in `src/baseline/producers/index.ts` — never an edit
    // here.
    const findings = (0, producers_1.runProducers)(producerCtx, producers_1.PRODUCERS);
    const toolNames = new Set();
    // A capability's provenance `tool` is a `uniqueJoin(', ')` of every
    // provider that contributed — e.g. secrets is `'gitleaks, grep-secrets'`
    // when both run. Split it back into individual names so each resolves
    // its own version (gitleaks → semver; grep-secrets → in-process tag)
    // rather than recording the joined string as one unversioned tool.
    const addTools = (joined) => {
        if (!joined)
            return;
        for (const name of joined
            .split(',')
            .map((s) => s.trim())
            .filter(Boolean)) {
            toolNames.add(name);
        }
    };
    addTools(aggregate.provenance.secrets.tool);
    addTools(aggregate.provenance.codePatterns.tool);
    addTools(aggregate.provenance.depVulns.tool);
    if (aggregate.provenance.tlsBypass.ran)
        toolNames.add('tls-bypass-registry');
    const tools = buildToolsMap([...toolNames].sort(), cwd);
    const analysisMeta = {
        ...buildAnalysisMeta(cwd),
        toolchainHash: hashContent(JSON.stringify(tools)),
    };
    // Scanner availability for the active stack. Recorded on the baseline
    // so a later guardrail check can detect when a category was never
    // scanned (tool missing) rather than scanned-and-clean.
    const coverage = (0, coverage_1.coverageFromToolStatuses)((0, tool_registry_1.checkAllTools)(analysisResult.stack.languages, cwd));
    return {
        findings,
        aggregate,
        repoState,
        saltMode,
        tools,
        coverage,
        analysisMeta,
        producerCtx,
    };
}
/**
 * Scanner-availability snapshot for `cwd`, independent of a full scan.
 *
 * Cheap pre-flight used by the CLI to warn — before paying for the
 * gather — when finding-contributing scanners are missing on this
 * machine, so a developer isn't surprised by a silently-incomplete
 * baseline. Detects the stack and probes each required tool.
 */
function gatherScanCoverage(cwd) {
    const resolved = path.resolve(cwd);
    return (0, coverage_1.coverageFromToolStatuses)((0, tool_registry_1.checkAllTools)((0, detect_1.detect)(resolved).languages, resolved));
}
/**
 * Run the baseline-create pipeline. Pure-orchestrator: resolve
 * the baseline mode, gather the current scan, then either:
 *
 *   - `committed-full` → write rich entries to disk (today's
 *     behavior).
 *   - `committed-sanitized` → sanitize every entry, then write.
 *     The cross-run matching contract is preserved; locator
 *     fields are stripped.
 *   - `ref-based` → no file write. The guardrail check will
 *     recompute the prior side from a git ref instead.
 *
 * In all three cases the returned `CreateBaselineResult` carries
 * `resolvedMode` so callers can log WHY a given mode was picked
 * (CLI flag / policy file / visibility auto-detect).
 */
async function createBaseline(options) {
    const cwd = path.resolve(options.cwd);
    const name = options.name ?? baseline_file_1.DEFAULT_BASELINE_NAME;
    const mode = options.resolvedMode ??
        (() => {
            const policy = (0, policy_1.loadPolicyFromCwd)(cwd);
            return (0, modes_1.resolveBaselineMode)({
                cwd,
                cliMode: options.cliMode,
                cliRef: options.cliRef,
                policyMode: policy.baseline?.mode,
                policyRef: policy.baseline?.ref,
            });
        })();
    if (mode.mode === 'ref-based') {
        // Ref-based mode keeps no committed baseline. We still run no
        // gather here — the guardrail check does it on demand against
        // the configured ref. Returning the resolved mode lets the CLI
        // surface a clear "ref-based mode active; no file written" log.
        return { mode };
    }
    const filePath = (0, baseline_file_1.pathForBaseline)(cwd, name);
    if (!options.force && fs.existsSync(filePath)) {
        throw new Error(`baseline already exists at ${filePath}. Pass force: true to overwrite, ` +
            `or use a different --name to keep both.`);
    }
    const scan = await gatherCurrentScan({ cwd, verbose: options.verbose });
    const richFile = {
        schemaVersion: baseline_file_1.BASELINE_SCHEMA_VERSION,
        name,
        createdAt: new Date().toISOString(),
        repo: scan.repoState,
        analysis: scan.analysisMeta,
        tools: scan.tools,
        saltMode: scan.saltMode,
        coverage: scan.coverage,
        findings: scan.findings,
    };
    const file = mode.mode === 'committed-sanitized' ? (0, sanitize_1.sanitizeFile)(richFile) : richFile;
    (0, baseline_file_1.writeBaselineFile)(filePath, file);
    return { mode, path: filePath, file };
}
//# sourceMappingURL=create.js.map