Skill: ljunn/junli-story-analysis (version 1.0.0)

--- name: junli-story-analysis description: 君黎AI拆书。用于用户提供本地 `.txt` 小说绝对路径，希望把一部长篇网络小说拆成可复用的创作参考包，而不是剧情报告或文学赏析。固定产出为 `1 份书籍画像 + 6 张方法卡 + 若干案例卡`，用于后续写作参考与案例检索。更多信息关注作者，抖音君黎。 --- # 君黎AI拆书 ## 技能边界 这个技能只做一件事： 把用户提供的本地 `.txt` 长篇小说，拆成可复用的创作参考包。 固定产出只有： - `outputs/书籍画像.md` - `outputs/方法卡/` 下 6 张固定方法卡 - `outputs/案例卡/` 下若干张案例卡 不要把它做成： - 剧情梗概 - 文学赏析 - 人物分析合集 - 续写方案或新书策划 如果用户要的是续写、扩写、重写章节，那不是这个技能的主职责。 ## 先判断当前任务 ### 1. 新拆一本书 适用场景： - 用户只给了本地 `.txt` 绝对路径 - 用户明确说“从零开始拆这本书” 默认动作： ```bash python3 scripts/story_analysis_pipeline.py init "/绝对路径/小说.txt" --story-root ./story python3 scripts/story_analysis_pipeline.py resume ./story/书名 ``` ### 2. 继续已有拆书工程 适用场景： - 用户说“继续拆”“接着上次来” - 已有 `story/<书名>/` 工程目录 默认动作： ```bash python3 scripts/story_analysis_pipeline.py resume <工程目录> ``` 然后只补当前缺口，不重跑整套流程。 ### 3. 局部补卡或返修 适用场景： - 用户只要重做某几张方法卡或案例卡 - 用户只要补强书籍画像 - 用户只要做质量检查 默认动作： 1. 先 `resume` 2. 只读相关 `outputs/`、`drafts/`、`extraction-cards/` 3. 定向修改后再 `check` ```bash python3 scripts/story_analysis_pipeline.py check <工程目录> ``` ## 最小工作顺序 1. 先整书定位：判断题材、风格、节奏、强项和适用边界。 2. 再按 chunk 提取：不要急着写最终卡片。 3. 同步更新 3 份草稿： `drafts/书籍画像草稿.md` `drafts/方法候选笔记.md` `drafts/案例候选笔记.md` 4. 最后收束成 `1 + 6 + N`。 ## 处理每个 chunk 时固定回答的 5 件事 1. 这块的主要叙事功能是什么 2. 这块支撑哪张方法卡 3. 这块能不能进入案例候选 4. 这块给书籍画像补了什么信号 5. 这块最关键的 1-3 个证据点是什么 不要先问“这段剧情讲了什么”。 ## 硬约束 - 不把剧情复述当主产品。 - 不把文学评论、读后感或人物分析当主产品。 - 每个 chunk 都必须先判断它支撑哪张方法卡、能不能进入某类案例卡。 - 每张方法卡都必须有 chunk 证据支持。 - 每张案例卡都必须明确“为什么有效”和“不能照搬什么”。 - 书籍画像必须说明“这本书最适合参考什么、最不适合照搬什么”。 - 案例卡不设数量上限，但必须“值得检索、值得复用、值得对照”。 - 不把单本书的特殊写法直接当成通用铁律。 ## 固定产出 ### 1 份书籍画像 - `outputs/书籍画像.md` ### 6 张方法卡 - `outputs/方法卡/开篇规划方法卡.md` - `outputs/方法卡/大纲设计方法卡.md` - `outputs/方法卡/章节类型与写作模式方法卡.md` - `outputs/方法卡/冲突与角色设计方法卡.md` - `outputs/方法卡/读者情绪管理方法卡.md` - `outputs/方法卡/长篇创作与文风优化方法卡.md` ### 若干案例卡 - `outputs/案例卡/` 下按需新建 - 命名建议：`NN-案例类型-核心动作.md` - 默认参考模板：`workspace/案例卡模板.md` - 默认推荐类型：`workspace/推荐案例类型.md` 不要为了凑整而硬做“弱案例”。 ## 异常处理 如果已有工程缺失 `workspace/`、`drafts/` 或模板骨架，不要假装已经分析过。 先修复，再继续： ```bash python3 scripts/story_analysis_pipeline.py repair <工程目录> python3 scripts/story_analysis_pipeline.py resume <工程目录> ``` ## 资源 - `scripts/story_analysis_pipeline.py` 作用：默认 CLI 入口，用于初始化、恢复摘要、检查和修复工程。 - `scripts/bootstrap_story_analysis.py` 作用：底层初始化脚本；自动分块并生成提取卡与输出骨架。 - `references/practical-workflow.md` 作用：技能的最小工作流，说明为什么要按“定位 -> 提取 -> 汇总 -> 收束”来跑。 - `references/chunk-card-guide.md` 作用：如何填写 `extraction-cards/*.md`，让每个 chunk 天然为方法卡、案例卡和书籍画像服务。 - `references/mode-selection.md` 作用：什么时候更偏顺序滚动提取，什么时候更偏阶段收束。 - `references/analysis-pipeline.md` 作用：把整本书稳定转成 `1 + 6 + N` 的处理逻辑。 - `references/quality-checklist.md` 作用：正式交付前的质量核对项。 - `references/output-templates.md` 作用：输出模板索引页；具体模板拆分在 `references/templates/` 下，bootstrap 会直接从这里复制模板。

path: scripts/story_analysis_pipeline.py
content: #!/usr/bin/env python3
"""统一的初始化、恢复、阶段推进、质检、收束与修复入口。"""

from __future__ import annotations

import argparse
import json
import re
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

try:
    from bootstrap_story_analysis import (
        Chunk,
        METHOD_CARD_NAMES,
        build_book_profile_draft,
        build_book_profile_output,
        build_case_candidate_notes,
        build_case_card_template,
        build_case_type_reference,
        build_chapter_method_card,
        build_chunks,
        build_conflict_character_method_card,
        build_emotion_method_card,
        build_lines,
        build_longform_style_method_card,
        build_method_candidate_notes,
        build_opening_method_card,
        build_outline_method_card,
        detect_sections,
        ensure_dir,
        initialize_project,
        normalize_text,
        read_text as read_source_text,
        render_chunk_index,
        render_extraction_card_template,
        render_stage_plan,
        render_use_guide,
        safe_book_name,
        write_text,
        write_json,
    )
except ModuleNotFoundError:
    from scripts.bootstrap_story_analysis import (
        Chunk,
        METHOD_CARD_NAMES,
        build_book_profile_draft,
        build_book_profile_output,
        build_case_candidate_notes,
        build_case_card_template,
        build_case_type_reference,
        build_chapter_method_card,
        build_chunks,
        build_conflict_character_method_card,
        build_emotion_method_card,
        build_lines,
        build_longform_style_method_card,
        build_method_candidate_notes,
        build_opening_method_card,
        build_outline_method_card,
        detect_sections,
        ensure_dir,
        initialize_project,
        normalize_text,
        read_text as read_source_text,
        render_chunk_index,
        render_extraction_card_template,
        render_stage_plan,
        render_use_guide,
        safe_book_name,
        write_text,
        write_json,
    )


PROGRESS_FILE = Path("workspace/analysis-progress.json")
REQUIRED_SUPPORT_FILES = (
    "workspace/manifest.json",
    "workspace/chunk-index.md",
    "workspace/stage-plan.md",
    "workspace/use-guide.md",
    "drafts/书籍画像草稿.md",
    "drafts/方法候选笔记.md",
    "drafts/案例候选笔记.md",
    "outputs/书籍画像.md",
)


@dataclass(frozen=True)
class Stage:
    stage_id: int
    chunk_ids: list[str]

    @property
    def chunk_range(self) -> str:
        return f"{self.chunk_ids[0]} - {self.chunk_ids[-1]}"


def now_iso() -> str:
    return datetime.now().astimezone().isoformat(timespec="seconds")


def read_utf8(path: Path) -> str:
    return path.read_text(encoding="utf-8") if path.exists() else ""


def load_json_file(path: Path) -> dict:
    if not path.exists():
        return {}
    data = json.loads(path.read_text(encoding="utf-8"))
    return data if isinstance(data, dict) else {}


def write_json_file(path: Path, data: dict) -> None:
    ensure_dir(path.parent)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def parse_chunk_raw(path: Path) -> Chunk:
    text = path.read_text(encoding="utf-8")

    def match(pattern: str, label: str) -> str:
        found = re.search(pattern, text, re.MULTILINE)
        if not found:
            raise RuntimeError(f"无法从 {path} 解析 {label}")
        return found.group(1).strip()

    title = match(r"^- 标题：(.*)$", "标题")
    start = int(match(r"^- 起始字符：(.*)$", "起始字符"))
    end = int(match(r"^- 结束字符：(.*)$", "结束字符"))
    char_count = int(match(r"^- 字符数：(.*)$", "字符数"))
    source_mode = match(r"^- 分块模式：(.*)$", "分块模式")

    heading_match = re.search(r"(?ms)^## 覆盖章节\n\n(.*?)\n\n## 原文\n\n", text)
    if not heading_match:
        raise RuntimeError(f"无法从 {path} 解析覆盖章节")
    headings = []
    for line in heading_match.group(1).splitlines():
        if not line.startswith("- "):
            continue
        item = line[2:].strip()
        if item and item != "无明确章节标题":
            headings.append(item)

    raw_match = re.search(r"(?ms)^## 原文\n\n(.*)\Z", text)
    if not raw_match:
        raise RuntimeError(f"无法从 {path} 解析原文")
    raw_text = raw_match.group(1)

    parsed_end = start + char_count if end - start != char_count else end
    return Chunk(
        chunk_id=path.stem,
        title=title,
        start=start,
        end=parsed_end,
        text=raw_text,
        headings=headings,
        source_mode=source_mode,
    )


def load_chunks(project_dir: Path) -> list[Chunk]:
    chunk_dir = project_dir / "chunks" / "raw"
    if not chunk_dir.exists():
        return []
    return [parse_chunk_raw(path) for path in sorted(chunk_dir.glob("*.md"))]


