novel-writer-cli 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/__tests__/advance-refine-invalidates-eval.test.js +37 -0
  2. package/dist/__tests__/character-voice.test.js +1 -1
  3. package/dist/__tests__/gate-decision.test.js +66 -0
  4. package/dist/__tests__/init.test.js +7 -2
  5. package/dist/__tests__/narrative-health-injection.test.js +8 -8
  6. package/dist/__tests__/next-step-gate-decision-routing.test.js +117 -0
  7. package/dist/__tests__/next-step-prejudge-guardrails.test.js +112 -16
  8. package/dist/__tests__/next-step-title-fix.test.js +64 -8
  9. package/dist/__tests__/orchestrator-state-routing.test.js +168 -0
  10. package/dist/__tests__/orchestrator-state-write-path.test.js +59 -0
  11. package/dist/__tests__/steps-id.test.js +23 -0
  12. package/dist/__tests__/volume-pipeline.test.js +227 -0
  13. package/dist/__tests__/volume-review-pipeline.test.js +112 -0
  14. package/dist/__tests__/volume-review-storyline-rhythm.test.js +19 -0
  15. package/dist/advance.js +145 -48
  16. package/dist/checkpoint.js +71 -12
  17. package/dist/cli.js +202 -8
  18. package/dist/commit.js +1 -0
  19. package/dist/fs-utils.js +18 -3
  20. package/dist/gate-decision.js +59 -0
  21. package/dist/init.js +2 -0
  22. package/dist/instructions.js +322 -24
  23. package/dist/next-step.js +198 -34
  24. package/dist/platform-profile.js +3 -0
  25. package/dist/steps.js +60 -17
  26. package/dist/validate.js +275 -2
  27. package/dist/volume-commit.js +101 -0
  28. package/dist/volume-planning.js +143 -0
  29. package/dist/volume-review.js +448 -0
  30. package/docs/user/novel-cli.md +29 -0
  31. package/package.json +3 -2
  32. package/schemas/platform-profile.schema.json +5 -0
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { advanceCheckpointForStep } from "../advance.js";
7
+ async function exists(absPath) {
8
+ try {
9
+ await stat(absPath);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ test("advanceCheckpointForStep(chapter:refine) invalidates eval and counts polish revisions after judge", async () => {
17
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-advance-refine-"));
18
+ await writeFile(join(rootDir, ".checkpoint.json"), `${JSON.stringify({
19
+ last_completed_chapter: 0,
20
+ current_volume: 1,
21
+ orchestrator_state: "WRITING",
22
+ pipeline_stage: "judged",
23
+ inflight_chapter: 1,
24
+ revision_count: 0
25
+ }, null, 2)}\n`, "utf8");
26
+ await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
27
+ await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "draft text\n", "utf8");
28
+ await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
29
+ await writeFile(join(rootDir, "staging/evaluations/chapter-001-eval.json"), `{"chapter":1,"overall":3.6,"recommendation":"polish"}\n`, "utf8");
30
+ assert.equal(await exists(join(rootDir, "staging/evaluations/chapter-001-eval.json")), true);
31
+ const updated = await advanceCheckpointForStep({ rootDir, step: { kind: "chapter", chapter: 1, stage: "refine" } });
32
+ assert.equal(updated.pipeline_stage, "refined");
33
+ assert.equal(updated.inflight_chapter, 1);
34
+ assert.equal(updated.revision_count, 1);
35
+ assert.equal(updated.orchestrator_state, "CHAPTER_REWRITE");
36
+ assert.equal(await exists(join(rootDir, "staging/evaluations/chapter-001-eval.json")), false);
37
+ });
@@ -407,7 +407,7 @@ test("buildInstructionPacket injects character voice drift directives into draft
407
407
  ]
408
408
  });
