"use strict";
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;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.typescript = void 0;
exports.buildTsTopLevelDepIndex = buildTsTopLevelDepIndex;
exports.extractTsImportsRaw = extractTsImportsRaw;
exports.resolveTsImportRaw = resolveTsImportRaw;
exports.parseIstanbulSummary = parseIstanbulSummary;
exports.parseIstanbulFinal = parseIstanbulFinal;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const hosted_git_info_1 = __importDefault(require("hosted-git-info"));
const coverage_1 = require("../analyzers/tools/coverage");
const walk_source_files_1 = require("../analyzers/tools/walk-source-files");
const npm_registry_1 = require("../analyzers/tools/npm-registry");
const osv_1 = require("../analyzers/tools/osv");
const osv_scanner_fix_1 = require("../analyzers/tools/osv-scanner-fix");
const runner_1 = require("../analyzers/tools/runner");
const run_tests_helper_1 = require("../analyzers/tools/run-tests-helper");
const tool_registry_1 = require("../analyzers/tools/tool-registry");
const TS_JS_EXT = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
/**
 * Tier an ESLint rule ID into the four-tier severity model.
 *
 * Priority is rule-name pattern first (security plugins, known-dangerous
 * rules), falling back to the rule's ESLint severity (2=error → high,
 * 1=warning → medium) for rules we don't recognize.
 *
 * Unknown rules default to 'low' so unfamiliar plugins don't inflate the
 * error count. Callers that also have the ESLint severity should prefer
 * mapLintMessageSeverity below.
 */
