Explorar el Código

docs(v7): spec 0.14 回填——第三轮 review 修复收口

- story-repo-spec 0.13→0.14:§4.5 名册「类型」值域(作者域中文/机器域英文+入库归一)与
  「别名」分隔口径(G-3)、§8.1 批次.json 持久面=章列表+派生量口径(E7)、
  停止判据以 front matter 声明为准附注(E9)、§14 决策 39
- database-guidelines 1.0→1.1:§5.4 事务边界钉死(B-S)——解析失败=跳过+warning、
  完整性违反=硬错冒泡 ROLLBACK、禁止内层 catch 架空事务;仅 ENOENT 视为无数据源
- SKILL.md:rosterUpserts 类型值域括注(中文词表)
- 任务工件:probe-after-fix.mjs 修复后探针 9/9 PASS 留档(P-1~P-7b 同场景断新语义),
  implement.md/prd.md 勾验(AC3 CI 项待 push 验证)
lingfengQAQ hace 1 día
padre
commit
3a86feb775

+ 7 - 2
.trellis/spec/backend/database-guidelines.md

@@ -1,6 +1,6 @@
 # 数据与存储规范
 
-> 版本:基线 1.0(2026-06-12)。依据:story-repo-spec 0.6 §11(缓存)、不变量 1/2、PRD §4-16(真源唯一)。
+> 版本:基线 1.0(2026-06-12);1.1(2026-07-07)补 §5.4 事务边界钉死(第三轮 review B-S)。依据:story-repo-spec 0.14 §11(缓存)、不变量 1/2、PRD §4-16(真源唯一)。
 
 ---
 
@@ -46,10 +46,15 @@
 
 5.1 全量重建必须在**单事务**内完成(`BEGIN` → DELETE 全表 → 扫描 INSERT → `COMMIT`),中途任何失败(含名册别名冲突等数据完整性违反)必须 `ROLLBACK`,禁止留半填库。
 
-5.2 软失败不回滚:名册缺失或格式解析不动属"best-effort 跳过",记 warning、不阻断重建、不回滚已写的 chapters/threads/secrets;只有别名冲突等硬错才回滚。名册非必需(角色卡可独立入 `entities`)。
+5.2 软失败不回滚:名册缺失或格式解析不动属"best-effort 跳过",记 warning、不阻断重建、不回滚已写的 chapters/threads/secrets;硬错分类见 §5.4。名册非必需(角色卡可独立入 `entities`)。
 
 5.3 缓存管理器禁止先删除旧 `index.db` 再重建。必须先在临时数据库中完成 DDL + 全量扫描,成功后再替换正式库;普通手动重建失败时保留上一份可用缓存,定稿后刷新失败时必须让旧缓存不可继续作为最新事实读取(删除/失效),避免 `next` 读旧章号重抄本章。
 
+5.4 **事务边界钉死**(1.1,第三轮 review B-S):错误分两类,不得混淆——
+- **解析失败**(front matter/表格格式坏)=跳过该文件 + push warning,不回滚;每个软跳过分支必须产出 warning,禁止静默。
+- **数据完整性违反**(UNIQUE/主键冲突、别名指向冲突等约束错误)=硬错,必须冒泡到重建器外层触发 ROLLBACK + 人话报错(源有重号该修源);禁止 scan 函数内层 catch 吞掉完整性错误架空外层事务意图。
+- 文件系统错误中仅 ENOENT 可视为"无此数据源"跳过,其余 IO 错误一律硬错冒泡。
+
 ## 6. 待增量补充
 
 - [ ] 各表的列定义与索引(实现 O4 精准读取接口时定稿)

+ 6 - 0
.trellis/tasks/07-06-fix-review-round3/check.jsonl

