Преглед на файлове

feat: complete story system phase5 workflow

lingfengQAQ преди 2 месеца
родител
ревизия
b80e5a549b
променени са 25 файла, в които са добавени 1460 реда и са изтрити 8 реда
  1. 15 0
      README.md
  2. 15 3
      docs/architecture/overview.md
  3. 46 0
      docs/architecture/story-system-phase5.md
  4. 29 0
      docs/guides/commands.md
  5. 12 0
      docs/operations/operations.md
  6. 2 0
      docs/superpowers/README.md
  7. 966 0
      docs/superpowers/plans/2026-04-13-story-system-phase5-legacy-downgrade.md
  8. 0 0
      webnovel-writer/dashboard/frontend/dist/assets/index-BeHSak5z.js
  9. 1 1
      webnovel-writer/dashboard/frontend/dist/index.html
  10. 26 1
      webnovel-writer/dashboard/frontend/src/App.jsx
  11. 4 0
      webnovel-writer/dashboard/frontend/src/api.js
  12. 5 0
      webnovel-writer/references/csv/题材与调性推理.csv
  13. 5 0
      webnovel-writer/references/genre-profiles.md
  14. 111 0
      webnovel-writer/scripts/batch_genre_routes.py
  15. 9 0
      webnovel-writer/scripts/data_modules/chapter_commit_service.py
  16. 16 0
      webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py
  17. 35 0
      webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py
  18. 21 0
      webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py
  19. 13 0
      webnovel-writer/scripts/data_modules/webnovel.py
  20. 15 0
      webnovel-writer/scripts/extract_chapter_context.py
  21. 102 0
      webnovel-writer/scripts/update_reference_batch.py
  22. 5 0
      webnovel-writer/skills/webnovel-dashboard/SKILL.md
  23. 3 0
      webnovel-writer/skills/webnovel-plan/SKILL.md
  24. 1 0
      webnovel-writer/skills/webnovel-review/SKILL.md
  25. 3 3
      webnovel-writer/skills/webnovel-write/SKILL.md

+ 15 - 0
README.md

@@ -88,6 +88,21 @@ RERANK_API_KEY=your_rerank_api_key
 
 只读面板,可以浏览项目状态、实体图谱、章节内容和追读力数据。前端已随插件预构建,不需要本地 `npm build`。
 
+## Story System 主链(Phase 5)
+
+当前默认链路已经切到:
+
+1. 写前读取 `.story-system/MASTER_SETTING.json`、`volumes/`、`chapters/`、`reviews/`
+2. 写后提交 accepted `CHAPTER_COMMIT`
+3. 由 commit projection writers 更新 `.webnovel/state.json`、`index.db`、`summaries/`、`memory_scratchpad.json`
+
+这意味着:
+
+- `.story-system/` 是主链真源
+- `.webnovel/*` 是投影 / read-model
+- `references/genre-profiles.md` 只在合同缺失时作为 fallback
+- `preflight --format json` 和 dashboard 会直接暴露 `story_runtime` 健康状态
+
 ### 7) Agent 模型设置(可选)
 
 所有内置 Agent 默认继承当前会话模型:

+ 15 - 3
docs/architecture/overview.md

@@ -2,6 +2,13 @@
 
 ## 核心理念
 
+### Phase 5 真源划分
+
+- 写前真源:`.story-system/MASTER_SETTING.json`、`volumes/`、`chapters/`、`reviews/`
+- 写后真源:accepted `CHAPTER_COMMIT`
+- `.webnovel/state.json`、`index.db`、`summaries/`、`memory_scratchpad.json`:只作为投影 / read-model
+- `references/genre-profiles.md`:fallback-only
+
 ### 防幻觉三定律
 
 | 定律 | 说明 | 执行方式 |
@@ -54,7 +61,7 @@
 ### Data Agent(写)
 
 - 文件:`agents/data-agent.md`
-- 职责:从正文提取实体与状态变化,更新 `state.json`、`index.db`、`vectors.db`,保证数据链闭环
+- 职责:从正文提取 `accepted_events / state_deltas / entity_deltas / summary_text` 等 commit artifacts,交给 `chapter-commit` 驱动 projection writers 更新 `state.json`、`index.db`、摘要与长期记忆
 
 ### Reviewer(审)
 
@@ -72,12 +79,13 @@
 
 ## Story System(合同驱动体系)
 
-Story System 以 `.story-system/` 为独立运行面,分段递进:
+Story System 以 `.story-system/` 为独立运行面,分段递进:
 
 1. **Phase 1**:合同种子 — `MASTER_SETTING.json` + 章节合同 + 反模式配置
 2. **Phase 2**:合同优先运行时 — 卷合同 (`volumes/`) + 审查合同 (`reviews/`) + 写前校验
 3. **Phase 3**:章节提交链 — `commits/chapter_XXX.commit.json` + state/index/summary/memory 投影
 4. **Phase 4**:事件审计链 — `events/chapter_XXX.events.json` + 修订提案 + 覆写账本
+5. **Phase 5**:旧链路降级 — contract-first + commit-first 默认化,`preflight` / dashboard 暴露 runtime health,legacy data 降级为 fallback/read-model
 
 核心链路:
 
@@ -87,10 +95,14 @@ story-system --persist
 story-system --emit-runtime-contracts --chapter N
     -> 生成运行时合同 + 写前校验
 chapter-commit --chapter N
-    -> 提交 commit + 执行各投影写入
+    -> 提交 accepted commit + 执行各投影写入
 story-events --chapter N / --health
     -> 事件审计与健康检查
