index.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. import { checkGitHealth } from './git-health.js'
  2. import { BookConfigReader } from '../storage/adapters/BookConfigReader.js'
  3. import { buildDto } from './dto.js'
  4. import * as d from './detectors.js'
  5. /**
  6. * 状态机单入口(spec §10):先跑 git 健康检查,再按序 0-6 命中即停判定下一步。
  7. * 只路由、不判业务、不调 AI——AI 态返回 needsAI=true + dto 交 M4。
  8. * @param {{repoPath: string, cache: object}} ctx
  9. * @returns {Promise<{ok, gitHealth, 序, state, needsAI, message, dto}>}
  10. */
  11. export async function determineNextState(ctx) {
  12. const { repoPath, cache } = ctx
  13. // 空工作目录(bin 定位后无当前书):没有书仓库可查,直接序1 建书引导(spec §10 序1「工作目录无任何书」)
  14. if (!repoPath) {
  15. return mk(1, 'create-book', true, '工作目录还没有书,进入建书引导。', { fixed: [], guidance: [] }, await buildDto(ctx, 1, {}))
  16. }
  17. const gitHealth = await checkGitHealth(ctx)
  18. // 序0 修复确认(检测=脚本,提议=AI)
  19. const failures = await d.detectParseFailures(repoPath)
  20. if (failures.length) {
  21. return mk(0, 'repair-confirm', true, `检测到 ${failures.length} 个源文件解析失败,需逐个修复确认。`, gitHealth, await buildDto(ctx, 0, { failures }))
  22. }
  23. // 序1 建书引导
  24. if (await d.bookMissing(repoPath)) {
  25. return mk(1, 'create-book', true, '当前目录还没有书,进入建书引导。', gitHealth, await buildDto(ctx, 1, {}))
  26. }
  27. // 序2 手改补登(检测=脚本;补登执行体也是脚本:relink 命令,作者确认后跑)
  28. const manualEdits = await d.listManualEdits(repoPath)
  29. if (manualEdits.length) {
  30. return mk(2, 'relink-manual-edits', false, `定稿/大纲 有 ${manualEdits.length} 处未登记的手改,问作者「补登吗」,确认后运行 relink --message=<一句话说明> 入档。`, gitHealth, await buildDto(ctx, 2, { manualEdits }))
  31. }
  32. // 序3 断点续跑
  33. const unfinished = await d.unfinishedWorkDetail(repoPath)
  34. if (unfinished.现存.length) {
  35. return mk(3, 'resume', false, `工作区有未完成的流程(${unfinished.现存.join('、')}),从「${unfinished.从哪继续}」继续。`, gitHealth, await buildDto(ctx, 3, unfinished))
  36. }
  37. // 序4/5/6 需章号信息
  38. const lastRows = await cache.query(
  39. 'SELECT chapter_num, volume_num, is_volume_end FROM chapters ORDER BY chapter_num DESC LIMIT 1'
  40. )
  41. const last = lastRows[0] || null
  42. const maxChapter = last?.chapter_num || 0
  43. const config = await new BookConfigReader(repoPath).read()
  44. const 体检周期 = (config.ok && config.data.体检周期) || 50
  45. // 序4 卷复盘(收卷声明制,spec 0.9 §10:最新定稿章声明了收卷;复盘完成以卷摘要存在为准,防重复触发。对谈=AI)
  46. if (last && last.is_volume_end && !(await d.volumeReviewDone(repoPath, last.volume_num))) {
  47. return mk(4, 'volume-review', true, `第 ${last.chapter_num} 章已收卷,进入第 ${last.volume_num} 卷复盘。`, gitHealth, await buildDto(ctx, 4, {
  48. 卷: last.volume_num,
  49. }))
  50. }
  51. // 序5 体检(距上次体检 ≥ 体检周期;记录存缓存 meta,丢失重测无害。统计项随 M5.5,最小体检=health-check 命令)
  52. const lastCheck = await readLastHealthCheck(cache)
  53. if (maxChapter > 0 && maxChapter - lastCheck >= 体检周期) {
  54. return mk(5, 'health-check', false, `距上次体检已 ${maxChapter - lastCheck} 章(周期 ${体检周期}),进入体检。`, gitHealth, {})
  55. }
  56. // 序6 起草新章细纲(近况=脚本,拟提案=AI)
  57. return mk(6, 'draft-outline', true, `起草第 ${maxChapter + 1} 章细纲。`, gitHealth, await buildDto(ctx, 6, {
  58. nextChapter: maxChapter + 1,
  59. }))
  60. }
  61. function mk(序, state, needsAI, message, gitHealth, dto) {
  62. return { ok: true, 序, state, needsAI, message, gitHealth, dto }
  63. }
  64. async function readLastHealthCheck(cache) {
  65. try {
  66. const rows = await cache.query("SELECT value FROM meta WHERE key = 'last_health_check_chapter'")
  67. return parseInt(rows[0]?.value || '0', 10) || 0
  68. } catch {
  69. return 0
  70. }
  71. }