"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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultMetrics = defaultMetrics;
exports.analyzeHealthWithMetrics = analyzeHealthWithMetrics;
exports.analyzeHealth = analyzeHealth;
exports.gatherAnalysisResultBody = gatherAnalysisResultBody;
exports.scoreAndFormatHealth = scoreAndFormatHealth;
/**
 * Health analyzer orchestrator.
 *
 * 1. detect() -> what stack is this?
 * 2. Run tools -> gather metrics (deterministic)
 * 3. Score metrics -> formulas (deterministic)
 * 4. Format report -> structured JSON
 *
 * Steps 1–2 are wrapped in an `AnalysisResult` and persisted via the
 * cross-process cache (`src/analyzers/cache.ts`). Step 3 + 4 (scoring
 * + formatting) run on every call but read from the cached result;
 * two `vyuh-dxkit health` invocations on the same commit produce
 * byte-identical reports without re-shelling out to any tool.
 */
const path = __importStar(require("path"));
const detect_1 = require("../detect");
const cache_1 = require("./cache");
const generic_1 = require("./tools/generic");
const parallel_1 = require("./tools/parallel");
const lint_label_1 = require("./tools/lint-label");
const coverage_1 = require("./tools/coverage");
const package_json_1 = require("./tools/package-json");
const gather_1 = require("./quality/gather");
const timing_1 = require("./tools/timing");
const dispatcher_1 = require("./dispatcher");
const descriptors_1 = require("../languages/capabilities/descriptors");
const capabilities_1 = require("../languages/capabilities");
const gather_2 = require("./security/gather");
const gather_3 = require("./licenses/gather");
const shallow_1 = require("./tests/shallow");
const shallow_2 = require("./quality/shallow");
const shallow_3 = require("./docs/shallow");
const shallow_4 = require("./security/shallow");
const shallow_5 = require("./maintainability/shallow");
const shallow_6 = require("./dx/shallow");
const scoring_1 = require("../scoring");
/** Default values for all HealthMetrics fields. */
function defaultMetrics() {
    return {
        sourceFiles: 0,
        testFiles: 0,
        totalLines: 0,
        testsPass: null,
        testsPassing: 0,
        testsFailing: 0,
        coverageConfigExists: false,
        typeErrors: null,
        filesOver500Lines: 0,
        largestFileLines: 0,
        largestFilePath: '',
        largestFiles: [],
        consoleLogCount: 0,
        anyTypeCount: 0,
        readmeExists: false,
        readmeLines: 0,
        docCommentFiles: 0,
        apiDocsExist: false,
        architectureDocsExist: false,
        contributingExists: false,
        changelogExists: false,
        evalCount: 0,
        privateKeyFiles: 0,
        envFilesInGit: 0,
        tlsDisabledCount: 0,
        todoCount: 0,
        fixmeCount: 0,
        hackCount: 0,
        staleFiles: 0,
        mixedLanguages: false,
        commentRatio: null,
        controllers: 0,
        models: 0,
        routeHandlerFiles: 0,
        directories: 0,
        languages: [],
        nodeEngineVersion: null,
        ciConfigCount: 0,
        dockerConfigCount: 0,
        precommitConfigCount: 0,
        makefileExists: false,
        envExampleExists: false,
        npmScriptsCount: 0,
        toolsUsed: [],
        toolsUnavailable: [],
        clocLanguages: null,
    };
}
/**
 * Run a full health analysis on a repository, returning both the summary
 * report and the underlying metrics. Used by --detailed to feed the
 * remediation planner without re-running the tool gather.
 */
async function analyzeHealthWithMetrics(repoPath, options = {}) {
    return analyzeHealthInternal(repoPath, options);
}
/** Run a full health analysis on a repository. */
async function analyzeHealth(repoPath, options = {}) {
    return (await analyzeHealthInternal(repoPath, options)).report;
}
async function analyzeHealthInternal(repoPath, options = {}) {
    const result = await (0, cache_1.readOrBuildAnalysisResult)({
        cwd: repoPath,
        build: (cwd) => gatherAnalysisResultBody(cwd, options),
    });
    const report = scoreAndFormatHealth(result);
    return { report, metrics: result.metrics };
}
/**
 * The gather pipeline — everything that produces an `AnalysisResultBody`
 * for a given repo. Pure with respect to the working tree: same SHA +
 * same `.dxkit-ignore` + same dxkit version means same output. Scoring
 * and formatting are deliberately NOT part of this — they live in
 * `scoreAndFormatHealth` so cache hits skip the tool gather entirely
 * but still re-render the report (cheap, deterministic, decoupled from
 * I/O).
 *
 * Called by the cache layer (`readOrBuildAnalysisResult`) on a miss.
 * Exported so other analyzers can drop into the same pattern in
 * subsequent migrations: each subcommand asks the cache for a
 * `AnalysisResult`, builder is supplied as `gatherAnalysisResultBody`,
 * and the subcommand renders its own report from the body.
 */
