# 📜 v0.3 纯净代码 · 一键 Copy 粘贴版

<aside>
✅

v0.3 纯净代码 · PROMPT 分支化 + MILESTONE_WORDCOUNTS 硬编码 · 桔子×熹微 · 2026-06-17

</aside>

> ⊢ 妈妈用代码块右上角 **Copy 按钮** 复制 · VS Code 粘贴 · Cmd+S 保存 · **不要手动框选**
> 

## 📥 妈妈三步操作

1. 已备份 v0.2 ✅（刚刚跑过了）
2. 终端跑：`nano ~/chenxing-aircraft/tools/run_l6_one_node.py`
3. 进入 nano 后：按 `Ctrl+K` 一直按清空旧内容 → 回本页 Copy 代码块 → 回 nano `Cmd+V` 粘贴 → `Ctrl+O` 回车 → `Ctrl+X` 退出

## ✅ 保存后语法验证

```bash
python3 -c "import ast; ast.parse(open('$HOME/chenxing-aircraft/tools/run_l6_one_node.py').read()); print('✅ v0.3 语法 OK')"
```

看到 `✅ v0.3 语法 OK` → 回来告诉宝宝 → 下一步 dry-run 验证。

---

## 🐍 完整脚本 v0.3

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
run_l6_one_node.py v0.3
单节点强制跑 L6 四层 + Notion 直传
桔子×熹微 · 2026-06-17

v0.3 升级（接力 v0.2 · 心脑记事本 #009 三层补救之架构层 + 设计层）：
- ① PROMPT 分支化（架构层补救）：
    - 拆 L6_LAYER4_PROMPT 为 _MIDPOINT 和 _TERMINAL 两份
    - MIDPOINT 含「下段预警」节 · 进行时语气 · 服务"故事如何长大"
    - TERMINAL 含「L7 候选」节 · 零「下段/本段」字眼 · 完成时语气 · 服务"故事最终长什么样"
    - 新增 pick_prompt() 判定函数：current == book_total → TERMINAL else MIDPOINT
- ② 字数段间距硬编码（设计层补救）：
    - 新增常量 MILESTONE_WORDCOUNTS · 11 个节点 · 中期每 10 万一段 · 封顶不放大