409
409
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(占位)\n`);
410
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
410
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
411
411
  const draftOut = (await buildInstructionPacket({
412
412
  rootDir,
413
413
  checkpoint,
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { computeGateDecision, detectHighConfidenceViolation } from "../gate-decision.js";
4
+ test("computeGateDecision maps score bands to decisions (no violations)", () => {
5
+ assert.equal(computeGateDecision({ overall_final: 4.0, revision_count: 0, has_high_confidence_violation: false }), "pass");
6
+ assert.equal(computeGateDecision({ overall_final: 3.9, revision_count: 0, has_high_confidence_violation: false }), "polish");
7
+ assert.equal(computeGateDecision({ overall_final: 3.5, revision_count: 0, has_high_confidence_violation: false }), "polish");
8
+ assert.equal(computeGateDecision({ overall_final: 3.4, revision_count: 0, has_high_confidence_violation: false }), "revise");
9
+ assert.equal(computeGateDecision({ overall_final: 3.0, revision_count: 0, has_high_confidence_violation: false }), "revise");
10
+ assert.equal(computeGateDecision({ overall_final: 2.9, revision_count: 0, has_high_confidence_violation: false }), "pause_for_user");
11
+ assert.equal(computeGateDecision({ overall_final: 2.0, revision_count: 0, has_high_confidence_violation: false }), "pause_for_user");
12
+ assert.equal(computeGateDecision({ overall_final: 1.99, revision_count: 0, has_high_confidence_violation: false }), "pause_for_user_force_rewrite");
13
+ });
14
+ test("computeGateDecision forces revise on high-confidence violations", () => {
15
+ assert.equal(computeGateDecision({ overall_final: 4.8, revision_count: 0, has_high_confidence_violation: true }), "revise");
16
+ });
17
+ test("computeGateDecision pauses for user when high-confidence violations persist beyond max_revisions", () => {
18
+ assert.equal(computeGateDecision({ overall_final: 4.8, revision_count: 2, has_high_confidence_violation: true }), "pause_for_user");
19
+ });
20
+ test("computeGateDecision allows force_passed when revisions exhausted and score >= 3.0", () => {
21
+ assert.equal(computeGateDecision({ overall_final: 3.2, revision_count: 2, has_high_confidence_violation: false }), "force_passed");
22
+ });
23
+ test("computeGateDecision force-passes polish band when revisions exhausted", () => {
24
+ assert.equal(computeGateDecision({ overall_final: 3.6, revision_count: 2, has_high_confidence_violation: false }), "force_passed");
25
+ });
26
+ test("computeGateDecision respects max_revisions override", () => {
27
+ assert.equal(computeGateDecision({ overall_final: 3.6, revision_count: 1, has_high_confidence_violation: false, max_revisions: 1 }), "force_passed");
28
+ });
29
+ test("computeGateDecision supports manual force_pass override", () => {
30
+ assert.equal(computeGateDecision({ overall_final: 1.0, revision_count: 0, has_high_confidence_violation: true, force_pass: true }), "force_passed");
31
+ });
32
+ test("detectHighConfidenceViolation returns false when contract_verification is missing", () => {
33
+ assert.deepEqual(detectHighConfidenceViolation({ overall: 4.0, recommendation: "pass" }), {
34
+ has_high_confidence_violation: false,
35
+ high_confidence_violations: []
36
+ });
37
+ });
38
+ test("detectHighConfidenceViolation detects l1/l2/l3 high-confidence violations", () => {
39
+ const res = detectHighConfidenceViolation({
40
+ contract_verification: {
41
+ l1_checks: [{ status: "violation", confidence: "high", rule: "L1-001" }],
42
+ l2_checks: [],
43
+ l3_checks: []
44
+ }
45
+ });
46
+ assert.equal(res.has_high_confidence_violation, true);
47
+ assert.equal(res.high_confidence_violations.length, 1);
48
+ });
49
+ test("detectHighConfidenceViolation ignores ls_checks soft violations", () => {
50
+ const res = detectHighConfidenceViolation({
51
+ contract_verification: {
52
+ ls_checks: [{ status: "violation", confidence: "high", constraint_type: "soft" }]
53
+ }
54
+ });
55
+ assert.equal(res.has_high_confidence_violation, false);
56
+ });
57
+ test("detectHighConfidenceViolation marks inferred constraint_type for ls_checks when missing", () => {
58
+ const res = detectHighConfidenceViolation({
59
+ contract_verification: {
60
+ ls_checks: [{ status: "violation", confidence: "high" }]
61
+ }
62
+ });
63
+ assert.equal(res.has_high_confidence_violation, true);
64
+ assert.equal(res.high_confidence_violations.length, 1);
65
+ assert.equal(res.high_confidence_violations[0].constraint_type_inferred, true);
66
+ });
@@ -58,15 +58,18 @@ test("initProject creates a runnable skeleton with all checkpoint fields", async
58
58
  assert.equal(result.rootDir, rootDir);
59
59
  // Exact created set (non-minimal = checkpoint + 4 templates)
60
60
  assert.deepEqual(result.created.sort(), [".checkpoint.json", "ai-blacklist.json", "brief.md", "style-profile.json", "web-novel-cliche-lint.json"].sort());
61
- // All 7 staging dirs ensured
62
- assert.equal(result.ensuredDirs.length, 7);
61
+ // All 9 staging dirs ensured
62
+ assert.equal(result.ensuredDirs.length, 9);
63
63
  assert.ok(result.ensuredDirs.includes("staging/chapters"));
64
64
  assert.ok(result.ensuredDirs.includes("staging/manifests"));
65
+ assert.ok(result.ensuredDirs.includes("staging/volumes"));
66
+ assert.ok(result.ensuredDirs.includes("staging/foreshadowing"));
65
67
  // Verify ALL checkpoint fields
66
68
  const checkpoint = await readCheckpoint(rootDir);
67
69
  assert.equal(checkpoint.last_completed_chapter, 0);
68
70
  assert.equal(checkpoint.current_volume, 1);
69
71
  assert.equal(checkpoint.pipeline_stage, "committed");
72
+ assert.equal(checkpoint.volume_pipeline_stage, null);
70
73
  assert.equal(checkpoint.inflight_chapter, null);
71
74
  assert.equal(checkpoint.revision_count, 0);
72
75
  assert.equal(checkpoint.hook_fix_count, 0);
@@ -83,6 +86,8 @@ test("initProject creates a runnable skeleton with all checkpoint fields", async
83
86
  "staging/evaluations",
84
87
  "staging/logs",
85
88
  "staging/storylines",
89
+ "staging/volumes",
90
+ "staging/foreshadowing",
86
91
  "staging/manifests"
87
92
  ]) {
88
93
  await assertDir(join(rootDir, relDir));
@@ -74,7 +74,7 @@ test("buildInstructionPacket injects compact narrative health summaries into dra
74
74
  has_blocking_issues: false
75
75
  });
76
76
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(占位)\n`);
77
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
77
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
78
78
  const draftOut = (await buildInstructionPacket({
79
79
  rootDir,
80
80
  checkpoint,
@@ -126,7 +126,7 @@ test("buildInstructionPacket marks degraded when latest reports exist but are in
126
126
  has_blocking_issues: false
127
127
  });
128
128
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(占位)\n`);
129
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
129
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
130
130
  const out = (await buildInstructionPacket({
131
131
  rootDir,
132
132
  checkpoint,
@@ -143,7 +143,7 @@ test("buildInstructionPacket marks degraded when latest reports exist but are in
143
143
  test("buildInstructionPacket does not inject narrative health when logs are missing (no summary, no degraded)", async () => {
144
144
  const rootDir = await mkdtemp(join(tmpdir(), "novel-narrative-health-no-logs-"));
145
145
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(占位)\n`);
146
- const checkpoint = { last_completed_chapter: 0, current_volume: 1 };
146
+ const checkpoint = { last_completed_chapter: 0, current_volume: 1, orchestrator_state: "WRITING" };
147
147
  const out = (await buildInstructionPacket({
148
148
  rootDir,
149
149
  checkpoint,
@@ -193,7 +193,7 @@ test("buildInstructionPacket does not inject narrative health summaries for stag
193
193
  has_blocking_issues: false
194
194
  });
195
195
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(占位)\n`);
196
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
196
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
197
197
  const summarizeOut = (await buildInstructionPacket({
198
198
  rootDir,
199
199
  checkpoint,
@@ -239,7 +239,7 @@ test("buildInstructionPacket marks degraded on schema_version mismatch when late
239
239
  has_blocking_issues: false
240
240
  });
241
241
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(占位)\n`);
242
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
242
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
243
243
  const out = (await buildInstructionPacket({
244
244
  rootDir,
245
245
  checkpoint,
@@ -279,7 +279,7 @@ test("buildInstructionPacket marks promise ledger degraded on schema_version mis
279
279
  issues: []
280
280
  });
281
281
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(占位)\n`);
282
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
282
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
283
283
  const out = (await buildInstructionPacket({
284
284
  rootDir,
285
285
  checkpoint,
@@ -298,7 +298,7 @@ test("buildInstructionPacket marks both degraded when both latest files are inva
298
298
  await writeText(join(rootDir, "logs/engagement/latest.json"), "not-json");
299
299
  await writeText(join(rootDir, "logs/promises/latest.json"), "not-json");
300
300
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), "# 第1章\n\n(占位)\n");
301
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
301
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
302
302
  const out = (await buildInstructionPacket({
303
303
  rootDir,
304
304
  checkpoint,
@@ -341,7 +341,7 @@ test("buildInstructionPacket treats oversized latest.json as degraded", async ()
341
341
  has_blocking_issues: false
342
342
  });
343
343
  await writeText(join(rootDir, "staging/chapters/chapter-001.md"), "# 第1章\n\n(占位)\n");
344
- const checkpoint = { last_completed_chapter: 10, current_volume: 1 };
344
+ const checkpoint = { last_completed_chapter: 10, current_volume: 1, orchestrator_state: "WRITING" };
345
345
  const out = (await buildInstructionPacket({
346
346
  rootDir,
347
347
  checkpoint,
@@ -0,0 +1,117 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import test from "node:test";
6
+ import { computeNextStep } from "../next-step.js";
7
+ async function writeJson(absPath, payload) {
8
+ await mkdir(dirname(absPath), { recursive: true });
9
+ await writeFile(absPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
10
+ }
11
+ test("computeNextStep routes judged+eval to commit on gate pass", async () => {
12
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-next-step-gate-pass-"));
13
+ await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
14
+ await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "chapter text\n", "utf8");
15
+ await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
16
+ await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 4.0, recommendation: "pass" });
17
+ const next = await computeNextStep(rootDir, {
18
+ last_completed_chapter: 0,
19
+ current_volume: 1,
20
+ orchestrator_state: "WRITING",
21
+ pipeline_stage: "judged",
22
+ inflight_chapter: 1,
23
+ revision_count: 0
24
+ });
25
+ assert.equal(next.step, "chapter:001:commit");
26
+ assert.equal(next.reason, "judged:gate:pass");
27
+ });
28
+ test("computeNextStep routes judged+eval to refine on gate polish", async () => {
29
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-next-step-gate-polish-"));
30
+ await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
31
+ await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "chapter text\n", "utf8");
32
+ await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
33
+ await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 3.6, recommendation: "polish" });
34
+ const next = await computeNextStep(rootDir, {
35
+ last_completed_chapter: 0,
36
+ current_volume: 1,
37
+ orchestrator_state: "WRITING",
38
+ pipeline_stage: "judged",
39
+ inflight_chapter: 1,
40
+ revision_count: 0
41
+ });
42
+ assert.equal(next.step, "chapter:001:refine");
43
+ assert.equal(next.reason, "judged:gate:polish");
44
+ });
45
+ test("computeNextStep routes judged+eval to draft on gate revise", async () => {
46
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-next-step-gate-revise-"));
47
+ await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
48
+ await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "chapter text\n", "utf8");
49
+ await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
50
+ await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 3.2, recommendation: "revise" });
51
+ const next = await computeNextStep(rootDir, {
52
+ last_completed_chapter: 0,
53
+ current_volume: 1,
54
+ orchestrator_state: "WRITING",
55
+ pipeline_stage: "judged",
56
+ inflight_chapter: 1,
57
+ revision_count: 0
58
+ });
59
+ assert.equal(next.step, "chapter:001:draft");
60
+ assert.equal(next.reason, "judged:gate:revise");
61
+ });
62
+ test("computeNextStep routes judged+eval to commit on force_passed when revisions exhausted", async () => {
63
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-next-step-gate-force-passed-"));
64
+ await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
65
+ await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "chapter text\n", "utf8");
66
+ await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
67
+ await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 3.2, recommendation: "revise" });
68
+ const next = await computeNextStep(rootDir, {
69
+ last_completed_chapter: 0,
70
+ current_volume: 1,
71
+ orchestrator_state: "WRITING",
72
+ pipeline_stage: "judged",
73
+ inflight_chapter: 1,
74
+ revision_count: 2
75
+ });
76
+ assert.equal(next.step, "chapter:001:commit");
77
+ assert.equal(next.reason, "judged:gate:force_passed");
78
+ });
79
+ test("computeNextStep routes judged+eval to manual review on pause bands", async () => {
80
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-next-step-gate-pause-"));
81
+ await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
82
+ await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "chapter text\n", "utf8");
83
+ await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
84
+ await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 2.4, recommendation: "pause" });
85
+ const next = await computeNextStep(rootDir, {
86
+ last_completed_chapter: 0,
87
+ current_volume: 1,
88
+ orchestrator_state: "WRITING",
89
+ pipeline_stage: "judged",
90
+ inflight_chapter: 1,
91
+ revision_count: 0
92
+ });
93
+ assert.equal(next.step, "chapter:001:review");
94
+ assert.equal(next.reason, "judged:gate:pause_for_user");
95
+ });
96
+ test("computeNextStep forces revise when eval has high-confidence violations", async () => {
97
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-next-step-gate-violation-"));
98
+ await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
99
+ await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "chapter text\n", "utf8");
100
+ await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
101
+ await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), {
102
+ chapter: 1,
103
+ overall: 4.8,
104
+ recommendation: "pass",
105
+ contract_verification: { l1_checks: [{ status: "violation", confidence: "high" }] }
106
+ });
107
+ const next = await computeNextStep(rootDir, {
108
+ last_completed_chapter: 0,
109
+ current_volume: 1,
110
+ orchestrator_state: "WRITING",
111
+ pipeline_stage: "judged",
112
+ inflight_chapter: 1,
113
+ revision_count: 0
114
+ });
115
+ assert.equal(next.step, "chapter:001:draft");
116
+ assert.equal(next.reason, "judged:gate:revise");
117
+ });
@@ -40,7 +40,13 @@ test("computeNextStep returns review when naming lint has blocking issues", asyn
40
40
  await mkdir(join(rootDir, "characters/active"), { recursive: true });
41
41
  await writeJson(join(rootDir, "characters/active/a.json"), { id: "a", display_name: "张三", aliases: [] });
42
42
  await writeJson(join(rootDir, "characters/active/b.json"), { id: "b", display_name: "张三", aliases: [] });
43
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
43
+ const checkpoint = {
44
+ last_completed_chapter: 0,
45
+ current_volume: 1,
46
+ orchestrator_state: "WRITING",
47
+ pipeline_stage: "judged",
48
+ inflight_chapter: 1
49
+ };
44
50
  const next = await computeNextStep(rootDir, checkpoint);
45
51
  assert.equal(next.step, "chapter:001:review");
46
52
  assert.equal(next.reason, "judged:prejudge_guardrails_blocking:naming_lint");
@@ -61,7 +67,13 @@ test("computeNextStep returns review when readability lint has blocking issues (
61
67
  await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "# 标题\n正文\n", "utf8");
62
68
  await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
63
69
  await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 4, recommendation: "pass" });
64
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
70
+ const checkpoint = {
71
+ last_completed_chapter: 0,
72
+ current_volume: 1,
73
+ orchestrator_state: "WRITING",
74
+ pipeline_stage: "judged",
75
+ inflight_chapter: 1
76
+ };
65
77
  const next = await computeNextStep(rootDir, checkpoint);
66
78
  assert.equal(next.step, "chapter:001:review");
67
79
  assert.equal(next.reason, "judged:prejudge_guardrails_blocking:readability_lint");
@@ -82,7 +94,13 @@ test("buildInstructionPacket (judge) includes prejudge guardrails report path an
82
94
  await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "# 标题\n正文\n", "utf8");
83
95
  await mkdir(join(rootDir, "staging/state"), { recursive: true });
84
96
  await writeJson(join(rootDir, "staging/state/chapter-001-crossref.json"), {});
85
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "refined", inflight_chapter: 1 };
97
+ const checkpoint = {
98
+ last_completed_chapter: 0,
99
+ current_volume: 1,
100
+ orchestrator_state: "WRITING",
101
+ pipeline_stage: "refined",
102
+ inflight_chapter: 1
103
+ };
86
104
  const built = await buildInstructionPacket({
87
105
  rootDir,
88
106
  checkpoint,
@@ -116,7 +134,13 @@ test("computeNextStep returns review on refined stage when naming lint blocks",
116
134
  await mkdir(join(rootDir, "characters/active"), { recursive: true });
117
135
  await writeJson(join(rootDir, "characters/active/a.json"), { id: "a", display_name: "张三", aliases: [] });
118
136
  await writeJson(join(rootDir, "characters/active/b.json"), { id: "b", display_name: "张三", aliases: [] });
119
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "refined", inflight_chapter: 1 };
137
+ const checkpoint = {
138
+ last_completed_chapter: 0,
139
+ current_volume: 1,
140
+ orchestrator_state: "WRITING",
141
+ pipeline_stage: "refined",
142
+ inflight_chapter: 1
143
+ };
120
144
  const next = await computeNextStep(rootDir, checkpoint);
121
145
  assert.equal(next.step, "chapter:001:review");
122
146
  assert.equal(next.reason, "refined:prejudge_guardrails_blocking:naming_lint");
@@ -130,7 +154,13 @@ test("computeNextStep returns draft (not crash) when judged but staging chapter
130
154
  }));
131
155
  await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
132
156
  await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 4, recommendation: "pass" });
133
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
157
+ const checkpoint = {
158
+ last_completed_chapter: 0,
159
+ current_volume: 1,
160
+ orchestrator_state: "WRITING",
161
+ pipeline_stage: "judged",
162
+ inflight_chapter: 1
163
+ };
134
164
  const next = await computeNextStep(rootDir, checkpoint);
135
165
  assert.equal(next.step, "chapter:001:draft");
136
166
  assert.equal(next.reason, "judged:missing_chapter");
@@ -151,7 +181,13 @@ test("computeNextStep tolerates invalid cached prejudge guardrails JSON (recompu
151
181
  await writeJson(join(rootDir, "characters/active/b.json"), { id: "b", display_name: "张三", aliases: [] });
152
182
  await mkdir(join(rootDir, "staging/guardrails"), { recursive: true });
153
183
  await writeFile(join(rootDir, "staging/guardrails/prejudge-guardrails-chapter-001.json"), "{not-json", "utf8");
154
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
184
+ const checkpoint = {
185
+ last_completed_chapter: 0,
186
+ current_volume: 1,
187
+ orchestrator_state: "WRITING",
188
+ pipeline_stage: "judged",
189
+ inflight_chapter: 1
190
+ };
155
191
  const next = await computeNextStep(rootDir, checkpoint);
156
192
  assert.equal(next.step, "chapter:001:review");
157
193
  assert.equal(next.reason, "judged:prejudge_guardrails_blocking:naming_lint");
@@ -170,7 +206,13 @@ test("computeNextStep does not use cached report when platform profile changes (
170
206
  await writeJson(join(rootDir, "characters/active/a.json"), { id: "a", display_name: "张三", aliases: [] });
171
207
  await writeJson(join(rootDir, "characters/active/b.json"), { id: "b", display_name: "张三", aliases: [] });
172
208
  // Generate and persist a cached guardrails report via judge instructions.
173
- const checkpointRefined = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "refined", inflight_chapter: 1 };
209
+ const checkpointRefined = {
210
+ last_completed_chapter: 0,
211
+ current_volume: 1,
212
+ orchestrator_state: "WRITING",
213
+ pipeline_stage: "refined",
214
+ inflight_chapter: 1
215
+ };
174
216
  await buildInstructionPacket({
175
217
  rootDir,
176
218
  checkpoint: checkpointRefined,
@@ -182,7 +224,13 @@ test("computeNextStep does not use cached report when platform profile changes (
182
224
  await writeJson(join(rootDir, "platform-profile.json"), makePlatformProfileRaw({ retention: null, readability: null, naming: null }));
183
225
  await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
184
226
  await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 4, recommendation: "pass" });
185
- const checkpointJudged = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
227
+ const checkpointJudged = {
228
+ last_completed_chapter: 0,
229
+ current_volume: 1,
230
+ orchestrator_state: "WRITING",
231
+ pipeline_stage: "judged",
232
+ inflight_chapter: 1
233
+ };
186
234
  const next = await computeNextStep(rootDir, checkpointJudged);
187
235
  assert.equal(next.step, "chapter:001:commit");
188
236
  });
@@ -201,7 +249,13 @@ test("computeNextStep uses cached prejudge guardrails report when fresh", async
201
249
  await writeJson(join(rootDir, "characters/active/a.json"), { id: "a", display_name: "张三", aliases: [] });
202
250
  await writeJson(join(rootDir, "characters/active/b.json"), { id: "b", display_name: "张三", aliases: [] });
203
251
  // Generate and persist a cached guardrails report via judge instructions.
204
- const checkpointRefined = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "refined", inflight_chapter: 1 };
252
+ const checkpointRefined = {
253
+ last_completed_chapter: 0,
254
+ current_volume: 1,
255
+ orchestrator_state: "WRITING",
256
+ pipeline_stage: "refined",
257
+ inflight_chapter: 1
258
+ };
205
259
  await buildInstructionPacket({
206
260
  rootDir,
207
261
  checkpoint: checkpointRefined,
@@ -211,7 +265,13 @@ test("computeNextStep uses cached prejudge guardrails report when fresh", async
211
265
  });
212
266
  await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
213
267
  await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 4, recommendation: "pass" });
214
- const checkpointJudged = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
268
+ const checkpointJudged = {
269
+ last_completed_chapter: 0,
270
+ current_volume: 1,
271
+ orchestrator_state: "WRITING",
272
+ pipeline_stage: "judged",
273
+ inflight_chapter: 1
274
+ };
215
275
  const next = await computeNextStep(rootDir, checkpointJudged);
216
276
  assert.equal(next.step, "chapter:001:review");
217
277
  assert.equal(next.reason, "judged:prejudge_guardrails_blocking:naming_lint");
@@ -234,7 +294,13 @@ test("computeNextStep ignores cached guardrails report when characters change",
234
294
  await writeJson(join(rootDir, "characters/active/a.json"), { id: "a", display_name: "张三", aliases: [] });
235
295
  await writeJson(join(rootDir, "characters/active/b.json"), { id: "b", display_name: "张三", aliases: [] });
236
296
  // Generate cached report with a blocking duplicate.
237
- const checkpointRefined = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "refined", inflight_chapter: 1 };
297
+ const checkpointRefined = {
298
+ last_completed_chapter: 0,
299
+ current_volume: 1,
300
+ orchestrator_state: "WRITING",
301
+ pipeline_stage: "refined",
302
+ inflight_chapter: 1
303
+ };
238
304
  await buildInstructionPacket({
239
305
  rootDir,
240
306
  checkpoint: checkpointRefined,
@@ -244,7 +310,13 @@ test("computeNextStep ignores cached guardrails report when characters change",
244
310
  });
245
311
  // Fix the duplicate by renaming a character; cache must be ignored.
246
312
  await writeJson(join(rootDir, "characters/active/b.json"), { id: "b", display_name: "李四五", aliases: [] });
247
- const checkpointJudged = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
313
+ const checkpointJudged = {
314
+ last_completed_chapter: 0,
315
+ current_volume: 1,
316
+ orchestrator_state: "WRITING",
317
+ pipeline_stage: "judged",
318
+ inflight_chapter: 1
319
+ };
248
320
  const next = await computeNextStep(rootDir, checkpointJudged);
249
321
  assert.equal(next.step, "chapter:001:commit");
250
322
  });
@@ -266,7 +338,13 @@ test("computeNextStep ignores cached guardrails report when characters/active is
266
338
  await writeJson(join(rootDir, "characters/shared-active/b.json"), { id: "b", display_name: "张三", aliases: [] });
267
339
  await symlink(join(rootDir, "characters/shared-active"), join(rootDir, "characters/active"));
268
340
  // Generate cached report with a blocking duplicate (via judge instructions).
269
- const checkpointRefined = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "refined", inflight_chapter: 1 };
341
+ const checkpointRefined = {
342
+ last_completed_chapter: 0,
343
+ current_volume: 1,
344
+ orchestrator_state: "WRITING",
345
+ pipeline_stage: "refined",
346
+ inflight_chapter: 1
347
+ };
270
348
  const built = await buildInstructionPacket({
271
349
  rootDir,
272
350
  checkpoint: checkpointRefined,
@@ -281,7 +359,13 @@ test("computeNextStep ignores cached guardrails report when characters/active is
281
359
  assert.equal(reportRaw.has_blocking_issues, true);
282
360
  // Fix the duplicate in the symlink target; cache must be ignored.
283
361
  await writeJson(join(rootDir, "characters/shared-active/b.json"), { id: "b", display_name: "李四五", aliases: [] });
284
- const checkpointJudged = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
362
+ const checkpointJudged = {
363
+ last_completed_chapter: 0,
364
+ current_volume: 1,
365
+ orchestrator_state: "WRITING",
366
+ pipeline_stage: "judged",
367
+ inflight_chapter: 1
368
+ };
285
369
  const next = await computeNextStep(rootDir, checkpointJudged);
286
370
  assert.equal(next.step, "chapter:001:commit");
287
371
  });
@@ -293,7 +377,13 @@ test("computeNextStep returns review when guardrails computation errors", async
293
377
  await mkdir(join(rootDir, "staging/chapters/chapter-001.md"), { recursive: true });
294
378
  await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
295
379
  await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1, overall: 4, recommendation: "pass" });
296
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "judged", inflight_chapter: 1 };
380
+ const checkpoint = {
381
+ last_completed_chapter: 0,
382
+ current_volume: 1,
383
+ orchestrator_state: "WRITING",
384
+ pipeline_stage: "judged",
385
+ inflight_chapter: 1
386
+ };
297
387
  const next = await computeNextStep(rootDir, checkpoint);
298
388
  assert.equal(next.step, "chapter:001:review");
299
389
  assert.equal(next.reason, "judged:prejudge_guardrails_error");
@@ -310,7 +400,13 @@ test("buildInstructionPacket (judge) sets prejudge_guardrails_degraded when repo
310
400
  await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
311
401
  // Intentionally create a directory at the chapter path to trigger fingerprint/read failure.
312
402
  await mkdir(join(rootDir, "staging/chapters/chapter-001.md"), { recursive: true });
313
- const checkpoint = { last_completed_chapter: 0, current_volume: 1, pipeline_stage: "refined", inflight_chapter: 1 };
403
+ const checkpoint = {
404
+ last_completed_chapter: 0,
405
+ current_volume: 1,
406
+ orchestrator_state: "WRITING",
407
+ pipeline_stage: "refined",
408
+ inflight_chapter: 1
409
+ };
314
410
  const built = await buildInstructionPacket({
315
411
  rootDir,
316
412
  checkpoint,