async function gatherAnalysisResultBody(repoPath, options = {}) {
    const verbose = !!options.verbose;
    // Step 1: Detect stack
    const stack = (0, timing_1.timed)('detect', verbose, () => (0, detect_1.detect)(repoPath));
    // Step 2: Gather metrics -- generic first, then language-specific, then optional.
    // Active-pack flags from `stack` drive the architecturalShape-aware
    // path counters (primary components, route handlers, data models).
    const generic = (0, timing_1.timed)('generic (Layer 0)', verbose, () => (0, generic_1.gatherGenericMetrics)(repoPath, stack.languages));
    const metrics = { ...defaultMetrics(), ...generic };
    // `package.json` metrics: npm-script count + `engines.node` pin. These
    // don't fit any capability (Node-specific by nature) and used to live
    // in the typescript pack's `gatherMetrics` body; extracted later into
    // a direct helper so they survive the per-pack channel deletion.
    const pkg = (0, timing_1.timed)('package.json', verbose, () => (0, package_json_1.gatherPackageJsonMetrics)(repoPath));
    metrics.npmScriptsCount = pkg.npmScriptsCount;
    metrics.nodeEngineVersion = pkg.nodeEngineVersion;
    // Layer 2: Optional enhanced tools — cloc line counts, gitleaks secret
    // counts, graphify AST stats. Reshaped from the dispatcher's cached
    // envelopes (`tools/parallel.ts`), so each tool shells out at most once
    // per analyzer run.
    const layer2 = await (0, timing_1.timedAsync)('layer2 (parallel)', verbose, () => (0, parallel_1.gatherLayer2Parallel)(repoPath, verbose));
    mergeLayer2(metrics, layer2);
    // Hygiene + comment-ratio metrics shared with the standalone Quality
    // report. Live in HealthMetrics so the canonical Quality scorer
    // sees the same values from both consumer paths — closes the
    // dual-Quality-formula drift class structurally. The standalone
    // analyzeQuality reads these straight off the cached AnalysisResult.
    const hygiene = (0, timing_1.timed)('hygiene (grep)', verbose, () => (0, gather_1.gatherHygieneMarkers)(repoPath));
    metrics.todoCount = hygiene.todoCount;
    metrics.fixmeCount = hygiene.fixmeCount;
    metrics.hackCount = hygiene.hackCount;
    // hygiene.consoleLogCount is intentionally NOT mirrored — Layer 2
    // already populates metrics.consoleLogCount via the shared
    // gatherDebugStatements helper, and both paths converge on the
    // same number by construction.
    metrics.staleFiles = hygiene.staleFiles.length;
    metrics.mixedLanguages = hygiene.mixedLanguages;
    const comments = (0, timing_1.timed)('cloc (comment ratio)', verbose, () => (0, gather_1.gatherCommentRatio)(repoPath));
    metrics.commentRatio = comments.ratio;
    // Surface the coverage tool name in `toolsUsed` even though its data
    // lives under `capabilities.coverage`. `loadCoverage` and the COVERAGE
    // dispatcher share the same underlying providers — the call here is
    // served from the dispatcher cache when `gatherCapabilityReport` runs.
    const coverage = await (0, timing_1.timedAsync)('coverage', verbose, () => (0, coverage_1.loadCoverage)(repoPath));
    if (coverage) {
        metrics.toolsUsed.push(`coverage:${coverage.source}`);
    }
    // Language breakdown -- prefer cloc data, fall back to detection
    metrics.languages = metrics.clocLanguages
        ? metrics.clocLanguages.map((l) => ({
            name: l.language,
            files: l.files,
            lines: l.code,
            percentage: 0, // computed below
        }))
        : buildLanguageBreakdown(stack);
    // Compute percentages from line counts
    const totalCodeLines = metrics.languages.reduce((sum, l) => sum + l.lines, 0);
    if (totalCodeLines > 0) {
        for (const lang of metrics.languages) {
            lang.percentage = Math.round((lang.lines / totalCodeLines) * 100);
        }
    }
    // Capability envelopes alongside legacy metrics. Dispatched in parallel;
    // providers the legacy path already ran are served from the dispatcher
    // cache (free). Scorers read capability-owned fields from this bundle.
    const capabilities = await (0, timing_1.timedAsync)('capabilities', verbose, () => gatherCapabilityReport(repoPath));
    // Synthesize per-pack tool names (eslint, npm-audit, ruff, pip-audit,
    // clippy, cargo-audit, golangci-lint, govulncheck, dotnet-format,
    // dotnet-vulnerable) into `metrics.toolsUsed` from the LINT + DEP_VULNS
    // envelopes. Sourced from the dispatcher's already-computed `tool` field
    // rather than each pack's gather body.
    for (const name of toolsFromCapabilities(capabilities)) {
        if (!metrics.toolsUsed.includes(name))
            metrics.toolsUsed.push(name);
    }
    // Push attempted-but-failed capabilities into metrics.toolsUnavailable
    // so consumers reading the report see the failure honestly rather
    // than as silent absence. Reads from the *Availability sibling
    // fields populated above. The reason text is consumer-renderable;
    // keep it concise (~one short sentence).
    pushUnavailable(metrics, capabilities.codePatternsAvailability, 'semgrep');
    pushUnavailable(metrics, capabilities.duplicationAvailability, 'jscpd');
    pushUnavailable(metrics, capabilities.structuralAvailability, 'graphify');
    pushUnavailable(metrics, capabilities.lintAvailability, 'lint');
    return { stack, capabilities, metrics };
}
/**
 * Append a `<toolName> (<reason>)` entry to `metrics.toolsUnavailable`
 * when an availability sibling reports the capability was attempted
 * but failed. No-ops when the capability is fully clean (available:
 * true) or when the sibling is absent. Deduplicates against any
 * entry already pushed by Layer 2's parallel.ts (which renders
 * gitleaks / graphify failures into the same field).
 */
