"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
Object.defineProperty(exports, "__esModule", { value: true });
exports.__TEST_ONLY__ = exports.AospKnowledgeIngester = void 0;
/**
 * AospKnowledgeIngester — pulls source files from an Android Open
 * Source Project mirror (or any source matching `AospFetcher`),
 * splits each file into chunks, and writes them to a `RagStore`
 * under the `aosp` source kind with a license stamp.
 *
 * Plan 55 M1 scope:
 * - Pluggable `AospFetcher` so tests stay offline.
 * - Mandatory license at ingestion time. Without a license the
 *   chunk is recorded with `unsupportedReason` and stays in the
 *   index for audit but never surfaces at retrieval.
 *   `RagStore.addChunk()` itself rejects aosp chunks without a
 *   license string — we double-up here so the operator sees the
 *   reason at ingestion time rather than via a thrown error.
 * - Section-based chunking by C++ / Java function boundaries when
 *   present, falling back to fixed-size paragraph packing for
 *   files without recognizable structure.
 * - Stable chunk ids: `sha256(filePath + offset)` first 16 hex
 *   chars; re-ingesting the same file replaces in place.
 *
 * Out of scope:
 * - Real AOSP fetcher (M2 — would call `git archive` against the
 *   pinned manifest snapshot).
 * - Cron-based incremental refresh (M2).
 *
 * @module aospKnowledgeIngester
 */
const crypto_1 = require("crypto");
const baseIngester_1 = require("./rag/baseIngester");
const DEFAULT_MAX_CHUNK_CHARS = 2000;
/** Heuristic regex for C++ / Java function boundaries. Triggers on
 * a line that looks like `[modifiers] returnType name(args) {` or
 * `class Name {`. Imperfect but enough to chop large files into
 * topical chunks. */
const FUNCTION_BOUNDARY_REGEX = /^(?:[\w<>:&,\s\*]+\s+)?[\w~]+\s*\([^)]*\)[\s\w:]*?\{$|^class\s+\w+/gm;
/** Section-based chunker. Tries to split on function / class
 * boundaries; falls back to fixed-size when no boundaries land
 * within `maxChars * 2`. */
function chunkSource(text, maxChars) {
    const symbolChunks = (0, baseIngester_1.chunkSourceBySymbols)(text, maxChars);
    if (symbolChunks.length > 0) {
        return symbolChunks.map(chunk => ({
            text: chunk.text,
            offset: chunk.startLine,
            startLine: chunk.startLine,
            endLine: chunk.endLine,
            symbol: chunk.symbol,
        }));
    }
    if (text.trim().length === 0)
        return [];
    const boundaries = [0];
    let m;
    // Reset regex internal state in case of repeated calls.
    FUNCTION_BOUNDARY_REGEX.lastIndex = 0;
    while ((m = FUNCTION_BOUNDARY_REGEX.exec(text)) !== null) {
        if (m.index > 0)
            boundaries.push(m.index);
    }
    boundaries.push(text.length);
    const out = [];
    let bufStart = boundaries[0];
    let bufEnd = boundaries[0];
    for (let i = 1; i < boundaries.length; i++) {
        const next = boundaries[i];
        if (next - bufStart > maxChars && bufEnd > bufStart) {
            // Emit the current section before the next boundary pushes us
            // over. Start a new buffer at the prior boundary.
            out.push({ text: text.slice(bufStart, bufEnd), offset: bufStart });
            bufStart = bufEnd;
        }
        bufEnd = next;
    }
    if (bufEnd > bufStart) {
        out.push({ text: text.slice(bufStart, bufEnd), offset: bufStart });
    }
    // Fallback: if any chunk exceeds 2× maxChars (because no boundaries
    // landed nearby), hard-split it into fixed-size pieces so the
    // retrieval path stays reasonable.
    const final = [];
    for (const c of out) {
        if (c.text.length <= maxChars * 2) {
            final.push(c);
            continue;
        }
        let cursor = 0;
        while (cursor < c.text.length) {
            const piece = c.text.slice(cursor, cursor + maxChars);
            final.push({ text: piece, offset: c.offset + cursor });
            cursor += maxChars;
        }
    }
    return final;
}
function makeChunkId(filePath, offset) {
    return (0, crypto_1.createHash)('sha256')
        .update(`${filePath}|${offset}`)
        .digest('hex')
        .slice(0, 16);
}
class AospKnowledgeIngester {
    constructor(store, fetcher) {
        this.store = store;
        this.fetcher = fetcher;
    }
    async ingest(opts = {}) {
        const maxChars = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
        const result = {
            filesProcessed: 0,
            chunksAdded: 0,
            chunksSkipped: 0,
            errors: [],
        };
        let files;
        try {
            files = await this.fetcher.fetchFiles({ pathPrefix: opts.pathPrefix });
        }
        catch (err) {
            result.errors.push({
                filePath: '<fetcher>',
                reason: err instanceof Error ? err.message : String(err),
            });
            return result;
        }
        for (const file of files) {
            result.filesProcessed++;
            if (!file.license || file.license.trim().length === 0) {
                result.errors.push({
                    filePath: file.filePath,
                    reason: 'license required for kind=aosp; entry rejected',
                });
                result.chunksSkipped++;
                continue;
            }
            try {
                const packed = chunkSource(file.content, maxChars);
                for (const p of packed) {
                    const chunk = {
                        chunkId: makeChunkId(file.filePath, p.offset),
                        kind: 'aosp',
                        uri: file.filePath,
                        title: file.title ?? file.filePath.split('/').pop(),
                        snippet: p.text,
                        tokenCount: (0, baseIngester_1.estimateTokenCount)(p.text),
                        license: file.license,
                        indexedAt: file.fetchedAt,
                        verifiedAt: file.fetchedAt,
                        filePath: file.filePath,
                        ...(p.startLine && p.endLine ? { lineRange: { start: p.startLine, end: p.endLine } } : {}),
                        ...(p.symbol ? { symbol: p.symbol } : {}),
                        language: (0, baseIngester_1.languageForPath)(file.filePath),
                        commitHash: file.commitHash,
                        ...(file.buildId ? { buildId: file.buildId } : {}),
                        ...(file.codebaseId ? { codebaseId: file.codebaseId, registryOrigin: 'codebase_registry' } : {}),
                    };
                    this.store.addChunk(chunk);
                    result.chunksAdded++;
                }
            }
            catch (err) {
                result.errors.push({
                    filePath: file.filePath,
                    reason: err instanceof Error ? err.message : String(err),
                });
                result.chunksSkipped++;
            }
        }
        return result;
    }
}
exports.AospKnowledgeIngester = AospKnowledgeIngester;
/** Test-only export of the chunker for boundary-detection coverage. */
exports.__TEST_ONLY__ = {
    chunkSource,
    makeChunkId,
};
//# sourceMappingURL=aospKnowledgeIngester.js.map