@@ -0,0 +1,6 @@
+{"file": ".trellis/tasks/07-06-fix-review-round3/prd.md", "reason": "验收标准 AC1-AC5:探针场景语义、P1 回归测试覆盖、全量绿、spec 回填、不引入新功能"}
+{"file": ".trellis/tasks/07-06-fix-review-round3/design.md", "reason": "六个行为决策(D1-D6)与每条修法——检查实现是否偏离设计(尤其类型归一映射、幂等重跑顺序、restorePlan 三段式)"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/review-m1-m7.md", "reason": "对照 40 条问题清单逐条核销;REFUTED 4 条不许被顺手改动"}
+{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "质检口径:双源禁令(新抽的 splitAliases/isWeakHook/TRACKED_SOURCE_PREFIXES/normalizeWorkspaceRel 不得残留旧副本)"}
+{"file": ".trellis/spec/backend/error-handling.md", "reason": "质检口径:新增守卫/失败路径的文案必须人话中文、不泄英文栈味"}
+{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "质检口径:重建事务边界(解析失败跳过+warning / 完整性违反 ROLLBACK)与 B-S 新条款一致性"}

+ 118 - 0
.trellis/tasks/07-06-fix-review-round3/design.md

@@ -0,0 +1,118 @@
+# 技术设计:第三轮 review 修复
+
+真源:`.trellis/tasks/07-05-m1-m7-review/review-m1-m7.md`(汇总)+ `research/review-{A..G}.md`(细节与行号)。本文按修复批次给出每条的技术方案与关键决策;实现时以分区报告的 file:line 为准。
+
+## 关键决策(呈用户裁决过的行为决策)
+
+| # | 决策 | 内容 |
+|---|------|------|
+| D1 | 实体类型值规范(G-3/R5) | **名册文件(作者域)写中文**(角色/地点/组织/物品),**缓存 entities.type(机器域)存英文 machine 值**。入库点做归一映射 `normalizeEntityType()`:角色→character、地点→location、组织/势力→organization、物品→item;英文值恒等收纳(character→character);未知值原样保留+warning。fixture 名册的 `character` 改中文「角色」(消除 G-2 掩盖源)。 |
+| D2 | 一对多别名降级(R10) | 迁移时同一别名指向多实体 → 按 facts.entities 数组序**首实体保留**该别名,其余实体剥离;剥离明细写 `定稿/设定/迁移待校对-别名歧义.md` 并进迁移报告待校对清单。整体迁移不再硬回滚。 |
+| D3 | finalize-batch 幂等重跑(E3 尾链) | 转正循环顺序改为「先更新批次.json(移除该章行)再删章目录」;目录删除失败 → 人话错误 + 指引重跑;finalizeBatch 开头对目标章做已定稿检测(定稿文件已存在且章号 ≤ 缓存 maxChapter → 跳过转正、只清目录),重跑不会二次定稿撞「已存在」。 |
+| D4 | 源只读硬承诺(F-6) | openReadOnly 的可写回退分支删除;readOnly 打开失败直接人话报错("源数据库无法只读打开(可能被占用),关闭占用程序后重试")。 |
+| D5 | imagery_top 陈旧(B) | rebuild 加 `preserveDerived` 选项:删缓存裸重建=保留(丢失重测无害的对称面),改源刷新(refreshCacheAfterSourceChange)=作废 imagery_top(宁可无提醒不给陈旧提醒)。 |
+| D6 | 序2/relink/goto 脏树范围(D3 区) | `定稿/大纲` 前缀集合扩为 `定稿/大纲/文风/book.yaml`,抽单一常量 `TRACKED_SOURCE_PREFIXES` 供 detectors.listManualEdits、relink、goto dirtyScoped 三处共用(防双写)。修复后的文风铁律/book.yaml 由此走序2 补登入档。 |
+
+## 批 0:测试基建(G-1)
+
+`test/commands/_helper.js` gitBookCtx 对齐 `persistCreateBook`(state-machine/persist.js:58-78):写 `.gitignore`(`.cache/`、`工作区/`)、`git config core.quotepath false`、只 add 跟踪面(不 `add -A`)。存量测试若依赖"工作区被跟踪"的断言,按真实形态修正断言(测试是探针不是约束)。
+
+## 批 1:数据安全 + 互斥守卫
+
+### R1/R2 批次互斥守卫(E1/E2)
+- `state-machine/flows/goto-chapter.js`:入口(needsConfirm 判定前)`readBatch(repoPath)`,`exists` → `{ok:false}` 人话:"有进行中的待定稿批次(第 X-Y 章)。先 `finalize-batch` 转正或 `batch-discard` 丢弃,再回退。" 附 gitHealth 照常返回。
+- `commands/finalize.js`:同守卫。文案区分两种情况:目标章在批内("第 N 章已在待定稿批次中,用 finalize-batch 转正")/ 批次存在但目标章不在批内("有进行中批次,手动定稿会造成章号错位")。
+- 守卫加在命令/flow 层,不加 finalizeChapter 本体(finalizeBatch 内部要调它)。
+- 回归测试用批 0 修正后的 gitBookCtx(工作区未跟踪,批次目录可如实存活)。
+
+### R3 persistRepair 分派校验(D1 区)
+`state-machine/persist.js:118-127`:新增 `validateRepairContent(file, content)` 按路径分派——`book.yaml` → parseBookConfig;`定稿/设定/名册.md`、`定稿/设定/时间线/` 前缀 → parseMarkdownTable;其余 → parseFrontMatter。分派规则与 detectors.js 检测器选型对齐(同一映射,注释互指)。
+
+### R4 finalize 清理不谎报(C1/C2/G-4/C4)
+`finalize/index.js:141-144`:
+- 清理循环搬进独立 try/catch,`fs.rm(..., { recursive: true, force: true })`;失败只收集 warning,**不改写 ok**(commit 已发生),返回 `{ok:true, commitHash, warnings}`。
+- 本体归一防御下沉:抽 `util/workspace-path.js` `normalizeWorkspaceRel(name)`——剥 `工作区/` 前缀 + 按路径段拒绝 `..`(`split(/[\\/]/).includes('..')`,替换 stageChapter 的 `includes('..')` 过宽判定=C4)。finalizeChapter、stageChapter、commands/finalize.js、commands/stage-chapter.js 四处共用(消除双源)。
+
+### R7/R8 yaml-dialect 补漏(A1/A2/F-2)
+`storage/serializers/yaml-dialect.js`:
+- `needsQuoting` 补:空串;`value !== value.trim()`;带符号/十六进制/科学计数数字形态(`/^[+-]?\d+(\.\d+)?([eE][+-]?\d+)?$/`、`/^0[xXoObB]/` 且后随合法数字);`~`、`.inf/.nan` 变体;起首 YAML 指示符 `[ ] { } * & ! | > @ % \` " ' ,`;`? ` 起首;含 ` #`(行内注释)。
+- 双引号分支完整转义:`\` → `\\`(先做)、`"` → `\"`、`\n` → `\n`、`\r`、`\t`、其余 C0 控制字符 → `\xXX`。
+- 验收=往返 property 测试:review 列的全部 DRIFT/PARSEFAIL 值(`""`、`"  甲  "`、`0x1F`、`1e3`、`+5`、`~`、`[快穿]反派`、`*追读`、`"引号"书`、`- 开局`、`C:\Users\x`、`*ptr`、`&a`、`[TBD`)serialize→parse 逐一断言相等。
+
+### R12 atomic 回滚时序(A3)
+`storage/atomic.js`:plan 增加 `renamedIn` 标志(第二循环 `rename(tmp→final)` 成功后置 true)。restorePlan 改为:rm(tmp) → `if (renamedIn) rm(final)` → `if (existed) rename(backup→final)`。existed=false 且 renamedIn=false 时**绝不碰 final**(本 bug 场景:原文件未动过)。附故障注入测试:mock writeFile 对第 2 个文件抛错,断言已存在的 B 原文完好。
+
+## 批 2:迁移链
+
+### R5 类型值归一(G-2 + 决策 D1)
+- `cache/rebuilder.js:257`(scanEntities)与 `storage/adapters/EntityReader.js` 文件降级路径:经 `util/entity-type.js` `normalizeEntityType()` 入库/过滤。
+- scanCharacters ON CONFLICT 补 type 更新(角色卡路径 'character' 可覆盖名册行)。
+- `test/fixtures/sample-book/定稿/设定/名册.md` 类型列改中文;migrate/transform 名册照写中文(现状已对)。
+- SKILL.md rosterUpserts 说明 + spec §4 名册条款补「类型」值域(中文词表)。
+
+### R6 别名分隔单源(A5/A13)
+staging 的 `splitAliases`(切 `[,,、]`、容 undefined/数组)抽到 `util/aliases.js`;`EntityReader.js:87`、`cache/rebuilder.js:262`、staging 三处共用。A13(名册缺「别名」列 undefined.split 抛错→整次重建 ROLLBACK)由 splitAliases 的 undefined 容错顺带修掉。
+
+### R9 book.yaml 走序列化器(F-1)
+`migrate/transform.js:41-48` 手写拼接改 `serializeYAML({ spec_version:'7.0', 书名, 类型, 每章目标字数:3000, 卷规模:40 })`。前置依赖 R7(`[`/`*` 起首书名先由 needsQuoting 兜住)。
+
+### R10 一对多别名降级(F-3 + 决策 D2)
+`migrate/transform.js` 名册生成前全实体扫别名:`Map<别名, 首实体>`,重复别名从后续实体剥离,剥离清单写 `迁移待校对-别名歧义.md` + report.待校对。
+
+### R11 表格读写对称(A4/A7/A8/A11)
+- `serializers/markdown-table.js`:单元格 `|` → `\|`,换行 → 空格(+warning 语义:表格单元格不含换行)。
+- `parsers/markdown-table.js`:识别 `\|` 转义(先占位替换再 split 再还原);行首为全角 `|` 时整行归一为 `|`(救"整表全角"场景);行 cell 多于表头时不再静默截断——保留进行对象(headers 并集),或至少 push warning。
+- `EntityWriter.upsertRosterRow`:`rows[idx] = { ...旧行, ...新行 }` 合并(保作者额外列);serializeMarkdownTable 的 headers 取传入 headers ∪ 行内额外键(额外列追加在尾),全表重写不再丢列。
+
+## 批 3:P2/S 清扫(按模块分组)
+
+**storage/adapters**
+- A6:ChapterReader/ThreadLedgerReader 缓存命中路径把行映射回中文键(与文件降级一致,接口形状统一为作者域中文);read-chapter/read-thread 输出随之稳定。
+- A9/C3:`_findThreadFile`(ThreadLedgerReader:213、ThreadLedgerWriter:99)与 SecretReader:115 改有界匹配 `f === id+'.md' || f.startsWith(id+'-')`(与 createThread 同口径)。
+- A10/F-7:EntityWriter.updateCharacter / EntityReader 读卡文件名统一过 `sanitizeFileName(name)`;finalize/index.js:93 的角色卡路径改用 EntityWriter 返回的 filePath(消除路径双源)。
+- A12:TimelineWriter.appendRow 按(卷,章)替换既有行(重定稿同章不叠行);迁移路径批量写不受影响(transform 直接 serializeMarkdownTable)。
+- A14:删 extractUnknownFields 死代码;A15:删 readRange 死代码;A16:删 book-config 死校验(requiredFields/missingFields);A17:appendUnderSection `includes` 改精确标题匹配(`^##\s*标题\s*$`)。
+
+**cache**
+- B1/B-P2:rebuilder 各 scan 的 parseFrontMatter 失败分支 push warning;readdir 的 catch 收窄为仅 ENOENT 当"目录不存在"。
+- B2/B-S:INSERT 完整性错误(UNIQUE)不再吞——冒泡触发外层 ROLLBACK + 人话错误(源有重号该修源);persist.js:96-99 卷复盘伏笔条目写前做有界查重(同 createThread 口径),撞号返回人话错误。
+- B(imagery):决策 D5,rebuild 加 preserveDerived 参数。
+
+**staging / dto**
+- E3:决策 D3(先 meta 后 rm + 幂等重跑 + 循环 try/catch 防裸栈)。
+- E4:stageChapter 落盘后的旧目录/工作区清理单独 try/catch,失败记 warning 不改写 ok。
+- E5:dto batchDetail——`--until` 后剩余章全为待审收时,建议改为「finalize-batch 转正剩余 X 章」。
+- E6:readBatch 重建时目录内三件套全缺 → 状态标「打回」(可 stage-chapter 覆盖恢复)而非死「受影响」。
+- E8:弱钩谓词抽 `isWeakHook()`(落 style-stats 或 util),staging×2、book-status、report-weak-hook-streak 四处共用。
+- D5 区:readBatch 加 `heal=false` 参数,dto 读路径不落盘自愈;D6:detectors.js:129 待定稿现存判定改用 readBatch().exists 口径。
+
+**state-machine / flows**
+- D3 区:决策 D6(TRACKED_SOURCE_PREFIXES 三处同源)。
+- D4:retcon 失败回滚收窄为逐文件(记录本次写入集合,对齐 finalize 模式)。
+- D7/G-7:determineNextState 章号查询包 try/catch;next.js 非 json 路径 `r.gitHealth?.fixed?.length` 防御。
+
+**migrate / export**
+- F-4:短题截断改按码点 `Array.from(s).slice(0,9).join('')`。
+- F-5:sweepStaleTmp 只删 mtime 超过 24h 的 `.migrate-tmp-*`(并行进程的活跃 tmp 不误删)。
+- F-6:决策 D4(删可写回退)。
+- F-8:migrate/index.js:86、read-v6.js:292/305、bin:147、cache/index.js:74(ensureReady)错误文案人话化——中文为主、原始 err.message 括号附注。
+- F-9:scanChapters 深层嵌套跳过时 push warning;transform 角色卡类型判定复用 normalizeEntityType(`character` 等英文类型也出卡)。
+- F-10:sanitizeFileName 处理 Windows 保留设备名(CON/NUL/PRN/AUX/COM1-9/LPT1-9 → 前置 `_`)。
+
+**bin / docs**
+- G-9:bin 命令名白名单 `/^[a-z0-9-]+$/`,不匹配走「未知命令」。
+- G-8:--help 删「41 个」硬编码计数。
+- G-5:package.json files 白名单加 `docs/`(migration-guide 随 npm 可达),迁移报告尾部指路一句。
+- G-6:bin spawn 冒烟测试 ×2——export(scope=book)+ migrate(scope=workdir),验参数解析/exitCode/cache.close。
+
+**spec / 文档(AC4)**
+- story-repo-spec:G-3 名册类型值域条款(§4 名册)、E7 批次.json 字段口径改为「章列表(起章/连续计数为派生量)」、E9 附注「停止判据以 front matter 声明为准,宿主须保证 payload 与声明一致」;决策表新增行(spec 0.14)。
+- database-guidelines:B-S 事务边界条款——解析失败=跳过+warning;完整性违反=硬错+ROLLBACK。
+- SKILL.md:rosterUpserts.类型 值域说明(只写指令,无注释噪音)。
+
+## 兼容性与回滚
+
+- 序列化器改动只影响**写路径**(新写文件引号覆盖变宽);读路径(js-yaml/parse)不变,存量文件全部照读。表格 `\|` 转义读写同批落地,旧表无 `\|` 不受影响。
+- normalizeEntityType 对英文值恒等,存量英文名册/缓存无破坏;缓存下次重建自动归一。
+- gitBookCtx 改动可能翻出依赖失真形态的存量断言——逐条按真实形态修正,不回避。
+- 回滚点:每批一个 commit,任一批出问题可独立 revert。

+ 12 - 0
.trellis/tasks/07-06-fix-review-round3/implement.jsonl

@@ -0,0 +1,12 @@
+{"file": ".trellis/tasks/07-05-m1-m7-review/review-m1-m7.md", "reason": "修复 backlog 真源:40 条问题的分层清单、探针证据编号、三批修复建议"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/research/review-A.md", "reason": "存储层(serializers/parsers/adapters/atomic)每条候选的 file:line、复现值表与建议探针——批1 R7/R8/R12、批2 R6/R11、批3 A 系全部细节"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/research/review-B.md", "reason": "缓存/统计区细节——批3 B1/B2/B-S/imagery 的行号与事务边界分析"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/research/review-C.md", "reason": "写章流程区细节——R4/C1/C2/C4 的防御不对称分析与建议探针"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/research/review-D.md", "reason": "状态机区细节——R3 分派校验、D3 脏树范围、D4 retcon 回滚、D5/D6/D7"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/research/review-E.md", "reason": "staging 区细节——R1/R2 守卫落点、E3-E9 与批次状态机交互分析"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/research/review-F.md", "reason": "迁移/导出区细节——R5/R9/R10 探针记录(A/B/C/D 组)与 F-4..F-10"}
+{"file": ".trellis/tasks/07-05-m1-m7-review/research/review-G.md", "reason": "命令壳/测试基建区细节——G-1 gitBookCtx 差异、G-2 类型断裂证据链、G-5..G-9"}
+{"file": "docs/architecture/story-repo-spec-2026-06-10.md", "reason": "宪法真源:不变量 5/8、§8.1 批次语义、§10 序 0-6——守卫文案与 spec 回填(G-3/E7/E9、决策表 0.14)都要对齐它"}
+{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "缓存规范:§5.1/5.2 事务与软失败边界——B-S 条款回填落点"}
+{"file": ".trellis/spec/backend/error-handling.md", "reason": "错误处理规范:人话化要求(F-8/B ensureReady 文案整改的依据)"}
+{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "质量规范:双源禁令、防呆序列化器要求——单源抽取(R6/E8/D6)的依据"}

+ 69 - 0
.trellis/tasks/07-06-fix-review-round3/implement.md

@@ -0,0 +1,69 @@
+# 执行计划:第三轮 review 修复
+
+按批推进,每批一个 commit(独立回滚点)。每批完成即跑该批验证命令;最后一轮全量。
+
+约定:测试命令在 `v7/` 下跑,Windows 需 `PYTHONUTF8=1`(涉 python 时)与 `$env:NODE_OPTIONS` 不动;node 测试 `npm test`(vitest)。
+
+## 批 0:测试基建(G-1)
+
+- [x] `test/commands/_helper.js` gitBookCtx:写 `.gitignore`(`.cache/`、`工作区/`)+ `core.quotepath false` + 只 add 跟踪面(对齐 persist.js:58-78)
+- [x] 跑全量测试,逐条修正依赖旧形态的断言(预期 goto/relink/finalize 相关有波动)
+- [x] 验证:`npm test` 全绿;`git -C <tmp仓库> ls-files | grep 工作区` 为空(用一条新断言固化)
+
+**commit**: `test(v7): gitBookCtx 对齐真实建书形态(G-1)`
+
+## 批 1:数据安全 + 互斥守卫(R1 R2 R3 R4 R7 R8 R12 + C2/C4/G-4)
+
+- [x] R7/R8 `serializers/yaml-dialect.js`:needsQuoting 补全 + 双引号分支完整转义;新增往返测试表(review 全部 DRIFT/PARSEFAIL 值)
+- [x] R12 `storage/atomic.js`:renamedIn 标志 + restorePlan 三段式;故障注入测试(第 2 文件 writeFile 抛错 → B 原文完好)
+- [x] R3 `state-machine/persist.js`:validateRepairContent 按路径分派(book.yaml→parseBookConfig / 名册·时间线→parseMarkdownTable / 其余→parseFrontMatter);测试覆盖三类文件修复被接受
+- [x] R4+C2+G-4+C4:`util/workspace-path.js` normalizeWorkspaceRel(剥前缀+按段拒 `..`);finalizeChapter/stageChapter/两命令壳接入;finalize 清理 recursive+try/warning 不改写 ok;测试:目录清理失败仍 ok:true、`..` 逃逸被拒、前缀直调不漏清
+- [x] R1 `flows/goto-chapter.js` + R2 `commands/finalize.js`:active 批次守卫 + 人话指引;测试:批次在场 goto 被拒、手动 finalize 被拒(用批 0 修正后的 gitBookCtx)
+- [x] 验证:`npm test`;针对性重放上轮探针场景 P-1/P-2/P-3/P-7b 断言新语义
+
+**commit**: `fix(v7): 互斥守卫+序列化器+原子写时序——第三轮 review 批1(R1-R4/R7/R8/R12)`
+
+## 批 2:迁移链(R5 R6 R9 R10 R11 + G-3 落点)
+
+- [x] R6 `util/aliases.js` splitAliases 单源;EntityReader:87 / rebuilder:262 / staging 接入(A13 顺带)
+- [x] R5 `util/entity-type.js` normalizeEntityType;rebuilder scanEntities/scanCharacters(ON CONFLICT 补 type)+ EntityReader 降级路径接入;fixture 名册类型改中文;测试:中文类型迁移书 list-characters/report/审稿名册可见
+- [x] R11 `serializers/markdown-table.js` + `parsers/markdown-table.js`:`\|` 转义读写对称、全角行归一、多余列保留;EntityWriter.upsertRosterRow 合并保额外列;往返测试
+- [x] R9 `migrate/transform.js` book.yaml 走 serializeYAML;测试:`[快穿]反派`/`*追读`/纯数字书名迁移后 parseBookConfig ok 且 books.jsonl 扫描重建不丢书
+- [x] R10 transform 一对多别名降级(首实体保留+待校对文件);测试:共享别名 v6 项目迁移成功、待校对文件在、rebuild 无别名冲突
+- [x] 验证:`npm test` + migrate e2e(v6-inline/v6-sqlite fixtures)+ 探针场景 P-5/P-6
+
+**commit**: `fix(v7): 迁移链类型/别名/序列化收口——第三轮 review 批2(R5/R6/R9-R11)`
+
+## 批 3:P2/S 清扫 + spawn 冒烟
+
+按 design.md 模块分组执行,可按模块拆多个小 commit:
+
+- [x] storage/adapters:A6 键名统一中文、A9/C3 有界匹配、A10/F-7 角色卡文件名同源、A12 时间线按(卷,章)替换、A14/A15/A16 死代码删除、A17 段匹配收紧
+- [x] cache:B1 warning 补齐+catch 收窄、B2 UNIQUE 冒泡 ROLLBACK+卷复盘条目查重、imagery preserveDerived(决策 D5)
+- [x] staging/dto:E3 幂等重跑(决策 D3)、E4 清理失败不谎报、E5 --until 建议、E6 打回空目录状态、E8 isWeakHook 单源、D5/D6 readBatch heal 参数+现存口径
+- [x] state-machine:D3 区 TRACKED_SOURCE_PREFIXES(决策 D6)、D4 retcon 逐文件回滚、D7/G-7 防御
+- [x] migrate/export:F-4 码点截断、F-5 mtime 阈值、F-6 删可写回退(决策 D4)、F-8 错误人话化、F-9 warning+类型归一复用、F-10 保留设备名
+- [x] bin/docs:G-9 命令名白名单、G-8 删硬编码计数、G-5 files 加 docs/
+- [x] G-6:bin spawn 冒烟 ×2(export/migrate)
+- [x] 验证:`npm test` 全量
+
+**commit**: `fix(v7): P2/S 清扫——第三轮 review 批3`(或按模块多 commit)
+
+## 收尾
+
+- [x] spec 回填:story-repo-spec 0.14(G-3/E7/E9 条款 + 决策表)、database-guidelines B-S 条款、SKILL.md rosterUpserts 值域
+- [x] 上轮探针脚本拷入本任务目录改造为修复后断言,跑通留档;有代表性的场景确认已固化进正式测试
+- [x] 全量验证:`npm test`(429+新增全绿)→ push 观察 CI 六 job
+- [x] AC 逐条勾验 → spec 更新(3.3)→ commit → 归档
+
+**commit**: `docs(v7): spec 0.14 回填——第三轮 review 修复收口`
+
+## 回滚点
+
+每批独立 commit;批 1/2 涉及序列化器写路径,若发现写格式回归(存量书读不回),revert 对应批 commit 即可——读路径未动,存量文件安全。
+
+## 风险与注意
+
+- 批 0 的断言修正量未知(gitBookCtx 被 goto/relink/finalize 多处测试用),预留弹性;修正原则=对齐真实形态,不为保绿而弱化断言。
+- E3 幂等重跑改了 finalizeBatch 循环顺序(先 meta 后 rm),注意与 readBatch「记录外目录按受影响纳入」的交互——meta 已删行+目录残留时目录会被重新纳入,因此删除失败必须报错止步(不能静默继续),设计已定。
+- A6 键名统一会改 read-chapter/read-thread 的缓存命中输出形状——检查 SKILL/roles 是否有依赖英文键的文案(预期无:作者域一直宣称中文)。

+ 34 - 0
.trellis/tasks/07-06-fix-review-round3/prd.md

@@ -0,0 +1,34 @@
+# M1-M7 第三轮 review 修复(P1×12 / P2×18 / S×10)
+
+## Goal
+
+把 07-05 第三轮全量 review 的修复 backlog 全部落地(真源:`.trellis/tasks/07-05-m1-m7-review/review-m1-m7.md`)。三个病根一次收口:
+
+1. **互斥不变量只写在 spec 没写进代码**——goto / 手动 finalize 对进行中批次零感知(R1/R2)。
+2. **"防呆"序列化器有系统性漏网**——写出的 front matter / book.yaml / 表格自己读不回(R7/R8/R9/R11)。
+3. **迁移书实体在缓存里隐身**——类型值中英文断裂、别名分隔三源分裂、一对多别名硬回滚(R5/R6/R10)。
+
+外加 P2×18 稳健性清扫、S×10 卫生项、3 条 spec 条款回填(G-3/E7/B-S)、测试基建补缺(G-1 gitBookCtx 对齐真实建书、G-6 bin spawn 冒烟)。
+
+## 范围与批次(按 backlog 三批 + 基建前置)
+
+- **批 0(测试基建前置)**:G-1 gitBookCtx 对齐 persistCreateBook 真实仓库形态——R1/R2 的回归测试依赖"工作区不被 git 跟踪"这一真实形态,必须先修脚手架。
+- **批 1(数据安全 + 互斥守卫)**:R1 R2 R3 R4 R7 R8 R12,顺带同文件的 C2/G-4(finalize 清理 `..` 守卫与前缀归一)、C4(stageChapter `..` 判定统一)。
+- **批 2(迁移链)**:R5(+G-3 规范落点) R6(+A13) R9 R10 R11(+A7/A8/A11 表格解析读写对称)。
+- **批 3(P2/S 清扫)**:A6 A9/C3 A10/F-7 A12 A14 A15 A16 A17 B1 B2 B-S E3 E4 E5 E6 E7 E8 E9 D3 D4 D5 D6 F-4 F-5 F-6 F-8 F-9 F-10 G-5 G-7 G-8 G-9 B(imagery_top) + G-6 spawn 冒烟。
+- **收尾**:上轮探针场景固化为回归测试 + 全量测试 + spec/PRD 版本回填。
+
+## Acceptance Criteria
+
+- [x] AC1 上轮 11 条 CONFIRMED P1 的探针场景全部按修复后语义通过:P-1/P-2(goto/手动 finalize 被守卫人话拒绝)、P-3(合法 book.yaml/名册修复被接受)、P-5(迁移书 list-characters 可见)、P-6(全角分隔别名全部命中)、P-7b(commit 后清理失败不改写 ok)、A/F 区序列化器往返用例全绿。
+- [x] AC2 每条 P1(R1-R12)至少一条针对性回归测试进正式测试套件;R12 附故障注入用例。
+- [ ] AC3 全量测试绿(存量 429 + 新增;过时的文案级断言按「测试是探针不是约束」修正),CI 六 job 绿。
+- [x] AC4 spec 侧收口:G-3 名册「类型」列取值约定、E7 批次.json 持久字段口径、B-S 重建事务边界(解析失败=跳过+warning / 完整性违反=硬错回滚)写入 story-repo-spec / database-guidelines,spec 版本号 + 决策表更新。
+- [x] AC5 修复不引入新功能、不改命令面(除新增守卫的人话拒绝路径);REFUTED 4 条不动。
+
+## Out of Scope
+
+- ❌ 批次 git 分支化 / PR 式审稿 / 本地 web 服务(用户已裁决:另行再议)
+- ❌ 批次审稿总单等体验增强(未裁决)
+- ❌ v6 遗产代码(webnovel-writer/ 冻结)
+- ❌ 性能优化(除非构成正确性问题)

+ 195 - 0
.trellis/tasks/07-06-fix-review-round3/probe-after-fix.mjs

@@ -0,0 +1,195 @@
+// 第三轮 review 修复后探针:与 07-05 probe-m1-m7.mjs / probe-followup.mjs 同场景,
+// 断言修复后语义(守卫拒绝/修复接受/角色可见/别名命中/ok 不改写/重号硬错)。
+// 库级直调真实路径(persistCreateBook 建书)。跑完自删临时目录,全 PASS 即收口留档。
+import { promises as fs } from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const V7 = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'v7')
+const m = (p) => import(`file:///${path.join(V7, p).replace(/\\/g, '/')}`)
+
+const { persistCreateBook, persistRepair } = await m('src/state-machine/persist.js')
+const { finalizeChapter } = await m('src/finalize/index.js')
+const { stageChapter, readBatch } = await m('src/staging/index.js')
+const { gotoChapter } = await m('src/state-machine/flows/goto-chapter.js')
+const { determineNextState } = await m('src/state-machine/index.js')
+const { CacheManager } = await m('src/cache/index.js')
+const { EntityReader } = await m('src/storage/adapters/EntityReader.js')
+const { migrateV6 } = await m('src/migrate/index.js')
+const { run: listCharacters } = await m('src/commands/list-characters.js')
+const { run: finalizeCmd } = await m('src/commands/finalize.js')
+const { createGit } = await m('src/finalize/git.js')
+
+const results = []
+const P = (id, pass, detail) => {
+  results.push({ id, pass })
+  console.log(`${pass ? '【PASS】' : '【FAIL】'} ${id}: ${detail}`)
+}
+
+const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-fix-probe-'))
+let n = 0
+
+async function makeBook(extraYaml = '') {
+  const repo = path.join(tmpRoot, `book-${++n}`)
+  await fs.mkdir(repo, { recursive: true })
+  const r = await persistCreateBook({ repoPath: repo, cache: null }, {
+    book: { spec_version: '7.0', 书名: `修复探针${n}`, 类型: '玄幻', 每章目标字数: 3000, 卷规模: 40 },
+    总纲: '# 总纲\n\n## 结局\n主角胜。\n',
+    卷纲: '# 第1卷\n\n第一卷纲要。\n',
+  })
+  if (!r.ok) throw new Error(`建书失败: ${r.error}`)
+  if (extraYaml) {
+    const p = path.join(repo, 'book.yaml')
+    await fs.writeFile(p, (await fs.readFile(p, 'utf8')) + extraYaml, 'utf8')
+  }
+  const cache = new CacheManager(path.join(repo, '.cache', 'index.db'))
+  await cache.ensureReady(repo)
+  return { repoPath: repo, cache }
+}
+
+const payloadOf = (num, extra = {}) => ({
+  chapterNum: num,
+  frontMatter: {
+    章号: num, 标题: `第${num}章题`, 卷: 1, 书内时间: `夏月初${num}`, 字数: 100,
+    章定位: '推进', 钩子: '危机钩-强', 情绪定位: '铺垫',
+  },
+  body: `第${num}章正文内容,情节推进一格。`,
+  summary: `第${num}章摘要。`,
+  commitLines: {},
+  workspaceFiles: [],
+  ...extra,
+})
+
+async function stage(ctx, num, extra = {}) {
+  await fs.mkdir(path.join(ctx.repoPath, '工作区'), { recursive: true })
+  await fs.writeFile(path.join(ctx.repoPath, '工作区', '审稿.md'), `# 第 ${num} 章审稿单\n\n> 共 0 个问题:0 阻断。\n`, 'utf8')
+  const r = await stageChapter(ctx, { chapterNum: num, payload: payloadOf(num, extra) })
+  if (!r.ok) throw new Error(`stage ${num} 失败: ${r.error}`)
+}
+
+// P-1 (R1):批次在场 goto → 人话拒绝,批次原样;无批次时 goto 照常可用
+try {
+  const ctx = await makeBook()
+  for (const c of [1, 2, 3]) {
+    const r = await finalizeChapter(ctx, payloadOf(c))
+    if (!r.ok) throw new Error(`定稿${c}: ${r.error}`)
+  }
+  await stage(ctx, 4)
+  const g = await gotoChapter(ctx, { chapterNum: 2, confirm: true })
+  const batch = await readBatch(ctx.repoPath)
+  const humane = /finalize-batch|batch-discard/.test(g.error || '')
+  P('P-1 goto×批次守卫(R1)', !g.ok && humane && batch.exists,
+    `goto被拒=${!g.ok} 指引人话=${humane} 批次完好=${batch.exists} err=${(g.error || '').slice(0, 80)}`)
+  await ctx.cache.close()
+} catch (e) { P('P-1 goto×批次守卫(R1)', false, `探针异常: ${e.message}`) }
+
+// P-2 (R2):批次在场手动 finalize(命令层)→ 双文案人话拒绝;finalizeChapter 本体不带守卫(finalizeBatch 内部要用)
+try {
+  const ctx = await makeBook()
+  await finalizeChapter(ctx, payloadOf(1))
+  await stage(ctx, 2)
+  const inBatch = await finalizeCmd(['2'], { payload: '不会被读到.json' }, ctx)
+  const outBatch = await finalizeCmd(['9'], { payload: '不会被读到.json' }, ctx)
+  const okIn = !inBatch.ok && /finalize-batch/.test(inBatch.error || '')
+  const okOut = !outBatch.ok && /章号错位|finalize-batch/.test(outBatch.error || '')
+  P('P-2 手动finalize守卫(R2)', okIn && okOut,
+    `批内被拒=${okIn} 批外被拒=${okOut} errIn=${(inBatch.error || '').slice(0, 60)}`)
+  await ctx.cache.close()
+} catch (e) { P('P-2 手动finalize守卫(R2)', false, `探针异常: ${e.message}`) }
+
+// P-3 (R3):book.yaml 坏 → 序0;合法修复被接受;修复后序离开 0
+try {
+  const ctx = await makeBook('卷规模: 40\n') // 追加重复键 → YAML 解析失败
+  const next = await determineNextState(ctx)
+  const failures = next.dto?.failures?.map((f) => f.file) || []
+  const good = 'spec_version: "7.0"\n书名: 探针修复\n类型: 玄幻\n每章目标字数: 3000\n卷规模: 40\n'
+  const rep = await persistRepair(ctx, { repairs: [{ file: 'book.yaml', content: good }] }, { allowedFiles: failures })
+  const after = await determineNextState(ctx)
+  P('P-3 修复分派校验(R3)', next.序 === 0 && failures.includes('book.yaml') && rep.ok && after.序 !== 0,
+    `序0=${next.序 === 0} 修复接受=${rep.ok} 修复后序=${after.序} err=${(rep.error || '').slice(0, 60)}`)
+  await ctx.cache.close()
+} catch (e) { P('P-3 修复分派校验(R3)', false, `探针异常: ${e.message}`) }
+
+// P-4 (B1):坏 front matter 章 → rebuild warning 不再静默;主链由序0 拦(上轮 REFUTED 主链保持)
+try {
+  const ctx = await makeBook()
+  for (const c of [1, 2, 3]) await finalizeChapter(ctx, payloadOf(c))
+  const chDir = path.join(ctx.repoPath, '定稿', '正文')
+  const f3 = (await fs.readdir(chDir)).find((f) => f.startsWith('0003-'))
+  await fs.writeFile(path.join(chDir, f3), '---\n章号: [broken\n---\n正文', 'utf8')
+  const rb = await ctx.cache.rebuildFromSource(ctx.repoPath)
+  const next = await determineNextState(ctx)
+  P('P-4 解析失败有warning(B1)', (rb.warnings || []).length > 0 && next.序 === 0,
+    `rebuild.ok=${rb.ok} warnings=${(rb.warnings || []).length} 条 next序=${next.序}(序0 修复确认拦住重抄)`)
+  await ctx.cache.close()
+} catch (e) { P('P-4 解析失败有warning(B1)', false, `探针异常: ${e.message}`) }
+
+// P-4b (B2):同章号两个合法文件 → UNIQUE 硬错冒泡人话报错,不再静默丢章
+try {
+  const ctx = await makeBook()
+  for (const c of [1, 2, 3]) await finalizeChapter(ctx, payloadOf(c))
+  const chDir = path.join(ctx.repoPath, '定稿', '正文')
+  const f3 = (await fs.readdir(chDir)).find((f) => f.startsWith('0003-'))
+  await fs.copyFile(path.join(chDir, f3), path.join(chDir, '0003-旧稿备份.md'))
+  const rb = await ctx.cache.rebuildFromSource(ctx.repoPath)
+  const errText = (rb.errors || []).join(';') // rebuild 返回 errors 数组(非 error 单数)
+  P('P-4b 重号硬错冒泡(B2)', rb.ok === false && /重复档案|同章号/.test(errText),
+    `rebuild.ok=${rb.ok} err=${errText.slice(0, 80)}`)
+  await ctx.cache.close()
+} catch (e) { P('P-4b 重号硬错冒泡(B2)', false, `探针异常: ${e.message}`) }
+
+// P-5 (R5):sqlite v6 迁移书 → list-characters 可见、entities.type 为英文 machine 值
+try {
+  const workdir = path.join(tmpRoot, 'workdir-g2')
+  await fs.mkdir(path.join(workdir, '.webnovel'), { recursive: true })
+  const { tempV6Sqlite } = await m('test/migrate/_v6.js')
+  const v6 = await tempV6Sqlite()
+  const mig = await migrateV6({ workdir }, v6.v6Path)
+  const repo = path.join(workdir, '潮汐之下')
+  const cache = new CacheManager(path.join(repo, '.cache', 'index.db'))
+  await cache.ensureReady(repo)
+  const lc = await listCharacters([], {}, { repoPath: repo, cache })
+  const rows = await cache.query('SELECT type FROM entities', [])
+  const machineTypes = rows.every((r) => /^[a-z]+$/.test(r.type))
+  P('P-5 迁移书角色可见(R5)', mig.ok && lc.ok && (lc.output || '').includes('江遥') && machineTypes,
+    `migrate.ok=${mig.ok} 含江遥=${(lc.output || '').includes('江遥')} type全machine值=${machineTypes} [${rows.map((r) => r.type).join(',')}]`)
+  await cache.close()
+  await v6.cleanup()
+} catch (e) { P('P-5 迁移书角色可见(R5)', false, `探针异常: ${e.message}`) }
+
+// P-6 (R6):名册全角/顿号混合分隔别名 → resolveAlias 全命中(英文类型恒等收纳兼验)
+try {
+  const ctx = await makeBook()
+  await fs.mkdir(path.join(ctx.repoPath, '定稿', '设定'), { recursive: true })
+  await fs.writeFile(path.join(ctx.repoPath, '定稿', '设定', '名册.md'),
+    '| 正名 | 别名 | 类型 | 首现章 |\n|---|---|---|---|\n| 林晚 | 阿晚,晚儿、小晚 | character | 1 |\n', 'utf8')
+  await ctx.cache.rebuildFromSource(ctx.repoPath)
+  const er = new EntityReader(ctx.repoPath, ctx.cache)
+  const hits = []
+  for (const a of ['阿晚', '晚儿', '小晚']) hits.push((await er.resolveAlias(a)).ok)
+  P('P-6 别名中文分隔全命中(R6)', hits.every(Boolean), `resolveAlias(阿晚/晚儿/小晚)=[${hits}]`)
+  await ctx.cache.close()
+} catch (e) { P('P-6 别名中文分隔全命中(R6)', false, `探针异常: ${e.message}`) }
+
+// P-7/P-7b (R4):workspaceFiles 含目录(带/不带 工作区/ 前缀)→ ok:true + commit 在 + 目录清掉
+try {
+  for (const [id, rel] of [['P-7 目录带前缀', '工作区/评审报告'], ['P-7b 目录裸名', '评审报告']]) {
+    const ctx = await makeBook()
+    await fs.mkdir(path.join(ctx.repoPath, '工作区', '评审报告'), { recursive: true })
+    await fs.writeFile(path.join(ctx.repoPath, '工作区', '评审报告', 'a.md'), 'x', 'utf8')
+    const r = await finalizeChapter(ctx, payloadOf(1, { workspaceFiles: [rel] }))
+    const committed = await createGit(ctx.repoPath).findChapterCommit(1)
+    const dirLeft = await fs.access(path.join(ctx.repoPath, '工作区', '评审报告')).then(() => true, () => false)
+    P(`${id}(R4)`, r.ok && !!committed && !dirLeft,
+      `finalize.ok=${r.ok} commit=${committed ? '在' : '无'} 目录残留=${dirLeft} warnings=${(r.warnings || []).length}`)
+    await ctx.cache.close()
+  }
+} catch (e) { P('P-7 finalize清理(R4)', false, `探针异常: ${e.message}`) }
+
+console.log('\n===== 修复后探针汇总 =====')
+for (const r of results) console.log(`${r.pass ? 'PASS' : 'FAIL'} | ${r.id}`)
+const failed = results.filter((r) => !r.pass).length
+console.log(failed ? `\n${failed} 项未过` : '\n全部通过')
+await fs.rm(tmpRoot, { recursive: true, force: true })
+process.exitCode = failed ? 1 : 0