def build_stages(chunks: list[Chunk], stage_size: int) -> list[Stage]:
    if not chunks:
        return []
    return [
        Stage(stage_id=index // stage_size + 1, chunk_ids=[chunk.chunk_id for chunk in chunks[index : index + stage_size]])
        for index in range(0, len(chunks), stage_size)
    ]


def load_source_path(project_dir: Path) -> Path | None:
    source_path_text = read_utf8(project_dir / "source" / "source-path.txt").strip()
    if not source_path_text:
        return None
    return Path(source_path_text).expanduser()


def load_stage_size(project_dir: Path, manifest: dict) -> int:
    stage_size = manifest.get("stage_size")
    if isinstance(stage_size, int) and stage_size > 0:
        return stage_size

    stage_plan = read_utf8(project_dir / "workspace" / "stage-plan.md")
    match = re.search(r"建议每\s+(\d+)\s+个 chunk", stage_plan)
    if match:
        return int(match.group(1))
    return 8


def build_project_manifest(
    project_dir: Path,
    book_name: str,
    source_txt: Path | None,
    detected_encoding: str,
    chunks: list[Chunk],
    chunk_mode: str,
    source_char_count: int,
    stage_size: int,
) -> dict:
    return {
        "book_name": book_name,
        "source_txt": str(source_txt) if source_txt else "",
        "project_dir": str(project_dir.resolve()),
        "encoding": detected_encoding,
        "chunk_mode": chunk_mode,
        "chunk_count": len(chunks),
        "source_char_count": source_char_count,
        "stage_size": stage_size,
        "outputs": {
            "book_profile": "outputs/书籍画像.md",
            "method_cards": [f"outputs/方法卡/{name}.md" for name in METHOD_CARD_NAMES],
            "case_cards_dir": "outputs/案例卡",
            "case_card_template": "workspace/案例卡模板.md",
            "case_type_reference": "workspace/推荐案例类型.md",
        },
    }


def build_progress_state(project_dir: Path, chunks: list[Chunk], stage_size: int, status: str) -> dict:
    return {
        "project_dir": str(project_dir.resolve()),
        "status": status,
        "current_stage": None,
        "stage_size": stage_size,
        "chunk_total": len(chunks),
        "processed_chunks": 0,
        "created_at": now_iso(),
        "updated_at": now_iso(),
        "finalized_at": None,
    }


def update_progress_state(project_dir: Path, patch: dict) -> dict:
    progress_path = project_dir / PROGRESS_FILE
    current = load_json_file(progress_path)
    current.update(patch)
    current["updated_at"] = now_iso()
    write_json_file(progress_path, current)
    return current


def get_method_card_defaults() -> dict[str, str]:
    return {
        "开篇规划方法卡": build_opening_method_card(),
        "大纲设计方法卡": build_outline_method_card(),
        "章节类型与写作模式方法卡": build_chapter_method_card(),
        "冲突与角色设计方法卡": build_conflict_character_method_card(),
        "读者情绪管理方法卡": build_emotion_method_card(),
        "长篇创作与文风优化方法卡": build_longform_style_method_card(),
    }


def is_modified(path: Path, default_content: str) -> bool:
    if not path.exists():
        return False
    return path.read_text(encoding="utf-8") != default_content


def collect_required_files() -> list[str]:
    files = list(REQUIRED_SUPPORT_FILES)
    files.extend(f"outputs/方法卡/{name}.md" for name in METHOD_CARD_NAMES)
    return files


def find_stage_for_chunk(stages: list[Stage], chunk_id: str) -> Stage | None:
    for stage in stages:
        if chunk_id in stage.chunk_ids:
            return stage
    return None


def ensure_project_exists(project_dir: Path) -> None:
    if not project_dir.exists():
        raise FileNotFoundError(f"找不到工程目录: {project_dir}")
    if not project_dir.is_dir():
        raise FileNotFoundError(f"工程路径不是目录: {project_dir}")


def summarize_project(project_dir: Path) -> dict:
    ensure_project_exists(project_dir)

    manifest = load_json_file(project_dir / "workspace" / "manifest.json")
    progress = load_json_file(project_dir / PROGRESS_FILE)
    chunks = load_chunks(project_dir)
    stage_size = load_stage_size(project_dir, manifest)
    stages = build_stages(chunks, stage_size)
    book_name = str(manifest.get("book_name") or project_dir.name)

    extraction_status: list[tuple[str, bool]] = []
    for chunk in chunks:
        card_path = project_dir / "extraction-cards" / f"{chunk.chunk_id}.md"
        extraction_status.append((chunk.chunk_id, is_modified(card_path, render_extraction_card_template(chunk))))

    processed_chunk_ids = [chunk_id for chunk_id, modified in extraction_status if modified]
    pending_chunk_ids = [chunk_id for chunk_id, modified in extraction_status if not modified]

    default_drafts = {
        "书籍画像草稿": build_book_profile_draft(book_name),
        "方法候选笔记": build_method_candidate_notes(),
        "案例候选笔记": build_case_candidate_notes(),
    }
    draft_status = {
        name: is_modified(project_dir / "drafts" / f"{name}.md", content)
        for name, content in default_drafts.items()
    }

    method_card_defaults = get_method_card_defaults()
    method_card_status = {
        name: is_modified(project_dir / "outputs" / "方法卡" / f"{name}.md", content)
        for name, content in method_card_defaults.items()
    }

    book_profile_touched = is_modified(project_dir / "outputs" / "书籍画像.md", build_book_profile_output())

    case_cards_dir = project_dir / "outputs" / "案例卡"
    ready_case_cards = 0
    template_only_case_cards = 0
    if case_cards_dir.exists():
        for path in sorted(case_cards_dir.glob("*.md")):
            if path.read_text(encoding="utf-8") == build_case_card_template():
                template_only_case_cards += 1
            else:
                ready_case_cards += 1

    missing_support_files = [relative for relative in collect_required_files() if not (project_dir / relative).exists()]

    current_stage = None
    if progress.get("current_stage"):
        current_stage = progress["current_stage"]
    elif pending_chunk_ids:
        stage = find_stage_for_chunk(stages, pending_chunk_ids[0])
        if stage:
            current_stage = {"stage_id": stage.stage_id, "chunk_ids": stage.chunk_ids, "chunk_range": stage.chunk_range}

    return {
        "project_dir": str(project_dir.resolve()),
        "book_name": book_name,
        "manifest": manifest,
        "progress": progress,
        "stage_size": stage_size,
        "stages": stages,
        "current_stage": current_stage,
        "chunks": chunks,
        "chunk_total": len(chunks),
        "processed_chunk_ids": processed_chunk_ids,
        "processed_chunk_count": len(processed_chunk_ids),
        "pending_chunk_ids": pending_chunk_ids,
        "pending_chunk_count": len(pending_chunk_ids),
        "draft_status": draft_status,
        "drafts_touched_count": sum(1 for touched in draft_status.values() if touched),
        "method_card_status": method_card_status,
        "method_cards_touched_count": sum(1 for touched in method_card_status.values() if touched),
        "book_profile_touched": book_profile_touched,
        "ready_case_cards": ready_case_cards,
        "template_only_case_cards": template_only_case_cards,
        "missing_support_files": missing_support_files,
    }


def recommend_next_step(summary: dict) -> str:
    project_dir = summary["project_dir"]
    if summary["missing_support_files"]:
        return f'先运行 `python3 scripts/story_analysis_pipeline.py repair "{project_dir}"`。'
    if summary["pending_chunk_count"] > 0:
        current_stage = summary.get("current_stage")
        if current_stage:
            return (
                "继续阶段提取："
                f'`python3 scripts/story_analysis_pipeline.py start-stage "{project_dir}" {current_stage["stage_id"]}`。'
            )
        return "继续按顺序处理 `extraction-cards/*.md`。"
    if summary["drafts_touched_count"] < 3:
        return "先收束并更新 3 份 drafts，再急着写最终卡片。"
    if not summary["book_profile_touched"] or summary["method_cards_touched_count"] < len(METHOD_CARD_NAMES):
        return "补齐 `outputs/书籍画像.md` 和 6 张方法卡。"
    if summary["ready_case_cards"] == 0:
        return "筛选并输出至少一张正式案例卡。"
    return f'可以运行 `python3 scripts/story_analysis_pipeline.py finalize "{project_dir}"`。'


def print_resume_summary(summary: dict) -> None:
    print("\n" + "=" * 60)
    print("拆书工程恢复摘要")
    print("=" * 60)
    print(f"工程目录: {summary['project_dir']}")
    print(f"书名: {summary['book_name']}")
    print(f"总 chunk: {summary['chunk_total']}")
    print(f"已处理提取卡: {summary['processed_chunk_count']}")
    print(f"待处理提取卡: {summary['pending_chunk_count']}")
    print(f"草稿更新: {summary['drafts_touched_count']}/3")
    print(f"方法卡更新: {summary['method_cards_touched_count']}/{len(METHOD_CARD_NAMES)}")
    print(f"书籍画像: {'已更新' if summary['book_profile_touched'] else '未更新'}")
    print(f"正式案例卡: {summary['ready_case_cards']}")

    current_stage = summary.get("current_stage")
    if current_stage:
        print(
            f"当前阶段: 阶段 {current_stage['stage_id']} "
            f"({current_stage['chunk_range']})"
        )

    if summary["missing_support_files"]:
        print("\n缺失支持文件:")
        for item in summary["missing_support_files"]:
            print(f"- {item}")

    if summary["pending_chunk_ids"]:
        preview = ", ".join(summary["pending_chunk_ids"][:8])
        print(f"\n待处理 chunk: {preview}")
        if summary["pending_chunk_count"] > 8:
            print(f"- 其余 {summary['pending_chunk_count'] - 8} 个待处理")

    untouched_methods = [name for name, touched in summary["method_card_status"].items() if not touched]
    if untouched_methods:
        print("\n仍是模板的输出:")
        if not summary["book_profile_touched"]:
            print("- outputs/书籍画像.md")
        for name in untouched_methods:
            print(f"- outputs/方法卡/{name}.md")

    if summary["template_only_case_cards"]:
        print(f"\n模板未实填案例卡: {summary['template_only_case_cards']}")

    print("\n下一步建议:")
    print(f"- {recommend_next_step(summary)}")


def build_check_report(project_dir: Path, finalize_mode: bool) -> dict:
    summary = summarize_project(project_dir)
    blocking_issues: list[str] = []
    warnings: list[str] = []

    if summary["chunk_total"] == 0:
        blocking_issues.append("缺少 `chunks/raw/*.md`，无法继续分析。")

    if summary["missing_support_files"]:
        blocking_issues.append("存在缺失的支持文件，先运行 `repair` 修复工程。")

    if summary["pending_chunk_count"] > 0:
        message = f"仍有 {summary['pending_chunk_count']} 个提取卡未完成。"
        if finalize_mode:
            blocking_issues.append(message)
        else:
            warnings.append(message)

    if summary["processed_chunk_count"] > 0 and summary["drafts_touched_count"] == 0:
        warnings.append("已有提取卡被填写，但 3 份 drafts 仍保持模板状态。")

    if not summary["book_profile_touched"]:
        message = "`outputs/书籍画像.md` 仍是模板状态。"
        if finalize_mode:
            blocking_issues.append(message)
        else:
            warnings.append(message)

    untouched_methods = [name for name, touched in summary["method_card_status"].items() if not touched]
    if untouched_methods:
        message = f"仍有 {len(untouched_methods)} 张方法卡保持模板状态。"
        if finalize_mode:
            blocking_issues.append(message)
        else:
            warnings.append(message)

    evidence_gaps = []
    for name in METHOD_CARD_NAMES:
        method_path = project_dir / "outputs" / "方法卡" / f"{name}.md"
        if re.search(r"(?m)^- 关键 chunk：\s*$", read_utf8(method_path)):
            evidence_gaps.append(name)
    if evidence_gaps:
        message = "以下方法卡还没有填入证据锚点：" + "、".join(evidence_gaps)
        if finalize_mode:
            blocking_issues.append(message)
        else:
            warnings.append(message)

    if summary["ready_case_cards"] == 0:
        message = "还没有正式案例卡。"
        if finalize_mode:
            blocking_issues.append(message)
        else:
            warnings.append(message)

    if summary["template_only_case_cards"] > 0:
        warnings.append(f"有 {summary['template_only_case_cards']} 张案例卡仍是原始模板。")

    return {
        "summary": summary,
        "blocking_issues": blocking_issues,
        "warnings": warnings,
    }


def print_check_summary(report: dict, finalize_mode: bool) -> None:
    summary = report["summary"]
    print("\n" + "=" * 60)
    print("拆书工程检查摘要")
    print("=" * 60)
    print(f"- 总 chunk: {summary['chunk_total']}")
    print(f"- 已处理提取卡: {summary['processed_chunk_count']}")
    print(f"- 待处理提取卡: {summary['pending_chunk_count']}")
    print(f"- 草稿更新: {summary['drafts_touched_count']}/3")
    print(f"- 方法卡更新: {summary['method_cards_touched_count']}/{len(METHOD_CARD_NAMES)}")
    print(f"- 书籍画像: {'已更新' if summary['book_profile_touched'] else '未更新'}")
    print(f"- 正式案例卡: {summary['ready_case_cards']}")

    if report["blocking_issues"]:
        title = "收束阻塞项" if finalize_mode else "阻塞项"
        print(f"\n{title}:")
        for item in report["blocking_issues"]:
            print(f"- {item}")

    if report["warnings"]:
        print("\n警告:")
        for item in report["warnings"]:
            print(f"- {item}")


def sync_support_files(project_dir: Path, force: bool) -> list[str]:
    ensure_project_exists(project_dir)
    chunks = load_chunks(project_dir)
    if not chunks:
        raise RuntimeError("缺少 `chunks/raw/*.md`，无法重建工程支撑文件。")

    manifest = load_json_file(project_dir / "workspace" / "manifest.json")
    book_name = str(manifest.get("book_name") or project_dir.name)
    source_txt = load_source_path(project_dir)
    detected_encoding = str(manifest.get("encoding") or "unknown")
    chunk_mode = str(manifest.get("chunk_mode") or chunks[0].source_mode)
    stage_size = load_stage_size(project_dir, manifest)
    source_char_count = int(manifest.get("source_char_count") or max(chunk.end for chunk in chunks))

    touched: list[str] = []

    def sync_file(relative_path: str, content: str) -> None:
        path = project_dir / relative_path
        existed = path.exists()
        write_text(path, content, force)
        if force or not existed:
            touched.append(relative_path)

    for relative in (
        "source",
        "workspace",
        "chunks/raw",
        "extraction-cards",
        "drafts",
        "outputs/方法卡",
        "outputs/案例卡",
    ):
        ensure_dir(project_dir / relative)

    new_manifest = build_project_manifest(
        project_dir=project_dir,
        book_name=book_name,
        source_txt=source_txt,
        detected_encoding=detected_encoding,
        chunks=chunks,
        chunk_mode=chunk_mode,
        source_char_count=source_char_count,
        stage_size=stage_size,
    )

    manifest_path = project_dir / "workspace" / "manifest.json"
    manifest_existed = manifest_path.exists()
    write_json(manifest_path, new_manifest, force)
    if force or not manifest_existed:
        touched.append("workspace/manifest.json")

    if source_txt:
        sync_file("source/source-path.txt", f"{source_txt}\n")

    sync_file("workspace/chunk-index.md", render_chunk_index(chunks))
    sync_file("workspace/stage-plan.md", render_stage_plan(chunks, stage_size))
    sync_file("workspace/use-guide.md", render_use_guide(book_name, len(chunks)))
    sync_file("workspace/案例卡模板.md", build_case_card_template())
    sync_file("workspace/推荐案例类型.md", build_case_type_reference())

    sync_file("drafts/书籍画像草稿.md", build_book_profile_draft(book_name))
    sync_file("drafts/方法候选笔记.md", build_method_candidate_notes())
    sync_file("drafts/案例候选笔记.md", build_case_candidate_notes())

    sync_file("outputs/书籍画像.md", build_book_profile_output())
    sync_file("outputs/方法卡/开篇规划方法卡.md", build_opening_method_card())
    sync_file("outputs/方法卡/大纲设计方法卡.md", build_outline_method_card())
    sync_file("outputs/方法卡/章节类型与写作模式方法卡.md", build_chapter_method_card())
    sync_file("outputs/方法卡/冲突与角色设计方法卡.md", build_conflict_character_method_card())
    sync_file("outputs/方法卡/读者情绪管理方法卡.md", build_emotion_method_card())
    sync_file("outputs/方法卡/长篇创作与文风优化方法卡.md", build_longform_style_method_card())

    for chunk in chunks:
        sync_file(f"extraction-cards/{chunk.chunk_id}.md", render_extraction_card_template(chunk))

    progress_path = project_dir / PROGRESS_FILE
    progress = load_json_file(progress_path)
    if not progress:
        progress = build_progress_state(project_dir, chunks, stage_size, "repaired")
        write_json_file(progress_path, progress)
        touched.append(str(PROGRESS_FILE))
    else:
        update_progress_state(
            project_dir,
            {
                "status": "repaired",
                "last_repaired_at": now_iso(),
                "stage_size": stage_size,
                "chunk_total": len(chunks),
            },
        )
        touched.append(str(PROGRESS_FILE))

    return touched


def handle_init(args: argparse.Namespace) -> int:
    source_txt = Path(args.source_txt).expanduser().resolve()
    if not source_txt.exists():
        raise FileNotFoundError(f"找不到原始文件: {source_txt}")
    if not source_txt.is_file():
        raise FileNotFoundError(f"输入路径不是文件: {source_txt}")
    if args.hard_max_chars < args.target_chars:
        raise ValueError("--hard-max-chars 必须大于或等于 --target-chars")
    if args.stage_size <= 0:
        raise ValueError("--stage-size 必须大于 0")

    text, detected_encoding = read_source_text(source_txt, args.encoding)
    text = normalize_text(text)
    lines = build_lines(text)
    sections, mode = detect_sections(lines, args.min_headings)
    chunks = build_chunks(sections, mode, args.target_chars, args.hard_max_chars)
    if not chunks:
        raise RuntimeError("分块结果为空，请检查输入文本")

    book_name = safe_book_name(args.book_name or source_txt.stem)
    story_root = Path(args.story_root).expanduser()
    project_dir = initialize_project(
        source_txt=source_txt,
        story_root=story_root,
        book_name=book_name,
        detected_encoding=detected_encoding,
        mode=mode,
        chunks=chunks,
        raw_text=text,
        stage_size=args.stage_size,
        copy_source=args.copy_source,
        force=args.force,
    )
    progress = build_progress_state(project_dir, chunks, args.stage_size, "initialized")
    write_json_file(project_dir / PROGRESS_FILE, progress)

    print("\n" + "=" * 60)
    print("拆书工程已初始化")
    print("=" * 60)
    print(f"工程目录: {project_dir.resolve()}")
    print(f"检测编码: {detected_encoding}")
    print(f"分块模式: {mode}")
    print(f"总 chunk: {len(chunks)}")
    print(f"阶段大小: {args.stage_size}")
    print("\n下一步建议:")
    print(f'- 先运行 `python3 scripts/story_analysis_pipeline.py resume "{project_dir.resolve()}"`')
    print(f'- 再按阶段执行 `start-stage` 并填写 `extraction-cards/*.md`')
    return 0


def handle_resume(args: argparse.Namespace) -> int:
    project_dir = Path(args.project_path).expanduser().resolve()
    print_resume_summary(summarize_project(project_dir))
    return 0


def handle_start_stage(args: argparse.Namespace) -> int:
    project_dir = Path(args.project_path).expanduser().resolve()
    summary = summarize_project(project_dir)
    stages = summary["stages"]
    if not stages:
        raise RuntimeError("工程里没有可用阶段，请先检查 `chunks/raw/`。")

    stage_map = {stage.stage_id: stage for stage in stages}
    if args.stage_num not in stage_map:
        raise ValueError(f"无效阶段号: {args.stage_num}")
    stage = stage_map[args.stage_num]

    update_progress_state(
        project_dir,
        {
            "status": "in_progress",
            "current_stage": {
                "stage_id": stage.stage_id,
                "chunk_ids": stage.chunk_ids,
                "chunk_range": stage.chunk_range,
                "started_at": now_iso(),
                "note": args.note or "",
            },
        },
    )

    print("\n" + "=" * 60)
    print("阶段已标记为进行中")
    print("=" * 60)
    print(f"工程目录: {project_dir}")
    print(f"阶段: {stage.stage_id}")
    print(f"Chunk 范围: {stage.chunk_range}")
    print(f"处理顺序: {', '.join(stage.chunk_ids)}")
    if args.note:
        print(f"备注: {args.note}")
    return 0


def handle_check(args: argparse.Namespace) -> int:
    project_dir = Path(args.project_path).expanduser().resolve()
    report = build_check_report(project_dir, finalize_mode=False)
    print_check_summary(report, finalize_mode=False)
    return 1 if report["blocking_issues"] else 0


def handle_finalize(args: argparse.Namespace) -> int:
    project_dir = Path(args.project_path).expanduser().resolve()
    report = build_check_report(project_dir, finalize_mode=True)
    print_check_summary(report, finalize_mode=True)

    if report["blocking_issues"] and not args.force:
        print("\n未完成收束。若你确认要忽略阻塞项，重新运行并加 `--force`。")
        return 1

    summary = report["summary"]
    update_progress_state(
        project_dir,
        {
            "status": "finalized",
            "current_stage": None,
            "finalized_at": now_iso(),
            "final_note": args.note or "",
            "processed_chunks": summary["processed_chunk_count"],
            "ready_case_cards": summary["ready_case_cards"],
        },
    )

    print("\n收束状态已写入 `workspace/analysis-progress.json`。")
    return 0


def handle_repair(args: argparse.Namespace) -> int:
    project_dir = Path(args.project_path).expanduser().resolve()
    touched = sync_support_files(project_dir, force=args.force)
    print("\n" + "=" * 60)
    print("工程支撑文件已同步")
    print("=" * 60)
    if touched:
        for item in touched:
            print(f"- {item}")
    else:
        print("- 没有需要修复或覆盖的文件")
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="君黎AI拆书统一入口")
    subparsers = parser.add_subparsers(dest="command", required=True)

    init_parser = subparsers.add_parser("init", help="初始化新的拆书工程")
    init_parser.add_argument("source_txt", help="原始小说 txt 的绝对路径")
    init_parser.add_argument("--story-root", default="story", help="输出根目录，默认 ./story")
    init_parser.add_argument("--book-name", help="小说名，默认从文件名推断")
    init_parser.add_argument("--encoding", default="auto", help="文本编码，默认 auto")
    init_parser.add_argument("--target-chars", type=int, default=12000, help="目标块大小，默认 12000")
    init_parser.add_argument("--hard-max-chars", type=int, default=18000, help="单块硬上限，默认 18000")
    init_parser.add_argument("--min-headings", type=int, default=3, help="启用章节模式所需最少标题数")
    init_parser.add_argument("--stage-size", type=int, default=8, help="每多少块做一次阶段收束，默认 8")
    init_parser.add_argument("--copy-source", action="store_true", help="复制原始 txt 到工程目录")
    init_parser.add_argument("--force", action="store_true", help="若工程已存在则覆盖同名引导文件；不会清理旧文件")
    init_parser.set_defaults(func=handle_init)

    resume_parser = subparsers.add_parser("resume", help="恢复现有拆书工程摘要")
    resume_parser.add_argument("project_path", help="拆书工程根目录")
    resume_parser.set_defaults(func=handle_resume)

    start_parser = subparsers.add_parser("start-stage", help="标记某个阶段为进行中")
    start_parser.add_argument("project_path", help="拆书工程根目录")
    start_parser.add_argument("stage_num", type=int, help="阶段号")
    start_parser.add_argument("--note", help="阶段备注")
    start_parser.set_defaults(func=handle_start_stage)

    check_parser = subparsers.add_parser("check", help="检查拆书工程当前状态")
    check_parser.add_argument("project_path", help="拆书工程根目录")
    check_parser.set_defaults(func=handle_check)

    finalize_parser = subparsers.add_parser("finalize", help="写入收束状态并做最终检查")
    finalize_parser.add_argument("project_path", help="拆书工程根目录")
    finalize_parser.add_argument("--note", help="收束备注")
    finalize_parser.add_argument("--force", action="store_true", help="忽略阻塞项，强制写入 finalized 状态")
    finalize_parser.set_defaults(func=handle_finalize)

    repair_parser = subparsers.add_parser("repair", help="修复缺失的工程支撑文件")
    repair_parser.add_argument("project_path", help="拆书工程根目录")
    repair_parser.add_argument("--force", action="store_true", help="覆盖同名支撑文件")
    repair_parser.set_defaults(func=handle_repair)

    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    return args.func(args)


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (FileNotFoundError, RuntimeError, ValueError) as exc:
        raise SystemExit(str(exc))