function pushUnavailable(metrics, avail, toolName) {
    if (!avail || avail.available)
        return;
    const entry = avail.unavailableReason ? `${toolName} (${avail.unavailableReason})` : toolName;
    if (!metrics.toolsUnavailable.some((e) => e === entry || e.startsWith(`${toolName} (`))) {
        metrics.toolsUnavailable.push(entry);
    }
}
/**
 * Score the six dimensions and format the result into a `HealthReport`.
 * Pure function over a cached `AnalysisResult` — no I/O, no tool
 * shell-outs. Provenance fields (commitSha, branch, analyzedAt) come
 * directly from the cached result, which means two `health` calls on
 * the same commit produce reports with the SAME `analyzedAt`
 * timestamp: it's "when this gather was built," not "when this read
 * happened." That's the cross-process-consistency contract.
 */
function scoreAndFormatHealth(result) {
    const { stack, capabilities, metrics } = result;
    const scoreInput = { metrics, capabilities, languageFlags: stack.languages };
    const dimensions = {
        testing: (0, shallow_1.scoreTestsDimension)(scoreInput),
        quality: (0, shallow_2.scoreQualityDimension)(scoreInput),
        documentation: (0, shallow_3.scoreDocsDimension)(scoreInput),
        security: (0, shallow_4.scoreSecurityDimension)(scoreInput),
        maintainability: (0, shallow_5.scoreMaintainabilityDimension)(scoreInput),
        developerExperience: (0, shallow_6.scoreDxDimension)(scoreInput),
    };
    const { overallScore, rating } = (0, scoring_1.computeOverall)(dimensions);
    return {
        repo: stack.projectName || path.basename(result.cwd),
        analyzedAt: result.builtAt,
        commitSha: result.commitSha,
        branch: result.branch,
        summary: { overallScore, rating },
        dimensions,
        languages: metrics.languages,
        largestFiles: metrics.largestFiles,
        toolsUsed: metrics.toolsUsed,
        toolsUnavailable: metrics.toolsUnavailable,
        capabilities,
    };
}
/**
 * Extract user-facing tool names from every capability envelope that
 * represents a real external scanner. `metrics.toolsUsed` ends up as a
 * complete mirror of what actually ran for the `health` report —
 * external tools (lint, depVulns, secrets, codePatterns, duplication,
 * structural) all contribute. Pseudo-tool envelopes (`imports.tool =
 * 'ts-imports'`, `testFramework.tool = 'typescript'`) intentionally
 * stay out — those are language-pack identifiers, not external tools
 * that could appear in `vyuh-dxkit tools`.
 *
 * Splits comma-joined names the descriptor aggregate produces (e.g.
 * `"ruff, eslint, golangci-lint"`) back into individual entries. Also
 * includes `osv.dev` when depVulns enrichment used it. The caller
 * dedupes against entries already pushed by Layer 2 (`tools/parallel.ts`
 * contributes `gitleaks` / `graphify` on the success path + reason
 * strings on failure to `toolsUnavailable`).
 */