+ 26 - 0
.trellis/tasks/07-06-fix-review-round3/task.json

@@ -0,0 +1,26 @@
+{
+  "id": "fix-review-round3",
+  "name": "fix-review-round3",
+  "title": "M1-M7 第三轮 review 修复(P1×12/P2×18/S×10)",
+  "description": "",
+  "status": "in_progress",
+  "dev_type": null,
+  "scope": null,
+  "package": null,
+  "priority": "P2",
+  "creator": "codex",
+  "assignee": "codex",
+  "createdAt": "2026-07-06",
+  "completedAt": null,
+  "branch": null,
+  "base_branch": "v7",
+  "worktree_path": null,
+  "commit": null,
+  "pr_url": null,
+  "subtasks": [],
+  "children": [],
+  "parent": null,
+  "relatedFiles": [],
+  "notes": "",
+  "meta": {}
+}

+ 16 - 4
docs/architecture/story-repo-spec-2026-06-10.md

@@ -1,6 +1,10 @@
-# Story Repo 格式规格(v7 草案 0.13
+# Story Repo 格式规格(v7 草案 0.14
 
-> 状态:0.13。相对 0.12 的变更(2026-07-05 M7 导出与迁移落地,决策 38 见 §14):
+> 状态:0.14。相对 0.13 的变更(2026-07-07 第三轮 review 修复收口,决策 39 见 §14):
+> - §4.5 名册「类型」列取值约定(G-3):名册文件(作者域)写中文词表,缓存 entities.type(机器域)存英文值,入库点统一归一
+> - §8.1 批次.json 持久字段口径(E7):持久面只有章列表,起章与连续无条目变动计数为派生量;停止判据附注(E9):以章 front matter 声明为准,宿主须保证 payload 与声明一致
+>
+> 0.13 相对 0.12 的变更(2026-07-05 M7 导出与迁移落地,决策 38 见 §14):
 > - §4.7 导出实现口径:`.txt`、单章无标题行、范围/全书合并文件以「第N章 标题」行分章(默认形态,beta 期可按作者反馈调整)
 > - §12 迁移实现注记:v6 双形态读取(state.json 全量内联 / 5.4 精简+index.db)、"人工过一遍再入"落「迁移待校对-」前缀文件、整体回退=临时目录建仓成功才落位、丢弃项进报告如实列出
 >
