/**
 * @module        core/wiki/reader
 * @description   wikiRead()：5 层确定性检索 + 类型优先级 + token 预算
 *
 * @layer         core
 *
 * doc-28 §10 Wiki 读取原则的代码实现。**这是 Worker prompt 注入的入口**。
 *
 * 5 层叠加：
 *   L1 永远 — hot.md 全文                           ~500 字
 *   L2 永远 — index.md 节选 top-N 行                ~500 字
 *   L3 优先 — pinned wiki_pages（card frontmatter）  ~50×N 字
 *   L4 按 skill — 卡 skills ∩ 页 tags → top-3        ~50×3 字
 *   L5 按关键词 — BM25(card.title + card.desc)→top-3 ~50×3 字
 *
 * 优先级排序（命中多页）：
 *   lesson = 3 / decision = 3 / concept = 2 / module = 1 / source = 1
 *   stale page → 跳过（status=stale 或 mtime 太老）
 *
 * Token 预算硬上限 1500 字（~2000 token），超出砍 L5 keyword 命中。
 *
 * 设计原则（Karpathy）：
 *   - **确定性**——同输入同输出，纯函数（除文件 I/O）
 *   - **Push 而非 Pull**——Worker 不需要学怎么查，结果已经摆好
 *   - **TL;DR 而非全文**——让 Worker 自己决定要不要 Read 完整 page
 */
import { readHot } from './hot.js';
import { readIndexSummary } from './index-builder.js';
import { getPageById, listValidPages } from './page.js';
import { extractTLDR, WikiSearcher } from './searcher.js';
const DEFAULT_OPTS = {
    indexLines: 30,
    skillTopN: 3,
    keywordTopN: 3,
    budgetTokens: 2000,
};
// 类型优先级（数字越大越靠前）
const TYPE_PRIORITY = {
    lesson: 3,
    decision: 3,
    concept: 2,
    module: 1,
    source: 1,
};
// 启发式：1 个汉字/英文单词 ≈ 1.5 token（粗估）
const CHARS_PER_TOKEN = 1.5;
// ─── 主入口 ───────────────────────────────────────────────────────
/**
 * 读取 wiki 注入到 Worker prompt 的上下文。
 *
 * 步骤：
 *   1. Layer 1: hot.md 全文
 *   2. Layer 2: index.md 节选
 *   3. Layer 3: pinned pages（按 id 取）
 *   4. 列所有 page 建临时 searcher
 *   5. Layer 4: skill 匹配
 *   6. Layer 5: BM25 关键词
 *   7. 合并去重 → 类型优先级排序 → 预算截断
 *   8. 装载 TL;DR 入 PageContextEntry
 *
 * 失败模式：任何 layer 失败（文件丢/解析错）单独 swallow，不阻塞其他 layer。
 */
export function wikiRead(input, opts = {}) {
    const cfg = { ...DEFAULT_OPTS, ...opts };
    // L1
    const hot = safeRead(() => readHot(input.repoDir), '');
    // L2
    const indexSummary = safeRead(() => readIndexSummary(input.repoDir, cfg.indexLines), '');
    // 拉所有 page 一次（reader.ts 调用频率低 = 卡启动一次；现读 OK）
    const pages = safeRead(() => listValidPages(input.repoDir), []);
    // L3: pinned
    const pinned = (input.pinnedPages ?? [])
        .map((id) => getPageById(input.repoDir, id))
        .filter((p) => p !== null);
    // 临时 searcher 用 IndexedDoc（pageToIndexed 来自 searcher.ts）
    const searcher = new WikiSearcher(pages.map(pageToIndexedAdapter));
    // L4: skill
    const bySkill = searcher
        .searchByTags(input.cardSkills, cfg.skillTopN)
        .map((r) => ({ pageId: r.pageId, source: 'skill' }));
    // L5: keyword
    const byKeyword = searcher
        .search(`${input.cardTitle} ${input.cardDesc}`, cfg.keywordTopN)
        .map((r) => ({ pageId: r.pageId, source: 'keyword' }));
    // 合并去重（按 source 优先级：pinned > skill > keyword）
    const dedup = new Map();
    for (const p of pinned)
        dedup.set(p.pageId, { source: 'pinned' });
    for (const r of bySkill) {
        if (!dedup.has(r.pageId))
            dedup.set(r.pageId, { source: r.source });
    }
    for (const r of byKeyword) {
        if (!dedup.has(r.pageId))
            dedup.set(r.pageId, { source: r.source });
    }
    // 把每个 page 加载完整 + 排序
    const entries = [];
    for (const [pageId, meta] of dedup) {
        const page = pages.find((p) => p.pageId === pageId);
        if (!page)
            continue;
        if (isStalePage(page))
            continue;
        entries.push({
            pageId,
            title: page.frontmatter.title,
            type: page.frontmatter.type,
            tldr: extractTLDR(page.body),
            source: meta.source,
            priority: priorityOf(page.frontmatter.type, meta.source),
        });
    }
    // 优先级排序
    entries.sort((a, b) => b.priority - a.priority);
    // 预算截断：估算 token，超出从尾部砍 keyword 命中（保 pinned + skill + 高优类型）
    const trimmed = applyBudget(entries, hot, indexSummary, cfg.budgetTokens);
    const tokensEstimate = estimateTokens(hot, indexSummary, trimmed);
    return {
        hot,
        indexSummary,
        pages: trimmed,
        tokensEstimate,
    };
}
// ─── Prompt 渲染 ──────────────────────────────────────────────────
/**
 * WikiContext → prompt 注入 markdown。
 *
 * 格式（doc-28 §10）：
 *   # 项目知识 - 当前状态
 *   <hot.md 全文>
 *   ---
 *   # 知识地图（节选）
 *   <index summary>
 *   ---
 *   # 与本任务相关的页
 *   ## [[id]] (type) [via source]
 *   TL;DR: ...
 */