function mapEslintRuleSeverity(ruleId) {
    if (!ruleId)
        return 'low';
    // Security plugins — both eslint-plugin-security and eslint-plugin-security-node.
    if (/^security(-node)?\//.test(ruleId))
        return 'critical';
    // Known-dangerous built-in rules: anything that permits code injection.
    if (ruleId === 'no-eval' ||
        ruleId === 'no-implied-eval' ||
        ruleId === 'no-new-func' ||
        ruleId === 'no-script-url' ||
        ruleId === 'no-proto') {
        return 'critical';
    }
    if (/^@typescript-eslint\/no-unsafe-(eval|function-type)/.test(ruleId))
        return 'critical';
    // Correctness / type-safety — bugs, not style.
    if (ruleId === 'no-undef' ||
        ruleId === 'no-unreachable' ||
        ruleId === 'no-duplicate-case' ||
        ruleId === 'no-dupe-keys' ||
        ruleId === 'no-dupe-args' ||
        ruleId === 'valid-typeof' ||
        ruleId === 'use-isnan' ||
        ruleId === 'no-cond-assign' ||
        ruleId === 'no-unsafe-negation' ||
        ruleId === 'no-obj-calls') {
        return 'high';
    }
    if (/^@typescript-eslint\/no-unsafe-/.test(ruleId))
        return 'high';
    if (/^react-hooks\/rules-of-hooks$/.test(ruleId))
        return 'high';
    // Best-practice / maintenance — not buggy but worth flagging.
    if (ruleId === 'no-console' ||
        ruleId === 'no-debugger' ||
        ruleId === 'no-var' ||
        ruleId === 'prefer-const' ||
        ruleId === 'eqeqeq') {
        return 'medium';
    }
    if (/^@typescript-eslint\/(no-explicit-any|no-unused-vars|ban-types)/.test(ruleId))
        return 'medium';
    if (/^react-hooks\/exhaustive-deps$/.test(ruleId))
        return 'medium';
    // Style / formatting plugins default to low.
    if (/^(prettier|import|react|jsx-a11y|unicorn)\//.test(ruleId))
        return 'low';
    return 'low';
}
/** Combine a rule-based tier with ESLint's own severity for unknown rules. */
function tierEslintMessage(ruleId, eslintSeverity) {
    const tiered = mapEslintRuleSeverity(ruleId);
    // For unknown rules (→ 'low'), use ESLint's own severity as a floor.
    if (tiered === 'low' && ruleId) {
        if (eslintSeverity === 2)
            return 'high';
        if (eslintSeverity === 1)
            return 'medium';
    }
    return tiered;
}
/**
 * Map npm-audit's severity vocabulary to the four-tier `SeverityCounts`
 * domain. npm uses `moderate`; we normalize to `medium` everywhere else.
 * Unknown values fall through to `low` to avoid silently inflating counts.
 */
function normalizeNpmSeverity(s) {
    switch (s) {
        case 'critical':
            return 'critical';
        case 'high':
            return 'high';
        case 'moderate':
        case 'medium':
            return 'medium';
        default:
            return 'low';
    }
}
/**
 * Extract the GHSA identifier from a GitHub advisory URL like
 * `https://github.com/advisories/GHSA-h5c3-5r3r-rr8q`. Returns null when
 * the URL doesn't match (e.g. legacy npm advisory pages).
 */
function extractGhsaId(url) {
    if (!url)
        return null;
    const m = url.match(/GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/i);
    return m ? m[0].toUpperCase() : null;
}
/**
 * Read the locally installed version of a package from
 * `node_modules/<pkg>/package.json`. Used to attach `installedVersion`
 * to dep-vuln findings — npm-audit's JSON only reports the affected
 * range, not which exact resolved version is on disk.
 */
function readInstalledTsVersion(cwd, pkgName) {
    try {
        const raw = fs.readFileSync(path.join(cwd, 'node_modules', pkgName, 'package.json'), 'utf-8');
        const parsed = JSON.parse(raw);
        return parsed.version;
    }
    catch {
        return undefined;
    }
}
/**
 * Build a per-package-name index mapping each installed npm package to
 * the set of root manifest entries (top-level deps) it rolls up under.
 *
 * Strategy: BFS the lockfile starting from each root `dependencies` /
 * `devDependencies` name, following each visited package's own
 * `dependencies` map. Each lockfile entry for a given name contributes
 * its direct-child names — duplicated copies (nested `node_modules/`)
 * are all consulted so attribution matches npm's resolution at any
 * ancestor path. Attribution is coarse at the package-name level
 * (ignores which exact version serves which parent) — matches Snyk's
 * own grouping in their UI and is what bom + HTML renders group on.
 *
 * Exported for unit tests; pure function over parsed lockfile JSON so
 * fixtures don't need filesystem setup.
 */
function buildTsTopLevelDepIndex(lock) {
    const result = new Map();
    if (!lock || typeof lock !== 'object')
        return new Map();
    const packages = lock.packages;
    if (!packages)
        return new Map();
    const root = packages[''];
    if (!root)
        return new Map();
    const topLevelNames = [
        ...Object.keys(root.dependencies ?? {}),
        ...Object.keys(root.devDependencies ?? {}),
    ];
    // Pre-index: installed package name -> every lockfile key where it lives.
    // Scoped packages ('@foo/bar') are kept intact since everything after
    // the last 'node_modules/' segment is the logical package name.
    const keysByName = new Map();
    const NM = 'node_modules/';
    for (const key of Object.keys(packages)) {
        const idx = key.lastIndexOf(NM);
        if (idx < 0)
            continue;
        const name = key.slice(idx + NM.length);
        if (!name)
            continue;
        const arr = keysByName.get(name) ?? [];
        arr.push(key);
        keysByName.set(name, arr);
    }
    for (const top of topLevelNames) {
        const visited = new Set();
        const queue = [top];
        while (queue.length > 0) {
            const name = queue.shift();
            if (visited.has(name))
                continue;
            visited.add(name);
            const bucket = result.get(name) ?? new Set();
            bucket.add(top);
            result.set(name, bucket);
            for (const key of keysByName.get(name) ?? []) {
                const entry = packages[key];
                const deps = entry?.dependencies;
                if (!deps)
                    continue;
                for (const childName of Object.keys(deps)) {
                    if (!visited.has(childName))
                        queue.push(childName);
                }
            }
        }
    }
    const sorted = new Map();
    for (const [pkg, parents] of result) {
        sorted.set(pkg, [...parents].sort());
    }
    return sorted;
}
/**
 * Read + parse the project's `package-lock.json`, then run the index.
 * Returns an empty map when the lockfile is missing or unparsable —
 * topLevelDep stays unattributed rather than blocking dep-vuln gather.
 */
function loadTsTopLevelDepIndex(cwd) {
    try {
        const raw = fs.readFileSync(path.join(cwd, 'package-lock.json'), 'utf-8');
        return buildTsTopLevelDepIndex(JSON.parse(raw));
    }
    catch {
        return new Map();
    }
}
/**
 * Single source of truth for the typescript pack's dep-vuln gathering.
 * Consumed by `tsDepVulnsProvider` (capability dispatcher).
 *
 * Counts derive from the per-package severity buckets (npm-audit's own
 * aggregation). Findings derive from the per-advisory `via[]` objects
 * — one finding per advisory, so a package with three GHSAs yields
 * three findings even though counts increment by one. This matches the
 * customer xlsx model: col 11 (Criticality) is per-package, col 12
 * (Vulnerability Issues) is per-advisory.
 */
async function gatherTsDepVulnsResult(cwd) {
    if (!(0, runner_1.fileExists)(cwd, 'package.json')) {
        return { kind: 'no-manifest', reason: 'no package.json' };
    }
    const auditRaw = (0, runner_1.run)('npm audit --json 2>&1', cwd, 60000);
    if (!auditRaw)
        return { kind: 'unavailable', reason: 'npm audit produced no output' };
    try {
        const auditData = JSON.parse(auditRaw);
        let critical = 0;
        let high = 0;
        let medium = 0;
        let low = 0;
        if (auditData.metadata?.vulnerabilities) {
            const v = auditData.metadata.vulnerabilities;
            critical = v.critical || 0;
            high = v.high || 0;
            medium = v.moderate || 0;
            low = v.low || 0;
        }
        else if (auditData.vulnerabilities) {
            for (const v of Object.values(auditData.vulnerabilities)) {
                if (v.severity === 'critical')
                    critical++;
                else if (v.severity === 'high')
                    high++;
                else if (v.severity === 'moderate')
                    medium++;
                else if (v.severity === 'low')
                    low++;
            }
        }
        const findings = [];
        if (auditData.vulnerabilities) {
            const versionCache = new Map();
            const installedVersion = (pkg) => {
                if (!versionCache.has(pkg))
                    versionCache.set(pkg, readInstalledTsVersion(cwd, pkg));
                return versionCache.get(pkg);
            };
            const topLevelIndex = loadTsTopLevelDepIndex(cwd);
            // npm-audit inlines the same advisory record on every consumer's
            // `via[]` across the vulnerability tree (e.g. minimatch's ReDoS
            // advisory appears on @loopback/cli, glob-parent, picomatch, etc.
            // all at once). Without dedup we emit N copies of one logical
            // advisory against the same package@version. Dedup on
            // (package, installedVersion, id) — each advisory-against-pkg pair
            // is the true identity of a finding.
            const seen = new Set();
            for (const [pkgName, entry] of Object.entries(auditData.vulnerabilities)) {
                // npm-audit's `fixAvailable` is the *consumer* upgrade command —
                // `{ name, version }` identifies which top-level dep to bump to
                // resolve the entire advisory tree under this entry. When `name`
                // matches the entry key, the fix is at this package's own
                // version (direct upgrade). When `name` differs, the fix is a
                // transitive parent upgrade — applying `fix.version` as THIS
                // package's fixedVersion is wrong (caused the "uuid@13 fixed at
                // 3.2.1" false positive surfaced by the 10h.3.10 benchmark).
                const fix = entry.fixAvailable;
                const fixIsObject = typeof fix === 'object';
                const directFix = fixIsObject && fix.name === pkgName ? fix : null;
                const transitiveFix = fixIsObject && fix.name !== pkgName ? fix : null;
                for (const v of entry.via ?? []) {
                    if (typeof v === 'string')
                        continue;
                    const advisoryPkg = v.name ?? pkgName;
                    const ghsa = extractGhsaId(v.url);
                    const id = ghsa ?? (v.source ? `npm-${v.source}` : `npm-${advisoryPkg}`);
                    const dedupeKey = `${advisoryPkg}@${installedVersion(advisoryPkg) ?? ''}|${id}`;
                    if (seen.has(dedupeKey))
                        continue;
                    seen.add(dedupeKey);
                    const finding = {
                        id,
                        package: advisoryPkg,
                        installedVersion: installedVersion(advisoryPkg),
                        tool: 'npm-audit',
                        packId: 'typescript',
                        severity: normalizeNpmSeverity(v.severity),
                    };
                    if (typeof v.cvss?.score === 'number')
                        finding.cvssScore = v.cvss.score;
                    if (directFix) {
                        finding.fixedVersion = directFix.version;
                        finding.breakingUpgrade = directFix.isSemVerMajor;
                    }
                    else if (transitiveFix && transitiveFix.version !== '0.0.0') {
                        // Parent-upgrade remediation — surface directly in upgradeAdvice
                        // rather than pretending the value is a direct fix. Bom render
                        // picks this up as the Tier-1 resolution for the row.
                        //
                        // The 0.0.0 guard handles the npm-audit sentinel: when no
                        // real transitive upgrade target can be computed (the parent
                        // dep has no compatible patched version), npm-audit emits
                        // `{ version: "0.0.0", isSemVerMajor: true }` rather than
                        // omitting the field. Templating that into a recommendation
                        // ("Upgrade react-scripts to 0.0.0") reads as actionable
                        // guidance — the resolution column should fall through to
                        // the Tier-1 "No fix available" message instead.
                        const majorNote = transitiveFix.isSemVerMajor ? ' [major]' : '';
                        finding.upgradeAdvice =
                            `Upgrade ${transitiveFix.name} to ${transitiveFix.version}${majorNote} ` +
                                `(transitive fix)`;
                    }
                    if (ghsa)
                        finding.aliases = [ghsa];
                    if (v.title)
                        finding.summary = v.title;
                    if (v.url)
                        finding.references = [v.url];
                    const parents = topLevelIndex.get(advisoryPkg);
                    if (parents && parents.length > 0)
                        finding.topLevelDep = parents;
                    findings.push(finding);
                }
            }
        }
        // Alias-fallback CVSS pass: npm-audit ships CVSS for ~100% of
        // advisories (smoke: 94/94 on platform), so this is typically a
        // no-op — every input has `embeddedCvss` set, no API calls fire.
        // Future-proof against the rare advisory missing CVSS.
        if (findings.length > 0) {
            const cvssInputs = findings.map((f) => ({
                primaryId: f.id,
                embeddedCvss: f.cvssScore ?? null,
                aliases: f.aliases ?? [],
            }));
            const resolved = await (0, osv_1.resolveCvssScores)(cvssInputs);
            for (const f of findings) {
                const score = resolved.get(f.id);
                if (score !== null && score !== undefined)
                    f.cvssScore = score;
            }
        }
        // Tier-2 enrichment — run osv-scanner fix to populate structured
        // `upgradePlan` on every finding it has a proposal for. Free-text
        // `upgradeAdvice` from npm-audit already set above stays as-is (it's
        // the human-readable form for markdown); `upgradePlan` is the
        // agent-consumable form. Degrades silently when osv-scanner is
        // unavailable — zero enrichment, existing advice preserved.
        if (findings.length > 0) {
            const plans = await (0, osv_scanner_fix_1.gatherOsvScannerFixPlans)(cwd);
            (0, osv_scanner_fix_1.enrichWithUpgradePlans)(findings, plans);
        }
        const envelope = {
            schemaVersion: 1,
            tool: 'npm-audit',
            enrichment: null,
            counts: { critical, high, medium, low },
            findings,
        };
        return { kind: 'success', envelope };
    }
    catch (err) {
        return { kind: 'unavailable', reason: `npm audit parse error: ${err.message}` };
    }
}
const tsDepVulnsProvider = {
    source: 'typescript',
    async gather(cwd) {
        const outcome = await gatherTsDepVulnsResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
    async gatherOutcome(cwd) {
        return gatherTsDepVulnsResult(cwd);
    },
};
function stripTsJsComments(src) {
    let out = src.replace(/\/\*[\s\S]*?\*\//g, '');
    out = out.replace(/(^|[^:"'/])\/\/[^\n]*/g, '$1');
    return out;
}
function isFile(p) {
    try {
        return fs.statSync(p).isFile();
    }
    catch {
        return false;
    }
}
function toRel(abs, cwd) {
    return path.relative(cwd, abs).split(path.sep).join('/');
}
/**
 * Capture raw TS/JS module specifiers from source text. The imports
 * capability batch-calls this while walking the pack's source extensions;
 * unit tests exercise it directly for parse-correctness cases.
 */
function extractTsImportsRaw(content) {
    const out = [];
    const stripped = stripTsJsComments(content);
    const importRe = /\bimport\s+(?:[^'";]*?from\s+)?['"]([^'"]+)['"]/g;
    const reexportRe = /\bexport\s+(?:[^'";]*?from\s+)['"]([^'"]+)['"]/g;
    const dynRe = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
    const reqRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
    for (const re of [importRe, reexportRe, dynRe, reqRe]) {
        let m;
        while ((m = re.exec(stripped)) !== null) {
            out.push(m[1]);
        }
    }
    return out;
}
/**
 * Resolve a TS/JS module specifier to an in-project relative file path,
 * or null for external packages and unresolvable specifiers. Exported so
 * unit tests can exercise resolution directly; the imports capability
 * calls it while building per-file edges.
 */
function resolveTsImportRaw(fromFile, spec, cwd) {
    if (!spec.startsWith('./') && !spec.startsWith('../'))
        return null;
    const fromDir = path.dirname(path.join(cwd, fromFile));
    const baseAbs = path.resolve(fromDir, spec);
    for (const ext of TS_JS_EXT) {
        if (baseAbs.endsWith(ext) && isFile(baseAbs)) {
            return toRel(baseAbs, cwd);
        }
    }
    for (const ext of TS_JS_EXT) {
        if (isFile(baseAbs + ext))
            return toRel(baseAbs + ext, cwd);
    }
    for (const ext of TS_JS_EXT) {
        const idx = path.join(baseAbs, 'index' + ext);
        if (isFile(idx))
            return toRel(idx, cwd);
    }
    return null;
}
/**
 * Single source of truth for the typescript pack's lint gathering.
 * Consumed by `tsLintProvider` (capability dispatcher).
 */
function gatherTsLintResult(cwd) {
    const lbEslintPath = 'node_modules/.bin/lb-eslint';
    const eslintPath = 'node_modules/.bin/eslint';
    const hasLbEslint = (0, runner_1.fileExists)(cwd, lbEslintPath);
    const hasEslint = (0, runner_1.fileExists)(cwd, eslintPath);
    if (!hasLbEslint && !hasEslint) {
        return { kind: 'unavailable', reason: 'not installed' };
    }
    const hasFlatConfig = (0, runner_1.fileExists)(cwd, 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', 'eslint.config.ts');
    const hasLegacyConfig = (0, runner_1.fileExists)(cwd, '.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', '.eslintrc.yaml', '.eslintrc.cjs');
    const binToCheck = hasEslint ? `./${eslintPath}` : `./${lbEslintPath}`;
    const versionOutput = (0, runner_1.run)(`${binToCheck} --version`, cwd);
    const majorMatch = versionOutput.match(/v?(\d+)/);
    const major = majorMatch ? parseInt(majorMatch[1]) : 0;
    if (major >= 9 && !hasFlatConfig) {
        if (hasLbEslint) {
            // lb-eslint may provide its own config; fall through to try it
        }
        else if (hasLegacyConfig) {
            return { kind: 'unavailable', reason: `v${major} but project uses legacy .eslintrc` };
        }
        else {
            return { kind: 'unavailable', reason: 'no eslint config found' };
        }
    }
    const bins = hasLbEslint ? [`./${lbEslintPath}`, `./${eslintPath}`] : [`./${eslintPath}`];
    for (const bin of bins) {
        if (!(0, runner_1.fileExists)(cwd, bin.replace('./', '')))
            continue;
        const result = (0, runner_1.runJSON)(`${bin} . --format json`, cwd, 120000);
        if (result && Array.isArray(result)) {
            const counts = { critical: 0, high: 0, medium: 0, low: 0 };
            for (const file of result) {
                for (const msg of file.messages || []) {
                    counts[tierEslintMessage(msg.ruleId, msg.severity)]++;
                }
            }
            const envelope = { schemaVersion: 1, tool: 'eslint', counts };
            return { kind: 'success', envelope };
        }
    }
    return { kind: 'unavailable', reason: 'config error' };
}
const tsLintProvider = {
    source: 'typescript',
    async gather(cwd) {
        const outcome = gatherTsLintResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
    async gatherOutcome(cwd) {
        return gatherTsLintResult(cwd);
    },
};
/** Istanbul coverage-summary.json: `{ "total": {...}, "/abs/path": {...} }`. */
function parseIstanbulSummary(raw, sourceFile, cwd) {
    const data = JSON.parse(raw);
    const files = new Map();
    let totalCovered = 0;
    let totalLines = 0;
    for (const [key, entry] of Object.entries(data)) {
        if (key === 'total')
            continue;
        const lines = entry?.lines;
        if (!lines || typeof lines.covered !== 'number' || typeof lines.total !== 'number')
            continue;
        const rel = (0, coverage_1.toRelative)(key, cwd);
        const fc = {
            path: rel,
            covered: lines.covered,
            total: lines.total,
            pct: (0, coverage_1.round1)(lines.total > 0 ? (lines.covered / lines.total) * 100 : 0),
        };
        files.set(rel, fc);
        totalCovered += lines.covered;
        totalLines += lines.total;
    }
    const total = data.total?.lines;
    const linePercent = total && typeof total.pct === 'number'
        ? (0, coverage_1.round1)(total.pct)
        : (0, coverage_1.round1)(totalLines > 0 ? (totalCovered / totalLines) * 100 : 0);
    return { source: 'istanbul-summary', sourceFile, linePercent, files };
}
/** Istanbul coverage-final.json: `{ "/abs/path": { s: { "0": 1, "1": 0, ... }, statementMap: {...} } }`. */
function parseIstanbulFinal(raw, sourceFile, cwd) {
    const data = JSON.parse(raw);
    const files = new Map();
    let totalCovered = 0;
    let totalStatements = 0;
    for (const [key, entry] of Object.entries(data)) {
        const s = entry?.s;
        if (!s || typeof s !== 'object')
            continue;
        const counts = Object.values(s);
        const total = counts.length;
        const covered = counts.filter((n) => typeof n === 'number' && n > 0).length;
        const rel = (0, coverage_1.toRelative)(entry.path || key, cwd);
        files.set(rel, {
            path: rel,
            covered,
            total,
            pct: (0, coverage_1.round1)(total > 0 ? (covered / total) * 100 : 0),
        });
        totalCovered += covered;
        totalStatements += total;
    }
    return {
        source: 'istanbul-final',
        sourceFile,
        linePercent: (0, coverage_1.round1)(totalStatements > 0 ? (totalCovered / totalStatements) * 100 : 0),
        files,
    };
}
/**
 * Single source of truth for the typescript pack's coverage gathering.
 * Consumed by `tsCoverageProvider` (capability dispatcher).
 */
function gatherTsCoverageResult(cwd) {
    const candidates = [
        { file: 'coverage/coverage-summary.json', parser: parseIstanbulSummary },
        { file: 'coverage/coverage-final.json', parser: parseIstanbulFinal },
    ];
    for (const c of candidates) {
        const abs = path.join(cwd, c.file);
        let raw;
        try {
            raw = fs.readFileSync(abs, 'utf-8');
        }
        catch {
            continue;
        }
        try {
            const coverage = c.parser(raw, c.file, cwd);
            return { schemaVersion: 1, tool: `coverage:${coverage.source}`, coverage };
        }
        catch {
            continue;
        }
    }
    return null;
}
/**
 * Resolve the command this repo uses to run tests with coverage (D021).
 *
 * Three-tier fallback, cheapest + most-respectful-of-project-config
 * first:
 *
 *   1. **Project-declared script** — if `package.json#scripts` defines
 *      a `test:coverage` (preferred) or `coverage` entry, use that.
 *      Honors whatever the project has chosen — pre/post hooks,
 *      monorepo workspace setup, custom flags. This is the right
 *      answer for any non-trivial codebase.
 *
 *   2. **Vitest in deps** — if `vitest` is installed, run
 *      `npx vitest run --coverage`. The `--coverage` flag triggers the
 *      configured coverage provider (typically `@vitest/coverage-v8`,
 *      which we surface as a project-local tool requirement). `run`
 *      mode is essential — without it vitest enters watch mode.
 *
 *   3. **Jest in deps** — if `jest` is installed, run
 *      `npx jest --coverage`. Istanbul-compatible output to
 *      `coverage/coverage-final.json` matches what `gatherTsCoverageResult`
 *      already reads.
 *
 * Returns null if none of the above can resolve (e.g. mocha-only
 * repos), letting the preflight surface an actionable hint instead of
 * the spawn flailing.
 */
function resolveTsCoverageCmd(cwd) {
    const pkgPath = path.join(cwd, 'package.json');
    try {
        const raw = fs.readFileSync(pkgPath, 'utf-8');
        const pkg = JSON.parse(raw);
        const scripts = pkg.scripts ?? {};
        if (typeof scripts['test:coverage'] === 'string')
            return 'npm run test:coverage';
        if (typeof scripts.coverage === 'string')
            return 'npm run coverage';
        const allDeps = {
            ...(pkg.dependencies ?? {}),
            ...(pkg.devDependencies ?? {}),
        };
        if ('vitest' in allDeps)
            return 'npx vitest run --coverage';
        if ('jest' in allDeps)
            return 'npx jest --coverage';
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Run the project's TS/JS test runner with coverage (D021).
 *
 * Preflight: require `package.json`. Without it this isn't a Node
 * project. Additionally, `resolveTsCoverageCmd` must return non-null —
 * otherwise we have no canonical command (mocha-only / ava / tap
 * projects fall through; users with those runners can add a
 * `test:coverage` script to opt in to tier 1).
 *
 * Artifact: `coverage/coverage-summary.json` is the canonical Istanbul
 * summary that `gatherTsCoverageResult` reads first. Tier-1 scripts
 * may emit elsewhere, but Istanbul's default conventions put the
 * artifact here for both vitest-v8 and jest.
 */
function runTsTestsWithCoverage(cwd) {
    return Promise.resolve((0, run_tests_helper_1.runTestsWithCoverage)({
        pack: 'typescript',
        cmd: resolveTsCoverageCmd(cwd) ?? 'npx vitest run --coverage',
        cwd,
        artifact: (cwd) => {
            for (const c of ['coverage/coverage-summary.json', 'coverage/coverage-final.json']) {
                if ((0, runner_1.fileExists)(cwd, c))
                    return c;
            }
            return null;
        },
        preflight: (cwd) => {
            if (!(0, runner_1.fileExists)(cwd, 'package.json')) {
                return 'no package.json in this directory — not a Node project';
            }
            if (resolveTsCoverageCmd(cwd) === null) {
                return 'no test:coverage script and no vitest/jest in deps — add a `test:coverage` npm script or install vitest/jest';
            }
            return null;
        },
    }));
}
const tsCoverageProvider = {
    source: 'typescript',
    async gather(cwd) {
        return gatherTsCoverageResult(cwd);
    },
    async runTests(cwd) {
        return runTsTestsWithCoverage(cwd);
    },
};
/**
 * Enumerate TS/JS source files under cwd and pre-compute the pack's
 * per-file imports (raw specifiers) and resolved edges. Uses the
 * cross-platform `walkSourceFiles` walker; `includeTests` +
 * `includeAutogen` keep the file set identical to the prior
 * `find \( -name "*.ts" … \)` enumeration, which filtered neither.
 * Exclusions come from `.gitignore` + `.dxkit-ignore` via the walker.
 *
 * Returns null when the repo has no TS/JS source, so the dispatcher
 * can skip this provider cleanly on pure Python/Go/Rust/C# trees.
 */
function gatherTsImportsResult(cwd) {
    const files = (0, walk_source_files_1.walkSourceFiles)(cwd, {
        extensions: TS_JS_EXT,
        includeTests: true,
        includeAutogen: true,
    });
    if (files.length === 0)
        return null;
    const extracted = new Map();
    const edges = new Map();
    for (const rel of files) {
        let content;
        try {
            content = fs.readFileSync(path.join(cwd, rel), 'utf-8');
        }
        catch {
            continue;
        }
        const specs = extractTsImportsRaw(content);
        extracted.set(rel, specs);
        const targets = new Set();
        for (const spec of specs) {
            const resolved = resolveTsImportRaw(rel, spec, cwd);
            if (resolved)
                targets.add(resolved);
        }
        if (targets.size > 0)
            edges.set(rel, targets);
    }
    if (extracted.size === 0)
        return null;
    return {
        schemaVersion: 1,
        tool: 'ts-imports',
        sourceExtensions: TS_JS_EXT,
        extracted,
        edges,
    };
}
const tsImportsProvider = {
    source: 'typescript',
    async gather(cwd) {
        return gatherTsImportsResult(cwd);
    },
};
/**
 * Detect the JS/TS test framework by inspecting the `scripts.test`
 * entry of `package.json` — that's the contract runners publish
 * themselves by and is cheap/deterministic. Returns null when there's
 * no package.json or no recognizable runner; 'unknown' is NOT returned
 * to the dispatcher because an 'unknown' string is worse than null
 * (the last-wins aggregate would prefer it over another pack's real
 * answer).
 */
function gatherTsTestFrameworkResult(cwd) {
    const testScript = (0, runner_1.run)("node -e \"const p=require('./package.json'); console.log(p.scripts?.test || '')\"", // slop-ok
    cwd);
    if (!testScript || testScript === 'echo "Error: no test specified" && exit 1')
        return null;
    let name = null;
    if (testScript.includes('vitest'))
        name = 'vitest';
    else if (testScript.includes('jest'))
        name = 'jest';
    else if (testScript.includes('mocha') || testScript.includes('lb-mocha'))
        name = 'mocha';
    else if (testScript.includes('ava'))
        name = 'ava';
    else if (testScript.includes('tap'))
        name = 'tap';
    if (!name)
        return null;
    return { schemaVersion: 1, tool: 'typescript', name };
}
const tsTestFrameworkProvider = {
    source: 'typescript',
    async gather(cwd) {
        return gatherTsTestFrameworkResult(cwd);
    },
};
function readTsPackageMetadata(cwd, pkgName) {
    try {
        const raw = fs.readFileSync(path.join(cwd, 'node_modules', pkgName, 'package.json'), 'utf-8');
        const parsed = JSON.parse(raw);
        let repositoryUrl;
        if (typeof parsed.repository === 'string') {
            repositoryUrl = parsed.repository;
        }
        else if (parsed.repository && typeof parsed.repository.url === 'string') {
            repositoryUrl = parsed.repository.url;
        }
        return { description: parsed.description, repositoryUrl };
    }
    catch {
        return {};
    }
}
/**
 * Canonicalise a repository URL via npm's `hosted-git-info`. Expands
 * shorthand (`user/repo`, `github:user/repo`), converts SCP-like SSH
 * (`git@github.com:user/repo.git`) to RFC (`git+ssh://...`), and emits
 * `git+https://` for standard https input — matching the format the
 * customer's benchmark xlsx records (Phase 10h.5). Preserves SSH intent
 * when the source URL is an SSH scheme; falls through unchanged for
 * non-GitHub/GitLab/Bitbucket hosts that hosted-git-info can't parse.
 */
function normalizeRepoUrl(raw) {
    if (!raw)
        return undefined;
    const info = hosted_git_info_1.default.fromUrl(raw);
    if (!info)
        return raw;
    const looksSsh = /^(git\+ssh:|ssh:|git@)/.test(raw.trim());
    return looksSsh ? info.sshurl({ noCommittish: true }) : info.https({ noCommittish: true });
}
/**
 * Split license-checker's `${name}@${version}` key, preserving a leading
 * '@' on scoped packages. Returns null on malformed keys (no '@' past
 * position 0) so the caller can skip cleanly.
 */
function splitTsLicenseCheckerKey(key) {
    const atIdx = key.lastIndexOf('@');
    if (atIdx <= 0)
        return null;
    return { package: key.slice(0, atIdx), version: key.slice(atIdx + 1) };
}
/**
 * Single source of truth for the typescript pack's license gathering.
 * Consumed by `tsLicensesProvider` (capability dispatcher).
 *
 * Returns null when the repo isn't a Node project OR the tool isn't
 * available (so the dispatcher skips the pack cleanly rather than
 * emitting an empty envelope). License-text is inlined by reading the
 * `licenseFile` path on disk — the customer's existing workflow does
 * the same thing.
 */
async function gatherTsLicensesResult(cwd) {
    if (!(0, runner_1.fileExists)(cwd, 'package.json')) {
        return { kind: 'no-manifest', reason: 'no package.json' };
    }
    // Pre-flight check before the shell-out. license-checker-rseidelsohn
    // walks `node_modules/` to harvest license metadata; on a project
    // that has `package.json` but hasn't run `npm install` (e.g. test
    // fixtures, freshly-cloned repos, CI nodes that skipped install) it
    // can hang for the full timeout window waiting on a directory that
    // never appears. Treating "no node_modules" as a separate
    // unavailable state lets the cache build return fast and keeps the
    // user-facing reason actionable ("run `npm install` first") instead
    // of generic "no output."
    if (!(0, runner_1.fileExists)(cwd, 'node_modules')) {
        return {
            kind: 'unavailable',
            reason: 'no node_modules directory; run `npm install` to populate dependencies first',
        };
    }
    const status = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS['license-checker-rseidelsohn'], cwd);
    if (!status.available || !status.path) {
        return { kind: 'unavailable', reason: 'license-checker-rseidelsohn not installed' };
    }
    const raw = (0, runner_1.run)(`${status.path} --json --excludePrivatePackages`, cwd, 120000);
    if (!raw)
        return { kind: 'unavailable', reason: 'license-checker produced no output' };
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch (err) {
        return {
            kind: 'unavailable',
            reason: `license-checker parse error: ${err.message}`,
        };
    }
    // Load the top-level index once and project it down to `isTopLevel`
    // per finding. The self-parent invariant — `index[top]` always
    // contains `top` itself, because BFS starts from each top-level —
    // is the cheapest signal; checking `parents.includes(pkg)` classifies
    // every row without a second manifest parse. Empty index (missing
    // lockfile, unparsable JSON) leaves isTopLevel unset so the bom
    // filter passes the row through rather than guessing wrong.
    const topLevelIndex = loadTsTopLevelDepIndex(cwd);
    const hasIndex = topLevelIndex.size > 0;
    const findings = [];
    for (const [key, entry] of Object.entries(data)) {
        const split = splitTsLicenseCheckerKey(key);
        if (!split)
            continue;
        const licenseType = Array.isArray(entry.licenses)
            ? entry.licenses.join(' OR ')
            : entry.licenses || 'UNKNOWN';
        let licenseText;
        if (entry.licenseFile) {
            try {
                licenseText = fs.readFileSync(entry.licenseFile, 'utf-8');
            }
            catch {
                // license file vanished between scan and read — skip silently
            }
        }
        const meta = readTsPackageMetadata(cwd, split.package);
        const parents = hasIndex ? topLevelIndex.get(split.package) : undefined;
        findings.push({
            package: split.package,
            version: split.version,
            licenseType,
            licenseText,
            // Prefer raw repository.url (byte-identical to `npm view`) over
            // license-checker's normalized form; normalise via hosted-git-info
            // to expand shorthand and canonicalise across SCP/SSH/HTTPS.
            sourceUrl: normalizeRepoUrl(meta.repositoryUrl || entry.repository || entry.url),
            description: meta.description,
            supplier: entry.publisher,
            isTopLevel: hasIndex ? (parents?.includes(split.package) ?? false) : undefined,
        });
    }
    // Populate releaseDate (xlsx col 10) from the npm registry.
    // Batched per unique package name; one HTTP call per package
    // regardless of how many versions are installed. Graceful fallback:
    // unreachable registry / unknown package leaves `releaseDate` unset.
    //
    // Hard time budget: the per-request 10s timeout + 20-way concurrency
    // are usually enough, but a flaky network or rate-limited registry
    // can push a 700-package run past the dispatcher's 720s deadline.
    // When that happens the entire licenses capability is dropped and
    // the baseline silently loses every license entry — a much worse
    // outcome than missing the optional releaseDate field. Race the
    // enrichment against a wall-clock budget; on timeout, proceed with
    // whatever the cache happened to fill in (often nothing) and let
    // releaseDate stay unset on the affected findings.
    let dateMap;
    try {
        dateMap = await Promise.race([
            (0, npm_registry_1.enrichReleaseDates)(findings.map((f) => ({ package: f.package, version: f.version }))),
            new Promise((_, reject) => {
                setTimeout(() => reject(new Error('enrichReleaseDates wall-clock budget exceeded')), 60_000);
            }),
        ]);
    }
    catch (err) {
        if (process.env.DXKIT_DEBUG_NPM_REGISTRY) {
            process.stderr.write(`[dxkit-licenses] release-date enrich aborted: ${err.message}\n`); // slop-ok
        }
        dateMap = new Map();
    }
    for (const f of findings) {
        const iso = dateMap.get(`${f.package}@${f.version}`);
        if (iso)
            f.releaseDate = iso;
    }
    const envelope = {
        schemaVersion: 1,
        tool: 'license-checker-rseidelsohn',
        findings,
    };
    return { kind: 'success', envelope };
}
const tsLicensesProvider = {
    source: 'typescript',
    async gather(cwd) {
        const outcome = await gatherTsLicensesResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
    async gatherOutcome(cwd) {
        return gatherTsLicensesResult(cwd);
    },
};
exports.typescript = {
    id: 'typescript',
    displayName: 'TypeScript / JavaScript',
    commentSyntax: { lineComment: '//', blockCommentStart: '/*', blockCommentEnd: '*/' },
    sourceExtensions: [...TS_JS_EXT],
    testFilePatterns: [
        '*.test.ts',
        '*.test.tsx',
        '*.test.js',
        '*.test.jsx',
        '*.test.mjs',
        '*.test.cjs',
        '*.spec.ts',
        '*.spec.tsx',
        '*.spec.js',
        '*.spec.jsx',
        '*.spec.mjs',
        '*.spec.cjs',
    ],
    extraExcludes: ['node_modules', 'dist', '.next', '.turbo', 'coverage', '.cache'],
    exportDetection: {
        reliability: 'full',
        strategy: 'TypeScript `export` keyword on top-level declarations + named `export { ... }` re-export blocks',
    },
    // D027 (2.4.7): JSDoc / TSDoc block comments are the conventional
    // documentation marker. Per-pack pattern replaces the pre-D027
    // hardcoded JS-shaped regex in generic.ts.
    docCommentPatterns: ['/\\*\\*'],
    // D034 (2.4.7): Node / browser TLS-bypass idioms. The env-var
    // `NODE_TLS_REJECT_UNAUTHORIZED=0` disables verification process-
    // wide; `rejectUnauthorized: false` is the per-request opt-out on
    // `https.Agent` / `tls.connect` / `node-fetch` agents.
    tlsBypassPatterns: ['NODE_TLS_REJECT_UNAUTHORIZED.*0', 'rejectUnauthorized.*false'],
    upgradeCommand(name, version) {
        return `npm install ${name}@${version}`;
    },
    // Path conventions span both backend Node frameworks (Express,
    // Loopback, NestJS, Fastify — controllers/services/repositories)
    // and the React / Next.js frontend world (components/pages/hooks).
    // Both are declared as `primaryComponentPaths` so a polyglot Node
    // backend + React frontend monorepo doesn't have to pick. The
    // `routePaths` subset narrows to HTTP-handler conventions so the
    // "Add API documentation" action stays silent on pure-React apps.
    architecturalShape: {
        primaryComponentPaths: [
            '/controllers/',
            '/handlers/',
            '/services/',
            '/repositories/',
            '/interceptors/',
            '/middleware/',
            '/components/',
            '/pages/',
            '/hooks/',
            '/screens/',
        ],
        routePaths: ['/controllers/', '/handlers/', '/routes/', '/pages/api/', '/app/api/'],
        modelPaths: ['/models/', '/entities/', '/schemas/', '/dto/', '/dtos/'],
        vocabulary: {
            components: 'controllers/components',
            models: 'models',
            routes: 'routes',
        },
        testGapPriority: {
            high: ['/controllers/', '/handlers/', '/services/'],
            medium: [
                '/interceptors/',
                '/middleware/',
                '/repositories/',
                '/components/',
                '/pages/',
                '/hooks/',
                '/screens/',
            ],
        },
    },
    clocLanguageNames: ['TypeScript', 'JavaScript', 'JSX', 'TSX'],
    detect(cwd) {
        return (0, runner_1.fileExists)(cwd, 'package.json');
    },
    mapLintSeverity: mapEslintRuleSeverity,
    tools: ['eslint', 'npm-audit', 'osv-scanner', 'vitest-coverage', 'license-checker-rseidelsohn'],
    semgrepRulesets: ['p/javascript', 'p/typescript'],
    capabilities: {
        depVulns: tsDepVulnsProvider,
        lint: tsLintProvider,
        coverage: tsCoverageProvider,
        imports: tsImportsProvider,
        testFramework: tsTestFrameworkProvider,
        licenses: tsLicensesProvider,
    },
    permissions: ['Bash(npm test:*)', 'Bash(npm run:*)', 'Bash(npx:*)'],
    defaultVersion: '20',
    versionKey: 'node',
    devcontainerFeature: {
        name: 'ghcr.io/devcontainers/features/node:1',
        // Node 22 LTS — matches the smoke workflow's `setup-node@v6` line.
        opts: { version: '22', nvmVersion: 'latest' },
    },
    devcontainerExtensions: ['dbaeumer.vscode-eslint', 'esbenp.prettier-vscode'],
    // No ruleFile — there is no `.claude/rules/typescript.md`. nextjs.md
    // is a framework rule (not language) and stays hardcoded in
    // generator.ts under `IF_NEXTJS`.
    cliBinaries: ['node', 'npm'],
};
//# sourceMappingURL=typescript.js.map