@@ -269,6 +273,8 @@ front matter 是机检消费的结构化字段(平铺、块列表);正文
 
 防"同人异名/同名异人"。表:`| 正名 | 别名 | 类型 | 首现章 |`。机检在定稿前比对正文新专名与名册,未登记的专名进审稿单请作者确认(新实体 or 笔误)。
 
+「类型」列取值(0.14 决策 39):名册是作者域,写中文——`角色`/`地点`/`组织`/`物品`;缓存 `entities.type`(机器域)存对应英文值 `character`/`location`/`organization`/`item`,入库点统一归一(英文写法恒等收纳;未知值原样入库并 warning,不硬拒作者的自造分类)。「别名」列多值分隔:顿号或逗号(中英文逗号均可)。
+
 ### 4.6 摘要层 `定稿/摘要/`
 
 - `章摘要/NNNN.md`:≤200 字,定稿前由模型生成、**放进审稿单供作者扫一眼**(可顺手改),随定稿入档。
@@ -450,10 +456,10 @@ ch(152): 北境的雪
 **全自动 ≠ 无控制,是控制上移到大纲层**:作者逐卷确认卷纲,一次确认管几十章。**自动模式与手动模式的差异只是确认粒度:逐章 vs 按批次**——状态机与八阶段零改动,没有"自动定稿"。
 
 - `自动确认细纲: true` → 第 2 步默认采纳细纲提案(提案必须含定位声明)。
