/** Rough tokens for a byte length (≈ 4 chars/token; good enough for an estimate). */
export function toTokens(chars) {
    return Math.round(chars / 4);
}
/** path → char size, from the file nodes the build sized. Skips nodes with no
 * `chars` (pre-upgrade graphs), so an old index just yields a smaller baseline
 * rather than a wrong one. */
function fileSizes(graph) {
    const m = new Map();
    for (const n of graph.nodes)
        if (n.kind === 'file' && typeof n.chars === 'number')
            m.set(n.path, n.chars);
    return m;
}
/** Baseline = whole size of the distinct `paths`, summed from file-node sizes.
 * Returns undefined when not a single path has a known size — the caller then
 * omits the estimate instead of claiming a bogus one. */
export function savingsFor(graph, paths) {
    const sizes = fileSizes(graph);
    let baselineChars = 0;
    let files = 0;
    for (const p of new Set(paths)) {
        const c = sizes.get(p);
        if (c === undefined)
            continue;
        baselineChars += c;
        files++;
    }
    return files > 0 ? { files, baselineChars } : undefined;
}
/** Appended to every retrieval footer so the agent reports the turn's running
 * total even when SKILL.md isn't loaded — the instruction rides along in the
 * tool output itself. Deliberately free of the `[graft] tokens saved ≈ <n>`
 * pattern so the PostToolUse accumulator that parses these footers never
 * mistakes the nudge (or its example) for a second number to count. */
export const SAVINGS_TURN_NUDGE = ' At the end of your reply, tell the user the total graft tokens saved this ' +
    'turn — sum each such line across your graft calls — e.g. "🌱 graft saved ~N tokens this turn".';
/** The one-line savings estimate for a command's text output, so the agent gets
 * the number for free — no extra tool call. `body` is the exact rendered output
 * the agent reads (the pack). Returns "" when there's nothing honest to claim:
 * no baseline, or the output isn't actually smaller than reading the files
 * (tiny files, where the pointers cost more than the source). */
export function savingsLine(body, saved) {
    if (!saved || saved.baselineChars <= 0)
        return '';
    const pack = toTokens(body.length);
    const base = toTokens(saved.baselineChars);
    if (base <= pack)
        return '';
    const delta = base - pack;
    const pct = Math.round((delta / base) * 100);
    return (`[graft] tokens saved ≈ ${delta.toLocaleString()} (${pct}%) — this output ≈ ` +
        `${pack.toLocaleString()} tok vs reading the ${saved.files} file(s) it covers whole ≈ ` +
        `${base.toLocaleString()} tok (estimate).` +
        SAVINGS_TURN_NUDGE);
}
/**
 * Sum every `[graft] tokens saved ≈ N` footer in a blob of text — the reader
 * half of {@link savingsLine}, kept next to the writer so the two never drift.
 * A single blob can carry several (an agent that made two graft calls in one
 * turn); the `SAVINGS_TURN_NUDGE` deliberately omits the pattern, so its example
 * text is not miscounted here.
 */
export function sumSavingsFooters(text) {
    let total = 0;
    for (const m of text.matchAll(/\[graft\] tokens saved ≈ ([\d,]+)/g)) {
        total += Number(m[1].replace(/,/g, '')) || 0;
    }
    return total;
}
/** Render `body` with the savings line on TOP.
 *
 * Deliberately a header, not a footer: agents routinely pipe graft through
 * `head -N` (and hosts truncate long tool output from the end), which silently
 * ate the number and, with it, the PostToolUse accumulator that feeds the
 * statusline's `~N tok saved`. Every clipper keeps the head, so the number
 * survives. Emitted once — a second copy at the bottom would be double-counted
 * by that accumulator's `matchAll`. */
export function withSavings(body, saved) {
    const line = savingsLine(body, saved);
    return line ? `${line}\n\n${body}` : body;
}
//# sourceMappingURL=savings.js.map