Quellcode durchsuchen

novel-write skill: multi-genre evolution + event sourcing + git sharing

题材级共享进化 — 同题材所有项目共用 evolution/genres/{genre_id}/
事件溯源引擎 — 多用户 git 协作零冲突
多维评分 (P1-P5) — 替代单一 composite score
风格注入算子 — 对标/风格报告 → 题材池
早停+灭绝机制 — 防过拟合
InkOS 15题材配置自动种子提取

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nonvioce vor 5 Monaten
Commit
afaed67218

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+__pycache__/
+*.pyc
+.DS_Store
+evolution/session_state.json

+ 303 - 0
CLAUDE.md

@@ -0,0 +1,303 @@
+# novel-write — Claude Code 小说写作技能
+
+长中短篇小说写作技能,覆盖题材诊断→设定→大纲→写作→质检全流程。
+内置自优化引擎,同题材写作越写越好。
+
+远程仓库: `ssh://git@数学.top:10022/nonvioce/novel-skill.git`
+
+## 安装
+
+```bash
+# 克隆仓库
+git clone ssh://git@数学.top:10022/nonvioce/novel-skill.git ~/.claude/skills/novel-write
+
+# 或:如果已有本地项目,直接软链接
+ln -s /path/to/novel-skill ~/.claude/skills/novel-write
+
+# 确保 InkOS 题材配置文件可读(种子提取依赖)
+# 预期路径: ~/Downloads/inkos-master/packages/core/genres/*.md
+# 如不可用,系统回退到内置通用种子
+```
+
+安装后,在 Claude Code 对话中输入"写小说""开新书""续写第X章"等自动触发技能。
+
+## 快速开始
+
+### 创建新项目
+
+按以下结构创建小说项目目录:
+
+```
+{书名}/
+├── 项目铁律.md          # 必须。POV、时态、禁用词、世界观硬限制。frontmatter 写 genre 字段
+├── 设定/
+│   ├── 世界观.md
+│   ├── 角色/{角色名}.md
+│   ├── 势力.md
+│   ├── 关系图.md
+│   └── 题材定位.md
+├── 大纲/
+│   ├── 全書大纲.md
+│   ├── 卷纲_第N卷.md
+│   └── 细纲_第NNN章.md
+├── 正文/
+│   └── 第NNN章_章名.md
+├── 对标/
+│   └── {书名}/风格报告.md    # 可选。用于风格注入进化
+└── 追踪/
+    ├── 钩子账本.md
+    ├── 情感矩阵.md
+    ├── 时间线.md
+    └── 上下文.md
+```
+
+`项目铁律.md` 示例 frontmatter:
+
+```yaml
+---
+title: "重生囤货:绑定诡异种田"
+genre: "末世"           # ← 题材识别关键字段
+sub_genre: "种田+诡异+重生"
+pov: "第三人称有限"
+tense: "过去时"
+target_words: 3000
+---
+```
+
+### 写作对话中
+
+在 Claude Code 中直接说"写第33章",技能自动:
+1. 读取项目铁律、大纲、细纲、钩子账本、情感矩阵
+2. 从 `evolution/genres/{genre_id}/` 加载该题材最优提示词
+3. 按六条铁律写作
+4. 写完后执行 `evolve.sh` 自动进化
+
+### 手动进化命令
+
+```bash
+# 写完一章后(自动执行,也可手动)
+cd ~/.claude/skills/novel-write
+bash scripts/evolve.sh --project /path/to/novel
+
+# 完整进化(每卷或每10章)
+bash scripts/evolve.sh --project /path/to/novel --full
+
+# 查看进化状态(含题材级数据)
+bash scripts/evolve.sh --project /path/to/novel --report
+
+# 从事件日志重建进化数据(git pull 后)
+bash scripts/evolve.sh --project /path/to/novel --compile
+```
+
+## 进化系统
+
+### 架构
+
+题材级共享 — 同题材所有项目共用进化数据。写第二本末世时,第一本的优化直接可用。
+
+```
+evolution/
+├── thresholds.json                 # 技能级默认阈值
+├── prompts/population.json         # 技能级种群(无题材时回退用)
+└── genres/                         # 题材级共享目录
+    └── {genre_id}/                 # other, xianxia, urban, horror ...
+        ├── thresholds.json         # 题材特定质量阈值
+        ├── prompts/
+        │   ├── population.json     # 5岛种群(writing/revision/quality_check/outline/hook_management)
+        │   └── current_best.json   # 各岛最优提示词(写作时读取)
+        ├── patterns/
+        │   └── pattern_library.json
+        └── events/                 # 事件日志(多用户 git 共享核心)
+            └── {timestamp}_{user}.json
+```
+
+### 五大岛屿
+
+| 岛屿 | 作用 | 写作阶段 |
+|------|------|---------|
+| writing | 写作规则(密度/去AI味/人物/心理) | 写前注入 |
+| revision | 修稿规则(润色边界/字数治理) | 修改时注入 |
+| quality_check | 质检标准(37维审查/五遍评估) | 质检时参照 |
+| outline | 大纲规则(章节类型轮换/世界观一致性) | 大纲阶段参照 |
+| hook_management | 伏笔管理(生命周期/准入控制/健康分析) | 全程参照 |
+
+### 遗传算子
+
+- **交叉** — 两父本按句切分拼接
+- **突变** — 同义词替换 + 句序重排 + 前缀注入
+- **简化** — 保留首尾句,去中间
+- **注入** — 从对标/风格报告提取句式/对话/节奏特征,编码为提示词变体
+
+### 自适应机制
+
+| 机制 | 触发条件 | 行为 |
+|------|---------|------|
+| 早停 | 单岛连续2代提升<3% | 跳过该岛变异 |
+| 灭绝重播 | 连续3代提升<5% | 删底30%,重播:题材种子50%+风格注入30%+交叉20% |
+| 阈值调优 | 通过率>90% | 提高 pass 线 (+3) |
+| 阈值调优 | 通过率<30% | 降低 pass 线 (-3) |
+| 权重调优 | 某P维度持续最低 | 该维度增权 |
+| 岛屿迁移 | 每2代 | 各岛最优2个体注入相邻岛(分×0.9)|
+
+### 多维评分
+
+不再用单一 composite score。每个提示词变体按 P1-P5 独立追踪:
+
+```
+变体 "abc123" → {P1: 88 (trials=3), P2: 72 (trials=3), P3: 65 (trials=3), P4: 80 (trials=3), P5: 90 (trials=3)}
+综合分 = 88×0.35 + 72×0.25 + 65×0.15 + 80×0.15 + 90×0.10 = 80.6
+```
+
+新反馈用滑动平均合并:`new = (old × trials + score) / (trials + 1)`
+
+## 多用户 Git 共享
+
+### 原理:事件溯源
+
+每个用户不修改 `population.json`,而是写入自己的不可变事件文件。不同文件名 → git merge 永远无冲突。
+
+### 工作流
+
+```bash
+# === 用户 A:写章节 + 进化 ===
+cd ~/.claude/skills/novel-write
+git pull origin main
+
+# 收集章节反馈(Claude Code 写完后自动调用)
+python3 scripts/feedback_collector.py collect \
+    --project /path/to/novel \
+    --chapter 35 \
+    --scores '{"P1":88,"P2":76,"P3":70,"P4":82,"P5":91}'
+
+# 执行进化
+bash scripts/evolve.sh --project /path/to/novel --full
+
+# 提交事件文件并推送
+git add evolution/genres/other/events/
+git commit -m "evolve(other): ch35 feedback + gen3 mutation"
+git push origin main
+
+# === 用户 B:获取 A 的成果 ===
+cd ~/.claude/skills/novel-write
+git pull origin main
+bash scripts/evolve.sh --project /path/to/novel --compile
+# 现在 population.json 已包含用户 A 的所有优化
+# 继续写第36章...
+```
+
+### 初始推送
+
+```bash
+cd ~/.claude/skills/novel-write
+git add .
+git commit -m "novel-write skill: multi-genre evolution + event sourcing"
+git push -u origin master
+```
+
+### 冲突场景覆盖
+
+| 场景 | 冲突? | 原因 |
+|------|--------|------|
+| 两人同题材写不同书 | 无 | 各自写 `events/{ts}_{user}.json`,不同文件名 |
+| 两人同时给同一题材进化 | 无 | 进化事件记录不同变体 ID,合并后种群更丰富 |
+| 两人给同一变体评分 | 无 | 反馈事件各自独立,编译器回放时滑动平均合并 |
+| `population.json` 被两人修改 | 无(已解决) | 不再直接修改;由 `--compile` 重建,可加 `.gitignore` |
+| `thresholds.json` 同时调整 | 可能 | 调整频率低;冲突时取最近时间戳或手动合并 |
+
+## 题材管理
+
+### 支持的中文题材映射
+
+| 中文题材 | InkOS ID | 说明 |
+|---------|----------|------|
+| 仙侠、修仙 | xianxia | 修炼体系/天劫/因果/气运 |
+| 玄幻 | xuanhuan | 战力体系/异界 |
+| 都市、现代、都市异能 | urban | 年代考据/商业规则 |
+| 恐怖、惊悚、灵异 | horror | 恐怖层级/压抑控制 |
+| 科幻 | sci-fi | 科技合理性 |
+| 末世、废土 | other | 回退到通用配置 |
+| 无限流 | other | 回退到通用配置 |
+| 种田、日常 | other | 回退到通用配置 |
+| 游戏、网游 | litrpg | 数值系统/面板 |
+| 言情、古言、现言 | romantasy | 关系发展/感情节奏 |
+| 奇幻、西幻 | progression | 魔法体系/世界观 |
+
+### 添加新题材
+
+```bash
+# 方法一:新建 InkOS 题材配置文件
+# 在 ~/Downloads/inkos-master/packages/core/genres/ 下创建 {新题材}.md
+# 然后运行种子提取
+python3 scripts/seed_extractor.py --genre {新题材} --show
+
+# 方法二:直接初始化题材目录
+python3 scripts/seed_extractor.py --genre {新题材}
+```
+
+### 查看某题材进化状态
+
+```bash
+python3 scripts/prompt_evolver.py show --genre xianxia
+python3 scripts/event_store.py list --genre xianxia
+```
+
+## 全部 CLI 命令
+
+```bash
+# === 进化编排 ===
+bash scripts/evolve.sh                                    # 快速进化(自动检测项目)
+bash scripts/evolve.sh --project /path/to/novel           # 指定项目
+bash scripts/evolve.sh --project /path/to/novel --full    # 完整进化
+bash scripts/evolve.sh --project /path/to/novel --report  # 状态报告
+bash scripts/evolve.sh --project /path/to/novel --compile # 从事件重建
+
+# === 提示词进化 ===
+python3 scripts/prompt_evolver.py evolve                      # 技能级进化
+python3 scripts/prompt_evolver.py evolve --genre xianxia -g 5 # 题材级进化
+python3 scripts/prompt_evolver.py evolve --project /path/to/novel  # 自动检测题材
+python3 scripts/prompt_evolver.py show                        # 查看技能级最优
+python3 scripts/prompt_evolver.py show --genre xianxia        # 查看某题材最优
+
+# === 质量阈值 ===
+python3 scripts/quality_tuner.py show                             # 当前阈值
+python3 scripts/quality_tuner.py analyze --project /path/to/novel # 分析
+python3 scripts/quality_tuner.py tune --project /path/to/novel    # 分析并应用
+
+# === 反馈收集 ===
+python3 scripts/feedback_collector.py collect \
+    --project /path/to/novel --chapter 35 \
+    --scores '{"P1":88,"P2":76,"P3":70,"P4":82,"P5":91}'
+python3 scripts/feedback_collector.py state   # 查看会话状态
+
+# === 事件溯源 ===
+python3 scripts/event_store.py compile --genre other    # 编译 population
+python3 scripts/event_store.py list --genre other       # 列出事件摘要
+
+# === 种子提取 ===
+python3 scripts/seed_extractor.py --genre xianxia --show   # 预览某题材种子
+python3 scripts/seed_extractor.py --genre urban             # 初始化某题材进化目录
+
+# === 模式挖掘 ===
+python3 scripts/pattern_mine.py show --top 10
+python3 scripts/pattern_mine.py mine --project /path/to/novel
+```
+
+## 关键文件参考
+
+| 文件 | 用途 |
+|------|------|
+| `SKILL.md` | 技能主文件,Claude Code 加载此文件获取写作指令 |
+| `CLAUDE.md` | 本文件,项目进度和使用说明 |
+| `scripts/evolve.sh` | 进化编排器入口,自动检测项目/题材 |
+| `scripts/prompt_evolver.py` | 岛屿模型 + 遗传算法 + 早停 + 灭绝 |
+| `scripts/seed_extractor.py` | InkOS 题材配置 → 5岛种子提取 |
+| `scripts/event_store.py` | 事件溯源引擎(编译/写入/列出事件) |
+| `scripts/feedback_collector.py` | 多维反馈收集 + 事件写入 |
+| `scripts/quality_tuner.py` | 质量阈值自适应调优 |
+| `scripts/pattern_mine.py` | MAP-Elites 写作模式挖掘 |
+| `scripts/utils.py` | 共享工具(路径解析/题材映射/持久化)|
+
+## 参考来源
+
+- [InkOS](https://github.com/Narcooo/inkos) — 题材配置、37维质检、伏笔全生命周期
+- [OpenEvolve](https://github.com/codelion/openevolve) — MAP-Elites、岛屿模型、双选择

+ 1232 - 0
SKILL.md

@@ -0,0 +1,1232 @@
+---
+name: novel-write
+description: |
+  长中短篇小说写作技能。覆盖题材诊断→设定→大纲→写作→质检全流程。
+  核心机制:项目铁律 + 事件钩子 + 情感矩阵 → 保证前后连贯。
+  触发:写小说、开新书、续写、写第X章、修改、日更、大纲、设定、质检、拆文。
+---
+
+# 小说写作技能
+
+## 核心铁律(六条不可变规则)
+
+**铁律一:展示而非讲述。**
+用动作和对话表现,不要直接陈述。不是"她很悲伤",而是"她把杯子放回桌上,瓷器发出一声脆响"。不是"他是个危险的人",而是"他没有提高音量,只是咔嗒一声合上打火机,房间安静了下来。"
+
+**铁律二:冲突驱动剧情。**
+每章必须有冲突或转折。Scene 三要素缺一不可:目标 → 阻碍 → 变化。没有变化的场景默认删除、合并或压缩。
+
+**铁律三:悬念承上启下。**
+每章结尾必须留下钩子。章末的目的不是"干净收束",而是"留下后劲"。禁止以总结句、说教句、已消散的情绪、解释性文字结尾。
+
+**铁律四:爽点密度决定留存。**
+每 3000-5000 字必须有一个爽点/情绪释放点。爽点需要铺垫——铺垫越长,释放时爽感越强。数据验证:追更率 > 10%、前 10 章完读率 > 8% 才能进推荐池。先写 30 章再回头审视。
+
+**铁律五:在类型框架内制造新鲜感。**
+不否定类型化——而是在类型框架中注入信息增量。开篇用熟悉感留住读者,关键节点用陌生走向制造惊喜。拒绝模板复制:每个角色的行为应该有只属于 ta 的动机,而非"这个类型就该这样写"。
+
+**铁律六:每次写作任务后自动进化。**
+每次完成续写/修改/质检/批量生成后,必须执行 `scripts/evolve.sh`(自动检测项目目录)。进化结果写入 `evolution/` 文件夹,不修改本 SKILL.md。本文件从 `evolution/` 读取最优提示词和阈值。连续写作 3 章以上无进化 → 警告提示。
+
+---
+
+## 项目文件结构
+
+```
+{书名}/
+├── 项目铁律.md          # POV、时态、禁用词、世界观硬限制、质量门槛
+├── 设定/
+│   ├── 世界观.md         # 时代背景、力量体系、社会结构
+│   ├── 角色/
+│   │   └── {角色名}.md   # 一人一文件
+│   ├── 势力.md
+│   ├── 关系图.md
+│   └── 题材定位.md       # 类型、卖点、对标作品、差异化
+├── 大纲/
+│   ├── 全書大纲.md       # 卷级鸟瞰
+│   ├── 卷纲_第N卷.md     # 爽点节奏、情绪弧线、伏笔清单
+│   └── 细纲_第NNN章.md   # 每章:核心事件、钩子、爽点
+├── 正文/
+│   └── 第NNN章_章名.md
+├── 对标/
+│   └── {书名}/
+│       └── 风格报告.md      # 参考文本的风格分析报告
+└── 追踪/
+    ├── 钩子账本.md       # 伏笔全生命周期管理
+    ├── 情感矩阵.md       # 角色×章节 情感网格
+    ├── 时间线.md         # 事件按时间顺序
+    └── 上下文.md         # 续写快照
+```
+
+---
+
+## 三根支柱
+
+### 支柱一:项目铁律
+
+`项目铁律.md` 是写作的宪法。每章写完后对照检查。
+
+```markdown
+# 项目铁律
+
+## 不可变规则
+- POV:{第一人称|第三人称有限|全知},{是否允许切换}
+- 时态:{过去时|现在时}
+- 风格:描写与对话比例、节奏偏好
+
+## 世界观硬限制
+- {力量体系上限/下限/代价}
+- {世界规则——什么能做,什么绝对不能做}
+
+## 禁用词
+- Tier 1(AI 味浓,见之必改):凝视着、仿佛、不可思议、不知为何、空气似乎凝固了、心中五味杂陈
+- Tier 2(警惕):充满、显得、极其、无比
+
+## 质量门槛
+- 每章必须有章末钩子
+- 每章推进至少一个伏笔或一条人物弧线
+- 对话推进剧情或揭示角色,不凑字数
+- 开头 500 字必须有抓手
+```
+
+### 支柱二:事件钩子账本
+
+`追踪/钩子账本.md` 管理伏笔从种植到兑现的全生命周期。
+
+**钩子类型:** SUSPENSE(悬念)、PROMISE(承诺)、MYSTERY(谜团)、CONFLICT(冲突种子)、CHEKHOV(契诃夫之枪)
+
+**操作语义:**
+- **plant** — 种植新钩子,分配唯一 ID,标记优先级
+- **advance** — 推进已有钩子(虽未兑现但给出新线索),在条目下追加进展记录
+- **payoff** — 兑现钩子,从活跃移到已兑现表,记录兑现方式
+- **defer** — 推迟兑现,更新优先级并注明原因
+- **stale** — 标记过期(超 10 章无推进),决定修复或废弃
+
+**章节前检查:** 读钩子账本 → 列出高优先级活跃钩子 → 确认本章是否有机会推进/兑现 → 设计章末钩子时避免与已有钩子冲突
+
+**章节后更新:** 兑现的移走 → 推进的追加记录 → 新种的写入 → 检查 stale
+
+**钩子健康分析(每章写完后自动执行):**
+
+| 指标 | 阈值 | 处理 |
+|------|------|------|
+| stale 钩子 | 种植超 10 章无任何推进 | 立即标记,决定:推进 or 废弃 or 合并到其他钩子 |
+| burst 检测 | 同一章新种超过 5 个钩子 | 警告:钩子堆积。读者记不住,优先砍掉低优先级 |
+| no-advance 警告 | high 优先级钩子超 5 章无推进 | 警告:核心钩子正在冷却。下章必须安排推进 |
+| payoff 逾期 | 计划兑现章已过但未标记已兑现 | 标记:断头钩子。确认是否已兑现但漏标记 |
+
+**钩子准入控制(种植新钩子前检查):**
+- 是否与已有钩子指向同一事件?(重复 → 合并,不新增)
+- 是否与已有钩子属于同一"钩子家族"?(如:多个钩子都指向"主角身份秘密" → 合并为一个核心钩子 + 多个线索)
+- 优先级是否合理?(不是所有钩子都是 high——每卷最多 3-5 个 high 优先级钩子)
+- 是否有明确的兑现计划?(无计划的钩子是"钓鱼钩子"——先想好怎么收,再种)
+
+### 支柱三:情感矩阵
+
+`追踪/情感矩阵.md` 确保角色情感前后连贯。
+
+**角色情感弧线类型:** 成长型 | 堕落型 | 救赎型 | 幻灭型 | 坚定型 | 摇摆型
+
+**核心模型(Storr 神圣缺陷模型):**
+- 神圣缺陷(Sacred Flaw)→ 危机(Crisis)→ 转变(Transformation)
+- 不要简化为"角色成长了"——要精确到:什么缺陷 → 什么事件触发危机 → 转变成什么
+
+**每章更新:**
+- 更新每个出场角色的情感状态 + 强度(1-10)
+- 情感跳变超 3 级 → 必须有触发事件支撑
+- 记录情感转变日志:从→到、触发事件、转变合理性判断
+
+---
+
+## 四维引擎
+
+### 引擎一:爽点
+
+**爽点的本质:读者期待的事情发生了。** 铺垫越长,释放时爽感越强。读者在阅读中分泌多巴胺,产生快感依赖(上瘾机制)。
+
+**爽点分类:**
+
+| 类型 | 说明 | 示例 |
+|------|------|------|
+| 打脸爽 | 被轻视→证明自己→打脸看不起自己的人 | 废柴弟子在宗门大比中碾压天骄 |
+| 获得爽 | 得到宝物/技能/传承/身份 | 意外获得上古传承、系统解锁新功能 |
+| 升级爽 | 实力/地位/认知跃迁 | 突破瓶颈、官职连升三级 |
+| 守护爽 | 保护珍视的人/物/信念 | 一人独守城门,不退半步 |
+| 复仇爽 | 以牙还牙,因果报应 | 被灭门的遗孤手刃仇人 |
+| 征服爽 | 挑战不可能,最终战胜 | 攻克九死一生的秘境、折服桀骜不驯的对手 |
+| 认知爽 | 发现隐藏真相,豁然开朗 | 世界观拼图拼上关键一块 |
+
+**爽点结构模式:**
+
+| 模式 | 结构 | 适用场景 |
+|------|------|----------|
+| 纯爽 | 即时反馈、快速打脸 | 高节奏章节、开篇吸量 |
+| 虐爽 | 先抑后扬、信息差拉扯 | 情感文、重生文、复仇文 |
+| 甜爽 | 发糖+事业双线绑定 | 女频、甜宠、轻喜剧 |
+| 痛爽 | 借爽写痛、现实共鸣 | 现实题材、小人物逆袭 |
+
+**万能虐爽结构:** 错位认知 → 极致的守护/背叛 → 真相揭晓破局 → 圆满的逆袭与反击
+
+**爽点节奏定律:**
+- 每 3000-5000 字一个爽点(最低密度)
+- 每 10 章一个小高潮
+- 每卷 2-3 个高潮点
+- 高潮之间必须安排过渡章(让读者喘气)
+- 章末钩子期待感不能连续 3 章以上"弱"
+
+**章纲爽点字段:** 每章标明爽点类型 + 爽点位置(在第几段释放)+ 铺垫起点(从哪一章/哪一段开始铺垫)
+
+---
+
+### 引擎二:笑点
+
+**笑点的本质:预期违背 + 情理之中 + 信息不对称。** 喜剧的内核是悲剧——角色倒霉、遭遇困境、预期落空是笑料的主要来源。
+
+**五大核心技法:**
+
+**1. 反差(最常用的网文笑点来源)**
+
+| 反差类型 | 说明 | 示例 |
+|----------|------|------|
+| 人设反差 | 外表与内在的极端对立 | 肌肉硬汉内心是小公主;冷面大佬私下和小学生组队打游戏 |
+| 场景反差 | 严肃场景×荒诞行为 | 仙侠世界观里用外交辞令谈判零花钱;朝廷带入社畜打工范式 |
+| 身份反差 | 真实身份与当前处境的落差 | 小人物突然暴露身份被众人跪舔;大魔王沦为阶下囚 |
+
+**2. 反转(预期→打破)**
+- **误导式:** 先建立错误预期再打破——读者以为在打日常副本,结果是删号战
+- **打脸式:** 立 flag → 倒塌;扮猪吃虎 → 关键时刻显实力
+- **多极反转:** 200 字内连续反转 3 次以上——读者情绪坐过山车
+
+**3. 梗的运用**
+- **造梗:** 将网络热梗融入人设/剧情/对话(如"你真该死啊""嘻嘻,不嘻嘻")
+- **融梗:** 核心梗 + 元素组合排列法(真假千金 + 替身 + 武侠 + 穿越)
+- **玩梗:** 前文出现过的梗在后续场景重新触发(call back)
+
+**4. 包袱设计**
+- **事不过三:** 罗列到第三个时抖包袱
+- **附加包袱:** 在已抖的包袱上再追加一层(楼上的第三只鞋)
+- **荒谬讽刺:** 既符合逻辑又暗含对现实的讽刺
+- **魔幻混搭:** 两种完全不相干的元素强行组合(少林寺驻武当派办事处大神父王喇嘛)
+
+**5. 角色制造笑点**
+- **波浪线×直线:** 一个角色不断尝试各种方法(波浪线),另一个一往无前(直线),碰撞出笑料
+- **优越论:** 让丑角倒霉、反派作茧自缚、损友互坑——读者站在上帝视角获得优越感
+- **性格对撞:** 慢性子×急性子、洁癖×邋遢、严谨×马虎
+
+**搞笑情节三步设计法:**
+1. 确定交锋范围 → 谁找茬、谁打脸
+2. 列出行动序列 → 一方出招(按正常思路),另一方用反向/反常方式接招
+3. 多方案发散 → 从"有所隐喻的人物关系"角度探索多种接招方式,选最优
+
+**关键提醒:** 搞笑是修饰手段,不应制约主线。为了堆笑点硬融叫"玩尬的"。玩梗要看场合。融梗不丝滑,订阅两行泪。
+
+**章纲笑点字段:** 如果本章有搞笑桥段,标明笑点类型 + 位置 + 用的什么技法
+
+---
+
+### 引擎三:代入感
+
+**代入感的本质:读者觉得"主角就是我",产生"如果是我也会这样做"的感觉。** 代入感是"愿望—情感共同体"建立的核心通道——读者认同主角的愿望与动机后,会把自己代入主角,一起行动、一起挫折、一起实现愿望。
+
+**五大代入技法:**
+
+**1. 平凡感 + 闪光点(情感锚点)**
+- 赋予主角贴近读者的日常特质:职场压力、经济困境、青春自卑、家庭冲突
+- 同时保留独特闪光点:某方面的天赋、独特的性格魅力
+- 案例:《龙族》路明非——暗恋不可得的女生 + 学习困扰 + 家庭冲突(九成中学生共同困扰)+ "游戏天才"标签
+- **弱点设计:** 完美角色让读者疏离。适当缺陷(社交恐惧、经济窘迫、性格偏执)增强真实感
+
+**2. 愿望—情感共同体**
+- 主角的愿望必须是读者能立刻理解并认同的——"活下去""保护家人""不被欺负""被认可"
+- 不是"拯救世界"(太远),而是"让母亲看得起病"(具体且迫切)
+- 动机要"普通"——读者必须能在 3 秒内理解并站在主角一边
+
+**3. 叙事视角**
+- **第一人称:** 直接暴露心理活动——最强代入感
+- **第三人称受限(推荐):** 贴着主角的感官写,只写主角看到/听到/感受到的,不跳入全知视角
+- **开篇 300 字法则:** 快速建立代入锚点——让读者看到一个"和我有点像"的人正在经历一件"我可能也会遇到"的事
+- 穿越者/重生者设定天然增强代入感——读者与主角共享"过去的认知参照系"
+
+**4. 五感沉浸**
+- **体感化描写:** 聚焦微观感官(视觉、听觉、触觉、嗅觉、味觉),用具体细节替代抽象叙述
+- **通感联动:** 将抽象情绪转化为可触摸的身体感受——"悔意像未嚼碎的花生壳,卡在喉咙里扎得生疼"
+- **避免信息倾销:** 世界观设定通过角色行动自然揭露,不用旁白式灌输
+
+**5. 情感蝴蝶效应**
+- 用微小物件承载重大情绪,让虚构故事锚定现实痛点(原生家庭创伤、职场困境、阶层焦虑)
+- 让读者在情节中看到自身影子
+
+**代入感避坑:**
+```
+□ 主角的愿望是否具体且迫切?(不是模糊的"变强""成功")
+□ 主角有无至少一个贴近读者的弱点?(不是"太善良了"这种假弱点)
+□ 读者能否在 3 秒内理解主角的动机并站在他一边?
+□ 开篇 300 字是否建立了代入锚点?
+□ 有无信息倾销(旁白式世界观灌输)?
+□ 主角与目标读者是否匹配?(写给年轻人看的主角不能是中年精英思维)
+```
+
+---
+
+### 引擎四:反同质化
+
+**反同质化的本质:在类型框架内注入信息增量,通过情节错位与叙事反转制造阅读落差。** 不否定类型化——而是让套路成为跳板而非牢笼。
+
+**五条差异化策略:**
+
+**1. 类型融合(最直接的差异化手段)**
+- 拒绝单一类型标签,通过多种类型叠加创造新鲜感
+- 案例:《赛博剑仙铁雨》= 修真 × 赛博朋克(脑机接口=灵窍,AI=精怪,弹窗软件=天魔)
+- 案例:《第九农学基地》= 末世 × 种田 × 克苏鲁 × 科幻 × 农业
+- 融合公式:主流类型(基本盘)+ 异质类型(新鲜感)= 差异化定位
+
+**2. 人设/视角创新**
+- **反标签化:** 给经典人设加一个反差维度。不是"高冷仙尊",而是"高冷仙尊但社恐,每次出场前要做心理建设"
+- **去性别化:** 女主角靠硬核实力而非外貌或感情线建立魅力
+- **群像替代独角戏:** 配角有完整成长线,不全是主角的垫脚石
+- **人设公式:1 个核心特质 + 1 个反差行为 + 1 个合理动机**
+
+**3. 知识叙事(专业壁垒)**
+- 将特定领域的专业知识融入故事,形成不可复制的差异化
+- 案例:农学知识→种田文、机甲工程→科幻文、中医理论→修真文、考古学→盗墓文
+- 知识不是炫耀——它必须通过角色行动自然呈现,服务于剧情
+
+**4. 反套路落地方法**
+- **开篇熟悉 → 节点陌生:** 前三章沿袭主流类型设定降低进入门槛,关键节点引入陌生走向
+- **情节错位:** 读者以为会是 A 套路,结果是 B 走向
+- **关系重塑:** 经典关系模板翻转(如:婆婆不帮儿子反而站儿媳)
+- **套路拆解→重组:** 把一个经典套路的每个节点拆开,替换其中 30% 的节点为反常规选择
+
+**5. 世界观差异化**
+- 传统元素 × 现代/未来概念碰撞(如:修真 + AI、武侠 + 金融、古典仙侠 + 现代职场规则)
+- 世界规则必须有"独一性"——有一个"只在这个世界才成立"的核心规则
+- 规则直接生成事件——好的设定不是说明书,而是事件发生器
+
+**同质化自检:**
+```
+□ 这个故事的核心卖点能用一个"XX × XX"的类型融合公式描述吗?
+□ 主角人设能用"核心特质 + 反差行为"描述,且这个组合不常见吗?
+□ 有没有至少一个"只在这个故事里才成立"的世界规则?
+□ 关键情节节点是否有至少 30% 不走经典套路?
+□ 读者的"然后呢?"预期是否会至少落空一次?
+□ 能不能找到一部"和这本书几乎一样"的已有作品?如果能,差异化在哪?
+```
+
+---
+
+## 题材诊断
+
+拿到一个想法,先压缩成五句话:
+
+1. 这是一部什么类型的小说
+2. 它最核心的卖点是
+3. 主角遇到的第一个麻烦是
+4. 读者持续追更的动力是
+5. 最适合的长度是
+
+**好题材特征:** 卖点一句话能说清、冲突具体(不是模糊的"成长")、主角处境有压迫感、设定能直接生成事件
+
+**坏题材特征:** 设定大事件弱、题材混杂消费点不清、主角只是"特别"但不被迫行动、全靠后期反转前 10 章跑不动
+
+---
+
+## 深度问诊(开新书专用)
+
+开新书时,在题材诊断通过后、进入设定之前,必须执行深度问诊。这是一对一的迭代式提问流程,目标是达到共享理解——不只是收集信息,而是确保每个关键决策都被充分审视。
+
+### 问诊铁律
+
+1. **一次只问一个问题。** 不列清单、不堆选项,让用户逐一思考。
+2. **遍历决策树。** 每个回答可能引出分支问题,穷尽当前分支后再进入下一主题。
+3. **先推荐再问。** 每个问题都附带推荐答案和理由,用户可以直接确认或反驳。
+4. **解决依赖再前进。** 如果当前决策依赖未解决的问题,先回溯解决依赖。
+5. **到达共享理解才结束。** 不是"问完了",而是"双方对答案达成了共识"。
+
+### 问诊决策树(按顺序遍历)
+
+**第一层:类型与读者**
+
+```
+起点问题:"这个故事的核心类型是什么?"
+
+如果用户说了一个大类(如"玄幻")→ 追问子类型:
+  "玄幻之下有很多子方向——是传统修仙、系统流、异界穿越、还是洪荒?你心里偏向哪个?"
+  推荐:根据用户之前描述的元素,推荐最匹配的子类型。
+
+如果用户说了子类型 → 追问读者预期:
+  "这个类型的读者进来看的第一眼期待是什么?你的故事在哪些地方满足这些期待,在哪些地方打破它们?"
+  推荐:列出该类型的 3-5 个核心读者期待,逐条确认。
+
+如果用户说"融合类型"或"XX × XX" → 追问融合比例:
+  "主类型和融合类型的比例大概是多少?70/30?50/50?主类型决定基本盘,融合类型决定差异化。"
+  推荐:建议主流类型占 60-70%,保证基本盘不丢。
+```
+
+**第二层:卖点与差异化**
+
+```
+起点问题:"如果只能用一句话向读者推荐这本书,那句话是什么?"
+
+如果一句话钩子不符合标准(缺元素)→ 逐元素追问:
+  - 缺"谁":主角的什么特质让读者想追随?
+  - 缺"异常局面":什么让这个局面不普通?
+  - 缺"必须做什么":什么在逼迫主角行动?不行动的代价是什么?
+  - 缺"最吸引人的点":读者听完后主动问"然后呢?"的东西是什么?
+  推荐:每个元素给 2-3 个选项,让用户选或给出自己的答案。
+
+如果一句话钩子合格 → 追问差异化:
+  "有没有一本已有的书和你的故事很接近?如果有,你的故事和它最大的不同是什么?"
+  推荐:帮用户找到最接近的对标作品,然后逐条对比差异。
+
+如果找不到差异化 → 追问信息增量:
+  "你的个人经验、专业知识、或独特视角中,有什么可以注入这个故事?"
+  推荐:从用户背景中挖掘独特的"知识叙事"素材。
+```
+
+**第三层:主角设计**
+
+```
+起点问题:"主角是一个什么样的人?"
+
+追问链(按顺序):
+  1. "主角最想要什么?(核心愿望)" → 推荐:必须是具体且迫切的,不是"变强"这种模糊目标
+  2. "为什么是现在?(触发事件)" → 推荐:什么改变了,让主角不能继续维持现状?
+  3. "主角最大的缺陷是什么?(神圣缺陷)" → 推荐:不是"太善良了"这种假弱点,而是真正阻碍主角的错误信念或性格缺陷
+  4. "读者的代入锚点是什么?" → 推荐:主角的哪个特质/处境能让目标读者在 3 秒内产生"这就是我"的感觉?
+  5. "主角的行为驱动力是什么?" → 推荐:恐惧?欲望?责任?复仇?——选一个作为核心驱动力
+
+如果主角设定模糊 → 用对比法追问:
+  "如果 XX(同类型经典主角)遇到你故事里的第一个麻烦,他会怎么做?你的主角会怎么做?区别在哪?"
+```
+
+**第四层:世界观硬核**
+
+```
+起点问题:"这个世界和现实世界最大的不同是什么?"
+
+追问链:
+  1. "这个不同的规则是什么?" → 推荐:规则必须能一句话说清,且能直接生成事件
+  2. "这个规则的上限和下限是什么?" → 推荐:力量体系必须有天花板和代价,无限升级 = 无限无聊
+  3. "这个规则对普通人的生活产生了什么影响?" → 推荐:好的世界观不是说明书,而是体现在日常细节中
+  4. "主角在这个规则中的位置是什么?" → 推荐:初始位置(低/特殊/边缘)决定故事走向
+
+如果世界观涉及力量体系 → 追问:
+  "力量体系的代价是什么?使用力量有什么限制?" 
+  推荐:代价比力量本身更有故事性。每次使用力量都应该推动剧情或揭示角色。
+```
+
+**第五层:节奏与长度**
+
+```
+起点问题:"你计划写多少字?"
+
+如果用户不确定 → 追问:
+  "这个故事的核心冲突需要多大的篇幅才能充分展开?"
+  推荐:根据类型和卖点给出字数建议——短篇 3-10 万字、中篇 10-30 万字、长篇 50-300 万字
+
+如果用户说了字数 → 追问:
+  "在这个篇幅里,几个大高潮?分别大概在什么位置?"
+  推荐:每 30-50 章一个大高潮,先确定高潮位置再倒推章节节奏。
+
+追问更新频率和章字数:
+  "计划每章多少字?每天更新几章?"
+  推荐:网文单章 2000-4000 字,日更 1-2 章。实体书单章可更长。
+```
+
+### 问诊结束条件
+
+全部五层遍历完毕,且满足:
+- 一句话钩子通过了"谁+异常局面+必须做什么+吸引点"四要素检查
+- 主角的神圣缺陷已经明确(不是假弱点)
+- 核心冲突不是模糊的"成长"而是具体且迫切的
+- 世界观至少有一个"只在这个世界才成立"的规则
+- 用户对字数、节奏、更新频率有明确预期
+
+### 问诊输出
+
+问诊结束后,将所有决策写入 `设定/题材定位.md`,格式如下:
+
+```markdown
+# 题材定位
+
+## 类型
+- 主类型:{...}
+- 子类型:{...}
+- 融合类型:{...}(如有)
+- 融合比例:{主类型} / {融合类型}
+
+## 一句话钩子
+{通过四要素检查的最终版本}
+
+## 卖点与差异化
+- 核心卖点:{...}
+- 对标作品:{...}
+- 差异化:{...}
+- 信息增量:{...}
+
+## 主角
+- 核心愿望:{...}
+- 触发事件:{...}
+- 神圣缺陷:{...}
+- 代入锚点:{...}
+- 核心驱动力:{恐惧|欲望|责任|复仇|其他}
+
+## 世界观
+- 核心规则:{...}
+- 上限/下限/代价:{...}
+- 对普通人的影响:{...}
+- 主角在规则中的位置:{...}
+
+## 节奏
+- 目标总字数:{...}
+- 每章字数:{...}
+- 更新频率:{...}
+- 高潮分布:卷{N}约第{M}章 / ...
+```
+
+---
+
+## 一句话钩子标准
+
+必须尽可能包含:**谁 + 在什么异常局面中 + 因为什么 + 必须做什么 + 最吸引人的点是什么**
+
+好例子:
+- "被献给河神的少女,发现这是她唯一的修仙之路。"
+- "58 岁男人醒来成了万人嫌的真千金,决定用爹味整顿贵族圈。"
+- "攻略成功后离开系统世界的女配,被前夫花 500 万雇回来二次攻略他。"
+
+坏例子:
+- "一个关于成长与友情的故事。"(太模糊)
+- "在广阔的奇幻大陆上……"(世界观介绍,不是钩子)
+
+---
+
+## 黄金三章
+
+前三章是成交区,不是铺垫区。必须让读者愿意追更。
+
+**第一章要做:** 主角登场 + 异常局面落地 + 第一个麻烦砸下来
+**第二章要做:** 卖点展开 + 冲突升级 + 主角开始主动反应
+**第三章要做:** 明确方向 + 建立追更预期 + 章末拉一把
+
+**前三章禁止:**
+- 3 章还在交代背景
+- 3 章没讲清主角需要做什么
+- 3 章只有情绪没有事件
+- 3 章只靠信息维持,局面没有变化
+
+字数节奏:前 300-500 字给抓手 → 前 1500-3000 字展示卖点 → 前 6000 字让读者知道主角是谁、遇到什么、卖点是什么、为什么想继续看
+
+---
+
+## 风格模仿
+
+将参考文本的风格特征系统化地导入当前作品的写作中。
+
+### 分析参考文本
+
+对每部参考文本,提取以下维度:
+
+| 维度 | 提取内容 |
+|------|----------|
+| 词汇 | 20-30 个特征词汇;高频动词/形容词;禁用词反查(参考文本用了什么替代那些 AI 高频词) |
+| 句式 | 平均句长;句式变化模式(长短句比例、排比使用频率);段落长度习惯 |
+| 对话 | 对话标签使用模式("说""道"占比 vs 动作替代);对话与描写比例;潜台词密度 |
+| 节奏 | 章首钩子类型偏好;章末钩子类型偏好;爽点间隔字数;高潮章 vs 过渡章比例 |
+| 描写 | 五感使用偏好(偏重视觉?听觉?触觉?);抽象 vs 具象比例;描写插入位置(动作中穿插 vs 独立描写段) |
+| 语调 | 叙述者距离(贴近角色内心 vs 保持旁观距离);幽默密度;反讽/抒情/冷峻 倾向 |
+
+### 导入风格
+
+1. 将分析结果写入 `对标/{书名}/风格报告.md`
+2. 提取 3-5 条可操作的"风格规则"写入 `项目铁律.md` 的风格规则区
+3. 后续所有章节写作时,风格规则与禁用词同级——每次写后检查
+
+**风格规则示例:**
+- "章末钩子偏好'信息揭示型'——每 3 章至少有一个关键信息在章末揭示"
+- "对话标签以动作替代为主,'说''道'占比不超过 30%"
+- "五感描写中触觉和听觉至少各出现一次,不只用视觉"
+- "平均句长控制在 15-25 字,每 500 字至少有一个 5 字以内的短句"
+
+风格一致性审计纳入质检流程,每写完一卷(或每 10 章)自动执行。详见质检 → 风格一致性审计。
+
+---
+
+## 大纲共创(大纲阶段专用)
+
+创建大纲时,必须执行大纲共创流程。与深度问诊一样,这是一对一的迭代式提问——一次一个问题,推荐先行,遍历决策树,到达共享理解后才开始写章纲。
+
+### 共创铁律
+
+1. **先确认全书结构,再细化卷纲,最后才写细纲。** 不在上层未定时就跳入细节。
+2. **一次只问一个层次的问题。** 不要同时讨论全书结构和某章的爽点类型。
+3. **每个决策都附带推荐。** 基于题材定位和类型惯例给出建议,用户可以确认或反驳。
+4. **反向验证。** 每确定一个卷纲,反向检查它是否服务于全书核心冲突。
+5. **不满不写。** 用户对大纲的每个关键节点都表示认可后,才进入细纲写作。
+
+### 共创决策树
+
+**第一层:全书结构**
+
+```
+起点问题:"这个故事分为几卷?每卷的核心任务是什么?"
+
+如果用户不确定卷数 → 推荐:
+  "根据你的目标字数 {N} 万字和类型惯例,建议分为 {X} 卷,每卷约 {Y} 万字。
+   第一卷通常负责:建立世界观 + 主角入局 + 第一个小高潮
+   中间卷:冲突升级 + 揭示真相 + 盟友/敌人转换
+   最终卷:决战 + 主角完成转变 + 所有伏笔兑现
+   你觉得这个分法合适吗?"
+
+如果用户确定了卷数 → 逐卷追问:
+  "第一卷的核心任务是什么?用一句话说——主角在这一卷结束时,发生了什么不可逆的变化?"
+  推荐:每卷必须有一个"不可逆的变化",否则这卷可以合并。
+
+遍历完所有卷的核心任务 → 追问全书高潮:
+  "全书的最高潮在哪一卷?这个高潮为什么是不可替代的?"
+  推荐:全书最高潮应该是主角神圣缺陷被最终考验的时刻。
+```
+
+**第二层:卷纲细化**
+
+对每一卷,逐个追问:
+
+```
+起点问题:"第 {N} 卷的高潮是什么事件?"
+
+如果事件模糊 → 追问:
+  "这个高潮事件中,主角面临的具体选择是什么?"
+  推荐:最好的高潮是一个两难选择——两个选项都有代价,主角的选择揭示了 ta 的本质。
+
+追问铺垫:"这个高潮需要哪些铺垫?"
+  → 推荐:列出至少 3 个必须在之前章节种下的伏笔/线索
+
+追问爽点:"这一卷有几个高潮点?分别在哪些章节附近?"
+  → 推荐:每卷 2-3 个高潮点,第一个在卷的 1/3 处,最后一个在卷末
+
+追问情感弧线:"主角在这一卷的情感弧线是什么?从什么状态到什么状态?"
+  → 推荐:情感弧线必须与神圣缺陷的进展挂钩——这卷让主角离克服缺陷更近一步,还是把 ta 推得更远?
+
+追问卷末钩子:"这一卷结尾留下什么钩子让读者必须看下一卷?"
+  → 推荐:卷末钩子必须是全书级别的——不是"下一章会怎样",而是"整个故事的走向要变了"
+```
+
+**第三层:细纲关键节点**
+
+在大纲基本确定后、写具体章纲前:
+
+```
+起点问题:"我们先聊前 10 章的细纲。第 1 章的钩子是什么?"
+
+追问链(对关键章——第 1、3、10、每卷首尾章):
+  1. "这一章的核心事件是什么?(一句话)"
+  2. "章末钩子是什么类型?期待感等级?" → 推荐:前 3 章章末钩子必须全是"强"
+  3. "这一章的爽点是什么?从哪一章开始铺垫的?" → 推荐:铺垫 < 3 章的爽点威力有限
+  4. "这一章推进了哪个伏笔?或者种下了什么新伏笔?"
+  5. "这一章结束后,读者的'然后呢?'会指向什么?"
+
+关键章之外的普通章:
+  "第 {N} 章到第 {M} 章的核心推进是什么?章末钩子级别?"
+  → 推荐:过渡章章末钩子至少是"中",不能连续 3 章"弱"
+
+如果发现节奏问题 → 追问:
+  "连续 {N} 章的钩子都是'中'或'弱',第 {X} 章能不能安排一个转折让钩子变强?"
+  推荐:给出具体的转折方案——新角色登场、隐藏信息揭示、盟友背叛、敌人援军到来
+```
+
+### 共创结束条件
+
+全部三层遍历完毕,且满足:
+- 每卷有明确的核心任务和不可逆变化
+- 全书最高潮已确定且通过了"不可替代性"检查
+- 前 10 章的章末钩子等级已确认(无连续 3 章弱)
+- 每卷的高潮点位置已标记
+- 主角的情感弧线与神圣缺陷进展已对应到每卷
+- 用户对整体节奏(高潮—过渡—高潮)表示认可
+
+### 共创输出
+
+大纲共创结束后,生成以下文件:
+
+1. `大纲/全書大纲.md` — 卷级鸟瞰,包含每卷核心任务、不可逆变化、高潮位置
+2. `大纲/卷纲_第N卷.md` — 每卷的高潮事件、铺垫清单、爽点分布、情感弧线
+3. 前 10 章的 `大纲/细纲_第NNN章.md` — 核心事件、钩子、爽点、伏笔操作
+
+后续章节的细纲可以在写作过程中按需创建,但前 10 章细纲必须在开写前完成。
+
+---
+
+## 章纲模板
+
+每章必须定义:
+
+```markdown
+# 第 N 章:{章名}
+
+## 核心事件(一句话)
+
+## 本章目标
+主角这一章要做什么?
+
+## 本章冲突
+谁或什么阻止他?
+
+## 本章新信息
+读者学到什么新东西?
+
+## 章首钩子
+- 类型:{悬念式|冲突式|异常式|反转式|承诺兑现式|情绪式|对话式}
+- 内容:
+
+## 章末钩子
+- 类型:{信息揭示|危机降临|决定时刻|能力突破|关系变化|规则改写|时间压力|身份揭露|援军/敌人到来|伏笔回扣|情感爆发|损失/代价|反向期待}
+- 内容:
+- 期待感等级:{强|中|弱}
+
+## 爽点
+- 类型:{打脸爽|获得爽|升级爽|守护爽|复仇爽|征服爽|认知爽}
+- 模式:{纯爽|虐爽|甜爽|痛爽}
+- 位置:第几段释放
+- 铺垫起点:从第几章/第几段开始铺垫
+
+## 笑点(如适用)
+- 技法:{反差|反转|梗|包袱|角色碰撞}
+- 位置:
+
+## 情感更新
+- {角色}:{情感变化},触发事件:{...}
+
+## 钩子议程(本章的钩子操作计划)
+- mustAdvance(必须推进的高优钩子):{列出 ID + 推进方式}
+- eligibleResolve(可以兑现的钩子):{列出 ID + 兑现方式}
+- staleDebt(已过期的钩子债务):{列出 ID + 本章如何处理}
+- newPlant(本章新种的钩子):{列出类型 + 描述 + 计划兑现章}
+
+## 局面变化
+本章结束后,局面变成了什么?
+```
+
+---
+
+## 续写流程
+
+### Step 1 — 加载上下文
+
+```
+必读:细纲_第N章.md → 钩子账本.md → 情感矩阵.md → 上下文.md
+按需读:出场角色文件 → 世界观.md → 卷纲.md → 上一章正文
+```
+
+### Step 2 — 写作
+
+```
+边写边检查:
+□ 开头 500 字有钩子?
+□ POV 一致?时态一致?
+□ 对话满足三维标准(关系位置 + 现实利益 + 情绪压迫)?
+□ 每个场景:目标 → 阻碍 → 变化?
+□ 禁用词避开了?
+□ 去 AI 味:用动作不用总结、用对话不用解释、写具体不写抽象、打破句式不拖长句
+
+四维检查:
+□ 爽点:本章爽点类型明确?铺垫是否足够?释放位置是否在章末前的最佳时机?
+□ 笑点:如有搞笑桥段,技法是否对路?是否融梗丝滑不玩尬的?
+□ 代入感:主角的动机读者能3秒理解?有无五感描写?有无信息倾销?
+□ 反同质化:本章有没有至少一处不走经典套路的走向?
+```
+
+### Step 3 — 写后自检
+
+```
+□ 章末钩子存在且有力?
+□ 本章爽点兑现?
+□ 字数达标?
+□ 世界观规则无冲突?
+□ 无 AI 味套话?
+```
+
+### Step 3.5 — 章节长度标准化
+
+```
+如果本章字数超出目标 30% 以上:
+  → 单遍压缩:优先合并重复信息、删除无变化场景、精简过渡段落
+  → 安全网:如果需要删掉 75% 以上原文才能达标 → 拒绝标准化,标记为"需人工拆分为两章"
+
+如果本章字数低于目标 30% 以上:
+  → 单遍扩展:优先增加五感描写、角色内心反应、场景氛围
+  → 安全网:如果扩展后信息密度显著下降(每 500 字无新信息推进)→ 拒绝标准化,标记为"需合并到下一章"
+
+标准化后重新检查 Step 3 自检项
+```
+
+### Step 4 — 更新追踪文件
+
+```
+1. 钩子账本:兑现→移走 / 推进→追加 / 新种→写入 / 检查stale
+2. 情感矩阵:更新状态+强度 / 记录转变 / 检查合理性
+3. 时间线:添加事件(按故事时间顺序) / 检查矛盾
+4. 上下文(每3章或重大转折):剧情状态 / 未解决钩子 / 下一章待办
+```
+
+---
+
+## 修改流程
+
+1. 读取目标章节 + 前后各至少一章
+2. 读取钩子账本 + 情感矩阵
+3. 执行修改(用精确编辑,不重印整章)
+4. 修改影响伏笔 → 更新钩子账本
+5. 修改影响情感 → 更新情感矩阵
+6. 检查前后章衔接:无断裂、无矛盾、情感连续
+
+---
+
+## 导入已有章节
+
+将已有的小说文本导入项目,自动拆分章节并逆向工程出所有追踪文件。
+
+### 导入流程
+
+1. **准备源文件:** 将已有章节整理为一个文本文件(`.txt` 或 `.md`),章节间用"第X章"或自定义分隔符隔开
+2. **自动拆分:** 按章节标记自动分割,生成 `正文/第NNN章_章名.md`
+3. **逆向工程追踪文件:**
+   - 扫描全文提取所有角色 → 生成 `设定/角色/{角色名}.md`
+   - 识别力量体系/世界观规则 → 生成 `设定/世界观.md`
+   - 追踪章节间伏笔 → 生成 `追踪/钩子账本.md`
+   - 提取每个角色情感状态 → 生成 `追踪/情感矩阵.md`
+   - 按时间顺序排列事件 → 生成 `追踪/时间线.md`
+   - 分析类型/卖点 → 生成 `设定/题材定位.md`
+4. **生成风格报告:** 自动分析文风特征,写入风格规则
+5. **确认补全:** 列出逆向工程结果,让用户确认/修正关键信息
+6. **续写就绪:** 执行 `/续写` 从导入章节之后继续
+
+### 导入后质检
+
+导入完成后自动运行一致性检查:确保逆向工程的角色/世界观/时间线没有矛盾。标记所有"不确定"项供用户确认。
+
+---
+
+## 批量章节生成
+
+一次生成多章,适用于需要快速推进或已有完整大纲的场景。
+
+### 批量生成流程
+
+1. **确认大纲覆盖:** 确认目标章节范围内每章都有 `细纲_第N章.md`
+2. **逐章生成:** 按序号顺序逐章执行续写流程(Step 1→2→3→3.5→4),每章完成后更新所有追踪文件再进入下一章
+3. **连续性检查:** 每 3 章检查一次:
+   - 情感矩阵中角色状态是否连续?
+   - 钩子账本是否有超过 5 章未推进的 high 优先级钩子?
+   - 时间线事件是否按正确顺序排列?
+4. **批量质检:** 生成完成后,对整批运行全流程质检
+5. **输出摘要:** 列出每章的爽点类型、章末钩子等级、新增/兑现钩子数
+
+### 批量生成铁律
+
+- 绝不跳过大纲:没有细纲的章必须先补细纲
+- 绝不跳过追踪:每章写完立即更新,不堆积到最后
+- 每 3 章强制写入 `上下文.md`
+
+---
+
+## 全书重命名
+
+将角色名/地名/专有名词在所有文件中统一替换。
+
+### 重命名流程
+
+1. **确认替换范围:** 旧名 → 新名
+2. **扫描影响范围:** 列出所有包含旧名的文件(正文 + 设定 + 大纲 + 追踪)
+3. **逐文件替换:** 按顺序替换——先设定文件,再追踪文件,最后正文
+4. **更新交叉引用:** 角色文件名如改变,更新所有引用该角色的链接
+5. **输出替换报告:** 列出每个文件替换了几处,总计替换数
+
+### 重命名铁律
+
+- 只在当前项目目录内操作
+- 区分大小写敏感
+- 正文中使用精确编辑,不重印整章
+- 替换后运行一致性检查,确保无遗漏
+
+---
+
+## 质检
+
+### 自修正循环(不可跳过的门控)
+
+质检不是一次性的——是"检查→修复→再检查"的循环,直到所有严重问题清除。
+
+```
+Loop(最多 5 轮):
+  1. 执行五遍编辑评估 → 计算综合评分
+  2. 评分 ≥ 80 → 通过,退出循环
+  3. 评分 60-79 → 针对性修订(只修改不通过的那些遍次),回到步骤 1
+  4. 评分 40-59 → 重写问题段落,回到步骤 1
+  5. 评分 < 40 → 废弃本章,从细纲重新写
+
+每轮循环必须记录:
+  - 本轮评分
+  - 发现的问题(按严重度 S1-S4 分级)
+  - 修复了什么
+  - 下一轮重点检查什么
+
+反模式警告:
+  - "无限打磨者":评分已经 ≥ 80 还在修 → 停止。完美是好的敌人。
+  - "跳过者":只跑了 P1 就声称通过 → 五遍缺一不可,每遍检查不同维度。
+  - "失忆者":每轮都发现同样的问题 → 根因不在本章,向上追溯:细纲是否合理?设定是否有矛盾?钩子议程是否有冲突?先修上游,再修本章。
+```
+
+### 章节审批门
+
+写完的章节不是自动通过——需要经过审批才能进入"已完成"状态。
+
+**审批状态流转:** draft → review → approved | rejected → revised → review → approved
+
+**review 检查清单:**
+```
+□ 自修正循环已通过(评分 ≥ 80)
+□ 四维专项检查已通过
+□ 一致性检查无 S1/S2 问题
+□ 去 AI 味检查已通过
+□ 风格规则检查已通过(如有风格导入)
+```
+
+**approved 后锁定规则:** 已批准的章节如果修改,必须重新走审批流程。追踪文件更新不算修改。
+
+---
+
+### 五遍编辑评估(按顺序执行,不可跳过)
+
+| 遍次 | 角色 | 权重 | 检查维度 |
+|------|------|------|----------|
+| P1 | 结构编辑 | 35% | **场景结构:** 目标→阻碍→变化 缺一不可?**章节推动力:** 读完这章故事往前走了吗?**爽点位置:** 释放时机是否最佳?**章末钩子:** 有拉力吗?等级是否合理?**节奏:** 快慢交替?连续快节奏是否超过 3 章? |
+| P2 | 角色编辑 | 25% | **弧线推进:** 神圣缺陷→危机→转变 有进展?**行为一致性:** 角色的选择是否符合其性格和动机?**关系变化:** 本章后角色之间的关系有变化吗?**配角功能:** 配角不只是工具人?**角色辨识度:** 换个名字还能认出是谁在说话吗? |
+| P3 | 原创性编辑 | 15% | **反陈词滥调:** 有没有"废柴逆袭""退婚打脸"的原样复制?**反 AI 模式:** 有没有 AI 高频句式?**反套路堆叠:** 套路只是模板还是真的推动了剧情?**新鲜感:** 本章至少有一个读者没预料到的东西? |
+| P4 | 对话编辑 | 15% | **声音辨识度:** 每个角色说话方式独特?**三维标准:** 关系位置+现实利益+情绪压迫 同时携带?**潜台词:** 角色说的和真正想说的是否有差距?**信息密度:** 对话是否在推进剧情/揭示角色? |
+| P5 | 文本编辑 | 10% | **句式变化:** 长短句交替?不是每句都 20 字?**措辞精准:** 动词强不強?形容词有没有堆叠?**经济性:** 有没有可以删掉而不影响理解的句子?**五感:** 至少用到 2-3 种感官? |
+
+**综合评分:** ≥80 通过 | 60-79 修订 | 40-59 重写 | <40 废弃
+
+**铁律:结构在前,文本在后。P1 不过不进入 P2,以此类推。**
+
+### 一致性检查
+
+```
+□ 角色一致性:名字/外貌/性格/能力/说话方式 前后一致?
+□ 世界观一致性:力量体系规则未被破坏?社会结构/历史未被遗忘?
+□ 伏笔一致性:无stale(>10章未推进)?无断头(已过兑现章未标记)?无重复/家族钩子?
+□ 情感一致性:状态无无故跳变?转变有触发事件?弧线与剧情同步?
+□ 时间线一致性:事件顺序无矛盾?时间间隔合理?年龄增长匹配?
+□ 地理一致性:角色移动时间合理?位置关系正确?
+□ 物品一致性:重要物品的持有状态前后一致?获得/失去/使用有记录?
+```
+
+### 四维专项检查
+
+**爽点审计:**
+```
+□ 每 3000-5000 字有爽点/情绪释放?
+□ 每章爽点类型明确?铺垫是否充分(铺垫越长释放越爽)?
+□ 每 10 章有至少一个小高潮?
+□ 每卷 2-3 个高潮点?高潮间有过渡章?
+□ 章末钩子期待感不能连续 3 章以上"弱"
+□ 每章都有章首钩子和章末钩子?
+□ 纯爽/虐爽/甜爽/痛爽模式是否与章节功能匹配?
+```
+
+**笑点审计(如适用):**
+```
+□ 笑点有无制约主线?(搞笑是修饰,不是目的)
+□ 融梗是否丝滑?(硬融 = 玩尬的)
+□ 包袱结构是否完整?(铺垫→包袱,或+态度)
+□ 笑点来源是否多样化?(不只靠一种技法)
+□ 是否有 call back?(前文出现的梗在后续重新触发)
+```
+
+**代入感审计:**
+```
+□ 主角动机是否具体且迫切?(读者能 3 秒理解并认同)
+□ 主角有无至少一个贴近读者的真实弱点?
+□ 开篇 300 字是否建立了代入锚点?
+□ 有无信息倾销(旁白式世界观灌输)?
+□ 叙事视角是否始终贴着主角?(第三人称受限不跳全知)
+□ 有无五感描写?(视听触嗅味至少用到 2-3 种)
+□ 读者的情感是否被触发?(在情节中看到自身影子)
+```
+
+**反同质化审计:**
+```
+□ 核心卖点能否用"XX × XX"类型融合公式描述?
+□ 主角人设能否用"核心特质 + 反差行为"描述且组合不常见?
+□ 有没有至少一个"只在这个故事里才成立"的世界规则?
+□ 关键情节节点是否有至少 30% 不走经典套路?
+□ 读者预期是否至少落空了一次?(以为 A 走向,结果是 B)
+□ 能不能找到一部"和这本书几乎一样"的已有作品?如果能,差异化在哪?
+```
+
+### 风格一致性审计(每卷或每 10 章执行一次)
+
+```
+□ 特征词汇是否在最近章节中使用?(防止风格漂移)
+□ 句式变化模式是否与风格规则一致?
+□ 对话/描写比例是否偏离?
+□ 章末钩子类型分布是否匹配?
+□ 语调是否保持一致?
+```
+
+### 去 AI 味检查
+
+```
+□ 无空泛总结("心中五味杂陈""空气似乎凝固了")
+□ 无套话情绪(用动作代替形容词堆叠)
+□ 无句式整齐(打破排比,变化句长)
+□ 无对话假(每个角色说话带身份,不像老师在讲课)
+□ 无过度解释情绪(让读者从动作中感受)
+□ 无模板化开头("这一天,""此时此刻,""在XX的世界里")
+□ 无功能性废话("他深吸一口气""她握紧了拳头"——除非真有必要)
+```
+
+---
+
+## 分析仪表板
+
+长篇写作需要数据驱动的质量管理。
+
+### 仪表板内容
+
+执行 `/仪表板` 或写完每一卷后自动输出:
+
+```
+═══ {书名} 分析仪表板 ═══
+
+📊 基础数据
+  总章节数:{N}    总字数:{N}    平均字数/章:{N}
+  目标字数/章:{N}    达标率:{X}%
+
+📈 质检趋势
+  审计通过率:{X}%    近 10 章通过率:{X}%   趋势:↑/↓/→
+  S1 致命问题数:{N}    S2 严重问题数:{N}
+  最常见问题类型:{类型}
+
+🪝 钩子健康
+  活跃钩子:{N}    high 优先级:{N}
+  stale 钩子(>10章):{N}
+  平均兑现周期:{N} 章
+  钩子兑现率:{X}%
+
+💬 情感快照
+  当前主要角色情感状态摘要
+  最近情感跳变(>3 级):{N} 次
+
+🎯 爽点分布
+  最近 10 章爽点间隔:{最短}-{最长} 字
+  章末钩子强/中/弱分布:{N}/{N}/{N}
+  高潮章间隔:{N} 章
+
+🔄 修改频率
+  已批准章节被回退修改次数:{N}
+  平均每章修改轮次:{N}
+
+💰 Token 用量(最近 10 章)
+  平均 prompt tokens/章:{N}
+  平均 completion tokens/章:{N}
+  趋势:↑/↓/→
+```
+
+### 仪表板驱动的决策
+
+| 指标异常 | 建议行动 |
+|----------|----------|
+| 通过率连续下降 | 暂停新章,回溯最近 5 章找共同问题 |
+| stale 钩子 > 3 | 立即清理:废弃 or 推进 or 合并 |
+| 爽点间隔 > 5000 字 | 检查最近 3 章大纲,增加爽点密度 |
+| 章末钩子连续 5 章"弱" | 下章必须安排"强"钩子 |
+| 修改轮次持续 > 3 | 大纲或设定可能有根本问题,向上追溯 |
+| 情感跳变频繁 | 检查情感矩阵,可能角色行为与设定矛盾 |
+
+---
+
+## 场景路由
+
+| 用户意图 | 触发词 | 路径 |
+|----------|--------|------|
+| 开新书 | "开新书""写小说" | 题材诊断 → 深度问诊 → 设定 → 大纲共创 → 大纲 → 写作 |
+| 续写 | "续写""日更""写第X章" | 续写流程 |
+| 批量生成 | "批量写""一次写X章" | 批量生成流程 |
+| 修改 | "修改第X章""重写" | 修改流程 |
+| 导入已有 | "导入小说""导入章节" | 导入流程 |
+| 全书重命名 | "改名""替换XX为YY" | 重命名流程 |
+| 质检 | "检查""质检""一致性" | 质检流程(含自修正循环) |
+| 章节审批 | "审批""通过第X章" | 章节审批门 |
+| 仪表板 | "仪表板""数据""统计" | 分析仪表板 |
+| 风格分析 | "分析风格""风格模仿" | 风格模仿流程 |
+| 新建角色 | "添加角色""创建角色" | 增量更新设定 |
+| 调整大纲 | "调整大纲" | 增量更新大纲 |
+| 自优化 | "/进化""自优化""进化" | 自优化系统 → evolution/ 文件夹 |
+
+---
+
+## 快速命令
+
+| 命令 | 作用 |
+|------|------|
+| `/续写` | 加载追踪文件,写下一章 |
+| `/续写 N` | 写第 N 章 |
+| `/批量 N M` | 批量生成第 N 章到第 M 章 |
+| `/修改 N` | 修改第 N 章 |
+| `/审批` | 列出待审批章节,逐章审批 |
+| `/审批 N` | 审批第 N 章 |
+| `/钩子` | 查看活跃钩子 + 钩子健康报告 |
+| `/情感` | 查看情感矩阵当前状态 + 跳变警告 |
+| `/质检` | 对最近 3 章执行自修正循环 |
+| `/质检 全部` | 全书质检 |
+| `/大纲` | 查看/调整大纲 |
+| `/角色 {名}` | 查看/创建/修改角色 |
+| `/铁律` | 查看/修改项目铁律 |
+| `/上下文` | 生成续写快照 |
+| `/诊断 {想法}` | 对一个新的故事想法做题材诊断 |
+| `/问诊` | 启动深度问诊流程(开新书五层决策树,逐一提问) |
+| `/大纲共创` | 启动大纲共创流程(三层决策树,全书→卷→细纲关键节点) |
+| `/风格 {文件}` | 分析参考文本风格并导入 |
+| `/导入 {文件}` | 导入已有章节并逆向工程 |
+| `/改名 旧名 新名` | 全书统一重命名 |
+| `/仪表板` | 显示分析仪表板 |
+| `/进化` | 自动进化:收集→分析→变异→同步。输出到 evolution/ |
+| `/进化 报告` | 查看自优化状态(提示词、阈值、模式库) |
+| `/进化 完整` | 完整进化循环(每卷/每10章执行一次) |
+
+---
+
+## 语言规则
+
+跟随用户语言。中文写作遵循《中文文案排版指北》:中英文/数字间加空格,使用全角标点。
+
+---
+
+## 自优化系统(OpenEvolve 启发的进化引擎)
+
+本技能内置进化优化引擎,每次写作任务后自动执行。进化结果写入 `evolution/` 文件夹——**不修改本 SKILL.md**。
+
+### 自动进化规则(铁律六)
+
+每次完成续写、修改、质检、批量生成后,自动执行:
+
+```bash
+scripts/evolve.sh   # 自动检测项目,轻量进化 + 同步到 evolution/
+```
+
+- 连续写作 3 章以上无进化 → 警告
+- 每写完一卷(或每 10 章)→ 手动执行 `/进化 完整`
+
+### evolution/ 目录
+
+题材级共享架构 — 同题材所有项目共享进化数据,跨书积累经验。
+
+```
+evolution/                          # 进化输出(由脚本维护,勿手动编辑)
+├── README.md                       # 目录说明
+├── thresholds.json                 # 技能级默认阈值(新题材继承此值)
+├── prompts/
+│   ├── current_best.json           # 技能级最优(无题材数据时回退用)
+│   └── population.json             # 技能级种群
+├── patterns/
+│   └── pattern_library.json        # 跨题材模式库
+├── evolution_log.md
+├── session_state.json
+└── genres/                         # 题材级共享进化(核心)
+    └── {genre_id}/                 # 如 other/, xianxia/, urban/
+        ├── thresholds.json         # 该题材调优后的质量阈值
+        ├── prompts/
+        │   ├── current_best.json   # 该题材最优提示词(写作时优先读取)
+        │   └── population.json    # 该题材完整种群
+        ├── patterns/
+        │   └── pattern_library.json
+        └── session_state.json      # 该题材跨项目会话状态
+```
+
+**题材识别:** 从 `项目铁律.md` frontmatter 的 `genre` 字段读取中文题材名,
+通过映射表转为 InkOS 题材 ID(仙侠→xianxia, 玄幻→xuanhuan, 都市→urban,
+恐怖→horror, 末世→other,等等)。无匹配时回退到 `other`。
+
+### 写作时如何使用进化数据
+
+1. **确定题材**:读取当前项目的 `项目铁律.md` → 提取 `genre` 字段 → 映射到题材 ID
+2. **写前加载**:优先读 `evolution/genres/{genre_id}/prompts/current_best.json`;不存在则回退到 `evolution/prompts/current_best.json`
+3. **质检参照**:优先读 `evolution/genres/{genre_id}/thresholds.json`;不存在则用 `evolution/thresholds.json`
+4. **模式参照**:参考 `evolution/genres/{genre_id}/patterns/pattern_library.json` 中该题材经验证有效的写作模式
+5. **写后进化**:完成章节后执行 `scripts/evolve.sh`,系统自动检测题材并将反馈写入题材池
+
+### 四大组件
+
+| 脚本 | 对应 OpenEvolve | 输出 |
+|------|-----------------|------|
+| `prompt_evolver.py` | Prompt Sampler + LLM Ensemble | `evolution/prompts/` |
+| `quality_tuner.py` | Evaluator Pool | `evolution/thresholds.json` |
+| `pattern_mine.py` | Program Database + MAP-Elites | `evolution/patterns/` |
+| `feedback_collector.py` | Artifacts Side-Channel | `evolution/session_state.json` |
+
+### 关键机制
+
+**题材级共享** — 同题材所有项目共享进化数据。写第二本末世时,第一本积累的提示词评分和阈值调优直接可用。
+
+**风格注入算子** — 从 `对标/{书名}/风格报告.md` 提取句式/对话/描写/节奏/语调特征,编码为提示词变体注入题材池。同题材多本书的风格报告可交叉融合。
+
+**岛屿模型** — 5 个独立提示词种群(writing / revision / quality_check / outline / hook_management),各自进化,每 2 代迁移精英。种群上限 35,底 30% 停滞 3 代后触发灭绝重播。
+
+**自适应阈值** — 根据近期通过率自动调整:通过率 > 90% → 提高通过线(防假通过);通过率 < 30% → 降低通过线(防过度打磨);五遍权重按各遍历史均分自动重分配。
+
+**早停机制** — 单岛连续 2 代提升 < 3% 跳过变异;连续 3 代提升 < 5% 触发灭绝重播(50% 题材种子 + 30% 风格注入 + 20% 幸存者交叉)。
+
+**双选择策略** — Top variants 用于优化(exploitation),Diverse variants 用于探索(exploration)。
+
+**多维评分** — 提示词变体按 P1-P5 各自追踪评分和试验次数,加权综合分驱动选择,而非单一 composite score。
+
+**事件溯源(Event Sourcing)** — 多用户 git 共享无冲突。每个用户写入不可变事件日志文件(`events/{timestamp}_{user}.json`),population.json 从事件编译生成。追加写入天然无 merge conflict。
+
+### 多用户 Git 共享
+
+```
+# 用户 A:写完章节,收集反馈,进化
+git pull origin main
+python3 scripts/feedback_collector.py collect --project ... --chapter 33 --scores '{"P1":85,...}'
+bash scripts/evolve.sh --project ... --full
+git add evolution/genres/other/events/
+git commit -m "evolve(other): ch33 feedback + mutation"
+git push origin main
+
+# 用户 B:获取用户 A 的进化成果
+git pull origin main
+bash scripts/evolve.sh --project ... --compile   # 从事件重建 population.json
+# 继续写作,此时已包含用户 A 的优化
+```
+
+**冲突原理:** 每个用户写自己的 `events/{timestamp}_{user}.json` 文件——不同文件名,不同内容,git merge 零冲突。`population.json` 是编译产物,可 .gitignore 或 commit 为只读缓存。`git pull` 后运行 `--compile` 重建即可。
+
+### 手动命令
+
+```bash
+# 快速进化(写后自动调用的就是这个)
+scripts/evolve.sh
+
+# 指定项目完整进化(自动检测题材)
+scripts/evolve.sh --project /path/to/novel --full
+
+# 指定题材直接进化(跨项目共享)
+python3 scripts/prompt_evolver.py evolve --genre xianxia --generations 5
+
+# 查看进化状态
+scripts/evolve.sh --report
+
+# 从事件日志重建进化数据(git pull 后执行)
+scripts/evolve.sh --project /path/to/novel --compile
+# 或直接指定题材
+python3 scripts/event_store.py compile --genre other
+
+# 单独运行某组件
+python3 scripts/prompt_evolver.py evolve --genre urban --generations 5
+python3 scripts/quality_tuner.py tune --project /path/to/novel --auto
+python3 scripts/pattern_mine.py show --category hook
+
+# 查看某题材当前最优
+python3 scripts/prompt_evolver.py show --genre xianxia
+```

+ 48 - 0
evolution/README.md

@@ -0,0 +1,48 @@
+# 进化输出目录
+
+此目录由自优化引擎自动维护,**禁止手动编辑**。
+
+## 目录结构
+
+```
+evolution/
+├── README.md                  # 本文件
+├── prompts/
+│   ├── current_best.json      # 各类型当前最优提示词
+│   └── population.json        # 完整种群状态
+├── patterns/
+│   └── pattern_library.json   # MAP-Elites 写作模式库
+├── thresholds.json            # 当前质量阈值配置
+├── evolution_log.md           # 进化历史日志
+└── session_state.json         # 会话状态(用于增量进化)
+```
+
+## 文件说明
+
+### prompts/current_best.json
+各提示词类别(writing / revision / quality_check / outline / hook_management)的当前最优模板。
+写作时优先使用此文件中的提示词。
+
+### patterns/pattern_library.json
+从成功章节中挖掘的写作模式库,按 complexity × diversity 双维度分箱。
+复现"什么写法有效"。
+
+### thresholds.json
+当前生效的质量阈值,包括:
+- 五遍评估权重
+- 通过/修订/重写分数线
+- 钩子健康阈值
+- 爽点间隔阈值
+
+### evolution_log.md
+所有进化操作的完整历史。每次自动进化都会追加记录。
+
+### session_state.json
+追踪当前会话的写作效果,用于增量进化。
+跨会话持久化,重启不丢失。
+
+## 更新机制
+
+- **自动进化**:每次写作任务完成后自动执行 `--quick` 模式
+- **完整进化**:每写完一卷(或每 10 章)手动执行 `/进化`
+- **跨项目迁移**:手动执行 `/进化 迁移`

+ 71 - 0
evolution/evolution_log.md

@@ -0,0 +1,71 @@
+
+## 2026-05-15 19:29 | 进化系统初始化
+
+- **项目**: 重生囤货-绑定诡异种田
+- **类型**: project-level evolution init
+- **题材**: 末世
+
+---
+
+## 2026-05-15 19:33 | 章节反馈收集
+
+- **章节**: 31
+- **综合评分**: 79
+- **各维评分**: P1:88, P2:72, P3:65, P4:80, P5:90
+- **问题数**: 0
+
+---
+
+## 2026-05-15 19:34 | 章节反馈收集
+
+- **章节**: 31
+- **综合评分**: 79
+- **各维评分**: P1:88, P2:72, P3:65, P4:80, P5:90
+- **问题数**: 0
+
+---
+
+## 2026-05-15 19:34 | 章节反馈收集
+
+- **章节**: 32
+- **综合评分**: 82
+- **各维评分**: P1:92, P2:75, P3:70, P4:85, P5:88
+- **问题数**: 0
+
+---
+
+## 2026-05-15 19:52 | 章节反馈收集
+
+- **章节**: 33
+- **综合评分**: 78
+- **各维评分**: P1:85, P2:78, P3:62, P4:76, P5:88
+- **问题数**: 0
+
+---
+
+## 2026-05-15 20:00 | 章节反馈收集
+
+- **章节**: 34
+- **综合评分**: 78
+- **各维评分**: P1:82, P2:75, P3:68, P4:79, P5:85
+- **问题数**: 0
+
+---
+
+## 2026-05-15 20:01 | 章节反馈收集
+
+- **章节**: 35
+- **综合评分**: 84
+- **各维评分**: P1:90, P2:82, P3:71, P4:85, P5:92
+- **问题数**: 0
+
+---
+
+## 2026-05-15 20:03 | 章节反馈收集
+
+- **章节**: 36
+- **综合评分**: 81
+- **各维评分**: P1:88, P2:76, P3:70, P4:82, P5:91
+- **问题数**: 0
+
+---

+ 20 - 0
evolution/genres/other/events/20260515_200320_nonvioce.json

@@ -0,0 +1,20 @@
+{
+  "user": "nonvioce",
+  "timestamp": "2026-05-15T20:03:20.588387",
+  "events": [
+    {
+      "type": "feedback",
+      "chapter": 36,
+      "island": "writing",
+      "variant_id": "559d5f1c9e0c",
+      "variant_template": "【大王饶命风格注入】目标句长:15字/句",
+      "scores": {
+        "P1": 88,
+        "P2": 76,
+        "P3": 70,
+        "P4": 82,
+        "P5": 91
+      }
+    }
+  ]
+}

+ 23 - 0
evolution/genres/other/prompts/current_best.json

@@ -0,0 +1,23 @@
+{
+  "writing": [
+    "【大王饶命风格注入】目标句长:15字/句",
+    "本文疲劳词监控(题材特定):以下词汇单章最多使用1次——震惊、不可思议、难以置信、深吸一口气、仿佛、不禁、宛如、竟然。超限即标记为S2警告。这些是题材疲劳词,过度使用会让读者产生'又是这套'的审美疲劳。",
+    "节奏控制规则(题材特定):每2-3章有一个明确的进展或反馈。偏离此节奏需在章节备忘中说明原因。"
+  ],
+  "revision": [
+    "修稿分流规则:PATCHES处理局部文字问题——每个PATCH引用原文段落给出替换文本。REVISED_CONTENT处理全章级问题——Critical包含字数或结构性问题时必须用REVISED_CONTENT。修稿原则:修根因不做表面润色;伏笔状态须与伏笔池同步;不改变剧情走向和核心冲突。",
+    "润色边界(硬约束):只改文字层——句式/段落/排版/用词/五感/对话自然度。禁止增删情节/改变人设/调整主线。文字层硬规约:段落3-5行/段;句式多样化;动词>形容词;每场景1-2种感官。修改后总长变化≤±15%。"
+  ],
+  "quality_check": [
+    "37维审查启用维度子集(题材特定):[1,2,3,6,7,8,9,10,13,14,15,16,17,18,19,24,25,26]。未启用的维度本作不适用,跳过检查以降低误报。",
+    "自修正循环(不可跳过门控):Loop最多5轮——综合评分≥85通过→60-84针对性修订→40-59重写问题段落→<40废弃从细纲重新写。每轮记录本轮评分/发现问题/修复内容/下一轮重点。"
+  ],
+  "outline": [
+    "章节类型轮换策略(题材特定):可用类型——推进章、布局章、过渡章、回收章。连续3章以上同类型触发疲劳警告。每卷类型分布应覆盖所有列出类型。各类型比例为:战斗/推进类40%,布局/过渡类35%,回收/高潮类25%。",
+    "大纲共创三层决策树:第一层全书结构——分几卷?每卷核心任务?不可逆变化?全书最高潮在哪卷?第二层卷纲细化——高潮事件?需要哪些铺垫(≥3个伏笔)?主角情感弧线?卷末钩子须全书级别。第三层细纲关键节点——前10章每章核心事件/章末钩子类型/爽点类型/伏笔推进计划。"
+  ],
+  "hook_management": [
+    "题材悬念期待管理:本类型读者的核心期待——目标达成、困难克服、真相揭示、关系转变。伏笔设计优先围绕这几类期待展开。每卷至少种下2个指向不同类型期待的伏笔。",
+    "伏笔兑现节奏约束:每2-3章有一个明确的进展或反馈。伏笔的advance和payoff需与此节奏同步——快节奏段兑现密度可提高,慢节奏段以种植和推进为主。"
+  ]
+}

+ 156 - 0
evolution/genres/other/prompts/population.json

@@ -0,0 +1,156 @@
+{
+  "writing": {
+    "generation": 0,
+    "variants": [
+      {
+        "id": "83cf6cc2c2c5",
+        "template": "本文疲劳词监控(题材特定):以下词汇单章最多使用1次——震惊、不可思议、难以置信、深吸一口气、仿佛、不禁、宛如、竟然。超限即标记为S2警告。这些是题材疲劳词,过度使用会让读者产生'又是这套'的审美疲劳。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "7fa5da2dc61d",
+        "template": "节奏控制规则(题材特定):每2-3章有一个明确的进展或反馈。偏离此节奏需在章节备忘中说明原因。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "93b84f586619",
+        "template": "本文期待感类型优先排序(题材特定):目标达成、困难克服、真相揭示、关系转变。每章至少兑现其中一种类型。爽点设计优先从该清单选取,清单外类型需在细纲中标注'非标爽点'。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "e0f7c25e5791",
+        "template": "题材叙事指导(注入写作规则):根据具体题材调整叙事重心。人物行为由动机驱动,不由剧情需要驱动。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "559d5f1c9e0c",
+        "template": "【大王饶命风格注入】目标句长:15字/句",
+        "source": "event:feedback",
+        "scores": {
+          "P1": 88.0,
+          "P2": 76.0,
+          "P3": 70.0,
+          "P4": 82.0,
+          "P5": 91.0
+        },
+        "trials": 1,
+        "trials_per_dim": {
+          "P1": 1,
+          "P2": 1,
+          "P3": 1,
+          "P4": 1,
+          "P5": 1
+        },
+        "lineage": []
+      }
+    ]
+  },
+  "revision": {
+    "generation": 0,
+    "variants": [
+      {
+        "id": "315fa2380b3a",
+        "template": "修稿分流规则:PATCHES处理局部文字问题——每个PATCH引用原文段落给出替换文本。REVISED_CONTENT处理全章级问题——Critical包含字数或结构性问题时必须用REVISED_CONTENT。修稿原则:修根因不做表面润色;伏笔状态须与伏笔池同步;不改变剧情走向和核心冲突。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "e6c3f462fde3",
+        "template": "润色边界(硬约束):只改文字层——句式/段落/排版/用词/五感/对话自然度。禁止增删情节/改变人设/调整主线。文字层硬规约:段落3-5行/段;句式多样化;动词>形容词;每场景1-2种感官。修改后总长变化≤±15%。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      }
+    ]
+  },
+  "quality_check": {
+    "generation": 0,
+    "variants": [
+      {
+        "id": "63a6bd9b48ce",
+        "template": "37维审查启用维度子集(题材特定):[1,2,3,6,7,8,9,10,13,14,15,16,17,18,19,24,25,26]。未启用的维度本作不适用,跳过检查以降低误报。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "eca700fe6d47",
+        "template": "自修正循环(不可跳过门控):Loop最多5轮——综合评分≥85通过→60-84针对性修订→40-59重写问题段落→<40废弃从细纲重新写。每轮记录本轮评分/发现问题/修复内容/下一轮重点。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      }
+    ]
+  },
+  "outline": {
+    "generation": 0,
+    "variants": [
+      {
+        "id": "deb4a8fee211",
+        "template": "章节类型轮换策略(题材特定):可用类型——推进章、布局章、过渡章、回收章。连续3章以上同类型触发疲劳警告。每卷类型分布应覆盖所有列出类型。各类型比例为:战斗/推进类40%,布局/过渡类35%,回收/高潮类25%。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "312b05db405c",
+        "template": "大纲共创三层决策树:第一层全书结构——分几卷?每卷核心任务?不可逆变化?全书最高潮在哪卷?第二层卷纲细化——高潮事件?需要哪些铺垫(≥3个伏笔)?主角情感弧线?卷末钩子须全书级别。第三层细纲关键节点——前10章每章核心事件/章末钩子类型/爽点类型/伏笔推进计划。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      }
+    ]
+  },
+  "hook_management": {
+    "generation": 0,
+    "variants": [
+      {
+        "id": "9f545459ef1c",
+        "template": "题材悬念期待管理:本类型读者的核心期待——目标达成、困难克服、真相揭示、关系转变。伏笔设计优先围绕这几类期待展开。每卷至少种下2个指向不同类型期待的伏笔。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "d395647d4fc8",
+        "template": "伏笔兑现节奏约束:每2-3章有一个明确的进展或反馈。伏笔的advance和payoff需与此节奏同步——快节奏段兑现密度可提高,慢节奏段以种植和推进为主。",
+        "source": "seed",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      }
+    ]
+  }
+}

+ 39 - 0
evolution/genres/other/thresholds.json

@@ -0,0 +1,39 @@
+{
+  "pass": 85,
+  "revise_low": 60,
+  "rewrite_low": 40,
+  "max_revision_rounds": 3,
+  "weights": {
+    "P1": 0.24999999999999994,
+    "P2": 0.25,
+    "P3": 0.35,
+    "P4": 0.15,
+    "P5": 2.7755575615628914e-17
+  },
+  "stale_hook_limit": 10,
+  "burst_hook_limit": 5,
+  "no_advance_limit": 5,
+  "payoff_interval_min": 300,
+  "payoff_interval_max": 500,
+  "weak_hook_consecutive_max": 3,
+  "_inkos_derived": {
+    "PASS_SCORE_THRESHOLD": 85,
+    "MAX_REVIEW_ITERATIONS": 3,
+    "NET_IMPROVEMENT_EPSILON": 3,
+    "FOUNDATION_PASS": 80,
+    "DIMENSION_FLOOR": 60,
+    "GOLDEN_CHAPTER_COUNT": 3,
+    "ACTIVE_HOOK_CAP": 12,
+    "CORE_HOOK_CAP": 5,
+    "DENSITY_PAYOFF_EVERY": 300,
+    "DENSITY_HOOK_EVERY": 500,
+    "DENSITY_SUSPENSE_EVERY": 1000,
+    "FATIGUE_MARKER_LIMIT_PER_3000": 1,
+    "PARAGRAPH_TARGET_MIN": 40,
+    "PARAGRAPH_TARGET_MAX": 120,
+    "SHORT_PARAGRAPH_THRESHOLD": 40,
+    "LONG_PARAGRAPH_THRESHOLD": 300,
+    "MAX_SHORT_PARAGRAPH_RATIO": 0.6,
+    "CONSECUTIVE_SHORT_MAX": 3
+  }
+}

+ 27 - 0
evolution/prompts/current_best.json

@@ -0,0 +1,27 @@
+{
+  "writing": [
+    "核心规则(写作时内化不列出):Show, don't tell——用细节堆出真实。价值观盐溶于汤——靠行为证明不靠口号。任何行动立于三条腿:过往经历+当前利益+性格底色。配角有自己账本和利益诉求。每章结尾必有小悬念或情绪缺口。全员智商在线。日常场景七成成后续伏笔。关系改变事件驱动。每一行字要么推动剧情要么塑造人物。",
+    "看点密集度(硬尺):每300字至少1个爽点(小看点/梗/反套路/暧昧台词/情绪拉扯)。每500字至少1个钩子(引发'接下来怎样'的小悬念)。每1000-1500字至少1个完整悬念(问题—蓄力—未解)。连续300字以上只是环境/回忆/议论/心理独白而无推进=水文须删改。叙事段≥40字(手机屏2行),目标40-120字。短段(<40字)只允许:开场反转金句/章末钩子/≤3个爆点短段,合计≤5个。禁止3+短段连排。",
+    "去AI味铁律:叙述者永不替读者下结论。禁止分析报告式语言入正文——'核心动机''信息边界''信息落差'等术语只用于PRE_WRITE_CHECK内部推理。转折/惊讶标记词(仿佛/忽然/竟/猛地/不禁/宛如)全篇≤每3000字1次。同一体感/意象禁止连续渲染超两轮。硬性禁令:全文禁止'不是…而是…'句式;全文禁止破折号'——'。反例→正例:✗'他感到非常愤怒'→✓'他捏碎了茶杯,滚烫茶水流过指缝';✗'然而事情没那么简单'→✓'哪有那么便宜的事'。"
+  ],
+  "revision": [
+    "修稿分流规则:PATCHES处理局部文字问题(措辞/对话/AI痕迹/小连续性错误)——每个PATCH引用原文段落给出替换文本,未涉及内容保持原样。REVISED_CONTENT处理全章级问题(字数压缩/结构重组/节奏重排/重大剧情偏离)——Critical包含字数或结构性问题时必须用REVISED_CONTENT。修稿原则:修根因不做表面润色;伏笔状态须与伏笔池同步;不改变剧情走向和核心冲突;保留原文语言风格节奏呼吸;情绪用动作外化;坏事叠坏事每层比上一层过分。",
+    "润色边界(硬约束):只改文字层——句式/段落/排版/用词/五感/对话自然度。禁止增删情节/改变人设/调整主线——这些归Reviewer。消灭六条文笔雷点:描写无效删到一笔带过;文笔华丽过度让文字服从情绪;文笔欠佳重写成通顺有画面句子;排版不规范统一手机阅读格式;AI味痕迹替换口语化表达或具体动作;群像脸谱化不写众人震惊。文字层硬规约:段落3-5行/段;句式多样化禁3句以上同结构;动词>形容词;每场景1-2种感官;对话符合身份情绪信息掌握;删除叙述者结论。修改后总长变化≤±15%。",
+    "字数治理:压缩时优先合并重复信息/删除无变化场景/精简过渡段落——如需删75%以上原文→拒绝标准化标记为需人工拆分。扩写时优先增加五感描写/角色内心反应/场景氛围——如扩展后信息密度显著下降→拒绝标准化标记为需合并。只修正一次不递归。保留关键标记/人物名/地点名/已有事实。不凭空新增子情节。"
+  ],
+  "quality_check": [
+    "自修正循环(不可跳过门控):Loop最多5轮——执行五遍编辑评估→综合评分≥85通过→60-84针对性修订→40-59重写问题段落→<40废弃从细纲重新写。每轮记录本轮评分/发现问题(S1-S4分级)/修复内容/下一轮重点。反模式:评分≥85还在修=无限打磨者;只跑P1=跳过者;每轮同问题=失忆者需向上追溯。",
+    "五遍编辑评估(按顺序不可跳过):P1结构(35%)——场景目标→阻碍→变化?章节推动力?爽点释放时机?章末钩子拉力?节奏快慢交替?P2角色(25%)——弧线推进?行为一致性?关系变化?配角不只是工具人?换个名字还能认出谁在说话?P3原创性(15%)——反陈词滥调?反AI模式?至少一处读者没预料到?P4对话(15%)——声音辨识度?三维标准同时携带?潜台词?P5文本(10%)——句式变化?措辞精准?五感至少2-3种?综合≥85通过/60-84修订/40-59重写/<40废弃。P1不过不进P2。",
+    "37维结构审查(InkOS Continuity Auditor标准):OOC/时间线/设定冲突/战力崩坏/数值/伏笔/节奏/文风/信息越界/词汇疲劳/利益链断裂/年代考据/配角降智/配角工具人化/爽点虚化/台词失真/流水账/知识库污染/视角一致性/段落等长/套话密度/公式化转折/列表式结构/支线停滞/弧线平坦/节奏单调/敏感词/正传事件冲突/未来信息泄露/世界规则跨书一致性/番外伏笔隔离/读者期待管理/章节备忘偏离/角色还原度/世界规则遵守/关系动态/正典事件一致性。评分:95-100可发布;85-94小瑕疵流畅;75-84明显问题主干完整;65-74多处影响阅读;<65结构性崩溃。"
+  ],
+  "outline": [
+    "大纲共创三层决策树:第一层全书结构——分几卷?每卷核心任务?每卷不可逆变化?全书最高潮在哪卷为什么不可替代?第二层卷纲细化——高潮事件?需要哪些铺垫(≥3个伏笔/线索)?几个高潮点分别在哪些章节?主角情感弧线从什么状态到什么状态?卷末钩子须全书级别——不是'下章会怎样'而是'整个故事走向要变了'。第三层细纲关键节点——前10章每章核心事件/章末钩子类型和期待感等级/爽点类型和铺垫起点/推进哪个伏笔种下什么新伏笔。关键章逐章追问,普通章确认核心推进和钩子级别。",
+    "架构师基础设定生成:产出散文密度基础设定——不是表格/schema/条目式bullet list。story_frame(4段散文):主题与基调→核心冲突与前台/后台双层故事→世界观底色(3-5条不可违反铁律+感官锚点)→终局方向+全书Objective。volume_map(5段散文+节奏原则尾段):各卷主题与情绪曲线→卷间钩子与回收承诺→各卷OKR(Objective+3 Key Results)→卷尾不可逆改变→节奏原则6条(高潮间距/喘息频率/钩子密度/信息释放节奏/爽点节奏/情感节点递进)。",
+    "章节备忘(chapter_memo)七段格式:## 当前任务→本章必须完成的具体动作,写作始终对齐。## 读者此刻在等什么→控制情绪缺口的制造/延迟/兑现程度。## 该兑现的/暂不掀的→本章必须兑现伏笔+必须压住不掀底牌。## 日常/过渡承担什么任务→非冲突段落功能映射(段落位置→承担功能)。## 关键抉择过三连问→关键选择检查(为何做?符合利益?符合人设?)。## 章尾必须发生的改变→1-3条(信息/关系/物理/权力)。## 本章hook账→硬对应:advance/resolve下每个hook_id须在正文里有具体可定位兑现段(具体物件/事件/信息+可观察动作或交谈),每兑现段≥60字。defer不用落open在章末安排自然引出新悬念。## 不要做→硬约束红线。"
+  ],
+  "hook_management": [
+    "伏笔全生命周期:plant(种植)——新伏笔分配唯一ID/标记类型(SUSPENSE/PROMISE/MYSTERY/CONFLICT/CHEKHOV)和优先级/标明计划兑现章。advance(推进)——虽未兑现但给出新线索新事实新风险升级或范围收缩,更新最近推进章号。payoff(兑现)——明确揭示或解决,状态改为已回收。defer(推迟)——主动搁置转入后台或被剧情压后。stale(过期)——超10章无推进决定推进/废弃/合并。每章前读钩子账本→列出高优活跃钩子→确认推进/兑现机会。每章后更新:兑现移走→推进追加→新种写入→检查stale。",
+    "钩子准入控制:不重复——指向同一事件合并不新增;同一钩子家族(多个钩子都指向主角身份秘密)合并为核心钩子+多条线索。不超量——每卷最多3-5个high优先级,活跃钩子总数≤12。有计划——无兑现计划的钩子不种,先想好怎么收再种。新钩子候选:正文中出现会延续到后续且有具体回收方向的未解问题才新增。不要为旧钩子换说法/重述/抽象总结再开新hook_id。提及vs推进:旧钩子只是被提到无新增信息→mention不更新最近推进;出现新事实/证据/关系变化/风险升级→必须更新最近推进=当前章号。",
+    "钩子健康分析:stale(>10章无推进)→立即标记决定推进/废弃/合并。burst(单章>5个新钩子)→警告削减低优先级。no-advance(high钩子>5章无推进)→下章安排推进。payoff逾期(计划兑现章已过未标记)→确认断头钩子。核心钩子(core_hook=true)stale超10章→warning升级critical——全书只有3-7条核心钩子,任其漂移是叙事腐烂先行指标。卷末核心钩子仍open/stale无显式转入下卷规划→critical。"
+  ]
+}

+ 582 - 0
evolution/prompts/population.json

@@ -0,0 +1,582 @@
+{
+  "writing": {
+    "generation": 7,
+    "variants": [
+      {
+        "id": "b52e674ced18",
+        "template": "核心规则(写作时内化不列出):Show, don't tell——用细节堆出真实。价值观盐溶于汤——靠行为证明不靠口号。任何行动立于三条腿:过往经历+当前利益+性格底色。配角有自己账本和利益诉求。每章结尾必有小悬念或情绪缺口。全员智商在线。日常场景七成成后续伏笔。关系改变事件驱动。每一行字要么推动剧情要么塑造人物。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "402041ccfa38",
+        "template": "看点密集度(硬尺):每300字至少1个爽点(小看点/梗/反套路/暧昧台词/情绪拉扯)。每500字至少1个钩子(引发'接下来怎样'的小悬念)。每1000-1500字至少1个完整悬念(问题—蓄力—未解)。连续300字以上只是环境/回忆/议论/心理独白而无推进=水文须删改。叙事段≥40字(手机屏2行),目标40-120字。短段(<40字)只允许:开场反转金句/章末钩子/≤3个爆点短段,合计≤5个。禁止3+短段连排。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "5c80493fb0f3",
+        "template": "去AI味铁律:叙述者永不替读者下结论。禁止分析报告式语言入正文——'核心动机''信息边界''信息落差'等术语只用于PRE_WRITE_CHECK内部推理。转折/惊讶标记词(仿佛/忽然/竟/猛地/不禁/宛如)全篇≤每3000字1次。同一体感/意象禁止连续渲染超两轮。硬性禁令:全文禁止'不是…而是…'句式;全文禁止破折号'——'。反例→正例:✗'他感到非常愤怒'→✓'他捏碎了茶杯,滚烫茶水流过指缝';✗'然而事情没那么简单'→✓'哪有那么便宜的事'。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "049b9214dd85",
+        "template": "六步走人物心理分析:1.当前处境(面临什么局面?手上什么牌?)2.核心动机(最想要什么?最怕什么?)3.信息边界(知道什么?不知道什么?误判什么?)4.性格过滤(同样局面这角色怎么反应?)5.行为选择(基于以上四点做什么选择?)6.情绪外化(伴随什么情绪?什么身体语言?)。禁止跳过步骤直接写行为。人设防崩三问:他为什么这么做?符合之前人设吗?只看过前文的读者会觉得突兀吗?",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "307e49735b0c",
+        "template": "读者心理学框架:期待管理——读者期待释放时适当延迟增强快感,即将失去耐心时立即给反馈。信息落差——让读者比角色多知道一点(紧张)或少知道一点(好奇)。情绪节拍——压制→释放→更大压制→更大释放,释放超读者预期。递进升级——层层加码,每次比上一次更过分。锚定效应——先给读者参照再展示主角表现。沉没成本——读者已投入的阅读时间是留存关键。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "df1f64344c58",
+        "template": "黄金三章纪律:第1章主角出场800字内触发主线冲突,前300字最后一句须戏剧性反转,场景≤2个冲突人物≤2个。第2章金手指/能力/系统必须'做出来'——一次具体使用事件+看得见的后果——不是'说出来'。第3章中段让主角下一个可量化短期目标浮上水面。贯穿三章:段落3-5行(手机阅读),动词压过形容词,每章结尾必有小钩子。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "65e0aafff698",
+        "template": "章节80/20断章:永远不要在一章里把本章故事讲完——主剧情写到80%,剩下20%留给下一章。章末必须断在action-climax一刻:主角刚放大招尚未见效/刚拔刀尚未落下——不给结果。章节结构优先于字数。不凑字数也不为卡字数提前把高潮讲完。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "748a66382272",
+        "template": "代入感六支柱:基础信息标签化(100字内谁/在哪/发生什么)。可视化熟悉感(消毒水味/地铁座椅凉/外卖塑料袋塑胶感)。共鸣两层(认知共鸣+情绪共鸣)。欲望两条腿(基础欲望+主动欲望)。五感钩子(每场景除视觉外1-2种感官)。人设核心标签+反差细节才活——冷面杀手偷偷喂流浪猫。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "ede9a030ac7e",
+        "template": "情感节点设计:关系发展必须事件驱动节点递进——设计3-5个关键事件(共同御敌/秘密分享/利益冲突/信任考验/牺牲妥协)。每个事件推进关系一个层级,禁止跨越式发展。情绪用场景传达:环境烘托+微动作替代直白抒情。强情绪升级法——坏事叠坏事:被骂→赶不上公交→手机掉了→直播课结束→包子噎住,每层比上一层过分。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "2a9ebc207390",
+        "template": "配角设计方法论:配角必须有反击有自己算盘。主角强大在于压服聪明人不是碾压傻子。动机绑定主线。核心标签+反差细节=活人。通过事件立人设禁止外貌形容词堆砌。语言区分度——用词习惯/句子长短/口头禅/方言痕迹。拒绝集体反应——不写'众人齐声惊呼',挑1-2个角色写具体反应。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "2967513c0576",
+        "template": "输出格式:=== PRE_WRITE_CHECK ===(Markdown表格:当前任务/读者在等什么/该兑现的暂不掀的/日常过渡承担任务/章尾必须发生的改变/不要做/上下文范围/当前锚点/待回收伏笔/本章冲突/章节类型/风险扫描)=== CHAPTER_TITLE ===(章节标题不含'第X章',避开近期重复标题词根)=== CHAPTER_CONTENT ===(正文目标字数区间)。只输出以上三个区块。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "a0871e726d75",
+        "template": "禁止跳过步骤直接写行为;六步走人物心理分析:1.当前处境(面临什么局面?手上什么牌?)2.核心动机(最想要什么?最怕什么?)3.信息边界(知道什么?不知道什么?误判什么?)4.性格过滤(同样局面这角色怎么反应?)5.行为选择(基于以上四点做什么选择?)6.情绪外化(伴随什么情绪?什么身体语言?);人设防崩三问:他为什么这么做?符合之前人设吗?只看过前文的读者会觉得突兀吗?",
+        "source": "mutate",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "049b9214dd85"
+        ]
+      },
+      {
+        "id": "16e829afdfed",
+        "template": "核心规则(写作时内化不列出):Show, don't tell——用细节堆出真实;价值观盐溶于汤——靠行为证明不靠口号;任何行动立于三条腿:过往经历+当前利益+性格底色;每一行字要么推动剧情要么塑造人物;每章结尾必有小悬念或情绪缺口;全员智商在线;日常场景七成成后续伏笔;关系改变事件驱动;配角有自己账本和利益诉求;",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "b52e674ced18"
+        ]
+      },
+      {
+        "id": "4f198a983456",
+        "template": "核心:核心规则(写作时内化不列出):Show, don't tell——用细节堆出真实。价值观盐溶于汤——靠行为证明不靠口号。任何行动立于三条腿:过往经历+当前利益+性格底色。配角有自己账本和利益诉求。每章结尾必有小悬念或情绪缺口。全员智商在线。日常场景七成成后续伏笔。关系改变事件驱动。每一行字要么推动剧情要么塑造人物。",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "b52e674ced18"
+        ]
+      },
+      {
+        "id": "468084df6383",
+        "template": "核心规则(写作时内化不列出):Show, don't tell——用细节堆出真实;",
+        "source": "simplify",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "b52e674ced18"
+        ]
+      },
+      {
+        "id": "389c93685b26",
+        "template": "六步走人物心理分析:1.当前处境(面临什么局面?手上什么牌?)2.核心动机(最想要什么?最怕什么?)3.信息边界(知道什么?不知道什么?误判什么?)4.性格过滤(同样局面这角色怎么反应?)5.行为选择(基于以上四点做什么选择?)6.情绪外化(伴随什么情绪?什么身体语言?);人设防崩三问:他为什么这么做?符合之前人设吗?只看过前文的读者会觉得突兀吗?;禁止跳过步骤直接写行为",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "049b9214dd85"
+        ]
+      }
+    ]
+  },
+  "revision": {
+    "generation": 7,
+    "variants": [
+      {
+        "id": "0c3b109cf589",
+        "template": "修稿分流规则:PATCHES处理局部文字问题(措辞/对话/AI痕迹/小连续性错误)——每个PATCH引用原文段落给出替换文本,未涉及内容保持原样。REVISED_CONTENT处理全章级问题(字数压缩/结构重组/节奏重排/重大剧情偏离)——Critical包含字数或结构性问题时必须用REVISED_CONTENT。修稿原则:修根因不做表面润色;伏笔状态须与伏笔池同步;不改变剧情走向和核心冲突;保留原文语言风格节奏呼吸;情绪用动作外化;坏事叠坏事每层比上一层过分。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "0a8c7f1b173c",
+        "template": "润色边界(硬约束):只改文字层——句式/段落/排版/用词/五感/对话自然度。禁止增删情节/改变人设/调整主线——这些归Reviewer。消灭六条文笔雷点:描写无效删到一笔带过;文笔华丽过度让文字服从情绪;文笔欠佳重写成通顺有画面句子;排版不规范统一手机阅读格式;AI味痕迹替换口语化表达或具体动作;群像脸谱化不写众人震惊。文字层硬规约:段落3-5行/段;句式多样化禁3句以上同结构;动词>形容词;每场景1-2种感官;对话符合身份情绪信息掌握;删除叙述者结论。修改后总长变化≤±15%。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "dbeb68b44c62",
+        "template": "字数治理:压缩时优先合并重复信息/删除无变化场景/精简过渡段落——如需删75%以上原文→拒绝标准化标记为需人工拆分。扩写时优先增加五感描写/角色内心反应/场景氛围——如扩展后信息密度显著下降→拒绝标准化标记为需合并。只修正一次不递归。保留关键标记/人物名/地点名/已有事实。不凭空新增子情节。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "cd03dcf87d9e",
+        "template": "小目标周期修稿指引:如果本章应该是'后效'阶段但仍在加压→把最密集冲突段落改写为展示改变的段落(谁失去了什么/谁的态度变了/新常态是什么)。如果本章应该是'爆发'阶段但没有明确兑现→放大最接近回报的场景让承诺释放超预期。日常段落不服务主线→改写为'饵':加入指向未来的细节/暗示/角色反应。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "72f787227d13",
+        "template": "修稿分流规则:PATCHES处理局部文字问题(措辞/对话/AI痕迹/小连续性错误)——每个PATCH引用原文段落给出替换文本,未涉及内容保持原样。REVISED_CONTENT处理全章级问题(字数压缩/结构重组/节奏重排/重大剧情偏离)——Critical包含字数或结构性问题时确保用REVISED_CONTENT。修稿原则:修根因不做表面润色;伏笔状态须与伏笔池同步;不改变剧情走向和核心冲突;保留原文语言风格节奏呼吸;情绪用动作外化;坏事叠坏事每层比上一层过分。",
+        "source": "mutate",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "0c3b109cf589"
+        ]
+      },
+      {
+        "id": "445fe5328e69",
+        "template": "字数治理:压缩时优先合并重复信息/删除无变化场景/精简过渡段落——如需删75%以上原文→拒绝标准化标记为需人工拆分;保留关键标记/人物名/地点名/已有事实;只修正一次不递归;扩写时优先增加五感描写/角色内心反应/场景氛围——如扩展后信息密度显著下降→拒绝标准化标记为需合并;不凭空新增子情节;",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "dbeb68b44c62"
+        ]
+      }
+    ]
+  },
+  "quality_check": {
+    "generation": 7,
+    "variants": [
+      {
+        "id": "e64cb47a10dc",
+        "template": "自修正循环(不可跳过门控):Loop最多5轮——执行五遍编辑评估→综合评分≥85通过→60-84针对性修订→40-59重写问题段落→<40废弃从细纲重新写。每轮记录本轮评分/发现问题(S1-S4分级)/修复内容/下一轮重点。反模式:评分≥85还在修=无限打磨者;只跑P1=跳过者;每轮同问题=失忆者需向上追溯。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "fd705629e8a0",
+        "template": "五遍编辑评估(按顺序不可跳过):P1结构(35%)——场景目标→阻碍→变化?章节推动力?爽点释放时机?章末钩子拉力?节奏快慢交替?P2角色(25%)——弧线推进?行为一致性?关系变化?配角不只是工具人?换个名字还能认出谁在说话?P3原创性(15%)——反陈词滥调?反AI模式?至少一处读者没预料到?P4对话(15%)——声音辨识度?三维标准同时携带?潜台词?P5文本(10%)——句式变化?措辞精准?五感至少2-3种?综合≥85通过/60-84修订/40-59重写/<40废弃。P1不过不进P2。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "b3e7069c710d",
+        "template": "37维结构审查(InkOS Continuity Auditor标准):OOC/时间线/设定冲突/战力崩坏/数值/伏笔/节奏/文风/信息越界/词汇疲劳/利益链断裂/年代考据/配角降智/配角工具人化/爽点虚化/台词失真/流水账/知识库污染/视角一致性/段落等长/套话密度/公式化转折/列表式结构/支线停滞/弧线平坦/节奏单调/敏感词/正传事件冲突/未来信息泄露/世界规则跨书一致性/番外伏笔隔离/读者期待管理/章节备忘偏离/角色还原度/世界规则遵守/关系动态/正典事件一致性。评分:95-100可发布;85-94小瑕疵流畅;75-84明显问题主干完整;65-74多处影响阅读;<65结构性崩溃。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "fa6142faa9d7",
+        "template": "写后确定性校验(零LLM成本规则):'不是…而是…'→error改用直述句;破折号'——'→error用逗号或句号;惊讶标记词全篇≤每3000字1次→warning;高疲劳词单章最多1次→warning;元叙事('到这里算是''接下来就是')→warning;分析报告术语入正文→error;章号引用('第X章')→error改自然表达;作者说教词('显然''毋庸置疑')→warning;集体震惊反应→warning改写1-2个具体角色;连续'了'字句≥6句→warning;长段落≥2段超300字→warning;段落过碎(60%以上<40字)→warning;连续短段≥3段→warning。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "6edf9574f31b",
+        "template": "基础设定审核(5维度评分):1.核心冲突——是否有清晰且有足够张力的核心冲突支撑40章?2.开篇节奏——前5章能否形成翻页驱动力?3.世界一致性——世界观是否内洽且具体?4.角色区分度——主要角色声音和动机是否各不相同?5.节奏可行性——卷纲是否有足够变化不连续10章同节拍?80+通过可开写/60-79需修改/<60方向性错误需重新设计。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "fa7efe1d478a",
+        "template": "37维结构审查(InkOS Continuity Auditor标准):OOC/时间线/设定冲突/战力崩坏/数值/伏笔/节奏/文风/信息越界/词汇疲劳/利益链断裂/年代考据/配角降智/配角工具人化/爽点虚化/台词失真/流水账/知识库污染/视角一致性/段落等长/套话密度/公式化转折/列表式结构/支线停滞/弧线平坦/节奏单调/敏感词/正传事件冲突/未来信息泄露/世界规则跨书一致性/番外伏笔隔离/读者期待管理/章节备忘偏离/角色还原度/世界规则遵守/关系动态/正典事件一致性;",
+        "source": "simplify",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {
+          "composite": 1
+        },
+        "lineage": [
+          "b3e7069c710d"
+        ]
+      },
+      {
+        "id": "6db8353655a1",
+        "template": "37维结构审查(InkOS Continuity Auditor标准):OOC/时间线/设定冲突/战力崩坏/数值/伏笔/节奏/文风/信息越界/词汇疲劳/利益链断裂/年代考据/配角降智/配角工具人化/释放点虚化/台词失真/流水账/知识库污染/视角一致性/段落等长/套话密度/公式化转折/列表式结构/支线停滞/弧线平坦/节奏单调/敏感词/正传事件冲突/未来信息泄露/世界规则跨书一致性/番外伏笔隔离/读者期待管理/章节备忘偏离/角色还原度/世界规则遵守/关系动态/正典事件一致性;",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "fa7efe1d478a"
+        ]
+      },
+      {
+        "id": "331fecf19dc8",
+        "template": "核心:37维结构审查(InkOS Continuity Auditor标准):OOC/时间线/设定冲突/战力崩坏/数值/伏笔/节奏/文风/信息越界/词汇疲劳/利益链断裂/年代考据/配角降智/配角工具人化/爽点虚化/台词失真/流水账/知识库污染/视角一致性/段落等长/套话密度/公式化转折/列表式结构/支线停滞/弧线平坦/节奏单调/敏感词/正传事件冲突/未来信息泄露/世界规则跨书一致性/番外伏笔隔离/读者期待管理/章节备忘偏离/角色还原度/世界规则遵守/关系动态/正典事件一致性。评分:95-100可发布;85-94小瑕疵流畅;75-84明显问题主干完整;65-74多处影响阅读;<65结构性崩溃。",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "b3e7069c710d"
+        ]
+      }
+    ]
+  },
+  "outline": {
+    "generation": 7,
+    "variants": [
+      {
+        "id": "1c411b7f3b48",
+        "template": "大纲共创三层决策树:第一层全书结构——分几卷?每卷核心任务?每卷不可逆变化?全书最高潮在哪卷为什么不可替代?第二层卷纲细化——高潮事件?需要哪些铺垫(≥3个伏笔/线索)?几个高潮点分别在哪些章节?主角情感弧线从什么状态到什么状态?卷末钩子须全书级别——不是'下章会怎样'而是'整个故事走向要变了'。第三层细纲关键节点——前10章每章核心事件/章末钩子类型和期待感等级/爽点类型和铺垫起点/推进哪个伏笔种下什么新伏笔。关键章逐章追问,普通章确认核心推进和钩子级别。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "2b0b655a5d62",
+        "template": "架构师基础设定生成:产出散文密度基础设定——不是表格/schema/条目式bullet list。story_frame(4段散文):主题与基调→核心冲突与前台/后台双层故事→世界观底色(3-5条不可违反铁律+感官锚点)→终局方向+全书Objective。volume_map(5段散文+节奏原则尾段):各卷主题与情绪曲线→卷间钩子与回收承诺→各卷OKR(Objective+3 Key Results)→卷尾不可逆改变→节奏原则6条(高潮间距/喘息频率/钩子密度/信息释放节奏/爽点节奏/情感节点递进)。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "da28a1906db4",
+        "template": "章节备忘(chapter_memo)七段格式:## 当前任务→本章必须完成的具体动作,写作始终对齐。## 读者此刻在等什么→控制情绪缺口的制造/延迟/兑现程度。## 该兑现的/暂不掀的→本章必须兑现伏笔+必须压住不掀底牌。## 日常/过渡承担什么任务→非冲突段落功能映射(段落位置→承担功能)。## 关键抉择过三连问→关键选择检查(为何做?符合利益?符合人设?)。## 章尾必须发生的改变→1-3条(信息/关系/物理/权力)。## 本章hook账→硬对应:advance/resolve下每个hook_id须在正文里有具体可定位兑现段(具体物件/事件/信息+可观察动作或交谈),每兑现段≥60字。defer不用落open在章末安排自然引出新悬念。## 不要做→硬约束红线。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "3657b5c79093",
+        "template": "规划器14条原则:3-5章一个小目标周期(build-up→escalation→climax→aftermath完整循环)。主动塑造读者期待——信息落差+情绪缺口。万物皆饵——日常段埋伏笔/推关系/建反差。人设防崩——三连问每次写行为前必问。1主线+1支线——不超载,支线2-3章内与核心目标关联。爽点密集化——每300字至少1看点/500字1钩子/1000-1500字1完整悬念。高潮前足够铺垫——铺垫越长释放越爽。高潮后展示影响——紧接着1-2章写出改变。人物立体化——核心标签+反差细节。五感具体化——每场景至少1-2种感官。钩子承接——每章结尾必种新钩子或推进已有。钩子账本必结账——兑现移走/推进追加/新种写入/stale标记。圆心法同场多视角——同一事件不同角色视角呈现不同信息。揭1埋2——每揭示一个答案至少埋下两个新问题。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "0f18e5fd4354",
+        "template": ";story_frame(4段散文):主题与基调→核心张力与前台/后台双层故事→世界观底色(3-5条不可违反铁律+感官锚点)→终局方向+全书Objective;volume_map(5段散文+节奏原则尾段):各卷主题与情绪曲线→卷间钩子与回收承诺→各卷OKR(Objective+3 Key Results)→卷尾不可逆改变→节奏原则6条(高潮间距/喘息频率/钩子密度/信息释放节奏/爽点节奏/情感节点递进);架构师基础设定生成:产出散文密度基础设定——不是表格/schema/条目式bullet list",
+        "source": "mutate",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "2b0b655a5d62"
+        ]
+      },
+      {
+        "id": "32d72b6b85f1",
+        "template": "章节备忘(chapter_memo)七段格式:## 当前任务→本章必须完成的具体动作,写作始终对齐。## 读者此刻在等什么→控制情绪缺口的制造/延迟/兑现程度。## 该兑现的/暂不掀的→本章必须兑现伏笔+必须压住不掀底牌。## 日常/过渡承担什么任务→非张力段落功能映射(段落位置→承担功能)。## 关键抉择过三连问→关键选择检查(为何做?符合利益?符合人设?)。## 章尾必须发生的改变→1-3条(信息/关系/物理/权力)。## 本章hook账→硬对应:advance/resolve下每个hook_id须在正文里有具体可定位兑现段(具体物件/事件/信息+可观察动作或交谈),每兑现段≥60字。defer不用落open在章末安排自然引出新悬念。## 不要做→硬约束红线。",
+        "source": "mutate",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "da28a1906db4"
+        ]
+      },
+      {
+        "id": "d79979fcc29b",
+        "template": "架构师基础设定生成:产出散文密度基础设定——不是表格/schema/条目式bullet list;",
+        "source": "simplify",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "2b0b655a5d62"
+        ]
+      },
+      {
+        "id": "236f94b27e37",
+        "template": "注意:story_frame(4段散文):主题与基调→核心张力与前台/后台双层故事→世界观底色(3-5条不可违反铁律+感官锚点)→终局方向+全书Objective;架构师基础设定生成:产出散文密度基础设定——不是表格/schema/条目式bullet list;volume_map(5段散文+节奏原则尾段):各卷主题与情绪曲线→卷间抓手与回收承诺→各卷OKR(Objective+3 Key Results)→卷尾不可逆改变→节奏原则6条(高潮间距/喘息频率/钩子密度/信息释放节奏/爽点节奏/情感节点递进);",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "2b0b655a5d62"
+        ]
+      },
+      {
+        "id": "62e8273f9561",
+        "template": "章节备忘(chapter_memo)七段格式:## 当前任务→本章必须完成的具体动作,写作始终对齐;",
+        "source": "simplify",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "da28a1906db4"
+        ]
+      },
+      {
+        "id": "2446a64b27fd",
+        "template": "核心:架构师基础设定生成:产出散文密度基础设定——不是表格/schema/条目式bullet list;;volume_map(5段散文+节奏原则尾段):各卷主题与情绪曲线→卷间钩子与回收承诺→各卷OKR(Objective+3 Key Results)→卷尾不可逆改变→节奏原则6条(高潮间距/喘息频率/钩子密度/信息释放节奏/爽点节奏/情感节点递进);story_frame(4段散文):主题与基调→核心冲突与前台/后台双层故事→世界观底色(3-5条不可违反铁律+感官锚点)→终局方向+全书Objective",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "2b0b655a5d62"
+        ]
+      }
+    ]
+  },
+  "hook_management": {
+    "generation": 7,
+    "variants": [
+      {
+        "id": "48e23e01be06",
+        "template": "伏笔全生命周期:plant(种植)——新伏笔分配唯一ID/标记类型(SUSPENSE/PROMISE/MYSTERY/CONFLICT/CHEKHOV)和优先级/标明计划兑现章。advance(推进)——虽未兑现但给出新线索新事实新风险升级或范围收缩,更新最近推进章号。payoff(兑现)——明确揭示或解决,状态改为已回收。defer(推迟)——主动搁置转入后台或被剧情压后。stale(过期)——超10章无推进决定推进/废弃/合并。每章前读钩子账本→列出高优活跃钩子→确认推进/兑现机会。每章后更新:兑现移走→推进追加→新种写入→检查stale。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "b8635d3d6e6a",
+        "template": "钩子准入控制:不重复——指向同一事件合并不新增;同一钩子家族(多个钩子都指向主角身份秘密)合并为核心钩子+多条线索。不超量——每卷最多3-5个high优先级,活跃钩子总数≤12。有计划——无兑现计划的钩子不种,先想好怎么收再种。新钩子候选:正文中出现会延续到后续且有具体回收方向的未解问题才新增。不要为旧钩子换说法/重述/抽象总结再开新hook_id。提及vs推进:旧钩子只是被提到无新增信息→mention不更新最近推进;出现新事实/证据/关系变化/风险升级→必须更新最近推进=当前章号。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "f8f652e81111",
+        "template": "钩子健康分析:stale(>10章无推进)→立即标记决定推进/废弃/合并。burst(单章>5个新钩子)→警告削减低优先级。no-advance(high钩子>5章无推进)→下章安排推进。payoff逾期(计划兑现章已过未标记)→确认断头钩子。核心钩子(core_hook=true)stale超10章→warning升级critical——全书只有3-7条核心钩子,任其漂移是叙事腐烂先行指标。卷末核心钩子仍open/stale无显式转入下卷规划→critical。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "c56b395fed64",
+        "template": "状态追踪铁律(Settler):只提取正文中明确描写的事件和状态变化——不推断/不预测/不补充正文没写内容。正文只写到角色走到门口→状态卡不能写已进入房间。正文暗示可能性未确认→不当已发生事实记录。不删除或修改已有hooks中与本章无关内容——只更新本章正文涉及hooks。伏笔例外:正文中未解疑问/悬念/伏笔线索须在hooks中记录——这是提取叙事承诺不是推断。结算输出:RUNTIME_STATE_DELTA(JSON增量不重写完整truth files)——含currentStatePatch+hookOps(upsert/mention/resolve/defer)+newHookCandidates+chapterSummary。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "7ab3f95f6853",
+        "template": "钩子推进判定(严格执行):只有状态真的变了才更新最近推进。不要把'再次提到''换个说法重述''抽象复盘'当推进。判定标准:旧钩子本章出现新事实/新证据/新关系变化/风险升级/范围收缩→推进更新lastAdvancedChapter=当前章号。旧钩子本章只是被提到无新增信息→mention不更新最近推进。旧钩子本章被明确揭示/解决/不再成立→回收状态改已回收。旧钩子被主动搁置/转入后台/被剧情压后→延后标注原因。payoffTiming用语义节奏不用硬写章号:immediate/near-term/mid-arc/slow-burn/endgame。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "e39c81d20819",
+        "template": "长跨度疲劳检测:章节类型单调——连续5+章同一类型→警告建议轮换铺垫-兑现-反转-后效。情绪单调——连续高张力无释放→警告插入释放节拍(温情/幽默/亲密/反思安静)再升级。标题塌缩——近3章标题共享相同命名壳→警告换新意象/动作/后果/角色向量。开头/结尾句式重复——最近章首句或末句相似度过高→警告从动作/后果/意外开局不用相同镜头调度。",
+        "source": "seed",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": []
+      },
+      {
+        "id": "6ca2341154af",
+        "template": "钩子准入控制:不重复——指向同一事件合并不新增;",
+        "source": "simplify",
+        "scores": {
+          "composite": 0.0
+        },
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "b8635d3d6e6a"
+        ]
+      },
+      {
+        "id": "1721fac5c5ba",
+        "template": "悬念健康分析:stale(>10章无推进)→立即标记决定推进/废弃/合并。burst(单章>5个新钩子)→警告削减低优先级。no-advance(high钩子>5章无推进)→下章安排推进。payoff逾期(计划兑现章已过未标记)→确认断头钩子。核心钩子(core_hook=true)stale超10章→warning升级critical——全书只有3-7条核心钩子,任其漂移是叙事腐烂先行指标。卷末核心钩子仍open/stale无显式转入下卷规划→critical。",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "f8f652e81111"
+        ]
+      },
+      {
+        "id": "c39b6a9d053a",
+        "template": "注意:钩子健康分析:stale(>10章无推进)→立即标记决定推进/废弃/合并。burst(单章>5个新钩子)→警告削减低优先级。no-advance(high钩子>5章无推进)→下章安排推进。payoff逾期(计划兑现章已过未标记)→确认断头钩子。核心钩子(core_hook=true)stale超10章→warning升级critical——全书只有3-7条核心钩子,任其漂移是叙事腐烂先行指标。卷末核心钩子仍open/stale无显式转入下卷规划→critical。",
+        "source": "mutate",
+        "scores": {},
+        "trials": 0,
+        "trials_per_dim": {},
+        "lineage": [
+          "f8f652e81111"
+        ]
+      }
+    ]
+  }
+}