-- **批次运行**:连写 `连写批次大小`(默认 8)章。每章走完 1-6 步后**不定稿**,暂存进 `工作区/待定稿/NNNN-标题/` 三件套:`草稿.md`(front matter+正文终稿,防呆方言序列化)、`定稿包.json`(完整定稿 payload,预登记的机器形态——**暂存的就是定稿时原样转正的数据,无第二套入档路径**)、`审稿单.md`(该章两审产物)。批次元数据 `待定稿/批次.json`(机器域 JSON):起章、各章 `{章号, 标题, 状态}`、连续无条目变动计数。**暂存前置:该章审稿单必须已生成**——自动模式砍掉的是"逐章问作者",不是"逐章审";两审既是质量闸门也是批量审稿的呈报材料。
+- **批次运行**:连写 `连写批次大小`(默认 8)章。每章走完 1-6 步后**不定稿**,暂存进 `工作区/待定稿/NNNN-标题/` 三件套:`草稿.md`(front matter+正文终稿,防呆方言序列化)、`定稿包.json`(完整定稿 payload,预登记的机器形态——**暂存的就是定稿时原样转正的数据,无第二套入档路径**)、`审稿单.md`(该章两审产物)。批次元数据 `待定稿/批次.json`(机器域 JSON):持久面只有章列表(各章 `{章号, 标题, 状态}`);起章与连续无条目变动计数是派生量——起章=章列表首项,计数由停止判定从暂存章 front matter 就地重算,不落盘防双源漂移(0.14 决策 39)。**暂存前置:该章审稿单必须已生成**——自动模式砍掉的是"逐章问作者",不是"逐章审";两审既是质量闸门也是批量审稿的呈报材料。
 - **叠加视图与版本链**(规范性要求,RFC 决策 A1):写批内后续章时,备料/审稿输入/机检组装"`定稿/` + 待定稿批次预登记"的叠加视图——批内前章的设定变更、条目推进、时间线行在工作区就位,定稿时原样转正。**支持批次内依赖**:第 K+1 章可以使用第 K 章的预登记事实。staged 数据只存工作区文件,**不进缓存表、不进指纹与意象派生物**(缓存永远只派生自定稿,删缓存重建叠加输出不变)。
 - **污染传播**(批次状态机,三态 `待审收`/`打回`/`受影响`):第 K 章被作者打回 → K 置"打回"并清工件(重写后重新暂存覆盖),K+1..N 置"受影响"(工件保留,作者裁量**重审或重写**:重审=重跑两审后收回待审收,重写=重走写章流程后覆盖暂存)。**批量定稿入口硬校验:任一章状态非"待审收"即整批拒绝并人话列障碍**——"批内污染不出批次"由入口保证,不靠宿主自觉。