export function formatWikiContext(ctx) {
    const sections = [];
    if (ctx.hot.trim().length > 0) {
        // hot 已经是带 frontmatter 的完整文档；展示时去掉 frontmatter 块
        const hotBody = stripFrontmatter(ctx.hot).trim();
        sections.push('# 项目知识 - 当前状态\n\n' + hotBody);
    }
    if (ctx.indexSummary.trim().length > 0) {
        sections.push('# 知识地图（节选）\n\n' + ctx.indexSummary.trim());
    }
    if (ctx.pages.length > 0) {
        const lines = ['# 与本任务相关的页', ''];
        for (const p of ctx.pages) {
            const tag = p.source === 'pinned' ? '📌 pinned' : p.source === 'skill' ? 'via skill' : 'via keyword';
            lines.push(`## [[${p.pageId}]] (${p.type}, ${tag})`);
            lines.push(`TL;DR: ${p.tldr.replace(/\s+/g, ' ').trim().slice(0, 300)}`);
            lines.push('');
        }
        lines.push('完整内容：直接 Read 文件，或 `sps wiki read "<keyword>"` 找更多。');
        sections.push(lines.join('\n'));
    }
    return sections.join('\n\n---\n\n');
}
// ─── Helpers (private) ────────────────────────────────────────────
function pageToIndexedAdapter(p) {
    // 复用 searcher 的工厂；这里手写一份避免循环引用复杂度
    return {
        pageId: p.pageId,
        title: p.frontmatter.title,
        tags: p.frontmatter.tags,
        tldr: extractTLDR(p.body),
        body: p.body,
        type: p.frontmatter.type,
    };
}
function priorityOf(type, source) {
    // pinned 永远最高（用户/Worker 显式指定）
    // 其他按类型权重
    const sourceBonus = source === 'pinned' ? 100 : source === 'skill' ? 10 : 0;
    return sourceBonus + (TYPE_PRIORITY[type] ?? 1);
}
function isStalePage(page) {
    return page.frontmatter.status === 'stale';
}
function applyBudget(entries, hot, indexSummary, budgetTokens) {
    const baseTokens = estimateChars(hot) + estimateChars(indexSummary);
    const baseTok = baseTokens / CHARS_PER_TOKEN;
    if (baseTok >= budgetTokens) {
        // hot+index 已超预算 → 不加任何 page
        return [];
    }
    const remaining = budgetTokens - baseTok;
    // 每页 TL;DR 估 ~80 token（300 字符 / 1.5 + 一些 metadata）
    const PER_PAGE_TOKENS = 80;
    const maxPages = Math.max(0, Math.floor(remaining / PER_PAGE_TOKENS));
    if (entries.length <= maxPages)
        return entries.slice();
    // 砍法：保留 pinned + skill，只砍 keyword 末尾
    const pinned = entries.filter((e) => e.source === 'pinned');
    const skill = entries.filter((e) => e.source === 'skill');
    const keyword = entries.filter((e) => e.source === 'keyword');
    const need = Math.max(0, maxPages - pinned.length - skill.length);
    return [...pinned, ...skill, ...keyword.slice(0, need)];
}
function estimateTokens(hot, indexSummary, entries) {
    const charCount = estimateChars(hot) +
        estimateChars(indexSummary) +
        entries.reduce((sum, e) => sum + e.title.length + e.tldr.length + 30, 0);
    return Math.ceil(charCount / CHARS_PER_TOKEN);
}
function estimateChars(s) {
    return s.length;
}
function stripFrontmatter(content) {
    return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
}
function safeRead(fn, fallback) {
    try {
        return fn();
    }
    catch {
        return fallback;
    }
}
//# sourceMappingURL=reader.js.map