"use strict";
/**
 * BOM analyzer data-gather. Joins LICENSES + DEP_VULNS via the
 * existing capability gather functions — no new tool invocation logic
 * (CLAUDE.md rule 2). The gather just calls each, then walks both
 * result sets to build a per-package join keyed by `package@version`.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.compareSemver = compareSemver;
exports.maxSemver = maxSemver;
exports.deriveTier1Resolution = deriveTier1Resolution;
exports.buildByTopLevelDep = buildByTopLevelDep;
exports.gatherBomEntries = gatherBomEntries;
const gather_1 = require("../licenses/gather");
const gather_2 = require("../security/gather");
const SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
/**
 * Compare two version strings as semver triples. Strips a leading
 * 'v' (Go-pack convention) and compares dot-separated numeric
 * components. Falls back to lexicographic comparison when either
 * input isn't a parseable triple — preserves a deterministic
 * ordering for non-semver versions (e.g. cargo's "0.10.55+echo.1").
 */
function compareSemver(a, b) {
    const norm = (v) => v.replace(/^v/, '');
    const pa = norm(a)
        .split('.')
        .map((p) => parseInt(p, 10));
    const pb = norm(b)
        .split('.')
        .map((p) => parseInt(p, 10));
    if (pa.some(isNaN) || pb.some(isNaN))
        return norm(a).localeCompare(norm(b));
    const len = Math.max(pa.length, pb.length);
    for (let i = 0; i < len; i++) {
        const xa = pa[i] ?? 0;
        const xb = pb[i] ?? 0;
        if (xa !== xb)
            return xa - xb;
    }
    return 0;
}
/** Highest version in `versions` per compareSemver. Returns the input
 *  unchanged when only one version is supplied. */
function maxSemver(versions) {
    if (versions.length === 0)
        return '';
    return versions.reduce((acc, v) => (compareSemver(v, acc) > 0 ? v : acc));
}
/**
 * Tier-1 resolution proposal derived purely from `fixedVersion`
 * data shipped by each pack's depVulns provider. Per checkpoint §9:
 *   - no vulns → empty
 *   - every vuln has fixedVersion → "PROPOSAL: Upgrade to <maxFix> (resolves N)"
 *   - some vuln lacks fixedVersion → "No fix available — evaluate replacement"
 *
 * Higher tiers (10h.4 Snyk, 10h.6 osv-scanner fix) populate
 * `upgradeAdvice` directly on each finding; the bom render layer
 * picks the richest available value per package.
 */
function deriveTier1Resolution(vulns) {
    if (vulns.length === 0)
        return '';
    const fixes = vulns.map((v) => v.fixedVersion).filter((v) => !!v);
    if (fixes.length !== vulns.length) {
        return 'No fix available — evaluate replacement';
    }
    const target = maxSemver(fixes);
    return `PROPOSAL: Upgrade to ${target} (resolves ${vulns.length} vuln${vulns.length === 1 ? '' : 's'})`;
}
/** Highest severity across the supplied vulns. */
function maxSeverityOf(vulns) {
    if (vulns.length === 0)
        return null;
    let best = 'low';
    for (const v of vulns) {
        if (SEV_RANK[v.severity] < SEV_RANK[best])
            best = v.severity;
    }
    return best;
}
/**
 * Build the per-top-level-dep rollup from a flat list of entries.
 * Walks every advisory under every entry; increments the counter for
 * each top-level the advisory lists (a vuln reachable from two
 * top-levels increments both, matching Snyk's grouping semantics).
 *
 * Entries with no topLevelDep attribution contribute nothing — the
 * rollup is best-effort based on what each pack populates. Pure
 * function; unit-testable.
 */
function buildByTopLevelDep(entries) {
    const accum = new Map();
    for (const e of entries) {
        for (const v of e.vulns) {
            const tops = v.topLevelDep;
            if (!tops || tops.length === 0)
                continue;
            for (const top of tops) {
                const cur = accum.get(top);
                if (!cur) {
                    accum.set(top, {
                        advisoryCount: 1,
                        maxSeverity: v.severity,
                        packages: new Set([e.package]),
                    });
                }
                else {
                    cur.advisoryCount++;
                    if (SEV_RANK[v.severity] < SEV_RANK[cur.maxSeverity])
                        cur.maxSeverity = v.severity;
                    cur.packages.add(e.package);
                }
            }
        }
    }
    const out = {};
    for (const [top, data] of accum) {
        out[top] = {
            advisoryCount: data.advisoryCount,
            maxSeverity: data.maxSeverity,
            packages: [...data.packages].sort(),
        };
    }
    return out;
}
/**
 * Both pre-gathered envelopes are optional. The override pattern lets
 * the analyzer layer hand BoM a canonical inventory + advisory set
 * built once at repo-root, so the gather pipeline never re-walks the
 * tree from a different cwd (the root cause of cross-consumer drift
 * on the same logical metric — e.g. licenses-vs-BoM package count
 * diverging on a deep C# monorepo because two walks visited two
 * different subsets of csproj files).
 *
 * `depVulnsOverride`: shared dep-vuln set across nested callers.
 *   Pre-fix BoM called `gatherDepVulns(absRoot)` per sub-root, and
 *   the csharp pack's gather was cwd-sensitive — at a sub-root with
 *   a stale `obj/project.assets.json` it returned 0 advisories via
 *   dotnet, while at repo-root with no `.csproj` it correctly fell
 *   back to `osv-scanner-nuget-direct` and surfaced them. Override +
 *   the pack-layer cwd-invariance work closed that gap.
 *
 * `licensesOverride`: pre-gathered canonical license inventory.
 *   When set, the gather skips its own `gatherLicensesResult(cwd)`
 *   call and uses the override directly. `null` (a deliberate
 *   "license inventory exists but is empty / unavailable") is
 *   distinguished from `undefined` ("gather it yourself") — the
 *   canonical analyzer-layer caller always passes the cached
 *   envelope shape verbatim.
 */
