1
0

review-m1-m4-deep.md 12 KB

M1–M4 深入 Review 报告(2026-06-29)

方法:逐文件精读 73 源文件(非上一份的 grep + 架构断言),交叉 spec 后端规范,跑全量 246 测试复现(绿)。 上一份 review(06-27)的结论(v6 清洁 / 架构自洽 / 246 绿)成立,但漏掉了核心写章循环里的两个 P0——它们正好落在 F1(后半段无 CLI 缝)与测试盲区里。

结论速览

代码风格、v6 清洁、DTO 隔离、确定性生成(drift)这些都没问题。但「备料→机检→两审→定稿→next」这条主循环在真实环境下跑不起来:定稿后缓存不重建且无 rebuild CLI,状态机会重抄最新章;建书流程不 git-init 书仓库。两者都被测试脚手架(预 init git、预落章、手动 rebuild)掩盖。spec 层有 1 处硬矛盾、多处「待增量补充」未闭环。


P0(阻断核心循环)

P0-1 定稿后缓存不重建 + 无 rebuild CLI → next 无限重抄最新章

  • finalizeChaptersrc/finalize/index.js:21)写章、commit、清工作区,全程不碰 cache(ctx 收了 cache? 却从不引用)。定稿后磁盘多了 定稿/正文/0003-...,但 .cache/index.dbMAX(chapter_num) 还是上一章。
  • bin/webnovel-writer.js:85 每条命令只调 cache.ensureReady,而 ensureReadysrc/cache/index.js:21仅在 db 缺失/损坏/空表时重建——db 存在且有非空表就直接复用,无任何陈旧检测
  • 全树 grep:生产路径里没有任何地方在定稿后或磁盘变更后调 rebuildFromSource(只有测试调)。
  • src/commands/没有 rebuild/refresh-cache 命令——宿主连「刷新缓存」的 CLI 都没有,唯一刷新手段是作者手动删 .cache/
  • 后果:定稿第 3 章 → 跑 webnovel-writer next → 缓存仍说 maxChapter=2 → 序6「起草第 3 章细纲」。第 3 章已在定稿里。主循环卡死,每章都要作者手动删缓存。
  • 为何上一份漏了test/finalize/finalize.test.js:96 在定稿后手动 ctx.cache.rebuildFromSource(...)——测试作者知道要重建,所以断言绿;test/state-machine/router.test.jsensureReady 之前就把章文件落盘,从不测「定稿后 next」。F1(无定稿 CLI)使整条 finalize→next 从没被端到端测过。

P0-2 建书流程不 git-init 书仓库 / 不设 core.quotepath / 不写 .gitignore

  • persistCreateBooksrc/state-machine/persist.js:29)只写 book.yaml + 总纲.md + 第01卷.mdgit init、不 git config core.quotepath false、不写 .gitignore
  • 后果链:
    • 新书目录不是 git 仓库 → finalizeChaptergit.add/commit 直接失败 → 每次定稿都回滚报「定稿中断」。
    • .cache/index.db 被当普通文件跟踪/提交,违反 spec database §2.1/2.2(缓存必须 gitignored)。
    • 工作区/ 不在 v7/.gitignore(只 ignore .cache/ node_modules/ dist/)→ 草稿/审稿被跟踪,定稿清工作区后留下「已删除未暂存」脏状态。
  • spec quality-guidelines §3.3 明确「书仓库初始化必须设 git config core.quotepath false」是 CI 硬约束——目前无任何代码落地,归属也不清(建书流程?M5 安装器?)。
  • 为何漏了router.test.js:17-29makeGitBook 手动 git init + git config + 写 .gitignore('.cache/\n工作区/\n'),把这三件事全替生产代码做了。测试绿,生产裸。

P0-1/P0-2 都不是「与 smoke 一起推迟」能解释的——它们是脚本面(M1-M4 范围内)的真实缺口,只是因为 smoke 推迟、没有端到端测试而没暴露。建议进 M5 前先补上,否则 M5 安装器接一个跑不通的循环。


P1(数据完整性 / 安全)

P1-1 缓存重建无事务 → 中途失败留半库

