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.
- 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 +7 -2
- 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 +71 -12
- package/dist/cli.js +202 -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 +2 -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 +29 -0
- package/package.json +3 -2
- 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
|
|
62
|
-
assert.equal(result.ensuredDirs.length,
|
|
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 = {
|
|
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,
|