-- **停止条件**(命中即停转人工,全部零 token 脚本判定,逐章暂存时判、早停少浪费):批次写满 / 批次质检不过线 / 卷纲耗尽或收卷声明(**绝不让模型裸奔编纲**;收卷后归卷复盘)/ 连续 `连写无条目变动上限`(默认 3)章无条目变动。**批次质检**即"体检不过线"的批次落点,判据:批尾连续弱钩 ≥ `连续弱钩上限`、任一 staged 章缺「书内时间」、staged 正文句式 vs 基线指纹超容差(口径同机检:平均句长 30%/句长方差 50%);例行体检(§9)仍只算定稿,在批量定稿成功后例行跑。
+- **停止条件**(命中即停转人工,全部零 token 脚本判定,逐章暂存时判、早停少浪费):批次写满 / 批次质检不过线 / 卷纲耗尽或收卷声明(**绝不让模型裸奔编纲**;收卷后归卷复盘)/ 连续 `连写无条目变动上限`(默认 3)章无条目变动。**批次质检**即"体检不过线"的批次落点,判据:批尾连续弱钩 ≥ `连续弱钩上限`、任一 staged 章缺「书内时间」、staged 正文句式 vs 基线指纹超容差(口径同机检:平均句长 30%/句长方差 50%);例行体检(§9)仍只算定稿,在批量定稿成功后例行跑。停止判据(收卷/条目变动/书内时间)一律以暂存章 **front matter 声明**为准,宿主须保证定稿包 payload 与声明一致——两者分叉时以声明面裁定(0.14 决策 39)。
 - **批量审稿与定稿**:批次停止 → 呈报作者(各章状态、审稿单要点、停止原因、质检结论)→ 作者裁决(整批接受 / 改某几章重审 / 从某章起打回重写)→ 作者敲定后**逐章按序原子定稿**(每章独立 commit;中途失败停在该章,已入档保留——原子性按章保留;支持只转正前段"前几章先发")→ 下一批次。批内错误未定稿,直接改,影响最多波及一个批次。
 - **整批不要 → 丢弃批次**:批次是工作区文件(未入 git),丢弃=删除待定稿目录,定稿零变化,需作者明确确认;"回到第 N 章"(§9)管**已定稿**回退,两者职责不混。已发布后才发现的错误 → 影响分析 + 顺势圆(§9)。
 