rebuildCachesrc/cache/rebuilder.js:12)DELETE 6 表后逐表 INSERT,BEGIN/COMMITscanEntities 在别名冲突时 return {ok:false}:241),此时 chapters/threads/secrets 已写入、entities/aliases/characters 空——返回失败但库已半填CacheManager 不在失败时重建。与上一份「数据完整性 SOUND」断言冲突。注:注释说「DELETE 五表」实际删 6 表。

P1-2 goto-chapter --confirm 不 stash 未提交改动 → reset --hard 丢手改

src/state-machine/flows/goto-chapter.js:33createBackupRef(只存 HEAD 指针)再 resetHard(hash)git reset --hard 丢弃所有已跟踪文件的未提交改动,rescue ref 不含工作树。若作者有序 2 式未登记手改(定稿/大纲已跟踪且改了),改动被静默抹掉、无法从 rescue ref 找回。该 flow 也不跑 checkGitHealth。应:reset 前先 git stash 或拒绝脏树。

P1-3 finalize 回滚是 tree-scoped 非 write-scoped + clean 未包 try

src/finalize/index.js:112-113 回滚用 git.restore(['定稿/','大纲/']) + git.clean(['定稿/','大纲/'])

  • 范围是整棵 定稿/大纲 子树,不是本次 written 集合。若本次只写第 5 章、但第 4 章有未提交手改,回滚会把第 4 章手改一起抹掉。注释自称「仅 定稿/大纲」是树级不是文件级。
  • git.cleansrc/finalize/git.js:33没有 try/catchrestore 有)。Windows 文件锁使 clean 抛错时,错误逃出 finalizeChapter 的 catch,破坏 {ok,error} 契约,调用方拿到 throw 而非结构化失败。

P2(健壮性 / 正确性,不阻断)

  • P2-1 章标题未做文件名净化ChapterWriter.writeChaptersrc/storage/adapters/ChapterWriter.js:24)直接 ${NNNN}-${标题}.md。标题含 ? * " < > | 或半角冒号时 Windows 写盘失败 → finalize 回滚。标题是 AI DTO,无 sanitize。
  • P2-2 重写同章不同标题留旧文件writeChapter 用 writeFile 覆盖同文件名,标题变了文件名就变,旧文件残留 → scanChapters 两条同 chapter_numINSERT ... PRIMARY KEY 冲突 → 重建失败。
  • P2-3 updateFrontMatter throw / installer 占位:已知 F3,无人依赖,诚实占位,留未来。
  • P2-4 yaml-dialect.needsQuoting 不全src/storage/serializers/yaml-dialect.js:79):只查数字/布尔/null/冒号/#/-/换行。漏 [ { & * ! | > % @ 等首字符 → 值如 [弱钩]{x} 会被 YAML 误判为数组/映射。当前数据形状碰不到,潜伏坑。
  • P2-5 线程更新遇嵌套未知字段必崩ThreadLedgerWriter.updateThreadsrc/storage/adapters/ThreadLedgerWriter.js:29)调 serializeFrontMatter(merged, parsed.body) 不传 originalYAML。merged 含全部原字段,平铺字段能保留;但若作者加了嵌套映射自定义字段,serializeYAML 抛「防呆方言禁止嵌套」→ updateThread 返回 ok:false → finalize 回滚。与 spec §4.5「未知字段必须保留原样写回」直接冲突(见 S2)。
  • P2-6 机检复读仅报首条 + 阈值偏低checkRepetitionsrc/mechanical-check/index.js:100break 在首条命中,漏报其余;L=6/阈值 3 对常见六字短语易误报。checkNewProperNouns 正则 ([一-龥]{2,3})(…|道|说|…) 对「他便笑道」类产生大量非阻断候选噪声(不拦截,仅噪)。
  • P2-7 两审「相关角色」按正名匹配漏别名assembleReviewInputsrc/review/index.js:43)用 草稿全文.includes(name)(name=文件名=正名)。草稿只用别名「大师兄」、文件名「林晚」时漏纳入,审查上下文缺失。
  • P2-8 session.assembleSessionContext 自愈不回写src/session/index.js:57):books.jsonl 损坏时只扫描重建到内存返回,不写回 books.jsonl,下个会话再扫一遍。函数名/注释「自愈」名不副实(spec 说本层只读、M5 写侧——但「自愈」措辞误导,建议改名或 spec 注明)。
  • P2-9 findChapterCommit 依赖 git 默认 BREgit.js:93 --grep=ch(${n}): 能匹配字面 ch(3): 仅因 git 默认 BRE 里 () 是字面。一旦 grep.extendedRegex 配置开启即失效。脆弱但当前正确。
  • P2-10 宿主壳生成器字符串拼接roleToToml/roleToMarkdownsrc/host-shells/generate.js:42-47)直接拼 description = "${role.description}"。description 含 " 或换行即破 TOML/YAML frontmatter。validator 只查源 roles 的 ABS_PATH,不校验生成物格式,drift check 也只比字节相等不比合法。
  • P2-11 validator.ABS_PATH 漏路径src/host-shells/validator.js:9):只匹配 [A-Za-z]:\\/(Users|home)//root//mnt/、裸 /etc/x 都漏。便携性校验有洞。

