Просмотр исходного кода

feat(v7): M2 P4——写章流程 CLI(prepare-chapter / mechanical-check)

- prepare-chapter <章号>:备料,写出 工作区/本章写作材料.md
- mechanical-check <章号> [--draft=path]:机检,输出 {pass,issues,candidates} JSON
- bin --help 增「写章流程(M2)」段
- finalize-chapter 不做 CLU:payload 结构复杂,是 M3 状态机编排的 Use Case,
  M2 由 finalize 测试程序化验证全程(细纲→定稿原子 commit)
lingfengQAQ 2 дней назад
Родитель
Сommit
8ce3097017

+ 4 - 0
v7/bin/webnovel-writer.js

@@ -45,6 +45,10 @@ if (!command || command === '--help') {
   console.log('  grep-story <关键词> [--regex=<pattern>]')
   console.log('  report-overdue-threads | report-secret-accumulation | report-thread-activity --卷=N')
   console.log('  report-weak-hook-streak | report-book-stats | report-style-drift')
+  console.log('')
+  console.log('写章流程(M2,零 AI 脚本面):')
+  console.log('  prepare-chapter <章号>                  备料:写出 工作区/本章写作材料.md')
+  console.log('  mechanical-check <章号> [--draft=<路径>]  机检:字数/禁词/禁句式/复读/新专名/信息差候选')
   process.exit(0)
 }
 

+ 24 - 0
v7/src/commands/mechanical-check.js

@@ -0,0 +1,24 @@
+import path from 'node:path'
+import { mechanicalCheck } from '../mechanical-check/index.js'
+
+/**
+ * mechanical-check <章号> [--draft=<path>] → JSON {pass, issues, candidates}(机检,零 token)
+ * 默认草稿 工作区/草稿-A.md。契约:纯返回 {ok, output?, error?}(见 design §6.2)。
+ */
+export async function run(args, options, ctx) {
+  const chapterNum = parseInt(args[0], 10)
+  if (isNaN(chapterNum)) {
+    return { ok: false, error: '章号必须是数字' }
+  }
+  const draftPath =
+    options.draft && options.draft !== true
+      ? path.resolve(ctx.repoPath, options.draft)
+      : path.join(ctx.repoPath, '工作区', '草稿-A.md')
+
+  const r = await mechanicalCheck(ctx, { chapterNum, draftPath })
+  if (!r.ok) return { ok: false, error: r.error }
+  return {
+    ok: true,
+    output: JSON.stringify({ pass: r.pass, issues: r.issues, candidates: r.candidates }, null, 2),
+  }
+}

+ 14 - 0
v7/src/commands/prepare-chapter.js

@@ -0,0 +1,14 @@
+import { prepareChapterMaterials } from '../prep/index.js'
+
+/**
+ * prepare-chapter <章号> → 组装并写出 工作区/本章写作材料.md(备料,零 AI)
+ * 契约:纯返回 {ok, output?, error?}(见 design §6.2)。
+ */
+export async function run(args, options, ctx) {
+  const chapterNum = parseInt(args[0], 10)
+  if (isNaN(chapterNum)) {
+    return { ok: false, error: '章号必须是数字' }
+  }
+  const r = await prepareChapterMaterials(ctx, { chapterNum })
+  return r.ok ? { ok: true, output: `已写出 ${r.filePath}` } : { ok: false, error: r.error }
+}

+ 42 - 0
v7/test/commands/mechanical-check.test.js

@@ -0,0 +1,42 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { promises as fs } from 'node:fs'
+import path from 'node:path'
+import { run } from '../../src/commands/mechanical-check.js'
+import { tempBookCtx } from './_helper.js'
+
+test('mechanical-check 输出 pass/issues/candidates JSON', async () => {
+  const { ctx, cleanup } = await tempBookCtx()
+  try {
+    const draft = `---
+章号: 3
+标题: 测
+卷: 1
+字数: 40
+章定位: 推进
+钩子: 危机钩-强
+情绪定位: 铺垫
+---
+林晚立于殿前,握紧令牌,心中默念定要查明旧案,不负师门多年栽培。`
+    await fs.writeFile(path.join(ctx.repoPath, '工作区', '草稿-A.md'), draft, 'utf8')
+
+    const r = await run(['3'], {}, ctx)
+    assert.equal(r.ok, true)
+    const out = JSON.parse(r.output)
+    assert.equal(typeof out.pass, 'boolean')
+    assert.ok(Array.isArray(out.issues))
+    assert.ok(Array.isArray(out.candidates))
+  } finally {
+    await cleanup()
+  }
+})
+
+test('mechanical-check 非数字章号 → ok=false', async () => {
+  const { ctx, cleanup } = await tempBookCtx()
+  try {
+    const r = await run(['abc'], {}, ctx)
+    assert.equal(r.ok, false)
+  } finally {
+    await cleanup()
+  }
+})

+ 27 - 0
v7/test/commands/prepare-chapter.test.js

@@ -0,0 +1,27 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import { promises as fs } from 'node:fs'
+import path from 'node:path'
+import { run } from '../../src/commands/prepare-chapter.js'
+import { tempBookCtx } from './_helper.js'
+
+test('prepare-chapter 写出本章写作材料', async () => {
+  const { ctx, cleanup } = await tempBookCtx()
+  try {
+    const r = await run(['3'], {}, ctx)
+    assert.equal(r.ok, true)
+    await fs.access(path.join(ctx.repoPath, '工作区', '本章写作材料.md'))
+  } finally {
+    await cleanup()
+  }
+})
+
+test('prepare-chapter 非数字章号 → ok=false', async () => {
+  const { ctx, cleanup } = await tempBookCtx()
+  try {
+    const r = await run(['abc'], {}, ctx)
+    assert.equal(r.ok, false)
+  } finally {
+    await cleanup()
+  }
+})