async function gatherBomEntries(cwd, options = {}) {
    const [licensesEnv, depVulns] = await Promise.all([
        options.licensesOverride !== undefined
            ? Promise.resolve(options.licensesOverride)
            : (0, gather_1.gatherLicensesResult)(cwd),
        options.depVulnsOverride ? Promise.resolve(options.depVulnsOverride) : (0, gather_2.gatherDepVulns)(cwd),
    ]);
    const licenseFindings = licensesEnv?.findings ?? [];
    const vulnFindings = depVulns.findings;
    // Index vulns by package@version (primary) and package (fallback).
    const vulnByPkgVer = new Map();
    const vulnByPkg = new Map();
    for (const v of vulnFindings) {
        if (v.installedVersion) {
            const key = `${v.package}@${v.installedVersion}`;
            const arr = vulnByPkgVer.get(key) ?? [];
            arr.push(v);
            vulnByPkgVer.set(key, arr);
        }
        const arr = vulnByPkg.get(v.package) ?? [];
        arr.push(v);
        vulnByPkg.set(v.package, arr);
    }
    const entries = [];
    const matchedVulnKeys = new Set();
    for (const lic of licenseFindings) {
        const exactKey = `${lic.package}@${lic.version}`;
        let attached = vulnByPkgVer.get(exactKey);
        if (attached) {
            matchedVulnKeys.add(exactKey);
        }
        else {
            // Version-less vuln entries fall back to package-name match.
            // Filter to only those that lacked installedVersion to avoid
            // double-counting against other version rows.
            const byPkg = vulnByPkg.get(lic.package) ?? [];
            const versionless = byPkg.filter((v) => !v.installedVersion);
            attached = versionless.length > 0 ? versionless : [];
        }
        entries.push(buildEntry(lic, attached, true));
    }
    // Vuln-only entries: vulns whose package@version did not match a
    // license row AND whose package is absent from every license entry
    // (versionless fallback is consumed above).
    const licensePackages = new Set(licenseFindings.map((l) => l.package));
    const vulnOnlyByPkgVer = new Map();
    for (const [key, vulns] of vulnByPkgVer) {
        if (matchedVulnKeys.has(key))
            continue;
        const pkg = vulns[0].package;
        if (licensePackages.has(pkg))
            continue; // already attached via versionless or other version
        const arr = vulnOnlyByPkgVer.get(key) ?? [];
        arr.push(...vulns);
        vulnOnlyByPkgVer.set(key, arr);
    }
    for (const [, vulns] of vulnOnlyByPkgVer) {
        const v0 = vulns[0];
        const synthLicense = {
            package: v0.package,
            version: v0.installedVersion ?? 'unknown',
            licenseType: 'UNKNOWN',
        };
        entries.push(buildEntry(synthLicense, vulns, false));
    }
    // Stable sort: package alphabetical, then version.
    entries.sort((a, b) => a.package.localeCompare(b.package) || compareSemver(a.version, b.version));
    const toolsUsed = new Set();
    if (licensesEnv?.tool) {
        for (const t of licensesEnv.tool.split(', '))
            if (t)
                toolsUsed.add(t);
    }
    if (depVulns.tool) {
        for (const t of depVulns.tool.split(', '))
            if (t)
                toolsUsed.add(t);
    }
    return {
        entries,
        toolsUsed: [...toolsUsed],
        toolsUnavailable: [],
    };
}
function buildEntry(lic, vulns, joinedFromBoth) {
    // Tier 2/4 fields on individual findings outrank Tier-1 derivation.
    // Pick the first non-empty `upgradeAdvice` from any finding; fall
    // back to derived advice when all vulns are Tier-1 only.
    const tieredAdvice = vulns.map((v) => v.upgradeAdvice).find((a) => a && a.length > 0);
    // For vuln-only synthetic rows (no LicenseFinding), treat the
    // package as top-level iff any finding lists itself in topLevelDep
    // or has no transitive attribution. Prevents the filter from
    // dropping pure-vuln rows silently on packs where licenses are
    // missing (e.g. workspace sub-packages pre-10h.5.0b).
    let isTopLevel = lic.isTopLevel;
    if (isTopLevel === undefined && !joinedFromBoth && vulns.length > 0) {
        isTopLevel = vulns.some((v) => !v.topLevelDep || v.topLevelDep.includes(lic.package));
    }
    return {
        package: lic.package,
        version: lic.version,
        licenseType: lic.licenseType,
        licenseText: lic.licenseText,
        sourceUrl: lic.sourceUrl,
        description: lic.description,
        supplier: lic.supplier,
        releaseDate: lic.releaseDate,
        vulns,
        maxSeverity: maxSeverityOf(vulns),
        upgradeAdvice: tieredAdvice ?? deriveTier1Resolution(vulns),
        joinedFromBoth,
        isTopLevel,
    };
}
//# sourceMappingURL=gather.js.map