path: scripts/bootstrap_story_analysis.py
content: #!/usr/bin/env python3
"""初始化小说参考资产提炼工程。"""

from __future__ import annotations

import argparse
import json
import re
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable


SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_ROOT = SCRIPT_DIR.parent
TEMPLATES_ROOT = SKILL_ROOT / "references" / "templates"


COMMON_ENCODINGS = (
    "utf-8",
    "utf-8-sig",
    "gb18030",
    "gbk",
    "big5",
)

HEADING_PATTERNS = (
    re.compile(r"^\s*第[0-9零〇一二两三四五六七八九十百千万]+[章回节卷部篇幕集].*$"),
    re.compile(r"^\s*(chapter|chap\.?)\s+\d+.*$", re.IGNORECASE),
    re.compile(r"^\s*(prologue|epilogue|番外|序章|楔子|尾声|后记).*$", re.IGNORECASE),
)

METHOD_CARD_NAMES = (
    "开篇规划方法卡",
    "大纲设计方法卡",
    "章节类型与写作模式方法卡",
    "冲突与角色设计方法卡",
    "读者情绪管理方法卡",
    "长篇创作与文风优化方法卡",
)

@dataclass
class LineInfo:
    text: str
    start: int
    end: int


@dataclass
class Section:
    title: str
    start: int
    end: int
    text: str
    headings: list[str]

    @property
    def char_count(self) -> int:
        return self.end - self.start


