probe-m1-m7.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // M1-M7 review 行为探针:库级直调真实路径(persistCreateBook 建书,非测试脚手架),
  2. // 逐条裁决候选 CONFIRMED / REFUTED。跑完自删临时目录。
  3. import { promises as fs } from 'node:fs'
  4. import os from 'node:os'
  5. import path from 'node:path'
  6. import { fileURLToPath } from 'node:url'
  7. const V7 = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'v7')
  8. const m = (p) => import(`file:///${path.join(V7, p).replace(/\\/g, '/')}`)
  9. const { persistCreateBook, persistRepair } = await m('src/state-machine/persist.js')
  10. const { finalizeChapter } = await m('src/finalize/index.js')
  11. const { stageChapter, finalizeBatch, readBatch, stagedFacts, overlayBookStatus } = await m('src/staging/index.js')
  12. const { gotoChapter } = await m('src/state-machine/flows/goto-chapter.js')
  13. const { determineNextState } = await m('src/state-machine/index.js')
  14. const { CacheManager } = await m('src/cache/index.js')
  15. const { assembleBookStatus } = await m('src/prep/book-status.js')
  16. const { EntityReader } = await m('src/storage/adapters/EntityReader.js')
  17. const { migrateV6 } = await m('src/migrate/index.js')
  18. const { run: listCharacters } = await m('src/commands/list-characters.js')
  19. const verdicts = []
  20. const V = (id, confirmed, detail) => {
  21. verdicts.push({ id, confirmed, detail })
  22. console.log(`${confirmed ? '【CONFIRMED】' : '【REFUTED】'} ${id}: ${detail}`)
  23. }
  24. const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-review-probe-'))
  25. let n = 0
  26. /** 真实建书:persistCreateBook + 独立 cache(不用 gitBookCtx——G-1 指其形态失真)。 */
  27. async function makeBook(extraYaml = '') {
  28. const repo = path.join(tmpRoot, `book-${++n}`)
  29. await fs.mkdir(repo, { recursive: true })
  30. const ctx0 = { repoPath: repo, cache: null }
  31. const r = await persistCreateBook(ctx0, {
  32. book: { spec_version: '7.0', 书名: `探针${n}`, 类型: '玄幻', 每章目标字数: 3000, 卷规模: 40 },
  33. 总纲: '# 总纲\n\n## 结局\n主角胜。\n',
  34. 卷纲: '# 第1卷\n\n第一卷纲要。\n',
  35. })
  36. if (!r.ok) throw new Error(`建书失败: ${r.error}`)
  37. if (extraYaml) {
  38. const p = path.join(repo, 'book.yaml')
  39. await fs.writeFile(p, (await fs.readFile(p, 'utf8')) + extraYaml, 'utf8')
  40. }
  41. const cache = new CacheManager(path.join(repo, '.cache', 'index.db'))
  42. await cache.ensureReady(repo)
  43. return { repoPath: repo, cache }
  44. }
  45. const payloadOf = (num, extra = {}) => ({
  46. chapterNum: num,
  47. frontMatter: {
  48. 章号: num, 标题: `第${num}章题`, 卷: 1, 书内时间: `夏月初${num}`, 字数: 100,
  49. 章定位: '推进', 钩子: '危机钩-强', 情绪定位: '铺垫',
  50. },
  51. body: `第${num}章正文内容,情节推进一格。`,
  52. summary: `第${num}章摘要。`,
  53. commitLines: {},
  54. workspaceFiles: [],
  55. ...extra,
  56. })
  57. async function stage(ctx, num, extra = {}) {
  58. await fs.mkdir(path.join(ctx.repoPath, '工作区'), { recursive: true })
  59. await fs.writeFile(path.join(ctx.repoPath, '工作区', '审稿.md'), `# 第 ${num} 章审稿单\n\n> 共 0 个问题:0 阻断。\n`, 'utf8')
  60. const r = await stageChapter(ctx, { chapterNum: num, payload: payloadOf(num, extra) })
  61. if (!r.ok) throw new Error(`stage ${num} 失败: ${r.error}`)
  62. }
  63. // ============ P-1 (E1/D2): goto 回退 × 进行中批次 → 孤儿批次 + 定稿章号缺口 ============
  64. try {
  65. const ctx = await makeBook()
  66. for (const c of [1, 2, 3]) {
  67. const r = await finalizeChapter(ctx, payloadOf(c))
  68. if (!r.ok) throw new Error(`定稿${c}: ${r.error}`)
  69. }
  70. await stage(ctx, 4)
  71. const g = await gotoChapter(ctx, { chapterNum: 2, confirm: true })
  72. const batch = await readBatch(ctx.repoPath)
  73. const next = await determineNextState(ctx)
  74. const fb = await finalizeBatch(ctx)
  75. const files = (await fs.readdir(path.join(ctx.repoPath, '定稿', '正文'))).sort()
  76. const nums = files.map((f) => Number(f.slice(0, 4)))
  77. const hasGap = fb.ok && nums.includes(4) && !nums.includes(3)
  78. V('P-1 goto×批次(E1)', g.ok && batch.exists && hasGap,
  79. `goto2=${g.ok} 批次残留=${batch.exists} next序=${next.序} finalizeBatch.ok=${fb.ok} 定稿=[${nums}]${hasGap ? ' ← 缺第3章,断档确认' : ''}`)
  80. await ctx.cache.close()
  81. } catch (e) { V('P-1 goto×批次(E1)', false, `探针异常: ${e.message}`) }
  82. // ============ P-2 (E2): 批次进行中手动 finalize 同章 → 双计 + finalize-batch 卡死 ============
  83. try {
  84. const ctx = await makeBook()
  85. await finalizeChapter(ctx, payloadOf(1))
  86. await stage(ctx, 2, { threadCreates: [{ id: '伏笔-201', 短题: '试线', frontMatter: { 强度: '中', 状态: '进行', 开启章: 2 }, body: '## 描述\n试。\n\n## 收尾计划\n第9章。\n\n## 履历\n- 第2章:埋下\n' }] })
  87. const manual = await finalizeChapter(ctx, payloadOf(2, { threadCreates: [{ id: '伏笔-201', 短题: '试线', frontMatter: { 强度: '中', 状态: '进行', 开启章: 2 }, body: '## 描述\n试。\n\n## 收尾计划\n第9章。\n\n## 履历\n- 第2章:埋下\n' }] }))
  88. const status = await assembleBookStatus(ctx)
  89. const facts = await stagedFacts(ctx.repoPath)
  90. const overlaid = overlayBookStatus(status.data ?? status, facts)
  91. const 总章数 = overlaid?.总章数 ?? overlaid?.data?.总章数
  92. const fb = await finalizeBatch(ctx)
  93. V('P-2 手动finalize×批次(E2)', manual.ok && !fb.ok,
  94. `手动finalize2.ok=${manual.ok} overlay总章数=${总章数}(定稿2+staged1=双计) finalizeBatch.ok=${fb.ok} err=${(fb.error || '').slice(0, 60)}`)
  95. await ctx.cache.close()
  96. } catch (e) { V('P-2 手动finalize×批次(E2)', false, `探针异常: ${e.message}`) }
  97. // ============ P-3 (D1): persistRepair 修 book.yaml 被 front matter 校验拒绝 ============
  98. try {
  99. const ctx = await makeBook('卷规模: 40\n') // 追加重复键 → YAML 解析失败
  100. const next = await determineNextState(ctx)
  101. const failures = next.dto?.failures?.map((f) => f.file) || []
  102. const good = 'spec_version: "7.0"\n书名: 探针修复\n类型: 玄幻\n每章目标字数: 3000\n卷规模: 40\n'
  103. const rep = await persistRepair(ctx, { repairs: [{ file: 'book.yaml', content: good }] }, { allowedFiles: failures })
  104. V('P-3 persistRepair锁死(D1)', next.序 === 0 && failures.includes('book.yaml') && !rep.ok,
  105. `序=${next.序} failures=[${failures}] 合法修复被拒=${!rep.ok} err=${(rep.error || '').slice(0, 60)}`)
  106. await ctx.cache.close()
  107. } catch (e) { V('P-3 persistRepair锁死(D1)', false, `探针异常: ${e.message}`) }
  108. // ============ P-4 (B1): 坏章 rebuild 静默截断 → MAX 偏小 → next 重抄本章 ============
  109. try {
  110. const ctx = await makeBook()
  111. for (const c of [1, 2, 3]) await finalizeChapter(ctx, payloadOf(c))
  112. const chDir = path.join(ctx.repoPath, '定稿', '正文')
  113. const f3 = (await fs.readdir(chDir)).find((f) => f.startsWith('0003-'))
  114. await fs.writeFile(path.join(chDir, f3), '---\n章号: [broken\n---\n正文', 'utf8')
  115. const rb = await ctx.cache.rebuildFromSource(ctx.repoPath)
  116. const max = (await ctx.cache.query('SELECT MAX(chapter_num) AS m FROM chapters', []))[0].m
  117. const next = await determineNextState(ctx)
  118. V('P-4 rebuild静默截断(B1)', rb.ok !== false && !(rb.warnings || []).length && max === 2 && next.dto?.nextChapter === 3,
  119. `rebuild.ok=${rb.ok} warnings=${(rb.warnings || []).length} MAX=${max} next序=${next.序} nextChapter=${next.dto?.nextChapter}(=3 即重抄已存在第3章)`)
  120. await ctx.cache.close()
  121. } catch (e) { V('P-4 rebuild静默截断(B1)', false, `探针异常: ${e.message}`) }
  122. // ============ P-5 (G2): migrate 名册中文类型 → 迁移书角色全不可见 ============
  123. try {
  124. const workdir = path.join(tmpRoot, 'workdir-g2')
  125. await fs.mkdir(path.join(workdir, '.webnovel'), { recursive: true })
  126. const { tempV6Sqlite } = await m('test/migrate/_v6.js')
  127. const v6 = await tempV6Sqlite()
  128. const mig = await migrateV6({ workdir }, v6.v6Path)
  129. const repo = path.join(workdir, '潮汐之下')
  130. const cache = new CacheManager(path.join(repo, '.cache', 'index.db'))
  131. await cache.ensureReady(repo)
  132. const ctx = { repoPath: repo, cache }
  133. const lc = await listCharacters([], {}, ctx)
  134. const rows = await cache.query("SELECT id, type FROM entities", [])
  135. const invisible = lc.ok && !(lc.output || '').includes('江遥')
  136. V('P-5 迁移角色不可见(G2)', mig.ok && invisible,
  137. `migrate.ok=${mig.ok} list-characters含江遥=${!invisible} entities.type=[${rows.map((r) => r.type).join(',')}]`)
  138. await cache.close()
  139. await v6.cleanup()
  140. } catch (e) { V('P-5 迁移角色不可见(G2)', false, `探针异常: ${e.message}`) }
  141. // ============ P-6 (A5 抽查): 名册中文分隔别名 → resolveAlias 全 MISS ============
  142. try {
  143. const ctx = await makeBook()
  144. await fs.mkdir(path.join(ctx.repoPath, '定稿', '设定'), { recursive: true })
  145. await fs.writeFile(path.join(ctx.repoPath, '定稿', '设定', '名册.md'),
  146. '| 正名 | 别名 | 类型 | 首现章 |\n|---|---|---|---|\n| 林晚 | 阿晚,晚儿、小晚 | character | 1 |\n', 'utf8')
  147. await ctx.cache.rebuildFromSource(ctx.repoPath)
  148. const er = new EntityReader(ctx.repoPath, ctx.cache)
  149. const hits = []
  150. for (const a of ['阿晚', '晚儿', '小晚']) hits.push((await er.resolveAlias(a)).ok)
  151. V('P-6 别名中文分隔(A5)', hits.every((h) => !h), `resolveAlias(阿晚/晚儿/小晚)=[${hits}](全 false 即全 MISS)`)
  152. await ctx.cache.close()
  153. } catch (e) { V('P-6 别名中文分隔(A5)', false, `探针异常: ${e.message}`) }
  154. // ============ P-7 (C1): finalize workspaceFiles 含目录 → 已 commit 却误报失败 ============
  155. try {
  156. const ctx = await makeBook()
  157. await fs.mkdir(path.join(ctx.repoPath, '工作区', '评审报告'), { recursive: true })
  158. await fs.writeFile(path.join(ctx.repoPath, '工作区', '评审报告', 'a.md'), 'x', 'utf8')
  159. const r = await finalizeChapter(ctx, payloadOf(1, { workspaceFiles: ['工作区/评审报告'] }))
  160. const { createGit } = await m('src/finalize/git.js')
  161. const git = createGit(ctx.repoPath)
  162. const committed = await git.findChapterCommit(1)
  163. V('P-7 finalize误报失败(C1)', !r.ok && !!committed,
  164. `finalize.ok=${r.ok} err=${(r.error || '').slice(0, 50)} 但 ch(1) commit=${committed ? '已存在' : '无'}`)
  165. await ctx.cache.close()
  166. } catch (e) { V('P-7 finalize误报失败(C1)', false, `探针异常: ${e.message}`) }
  167. // ============ 汇总 ============
  168. console.log('\n===== 裁决汇总 =====')
  169. for (const v of verdicts) console.log(`${v.confirmed ? 'CONFIRMED' : 'REFUTED '} | ${v.id}`)
  170. await fs.rm(tmpRoot, { recursive: true, force: true })