/**
 * Phase 5 (v13) path resolution — prefer the new prose outline files, fall
 * back to legacy paths so older books keep working during transition.
 *
 * Maps:
 *   story/outline/story_frame.md  →  preferred replacement for story_bible.md
 *   story/outline/volume_map.md   →  preferred replacement for volume_outline.md
 *   story/roles/主要角色/*.md +
 *   story/roles/次要角色/*.md    →  preferred replacement for character_matrix.md
 *
 * All helpers accept a bookDir (path to a book root, containing `story/`)
 * and return a string — either the new-file content when it exists, or the
 * legacy file content, or an empty default placeholder.
 */
import { readFile, readdir, access } from "node:fs/promises";
import { join } from "node:path";
/**
 * Detect whether a book uses the Phase 5 new layout (outline/story_frame.md
 * exists on disk). If yes, story_bible.md / book_rules.md are compat shims.
 * If no, those files ARE the authoritative source.
 */
export async function isNewLayoutBook(bookDir) {
    try {
        await access(join(bookDir, "story", "outline", "story_frame.md"));
        return true;
    }
    catch {
        return false;
    }
}
async function readOr(path, fallback) {
    try {
        return await readFile(path, "utf-8");
    }
    catch {
        return fallback;
    }
}
/** Read story_frame.md, falling back to legacy story_bible.md. */
export async function readStoryFrame(bookDir, fallbackPlaceholder = "") {
    const newPath = join(bookDir, "story", "outline", "story_frame.md");
    const legacyPath = join(bookDir, "story", "story_bible.md");
    const newContent = await readOr(newPath, "");
    if (newContent.trim())
        return newContent;
    return readOr(legacyPath, fallbackPlaceholder);
}
/** Read volume_map.md, falling back to legacy volume_outline.md. */
export async function readVolumeMap(bookDir, fallbackPlaceholder = "") {
    const newPath = join(bookDir, "story", "outline", "volume_map.md");
    const legacyPath = join(bookDir, "story", "volume_outline.md");
    const newContent = await readOr(newPath, "");
    if (newContent.trim())
        return newContent;
    return readOr(legacyPath, fallbackPlaceholder);
}
/** Read the rhythm principles file (zh or en variant). */
export async function readRhythmPrinciples(bookDir) {
    const zhPath = join(bookDir, "story", "outline", "节奏原则.md");
    const enPath = join(bookDir, "story", "outline", "rhythm_principles.md");
    const zh = await readOr(zhPath, "");
    if (zh.trim())
        return zh;
    return readOr(enPath, "");
}
/**
 * Read the roles/ directory. Returns [] when no roles are present (e.g. old
 * books still on character_matrix.md).
 */
export async function readRoleCards(bookDir) {
    const rolesRoot = join(bookDir, "story", "roles");
    const majorDirZh = join(rolesRoot, "主要角色");
    const minorDirZh = join(rolesRoot, "次要角色");
    const majorDirEn = join(rolesRoot, "major");
    const minorDirEn = join(rolesRoot, "minor");
    const cards = [];
    await Promise.all([
        collectRoleDir(majorDirZh, "major", cards),
        collectRoleDir(minorDirZh, "minor", cards),
        collectRoleDir(majorDirEn, "major", cards),
        collectRoleDir(minorDirEn, "minor", cards),
    ]);
    return cards;
}
async function collectRoleDir(dir, tier, out) {
    let entries;
    try {
        entries = await readdir(dir);
    }
    catch {
        return;
    }
    const reads = entries
        .filter((entry) => entry.endsWith(".md"))
        .map(async (entry) => {
        const content = await readOr(join(dir, entry), "");
        if (!content.trim())
            return;
        out.push({
            tier,
            name: entry.replace(/\.md$/, ""),
            content,
        });
    });
    await Promise.all(reads);
}
/**
 * Render role cards in a format compatible with downstream consumers that
 * previously expected character_matrix.md prose. When no role cards exist,
 * returns the legacy character_matrix.md content or the placeholder.
 */