- ③ CLI 新增 --book-total-words 参数 · 用于中段/终态判定（必填）
- ④ 父页元信息加路径标签 MIDPOINT / TERMINAL
"""

import os
import sys
import argparse
import time
from pathlib import Path
from openai import OpenAI
from notion_client import Client as Notion

#===配置区===
BOOKS_DIR = Path.home() / "chenxing-aircraft" / "output"
OUTPUT_DIR = Path.home() / "chenxing-aircraft" / "output" / "l6_one_node"
OPENAI_MODEL = "deepseek-chat"
OPENAI_BASE_URL = "https://api.deepseek.com"
MAX_RETRIES = 3
RETRY_DELAY = 5
MAX_TOKENS = 6000

#===L6 字数段间距铁律（v0.3 立·2026-06-17）===
# 来源：心脑记事本 #009 · 妈妈元层追问拆穿
# 原则：早期密（1/3/5/10 万） · 中期之后每 10 万一段 · 封顶不放大
# 任何 > 80 万的书 · 按 90/100/110... 每 10 万延伸
MILESTONE_WORDCOUNTS = [
    10_000,    # 1 万
    30_000,    # 3 万    ┐
    50_000,    # 5 万    ├ 早期·密
    100_000,   # 10 万   ┘
    200_000,   # 20 万   ┐
    300_000,   # 30 万   │
    400_000,   # 40 万   ├ 中期·中等（每 10 万一段）
    500_000,   # 50 万   │
    600_000,   # 60 万   │
    700_000,   # 70 万   │
    800_000,   # 80 万   ┘
    # 收官（终态）= 实际字数 · 由 --book-total-words 动态确定 · 自动走 PROMPT_TERMINAL
]

#===环境变量加载===
def _load_env_from_dotfile():
    env_path = Path.home() / "chenxing-aircraft" / ".env"
    if env_path.exists():
        for line in env_path.read_text(encoding="utf-8").split("\n"):
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            k, _, v = line.partition("=")
            os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))

_load_env_from_dotfile()

def _get_llm_key():
    return os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENAI_API_KEY") or ""

def _get_notion_token():
    return os.environ.get("NOTION_API_KEY") or os.environ.get("NOTION_TOKEN") or ""

#===L6 v2.0 四层提示词·v0.3===
# ①②③ 层保持 v0.2 不变 · ④ 层拆分为 MIDPOINT / TERMINAL 两份

L6_LAYER1_PROMPT = '''小说《{book}》已拆到第{last_ch}章，累计{words}字。

以下是《{book}》第1-{last_ch}章共 {arc_count} 段弧段总结（L5 产出，每段已是合并 7-10 章后的段落级颗粒）：
{l5_arcs}

{previous_l6}
---

请基于以上 L5 弧段总结，生成 **{label}节点 · ① 人物生态系统** 总结。

# ① 人物生态系统 · {label}节点（第1-{last_ch}章）

## 人物规模统计
## 关系网拓扑（本节点结束时）
## 重要配角弧线
## 本段新增人物
## 本段退出/转变人物

⚠️ 注意：
- 主角的详细弧线移至③主角成长层
- 关系网拓扑只列名字+关系性质·不展开长篇
- 保护层/敌对层等不要塞章号列表

只输出①人物生态系统层，不要输出其他层。'''

L6_LAYER2_PROMPT = '''小说《{book}》已拆到第{last_ch}章，累计{words}字。

参考已完成的①层摘要：
{layer1_context}

以下是《{book}》第1-{last_ch}章共 {arc_count} 段弧段总结：
{l5_arcs}

{previous_l6}
---

请基于以上 L5 弧段总结和①层结果，生成 **{label}节点 · ② 世界层** 总结。

# ② 世界层 · {label}节点（第1-{last_ch}章）

## 活跃场景
## 地图扩展
## 势力格局

只输出②世界层，不要输出其他层。'''

L6_LAYER3_PROMPT = '''小说《{book}》已拆到第{last_ch}章，累计{words}字。

参考已完成的①②层摘要：
{layer1_context}
{layer2_context}

以下是《{book}》第1-{last_ch}章共 {arc_count} 段弧段总结：
{l5_arcs}

{previous_l6}
---

请基于以上 L5 弧段总结和①②层结果，生成 **{label}节点 · ③ 主角成长层** 总结。

# ③ 主角成长层 · {label}节点（第1-{last_ch}章）

## 资源盘点五维
- 人力 / 财力 / 信息 / 权力 / 情感（各 1-3 句）

## 核心关系质量 ⭐
主角→角色：当前阶段 + ⭐评分（1-5） + 本段变化

## 主角弧线里程碑
- 本段起点状态
- 本段完成的核心跨越（3-5条）
- 下一段方向预判

## 里程碑事件
数量要求：{milestone_count}个。
格式（每个2-4句）：
**事件名**（第X-Y章）：发生了什么 — 为什么是里程碑 — 不可逆跨越。

⚠️ 分布约束（v0.2 起·必须遵守）：
- 上述 {milestone_count} 个里程碑必须从上面 {arc_count} 段弧段中均匀挑选
- 每段弧段至少考虑 1 个候选事件
- 不允许 3 段以上连续弧段都没有里程碑（如弧段 5-6-7 全空 = 不合格）
- 事件章号跨度应覆盖第1章到第{last_ch}章的全范围
- 如某段弧段中确实没有「不可逆跨越」级事件·可以选「跨弧联动」事件补位（但需明标跨哪几段）

反模式：不要全部里程碑集中在前 3 段或后 3 段 · 不要写数据卡级别的一句话总结。
正例：14 个事件应大体平均落在 14 段弧里·详细的事件 + 跨越意义。

只输出③主角成长层，不要输出其他层。'''

# ============ v0.3 新增：④ 层拆分为 MIDPOINT 和 TERMINAL 两份 ============

L6_LAYER4_PROMPT_MIDPOINT = '''小说《{book}》已拆到第{last_ch}章，累计{words}字（中段·全书共 {book_total_words} 字）。

参考已完成的①②③层摘要：
{layer1_context}
{layer2_context}
{layer3_context}

以下是《{book}》第1-{last_ch}章共 {arc_count} 段弧段总结：
{l5_arcs}

{previous_l6}
---

请基于以上 L5 弧段总结和①②③层结果，生成 **{label}节点 · ④ 节奏层（中段·序列性）** 总结。

# ④ 节奏层 · {label}节点（第1-{last_ch}章·中段）

## 本段小高潮密度
（本段已展开的小高潮 · 用进行时语气）

## 本段典型爆发（TOP 2-3）

## 下段预警信号（3-5条·必填·v0.3 中段铁律）
⊢ 本节点 = {label}（{words}字）· 下一个里程碑节点 = {next_label}（约 {next_words} 字）
⊢ 预判 {label} → {next_label} 区间将出现的高潮 / 转折 / 关键事件信号
⊢ 每条要给出"为什么是下段的"判断依据（基于已知的伏笔 / 未结的悬念 / 角色弧线张力）

## 规律锁定（2-5条·格式：⊢ 规律锁定·[名]=[结论] — [场景+为什么有效+适用] 置信=[最高]）

⚠️ 中段·序列性原则（v0.3 铁律）：
- 语气用进行时·避免「最终」「全书定型」「整本书」等终态词
- 必须有「下段预警」节·这是中段节点的服务目标
- 服务目标："故事如何长大"·给后续 L6/L7 节点和晨星记忆引擎写新章节时用

只输出④节奏层，不要输出其他层。'''

L6_LAYER4_PROMPT_TERMINAL = '''小说《{book}》第1-{last_ch}章已全部拆完，累计{words}字（= 全书总字数 {book_total_words} 字 · 终态节点）。

参考已完成的①②③层摘要：
{layer1_context}
{layer2_context}
{layer3_context}

以下是《{book}》第1-{last_ch}章共 {arc_count} 段弧段总结：
{l5_arcs}

{previous_l6}
---

请基于以上 L5 弧段总结和①②③层结果，生成 **{label}终态节点 · ④ 节奏层（横截面）** 总结。

# ④ 节奏层 · {label}终态节点（全书定型画像）

## 全书高潮密度统计
（全书最终形成的高潮分布 · 用完成时语气）

## 全书典型爆发（TOP 3-5）

## 规律锁定（2-5条·格式：⊢ 规律锁定·[名]=[结论] — [场景+为什么有效+适用] 置信=[最高]）

## L7 候选 · 全书定型画像（必填·v0.3 终态铁律）
⊢ 人物总数 / 世界层规模 / 主线 + 暗线数 / 全书总转折点
⊢ 节奏结构形态（如：三幕递进式 / 双线并行 / 螺旋上升·每幕约多少字 / 高潮密度变化曲线）
⊢ 这一节是 L7 全书总结的草稿·可被直接升格为 L7

⚠️ 终态·横截面原则（v0.3 铁律）：
- **严禁出现「下段」「本段」「本节点」「即将」「下一段」「向前推进」等措辞**
- 全部用「全书」「最终」「形成了」「定型」「整本书」等终态词
- **不要有「下段预警」节** · 该位置改为「L7 候选 · 全书定型画像」
- 服务目标："故事最终长什么样"·成为 L7 草稿

只输出④节奏层，不要输出其他层。'''

#===v0.3 新增：中段 vs 终态判定函数===
def pick_prompt(current_wordcount: int, book_total_wordcount: int) -> str:
    """
    中段 vs 终态判定（v0.3 架构层补救）：
    - 终态 = 当前字数 == 全书实际字数（最后一个节点）
    - 中段 = 其余所有节点（含 10/20/30...80 万）
    返回："TERMINAL" 或 "MIDPOINT"
    """
    if current_wordcount == book_total_wordcount:
        return "TERMINAL"
    return "MIDPOINT"

def get_next_milestone_label(current_wordcount: int, book_total_wordcount: int):
    """
    给中段节点找下一个里程碑·用于「下段预警」节
    返回：(下段标签, 下段字数)
    优先从 MILESTONE_WORDCOUNTS 中找 > current 且 < book_total 的最小里程碑
    找不到 → 下一个就是全书终态
    """
    for m in MILESTONE_WORDCOUNTS:
        if m > current_wordcount and m < book_total_wordcount:
            return (f"{m // 10000}万字", m)
    return (f"全书终态（{book_total_wordcount // 10000}万字）", book_total_wordcount)

#===工具函数===
def parse_args():
    p = argparse.ArgumentParser(description="单节点强制跑 L6 + Notion 直传 v0.3")
    p.add_argument("--book", required=True, help="书名")
    p.add_argument("--label", required=True, help="节点标签·如 40万字 / 52.8万字")
    p.add_argument("--chapters", required=True, help="章节范围·如 1-80")
    p.add_argument("--total-words", type=int, required=True, help="本节点累计字数")
    p.add_argument("--book-total-words", type=int, required=True, help="全书总字数·v0.3 必填·用于中段/终态判定")
    p.add_argument("--milestone-count", type=int, default=14, help="里程碑事件数·默认 14")
    p.add_argument("--parent-page-id", required=True, help="Notion 父页 ID")
    p.add_argument("--force", action="store_true", help="强制覆盖已存在的节点")
    p.add_argument("--dry-run", action="store_true", help="只生成不上传")
    return p.parse_args()

def load_l5_arcs(book):
    """
    读取 output/<书>/L5_arcs/ 下所有 .md 弧段总结
    返回：(拼接后的弧段文本, 弧段数量)
    不裁剪·不跳过。
    """
    arcs_dir = BOOKS_DIR / book / "L5_arcs"
    if not arcs_dir.exists():
        sys.exit(f"❌ L5 弧段目录不存在：{arcs_dir}\n   请先跑 run_l5_one_arc.py 或 backfill_l5_notion.py 生成 L5")
    arc_files = sorted(arcs_dir.glob("*.md"))
    if not arc_files:
        sys.exit(f"❌ L5 弧段目录为空：{arcs_dir}")
    print(f"   找到 {len(arc_files)} 个弧段文件：")
    chunks = []
    for f in arc_files:
        print(f"     - {f.name}")
        chunks.append(f.read_text(encoding="utf-8"))
    raw = "\n\n===\n\n".join(chunks)
    print(f"   总长度：{len(raw):,} 字符（不裁剪）")
    return raw, len(arc_files)

def call_llm(client, prompt, layer_name):
    for attempt in range(MAX_RETRIES):
        try:
            print(f"🤖 调用 LLM 跑 {layer_name}（attempt {attempt + 1}）...")
            resp = client.chat.completions.create(
                model=OPENAI_MODEL,
                messages=[
                    {"role": "system", "content": "你是晨星拆书法 V1.0 的 L6 字数段总结生成器·严格遵循模板格式。"},
                    {"role": "user", "content": prompt},
                ],
                temperature=0.7,
                max_tokens=MAX_TOKENS,
            )
            return resp.choices[0].message.content.strip()
        except Exception as e:
            print(f"❌ {layer_name} 调用失败：{e}")
            if attempt < MAX_RETRIES - 1:
                time.sleep(RETRY_DELAY)
            else:
                raise

def extract_summary(text, max_chars=1500):
    if len(text) <= max_chars:
        return text
    return text[:max_chars] + "\n\n... [摘要截断]"

def notion_create_page(notion, parent_id, title, content_md, icon="📍"):
    blocks = []
    for line in content_md.split("\n"):
        if not line.strip():
            continue
        if line.startswith("# "):
            blocks.append({"object": "block", "type": "heading_1", "heading_1": {"rich_text": [{"type": "text", "text": {"content": line[2:]}}]}})
        elif line.startswith("## "):
            blocks.append({"object": "block", "type": "heading_2", "heading_2": {"rich_text": [{"type": "text", "text": {"content": line[3:]}}]}})
        elif line.startswith("### "):
            blocks.append({"object": "block", "type": "heading_3", "heading_3": {"rich_text": [{"type": "text", "text": {"content": line[4:]}}]}})
        elif line.startswith("- "):
            blocks.append({"object": "block", "type": "bulleted_list_item", "bulleted_list_item": {"rich_text": [{"type": "text", "text": {"content": line[2:]}}]}})
        elif line.startswith("> "):
            blocks.append({"object": "block", "type": "quote", "quote": {"rich_text": [{"type": "text", "text": {"content": line[2:]}}]}})
        else:
            blocks.append({"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": line}}]}})
    page = notion.pages.create(
        parent={"page_id": parent_id},
        icon={"type": "emoji", "emoji": icon},
        properties={"title": {"title": [{"text": {"content": title}}]}},
        children=blocks[:100],
    )
    page_id = page["id"]
    for i in range(100, len(blocks), 100):
        notion.blocks.children.append(block_id=page_id, children=blocks[i:i + 100])
    return page_id

#===主流程===
def main():
    args = parse_args()
    ch_start, ch_end = map(int, args.chapters.split("-"))

    # v0.3 · 中段 vs 终态判定
    path = pick_prompt(args.total_words, args.book_total_words)
    is_terminal = path == "TERMINAL"

    print("\n" + "=" * 60)
    print(f"🎯 run_l6_one_node.py v0.3")
    print(f"📖 书名：{args.book}")
    print(f"🏷️  节点：{args.label}（第{ch_start}-{ch_end}章·{args.total_words:,}字）")
    print(f"📚 全书总字数：{args.book_total_words:,}字")
    print(f"🛤️  路径判定：{path}（{'终态·横截面·走 L7 候选' if is_terminal else '中段·序列性·走下段预警'}）")
    print(f"📊 milestone_count = {args.milestone_count}")
    print(f"📤 Notion 父页：{args.parent_page_id}")
    print(f"🧪 dry-run：{args.dry_run}")
    print("=" * 60 + "\n")

    # 1. 加载 L5 弧段总结
    print("📂 加载 L5 弧段总结...")
    l5_arcs, arc_count = load_l5_arcs(args.book)

    # 2. LLM 客户端
    client = OpenAI(api_key=_get_llm_key(), base_url=OPENAI_BASE_URL)

    # 3. 准备 common_vars
    if is_terminal:
        previous_l6_text = "（本节点为终态·无后续 L6 节点）"
    else:
        previous_l6_text = "（中段节点·下一个里程碑见④层「下段预警信号」节）"

    common_vars = {
        "book": args.book,
        "last_ch": ch_end,
        "words": f"{args.total_words:,}",
        "book_total_words": f"{args.book_total_words:,}",
        "label": args.label,
        "l5_arcs": l5_arcs,
        "arc_count": arc_count,
        "previous_l6": previous_l6_text,
    }

    # 中段需要 next_label / next_words
    if not is_terminal:
        next_label, next_words = get_next_milestone_label(args.total_words, args.book_total_words)
        common_vars["next_label"] = next_label
        common_vars["next_words"] = f"{next_words:,}"
        print(f"🎯 下段预警目标：{next_label}（{next_words:,} 字）\n")

    # 4. 跑①②③层（与 v0.2 一致）
    layer1 = call_llm(client, L6_LAYER1_PROMPT.format(**common_vars), "① 人物生态系统层")
    layer1_ctx = extract_summary(layer1)

    layer2 = call_llm(client, L6_LAYER2_PROMPT.format(layer1_context=layer1_ctx, **common_vars), "② 世界层")
    layer2_ctx = extract_summary(layer2)

    layer3 = call_llm(
        client,
        L6_LAYER3_PROMPT.format(
            layer1_context=layer1_ctx,
            layer2_context=layer2_ctx,
            milestone_count=args.milestone_count,
            **common_vars,
        ),
        "③ 主角成长层",
    )
    layer3_ctx = extract_summary(layer3)

    # 5. 跑④层（v0.3 分支化）
    layer4_template = L6_LAYER4_PROMPT_TERMINAL if is_terminal else L6_LAYER4_PROMPT_MIDPOINT
    layer4 = call_llm(
        client,
        layer4_template.format(
            layer1_context=layer1_ctx,
            layer2_context=layer2_ctx,
            layer3_context=layer3_ctx,
            **common_vars,
        ),
        f"④ 节奏层（{path}）",
    )

    # 6. 落盘
    out_dir = OUTPUT_DIR / args.label
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "layer1_人物生态系统.md").write_text(layer1, encoding="utf-8")
    (out_dir / "layer2_世界.md").write_text(layer2, encoding="utf-8")
    (out_dir / "layer3_主角成长.md").write_text(layer3, encoding="utf-8")
    (out_dir / "layer4_节奏.md").write_text(layer4, encoding="utf-8")
    print(f"\n✅ 四层已落盘：{out_dir}")

    if args.dry_run:
        print(f"\n🧪 dry-run 模式·跳过 Notion 上传。请人工预览：")
        print(f"   - layer3_主角成长.md 里里程碑分布（{args.milestone_count} 个·{arc_count} 段均匀挑）")
        if is_terminal:
            print(f"   - layer4_节奏.md 必须含「L7 候选」节·零「下段/本段」字眼")
        else:
            print(f"   - layer4_节奏.md 必须含「下段预警」节·目标 = {common_vars['next_label']}")
        print(f"   如达标·去掉 --dry-run 再跑。")
        return

    # 7. Notion 直传
    notion = Notion(auth=_get_notion_token())
    print("\n📤 上传 Notion...")

    path_tag = "TERMINAL（终态·横截面·L7 候选）" if is_terminal else f"MIDPOINT（中段·下段预警目标={common_vars['next_label']}）"
    parent_title = f"📍《{args.book}》{args.label}节点总结"
    parent_meta = (
        f"# {parent_title}\n\n"
        f"累计字数：{args.total_words:,}字\n"
        f"全书总字数：{args.book_total_words:,}字\n"
        f"章节范围：第{ch_start}-{ch_end}章\n"
        f"生成时间：{time.strftime('%Y-%m-%d %H:%M:%S')}\n"
        f"生成工具：run_l6_one_node.py v0.3\n"
        f"输入源：L5 弧段 × {arc_count} 段\n"
        f"字数段路径：{path_tag}"
    )
    parent_id = notion_create_page(notion, args.parent_page_id, parent_title, parent_meta, icon="📍")
    print(f"   ✅ 父页创建：{parent_id}")

    for icon, title_suffix, content in [
        ("👥", "① 人物生态系统层", layer1),
        ("🌍", "② 世界层", layer2),
        ("🌱", "③ 主角成长层", layer3),
        ("🎵", f"④ 节奏层（{path}）", layer4),
    ]:
        child_title = f"📍《{args.book}》{args.label}节点总结 · {title_suffix}"
        child_id = notion_create_page(notion, parent_id, child_title, content, icon=icon)
        print(f"   ✅ 子页创建（{title_suffix}）：{child_id}")

    clean_id = parent_id.replace("-", "")
    print(f"\n🎉 全部完成！打开 Notion 看父页：https://notion.so/{clean_id}")

if __name__ == "__main__":
    main()
```

---

## 🔬 这一版跟 v0.2 代码层差异高亮

- 新增常量 `MILESTONE_WORDCOUNTS`（8 节 → **11 节**）·中期 30/40/50/60/70/80 万等间距·封顶 80 万不放大
- 拆 `L6_LAYER4_PROMPT` 为 `L6_LAYER4_PROMPT_MIDPOINT` + `L6_LAYER4_PROMPT_TERMINAL` 两份
- 新增函数 `pick_prompt(current, book_total)` · 返回 `"TERMINAL"` 或 `"MIDPOINT"`
- 新增函数 `get_next_milestone_label(current, book_total)` · 给中段节点找下段预警目标
- CLI 新增 `--book-total-words`（必填） · 用于中段/终态判定
- `main()` 开头加路径判定 + 环境报表打印· ④ 层调用处根据 is_terminal 选择模板
- 中段节点 common_vars 多了 `next_label` + `next_words` · 用于「下段预警」节
- 父页元信息 md 加两行：全书总字数 + 字数段路径 MIDPOINT/TERMINAL
- `previous_l6` 从 v0.2 的 hardcoded「终态」·改为按 is_terminal 动态选中段/终态文案

---

## 🧪 dry-run 三个验证点（语法 OK 后跑）

### 验证 ①：40 万中段（·分手》缺口补完·主要跟实际需求）

```bash
cd ~/chenxing-aircraft
python3 tools/run_l6_one_node.py \
  --book "全世界都在等我们分手" \
  --label "40万字" \
  --chapters 1-80 \
  --total-words 400000 \
  --book-total-words 528108 \
  --milestone-count 14 \
  --parent-page-id <《分手》书文件夹 Notion 页 ID> \
  --dry-run
```

验收点：

- 控制台打印「🛤️  路径判定：MIDPOINT（中段·序列性·走下段预警）」
- 控制台打印「🎯 下段预警目标：50万字（50,0000 字）」
- `output/l6_one_node/40万字/layer4_节奏.md` 含「## 下段预警信号」节·提及「下一个里程碑节点 = 50万字」

### 验证 ②：52.8 万终态（·分手》完结）

```bash
python3 tools/run_l6_one_node.py \
  --book "全世界都在等我们分手" \
  --label "52.8万字" \
  --chapters 1-106 \
  --total-words 528108 \
  --book-total-words 528108 \
  --milestone-count 14 \
  --parent-page-id <同上> \
  --dry-run
```

验收点：

- 控制台打印「🛤️  路径判定：TERMINAL（终态·横截面·走 L7 候选）」
- `output/l6_one_node/52.8万字/layer4_节奏.md` 含「## L7 候选 · 全书定型画像」节
- `output/l6_one_node/52.8万字/layer4_节奏.md` **不含**「下段」「本段」「本节点」字眼（grep 验证）

### 验证 ③：30 万中段（跟已跑过的 v0.2 节点对比）

```bash
python3 tools/run_l6_one_node.py \
  --book "全世界都在等我们分手" \
  --label "30万字" \
  --chapters 1-60 \
  --total-words 300000 \
  --book-total-words 528108 \
  --milestone-count 14 \
  --parent-page-id <同上> \
  --dry-run
```

验收点：

- 控制台打印「🎯 下段预警目标：40万字」（则 v0.3 里程碑表生效 ✅）
- `layer4_节奏.md` 「下段预警」节明说 30 → 40 万区间的高潮信号

---

## 🔗 关联档案

- 上一版代码：v0.2 纯净代码（同父页·本页同级）
- v0.3 升级蓝图：🔧 run_l6_one_node v0.3 · 升级蓝图（本页父页）
- 心脑记事本 #009：指导依据·三层补救之架构层 + 设计层

---

> 📜 v0.3 纯净代码 · 桔子×熹微 · 2026-06-17 20:50
> 

> ⊢ 语法验证 OK 后·妈妈截图告诉宝宝·下一步跑 dry-run 验证点 ③（30 万中段）验证下段预警节生成
>