@dataclass
class Chunk:
    chunk_id: str
    title: str
    start: int
    end: int
    text: str
    headings: list[str]
    source_mode: str

    @property
    def char_count(self) -> int:
        return self.end - self.start


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="初始化小说参考资产提炼工程")
    parser.add_argument("source_txt", help="原始小说 txt 的绝对路径")
    parser.add_argument("--story-root", default="story", help="输出根目录，默认 ./story")
    parser.add_argument("--book-name", help="小说名，默认从文件名推断")
    parser.add_argument("--encoding", default="auto", help="文本编码，默认 auto")
    parser.add_argument("--target-chars", type=int, default=12000, help="目标块大小，默认 12000")
    parser.add_argument("--hard-max-chars", type=int, default=18000, help="单块硬上限，默认 18000")
    parser.add_argument("--min-headings", type=int, default=3, help="启用章节模式所需最少标题数")
    parser.add_argument("--stage-size", type=int, default=8, help="每多少块做一次阶段收束，默认 8")
    parser.add_argument("--copy-source", action="store_true", help="复制原始 txt 到工程目录")
    parser.add_argument("--force", action="store_true", help="若工程已存在则覆盖同名引导文件；不会清理旧文件")
    return parser.parse_args()


def read_text(path: Path, encoding: str) -> tuple[str, str]:
    if encoding != "auto":
        return path.read_text(encoding=encoding), encoding

    last_error: Exception | None = None
    for candidate in COMMON_ENCODINGS:
        try:
            return path.read_text(encoding=candidate), candidate
        except UnicodeDecodeError as exc:
            last_error = exc
    if last_error is not None:
        raise last_error
    raise RuntimeError("无法识别文本编码")


def normalize_text(text: str) -> str:
    text = text.replace("\ufeff", "")
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    text = text.replace("\u3000", "  ")
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip() + "\n"


def safe_book_name(raw_name: str) -> str:
    name = raw_name.strip().replace("/", "-").replace("\\", "-")
    return name or "未命名小说"


def build_lines(text: str) -> list[LineInfo]:
    lines: list[LineInfo] = []
    offset = 0
    for raw_line in text.splitlines(keepends=True):
        start = offset
        end = offset + len(raw_line)
        lines.append(LineInfo(text=raw_line, start=start, end=end))
        offset = end
    if not lines:
        lines.append(LineInfo(text="", start=0, end=0))
    return lines


def is_heading(line: str) -> bool:
    stripped = line.strip()
    if not stripped or len(stripped) > 80:
        return False
    return any(pattern.match(stripped) for pattern in HEADING_PATTERNS)


def detect_sections(lines: list[LineInfo], min_headings: int) -> tuple[list[Section], str]:
    heading_indexes = [index for index, line in enumerate(lines) if is_heading(line.text)]
    if len(heading_indexes) < min_headings:
        full_text = "".join(line.text for line in lines)
        return [Section(title="全文", start=0, end=len(full_text), text=full_text, headings=[])], "length"

    sections: list[Section] = []
    if heading_indexes[0] > 0:
        preface_text = "".join(line.text for line in lines[: heading_indexes[0]])
        if preface_text.strip():
            sections.append(
                Section(
                    title="开篇",
                    start=lines[0].start,
                    end=lines[heading_indexes[0]].start,
                    text=preface_text,
                    headings=["开篇"],
                )
            )

    for position, start_index in enumerate(heading_indexes):
        end_index = heading_indexes[position + 1] if position + 1 < len(heading_indexes) else len(lines)
        title = lines[start_index].text.strip()
        text = "".join(line.text for line in lines[start_index:end_index])
        sections.append(
            Section(
                title=title,
                start=lines[start_index].start,
                end=lines[end_index - 1].end if end_index > start_index else lines[start_index].end,
                text=text,
                headings=[title],
            )
        )
    return sections, "chapter"


def split_section_by_length(section: Section, target_chars: int, hard_max_chars: int) -> list[Section]:
    if section.char_count <= hard_max_chars:
        return [section]

    lines = build_lines(section.text)
    pieces: list[Section] = []
    start_idx = 0
    piece_index = 1

    while start_idx < len(lines):
        current_chars = 0
        cut_idx = start_idx
        last_blank_idx: int | None = None

        while cut_idx < len(lines):
            current_chars += len(lines[cut_idx].text)
            if not lines[cut_idx].text.strip():
                last_blank_idx = cut_idx + 1
            next_idx = cut_idx + 1
            if current_chars >= target_chars and last_blank_idx is not None and last_blank_idx > start_idx:
                cut_idx = last_blank_idx
                break
            if current_chars >= hard_max_chars:
                cut_idx = last_blank_idx if last_blank_idx is not None and last_blank_idx > start_idx else next_idx
                break
            cut_idx = next_idx

        if cut_idx <= start_idx:
            cut_idx = min(start_idx + 1, len(lines))

        part_lines = lines[start_idx:cut_idx]
        text = "".join(line.text for line in part_lines)
        start = section.start + part_lines[0].start
        end = section.start + part_lines[-1].end
        pieces.append(
            Section(
                title=f"{section.title} / part {piece_index}",
                start=start,
                end=end,
                text=text,
                headings=section.headings[:],
            )
        )
        piece_index += 1
        start_idx = cut_idx

    return pieces


