"use strict";
/**
 * jscpd integration — duplicate-code detection.
 *
 * The `jscpdProvider` is registered in `GLOBAL_CAPABILITIES.duplication`
 * (src/languages/capabilities/global.ts). jscpd runs once per repo with
 * a cross-language source-file pattern and respects the project's
 * `.gitignore` via the `--gitignore` flag — crucial on large repos,
 * jscpd without it walks into `node_modules` and OOMs.
 *
 * The source-file pattern is the union of every pack's
 * `sourceExtensions` (LP-recipe pattern, Phase 10i.0-LP). jscpd's
 * tokenizer is language-aware so a single union glob is safe; pack-
 * driven derivation keeps adding a new language to a one-line scaffold
 * change rather than a cross-cutting 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.jscpdProvider = void 0;
exports.gatherJscpdResult = gatherJscpdResult;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const languages_1 = require("../../languages");
const exclusions_1 = require("./exclusions");
const runner_1 = require("./runner");
const tool_registry_1 = require("./tool-registry");
/**
 * Union of source extensions across all language packs, derived
 * pack-side. Iterates LANGUAGES fresh on every call so a new pack lands
 * in the glob without an edit here (matches `allSourceExtensions`'s
 * pattern from Phase 10i.0-LP.3). A union pattern (rather than per-pack
 * invocation) so mixed-stack repos pay for one jscpd run, not N.
 *
 * Note: returns a function so the registry mutation in
 * `test/recipe-playbook.test.ts` (which appends a synthetic pack to
 * LANGUAGES) takes effect — module-load capture would freeze the union
 * before the test injection.
 */
function buildJscpdPattern() {
    const exts = new Set();
    for (const lang of languages_1.LANGUAGES) {
        for (const e of lang.sourceExtensions) {
            exts.add(e.replace(/^\./, ''));
        }
    }
    return `**/*.{${[...exts].sort().join(',')}}`;
}
/** Extract the top N clone pairs sorted largest-first. */
function topClonesFrom(duplicates, limit = 15) {
    return duplicates
        .filter((d) => d.firstFile?.name && d.secondFile?.name && d.lines)
        .map((d) => ({
        lines: d.lines || 0,
        tokens: d.tokens || 0,
        a: {
            file: d.firstFile.name,
            startLine: d.firstFile.start || 0,
            endLine: d.firstFile.end || 0,
        },
        b: {
            file: d.secondFile.name,
            startLine: d.secondFile.start || 0,
            endLine: d.secondFile.end || 0,
        },
    }))
        .sort((x, y) => y.lines - x.lines)
        .slice(0, limit);
}
/**
 * Single source of truth for the jscpd invocation. Consumed by
 * `jscpdProvider` (capability dispatcher).
 *
 * Failure-mode honesty: when jscpd doesn't produce a parseable
 * report, the returned `reason` distinguishes timeout, non-zero
 * exit (with first stderr line), or the rare true "no output"
 * case. Same shape as the semgrep gather — switched from execSync
 * to spawn-with-process-group so jscpd's worker pool (it splits
 * the scan across multiple Node workers internally) isn't killed
 * mid-run when execSync's wall-clock timer fires.
 */
