"use strict";
/**
 * `vyuh-dxkit context <query>` — the token-reduction surface. Returns
 * a slim, relevance-ranked structural slice for a query instead of the
 * full-file dump an agent would otherwise grep + read. Same payload
 * serves the human at a terminal and the LLM via the PreToolUse hook.
 *
 * Output is lean markdown by default (LLMs parse it natively + it's
 * more token-efficient than JSON); `--json` emits the stable envelope
 * for pipelines. The content is anchor-first ("start here"), grouped
 * by community for orientation, framed by blast radius, and honest
 * about truncation + same-name conflation.
 */
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.runContext = runContext;
const node_fs_1 = require("node:fs");
const path = __importStar(require("node:path"));
const queries_1 = require("../queries");
const format_1 = require("../format");
const source_slice_1 = require("../source-slice");
const languages_1 = require("../../languages");
const DEFAULT_BUDGET = 2000;
/**
 * `<path>:<line>` form detector. A symbol query virtually never ends in
 * `:digits`, so this cleanly separates the location surface
 * (`context src/a.ts:42`) from the keyword surface (`context auth`).
 */
const FILE_LINE_RE = /^(.+):(\d+)$/;
function runContext(graph, positionals, values, cwd) {
    const arg = positionals[0];
    if (!arg) {
        process.stderr.write('Usage: vyuh-dxkit context <query|file:line> [--substring] [--budget 2000] [--depth N] [--json]\n');
        process.exit(1);
    }
    // Route the `file:line` form to the source-slice surface; everything
    // else is a keyword query.
    const m = FILE_LINE_RE.exec(arg);
    if (m) {
        runFileLineContext(graph, m[1], parseInt(m[2], 10), values, cwd);
        return;
    }
    const keyword = arg;
    const budget = parsePositiveInt(values.budget, DEFAULT_BUDGET);
    const depth = values.depth !== undefined ? parsePositiveInt(values.depth, Infinity) : undefined;
    const substring = !!values.substring;
    const result = (0, queries_1.contextQuery)(graph, keyword, { budget, substring, maxDepth: depth });
    if (values.json) {
        (0, format_1.printJson)((0, format_1.envelope)('context', { query: keyword, budget, substring, depth: depth ?? null }, graph, result));
        return;
    }
    const sections = [(0, format_1.markdownHeader)('Context', `\`${keyword}\``, graph)];
    if (!result.matched) {
        if (result.suggestions.length > 0) {
            const lines = result.suggestions
                .map((s) => `  - \`${s.key}\` (${s.hits} hit${s.hits === 1 ? '' : 's'})`)
                .join('\n');
            sections.push(`No symbols matched \`${keyword}\`. Did you mean:\n\n${lines}\n\nRerun with \`--substring\` to expand from these, or pick one above.`);
        }
        else {
            sections.push(`No symbols matched \`${keyword}\` (no close alternatives either). Try a different keyword or \`--substring\` for broader matching.`);
        }
        (0, format_1.printMarkdown)(...sections);
        return;
    }
    // Anchor — the "start here" line.
    if (result.anchor) {
        const a = result.anchor;
        const where = a.line ? `${a.sourceFile}:${a.line}` : a.sourceFile;
        sections.push(`**Start here:** \`${a.symbol}\` — \`${where}\` (called from ${a.calledFrom} place${a.calledFrom === 1 ? '' : 's'}).`);
    }
    // Blast radius — change-impact framing.
    const { callers, callerFiles } = result.blastRadius;
    if (callers > 0) {
        sections.push(`**Blast radius:** changing the matched symbol${result.selection.some((s) => s.hop === 0) ? '(s)' : ''} touches ${callers} caller${callers === 1 ? '' : 's'} across ${callerFiles} file${callerFiles === 1 ? '' : 's'}.`);
    }
    else {
        sections.push('**Blast radius:** no internal callers — likely an entry point or public API.');
    }
    // Community-grouped symbol listing (orientation). Each group is
    // ranked seeds-first then by call in-degree, and capped for
    // readability — the markdown is a scannable map, not an exhaustive
    // dump. The full selection is always available via `--json`.
    const PER_COMMUNITY_CAP = 12;
    for (const group of result.byCommunity) {
        const heading = group.communityId !== undefined
            ? `### ${group.role} (community ${group.communityId})`
            : `### ${group.role}`;
        const lines = [heading];
        const groupSel = dedupeBySymbol(result.selection
            .filter((s) => group.symbols.includes(s.symbol))
            .sort((a, b) => a.hop - b.hop || b.callsIn - a.callsIn));
        for (const s of groupSel.slice(0, PER_COMMUNITY_CAP)) {
            const where = s.line ? `${s.sourceFile}:${s.line}` : s.sourceFile;
            const seed = s.hop === 0 ? ' _[seed]_' : '';
            lines.push(`- \`${s.symbol}\` — \`${where}\` (${s.callsIn} in / ${s.callsOut} out)${seed}`);
        }
        if (groupSel.length > PER_COMMUNITY_CAP) {
            lines.push(`- _+${groupSel.length - PER_COMMUNITY_CAP} more in this cluster_`);
        }
        sections.push(lines.join('\n'));
    }
    // Honest truncation footer.
    if (result.truncated) {
        sections.push(`_+${result.omittedCount} more symbol${result.omittedCount === 1 ? '' : 's'} omitted to fit the ${result.budget}-token budget — narrow the query or raise \`--budget\`._`);
    }
    // Same-name conflation caveat — only when there's any risk worth noting.
    sections.push('_Note: graphify conflates same-name symbols across files, so call counts are best-effort._');
    (0, format_1.printMarkdown)(...sections);
}
/**
 * `vyuh-dxkit context <file:line>` — hand back the focused source chunk
 * around a location plus its structural neighborhood, so an agent reads
 * ~the enclosing symbol instead of the whole file. The graph resolves
 * the enclosing symbol + blast radius + callers/callees; the source
 * slice itself is read from disk (the graph carries no source text) and
 * carved to the budget by `extractWindow`, centered on the requested
 * line.
 *
 * Degrades in layers: if the file isn't in the graph we still return a
 * centered raw-line window (no structural context); if the file can't
 * be read we exit non-zero with a clear message.
 */