function toolsFromCapabilities(caps) {
    const names = [];
    if (caps.lint)
        names.push(...splitToolNames(caps.lint.tool));
    if (caps.depVulns) {
        names.push(...splitToolNames(caps.depVulns.tool));
        if (caps.depVulns.enrichment === 'osv.dev')
            names.push('osv.dev');
    }
    if (caps.secrets)
        names.push(...splitToolNames(caps.secrets.tool));
    if (caps.codePatterns)
        names.push(...splitToolNames(caps.codePatterns.tool));
    if (caps.duplication)
        names.push(...splitToolNames(caps.duplication.tool));
    if (caps.structural)
        names.push(...splitToolNames(caps.structural.tool));
    return names;
}
function splitToolNames(tool) {
    // The lint label may carry a `(not run: <pack> — <reason>[, ...])`
    // parenthetical (built by the lint augmentation block earlier in
    // this file). The Tools-used footer wants only the actual tool that
    // ran — leaving the parenthetical in makes the footer row read as
    // "ruff didn't run because of typescript" and also breaks the
    // comma-split here when multiple packs are skipped (internal commas
    // inside the parenthetical get treated as tool boundaries). Strip
    // it before splitting. The not-run packs still surface via the
    // dedicated Lint coverage gap row downstream.
    return (0, lint_label_1.stripNotRunSuffix)(tool)
        .split(',')
        .map((s) => s.trim())
        .filter((s) => s.length > 0);
}
/**
 * Dispatch all 9 capabilities and bundle the non-null envelopes into a
 * `CapabilityReport`. Each capability resolves a different provider set
 * (per-pack capabilities union the active language packs; global
 * capabilities hit a single registered provider). Dispatches run in
 * parallel — the descriptor aggregates are pure, and the dispatcher
 * isolates provider failures.
 */
