Ver Fonte

feat(v5.4): 审查指标追踪与故事骨架采样

新增功能:
- review_metrics 表:记录审查评分/维度/问题数
- get-review-trend-stats:查询近期审查均值和短板
- story_skeleton:每 N 章采样历史摘要,构建长篇感知
- context_manager 配置项扩展(骨架间隔/样本数/摘要字数)

代码清理:
- 删除 context_pack_builder.py(功能已整合到 context_manager)
- 删除 context-engineering-upgrade-plan-v1.2.md(升级计划已完成)
- 术语规范化:"新增" → "引入"(首次出现)

测试: 97 passed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
lingfengQAQ há 4 meses atrás
pai
commit
73532312fb
40 ficheiros alterados com 614 adições e 1179 exclusões
  1. 19 14
      .claude/agents/context-agent.md
  2. 5 5
      .claude/agents/data-agent.md
  3. 5 5
      .claude/agents/high-point-checker.md
  4. 5 5
      .claude/agents/reader-pull-checker.md
  5. 0 410
      .claude/references/context-engineering-upgrade-plan-v1.2.md
  6. 3 1
      .claude/references/entity-management-spec.md
  7. 4 1
      .claude/references/genre-profiles.md
  8. 4 1
      .claude/references/reading-power-taxonomy.md
  9. 1 1
      .claude/scripts/__init__.py
  10. 10 10
      .claude/scripts/archive_manager.py
  11. 0 595
      .claude/scripts/context_pack_builder.py
  12. 2 1
      .claude/scripts/data_modules/__init__.py
  13. 1 1
      .claude/scripts/data_modules/api_client.py
  14. 6 1
      .claude/scripts/data_modules/config.py
  15. 77 13
      .claude/scripts/data_modules/context_manager.py
  16. 7 7
      .claude/scripts/data_modules/entity_linker.py
  17. 222 22
      .claude/scripts/data_modules/index_manager.py
  18. 3 3
      .claude/scripts/data_modules/migrate_state_to_sqlite.py
  19. 1 1
      .claude/scripts/data_modules/snapshot_manager.py
  20. 5 5
      .claude/scripts/data_modules/sql_state_manager.py
  21. 37 38
      .claude/scripts/data_modules/state_manager.py
  22. 77 0
      .claude/scripts/data_modules/tests/test_data_modules.py
  23. 1 1
      .claude/scripts/init_project.py
  24. 5 5
      .claude/scripts/status_reporter.py
  25. 1 1
      .claude/scripts/update_state.py
  26. 2 2
      .claude/scripts/workflow_manager.py
  27. 2 1
      .claude/skills/webnovel-init/references/system-data-flow.md
  28. 2 1
      .claude/skills/webnovel-query/references/system-data-flow.md
  29. 3 3
      .claude/skills/webnovel-query/references/tag-specification.md
  30. 2 1
      .claude/skills/webnovel-resume/references/system-data-flow.md
  31. 3 2
      .claude/skills/webnovel-resume/references/workflow-resume.md
  32. 30 0
      .claude/skills/webnovel-review/SKILL.md
  33. 4 3
      .claude/skills/webnovel-review/references/core-constraints.md
  34. 33 5
      .claude/skills/webnovel-write/SKILL.md
  35. 3 2
      .claude/skills/webnovel-write/references/core-constraints.md
  36. 3 3
      .claude/skills/webnovel-write/references/polish-guide.md
  37. 1 1
      .claude/templates/golden-finger-templates.md
  38. 3 1
      .claude/templates/output/index-schema.md
  39. 3 1
      .claude/templates/output/state-schema.md
  40. 19 6
      README.md

+ 19 - 14
.claude/agents/context-agent.md

@@ -1,16 +1,16 @@
 ---
 name: context-agent
-description: 上下文搜集Agent (v5.3),输出创作任务书(人话版),集成追读力设计、题材Profile、债务状态。
+description: 上下文搜集Agent (v5.4),输出创作任务书(人话版),集成追读力设计、题材Profile、债务状态。
 tools: Read, Grep, Bash
 ---
 
-# context-agent (上下文搜集Agent v5.3)
+# context-agent (上下文搜集Agent v5.4)
 
 > **Role**: 创作任务书生成器。目标不是堆信息,而是给写作"能直接开写"的明确指令。
 >
 > **Philosophy**: 按需召回 + 推断补全,保证"接住上章、带出情绪、留出钩子"。
 >
-> **v5.3 新增**: 追读力设计块、题材Profile引用、债务状态提醒。
+> **v5.3 引入(v5.4 沿用)**: 追读力设计块、题材Profile引用、债务状态提醒。
 
 ## 核心参考
 
@@ -30,18 +30,18 @@ tools: Read, Grep, Bash
 
 ## 输出格式:创作任务书(人话版)
 
-必须按以下 **10 个章节** 输出(v5.3 增加 2 个):
+必须按以下 **10 个章节** 输出(v5.3 增加 2 个,v5.4 沿用):
 
 1. **本章核心任务**(冲突一句话、必须完成、绝对不能)
 2. **接住上章**(上章钩子、读者期待、开头必须)
 3. **出场角色**(状态、动机、情绪底色、说话风格、红线)
 4. **场景与力量约束**(地点、可用能力、禁用能力)