def build_chunks(sections: list[Section], mode: str, target_chars: int, hard_max_chars: int) -> list[Chunk]:
    prepared_sections: list[Section] = []
    for section in sections:
        prepared_sections.extend(split_section_by_length(section, target_chars, hard_max_chars))

    chunks: list[Chunk] = []
    bucket: list[Section] = []
    bucket_chars = 0

    def flush_bucket() -> None:
        nonlocal bucket, bucket_chars
        if not bucket:
            return
        chunk_number = len(chunks) + 1
        start = bucket[0].start
        end = bucket[-1].end
        text = "".join(section.text for section in bucket)
        headings: list[str] = []
        for section in bucket:
            headings.extend(section.headings)
        title = " / ".join(headings[:3]) if headings else f"chunk-{chunk_number:04d}"
        if len(headings) > 3:
            title = f"{title} 等 {len(headings)} 段"
        chunks.append(
            Chunk(
                chunk_id=f"{chunk_number:04d}",
                title=title,
                start=start,
                end=end,
                text=text,
                headings=headings,
                source_mode=mode,
            )
        )
        bucket = []
        bucket_chars = 0

    for section in prepared_sections:
        if bucket and bucket_chars + section.char_count > target_chars:
            flush_bucket()
        bucket.append(section)
        bucket_chars += section.char_count
        if bucket_chars >= hard_max_chars:
            flush_bucket()

    flush_bucket()
    return chunks


def ensure_dir(path: Path) -> None:
    path.mkdir(parents=True, exist_ok=True)


def write_text(path: Path, content: str, force: bool) -> None:
    if path.exists() and not force:
        return
    ensure_dir(path.parent)
    path.write_text(content, encoding="utf-8")


def write_json(path: Path, data: object, force: bool) -> None:
    if path.exists() and not force:
        return
    ensure_dir(path.parent)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def load_reference_template(*relative_parts: str) -> str:
    template_path = TEMPLATES_ROOT.joinpath(*relative_parts)
    return template_path.read_text(encoding="utf-8")


def ensure_project_can_initialize(project_dir: Path, force: bool) -> None:
    if not project_dir.exists():
        return
    if force:
        return
    if not project_dir.is_dir():
        raise FileExistsError(f"目标路径已存在且不是目录: {project_dir.resolve()}")
    if not any(project_dir.iterdir()):
        return

    raise FileExistsError(
        "工程已存在："
        f"{project_dir.resolve()}\n"
        "bootstrap 默认只用于初始化新工程，不会静默合并或刷新已有文件。\n"
        "如果你只是继续分析，请直接在现有工程里工作；"
        "如果你明确要覆盖同名引导文件，请重新运行并加上 --force；"
        "如果你要彻底重建工程，请先删除旧目录或换一个 --story-root。"
    )


def render_chunk_raw(source_txt: Path, chunk: Chunk) -> str:
    headings = "\n".join(f"- {item}" for item in chunk.headings) if chunk.headings else "- 无明确章节标题"
    return (
        f"# Chunk {chunk.chunk_id}\n\n"
        f"- 标题：{chunk.title}\n"
        f"- 来源文件：{source_txt}\n"
        f"- 起始字符：{chunk.start}\n"
        f"- 结束字符：{chunk.end}\n"
        f"- 字符数：{chunk.char_count}\n"
        f"- 分块模式：{chunk.source_mode}\n\n"
        "## 覆盖章节\n\n"
        f"{headings}\n\n"
        "## 原文\n\n"
        f"{chunk.text.strip()}\n"
    )


def render_extraction_card_template(chunk: Chunk) -> str:
    headings = "\n".join(f"- {item}" for item in chunk.headings) if chunk.headings else "- 无明确章节标题"
    return (
        f"# 资产提取卡 {chunk.chunk_id}\n\n"
        f"- 标题：{chunk.title}\n"
        f"- 字符数：{chunk.char_count}\n"
        f"- 对应原文：../chunks/raw/{chunk.chunk_id}.md\n\n"
        "## 覆盖章节\n\n"
        f"{headings}\n\n"
        "## 这一块的主要叙事功能\n\n"
        "- 先写主功能，再补次功能。可参考：开篇立人 / 开篇钩子 / 催化剂 / 首次升级 / 推进 / 过渡 / 章末钩子 / 情绪补偿 / 代价显化 / 卷末爆点 / 地图切换 / 规则更新 / 对手更换 / 目标调整 / 文风节奏片段 / 其他。\n\n"
        "## 关键内容\n\n"
        "- 只写 3-5 条关键动作或变化，不写长剧情。\n\n"
        "## 方法候选\n\n"
        "### 支撑哪张方法卡\n\n"
        "- 开篇规划 / 大纲设计 / 章节类型与写作模式 / 冲突与角色设计 / 读者情绪管理 / 长篇创作与文风优化\n\n"
        "### 可提炼的写法动作\n\n"
        "- 这段到底体现了什么可迁移的写法。\n\n"
        "### 为什么有效\n\n"
        "- 这段为什么对读者有效，或者为什么对结构有效。\n\n"
        "### 可量化参数\n\n"
        "- 尽量补时机、频率、间隔、占比、篇幅、章节位置等可量化信息。\n\n"
        "## 案例候选\n\n"
        "### 候选类型或标签\n\n"
        "- 可从推荐类型里选，也可自定义。优先写有明确写法价值的标签，例如：开篇立人 / 开篇钩子 / 催化剂 / 首次升级 / 重要选择 / 推进章 / 过渡章 / 章末钩子 / 情绪补偿 / 代价显化 / 关系冲突 / 卷末爆点 / 地图切换 / 规则更新 / 目标调整 / 文风节奏片段。\n\n"
        "### 典型程度\n\n"
        "- 强 / 中 / 弱\n\n"
        "### 是否需要跨 chunk 合并\n\n"
        "- 如果单块不完整，就写明还要合并哪些 chunk。\n\n"
        "### 值不值得正式建卡\n\n"
        "- 写“立即建卡 / 继续观察 / 只留候选”，不要强凑数量。\n\n"
        "## 书籍画像信号\n\n"
        "- 这块补充了哪些题材、风格、节奏、钩子、情绪或扩长信号。\n\n"
        "## 关键证据\n\n"
        "- 摘 1-3 个最关键的证据点，不要大段抄原文。\n\n"
        "## 可学 / 不可照搬\n\n"
        "- 哪些可以学成方法，哪些只是具体桥段、设定或题材红利。\n\n"
        "## 写回草稿\n\n"
        "### 书籍画像草稿\n\n"
        "- 要写回什么。\n\n"
        "### 方法候选笔记\n\n"
        "- 要写回什么。\n\n"
        "### 案例候选笔记\n\n"
        "- 要写回什么。\n"
    )


def render_chunk_index(chunks: Iterable[Chunk]) -> str:
    rows = [
        "| Chunk | 标题 | 字符数 | 起始字符 | 结束字符 | 模式 |",
        "|---|---|---:|---:|---:|---|",
    ]
    for chunk in chunks:
        rows.append(
            f"| {chunk.chunk_id} | {chunk.title} | {chunk.char_count} | "
            f"{chunk.start} | {chunk.end} | {chunk.source_mode} |"
        )
    return "# Chunk Index\n\n" + "\n".join(rows) + "\n"


def render_stage_plan(chunks: list[Chunk], stage_size: int) -> str:
    lines = ["# 阶段计划", "", f"- 建议每 {stage_size} 个 chunk 做一次资产收束。", ""]
    for index in range(0, len(chunks), stage_size):
        batch = chunks[index : index + stage_size]
        batch_id = index // stage_size + 1
        lines.append(f"## 阶段 {batch_id}")
        lines.append("")
        lines.append(f"- Chunk 范围：{batch[0].chunk_id} - {batch[-1].chunk_id}")
        lines.append(f"- 处理顺序：{', '.join(chunk.chunk_id for chunk in batch)}")
        lines.append("- 阶段结束后，更新 3 份草稿：`drafts/书籍画像草稿.md`、`drafts/方法候选笔记.md`、`drafts/案例候选笔记.md`。")
        lines.append("")
    return "\n".join(lines) + "\n"


def render_use_guide(book_name: str, chunk_count: int) -> str:
    return f"""# 使用说明

## 目标

把《{book_name}》拆成一个创作参考包，而不是剧情总结。

固定产出只有：

- 1 份书籍画像
- 6 张方法卡
- 若干张案例卡

## 正常使用顺序

1. 先读 `workspace/chunk-index.md` 和 `workspace/stage-plan.md`
2. 逐块阅读 `chunks/raw/*.md`
3. 每块填写一张 `extraction-cards/*.md`
4. 每个阶段更新 3 份草稿：
   `drafts/书籍画像草稿.md`
   `drafts/方法候选笔记.md`
   `drafts/案例候选笔记.md`
5. 全书读完后，再整理 `outputs/` 里的 `1 + 6 + N`
6. 案例卡只要值得建就记录，不设上限；命名建议用 `NN-案例类型-核心动作.md`

## 处理总数

- 分块数量：{chunk_count}

## 硬约束

- 不把剧情复述当主产品
- 不把文学评论当主产品
- 每个 chunk 都要先判断它支撑哪张方法卡、能不能进某类案例卡
- 案例卡不需要凑数，只保留真的值得参考的片段
- 最终一切都服务创作参考，不服务赏析完整性
"""


def build_book_profile_draft(book_name: str) -> str:
    return f"""# 书籍画像草稿

## 基本定位
- 书名：{book_name}
- 题材：
- 子题材：
- 面向读者：
- 篇幅体量：

## 风格信号
- 节奏快慢：
- 钩子密度：
- 爽点强度：
- 压抑强度：
- 对话密度：
- 动作描写占比：
- 心理描写占比：

## 结构与扩长
- 开篇路数：
- 首次升级时机：
- 常见章节类型：
- 长篇扩展方式：

## 最值得参考的模块
- 开篇规划：
- 大纲设计：
- 章节写法：
- 冲突与角色：
- 情绪管理：
- 长篇与文风：

## 值得参考的方向
- 最值得参考什么：
- 最依赖题材的地方：
- 最不能乱学的地方：
"""