async function gatherCapabilityReport(cwd) {
    // D025b (2.4.7): depVulns gathers via `gatherDepVulnsWithAvailability`
    // (NOT the dispatcher) so the per-pack availability discriminant
    // survives to the health-side scorer. Health doesn't need the
    // enrichment passes (EPSS/KEV/reachability/risk) that the standalone
    // vuln scan does — those run on the standalone path inside
    // `gatherDepVulns`. The shared primitive returns the same
    // `DepVulnResult` envelope shape the dispatcher would produce, plus
    // the availability metadata.
    const [depVulnsWithAvail, lintOutcome, coverage, imports, testFramework, secrets, codePatternsOutcome, duplicationOutcome, structuralOutcome, licensesWithAvail,] = await Promise.all([
        (0, gather_2.gatherDepVulnsWithAvailability)(cwd),
        // gatherWithProvenance (not gather) so the cached LintResult.tool
        // can carry the "(not run: <packs>)" suffix when one of the
        // active packs returned null silently. Standalone analyzeQuality
        // reads the augmented label off the cached envelope — closes the
        // cross-process drift class where two surfaces could disagree on
        // whether a linter ran on a given pack.
        dispatcher_1.defaultDispatcher.gatherWithProvenance(cwd, descriptors_1.LINT, (0, capabilities_1.providersFor)(descriptors_1.LINT, cwd)),
        dispatcher_1.defaultDispatcher.gather(cwd, descriptors_1.COVERAGE, (0, capabilities_1.providersFor)(descriptors_1.COVERAGE, cwd)),
        dispatcher_1.defaultDispatcher.gather(cwd, descriptors_1.IMPORTS, (0, capabilities_1.providersFor)(descriptors_1.IMPORTS, cwd)),
        dispatcher_1.defaultDispatcher.gather(cwd, descriptors_1.TEST_FRAMEWORK, (0, capabilities_1.providersFor)(descriptors_1.TEST_FRAMEWORK, cwd)),
        dispatcher_1.defaultDispatcher.gather(cwd, descriptors_1.SECRETS, (0, capabilities_1.providersFor)(descriptors_1.SECRETS, cwd)),
        // gatherWithProvenance so the cache builder can plumb per-capability
        // availability metadata for tools that may legitimately gather
        // attempted-but-failed (semgrep / jscpd / graphify under resource
        // contention; tool not installed; etc.). Without this the cache's
        // metrics.toolsUnavailable can't distinguish "tool didn't try" from
        // "tool tried and failed silently" — the same dishonest-rendering
        // class as the lint case the LINT switch above closes.
        dispatcher_1.defaultDispatcher.gatherWithProvenance(cwd, descriptors_1.CODE_PATTERNS, (0, capabilities_1.providersFor)(descriptors_1.CODE_PATTERNS, cwd)),
        dispatcher_1.defaultDispatcher.gatherWithProvenance(cwd, descriptors_1.DUPLICATION, (0, capabilities_1.providersFor)(descriptors_1.DUPLICATION, cwd)),
        dispatcher_1.defaultDispatcher.gatherWithProvenance(cwd, descriptors_1.STRUCTURAL, (0, capabilities_1.providersFor)(descriptors_1.STRUCTURAL, cwd)),
        (0, gather_3.gatherLicensesWithAvailability)(cwd),
    ]);
    const codePatterns = codePatternsOutcome.envelope;
    const duplication = duplicationOutcome.envelope;
    const structural = structuralOutcome.envelope;
    const report = {};
    if (depVulnsWithAvail.envelope)
        report.depVulns = depVulnsWithAvail.envelope;
    // Always plumb availability — even when envelope is null, the bool
    // disambiguates "no active pack" (available=true, no cap) from
    // "active pack returned unavailable" (available=false, cap fires).
    report.depVulnsAvailability = {
        available: depVulnsWithAvail.available,
        unavailableReason: depVulnsWithAvail.unavailableReason,
    };
    // Augment the lint envelope's tool label with skipped-pack
    // provenance before caching, so consumers see the same label
    // analyzeQuality's old standalone gather produced. Reconstructed
    // (not mutated) because envelope.tool is readonly.
    //
    // When `skipReasons` carries a per-pack reason (the typescript +
    // python lint providers expose `gatherOutcome` for exactly this),
    // the label includes it so customers can act on the disclosure
    // instead of being told "typescript lint not run" with no
    // explanation. Example post-fix label on a platform-style polyglot:
    //   `ruff (not run: typescript — no eslint config found)`
    const lint = (() => {
        if (!lintOutcome.envelope || lintOutcome.skipped.length === 0) {
            return lintOutcome.envelope;
        }
        const annotated = lintOutcome.skipped
            .map((src) => {
            const reason = lintOutcome.skipReasons[src];
            return reason ? `${src} — ${reason}` : src;
        })
            .join(', ');
        return {
            ...lintOutcome.envelope,
            tool: `${lintOutcome.envelope.tool} (not run: ${annotated})`,
        };
    })();
    if (lint)
        report.lint = lint;
    // Lint availability so consumers can distinguish "no active
    // lint-capable pack" (vacuous clean, available=true, no envelope)
    // from "every active provider returned null" (available=false —
    // actionable "try installing dependencies"). Same shape as
    // licensesAvailability + depVulnsAvailability.
    if (lintOutcome.attempted.length > 0 && lintOutcome.succeeded.length === 0) {
        // Every attempted provider returned null — surface the per-pack
        // reasons (when available via `gatherOutcome`) so the customer
        // sees what to fix, not just which packs gave up.
        const annotated = lintOutcome.attempted
            .map((src) => {
            const reason = lintOutcome.skipReasons[src];
            return reason ? `${src} — ${reason}` : src;
        })
            .join('; ');
        const pluralS = lintOutcome.attempted.length === 1 ? '' : 's';
        report.lintAvailability = {
            available: false,
            unavailableReason: `provider${pluralS} returned no data: ${annotated}`,
        };
    }
    else {
        report.lintAvailability = { available: true, unavailableReason: '' };
    }
    if (coverage)
        report.coverage = coverage;
    if (imports)
        report.imports = imports;
    if (testFramework)
        report.testFramework = testFramework;
    if (secrets)
        report.secrets = secrets;
    if (codePatterns)
        report.codePatterns = codePatterns;
    // Same shape as lintAvailability — distinguishes "no rulesets
    // active" (vacuous clean) from "semgrep/jscpd/graphify was
    // attempted but every provider returned null" (actionable —
    // the customer's report shouldn't silently miss the tool).
    report.codePatternsAvailability = availabilityFromOutcome(codePatternsOutcome, 'semgrep');
    if (duplication)
        report.duplication = duplication;
    report.duplicationAvailability = availabilityFromOutcome(duplicationOutcome, 'jscpd');
    if (structural)
        report.structural = structural;
    report.structuralAvailability = availabilityFromOutcome(structuralOutcome, 'graphify');
    if (licensesWithAvail.envelope)
        report.licenses = licensesWithAvail.envelope;
    // Always plumb availability — even when envelope is null, the bool
    // disambiguates "no active pack with a licenses provider" (vacuous
    // success) from "active pack returned unavailable" (banner fires).
    report.licensesAvailability = {
        available: licensesWithAvail.available,
        unavailableReason: licensesWithAvail.unavailableReason,
    };
    // G_v4_8 (C1.3): build the canonical aggregate from everything we
    // just gathered, plus the two security finders not represented in
    // the capability layer (tls-bypass-registry walk, file findings).
    // Stored on the CapabilityReport so dimension scorers — currently
    // `security/shallow.ts` — read the SAME aggregate the standalone
    // vuln-scan reads. Closes the D086 class of cross-consumer drift
    // by construction.
    report.securityAggregate = await (0, gather_2.buildSecurityAggregateForHealth)(cwd, secrets ?? undefined, codePatterns ?? undefined, depVulnsWithAvail.envelope ?? undefined, depVulnsWithAvail.available, depVulnsWithAvail.unavailableReason);
    return report;
}
/**
 * Derive `<cap>Availability` metadata from a gatherWithProvenance
 * outcome. Mirrors the depVulnsAvailability / licensesAvailability
 * pattern: `available === false` only when at least one provider was
 * attempted AND none succeeded (i.e. tried + failed silently).
 * Empty `attempted` list reports available: true (vacuous — nothing
 * to try, nothing to fail).
 */
