import type { RagStore } from './ragStore';
import type { RagChunk } from '../types/sparkContracts';
/** What a fetcher returns for one post. Format-agnostic — `content` may
 * be HTML or Markdown; the ingester strips HTML when needed. */
export interface BlogPostFetch {
    /** Canonical URL of the post. Drives chunkId derivation. */
    url: string;
    /** Wall-clock fetch time (epoch ms). */
    fetchedAt: number;
    /** Post body. */
    content: string;
    contentType: 'html' | 'markdown';
    title?: string;
    /** Original publication time (epoch ms), if known. */
    publishedAt?: number;
    author?: string;
}
/** Pluggable source for blog posts. Production wiring uses RSS + HTTP;
 * tests inject a stub. */
export interface BlogFetcher {
    /** Return the posts the caller should ingest. `since` lets the
     * ingester ask for posts published after a given epoch ms. */
    fetchRecent(opts?: {
        since?: number;
    }): Promise<BlogPostFetch[]>;
}
export interface BlogIngestOptions {
    /** Maximum characters per chunk. Defaults to 1500. */
    maxChunkChars?: number;
    /** Only ingest posts published after this epoch ms (forwarded to fetcher). */
    since?: number;
}
export interface BlogIngestError {
    url: string;
    reason: string;
}
export interface BlogIngestResult {
    postsProcessed: number;
    chunksAdded: number;
    chunksSkipped: number;
    errors: BlogIngestError[];
}
/** Decode the small set of HTML entities we expect from blog markup.
 * Numeric entities (`&#39;`, `&#x27;`) plus the named ones below cover
 * 99% of authored content; anything richer would warrant a real DOM
 * parser, which M0 does not include. */
declare function decodeHtmlEntities(text: string): string;
/** Strip HTML to plain text. Removes script/style first so their
 * contents don't leak into the snippet, then drops every remaining tag
 * and collapses whitespace. */
declare function stripHtml(html: string): string;
/** Normalize markdown to plain text. Keeps the prose intact; removes
 * fenced code block delimiters but leaves the code so token-overlap
 * search can still find symbol names. */
declare function normalizeMarkdown(markdown: string): string;
/** Split text into paragraphs (double-newline separated). Paragraph
 * boundaries are the chunk boundary candidates. */
declare function splitParagraphs(text: string): string[];
/** Pack paragraphs into `maxChars`-bounded groups. A paragraph longer
 * than `maxChars` becomes its own oversized chunk — splitting it
 * mid-sentence would be worse than the size violation, and these tend
 * to be code blocks the search will still rank reasonably. */
interface PackedChunk {
    text: string;
    offset: number;
}
declare function packParagraphs(paragraphs: string[], maxChars: number): PackedChunk[];
/** sha256(url + offset).slice(0, 16). Stable and short. */
declare function makeChunkId(url: string, offset: number): string;
export declare class BlogKnowledgeIngester {
    private readonly store;
    private readonly fetcher;
    constructor(store: RagStore, fetcher: BlogFetcher);
    ingest(opts?: BlogIngestOptions): Promise<BlogIngestResult>;
    /** Convert one post into chunks. Public for tests so we can poke the
     * chunking decision in isolation. */
    chunkPost(post: BlogPostFetch, maxChars: number): RagChunk[];
}
/** Re-export internals for tests. Not part of the runtime API. */
export declare const __TEST_ONLY__: {
    decodeHtmlEntities: typeof decodeHtmlEntities;
    stripHtml: typeof stripHtml;
    normalizeMarkdown: typeof normalizeMarkdown;
    splitParagraphs: typeof splitParagraphs;
    packParagraphs: typeof packParagraphs;
    makeChunkId: typeof makeChunkId;
};
export {};
//# sourceMappingURL=blogKnowledgeIngester.d.ts.map