Spec 层问题

  • S1 表数不符database-guidelines §2.4 列五表(chapters/threads/secrets/entities/fingerprints),实现六表(多了 entity_aliases)。spec 漏列 entity_aliases,文档与代码不一致。
  • S2 硬矛盾:保留未知字段 vs 禁止嵌套:§4.5「未知字段必须保留原样写回」与 §4.1「禁止嵌套映射」+ 防呆序列化器拒嵌套(P2-5)冲突。作者加嵌套自定义字段时无法两全。spec 需明确:未知字段仅限平铺标量/列表,嵌套字段如何处置(拒绝?降级为正文?)。
  • S3 core.quotepath 落地无归属quality §3.3 要求书仓库初始化设 core.quotepath false,但未指定由哪一步负责(建书流程?安装器?),导致无人实现(P0-2)。spec 应钉死责任方。
  • S4 AI 预算上限 pendingquality §2.2「AI 调用每章预算上限(实现时定数)」M4 已过仍未定数,runReviews 固定 2 次调用无上限约束。应在 M4 收口或显式推迟到 beta。
  • S5 多文件原子性豁免缺失error-handling §3.1「所有多文件写入操作必须原子」,但 persistReviewReportsrc/review/index.js:104)写 3 个文件非原子无回滚。spec 未给「工作区多文件写入」豁免边界,要么补豁免、要么补实现。
  • S6 「待增量补充」大量未闭环:database §5(表列定义/增量更新策略)、error §5(错误类型与退出码、git 异常清单)、quality §6(lint 选型、测试覆盖率、退出码约定)自基线 1.0(06-12)至今未填。M1-M4 已落地,这些空白该回填,否则后续任务无据。
  • S7 书仓库工程文件边界含糊directory-structure §3.3「书仓库内零工程文件(唯一例外 AGENTS.md)」未说明 .gitignore 归位,而 .cache/ 必须被 ignore——存在规范真空,导致 P0-2 里 .cache 被跟踪无人挡。
  • S8 版本号未对齐:README 版本徽章 6.2.0(v6/master),v7/package.json0.0.0。发布前须设版本;README 版本表是 CI 硬约束,M5 发版前要对齐。

测试盲区(解释为何 246 绿却藏 P0/P1)

  1. 无 finalize→next 端到端(F1 无定稿 CLI)→ 主循环从未被测。
  2. router.testensureReady 前已落章 → 不测「定稿后 next 陈旧」。
  3. finalize.test 手动 rebuildFromSource → 掩盖「无自动重建」。
  4. makeGitBook 手动 git init+config+.gitignore → 掩盖「建书不 init git」。
  5. goto-chapter.test 未构造「脏工作树 + confirm」用例 → P1-2 不触发。

建议补一条端到端集成:init 书 → 备料 → 机检 → runReviews(桩) → finalize → next,期望 next 报序6 且 nextChapter = 已定稿章 +1。这条一加,P0-1 立刻红。


优先级建议

  1. 先修 P0-1(定稿后重建缓存 + 补 rebuild CLI 或在 next/finalize 入口刷新)+ P0-2(建书流程 git init + .gitignore + core.quotepath)——这是让主循环跑通的前提。
  2. 再补上面那条端到端集成测试,把 P0 锁死。
  3. P1(事务、goto stash、回滚范围+clean try)在 M5 接线前修。
  4. P2 与 S1-S8 可在 M5 期间或单独一个 spec 回填任务里清。