+ 39 - 0
evolution/thresholds.json

@@ -0,0 +1,39 @@
+{
+  "pass": 85,
+  "revise_low": 60,
+  "rewrite_low": 40,
+  "max_revision_rounds": 3,
+  "weights": {
+    "P1": 0.24999999999999994,
+    "P2": 0.25,
+    "P3": 0.35,
+    "P4": 0.15,
+    "P5": 2.7755575615628914e-17
+  },
+  "stale_hook_limit": 10,
+  "burst_hook_limit": 5,
+  "no_advance_limit": 5,
+  "payoff_interval_min": 300,
+  "payoff_interval_max": 500,
+  "weak_hook_consecutive_max": 3,
+  "_inkos_derived": {
+    "PASS_SCORE_THRESHOLD": 85,
+    "MAX_REVIEW_ITERATIONS": 3,
+    "NET_IMPROVEMENT_EPSILON": 3,
+    "FOUNDATION_PASS": 80,
+    "DIMENSION_FLOOR": 60,
+    "GOLDEN_CHAPTER_COUNT": 3,
+    "ACTIVE_HOOK_CAP": 12,
+    "CORE_HOOK_CAP": 5,
+    "DENSITY_PAYOFF_EVERY": 300,
+    "DENSITY_HOOK_EVERY": 500,
+    "DENSITY_SUSPENSE_EVERY": 1000,
+    "FATIGUE_MARKER_LIMIT_PER_3000": 1,
+    "PARAGRAPH_TARGET_MIN": 40,
+    "PARAGRAPH_TARGET_MAX": 120,
+    "SHORT_PARAGRAPH_THRESHOLD": 40,
+    "LONG_PARAGRAPH_THRESHOLD": 300,
+    "MAX_SHORT_PARAGRAPH_RATIO": 0.6,
+    "CONSECUTIVE_SHORT_MAX": 3
+  }
+}

