novel-writer-cli 0.0.2 → 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.
- package/README.md +9 -1
- package/dist/__tests__/advance-refine-invalidates-eval.test.js +37 -0
- package/dist/__tests__/character-voice.test.js +1 -1
- package/dist/__tests__/gate-decision.test.js +66 -0
- package/dist/__tests__/init.test.js +245 -0
- package/dist/__tests__/narrative-health-injection.test.js +8 -8
- package/dist/__tests__/next-step-gate-decision-routing.test.js +117 -0
- package/dist/__tests__/next-step-prejudge-guardrails.test.js +112 -16
- package/dist/__tests__/next-step-title-fix.test.js +64 -8
- package/dist/__tests__/orchestrator-state-routing.test.js +168 -0
- package/dist/__tests__/orchestrator-state-write-path.test.js +59 -0
- package/dist/__tests__/steps-id.test.js +23 -0
- package/dist/__tests__/volume-pipeline.test.js +227 -0
- package/dist/__tests__/volume-review-pipeline.test.js +112 -0
- package/dist/__tests__/volume-review-storyline-rhythm.test.js +19 -0
- package/dist/advance.js +145 -48
- package/dist/checkpoint.js +83 -12
- package/dist/cli.js +235 -8
- package/dist/commit.js +1 -0
- package/dist/fs-utils.js +18 -3
- package/dist/gate-decision.js +59 -0
- package/dist/init.js +165 -0
- package/dist/instructions.js +322 -24
- package/dist/next-step.js +198 -34
- package/dist/platform-profile.js +3 -0
- package/dist/steps.js +60 -17
- package/dist/validate.js +275 -2
- package/dist/volume-commit.js +101 -0
- package/dist/volume-planning.js +143 -0
- package/dist/volume-review.js +448 -0
- package/docs/user/novel-cli.md +57 -0
- package/package.json +3 -2
- package/schemas/platform-profile.schema.json +5 -0
|
@@ -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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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 = {
|
|
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,
|
|
@@ -54,7 +54,14 @@ test("computeNextStep returns title-fix on hard title violations when auto_fix=t
|
|
|
54
54
|
await writeJson(join(rootDir, "platform-profile.json"), makePlatformProfileRaw({ enabled: true, auto_fix: true }));
|
|
55
55
|
await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
|
|
56
56
|
await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "正文\n", "utf8");
|
|
57
|
-
const checkpoint = {
|
|
57
|
+
const checkpoint = {
|
|
58
|
+
last_completed_chapter: 0,
|
|
59
|
+
current_volume: 1,
|
|
60
|
+
orchestrator_state: "WRITING",
|
|
61
|
+
pipeline_stage: "refined",
|
|
62
|
+
inflight_chapter: 1,
|
|
63
|
+
title_fix_count: 0
|
|
64
|
+
};
|
|
58
65
|
const next = await computeNextStep(rootDir, checkpoint);
|
|
59
66
|
assert.equal(next.step, "chapter:001:title-fix");
|
|
60
67
|
});
|
|
@@ -63,7 +70,14 @@ test("computeNextStep returns review after title-fix was already attempted", asy
|
|
|
63
70
|
await writeJson(join(rootDir, "platform-profile.json"), makePlatformProfileRaw({ enabled: true, auto_fix: true }));
|
|
64
71
|
await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
|
|
65
72
|
await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "正文\n", "utf8");
|
|
66
|
-
const checkpoint = {
|
|
73
|
+
const checkpoint = {
|
|
74
|
+
last_completed_chapter: 0,
|
|
75
|
+
current_volume: 1,
|
|
76
|
+
orchestrator_state: "WRITING",
|
|
77
|
+
pipeline_stage: "refined",
|
|
78
|
+
inflight_chapter: 1,
|
|
79
|
+
title_fix_count: 1
|
|
80
|
+
};
|
|
67
81
|
const next = await computeNextStep(rootDir, checkpoint);
|
|
68
82
|
assert.equal(next.step, "chapter:001:review");
|
|
69
83
|
});
|
|
@@ -72,7 +86,14 @@ test("computeNextStep returns review on hard title violations when auto_fix=fals
|
|
|
72
86
|
await writeJson(join(rootDir, "platform-profile.json"), makePlatformProfileRaw({ enabled: true, auto_fix: false }));
|
|
73
87
|
await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
|
|
74
88
|
await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "正文\n", "utf8");
|
|
75
|
-
const checkpoint = {
|
|
89
|
+
const checkpoint = {
|
|
90
|
+
last_completed_chapter: 0,
|
|
91
|
+
current_volume: 1,
|
|
92
|
+
orchestrator_state: "WRITING",
|
|
93
|
+
pipeline_stage: "refined",
|
|
94
|
+
inflight_chapter: 1,
|
|
95
|
+
title_fix_count: 0
|
|
96
|
+
};
|
|
76
97
|
const next = await computeNextStep(rootDir, checkpoint);
|
|
77
98
|
assert.equal(next.step, "chapter:001:review");
|
|
78
99
|
});
|
|
@@ -81,7 +102,14 @@ test("computeNextStep does not block on warn-only title issues when auto_fix=fal
|
|
|
81
102
|
await writeJson(join(rootDir, "platform-profile.json"), makePlatformProfileRaw({ enabled: true, auto_fix: false, max_chars: 3 }));
|
|
82
103
|
await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
|
|
83
104
|
await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "# 太长的标题\n正文\n", "utf8");
|
|
84
|
-
const checkpoint = {
|
|
105
|
+
const checkpoint = {
|
|
106
|
+
last_completed_chapter: 0,
|
|
107
|
+
current_volume: 1,
|
|
108
|
+
orchestrator_state: "WRITING",
|
|
109
|
+
pipeline_stage: "refined",
|
|
110
|
+
inflight_chapter: 1,
|
|
111
|
+
title_fix_count: 0
|
|
112
|
+
};
|
|
85
113
|
const next = await computeNextStep(rootDir, checkpoint);
|
|
86
114
|
assert.equal(next.step, "chapter:001:judge");
|
|
87
115
|
});
|
|
@@ -90,7 +118,14 @@ test("computeNextStep returns title-fix on warn-only title issues when auto_fix=
|
|
|
90
118
|
await writeJson(join(rootDir, "platform-profile.json"), makePlatformProfileRaw({ enabled: true, auto_fix: true, max_chars: 3 }));
|
|
91
119
|
await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
|
|
92
120
|
await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "# 太长的标题\n正文\n", "utf8");
|
|
93
|
-
const checkpoint = {
|
|
121
|
+
const checkpoint = {
|
|
122
|
+
last_completed_chapter: 0,
|
|
123
|
+
current_volume: 1,
|
|
124
|
+
orchestrator_state: "WRITING",
|
|
125
|
+
pipeline_stage: "refined",
|
|
126
|
+
inflight_chapter: 1,
|
|
127
|
+
title_fix_count: 0
|
|
128
|
+
};
|
|
94
129
|
const next = await computeNextStep(rootDir, checkpoint);
|
|
95
130
|
assert.equal(next.step, "chapter:001:title-fix");
|
|
96
131
|
});
|
|
@@ -101,7 +136,14 @@ test("computeNextStep returns title-fix on judged stage when eval exists and tit
|
|
|
101
136
|
await writeFile(join(rootDir, "staging/chapters/chapter-001.md"), "正文\n", "utf8");
|
|
102
137
|
await mkdir(join(rootDir, "staging/evaluations"), { recursive: true });
|
|
103
138
|
await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), {});
|
|
104
|
-
const checkpoint = {
|
|
139
|
+
const checkpoint = {
|
|
140
|
+
last_completed_chapter: 0,
|
|
141
|
+
current_volume: 1,
|
|
142
|
+
orchestrator_state: "WRITING",
|
|
143
|
+
pipeline_stage: "judged",
|
|
144
|
+
inflight_chapter: 1,
|
|
145
|
+
title_fix_count: 0
|
|
146
|
+
};
|
|
105
147
|
const next = await computeNextStep(rootDir, checkpoint);
|
|
106
148
|
assert.equal(next.step, "chapter:001:title-fix");
|
|
107
149
|
});
|
|
@@ -111,7 +153,14 @@ test("title-fix snapshot is write-once (rerunning instructions does not bypass b
|
|
|
111
153
|
await mkdir(join(rootDir, "staging/chapters"), { recursive: true });
|
|
112
154
|
const chapterAbs = join(rootDir, "staging/chapters/chapter-001.md");
|
|
113
155
|
await writeFile(chapterAbs, "# 标题\n正文\n", "utf8");
|
|
114
|
-
const checkpoint = {
|
|
156
|
+
const checkpoint = {
|
|
157
|
+
last_completed_chapter: 0,
|
|
158
|
+
current_volume: 1,
|
|
159
|
+
orchestrator_state: "WRITING",
|
|
160
|
+
pipeline_stage: "refined",
|
|
161
|
+
inflight_chapter: 1,
|
|
162
|
+
title_fix_count: 0
|
|
163
|
+
};
|
|
115
164
|
await buildInstructionPacket({
|
|
116
165
|
rootDir,
|
|
117
166
|
checkpoint,
|
|
@@ -145,7 +194,14 @@ test("advance draft cleans up title-fix snapshot to avoid stale reuse", async ()
|
|
|
145
194
|
await mkdir(join(rootDir, "staging/logs"), { recursive: true });
|
|
146
195
|
const snapshotRel = titleFixSnapshotRel(1);
|
|
147
196
|
await writeFile(join(rootDir, snapshotRel), "old snapshot\n", "utf8");
|
|
148
|
-
const checkpoint = {
|
|
197
|
+
const checkpoint = {
|
|
198
|
+
last_completed_chapter: 0,
|
|
199
|
+
current_volume: 1,
|
|
200
|
+
orchestrator_state: "WRITING",
|
|
201
|
+
pipeline_stage: null,
|
|
202
|
+
inflight_chapter: null,
|
|
203
|
+
title_fix_count: 1
|
|
204
|
+
};
|
|
149
205
|
await writeJson(join(rootDir, ".checkpoint.json"), checkpoint);
|
|
150
206
|
await advanceCheckpointForStep({ rootDir, step: { kind: "chapter", chapter: 1, stage: "draft" } });
|
|
151
207
|
// validate cleanup is best-effort; the file should be gone.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, 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 { readCheckpoint } from "../checkpoint.js";
|
|
7
|
+
import { computeNextStep } from "../next-step.js";
|
|
8
|
+
async function writeJson(absPath, payload) {
|
|
9
|
+
await writeFile(absPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
10
|
+
}
|
|
11
|
+
test("readCheckpoint injects orchestrator_state via legacy inference", async () => {
|
|
12
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-state-legacy-"));
|
|
13
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
14
|
+
last_completed_chapter: 0,
|
|
15
|
+
current_volume: 1,
|
|
16
|
+
pipeline_stage: null,
|
|
17
|
+
inflight_chapter: null
|
|
18
|
+
});
|
|
19
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
20
|
+
assert.equal(checkpoint.orchestrator_state, "WRITING");
|
|
21
|
+
});
|
|
22
|
+
test("readCheckpoint infers CHAPTER_REWRITE when pipeline_stage=revising", async () => {
|
|
23
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-state-revising-"));
|
|
24
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
25
|
+
last_completed_chapter: 0,
|
|
26
|
+
current_volume: 1,
|
|
27
|
+
pipeline_stage: "revising",
|
|
28
|
+
inflight_chapter: 7
|
|
29
|
+
});
|
|
30
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
31
|
+
assert.equal(checkpoint.orchestrator_state, "CHAPTER_REWRITE");
|
|
32
|
+
});
|
|
33
|
+
test("readCheckpoint infers ERROR_RETRY when pipeline_stage=revising but inflight_chapter is missing", async () => {
|
|
34
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-state-revising-missing-inflight-"));
|
|
35
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
36
|
+
last_completed_chapter: 0,
|
|
37
|
+
current_volume: 1,
|
|
38
|
+
pipeline_stage: "revising",
|
|
39
|
+
inflight_chapter: null
|
|
40
|
+
});
|
|
41
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
42
|
+
assert.equal(checkpoint.orchestrator_state, "ERROR_RETRY");
|
|
43
|
+
});
|
|
44
|
+
test("readCheckpoint infers ERROR_RETRY when inflight_chapter is set but pipeline_stage is idle", async () => {
|
|
45
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-state-idle-inflight-"));
|
|
46
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
47
|
+
last_completed_chapter: 0,
|
|
48
|
+
current_volume: 1,
|
|
49
|
+
pipeline_stage: null,
|
|
50
|
+
inflight_chapter: 7
|
|
51
|
+
});
|
|
52
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
53
|
+
assert.equal(checkpoint.orchestrator_state, "ERROR_RETRY");
|
|
54
|
+
});
|
|
55
|
+
test("readCheckpoint rejects inflight_chapter=0", async () => {
|
|
56
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-state-inflight-zero-"));
|
|
57
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
58
|
+
last_completed_chapter: 0,
|
|
59
|
+
current_volume: 1,
|
|
60
|
+
pipeline_stage: "drafting",
|
|
61
|
+
inflight_chapter: 0
|
|
62
|
+
});
|
|
63
|
+
await assert.rejects(() => readCheckpoint(rootDir), /inflight_chapter must be an int >= 1/);
|
|
64
|
+
});
|
|
65
|
+
test("computeNextStep throws for INIT placeholder", async () => {
|
|
66
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-init-"));
|
|
67
|
+
await assert.rejects(() => computeNextStep(rootDir, {
|
|
68
|
+
last_completed_chapter: 0,
|
|
69
|
+
current_volume: 1,
|
|
70
|
+
orchestrator_state: "INIT",
|
|
71
|
+
pipeline_stage: null,
|
|
72
|
+
inflight_chapter: null
|
|
73
|
+
}), /Not implemented: orchestrator_state=INIT/);
|
|
74
|
+
});
|
|
75
|
+
test("computeNextStep throws when pipeline_stage=committed but inflight_chapter is set", async () => {
|
|
76
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-committed-inflight-"));
|
|
77
|
+
await assert.rejects(() => computeNextStep(rootDir, {
|
|
78
|
+
last_completed_chapter: 0,
|
|
79
|
+
current_volume: 1,
|
|
80
|
+
orchestrator_state: "WRITING",
|
|
81
|
+
pipeline_stage: "committed",
|
|
82
|
+
inflight_chapter: 7
|
|
83
|
+
}), /Checkpoint inconsistent: pipeline_stage=committed but inflight_chapter=7/);
|
|
84
|
+
});
|
|
85
|
+
test("computeNextStep throws for QUICK_START placeholder", async () => {
|
|
86
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-quickstart-"));
|
|
87
|
+
await assert.rejects(() => computeNextStep(rootDir, {
|
|
88
|
+
last_completed_chapter: 0,
|
|
89
|
+
current_volume: 1,
|
|
90
|
+
orchestrator_state: "QUICK_START",
|
|
91
|
+
pipeline_stage: null,
|
|
92
|
+
inflight_chapter: null
|
|
93
|
+
}), /Not implemented: orchestrator_state=QUICK_START/);
|
|
94
|
+
});
|
|
95
|
+
test("computeNextStep prefixes reason for ERROR_RETRY", async () => {
|
|
96
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-error-retry-"));
|
|
97
|
+
const next = await computeNextStep(rootDir, {
|
|
98
|
+
last_completed_chapter: 0,
|
|
99
|
+
current_volume: 1,
|
|
100
|
+
orchestrator_state: "ERROR_RETRY",
|
|
101
|
+
pipeline_stage: null,
|
|
102
|
+
inflight_chapter: null
|
|
103
|
+
});
|
|
104
|
+
assert.equal(next.step, "chapter:001:draft");
|
|
105
|
+
assert.equal(next.reason, "error_retry:fresh");
|
|
106
|
+
});
|
|
107
|
+
test("computeNextStep heals ERROR_RETRY when pipeline_stage is committed but inflight_chapter is set", async () => {
|
|
108
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-error-retry-heal-committed-"));
|
|
109
|
+
const next = await computeNextStep(rootDir, {
|
|
110
|
+
last_completed_chapter: 0,
|
|
111
|
+
current_volume: 1,
|
|
112
|
+
orchestrator_state: "ERROR_RETRY",
|
|
113
|
+
pipeline_stage: "committed",
|
|
114
|
+
inflight_chapter: 7
|
|
115
|
+
});
|
|
116
|
+
assert.equal(next.step, "chapter:001:draft");
|
|
117
|
+
assert.equal(next.reason, "error_retry:healed_drop_inflight:fresh");
|
|
118
|
+
});
|
|
119
|
+
test("computeNextStep heals ERROR_RETRY when pipeline_stage is in-flight but inflight_chapter is missing", async () => {
|
|
120
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-error-retry-heal-drafting-"));
|
|
121
|
+
const next = await computeNextStep(rootDir, {
|
|
122
|
+
last_completed_chapter: 6,
|
|
123
|
+
current_volume: 1,
|
|
124
|
+
orchestrator_state: "ERROR_RETRY",
|
|
125
|
+
pipeline_stage: "drafting",
|
|
126
|
+
inflight_chapter: null
|
|
127
|
+
});
|
|
128
|
+
assert.equal(next.step, "chapter:007:draft");
|
|
129
|
+
assert.equal(next.reason, "error_retry:healed_infer_inflight:drafting:missing_chapter");
|
|
130
|
+
});
|
|
131
|
+
test("computeNextStep delegates CHAPTER_REWRITE to chapter routing", async () => {
|
|
132
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-chapter-rewrite-"));
|
|
133
|
+
const next = await computeNextStep(rootDir, {
|
|
134
|
+
last_completed_chapter: 6,
|
|
135
|
+
current_volume: 1,
|
|
136
|
+
orchestrator_state: "CHAPTER_REWRITE",
|
|
137
|
+
pipeline_stage: "revising",
|
|
138
|
+
inflight_chapter: 7
|
|
139
|
+
});
|
|
140
|
+
assert.equal(next.step, "chapter:007:draft");
|
|
141
|
+
assert.equal(next.reason, "revising:restart_draft");
|
|
142
|
+
assert.deepEqual(next.inflight, { chapter: 7, pipeline_stage: "revising" });
|
|
143
|
+
});
|
|
144
|
+
test("computeNextStep routes VOL_PLANNING to volume pipeline", async () => {
|
|
145
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-vol-planning-"));
|
|
146
|
+
const next = await computeNextStep(rootDir, {
|
|
147
|
+
last_completed_chapter: 0,
|
|
148
|
+
current_volume: 1,
|
|
149
|
+
orchestrator_state: "VOL_PLANNING",
|
|
150
|
+
pipeline_stage: null,
|
|
151
|
+
inflight_chapter: null,
|
|
152
|
+
volume_pipeline_stage: null
|
|
153
|
+
});
|
|
154
|
+
assert.equal(next.step, "volume:outline");
|
|
155
|
+
assert.equal(next.reason, "vol_planning:outline");
|
|
156
|
+
});
|
|
157
|
+
test("computeNextStep routes VOL_REVIEW to review pipeline", async () => {
|
|
158
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-vol-review-"));
|
|
159
|
+
const next = await computeNextStep(rootDir, {
|
|
160
|
+
last_completed_chapter: 0,
|
|
161
|
+
current_volume: 1,
|
|
162
|
+
orchestrator_state: "VOL_REVIEW",
|
|
163
|
+
pipeline_stage: null,
|
|
164
|
+
inflight_chapter: null
|
|
165
|
+
});
|
|
166
|
+
assert.equal(next.step, "review:collect");
|
|
167
|
+
assert.equal(next.reason, "vol_review:missing_quality_summary");
|
|
168
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, 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 { advanceCheckpointForStep } from "../advance.js";
|
|
7
|
+
import { readCheckpoint } from "../checkpoint.js";
|
|
8
|
+
import { commitChapter } from "../commit.js";
|
|
9
|
+
async function writeText(absPath, contents) {
|
|
10
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
11
|
+
await writeFile(absPath, contents, "utf8");
|
|
12
|
+
}
|
|
13
|
+
async function writeJson(absPath, payload) {
|
|
14
|
+
await writeText(absPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
15
|
+
}
|
|
16
|
+
test("advanceCheckpointForStep normalizes orchestrator_state to WRITING for chapter pipeline", async () => {
|
|
17
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-advance-state-"));
|
|
18
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
19
|
+
last_completed_chapter: 0,
|
|
20
|
+
current_volume: 1,
|
|
21
|
+
orchestrator_state: "INIT",
|
|
22
|
+
pipeline_stage: null,
|
|
23
|
+
inflight_chapter: null
|
|
24
|
+
});
|
|
25
|
+
await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(测试)\n`);
|
|
26
|
+
const updated = await advanceCheckpointForStep({
|
|
27
|
+
rootDir,
|
|
28
|
+
step: { kind: "chapter", chapter: 1, stage: "draft" }
|
|
29
|
+
});
|
|
30
|
+
assert.equal(updated.orchestrator_state, "WRITING");
|
|
31
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
32
|
+
assert.equal(checkpoint.orchestrator_state, "WRITING");
|
|
33
|
+
});
|
|
34
|
+
test("commitChapter resets orchestrator_state to WRITING", async () => {
|
|
35
|
+
const rootDir = await mkdtemp(join(tmpdir(), "novel-orchestrator-commit-state-"));
|
|
36
|
+
await writeJson(join(rootDir, ".checkpoint.json"), {
|
|
37
|
+
last_completed_chapter: 0,
|
|
38
|
+
current_volume: 1,
|
|
39
|
+
orchestrator_state: "CHAPTER_REWRITE",
|
|
40
|
+
pipeline_stage: "judged",
|
|
41
|
+
inflight_chapter: 1
|
|
42
|
+
});
|
|
43
|
+
await writeText(join(rootDir, "staging/chapters/chapter-001.md"), `# 第1章\n\n(测试)\n`);
|
|
44
|
+
await writeText(join(rootDir, "staging/summaries/chapter-001-summary.md"), `## 第 1 章摘要\n\n- 测试事件\n`);
|
|
45
|
+
await writeJson(join(rootDir, "staging/state/chapter-001-crossref.json"), { schema_version: 1, chapter: 1, entities: [] });
|
|
46
|
+
await writeJson(join(rootDir, "staging/evaluations/chapter-001-eval.json"), { chapter: 1 });
|
|
47
|
+
await writeText(join(rootDir, "staging/storylines/main-arc/memory.md"), `- 测试记忆\n`);
|
|
48
|
+
await writeJson(join(rootDir, "staging/state/chapter-001-delta.json"), {
|
|
49
|
+
chapter: 1,
|
|
50
|
+
base_state_version: 0,
|
|
51
|
+
storyline_id: "main-arc",
|
|
52
|
+
ops: [{ op: "set", path: "characters.hero.display_name", value: "阿宁" }]
|
|
53
|
+
});
|
|
54
|
+
await commitChapter({ rootDir, chapter: 1, dryRun: false });
|
|
55
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
56
|
+
assert.equal(checkpoint.orchestrator_state, "WRITING");
|
|
57
|
+
assert.equal(checkpoint.pipeline_stage, "committed");
|
|
58
|
+
assert.equal(checkpoint.inflight_chapter, null);
|
|
59
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { formatStepId, parseStepId } from "../steps.js";
|
|
4
|
+
test("formatStepId formats chapter ids with pad3", () => {
|
|
5
|
+
assert.equal(formatStepId({ kind: "chapter", chapter: 7, stage: "draft" }), "chapter:007:draft");
|
|
6
|
+
});
|
|
7
|
+
test("formatStepId formats volume/quickstart/review ids", () => {
|
|
8
|
+
assert.equal(formatStepId({ kind: "volume", phase: "outline" }), "volume:outline");
|
|
9
|
+
assert.equal(formatStepId({ kind: "quickstart", phase: "world" }), "quickstart:world");
|
|
10
|
+
assert.equal(formatStepId({ kind: "review", phase: "report" }), "review:report");
|
|
11
|
+
});
|
|
12
|
+
test("parseStepId parses chapter ids and trims whitespace", () => {
|
|
13
|
+
assert.deepEqual(parseStepId(" chapter:7:refine "), { kind: "chapter", chapter: 7, stage: "refine" });
|
|
14
|
+
});
|
|
15
|
+
test("parseStepId parses volume/quickstart/review ids", () => {
|
|
16
|
+
assert.deepEqual(parseStepId("volume:validate"), { kind: "volume", phase: "validate" });
|
|
17
|
+
assert.deepEqual(parseStepId("quickstart:trial"), { kind: "quickstart", phase: "trial" });
|
|
18
|
+
assert.deepEqual(parseStepId("review:cleanup"), { kind: "review", phase: "cleanup" });
|
|
19
|
+
});
|
|
20
|
+
test("parseStepId rejects unknown kind and invalid phases", () => {
|
|
21
|
+
assert.throws(() => parseStepId("foo:bar"), /Supported kinds: chapter, volume, quickstart, review/);
|
|
22
|
+
assert.throws(() => parseStepId("volume:badphase"), /Phase must be one of:/);
|
|
23
|
+
});
|