"use strict";
// Local vector RAG over arbitrary project files.
//
// Single-file SQLite-free implementation: each index is a JSON document
// at `<projectDir>/.harness/rag/<name>.json` holding chunked embeddings
// produced by either the Ollama embeddings API (when reachable) or a
// deterministic feature-hash fallback (so the index works offline and
// in CI with no model server).
//
// The fallback isn't great quality but it's reproducible and lets the
// UI show meaningful results out of the box.  Backend choice is pinned
// into the index meta; queries refuse to mix encoders.
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.SKIPPED_DIR_NAMES = exports.TEXT_EXTENSIONS = void 0;
exports.readIndexPrefs = readIndexPrefs;
exports.writeIndexPrefs = writeIndexPrefs;
exports.selectBackend = selectBackend;
exports.listIndexes = listIndexes;
exports.previewBuild = previewBuild;
exports.iterateBuild = iterateBuild;
exports.build = build;
exports.search = search;
exports.dropIndex = dropIndex;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const DEFAULT_CHUNK_CHARS = 1200;
const DEFAULT_OVERLAP = 200;
const HASH_DIM = 256;
exports.TEXT_EXTENSIONS = new Set([
    '.md', '.txt', '.rst', '.json', '.yaml', '.yml', '.toml',
    '.js', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs',
    '.java', '.kt', '.swift', '.cs', '.cpp', '.c', '.h',
    '.html', '.css', '.scss', '.sh', '.ps1', '.sql',
]);
exports.SKIPPED_DIR_NAMES = new Set(['node_modules', '.git', 'dist', '.harness']);
function ragDir(projectDir) {
    return path.join(projectDir, '.harness', 'rag');
}
function safeIndexName(name) {
    return name.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80) || 'default';
}
function indexPath(projectDir, name) {
    return path.join(ragDir(projectDir), `${safeIndexName(name)}.json`);
}
function prefsPath(projectDir, name) {
    return path.join(ragDir(projectDir), `${safeIndexName(name)}.prefs.json`);
}
async function readIndexPrefs(projectDir, name) {
    try {
        const raw = await fs.readFile(prefsPath(projectDir, name), 'utf-8');
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed.paths))
            return null;
        return parsed;
    }
    catch {
        return null;
    }
}
async function writeIndexPrefs(projectDir, name, prefs) {
    await fs.mkdir(ragDir(projectDir), { recursive: true });
    const payload = { ...prefs, updatedAt: new Date().toISOString() };
    await fs.writeFile(prefsPath(projectDir, name), JSON.stringify(payload, null, 2), 'utf-8');
}
async function fileExists(p) {
    try {
        await fs.access(p);
        return true;
    }
    catch {
        return false;
    }
}
// ─── Embedding backends ─────────────────────────────────────────────
async function embedWithOllama(texts, model, host) {
    // We call the REST endpoint directly so this module has no fresh deps
    // (the `ollama` npm package is already used elsewhere; using fetch keeps
    // the abstraction shallow and easier to mock in tests).
    const out = [];
    for (const text of texts) {
        const response = await fetch(`${host.replace(/\/$/, '')}/api/embeddings`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ model, prompt: text }),
        });
        if (!response.ok) {
            const detail = await response.text().catch(() => '');
            throw new Error(`Ollama embeddings ${response.status}: ${detail.slice(0, 200)}`);
        }
        const data = (await response.json());
        if (!Array.isArray(data.embedding) || data.embedding.length === 0) {
            throw new Error('Ollama returned empty embedding');
        }
        out.push(data.embedding.map((v) => Number(v)));
    }
    return out;
}
function embedWithHash(texts, dim = HASH_DIM) {
    // Token-feature-hash → unit vector. Deterministic, no network, fine for
    // smoke-testing semantic-ish behavior even when no model is reachable.
    return texts.map((text) => {
        const vec = new Array(dim).fill(0);
        for (const token of text.toLowerCase().split(/\s+/)) {
            if (!token)
                continue;
            const h = crypto.createHash('md5').update(token).digest();
            const slot = h.readUInt32LE(0) % dim;
            const sign = (h[4] & 1) === 0 ? 1 : -1;
            vec[slot] += sign;
        }
        const norm = Math.sqrt(vec.reduce((s, v) => s + v * v, 0)) || 1;
        return vec.map((v) => v / norm);
    });
}
async function selectBackend(host, prefer) {
    if (prefer === 'hash')
        return { name: 'hash', model: 'feature-hash-256', dim: HASH_DIM };
    if (prefer === 'ollama')
        return { name: 'ollama', model: process.env.HARNESS_RAG_MODEL || 'nomic-embed-text', dim: 768 };
    // Auto: probe Ollama, fall back to hash on any failure.
    try {
        const probe = await fetch(`${host.replace(/\/$/, '')}/api/tags`, { signal: AbortSignal.timeout(800) });
        if (probe.ok)
            return { name: 'ollama', model: process.env.HARNESS_RAG_MODEL || 'nomic-embed-text', dim: 768 };
    }
    catch { /* fall through */ }
    return { name: 'hash', model: 'feature-hash-256', dim: HASH_DIM };
}
async function embedBatch(texts, backend, host) {
    if (backend.name === 'ollama')
        return embedWithOllama(texts, backend.model, host);
    return embedWithHash(texts, backend.dim);
}
// ─── File walking + chunking ───────────────────────────────────────
async function walkText(absRoot, projectDir, results = [], depth = 0) {
    if (depth > 12)
        return results;
    let stat;
    try {
        stat = await fs.stat(absRoot);
    }
    catch {
        return results;
    }
    if (stat.isFile()) {
        const ext = path.extname(absRoot).toLowerCase();
        if (exports.TEXT_EXTENSIONS.has(ext))
            results.push(absRoot);
        return results;
    }
    if (!stat.isDirectory())
        return results;
    // Skip noisy folders.
    const base = path.basename(absRoot);
    if (exports.SKIPPED_DIR_NAMES.has(base))
        return results;
    const entries = await fs.readdir(absRoot, { withFileTypes: true });
    for (const entry of entries) {
        await walkText(path.join(absRoot, entry.name), projectDir, results, depth + 1);
    }
    return results;
}
function chunkText(text, chunkChars = DEFAULT_CHUNK_CHARS, overlap = DEFAULT_OVERLAP) {
    const trimmed = (text || '').trim();
    if (!trimmed)
        return [];
    if (trimmed.length <= chunkChars)
        return [trimmed];
    const out = [];
    let cursor = 0;
    while (cursor < trimmed.length) {
        const end = Math.min(trimmed.length, cursor + chunkChars);
        out.push(trimmed.slice(cursor, end));
        if (end >= trimmed.length)
            break;
        cursor = end - overlap;
    }
    return out;
}
// ─── Public API ─────────────────────────────────────────────────────
async function listIndexes(projectDir) {
    const dir = ragDir(projectDir);
    let entries;
    try {
        entries = await fs.readdir(dir);
    }
    catch {
        return [];
    }
    const out = [];
    for (const file of entries) {
        if (!file.endsWith('.json') || file.endsWith('.prefs.json'))
            continue;
        try {
            const content = await fs.readFile(path.join(dir, file), 'utf-8');
            const idx = JSON.parse(content);
            const sources = new Set(idx.chunks.map((c) => c.source));
            const prefs = await readIndexPrefs(projectDir, idx.name);
            out.push({
                name: idx.name,
                path: path.join(dir, file),
                chunks: idx.chunks.length,
                files: sources.size,
                backend: idx.backend.name,
                model: idx.backend.model,
                dim: idx.backend.dim,
                updatedAt: idx.updatedAt,
                prefs: prefs ?? undefined,
            });
        }
        catch { /* skip corrupt */ }
    }
    out.sort((a, b) => a.name.localeCompare(b.name));
    return out;
}
async function previewBuild(projectDir, paths) {
    const diagnostics = [];
    const allFiles = new Set();
    for (const raw of paths) {
        const trimmed = (raw || '').trim();
        if (!trimmed)
            continue;
        diagnostics.push(await diagnosePath(trimmed, projectDir, allFiles));
    }
    return {
        paths: diagnostics,
        totalFiles: allFiles.size,
        supportedExtensions: Array.from(exports.TEXT_EXTENSIONS).sort(),
        skippedDirectories: Array.from(exports.SKIPPED_DIR_NAMES).sort(),
    };
}
async function diagnosePath(input, projectDir, collected) {
    const abs = path.isAbsolute(input) ? input : path.resolve(projectDir, input);
    let stat;
    try {
        stat = await fs.stat(abs);
    }
    catch {
        return { input, resolved: abs, status: 'missing', kind: 'none', fileCount: 0, sampleFiles: [], message: 'Path not found.' };
    }
    if (stat.isFile()) {
        const ext = path.extname(abs).toLowerCase();
        if (!exports.TEXT_EXTENSIONS.has(ext)) {
            return { input, resolved: abs, status: 'unsupported-extension', kind: 'file', fileCount: 0, sampleFiles: [], message: `Files with extension '${ext || '(none)'}' are not indexed.` };
        }
        collected.add(abs);
        return { input, resolved: abs, status: 'matched', kind: 'file', fileCount: 1, sampleFiles: [path.relative(projectDir, abs) || path.basename(abs)], message: 'Single text file matched.' };
    }
    if (!stat.isDirectory()) {
        return { input, resolved: abs, status: 'missing', kind: 'none', fileCount: 0, sampleFiles: [], message: 'Path is not a regular file or directory.' };
    }
    const matches = [];
    await walkText(abs, projectDir, matches);
    for (const file of matches)
        collected.add(file);
    if (matches.length === 0) {
        return { input, resolved: abs, status: 'empty-directory', kind: 'directory', fileCount: 0, sampleFiles: [], message: 'Directory contains no indexable text files. Skipped folders include node_modules, .git, dist, and .harness.' };
    }
    const sampleFiles = matches.slice(0, 5).map((file) => path.relative(projectDir, file) || file);
    return { input, resolved: abs, status: 'matched', kind: 'directory', fileCount: matches.length, sampleFiles, message: `Matched ${matches.length} text file(s).` };
}
async function* iterateBuild(projectDir, name, paths, options = { ollamaHost: 'http://localhost:11434' }) {
    await fs.mkdir(ragDir(projectDir), { recursive: true });
    const preview = await previewBuild(projectDir, paths);
    yield { stage: 'preview', preview, totalFiles: preview.totalFiles };
    const backend = await selectBackend(options.ollamaHost, options.backend);
    yield { stage: 'backend', backend };
    const filesToRead = [];
    for (const raw of paths) {
        const abs = path.isAbsolute(raw) ? raw : path.resolve(projectDir, raw);
        await walkText(abs, projectDir, filesToRead);
    }
    const chunks = [];
    for (let fileIndex = 0; fileIndex < filesToRead.length; fileIndex++) {
        const filePath = filesToRead[fileIndex];
        let content;
        try {
            content = await fs.readFile(filePath, 'utf-8');
        }
        catch {
            continue;
        }
        const sha = crypto.createHash('sha1').update(content).digest('hex');
        const pieces = chunkText(content, options.chunkChars, options.overlap);
        if (pieces.length === 0)
            continue;
        const embeddings = await embedBatch(pieces, backend, options.ollamaHost);
        pieces.forEach((piece, i) => {
            chunks.push({
                id: crypto.randomUUID(),
                source: filePath,
                chunkNo: i,
                content: piece,
                embedding: embeddings[i],
                sha1: sha,
            });
        });
        yield { stage: 'file', fileIndex: fileIndex + 1, totalFiles: filesToRead.length, source: filePath, chunks: pieces.length };
    }
    const file = {
        version: 1,
        name: safeIndexName(name),
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        backend,
        chunks,
    };
    await fs.writeFile(indexPath(projectDir, name), JSON.stringify(file), 'utf-8');
    await writeIndexPrefs(projectDir, name, { paths, backend: options.backend });
    yield { stage: 'done', files: new Set(chunks.map((c) => c.source)).size, totalChunks: chunks.length, backend, preview };
}
async function build(projectDir, name, paths, options = { ollamaHost: 'http://localhost:11434' }) {
    let files = 0;
    let chunks = 0;
    let backend;
    let preview;
    for await (const event of iterateBuild(projectDir, name, paths, options)) {
        if (event.stage === 'done') {
            files = event.files ?? 0;
            chunks = event.totalChunks ?? 0;
            backend = event.backend;
            preview = event.preview;
        }
    }
    if (!backend || !preview)
        throw new Error('iterateBuild did not produce a final result');
    return { files, chunks, backend, preview };
}
async function search(projectDir, name, query, options) {
    const idxPath = indexPath(projectDir, name);
    if (!(await fileExists(idxPath))) {
        throw new Error(`No such index: ${name} (build with POST /api/rag/build)`);
    }
    const raw = await fs.readFile(idxPath, 'utf-8');
    const idx = JSON.parse(raw);
    if (idx.chunks.length === 0)
        return [];
    const [qvec] = await embedBatch([query], idx.backend, options.ollamaHost);
    const kRaw = options.k;
    const k = Math.max(1, Math.min(Number.isFinite(kRaw) ? Number(kRaw) : 5, 20));
    const qNorm = Math.sqrt(qvec.reduce((s, v) => s + v * v, 0)) || 1;
    const scored = idx.chunks.map((chunk) => {
        let dot = 0;
        let cn = 0;
        const len = Math.min(chunk.embedding.length, qvec.length);
        for (let i = 0; i < len; i++) {
            dot += chunk.embedding[i] * qvec[i];
            cn += chunk.embedding[i] * chunk.embedding[i];
        }
        const score = dot / ((Math.sqrt(cn) || 1) * qNorm);
        return { chunk, score };
    });
    scored.sort((a, b) => b.score - a.score);
    return scored.slice(0, k).map(({ chunk, score }) => ({
        source: chunk.source,
        chunkNo: chunk.chunkNo,
        content: chunk.content,
        score: Math.max(0, Math.min(1, score)),
    }));
}
async function dropIndex(projectDir, name) {
    const idxPath = indexPath(projectDir, name);
    try {
        await fs.rm(idxPath, { force: true });
        await fs.rm(prefsPath(projectDir, name), { force: true });
        return true;
    }
    catch {
        return false;
    }
}
//# sourceMappingURL=ragIndex.js.map