async function gatherJscpdResult(cwd) {
    const status = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS.jscpd, cwd);
    if (!status.available || !status.path)
        return { kind: 'unavailable', reason: 'not installed' };
    const reportDir = path.join(os.tmpdir(), `dxkit-jscpd-${Date.now()}`);
    const pattern = buildJscpdPattern();
    // jscpd's `--ignore` receives the union of:
    //
    //   1. dxkit's centralized exclusion set (`getJscpdIgnorePatterns`) —
    //      the same dirs / sourcePaths / filePatterns the in-process
    //      walkers (cloc, grep, semgrep, graphify's Python filter)
    //      honor. Without this, committed-vendored trees that aren't
    //      listed in the project's `.gitignore` (the `--gitignore` flag's
    //      only input) — minified bundles, hash-versioned webpack
    //      chunks, vendored library copies under `public/` — would
    //      force jscpd to tokenize them, exhaust heap, and OOM-kill
    //      before flushing its JSON report. The report would then read
    //      "Duplication unavailable" on the densest repos.
    //
    //   2. Pack-declared autogen patterns (`*.Designer.cs`, WCF
    //      `Reference.cs`, MSBuild `*.AssemblyInfo.cs`, etc.) so
    //      duplication detection skips the same files generic.ts +
    //      test-gaps' source walk already skip. Autogen scaffolding
    //      duplicates verbatim by its nature; including it inflates
    //      the duplication percentage and points "extract this" advice
    //      at code the developer never authored.
    //
    // Patterns get a `**/` prefix so they match at any directory depth.
    const exclusionIgnore = (0, exclusions_1.getJscpdIgnorePatterns)(cwd);
    const autogenIgnore = (0, languages_1.allAutogenSourcePatterns)().map((p) => `**/${p}`);
    const ignorePatterns = [...exclusionIgnore, ...autogenIgnore];
    const args = ['--reporters', 'json', '--output', reportDir, '--gitignore', '--pattern', pattern];
    if (ignorePatterns.length > 0) {
        args.push('--ignore', ignorePatterns.join(','));
    }
    args.push('--min-lines', '5', '--min-tokens', '50', cwd);
    const outcome = await (0, runner_1.runDetached)(status.path, args, { cwd, timeoutMs: 600000 });
    // Read the report file directly. Pre-D-fix this used
    // `run('cat <path>')` which routed through execSync with the default
    // 1MB maxBuffer — jscpd reports on enterprise codebases routinely
    // exceed that (the .NET WinForms benchmark's was 25MB / 395k lines), causing execSync
    // to truncate the output to empty and the gather to misreport
    // jscpd as "unavailable" even after a fully-successful run.
    // Direct file read sidesteps the buffer entirely.
    const reportPath = path.join(reportDir, 'jscpd-report.json');
    let reportRaw;
    try {
        reportRaw = fs.readFileSync(reportPath, 'utf-8');
    }
    catch {
        reportRaw = '';
    }
    try {
        fs.rmSync(reportDir, { recursive: true, force: true });
    }
    catch {
        /* dir already gone or never written — fine */
    }
    if (!reportRaw) {
        if (outcome.timedOut) {
            return {
                kind: 'unavailable',
                reason: 'timed out at 600s (try narrowing scan scope via .dxkit-ignore)',
            };
        }
        const stderrFirstLine = outcome.stderr
            .split('\n')
            .map((l) => l.trim())
            .find((l) => l.length > 0);
        if (outcome.code !== 0 && outcome.code !== null) {
            const ctx = stderrFirstLine ? ` (stderr: ${stderrFirstLine})` : '';
            return { kind: 'unavailable', reason: `exit code ${outcome.code}${ctx}` };
        }
        if (stderrFirstLine) {
            return { kind: 'unavailable', reason: `no output (stderr: ${stderrFirstLine})` };
        }
        return { kind: 'unavailable', reason: 'no output' };
    }
    let data;
    try {
        data = JSON.parse(reportRaw);
    }
    catch {
        return { kind: 'unavailable', reason: 'parse error' };
    }
    const t = data.statistics?.total;
    if (!t)
        return { kind: 'unavailable', reason: 'no total stats' };
    const duplicates = data.duplicates || [];
    const envelope = {
        schemaVersion: 1,
        tool: 'jscpd',
        totalLines: t.lines,
        duplicatedLines: t.duplicatedLines,
        percentage: Math.round(t.percentage * 100) / 100,
        cloneCount: duplicates.length,
        topClones: topClonesFrom(duplicates),
    };
    return { kind: 'success', envelope };
}
/**
 * Capability-shaped provider. Registered in
 * `src/languages/capabilities/global.ts:GLOBAL_CAPABILITIES.duplication`.
 */
// Implements the optional `gatherOutcome` channel the dispatcher reads
// to populate `DispatchOutcome.skipReasons`. Without it, a failed jscpd
// run collapses to `null` at the gather boundary and the actual failure
// reason ("not installed" / "timed out at 600s" / "exit code 137" /
// "no output" / "parse error") is dropped — `availabilityFromOutcome`
// in health.ts then synthesizes generic prose that conflates
// install-missing with attempted-but-failed. Exposing the real outcome
// here lets the report show why jscpd didn't contribute, in jscpd's
// own words.
exports.jscpdProvider = {
    source: 'jscpd',
    async gather(cwd) {
        const outcome = await gatherJscpdResult(cwd);
        return outcome.kind === 'success' ? outcome.envelope : null;
    },
    async gatherOutcome(cwd) {
        return gatherJscpdResult(cwd);
    },
};
//# sourceMappingURL=jscpd.js.map