M3 是编排层:单入口状态机先跑 git 健康检查,再按 §10 序 0-6 命中即停判定「当前该做什么」。状态机只路由、不判业务、不调 AI——AI 态备好 DTO 返回「需 AI」交 M4(架构原则 §1.5)。
执行体分裂:
needsAI。模块落点(填 M0 占位 v7/src/state-machine/):
v7/src/state-machine/
├── index.js # determineNextState(ctx):git健康 → 序0-6 路由
├── git-health.js # 5 类异常检测 + 激进自动修复(D2,可恢复安全网)
├── detectors.js # 序0-6 各态检测(命中即停)
├── flows/
│ ├── impact.js # 影响分析(grep 正文+履历+时间线)
│ ├── goto-chapter.js # 回到第N章(git 回滚包装 + 影响范围)
│ └── retcon.js # 吃书(retcon commit + 设定/条目同步)
└── dto.js # AI 态上下文 DTO 组装
复用 M1(grep-story/reports/cache)、M2(finalize/git.js 扩展、assembleBookStatus)。
// determineNextState(ctx) → 作者的「继续」按钮背后
// @param {{repoPath, cache}} ctx
// @returns {Promise<{
// ok, gitHealth: {fixed:[], guidance:[], rescued:[]},
// 序: 0..6, state: string, needsAI: boolean, dto?: object,
// message: string // 中文,告诉作者/宿主当前该做什么
// }>}
CLI 单入口:v7/src/commands/next.js(webnovel-writer next,run 契约)→ 调 determineNextState,打印 message + 必要数据。SessionStart hook 注入留 M4/M5(无 hook 宿主走这个 CLI 等价路径)。
checkGitHealth(ctx) → {ok, fixed:[], guidance:[], rescued:[]}。在序 0 前跑。改 git 状态前必先快照;返回中文小结,作者永不见 git 英文报错(不变量 8)。
| 异常 | 检测 | 处理(B 激进) | 安全网 |
|---|---|---|---|
陈旧锁文件 .git/index.lock |
文件存在且陈旧(mtime 超阈值,无活跃 git 进程迹象) | 自动删 | 删前记 rescued 日志 |
网盘冲突副本 xxx (1).md/xxx 的冲突副本 |
文件名模式匹配 | 自动归档到 工作区/.救援/网盘副本/ |
移动非删除,可找回 |
| 半提交(中断的定稿,工作树脏) | git status --porcelain 非空且非正常流程 |
自动 git stash(push 到备份) |
stash 可 pop 恢复 |
| 合并冲突 | git status 含 UU/MERGING |
先 git stash/备份 ref,再 git merge --abort |
备份 ref 可回 |
.git 损坏 |
git fsck/git status 报错 |
例外:不自动修,检测 + 中文指引(建议备份后 git fsck) |
不动用户仓库 |
实现:git 操作复用并扩展 M2 src/finalize/git.js(加 status/stash/mergeAbort/fsck/config)。每次自动修在 工作区/.救援/修复日志.md 追加一行(修了什么、时间、怎么撤)。
detectors.js 导出按序判定函数,determineNextState 依序调用,首个命中返回。
| 序 | 检测(脚本) | needsAI | dto / 动作 |
|---|---|---|---|
| 0 修复确认 | 扫源文件,任一 parseFrontMatter/parseMarkdownTable 失败;全角冒号/逗号在结构位 = 确定性错误(预修复) |
是(AI 提议保意图修复) | dto: {文件, 行, 上下文, 确定性预修复结果} |
| 1 建书引导 | 无 book.yaml / books.jsonl 当前书不存在 |
是(问答) | 脚本可先建空骨架;dto: {缺什么} |
| 2 手改补登 | git status --porcelain -- 定稿 大纲 非空(未登记手改) |
否 | 动作:提议 fix(...) commit(复用 M2 git) |
| 3 断点续跑 | 工作区/ 有未完成流程(草稿/审稿/待定稿批次存在但未定稿) |
否 | 动作:返回中断阶段,引导续跑 |
| 4 卷复盘 | 最大已定稿章 = 卷末(卷规模整除 / 卷纲耗尽) | 是(对谈) | 脚本清账(本卷开/收/悬了太久);dto 给 AI |
| 5 体检 | 章号到体检周期(book.yaml,默认每 50 章) | 部分 | 脚本:条目活跃率(M1)/时间线孤儿;指纹推 M3+ |
| 6 起草细纲 | 其余 | 是(拟提案) | 脚本:assembleBookStatus(M2);dto 给 AI 拟提案 |
序 0 的「确定性错误预修复」:全角 :, 出现在 YAML 键值结构位 → 脚本可确定性替换为半角后只报不问(spec §10);其余解析失败 → needsAI。
analyzeImpact(ctx, {关键词|实体}):grep-story 正文 + 扫条目履历 + 时间线,列引用章号;按「已发布章号」(book.yaml 已发布到章,默认 0=全未发布)分已发布/未发布两清单。返回结构化,不改文件。gotoChapter(ctx, {chapterNum, confirm}):git log --grep "ch(N):" 定位 commit → 先展示影响范围(该章之后的章/commit)→ confirm=true 才 git reset(先建备份 ref 救援/回退前-<时间>)。不变量 8:作者只说「回到第 N 章」,不碰 git。retcon(ctx, {chapterNum, 原因, 设定变更, 条目变更}):改定稿 + retcon(N): 原因 commit + 设定/条目同步(复用 M2 Writer + finalize 编排范式)。圆设定(AI)留 M4。dto.js:为序 0/1/4/6 + 分支试写组装 AI 所需上下文(不调 AI,只备料)。形如 {state, 上下文片段, 作者可选项, 期望产物}。M4 吃 DTO 出结构化产物,回流由 M3 落盘(M3 提供落盘函数,M4 不碰文件)。M3 测试断言 DTO 字段齐全 + needsAI=true。
git 异常样本库(核心出口):用 mkdtemp + git init 构造每种异常的仓库样本,断言 checkGitHealth 自动修复/归档/指引正确 + 安全网可恢复 + 输出零英文堆栈:
index.lock → 断言自动删 + rescued 记录章节 (1).md → 断言归档到 工作区/.救援/.git 损坏(截断 HEAD)→ 断言只指引、不乱动、message 中文状态机路由:test/state-machine/detectors 构造命中各序 0-6 的仓库,断言 determineNextState 返回正确 序/state/needsAI/dto。命中即停顺序也要测(如同时有手改 + 卷末 → 先报序 2)。
外环流程:影响分析(grep 命中 + 已发布/未发布分组)、回到第N章(reset 到目标 + 备份 ref 存在)、吃书(retcon commit message + 设定同步)。
不变量回归:删 .cache 重建一致、定稿原子不破坏。
.git 损坏的自动修复(D2 例外,只指引)