-5. **风格指导**(本章类型、参考样本、最近模式、本章建议)
+5. **风格指导**(本章类型、参考样本、最近模式、本章建议、近期审查趋势
 6. **伏笔管理**(必须处理、可选提及)
 7. **连贯性检查点**(时间、位置、情绪)
 8. **章末钩子设置**(建议类型、禁止事项)
-9. **追读力设计**(v5.3 新增
-10. **债务与Override状态**(v5.3 新增
+9. **追读力设计**(v5.3 引入
+10. **债务与Override状态**(v5.3 引入
 
 ---
 
@@ -55,6 +55,7 @@ tools: Read, Grep, Bash
 | 角色动机 | 从大纲+角色状态推断 | **必须推断,无默认值** |
 | 题材Profile | `state.json → project.genre` | 默认 "shuangwen" |
 | 当前债务 | `index.db → chase_debt` | 0 |
+| 近期审查趋势 | `index.db → review_metrics` | 无数据则跳过 |
 
 **缺失处理**:
 - 若 `chapter_meta` 不存在(如第1章),跳过"接住上章"部分
@@ -68,6 +69,7 @@ tools: Read, Grep, Bash
 
 - `state.json`: 进度、主角状态、strand_tracker、chapter_meta、project.genre
 - `index.db`: 实体/别名/关系/状态变化/override_contracts/chase_debt/chapter_reading_power
+- `index.db`: review_metrics(审查趋势)
 - `.webnovel/summaries/ch{NNNN}.md`: 章节摘要(含钩子/结束状态)
 - `.webnovel/context_snapshots/`: 上下文快照(优先复用)
 - `.webnovel/preferences.json`: 用户偏好(阶段3)
@@ -87,6 +89,7 @@ python -m data_modules.context_manager --chapter {NNNN} --project-root "."
 - 若存在兼容快照,直接读取
 - 版本不兼容时自动重建并保存
 - 过滤 confirmed 的 invalid_facts,pending 标记为提示
+- context_pack 包含 story_skeleton(按间隔采样的历史摘要)
 
 ### Step 1: 读取题材Profile
 ```bash
@@ -125,6 +128,7 @@ cat ".claude/references/genre-profiles.md"
 python -m data_modules.index_manager get-recent-reading-power --limit 5 --project-root "."
 python -m data_modules.index_manager get-pattern-usage-stats --last-n 20 --project-root "."
 python -m data_modules.index_manager get-hook-type-stats --last-n 20 --project-root "."
+python -m data_modules.index_manager get-review-trend-stats --last-n 5 --project-root "."
 ```
 
 ### Step 5: 查询债务状态
@@ -137,24 +141,24 @@ python -m data_modules.index_manager get-pending-overrides --before-chapter {cur
 - 对所有事实性引用追加来源标注,例如:
   - `【来源: summaries/ch0100.md】`
   - `【来源: 正文/第0100章.md#scene_2】`
-- 若为推断信息,明确标注“推断”
+- 若为推断信息,明确标注"推断"
 
-### Step 6: 查询实体与关系(index.db)
+### Step 7: 查询实体与关系(index.db)
 ```bash
 python -m data_modules.index_manager get-core-entities --project-root "."
 python -m data_modules.index_manager recent-appearances --limit 20 --project-root "."
 python -m data_modules.index_manager get-relationships --entity "{protagonist}" --project-root "."
 ```
 
-### Step 7: 读取最近摘要
+### Step 8: 读取最近摘要
 - 优先读取 `.webnovel/summaries/ch{NNNN}.md`
 - 若缺失,降级为章节正文前 300-500 字概述
 
-### Step 8: 伏笔与风格样本
+### Step 9: 伏笔与风格样本
 - 伏笔:优先取 `foreshadowing_index`(若可用)
 - 风格样本:按本章类型选择 1-3 个高质量片段
 
-### Step 9: 推断补全
+### Step 10: 推断补全
 **推断规则(必须执行)**:
 - 动机 = 角色目标 + 当前处境 + 上章钩子压力
 - 情绪底色 = 上章结束情绪 + 事件走向
@@ -190,6 +194,7 @@ python -m data_modules.index_manager get-relationships --entity "{protagonist}"
 - 参考样本:第42章突破片段
 - 最近模式:危机钩(2次)、渴望钩(1次)
 - 本章建议:使用渴望钩,避免与上章重复
+- 近期审查趋势:过去2次总评均值 48/60,短板在节奏控制(均值6/10)
 
 ### 六、伏笔管理
 - 必须处理:药老提到的"那个人"(第87章埋下)
@@ -205,7 +210,7 @@ python -m data_modules.index_manager get-relationships --entity "{protagonist}"
 - 强度建议:medium(非卷末)
 - 禁止事项:不要用"他沉沉睡去"收尾
 
-### 九、追读力设计(v5.3 新增
+### 九、追读力设计(v5.3 引入
 
 #### 9.1 题材配置(当前:玄幻)
 - 偏好钩子:危机钩、渴望钩、选择钩
@@ -227,7 +232,7 @@ python -m data_modules.index_manager get-relationships --entity "{protagonist}"
 - 最近5章爽点模式:装逼打脸(2)、越级反杀(1)、扮猪吃虎(1)
 - 本章建议:避免装逼打脸,考虑使用迪化误解(配角对突破的误解)
 
-### 十、债务与Override状态(v5.3 新增
+### 十、债务与Override状态(v5.3 引入
 
 #### 10.1 当前债务
 - 活跃债务:1笔

+ 5 - 5
.claude/agents/data-agent.md

@@ -1,16 +1,16 @@
 ---
 name: data-agent
-description: 数据处理Agent (v5.2),负责AI实体提取、场景切片、索引构建,并记录钩子/模式/结束状态与章节摘要。
+description: 数据处理Agent (v5.4),负责 AI 实体提取、场景切片、索引构建,并记录钩子/模式/结束状态与章节摘要。
 tools: Read, Write, Bash
 ---
 
-# data-agent (数据处理Agent v5.2)
+# data-agent (数据处理Agent v5.4)
 
 > **Role**: 智能数据工程师,负责从章节正文中提取结构化信息并写入数据链。
 >
 > **Philosophy**: AI驱动提取,智能消歧 - 用语义理解替代正则匹配,用置信度控制质量。
 
-**v5.2 变更**:
+**v5.2 变更(v5.4 沿用)**:
 - 章节摘要不再追加到正文,改为 `.webnovel/summaries/ch{NNNN}.md`
 - 在 state.json 写入 `chapter_meta`(钩子/模式/结束状态)
 
@@ -93,7 +93,7 @@ python -m data_modules.index_manager get-by-alias --alias "萧炎" --project-roo
 | 0.5 - 0.8 | 采用建议值,记录 warning |
 | < 0.5 | 标记待人工确认,不自动写入 |
 
-### Step D: 写入存储 (v5.2)
+### Step D: 写入存储 (v5.2 引入)
 
 **写入 index.db (实体/别名/状态变化/关系)**:
 ```bash
@@ -108,7 +108,7 @@ python -m data_modules.index_manager upsert-relationship --data '{...}' --projec
 python -m data_modules.state_manager process-chapter --chapter 100 --data '{...}' --project-root "."
 ```
 
-写入内容 (v5.2):
+写入内容 (v5.2 引入):
 - 更新 `progress.current_chapter`
 - 更新 `protagonist_state`
 - 更新 `strand_tracker`

+ 5 - 5
.claude/agents/high-point-checker.md

@@ -1,10 +1,10 @@
 ---
 name: high-point-checker
-description: 爽点密度检查 v5.3,支持迪化误解/身份掉马模式,输出结构化报告
+description: 爽点密度检查 v5.4,支持迪化误解/身份掉马模式,输出结构化报告
 tools: Read, Grep, Bash
 ---
 
-# high-point-checker (爽点检查器) v5.3
+# high-point-checker (爽点检查器) v5.4
 
 > **Role**: Quality assurance specialist focused on reader satisfaction mechanics (爽点设计).
 
@@ -40,7 +40,7 @@ Scan for the **8 standard execution modes** (执行模式):
 | **迪化误解** (Misunderstanding Elevation) | 主角随意行为 → 配角脑补升华 → 读者优越感 | Casual Action + Info Gap + Misinterpretation + Reader Superiority |
 | **身份掉马** (Identity Reveal) | 身份伪装 → 关键时刻揭露 → 周围震惊 | Concealment (long-term) + Trigger Event + Reveal + Mass Reaction |
 
-### Step 2.1: 迪化误解模式检测(v5.3 新增
+### Step 2.1: 迪化误解模式检测(v5.3 引入
 
 **核心结构**:
 1. 主角随意行为(无心插柳)
@@ -58,7 +58,7 @@ Scan for the **8 standard execution modes** (执行模式):
 - B级:脑补尚可,效果一般
 - C级:脑补太刻意,配角显得蠢
 
-### Step 2.2: 身份掉马模式检测(v5.3 新增
+### Step 2.2: 身份掉马模式检测(v5.3 引入
 
 **核心结构**:
 1. 身份伪装(需长期铺垫)
@@ -192,7 +192,7 @@ Chapters {N} - {M}
 - 身份掉马需有铺垫
 - Report includes actionable recommendations
 
-## v5.3 输出格式增强
+## v5.3 输出格式增强(v5.4 沿用)
 
 ```json
 {

+ 5 - 5
.claude/agents/reader-pull-checker.md

@@ -1,10 +1,10 @@
 ---
 name: reader-pull-checker
-description: 追读力检查器 v5.3,评估钩子/微兑现/约束分层,支持Override Contract
+description: 追读力检查器 v5.4,评估钩子/微兑现/约束分层,支持 Override Contract
 tools: Read, Grep, Bash
 ---
 
-# reader-pull-checker (追读力检查器) v5.3
+# reader-pull-checker (追读力检查器) v5.4
 
 > **Role**: 审查"读者为什么会点下一章",执行 Hard/Soft 约束分层。
 
@@ -21,7 +21,7 @@ tools: Read, Grep, Bash
 - 题材 Profile(从 `state.json → project.genre`)
 - 是否为过渡章标记
 
-## 输出格式(v5.3 增强
+## 输出格式(v5.3 引入,v5.4 沿用
 
 ```json
 {
@@ -113,7 +113,7 @@ tools: Read, Grep, Bash
 
 ---
 
-## 二、钩子类型扩展(v5.3)
+## 二、钩子类型扩展(v5.3 引入
 
 ### 2.1 完整钩子类型
 
@@ -135,7 +135,7 @@ tools: Read, Grep, Bash
 
 ---
 
-## 三、微兑现检测(v5.3 新增
+## 三、微兑现检测(v5.3 引入
 
 ### 3.1 微兑现类型
 

+ 0 - 410
.claude/references/context-engineering-upgrade-plan-v1.2.md

@@ -1,410 +0,0 @@
-# Webnovel-Writer 上下文工程升级方案 v1.2
-
-> **状态**: 设计确认,待实施
-> **参与者**: Claude (Opus 4.5) + Codex + 用户确认
-> **日期**: 2026-02-02
-> **v1.2 变更**: 修正向量存储路径、/learn 路径、依赖清单
-> **兼容性**: 不考虑向前兼容(允许重建 `vectors.db` 与重跑索引)
-
----
-
-## 一、关键决策确认
-
-| 决策项 | 最终决定 | 理由 |
-|--------|---------|------|
-| 向量存储 | **保留 SQLite vectors.db** | 现有 rag_adapter 完整可用,重写成本高 |
-| 父子文档 | **在 vectors 表增加 parent_chunk_id** | 无需新建 FAISS/scenes 目录 |
-| 向前兼容 | **不考虑** | 允许重建 vectors.db 与父子索引 |
-| /webnovel-learn 位置 | **skills/webnovel-learn/SKILL.md** | 遵循现有扩展体系,统一前缀 |
-| 日志表位置 | **index.db** | 统一管理 |
-| scenes/ 目录 | **不新建** | 场景数据存 scenes 表 + vectors 表 |
-
----
-
-## 二、闭环确认清单(修正版)
-
-| 模块 | 写入方 | 读取方 | 存储位置 | 状态 |
-|------|--------|--------|---------|------|
-| ContextManager | - | context-agent 调用 | 工具类(无持久化) | 待实现 |
-| context_snapshots/ | context-agent | context-agent(下章) | .webnovel/context_snapshots/ | 待实现 |
-| cli_output.py | - | 所有 CLI 输出 | 工具类 | 待实现 |
-| schemas.py | - | data-agent 校验 | 工具类 | 待实现 |
-| invalid_facts 表 | 用户+checker | context-agent | index.db | 待实现 |
-| mark-invalid 命令 | 用户 | invalid_facts | index_manager.py | 待实现 |
-| 父子向量索引 | data-agent | rag_adapter | vectors.db(重建+新增字段) | 待实现 |
-| query_router.py | - | context-agent | 工具类 | 待实现 |
-| 来源标注 | context-agent | 人读 | 任务书文本 | 待实现 |
-| rag_query_log | rag_adapter | 手动SQL | index.db | 待实现 |
-| tool_call_stats | CLI工具 | 手动SQL | index.db | 待实现 |
-| preferences.json | 用户/init | context-agent | .webnovel/ | 待实现 |
-| project_memory.json | /webnovel-learn | context-agent | .webnovel/ | 待实现 |
-
----
-
-## 三、修正:向量存储方案
-
-### 3.1 现有结构(保留)
-
-```python
-# rag_adapter.py 使用 SQLite 存储向量
-# 表结构:
-CREATE TABLE vectors (
-    chunk_id TEXT PRIMARY KEY,
-    chapter INTEGER,
-    scene_index INTEGER,
-    content TEXT,
-    embedding BLOB,
-    created_at TIMESTAMP
-);
-```
-
-### 3.2 扩展方案(父子文档)
-
-```sql
--- 不考虑向前兼容:重建 vectors 表(旧向量不保留)
-DROP TABLE IF EXISTS vectors;
-CREATE TABLE vectors (
-    chunk_id TEXT PRIMARY KEY,
-    chapter INTEGER,
-    scene_index INTEGER,
-    content TEXT,
-    embedding BLOB,
-    parent_chunk_id TEXT,
-    chunk_type TEXT DEFAULT 'scene',  -- 'summary' | 'scene'
-    source_file TEXT,                -- 来源文件路径
-    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX idx_vectors_chapter ON vectors(chapter);
-CREATE INDEX idx_vectors_parent ON vectors(parent_chunk_id);
-CREATE INDEX idx_vectors_type ON vectors(chunk_type);
-```
-
-**父子关系**:
-- 父块 (summary): chunk_type='summary', parent_chunk_id=NULL
-- 子块 (scene): chunk_type='scene', parent_chunk_id='ch0100_summary'
-
-**source_file 规范**:
-- summary: `summaries/chNNNN.md`
-- scene: `正文/第NNNN章.md#scene_{scene_index}`
-
-**检索 + 回溯**:
-```python
-def search_with_backtrack(self, query: str, top_k: int = 5) -> list:
-    # 1. 检索子块
-    child_results = self._vector_search(query, chunk_type='scene', top_k=top_k*2)
-
-    # 2. 收集父块 ID
-    parent_ids = set(r.parent_chunk_id for r in child_results if r.parent_chunk_id)
-
-    # 3. 查询父块上下文
-    parent_contexts = self._get_chunks_by_ids(parent_ids)
-
-    # 4. 合并返回
-    return self._merge_results(parent_contexts, child_results[:top_k])
-```
-
-**实施方式**:
-- 删除 `.webnovel/vectors.db` 后由 `rag_adapter` 重建
-- 或在初始化阶段执行 `DROP TABLE IF EXISTS vectors` 并重建
-
----
-
-## 四、修正:/learn 位置
-
-### 4.1 目录结构
-
-```
-.claude/
-├── skills/
-│   ├── webnovel-learn/               # 【新增】学习命令
-│   │   └── SKILL.md
-│   ├── webnovel-init/
-│   ├── webnovel-write/
-│   └── ...
-```
-
-### 4.2 skills/webnovel-learn/SKILL.md
-
-```markdown
----
-name: webnovel-learn
-description: 从当前会话中提取成功的写作模式并持久化到 project_memory.json
-allowed-tools: Read Write Bash
----
-
-# /webnovel-learn 命令
-
-## 触发条件
-- 用户主动调用 /webnovel-learn
-- 章节审查得分 > 85 时提示用户调用
-
-## 执行流程
-1. 分析当前会话中的成功模式
-2. 提取可复用的技巧(钩子设计、节奏控制、对话技巧等)
-3. 写入 `.webnovel/project_memory.json`
-
-## 输入
-```bash
-/webnovel-learn "本章的危机钩设计很有效,悬念拉满"
-```
-
-## 输出
-```json
-{
-  "status": "success",
-  "learned": {
-    "pattern_type": "hook",
-    "description": "危机钩设计:悬念拉满",
-    "source_chapter": 100,
-    "learned_at": "2026-02-02T12:00:00Z"
-  }
-}
-```
-```
-
----
-
-## 五、修正:依赖清单
-
-### 5.1 requirements.txt 更新
-
-```txt
-# Webnovel Writer - Python Dependencies
-# Python >= 3.8 required
-
-# 核心依赖
-aiohttp>=3.8.0          # 异步 HTTP 客户端(API 调用)
-filelock>=3.0.0         # 文件锁(状态文件并发控制)
-pydantic>=2.0.0         # 【新增】Schema 校验
-
-# 可选依赖(开发/测试)
-pytest>=7.0.0           # 单元测试
-pytest-cov>=4.1.0       # 覆盖率统计
-```
-
----
-
-## 六、invalid_facts 详细设计(保持不变)
-
-### 6.1 表结构
-
-```sql
-CREATE TABLE invalid_facts (
-    id INTEGER PRIMARY KEY,
-    source_type TEXT NOT NULL,      -- entity/relationship/state_change
-    source_id TEXT NOT NULL,
-    reason TEXT NOT NULL,
-    status TEXT DEFAULT 'pending',  -- pending/confirmed
-    marked_by TEXT NOT NULL,        -- user/consistency-checker
-    marked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    confirmed_at TIMESTAMP,
-    chapter_discovered INTEGER
-);
-
-CREATE INDEX idx_invalid_status ON invalid_facts(status);
-CREATE INDEX idx_invalid_source ON invalid_facts(source_type, source_id);
-```
-
-### 6.2 CLI 命令
-
-```bash
-# 标记无效
-python -m data_modules.index_manager mark-invalid \
-    --source-type entity \
-    --source-id 123 \
-    --reason "境界描述与第50章矛盾" \
-    --chapter 75 \
-    --project-root "."
-
-# 确认无效
-python -m data_modules.index_manager resolve-invalid \
-    --id 1 --action confirm --project-root "."
-
-# 查看待确认
-python -m data_modules.index_manager list-invalid \
-    --status pending --project-root "."
-```
-
-### 6.3 consistency-checker 集成
-
-需要在 `consistency-checker.md` 的 Step 4 后增加:
-
-```markdown
-### Step 5: 标记无效事实(新增)
-
-对于发现的 CRITICAL 级别问题,自动标记到 invalid_facts:
-
-```bash
-python -m data_modules.index_manager mark-invalid \
-    --source-type entity \
-    --source-id {entity_id} \
-    --reason "{问题描述}" \
-    --marked-by consistency-checker \
-    --chapter {current_chapter} \
-    --project-root "."
-```
-
-**注意**: 自动标记的状态为 `pending`,需用户确认后才生效。
-```
-
----
-
-## 七、日志表设计
-
-### 7.1 rag_query_log(在 index.db)
-
-```sql
-CREATE TABLE rag_query_log (
-    id INTEGER PRIMARY KEY,
-    query TEXT,
-    query_type TEXT,           -- entity/plot/scene/setting
-    results_count INTEGER,
-    hit_sources TEXT,          -- JSON: {"summary": 2, "scene": 3}
-    latency_ms INTEGER,
-    chapter INTEGER,           -- 发起查询的章节
-    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX idx_rag_query_type ON rag_query_log(query_type);
-CREATE INDEX idx_rag_query_chapter ON rag_query_log(chapter);
-```
-
-### 7.2 tool_call_stats(在 index.db)
-
-```sql
-CREATE TABLE tool_call_stats (
-    id INTEGER PRIMARY KEY,
-    tool_name TEXT,
-    success BOOLEAN,
-    retry_count INTEGER DEFAULT 0,
-    error_code TEXT,
-    error_message TEXT,
-    chapter INTEGER,
-    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX idx_tool_stats_name ON tool_call_stats(tool_name);
-CREATE INDEX idx_tool_stats_chapter ON tool_call_stats(chapter);
-```
-
----
-
-## 八、目录结构(修正版)
-
-```
-.claude/
-├── scripts/
-│   └── data_modules/
-│       ├── context_manager.py      # 【新增】上下文管理器
-│       ├── cli_output.py           # 【新增】统一输出格式
-│       ├── schemas.py              # 【新增】Pydantic Schema
-│       ├── query_router.py         # 【新增】查询路由器 (阶段2)
-│       ├── snapshot_manager.py     # 【新增】快照版本管理
-│       ├── index_manager.py        # 【修改】增加 invalid_facts + 日志表
-│       └── rag_adapter.py          # 【修改】增加父子索引 + 回溯
-├── skills/
-│   └── webnovel-learn/                 # 【新增】阶段3
-│       └── SKILL.md
-├── agents/
-│   ├── context-agent.md            # 【修改】集成 ContextManager + 来源标注
-│   └── consistency-checker.md      # 【修改】集成 invalid_facts 写入
-└── references/
-    └── context-engineering-upgrade-plan-v1.2.md  # 本文档
-
-.webnovel/  # (在用户的小说项目中)
-├── context_snapshots/              # 【新增】上下文快照
-│   └── ch0100.json
-├── index.db                        # 现有(增加新表)
-├── vectors.db                      # 现有(增加字段)
-├── state.json                      # 现有
-├── summaries/                      # 现有
-├── preferences.json                # 【新增】阶段3
-└── project_memory.json             # 【新增】阶段3
-```
-
-**注意**: 删除了原计划中的:
-- `.webnovel/scenes/` 目录(场景数据存表)
-- `.webnovel/vectors/*.faiss` 文件(保留 SQLite)
-
----
-
-## 九、阶段划分(修正版)
-
-### 阶段1:上下文控制与结构化输出
-
-| 任务 | 文件 | 变更类型 | 依赖 |
-|------|------|---------|------|
-| 1. requirements.txt 更新 | requirements.txt | 修改 | 无 |
-| 2. invalid_facts 表 | index_manager.py | 新增表 | 无 |
-| 3. mark-invalid 命令 | index_manager.py | 新增命令 | 任务2 |
-| 4. resolve-invalid 命令 | index_manager.py | 新增命令 | 任务2 |
-| 5. list-invalid 命令 | index_manager.py | 新增命令 | 任务2 |
-| 6. cli_output.py | 新文件 | 新增 | 无 |
-| 7. schemas.py | 新文件 | 新增 | 任务1 |
-| 8. snapshot_manager.py | 新文件 | 新增 | 任务6 |
-| 9. ContextManager | 新文件 | 新增 | 任务6,7,8 |
-| 10. consistency-checker 集成 | agents/*.md | 修改 | 任务3 |
-| 11. context-agent 集成 | agents/*.md | 修改 | 任务9 |
-
-### 阶段2:RAG 增强
-
-| 任务 | 文件 | 变更类型 | 依赖 |
-|------|------|---------|------|
-| 1. vectors 表重建(不兼容) | rag_adapter.py | 修改表结构 | 无 |
-| 2. 父子索引构建 | rag_adapter.py | 新增方法 | 任务1 |
-| 3. 回溯检索 | rag_adapter.py | 新增方法 | 任务2 |
-| 4. query_router.py | 新文件 | 新增 | 无 |
-| 5. rag_query_log 表 | index_manager.py | 新增表 | 无 |
-| 6. tool_call_stats 表 | index_manager.py | 新增表 | 无 |
-| 7. 来源标注 | context-agent.md | 修改输出 | 任务3 |
-| 8. data-agent 集成 | data-agent.md | 修改 | 任务1,2 |
-
-### 阶段3:记忆与评估
-
-| 任务 | 文件 | 变更类型 | 依赖 |
-|------|------|---------|------|
-| 1. preferences.json 设计 | 文档 | 设计 | 无 |
-| 2. project_memory.json 设计 | 文档 | 设计 | 无 |
-| 3. skills/webnovel-learn/SKILL.md | 新文件 | 新增 | 任务2 |
-| 4. 置信度过滤 | context_manager.py | 新增方法 | 阶段1 |
-| 5. context-agent 读取记忆 | context-agent.md | 修改 | 任务1,2 |
-
----
-
-## 十、验收标准(修正版)
-
-### 阶段1 验收
-
-- [ ] requirements.txt 包含 pydantic>=2.0.0
-- [ ] index.db 包含 invalid_facts 表
-- [ ] mark-invalid / resolve-invalid / list-invalid 命令可用
-- [ ] consistency-checker 可自动写入 pending 状态
-- [ ] context-agent 过滤 confirmed,提示 pending
-- [ ] cli_output.py 提供统一的 success/error 格式
-- [ ] schemas.py 提供 DataAgentOutput 等 Schema
-- [ ] snapshot_manager.py 可保存/加载快照
-- [ ] ContextManager 可按优先级组装上下文
-
-### 阶段2 验收
-
-- [ ] vectors.db 已重建(旧向量不保留)
-- [ ] vectors.db 包含 parent_chunk_id, chunk_type 字段
-- [ ] data-agent 写入时设置正确的父子关系
-- [ ] rag_adapter 支持 search_with_backtrack
-- [ ] query_router 可按问题类型分流
-- [ ] index.db 包含 rag_query_log, tool_call_stats 表
-- [ ] 任务书包含来源标注
-
-### 阶段3 验收
-
-- [ ] preferences.json 可被 context-agent 读取
-- [ ] project_memory.json 可被 context-agent 读取
-- [ ] /webnovel-learn 命令可写入 project_memory.json
-- [ ] 置信度过滤生效
-
----
-
-*文档版本: 1.2*
-*状态: 设计确认,待实施*
-*日期: 2026-02-02*

+ 3 - 1
.claude/references/entity-management-spec.md

@@ -1,8 +1,10 @@
 # 实体管理规范 (Entity Management Specification)
 
-> **版本**: 5.1
+> **版本**: 5.4(基于 5.1 规范)
 > **适用范围**: 所有实体类型(角色/地点/物品/势力/招式)
 > **核心目标**: AI 驱动的实体提取、别名管理、版本追踪
+>
+> **v5.4**: 版本号对齐,规范沿用 v5.1。
 
 ---
 

+ 4 - 1
.claude/references/genre-profiles.md

@@ -1,8 +1,10 @@
-# 题材配置档案 (Genre Profiles) v5.3
+# 题材配置档案 (Genre Profiles) v5.4
 
 > **定位**:本文档定义各题材的追读力配置参数,供 Step 1.5 / Context Agent / Checkers 读取。
 >
 > **原则**:配置用于"调整权重和建议",不做硬性裁决。
+>
+> **v5.4**:版本号对齐,内容沿用 v5.3。
 
 ---
 
@@ -466,4 +468,5 @@ override_config:
 
 | 版本 | 日期 | 变更 |
 |------|------|------|
+| v5.4 | 2026-02-03 | 版本号对齐,内容不变 |
 | v5.3 | 2026-02-01 | 初版,包含8个内置题材profile |

+ 4 - 1
.claude/references/reading-power-taxonomy.md

@@ -1,8 +1,10 @@
-# 追读力分类标准 (Reading Power Taxonomy) v5.3
+# 追读力分类标准 (Reading Power Taxonomy) v5.4
 
 > **定位**:本文档定义"追读力"相关的统一分类标准,供 Step 1.5 / Context Agent / Checkers 共享使用。
 >
 > **原则**:所有分类用于"指导性建议",不做硬性评分裁决。
+>
+> **v5.4**:版本号对齐,内容沿用 v5.3。
 
 ---
 
@@ -351,4 +353,5 @@
 
 | 版本 | 日期 | 变更 |
 |------|------|------|
+| v5.4 | 2026-02-03 | 版本号对齐,内容不变 |
 | v5.3 | 2026-02-01 | 初版,新增情绪钩/选择钩/渴望钩、迪化误解/身份掉马、微兑现体系、Hard/Soft分层 |

+ 1 - 1
.claude/scripts/__init__.py

@@ -4,7 +4,7 @@ webnovel-writer scripts package
 This package contains all Python scripts for the webnovel-writer plugin.
 """
 
-__version__ = "5.0.0"
+__version__ = "5.4.0"
 __author__ = "lcy"
 
 # Expose main modules

+ 10 - 10
.claude/scripts/archive_manager.py

@@ -45,7 +45,7 @@ from pathlib import Path
 from security_utils import create_secure_directory, atomic_write_json
 from project_locator import resolve_project_root
 
-# v5.1: 使用 IndexManager 读取实体
+# v5.1 引入: 使用 IndexManager 读取实体
 try:
     from data_modules.index_manager import IndexManager
     from data_modules.config import get_config
@@ -74,7 +74,7 @@ class ArchiveManager:
         self.state_file = project_root / ".webnovel" / "state.json"
         self.archive_dir = project_root / ".webnovel" / "archive"
 
-        # v5.1: IndexManager 用于读取实体
+        # v5.1 引入: IndexManager 用于读取实体
         self._config = get_config(project_root)
         self._index_manager = IndexManager(self._config)
 
@@ -147,11 +147,11 @@ class ArchiveManager:
         }
 
     def identify_inactive_characters(self, state):
-        """识别不活跃的次要角色 (v5.1 SQLite)"""
+        """识别不活跃的次要角色(v5.1 引入,v5.4 沿用)"""
         current_chapter = state.get("progress", {}).get("current_chapter", 0)
         threshold = self.config["character_inactive_threshold"]
 
-        # v5.1: 从 SQLite 获取所有角色实体
+        # v5.1 引入: 从 SQLite 获取所有角色实体
         characters = self._index_manager.get_entities_by_type("角色")
 
         inactive = []
@@ -296,7 +296,7 @@ class ArchiveManager:
         return old_reviews
 
     def archive_characters(self, inactive_list, dry_run=False):
-        """归档不活跃角色(v5.1: 使用 IndexManager 更新状态)"""
+        """归档不活跃角色(v5.1 引入:使用 IndexManager 更新状态)"""
         if not inactive_list:
             return 0
 
@@ -309,7 +309,7 @@ class ArchiveManager:
             item["character"]["archived_at"] = timestamp
             archived.append(item["character"])
 
-            # v5.1: 通过 IndexManager 更新实体状态
+            # v5.1 引入: 通过 IndexManager 更新实体状态
             if not dry_run:
                 try:
                     entity_id = item["character"].get("id")
@@ -365,8 +365,8 @@ class ArchiveManager:
         return len(old_reviews_list)
 
     def remove_from_state(self, state, inactive_chars, resolved_threads, old_reviews):
-        """从 state.json/SQLite 中移除已归档的数据 (v5.1)"""
-        # v5.1: 角色数据在 SQLite,archive_characters 已处理状态更新
+        """从 state.json/SQLite 中移除已归档的数据(v5.1 引入,v5.4 沿用)"""
+        # v5.1 引入: 角色数据在 SQLite,archive_characters 已处理状态更新
         # 这里只需要处理 state.json 中的伏笔和审查报告
 
         # 移除已归档的伏笔
@@ -477,7 +477,7 @@ class ArchiveManager:
         print(f"\n💾 文件大小: {trigger['file_size_mb']:.2f} MB → {new_size_mb:.2f} MB (节省 {saved_mb:.2f} MB)")
 
     def restore_character(self, name):
-        """恢复归档的角色(v5.1: 使用 IndexManager 恢复状态)"""
+        """恢复归档的角色(v5.1 引入:使用 IndexManager 恢复状态)"""
         archived = self.load_archive(self.characters_archive)
 
         # 查找角色
@@ -498,7 +498,7 @@ class ArchiveManager:
         archived = [char for char in archived if char["name"] != name]
         self.save_archive(self.characters_archive, archived)
 
-        # v5.1: 恢复到 SQLite (通过 IndexManager)
+        # v5.1 引入: 恢复到 SQLite (通过 IndexManager)
         char_id = char_to_restore.get("id", char_to_restore.get("name", "unknown"))
         try:
             # 更新实体状态为 active

+ 0 - 595
.claude/scripts/context_pack_builder.py

@@ -1,595 +0,0 @@
-#!/usr/bin/env python3
-"""
-Context Pack Builder v5.2
-
-为章节写作生成结构化上下文包,取代直接读取 state.json。
-
-v5.2 变更:
-- 使用 v5.1 index_manager schema (entities.id, aliases, current_json)
-- 移除对 entity_kv 表的依赖,改用 current_json 字段
-- 移除对 entity_aliases 表的依赖,改用 aliases 表
-- 章节摘要改为读取 .webnovel/summaries/chNNNN.md
-
-输出 Schema:
-{
-  "core": {
-    "chapter_outline": "本章大纲内容",
-    "protagonist_snapshot": {...},
-    "recent_summaries": [{...}, ...],
-    "recent_meta": [{...}, ...]
-  },
-  "scene": {
-    "location_context": {...},
-    "appearing_characters": [{entity_id, name, snapshot}, ...],
-    "urgent_foreshadowing": [{...}, ...]
-  },
-  "global": {
-    "worldview_skeleton": "...",
-    "power_system_skeleton": "...",
-    "style_contract_ref": "..."
-  }
-}
-
-使用方式:
-  python context_pack_builder.py --chapter 45 --project-root /path/to/project
-  python context_pack_builder.py --chapter 45 --output /tmp/context_pack.json
-"""
-
-import json
-import os
-import sys
-import argparse
-import re
-import sqlite3
-from pathlib import Path
-from typing import Optional, Dict, List, Any
-
-# 导入项目工具
-from project_locator import resolve_project_root
-from chapter_paths import find_chapter_file
-
-# 导入配置
-try:
-    from data_modules.config import get_config, DataModulesConfig
-except ImportError:
-    from scripts.data_modules.config import get_config, DataModulesConfig
-
-
-class ContextPackBuilder:
-    """上下文包构建器"""
-
-    def __init__(self, project_root: Path = None):
-        if project_root is None:
-            try:
-                project_root = resolve_project_root()
-            except FileNotFoundError:
-                project_root = Path.cwd()
-        else:
-            project_root = Path(project_root)
-
-        self.project_root = project_root
-        self.config = get_config(project_root)
-        self.state_file = project_root / ".webnovel" / "state.json"
-        self.index_db = project_root / ".webnovel" / "index.db"
-        self.summaries_dir = project_root / ".webnovel" / "summaries"
-        self.outline_dir = project_root / "大纲"
-        self.settings_dir = project_root / "设定集"
-        self.chapters_dir = project_root / "正文"
-
-        self._conn: Optional[sqlite3.Connection] = None
-
-    def _conn_index(self) -> Optional[sqlite3.Connection]:
-        if self._conn is not None:
-            return self._conn
-        if not self.index_db.exists():
-            return None
-        conn = sqlite3.connect(str(self.index_db))
-        conn.row_factory = sqlite3.Row
-        self._conn = conn
-        return conn
-
-    def build(self, chapter_num: int) -> Dict[str, Any]:
-        """构建完整上下文包"""
-        state = self._load_state()
-
-        return {
-            "meta": {
-                "chapter": chapter_num,
-                "project_root": str(self.project_root),
-                "version": "5.2"
-            },
-            "core": self._build_core(chapter_num),
-            "scene": self._build_scene(chapter_num),
-            "global": self._build_global(),
-            "alerts": self._build_alerts(state)
-        }
-
-    def _build_core(self, chapter_num: int) -> Dict[str, Any]:
-        """核心上下文:大纲、主角状态、近期摘要"""
-        state = self._load_state()
-
-        return {
-            "chapter_outline": self._get_chapter_outline(chapter_num),
-            "protagonist_snapshot": self._get_protagonist_snapshot(state),
-            "recent_summaries": self._get_recent_summaries(
-                chapter_num, window=self.config.context_recent_summaries_window
-            ),
-            "recent_meta": self._get_recent_chapter_meta(chapter_num, window=3),
-        }
-
-    def _build_scene(self, chapter_num: int) -> Dict[str, Any]:
-        """场景上下文:地点、出场角色、紧急伏笔"""
-        state = self._load_state()
-
-        # 从大纲推断本章地点和角色
-        outline = self._get_chapter_outline(chapter_num)
-        predicted_location = self._predict_location(outline, state)
-        predicted_characters = self._predict_characters(outline, state)
-
-        return {
-            "location_context": predicted_location,
-            "appearing_characters": predicted_characters,
-            "urgent_foreshadowing": self._get_urgent_foreshadowing(state, chapter_num)
-        }
-
-    def _build_global(self) -> Dict[str, Any]:
-        """全局上下文:世界观、力量体系、风格契约"""
-        return {
-            "worldview_skeleton": self._load_skeleton("世界观"),
-            "power_system_skeleton": self._load_skeleton("力量体系"),
-            "style_contract_ref": self._get_style_contract_ref()
-        }
-
-    def _build_alerts(self, state: Dict) -> Dict[str, Any]:
-        """风险提示:消歧警告、待确认项(v5.0)"""
-        slice_size = self.config.context_alerts_slice
-        return {
-            "disambiguation_warnings": state.get("disambiguation_warnings", [])[-slice_size:],
-            "disambiguation_pending": state.get("disambiguation_pending", [])[-slice_size:]
-        }
-
-    # ================== 辅助方法 ==================
-
-    def _load_state(self) -> Dict:
-        """加载 state.json"""
-        if not self.state_file.exists():
-            return {}
-        with open(self.state_file, 'r', encoding='utf-8') as f:
-            return json.load(f)
-
-    def _get_chapter_outline(self, chapter_num: int) -> str:
-        """获取本章大纲"""
-        # 尝试多种大纲文件格式
-        patterns = [
-            f"第{chapter_num}章*.md",
-            f"第{chapter_num:02d}章*.md",
-            f"第{chapter_num:03d}章*.md",
-            f"第{chapter_num:04d}章*.md",
-            f"章纲/第{chapter_num}章*.md",
-            f"章纲/第{chapter_num:02d}章*.md",
-        ]
-
-        for pattern in patterns:
-            matches = list(self.outline_dir.glob(pattern))
-            if matches:
-                with open(matches[0], 'r', encoding='utf-8') as f:
-                    return f.read()
-
-        # 尝试从卷纲中提取
-        volume_outline = self._extract_from_volume_outline(chapter_num)
-        if volume_outline:
-            return volume_outline
-
-        return f"[大纲未找到: 第{chapter_num}章]"
-
-    def _extract_from_volume_outline(self, chapter_num: int) -> Optional[str]:
-        """从卷纲中提取章节大纲"""
-        volume_files = list(self.outline_dir.glob("卷纲*.md")) + list(self.outline_dir.glob("*卷*.md"))
-
-        for vf in volume_files:
-            with open(vf, 'r', encoding='utf-8') as f:
-                content = f.read()
-
-            # 查找章节标记(兼容空格/中英文冒号/不同标题级别)
-            # 常见格式:### 第 1 章:标题 或 ### 第1章: 标题
-            heading_pattern = (
-                rf"(?m)^#+\s*第\s*{chapter_num}\s*章[::][^\n]*\n"
-                rf".*?(?=^#+\s*第\s*\d+\s*章|^##\s|\Z)"
-            )
-            match = re.search(heading_pattern, content, re.DOTALL)
-            if match:
-                return match.group(0).strip()
-
-            # 兼容无标题级别的格式:第 1 章 标题
-            plain_pattern = (
-                rf"(?m)^第\s*{chapter_num}\s*章[^\n]*\n"
-                rf".*?(?=^第\s*\d+\s*章|\Z)"
-            )
-            match = re.search(plain_pattern, content, re.DOTALL)
-            if match:
-                return match.group(0).strip()
-
-        return None
-
-    def _get_protagonist_snapshot(self, state: Dict) -> Dict:
-        """获取主角状态快照"""
-        protagonist = state.get("protagonist_state", {}) or {}
-        power = protagonist.get("power", {}) or {}
-        location = protagonist.get("location", {}) or {}
-
-        snapshot: Dict[str, Any] = {
-            "entity_id": str(protagonist.get("entity_id", "") or "").strip(),
-            "name": str(protagonist.get("name", "") or "").strip() or "主角",
-            "realm": str(power.get("realm", "") or "").strip(),
-            "layer": power.get("layer", 0),
-            "bottleneck": str(power.get("bottleneck", "") or "").strip(),
-            "golden_finger": protagonist.get("golden_finger", {}) or {},
-            "location": str(location.get("current", "") or "").strip(),
-        }
-
-        # 可选:从 index.db 补齐(以 entity_id 为准)
-        protagonist_id = snapshot.get("entity_id", "")
-        conn = self._conn_index()
-        if protagonist_id and conn is not None:
-            # v5.1 schema: entities 表使用 id 字段,current_json 存储状态
-            row = conn.execute(
-                "SELECT canonical_name, current_json FROM entities WHERE id = ? LIMIT 1",
-                (protagonist_id,),
-            ).fetchone()
-            if row:
-                if row["canonical_name"]:
-                    snapshot["name"] = row["canonical_name"]
-                # 从 current_json 解析状态
-                if row["current_json"]:
-                    try:
-                        current = json.loads(row["current_json"])
-                        if isinstance(current.get("realm"), str) and current.get("realm"):
-                            snapshot["realm"] = current["realm"]
-                        if current.get("layer") is not None and current.get("layer") != "":
-                            snapshot["layer"] = current["layer"]
-                        if isinstance(current.get("bottleneck"), str) and current.get("bottleneck"):
-                            snapshot["bottleneck"] = current["bottleneck"]
-                        if isinstance(current.get("location"), str) and current.get("location"):
-                            snapshot["location"] = current["location"]
-                    except (json.JSONDecodeError, TypeError):
-                        pass
-
-        return snapshot
-
-    def _get_recent_summaries(self, chapter_num: int, window: int = 3) -> List[Dict]:
-        """获取最近 N 章的摘要"""
-        summaries = []
-        start = max(1, chapter_num - window)
-
-        for ch in range(start, chapter_num):
-            summary = self._load_summary_file(ch)
-            if summary:
-                summaries.append(summary)
-                continue
-
-            # 兼容降级:若摘要文件不存在,尝试从章节正文提取
-            chapter_file = find_chapter_file(self.project_root, ch)
-            if chapter_file and chapter_file.exists():
-                fallback = self._extract_summary_from_chapter(chapter_file, ch)
-                if fallback:
-                    summaries.append(fallback)
-
-        return summaries
-
-    def _extract_summary_from_chapter(self, chapter_file: Path, chapter_num: int) -> Optional[Dict]:
-        """从章节文件中提取摘要"""
-        with open(chapter_file, 'r', encoding='utf-8') as f:
-            content = f.read()
-
-        # 查找摘要区块
-        summary_match = re.search(r'## 本章摘要\s*\r?\n(.*?)(?=\r?\n##|$)', content, re.DOTALL)
-        if summary_match:
-            summary_text = summary_match.group(1).strip()
-            return {
-                "chapter": chapter_num,
-                "summary": summary_text
-            }
-
-        # 没有摘要,返回章节标题
-        title_match = re.match(r'^#\s*(.+)', content)
-        title = title_match.group(1).strip() if title_match else f"第{chapter_num}章"
-
-        return {
-            "chapter": chapter_num,
-            "title": title,
-            "summary": None
-        }
-
-    def _load_summary_file(self, chapter_num: int) -> Optional[Dict]:
-        """从 .webnovel/summaries/chNNNN.md 读取摘要"""
-        summary_path = self.summaries_dir / f"ch{chapter_num:04d}.md"
-        if not summary_path.exists():
-            return None
-
-        text = summary_path.read_text(encoding="utf-8")
-
-        # 解析 YAML 头部(--- ... ---)
-        meta: Dict[str, Any] = {}
-        fm_match = re.match(r"^---\s*\r?\n(.*?)\r?\n---\s*\r?\n", text, re.DOTALL)
-        if fm_match:
-            fm = fm_match.group(1)
-            for line in fm.splitlines():
-                if ":" not in line:
-                    continue
-                key, _, value = line.partition(":")
-                key = key.strip()
-                value = value.strip()
-                if not key:
-                    continue
-                # 简单解析列表
-                if value.startswith("[") and value.endswith("]"):
-                    items = [v.strip().strip('\"').strip("'") for v in value[1:-1].split(",") if v.strip()]
-                    meta[key] = items
-                else:
-                    meta[key] = value.strip('\"').strip("'")
-
-        # 提取剧情摘要段落
-        summary_match = re.search(r"##\s*剧情摘要\s*\r?\n(.*?)(?=\r?\n##|\Z)", text, re.DOTALL)
-        summary_text = summary_match.group(1).strip() if summary_match else ""
-
-        result = {
-            "chapter": chapter_num,
-            "summary": summary_text
-        }
-        # 附加部分元数据(可选)
-        for k in ["hook_type", "hook_strength", "time", "location"]:
-            if k in meta:
-                result[k] = meta[k]
-        return result
-
-    def _get_recent_chapter_meta(self, chapter_num: int, window: int = 3) -> List[Dict[str, Any]]:
-        """读取最近 N 章的 chapter_meta(用于模式重复检查)"""
-        state = self._load_state()
-        meta = state.get("chapter_meta", {}) or {}
-        items: List[Dict[str, Any]] = []
-        for ch in range(max(1, chapter_num - window), chapter_num):
-            key_candidates = [f"{ch:04d}", str(ch)]
-            entry = None
-            for key in key_candidates:
-                if key in meta:
-                    entry = meta.get(key)
-                    break
-            if entry:
-                items.append({"chapter": ch, **entry})
-        return items
-
-    def _predict_location(self, outline: str, state: Dict) -> Dict:
-        """从大纲推断地点(优先使用 index.db 别名表)"""
-        conn = self._conn_index()
-        if conn is None:
-            return {"name": "未知地点", "desc": ""}
-
-        # v5.1 schema: 使用 aliases 表(替代 entity_aliases)
-        rows = conn.execute(
-            "SELECT alias, entity_id FROM aliases WHERE entity_type = ?",
-            ("地点",),
-        ).fetchall()
-        if not rows:
-            return {"name": "未知地点", "desc": ""}
-
-        # 先匹配更长的别名,降低误命中
-        candidates = sorted(
-            ((r["alias"], r["entity_id"]) for r in rows if r["alias"]),
-            key=lambda x: len(x[0]),
-            reverse=True,
-        )
-        for alias, entity_id in candidates:
-            if len(alias) < 2:
-                continue
-            if alias not in outline:
-                continue
-
-            # v5.1 schema: entities 表使用 id 字段
-            e = conn.execute(
-                "SELECT canonical_name, desc FROM entities WHERE id = ? LIMIT 1",
-                (entity_id,),
-            ).fetchone()
-            return {
-                "entity_id": entity_id,
-                "name": (e["canonical_name"] if e else "") or alias,
-                "desc": (e["desc"] if e else "") or "",
-                "match": alias,
-            }
-
-        return {"name": "未知地点", "desc": ""}
-
-    def _predict_characters(self, outline: str, state: Dict) -> List[Dict]:
-        """从大纲推断出场角色(优先使用 index.db 别名表)"""
-        conn = self._conn_index()
-        if conn is None:
-            return []
-
-        # v5.1 schema: 使用 aliases 表(替代 entity_aliases)
-        rows = conn.execute(
-            "SELECT alias, entity_id FROM aliases WHERE entity_type = ?",
-            ("角色",),
-        ).fetchall()
-        if not rows:
-            return []
-
-        matched_ids: set[str] = set()
-        for r in rows:
-            alias = r["alias"] or ""
-            if len(alias) < 2:
-                continue
-            if alias in outline:
-                matched_ids.add(r["entity_id"])
-
-        if not matched_ids:
-            return []
-
-        tier_order = {"核心": 0, "支线": 1, "装饰": 2, "": 3}
-        matched: List[Dict[str, Any]] = []
-        for entity_id in matched_ids:
-            # v5.1 schema: entities 表使用 id 字段,current_json 存储状态
-            e = conn.execute(
-                "SELECT canonical_name, tier, current_json FROM entities WHERE id = ? LIMIT 1",
-                (entity_id,),
-            ).fetchone()
-            if not e:
-                continue
-
-            # 从 current_json 解析快照
-            snapshot = {}
-            if e["current_json"]:
-                try:
-                    snapshot = json.loads(e["current_json"])
-                except (json.JSONDecodeError, TypeError):
-                    pass
-
-            matched.append(
-                {
-                    "entity_id": entity_id,
-                    "name": e["canonical_name"] or entity_id,
-                    "tier": e["tier"] or "",
-                    "snapshot": snapshot,
-                }
-            )
-
-        matched.sort(key=lambda x: tier_order.get(x.get("tier", ""), 3))
-        return matched[:self.config.context_max_appearing_characters]
-
-    def _get_urgent_foreshadowing(self, state: Dict, chapter_num: int) -> List[Dict]:
-        """获取紧急伏笔(优先使用 index.db 伏笔索引)"""
-        conn = self._conn_index()
-        if conn is not None:
-            try:
-                rows = conn.execute(
-                    "SELECT content, introduced_chapter, resolved_chapter, status, urgency, location "
-                    "FROM foreshadowing_index WHERE status = '未回收' ORDER BY urgency DESC LIMIT 5"
-                ).fetchall()
-                return [dict(r) for r in rows] if rows else []
-            except sqlite3.Error:
-                pass
-
-        # fallback:项目未建索引时直接读取 state.json
-        plot_threads = state.get("plot_threads", {}) or {}
-        items = plot_threads.get("foreshadowing", []) or []
-        urgent: List[Dict[str, Any]] = []
-
-        for fs in items:
-            if not isinstance(fs, dict):
-                continue
-            status = str(fs.get("status", "")).strip()
-            if status in {"已回收"}:
-                continue
-
-            planted_chapter = fs.get("planted_chapter") or fs.get("introduced_chapter") or 0
-            target_chapter = fs.get("target_chapter") or fs.get("target") or 0
-            try:
-                planted_chapter = int(planted_chapter)
-            except (TypeError, ValueError):
-                planted_chapter = 0
-            try:
-                target_chapter = int(target_chapter) if target_chapter else 0
-            except (TypeError, ValueError):
-                target_chapter = 0
-
-            chapters_pending = chapter_num - planted_chapter if planted_chapter else 0
-
-            # 使用配置的紧急度阈值
-            cfg = self.config
-            if chapters_pending > cfg.foreshadowing_urgency_pending_high:
-                urgency = cfg.foreshadowing_urgency_score_high
-            elif chapters_pending > cfg.foreshadowing_urgency_pending_medium:
-                urgency = cfg.foreshadowing_urgency_score_medium
-            elif target_chapter and chapter_num >= target_chapter - cfg.foreshadowing_urgency_target_proximity:
-                urgency = cfg.foreshadowing_urgency_score_target
-            else:
-                urgency = cfg.foreshadowing_urgency_score_low
-
-            if urgency >= cfg.foreshadowing_urgency_threshold_show:
-                urgent.append(
-                    {
-                        "content": fs.get("content") or fs.get("description") or "",
-                        "planted_chapter": planted_chapter,
-                        "target_chapter": target_chapter,
-                        "tier": fs.get("tier", ""),
-                        "urgency": urgency,
-                    }
-                )
-
-        urgent.sort(key=lambda x: x.get("urgency", 0), reverse=True)
-        return urgent[:self.config.context_max_urgent_foreshadowing]
-
-    def _load_skeleton(self, setting_type: str) -> str:
-        """加载设定骨架"""
-        patterns = [
-            f"{setting_type}.md",
-            f"{setting_type}/*.md",
-            f"*{setting_type}*.md"
-        ]
-
-        for pattern in patterns:
-            matches = list(self.settings_dir.glob(pattern))
-            if matches:
-                # 如果是目录,合并所有文件
-                if matches[0].is_dir():
-                    content = []
-                    for f in sorted(matches[0].glob("*.md")):
-                        with open(f, 'r', encoding='utf-8') as file:
-                            content.append(f"## {f.stem}\n{file.read()}")
-                    return "\n\n".join(content)
-                else:
-                    with open(matches[0], 'r', encoding='utf-8') as f:
-                        return f.read()
-
-        return f"[{setting_type}设定未找到]"
-
-    def _get_style_contract_ref(self) -> str:
-        """获取风格契约引用"""
-        style_file = self.settings_dir / "风格契约.md"
-        if style_file.exists():
-            with open(style_file, 'r', encoding='utf-8') as f:
-                return f.read()
-
-        # 检查其他可能的位置
-        for pattern in ["风格*.md", "写作风格*.md", "style*.md"]:
-            matches = list(self.settings_dir.glob(pattern))
-            if matches:
-                with open(matches[0], 'r', encoding='utf-8') as f:
-                    return f.read()
-
-        return "[风格契约未定义]"
-
-
-def main():
-    parser = argparse.ArgumentParser(description="Context Pack Builder v5.2")
-    parser.add_argument("--chapter", type=int, required=True, help="章节编号")
-    parser.add_argument("--project-root", metavar="PATH", help="项目根目录")
-    parser.add_argument("--output", metavar="FILE", help="输出文件路径(默认输出到 stdout)")
-    parser.add_argument("--pretty", action="store_true", help="格式化 JSON 输出")
-
-    args = parser.parse_args()
-
-    # 构建上下文包
-    builder = ContextPackBuilder(project_root=args.project_root)
-    context_pack = builder.build(args.chapter)
-
-    # 输出
-    if args.pretty:
-        output = json.dumps(context_pack, ensure_ascii=False, indent=2)
-    else:
-        output = json.dumps(context_pack, ensure_ascii=False)
-
-    if args.output:
-        with open(args.output, 'w', encoding='utf-8') as f:
-            f.write(output)
-        print(f"✅ 上下文包已保存到: {args.output}")
-    else:
-        print(output)
-
-
-if __name__ == "__main__":
-    # Windows UTF-8 编码修复
-    if sys.platform == 'win32':
-        import io
-        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
-
-    main()

+ 2 - 1
.claude/scripts/data_modules/__init__.py

@@ -16,7 +16,7 @@ from .config import DataModulesConfig, get_config, set_project_root
 from .api_client import ModalAPIClient, get_client
 from .entity_linker import EntityLinker, DisambiguationResult
 from .state_manager import StateManager, EntityState, Relationship, StateChange
-from .index_manager import IndexManager, ChapterMeta, SceneMeta
+from .index_manager import IndexManager, ChapterMeta, SceneMeta, ReviewMetrics
 from .rag_adapter import RAGAdapter, SearchResult
 from .context_manager import ContextManager
 from .snapshot_manager import SnapshotManager
@@ -43,6 +43,7 @@ __all__ = [
     "IndexManager",
     "ChapterMeta",
     "SceneMeta",
+    "ReviewMetrics",
     # RAG Adapter
     "RAGAdapter",
     "SearchResult",

+ 1 - 1
.claude/scripts/data_modules/api_client.py

@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 """
-Data Modules - API 客户端 (v5.0 OpenAI 兼容接口)
+Data Modules - API 客户端 (v5.4,v5.0 OpenAI 兼容接口沿用)
 
 支持两种 API 类型:
 1. openai: OpenAI 兼容的 /v1/embeddings 和 /v1/rerank 接口

+ 6 - 1
.claude/scripts/data_modules/config.py

@@ -58,7 +58,7 @@ class DataModulesConfig:
     def index_db(self) -> Path:
         return self.webnovel_dir / "index.db"
 
-    # v5.1: alias_index_file 已废弃,别名存储在 index.db aliases 表
+    # v5.1 引入: alias_index_file 已废弃,别名存储在 index.db aliases 表
 
     @property
     def chapters_dir(self) -> Path:
@@ -126,9 +126,14 @@ class DataModulesConfig:
     max_state_changes: int = 2000
 
     context_recent_summaries_window: int = 3
+    context_recent_meta_window: int = 3
     context_alerts_slice: int = 10
     context_max_appearing_characters: int = 10
     context_max_urgent_foreshadowing: int = 5
+    context_story_skeleton_interval: int = 20
+    context_story_skeleton_max_samples: int = 5
+    context_story_skeleton_snippet_chars: int = 400
+    context_extra_section_budget: int = 800
 
     export_recent_changes_slice: int = 20
     export_disambiguation_slice: int = 20

+ 77 - 13
.claude/scripts/data_modules/context_manager.py

@@ -6,6 +6,7 @@ ContextManager - assemble context packs with weighted priorities.
 from __future__ import annotations
 
 import json
+import re
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
@@ -22,6 +23,9 @@ class ContextManager:
         "emotion": {"core": 0.45, "scene": 0.35, "global": 0.20},
         "transition": {"core": 0.50, "scene": 0.25, "global": 0.25},
     }
+    EXTRA_SECTIONS = {"story_skeleton", "memory", "preferences", "alerts"}
+    SECTION_ORDER = ["core", "scene", "global", "story_skeleton", "memory", "preferences", "alerts"]
+    SUMMARY_SECTION_RE = re.compile(r"##\s*剧情摘要\s*\r?\n(.*?)(?=\r?\n##|\Z)", re.DOTALL)
 
     def __init__(self, config=None, snapshot_manager: Optional[SnapshotManager] = None):
         self.config = config or get_config()
@@ -66,16 +70,22 @@ class ContextManager:
     ) -> Dict[str, Any]:
         weights = self.TEMPLATE_WEIGHTS.get(template, self.TEMPLATE_WEIGHTS[self.DEFAULT_TEMPLATE])
         max_chars = max_chars or 8000
+        extra_budget = int(self.config.context_extra_section_budget or 0)
 
         sections = {}
-        for section_name in ["core", "scene", "global", "memory", "preferences", "alerts"]:
+        for section_name in self.SECTION_ORDER:
             if section_name in pack:
                 sections[section_name] = pack[section_name]
 
         assembled: Dict[str, Any] = {"meta": pack.get("meta", {}), "sections": {}}
         for name, content in sections.items():
             weight = weights.get(name, 0.0)
-            budget = int(max_chars * weight) if weight > 0 else None
+            if weight > 0:
+                budget = int(max_chars * weight)
+            elif name in self.EXTRA_SECTIONS and extra_budget > 0:
+                budget = extra_budget
+            else:
+                budget = None
             text = json.dumps(content, ensure_ascii=False)
             if budget is not None and len(text) > budget:
                 text = text[:budget]
@@ -112,13 +122,22 @@ class ContextManager:
         core = {
             "chapter_outline": self._load_outline(chapter),
             "protagonist_snapshot": state.get("protagonist_state", {}),
-            "recent_summaries": self._load_recent_summaries(chapter, window=3),
-            "recent_meta": self._load_recent_meta(state, chapter, window=3),
+            "recent_summaries": self._load_recent_summaries(
+                chapter,
+                window=self.config.context_recent_summaries_window,
+            ),
+            "recent_meta": self._load_recent_meta(
+                state,
+                chapter,
+                window=self.config.context_recent_meta_window,
+            ),
         }
 
         scene = {
             "location_context": state.get("protagonist_state", {}).get("location", {}),
-            "appearing_characters": self._load_recent_appearances(),
+            "appearing_characters": self._load_recent_appearances(
+                limit=self.config.context_max_appearing_characters,
+            ),
         }
         scene["appearing_characters"] = self.filter_invalid_items(
             scene["appearing_characters"], source_type="entity", id_key="entity_id"
@@ -132,17 +151,24 @@ class ContextManager:
 
         preferences = self._load_json_optional(self.config.webnovel_dir / "preferences.json")
         memory = self._load_json_optional(self.config.webnovel_dir / "project_memory.json")
+        story_skeleton = self._load_story_skeleton(chapter)
+        alert_slice = max(0, int(self.config.context_alerts_slice))
 
         return {
             "meta": {"chapter": chapter},
             "core": core,
             "scene": scene,
             "global": global_ctx,
+            "story_skeleton": story_skeleton,
             "preferences": preferences,
             "memory": memory,
             "alerts": {
-                "disambiguation_warnings": state.get("disambiguation_warnings", [])[-10:],
-                "disambiguation_pending": state.get("disambiguation_pending", [])[-10:],
+                "disambiguation_warnings": (
+                    state.get("disambiguation_warnings", [])[-alert_slice:] if alert_slice else []
+                ),
+                "disambiguation_pending": (
+                    state.get("disambiguation_pending", [])[-alert_slice:] if alert_slice else []
+                ),
             },
         }
 
@@ -168,11 +194,10 @@ class ContextManager:
 
     def _load_recent_summaries(self, chapter: int, window: int = 3) -> List[Dict[str, Any]]:
         summaries = []
-        summaries_dir = self.config.webnovel_dir / "summaries"
         for ch in range(max(1, chapter - window), chapter):
-            path = summaries_dir / f"ch{ch:04d}.md"
-            if path.exists():
-                summaries.append({"chapter": ch, "summary": path.read_text(encoding="utf-8")})
+            summary = self._load_summary_text(ch)
+            if summary:
+                summaries.append(summary)
         return summaries
 
     def _load_recent_meta(self, state: Dict[str, Any], chapter: int, window: int = 3) -> List[Dict[str, Any]]:
@@ -185,8 +210,8 @@ class ContextManager:
                     break
         return results
 
-    def _load_recent_appearances(self) -> List[Dict[str, Any]]:
-        appearances = self.index_manager.get_recent_appearances()
+    def _load_recent_appearances(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
+        appearances = self.index_manager.get_recent_appearances(limit=limit)
         return appearances or []
 
     def _load_setting(self, keyword: str) -> str:
@@ -203,6 +228,45 @@ class ContextManager:
             return matches[0].read_text(encoding="utf-8")
         return f"[{keyword}设定未找到]"
 
+    def _extract_summary_excerpt(self, text: str, max_chars: int) -> str:
+        if not text:
+            return ""
+        match = self.SUMMARY_SECTION_RE.search(text)
+        excerpt = match.group(1).strip() if match else text.strip()
+        if max_chars > 0 and len(excerpt) > max_chars:
+            return excerpt[:max_chars].rstrip()
+        return excerpt
+
+    def _load_summary_text(self, chapter: int, snippet_chars: Optional[int] = None) -> Optional[Dict[str, Any]]:
+        summary_path = self.config.webnovel_dir / "summaries" / f"ch{chapter:04d}.md"
+        if not summary_path.exists():
+            return None
+        text = summary_path.read_text(encoding="utf-8")
+        if snippet_chars:
+            summary_text = self._extract_summary_excerpt(text, snippet_chars)
+        else:
+            summary_text = text
+        return {"chapter": chapter, "summary": summary_text}
+
+    def _load_story_skeleton(self, chapter: int) -> List[Dict[str, Any]]:
+        interval = max(1, int(self.config.context_story_skeleton_interval))
+        max_samples = max(0, int(self.config.context_story_skeleton_max_samples))
+        snippet_chars = int(self.config.context_story_skeleton_snippet_chars)
+
+        if max_samples <= 0 or chapter <= interval:
+            return []
+
+        samples: List[Dict[str, Any]] = []
+        cursor = chapter - interval
+        while cursor >= 1 and len(samples) < max_samples:
+            summary = self._load_summary_text(cursor, snippet_chars=snippet_chars)
+            if summary and summary.get("summary"):
+                samples.append(summary)
+            cursor -= interval
+
+        samples.reverse()
+        return samples
+
     def _load_json_optional(self, path: Path) -> Dict[str, Any]:
         if not path.exists():
             return {}

+ 7 - 7
.claude/scripts/data_modules/entity_linker.py

@@ -1,14 +1,14 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 """
-Entity Linker - 实体消歧辅助模块 (v5.1)
+Entity Linker - 实体消歧辅助模块 (v5.4)
 
 为 Data Agent 提供实体消歧的辅助功能:
 - 置信度判断
 - 别名索引管理 (通过 index.db aliases 表)
 - 消歧结果记录
 
-v5.1 变更:
+v5.1 变更(v5.4 沿用):
 - 别名存储从 state.json 迁移到 index.db aliases 表
 - 使用 IndexManager 进行别名读写
 - 移除对 state.json 的直接操作
@@ -33,16 +33,16 @@ class DisambiguationResult:
 
 
 class EntityLinker:
-    """实体链接器 - 辅助 Data Agent 进行实体消歧 (v5.1 SQLite)"""
+    """实体链接器 - 辅助 Data Agent 进行实体消歧 (v5.1 SQLite,v5.4 沿用)"""
 
     def __init__(self, config=None):
         self.config = config or get_config()
         self._index_manager = IndexManager(self.config)
 
-    # ==================== 别名管理 (v5.1 SQLite) ====================
+    # ==================== 别名管理 (v5.1 SQLite,v5.4 沿用) ====================
 
     def register_alias(self, entity_id: str, alias: str, entity_type: str = "角色") -> bool:
-        """注册新别名(v5.1: 写入 index.db aliases 表)"""
+        """注册新别名(v5.1 引入:写入 index.db aliases 表)"""
         if not alias or not entity_id:
             return False
         return self._index_manager.register_alias(alias, entity_id, entity_type)
@@ -147,7 +147,7 @@ class EntityLinker:
         new_entities: List[Dict]
     ) -> List[str]:
         """
-        注册新实体的别名 (v5.1)
+        注册新实体的别名 (v5.1 引入,v5.4 沿用)
 
         返回注册的实体ID列表
         """
@@ -182,7 +182,7 @@ def main():
     from .cli_output import print_success, print_error
     from .index_manager import IndexManager
 
-    parser = argparse.ArgumentParser(description="Entity Linker CLI (v5.1 SQLite)")
+    parser = argparse.ArgumentParser(description="Entity Linker CLI (v5.4 SQLite)")
     parser.add_argument("--project-root", type=str, help="项目根目录")
 
     subparsers = parser.add_subparsers(dest="command")

+ 222 - 22
.claude/scripts/data_modules/index_manager.py

@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 """
-Index Manager - 索引管理模块 (v5.3)
+Index Manager - 索引管理模块 (v5.4)
 
 管理 index.db (SQLite) 的读写操作:
 - 章节元数据索引
@@ -12,7 +12,12 @@ Index Manager - 索引管理模块 (v5.3)
 - 状态变化记录
 - 关系存储
 - 快速查询接口
-- 追读力债务管理 (v5.3 新增)
+- 追读力债务管理 (v5.3 引入,v5.4 沿用)
+
+v5.4 变更:
+- 新增 invalid_facts 表:追踪无效事实 (pending/confirmed)
+- 新增 tool_call_stats 表:记录工具调用成功率与错误信息
+- 新增 review_metrics 表:记录审查指标与趋势数据
 
 v5.3 变更:
 - 新增 override_contracts 表:记录违背软建议时的Override Contract
@@ -65,7 +70,7 @@ class SceneMeta:
 
 @dataclass
 class EntityMeta:
-    """实体元数据 (v5.1 新增)"""
+    """实体元数据 (v5.1 引入)"""
 
     id: str
     type: str  # 角色/地点/物品/势力/招式
@@ -81,7 +86,7 @@ class EntityMeta:
 
 @dataclass
 class StateChangeMeta:
-    """状态变化记录 (v5.1 新增)"""
+    """状态变化记录 (v5.1 引入)"""
 
     entity_id: str
     field: str
@@ -93,7 +98,7 @@ class StateChangeMeta:
 
 @dataclass
 class RelationshipMeta:
-    """关系记录 (v5.1 新增)"""
+    """关系记录 (v5.1 引入)"""
 
     from_entity: str
     to_entity: str
@@ -104,7 +109,7 @@ class RelationshipMeta:
 
 @dataclass
 class OverrideContractMeta:
-    """Override Contract (v5.3 新增)"""
+    """Override Contract (v5.3 引入)"""
 
     chapter: int
     constraint_type: str  # SOFT_HOOK_STRENGTH / SOFT_MICROPAYOFF / etc.
@@ -118,7 +123,7 @@ class OverrideContractMeta:
 
 @dataclass
 class ChaseDebtMeta:
-    """追读力债务 (v5.3 新增)"""
+    """追读力债务 (v5.3 引入)"""
 
     id: int = 0
     debt_type: str = ""  # hook_strength / micropayoff / coolpoint / etc.
@@ -133,7 +138,7 @@ class ChaseDebtMeta:
 
 @dataclass
 class DebtEventMeta:
-    """债务事件日志 (v5.3 新增)"""
+    """债务事件日志 (v5.3 引入)"""
 
     debt_id: int
     event_type: (
@@ -146,7 +151,7 @@ class DebtEventMeta:
 
 @dataclass
 class ChapterReadingPowerMeta:
-    """章节追读力元数据 (v5.3 新增)"""
+    """章节追读力元数据 (v5.3 引入)"""
 
     chapter: int
     hook_type: str = ""  # 章末钩子类型
@@ -160,6 +165,20 @@ class ChapterReadingPowerMeta:
     debt_balance: float = 0.0  # 当前债务余额
 
 
+@dataclass
+class ReviewMetrics:
+    """审查指标记录 (v5.4 引入)"""
+
+    start_chapter: int
+    end_chapter: int
+    overall_score: float = 0.0
+    dimension_scores: Dict[str, float] = field(default_factory=dict)
+    severity_counts: Dict[str, int] = field(default_factory=dict)
+    critical_issues: List[str] = field(default_factory=list)
+    report_file: str = ""
+    notes: str = ""
+
+
 class IndexManager:
     """索引管理器"""
 
@@ -225,7 +244,7 @@ class IndexManager:
                 "CREATE INDEX IF NOT EXISTS idx_appearances_chapter ON appearances(chapter)"
             )
 
-            # ==================== v5.1 新增表 ====================
+            # ==================== v5.1 引入表 ====================
 
             # 实体表 (替代 state.json 中的 entities_v3)
             cursor.execute("""
@@ -284,7 +303,7 @@ class IndexManager:
                 )
             """)
 
-            # v5.1 索引
+            # v5.1 引入索引
             cursor.execute(
                 "CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type)"
             )
@@ -316,7 +335,7 @@ class IndexManager:
                 "CREATE INDEX IF NOT EXISTS idx_relationships_chapter ON relationships(chapter)"
             )
 
-            # ==================== v5.3 新增表:追读力债务管理 ====================
+            # ==================== v5.3 引入表:追读力债务管理 ====================
 
             # Override Contract 表
             cursor.execute("""
@@ -386,7 +405,7 @@ class IndexManager:
                 )
             """)
 
-            # v5.3 索引
+            # v5.3 引入索引
             cursor.execute(
                 "CREATE INDEX IF NOT EXISTS idx_override_contracts_chapter ON override_contracts(chapter)"
             )
@@ -436,6 +455,26 @@ class IndexManager:
                 "CREATE INDEX IF NOT EXISTS idx_invalid_source ON invalid_facts(source_type, source_id)"
             )
 
+            # 审查指标表
+            cursor.execute("""
+                CREATE TABLE IF NOT EXISTS review_metrics (
+                    start_chapter INTEGER NOT NULL,
+                    end_chapter INTEGER NOT NULL,
+                    overall_score REAL DEFAULT 0,
+                    dimension_scores TEXT,
+                    severity_counts TEXT,
+                    critical_issues TEXT,
+                    report_file TEXT,
+                    notes TEXT,
+                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                    PRIMARY KEY (start_chapter, end_chapter)
+                )
+            """)
+            cursor.execute(
+                "CREATE INDEX IF NOT EXISTS idx_review_metrics_end ON review_metrics(end_chapter)"
+            )
+
             # RAG 查询日志
             cursor.execute("""
                 CREATE TABLE IF NOT EXISTS rag_query_log (
@@ -1767,6 +1806,128 @@ class IndexManager:
                 stats[hook] = stats.get(hook, 0) + 1
             return stats
 
+    # ==================== v5.4 审查指标 ====================
+
+    def save_review_metrics(self, metrics: ReviewMetrics) -> None:
+        """保存审查指标记录"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute(
+                """
+                INSERT INTO review_metrics
+                (start_chapter, end_chapter, overall_score, dimension_scores,
+                 severity_counts, critical_issues, report_file, notes, created_at, updated_at)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+                ON CONFLICT(start_chapter, end_chapter)
+                DO UPDATE SET
+                    overall_score = excluded.overall_score,
+                    dimension_scores = excluded.dimension_scores,
+                    severity_counts = excluded.severity_counts,
+                    critical_issues = excluded.critical_issues,
+                    report_file = excluded.report_file,
+                    notes = excluded.notes,
+                    updated_at = CURRENT_TIMESTAMP
+            """,
+                (
+                    metrics.start_chapter,
+                    metrics.end_chapter,
+                    metrics.overall_score,
+                    json.dumps(metrics.dimension_scores, ensure_ascii=False),
+                    json.dumps(metrics.severity_counts, ensure_ascii=False),
+                    json.dumps(metrics.critical_issues, ensure_ascii=False),
+                    metrics.report_file,
+                    metrics.notes,
+                ),
+            )
+            conn.commit()
+
+    def get_recent_review_metrics(self, limit: int = 5) -> List[Dict]:
+        """获取最近审查记录"""
+        with self._get_conn() as conn:
+            cursor = conn.cursor()
+            cursor.execute(
+                """
+                SELECT * FROM review_metrics
+                ORDER BY end_chapter DESC, start_chapter DESC
+                LIMIT ?
+            """,
+                (limit,),
+            )
+            return [
+                self._row_to_dict(
+                    row,
+                    parse_json=["dimension_scores", "severity_counts", "critical_issues"],
+                )
+                for row in cursor.fetchall()
+            ]
+
+    def get_review_trend_stats(self, last_n: int = 5) -> Dict[str, Any]:
+        """获取审查趋势统计"""
+        records = self.get_recent_review_metrics(last_n)
+        if not records:
+            return {
+                "count": 0,
+                "overall_avg": 0.0,
+                "dimension_avg": {},
+                "severity_totals": {},
+                "recent_ranges": [],
+            }
+
+        overall_scores: List[float] = []
+        dimension_totals: Dict[str, float] = {}
+        dimension_counts: Dict[str, int] = {}
+        severity_totals: Dict[str, int] = {}
+
+        for record in records:
+            score = record.get("overall_score")
+            if score is not None:
+                try:
+                    overall_scores.append(float(score))
+                except (TypeError, ValueError):
+                    pass
+
+            dimensions = record.get("dimension_scores") or {}
+            if isinstance(dimensions, dict):
+                for key, value in dimensions.items():
+                    try:
+                        val = float(value)
+                    except (TypeError, ValueError):
+                        continue
+                    dimension_totals[key] = dimension_totals.get(key, 0.0) + val
+                    dimension_counts[key] = dimension_counts.get(key, 0) + 1
+
+            severities = record.get("severity_counts") or {}
+            if isinstance(severities, dict):
+                for key, value in severities.items():
+                    try:
+                        count = int(value)
+                    except (TypeError, ValueError):
+                        continue
+                    severity_totals[key] = severity_totals.get(key, 0) + count
+
+        overall_avg = round(sum(overall_scores) / len(overall_scores), 2) if overall_scores else 0.0
+        dimension_avg = {
+            key: round(dimension_totals[key] / dimension_counts[key], 2)
+            for key in dimension_totals
+            if dimension_counts.get(key, 0) > 0
+        }
+        recent_ranges = [
+            {
+                "start_chapter": record.get("start_chapter"),
+                "end_chapter": record.get("end_chapter"),
+                "overall_score": record.get("overall_score", 0),
+            }
+            for record in records
+        ]
+
+        return {
+            "count": len(records),
+            "overall_avg": overall_avg,
+            "dimension_avg": dimension_avg,
+            "severity_totals": severity_totals,
+            "recent_ranges": recent_ranges,
+        }
+
     def get_debt_summary(self) -> Dict[str, Any]:
         """获取债务汇总信息"""
         with self._get_conn() as conn:
@@ -2009,7 +2170,7 @@ class IndexManager:
             cursor.execute("SELECT MAX(chapter) FROM chapters")
             max_chapter = cursor.fetchone()[0] or 0
 
-            # v5.1 新增统计
+            # v5.1 引入统计
             cursor.execute("SELECT COUNT(*) FROM entities")
             entities = cursor.fetchone()[0]
 
@@ -2025,7 +2186,7 @@ class IndexManager:
             cursor.execute("SELECT COUNT(*) FROM relationships")
             relationships = cursor.fetchone()[0]
 
-            # v5.3 新增统计
+            # v5.3 引入统计
             cursor.execute("SELECT COUNT(*) FROM override_contracts")
             override_contracts = cursor.fetchone()[0]
 
@@ -2045,23 +2206,27 @@ class IndexManager:
             cursor.execute("SELECT COUNT(*) FROM chapter_reading_power")
             reading_power_records = cursor.fetchone()[0]
 
+            cursor.execute("SELECT COUNT(*) FROM review_metrics")
+            review_metrics = cursor.fetchone()[0]
+
             return {
                 "chapters": chapters,
                 "scenes": scenes,
                 "appearances": appearances,
                 "max_chapter": max_chapter,
-                # v5.1 新增
+                # v5.1 引入
                 "entities": entities,
                 "active_entities": active_entities,
                 "aliases": aliases,
                 "state_changes": state_changes,
                 "relationships": relationships,
-                # v5.3 新增
+                # v5.3 引入
                 "override_contracts": override_contracts,
                 "pending_overrides": pending_overrides,
                 "active_debts": active_debts,
                 "total_debt": total_debt,
                 "reading_power_records": reading_power_records,
+                "review_metrics": review_metrics,
             }
 
 
@@ -2072,7 +2237,7 @@ def main():
     import argparse
     from .cli_output import print_success, print_error
 
-    parser = argparse.ArgumentParser(description="Index Manager CLI (v5.3)")
+    parser = argparse.ArgumentParser(description="Index Manager CLI (v5.4)")
     parser.add_argument("--project-root", type=str, help="项目根目录")
 
     subparsers = parser.add_subparsers(dest="command")
@@ -2107,7 +2272,7 @@ def main():
     process_parser.add_argument("--entities", required=True, help="JSON 格式的实体列表")
     process_parser.add_argument("--scenes", required=True, help="JSON 格式的场景列表")
 
-    # ==================== v5.1 新增命令 ====================
+    # ==================== v5.1 引入命令 ====================
 
     # 获取实体
     get_entity_parser = subparsers.add_parser("get-entity")
@@ -2183,7 +2348,16 @@ def main():
     list_invalid_parser = subparsers.add_parser("list-invalid")
     list_invalid_parser.add_argument("--status", choices=["pending", "confirmed"], default=None)
 
-    # ==================== v5.3 新增命令 ====================
+    review_save_parser = subparsers.add_parser("save-review-metrics")
+    review_save_parser.add_argument("--data", required=True, help="JSON 格式的审查指标数据")
+
+    review_recent_parser = subparsers.add_parser("get-recent-review-metrics")
+    review_recent_parser.add_argument("--limit", type=int, default=5)
+
+    review_trend_parser = subparsers.add_parser("get-review-trend-stats")
+    review_trend_parser.add_argument("--last-n", type=int, default=5)
+
+    # ==================== v5.3 引入命令 ====================
 
     # 获取债务汇总
     subparsers.add_parser("get-debt-summary")
@@ -2310,7 +2484,7 @@ def main():
         )
         emit_success(stats, message="chapter_processed", chapter=args.chapter)
 
-    # ==================== v5.1 新增命令处理 ====================
+    # ==================== v5.1 引入命令处理 ====================
 
     elif args.command == "get-entity":
         entity = manager.get_entity(args.id)
@@ -2434,7 +2608,33 @@ def main():
         rows = manager.list_invalid_facts(args.status)
         emit_success(rows, message="invalid_list")
 
-    # ==================== v5.3 新增命令处理 ====================
+    elif args.command == "save-review-metrics":
+        data = json.loads(args.data)
+        metrics = ReviewMetrics(
+            start_chapter=data["start_chapter"],
+            end_chapter=data["end_chapter"],
+            overall_score=data.get("overall_score", 0.0),
+            dimension_scores=data.get("dimension_scores", {}),
+            severity_counts=data.get("severity_counts", {}),
+            critical_issues=data.get("critical_issues", []),
+            report_file=data.get("report_file", ""),
+            notes=data.get("notes", ""),
+        )
+        manager.save_review_metrics(metrics)
+        emit_success(
+            {"start_chapter": metrics.start_chapter, "end_chapter": metrics.end_chapter},
+            message="review_metrics_saved",
+        )
+
+    elif args.command == "get-recent-review-metrics":
+        records = manager.get_recent_review_metrics(args.limit)
+        emit_success(records, message="recent_review_metrics")
+
+    elif args.command == "get-review-trend-stats":
+        stats = manager.get_review_trend_stats(args.last_n)
+        emit_success(stats, message="review_trend_stats")
+
+    # ==================== v5.3 引入命令处理 ====================
 
     elif args.command == "get-debt-summary":
         summary = manager.get_debt_summary()

+ 3 - 3
.claude/scripts/data_modules/migrate_state_to_sqlite.py

@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 """
-migrate_state_to_sqlite.py - 数据迁移脚本 (v5.1)
+migrate_state_to_sqlite.py - 数据迁移脚本 (v5.4)
 
 将 state.json 中的大数据迁移到 SQLite (index.db):
 - entities_v3 → entities 表
@@ -249,7 +249,7 @@ def migrate_state_to_sqlite(
             "review_checkpoints": state.get("review_checkpoints", [])[-10:],  # 只保留最近10个
             "disambiguation_warnings": state.get("disambiguation_warnings", [])[-20:],
             "disambiguation_pending": state.get("disambiguation_pending", [])[-10:],
-            # v5.1 标记
+            # v5.1 引入标记
             "_migrated_to_sqlite": True,
             "_migration_timestamp": datetime.now().isoformat()
         }
@@ -327,7 +327,7 @@ def main():
     from .cli_output import print_success, print_error
     from .index_manager import IndexManager
 
-    parser = argparse.ArgumentParser(description="迁移 state.json 到 SQLite (v5.1)")
+    parser = argparse.ArgumentParser(description="迁移 state.json 到 SQLite (v5.4)")
     parser.add_argument("--project-root", type=str, required=True, help="项目根目录")
     parser.add_argument("--dry-run", action="store_true", help="只分析不实际写入")
     parser.add_argument("--backup", action="store_true", default=True, help="迁移前备份")

+ 1 - 1
.claude/scripts/data_modules/snapshot_manager.py

@@ -13,7 +13,7 @@ from typing import Any, Dict, Optional
 
 from .config import get_config
 
-SNAPSHOT_VERSION = "1.0"
+SNAPSHOT_VERSION = "1.1"
 
 
 class SnapshotVersionMismatch(RuntimeError):

+ 5 - 5
.claude/scripts/data_modules/sql_state_manager.py

@@ -1,12 +1,12 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 """
-SQL State Manager - SQLite 状态管理模块 (v5.1)
+SQL State Manager - SQLite 状态管理模块 (v5.4)
 
 基于 IndexManager 扩展,提供与 StateManager 兼容的高级接口,
 将大数据(实体、别名、状态变化、关系)存储到 SQLite 而非 JSON。
 
-目标:
+目标(v5.1 引入,v5.4 沿用)
 - 替代 state.json 中的大数据字段
 - 保持与 Data Agent / Context Agent 的接口兼容
 - 支持增量写入和按需查询
@@ -43,7 +43,7 @@ class EntityData:
 
 class SQLStateManager:
     """
-    SQLite 状态管理器 (v5.1)
+    SQLite 状态管理器(v5.1 引入,v5.4 沿用)
 
     提供与 StateManager 兼容的接口,但数据存储在 SQLite (index.db) 中。
     用于替代 state.json 中膨胀的数据结构。
@@ -89,7 +89,7 @@ class SQLStateManager:
     ```
     """
 
-    # v5.0 支持的实体类型
+    # v5.0 引入的实体类型
     ENTITY_TYPES = ["角色", "地点", "物品", "势力", "招式"]
 
     def __init__(self, config=None):
@@ -473,7 +473,7 @@ def main():
     from .cli_output import print_success, print_error
     from .index_manager import IndexManager
 
-    parser = argparse.ArgumentParser(description="SQL State Manager CLI (v5.1)")
+    parser = argparse.ArgumentParser(description="SQL State Manager CLI (v5.4)")
     parser.add_argument("--project-root", type=str, help="项目根目录")
 
     subparsers = parser.add_subparsers(dest="command")

+ 37 - 38
.claude/scripts/data_modules/state_manager.py

@@ -1,14 +1,14 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 """
-State Manager - 状态管理模块 (v5.1)
+State Manager - 状态管理模块 (v5.4)
 
 管理 state.json 的读写操作:
 - 实体状态管理
 - 进度追踪
 - 关系记录
 
-v5.1 变更:
+v5.1 变更(v5.4 沿用):
 - 集成 SQLStateManager,同步写入 SQLite (index.db)
 - state.json 保留精简数据,大数据自动迁移到 SQLite
 """
@@ -79,9 +79,9 @@ class _EntityPatch:
 
 
 class StateManager:
-    """状态管理器 (v5.1 entities_v3 格式 + SQLite 同步)"""
+    """状态管理器(v5.1 entities_v3 格式 + SQLite 同步,v5.4 沿用)"""
 
-    # v5.0 支持的实体类型
+    # v5.0 引入的实体类型
     ENTITY_TYPES = ["角色", "地点", "物品", "势力", "招式"]
 
     def __init__(self, config=None, enable_sqlite_sync: bool = True):
@@ -97,7 +97,7 @@ class StateManager:
         # 与 security_utils.atomic_write_json 保持一致:state.json.lock
         self._lock_path = self.config.state_file.with_suffix(self.config.state_file.suffix + ".lock")
 
-        # v5.1: SQLite 同步
+        # v5.1 引入: SQLite 同步
         self._enable_sqlite_sync = enable_sqlite_sync
         self._sql_state_manager = None
         if enable_sqlite_sync:
@@ -118,7 +118,7 @@ class StateManager:
         self._pending_progress_words_delta: int = 0
         self._pending_chapter_meta: Dict[str, Any] = {}
 
-        # v5.1: 缓存待同步到 SQLite 的数据
+        # v5.1 引入: 缓存待同步到 SQLite 的数据
         self._pending_sqlite_data: Dict[str, Any] = {
             "entities_appeared": [],
             "entities_new": [],
@@ -168,7 +168,7 @@ class StateManager:
         )
 
         entities_v3 = state.get("entities_v3")
-        # v5.1: entities_v3, alias_index, state_changes, structured_relationships 已迁移到 index.db
+        # v5.1 引入: entities_v3, alias_index, state_changes, structured_relationships 已迁移到 index.db
         # 不再在 state.json 中初始化或维护这些字段
 
         if not isinstance(state.get("disambiguation_warnings"), list):
@@ -255,7 +255,7 @@ class StateManager:
 
                     progress["last_updated"] = self._now_progress_timestamp()
 
-                # v5.1: 强制使用 SQLite 模式,移除大数据字段
+                # v5.1 引入: 强制使用 SQLite 模式,移除大数据字段
                 # 确保 state.json 中不存在这些膨胀字段
                 for field in ["entities_v3", "alias_index", "state_changes", "structured_relationships"]:
                     disk_state.pop(field, None)
@@ -332,7 +332,7 @@ class StateManager:
                 # 原子写入(锁已持有,不再二次加锁)
                 atomic_write_json(self.config.state_file, disk_state, use_lock=False, backup=True)
 
-                # v5.1: 同步到 SQLite(必须在清空 pending 之前调用)
+                # v5.1 引入: 同步到 SQLite(必须在清空 pending 之前调用)
                 self._sync_to_sqlite()
 
                 # 同步内存为磁盘最新快照,并清空增量队列
@@ -351,7 +351,7 @@ class StateManager:
             raise RuntimeError("无法获取 state.json 文件锁,请稍后重试")
 
     def _sync_to_sqlite(self):
-        """v5.1: 同步待处理数据到 SQLite"""
+        """同步待处理数据到 SQLite(v5.1 引入,v5.4 沿用)"""
         if not self._sql_state_manager:
             return
 
@@ -380,17 +380,16 @@ class StateManager:
                     if eid:
                         processed_appearances.add((eid, chapter))
             except Exception:
-                pass  # SQLite 同步失败时静默降级,不影响主流程
+                pass  # SQLite 同步失败时静默降级(避免中断主流程)
 
-        # 方式2: 通过 add_entity/update_entity 等直接调用收集的数据
-        # 这些数据存在 _pending_entity_patches 等变量中
+        # 方式2: 使用 add_entity/update_entity 收集的增量数据。
+        # 数据存在 _pending_entity_patches 等变量中
         self._sync_pending_patches_to_sqlite(processed_appearances)
 
-        # 清空
         self._clear_pending_sqlite_data()
 
     def _sync_pending_patches_to_sqlite(self, processed_appearances: set = None):
-        """v5.1: 同步 _pending_entity_patches 等到 SQLite
+        """同步 _pending_entity_patches 等到 SQLite(v5.1 引入,v5.4 沿用)
 
         Args:
             processed_appearances: 已通过 process_chapter_entities 处理的 (entity_id, chapter) 集合,
@@ -574,8 +573,8 @@ class StateManager:
     # ==================== 实体管理 (v5.1 SQLite-first) ====================
 
     def get_entity(self, entity_id: str, entity_type: str = None) -> Optional[Dict]:
-        """获取实体 (v5.1: 优先从 SQLite 读取)"""
-        # v5.1: 优先从 SQLite 读取
+        """获取实体(v5.1 引入:优先从 SQLite 读取)"""
+        # v5.1 引入: 优先从 SQLite 读取
         if self._sql_state_manager:
             entity = self._sql_state_manager._index_manager.get_entity(entity_id)
             if entity:
@@ -594,7 +593,7 @@ class StateManager:
 
     def get_entity_type(self, entity_id: str) -> Optional[str]:
         """获取实体所属类型"""
-        # v5.1: 优先从 SQLite 读取
+        # v5.1 引入: 优先从 SQLite 读取
         if self._sql_state_manager:
             entity = self._sql_state_manager._index_manager.get_entity(entity_id)
             if entity:
@@ -608,7 +607,7 @@ class StateManager:
 
     def get_all_entities(self) -> Dict[str, Dict]:
         """获取所有实体(扁平化视图)"""
-        # v5.1: 优先从 SQLite 读取
+        # v5.1 引入: 优先从 SQLite 读取
         if self._sql_state_manager:
             result = {}
             for entity_type in self.ENTITY_TYPES:
@@ -629,7 +628,7 @@ class StateManager:
 
     def get_entities_by_type(self, entity_type: str) -> Dict[str, Dict]:
         """按类型获取实体"""
-        # v5.1: 优先从 SQLite 读取
+        # v5.1 引入: 优先从 SQLite 读取
         if self._sql_state_manager:
             entities = self._sql_state_manager._index_manager.get_entities_by_type(entity_type)
             if entities:
@@ -640,7 +639,7 @@ class StateManager:
 
     def get_entities_by_tier(self, tier: str) -> Dict[str, Dict]:
         """按层级获取实体"""
-        # v5.1: 优先从 SQLite 读取
+        # v5.1 引入: 优先从 SQLite 读取
         if self._sql_state_manager:
             result = {}
             for entity_type in self.ENTITY_TYPES:
@@ -661,7 +660,7 @@ class StateManager:
         return result
 
     def add_entity(self, entity: EntityState) -> bool:
-        """添加新实体 (v5.0 entities_v3 格式)"""
+        """添加新实体(v5.0 entities_v3 格式,v5.4 沿用)"""
         entity_type = entity.type
         if entity_type not in self.ENTITY_TYPES:
             entity_type = "角色"
@@ -696,7 +695,7 @@ class StateManager:
         patch.replace = True
         patch.base_entity = v3_entity
 
-        # v5.1: 注册别名到 index.db (通过 SQLStateManager)
+        # v5.1 引入: 注册别名到 index.db (通过 SQLStateManager)
         if self._sql_state_manager:
             self._sql_state_manager._index_manager.register_alias(entity.name, entity.id, entity_type)
             for alias in entity.aliases:
@@ -706,15 +705,15 @@ class StateManager:
         return True
 
     def _register_alias_internal(self, entity_id: str, entity_type: str, alias: str):
-        """内部方法:注册别名到 index.db (v5.1)"""
+        """内部方法:注册别名到 index.db(v5.1 引入)"""
         if not alias:
             return
-        # v5.1: 直接写入 SQLite
+        # v5.1 引入: 直接写入 SQLite
         if self._sql_state_manager:
             self._sql_state_manager._index_manager.register_alias(alias, entity_id, entity_type)
 
     def update_entity(self, entity_id: str, updates: Dict[str, Any], entity_type: str = None) -> bool:
-        """更新实体属性 (v5.0)"""
+        """更新实体属性(v5.0 引入,v5.4 沿用)"""
         # 查找实体
         if entity_type:
             if entity_id not in self._state.get("entities_v3", {}).get(entity_type, {}):
@@ -728,7 +727,7 @@ class StateManager:
 
         for key, value in updates.items():
             if key == "attributes" and isinstance(value, dict):
-                # v5.0: attributes 存在 current 字段
+                # v5.0 引入: attributes 存在 current 字段
                 if "current" not in entity:
                     entity["current"] = {}
                 entity["current"].update(value)
@@ -842,7 +841,7 @@ class StateManager:
             chapter=chapter
         )
 
-        # v5.0: 实体关系存入 structured_relationships,避免与 relationships(人物关系字典) 冲突
+        # v5.0 引入: 实体关系存入 structured_relationships,避免与 relationships(人物关系字典) 冲突
         if "structured_relationships" not in self._state:
             self._state["structured_relationships"] = []
         rel_dict = asdict(rel)
@@ -952,7 +951,7 @@ class StateManager:
 
     def process_chapter_result(self, chapter: int, result: Dict) -> List[str]:
         """
-        处理 Data Agent 的章节处理结果 (v5.1)
+        处理 Data Agent 的章节处理结果(v5.1 引入,v5.4 沿用)
 
         输入格式:
         - entities_appeared: 出场实体列表
@@ -964,7 +963,7 @@ class StateManager:
         """
         warnings = []
 
-        # v5.1: 记录章节号用于 SQLite 同步
+        # v5.1 引入: 记录章节号用于 SQLite 同步
         self._pending_sqlite_data["chapter"] = chapter
 
         # 处理出场实体
@@ -973,7 +972,7 @@ class StateManager:
             entity_type = entity.get("type")
             if entity_id:
                 self.update_entity_appearance(entity_id, chapter, entity_type)
-                # v5.1: 缓存用于 SQLite 同步
+                # v5.1 引入: 缓存用于 SQLite 同步
                 self._pending_sqlite_data["entities_appeared"].append(entity)
 
         # 处理新实体
@@ -991,7 +990,7 @@ class StateManager:
                 )
                 if not self.add_entity(new_entity):
                     warnings.append(f"实体已存在: {entity_id}")
-                # v5.1: 缓存用于 SQLite 同步
+                # v5.1 引入: 缓存用于 SQLite 同步
                 self._pending_sqlite_data["entities_new"].append(entity)
 
         # 处理状态变化
@@ -1004,7 +1003,7 @@ class StateManager:
                 reason=change.get("reason", ""),
                 chapter=chapter
             )
-            # v5.1: 缓存用于 SQLite 同步
+            # v5.1 引入: 缓存用于 SQLite 同步
             self._pending_sqlite_data["state_changes"].append(change)
 
         # 处理关系
@@ -1016,7 +1015,7 @@ class StateManager:
                 description=rel.get("description", ""),
                 chapter=chapter
             )
-            # v5.1: 缓存用于 SQLite 同步
+            # v5.1 引入: 缓存用于 SQLite 同步
             self._pending_sqlite_data["relationships_new"].append(rel)
 
         # 处理消歧不确定项(不影响实体写入,但必须对 Writer 可见)
@@ -1041,7 +1040,7 @@ class StateManager:
     # ==================== 导出 ====================
 
     def export_for_context(self) -> Dict:
-        """导出用于上下文的精简版状态 (v5.0)"""
+        """导出用于上下文的精简版状态(v5.0 引入,v5.4 沿用)"""
         # 从 entities_v3 构建精简视图
         entities_flat = {}
         for type_name, entities in self._state.get("entities_v3", {}).items():
@@ -1056,9 +1055,9 @@ class StateManager:
         return {
             "progress": self._state.get("progress", {}),
             "entities": entities_flat,
-            # v5.1: alias_index 已迁移到 index.db,这里返回空(兼容性)
+            # v5.1 引入: alias_index 已迁移到 index.db,这里返回空(兼容性)
             "alias_index": {},
-            "recent_changes": [],  # v5.1: 从 index.db 查询
+            "recent_changes": [],  # v5.1 引入: 从 index.db 查询
             "disambiguation": {
                 "warnings": self._state.get("disambiguation_warnings", [])[-self.config.export_disambiguation_slice:],
                 "pending": self._state.get("disambiguation_pending", [])[-self.config.export_disambiguation_slice:],
@@ -1168,7 +1167,7 @@ def main():
     from .schemas import validate_data_agent_output, format_validation_error, normalize_data_agent_output
     from .index_manager import IndexManager
 
-    parser = argparse.ArgumentParser(description="State Manager CLI (v5.2)")
+    parser = argparse.ArgumentParser(description="State Manager CLI (v5.4)")
     parser.add_argument("--project-root", type=str, help="项目根目录")
 
     subparsers = parser.add_subparsers(dest="command")

+ 77 - 0
.claude/scripts/data_modules/tests/test_data_modules.py

@@ -31,6 +31,7 @@ from data_modules.index_manager import (
     OverrideContractMeta,
     ChaseDebtMeta,
     ChapterReadingPowerMeta,
+    ReviewMetrics,
 )
 
 
@@ -847,6 +848,54 @@ class TestIndexManager:
         assert manager.fulfill_override(other_id) is True
         assert manager.get_chapter_overrides(4)[0]["status"] == "fulfilled"
 
+    def test_review_metrics_and_trends(self, temp_project):
+        manager = IndexManager(temp_project)
+
+        manager.save_review_metrics(
+            ReviewMetrics(
+                start_chapter=1,
+                end_chapter=1,
+                overall_score=48,
+                dimension_scores={
+                    "爽点密度": 8,
+                    "设定一致性": 7,
+                    "节奏控制": 7,
+                    "人物塑造": 8,
+                    "连贯性": 9,
+                    "追读力": 9,
+                },
+                severity_counts={"critical": 0, "high": 1, "medium": 2, "low": 0},
+                critical_issues=[],
+                report_file="审查报告/第1-1章审查报告.md",
+            )
+        )
+        manager.save_review_metrics(
+            ReviewMetrics(
+                start_chapter=2,
+                end_chapter=2,
+                overall_score=42,
+                dimension_scores={
+                    "爽点密度": 6,
+                    "设定一致性": 8,
+                    "节奏控制": 7,
+                    "人物塑造": 7,
+                    "连贯性": 7,
+                    "追读力": 7,
+                },
+                severity_counts={"critical": 1, "high": 0, "medium": 1, "low": 2},
+                critical_issues=["设定自相矛盾"],
+                report_file="审查报告/第2-2章审查报告.md",
+            )
+        )
+
+        recent = manager.get_recent_review_metrics(limit=2)
+        assert len(recent) == 2
+
+        trends = manager.get_review_trend_stats(last_n=5)
+        assert trends["count"] == 2
+        assert trends["overall_avg"] > 0
+        assert "爽点密度" in trends["dimension_avg"]
+
     def test_index_manager_cli(self, temp_project, monkeypatch, capsys):
         root = str(temp_project.project_root)
         manager = IndexManager(temp_project)
@@ -1152,6 +1201,34 @@ class TestIndexManager:
             ]
         )
 
+        review_payload = {
+            "start_chapter": 1,
+            "end_chapter": 1,
+            "overall_score": 50,
+            "dimension_scores": {
+                "爽点密度": 8,
+                "设定一致性": 7,
+                "节奏控制": 8,
+                "人物塑造": 8,
+                "连贯性": 9,
+                "追读力": 10,
+            },
+            "severity_counts": {"critical": 0, "high": 1, "medium": 2, "low": 0},
+            "critical_issues": [],
+            "report_file": "审查报告/第1-1章审查报告.md",
+        }
+        run_cli(
+            [
+                "--project-root",
+                root,
+                "save-review-metrics",
+                "--data",
+                json.dumps(review_payload, ensure_ascii=False),
+            ]
+        )
+        run_cli(["--project-root", root, "get-recent-review-metrics", "--limit", "5"])
+        run_cli(["--project-root", root, "get-review-trend-stats", "--last-n", "5"])
+
         capsys.readouterr()
 
 

+ 1 - 1
.claude/scripts/init_project.py

@@ -49,7 +49,7 @@ def _write_text_if_missing(path: Path, content: str) -> None:
 
 
 def _ensure_state_schema(state: Dict[str, Any]) -> Dict[str, Any]:
-    """确保 state.json 具备 v5.1 架构所需的字段集合。
+    """确保 state.json 具备 v5.1 架构所需的字段集合(v5.4 沿用)
 
     v5.1 变更:
     - entities_v3 和 alias_index 已迁移到 index.db,不再存储在 state.json

+ 5 - 5
.claude/scripts/status_reporter.py

@@ -133,7 +133,7 @@ class StatusReporter:
         self.state = None
         self.chapters_data = []
 
-        # v5.1: 使用 IndexManager 读取实体
+        # v5.1 引入: 使用 IndexManager 读取实体
         self._index_manager = IndexManager(self.config)
 
     def _extract_stats_field(self, content: str, field_name: str) -> str:
@@ -170,7 +170,7 @@ class StatusReporter:
         # 2) 正文/第1卷/第001章-标题.md
         chapter_files = sorted(self.chapters_dir.rglob("第*.md"))
 
-        # v5.1: 从 SQLite 获取已知角色名
+        # v5.1 引入: 从 SQLite 获取已知角色名
         known_character_names: List[str] = []
         protagonist_name = ""
         if self.state:
@@ -206,7 +206,7 @@ class StatusReporter:
             dominant_strand = (self._extract_stats_field(content, "主导Strand") or "").lower()
             cool_point_type = self._extract_stats_field(content, "爽点")
 
-            # v5.1: 角色提取从 SQLite chapters 表读取
+            # v5.1 引入: 角色提取从 SQLite chapters 表读取
             characters: List[str] = []
             try:
                 chapter_info = self._index_manager.get_chapter(chapter_num)
@@ -251,13 +251,13 @@ class StatusReporter:
             })
 
     def analyze_characters(self) -> Dict:
-        """分析角色活跃度 (v5.1 SQLite)"""
+        """分析角色活跃度(v5.1 引入,v5.4 沿用)"""
         if not self.state:
             return {}
 
         current_chapter = self.state.get("progress", {}).get("current_chapter", 0)
 
-        # v5.1: 从 SQLite 获取所有角色
+        # v5.1 引入: 从 SQLite 获取所有角色
         try:
             characters_list = self._index_manager.get_entities_by_type("角色")
         except Exception:

+ 1 - 1
.claude/scripts/update_state.py

@@ -74,7 +74,7 @@ class StateUpdater:
         self.state = None
 
     def _validate_schema(self, state: Dict) -> bool:
-        """验证 state.json 的基本结构 (v5.0)"""
+        """验证 state.json 的基本结构(v5.0 引入,v5.4 沿用)"""
         required_keys = [
             "project_info",
             "progress",

+ 2 - 2
.claude/scripts/workflow_manager.py

@@ -415,9 +415,9 @@ def save_state(state):
     atomic_write_json(state_file, state, use_lock=True, backup=False)
 
 def get_pending_steps(command):
-    """获取待执行步骤列表 (v5.2)"""
+    """获取待执行步骤列表(v5.2 引入,v5.4 沿用)"""
     if command == 'webnovel-write':
-        # v5.2 工作流:8 步(含 Step 1.5 & 2A/2B
+        # v5.2 引入的 8 步工作流(v5.4 沿用
         # Step 1: Context Agent 搜集上下文
         # Step 1.5: 章节设计(开头/钩子/爽点模式)
         # Step 2A: 生成粗稿

+ 2 - 1
.claude/skills/webnovel-init/references/system-data-flow.md

@@ -5,13 +5,14 @@ purpose: 重定向到权威版本
 
 <context>
 此文件已迁移到统一位置,避免多版本不同步问题。
+v5.4:版本号对齐,权威版本已更新。
 </context>
 
 <instructions>
 
 ## 权威版本位置
 
-`skills/webnovel-query/references/system-data-flow.md` (v5.1)
+`skills/webnovel-query/references/system-data-flow.md` (v5.4)
 
 ## 加载方式
 

+ 2 - 1
.claude/skills/webnovel-query/references/system-data-flow.md

@@ -1,11 +1,12 @@
 ---
 name: system-data-flow
 purpose: 项目初始化和状态查询时加载,理解数据结构
-version: "5.2"
+version: "5.4"
 ---
 
 <context>
 此文件用于项目数据结构参考。Claude 已知一般文件组织,这里只补充网文工作流特定的目录约定和脚本职责。
+v5.4:版本号对齐,内容沿用 v5.2。
 </context>
 
 <instructions>

+ 3 - 3
.claude/skills/webnovel-query/references/tag-specification.md

@@ -1,13 +1,13 @@
 ---
 name: tag-specification
-purpose: XML 标签格式参考(v5.1 可选使用)
-version: "5.1"
+purpose: XML 标签格式参考(v5.1 引入,v5.4 可选使用)
+version: "5.4"
 ---
 
 <context>
 此文件用于 XML 标签格式参考。
 
-**v5.1 重要变更**:
+**v5.1 引入的变更(v5.4 沿用)**:
 - 章节写作时**不再要求**添加 XML 标签
 - Data Agent 会自动从纯正文中提取实体,写入 index.db
 - 标签仅用于**手动标注**场景(如明确标记重要实体、补充提取遗漏)

+ 2 - 1
.claude/skills/webnovel-resume/references/system-data-flow.md

@@ -5,13 +5,14 @@ purpose: 重定向到权威版本
 
 <context>
 此文件已迁移到统一位置,避免多版本不同步问题。
+v5.4:版本号对齐,权威版本已更新。
 </context>
 
 <instructions>
 
 ## 权威版本位置
 
-`skills/webnovel-query/references/system-data-flow.md` (v5.1)
+`skills/webnovel-query/references/system-data-flow.md` (v5.4)
 
 ## 加载方式
 

+ 3 - 2
.claude/skills/webnovel-resume/references/workflow-resume.md

@@ -1,16 +1,17 @@
 ---
 name: workflow-resume
 purpose: 任务恢复时加载,指导中断恢复流程
-version: "5.2"
+version: "5.4"
 ---
 
 <context>
 此文件用于中断任务恢复。Claude 已知错误处理流程,这里只补充网文创作工作流特定的 Step 难度分级和恢复策略。
+v5.4:版本号对齐,内容沿用 v5.2。
 </context>
 
 <instructions>
 
-## Step 中断难度分级 (v5.2)
+## Step 中断难度分级 (v5.4)
 
 | Step | 名称 | 影响 | 难度 | 默认策略 |
 |------|------|------|------|----------|

+ 30 - 0
.claude/skills/webnovel-review/SKILL.md

@@ -87,6 +87,8 @@ cat .webnovel/state.json
 
 **注意**:Claude 会自动根据描述匹配并调用对应的子代理
 
+**汇总要求**:统计 critical / high / medium / low 的问题数量(用于趋势记录)
+
 ## Step 8: 生成审查报告
 
 保存到: `审查报告/第{start}-{end}章审查报告.md`
@@ -129,6 +131,34 @@ cat .webnovel/state.json
 - 5-6: 及格
 - <5: 不及格(高流失风险)
 
+**审查指标 JSON(必须输出,用于趋势统计)**:
+```json
+{
+  "start_chapter": {start},
+  "end_chapter": {end},
+  "overall_score": 48,
+  "dimension_scores": {
+    "爽点密度": 8,
+    "设定一致性": 7,
+    "节奏控制": 7,
+    "人物塑造": 8,
+    "连贯性": 9,
+    "追读力": 9
+  },
+  "severity_counts": {"critical": 1, "high": 2, "medium": 3, "low": 1},
+  "critical_issues": ["设定自相矛盾"],
+  "report_file": "审查报告/第{start}-{end}章审查报告.md",
+  "notes": ""
+}
+```
+
+**保存审查指标**:
+```bash
+python -m data_modules.index_manager save-review-metrics \
+  --data '{...}' \
+  --project-root "."
+```
+
 ## Step 9: 处理关键问题
 
 如发现 🔴 问题,询问用户:

+ 4 - 3
.claude/skills/webnovel-review/references/core-constraints.md

@@ -1,11 +1,12 @@
 ---
 name: core-constraints
 purpose: 每次章节写作前加载,确保三大定律执行
-version: "5.2"
+version: "5.4"
 ---
 
 <context>
 此文件用于章节创作时的核心约束检查。Claude 已知一般写作规范,这里只补充网文特定的防幻觉协议。
+v5.4:版本号对齐,内容沿用 v5.2。
 </context>
 
 <instructions>
@@ -18,9 +19,9 @@ version: "5.2"
 | **设定即物理** | 实力/招式/物品 ≤ index.db 记录 | 写作前查询确认 |
 | **发明需识别** | 新实体由 Data Agent 自动提取 | 章节完成后处理 |
 
-## 新实体处理流程(v5.2)
+## 新实体处理流程(v5.2 引入,v5.4 沿用
 
-v5.2 不再要求在正文中写 XML 标签:
+v5.2 引入的规则,v5.4 沿用:正文不再要求 XML 标签:
 1. **写作时**: 直接写纯正文,新角色/地点/物品正常描写
 2. **完成后**: Data Agent 自动识别新实体并写入 index.db
 3. **不确定实体**: Data Agent 标记为 uncertain,由人工确认

+ 33 - 5
.claude/skills/webnovel-write/SKILL.md

@@ -1,6 +1,6 @@
 ---
 name: webnovel-write
-description: Writes webnovel chapters (3000-5000 words) using v5.2 architecture. Context Agent outputs creative brief, writer produces pure text, review agents report issues, webnovel polish fixes problems, Data Agent extracts entities and records hooks/patterns.
+description: Writes webnovel chapters (3000-5000 words) using v5.4 architecture. Context Agent outputs creative brief, writer produces pure text, review agents report issues, webnovel polish fixes problems, Data Agent extracts entities and records hooks/patterns.
 allowed-tools: Read Write Edit Grep Bash Task
 ---
 
@@ -11,7 +11,7 @@ allowed-tools: Read Write Edit Grep Bash Task
 ⚠️ **强制要求**: 开始写作前,**必须复制以下清单**到回复中并逐项勾选。跳过任何步骤视为工作流不完整。
 
 ```
-章节创作进度 (v5.2):
+章节创作进度 (v5.4):
 - [ ] Step 1: Context Agent 搜集上下文(创作任务书)
 - [ ] Step 1.5: 章节设计(开头/钩子/爽点模式)
 - [ ] Step 2A: 生成粗稿(剧情正确、场面成立)
@@ -60,7 +60,7 @@ allowed-tools: Read Write Edit Grep Bash Task
 **Agent 自动完成**:
 1. 读取本章大纲,分析需要什么信息
 2. 读取 state.json 获取主角状态快照
-3. 查询 index.db (v5.1 schema) 召回实体/别名/关系
+3. 查询 index.db (v5.1+ schema) 召回实体/别名/关系
 4. 调用 data_modules.rag_adapter 语义检索
 5. Grep 设定集搜索相关设定
 6. 评估伏笔紧急度
@@ -83,7 +83,7 @@ allowed-tools: Read Write Edit Grep Bash Task
 
 ---
 
-## Step 1.5: 章节设计(v5.3 增强
+## Step 1.5: 章节设计(v5.3 引入,v5.4 沿用
 
 **目标**: 在写作前明确本章结构与变体,避免模式重复,设计追读力策略。
 
@@ -114,7 +114,7 @@ cat "${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md"
 - 信息密度(low/medium/high)
 - 是否过渡章(true/false)
 
-### 1.5.3 追读力设计块(v5.3 新增
+### 1.5.3 追读力设计块(v5.3 引入
 
 **必须输出以下设计**:
 
@@ -280,6 +280,34 @@ cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/style-adapter.md"
 └─────────────────────────────────────────────────┘
 ```
 
+**审查指标 JSON(必须输出,用于趋势统计)**:
+```json
+{
+  "start_chapter": {chapter_num},
+  "end_chapter": {chapter_num},
+  "overall_score": 48,
+  "dimension_scores": {
+    "爽点密度": 8,
+    "设定一致性": 7,
+    "节奏控制": 7,
+    "人物塑造": 8,
+    "连贯性": 9,
+    "追读力": 9
+  },
+  "severity_counts": {"critical": 1, "high": 2, "medium": 3, "low": 1},
+  "critical_issues": ["设定自相矛盾"],
+  "report_file": "",
+  "notes": ""
+}
+```
+
+**保存审查指标**:
+```bash
+python -m data_modules.index_manager save-review-metrics \
+  --data '{...}' \
+  --project-root "."
+```
+
 **Only proceed to Step 4 when:**
 1. 已收到全部审查报告(或 minimal 模式仅 3 份)
 2. 已输出汇总表格

+ 3 - 2
.claude/skills/webnovel-write/references/core-constraints.md

@@ -1,11 +1,12 @@
 ---
 name: core-constraints
 purpose: 每次章节写作前加载,确保三大定律执行
-version: "5.2"
+version: "5.4"
 ---
 
 <context>
 此文件用于章节创作时的核心约束检查。Claude 已知一般写作规范,这里只补充网文特定的防幻觉协议。
+v5.4:版本号对齐,内容沿用 v5.2。
 </context>
 
 <instructions>
@@ -20,7 +21,7 @@ version: "5.2"
 
 ## 新实体处理流程
 
-v5.2 不再要求在正文中写 XML 标签。新实体由 Data Agent 在章节完成后自动提取:
+v5.2 引入的规则,v5.4 沿用:正文不再要求 XML 标签。新实体由 Data Agent 在章节完成后自动提取:
 
 1. **写作时**: 直接写纯正文,新角色/地点/物品正常描写
 2. **完成后**: Data Agent 自动识别新实体并写入 index.db

+ 3 - 3
.claude/skills/webnovel-write/references/polish-guide.md

@@ -1,11 +1,11 @@
 ---
 name: polish-guide
 purpose: 章节生成后的润色阶段加载,基于审查报告修复问题 + 强化网文口感
-version: "5.2"
+version: "5.4"
 ---
 
 <context>
-此文件用于内容润色,v5.2 强调网文口感与追读力
+此文件用于内容润色,v5.2 引入“网文口感 + 追读力”重点,v5.4 沿用
 
 润色步骤接收两个输入:
 1. 章节正文
@@ -14,7 +14,7 @@ version: "5.2"
 
 <instructions>
 
-## v5.2:基于审查报告修复
+## v5.2 引入(v5.4 沿用):基于审查报告修复
 
 ### 输入格式
 

+ 1 - 1
.claude/templates/golden-finger-templates.md

@@ -376,7 +376,7 @@
 <entity type="未来事件" name="事件名称" desc="事件描述" tier="层级" time="发生时间" strategy="应对策略"/>
 ```
 
-> **v5.0 注意**: 这些标签为**可选**。Data Agent 可从纯正文自动提取实体,标签仅用于手动标注场景。
+> **v5.0 引入(v5.4 沿用)**: 这些标签为**可选**。Data Agent 可从纯正文自动提取实体,标签仅用于手动标注场景。
 
 ---
 

+ 3 - 1
.claude/templates/output/index-schema.md

@@ -1,6 +1,8 @@
-# index.db 表结构说明 (v5.1/v5.2)
+# index.db 表结构说明 (v5.4,基于 v5.1/v5.2)
 
 > 以 SQLite 存储大规模数据(实体/别名/场景/关系)。
+>
+> **v5.4**:结构沿用 v5.1/v5.2,并在脚本侧扩展新表。
 
 ## 表一览
 

+ 3 - 1
.claude/templates/output/state-schema.md

@@ -1,6 +1,8 @@
-# state.json 结构说明 (v5.2)
+# state.json 结构说明 (v5.4)
 
 > 该文件为运行态精简状态,避免体量膨胀。实体等大数据存于 index.db。
+>
+> **v5.4**:结构沿用 v5.2。
 
 ```json
 {

+ 19 - 6
README.md

@@ -35,7 +35,7 @@
 | **设定即物理** | 遵守设定,不自相矛盾 | Consistency Checker 实时校验 |
 | **发明需识别** | 新实体必须入库管理 | Data Agent 自动提取并消歧 |
 
-### 追读力机制 (v5.3 新增)
+### 追读力机制 (v5.3 引入)
 
 **约束分层系统**:
 
@@ -322,8 +322,8 @@ Step 6: Git 自动提交备份
 6. **伏笔管理**(必须处理、可选提及)
 7. **连贯性检查点**(时间、位置、情绪)
 8. **章末钩子设置**(建议类型、禁止事项)
-9. **追读力设计**(v5.3 新增:钩子策略、微兑现规划、模式差异化)
-10. **债务与Override状态**(v5.3 新增:当前债务、待偿还Override)
+9. **追读力设计**(v5.3 引入:钩子策略、微兑现规划、模式差异化)
+10. **债务与Override状态**(v5.3 引入:当前债务、待偿还Override)
 
 ---
 
@@ -534,8 +534,8 @@ your-novel-project/
 │   │   │   ├── context_manager.py  # Token预算管理(v5.4新增)
 │   │   │   ├── api_client.py       # API 客户端
 │   │   │   └── config.py           # 配置管理
-│   │   ├── context_pack_builder.py # 上下文包构建器
-│   │   └── ...
+│   │   ├── ...
+│   │   └── 注:旧的 context_pack_builder.py 已废弃,统一使用 context_manager.py
 │   ├── templates/              # 题材模板
 │   │   └── genres/
 │   │       ├── 修仙.md
@@ -592,7 +592,7 @@ done
 python -m data_modules.index_manager stats --project-root "."
 ```
 
-### 追读力数据查询 (v5.3)
+### 追读力数据查询 (v5.3 引入)
 
 ```bash
 # 查看债务汇总
@@ -614,6 +614,16 @@ python -m data_modules.index_manager get-pending-overrides --project-root "."
 python -m data_modules.index_manager accrue-interest --current-chapter 100 --project-root "."
 ```
 
+### 审查趋势查询 (v5.4 引入)
+
+```bash
+# 查看最近审查记录
+python -m data_modules.index_manager get-recent-review-metrics --limit 5 --project-root "."
+
+# 查看审查趋势统计(均值/短板分析)
+python -m data_modules.index_manager get-review-trend-stats --last-n 5 --project-root "."
+```
+
 ### 向量重建
 
 当 `vectors.db` 损坏或嵌入模型更换时:
@@ -643,6 +653,9 @@ git checkout ch0045
 ## 版本历史
 
 ### v5.4 (当前)
+- **审查指标追踪**:review_metrics 表记录每次审查的评分/维度/问题数
+- **审查趋势统计**:get-review-trend-stats 查询近期审查均值和短板
+- **故事骨架采样**:context_manager 每 N 章采样历史摘要,构建长篇感知
 - **上下文工程升级**:基于 Context Engineering Guide 优化
 - **invalid_facts 表**:追踪无效事实,支持 pending/confirmed 状态
 - **父子向量索引**:parent_chunk_id 支持摘要-场景层级检索