@@ -566,6 +572,12 @@ v6 的 8 个命令全部内化为以上状态。作者只需要一个入口和"
 
 ## 14. 决策记录
 
+### 0.13 → 0.14(依据:2026-07-06/07 第三轮全量 review 修复收口)
+
+| # | 变更 | 落点 | 来源 |
+|---|------|------|------|
+| 39 | 第三轮 review 规范面收口:名册「类型」列值域钉死——名册文件(作者域)写中文(角色/地点/组织/物品),缓存 entities.type(机器域)存英文(character/location/organization/item),入库点 `normalizeEntityType` 统一归一(英文恒等收纳、未知值原样+warning);「别名」列分隔口径钉死(顿号/中英文逗号,`splitAliases` 单源);批次.json 持久面=章列表(起章/连续无条目变动计数为派生量,不落盘防双源漂移);停止判据以暂存章 front matter 声明为准,宿主须保证定稿包 payload 与声明一致 | §4.5、§8.1 | 第三轮 review(`.trellis/tasks/07-05-m1-m7-review/`)G-3/E7/E9;修复任务 07-06 决策 D1 |
+
 ### 0.12 → 0.13(依据:2026-07-05 M7 导出与迁移落地)
 
 | # | 变更 | 落点 | 来源 |

+ 1 - 1
v7/skills/webnovel-writer/SKILL.md