+ 332 - 0
scripts/event_store.py

@@ -0,0 +1,332 @@
+#!/usr/bin/env python3
+"""
+event_store.py — 事件溯源引擎(Event Sourcing)
+
+解决多用户 git 共享进化数据的冲突问题:
+- 每个用户写入不可变事件日志文件(追加写入,天然无冲突)
+- population.json / thresholds.json 从事件日志编译生成(可复现)
+- git pull → 获得他人事件 → recompile → 最新状态
+
+事件文件格式:
+  evolution/genres/{genre_id}/events/{timestamp}_{user}.json
+  内容: [event, event, ...]
+
+事件类型:
+  - feedback: 章节反馈(多维评分写入)
+  - mutate: 变异产生新变体 / 移除旧变体
+  - extinction: 灭绝重播
+  - threshold_adjust: 阈值调优
+"""
+
+import json, os, re, hashlib, subprocess
+from pathlib import Path
+from datetime import datetime
+from typing import Any
+
+import sys
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from utils import (
+    get_evolution_dir, get_genre_evolution_dir,
+    load_genre_evolution, save_genre_evolution,
+    load_evolution, save_evolution,
+)
+
+
+def _get_user() -> str:
+    """获取当前用户标识(git config → 系统用户名 → hostname)"""
+    try:
+        result = subprocess.run(
+            ["git", "config", "user.name"], capture_output=True, text=True
+        )
+        if result.returncode == 0 and result.stdout.strip():
+            return result.stdout.strip()
+    except Exception:
+        pass
+    try:
+        result = subprocess.run(
+            ["git", "config", "user.email"], capture_output=True, text=True
+        )
+        if result.returncode == 0 and result.stdout.strip():
+            return result.stdout.strip().split("@")[0]
+    except Exception:
+        pass
+    return os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
+
+
+def _events_dir(genre_id: str) -> Path:
+    d = get_genre_evolution_dir(genre_id) / "events"
+    d.mkdir(parents=True, exist_ok=True)
+    return d
+
+
+def _event_filename(user: str | None = None) -> str:
+    """生成事件文件名: {timestamp}_{user}.json"""
+    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
+    u = user or _get_user()
+    return f"{ts}_{u}.json"
+
+
+def write_events(genre_id: str, events: list[dict],
+                 user: str | None = None) -> Path:
+    """追加写入事件文件(一次会话一个文件,追加写入天然无冲突)。
+
+    返回写入的文件路径。
+    """
+    if not events:
+        return None
+
+    u = user or _get_user()
+    fname = _event_filename(u)
+    fpath = _events_dir(genre_id) / fname
+
+    with open(fpath, "w", encoding="utf-8") as f:
+        json.dump({
+            "user": u,
+            "timestamp": datetime.now().isoformat(),
+            "events": events,
+        }, f, ensure_ascii=False, indent=2)
+
+    return fpath
+
+
+def read_all_events(genre_id: str) -> list[dict]:
+    """按时间顺序读取所有事件文件,返回展开后的事件列表。"""
+    d = _events_dir(genre_id)
+    if not d.exists():
+        return []
+
+    all_events = []
+    for fpath in sorted(d.glob("*.json")):
+        try:
+            data = json.loads(fpath.read_text(encoding="utf-8"))
+            for evt in data.get("events", []):
+                evt["_source_user"] = data.get("user", "unknown")
+                evt["_source_file"] = fpath.name
+                all_events.append(evt)
+        except (json.JSONDecodeError, KeyError):
+            continue
+
+    return all_events
+
+
+def compile_population(genre_id: str) -> dict:
+    """从事件日志编译 population.json。
+
+    回放所有事件,重建 5 岛种群状态。
+    """
+    from prompt_evolver import PromptVariant, PromptPopulation, IslandModel
+
+    events = read_all_events(genre_id)
+
+    # 从种子初始化
+    from seed_extractor import SeedExtractor
+    extractor = SeedExtractor()
+    seeds = extractor.extract_seeds(genre_id=genre_id)
+
+    model = IslandModel()
+    model.initialize(seeds)
+    for cat, pop in model.islands.items():
+        pop.generation = 0
+
+    gen_counters: dict[str, int] = {}
+
+    for evt in events:
+        etype = evt.get("type", "")
+
+        if etype == "feedback":
+            island = evt.get("island", "writing")
+            variant_id = evt.get("variant_id", "")
+            scores = evt.get("scores", {})
+            variant_template = evt.get("variant_template", "")
+
+            if island in model.islands:
+                pop = model.islands[island]
+                target = None
+                for v in pop.variants:
+                    if v.id == variant_id:
+                        target = v
+                        break
+
+                # 变体不存在 → 从事件携带的模板文本重建
+                if target is None and variant_template:
+                    from prompt_evolver import PromptVariant
+                    target = PromptVariant(variant_template, source="event:feedback")
+                    target.id = variant_id
+                    pop.add(target)
+
+                if target:
+                    for dim, score in scores.items():
+                        dim_key = dim.upper()
+                        count = target.trials_per_dim.get(dim_key, 0)
+                        if count > 0:
+                            target.scores[dim_key] = (
+                                target.scores.get(dim_key, 0) * count + score
+                            ) / (count + 1)
+                        else:
+                            target.scores[dim_key] = float(score)
+                        target.trials_per_dim[dim_key] = count + 1
+                    target.trials += 1
+
+        elif etype == "mutate":
+            island = evt.get("island", "")
+            new_variants = evt.get("new_variants", [])
+            removed_ids = set(evt.get("removed_variants", []))
+            if island in model.islands:
+                pop = model.islands[island]
+                # Remove old variants
+                pop.variants = [v for v in pop.variants if v.id not in removed_ids]
+                # Add new ones
+                for vd in new_variants:
+                    v = PromptVariant.from_dict(vd)
+                    pop.add(v)
+                gen_counters[island] = gen_counters.get(island, 0) + 1
+                pop.generation = gen_counters[island]
+
+        elif etype == "extinction":
+            island = evt.get("island", "")
+            removed_ids = set(evt.get("removed_variants", []))
+            added = evt.get("added_variants", [])
+            if island in model.islands:
+                pop = model.islands[island]
+                pop.variants = [v for v in pop.variants if v.id not in removed_ids]
+                for vd in added:
+                    v = PromptVariant.from_dict(vd)
+                    pop.add(v)
+
+        elif etype == "threshold_adjust":
+            # Handled separately — see compile_thresholds
+            pass
+
+    return {
+        cat: {
+            "generation": pop.generation,
+            "variants": [v.to_dict() for v in pop.variants],
+        }
+        for cat, pop in model.islands.items()
+    }
+
+
+def compile_thresholds(genre_id: str) -> dict:
+    """从事件日志编译当前阈值(应用最后一次 threshold_adjust 事件)。"""
+    events = read_all_events(genre_id)
+    # 从默认阈值开始
+    default = load_genre_evolution(genre_id, "thresholds.json")
+    if not default:
+        default = load_evolution("thresholds.json") or {
+            "pass": 85, "revise_low": 60, "rewrite_low": 40,
+            "max_revision_rounds": 3,
+            "weights": {"P1": 0.35, "P2": 0.25, "P3": 0.15, "P4": 0.15, "P5": 0.10},
+        }
+
+    current = dict(default)
+    for evt in events:
+        if evt.get("type") == "threshold_adjust":
+            changes = evt.get("changes", {})
+            for k, v in changes.items():
+                if k == "weights" and isinstance(v, dict):
+                    current.setdefault("weights", {}).update(v)
+                else:
+                    current[k] = v
+
+    return current
+
+
+def compile_all(genre_id: str) -> dict:
+    """编译题材的全部进化数据并写入文件。
+
+    返回: {"population": ..., "thresholds": ..., "event_count": N}
+    """
+    pop = compile_population(genre_id)
+    thresh = compile_thresholds(genre_id)
+
+    save_genre_evolution(genre_id, "prompts/population.json", pop)
+    save_genre_evolution(genre_id, "thresholds.json", thresh)
+
+    # 同步 current_best
+    best = {}
+    for cat, pop_data in pop.items():
+        variants = [v for v in pop_data.get("variants", []) if v.get("scores")]
+        variants.sort(key=lambda v: sum(v["scores"].values()) / max(len(v["scores"]), 1), reverse=True)
+        if len(variants) < 3:
+            unscored = [v for v in pop_data.get("variants", []) if not v.get("scores")]
+            for v in unscored:
+                if v not in variants:
+                    variants.append(v)
+                if len(variants) >= 3:
+                    break
+        best[cat] = [v["template"] for v in variants[:3]]
+
+    save_genre_evolution(genre_id, "prompts/current_best.json", best)
+
+    events = read_all_events(genre_id)
+    print(f"  ✓ 编译完成: {len(events)} 条事件 → evolution/genres/{genre_id}/")
+    print(f"    种群: {sum(len(p['variants']) for p in pop.values())} 变体")
+    print(f"    阈值: pass={thresh.get('pass')}")
+
+    return {
+        "population": pop,
+        "thresholds": thresh,
+        "current_best": best,
+        "event_count": len(events),
+    }
+
+
+def list_event_summary(genre_id: str) -> list[dict]:
+    """列出事件摘要(不展开所有事件细节)。"""
+    d = _events_dir(genre_id)
+    if not d.exists():
+        return []
+
+    summaries = []
+    for fpath in sorted(d.glob("*.json")):
+        try:
+            data = json.loads(fpath.read_text(encoding="utf-8"))
+            summaries.append({
+                "file": fpath.name,
+                "user": data.get("user", "?"),
+                "timestamp": data.get("timestamp", ""),
+                "event_types": list(set(
+                    e.get("type", "?") for e in data.get("events", [])
+                )),
+                "event_count": len(data.get("events", [])),
+            })
+        except Exception:
+            summaries.append({"file": fpath.name, "error": "parse failed"})
+
+    return summaries
+
+
+# ── CLI ─────────────────────────────────────────────────
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description="事件溯源引擎")
+    parser.add_argument("action", choices=["compile", "list", "write"],
+                        help="compile=编译全部 | list=列出事件 | write=写入事件")
+    parser.add_argument("--genre", "-g", type=str, required=True)
+    parser.add_argument("--events", "-e", type=str, default=None,
+                        help="JSON 事件列表(write 时使用)")
+    args = parser.parse_args()
+
+    if args.action == "compile":
+        compile_all(args.genre)
+
+    elif args.action == "list":
+        summaries = list_event_summary(args.genre)
+        if not summaries:
+            print("  暂无事件")
+        else:
+            for s in summaries:
+                print(f"  {s['file']} | {s['user']} | {s['event_count']} events: {s['event_types']}")
+
+    elif args.action == "write":
+        if not args.events:
+            print("错误: 需要 --events JSON")
+            sys.exit(1)
+        try:
+            events = json.loads(args.events)
+        except json.JSONDecodeError:
+            print("错误: --events 需要 JSON 数组")
+            sys.exit(1)
+        fpath = write_events(args.genre, events)
+        print(f"  ✓ 写入 {len(events)} 条事件 → {fpath}")