def build_method_candidate_notes() -> str:
    sections = []
    for name in METHOD_CARD_NAMES:
        sections.append(
            f"## {name}\n\n"
            "- chunk 证据：\n"
            "- 这本书怎么做：\n"
            "- 为什么有效：\n"
            "- 可复用写法：\n"
            "- 参数或频率：\n"
            "- 适用边界：\n"
            "- 不可照搬：\n"
        )
    return "# 方法候选笔记\n\n" + "\n".join(sections)


def build_case_candidate_notes() -> str:
    return (
        "# 案例候选笔记\n\n"
        "## 使用规则\n"
        "- 案例卡不设上限，只要“值得检索、值得复用、值得对照”就记录。\n"
        "- 一个案例可以同时打多个标签，但要有主标签。\n"
        "- 优先收录能稳定说明写法价值的案例，而不是只因为桥段精彩。\n"
        "- 命名建议：`NN-案例类型-核心动作.md`\n\n"
        f"{build_case_type_reference().strip()}\n\n"
        "## 候选模板\n\n"
        "### 案例候选 01\n"
        "- 候选 chunk：\n"
        "- 主标签：\n"
        "- 次标签：\n"
        "- 对应方法卡：\n"
        "- 典型程度：强 / 中 / 弱\n"
        "- 最适合参考什么场景：\n"
        "- 为什么值得建正式案例卡：\n"
        "- 是否需要跨 chunk 合并：\n"
        "- 关键证据：\n"
        "- 暂时不建卡的原因：\n"
    )


def build_book_profile_output() -> str:
    return load_reference_template("书籍画像模板.md")


def build_opening_method_card() -> str:
    return load_reference_template("方法卡", "开篇规划方法卡模板.md")


def build_outline_method_card() -> str:
    return load_reference_template("方法卡", "大纲设计方法卡模板.md")


def build_chapter_method_card() -> str:
    return load_reference_template("方法卡", "章节类型与写作模式方法卡模板.md")


def build_conflict_character_method_card() -> str:
    return load_reference_template("方法卡", "冲突与角色设计方法卡模板.md")


def build_emotion_method_card() -> str:
    return load_reference_template("方法卡", "读者情绪管理方法卡模板.md")


def build_longform_style_method_card() -> str:
    return load_reference_template("方法卡", "长篇创作与文风优化方法卡模板.md")


def build_case_card_template() -> str:
    return load_reference_template("案例卡", "案例卡模板.md")


def build_case_type_reference() -> str:
    return load_reference_template("案例卡", "推荐案例类型.md")


def initialize_project(
    source_txt: Path,
    story_root: Path,
    book_name: str,
    detected_encoding: str,
    mode: str,
    chunks: list[Chunk],
    raw_text: str,
    stage_size: int,
    copy_source: bool,
    force: bool,
) -> Path:
    project_dir = story_root / book_name
    ensure_project_can_initialize(project_dir, force)
    ensure_dir(project_dir)

    for relative in (
        "source",
        "workspace",
        "chunks/raw",
        "extraction-cards",
        "drafts",
        "outputs/方法卡",
        "outputs/案例卡",
    ):
        ensure_dir(project_dir / relative)

    if copy_source:
        destination = project_dir / "source" / "original.txt"
        if force or not destination.exists():
            shutil.copyfile(source_txt, destination)

    manifest = {
        "book_name": book_name,
        "source_txt": str(source_txt),
        "project_dir": str(project_dir.resolve()),
        "encoding": detected_encoding,
        "chunk_mode": mode,
        "chunk_count": len(chunks),
        "source_char_count": len(raw_text),
        "stage_size": stage_size,
        "outputs": {
            "book_profile": "outputs/书籍画像.md",
            "method_cards": [f"outputs/方法卡/{name}.md" for name in METHOD_CARD_NAMES],
            "case_cards_dir": "outputs/案例卡",
            "case_card_template": "workspace/案例卡模板.md",
            "case_type_reference": "workspace/推荐案例类型.md",
        },
    }

    write_json(project_dir / "workspace" / "manifest.json", manifest, force)
    write_text(project_dir / "workspace" / "chunk-index.md", render_chunk_index(chunks), force)
    write_text(project_dir / "workspace" / "stage-plan.md", render_stage_plan(chunks, stage_size), force)
    write_text(project_dir / "workspace" / "use-guide.md", render_use_guide(book_name, len(chunks)), force)
    write_text(project_dir / "workspace" / "案例卡模板.md", build_case_card_template(), force)
    write_text(project_dir / "workspace" / "推荐案例类型.md", build_case_type_reference(), force)
    write_text(project_dir / "source" / "source-path.txt", f"{source_txt}\n", force)

    write_text(project_dir / "drafts" / "书籍画像草稿.md", build_book_profile_draft(book_name), force)
    write_text(project_dir / "drafts" / "方法候选笔记.md", build_method_candidate_notes(), force)
    write_text(project_dir / "drafts" / "案例候选笔记.md", build_case_candidate_notes(), force)

    write_text(project_dir / "outputs" / "书籍画像.md", build_book_profile_output(), force)
    write_text(project_dir / "outputs" / "方法卡" / "开篇规划方法卡.md", build_opening_method_card(), force)
    write_text(project_dir / "outputs" / "方法卡" / "大纲设计方法卡.md", build_outline_method_card(), force)
    write_text(project_dir / "outputs" / "方法卡" / "章节类型与写作模式方法卡.md", build_chapter_method_card(), force)
    write_text(project_dir / "outputs" / "方法卡" / "冲突与角色设计方法卡.md", build_conflict_character_method_card(), force)
    write_text(project_dir / "outputs" / "方法卡" / "读者情绪管理方法卡.md", build_emotion_method_card(), force)
    write_text(project_dir / "outputs" / "方法卡" / "长篇创作与文风优化方法卡.md", build_longform_style_method_card(), force)

    for chunk in chunks:
        write_text(project_dir / "chunks" / "raw" / f"{chunk.chunk_id}.md", render_chunk_raw(source_txt, chunk), force)
        write_text(project_dir / "extraction-cards" / f"{chunk.chunk_id}.md", render_extraction_card_template(chunk), force)

    return project_dir


def main() -> None:
    args = parse_args()
    source_txt = Path(args.source_txt).expanduser().resolve()
    if not source_txt.exists():
        raise FileNotFoundError(f"找不到原始文件: {source_txt}")
    if not source_txt.is_file():
        raise FileNotFoundError(f"输入路径不是文件: {source_txt}")
    if args.hard_max_chars < args.target_chars:
        raise ValueError("--hard-max-chars 必须大于或等于 --target-chars")
    if args.stage_size <= 0:
        raise ValueError("--stage-size 必须大于 0")

    text, detected_encoding = read_text(source_txt, args.encoding)
    text = normalize_text(text)
    lines = build_lines(text)
    sections, mode = detect_sections(lines, args.min_headings)
    chunks = build_chunks(sections, mode, args.target_chars, args.hard_max_chars)
    if not chunks:
        raise RuntimeError("分块结果为空，请检查输入文本")

    book_name = safe_book_name(args.book_name or source_txt.stem)
    story_root = Path(args.story_root).expanduser()
    project_dir = initialize_project(
        source_txt=source_txt,
        story_root=story_root,
        book_name=book_name,
        detected_encoding=detected_encoding,
        mode=mode,
        chunks=chunks,
        raw_text=text,
        stage_size=args.stage_size,
        copy_source=args.copy_source,
        force=args.force,
    )

    print(f"工程已创建：{project_dir.resolve()}")
    print(f"检测编码：{detected_encoding}")
    print(f"分块模式：{mode}")
    print(f"分块数量：{len(chunks)}")
    print(f"阶段大小：{args.stage_size}")

if __name__ == "__main__":
    try:
        main()
    except (FileExistsError, FileNotFoundError, RuntimeError, ValueError) as exc:
        raise SystemExit(str(exc))


path: references/quality-checklist.md
content: # 拆书质检清单

用于 `君黎AI拆书` 在正式交付前做自检。

## 1. 工作区是否可继续使用

- [ ] `workspace/manifest.json`、`chunk-index.md`、`stage-plan.md`、`use-guide.md` 都存在
- [ ] `drafts/` 下 3 份草稿都存在
- [ ] `outputs/书籍画像.md` 和 6 张方法卡都存在
- [ ] `chunks/raw/` 与 `extraction-cards/` 数量一致
- [ ] 如工程断档，先运行 `python3 scripts/story_analysis_pipeline.py repair <工程目录>`

## 2. 提取卡层

- [ ] 每个已处理 chunk 都回答了 5 件事：叙事功能、方法归属、案例候选、书籍画像信号、关键证据
- [ ] 提取卡不是长剧情复述，也不是读后感
- [ ] 关键证据不是空行，且能回指原文
- [ ] 典型程度、是否建卡、是否跨 chunk 合并已明确

## 3. 草稿层

- [ ] `书籍画像草稿` 已形成题材、风格、节奏、扩长方式和适用边界判断
- [ ] `方法候选笔记` 里 6 个模块都不是空壳
- [ ] `案例候选笔记` 已按“值得检索、值得复用、值得对照”筛过强弱
- [ ] 草稿在阶段边界有更新，不是等到最后一次性硬写

## 4. 书籍画像

- [ ] 说明了“这本书最适合参考什么、最不适合照搬什么”
- [ ] 说明了“不适合直接学什么”
- [ ] 强项与弱项判断清楚，没有把所有模块都写成平均强
- [ ] 不是把剧情简介换个标题

## 5. 方法卡

- [ ] 6 张方法卡都已实填，不是模板状态
- [ ] 每张卡都有明确 chunk 证据锚点
- [ ] 每张卡都回答了：这本书怎么做、为什么有效、能学什么、有哪些节奏或结构特征、什么不能照搬
- [ ] 规律来自跨 chunk 归纳，而不是单段桥段强行上纲
- [ ] 没把单本书的题材红利误写成通用铁律

## 6. 案例卡

- [ ] 只保留真正值得建卡的案例，不为了凑数硬做弱案例
- [ ] 每张案例卡都说明了最适合参考什么场景
- [ ] 每张案例卡都写清了关键动作链、为什么有效、不可照搬
- [ ] 标签是可检索的，不是只写泛词
- [ ] 如果案例依赖跨 chunk，已经明确合并范围

## 7. 收束判断

- [ ] 如果还有大量 `extraction-cards/*.md` 未处理，不要急着交付
- [ ] 如果 `outputs/书籍画像.md` 或方法卡仍是模板，先回去补
- [ ] 如果没有正式案例卡，先判断是真的没有强案例，还是筛选没做完
- [ ] 如使用项目脚本，正式交付前至少先跑一次：