@@ -36,7 +36,7 @@ SessionStart 已注入「当前在写哪本 / 共几本 / 全书近况入口」
    兼容模式——按 `事实审查`、`编辑审` 两份任务书顺序自审,`mode` 填 `degraded`。
 {{/unless}}
    两份报告合成 `{"事实审查","编辑审","mode","待确认新专名","章摘要"}`,运行 `{{cmd}} save-review <章号> --file=<json路径>`;审稿单落 `工作区/审稿.md`,交作者:接受 / 改完接受 / 打回。
-4. 定稿:作者敲定后组定稿包(`frontMatter`、`body`、`summary`、`threadCreates`(本章「埋下/设下/开启」的新条目,`{id, 短题, frontMatter, body}`)、`threadUpdates`、`characterUpdates`、`rosterUpserts`、`timelineRows`、`secretWrites`、`commitLines`、`workspaceFiles`——本章用过的工作区文件全列进 `workspaceFiles`),运行 `{{cmd}} finalize <章号> --payload=<json路径>`,再运行 `{{cmd}} next --json` 进下一步。
+4. 定稿:作者敲定后组定稿包(`frontMatter`、`body`、`summary`、`threadCreates`(本章「埋下/设下/开启」的新条目,`{id, 短题, frontMatter, body}`)、`threadUpdates`、`characterUpdates`、`rosterUpserts`(`类型` 用中文:`角色`/`地点`/`组织`/`物品`)、`timelineRows`、`secretWrites`、`commitLines`、`workspaceFiles`——本章用过的工作区文件全列进 `workspaceFiles`),运行 `{{cmd}} finalize <章号> --payload=<json路径>`,再运行 `{{cmd}} next --json` 进下一步。
 
 ## 自动模式(连写,作者说「连写/挂机写一批」才进入)
 1. 批内每章走写章流程 1-3;`next --json` 返回 `dto.自动确认细纲 = true` 时细纲提案直接 `{{cmd}} persist-outline` 生效,不问作者。