/**
 * Deterministic prose chunker for oversize document classification.
 *
 * Splits a document into overlapping fixed-size chunks so each chunk fits
 * inside Haiku's input context window. The chunker is purely mechanical —
 * it makes no semantic claim about where chunk boundaries should fall.
 * Ontological boundaries remain Haiku's job (the document
 * chunker that *did* try to be semantic was deleted because it leaked
 * sections at the boundaries it picked).
 *
 * Overlap exists so a section straddling a chunk boundary appears in BOTH
 * surrounding chunks; the merge step then unions the same-kind ranges so
 * the boundary section isn't double-counted in the writer.
 *
 * Char counts are estimated from token counts via a fixed 3.5 chars/token
 * ratio (English prose average). The estimate is conservative — Haiku
 * tokenises slightly differently per script, but 3.5 leaves ~10% headroom
 * for non-English content before bumping into the model's hard ceiling.
 */
export function chunkDocument(text, opts) {
    const { chunkSize, overlap } = opts;
    if (chunkSize <= 0) {
        throw new Error(`chunkDocument: chunkSize must be positive, got ${chunkSize}`);
    }
    if (overlap < 0) {
        throw new Error(`chunkDocument: overlap must be non-negative, got ${overlap}`);
    }
    if (overlap >= chunkSize) {
        throw new Error(`chunkDocument: overlap (${overlap}) must be less than chunkSize (${chunkSize})`);
    }
    if (text.length === 0)
        return [];
    if (text.length <= chunkSize) {
        // One-chunk fast path so callers that always-chunk don't pay the
        // window-stepping arithmetic for inputs that already fit.
        return [{ chunkText: text, baseOffset: 0 }];
    }
    const chunks = [];
    const stride = chunkSize - overlap;
    let start = 0;
    while (start < text.length) {
        const end = Math.min(start + chunkSize, text.length);
        chunks.push({ chunkText: text.slice(start, end), baseOffset: start });
        if (end >= text.length)
            break;
        start += stride;
    }
    return chunks;
}
// ---------------------------------------------------------------------------
// mergeOverlappingSections — collates per-chunk classifier results.
//
// Algorithm: group sections by `kind`, sort by `sourceStart`, then walk and
// union consecutive same-kind ranges whose intersection covers more than
// MERGE_OVERLAP_THRESHOLD of the smaller range. The longer summary wins on
// merge — empirically Haiku's longer summary on a chunk that saw more
// surrounding context tends to be the better one.
//
// Cross-kind overlap is preserved: chunk A's `Position` and chunk B's
// `Other` covering the same range are kept as two distinct sections (per
// eng review). The classifier disagreed about kind; the writer's downstream
// :Section:Other surfacing will let the operator decide which one wins
// during ontology growth review.
//
// Disjoint same-kind sections are also preserved — only adjacent ranges
// with material overlap are merged.
// ---------------------------------------------------------------------------
export const MERGE_OVERLAP_THRESHOLD = 0.5;
export function mergeOverlappingSections(input) {
    if (input.length <= 1)
        return input.slice();
    // Group by kind so we never accidentally merge across kinds.
    const byKind = new Map();
    for (const s of input) {
        const arr = byKind.get(s.kind);
        if (arr)
            arr.push(s);
        else
            byKind.set(s.kind, [s]);
    }
    const merged = [];
    for (const group of byKind.values()) {
        group.sort((a, b) => a.sourceStart - b.sourceStart || a.sourceEnd - b.sourceEnd);
        let current = null;
        for (const s of group) {
            if (current === null) {
                current = { ...s };
                continue;
            }
            const intersection = Math.max(0, Math.min(current.sourceEnd, s.sourceEnd) - Math.max(current.sourceStart, s.sourceStart));
            if (intersection === 0) {
                merged.push(current);
                current = { ...s };
                continue;
            }
            const currentLen = current.sourceEnd - current.sourceStart;
            const sLen = s.sourceEnd - s.sourceStart;
            const overlapFraction = intersection / Math.min(currentLen, sLen);
            if (overlapFraction > MERGE_OVERLAP_THRESHOLD) {
                // Union the range; the section with the longer body contributes its
                // non-range fields (title, properties, anchorEdge, related, etc.) on
                // the assumption that a wider classification window grounded its
                // properties more reliably. Summary always picks the longer of the two.
                const fieldsWinner = sLen > currentLen ? s : current;
                current = {
                    ...fieldsWinner,
                    sourceStart: Math.min(current.sourceStart, s.sourceStart),
                    sourceEnd: Math.max(current.sourceEnd, s.sourceEnd),
                    summary: s.summary.length > current.summary.length ? s.summary : current.summary,
                };
            }
            else {
                merged.push(current);
                current = { ...s };
            }
        }
        if (current !== null)
            merged.push(current);
    }
    // Return in whole-document reading order so the writer's :NEXT chain
    // maps to source order.
    merged.sort((a, b) => a.sourceStart - b.sourceStart);
    return merged;
}
//# sourceMappingURL=document-chunker.js.map