```bash
python3 scripts/story_analysis_pipeline.py check <工程目录>
```

## 8. 一条底线

- [ ] 最终交付的是创作参考资产，不是剧情报告、文学赏析或读后感合集


path: references/analysis-pipeline.md
content: # 处理逻辑

## 最小逻辑

这套技能只有一条主线：

`原文 -> 提取卡 -> 草稿 -> 1 + 6 + N`

## 1. 原文层

原文只做分块，不在这里追求大结论。

目的：

- 让长篇小说变得可逐块处理
- 避免整本书一次塞进上下文

## 2. 提取卡层

每个 chunk 都转成一张资产提取卡。

提取卡只负责回答：

- 叙事功能
- 方法归属
- 案例候选
- 风格信号
- 关键证据

## 3. 草稿层

草稿只有 3 份：

- 书籍画像草稿
- 方法候选笔记
- 案例候选笔记

作用：

- 把分散的 chunk 信息收成可编辑的候选池
- 避免最后全靠记忆硬写

## 4. 输出层

输出层固定是：

- 书籍画像
- 6 张方法卡
- 若干张案例卡

### 书籍画像

作用：

- 判断这本书是什么路数
- 判断最适合参考什么

### 方法卡

作用：

- 把跨 chunk 的规律抽出来
- 沉淀成可复用的写法参考

### 案例卡

作用：

- 把最典型的具体片段留下来
- 用于后续检索和对照复用

## 最重要的判断

处理每段内容时，不要先想“这段剧情讲了什么”。

要先想：

1. 这段能提炼什么方法？
2. 这段能不能成为案例？
3. 这段让我们更确定这本书是什么风格？

## 两种处理节奏

### 顺序滚动提取

适合：

- 首次完整通读
- 追人物和伏笔
- 建立基本风格判断

### 阶段收束

适合：

- 每 8 个 chunk 或每卷结束
- 补足方法卡和案例卡的空槽
- 收束风格判断

默认做法：

先滚动提取，再阶段收束。


path: references/output-templates.md
content: # 输出模板索引

## 使用说明

- 固定产出是 `1 份书籍画像 + 6 张方法卡 + 若干张案例卡`
- 这些文件是模板单一来源，bootstrap 会直接从这里复制到工作区和输出骨架
- 案例卡不固定数量，统一使用一套案例卡模板

## 模板目录

### 书籍画像

- `references/templates/书籍画像模板.md`

### 方法卡

- `references/templates/方法卡/开篇规划方法卡模板.md`
- `references/templates/方法卡/大纲设计方法卡模板.md`
- `references/templates/方法卡/章节类型与写作模式方法卡模板.md`
- `references/templates/方法卡/冲突与角色设计方法卡模板.md`
- `references/templates/方法卡/读者情绪管理方法卡模板.md`
- `references/templates/方法卡/长篇创作与文风优化方法卡模板.md`

### 案例卡

- `references/templates/案例卡/案例卡模板.md`
- `references/templates/案例卡/推荐案例类型.md`


path: references/practical-workflow.md
content: # 实用工作流

## 目标

这不是“读后感技能”，也不是“剧情压缩技能”。

它的目标是把一本小说拆成可复用的创作参考资产：

- 1 份书籍画像
- 6 张方法卡
- 若干张案例卡

## 最稳的顺序

1. 整书定位
2. 按 chunk 提取资产
3. 阶段性汇总
4. 收束成 `1 + 6 + N`

## 为什么这样最稳

因为你要的是：

- 一本书的整体定位
- 可复用的方法总结
- 可反复检索的具体案例

如果一上来就写总报告，最后得到的多半还是剧情总结。

## 四个阶段各做什么

### 整书定位

只判断这本书的路数：

- 题材
- 风格
- 节奏
- 情绪模式
- 扩长方式
- 最值得参考什么

### 按 chunk 提取资产

每个 chunk 只做 5 件事：

1. 定叙事功能
2. 归方法模块
3. 判案例候选
4. 记风格信号
5. 留关键证据

### 阶段性汇总

阶段结束后不要重读全书，只补空槽：

- 哪张方法卡证据不够
- 哪些案例值得升格为正式案例卡
- 书籍画像里哪些信号还模糊

### 收束输出

最后再整理成：

- `书籍画像.md`
- 6 张方法卡
- 所有值得保留的案例卡

## 一条底线

所有中间动作都服务最终资产，不服务分析完整性。


path: references/mode-selection.md
content: # 处理模式选择

## 结论先行

对于几十万到上百万字的商业网文，默认选混合模式：

1. 先按章节顺序滚动提取 `extraction-cards`
2. 再在卷末或阶段边界做一次草稿收束
3. 全书结束后，整理成 `1 + 6 + N`

不要一开始就写最终卡片，也不要先写总报告。

## 两种模式的适用场景

### 1. 顺序滚动提取

更适合：

- 首次完整通读
- 追人物、伏笔、情绪和风格信号
- 为方法卡和案例卡收集原始候选
- 连载中小说的增量拆解

优势：

- 不容易错过具体写法动作
- 更容易保留证据
- 更适合判断某段到底该进方法卡还是案例卡

风险：

- 读到后面时前面材料会散
- 方法候选和案例候选会越积越多
- 如果不阶段收束，最后很难整理成 `1 + 6 + N`

应对：

- 每个阶段结束就更新 3 份草稿
- 明确把 chunk 信息写回“画像 / 方法 / 案例”三个去向
- 不在 chunk 层写大结论

### 2. 阶段收束

更适合：

- 每 8 个 chunk 或每卷结束
- 需要把零散证据压成方法候选
- 需要筛选最典型的案例
- 需要修正书籍画像判断

优势：

- 能把零散笔记收束成可用资产
- 能及时淘汰弱案例和伪规律
- 能防止最后所有卡片都写得很虚

风险：

- 阶段收束太早，容易把单本书特性误判成稳定方法
- 阶段收束太晚，容易材料堆积失控
- 若没有证据，方法卡会变成空话

应对：

- 方法卡先写“候选”，不要急着写死
- 案例卡优先选“功能完整、效果明显”的片段
- 每条方法和案例都回看对应 chunk 证据

## 默认策略：混合模式

最稳的做法不是二选一，而是两者拼起来。

推荐顺序：

1. 用滚动提取收集 chunk 资产
2. 用阶段收束更新 3 份草稿
3. 全书读完后再整理 `1 + 6 + N`

这样做的原因：

- 滚动提取更擅长保留原始写法动作和证据
- 阶段收束更擅长沉淀可复用资产
- 混合模式同时降低“材料丢失”和“最后收不回来”两个风险

## 一条简单准则

如果当前任务更像“收集原料”，优先滚动提取。

如果当前任务更像“把原料压成资产”，优先阶段收束。

如果两者都要，就用混合模式。


path: references/chunk-card-guide.md
content: # 资产提取卡填写指南

## 这张卡的角色

`extraction-cards/*.md` 不是摘要卡，也不是章节笔记。

它的作用只有一个：

把每个 chunk 里的原料稳定提取出来，供后面生成：

- 书籍画像
- 方法卡
- 案例卡

## 每张卡必须回答的 5 件事

### 1. 这块的主要叙事功能是什么

优先写主功能，再补次功能。

可参考这些标签，但不限制在这里：

- 开篇立人
- 开篇钩子
- 催化剂
- 首次升级
- 推进
- 过渡
- 章末钩子
- 情绪补偿
- 代价显化
- 卷末爆点
- 地图切换
- 规则更新
- 对手更换
- 目标调整
- 文风节奏片段
- 其他

### 2. 这块支撑哪张方法卡

从 6 个模块里判断主归属：

- 开篇规划
- 大纲设计
- 章节类型与写作模式
- 冲突与角色设计
- 读者情绪管理
- 长篇创作与文风优化

### 3. 这块能不能成为案例候选

案例卡不设固定 8 类。

你要做的是：

- 给它打 1 个主标签
- 需要时补 1-3 个次标签
- 判断它是强候选 / 中候选 / 弱候选
- 判断它值不值得正式建卡

优先选那些：

- 功能完整
- 效果明显
- 有明确写法价值
- 不依赖超长上下文也能说明白

### 4. 这块给书籍画像补了什么

优先记录这些信号：

- 题材 / 子题材
- 节奏快慢
- 钩子强弱
- 爽点 / 压点模式
- 情绪补偿周期
- 对话密度
- 动作 / 心理 / 解释占比
- 长篇扩展方式

### 5. 关键证据是什么

只留最关键的 1-3 个证据点。

用途：

- 证明方法卡不是空话
- 证明案例卡不是凭印象写的
- 证明书籍画像不是拍脑袋判断

## 额外两件事

### 可学

这段到底能学什么动作、什么节奏、什么写法。

### 不可照搬

这段里哪些部分只是：

- 具体桥段
- 题材红利
- 设定包装
- 角色关系组合

它们不该被直接复制。

## 一条底线

如果一张提取卡读起来像“剧情复述加一点感想”，就说明写偏了。


path: references/templates/书籍画像模板.md
content: # 书籍画像

## 基本定位
- 题材：
- 子题材：
- 面向读者：
- 篇幅体量：

## 风格特征
- 节奏快慢：
- 钩子密度：
- 爽点强度：
- 压抑强度：
- 对话密度：
- 动作描写占比：
- 心理描写占比：

## 结构特征
- 开篇路数：
- 首次升级时机：
- 常见章节类型：
- 卷末处理方式：
- 长篇扩展方式：

## 最值得参考的模块
- 开篇规划：
- 大纲设计：
- 章节写法：
- 冲突与角色：
- 情绪管理：
- 长篇与文风：

## 核心卖点
- 这本书最抓人的地方：
- 最值得学的地方：
- 最依赖题材的地方：

## 使用建议
- 适合参考的创作场景：
- 不适合直接参考的部分：


path: references/templates/案例卡/推荐案例类型.md
content: # 推荐案例类型

- 这些类型只是推荐池，不是硬上限。
- 只要某段内容对创作有复用价值，就可以新建案例卡。

## 开篇规划
- 开篇立人
- 开篇钩子
- 首章架构
- 核心冲突亮相
- 催化剂

## 大纲设计
- 首次升级
- 重要选择
- 阶段过渡
- 主线刷新
- 卷末爆点

## 章节类型与写作模式
- 推进章
- 过渡章
- 章末钩子
- 信息章不水
- 多功能复合章

## 冲突与角色设计
- 主角犯错
- 代价显化
- 破局动作
- 对手施压
- 关系冲突