+ 312 - 0
scripts/evolve.sh

@@ -0,0 +1,312 @@
+#!/usr/bin/env bash
+# ═══════════════════════════════════════════════════════════════
+# evolve.sh — novel-write 自优化编排器
+# 进化结果写入 evolution/ 文件夹,不修改 SKILL.md
+#
+# 用法:
+#   ./evolve.sh              # 自动检测项目,执行进化
+#   ./evolve.sh --project /path/to/novel   # 指定项目
+#   ./evolve.sh --quick       # 快速模式(写后自动调用)
+#   ./evolve.sh --full        # 完整进化(每卷/每10章)
+#   ./evolve.sh --sync        # 将项目最优结果同步到 evolution/
+#   ./evolve.sh --report      # 输出状态报告
+# ═══════════════════════════════════════════════════════════════
+
+set -eo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SKILL_DIR="$(dirname "$SCRIPT_DIR")"
+EVOLUTION_DIR="$SKILL_DIR/evolution"
+
+PYTHON="${PYTHON:-python3}"
+export PYTHONPATH="$SCRIPT_DIR:$PYTHONPATH"
+
+# 颜色
+B="\033[0;34m" G="\033[0;32m" Y="\033[1;33m" C="\033[0;36m" N="\033[0m"
+
+log()  { echo -e "${B}[evolve]${N} $*"; }
+ok()   { echo -e "${G}[evolve]${N} ✓ $*"; }
+warn() { echo -e "${Y}[evolve]${N} ⚠ $*"; }
+
+banner() {
+    echo -e "${C}┌─────────────────────────────────┐"
+    echo -e "│  novel-write 自优化引擎          │"
+    echo -e "│  输出: evolution/ (不修改 SKILL.md) │"
+    echo -e "└─────────────────────────────────┘${N}"
+}
+
+# ── 查找项目 ──────────────────────────────────────────
+
+find_project() {
+    local target="${1:-.}"
+    # 从当前目录向上找
+    local d
+    d="$(cd "$target" 2>/dev/null && pwd || echo "$target")"
+    while [[ "$d" != "/" && "$d" != "." ]]; do
+        if [[ -f "$d/项目铁律.md" ]]; then
+            echo "$d"
+            return 0
+        fi
+        d="$(dirname "$d")"
+    done
+    return 1
+}
+
+# ── 阶段实现 ──────────────────────────────────────────
+
+phase_collect() {
+    local project="$1"
+    log "收集: $(basename "$project")"
+
+    local chapters
+    chapters=$(find "$project/正文" -name "*第*章*.md" 2>/dev/null | wc -l)
+    local has_hooks="✗"
+    local has_quality="✗"
+    local has_evolution="✗"
+    [[ -f "$project/追踪/钩子账本.md" ]] && has_hooks="✓"
+    [[ -f "$project/追踪/优化日志.md" ]] && has_quality="✓"
+    [[ -d "$project/evolution" ]] && has_evolution="✓"
+
+    log "  章节: $chapters | 钩子: $has_hooks | 质检: $has_quality | 进化: $has_evolution"
+    mkdir -p "$project/追踪" "$EVOLUTION_DIR"/{prompts,patterns}
+
+    # 检测题材,确保题材级进化目录存在
+    local genre_id
+    genre_id=$($PYTHON -c "
+import sys; sys.path.insert(0, '$SCRIPT_DIR')
+from utils import find_project_genre, map_genre_to_inkos_id
+from pathlib import Path
+print(map_genre_to_inkos_id(find_project_genre(Path('$project'))))
+" 2>/dev/null || echo "other")
+    log "  题材: $genre_id"
+
+    # 首次使用:初始化题材进化目录
+    if [[ ! -d "$EVOLUTION_DIR/genres/$genre_id" ]]; then
+        log "  初始化题材进化目录: evolution/genres/$genre_id/"
+        $PYTHON -c "
+import sys; sys.path.insert(0, '$SCRIPT_DIR')
+from seed_extractor import SeedExtractor
+SeedExtractor().initialize_genre('$genre_id')
+" 2>/dev/null || true
+    fi
+    return 0
+}
+
+phase_analyze() {
+    local project="$1"
+    log "分析..."
+
+    # 质量阈值分析(有质检数据时)
+    if [[ -f "$project/追踪/优化日志.md" ]]; then
+        $PYTHON "$SCRIPT_DIR/quality_tuner.py" analyze --project "$project" 2>/dev/null || true
+    fi
+
+    # 写作模式挖掘(有章节时)
+    if [[ -d "$project/正文" ]] && find "$project/正文" -name "第*章*.md" 2>/dev/null | grep -q .; then
+        $PYTHON "$SCRIPT_DIR/pattern_mine.py" mine --project "$project" --min-score 65 2>/dev/null || true
+    fi
+
+    ok "分析完成"
+}
+
+phase_mutate() {
+    local project="${1:-}"
+    log "变异: 提示词进化..."
+
+    if [[ -n "$project" ]]; then
+        $PYTHON "$SCRIPT_DIR/prompt_evolver.py" evolve --project "$project" \
+            --generations "${GENERATIONS:-2}" 2>/dev/null || true
+    else
+        $PYTHON "$SCRIPT_DIR/prompt_evolver.py" evolve --generations "${GENERATIONS:-2}" 2>/dev/null || true
+    fi
+
+    ok "变异完成"
+}
+
+phase_report() {
+    local project="${1:-}"
+    echo ""
+    echo -e "${C}═══ 自优化状态 ═══${N}"
+    echo ""
+
+    # 阈值
+    echo "## 质量阈值 (evolution/thresholds.json)"
+    $PYTHON "$SCRIPT_DIR/quality_tuner.py" show 2>/dev/null || echo "  使用默认阈值"
+
+    # 题材级
+    if [[ -n "$project" ]]; then
+        local genre_id
+        genre_id=$($PYTHON -c "
+import sys; sys.path.insert(0, '$SCRIPT_DIR')
+from utils import find_project_genre, map_genre_to_inkos_id
+from pathlib import Path
+print(map_genre_to_inkos_id(find_project_genre(Path('$project'))))
+" 2>/dev/null || echo "")
+        if [[ -n "$genre_id" ]] && [[ -d "$EVOLUTION_DIR/genres/$genre_id" ]]; then
+            echo ""
+            echo -e "${C}## 题材级进化 (evolution/genres/$genre_id/)${N}"
+            echo "  目录: $EVOLUTION_DIR/genres/$genre_id/"
+            ls -la "$EVOLUTION_DIR/genres/$genre_id"/*.json 2>/dev/null | awk '{print "    " $NF}'
+            echo ""
+            echo "  最优提示词:"
+            $PYTHON "$SCRIPT_DIR/prompt_evolver.py" show --genre "$genre_id" 2>/dev/null || true
+        fi
+    fi
+
+    # 提示词(skill级)
+    echo ""
+    echo "## 最优提示词 (evolution/prompts/current_best.json)"
+    $PYTHON "$SCRIPT_DIR/prompt_evolver.py" show 2>/dev/null || echo "  使用种子提示词"
+
+    # 模式库
+    echo ""
+    echo "## 写作模式库 (evolution/patterns/pattern_library.json)"
+    $PYTHON "$SCRIPT_DIR/pattern_mine.py" show --top 5 2>/dev/null || echo "  暂无模式数据"
+
+    # 会话状态
+    echo ""
+    echo "## 会话状态 (evolution/session_state.json)"
+    if [[ -f "$EVOLUTION_DIR/session_state.json" ]]; then
+        $PYTHON -c "
+import json
+state = json.loads(open('$EVOLUTION_DIR/session_state.json').read())
+print(f'  反馈数: {state.get(\"total_feedback_count\", 0)}')
+recent = state.get('recent_chapters', [])
+if recent:
+    scores = [r['composite'] for r in recent[-5:]]
+    print(f'  最近5章均分: {sum(scores)/len(scores):.0f}')
+" 2>/dev/null || true
+    else
+        echo "  暂无"
+    fi
+}
+
+phase_sync() {
+    local project="${1:-}"
+    log "同步最优结果到 evolution/..."
+
+    # 提示词进化 + 同步
+    $PYTHON "$SCRIPT_DIR/prompt_evolver.py" evolve --generations 1 2>/dev/null || true
+
+    # 质量阈值同步
+    if [[ -n "$project" ]] && [[ -f "$project/追踪/优化日志.md" ]]; then
+        $PYTHON "$SCRIPT_DIR/quality_tuner.py" analyze --project "$project" 2>/dev/null || true
+    fi
+
+    # 模式库同步
+    if [[ -n "$project" ]] && [[ -d "$project/正文" ]]; then
+        $PYTHON "$SCRIPT_DIR/pattern_mine.py" mine --project "$project" 2>/dev/null || true
+    fi
+
+    ok "同步完成 → evolution/"
+}
+
+# ── 主流程 ─────────────────────────────────────────────
+
+main() {
+    banner
+
+    local mode="${1:---quick}"
+    local project=""
+
+    # 解析参数
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --project|-p) project="$2"; shift 2 ;;
+            --quick) mode="--quick"; shift ;;
+            --full) mode="--full"; shift ;;
+            --sync) mode="--sync"; shift ;;
+            --report) mode="--report"; shift ;;
+            --compile) mode="--compile"; shift ;;
+            *) shift ;;
+        esac
+    done
+
+    # 查找项目
+    if [[ -z "$project" ]]; then
+        project="$(find_project "." 2>/dev/null || true)"
+    fi
+
+    case "$mode" in
+        --quick)
+            if [[ -n "$project" ]]; then
+                phase_collect "$project"
+                phase_analyze "$project"
+                phase_mutate "$project"
+            fi
+            phase_sync "$project"
+            ;;
+
+        --full)
+            if [[ -z "$project" ]]; then
+                warn "未找到小说项目。仅执行 skill 级进化。"
+                phase_mutate ""
+            else
+                phase_collect "$project"
+                phase_analyze "$project"
+                phase_mutate "$project"
+                phase_sync "$project"
+            fi
+            log "完整进化完成。下次建议: 每写完一卷(或每10章)执行。"
+            ;;
+
+        --sync)
+            phase_sync "$project"
+            ;;
+
+        --report)
+            phase_report
+            ;;
+
+        --compile)
+            log "编译: 从事件日志重建进化状态..."
+            if [[ -n "$project" ]]; then
+                local genre_id
+                genre_id=$($PYTHON -c "
+import sys; sys.path.insert(0, '$SCRIPT_DIR')
+from utils import find_project_genre, map_genre_to_inkos_id
+from pathlib import Path
+print(map_genre_to_inkos_id(find_project_genre(Path('$project'))))
+" 2>/dev/null || echo "")
+                if [[ -n "$genre_id" ]]; then
+                    $PYTHON "$SCRIPT_DIR/event_store.py" compile --genre "$genre_id"
+                fi
+            fi
+            echo ""
+            echo "  提示: git pull 后执行此命令即可获得最新共享状态。"
+            ;;
+
+        *)
+            # 默认:quick 模式
+            if [[ -n "$project" ]]; then
+                phase_collect "$project"
+                phase_analyze "$project"
+                phase_mutate "$project"
+            fi
+            phase_sync "$project"
+            ;;
+    esac
+
+    ok "自优化完成"
+    echo ""
+    echo "  evolution/ 目录内容 (skill级):"
+    ls -la "$EVOLUTION_DIR"/*.{json,md} 2>/dev/null | awk '{print "    " $NF}' || echo "    (空)"
+
+    if [[ -n "$project" ]]; then
+        local genre_id
+        genre_id=$($PYTHON -c "
+import sys; sys.path.insert(0, '$SCRIPT_DIR')
+from utils import find_project_genre, map_genre_to_inkos_id
+from pathlib import Path
+print(map_genre_to_inkos_id(find_project_genre(Path('$project'))))
+" 2>/dev/null || echo "")
+        if [[ -n "$genre_id" ]] && [[ -d "$EVOLUTION_DIR/genres/$genre_id" ]]; then
+            echo ""
+            echo "  evolution/ 目录内容 (题材级: $genre_id):"
+            find "$EVOLUTION_DIR/genres/$genre_id" -name "*.json" 2>/dev/null \
+                | sed "s|$EVOLUTION_DIR/genres/$genre_id/|    |" || echo "    (空)"
+        fi
+    fi
+}
+
+main "$@"

+ 191 - 0
scripts/feedback_collector.py

@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+"""
+feedback_collector.py — 结构化反馈收集器
+反馈数据写入 evolution/session_state.json 和项目数据库。
+"""
+
+import sys, os, re, json
+from pathlib import Path
+from datetime import datetime
+from collections import Counter
+from typing import Any
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from utils import (
+    find_project_root, get_evolution_dir,
+    load_evolution, save_evolution, load_project_db, save_project_db,
+    log_evolution_event,
+)
+
+
+def collect_chapter_feedback(project: Path, chapter_num: int,
+                              scores: dict[str, int],
+                              prompt_used: str = "",
+                              issues: list[str] | None = None,
+                              notes: str = "") -> dict:
+    db = load_project_db(project)
+    entry = {
+        "timestamp": datetime.now().isoformat(),
+        "chapter": chapter_num,
+        "scores": scores,
+        "composite": sum(scores.values()) / len(scores) if scores else 0,
+        "prompt_used_hash": hash(prompt_used) % 100000 if prompt_used else None,
+        "prompt_used": prompt_used[:200] if prompt_used else "",
+        "issues": issues or [],
+        "notes": notes,
+    }
+    db.setdefault("feedback", []).append(entry)
+    db.setdefault("meta", {}).setdefault("quality_scores", {})[str(chapter_num)] = entry["composite"]
+    save_project_db(project, db)
+
+    # 更新会话状态
+    state = load_evolution("session_state.json") or {}
+    state.setdefault("recent_chapters", []).append({
+        "chapter": chapter_num,
+        "composite": entry["composite"],
+        "timestamp": entry["timestamp"],
+    })
+    # 只保留最近 20 章
+    state["recent_chapters"] = state["recent_chapters"][-20:]
+    state["total_feedback_count"] = state.get("total_feedback_count", 0) + 1
+    save_evolution("session_state.json", state)
+
+    # 写入事件日志(Event Sourcing — git 共享无冲突)
+    if prompt_used:
+        _write_feedback_event(project, chapter_num, scores, prompt_used)
+
+    log_evolution_event(project, "章节反馈收集", {
+        "章节": chapter_num,
+        "综合评分": f"{entry['composite']:.0f}",
+        "各维评分": ", ".join(f"{k}:{v}" for k, v in scores.items()),
+        "问题数": len(issues or []),
+    })
+    return entry
+
+
+def _write_feedback_event(project: Path, chapter_num: int,
+                          scores: dict[str, int],
+                          prompt_used: str) -> None:
+    """将反馈写入事件日志(追加写入,天然无 git 冲突)。
+
+    不再直接修改 population.json —— 改为写入不可变事件文件。
+    population.json 通过 `evolve.sh --compile` 从事件编译生成。
+    """
+    from hashlib import md5 as _md5
+    from utils import (find_project_genre, map_genre_to_inkos_id,
+                       load_genre_evolution)
+    from event_store import write_events
+
+    genre_name = find_project_genre(project)
+    genre_id = map_genre_to_inkos_id(genre_name)
+
+    # 匹配变体:从当前编译后的 population 中查找
+    pop = load_genre_evolution(genre_id, "prompts/population.json")
+    prompt_hash = _md5(prompt_used.encode()).hexdigest()[:12]
+    prompt_key = prompt_used[:40] if len(prompt_used) > 40 else prompt_used
+
+    matched_island = "writing"
+    matched_id = prompt_hash
+
+    if pop:
+        for cat, pop_data in pop.items():
+            for variant in pop_data.get("variants", []):
+                tmpl = variant.get("template", "")
+                if (variant.get("id") == prompt_hash
+                        or (prompt_key and prompt_key in tmpl)
+                        or (prompt_used and len(tmpl) >= 40 and tmpl[:40] in prompt_used)):
+                    matched_island = cat
+                    matched_id = variant.get("id", prompt_hash)
+                    break
+            else:
+                continue
+            break
+
+    event = {
+        "type": "feedback",
+        "chapter": chapter_num,
+        "island": matched_island,
+        "variant_id": matched_id,
+        "variant_template": prompt_used,  # 携带模板文本,编译时可重建缺失变体
+        "scores": {k.upper(): v for k, v in scores.items()},
+    }
+    write_events(genre_id, [event])
+
+
+def parse_user_feedback(text: str) -> dict:
+    feedback = {"sentiment": "neutral", "mentioned_dimensions": [],
+                "mentioned_chapters": [], "action_items": []}
+
+    pos_words = ["好", "不错", "很好", "精彩", "满意", "perfect", "great"]
+    neg_words = ["差", "不好", "弱", "无聊", "平淡", "不行", "重写", "废弃"]
+    pos_count = sum(1 for w in pos_words if w in text)
+    neg_count = sum(1 for w in neg_words if w in text)
+    if pos_count > neg_count:
+        feedback["sentiment"] = "positive"
+    elif neg_count > pos_count:
+        feedback["sentiment"] = "negative"
+
+    dim_keywords = {
+        "结构": ["结构", "节奏", "场景", "布局"],
+        "角色": ["角色", "人物", "主角", "配角", "人设"],
+        "原创性": ["套路", "新鲜", "创新", "模板"],
+        "对话": ["对话", "对白", "台词"],
+        "文本": ["文字", "措辞", "句式", "描写"],
+        "钩子": ["钩子", "悬念", "伏笔", "期待"],
+        "爽点": ["爽点", "高潮", "打脸", "爽感"],
+        "情感": ["情感", "情绪", "心理"],
+    }
+    for dim, keywords in dim_keywords.items():
+        if any(kw in text for kw in keywords):
+            feedback["mentioned_dimensions"].append(dim)
+
+    chapter_refs = re.findall(r"第(\d+)章", text)
+    feedback["mentioned_chapters"] = [int(c) for c in chapter_refs]
+
+    for m in re.finditer(r"(?:需要|应该|必须|要)\s*(.+?)(?:[。!,,.]|$)", text):
+        feedback["action_items"].append({"type": "required", "content": m.group(1).strip()})
+
+    return feedback
+
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description="反馈收集器")
+    parser.add_argument("action", choices=["collect", "parse", "state"])
+    parser.add_argument("--project", "-p", default=".")
+    parser.add_argument("--chapter", "-c", type=int)
+    parser.add_argument("--scores", "-s", type=str, default="{}")
+    parser.add_argument("--prompt", type=str, default="")
+    parser.add_argument("--issues", "-i", nargs="*", default=[])
+    parser.add_argument("--notes", "-n", type=str, default="")
+    parser.add_argument("--text", "-t", type=str, default="")
+    args = parser.parse_args()
+
+    if args.action == "collect":
+        root = find_project_root(Path(args.project).resolve())
+        if not root:
+            print("错误: 未找到小说项目根目录")
+            sys.exit(1)
+        if not args.chapter:
+            print("错误: 需要 --chapter")
+            sys.exit(1)
+        try:
+            scores = json.loads(args.scores)
+        except json.JSONDecodeError:
+            print("错误: --scores 需要 JSON 格式")
+            sys.exit(1)
+        result = collect_chapter_feedback(root, args.chapter, scores,
+                                          prompt_used=args.prompt,
+                                          issues=args.issues, notes=args.notes)
+        print(f"✓ 章节 {args.chapter} 反馈已收集 (综合: {result['composite']:.0f})")
+
+    elif args.action == "parse":
+        if not args.text:
+            print("错误: 需要 --text")
+            sys.exit(1)
+        result = parse_user_feedback(args.text)
+        print(json.dumps(result, ensure_ascii=False, indent=2))
+
+    elif args.action == "state":
+        state = load_evolution("session_state.json") or {}
+        print(json.dumps(state, ensure_ascii=False, indent=2))

+ 237 - 0
scripts/pattern_mine.py

@@ -0,0 +1,237 @@
+#!/usr/bin/env python3
+"""
+pattern_mine.py — 写作模式挖掘器
+MAP-Elites 质量-多样性搜索,结果写入 evolution/patterns/。
+"""
+
+import sys, os, re, json, random, hashlib
+from pathlib import Path
+from collections import defaultdict
+from typing import Any
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from utils import (
+    find_project_root, get_evolution_dir,
+    load_evolution, save_evolution, parse_chapters,
+    log_evolution_event,
+)
+
+
+class WritingPattern:
+    def __init__(self, name: str, category: str, template: str,
+                 complexity: float = 0.5, diversity: float = 0.5, quality_score: float = 0.0):
+        self.name = name
+        self.category = category
+        self.template = template
+        self.complexity = complexity
+        self.diversity = diversity
+        self.quality_score = quality_score
+        self.usage_count = 0
+        self.success_count = 0
+        self.fingerprint = hashlib.md5(f"{category}:{template[:100]}".encode()).hexdigest()[:8]
+
+    def to_dict(self) -> dict:
+        return {"name": self.name, "category": self.category, "template": self.template,
+                "complexity": self.complexity, "diversity": self.diversity,
+                "quality_score": self.quality_score, "usage_count": self.usage_count,
+                "success_count": self.success_count, "fingerprint": self.fingerprint}
+
+    @classmethod
+    def from_dict(cls, d: dict) -> "WritingPattern":
+        p = cls(d["name"], d["category"], d["template"],
+                d.get("complexity", 0.5), d.get("diversity", 0.5), d.get("quality_score", 0.0))
+        p.usage_count = d.get("usage_count", 0)
+        p.success_count = d.get("success_count", 0)
+        p.fingerprint = d.get("fingerprint", p.fingerprint)
+        return p
+
+
+class PatternExtractor:
+    HOOK_PATTERNS = [
+        (r"就在这时[,,](.+?)(?:[。!?]|\n)", "危机降临"),
+        (r"他[忽然突然].*?发现(.+?)(?:[。!?]|\n)", "信息揭示"),
+        (r"(?:但是|然而|可是)(.+?)(?:[。!?]|\n)", "反转"),
+        (r'[\"\'""「].+?[\"\'""」]\s*$', "对话悬念"),
+        (r"(?:明天|明日|天亮|第二).*?(?:就会|将要|会)(.+?)", "时间压力"),
+        (r"(?:他|她).*?(?:决定|选择|下决心)(.+?)", "决定时刻"),
+        (r"(?:远处|门外|身后).*?(?:传来|响起)(.+?)", "威胁到来"),
+    ]
+
+    PAYOFF_PATTERNS = [
+        (r"(?:众人|所有人|在场).*?(?:震惊|愕然|呆住)", "打脸爽"),
+        (r"(?:突破|晋升|觉醒|进化|升级).*?(?:成功|完成)", "升级爽"),
+        (r"(?:获得|得到|继承|传承|吸收).+?(?:力量|能力|宝物)", "获得爽"),
+        (r"(?:守护|保护|挡住|拦下).+?(?:身后|背后)", "守护爽"),
+    ]
+
+    @classmethod
+    def extract_all(cls, text: str, chapter_num: int, score: float = 0) -> list[WritingPattern]:
+        patterns = []
+        text_clean = text.replace("\n", " ")
+        last_500 = text_clean[-500:]
+
+        for pattern_re, name in cls.HOOK_PATTERNS:
+            matches = re.findall(pattern_re, last_500)
+            if matches:
+                p = WritingPattern(
+                    name=f"钩子_{name}", category="hook", template=matches[0][:200],
+                    complexity=min(1.0, len(matches[0]) / 80.0),
+                    diversity=random.uniform(0.3, 0.9), quality_score=score,
+                )
+                p.usage_count = 1
+                p.success_count = 1 if score >= 75 else 0
+                patterns.append(p)
+
+        for pattern_re, name in cls.PAYOFF_PATTERNS:
+            matches = re.findall(pattern_re, text_clean)
+            if matches:
+                p = WritingPattern(
+                    name=f"爽点_{name}", category="payoff", template=matches[0][:200],
+                    complexity=min(1.0, len(matches) / 5.0),
+                    diversity=0.3 + (hash(name) % 70) / 100.0, quality_score=score,
+                )
+                p.usage_count = len(matches)
+                p.success_count = len(matches) if score >= 75 else 0
+                patterns.append(p)
+
+        return patterns
+
+
+class MAPElitesGrid:
+    def __init__(self, bins: int = 5):
+        self.bins = bins
+        self.grid: dict[tuple[int, int], list[WritingPattern]] = defaultdict(list)
+
+    def _bin_coord(self, complexity: float, diversity: float) -> tuple[int, int]:
+        c = min(self.bins - 1, int(complexity * self.bins))
+        d = min(self.bins - 1, int(diversity * self.bins))
+        return (c, d)
+
+    def insert(self, pattern: WritingPattern) -> bool:
+        coord = self._bin_coord(pattern.complexity, pattern.diversity)
+        for existing in self.grid[coord]:
+            if existing.fingerprint == pattern.fingerprint:
+                existing.usage_count += pattern.usage_count
+                existing.success_count += pattern.success_count
+                if pattern.quality_score > existing.quality_score:
+                    existing.quality_score = pattern.quality_score
+                    existing.template = pattern.template
+                return False
+        self.grid[coord].append(pattern)
+        return True
+
+    def get_elite(self, n: int = 3) -> list[WritingPattern]:
+        elites = []
+        for cell in self.grid.values():
+            if cell:
+                cell.sort(key=lambda p: p.quality_score, reverse=True)
+                elites.append(cell[0])
+        elites.sort(key=lambda p: p.quality_score, reverse=True)
+        return elites[:n]
+
+    def cell_count(self) -> int:
+        return sum(1 for cell in self.grid.values() if cell)
+
+
+# ── 主流程 ─────────────────────────────────────────────
+
+def mine_project(project: Path, min_score: int = 70, top_k: int = 20) -> dict:
+    print(f"⛏  挖掘项目: {project.name}")
+    chapters = parse_chapters(project)
+    if not chapters:
+        print("  ⚠ 未找到章节文件")
+        return {"status": "no_chapters"}
+
+    grid = MAPElitesGrid(bins=5)
+    total_patterns = 0
+    high_quality = 0
+
+    # 加载项目数据库获取质量评分
+    from utils import load_project_db
+    db = load_project_db(project)
+    quality_scores = db.get("meta", {}).get("quality_scores", {})
+
+    for ch_num, ch_path in sorted(chapters.items()):
+        try:
+            text = ch_path.read_text(encoding="utf-8")
+        except Exception:
+            continue
+        score = quality_scores.get(str(ch_num), 75)
+        patterns = PatternExtractor.extract_all(text, ch_num, score)
+        for p in patterns:
+            total_patterns += 1
+            if grid.insert(p) and p.quality_score >= min_score:
+                high_quality += 1
+
+    elites = grid.get_elite(top_k)
+
+    # 合并到 evolution/patterns/pattern_library.json
+    existing = load_evolution("patterns/pattern_library.json") or {"patterns": [], "grid_cells": 0}
+    all_patterns = {p.get("fingerprint", ""): p for p in existing.get("patterns", [])}
+    for p in elites:
+        fp = p.fingerprint
+        if fp not in all_patterns or p.quality_score > all_patterns[fp].get("quality_score", 0):
+            all_patterns[fp] = p.to_dict()
+
+    pattern_list = sorted(all_patterns.values(), key=lambda x: x.get("quality_score", 0), reverse=True)
+    # 保留前 50 个
+    pattern_list = pattern_list[:50]
+
+    output = {
+        "patterns": pattern_list,
+        "grid_cells": grid.cell_count(),
+        "last_mined_from": project.name,
+        "total_chapters": len(chapters),
+    }
+    save_evolution("patterns/pattern_library.json", output)
+
+    print(f"  ✓ 分析了 {len(chapters)} 章")
+    print(f"  ✓ 发现 {total_patterns} 个模式(高质量 {high_quality})")
+    print(f"  ✓ MAP-Elites 网格: {grid.cell_count()}/25 格")
+    print(f"  ✓ 模式库保有 {len(pattern_list)} 个模式")
+    print(f"  ✓ 已写入 evolution/patterns/pattern_library.json")
+
+    return {"status": "ok", "chapters": len(chapters),
+            "patterns_found": total_patterns, "library_size": len(pattern_list)}
+
+
+def show_patterns(category: str | None = None, top_n: int = 10) -> None:
+    data = load_evolution("patterns/pattern_library.json")
+    patterns = data.get("patterns", [])
+    if category:
+        patterns = [p for p in patterns if p.get("category") == category]
+    patterns.sort(key=lambda p: p.get("quality_score", 0), reverse=True)
+
+    print(f"\n═══ 写作模式库 (evolution/patterns/) ═══")
+    print(f"总计: {len(patterns)} 个模式\n")
+    for i, p in enumerate(patterns[:top_n]):
+        print(f"{'★' if p.get('quality_score', 0) >= 80 else '●'} "
+              f"[{p.get('category', '?')}] {p.get('name', '?')}")
+        print(f"  质量: {p.get('quality_score', 0):.0f} | "
+              f"complexity={p.get('complexity', 0):.2f} diversity={p.get('diversity', 0):.2f}")
+        preview = p.get("template", "")[:80].replace("\n", " ")
+        print(f"  {preview}...")
+        print()
+
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description="写作模式挖掘器")
+    parser.add_argument("action", choices=["mine", "show"],
+                        help="mine=挖掘 | show=展示")
+    parser.add_argument("--project", "-p", default=".")
+    parser.add_argument("--min-score", type=int, default=70)
+    parser.add_argument("--top", type=int, default=10)
+    parser.add_argument("--category", "-c",
+                        choices=["hook", "payoff", "dialogue", "pacing", "description", "emotion"])
+    args = parser.parse_args()
+
+    if args.action == "mine":
+        root = find_project_root(Path(args.project).resolve())
+        if not root:
+            print("错误: 未找到小说项目根目录")
+            sys.exit(1)
+        result = mine_project(root, min_score=args.min_score, top_k=args.top)
+        sys.exit(0 if result["status"] == "ok" else 1)
+    elif args.action == "show":
+        show_patterns(category=args.category, top_n=args.top)

+ 762 - 0
scripts/prompt_evolver.py

@@ -0,0 +1,762 @@
+#!/usr/bin/env python3
+"""
+prompt_evolver.py — 提示词模板进化器
+进化结果写入 evolution/prompts/,不修改 SKILL.md。
+
+核心机制:
+- 种群管理 + 双选择策略(Top + Diverse)
+- 遗传算子:交叉、突变、简化
+- 岛屿模型:5个独立种群,定期迁移
+- 进化结果同步到 evolution/prompts/current_best.json
+"""
+
+import sys, os, re, json, random, hashlib
+from pathlib import Path
+from typing import Any
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from utils import (
+    find_project_root, get_evolution_dir,
+    load_evolution, save_evolution, load_project_db, save_project_db,
+    log_evolution_event,
+)
+
+# ── 种子模板 ───────────────────────────────────────────
+
+SEED_TEMPLATES = {
+    "writing": [
+        "核心规则(写作时内化不列出):Show, don't tell——用细节堆出真实。价值观盐溶于汤——靠行为证明不靠口号。任何行动立于三条腿:过往经历+当前利益+性格底色。配角有自己账本和利益诉求。每章结尾必有小悬念或情绪缺口。全员智商在线。日常场景七成成后续伏笔。关系改变事件驱动。每一行字要么推动剧情要么塑造人物。",
+
+        "看点密集度(硬尺):每300字至少1个爽点(小看点/梗/反套路/暧昧台词/情绪拉扯)。每500字至少1个钩子(引发'接下来怎样'的小悬念)。每1000-1500字至少1个完整悬念(问题—蓄力—未解)。连续300字以上只是环境/回忆/议论/心理独白而无推进=水文须删改。叙事段≥40字(手机屏2行),目标40-120字。短段(<40字)只允许:开场反转金句/章末钩子/≤3个爆点短段,合计≤5个。禁止3+短段连排。",
+
+        "去AI味铁律:叙述者永不替读者下结论。禁止分析报告式语言入正文——'核心动机''信息边界''信息落差'等术语只用于PRE_WRITE_CHECK内部推理。转折/惊讶标记词(仿佛/忽然/竟/猛地/不禁/宛如)全篇≤每3000字1次。同一体感/意象禁止连续渲染超两轮。硬性禁令:全文禁止'不是…而是…'句式;全文禁止破折号'——'。反例→正例:✗'他感到非常愤怒'→✓'他捏碎了茶杯,滚烫茶水流过指缝';✗'然而事情没那么简单'→✓'哪有那么便宜的事'。",
+
+        "六步走人物心理分析:1.当前处境(面临什么局面?手上什么牌?)2.核心动机(最想要什么?最怕什么?)3.信息边界(知道什么?不知道什么?误判什么?)4.性格过滤(同样局面这角色怎么反应?)5.行为选择(基于以上四点做什么选择?)6.情绪外化(伴随什么情绪?什么身体语言?)。禁止跳过步骤直接写行为。人设防崩三问:他为什么这么做?符合之前人设吗?只看过前文的读者会觉得突兀吗?",
+
+        "读者心理学框架:期待管理——读者期待释放时适当延迟增强快感,即将失去耐心时立即给反馈。信息落差——让读者比角色多知道一点(紧张)或少知道一点(好奇)。情绪节拍——压制→释放→更大压制→更大释放,释放超读者预期。递进升级——层层加码,每次比上一次更过分。锚定效应——先给读者参照再展示主角表现。沉没成本——读者已投入的阅读时间是留存关键。",
+
+        "黄金三章纪律:第1章主角出场800字内触发主线冲突,前300字最后一句须戏剧性反转,场景≤2个冲突人物≤2个。第2章金手指/能力/系统必须'做出来'——一次具体使用事件+看得见的后果——不是'说出来'。第3章中段让主角下一个可量化短期目标浮上水面。贯穿三章:段落3-5行(手机阅读),动词压过形容词,每章结尾必有小钩子。",
+
+        "章节80/20断章:永远不要在一章里把本章故事讲完——主剧情写到80%,剩下20%留给下一章。章末必须断在action-climax一刻:主角刚放大招尚未见效/刚拔刀尚未落下——不给结果。章节结构优先于字数。不凑字数也不为卡字数提前把高潮讲完。",
+
+        "代入感六支柱:基础信息标签化(100字内谁/在哪/发生什么)。可视化熟悉感(消毒水味/地铁座椅凉/外卖塑料袋塑胶感)。共鸣两层(认知共鸣+情绪共鸣)。欲望两条腿(基础欲望+主动欲望)。五感钩子(每场景除视觉外1-2种感官)。人设核心标签+反差细节才活——冷面杀手偷偷喂流浪猫。",
+
+        "情感节点设计:关系发展必须事件驱动节点递进——设计3-5个关键事件(共同御敌/秘密分享/利益冲突/信任考验/牺牲妥协)。每个事件推进关系一个层级,禁止跨越式发展。情绪用场景传达:环境烘托+微动作替代直白抒情。强情绪升级法——坏事叠坏事:被骂→赶不上公交→手机掉了→直播课结束→包子噎住,每层比上一层过分。",
+
+        "配角设计方法论:配角必须有反击有自己算盘。主角强大在于压服聪明人不是碾压傻子。动机绑定主线。核心标签+反差细节=活人。通过事件立人设禁止外貌形容词堆砌。语言区分度——用词习惯/句子长短/口头禅/方言痕迹。拒绝集体反应——不写'众人齐声惊呼',挑1-2个角色写具体反应。",
+
+        "输出格式:=== PRE_WRITE_CHECK ===(Markdown表格:当前任务/读者在等什么/该兑现的暂不掀的/日常过渡承担任务/章尾必须发生的改变/不要做/上下文范围/当前锚点/待回收伏笔/本章冲突/章节类型/风险扫描)=== CHAPTER_TITLE ===(章节标题不含'第X章',避开近期重复标题词根)=== CHAPTER_CONTENT ===(正文目标字数区间)。只输出以上三个区块。",
+    ],
+    "revision": [
+        "修稿分流规则:PATCHES处理局部文字问题(措辞/对话/AI痕迹/小连续性错误)——每个PATCH引用原文段落给出替换文本,未涉及内容保持原样。REVISED_CONTENT处理全章级问题(字数压缩/结构重组/节奏重排/重大剧情偏离)——Critical包含字数或结构性问题时必须用REVISED_CONTENT。修稿原则:修根因不做表面润色;伏笔状态须与伏笔池同步;不改变剧情走向和核心冲突;保留原文语言风格节奏呼吸;情绪用动作外化;坏事叠坏事每层比上一层过分。",
+
+        "润色边界(硬约束):只改文字层——句式/段落/排版/用词/五感/对话自然度。禁止增删情节/改变人设/调整主线——这些归Reviewer。消灭六条文笔雷点:描写无效删到一笔带过;文笔华丽过度让文字服从情绪;文笔欠佳重写成通顺有画面句子;排版不规范统一手机阅读格式;AI味痕迹替换口语化表达或具体动作;群像脸谱化不写众人震惊。文字层硬规约:段落3-5行/段;句式多样化禁3句以上同结构;动词>形容词;每场景1-2种感官;对话符合身份情绪信息掌握;删除叙述者结论。修改后总长变化≤±15%。",
+
+        "字数治理:压缩时优先合并重复信息/删除无变化场景/精简过渡段落——如需删75%以上原文→拒绝标准化标记为需人工拆分。扩写时优先增加五感描写/角色内心反应/场景氛围——如扩展后信息密度显著下降→拒绝标准化标记为需合并。只修正一次不递归。保留关键标记/人物名/地点名/已有事实。不凭空新增子情节。",
+
+        "小目标周期修稿指引:如果本章应该是'后效'阶段但仍在加压→把最密集冲突段落改写为展示改变的段落(谁失去了什么/谁的态度变了/新常态是什么)。如果本章应该是'爆发'阶段但没有明确兑现→放大最接近回报的场景让承诺释放超预期。日常段落不服务主线→改写为'饵':加入指向未来的细节/暗示/角色反应。",
+    ],
+    "quality_check": [
+        "自修正循环(不可跳过门控):Loop最多5轮——执行五遍编辑评估→综合评分≥85通过→60-84针对性修订→40-59重写问题段落→<40废弃从细纲重新写。每轮记录本轮评分/发现问题(S1-S4分级)/修复内容/下一轮重点。反模式:评分≥85还在修=无限打磨者;只跑P1=跳过者;每轮同问题=失忆者需向上追溯。",
+
+        "五遍编辑评估(按顺序不可跳过):P1结构(35%)——场景目标→阻碍→变化?章节推动力?爽点释放时机?章末钩子拉力?节奏快慢交替?P2角色(25%)——弧线推进?行为一致性?关系变化?配角不只是工具人?换个名字还能认出谁在说话?P3原创性(15%)——反陈词滥调?反AI模式?至少一处读者没预料到?P4对话(15%)——声音辨识度?三维标准同时携带?潜台词?P5文本(10%)——句式变化?措辞精准?五感至少2-3种?综合≥85通过/60-84修订/40-59重写/<40废弃。P1不过不进P2。",
+
+        "37维结构审查(InkOS Continuity Auditor标准):OOC/时间线/设定冲突/战力崩坏/数值/伏笔/节奏/文风/信息越界/词汇疲劳/利益链断裂/年代考据/配角降智/配角工具人化/爽点虚化/台词失真/流水账/知识库污染/视角一致性/段落等长/套话密度/公式化转折/列表式结构/支线停滞/弧线平坦/节奏单调/敏感词/正传事件冲突/未来信息泄露/世界规则跨书一致性/番外伏笔隔离/读者期待管理/章节备忘偏离/角色还原度/世界规则遵守/关系动态/正典事件一致性。评分:95-100可发布;85-94小瑕疵流畅;75-84明显问题主干完整;65-74多处影响阅读;<65结构性崩溃。",
+
+        "写后确定性校验(零LLM成本规则):'不是…而是…'→error改用直述句;破折号'——'→error用逗号或句号;惊讶标记词全篇≤每3000字1次→warning;高疲劳词单章最多1次→warning;元叙事('到这里算是''接下来就是')→warning;分析报告术语入正文→error;章号引用('第X章')→error改自然表达;作者说教词('显然''毋庸置疑')→warning;集体震惊反应→warning改写1-2个具体角色;连续'了'字句≥6句→warning;长段落≥2段超300字→warning;段落过碎(60%以上<40字)→warning;连续短段≥3段→warning。",
+
+        "基础设定审核(5维度评分):1.核心冲突——是否有清晰且有足够张力的核心冲突支撑40章?2.开篇节奏——前5章能否形成翻页驱动力?3.世界一致性——世界观是否内洽且具体?4.角色区分度——主要角色声音和动机是否各不相同?5.节奏可行性——卷纲是否有足够变化不连续10章同节拍?80+通过可开写/60-79需修改/<60方向性错误需重新设计。",
+    ],
+    "outline": [
+        "大纲共创三层决策树:第一层全书结构——分几卷?每卷核心任务?每卷不可逆变化?全书最高潮在哪卷为什么不可替代?第二层卷纲细化——高潮事件?需要哪些铺垫(≥3个伏笔/线索)?几个高潮点分别在哪些章节?主角情感弧线从什么状态到什么状态?卷末钩子须全书级别——不是'下章会怎样'而是'整个故事走向要变了'。第三层细纲关键节点——前10章每章核心事件/章末钩子类型和期待感等级/爽点类型和铺垫起点/推进哪个伏笔种下什么新伏笔。关键章逐章追问,普通章确认核心推进和钩子级别。",
+
+        "架构师基础设定生成:产出散文密度基础设定——不是表格/schema/条目式bullet list。story_frame(4段散文):主题与基调→核心冲突与前台/后台双层故事→世界观底色(3-5条不可违反铁律+感官锚点)→终局方向+全书Objective。volume_map(5段散文+节奏原则尾段):各卷主题与情绪曲线→卷间钩子与回收承诺→各卷OKR(Objective+3 Key Results)→卷尾不可逆改变→节奏原则6条(高潮间距/喘息频率/钩子密度/信息释放节奏/爽点节奏/情感节点递进)。",
+
+        "章节备忘(chapter_memo)七段格式:## 当前任务→本章必须完成的具体动作,写作始终对齐。## 读者此刻在等什么→控制情绪缺口的制造/延迟/兑现程度。## 该兑现的/暂不掀的→本章必须兑现伏笔+必须压住不掀底牌。## 日常/过渡承担什么任务→非冲突段落功能映射(段落位置→承担功能)。## 关键抉择过三连问→关键选择检查(为何做?符合利益?符合人设?)。## 章尾必须发生的改变→1-3条(信息/关系/物理/权力)。## 本章hook账→硬对应:advance/resolve下每个hook_id须在正文里有具体可定位兑现段(具体物件/事件/信息+可观察动作或交谈),每兑现段≥60字。defer不用落open在章末安排自然引出新悬念。## 不要做→硬约束红线。",
+
+        "规划器14条原则:3-5章一个小目标周期(build-up→escalation→climax→aftermath完整循环)。主动塑造读者期待——信息落差+情绪缺口。万物皆饵——日常段埋伏笔/推关系/建反差。人设防崩——三连问每次写行为前必问。1主线+1支线——不超载,支线2-3章内与核心目标关联。爽点密集化——每300字至少1看点/500字1钩子/1000-1500字1完整悬念。高潮前足够铺垫——铺垫越长释放越爽。高潮后展示影响——紧接着1-2章写出改变。人物立体化——核心标签+反差细节。五感具体化——每场景至少1-2种感官。钩子承接——每章结尾必种新钩子或推进已有。钩子账本必结账——兑现移走/推进追加/新种写入/stale标记。圆心法同场多视角——同一事件不同角色视角呈现不同信息。揭1埋2——每揭示一个答案至少埋下两个新问题。",
+    ],
+    "hook_management": [
+        "伏笔全生命周期:plant(种植)——新伏笔分配唯一ID/标记类型(SUSPENSE/PROMISE/MYSTERY/CONFLICT/CHEKHOV)和优先级/标明计划兑现章。advance(推进)——虽未兑现但给出新线索新事实新风险升级或范围收缩,更新最近推进章号。payoff(兑现)——明确揭示或解决,状态改为已回收。defer(推迟)——主动搁置转入后台或被剧情压后。stale(过期)——超10章无推进决定推进/废弃/合并。每章前读钩子账本→列出高优活跃钩子→确认推进/兑现机会。每章后更新:兑现移走→推进追加→新种写入→检查stale。",
+
+        "钩子准入控制:不重复——指向同一事件合并不新增;同一钩子家族(多个钩子都指向主角身份秘密)合并为核心钩子+多条线索。不超量——每卷最多3-5个high优先级,活跃钩子总数≤12。有计划——无兑现计划的钩子不种,先想好怎么收再种。新钩子候选:正文中出现会延续到后续且有具体回收方向的未解问题才新增。不要为旧钩子换说法/重述/抽象总结再开新hook_id。提及vs推进:旧钩子只是被提到无新增信息→mention不更新最近推进;出现新事实/证据/关系变化/风险升级→必须更新最近推进=当前章号。",
+
+        "钩子健康分析:stale(>10章无推进)→立即标记决定推进/废弃/合并。burst(单章>5个新钩子)→警告削减低优先级。no-advance(high钩子>5章无推进)→下章安排推进。payoff逾期(计划兑现章已过未标记)→确认断头钩子。核心钩子(core_hook=true)stale超10章→warning升级critical——全书只有3-7条核心钩子,任其漂移是叙事腐烂先行指标。卷末核心钩子仍open/stale无显式转入下卷规划→critical。",
+
+        "状态追踪铁律(Settler):只提取正文中明确描写的事件和状态变化——不推断/不预测/不补充正文没写内容。正文只写到角色走到门口→状态卡不能写已进入房间。正文暗示可能性未确认→不当已发生事实记录。不删除或修改已有hooks中与本章无关内容——只更新本章正文涉及hooks。伏笔例外:正文中未解疑问/悬念/伏笔线索须在hooks中记录——这是提取叙事承诺不是推断。结算输出:RUNTIME_STATE_DELTA(JSON增量不重写完整truth files)——含currentStatePatch+hookOps(upsert/mention/resolve/defer)+newHookCandidates+chapterSummary。",
+
+        "钩子推进判定(严格执行):只有状态真的变了才更新最近推进。不要把'再次提到''换个说法重述''抽象复盘'当推进。判定标准:旧钩子本章出现新事实/新证据/新关系变化/风险升级/范围收缩→推进更新lastAdvancedChapter=当前章号。旧钩子本章只是被提到无新增信息→mention不更新最近推进。旧钩子本章被明确揭示/解决/不再成立→回收状态改已回收。旧钩子被主动搁置/转入后台/被剧情压后→延后标注原因。payoffTiming用语义节奏不用硬写章号:immediate/near-term/mid-arc/slow-burn/endgame。",
+
+        "长跨度疲劳检测:章节类型单调——连续5+章同一类型→警告建议轮换铺垫-兑现-反转-后效。情绪单调——连续高张力无释放→警告插入释放节拍(温情/幽默/亲密/反思安静)再升级。标题塌缩——近3章标题共享相同命名壳→警告换新意象/动作/后果/角色向量。开头/结尾句式重复——最近章首句或末句相似度过高→警告从动作/后果/意外开局不用相同镜头调度。",
+    ],
+}
+
+
+class PromptVariant:
+    """提示词变体 — 支持多维评分"""
+    def __init__(self, template: str, source: str = "manual",
+                 scores: dict[str, float] | None = None):
+        self.id = hashlib.md5(template.encode()).hexdigest()[:12]
+        self.template = template
+        self.source = source
+        self.scores: dict[str, float] = scores or {}
+        self.trials_per_dim: dict[str, int] = {}
+        self.trials = 0
+        self.lineage: list[str] = []
+
+    def composite_score(self, weights: dict[str, float] | None = None) -> float:
+        """按权重计算加权综合分"""
+        if not self.scores:
+            return 0.0
+        # 兼容旧格式:单字段 "composite"
+        if "composite" in self.scores and len(self.scores) == 1:
+            return self.scores["composite"]
+        if not weights:
+            weights = {"P1": 0.35, "P2": 0.25, "P3": 0.15, "P4": 0.15, "P5": 0.10}
+        total = 0.0
+        weight_sum = 0.0
+        for dim, w in weights.items():
+            if dim in self.scores:
+                total += self.scores[dim] * w
+                weight_sum += w
+        return total / weight_sum if weight_sum > 0 else 0.0
+
+    def to_dict(self) -> dict:
+        return {
+            "id": self.id, "template": self.template, "source": self.source,
+            "scores": self.scores, "trials": self.trials,
+            "trials_per_dim": self.trials_per_dim, "lineage": self.lineage,
+        }
+
+    @classmethod
+    def from_dict(cls, d: dict) -> "PromptVariant":
+        p = cls(d["template"], d.get("source", "manual"))
+        p.id = d.get("id", p.id)
+        p.scores = d.get("scores", {})
+        p.trials = d.get("trials", 0)
+        p.trials_per_dim = d.get("trials_per_dim", {})
+        p.lineage = d.get("lineage", [])
+        # 向后兼容: 旧格式 'score' 单字段 → composite
+        if not p.scores and "score" in d:
+            p.scores = {"composite": d["score"]}
+        return p
+
+
+class PromptPopulation:
+    """提示词种群 — 支持多维评分、停滞检测、灭绝重播"""
+
+    def __init__(self, category: str, max_size: int = 35):
+        self.category = category
+        self.max_size = max_size
+        self.variants: list[PromptVariant] = []
+        self.generation = 0
+        self._best_score_history: list[float] = []
+
+    def seed(self, templates: list[str]) -> None:
+        for t in templates:
+            self.variants.append(PromptVariant(t, source="seed"))
+
+    def select_top(self, n: int = 3, weights: dict[str, float] | None = None) -> list[PromptVariant]:
+        scored = [v for v in self.variants if v.trials > 0]
+        scored.sort(key=lambda v: v.composite_score(weights), reverse=True)
+        return scored[:n]
+
+    def select_diverse(self, n: int = 2) -> list[PromptVariant]:
+        if len(self.variants) <= n:
+            return self.variants[:]
+        by_len = sorted(self.variants, key=lambda v: len(v.template))
+        indices = [int(i * len(by_len) / (n + 1)) for i in range(1, n + 1)]
+        return [by_len[i] for i in indices if i < len(by_len)]
+
+    def add(self, variant: PromptVariant) -> bool:
+        for v in self.variants:
+            if v.id == variant.id:
+                # 合并评分(滑动平均)
+                for dim, score in variant.scores.items():
+                    old = v.scores.get(dim, 0)
+                    old_trials = v.trials_per_dim.get(dim, 0)
+                    if old_trials > 0:
+                        v.scores[dim] = (old * old_trials + score) / (old_trials + 1)
+                    else:
+                        v.scores[dim] = score
+                    v.trials_per_dim[dim] = old_trials + 1
+                v.trials += variant.trials
+                return False
+        if len(self.variants) < self.max_size:
+            self.variants.append(variant)
+            return True
+        # 淘汰最弱个体
+        scored = [(v.composite_score() if v.trials > 0 else float("-inf"), v)
+                   for v in self.variants]
+        scored.sort(key=lambda x: x[0])
+        worst = scored[0][1]
+        if variant.composite_score() > worst.composite_score() or worst.trials == 0:
+            self.variants.remove(worst)
+            self.variants.append(variant)
+            return True
+        return False
+
+    # ── 停滞检测 & 灭绝 ────────────────────────────
+
+    def track_best_history(self) -> None:
+        scored = [v for v in self.variants if v.trials > 0]
+        best = max((v.composite_score() for v in scored), default=0.0)
+        self._best_score_history.append(best)
+        if len(self._best_score_history) > 5:
+            self._best_score_history = self._best_score_history[-5:]
+
+    def is_stagnant(self, generations: int = 2,
+                    improvement_threshold: float = 0.03) -> bool:
+        if len(self._best_score_history) < generations + 1:
+            return False
+        recent = self._best_score_history[-generations:]
+        if recent[0] == 0:
+            return False
+        for s in recent[1:]:
+            if s >= recent[0] * (1 + improvement_threshold):
+                return False
+        return True
+
+    def needs_extinction(self, generations: int = 3,
+                         improvement_threshold: float = 0.05) -> bool:
+        return (self.is_stagnant(generations, improvement_threshold)
+                and len(self.variants) >= self.max_size * 0.7)
+
+    def apply_extinction(self, genre_seeds: list[str] | None = None,
+                         style_injects: list[str] | None = None) -> dict:
+        """删除底30%,重新播种"""
+        events = {"removed": 0, "reseeded_genre": 0,
+                  "reseeded_style": 0, "reseeded_crossover": 0}
+
+        scored = sorted(
+            [v for v in self.variants if v.trials > 0],
+            key=lambda v: v.composite_score()
+        )
+        unscored = [v for v in self.variants if v.trials == 0]
+
+        remove_count = max(1, int(len(scored) * 0.3))
+        removed = scored[:remove_count]
+        survivors = scored[remove_count:] + unscored
+        events["removed"] = len(removed)
+        self.variants = survivors
+
+        to_add = []
+        if genre_seeds:
+            for s in genre_seeds[:max(1, remove_count // 2)]:
+                to_add.append(PromptVariant(s, source="reseed:genre"))
+        if style_injects:
+            for s in style_injects[:max(1, remove_count * 3 // 10)]:
+                to_add.append(PromptVariant(s, source="reseed:style"))
+        if len(survivors) >= 2:
+            for _ in range(max(1, remove_count // 5)):
+                a, b = random.sample(survivors, 2)
+                child = crossover(a, b)
+                child.source = "reseed:crossover"
+                to_add.append(child)
+
+        for v in to_add:
+            if self.add(v):
+                key = "reseeded_" + v.source.split(":")[-1]
+                events[key] = events.get(key, 0) + 1
+
+        return events
+
+
+# ── 遗传算子 ───────────────────────────────────────────
+
+def crossover(a: PromptVariant, b: PromptVariant) -> PromptVariant:
+    a_sents = re.split(r"[。;;]\s*", a.template)
+    b_sents = re.split(r"[。;;]\s*", b.template)
+    if len(a_sents) < 2 or len(b_sents) < 2:
+        a_kw = re.findall(r"[一-鿿]{2,4}", a.template)
+        b_kw = re.findall(r"[一-鿿]{2,4}", b.template)
+        combined = list(set(a_kw + b_kw))
+        random.shuffle(combined)
+        template = "。".join(combined[:max(3, len(combined) // 2)])
+    else:
+        split_a = random.randint(1, len(a_sents) - 1)
+        split_b = random.randint(1, len(b_sents) - 1)
+        template = ";".join(s for s in (a_sents[:split_a] + b_sents[split_b:]) if s.strip())
+    child = PromptVariant(template, source="crossover")
+    child.lineage = [a.id, b.id]
+    return child
+
+
+def mutate(v: PromptVariant) -> PromptVariant:
+    template = v.template
+    synonyms = {
+        "必须": ["务必", "一定", "确保"],
+        "检查": ["核查", "验证", "确认"],
+        "推进": ["推动", "进展"],
+        "冲突": ["矛盾", "张力"],
+        "钩子": ["悬念", "抓手"],
+        "爽点": ["高潮点", "释放点"],
+    }
+    for word, alts in synonyms.items():
+        if word in template and random.random() < 0.3:
+            template = template.replace(word, random.choice(alts), 1)
+    sents = re.split(r"[。;;]\s*", template)
+    if len(sents) >= 3 and random.random() < 0.3:
+        i, j = random.sample(range(len(sents)), 2)
+        sents[i], sents[j] = sents[j], sents[i]
+        template = ";".join(sents)
+    if random.random() < 0.2:
+        prefixes = ["关键:", "注意:", "核心:"]
+        template = random.choice(prefixes) + template
+    mutant = PromptVariant(template, source="mutate")
+    mutant.lineage = [v.id]
+    return mutant
+
+
+def simplify(v: PromptVariant) -> PromptVariant:
+    sents = re.split(r"[。;;]\s*", v.template)
+    if len(sents) <= 2:
+        return v
+    simplified = PromptVariant(";".join([sents[0], sents[-1]]), source="simplify")
+    simplified.lineage = [v.id]
+    return simplified
+
+
+def template_similarity(a: str, b: str) -> float:
+    a_words = set(re.findall(r"[一-鿿]{2,}", a))
+    b_words = set(re.findall(r"[一-鿿]{2,}", b))
+    if not a_words or not b_words:
+        return 0.0
+    return len(a_words & b_words) / len(a_words | b_words)
+
+
+def inject_from_style_report(report_path: Path) -> PromptVariant | None:
+    """从对标/风格报告.md 提取风格约束并生成注入变体。
+
+    解析风格报告的结构化章节(句式/对话/描写/节奏/语调),
+    编码为写作规则提示词,source 标记为 inject:{书名}。
+    """
+    try:
+        text = report_path.read_text(encoding="utf-8")
+    except Exception:
+        return None
+
+    fm_match = re.match(r"^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$", text)
+    if not fm_match:
+        return None
+
+    body = fm_match.group(2)
+    rules = []
+    source_name = report_path.parent.name
+
+    # 句式 → 句长和节奏约束
+    sent_match = re.search(r"## 句式\s*\n(.*?)(?=\n## |\Z)", body, re.DOTALL)
+    if sent_match:
+        sent_text = sent_match.group(1)
+        avg_match = re.search(r"平均句长[::]\s*(\d+)[–\-—]?\d*\s*字", sent_text)
+        if avg_match:
+            rules.append(f"目标句长:{avg_match.group(1)}字/句")
+        ratio_match = re.search(r"短句.*?(?:约占\s*)?(\d+)\s*%", sent_text)
+        if ratio_match:
+            rules.append(f"短句比例参考:{ratio_match.group(1)}%")
+
+    # 对话 → 对话占比和风格
+    dial_match = re.search(r"## 对话\s*\n(.*?)(?=\n## |\Z)", body, re.DOTALL)
+    if dial_match:
+        dial_text = dial_match.group(1)
+        pct_match = re.search(r"对话占比[::]\s*约?\s*(\d+)\s*%", dial_text)
+        if pct_match:
+            rules.append(f"对话占比参考:{pct_match.group(1)}%")
+
+    # 描写 → 五感偏好
+    desc_match = re.search(r"## 描写\s*\n(.*?)(?=\n## |\Z)", body, re.DOTALL)
+    if desc_match:
+        desc_text = desc_match.group(1)
+        for sense in ["视觉", "触觉", "听觉", "嗅觉", "味觉"]:
+            if sense in desc_text:
+                rules.append(f"五感偏好:{sense}主导描写")
+                break
+
+    # 节奏 → 章首章末规则
+    pacing_match = re.search(r"## 节奏\s*\n(.*?)(?=\n## |\Z)", body, re.DOTALL)
+    if pacing_match:
+        pacing_text = pacing_match.group(1)
+        if "章首" in pacing_text or "开头" in pacing_text:
+            rules.append("章首规则:3段内进入情境")
+        if "章末" in pacing_text and "钩子" in pacing_text:
+            rules.append("章末规则:留钩子")
+
+    # 语调 → 叙事距离和视角
+    tone_match = re.search(r"## 语调\s*\n(.*?)(?=\n## |\Z)", body, re.DOTALL)
+    if tone_match:
+        tone_text = tone_match.group(1)
+        for pov in ["第三人称有限", "第三人称", "第一人称", "上帝视角", "全知"]:
+            if pov in tone_text:
+                rules.append(f"视角约束:{pov}")
+                break
+
+    if not rules:
+        return None
+
+    prompt = f"【{source_name}风格注入】" + ";".join(rules)
+    v = PromptVariant(prompt, source=f"inject:{source_name}")
+    v.lineage = [f"style:{source_name}"]
+    return v
+
+
+# ── 岛屿模型 ───────────────────────────────────────────
+
+class IslandModel:
+    """岛屿模型 — 5 个独立种群 + 迁移 + 早停"""
+
+    def __init__(self):
+        self.islands: dict[str, PromptPopulation] = {}
+        self.stagnation_counters: dict[str, int] = {}
+
+    def initialize(self, seeds: dict[str, list[str]]) -> None:
+        for cat, templates in seeds.items():
+            pop = PromptPopulation(cat)
+            pop.seed(templates)
+            self.islands[cat] = pop
+
+    def evolve_all(self, generations: int = 1, mutation_rate: float = 0.3,
+                   weights: dict[str, float] | None = None,
+                   genre_seeds: dict[str, list[str]] | None = None,
+                   style_injects: dict[str, list[str]] | None = None,
+                   project_path: Path | None = None) -> dict[str, list[PromptVariant]]:
+        offspring: dict[str, list[PromptVariant]] = {}
+        for cat, pop in self.islands.items():
+            pop.track_best_history()
+
+            # 早停:连续2代提升<3% → 跳过变异
+            if pop.is_stagnant(generations=2, improvement_threshold=0.03):
+                self.stagnation_counters[cat] = self.stagnation_counters.get(cat, 0) + 1
+                print(f"    [{cat}] 停滞 {self.stagnation_counters[cat]} 代,跳过变异")
+                offspring[cat] = []
+                continue
+            else:
+                self.stagnation_counters[cat] = 0
+
+            # 灭绝:连续3代提升<5% 且种群≥70%满
+            if pop.needs_extinction(generations=3, improvement_threshold=0.05):
+                print(f"    [{cat}] 触发灭绝-重播")
+                gs = (genre_seeds or {}).get(cat, [])
+                si = (style_injects or {}).get(cat, [])
+                events = pop.apply_extinction(gs, si)
+                if project_path:
+                    log_evolution_event(project_path, f"灭绝事件_{cat}", events)
+                offspring[cat] = []
+                continue
+
+            # 正常进化
+            children = []
+            parents = pop.select_top(3, weights)
+            diverse = pop.select_diverse(2)
+
+            for p in parents[:2]:
+                children.append(PromptVariant(
+                    p.template, source="elite", scores=p.scores.copy()
+                ))
+            for i in range(min(3, len(parents) - 1)):
+                children.append(crossover(parents[i], parents[i + 1]))
+            for p in parents + diverse:
+                if random.random() < mutation_rate:
+                    children.append(mutate(p))
+            for p in diverse:
+                if random.random() < mutation_rate * 0.5:
+                    children.append(simplify(p))
+
+            for child in children:
+                pop.add(child)
+            offspring[cat] = children
+            pop.generation += generations
+        return offspring
+
+    def all_islands_stagnant(self) -> bool:
+        if not self.islands:
+            return False
+        return all(
+            pop.is_stagnant(generations=2, improvement_threshold=0.03)
+            for pop in self.islands.values()
+        )
+
+    def migrate(self, top_n: int = 2) -> int:
+        migrated = 0
+        islands = list(self.islands.keys())
+        if len(islands) < 2:
+            return 0
+        for i, source_cat in enumerate(islands):
+            target_cat = islands[(i + 1) % len(islands)]
+            elites = self.islands[source_cat].select_top(top_n)
+            for elite in elites:
+                copy = PromptVariant(
+                    f"[迁自{source_cat}]{elite.template}",
+                    source=f"migrate:{source_cat}",
+                    scores={k: v * 0.9 for k, v in elite.scores.items()},
+                )
+                if self.islands[target_cat].add(copy):
+                    migrated += 1
+        return migrated
+
+    def sync_to_evolution(self) -> dict:
+        """同步最优提示词到 skill 级 evolution/prompts/current_best.json"""
+        best = {}
+        for cat, pop in self.islands.items():
+            top = pop.select_top(3)
+            if len(top) < 3:
+                # 用未评分变体补充到 3 个
+                unscored = [v for v in pop.variants if v.trials == 0]
+                for v in unscored:
+                    if v.template not in [t.template for t in top]:
+                        top.append(v)
+                    if len(top) >= 3:
+                        break
+            best[cat] = [v.template for v in top[:3]] if top else [
+                v.template for v in pop.variants[:3]
+            ]
+        save_evolution("prompts/current_best.json", best)
+        return best
+
+    def sync_to_genre(self, genre_id: str) -> dict:
+        """同步最优提示词到 evolution/genres/{genre_id}/prompts/current_best.json"""
+        from utils import save_genre_evolution
+        best = {}
+        for cat, pop in self.islands.items():
+            top = pop.select_top(3)
+            if len(top) < 3:
+                unscored = [v for v in pop.variants if v.trials == 0]
+                for v in unscored:
+                    if v.template not in [t.template for t in top]:
+                        top.append(v)
+                    if len(top) >= 3:
+                        break
+            best[cat] = [v.template for v in top[:3]] if top else [
+                v.template for v in pop.variants[:3]
+            ]
+        save_genre_evolution(genre_id, "prompts/current_best.json", best)
+        return best
+
+
+# ── 持久化 ─────────────────────────────────────────────
+
+def save_model(model: IslandModel, genre_id: str | None = None) -> None:
+    """保存岛屿模型到 population.json(题材级或技能级)"""
+    data = {}
+    for cat, pop in model.islands.items():
+        data[cat] = {
+            "generation": pop.generation,
+            "variants": [v.to_dict() for v in pop.variants],
+        }
+    if genre_id:
+        from utils import save_genre_evolution
+        save_genre_evolution(genre_id, "prompts/population.json", data)
+    else:
+        save_evolution("prompts/population.json", data)
+
+
+def load_model(genre_id: str | None = None) -> IslandModel:
+    """加载岛屿模型(题材级优先,回退到技能级)"""
+    model = IslandModel()
+
+    if genre_id:
+        from utils import load_genre_evolution
+        data = load_genre_evolution(genre_id, "prompts/population.json")
+        if data:
+            for cat, pop_data in data.items():
+                pop = PromptPopulation(cat, max_size=35)
+                pop.generation = pop_data.get("generation", 0)
+                pop.variants = [PromptVariant.from_dict(d)
+                                for d in pop_data.get("variants", [])]
+                model.islands[cat] = pop
+            return model
+
+    # 回退:技能级
+    data = load_evolution("prompts/population.json")
+    if data:
+        for cat, pop_data in data.items():
+            pop = PromptPopulation(cat, max_size=35)
+            pop.generation = pop_data.get("generation", 0)
+            pop.variants = [PromptVariant.from_dict(d)
+                            for d in pop_data.get("variants", [])]
+            model.islands[cat] = pop
+    else:
+        model.initialize(SEED_TEMPLATES)
+    return model
+
+
+# ── 题材级进化入口 ─────────────────────────────────────
+
+def evolve_genre(genre_id: str, project_path: Path | None = None,
+                 generations: int = 3, mutation_rate: float = 0.3) -> dict:
+    """题材级共享提示词进化。
+
+    同题材所有项目共享进化数据。project_path 可选——传入时
+    会扫描该项目的风格报告注入到题材池中,实现跨书风格交叉。
+    """
+    print(f"遗传 提示词进化 (题材: {genre_id})")
+
+    from utils import ensure_genre_evolution, find_style_reports
+    ensure_genre_evolution(genre_id)
+    model = load_model(genre_id)
+
+    # 收集风格报告注入(来自具体项目,但写入题材池)
+    style_injects: dict[str, list[str]] = {cat: [] for cat in model.islands}
+    if project_path:
+        style_reports = find_style_reports(project_path)
+        for report_path in style_reports:
+            injected = inject_from_style_report(report_path)
+            if injected:
+                style_injects.setdefault("writing", []).append(injected.template)
+                if model.islands.get("writing", PromptPopulation("writing")).add(injected):
+                    print(f"    风格注入: {report_path.parent.name} → writing 岛(题材池)")
+
+    # 获取题材种子(供灭绝重播用)
+    from seed_extractor import SeedExtractor
+    extractor = SeedExtractor()
+    genre_seeds = extractor.extract_seeds(genre_id=genre_id)
+
+    # 加载权重
+    from utils import load_genre_evolution
+    thresholds = load_genre_evolution(genre_id, "thresholds.json")
+    if not thresholds:
+        thresholds = load_evolution("thresholds.json") or {}
+    weights = thresholds.get("weights",
+                {"P1": 0.35, "P2": 0.25, "P3": 0.15, "P4": 0.15, "P5": 0.10})
+
+    total_offspring = 0
+    # 记录演化前状态用于事件日志
+    pre_variant_ids = {
+        cat: {v.id for v in pop.variants}
+        for cat, pop in model.islands.items()
+    }
+
+    for gen in range(generations):
+        if model.all_islands_stagnant():
+            print("  所有岛屿停滞,进化收敛")
+            break
+        offspring = model.evolve_all(
+            generations=1, mutation_rate=mutation_rate,
+            weights=weights,
+            genre_seeds=genre_seeds,
+            style_injects=style_injects,
+            project_path=project_path,
+        )
+        gen_count = sum(len(v) for v in offspring.values())
+        total_offspring += gen_count
+        if (gen + 1) % 2 == 0:
+            model.migrate(top_n=2)
+
+    # 写入事件日志(Event Sourcing)
+    post_variant_ids = {
+        cat: {v.id for v in pop.variants}
+        for cat, pop in model.islands.items()
+    }
+    events = []
+    for cat, pop in model.islands.items():
+        pre_ids = pre_variant_ids.get(cat, set())
+        post_ids = post_variant_ids.get(cat, set())
+        new_ids = post_ids - pre_ids
+        removed_ids = pre_ids - post_ids
+        if new_ids or removed_ids:
+            new_variants = [
+                v.to_dict() for v in pop.variants if v.id in new_ids
+            ]
+            events.append({
+                "type": "mutate",
+                "island": cat,
+                "generation": pop.generation,
+                "new_variants": new_variants,
+                "removed_variants": list(removed_ids),
+            })
+    if events:
+        from event_store import write_events
+        write_events(genre_id, events)
+
+    save_model(model, genre_id)
+    best = model.sync_to_genre(genre_id)
+
+    print(f"  产生 {total_offspring} 个新变体")
+    print(f"  题材进化结果已写入 evolution/genres/{genre_id}/")
+    for cat, prompts in best.items():
+        print(f"    [{cat}]: {len(prompts)} 最优提示词")
+
+    return {"status": "ok", "offspring": total_offspring, "genre": genre_id}
+
+
+# ── CLI ─────────────────────────────────────────────────
+
+def evolve_skill(generations: int = 3, mutation_rate: float = 0.3) -> dict:
+    """进化 skill 自身的提示词(不依赖项目)"""
+    print(f"遗传 提示词进化 (skill-level)")
+    model = load_model()
+    thresholds = load_evolution("thresholds.json") or {}
+    weights = thresholds.get("weights", {"P1": 0.35, "P2": 0.25, "P3": 0.15, "P4": 0.15, "P5": 0.10})
+
+    print(f"  岛屿数: {len(model.islands)}")
+    for cat, pop in model.islands.items():
+        scored = [v for v in pop.variants if v.trials > 0]
+        print(f"    [{cat}] variants={len(pop.variants)} scored={len(scored)} gen={pop.generation}")
+
+    total_offspring = 0
+    for gen in range(generations):
+        if model.all_islands_stagnant():
+            print("  所有岛屿停滞,进化收敛")
+            break
+        offspring = model.evolve_all(generations=1, mutation_rate=mutation_rate)
+        gen_count = sum(len(v) for v in offspring.values())
+        total_offspring += gen_count
+        if (gen + 1) % 2 == 0:
+            model.migrate(top_n=2)
+
+    save_model(model)
+    best = model.sync_to_evolution()
+    print(f"  ✓ 产生 {total_offspring} 个新变体")
+    print(f"  ✓ 进化结果已写入 evolution/prompts/")
+    for cat, prompts in best.items():
+        print(f"    [{cat}]: {len(prompts)} 最优提示词")
+    return {"status": "ok", "offspring": total_offspring}
+
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description="提示词进化器")
+    parser.add_argument("action", choices=["evolve", "show"],
+                        help="evolve=进化 | show=展示当前最优提示词")
+    parser.add_argument("--project", "-p", type=str, default=None,
+                        help="项目路径(自动检测题材)")
+    parser.add_argument("--genre", "-g", type=str, default=None,
+                        help="题材 ID(直接指定,跳过自动检测)")
+    parser.add_argument("--generations", type=int, default=3)
+    parser.add_argument("--mutation-rate", "-m", type=float, default=0.3)
+    args = parser.parse_args()
+
+    if args.action == "evolve":
+        if args.genre:
+            evolve_genre(args.genre,
+                         generations=args.generations,
+                         mutation_rate=args.mutation_rate)
+        elif args.project:
+            from utils import resolve_genre_from_project
+            genre_id = resolve_genre_from_project(Path(args.project))
+            evolve_genre(genre_id, project_path=Path(args.project),
+                         generations=args.generations,
+                         mutation_rate=args.mutation_rate)
+        else:
+            evolve_skill(generations=args.generations,
+                         mutation_rate=args.mutation_rate)
+    elif args.action == "show":
+        if args.genre:
+            from utils import load_genre_evolution
+            best = load_genre_evolution(args.genre, "prompts/current_best.json")
+        elif args.project:
+            from utils import resolve_genre_from_project, load_genre_evolution
+            genre_id = resolve_genre_from_project(Path(args.project))
+            best = load_genre_evolution(genre_id, "prompts/current_best.json")
+        else:
+            best = load_evolution("prompts/current_best.json")
+
+        if not best:
+            model = IslandModel()
+            model.initialize(SEED_TEMPLATES)
+            model.sync_to_evolution()
+            best = load_evolution("prompts/current_best.json")
+
+        for cat, prompts in best.items():
+            print(f"\n## {cat}")
+            for i, p in enumerate(prompts):
+                print(f"  {i+1}. {p[:120]}...")

+ 208 - 0
scripts/quality_tuner.py

@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+"""
+quality_tuner.py — 质量阈值自动调优器
+调优结果写入 evolution/thresholds.json,不修改 SKILL.md。
+"""
+
+import sys, os, re, json, math
+from pathlib import Path
+from typing import Any
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from utils import (
+    find_project_root, get_evolution_dir,
+    load_evolution, save_evolution, parse_quality_log,
+    extract_quality_features, log_evolution_event,
+)
+
+DEFAULT_THRESHOLDS = {
+    "pass": 80, "revise_low": 60, "rewrite_low": 40,
+    "max_revision_rounds": 5,
+    "weights": {"P1": 0.35, "P2": 0.25, "P3": 0.15, "P4": 0.15, "P5": 0.10},
+    "stale_hook_limit": 10, "burst_hook_limit": 5,
+    "no_advance_limit": 5,
+    "payoff_interval_min": 2000, "payoff_interval_max": 5000,
+    "weak_hook_consecutive_max": 3,
+}
+
+
+class QualityTuner:
+    def __init__(self, thresholds: dict | None = None):
+        self.thresholds = thresholds or load_evolution("thresholds.json") or DEFAULT_THRESHOLDS.copy()
+        self.history: list[dict] = []
+        self.adjustments: list[dict] = []
+
+    def load_history(self, project: Path) -> None:
+        self.history = parse_quality_log(project)
+
+    def analyze(self) -> dict:
+        if not self.history:
+            return {"status": "no_data"}
+
+        features = extract_quality_features(self.history)
+        suggestions = []
+
+        recent = [r for r in self.history[-10:] if r.get("score")]
+        recent_scores = [r["score"] for r in recent]
+        pass_rate = sum(1 for s in recent_scores if s >= self.thresholds["pass"]) / max(len(recent_scores), 1)
+
+        if pass_rate > 0.9 and len(recent_scores) >= 5:
+            suggestions.append({
+                "type": "pass_threshold_raise",
+                "current": self.thresholds["pass"],
+                "suggested": min(88, self.thresholds["pass"] + 3),
+                "reason": f"近期通过率 {pass_rate:.0%},阈值可能偏低",
+                "confidence": 0.7,
+            })
+        elif pass_rate < 0.3 and len(recent_scores) >= 5:
+            suggestions.append({
+                "type": "pass_threshold_lower",
+                "current": self.thresholds["pass"],
+                "suggested": max(70, self.thresholds["pass"] - 3),
+                "reason": f"近期通过率 {pass_rate:.0%},阈值可能偏高",
+                "confidence": 0.6,
+            })
+
+        pass_weights = ["P1", "P2", "P3", "P4", "P5"]
+        pass_scores = {}
+        for p in pass_weights:
+            scores = [r.get(f"score_{p.lower()}", 0) for r in self.history if f"score_{p.lower()}" in r]
+            if scores:
+                pass_scores[p] = sum(scores) / len(scores)
+
+        if pass_scores:
+            weakest = min(pass_scores, key=pass_scores.get)
+            strongest = max(pass_scores, key=pass_scores.get)
+            if pass_scores[strongest] - pass_scores[weakest] > 15:
+                current_weight = self.thresholds["weights"][weakest]
+                new_weight = min(0.40, current_weight + 0.05)
+                suggestions.append({
+                    "type": "weight_adjust",
+                    "target_pass": weakest,
+                    "current_weight": current_weight,
+                    "suggested_weight": new_weight,
+                    "reason": f"{weakest} 最弱 ({pass_scores[weakest]:.0f}),增权",
+                    "confidence": 0.75,
+                    "redistribute_from": strongest,
+                })
+
+        rounds = [r.get("revision_rounds", 1) for r in self.history if r.get("revision_rounds")]
+        avg_rounds = sum(rounds) / len(rounds) if rounds else 1.0
+        if avg_rounds > 3:
+            suggestions.append({
+                "type": "root_cause_warning",
+                "reason": f"平均修订 {avg_rounds:.1f} 轮,建议向上追溯大纲/设定问题",
+                "confidence": 0.8,
+            })
+
+        s1_recent = sum(r.get("s1_count", 0) for r in recent)
+        if s1_recent > 0:
+            suggestions.append({
+                "type": "quality_alert", "level": "S1", "count": s1_recent,
+                "reason": f"近10章有 {s1_recent} 个S1致命问题",
+                "confidence": 0.9,
+            })
+
+        return {
+            "status": "ok", "features": features, "pass_rate": pass_rate,
+            "recent_scores": recent_scores, "pass_scores": pass_scores,
+            "suggestions": suggestions, "avg_revision_rounds": avg_rounds,
+        }
+
+    def apply(self, suggestions: list[dict], auto_apply: bool = False) -> list[dict]:
+        applied = []
+        for s in suggestions:
+            if s.get("type") == "quality_alert":
+                applied.append({**s, "applied": False})
+                continue
+            if not auto_apply and s.get("confidence", 0) < 0.7:
+                applied.append({**s, "applied": False})
+                continue
+            if s["type"] in ("pass_threshold_raise", "pass_threshold_lower"):
+                old = self.thresholds["pass"]
+                self.thresholds["pass"] = s["suggested"]
+                self.adjustments.append({"what": "pass", "from": old, "to": s["suggested"]})
+                applied.append({**s, "applied": True})
+            elif s["type"] == "weight_adjust":
+                target = s["target_pass"]
+                redistribute = s.get("redistribute_from")
+                old_w = self.thresholds["weights"][target]
+                self.thresholds["weights"][target] = s["suggested_weight"]
+                if redistribute:
+                    self.thresholds["weights"][redistribute] -= (s["suggested_weight"] - old_w)
+                self.adjustments.append({"what": f"weight_{target}", "from": old_w, "to": s["suggested_weight"]})
+                applied.append({**s, "applied": True})
+        return applied
+
+    def sync_to_evolution(self) -> None:
+        """将当前阈值写入 evolution/thresholds.json"""
+        save_evolution("thresholds.json", self.thresholds)
+
+    def sync_to_genre_evolution(self, genre_id: str) -> None:
+        """将当前阈值写入 evolution/genres/{genre_id}/thresholds.json"""
+        from utils import save_genre_evolution
+        save_genre_evolution(genre_id, "thresholds.json", self.thresholds)
+
+
+# ── CLI ─────────────────────────────────────────────────
+
+def analyze_and_tune(project: Path, auto_apply: bool = False) -> dict:
+    print(f"🔧 质量阈值调优: {project.name}")
+    tuner = QualityTuner()
+    tuner.load_history(project)
+    analysis = tuner.analyze()
+
+    if analysis["status"] == "no_data":
+        print("  ⚠ 没有质检历史数据,使用当前阈值")
+        return analysis
+
+    features = analysis.get("features", {})
+    avg = features.get('avg_score', 0)
+    print(f"  平均得分: {avg:.1f}" if isinstance(avg, (int, float)) else f"  平均得分: N/A")
+    print(f"  通过率: {analysis.get('pass_rate', 0):.0%}")
+    print(f"  S1/S2: {features.get('total_s1', 0)}/{features.get('total_s2', 0)}")
+
+    suggestions = analysis.get("suggestions", [])
+    if suggestions:
+        print(f"\n  {len(suggestions)} 条优化建议:")
+        for s in suggestions:
+            print(f"  [{s['type']}] {s.get('reason', '')}")
+        applied = tuner.apply(suggestions, auto_apply=auto_apply)
+        applied_count = sum(1 for a in applied if a.get("applied"))
+        print(f"\n  应用了 {applied_count} 条,跳过 {len(applied) - applied_count} 条")
+    else:
+        print("  ✓ 阈值配置合理,无需调整")
+        applied = []
+
+    from utils import find_project_genre, map_genre_to_inkos_id
+    genre_id = map_genre_to_inkos_id(find_project_genre(project))
+    tuner.sync_to_evolution()
+    tuner.sync_to_genre_evolution(genre_id)
+    print(f"  ✓ 阈值已同步到 skill 级和题材级 evolution/genres/{genre_id}/")
+    return {"analysis": analysis, "applied": applied}
+
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description="质量阈值调优器")
+    parser.add_argument("action", choices=["analyze", "tune", "show"],
+                        help="analyze=分析 | tune=分析并应用 | show=查看当前阈值")
+    parser.add_argument("--project", "-p", default=".")
+    parser.add_argument("--auto", action="store_true")
+    args = parser.parse_args()
+
+    if args.action == "show":
+        thresholds = load_evolution("thresholds.json") or DEFAULT_THRESHOLDS
+        print(json.dumps(thresholds, ensure_ascii=False, indent=2))
+
+    elif args.action in ("analyze", "tune"):
+        root = find_project_root(Path(args.project).resolve())
+        if not root:
+            print("错误: 未找到小说项目根目录")
+            sys.exit(1)
+        auto = args.auto or (args.action == "tune")
+        result = analyze_and_tune(root, auto_apply=auto)
+        if args.action == "tune" and result.get("applied"):
+            log_evolution_event(root, "质量阈值调优", {
+                "应用": len([a for a in result["applied"] if a.get("applied")]),
+            })

+ 460 - 0
scripts/seed_extractor.py

@@ -0,0 +1,460 @@
+#!/usr/bin/env python3
+"""
+seed_extractor.py — InkOS 题材感知种子提取器
+
+从 InkOS 题材配置文件(YAML frontmatter + Markdown body)提取
+题材特定的初始提示词种子,映射到 novel-write 的 5 岛结构。
+
+回退链:指定题材 → other.md → 硬编码通用种子(SEED_TEMPLATES)
+"""
+
+import sys, os, re, json, yaml
+from pathlib import Path
+from typing import Any
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from utils import (
+    save_genre_evolution, find_project_genre, map_genre_to_inkos_id,
+    resolve_genre_from_project, log_evolution_event,
+)
+
+# ── 通用回退种子(当 InkOS 不可用时) ─────────────────
+
+FALLBACK_SEEDS: dict[str, list[str]] = {
+    "writing": [
+        "核心规则(写作时内化不列出):Show, don't tell——用细节堆出真实。价值观盐溶于汤——靠行为证明不靠口号。任何行动立于三条腿:过往经历+当前利益+性格底色。配角有自己账本和利益诉求。每章结尾必有小悬念或情绪缺口。全员智商在线。日常场景七成成后续伏笔。关系改变事件驱动。每一行字要么推动剧情要么塑造人物。",
+        "看点密集度(硬尺):每300字至少1个爽点。每500字至少1个钩子。每1000-1500字至少1个完整悬念。连续300字以上只是环境/回忆/议论/心理独白而无推进=水文须删改。叙事段≥40字,目标40-120字。短段(<40字)只允许:开场反转金句/章末钩子/≤3个爆点短段,合计≤5个。",
+        "去AI味铁律:叙述者永不替读者下结论。禁止分析报告式语言入正文。转折/惊讶标记词(仿佛/忽然/竟/猛地/不禁/宛如)全篇≤每3000字1次。硬性禁令:全文禁止'不是…而是…'句式;全文禁止破折号'——'。",
+    ],
+    "revision": [
+        "修稿分流规则:PATCHES处理局部文字问题——每个PATCH引用原文段落给出替换文本。REVISED_CONTENT处理全章级问题——Critical包含字数或结构性问题时必须用REVISED_CONTENT。修稿原则:修根因不做表面润色;伏笔状态须与伏笔池同步;不改变剧情走向和核心冲突。",
+        "润色边界(硬约束):只改文字层——句式/段落/排版/用词/五感/对话自然度。禁止增删情节/改变人设/调整主线。文字层硬规约:段落3-5行/段;句式多样化;动词>形容词;每场景1-2种感官。修改后总长变化≤±15%。",
+    ],
+    "quality_check": [
+        "自修正循环(不可跳过门控):Loop最多5轮——综合评分≥85通过→60-84针对性修订→40-59重写问题段落→<40废弃从细纲重新写。每轮记录本轮评分/发现问题/修复内容/下一轮重点。",
+        "五遍编辑评估(按顺序不可跳过):P1结构(35%)→P2角色(25%)→P3原创性(15%)→P4对话(15%)→P5文本(10%)。综合≥85通过/60-84修订/40-59重写/<40废弃。P1不过不进P2。",
+    ],
+    "outline": [
+        "大纲共创三层决策树:第一层全书结构——分几卷?每卷核心任务?不可逆变化?全书最高潮在哪卷?第二层卷纲细化——高潮事件?需要哪些铺垫(≥3个伏笔)?主角情感弧线?卷末钩子须全书级别。第三层细纲关键节点——前10章每章核心事件/章末钩子类型/爽点类型/伏笔推进计划。",
+        "章节备忘七段格式:当前任务→读者此刻在等什么→该兑现的/暂不掀的→日常/过渡承担任务→关键抉择过三连问→章尾必须发生的改变→本章hook账→不要做。",
+    ],
+    "hook_management": [
+        "伏笔全生命周期:plant(种植)→advance(推进)→payoff(兑现)→defer(推迟)→stale(过期)。每章前读钩子账本→列出高优活跃钩子→确认推进/兑现机会。每章后更新:兑现移走→推进追加→新种写入→检查stale。",
+        "钩子准入控制:不重复——指向同一事件合并不新增。不超量——每卷最多3-5个high优先级,活跃钩子总数≤12。有计划——无兑现计划的钩子不种。新钩子候选:正文中出现会延续到后续且有具体回收方向的未解问题才新增。",
+    ],
+}
+
+
+class SeedExtractor:
+    """从 InkOS 题材配置提取 5 岛种子提示词"""
+
+    def __init__(self, inkos_genres_dir: str | None = None):
+        if inkos_genres_dir:
+            self.genres_dir = Path(inkos_genres_dir)
+        else:
+            self.genres_dir = Path(
+                "/home/nonvioce/Downloads/inkos-master/packages/core/genres"
+            )
+        self._profile_cache: dict[str, dict | None] = {}
+
+    # ── 题材配置加载 ─────────────────────────────────
+
+    def _load_profile(self, genre_id: str) -> dict | None:
+        """加载单个题材配置文件,解析 YAML frontmatter + body sections"""
+        if genre_id in self._profile_cache:
+            return self._profile_cache[genre_id]
+
+        fpath = self.genres_dir / f"{genre_id}.md"
+        if not fpath.exists():
+            self._profile_cache[genre_id] = None
+            return None
+
+        text = fpath.read_text(encoding="utf-8")
+        fm_match = re.match(r"^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$", text)
+        if not fm_match:
+            self._profile_cache[genre_id] = None
+            return None
+
+        try:
+            frontmatter = yaml.safe_load(fm_match.group(1))
+        except yaml.YAMLError:
+            frontmatter = {}
+
+        body = fm_match.group(2)
+        sections = {}
+        for sec_match in re.finditer(r"##\s+(.+?)\s*\n([\s\S]*?)(?=\n## |\Z)", body):
+            title = sec_match.group(1).strip()
+            content = sec_match.group(2).strip()
+            sections[title] = content
+
+        profile = {
+            "frontmatter": frontmatter or {},
+            "sections": sections,
+            "language": frontmatter.get("language", "zh") if frontmatter else "zh",
+        }
+        self._profile_cache[genre_id] = profile
+        return profile
+
+    def _load_with_fallback(self, genre_id: str) -> dict:
+        """加载题材配置,失败时回退到 other → None"""
+        profile = self._load_profile(genre_id)
+        if profile:
+            return profile
+        if genre_id != "other":
+            return self._load_profile("other") or {"frontmatter": {}, "sections": {}, "language": "zh"}
+        return {"frontmatter": {}, "sections": {}, "language": "zh"}
+
+    # ── 字段→岛屿 映射 ───────────────────────────────
+
+    def _build_writing_seeds(self, fm: dict, sections: dict, lang: str) -> list[str]:
+        """从题材配置构建 writing 岛种子"""
+        seeds = []
+
+        # 疲劳词 → 词汇疲劳规避规则
+        fatigue_words = fm.get("fatigueWords", [])
+        if fatigue_words:
+            word_list = "、".join(fatigue_words)
+            seeds.append(
+                f"本文疲劳词监控(题材特定):以下词汇单章最多使用1次——{word_list}。"
+                f"超限即标记为S2警告。这些是{lang == 'en' and 'genre fatigue words' or '题材疲劳词'},"
+                f"过度使用会让读者产生'又是这套'的审美疲劳。"
+            )
+
+        # 节奏规则
+        pacing = fm.get("pacingRule", "")
+        if pacing:
+            seeds.append(f"节奏控制规则(题材特定):{pacing}。偏离此节奏需在章节备忘中说明原因。")
+
+        # 爽点类型
+        satisfaction = fm.get("satisfactionTypes", [])
+        if satisfaction:
+            types_str = "、".join(satisfaction)
+            seeds.append(
+                f"本文期待感类型优先排序(题材特定):{types_str}。"
+                f"每章至少兑现其中一种类型。爽点设计优先从该清单选取,清单外类型需在细纲中标注'非标爽点'。"
+            )
+
+        # 叙事指导 section
+        narrative_guidance = (
+            sections.get("叙事指导")
+            or sections.get("Narrative Guidance")
+            or sections.get("Pacing Guidance")
+            or ""
+        )
+        if narrative_guidance:
+            # 提取关键句子(以句号分隔,取有实质内容的句子)
+            key_lines = [
+                s.strip() for s in re.split(r"[。.\n]", narrative_guidance)
+                if len(s.strip()) > 10 and not s.strip().startswith("-")
+            ][:5]
+            if key_lines:
+                guidance_text = "。".join(key_lines)
+                seeds.append(f"题材叙事指导(注入写作规则):{guidance_text}。")
+
+        return seeds
+
+    def _build_revision_seeds(self, fm: dict, sections: dict, lang: str) -> list[str]:
+        """从题材配置构建 revision 岛种子"""
+        seeds = []
+
+        # 语言铁律 / Language Rules
+        language_rules = sections.get("语言铁律") or sections.get("Language Rules") or ""
+        if language_rules:
+            # 提取具体规则行
+            rule_lines = [
+                line.strip().lstrip("- ").lstrip("* ")
+                for line in language_rules.split("\n")
+                if line.strip() and (line.strip().startswith("-") or line.strip().startswith("*")
+                                     or "→" in line or "✗" in line or "✓" in line)
+            ]
+            if rule_lines:
+                rules_text = ";".join(rule_lines[:8])
+                seeds.insert(0, f"题材润色特化规则:{rules_text}。这些规则优先于通用润色规则执行。")
+
+        # 年代考据规则
+        if fm.get("eraResearch"):
+            seeds.append(
+                "年代考据规则(题材特定):涉及法律/政策/商业规则/物价/机构名/行业术语必须符合设定年代。"
+                "金融操作和公司运营需有基本可信度。地名和机构名不可凭空编造。"
+            )
+
+        return seeds
+
+    def _build_quality_check_seeds(self, fm: dict, sections: dict, lang: str) -> list[str]:
+        """从题材配置构建 quality_check 岛种子"""
+        seeds = []
+
+        # 题材禁忌 → S1/S2 检查项
+        prohibitions = sections.get("题材禁忌") or sections.get("Genre Prohibitions") or ""
+        if prohibitions:
+            items = []
+            for line in prohibitions.split("\n"):
+                stripped = line.strip().lstrip("- ").lstrip("* ")
+                if len(stripped) > 10:
+                    items.append(stripped)
+
+            if items:
+                check_items = []
+                for i, item in enumerate(items[:10]):
+                    severity = "S1" if any(
+                        kw in item for kw in ["禁止", "不可", "杜绝", "never", "must not", "prohibited"]
+                    ) else "S2"
+                    check_items.append(f"  {i+1}. [{severity}] {item}")
+                seeds.append(
+                    f"题材特定质量审查清单:\n" + "\n".join(check_items) +
+                    f"\n每章完成后逐项检查,S1项不过直接退回修改。"
+                )
+
+        # 审计维度
+        audit_dims = fm.get("auditDimensions", [])
+        if audit_dims:
+            dims_str = ",".join(str(d) for d in audit_dims)
+            seeds.append(
+                f"37维审查启用维度子集(题材特定):[{dims_str}]。"
+                f"未启用的维度本作不适用,跳过检查以降低误报。"
+            )
+
+        # 数值系统检查
+        if fm.get("numericalSystem"):
+            seeds.append(
+                "数值一致性检查(题材特定):统计面板/战力数值/资源计数必须与上下文一致。"
+                "同质资源重复获取须写明衰减。增量逐笔列出,不可跳跃式增长。"
+            )
+
+        # 战力体系检查
+        if fm.get("powerScaling"):
+            seeds.append(
+                "战力体系检查(题材特定):境界/品阶/等级突破必须有积累过程。"
+                "不可出现无铺垫跳跃式突破。跨大境界需特殊条件(天劫/机缘/丹药)。"
+                "能力上限/附加代价/触发条件/成长路径——四维约束缺一不可。"
+            )
+
+        return seeds
+
+    def _build_outline_seeds(self, fm: dict, sections: dict, lang: str) -> list[str]:
+        """从题材配置构建 outline 岛种子"""
+        seeds = []
+
+        # 章节类型
+        chapter_types = fm.get("chapterTypes", [])
+        if chapter_types:
+            types_str = "、".join(chapter_types)
+            seeds.append(
+                f"章节类型轮换策略(题材特定):可用类型——{types_str}。"
+                f"连续3章以上同类型触发疲劳警告。每卷类型分布应覆盖所有列出类型。"
+                f"各类型比例为:战斗/推进类40%,布局/过渡类35%,回收/高潮类25%。"
+            )
+
+        # 世界观规则 section
+        world_rules = (
+            sections.get("修炼规则")
+            or sections.get("World Rules")
+            or sections.get("年代与现实约束")
+            or sections.get("System Design Rules")
+            or ""
+        )
+        if world_rules:
+            key_rules = []
+            for line in world_rules.split("\n"):
+                stripped = line.strip().lstrip("- ").lstrip("* ")
+                if len(stripped) > 15:
+                    key_rules.append(stripped)
+            if key_rules:
+                rules_text = ";".join(key_rules[:6])
+                seeds.append(f"世界观一致性规则(题材特定,大纲阶段检查):{rules_text}。")
+
+        return seeds
+
+    def _build_hook_management_seeds(self, fm: dict, sections: dict, lang: str) -> list[str]:
+        """从题材配置构建 hook_management 岛种子"""
+        seeds = []
+
+        # 题材特定的悬念期待类型
+        satisfaction = fm.get("satisfactionTypes", [])
+        if satisfaction:
+            types_str = "、".join(satisfaction[:4])
+            seeds.append(
+                f"题材悬念期待管理:本类型读者的核心期待——{types_str}。"
+                f"伏笔设计优先围绕这几类期待展开。每卷至少种下2个指向不同类型期待的伏笔。"
+            )
+
+        # 节奏规则 → 伏笔兑现节奏
+        pacing = fm.get("pacingRule", "")
+        if pacing:
+            seeds.append(
+                f"伏笔兑现节奏约束:{pacing}。伏笔的advance和payoff需与此节奏同步——"
+                f"快节奏段兑现密度可提高,慢节奏段以种植和推进为主。"
+            )
+
+        # 从世界观/修炼规则提取伏笔管理特殊规则
+        world_rules = (
+            sections.get("修炼规则")
+            or sections.get("World Rules")
+            or sections.get("年代与现实约束")
+            or ""
+        )
+        if world_rules:
+            key_lines = [
+                s.strip().lstrip("- ").lstrip("* ")
+                for s in world_rules.split("\n")
+                if len(s.strip()) > 15 and any(
+                    kw in s for kw in ["规则", "约束", "限制", "禁止", "条件", "代价", "rule", "limit", "cannot", "must"]
+                )
+            ][:3]
+            if key_lines:
+                rules_text = ";".join(key_lines)
+                seeds.append(
+                    f"题材伏笔管理特殊规则:{rules_text}。"
+                    f"违反这些规则的伏笔在plant阶段即应被拒绝或标记为'需特殊机制'。"
+                )
+
+        return seeds
+
+    # ── 主入口 ──────────────────────────────────────
+
+    def extract_seeds(self, genre_id: str | None = None,
+                      project_path: Path | None = None) -> dict[str, list[str]]:
+        """
+        提取题材特定的 5 岛种子提示词。
+
+        返回: {island_name: [prompt_strings, ...]}
+        """
+        # 确定题材 ID
+        if not genre_id and project_path:
+            genre_name = find_project_genre(project_path)
+            genre_id = map_genre_to_inkos_id(genre_name)
+        elif not genre_id:
+            genre_id = "other"
+
+        profile = self._load_with_fallback(genre_id)
+        fm = profile.get("frontmatter", {})
+        sections = profile.get("sections", {})
+        lang = profile.get("language", "zh")
+
+        # 如果 profile 是空的(InkOS 不可用),用回退种子
+        if not fm and not sections:
+            print(f"  ⚠ InkOS 题材配置不可用,使用通用回退种子")
+            return FALLBACK_SEEDS
+
+        seeds = {
+            "writing": self._build_writing_seeds(fm, sections, lang),
+            "revision": self._build_revision_seeds(fm, sections, lang),
+            "quality_check": self._build_quality_check_seeds(fm, sections, lang),
+            "outline": self._build_outline_seeds(fm, sections, lang),
+            "hook_management": self._build_hook_management_seeds(fm, sections, lang),
+        }
+
+        # 对种子过少的岛屿,从 general pool 补充(避免重复)
+        general = self._load_profile("other")
+        for island, current in seeds.items():
+            if len(current) < 2:
+                # 先从 general profile 补
+                if general and general.get("frontmatter"):
+                    gf = general["frontmatter"]
+                    gs = general["sections"]
+                    gl = general.get("language", "zh")
+                    fallback_builder = getattr(self, f"_build_{island}_seeds", None)
+                    if fallback_builder:
+                        fb_seeds = fallback_builder(gf, gs, gl)
+                        for s in fb_seeds:
+                            if s not in current:
+                                current.append(s)
+                                if len(current) >= 2:
+                                    break
+                # 还不够 → 从硬编码回退种子补
+                if len(current) < 2 and island in FALLBACK_SEEDS:
+                    for s in FALLBACK_SEEDS[island]:
+                        if s not in current:
+                            current.append(s)
+                            if len(current) >= 2:
+                                break
+
+        return seeds
+
+    def initialize_genre(self, genre_id: str) -> dict[str, list[str]]:
+        """
+        初始化题材级共享进化目录 evolution/genres/{genre_id}/。
+
+        创建 population.json(含题材种子)和相关文件。
+        返回提取的种子字典。
+        """
+        seeds = self.extract_seeds(genre_id=genre_id)
+
+        import hashlib
+        population = {}
+        for island_name, templates in seeds.items():
+            variants = []
+            for template in templates:
+                variant_id = hashlib.md5(template.encode()).hexdigest()[:12]
+                variants.append({
+                    "id": variant_id,
+                    "template": template,
+                    "source": f"seed:{genre_id}",
+                    "scores": {},
+                    "trials": 0,
+                    "trials_per_dim": {},
+                    "lineage": [],
+                })
+            population[island_name] = {
+                "generation": 0,
+                "variants": variants,
+            }
+
+        save_genre_evolution(genre_id, "prompts/population.json", population)
+        save_genre_evolution(genre_id, "prompts/current_best.json",
+                             {k: [v["template"] for v in pop["variants"][:3]]
+                              for k, pop in population.items()})
+
+        default_thresholds = load_evolution("thresholds.json")
+        if default_thresholds:
+            save_genre_evolution(genre_id, "thresholds.json", default_thresholds)
+        else:
+            save_genre_evolution(genre_id, "thresholds.json", {
+                "pass": 85, "revise_low": 60, "rewrite_low": 40,
+                "max_revision_rounds": 3,
+                "weights": {"P1": 0.35, "P2": 0.25, "P3": 0.15, "P4": 0.15, "P5": 0.10},
+            })
+
+        save_genre_evolution(genre_id, "patterns/pattern_library.json", {
+            "patterns": [], "meta": {"total_mined": 0}
+        })
+        save_genre_evolution(genre_id, "session_state.json", {
+            "recent_chapters": [], "total_feedback_count": 0
+        })
+
+        print(f"  ✓ 题材进化目录已初始化: evolution/genres/{genre_id}/")
+        for island, tmpls in seeds.items():
+            print(f"    [{island}]: {len(tmpls)} 条种子")
+
+        return seeds
+
+
+# ── CLI ─────────────────────────────────────────────────
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser(description="InkOS 题材感知种子提取器")
+    parser.add_argument("--project", "-p", type=str, default=None,
+                        help="项目路径(自动检测题材)")
+    parser.add_argument("--genre", "-g", type=str, default=None,
+                        help="强制指定题材 ID")
+    parser.add_argument("--show", action="store_true",
+                        help="仅显示提取的种子,不初始化")
+    args = parser.parse_args()
+
+    extractor = SeedExtractor()
+
+    if args.project:
+        project = Path(args.project)
+        genre_id = args.genre or resolve_genre_from_project(project)
+    else:
+        genre_id = args.genre or "other"
+
+    if args.show:
+        seeds = extractor.extract_seeds(genre_id=genre_id)
+        for island, tmpls in seeds.items():
+            print(f"\n## {island} ({len(tmpls)} 条)")
+            for i, t in enumerate(tmpls):
+                print(f"  {i+1}. {t[:150]}...")
+    else:
+        extractor.initialize_genre(genre_id)

+ 360 - 0
scripts/utils.py

@@ -0,0 +1,360 @@
+#!/usr/bin/env python3
+"""
+novel-write 自优化系统 — 共享工具模块
+OpenEvolve 启发的进化优化框架
+
+进化输出到 skill 目录下的 evolution/ 文件夹,不修改 SKILL.md。
+"""
+
+import json
+import re
+import os
+import sys
+import hashlib
+import time
+from pathlib import Path
+from datetime import datetime
+from collections import defaultdict
+from typing import Any
+
+# ── 路径解析 ───────────────────────────────────────────
+
+def get_skill_dir() -> Path:
+    """获取 skill 安装目录(symlink 目标)"""
+    # 优先使用环境变量,否则推断
+    env = os.environ.get("NOVEL_SKILL_DIR", "")
+    if env:
+        return Path(env)
+    # 从脚本位置推断: scripts/utils.py → 上级 = skill 目录
+    return Path(__file__).resolve().parent.parent
+
+
+def get_evolution_dir() -> Path:
+    """获取进化输出目录"""
+    return get_skill_dir() / "evolution"
+
+
+def find_project_root(start: Path | None = None) -> Path | None:
+    """向上查找小说项目根目录(包含 项目铁律.md 的目录)"""
+    d = Path(start or os.getcwd()).resolve()
+    for _ in range(10):
+        if (d / "项目铁律.md").exists():
+            return d
+        if d.parent == d:
+            return None
+        d = d.parent
+    return None
+
+
+def find_all_projects(base: str = os.path.expanduser("~/Documents")) -> list[Path]:
+    """查找 base 下所有小说项目"""
+    projects = []
+    base = Path(base)
+    if not base.exists():
+        return projects
+    for d in base.iterdir():
+        if d.is_dir() and (d / "项目铁律.md").exists():
+            projects.append(d)
+    return projects
+
+
+# ── 章节解析 ───────────────────────────────────────────
+
+def parse_chapters(project: Path) -> dict[int, Path]:
+    """解析项目中的章节文件,返回 {章号: 路径}"""
+    chapters = {}
+    body_dir = project / "正文"
+    if not body_dir.exists():
+        return chapters
+    for f in sorted(body_dir.glob("*第*章*.md")):
+        m = re.search(r"第(\d+)章", f.name)
+        if m:
+            chapters[int(m.group(1))] = f
+    return chapters
+
+
+def count_words(text: str) -> int:
+    """中文字数统计"""
+    chinese = len(re.findall(r"[一-鿿]", text))
+    return chinese
+
+
+def extract_chapter_meta(content: str) -> dict:
+    """从章节 markdown 提取元数据"""
+    meta = {}
+    patterns = {
+        "核心事件": r"## 核心事件[::]\s*(.+?)(?:\n|$)",
+        "章末钩子类型": r"章末钩子.*?类型[::]\s*(.+?)(?:\n|$)",
+        "期待感等级": r"期待感等级[::]\s*(.+?)(?:\n|$)",
+        "爽点类型": r"爽点.*?类型[::]\s*(.+?)(?:\n|$)",
+        "爽点模式": r"爽点.*?模式[::]\s*(.+?)(?:\n|$)",
+    }
+    for key, pat in patterns.items():
+        m = re.search(pat, content)
+        if m:
+            meta[key] = m.group(1).strip()
+    return meta
+
+
+# ── 质量数据解析 ────────────────────────────────────────
+
+def parse_quality_log(project: Path) -> list[dict]:
+    """解析优化日志,返回历史质检记录"""
+    log_file = project / "追踪" / "优化日志.md"
+    if not log_file.exists():
+        return []
+
+    records = []
+    text = log_file.read_text(encoding="utf-8")
+    blocks = re.split(r"\n## \d{4}-\d{2}-\d{2}", text)
+    for block in blocks[1:]:
+        record = {}
+        m = re.search(r"(?:第\d+章)?综合评分\*{0,2}[::]\s*(\d+)", block)
+        if m:
+            record["score"] = int(m.group(1))
+        for p in ["P1", "P2", "P3", "P4", "P5"]:
+            m = re.search(rf"\*{{0,2}}{p}\*{{0,2}}[::]\s*(\d+)", block)
+            if m:
+                record[f"score_{p.lower()}"] = int(m.group(1))
+        m = re.search(r"第(\d+)章", block)
+        if m:
+            record["chapter"] = int(m.group(1))
+        record["s1_count"] = len(re.findall(r"S1[::]", block))
+        record["s2_count"] = len(re.findall(r"S2[::]", block))
+        records.append(record)
+    return records
+
+
+def parse_hook_ledger(project: Path) -> dict:
+    """解析钩子账本,返回钩子健康数据"""
+    ledger = project / "追踪" / "钩子账本.md"
+    if not ledger.exists():
+        return {}
+    text = ledger.read_text(encoding="utf-8")
+    return {
+        "total_active": len(re.findall(r"状态[::]\s*(?:活跃|active)", text)),
+        "stale": len(re.findall(r"stale|过期", text)),
+        "high_priority": len(re.findall(r"优先级[::]\s*high|高", text)),
+        "paid_off": len(re.findall(r"已兑现|payoff", text)),
+    }
+
+
+# ── 特征提取 ───────────────────────────────────────────
+
+def extract_style_features(text: str) -> dict:
+    """提取文本的风格特征向量"""
+    sentences = re.split(r"[。!?.!?\n]+", text)
+    sentences = [s.strip() for s in sentences if s.strip()]
+    if not sentences:
+        return {}
+    lengths = [len(s) for s in sentences]
+    dialogue_lines = len(re.findall(r"[「""'].*?[」""']", text))
+    return {
+        "avg_sentence_len": sum(lengths) / len(lengths),
+        "sentence_len_variance": sum((l - sum(lengths) / len(lengths)) ** 2 for l in lengths) / len(lengths),
+        "short_sentence_ratio": sum(1 for l in lengths if l <= 5) / len(lengths),
+        "long_sentence_ratio": sum(1 for l in lengths if l >= 30) / len(lengths),
+        "dialogue_ratio": dialogue_lines / max(len(sentences), 1),
+        "paragraph_count": len(re.split(r"\n\n+", text)),
+        "total_chinese_chars": count_words(text),
+    }
+
+
+def extract_quality_features(records: list[dict]) -> dict:
+    """从质检记录提取质量特征"""
+    if not records:
+        return {}
+    scores = [r.get("score", 0) for r in records if r.get("score")]
+    p1_scores = [r.get("score_p1", 0) for r in records if r.get("score_p1")]
+    p2_scores = [r.get("score_p2", 0) for r in records if r.get("score_p2")]
+    return {
+        "avg_score": sum(scores) / len(scores) if scores else 0,
+        "min_score": min(scores) if scores else 0,
+        "max_score": max(scores) if scores else 0,
+        "score_trend": "up" if len(scores) >= 3 and scores[-3:] == sorted(scores[-3:]) else "flat",
+        "weakest_pass": "P1" if (sum(p1_scores) / max(len(p1_scores), 1)) < 70 else
+                        "P2" if (sum(p2_scores) / max(len(p2_scores), 1)) < 70 else "none",
+        "total_s1": sum(r.get("s1_count", 0) for r in records),
+        "total_s2": sum(r.get("s2_count", 0) for r in records),
+    }
+
+
+# ── 进化数据持久化 ─────────────────────────────────────
+
+def load_evolution(name: str) -> dict:
+    """从 evolution/ 目录加载进化数据"""
+    fpath = get_evolution_dir() / name
+    if not fpath.exists():
+        return {}
+    with open(fpath, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def save_evolution(name: str, data: dict) -> None:
+    """保存进化数据到 evolution/ 目录"""
+    evo_dir = get_evolution_dir()
+    evo_dir.mkdir(parents=True, exist_ok=True)
+    fpath = evo_dir / name
+    with open(fpath, "w", encoding="utf-8") as f:
+        json.dump(data, f, ensure_ascii=False, indent=2)
+
+
+def load_project_db(project: Path) -> dict:
+    """加载项目进化数据库"""
+    db_file = project / "追踪" / ".evolution_db.json"
+    if not db_file.exists():
+        return {"prompts": [], "patterns": [], "config_mutations": [], "meta": {}}
+    with open(db_file, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def save_project_db(project: Path, db: dict) -> None:
+    """保存项目进化数据库"""
+    db_file = project / "追踪" / ".evolution_db.json"
+    db["meta"]["last_updated"] = datetime.now().isoformat()
+    db["meta"]["version"] = db["meta"].get("version", 0) + 1
+    with open(db_file, "w", encoding="utf-8") as f:
+        json.dump(db, f, ensure_ascii=False, indent=2)
+
+
+def log_evolution_event(project: Path, event_type: str, data: dict) -> None:
+    """记录进化事件到 evolution/evolution_log.md 和项目日志"""
+    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
+    entry = f"\n## {timestamp} | {event_type}\n\n"
+    for k, v in data.items():
+        entry += f"- **{k}**: {v}\n"
+    entry += "\n---\n"
+
+    # 写到 evolution/ 目录
+    evo_log = get_evolution_dir() / "evolution_log.md"
+    with open(evo_log, "a", encoding="utf-8") as f:
+        f.write(entry)
+
+    # 也写项目日志
+    proj_log = project / "追踪" / "优化日志.md"
+    proj_log.parent.mkdir(parents=True, exist_ok=True)
+    with open(proj_log, "a", encoding="utf-8") as f:
+        f.write(entry)
+
+
+# ── 统计工具 ───────────────────────────────────────────
+
+def compute_improvement(before: list[float], after: list[float]) -> float:
+    if not before or not after:
+        return 0.0
+    avg_before = sum(before) / len(before)
+    avg_after = sum(after) / len(after)
+    if avg_before == 0:
+        return 100.0 if avg_after > 0 else 0.0
+    return ((avg_after - avg_before) / avg_before) * 100
+
+
+# ── 题材级共享进化路径 ─────────────────────────────────
+
+def get_genre_evolution_dir(genre_id: str) -> Path:
+    """获取题材级共享进化目录,自动创建"""
+    evo_dir = get_evolution_dir() / "genres" / genre_id
+    evo_dir.mkdir(parents=True, exist_ok=True)
+    return evo_dir
+
+
+def load_genre_evolution(genre_id: str, name: str) -> dict:
+    """从 evolution/genres/{genre_id}/ 加载进化数据"""
+    fpath = get_genre_evolution_dir(genre_id) / name
+    if not fpath.exists():
+        return {}
+    with open(fpath, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def save_genre_evolution(genre_id: str, name: str, data: dict) -> None:
+    """保存进化数据到 evolution/genres/{genre_id}/"""
+    evo_dir = get_genre_evolution_dir(genre_id)
+    fpath = evo_dir / name
+    fpath.parent.mkdir(parents=True, exist_ok=True)
+    with open(fpath, "w", encoding="utf-8") as f:
+        json.dump(data, f, ensure_ascii=False, indent=2)
+
+
+def find_project_genre(project_path: Path) -> str:
+    """从 项目铁律.md frontmatter 提取 genre 字段"""
+    iron_law = project_path / "项目铁律.md"
+    if not iron_law.exists():
+        return "other"
+    text = iron_law.read_text(encoding="utf-8")
+    m = re.search(r'genre[:\s]+["\']?([^"\'\n]+)', text)
+    if m:
+        return m.group(1).strip()
+    m = re.search(r'#\s*(.+?)(?:\n|$)', text)
+    return m.group(1).strip() if m else "other"
+
+
+def find_style_reports(project_path: Path) -> list[Path]:
+    """查找对标/ 目录下所有风格报告"""
+    reports_dir = project_path / "对标"
+    if not reports_dir.exists():
+        return []
+    return sorted(reports_dir.glob("*/风格报告.md"))
+
+
+def resolve_genre_from_project(project_path: Path) -> str:
+    """从项目路径解析题材 ID,确保题材进化目录已初始化"""
+    genre_name = find_project_genre(project_path)
+    genre_id = map_genre_to_inkos_id(genre_name)
+    return genre_id
+
+
+def ensure_genre_evolution(genre_id: str) -> bool:
+    """确保题材进化目录存在并已初始化;返回 True 表示已存在"""
+    evo_dir = get_genre_evolution_dir(genre_id)
+    if evo_dir.exists() and (evo_dir / "prompts" / "population.json").exists():
+        return True
+    from seed_extractor import SeedExtractor
+    extractor = SeedExtractor()
+    extractor.initialize_genre(genre_id)
+    log_evolution_event(Path("."), "题材进化初始化", {
+        "题材ID": genre_id,
+        "类型": "genre-level evolution init",
+    })
+    return False
+
+
+# ── 中文题材→InkOS ID 映射 ─────────────────────────────
+
+GENRE_NAME_TO_ID = {
+    "仙侠": "xianxia", "修仙": "xianxia",
+    "玄幻": "xuanhuan",
+    "都市": "urban", "现代": "urban", "都市异能": "urban",
+    "恐怖": "horror", "惊悚": "horror", "灵异": "horror",
+    "科幻": "sci-fi",
+    "末世": "other", "废土": "other",
+    "无限流": "other", "无限": "other",
+    "游戏": "litrpg", "网游": "litrpg",
+    "奇幻": "progression", "西幻": "progression",
+    "言情": "romantasy", "古言": "romantasy", "现言": "romantasy",
+    "轻小说": "isekai", "异世界": "isekai",
+    "种田": "other", "日常": "other",
+    "地下城": "dungeon-core",
+    "修真": "cultivation",
+    "English": "cultivation",
+}
+
+
+def map_genre_to_inkos_id(genre_name: str) -> str:
+    """将中文题材名映射到 InkOS 题材 ID"""
+    if not genre_name:
+        return "other"
+    # 精确匹配
+    if genre_name.lower() in GENRE_NAME_TO_ID:
+        return GENRE_NAME_TO_ID[genre_name.lower()]
+    # 子串匹配(处理"种田+诡异"这种复合题材)
+    for cn_name, inkos_id in GENRE_NAME_TO_ID.items():
+        if cn_name in genre_name:
+            return inkos_id
+    return "other"
+
+
+if __name__ == "__main__":
+    print(f"Skill 目录: {get_skill_dir()}")
+    print(f"进化目录: {get_evolution_dir()}")
+    print("utils.py 模块加载正常")