export async function readCharacterContext(bookDir, fallbackPlaceholder = "") {
    const cards = await readRoleCards(bookDir);
    if (cards.length > 0) {
        const groups = { major: [], minor: [] };
        for (const card of cards)
            groups[card.tier].push(card);
        const render = (tierCards, heading) => {
            if (tierCards.length === 0)
                return "";
            const sections = tierCards.map((card) => `### ${card.name}\n\n${card.content.trim()}`);
            return `## ${heading}\n\n${sections.join("\n\n")}`;
        };
        const blocks = [
            render(groups.major, "主要角色 / Major characters"),
            render(groups.minor, "次要角色 / Minor characters"),
        ].filter(Boolean);
        return blocks.join("\n\n");
    }
    // Fallback: legacy character_matrix.md (may itself be a shim pointer).
    const legacyPath = join(bookDir, "story", "character_matrix.md");
    return readOr(legacyPath, fallbackPlaceholder);
}
// ---------------------------------------------------------------------------
// Phase 5 consolidation: current_state.md initial fallback
//
// After architect consolidation (7→5 sections), current_state.md is seeded
// with a tiny placeholder at book creation. Real content only arrives once
// the consolidator has appended output from chapter 1 onward. Readers that
// previously relied on architect-provided initial state (writer phase-1
// creative prompt, continuity, chapter-analyzer, reviser, composer) should
// substitute a derived initial-state block when the seed placeholder is all
// that's on disk — otherwise the "## 当前状态卡" block in prompts degenerates
// into a meta note about runtime append behaviour.
// ---------------------------------------------------------------------------
/**
 * Marker substring emitted by architect.writeFoundationFiles when seeding
 * current_state.md. Its presence is how readers detect "nothing real yet".
 */
const CURRENT_STATE_SEED_MARKERS = [
    "建书时占位",
    "Seeded at book creation",
];
export function isCurrentStateSeedPlaceholder(raw) {
    const trimmed = raw.trim();
    if (!trimmed)
        return true;
    // Heuristic: short file AND contains one of the seed markers.
    if (trimmed.length > 600)
        return false;
    return CURRENT_STATE_SEED_MARKERS.some((marker) => trimmed.includes(marker));
}
function extractCurrentStateFromRole(content) {
    // Accept both zh (`## 当前现状`) and en (`## Current_State` / `## Current State`).
    const pattern = /^##\s*(?:当前现状|Current[_\s]?State)[^\n]*$/im;
    const match = content.match(pattern);
    if (!match || match.index === undefined)
        return null;
    const after = content.slice(match.index + match[0].length);
    // Cut at next `## ` heading (same or higher level).
    const nextHeading = after.search(/^##\s/m);
    const raw = nextHeading >= 0 ? after.slice(0, nextHeading) : after;
    const text = raw.trim();
    return text.length > 0 ? text : null;
}
function extractSeedHooksFromPendingHooks(raw) {
    if (!raw.trim())
        return [];
    const lines = raw.split("\n").map((line) => line.trim()).filter(Boolean);
    const seedRows = [];
    for (const line of lines) {
        if (!line.startsWith("|"))
            continue;
        if (/^\|\s*-/.test(line))
            continue;
        const cells = line.split("|").slice(1, -1).map((cell) => cell.trim());
        if (cells.length < 2)
            continue;
        if (cells[0]?.toLowerCase() === "hook_id" || cells[0] === "hookId")
            continue;
        const startCh = Number.parseInt(cells[1] ?? "", 10);
        if (!Number.isFinite(startCh) || startCh !== 0)
            continue;
        // cells[2] type, cells[5] expected payoff, last cell notes
        const notes = cells[cells.length - 1] ?? "";
        const summary = [cells[0], cells[2], notes].filter(Boolean).join(" · ");
        if (summary)
            seedRows.push(summary);
    }
    return seedRows;
}
/**
 * Read current_state.md; when the file is only a seed placeholder (chapter 0,
 * before consolidator has appended anything), derive an initial-state block
 * from roles/*.Current_State + pending_hooks startChapter=0 rows so callers
 * still have substantive content to feed into writer / analyzer prompts.
 */
export async function readCurrentStateWithFallback(bookDir, fallbackPlaceholder = "") {
    const storyDir = join(bookDir, "story");
    const currentStatePath = join(storyDir, "current_state.md");
    const raw = await readOr(currentStatePath, "");
    if (!isCurrentStateSeedPlaceholder(raw)) {
        return raw;
    }
    const [cards, pendingHooks] = await Promise.all([
        readRoleCards(bookDir),
        readOr(join(storyDir, "pending_hooks.md"), ""),
    ]);
    const roleLines = cards
        .map((card) => {
        const state = extractCurrentStateFromRole(card.content);
        if (!state)
            return null;
        const tierLabel = card.tier === "major" ? "主要" : "次要";
        return `- ${card.name}（${tierLabel}）：${state.replace(/\s+/g, " ")}`;
    })
        .filter((line) => line !== null);
    const hookLines = extractSeedHooksFromPendingHooks(pendingHooks);
    if (roleLines.length === 0 && hookLines.length === 0) {
        return raw.trim() ? raw : fallbackPlaceholder;
    }
    const parts = ["# 初始状态（第 0 章，由 roles + 种子伏笔派生）"];
    if (roleLines.length > 0) {
        parts.push("\n## 角色初始位置 / 处境");
        parts.push(...roleLines);
    }
    if (hookLines.length > 0) {
        parts.push("\n## 种子伏笔（startChapter = 0）");
        parts.push(...hookLines.map((line) => `- ${line}`));
    }
    return parts.join("\n");
}
//# sourceMappingURL=outline-paths.js.map