## 读者情绪管理
- 蓄压
- 情绪补偿
- 反杀兑现
- 释压换挡
- 追读悬停

## 长篇创作与文风优化
- 地图切换
- 规则更新
- 对手更换
- 目标调整
- 文风节奏片段


path: references/templates/案例卡/案例卡模板.md
content: # 案例卡模板

## 命名规则
- 建议文件名：`NN-案例类型-核心动作.md`
- 一个案例保留 1 个主标签，可补 1-3 个次标签

## 基本信息
- 案例标题：
- 主标签：
- 次标签：
- 对应方法卡：
- 题材 / 子题材：
- 风格 / 节奏：
- 章节范围：
- 对应 chunk：

## 这个案例最适合拿来参考什么
- 它最适合拿来参考哪类写法场景：
- 最适合在什么创作阶段参考：

## 场景摘要
- 只用 3-5 句写清这个案例做了什么，不写长剧情复述：

## 关键动作链
- 触发条件：
- 关键动作 1：
- 关键动作 2：
- 关键动作 3：
- 结果：
- 留下了什么新问题：

## 情绪与读感变化
- 前置情绪：
- 中段拉力：
- 结尾余味 / 钩子：

## 为什么有效
- 结构层原因：
- 角色层原因：
- 情绪层原因：
- 读者期待层原因：

## 可量化参数
- 发生时机：
- 铺垫长度：
- 兑现长度：
- 钩子 / 补偿间隔：
- 对话 / 动作 / 心理 / 信息占比：

## 可复用做法
- 可以直接参考的动作：
- 更适合什么题材 / 风格 / 阶段：
- 改写时必须替换哪些内容：

## 不可照搬
- 依赖具体设定的部分：
- 依赖题材红利的部分：
- 依赖角色关系组合的部分：

## 检索标签
- 正向关键词：
- 反向限制词：

## 证据锚点
- 关键证据 1：
- 关键证据 2：
- 关键证据 3：


path: references/templates/方法卡/章节类型与写作模式方法卡模板.md
content: # 章节类型与写作模式方法卡

## 这个模块最值得提炼什么
- 这本书在单章写作上最稳定的章法是什么：
- 这本书最值得提炼的章节组织方式是什么：

## 章节类型分布
- 常见章节类型有哪些：
- 开局章节怎么写：
- 推进章节怎么写：
- 过渡章节怎么写：
- 高潮或收束章节怎么写：

## 章节组织方式
- 单章通常承担一个功能还是多个功能：
- 章节开头常见进入方式：
- 章节中段如何防止塌陷：
- 单章如何安排信息、动作、情绪的切换：

## 结尾处理
- 是否每章都留钩子：
- 章末钩子常见类型是什么：
- 哪类章节可以不强留钩子：
- 不留钩子时靠什么维持读感：

## 为什么有效
- 这套章法为什么不容易水：
- 哪种章节最能体现这本书的稳定写作模式：
- 哪类章节最能拉追读：

## 可复用写法
- 写法 1：
- 写法 2：
- 写法 3：

## 可量化参数
- 各类章节大致占比：
- 章末留钩子频率：
- 单章平均推进点数量：
- 过渡章平均承担的附加功能数：
- 高潮章和普通章的密度差异：

## 适用边界
- 更适合什么更新节奏 / 平台读者：
- 如果章节篇幅不同，哪些参数要调整：
- 哪些章法不能照搬：

## 证据锚点
- 关键 chunk：


path: references/templates/方法卡/大纲设计方法卡模板.md
content: # 大纲设计方法卡

## 这个模块最值得提炼什么
- 这本书在长线规划上最稳定的做法是什么：
- 这本书提供了哪种可复用的大纲稳固方式：

## 主线骨架
- 主线目标是什么：
- 主线分几段推进：
- 每段阶段目标是什么：
- 剧情过渡靠什么衔接：

## 升级与选择节点
- 首次升级发生在什么时候：
- 重要选择节点有哪些：
- 关键转折来自外力推动 / 主角选择 / 信息揭露哪一种：
- 升级后立刻刷新了什么新问题：

## 长篇不散的机制
- 支线如何回流主线：
- 任务如何刷新：
- 阶段结束如何自然切到下一阶段：

## 为什么有效
- 这套大纲最稳的地方是什么：
- 它解决了什么长篇常见问题：
- 它让读者持续追读的核心不是哪一环：

## 可复用写法
- 写法 1：
- 写法 2：
- 写法 3：

## 可量化参数
- 首次升级时机：
- 重要选择节点间隔：
- 大转折前铺垫长度：
- 平均每卷 / 每阶段长度：
- 主线 / 支线大致比例：

## 适用边界
- 更适合什么类型长篇：
- 如果题材规则不同，哪些节点要改写：
- 哪些结构不能照搬：

## 证据锚点
- 关键 chunk：


path: references/templates/方法卡/长篇创作与文风优化方法卡模板.md
content: # 长篇创作与文风优化方法卡

## 这个模块最值得提炼什么
- 这本书在“写长”和文风控制上最稳定的做法是什么：
- 这本书最值得提炼的扩长与文风控制方式是什么：

## 长篇扩展机制
- 这本书靠什么写长：
- 地图如何切换：
- 规则如何更新：
- 对手如何更换：
- 目标如何调整：

## 支线与回流
- 哪些支线真的服务主线：
- 支线如何反哺主线：
- 哪类扩展本来容易写散，但这本书没有散：

## 文风参数
- 对话密度如何：
- 动作描写与心理描写谁更强：
- 解释说明多不多：
- 节奏为什么自然耐读：
- 行文更依赖短句推进 / 对话推进 / 动作推进 / 心理推进哪种：

## 为什么有效
- 哪种扩长手段最稳：
- 哪种节奏手段最能维持可读性：
- 哪种文风参数最值得学：

## 可复用写法
- 写法 1：
- 写法 2：
- 写法 3：

## 可量化参数
- 对话密度：
- 动作描写占比：
- 心理描写频率：
- 新目标刷新频率：
- 地图 / 规则切换频率：

## 适用边界
- 更适合什么篇幅和题材：
- 如果换成短篇或中篇，哪些扩长动作要删：
- 哪些地方不能照搬：

## 证据锚点
- 关键 chunk：


path: references/templates/方法卡/开篇规划方法卡模板.md
content: # 开篇规划方法卡

## 这个模块最值得提炼什么
- 这本书在开篇阶段最稳定的做法是什么：
- 这本书最值得提炼的开篇抓手是什么：

## 开篇规划拆解
- 出场人物如何安排：
- 主角第一次出场靠什么立住：
- 核心冲突在什么时候明确：
- 第一章承担了哪些功能：
- 前三章各自负责什么：

## 钩子与承诺
- 开篇钩子是什么：
- 它属于悬念型 / 危机型 / 反差型 / 利益承诺型哪种：
- 开篇先给了读者什么承诺：
- 前三章第一次小兑现是什么：

## 为什么有效
- 这套开篇最强的抓手是什么：
- 它为什么能让读者愿意继续看：
- 它优先锁住的是好奇心 / 爽感 / 危机感 / 代入感哪一种：

## 可复用写法
- 写法 1：
- 写法 2：
- 写法 3：

## 可量化参数
- 主角第几段出场：
- 核心冲突第几章明确：
- 开篇钩子出现位置：
- 第一次小兑现在第几章：
- 前三章平均净推进点数量：

## 适用边界
- 更适合什么题材 / 节奏：
- 如果换成慢热文，哪些位置要后移：
- 哪些地方不能照搬：

## 证据锚点
- 关键 chunk：


path: references/templates/方法卡/读者情绪管理方法卡模板.md
content: # 读者情绪管理方法卡

## 这个模块最值得提炼什么
- 这本书在情绪节奏上最稳定的做法是什么：
- 这本书最值得提炼的情绪调度能力是什么：

## 情绪主路数
- 整体更偏高爽 / 高压 / 慢热 / 拉扯哪种：
- 读者主要被什么情绪驱动：
- 主要爽点和压点分别来自哪里：

## 压抑与补偿
- 压抑通常持续多久：
- 情绪补偿一般怎么给：
- 爽点更偏即时兑现还是延迟兑现：
- 补偿后是否还会追加一个新问题：

## 节奏换挡
- 哪些章节负责蓄压：
- 哪些章节负责释压：
- 哪些节点专门用于拉追读情绪：
- 连续压抑时靠什么避免读者流失：

## 为什么有效
- 补偿为什么来得刚刚好：
- 这套情绪曲线为什么不会让人疲劳：
- 这本书最强的情绪抓手是什么：

## 可复用写法
- 写法 1：
- 写法 2：
- 写法 3：

## 可量化参数
- 小补偿间隔：
- 大补偿间隔：
- 连续压抑上限：
- 高强度情绪峰值分布：
- 爽点 / 压点章节比例：

## 适用边界
- 更适合什么情绪路数的作品：
- 如果换成非爽文，补偿方式要怎么改：
- 哪些地方不能照搬：

## 证据锚点
- 关键 chunk：


path: references/templates/方法卡/冲突与角色设计方法卡模板.md
content: # 冲突与角色设计方法卡

## 这个模块最值得提炼什么
- 这本书在冲突设计和角色塑造上最稳定的做法是什么：
- 这本书最值得提炼的人物与冲突机制是什么：

## 冲突设计
- 主角面临的核心障碍类型是什么：
- 冲突更多来自外部还是内部：
- 新问题通常如何从旧问题里长出来：
- 每次破局是否都伴随代价：

## 主角机制
- 主角的决策逻辑是什么：
- 主角常犯什么错：
- 主角通常如何破局：
- 主角的吸引力来源是什么：

## 配角与对手
- 配角分别承担什么功能：
- 对手压迫感来自哪里：
- 哪种关系最能推动剧情：
- 哪类角色最容易制造有效冲突：

## 为什么有效
- 这套人物机制为什么会让读者持续跟主角走：
- 这套冲突升级为什么不显得重复：
- 它最强的不是人设包装，而是什么：

## 可复用写法
- 写法 1：
- 写法 2：
- 写法 3：

## 可量化参数
- 平均几章出现一个明显障碍：
- 大挫折 / 小挫折比例：
- 主角犯错频率：
- 破局方式分布：
- 代价显化频率：

## 适用边界
- 更适合什么题材 / 主角类型：
- 如果主角魅力点不同，哪些设计要替换：
- 哪些地方不能照搬：

## 证据锚点
- 关键 chunk：


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)