function availabilityFromOutcome(outcome, _toolLabel) {
    if (outcome.attempted.length === 0) {
        return { available: true, unavailableReason: '' };
    }
    if (outcome.succeeded.length > 0) {
        return { available: true, unavailableReason: '' };
    }
    // Prefer the per-source reason from the dispatcher's skipReasons map
    // when present — that channel carries the tool's actual failure mode
    // (`not installed` / `timed out at 600s` / `exit code 137 (stderr: ...)`
    // / `no output` / `parse error`). Falls back to the generic prose for
    // legacy providers without a `gatherOutcome` method, which collapse
    // every failure to "null returned from gather()" with no reason text.
    //
    // The reason intentionally omits the tool name — pushUnavailable
    // prepends it as `<tool> (<reason>)`, so duplicating the name here
    // would render as `jscpd (jscpd attempted ...)`.
    const skipReasons = outcome.skipReasons ?? {};
    const firstSkipWithReason = outcome.skipped.find((s) => skipReasons[s]);
    if (firstSkipWithReason) {
        return {
            available: false,
            unavailableReason: skipReasons[firstSkipWithReason],
        };
    }
    return {
        available: false,
        unavailableReason: 'attempted but produced no output (likely killed by resource limits — try running dxkit on this repo alone)',
    };
}
/**
 * Merge `gatherLayer2Parallel` output (cloc + gitleaks + graphify reshape)
 * into the accumulator. Layer 2 only writes non-capability fields and
 * array-valued `toolsUsed` / `toolsUnavailable`; no depVuln* or depAuditTool
 * aggregation is needed here since those live under `capabilities.depVulns`.
 * Null / undefined values pass through unchanged so Layer 2 can signal
 * "no data" per field.
 */
function mergeLayer2(base, overlay) {
    for (const key of Object.keys(overlay)) {
        const value = overlay[key];
        if (value === undefined || value === null)
            continue;
        if (key === 'toolsUsed' && Array.isArray(value)) {
            base.toolsUsed.push(...value);
        }
        else if (key === 'toolsUnavailable' && Array.isArray(value)) {
            base.toolsUnavailable.push(...value);
        }
        else {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            base[key] = value;
        }
    }
}
/** Build language breakdown from detection results (fallback when cloc unavailable). */
function buildLanguageBreakdown(stack) {
    const langs = [];
    const detected = Object.entries(stack.languages).filter(([, v]) => v);
    for (const [name] of detected) {
        langs.push({ name, files: 0, lines: 0, percentage: 0 });
    }
    if (langs.length === 1) {
        langs[0].percentage = 100;
    }
    else if (langs.length > 1) {
        const pct = Math.round(100 / langs.length);
        for (const lang of langs) {
            lang.percentage = pct;
        }
    }
    return langs;
}
//# sourceMappingURL=health.js.map