function runFileLineContext(graph, file, line, values, cwd) {
    const budget = parsePositiveInt(values.budget, DEFAULT_BUDGET);
    // Read the source from disk. The graph stores project-relative paths,
    // so resolve against cwd. A missing file is the one hard error here —
    // there's nothing to slice.
    let text;
    try {
        text = (0, node_fs_1.readFileSync)(path.join(cwd, file), 'utf-8');
    }
    catch {
        process.stderr.write(`Cannot read ${file} (resolved under ${cwd}). Pass a path relative to the repo root.\n`);
        process.exit(1);
    }
    const ctx = (0, queries_1.fileLineContextQuery)(graph, file, line);
    // Stamp call-graph reliability from the language pack (Rule 6), so the
    // blast-radius reads honestly for languages graphify can't resolve.
    const rel = (0, languages_1.languageForFile)(file)?.callGraphReliability;
    if (rel && rel !== 'full')
        ctx.callGraphReliability = rel;
    const chunk = (0, source_slice_1.extractWindow)(text, line, {
        budgetTokens: budget,
        spanStart: ctx.span?.startLine,
        spanEndExclusive: ctx.span?.endLineExclusive,
    });
    if (values.json) {
        (0, format_1.printJson)((0, format_1.envelope)('context', { file, line, budget }, graph, { ...ctx, chunk }));
        return;
    }
    const sections = [(0, format_1.markdownHeader)('Context', `\`${file}:${line}\``, graph)];
    // Enclosing-symbol + module + blast-radius framing.
    if (ctx.enclosingSymbol) {
        const s = ctx.enclosingSymbol;
        sections.push(`**Enclosing symbol:** \`${s.symbol}\` — \`${file}:${s.line}\` · ${s.kind} (${s.callsIn} in / ${s.callsOut} out). _Heuristic: declaration-to-next-declaration; confirm the boundary before editing._`);
    }
    else if (ctx.found) {
        sections.push('**Enclosing symbol:** none at-or-above this line (top-of-file / imports). Showing a centered window.');
    }
    else {
        sections.push('**Enclosing symbol:** file not in the code graph (vendored / autogenerated / unsupported, or not yet scanned). Showing a centered raw-line window — no structural context.');
    }
    if (ctx.found) {
        const role = ctx.community?.role ?? 'unclustered';
        if (ctx.callGraphReliability === 'unreliable') {
            sections.push(`**Module:** ${role} · blast radius n/a (call graph can't be resolved here).`);
        }
        else {
            const n = ctx.blastRadius.callerFiles;
            sections.push(`**Module:** ${role} · blast radius: ${n} caller file${n === 1 ? '' : 's'}.`);
        }
    }
    // The source chunk — line-numbered for navigation, fenced with the
    // file's language for syntax highlighting.
    sections.push(renderChunk(file, chunk));
    if (chunk.truncated) {
        sections.push(`_Showing lines ${chunk.startLine}–${chunk.endLine} of the ${chunk.spanLines}-line span — raise \`--budget\` to widen the window._`);
    }
    // Callers / callees of the enclosing symbol (structural neighborhood).
    if (ctx.callers.length > 0) {
        sections.push(['**Callers (who reaches this symbol):**', ...ctx.callers.map(refLine)].join('\n'));
    }
    if (ctx.callees.length > 0) {
        sections.push(['**Calls out to:**', ...ctx.callees.map(refLine)].join('\n'));
    }
    if (ctx.found) {
        sections.push('_Note: blast radius is file-level and graphify can conflate same-name symbols, so call counts are best-effort._');
    }
    (0, format_1.printMarkdown)(...sections);
}
/** One `- \`symbol\` — \`file:line\`` reference line. */
function refLine(r) {
    const where = r.line ? `${r.sourceFile}:${r.line}` : r.sourceFile;
    return `- \`${r.symbol}\` — \`${where}\``;
}
/** Render the slice as a line-numbered, language-fenced code block. */
function renderChunk(file, chunk) {
    const lang = fenceLanguage(file);
    const width = String(chunk.startLine + chunk.lines.length - 1).length;
    const body = chunk.lines
        .map((l, i) => `${String(chunk.startLine + i).padStart(width)}  ${l}`)
        .join('\n');
    return '```' + lang + '\n' + body + '\n```';
}
/** Map a file extension to a markdown code-fence language tag. */
function fenceLanguage(file) {
    const i = file.lastIndexOf('.');
    if (i < 0)
        return '';
    const map = {
        '.ts': 'ts',
        '.tsx': 'tsx',
        '.js': 'js',
        '.jsx': 'jsx',
        '.mjs': 'js',
        '.cjs': 'js',
        '.py': 'python',
        '.go': 'go',
        '.rs': 'rust',
        '.cs': 'csharp',
        '.kt': 'kotlin',
        '.kts': 'kotlin',
        '.java': 'java',
        '.rb': 'ruby',
    };
    return map[file.slice(i).toLowerCase()] ?? '';
}
/**
 * Collapse repeated symbol names within a community group. The BFS can
 * surface the same stripped label from two different files (graphify's
 * same-name conflation); the listing keeps the first (lowest-hop)
 * occurrence so the output stays scannable.
 */
function dedupeBySymbol(nodes) {
    const seen = new Set();
    const out = [];
    for (const n of nodes) {
        const key = `${n.symbol}\x00${n.sourceFile}`;
        if (seen.has(key))
            continue;
        seen.add(key);
        out.push(n);
    }
    return out;
}
function parsePositiveInt(raw, defaultValue) {
    if (!raw)
        return defaultValue;
    const n = parseInt(raw, 10);
    if (isNaN(n) || n < 1)
        return defaultValue;
    return n;
}
//# sourceMappingURL=context.js.map