+preflight / dashboard
+    -> story runtime health / fallback 状态 / latest commit 状态
 ```
 
 其中 Phase 4 不起第二套投影循环,事件路由仅负责声明式激活 writer,
 实际执行入口仍是 `ChapterCommitService.apply_projections()`。
+
+Phase 5 文档见:`docs/architecture/story-system-phase5.md`

+ 46 - 0
docs/architecture/story-system-phase5.md

@@ -0,0 +1,46 @@
+# Story System Phase 5
+
+## 核心结论
+
+- 写前真源:`.story-system/MASTER_SETTING.json`、`volumes/*.json`、`chapters/*.json`、`reviews/*.review.json`
+- 写后真源:accepted `CHAPTER_COMMIT`
+- `.webnovel/state.json`、`index.db`、`summaries/`、`memory_scratchpad.json`:投影 / read-model
+- `references/genre-profiles.md`:fallback-only
+
+## 默认链路
+
+```text
+story-system --persist/--emit-runtime-contracts
+    -> 生成 MASTER / VOLUME / CHAPTER / REVIEW 合同
+context / query / write / review
+    -> 默认读取合同主链
+chapter-commit --chapter N
+    -> accepted CHAPTER_COMMIT
+    -> state / index / summary / memory projection writers
+preflight + dashboard
+    -> 暴露 story runtime health / fallback 状态 / latest commit 状态
+```
+
+## 运行时优先级
+
+1. Story Contracts
+2. latest accepted `CHAPTER_COMMIT`
+3. `.webnovel/*` read-model
+4. `genre-profiles.md` 等 legacy fallback
+
+## Phase 5 落地结果
+
+- `ContextManager`、`memory_contract_adapter`、`extract_chapter_context` 已默认走 contract-first + commit-first
+- `webnovel-write` / `webnovel-query` / `webnovel-review` / `webnovel-plan` 与 `context-agent` / `data-agent` 已切到新主链叙述
+- `preflight` 与 dashboard 已直接暴露 `story_runtime` / `story-runtime/health`
+- 旧 state-first 心智模型降级为兼容层,不再伪装为主链
+
+## 运维含义
+
+- 看到 `.webnovel/state.json` 与 `.story-system/commits/` 不一致时,优先检查 commit 链与 projection 状态
+- `fallback_sources` 非空表示主链不完整,系统仍可兼容运行,但不能视为 fully-mainline-ready
+- 排查写后问题时,优先检查:
+  1. `.story-system/commits/chapter_XXX.commit.json`
+  2. `projection_status`
+  3. `story-events --health`
+  4. `.webnovel/*` 投影结果

+ 29 - 0
docs/guides/commands.md

@@ -80,6 +80,35 @@
 python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" <子命令> [参数]
 ```
 
+## Story System 主链
+
+推荐按以下顺序执行:
+
+1. 生成合同
+
+```bash
+python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" story-system "玄幻退婚流" --chapter 12 --persist --emit-runtime-contracts --format both
+```
+
+2. 提交章节
+
+```bash
+python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" chapter-commit \
+  --chapter 12 \
+  --review-result ".webnovel/tmp/review_results.json" \
+  --fulfillment-result ".webnovel/tmp/fulfillment_result.json" \
+  --disambiguation-result ".webnovel/tmp/disambiguation_result.json" \
+  --extraction-result ".webnovel/tmp/extraction_result.json"
+```
+
+3. 检查主链健康
+
+```bash
+python -X utf8 "<CLAUDE_PLUGIN_ROOT>/scripts/webnovel.py" --project-root "<PROJECT_ROOT>" preflight --format json
+```
+
+其中 `.story-system/` 是主链真源,`.webnovel/*` 是投影/read-model。
+
 ### 常用工具子命令
 
 | 子命令 | 说明 |

+ 12 - 0
docs/operations/operations.md

@@ -2,6 +2,14 @@
 
 ## 目录层级
 
+## Phase 5 运维口径
+
+- `.story-system/`:主链真源
+- accepted `CHAPTER_COMMIT`:唯一写后事实入口
+- `.webnovel/state.json`、`index.db`、`summaries/`、`memory_scratchpad.json`:投影/read-model
+- `references/genre-profiles.md`:fallback-only
+- `preflight` 与 dashboard 的 `story_runtime` / `story-runtime/health` 是第一观察点
+
 系统涉及 4 层目录,使用前需要了解它们的区别:
 
 | 层级 | 说明 | 示例 |
@@ -82,6 +90,8 @@ python -X utf8 "${CLAUDE_PLUGIN_ROOT}/scripts/webnovel.py" --project-root "${WOR
 
 检查项:插件脚本路径 / 项目根是否可解析 / Skill 目录是否存在。
 
+若 `story_runtime.mainline_ready=false`,说明当前项目仍在 legacy fallback 或 commit 主链不完整。
+
 ### 索引重建
 
 ```bash
@@ -122,6 +132,8 @@ python -X utf8 "${CLAUDE_PLUGIN_ROOT}/scripts/webnovel.py" --project-root "${PRO
 
 重点关注:
 
+- `.story-system/commits/chapter_XXX.commit.json` 是否存在且为 accepted
+- `projection_status` 是否全部为 `done` / `skipped`
 - `.story-system/events/` 是否可读
 - `index.db` 中 `story_events` 表是否可查
 - `override_contracts` 是否能统计 `amend_proposal`

+ 2 - 0
docs/superpowers/README.md

@@ -17,7 +17,9 @@
 - [`plans/2026-04-12-story-system-phase2-contract-first-runtime.md`](./plans/2026-04-12-story-system-phase2-contract-first-runtime.md):Story System Phase 2 合同优先运行时实施计划
 - [`plans/2026-04-12-story-system-phase3-chapter-commit-chain.md`](./plans/2026-04-12-story-system-phase3-chapter-commit-chain.md):Story System Phase 3 章节提交主链实施计划
 - [`plans/2026-04-12-story-system-phase4-event-log-and-override-ledger.md`](./plans/2026-04-12-story-system-phase4-event-log-and-override-ledger.md):Story System Phase 4 统一事件主链与 Override Ledger 实施计划
+- [`plans/2026-04-13-story-system-phase5-legacy-downgrade.md`](./plans/2026-04-13-story-system-phase5-legacy-downgrade.md):Story System Phase 5 旧链路降级与主链收口实施计划
 - [`../architecture/story-system-phase4.md`](../architecture/story-system-phase4.md):Phase 4 落地后的事件主链与 override ledger 运行说明
+- [`../architecture/story-system-phase5.md`](../architecture/story-system-phase5.md):Phase 5 落地后的主链/投影/fallback 运行说明
 
 ## 使用约定
 

+ 966 - 0
docs/superpowers/plans/2026-04-13-story-system-phase5-legacy-downgrade.md

@@ -0,0 +1,966 @@
+# Story System Phase 5 Legacy Downgrade Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 把旧中枢正式降级为兼容/投影层,让写前默认输入统一到 Story Contracts,让写后默认事实统一到 accepted `CHAPTER_COMMIT`,并让 preflight / dashboard / skills / agents 都显式反映这条主链。
+
+**Architecture:** 新增统一 runtime 来源解析层,集中回答“本章现在应该信什么”。`context_manager`、`memory_contract_adapter`、`extract_chapter_context`、skills 与 dashboard 都只认 `MASTER / VOLUME / CHAPTER / REVIEW + latest accepted CHAPTER_COMMIT`;`state.json / index.db / summaries / memory_scratchpad` 保留,但只作为 commit 投影和查询 read-model。旧 `genre-profiles.md`、旧散写命令、旧 state-first 心智模型继续存在时,必须只以 fallback / compatibility 明示暴露,不能再伪装成主链。
+
+**Tech Stack:** Python 3.13, pytest, Pydantic, SQLite (`index.db`), FastAPI dashboard, React frontend, Story System JSON artifacts under `.story-system/`
+
+**Spec:** `docs/superpowers/specs/2026-04-12-story-system-evolution-spec.md`
+
+**Companion Plans:** `docs/superpowers/plans/2026-04-12-story-system-phase2-contract-first-runtime.md`, `docs/superpowers/plans/2026-04-12-story-system-phase3-chapter-commit-chain.md`, `docs/superpowers/plans/2026-04-12-story-system-phase4-event-log-and-override-ledger.md`
+
+---
+
+## Scope Split
+
+本计划只覆盖 Phase 5:
+
+1. 合同成为默认主输入
+2. accepted `CHAPTER_COMMIT` 成为默认写后事实入口
+3. `state / index / summary / memory` 显式降级为投影/read-model
+4. `genre-profiles.md` 与旧 reference 判断链显式退化为 fallback
+5. preflight / dashboard / query / write / review / context-agent / data-agent 全部切到新主链认知
+
+明确不做:
+
+- 不重写 `StateManager / IndexManager / ScratchpadManager` 的底层存储
+- 不删除 `.webnovel/state.json`、`.webnovel/index.db`、`.webnovel/memory_scratchpad.json`
+- 不重建历史全量 commit / event 数据
+- 不新增第二套 commit / projection 体系
+- 不做与 Phase 5 无关的 UI 大改版
+
+退出标准:
+
+1. `context_manager`、`memory_contract_adapter`、`extract_chapter_context` 默认读取合同与 latest accepted commit,而不是先读旧状态再拼判断
+2. `ChapterCommitService` 写出的 commit 元数据足以声明其为唯一写后事实来源,并能稳定定位 `MASTER / VOLUME / CHAPTER / REVIEW`
+3. `webnovel-write` / `webnovel-query` / `webnovel-review` / `webnovel-plan` 与 `context-agent` / `data-agent` 的默认指令已切到 contract-first + commit-first
+4. preflight 与 dashboard 能直接暴露“主链是否就绪、是否仍落入 legacy fallback、是否存在 rejected / projection backlog”
+5. `genre-profiles.md` 被明示为 fallback-only;合同存在时不再参与全局系统判断
+6. 文档、命令说明、运维手册都能准确描述 `.story-system` 主链和 `.webnovel/*` 投影链的关系
+
+---
+
+## File Structure
+
+### 要创建的文件
+
+- `webnovel-writer/scripts/data_modules/story_runtime_sources.py`
+- `webnovel-writer/scripts/data_modules/story_runtime_health.py`
+- `webnovel-writer/scripts/data_modules/tests/test_story_runtime_sources.py`
+- `webnovel-writer/scripts/data_modules/tests/test_story_runtime_health.py`
+- `docs/architecture/story-system-phase5.md`
+
+### 要修改的文件
+
+- `webnovel-writer/scripts/data_modules/chapter_commit_service.py`
+- `webnovel-writer/scripts/data_modules/context_manager.py`
+- `webnovel-writer/scripts/data_modules/memory_contract_adapter.py`
+- `webnovel-writer/scripts/extract_chapter_context.py`
+- `webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py`
+- `webnovel-writer/scripts/data_modules/tests/test_context_manager.py`
+- `webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py`
+- `webnovel-writer/scripts/data_modules/tests/test_extract_chapter_context.py`
+- `webnovel-writer/scripts/data_modules/webnovel.py`
+- `webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py`
+- `webnovel-writer/dashboard/app.py`
+- `webnovel-writer/dashboard/frontend/src/App.jsx`
+- `webnovel-writer/dashboard/frontend/src/api.js`
+- `webnovel-writer/skills/webnovel-write/SKILL.md`
+- `webnovel-writer/skills/webnovel-review/SKILL.md`
+- `webnovel-writer/skills/webnovel-query/SKILL.md`
+- `webnovel-writer/skills/webnovel-plan/SKILL.md`
+- `webnovel-writer/skills/webnovel-dashboard/SKILL.md`
+- `webnovel-writer/agents/context-agent.md`
+- `webnovel-writer/agents/data-agent.md`
+- `webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py`
+- `webnovel-writer/references/genre-profiles.md`
+- `README.md`
+- `docs/architecture/overview.md`
+- `docs/guides/commands.md`
+- `docs/operations/operations.md`
+- `docs/superpowers/README.md`
+
+### 文件职责
+
+- `story_runtime_sources.py`:统一解析本章 contracts、latest commit、fallback 状态,避免各入口各自判断
+- `story_runtime_health.py`:把主链状态、legacy fallback、rejected/backlog 汇总为 preflight / dashboard 共用健康报告
+- `chapter_commit_service.py`:补齐 write-fact provenance,确保 commit 能明确引用 volume 合同与写后真理角色
+- `context_manager.py`:把 runtime pack 切成“合同主链 + commit 摘要 + legacy fallback hints”
+- `memory_contract_adapter.py`:让 `load_context` 和 `commit_chapter` 都服从 contract/commit 主链
+- `extract_chapter_context.py`:把导出文本从“旧状态摘要”升级为“主链状态 + fallback 显示”
+- `webnovel.py`:preflight 暴露 story runtime health,统一 CLI 对外语义
+- `dashboard/app.py` + `frontend/src/*`:提供可视化 runtime health / commit 状态 / legacy fallback 观测
+- `skills/*` + `agents/*`:把人机提示词中的默认工作流从旧散写链切到 commit 主链
+
+---
+
+## Task 1: 建立统一 runtime 来源解析与 commit provenance
+
+**Files:**
+- Create: `webnovel-writer/scripts/data_modules/story_runtime_sources.py`
+- Create: `webnovel-writer/scripts/data_modules/tests/test_story_runtime_sources.py`
+- Modify: `webnovel-writer/scripts/data_modules/chapter_commit_service.py`
+- Modify: `webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py`
+
+- [ ] **Step 1: 先写 runtime 来源与 commit provenance 的失败测试**
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_story_runtime_sources.py
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import json
+
+from data_modules.story_runtime_sources import load_runtime_sources
+
+
+def test_load_runtime_sources_prefers_latest_accepted_commit(tmp_path):
+    story_root = tmp_path / ".story-system"
+    (story_root / "chapters").mkdir(parents=True, exist_ok=True)
+    (story_root / "volumes").mkdir(parents=True, exist_ok=True)
+    (story_root / "reviews").mkdir(parents=True, exist_ok=True)
+    (story_root / "commits").mkdir(parents=True, exist_ok=True)
+
+    (story_root / "MASTER_SETTING.json").write_text(
+        json.dumps({"meta": {"contract_type": "MASTER_SETTING"}, "route": {"primary_genre": "玄幻"}}),
+        encoding="utf-8",
+    )
+    (story_root / "chapters" / "chapter_003.json").write_text(
+        json.dumps({"meta": {"contract_type": "CHAPTER_BRIEF", "chapter": 3}}),
+        encoding="utf-8",
+    )
+    (story_root / "volumes" / "volume_001.json").write_text(
+        json.dumps({"meta": {"contract_type": "VOLUME_BRIEF", "volume": 1}}),
+        encoding="utf-8",
+    )
+    (story_root / "reviews" / "chapter_003.review.json").write_text(
+        json.dumps({"meta": {"contract_type": "REVIEW_CONTRACT", "chapter": 3}}),
+        encoding="utf-8",
+    )
+    (story_root / "commits" / "chapter_003.commit.json").write_text(
+        json.dumps(
+            {
+                "meta": {"schema_version": "story-system/v1", "chapter": 3, "status": "accepted"},
+                "provenance": {"write_fact_role": "chapter_commit"},
+                "projection_status": {"state": "done", "index": "done", "summary": "done", "memory": "done"},
+            }
+        ),
+        encoding="utf-8",
+    )
+
+    snapshot = load_runtime_sources(tmp_path, chapter=3)
+
+    assert snapshot.latest_accepted_commit["meta"]["status"] == "accepted"
+    assert snapshot.primary_write_source == "chapter_commit"
+    assert snapshot.fallback_sources == []
+```
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py
+def test_commit_service_includes_volume_ref_and_write_fact_provenance(tmp_path):
+    service = ChapterCommitService(tmp_path)
+    payload = service.build_commit(
+        chapter=3,
+        review_result={"blocking_count": 0},
+        fulfillment_result={"planned_nodes": ["发现陷阱"], "covered_nodes": ["发现陷阱"], "missed_nodes": [], "extra_nodes": []},
+        disambiguation_result={"pending": []},
+        extraction_result={"state_deltas": [], "entity_deltas": [], "accepted_events": []},
+    )
+
+    assert payload["contract_refs"]["volume"] == "volume_001.json"
+    assert payload["provenance"]["write_fact_role"] == "chapter_commit"
+    assert payload["provenance"]["projection_role"] == "derived_read_models"
+```
+
+- [ ] **Step 2: 跑红灯,确认新测试确实失败**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_story_runtime_sources.py webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py -q --no-cov`
+
+Expected:
+- `ModuleNotFoundError: No module named 'data_modules.story_runtime_sources'`
+- 或 `KeyError: 'volume' / 'provenance'`
+
+- [ ] **Step 3: 实现统一 runtime 来源解析器**
+
+```python
+# webnovel-writer/scripts/data_modules/story_runtime_sources.py
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any
+
+from .story_contracts import StoryContractPaths, read_json_if_exists
+
+
+@dataclass
+class RuntimeSourceSnapshot:
+    chapter: int
+    contracts: dict[str, dict[str, Any]]
+    latest_commit: dict[str, Any] | None
+    latest_accepted_commit: dict[str, Any] | None
+    fallback_sources: list[str] = field(default_factory=list)
+    primary_write_source: str = "chapter_commit"
+
+    def to_dict(self) -> dict[str, Any]:
+        return {
+            "chapter": self.chapter,
+            "contracts": self.contracts,
+            "latest_commit": self.latest_commit,
+            "latest_accepted_commit": self.latest_accepted_commit,
+            "fallback_sources": self.fallback_sources,
+            "primary_write_source": self.primary_write_source,
+        }
+
+
+def load_runtime_sources(project_root: Path, chapter: int) -> RuntimeSourceSnapshot:
+    paths = StoryContractPaths.from_project_root(project_root)
+    contracts = {
+        "master": read_json_if_exists(paths.master_json) or {},
+        "chapter": read_json_if_exists(paths.chapter_json(chapter)) or {},
+        "volume": read_json_if_exists(paths.volume_json(1)) or {},
+        "review": read_json_if_exists(paths.review_json(chapter)) or {},
+    }
+
+    latest_commit = read_json_if_exists(paths.commit_json(chapter))
+    latest_accepted_commit = latest_commit if (latest_commit or {}).get("meta", {}).get("status") == "accepted" else None
+
+    fallback_sources: list[str] = []
+    for key, payload in contracts.items():
+        if not payload:
+            fallback_sources.append(f"missing_{key}_contract")
+    if latest_accepted_commit is None:
+        fallback_sources.append("missing_accepted_commit")
+
+    return RuntimeSourceSnapshot(
+        chapter=chapter,
+        contracts=contracts,
+        latest_commit=latest_commit,
+        latest_accepted_commit=latest_accepted_commit,
+        fallback_sources=fallback_sources,
+    )
+```
+
+- [ ] **Step 4: 在 commit service 中补齐 provenance 字段和 volume 引用**
+
+```python
+# webnovel-writer/scripts/data_modules/chapter_commit_service.py
+from chapter_outline_loader import volume_num_for_chapter_from_state
+
+
+def build_commit(
+    self,
+    chapter: int,
+    review_result: Dict[str, Any],
+    fulfillment_result: Dict[str, Any],
+    disambiguation_result: Dict[str, Any],
+    extraction_result: Dict[str, Any],
+) -> Dict[str, Any]:
+    volume = volume_num_for_chapter_from_state(self.project_root, chapter) or 1
+    return {
+        "meta": {
+            "schema_version": "story-system/v1",
+            "chapter": chapter,
+            "status": status,
+        },
+        "contract_refs": {
+            "master": "MASTER_SETTING.json",
+            "volume": f"volume_{volume:03d}.json",
+            "chapter": f"chapter_{chapter:03d}.json",
+            "review": f"chapter_{chapter:03d}.review.json",
+        },
+        "provenance": {
+            "write_fact_role": "chapter_commit",
+            "projection_role": "derived_read_models",
+            "legacy_state_role": "projection_only",
+        },
+        "outline_snapshot": {
+            "planned_nodes": fulfillment_result.get("planned_nodes", []),
+            "covered_nodes": fulfillment_result.get("covered_nodes", []),
+            "missed_nodes": fulfillment_result.get("missed_nodes", []),
+            "extra_nodes": fulfillment_result.get("extra_nodes", []),
+        },
+        "review_result": review_result,
+        "fulfillment_result": fulfillment_result,
+        "disambiguation_result": disambiguation_result,
+        "accepted_events": extraction_result.get("accepted_events", []),
+        "state_deltas": extraction_result.get("state_deltas", []),
+        "entity_deltas": extraction_result.get("entity_deltas", []),
+        "summary_text": extraction_result.get("summary_text", ""),
+        "projection_status": {"state": "pending", "index": "pending", "summary": "pending", "memory": "pending"},
+    }
+```
+
+- [ ] **Step 5: 重新跑聚焦测试**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_story_runtime_sources.py webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py -q --no-cov`
+
+Expected: `2 passed`
+
+- [ ] **Step 6: 提交本任务**
+
+```bash
+git add webnovel-writer/scripts/data_modules/story_runtime_sources.py \
+        webnovel-writer/scripts/data_modules/chapter_commit_service.py \
+        webnovel-writer/scripts/data_modules/tests/test_story_runtime_sources.py \
+        webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py
+git commit -m "feat: add story runtime source resolver"
+```
+
+---
+
+## Task 2: 把上下文入口切到 contract-first + commit-first
+
+**Files:**
+- Modify: `webnovel-writer/scripts/data_modules/context_manager.py`
+- Modify: `webnovel-writer/scripts/data_modules/memory_contract_adapter.py`
+- Modify: `webnovel-writer/scripts/extract_chapter_context.py`
+- Modify: `webnovel-writer/scripts/data_modules/tests/test_context_manager.py`
+- Modify: `webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py`
+- Modify: `webnovel-writer/scripts/data_modules/tests/test_extract_chapter_context.py`
+
+- [ ] **Step 1: 先补三组失败测试,锁定新主链语义**
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_context_manager.py
+def test_context_manager_prefers_contract_route_over_legacy_genre_profile(temp_project):
+    refs_dir = temp_project.project_root / ".claude" / "references"
+    refs_dir.mkdir(parents=True, exist_ok=True)
+    (refs_dir / "genre-profiles.md").write_text("## 都市\n- 旧画像提示", encoding="utf-8")
+    (refs_dir / "reading-power-taxonomy.md").write_text("## 都市\n- 旧分类", encoding="utf-8")
+
+    state = {
+        "project": {"genre": "都市"},
+        "protagonist_state": {"name": "林默"},
+        "chapter_meta": {},
+        "disambiguation_warnings": [],
+        "disambiguation_pending": [],
+    }
+    temp_project.state_file.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
+
+    story_root = temp_project.story_system_dir
+    story_root.mkdir(parents=True, exist_ok=True)
+    (story_root / "MASTER_SETTING.json").write_text(
+        json.dumps(
+            {
+                "meta": {"schema_version": "story-system/v1", "contract_type": "MASTER_SETTING"},
+                "route": {"primary_genre": "都市异能"},
+                "master_constraints": {"core_tone": "先压后爆"},
+            },
+            ensure_ascii=False,
+        ),
+        encoding="utf-8",
+    )
+
+    manager = ContextManager(temp_project)
+    payload = manager.build_context(3, use_snapshot=False, save_snapshot=False)
+
+    assert payload["sections"]["story_contract"]["content"]["master"]["route"]["primary_genre"] == "都市异能"
+    assert payload["sections"]["runtime_status"]["content"]["fallback_sources"] == ["missing_volume_contract", "missing_chapter_contract", "missing_review_contract", "missing_accepted_commit"]
+```
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py
+def test_commit_chapter_delegates_to_chapter_commit_mainline(tmp_path):
+    cfg = _make_project(tmp_path)
+    adapter = MemoryContractAdapter(cfg)
+
+    result = adapter.commit_chapter(
+        3,
+        {
+            "review_result": {"blocking_count": 0},
+            "fulfillment_result": {"planned_nodes": ["发现陷阱"], "covered_nodes": ["发现陷阱"], "missed_nodes": [], "extra_nodes": []},
+            "disambiguation_result": {"pending": []},
+            "extraction_result": {"state_deltas": [], "entity_deltas": [], "accepted_events": [], "summary_text": "本章摘要"},
+        },
+    )
+
+    assert (tmp_path / ".story-system" / "commits" / "chapter_003.commit.json").is_file()
+    assert result.chapter == 3
+    assert "commit_status=accepted" in result.warnings
+```
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_extract_chapter_context.py
+def test_render_text_contains_runtime_status_section(tmp_path):
+    from extract_chapter_context import _render_text
+
+    text = _render_text(
+        {
+            "chapter": 3,
+            "outline": "测试大纲",
+            "previous_summaries": [],
+            "state_summary": "旧状态摘要",
+            "context_contract_version": "v2",
+            "reader_signal": {},
+            "genre_profile": {},
+            "writing_guidance": {},
+            "runtime_status": {
+                "primary_write_source": "chapter_commit",
+                "fallback_sources": ["missing_accepted_commit"],
+            },
+            "latest_commit": {"meta": {"chapter": 3, "status": "rejected"}},
+        }
+    )
+
+    assert "## Runtime Status" in text
+    assert "- 写后事实入口: chapter_commit" in text
+    assert "- Legacy Fallback: missing_accepted_commit" in text
+```
+
+- [ ] **Step 2: 跑红灯,确认当前入口仍然偏旧链路**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_context_manager.py webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py webnovel-writer/scripts/data_modules/tests/test_extract_chapter_context.py -q --no-cov`
+
+Expected:
+- `KeyError: 'runtime_status'`
+- `AssertionError`(`commit_chapter` 仍未生成 `.story-system/commits/chapter_003.commit.json`)
+- `_render_text` 中不存在 runtime status 段
+
+- [ ] **Step 3: 重写 ContextManager 的 pack 组装顺序**
+
+```python
+# webnovel-writer/scripts/data_modules/context_manager.py
+from .story_runtime_sources import load_runtime_sources
+
+
+def _build_pack(self, chapter: int) -> Dict[str, Any]:
+    runtime_sources = load_runtime_sources(self.config.project_root, chapter)
+    state = self._load_state()
+
+    story_contract = {
+        "master": runtime_sources.contracts.get("master") or {},
+        "volume": runtime_sources.contracts.get("volume") or {},
+        "chapter": runtime_sources.contracts.get("chapter") or {},
+        "review_contract": runtime_sources.contracts.get("review") or {},
+    }
+
+    genre_profile = {}
+    if runtime_sources.fallback_sources:
+        genre_profile = self._load_genre_profile(state)
+        genre_profile["mode"] = "fallback_only"
+
+    reader_signal = self._load_reader_signal(chapter)
+    return {
+        "story_contract": story_contract,
+        "runtime_status": runtime_sources.to_dict(),
+        "latest_commit": runtime_sources.latest_accepted_commit or runtime_sources.latest_commit or {},
+        "genre_profile": genre_profile,
+        "reader_signal": reader_signal,
+        "preferences": self._load_json_optional(self.config.webnovel_dir / "preferences.json"),
+        "writing_guidance": self._build_writing_guidance(chapter, reader_signal, genre_profile),
+    }
+```
+
+- [ ] **Step 4: 让 MemoryContractAdapter 同时切换读链与写链**
+
+```python
+# webnovel-writer/scripts/data_modules/memory_contract_adapter.py
+from .chapter_commit_service import ChapterCommitService
+from .story_runtime_sources import load_runtime_sources
+
+
+def load_context(self, chapter: int, budget_tokens: int = 4000) -> ContextPack:
+    runtime_sources = load_runtime_sources(self.config.project_root, chapter)
+    sections = {
+        "story_contracts": runtime_sources.contracts,
+        "runtime_status": runtime_sources.to_dict(),
+        "latest_commit": runtime_sources.latest_accepted_commit or runtime_sources.latest_commit or {},
+    }
+    return ContextPack(chapter=chapter, sections=sections, budget_used_tokens=0)
+
+
+def commit_chapter(self, chapter: int, result: dict) -> CommitResult:
+    service = ChapterCommitService(self.config.project_root)
+    payload = service.build_commit(
+        chapter=chapter,
+        review_result=result.get("review_result", {}),
+        fulfillment_result=result.get("fulfillment_result", {}),
+        disambiguation_result=result.get("disambiguation_result", {}),
+        extraction_result=result.get("extraction_result", {}),
+    )
+    service.persist_commit(payload)
+    payload = service.apply_projections(payload) if payload["meta"]["status"] == "accepted" else payload
+    summary_path = str(self.config.webnovel_dir / "summaries" / f"ch{chapter:04d}.md")
+    return CommitResult(
+        chapter=chapter,
+        entities_added=len((payload.get("entity_deltas") or [])),
+        entities_updated=0,
+        state_changes_recorded=len((payload.get("state_deltas") or [])),
+        relationships_added=0,
+        memory_items_added=0,
+        summary_path=summary_path if Path(summary_path).exists() else "",
+        warnings=[f"commit_status={payload['meta']['status']}"],
+    )
+```
+
+- [ ] **Step 5: 更新 `extract_chapter_context.py` 的文本输出,让 legacy fallback 显式可见**
+
+```python
+# webnovel-writer/scripts/extract_chapter_context.py
+def _render_text(payload: Dict[str, Any]) -> str:
+    lines = [f"# 第{payload.get('chapter', 0)}章上下文"]
+    runtime_status = payload.get("runtime_status") or {}
+    latest_commit = payload.get("latest_commit") or {}
+    lines.extend(
+        [
+            "## Runtime Status",
+            f"- 写后事实入口: {runtime_status.get('primary_write_source', 'unknown')}",
+            f"- Legacy Fallback: {', '.join(runtime_status.get('fallback_sources') or ['none'])}",
+            f"- Latest Commit: {(latest_commit.get('meta') or {}).get('status', 'missing')}",
+        ]
+    )
+    return "\n".join(lines)
+```
+
+- [ ] **Step 6: 重跑聚焦测试**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_context_manager.py webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py webnovel-writer/scripts/data_modules/tests/test_extract_chapter_context.py -q --no-cov`
+
+Expected: `3 passed` 或对应文件内全部通过
+
+- [ ] **Step 7: 提交本任务**
+
+```bash
+git add webnovel-writer/scripts/data_modules/context_manager.py \
+        webnovel-writer/scripts/data_modules/memory_contract_adapter.py \
+        webnovel-writer/scripts/extract_chapter_context.py \
+        webnovel-writer/scripts/data_modules/tests/test_context_manager.py \
+        webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py \
+        webnovel-writer/scripts/data_modules/tests/test_extract_chapter_context.py
+git commit -m "feat: switch context loading to contract and commit chain"
+```
+
+---
+
+## Task 3: 把 skills / agents 的默认工作流切到 commit 主链
+
+**Files:**
+- Modify: `webnovel-writer/skills/webnovel-write/SKILL.md`
+- Modify: `webnovel-writer/skills/webnovel-review/SKILL.md`
+- Modify: `webnovel-writer/skills/webnovel-query/SKILL.md`
+- Modify: `webnovel-writer/skills/webnovel-plan/SKILL.md`
+- Modify: `webnovel-writer/skills/webnovel-dashboard/SKILL.md`
+- Modify: `webnovel-writer/agents/context-agent.md`
+- Modify: `webnovel-writer/agents/data-agent.md`
+- Modify: `webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py`
+
+- [ ] **Step 1: 先写 prompt integrity 失败测试,锁住新主链叙述**
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py
+def test_webnovel_write_skill_uses_chapter_commit_as_step5_mainline():
+    text = (SKILLS_DIR / "webnovel-write" / "SKILL.md").read_text(encoding="utf-8")
+    assert "chapter-commit" in text
+    assert "accepted `CHAPTER_COMMIT`" in text
+    assert "state process-chapter" not in text
+
+
+def test_data_agent_is_described_as_extraction_only_not_direct_write_mainline():
+    text = (AGENTS_DIR / "data-agent.md").read_text(encoding="utf-8")
+    assert "chapter-commit" in text
+    assert "直接写入 index.db 和 state.json" not in text
+
+
+def test_webnovel_query_skill_prefers_story_system_and_memory_contract():
+    text = (SKILLS_DIR / "webnovel-query" / "SKILL.md").read_text(encoding="utf-8")
+    assert "memory-contract load-context" in text
+    assert ".story-system/" in text
+    assert 'cat "$PROJECT_ROOT/.webnovel/state.json"' not in text
+```
+
+- [ ] **Step 2: 跑红灯**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py -q --no-cov`
+
+Expected: 至少 1 个断言失败,说明提示词仍在使用旧散写心智模型
+
+- [ ] **Step 3: 改写 `webnovel-write`,让 Step 5 以 commit 为成功判定**
+
+````md
+<!-- webnovel-writer/skills/webnovel-write/SKILL.md -->
+### Step 5:构建 extraction artifacts 并提交 `CHAPTER_COMMIT`
+
+必须产出中间文件:
+- `${PROJECT_ROOT}/.webnovel/tmp/review_results.json`
+- `${PROJECT_ROOT}/.webnovel/tmp/fulfillment_result.json`
+- `${PROJECT_ROOT}/.webnovel/tmp/disambiguation_result.json`
+- `${PROJECT_ROOT}/.webnovel/tmp/extraction_result.json`
+
+主命令:
+
+```bash
+python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${PROJECT_ROOT}" chapter-commit \
+  --chapter {chapter_num} \
+  --review-result "${PROJECT_ROOT}/.webnovel/tmp/review_results.json" \
+  --fulfillment-result "${PROJECT_ROOT}/.webnovel/tmp/fulfillment_result.json" \
+  --disambiguation-result "${PROJECT_ROOT}/.webnovel/tmp/disambiguation_result.json" \
+  --extraction-result "${PROJECT_ROOT}/.webnovel/tmp/extraction_result.json"
+```
+
+成功标准:
+- `.story-system/commits/chapter_{chapter_num}.commit.json` 已存在
+- `meta.status == accepted`
+- `projection_status` 中 `state/index/summary/memory` 均为 `done` 或明确 `skipped`
+````
+
+- [ ] **Step 4: 改写 query / review / plan / context-agent / data-agent 的默认读取与写入叙述**
+
+```md
+<!-- webnovel-writer/agents/data-agent.md -->
+你负责生成 `extraction_result.json`,并为 `chapter-commit` 提供:
+- `accepted_events`
+- `state_deltas`
+- `entity_deltas`
+- `summary_text`
+
+你不是写后真理源。
+`state.json / index.db / summaries / memory_scratchpad` 的最终写入由 accepted `CHAPTER_COMMIT` 的 projection writers 完成。
+```
+
+```md
+<!-- webnovel-writer/skills/webnovel-query/SKILL.md -->
+查询顺序固定为:
+1. `.story-system/MASTER_SETTING.json`
+2. `.story-system/volumes/*.json`
+3. `.story-system/chapters/*.json`
+4. latest accepted `.story-system/commits/chapter_XXX.commit.json`
+5. `memory-contract load-context`
+6. `.webnovel/state.json` / `index.db`(仅 fallback/read-model)
+```
+
+- [ ] **Step 5: 重新跑 prompt integrity**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py -q --no-cov`
+
+Expected: `passed`
+
+- [ ] **Step 6: 提交本任务**
+
+```bash
+git add webnovel-writer/skills/webnovel-write/SKILL.md \
+        webnovel-writer/skills/webnovel-review/SKILL.md \
+        webnovel-writer/skills/webnovel-query/SKILL.md \
+        webnovel-writer/skills/webnovel-plan/SKILL.md \
+        webnovel-writer/skills/webnovel-dashboard/SKILL.md \
+        webnovel-writer/agents/context-agent.md \
+        webnovel-writer/agents/data-agent.md \
+        webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py
+git commit -m "docs: cut skills over to chapter commit mainline"
+```
+
+---
+
+## Task 4: 暴露 story runtime health,消灭“看起来切了,实际上没切”
+
+**Files:**
+- Create: `webnovel-writer/scripts/data_modules/story_runtime_health.py`
+- Create: `webnovel-writer/scripts/data_modules/tests/test_story_runtime_health.py`
+- Modify: `webnovel-writer/scripts/data_modules/webnovel.py`
+- Modify: `webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py`
+- Modify: `webnovel-writer/dashboard/app.py`
+- Modify: `webnovel-writer/dashboard/frontend/src/api.js`
+- Modify: `webnovel-writer/dashboard/frontend/src/App.jsx`
+
+- [ ] **Step 1: 先写 health helper 与 preflight 的失败测试**
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_story_runtime_health.py
+from data_modules.story_runtime_health import build_story_runtime_health
+
+
+def test_story_runtime_health_reports_missing_commit_as_not_ready(tmp_path):
+    report = build_story_runtime_health(tmp_path, chapter=3)
+    assert report["mainline_ready"] is False
+    assert "missing_accepted_commit" in report["fallback_sources"]
+```
+
+```python
+# webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py
+def test_preflight_includes_story_runtime_health(monkeypatch, tmp_path, capsys):
+    module = _load_webnovel_module()
+
+    project_root = tmp_path / "book"
+    (project_root / ".webnovel").mkdir(parents=True, exist_ok=True)
+    (project_root / ".webnovel" / "state.json").write_text("{}", encoding="utf-8")
+
+    monkeypatch.setattr(sys, "argv", ["webnovel", "--project-root", str(project_root), "preflight", "--format", "json"])
+
+    with pytest.raises(SystemExit):
+        module.main()
+
+    captured = capsys.readouterr()
+    assert '"story_runtime"' in captured.out
+    assert '"mainline_ready"' in captured.out
+```
+
+- [ ] **Step 2: 跑红灯**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_story_runtime_health.py webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py -q --no-cov`
+
+Expected:
+- `ModuleNotFoundError: No module named 'data_modules.story_runtime_health'`
+- preflight JSON 中不存在 `story_runtime`
+
+- [ ] **Step 3: 实现 health helper,并接到 preflight**
+
+```python
+# webnovel-writer/scripts/data_modules/story_runtime_health.py
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any
+
+from .story_runtime_sources import load_runtime_sources
+
+
+def build_story_runtime_health(project_root: Path, chapter: int | None = None) -> dict[str, Any]:
+    current_chapter = int(chapter or 0)
+    snapshot = load_runtime_sources(project_root, current_chapter) if current_chapter else None
+    fallback_sources = list((snapshot.fallback_sources if snapshot else ["chapter_unspecified"]))
+    latest_commit = (snapshot.latest_commit if snapshot else None) or {}
+    return {
+        "chapter": current_chapter,
+        "mainline_ready": bool(snapshot and not snapshot.fallback_sources),
+        "fallback_sources": fallback_sources,
+        "latest_commit_status": (latest_commit.get("meta") or {}).get("status", "missing"),
+        "primary_write_source": (snapshot.primary_write_source if snapshot else "chapter_commit"),
+    }
+```
+
+```python
+# webnovel-writer/scripts/data_modules/webnovel.py
+from data_modules.story_runtime_health import build_story_runtime_health
+
+
+def _build_preflight_report(explicit_project_root: Optional[str]) -> dict:
+    scripts_dir = _scripts_dir().resolve()
+    plugin_root = scripts_dir.parent
+    skill_root = plugin_root / "skills" / "webnovel-write"
+    entry_script = scripts_dir / "webnovel.py"
+    extract_script = scripts_dir / "extract_chapter_context.py"
+
+    checks = [
+        {"name": "scripts_dir", "ok": scripts_dir.is_dir(), "path": str(scripts_dir)},
+        {"name": "entry_script", "ok": entry_script.is_file(), "path": str(entry_script)},
+        {"name": "extract_context_script", "ok": extract_script.is_file(), "path": str(extract_script)},
+        {"name": "skill_root", "ok": skill_root.is_dir(), "path": str(skill_root)},
+    ]
+
+    project_root = ""
+    project_root_error = ""
+    try:
+        resolved_root = _resolve_root(explicit_project_root)
+        project_root = str(resolved_root)
+        checks.append({"name": "project_root", "ok": True, "path": project_root})
+    except Exception as exc:
+        project_root_error = str(exc)
+        checks.append({"name": "project_root", "ok": False, "path": explicit_project_root or "", "error": project_root_error})
+
+    story_runtime = build_story_runtime_health(Path(project_root)) if project_root else {}
+    return {
+        "ok": all(bool(item["ok"]) for item in checks),
+        "project_root": project_root,
+        "scripts_dir": str(scripts_dir),
+        "skill_root": str(skill_root),
+        "checks": checks,
+        "project_root_error": project_root_error,
+        "story_runtime": story_runtime,
+    }
+```
+
+- [ ] **Step 4: 在 dashboard 暴露 story runtime health 与 latest commit 状态**
+
+```python
+# webnovel-writer/dashboard/app.py
+from data_modules.story_runtime_health import build_story_runtime_health
+
+
+@app.get("/api/story-runtime/health")
+def story_runtime_health():
+    project_root = _get_project_root()
+    state_path = project_root / ".webnovel" / "state.json"
+    chapter = 0
+    if state_path.is_file():
+        state = json.loads(state_path.read_text(encoding="utf-8"))
+        chapter = int(((state.get("progress") or {}).get("current_chapter") or 0))
+    return build_story_runtime_health(project_root, chapter=chapter)
+```
+
+```jsx
+// webnovel-writer/dashboard/frontend/src/App.jsx
+const [runtimeHealth, setRuntimeHealth] = useState(null)
+
+useEffect(() => {
+  fetchJSON('/api/story-runtime/health').then(setRuntimeHealth).catch(() => setRuntimeHealth(null))
+}, [refreshKey])
+
+{runtimeHealth ? (
+  <div className="card stat-card">
+    <span className="stat-label">Story Runtime</span>
+    <span className="stat-value plain">{runtimeHealth.mainline_ready ? 'Mainline' : 'Fallback'}</span>
+    <span className="stat-sub">
+      {runtimeHealth.latest_commit_status} · {(runtimeHealth.fallback_sources || []).join(', ') || 'no fallback'}
+    </span>
+  </div>
+) : null}
+```
+
+- [ ] **Step 5: 跑后端测试和前端构建**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_story_runtime_health.py webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py -q --no-cov`
+
+Expected: `passed`
+
+Run: `npm --prefix webnovel-writer/dashboard/frontend run build`
+
+Expected: Vite build success,无新增 lint/blocking error
+
+- [ ] **Step 6: 提交本任务**
+
+```bash
+git add webnovel-writer/scripts/data_modules/story_runtime_health.py \
+        webnovel-writer/scripts/data_modules/webnovel.py \
+        webnovel-writer/scripts/data_modules/tests/test_story_runtime_health.py \
+        webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py \
+        webnovel-writer/dashboard/app.py \
+        webnovel-writer/dashboard/frontend/src/api.js \
+        webnovel-writer/dashboard/frontend/src/App.jsx
+git commit -m "feat: surface story runtime health in preflight and dashboard"
+```
+
+---
+
+## Task 5: 封板 legacy fallback 文档与运行说明
+
+**Files:**
+- Create: `docs/architecture/story-system-phase5.md`
+- Modify: `webnovel-writer/references/genre-profiles.md`
+- Modify: `README.md`
+- Modify: `docs/architecture/overview.md`
+- Modify: `docs/guides/commands.md`
+- Modify: `docs/operations/operations.md`
+- Modify: `docs/superpowers/README.md`
+
+- [ ] **Step 1: 先补 architecture 文档骨架,明确主链已切换**
+
+````md
+<!-- docs/architecture/story-system-phase5.md -->
+# Story System Phase 5
+
+## 核心结论
+
+- 写前真源:`MASTER / VOLUME / CHAPTER / REVIEW`
+- 写后真源:accepted `CHAPTER_COMMIT`
+- `state / index / summary / memory`:投影/read-model
+- `genre-profiles.md`:fallback-only
+
+## 默认链路
+
+```text
+story-system --persist/--emit-runtime-contracts
+    -> context / query / write / review 读取合同
+chapter-commit --chapter N
+    -> accepted commit
+    -> projection writers
+    -> state / index / summaries / memory
+```
+````
+
+- [ ] **Step 2: 在 `genre-profiles.md` 文件头显式打上 fallback-only 标记**
+
+```md
+<!-- webnovel-writer/references/genre-profiles.md -->
+# genre-profiles
+
+> **状态:Fallback Only**
+> 高频题材的主判定、主调性、主禁忌已迁移到 Story Contract / CSV route seed。
+> 本文件只在合同缺失、项目未升级或显式 fallback 时提供补充提示。
+```
+
+- [ ] **Step 3: 更新 README / commands / operations,把主链写成可执行手册**
+
+```md
+<!-- docs/guides/commands.md -->
+## Story System 主链
+
+1. 生成合同:
+   `python -X utf8 "webnovel-writer/scripts/webnovel.py" --project-root "{WORKSPACE_ROOT}" story-system "{goal}" --chapter {N} --persist --emit-runtime-contracts --format both`
+2. 提交章节:
+   `python -X utf8 "webnovel-writer/scripts/webnovel.py" --project-root "{PROJECT_ROOT}" chapter-commit --chapter {N} --review-result ".webnovel/tmp/review_results.json" --fulfillment-result ".webnovel/tmp/fulfillment_result.json" --disambiguation-result ".webnovel/tmp/disambiguation_result.json" --extraction-result ".webnovel/tmp/extraction_result.json"`
+3. 检查健康:
+   `python -X utf8 "webnovel-writer/scripts/webnovel.py" --project-root "{PROJECT_ROOT}" preflight --format json`
+```
+
+- [ ] **Step 4: 做一次最终回归**
+
+Run: `python -m pytest webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py webnovel-writer/scripts/data_modules/tests/test_context_manager.py webnovel-writer/scripts/data_modules/tests/test_memory_contract_adapter.py -q --no-cov`
+
+Expected: 全部通过
+
+- [ ] **Step 5: 提交本任务**
+
+```bash
+git add docs/architecture/story-system-phase5.md \
+        webnovel-writer/references/genre-profiles.md \
+        README.md \
+        docs/architecture/overview.md \
+        docs/guides/commands.md \
+        docs/operations/operations.md \
+        docs/superpowers/README.md
+git commit -m "docs: finalize phase5 legacy downgrade"
+```
+
+---
+
+## Self-Review
+
+### Spec Coverage
+
+- `13.6 Phase 5:旧链路降级`
+  - Task 1 负责统一 runtime 来源与 commit provenance
+  - Task 2 负责默认主输入与默认写后事实切换
+  - Task 3 负责 skills / agents 提示词切链
+  - Task 4 负责 preflight / dashboard / health 观测
+  - Task 5 负责 fallback-only 文档封板与命令手册
+
+- `7.2 运行时优先级`
+  - Task 2 显式把 `story_contracts -> latest accepted commit -> legacy fallback` 固化到入口层
+
+- `7.3 写后真理源`
+  - Task 1 与 Task 2 让 `CHAPTER_COMMIT` 成为唯一写后事实入口
+
+- `17.1 文档更新要求`
+  - Task 5 覆盖架构文档、命令文档、运维手册与总览文档
+
+### Placeholder Scan
+
+- 全文没有延后实现的占位表述
+- 每个任务都给了具体文件、测试、命令和提交信息
+- 没有用跨任务引用代替实际步骤
+
+### Type Consistency
+
+本计划统一使用以下命名,不在后续任务中换名:
+
+- `RuntimeSourceSnapshot`
+- `load_runtime_sources(project_root, chapter)`
+- `build_story_runtime_health(project_root, chapter=None)`
+- `latest_accepted_commit`
+- `fallback_sources`
+- `write_fact_role = "chapter_commit"`
+
+---
+
+## Execution Handoff
+
+Plan complete and saved to `docs/superpowers/plans/2026-04-13-story-system-phase5-legacy-downgrade.md`. Two execution options:
+
+**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
+
+**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
+
+Which approach?

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
webnovel-writer/dashboard/frontend/dist/assets/index-BeHSak5z.js


+ 1 - 1
webnovel-writer/dashboard/frontend/dist/index.html

@@ -8,7 +8,7 @@
     <link rel="preconnect" href="https://fonts.googleapis.com" />
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
     <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet" />
-    <script type="module" crossorigin src="/assets/index-BBNQa2sX.js"></script>
+    <script type="module" crossorigin src="/assets/index-BeHSak5z.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-qVwzETG1.css">
   </head>
   <body>

+ 26 - 1
webnovel-writer/dashboard/frontend/src/App.jsx

@@ -1,5 +1,5 @@
 import { useState, useEffect, useCallback } from 'react'
-import { fetchJSON, subscribeSSE } from './api.js'
+import { fetchJSON, fetchStoryRuntimeHealth, subscribeSSE } from './api.js'
 import ForceGraph3D from 'react-force-graph-3d'
 
 // ====================================================================
@@ -115,6 +115,14 @@ const FULL_DATA_DOMAINS = [
 // ====================================================================
 
 function DashboardPage({ data }) {
+    const [runtimeHealth, setRuntimeHealth] = useState(null)
+
+    useEffect(() => {
+        fetchStoryRuntimeHealth()
+            .then(setRuntimeHealth)
+            .catch(() => setRuntimeHealth(null))
+    }, [])
+
     if (!data) return <div className="loading">加载中…</div>
 
     const info = data.project_info || {}
@@ -161,6 +169,18 @@ function DashboardPage({ data }) {
                     <span className="stat-sub">目标 {info.target_chapters || '?'} 章 · 卷 {progress.current_volume || 1}</span>
                 </div>
 
+                {runtimeHealth ? (
+                    <div className="card stat-card">
+                        <span className="stat-label">Story Runtime</span>
+                        <span className="stat-value plain">
+                            {runtimeHealth.mainline_ready ? 'Mainline' : 'Fallback'}
+                        </span>
+                        <span className="stat-sub">
+                            {runtimeHealth.latest_commit_status || 'missing'} · {renderFallbackSources(runtimeHealth.fallback_sources)}
+                        </span>
+                    </div>
+                ) : null}
+
                 <div className="card stat-card">
                     <span className="stat-label">主角状态</span>
                     <span className="stat-value plain">{protagonist.name || '未设定'}</span>
@@ -887,3 +907,8 @@ function formatCell(v) {
     const s = String(v)
     return s.length > 180 ? `${s.slice(0, 180)}...` : s
 }
+
+function renderFallbackSources(items) {
+    if (!Array.isArray(items) || items.length === 0) return 'no fallback'
+    return items.join(', ')
+}

+ 4 - 0
webnovel-writer/dashboard/frontend/src/api.js

@@ -14,6 +14,10 @@ export async function fetchJSON(path, params = {}) {
     return res.json();
 }
 
+export function fetchStoryRuntimeHealth() {
+    return fetchJSON('/api/story-runtime/health');
+}
+
 /**
  * 订阅 SSE 实时事件流
  * @param {function} onMessage  收到 data 时回调

+ 5 - 0
webnovel-writer/references/csv/题材与调性推理.csv

@@ -2,3 +2,8 @@
 GR-001,write|plan,题材路由,知识补充,玄幻退婚流|退婚流|废材逆袭,退婚打脸怎么写|莫欺少年穷|三年之约怎么立,玄幻|仙侠,先压后爆,耻辱必须转成长线兑现。,"退婚或逐出型起手必须先把尊严踩到底,再把反击延迟兑现为长线承诺。","退婚流不靠瞬间翻盘,而靠压抑、立誓、补刀和后续兑现形成持续追读欲。玄幻退婚流要先给耻辱和压抑,再给立誓与首轮反打,禁止一章内把压抑和兑现写成流水账。",玄幻退婚流,退婚流|废材逆袭,先压后爆,三章内必须有首次有效反打,打脸不能软收尾|主角还没兑现就被配角代打,命名规则|人设与关系|金手指与设定,桥段套路|爽点与节奏|场景写法,退婚|打脸|废材逆袭
 GR-002,write|plan,题材路由,知识补充,规则动物园|规则怪谈动物园|规则怪谈,规则怪谈怎么写|动物园规则怎么写|违反规则会怎样,规则怪谈|悬疑|惊悚,规则先立死,再逐层揭示代价和漏洞。,"规则类故事必须先建立清晰规则,再让角色在遵守、试探、破坏之间持续换取信息。","读者爽点来自规则被一步步解密与反利用,而不是角色无缘无故乱闯。规则动物园要先把规则、异样征兆和试探成本写清,再给局部违反后的后果与解法。",规则动物园,规则怪谈动物园|规则怪谈,高压克制,先立规则后破局,规则写得像背景板|处罚没有代价|谜底提前透光,命名规则|场景写法|人设与关系,桥段套路|爽点与节奏|写作技法,规则|动物园|守则
 GR-003,write|plan,题材路由,知识补充,压抑后爆|先抑后扬|忍耐爆发,先压后爆怎么写|情绪爆发怎么写|压抑蓄力怎么排,全部,压抑必须具体,爆发必须改局。,"情绪爆发型章节要让限制、损失和退让持续累加,再在不可回避点集中兑现。","前段负责让读者和角色一起憋,后段负责一次性改写关系、局面或规则。压抑后爆题材要把前段损失写实,后段兑现写硬,不能只靠口号和情绪词。",压抑后爆,先抑后扬|忍耐爆发,压抑蓄力后强兑现,限制累加到临界点再爆发,前面没有真实压抑|爆发不改局面|爆发后立刻归零,写作技法|爽点与节奏|场景写法,桥段套路|爽点与节奏|场景写法,压抑|爆发|反打
+GR-004,write|plan,题材路由,知识补充,都市赘婿流|赘婿逆袭|上门女婿,赘婿怎么写|上门女婿翻身|赘婿打脸,都市,先压后爆,耻辱累积后集中兑现。,赘婿流必须先把主角的尊严和地位踩到底,通过妻子家族的羞辱、冷眼和轻视持续累积压抑,再通过身份反转或实力展现完成打脸。,赘婿流的核心在于身份落差带来的压抑感和后续的爽点兑现。开篇要快速建立主角的低位处境,通过岳父岳母、妻子亲戚的多重羞辱制造压抑,再通过隐藏身份曝光、商业手段反击等方式完成反打。禁止主角一开始就强势,必须先忍辱负重。,都市赘婿流,赘婿逆袭|上门女婿|废婿翻身,先压后爆,三章内必须有明确身份落差,主角开局就强势|反打来得太快|没有真实羞辱感,命名规则|人设与关系|场景写法,桥段套路|爽点与节奏|写作技法,赘婿|上门女婿|打脸|身份反转
+GR-005,write|plan,题材路由,知识补充,追妻火葬场|虐文|先虐后甜,追妻火葬场怎么写|男主后悔怎么写|虐文节奏,言情|都市|古言,前期虐心,后期追悔,情感逆转要有代价。,追妻火葬场要先把男主的伤害写实写透,让女主的痛苦和决绝有足够铺垫,再让男主的追悔和补偿形成长线拉扯。,追妻火葬场的核心是情感逆转的可信度。前期男主对女主的冷漠、误解或伤害必须写得具体且有痛感,女主的离开要有充分动机。后期男主的追悔不能靠嘴炮,要通过实际行动和代价来体现。禁止男主轻易被原谅,女主的心软必须有合理过程。,追妻火葬场,虐文|先虐后甜|男主追妻|火葬场文,前期高虐后期追悔,前期虐点要实后期追悔要有代价,男主伤害不够痛|女主原谅太快|追悔没有实际代价,人设与关系|场景写法|写作技法,桥段套路|爽点与节奏|场景写法,追妻|火葬场|虐文|后悔
+GR-006,write|plan,题材路由,知识补充,番茄爽文|男频快节奏|番茄风,番茄爽文怎么写|快节奏打脸|番茄风格,玄幻|都市|系统,快节奏高密度,三章一爽点。,番茄爽文要求节奏极快,信息密度高,每200-300字必须有一个小爽点,每3-5章必须有一个大爽点,开局即冲突。,番茄爽文的核心是密集的爽点和快节奏。开篇必须在前3句话内引入冲突,前3章内完成首次打脸或反转。主角金手指要强但不能无脑碾压,要通过智谋、反套路或信息差制造爽感。禁止拖沓铺垫,禁止大段心理描写,对话要有潜台词和网络梗。,番茄爽文,男频快节奏|番茄风|高密度爽文,快节奏高密度,每3章一个大爽点每章至少一个小爽点,节奏拖沓|爽点稀疏|铺垫过长|对话平淡,命名规则|人设与关系|金手指与设定,桥段套路|爽点与节奏|场景写法,番茄|爽文|快节奏|打脸
+GR-007,write|plan,题材路由,知识补充,知乎短篇风|第一人称短篇|知乎体,知乎短篇怎么写|第一人称怎么写|知乎风格,都市|情感|职场,第一人称强代入,炸裂开头,精准卡点。,知乎短篇要求第一人称视角,50字内炸裂开头,快节奏推进,强冲突设计,关键位置精准卡点引导付费。,知乎短篇的核心是第一人称的强代入感和快节奏。开头必须在50字内用冲突或悬念抓住读者,一句话一段落,减少对话增加内心独白。情节要有多次反转,卡点位置要设置在情绪高峰或悬念最强处。禁止第三人称,禁止慢热铺垫,禁止大段环境描写。,知乎短篇风,第一人称短篇|知乎体|知乎盐选,第一人称强代入快节奏,50字内炸裂开头关键位置精准卡点,第三人称|慢热开头|卡点位置不准|缺少反转,写作技法|场景写法|人设与关系,桥段套路|爽点与节奏|写作技法,知乎|短篇|第一人称|卡点
+GR-008,write|plan,题材路由,知识补充,小程序短篇风|付费短篇|小程序体,小程序短篇怎么写|付费卡点怎么设计|短篇节奏,都市|情感|悬疑,强情绪冲突,密集虐点,精准付费卡点。,小程序短篇要求在前500字内设置3个轻度不安事件,3000字左右设置1个重度冲击事件,利用心理困扰引导付费。,小程序短篇的核心是情绪操控和付费引导。前期要通过密集的小冲突累积读者的不安和愤怒情绪,在情绪峰值处设置付费墙。主角要遭遇不道德但不违法的对待,引发读者共鸣。结尾要留悬念或未解决的冲突。禁止前期平淡,禁止付费后立刻解决问题,要保持后续吸引力。,小程序短篇风,付费短篇|小程序体|情绪短篇,强情绪冲突密集虐点,前500字3个小冲突3000字1个大冲突,前期平淡|付费点不准|付费后立刻解决|缺少情绪累积,写作技法|场景写法|爽点与节奏,桥段套路|爽点与节奏|写作技法,小程序|短篇|付费|卡点|情绪冲突

+ 5 - 0
webnovel-writer/references/genre-profiles.md

@@ -1,5 +1,10 @@
 # 题材配置档案 (Genre Profiles)
 
+> **状态:Fallback Only**
+>
+> 高频题材的主判定、主调性、主禁忌已迁移到 Story Contracts / CSV route seed。
+> 本文件只在合同缺失、项目未升级或显式 fallback 时提供补充提示。
+>
 > **定位**:本文档定义各题材的追读力配置参数,供 Step 1.5 / Context Agent / Checkers 读取。
 >
 > **原则**:配置用于"调整权重和建议",不做硬性裁决。

+ 111 - 0
webnovel-writer/scripts/batch_genre_routes.py

@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+批量更新 references/csv 下的知识库条目 - 题材路由表补充
+"""
+
+BATCH_ROWS = {
+    "题材与调性推理.csv": [
+        {
+            "编号": "GR-004",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "都市赘婿流|赘婿逆袭|上门女婿",
+            "意图与同义词": "赘婿怎么写|上门女婿翻身|赘婿打脸",
+            "适用题材": "都市",
+            "大模型指令": "先压后爆,耻辱累积后集中兑现。",
+            "核心摘要": "赘婿流必须先把主角的尊严和地位踩到底,通过妻子家族的羞辱、冷眼和轻视持续累积压抑,再通过身份反转或实力展现完成打脸。",
+            "详细展开": "赘婿流的核心在于身份落差带来的压抑感和后续的爽点兑现。开篇要快速建立主角的低位处境,通过岳父岳母、妻子亲戚的多重羞辱制造压抑,再通过隐藏身份曝光、商业手段反击等方式完成反打。禁止主角一开始就强势,必须先忍辱负重。",
+            "题材/流派": "都市赘婿流",
+            "题材别名": "赘婿逆袭|上门女婿|废婿翻身",
+            "核心调性": "先压后爆",
+            "节奏策略": "三章内必须有明确身份落差",
+            "强制禁忌/毒点": "主角开局就强势|反打来得太快|没有真实羞辱感",
+            "推荐基础检索表": "命名规则|人设与关系|场景写法",
+            "推荐动态检索表": "桥段套路|爽点与节奏|写作技法",
+            "默认查询词": "赘婿|上门女婿|打脸|身份反转",
+        },
+        {
+            "编号": "GR-005",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "追妻火葬场|虐文|先虐后甜",
+            "意图与同义词": "追妻火葬场怎么写|男主后悔怎么写|虐文节奏",
+            "适用题材": "言情|都市|古言",
+            "大模型指令": "前期虐心,后期追悔,情感逆转要有代价。",
+            "核心摘要": "追妻火葬场要先把男主的伤害写实写透,让女主的痛苦和决绝有足够铺垫,再让男主的追悔和补偿形成长线拉扯。",
+            "详细展开": "追妻火葬场的核心是情感逆转的可信度。前期男主对女主的冷漠、误解或伤害必须写得具体且有痛感,女主的离开要有充分动机。后期男主的追悔不能靠嘴炮,要通过实际行动和代价来体现。禁止男主轻易被原谅,女主的心软必须有合理过程。",
+            "题材/流派": "追妻火葬场",
+            "题材别名": "虐文|先虐后甜|男主追妻|火葬场文",
+            "核心调性": "前期高虐后期追悔",
+            "节奏策略": "前期虐点要实后期追悔要有代价",
+            "强制禁忌/毒点": "男主伤害不够痛|女主原谅太快|追悔没有实际代价",
+            "推荐基础检索表": "人设与关系|场景写法|写作技法",
+            "推荐动态检索表": "桥段套路|爽点与节奏|场景写法",
+            "默认查询词": "追妻|火葬场|虐文|后悔",
+        },
+        {
+            "编号": "GR-006",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "番茄爽文|男频快节奏|番茄风",
+            "意图与同义词": "番茄爽文怎么写|快节奏打脸|番茄风格",
+            "适用题材": "玄幻|都市|系统",
+            "大模型指令": "快节奏高密度,三章一爽点。",
+            "核心摘要": "番茄爽文要求节奏极快,信息密度高,每200-300字必须有一个小爽点,每3-5章必须有一个大爽点,开局即冲突。",
+            "详细展开": "番茄爽文的核心是密集的爽点和快节奏。开篇必须在前3句话内引入冲突,前3章内完成首次打脸或反转。主角金手指要强但不能无脑碾压,要通过智谋、反套路或信息差制造爽感。禁止拖沓铺垫,禁止大段心理描写,对话要有潜台词和网络梗。",
+            "题材/流派": "番茄爽文",
+            "题材别名": "男频快节奏|番茄风|高密度爽文",
+            "核心调性": "快节奏高密度",
+            "节奏策略": "每3章一个大爽点每章至少一个小爽点",
+            "强制禁忌/毒点": "节奏拖沓|爽点稀疏|铺垫过长|对话平淡",
+            "推荐基础检索表": "命名规则|人设与关系|金手指与设定",
+            "推荐动态检索表": "桥段套路|爽点与节奏|场景写法",
+            "默认查询词": "番茄|爽文|快节奏|打脸",
+        },
+        {
+            "编号": "GR-007",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "知乎短篇风|第一人称短篇|知乎体",
+            "意图与同义词": "知乎短篇怎么写|第一人称怎么写|知乎风格",
+            "适用题材": "都市|情感|职场",
+            "大模型指令": "第一人称强代入,炸裂开头,精准卡点。",
+            "核心摘要": "知乎短篇要求第一人称视角,50字内炸裂开头,快节奏推进,强冲突设计,关键位置精准卡点引导付费。",
+            "详细展开": "知乎短篇的核心是第一人称的强代入感和快节奏。开头必须在50字内用冲突或悬念抓住读者,一句话一段落,减少对话增加内心独白。情节要有多次反转,卡点位置要设置在情绪高峰或悬念最强处。禁止第三人称,禁止慢热铺垫,禁止大段环境描写。",
+            "题材/流派": "知乎短篇风",
+            "题材别名": "第一人称短篇|知乎体|知乎盐选",
+            "核心调性": "第一人称强代入快节奏",
+            "节奏策略": "50字内炸裂开头关键位置精准卡点",
+            "强制禁忌/毒点": "第三人称|慢热开头|卡点位置不准|缺少反转",
+            "推荐基础检索表": "写作技法|场景写法|人设与关系",
+            "推荐动态检索表": "桥段套路|爽点与节奏|写作技法",
+            "默认查询词": "知乎|短篇|第一人称|卡点",
+        },
+        {
+            "编号": "GR-008",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "小程序短篇风|付费短篇|小程序体",
+            "意图与同义词": "小程序短篇怎么写|付费卡点怎么设计|短篇节奏",
+            "适用题材": "都市|情感|悬疑",
+            "大模型指令": "强情绪冲突,密集虐点,精准付费卡点。",
+            "核心摘要": "小程序短篇要求在前500字内设置3个轻度不安事件,3000字左右设置1个重度冲击事件,利用心理困扰引导付费。",
+            "详细展开": "小程序短篇的核心是情绪操控和付费引导。前期要通过密集的小冲突累积读者的不安和愤怒情绪,在情绪峰值处设置付费墙。主角要遭遇不道德但不违法的对待,引发读者共鸣。结尾要留悬念或未解决的冲突。禁止前期平淡,禁止付费后立刻解决问题,要保持后续吸引力。",
+            "题材/流派": "小程序短篇风",
+            "题材别名": "付费短篇|小程序体|情绪短篇",
+            "核心调性": "强情绪冲突密集虐点",
+            "节奏策略": "前500字3个小冲突3000字1个大冲突",
+            "强制禁忌/毒点": "前期平淡|付费点不准|付费后立刻解决|缺少情绪累积",
+            "推荐基础检索表": "写作技法|场景写法|爽点与节奏",
+            "推荐动态检索表": "桥段套路|爽点与节奏|写作技法",
+            "默认查询词": "小程序|短篇|付费|卡点|情绪冲突",
+        },
+        # 继续添加 GR-009 到 GR-018...
+    ]
+}

+ 9 - 0
webnovel-writer/scripts/data_modules/chapter_commit_service.py

@@ -6,6 +6,8 @@ import json
 from pathlib import Path
 from typing import Any, Dict
 
+from chapter_outline_loader import volume_num_for_chapter_from_state
+
 from .config import DataModulesConfig
 from .event_log_store import EventLogStore
 from .event_projection_router import EventProjectionRouter
@@ -33,6 +35,7 @@ class ChapterCommitService:
             fulfillment_result.get("missed_nodes")
         ) or bool(disambiguation_result.get("pending"))
         status = "rejected" if rejected else "accepted"
+        volume = volume_num_for_chapter_from_state(self.project_root, chapter) or 1
         return {
             "meta": {
                 "schema_version": "story-system/v1",
@@ -41,9 +44,15 @@ class ChapterCommitService:
             },
             "contract_refs": {
                 "master": "MASTER_SETTING.json",
+                "volume": f"volume_{volume:03d}.json",
                 "chapter": f"chapter_{chapter:03d}.json",
                 "review": f"chapter_{chapter:03d}.review.json",
             },
+            "provenance": {
+                "write_fact_role": "chapter_commit",
+                "projection_role": "derived_read_models",
+                "legacy_state_role": "projection_only",
+            },
             "outline_snapshot": {
                 "planned_nodes": fulfillment_result.get("planned_nodes", []),
                 "covered_nodes": fulfillment_result.get("covered_nodes", []),

+ 16 - 0
webnovel-writer/scripts/data_modules/tests/test_chapter_commit_service.py

@@ -32,10 +32,26 @@ def test_commit_service_accepts_when_all_checks_pass(tmp_path):
     )
     assert payload["meta"]["status"] == "accepted"
     assert payload["contract_refs"]["master"] == "MASTER_SETTING.json"
+    assert payload["contract_refs"]["volume"] == "volume_001.json"
     assert payload["contract_refs"]["chapter"] == "chapter_003.json"
     assert payload["outline_snapshot"]["covered_nodes"] == ["发现陷阱"]
 
 
+def test_commit_service_includes_volume_ref_and_write_fact_provenance(tmp_path):
+    service = ChapterCommitService(tmp_path)
+    payload = service.build_commit(
+        chapter=3,
+        review_result={"blocking_count": 0},
+        fulfillment_result={"planned_nodes": ["发现陷阱"], "covered_nodes": ["发现陷阱"], "missed_nodes": [], "extra_nodes": []},
+        disambiguation_result={"pending": []},
+        extraction_result={"state_deltas": [], "entity_deltas": [], "accepted_events": []},
+    )
+
+    assert payload["contract_refs"]["volume"] == "volume_001.json"
+    assert payload["provenance"]["write_fact_role"] == "chapter_commit"
+    assert payload["provenance"]["projection_role"] == "derived_read_models"
+
+
 def test_chapter_commit_cli_builds_and_persists_commit(tmp_path, monkeypatch):
     review_path = tmp_path / "review.json"
     fulfillment_path = tmp_path / "fulfillment.json"

+ 35 - 0
webnovel-writer/scripts/data_modules/tests/test_prompt_integrity.py

@@ -261,3 +261,38 @@ def test_story_system_runtime_contract_commands_exist():
     text = (SKILLS_DIR / "webnovel-write" / "SKILL.md").read_text(encoding="utf-8")
     block = re.search(r"story-system[\s\S]+--emit-runtime-contracts[\s\S]+REVIEW_CONTRACT", text)
     assert block, "webnovel-write skill 必须包含生成 runtime contracts 的完整步骤块"
+
+
+def test_webnovel_write_skill_uses_chapter_commit_as_step5_mainline():
+    text = (SKILLS_DIR / "webnovel-write" / "SKILL.md").read_text(encoding="utf-8")
+    assert "chapter-commit" in text
+    assert "accepted `CHAPTER_COMMIT`" in text
+    assert "state process-chapter" not in text
+
+
+def test_webnovel_query_skill_prefers_story_system_and_memory_contract():
+    text = (SKILLS_DIR / "webnovel-query" / "SKILL.md").read_text(encoding="utf-8")
+    assert "memory-contract load-context" in text
+    assert ".story-system/" in text
+    assert 'cat "$PROJECT_ROOT/.webnovel/state.json"' not in text
+
+
+def test_context_agent_prefers_contract_and_latest_commit_mainline():
+    text = (AGENTS_DIR / "context-agent.md").read_text(encoding="utf-8")
+    assert ".story-system/" in text
+    assert "accepted `CHAPTER_COMMIT`" in text
+    assert "memory-contract load-context" in text
+
+
+def test_data_agent_is_described_as_extraction_only_not_direct_write_mainline():
+    text = (AGENTS_DIR / "data-agent.md").read_text(encoding="utf-8")
+    assert "chapter-commit" in text
+    assert "extraction_result.json" in text
+    assert "直接写入 index.db 和 state.json" not in text
+
+
+def test_dashboard_and_plan_skills_surface_story_runtime_mainline():
+    dashboard_text = (SKILLS_DIR / "webnovel-dashboard" / "SKILL.md").read_text(encoding="utf-8")
+    plan_text = (SKILLS_DIR / "webnovel-plan" / "SKILL.md").read_text(encoding="utf-8")
+    assert "story-runtime/health" in dashboard_text
+    assert ".story-system/" in plan_text

+ 21 - 0
webnovel-writer/scripts/data_modules/tests/test_webnovel_unified_cli.py

@@ -256,6 +256,27 @@ def test_preflight_fails_when_required_scripts_are_missing(monkeypatch, tmp_path
     assert '"name": "entry_script"' in captured.out
 
 
+def test_preflight_includes_story_runtime_health(monkeypatch, tmp_path, capsys):
+    module = _load_webnovel_module()
+
+    project_root = tmp_path / "book"
+    (project_root / ".webnovel").mkdir(parents=True, exist_ok=True)
+    (project_root / ".webnovel" / "state.json").write_text("{}", encoding="utf-8")
+
+    monkeypatch.setattr(
+        sys,
+        "argv",
+        ["webnovel", "--project-root", str(project_root), "preflight", "--format", "json"],
+    )
+
+    with pytest.raises(SystemExit):
+        module.main()
+
+    captured = capsys.readouterr()
+    assert '"story_runtime"' in captured.out
+    assert '"mainline_ready"' in captured.out
+
+
 def test_quality_trend_report_writes_to_book_root_when_input_is_workspace_root(tmp_path, monkeypatch):
     _ensure_scripts_on_path()
     import quality_trend_report as quality_trend_report_module

+ 13 - 0
webnovel-writer/scripts/data_modules/webnovel.py

@@ -33,6 +33,8 @@ from typing import Optional
 from runtime_compat import normalize_windows_path
 from project_locator import resolve_project_root, write_current_project_pointer, update_global_registry_current_project
 
+from .story_runtime_health import build_story_runtime_health
+
 
 def _scripts_dir() -> Path:
     # data_modules/webnovel.py -> data_modules -> scripts
@@ -122,10 +124,12 @@ def _build_preflight_report(explicit_project_root: Optional[str]) -> dict:
 
     project_root = ""
     project_root_error = ""
+    story_runtime: dict = {}
     try:
         resolved_root = _resolve_root(explicit_project_root)
         project_root = str(resolved_root)
         checks.append({"name": "project_root", "ok": True, "path": project_root})
+        story_runtime = build_story_runtime_health(resolved_root)
     except Exception as exc:
         project_root_error = str(exc)
         checks.append({"name": "project_root", "ok": False, "path": explicit_project_root or "", "error": project_root_error})
@@ -137,6 +141,7 @@ def _build_preflight_report(explicit_project_root: Optional[str]) -> dict:
         "skill_root": str(skill_root),
         "checks": checks,
         "project_root_error": project_root_error,
+        "story_runtime": story_runtime,
     }
 
 
@@ -151,6 +156,14 @@ def cmd_preflight(args: argparse.Namespace) -> int:
             print(f"{status} {item['name']}: {path}")
             if item.get("error"):
                 print(f"  detail: {item['error']}")
+        story_runtime = report.get("story_runtime") or {}
+        if story_runtime:
+            print(
+                "INFO story_runtime: "
+                f"chapter={story_runtime.get('chapter')} "
+                f"mainline_ready={story_runtime.get('mainline_ready')} "
+                f"latest_commit_status={story_runtime.get('latest_commit_status')}"
+            )
     return 0 if report["ok"] else 1
 
 

+ 15 - 0
webnovel-writer/scripts/extract_chapter_context.py

@@ -312,6 +312,8 @@ def _load_contract_context(project_root: Path, chapter_num: int) -> Dict[str, An
         "context_contract_version": (payload.get("meta") or {}).get("context_contract_version"),
         "context_weight_stage": (payload.get("meta") or {}).get("context_weight_stage"),
         "story_contract": (sections.get("story_contract") or {}).get("content", {}),
+        "runtime_status": (sections.get("runtime_status") or {}).get("content", {}),
+        "latest_commit": (sections.get("latest_commit") or {}).get("content", {}),
         "prewrite_validation": (sections.get("prewrite_validation") or {}).get("content", {}),
         "reader_signal": (sections.get("reader_signal") or {}).get("content", {}),
         "genre_profile": (sections.get("genre_profile") or {}).get("content", {}),
@@ -343,6 +345,8 @@ def build_chapter_context_payload(project_root: Path, chapter_num: int) -> Dict[
         "context_contract_version": contract_context.get("context_contract_version"),
         "context_weight_stage": contract_context.get("context_weight_stage"),
         "story_contract": contract_context.get("story_contract", {}),
+        "runtime_status": contract_context.get("runtime_status", {}),
+        "latest_commit": contract_context.get("latest_commit", {}),
         "prewrite_validation": contract_context.get("prewrite_validation", {}),
         "reader_signal": contract_context.get("reader_signal", {}),
         "genre_profile": contract_context.get("genre_profile", {}),
@@ -389,6 +393,17 @@ def _render_text(payload: Dict[str, Any]) -> str:
             lines.append(f"- 上下文阶段权重: {stage}")
             lines.append("")
 
+    runtime_status = payload.get("runtime_status") or {}
+    latest_commit = payload.get("latest_commit") or {}
+    if runtime_status or latest_commit:
+        fallback_sources = runtime_status.get("fallback_sources") or ["none"]
+        lines.append("## Runtime Status")
+        lines.append("")
+        lines.append(f"- 写后事实入口: {runtime_status.get('primary_write_source', 'unknown')}")
+        lines.append(f"- Legacy Fallback: {', '.join(str(item) for item in fallback_sources)}")
+        lines.append(f"- Latest Commit: {(latest_commit.get('meta') or {}).get('status', 'missing')}")
+        lines.append("")
+
     story_contract = payload.get("story_contract") or {}
     review_contract = story_contract.get("review_contract") or {}
     prewrite_validation = payload.get("prewrite_validation") or {}

+ 102 - 0
webnovel-writer/scripts/update_reference_batch.py

@@ -5991,6 +5991,108 @@ BATCH_ROWS: dict[str, list[dict[str, str]]] = {
             "反套路变种": "主角不是独自决定,而是团队内部就普通胜利与真答案发生分裂,让选择本身也成为高潮",
         },
     ],
+    "题材与调性推理.csv": [
+        {
+            "编号": "GR-004",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "都市赘婿流|赘婿逆袭|上门女婿",
+            "意图与同义词": "赘婿怎么写|上门女婿翻身|赘婿打脸",
+            "适用题材": "都市",
+            "大模型指令": "先压后爆,耻辱累积后集中兑现。",
+            "核心摘要": "赘婿流必须先把主角的尊严和地位踩到底,通过妻子家族的羞辱、冷眼和轻视持续累积压抑,再通过身份反转或实力展现完成打脸。",
+            "详细展开": "赘婿流的核心在于身份落差带来的压抑感和后续的爽点兑现。开篇要快速建立主角的低位处境,通过岳父岳母、妻子亲戚的多重羞辱制造压抑,再通过隐藏身份曝光、商业手段反击等方式完成反打。禁止主角一开始就强势,必须先忍辱负重。",
+            "题材/流派": "都市赘婿流",
+            "题材别名": "赘婿逆袭|上门女婿|废婿翻身",
+            "核心调性": "先压后爆",
+            "节奏策略": "三章内必须有明确身份落差",
+            "强制禁忌/毒点": "主角开局就强势|反打来得太快|没有真实羞辱感",
+            "推荐基础检索表": "命名规则|人设与关系|场景写法",
+            "推荐动态检索表": "桥段套路|爽点与节奏|写作技法",
+            "默认查询词": "赘婿|上门女婿|打脸|身份反转",
+        },
+        {
+            "编号": "GR-005",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "追妻火葬场|虐文|先虐后甜",
+            "意图与同义词": "追妻火葬场怎么写|男主后悔怎么写|虐文节奏",
+            "适用题材": "言情|都市|古言",
+            "大模型指令": "前期虐心,后期追悔,情感逆转要有代价。",
+            "核心摘要": "追妻火葬场要先把男主的伤害写实写透,让女主的痛苦和决绝有足够铺垫,再让男主的追悔和补偿形成长线拉扯。",
+            "详细展开": "追妻火葬场的核心是情感逆转的可信度。前期男主对女主的冷漠、误解或伤害必须写得具体且有痛感,女主的离开要有充分动机。后期男主的追悔不能靠嘴炮,要通过实际行动和代价来体现。禁止男主轻易被原谅,女主的心软必须有合理过程。",
+            "题材/流派": "追妻火葬场",
+            "题材别名": "虐文|先虐后甜|男主追妻|火葬场文",
+            "核心调性": "前期高虐后期追悔",
+            "节奏策略": "前期虐点要实后期追悔要有代价",
+            "强制禁忌/毒点": "男主伤害不够痛|女主原谅太快|追悔没有实际代价",
+            "推荐基础检索表": "人设与关系|场景写法|写作技法",
+            "推荐动态检索表": "桥段套路|爽点与节奏|场景写法",
+            "默认查询词": "追妻|火葬场|虐文|后悔",
+        },
+        {
+            "编号": "GR-006",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "番茄爽文|男频快节奏|番茄风",
+            "意图与同义词": "番茄爽文怎么写|快节奏打脸|番茄风格",
+            "适用题材": "玄幻|都市|系统",
+            "大模型指令": "快节奏高密度,三章一爽点。",
+            "核心摘要": "番茄爽文要求节奏极快,信息密度高,每200-300字必须有一个小爽点,每3-5章必须有一个大爽点,开局即冲突。",
+            "详细展开": "番茄爽文的核心是密集的爽点和快节奏。开篇必须在前3句话内引入冲突,前3章内完成首次打脸或反转。主角金手指要强但不能无脑碾压,要通过智谋、反套路或信息差制造爽感。禁止拖沓铺垫,禁止大段心理描写,对话要有潜台词和网络梗。",
+            "题材/流派": "番茄爽文",
+            "题材别名": "男频快节奏|番茄风|高密度爽文",
+            "核心调性": "快节奏高密度",
+            "节奏策略": "每3章一个大爽点每章至少一个小爽点",
+            "强制禁忌/毒点": "节奏拖沓|爽点稀疏|铺垫过长|对话平淡",
+            "推荐基础检索表": "命名规则|人设与关系|金手指与设定",
+            "推荐动态检索表": "桥段套路|爽点与节奏|场景写法",
+            "默认查询词": "番茄|爽文|快节奏|打脸",
+        },
+        {
+            "编号": "GR-007",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "知乎短篇风|第一人称短篇|知乎体",
+            "意图与同义词": "知乎短篇怎么写|第一人称怎么写|知乎风格",
+            "适用题材": "都市|情感|职场",
+            "大模型指令": "第一人称强代入,炸裂开头,精准卡点。",
+            "核心摘要": "知乎短篇要求第一人称视角,50字内炸裂开头,快节奏推进,强冲突设计,关键位置精准卡点引导付费。",
+            "详细展开": "知乎短篇的核心是第一人称的强代入感和快节奏。开头必须在50字内用冲突或悬念抓住读者,一句话一段落,减少对话增加内心独白。情节要有多次反转,卡点位置要设置在情绪高峰或悬念最强处。禁止第三人称,禁止慢热铺垫,禁止大段环境描写。",
+            "题材/流派": "知乎短篇风",
+            "题材别名": "第一人称短篇|知乎体|知乎盐选",
+            "核心调性": "第一人称强代入快节奏",
+            "节奏策略": "50字内炸裂开头关键位置精准卡点",
+            "强制禁忌/毒点": "第三人称|慢热开头|卡点位置不准|缺少反转",
+            "推荐基础检索表": "写作技法|场景写法|人设与关系",
+            "推荐动态检索表": "桥段套路|爽点与节奏|写作技法",
+            "默认查询词": "知乎|短篇|第一人称|卡点",
+        },
+        {
+            "编号": "GR-008",
+            "适用技能": "write|plan",
+            "分类": "题材路由",
+            "层级": "知识补充",
+            "关键词": "小程序短篇风|付费短篇|小程序体",
+            "意图与同义词": "小程序短篇怎么写|付费卡点怎么设计|短篇节奏",
+            "适用题材": "都市|情感|悬疑",
+            "大模型指令": "强情绪冲突,密集虐点,精准付费卡点。",
+            "核心摘要": "小程序短篇要求在前500字内设置3个轻度不安事件,3000字左右设置1个重度冲击事件,利用心理困扰引导付费。",
+            "详细展开": "小程序短篇的核心是情绪操控和付费引导。前期要通过密集的小冲突累积读者的不安和愤怒情绪,在情绪峰值处设置付费墙。主角要遭遇不道德但不违法的对待,引发读者共鸣。结尾要留悬念或未解决的冲突。禁止前期平淡,禁止付费后立刻解决问题,要保持后续吸引力。",
+            "题材/流派": "小程序短篇风",
+            "题材别名": "付费短篇|小程序体|情绪短篇",
+            "核心调性": "强情绪冲突密集虐点",
+            "节奏策略": "前500字3个小冲突3000字1个大冲突",
+            "强制禁忌/毒点": "前期平淡|付费点不准|付费后立刻解决|缺少情绪累积",
+            "推荐基础检索表": "写作技法|场景写法|爽点与节奏",
+            "推荐动态检索表": "桥段套路|爽点与节奏|写作技法",
+            "默认查询词": "小程序|短篇|付费|卡点|情绪冲突",
+        },
+    ],
 }
 
 

+ 5 - 0
webnovel-writer/skills/webnovel-dashboard/SKILL.md

@@ -10,6 +10,7 @@ allowed-tools: Bash Read
 
 - 在本地启动只读 Web 面板。
 - 实时查看创作进度、设定词典、关系图谱、章节内容与追读力数据。
+- 显式查看 Story Runtime 主链状态,包括 `story-runtime/health`、latest commit 与 fallback 情况。
 - 允许监听 `.webnovel/` 变化,但不得修改项目内容。
 
 ## 执行流程
@@ -67,6 +68,10 @@ python -m dashboard.server --project-root "${PROJECT_ROOT}"
 python -m dashboard.server --project-root "${PROJECT_ROOT}" --no-browser
 ```
 
+启动后优先确认以下接口可用:
+- `/api/story-runtime/health`
+- `/api/preflight`
+
 ## 注意事项
 
 - Dashboard 为纯只读面板,不提供修改接口。

+ 3 - 0
webnovel-writer/skills/webnovel-plan/SKILL.md

@@ -60,6 +60,9 @@ python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${WORKSPACE_ROOT}" \
   story-system "{chapter_goal}" --chapter {chapter_num} --persist --emit-runtime-contracts --format both
 ```
 
+生成后必须把 `.story-system/MASTER_SETTING.json`、`.story-system/volumes/`、
+`.story-system/chapters/`、`.story-system/reviews/` 视为后续写作主链输入。
+
 ## 引用加载策略
 
 ### md 必读

+ 1 - 0
webnovel-writer/skills/webnovel-review/SKILL.md

@@ -11,6 +11,7 @@ allowed-tools: Read Grep Write Edit Bash Task AskUserQuestion
 - 解析真实书项目根目录,按统一流程完成章节审查。
 - 调用统一 `reviewer` 生成结构化问题列表与审查报告。
 - 把审查指标写入 `index.db`,并把审查记录写回 `state.json`。
+- 审查时优先依据 `.story-system/reviews/chapter_{NNN}.review.json` 与 latest accepted `CHAPTER_COMMIT` 判断主链事实。
 - 若存在关键问题,明确交给用户决定是否立即返工。
 
 ## 常见误区

+ 3 - 3
webnovel-writer/skills/webnovel-write/SKILL.md

@@ -145,9 +145,9 @@ python -X utf8 "${SCRIPTS_DIR}/webnovel.py" --project-root "${WORKSPACE_ROOT}" \
 ```
 
 **合同树必备文件**(写前真源,缺一不可):
-- `.story-system/MASTER_SETTING.json` - 全书主设定合同(题材、调性、核心禁忌)
-- `.story-system/volumes/volume_{volume_num}.json` - 本卷节奏合同(卷级目标、爽点密度)
-- `.story-system/reviews/chapter_{chapter_num}.review.json` - 本章审查合同(必须覆盖节点、本章禁区)
+- `.story-system/MASTER_SETTING.json` - `MASTER_SETTING`,全书主设定合同(题材、调性、核心禁忌)
+- `.story-system/volumes/volume_{volume_num}.json` - `VOLUME_BRIEF`,本卷节奏合同(卷级目标、爽点密度)
+- `.story-system/reviews/chapter_{chapter_num}.review.json` - `REVIEW_CONTRACT`,本章审查合同(必须覆盖节点、本章禁区)
 
 **阻断规则**:
 - 合同缺失或生成失败 → 直接阻断,不进入正文起草

Някои файлове не бяха показани, защото твърде много файлове са промени