"use strict";
// src/orchestrator/context/ContextComposer.ts
// v1.14.0 — Multi-source budgeted context composer.
// v1.14.1 — Performance: module-level singletons for StoryRegistry +
// GraphStore, plus LRU cache on compose() output. Brings warm latency
// from ~1250ms to <100ms. Issue #12.
//
// Pulls relevant items from Stories, RAG chunks, and Codebase Graph;
// cross-ranks them; packs into a "Contexto do projeto" block ready
// to be appended to a systemPrompt by Gatekeeper.handleFastPath.
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.ContextComposer = void 0;
exports._resetComposerCache = _resetComposerCache;
const ContextBudget_1 = require("./ContextBudget");
const SOURCE_WEIGHTS = {
    story: 1.0,
    chunk: 0.85,
    symbol: 0.65,
};
const DEFAULT_BUDGET = 6000;
const DEFAULT_TOPK = 6;
// ─── v1.14.1 caching infrastructure ───────────────────────────────────────
/** TTL for compose() result cache and for source-singleton freshness. */
const CACHE_TTL_MS = 60_000;
const CACHE_MAX_ENTRIES = 50;
/** TTL for story singleton — stories edits are picked up within this window. */
const STORY_SINGLETON_TTL_MS = 60_000;
/** Map iteration preserves insertion order; we delete+set on access for LRU. */
const composeCache = new Map();
let storySingleton = null;
let graphSingleton = null;
function cacheEnabled() {
    return (process.env.OPENLIFE_CONTEXT_COMPOSER_CACHE || 'on').toLowerCase() !== 'off';
}
function cacheKey(query, opts) {
    return JSON.stringify({
        q: query,
        sources: (opts.sources || ['story', 'rag', 'graph']).slice().sort(),
        topK: opts.topKPerSource ?? DEFAULT_TOPK,
        budget: opts.budgetTokens ?? DEFAULT_BUDGET,
    });
}
function checkCache(key) {
    const entry = composeCache.get(key);
    if (!entry)
        return null;
    if (Date.now() - entry.ts > CACHE_TTL_MS) {
        composeCache.delete(key);
        return null;
    }
    // LRU touch — move to most-recent slot.
    composeCache.delete(key);
    composeCache.set(key, entry);
    return entry.result;
}
function storeInCache(key, result) {
    if (composeCache.size >= CACHE_MAX_ENTRIES) {
        // Evict oldest (Map keeps insertion order; iterator first = oldest).
        const oldest = composeCache.keys().next().value;
        if (oldest)
            composeCache.delete(oldest);
    }
    composeCache.set(key, { result, ts: Date.now() });
}
/** Exposed for tests + callers that need to drop singleton+cache state. */
function _resetComposerCache() {
    composeCache.clear();
    storySingleton = null;
    if (graphSingleton) {
        try {
            graphSingleton.store.close();
        }
        catch { /* ignore */ }
        graphSingleton = null;
    }
}
// ─── Composer ─────────────────────────────────────────────────────────────
class ContextComposer {
    async compose(query, opts = {}) {
        if (process.env.OPENLIFE_CONTEXT_COMPOSER === 'off') {
            return { block: '', usedTokens: 0, hits: [], trace: [{ source: 'disabled', n: 0 }] };
        }
        // v1.14.1 — cache lookup. Skipped when caller passed any test stub
        // (storyHits/ragHits/graphHits) since stubs are deterministic-but-distinct.
        const useCache = cacheEnabled() && !opts.storyHits && !opts.ragHits && !opts.graphHits;
        const key = useCache ? cacheKey(query, opts) : null;
        if (key) {
            const hit = checkCache(key);
            if (hit)
                return hit;
        }
        const sources = opts.sources ?? ['story', 'rag', 'graph'];
        const topK = opts.topKPerSource ?? DEFAULT_TOPK;
        const budget = opts.budgetTokens ?? DEFAULT_BUDGET;
        const trace = [];
        const tasks = [];
        if (sources.includes('story'))
            tasks.push(safeRun('story', opts.storyHits || ((q) => defaultStoryHits(q, topK)), query, trace));
        if (sources.includes('rag'))
            tasks.push(safeRun('rag', opts.ragHits || ((q) => defaultRagHits(q, topK)), query, trace));
        if (sources.includes('graph'))
            tasks.push(safeRun('graph', opts.graphHits || ((q) => defaultGraphHits(q, topK)), query, trace));
        const buckets = await Promise.all(tasks);
        const merged = [];
        for (const b of buckets) {
            for (const h of b) {
                const weight = SOURCE_WEIGHTS[h.kind] ?? 0.5;
                merged.push({ ...h, score: h.score * weight });
            }
        }
        merged.sort((a, b) => b.score - a.score);
        const { kept, usedTokens } = (0, ContextBudget_1.pack)(merged, budget);
        let result;
        if (kept.length === 0) {
            result = { block: '', usedTokens: 0, hits: [], trace };
        }
        else {
            const lines = ['### Contexto do projeto', ''];
            for (const h of kept) {
                lines.push(`#### ${h.label}  \`${h.ref}\``);
                lines.push(h.text);
                lines.push('');
            }
            result = { block: lines.join('\n'), usedTokens, hits: kept, trace };
        }
        if (key)
            storeInCache(key, result);
        return result;
    }
}
exports.ContextComposer = ContextComposer;
async function safeRun(name, fn, q, trace) {
    try {
        const out = await fn(q);
        trace.push({ source: name, n: out.length });
        return out;
    }
    catch (err) {
        if ((process.env.OPENLIFE_DEBUG || '').toLowerCase() === 'true') {
            console.warn(`[CONTEXT] source ${name} failed: ${err instanceof Error ? err.message : String(err)}`);
        }
        trace.push({ source: `${name}-error`, n: 0 });
        return [];
    }
}
async function getStoryRegistry() {
    if (storySingleton && Date.now() - storySingleton.loadedAt < STORY_SINGLETON_TTL_MS) {
        return storySingleton.reg;
    }
    const { StoryRegistry } = await Promise.resolve().then(() => __importStar(require('../story/StoryRegistry')));
    const reg = new StoryRegistry();
    await reg.load();
    storySingleton = { reg, loadedAt: Date.now() };
    return reg;
}
function getGraphStore() {
    if (graphSingleton)
        return graphSingleton.store;
    try {
        // Lazy require so test environments without better-sqlite3 still load
        // this module. defaultGraphHits below already returns [] when the
        // store throws.
        const { GraphStore } = require('../graph/GraphStore');
        const store = new GraphStore();
        graphSingleton = { store, openedAt: Date.now() };
        return store;
    }
    catch {
        return null;
    }
}
// Close graph DB on process exit so the WAL doesn't grow unbounded.
process.once('exit', () => {
    if (graphSingleton) {
        try {
            graphSingleton.store.close();
        }
        catch { /* ignore */ }
        graphSingleton = null;
    }
});
// ─── Source implementations (warm via singletons in v1.14.1) ──────────────
async function defaultStoryHits(query, topK) {
    const reg = await getStoryRegistry();
    const all = reg.list();
    const terms = query.toLowerCase().match(/[a-z0-9_.-]+/g) || [];
    if (terms.length === 0)
        return [];
    const scored = all.map(s => {
        const hay = `${s.id} ${s.title} ${s.cluster || ''} ${s.description || ''} ${s.acceptanceCriteria.map(a => a.text).join(' ')}`.toLowerCase();
        let score = 0;
        for (const t of terms)
            if (hay.includes(t))
                score++;
        return { story: s, score: score / Math.max(1, terms.length) };
    }).filter(x => x.score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, topK);
    return scored.map(x => {
        const body = [
            `**StoryId:** ${x.story.id}`,
            `**Epic:** ${x.story.epicId}`,
            `**Status:** ${x.story.status}`,
            x.story.description ? `\n${x.story.description.slice(0, 400)}` : '',
        ].filter(Boolean).join('\n');
        return {
            kind: 'story',
            ref: `story:${x.story.id}`,
            label: `Story ${x.story.id} — ${x.story.title}`,
            text: body,
            score: x.score,
            tokens: (0, ContextBudget_1.countTokens)(body),
        };
    });
}
async function defaultRagHits(query, topK) {
    const { EmbeddingRegistry } = await Promise.resolve().then(() => __importStar(require('../rag/EmbeddingRegistry')));
    const { VectorStoreRegistry } = await Promise.resolve().then(() => __importStar(require('../rag/VectorStoreRegistry')));
    const { Retriever } = await Promise.resolve().then(() => __importStar(require('../rag/Retriever')));
    const embed = EmbeddingRegistry.create();
    if (!(await embed.isAvailable()))
        return [];
    const store = VectorStoreRegistry.create(embed.dim());
    if ((await store.size()) === 0) {
        await store.close();
        return [];
    }
    const ret = new Retriever(embed, store);
    const hits = await ret.retrieve(query, { topK });
    await store.close();
    return hits.map(h => ({
        kind: 'chunk',
        ref: h.ref,
        label: `${h.file.split('/').slice(-2).join('/')}:${h.startLine}`,
        text: h.content.length > 1200 ? `${h.content.slice(0, 1200)}\n…` : h.content,
        score: h.score,
        tokens: (0, ContextBudget_1.countTokens)(h.content),
    }));
}
async function defaultGraphHits(query, topK) {
    const store = getGraphStore();
    if (!store)
        return [];
    const tokens = query.match(/[A-Za-z_][A-Za-z0-9_.]+/g) || [];
    const seen = new Set();
    const out = [];
    for (const t of tokens) {
        if (t.length < 3)
            continue;
        const found = store.findUsages(t);
        for (const s of found) {
            const ref = `${s.file}:${s.startLine}-${s.endLine}`;
            if (seen.has(ref))
                continue;
            seen.add(ref);
            out.push({
                kind: 'symbol',
                ref,
                label: `[${s.kind}] ${s.name}`,
                text: `${s.kind} ${s.name} at ${s.file}:${s.startLine}-${s.endLine}`,
                score: 0.6,
                tokens: (0, ContextBudget_1.countTokens)(s.name) + 8,
            });
            if (out.length >= topK)
                break;
        }
        if (out.length >= topK)
            break;
    }
    return out;
}
