novel-writer-cli 0.3.0 → 0.5.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 (75) hide show
  1. package/README.md +1 -1
  2. package/agents/chapter-writer.md +43 -14
  3. package/agents/character-weaver.md +7 -1
  4. package/agents/plot-architect.md +20 -7
  5. package/agents/quality-judge.md +199 -20
  6. package/agents/style-analyzer.md +14 -8
  7. package/agents/style-refiner.md +10 -3
  8. package/agents/world-builder.md +8 -1
  9. package/dist/__tests__/agent-prompts-anti-ai-upgrade.test.js +194 -6
  10. package/dist/__tests__/agent-prompts-platform-expansion.test.js +33 -0
  11. package/dist/__tests__/anti-ai-infrastructure.test.js +548 -0
  12. package/dist/__tests__/anti-ai-templates.test.js +2 -2
  13. package/dist/__tests__/canon-status-lifecycle.test.js +481 -0
  14. package/dist/__tests__/commit-gate-decision.test.js +65 -0
  15. package/dist/__tests__/commit-prototype-pollution.test.js +1 -1
  16. package/dist/__tests__/excitement-type-annotation.test.js +240 -0
  17. package/dist/__tests__/excitement-type.test.js +21 -0
  18. package/dist/__tests__/gate-decision.test.js +62 -15
  19. package/dist/__tests__/genre-excitement-mapping.test.js +355 -0
  20. package/dist/__tests__/golden-chapter-gates.test.js +79 -0
  21. package/dist/__tests__/golden-chapter-mini-planning.test.js +485 -0
  22. package/dist/__tests__/helpers/quickstart-mini-planning.js +61 -0
  23. package/dist/__tests__/init.test.js +57 -5
  24. package/dist/__tests__/instructions-platform-expansion.test.js +125 -0
  25. package/dist/__tests__/next-step-gate-decision-routing.test.js +98 -0
  26. package/dist/__tests__/orchestrator-state-write-path.test.js +1 -1
  27. package/dist/__tests__/platform-profile.test.js +57 -1
  28. package/dist/__tests__/quickstart-pipeline.test.js +73 -6
  29. package/dist/__tests__/scoring-weights.test.js +193 -0
  30. package/dist/__tests__/steps-id.test.js +2 -0
  31. package/dist/__tests__/validate-quickstart-prereqs.test.js +2 -0
  32. package/dist/advance.js +27 -2
  33. package/dist/anti-ai-context.js +535 -0
  34. package/dist/cli.js +3 -1
  35. package/dist/commit.js +22 -0
  36. package/dist/excitement-type.js +12 -0
  37. package/dist/gate-decision.js +98 -2
  38. package/dist/golden-chapter-gates.js +143 -0
  39. package/dist/init.js +76 -7
  40. package/dist/instructions.js +552 -6
  41. package/dist/next-step.js +124 -88
  42. package/dist/platform-profile.js +20 -8
  43. package/dist/quickstart-mini-planning.js +30 -0
  44. package/dist/scoring-weights.js +38 -3
  45. package/dist/steps.js +1 -1
  46. package/dist/validate.js +293 -214
  47. package/dist/volume-commit.js +271 -5
  48. package/dist/volume-planning.js +78 -3
  49. package/docs/user/README.md +1 -0
  50. package/docs/user/migration-guide.md +166 -0
  51. package/docs/user/novel-cli.md +4 -3
  52. package/docs/user/quick-start.md +354 -57
  53. package/package.json +1 -1
  54. package/schemas/platform-profile.schema.json +2 -2
  55. package/scripts/lint-blacklist.sh +221 -76
  56. package/scripts/lint-structural.sh +538 -0
  57. package/skills/continue/SKILL.md +6 -0
  58. package/skills/continue/references/context-contracts.md +71 -6
  59. package/skills/continue/references/periodic-maintenance.md +12 -1
  60. package/skills/novel-writing/references/quality-rubric.md +79 -26
  61. package/skills/novel-writing/references/style-guide.md +129 -19
  62. package/skills/start/SKILL.md +23 -3
  63. package/skills/start/references/vol-planning.md +12 -3
  64. package/templates/ai-blacklist.json +1024 -246
  65. package/templates/ai-sentence-patterns.json +167 -0
  66. package/templates/genre-excitement-map.json +48 -0
  67. package/templates/genre-golden-standards.json +80 -0
  68. package/templates/genre-weight-profiles.json +15 -0
  69. package/templates/golden-chapter-gates.json +230 -0
  70. package/templates/novel-ask/example.question.json +3 -2
  71. package/templates/platform-profile.json +141 -1
  72. package/templates/platforms/fanqie.md +35 -0
  73. package/templates/platforms/jinjiang.md +35 -0
  74. package/templates/platforms/qidian.md +35 -0
  75. package/templates/style-profile-template.json +3 -0
@@ -0,0 +1,240 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, rm, 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 { fileURLToPath } from "node:url";
7
+ import { buildInstructionPacket } from "../instructions.js";
8
+ import { validateStep } from "../validate.js";
9
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
10
+ async function readRepoText(relPath) {
11
+ return readFile(join(repoRoot, relPath), "utf8");
12
+ }
13
+ async function writeText(absPath, contents) {
14
+ await mkdir(dirname(absPath), { recursive: true });
15
+ await writeFile(absPath, contents, "utf8");
16
+ }
17
+ async function writeJson(absPath, payload) {
18
+ await writeText(absPath, `${JSON.stringify(payload, null, 2)}\n`);
19
+ }
20
+ function makeJudgeCheckpoint(chapter) {
21
+ return {
22
+ last_completed_chapter: chapter - 1,
23
+ current_volume: 1,
24
+ orchestrator_state: "WRITING",
25
+ pipeline_stage: "refined",
26
+ inflight_chapter: chapter,
27
+ revision_count: 0,
28
+ hook_fix_count: 0,
29
+ title_fix_count: 0
30
+ };
31
+ }
32
+ function makeDraftCheckpoint(chapter) {
33
+ return {
34
+ last_completed_chapter: chapter - 1,
35
+ current_volume: 1,
36
+ orchestrator_state: "WRITING",
37
+ pipeline_stage: "committed",
38
+ inflight_chapter: chapter,
39
+ revision_count: 0,
40
+ hook_fix_count: 0,
41
+ title_fix_count: 0
42
+ };
43
+ }
44
+ function makeVolumeCheckpoint() {
45
+ return {
46
+ last_completed_chapter: 58,
47
+ current_volume: 2,
48
+ orchestrator_state: "VOL_PLANNING",
49
+ pipeline_stage: null,
50
+ volume_pipeline_stage: null,
51
+ inflight_chapter: null,
52
+ revision_count: 0,
53
+ hook_fix_count: 0,
54
+ title_fix_count: 0
55
+ };
56
+ }
57
+ test("issue 129 prompts and skills describe excitement_type flow", async () => {
58
+ const plotArchitect = await readRepoText("agents/plot-architect.md");
59
+ const qualityJudge = await readRepoText("agents/quality-judge.md");
60
+ const continueSkill = await readRepoText("skills/continue/SKILL.md");
61
+ const volumePlanning = await readRepoText("skills/start/references/vol-planning.md");
62
+ assert.match(plotArchitect, /excitement_type/);
63
+ assert.match(plotArchitect, /\*\*ExcitementType\*\*/);
64
+ assert.match(qualityJudge, /excitement_type/);
65
+ assert.match(qualityJudge, /excitement_landing/);
66
+ assert.match(qualityJudge, /铺垫有效性/);
67
+ assert.match(continueSkill, /packet\.manifest\.inline\.excitement_type/);
68
+ assert.match(volumePlanning, /ExcitementType/);
69
+ assert.match(volumePlanning, /未知值仅警告并按 `null` 处理/);
70
+ });
71
+ test("buildInstructionPacket injects excitement_type for judge packets with contract-first fallback", async () => {
72
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-excitement-packet-"));
73
+ try {
74
+ await writeText(join(rootDir, "volumes/vol-01/outline.md"), [
75
+ "## 第 1 卷大纲",
76
+ "",
77
+ "### 第 1 章: 测试",
78
+ "- **Storyline**: main-arc",
79
+ "- **POV**: hero",
80
+ "- **Location**: city",
81
+ "- **Conflict**: test",
82
+ "- **Arc**: test",
83
+ "- **Foreshadowing**: test",
84
+ "- **StateChanges**: test",
85
+ "- **TransitionHint**: {}",
86
+ "- **ExcitementType**: face_slap",
87
+ ""
88
+ ].join("\n"));
89
+ await writeJson(join(rootDir, "volumes/vol-01/chapter-contracts/chapter-001.json"), {
90
+ chapter: 1,
91
+ storyline_id: "main-arc",
92
+ objectives: [{ id: "OBJ-1", required: true, description: "x" }]
93
+ });
94
+ await writeText(join(rootDir, "staging/chapters/chapter-001.md"), "# 第1章\n\n正文\n");
95
+ const fromOutline = (await buildInstructionPacket({
96
+ rootDir,
97
+ checkpoint: makeJudgeCheckpoint(1),
98
+ step: { kind: "chapter", chapter: 1, stage: "judge" },
99
+ embedMode: null,
100
+ writeManifest: false
101
+ }));
102
+ assert.equal(fromOutline.packet.manifest.inline.excitement_type, "face_slap");
103
+ await writeJson(join(rootDir, "volumes/vol-01/chapter-contracts/chapter-001.json"), {
104
+ chapter: 1,
105
+ storyline_id: "main-arc",
106
+ excitement_type: "setup",
107
+ objectives: [{ id: "OBJ-1", required: true, description: "x" }]
108
+ });
109
+ const fromContract = (await buildInstructionPacket({
110
+ rootDir,
111
+ checkpoint: makeJudgeCheckpoint(1),
112
+ step: { kind: "chapter", chapter: 1, stage: "judge" },
113
+ embedMode: null,
114
+ writeManifest: false
115
+ }));
116
+ assert.equal(fromContract.packet.manifest.inline.excitement_type, "setup");
117
+ await writeJson(join(rootDir, "volumes/vol-01/chapter-contracts/chapter-001.json"), {
118
+ chapter: 1,
119
+ storyline_id: "main-arc",
120
+ excitement_type: "boom",
121
+ objectives: [{ id: "OBJ-1", required: true, description: "x" }]
122
+ });
123
+ const unknownContract = (await buildInstructionPacket({
124
+ rootDir,
125
+ checkpoint: makeJudgeCheckpoint(1),
126
+ step: { kind: "chapter", chapter: 1, stage: "judge" },
127
+ embedMode: null,
128
+ writeManifest: false
129
+ }));
130
+ assert.equal(unknownContract.packet.manifest.inline.excitement_type, null);
131
+ await writeJson(join(rootDir, "volumes/vol-01/chapter-contracts/chapter-001.json"), {
132
+ chapter: 1,
133
+ storyline_id: "main-arc",
134
+ excitement_type: null,
135
+ objectives: [{ id: "OBJ-1", required: true, description: "x" }]
136
+ });
137
+ const explicitNull = (await buildInstructionPacket({
138
+ rootDir,
139
+ checkpoint: makeJudgeCheckpoint(1),
140
+ step: { kind: "chapter", chapter: 1, stage: "judge" },
141
+ embedMode: null,
142
+ writeManifest: false
143
+ }));
144
+ assert.equal(explicitNull.packet.manifest.inline.excitement_type, null);
145
+ }
146
+ finally {
147
+ await rm(rootDir, { recursive: true, force: true });
148
+ }
149
+ });
150
+ test("buildInstructionPacket does not inject excitement_type for non-judge packets", async () => {
151
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-excitement-draft-packet-"));
152
+ try {
153
+ await writeText(join(rootDir, "volumes/vol-01/outline.md"), [
154
+ "## 第 1 卷大纲",
155
+ "",
156
+ "### 第 1 章: 测试",
157
+ "- **Storyline**: main-arc",
158
+ "- **POV**: hero",
159
+ "- **Location**: city",
160
+ "- **Conflict**: test",
161
+ "- **Arc**: test",
162
+ "- **Foreshadowing**: test",
163
+ "- **StateChanges**: test",
164
+ "- **TransitionHint**: {}",
165
+ "- **ExcitementType**: face_slap",
166
+ ""
167
+ ].join("\n"));
168
+ await writeJson(join(rootDir, "volumes/vol-01/chapter-contracts/chapter-001.json"), {
169
+ chapter: 1,
170
+ storyline_id: "main-arc",
171
+ excitement_type: "face_slap",
172
+ objectives: [{ id: "OBJ-1", required: true, description: "x" }]
173
+ });
174
+ const packet = (await buildInstructionPacket({
175
+ rootDir,
176
+ checkpoint: makeDraftCheckpoint(1),
177
+ step: { kind: "chapter", chapter: 1, stage: "draft" },
178
+ embedMode: null,
179
+ writeManifest: false
180
+ }));
181
+ assert.equal(Object.prototype.hasOwnProperty.call(packet.packet.manifest.inline, "excitement_type"), false);
182
+ }
183
+ finally {
184
+ await rm(rootDir, { recursive: true, force: true });
185
+ }
186
+ });
187
+ test("validateStep(volume:outline) warns for missing or unknown excitement_type without blocking", async () => {
188
+ const rootDir = await mkdtemp(join(tmpdir(), "novel-excitement-validate-"));
189
+ try {
190
+ await writeText(join(rootDir, "staging/volumes/vol-02/outline.md"), [
191
+ "## 第 2 卷大纲",
192
+ "",
193
+ "### 第59章: 测试",
194
+ "- **Storyline**: main-arc",
195
+ "- **POV**: hero",
196
+ "- **Location**: city",
197
+ "- **Conflict**: test",
198
+ "- **Arc**: test",
199
+ "- **Foreshadowing**: test",
200
+ "- **StateChanges**: test",
201
+ "- **TransitionHint**: {}",
202
+ "- **ExcitementType**: setup",
203
+ "",
204
+ "### 第60章: 测试",
205
+ "- **Storyline**: main-arc",
206
+ "- **POV**: hero",
207
+ "- **Location**: city",
208
+ "- **Conflict**: test",
209
+ "- **Arc**: test",
210
+ "- **Foreshadowing**: test",
211
+ "- **StateChanges**: test",
212
+ "- **TransitionHint**: {}",
213
+ "- **ExcitementType**: galaxy_brain",
214
+ ""
215
+ ].join("\n"));
216
+ await writeJson(join(rootDir, "staging/volumes/vol-02/storyline-schedule.json"), { active_storylines: ["main-arc"] });
217
+ await writeJson(join(rootDir, "staging/volumes/vol-02/foreshadowing.json"), { schema_version: 1, items: [] });
218
+ await writeJson(join(rootDir, "staging/volumes/vol-02/new-characters.json"), []);
219
+ const contractBase = {
220
+ storyline_id: "main-arc",
221
+ objectives: [{ id: "OBJ", required: true, description: "x" }],
222
+ preconditions: { character_states: { Alice: { location: "city" } } },
223
+ postconditions: { state_changes: {} }
224
+ };
225
+ await writeJson(join(rootDir, "staging/volumes/vol-02/chapter-contracts/chapter-059.json"), { chapter: 59, ...contractBase });
226
+ await writeJson(join(rootDir, "staging/volumes/vol-02/chapter-contracts/chapter-060.json"), {
227
+ chapter: 60,
228
+ excitement_type: "boom",
229
+ ...contractBase
230
+ });
231
+ const report = await validateStep({ rootDir, checkpoint: makeVolumeCheckpoint(), step: { kind: "volume", phase: "outline" } });
232
+ assert.equal(report.ok, true);
233
+ assert.ok(report.warnings.some((w) => w.includes("chapter 60") && w.includes("ExcitementType") && w.includes("galaxy_brain")));
234
+ assert.ok(report.warnings.some((w) => w.includes("chapter-059.json") && w.includes("Missing optional excitement_type")));
235
+ assert.ok(report.warnings.some((w) => w.includes("chapter-060.json") && w.includes("boom")));
236
+ }
237
+ finally {
238
+ await rm(rootDir, { recursive: true, force: true });
239
+ }
240
+ });
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { EXCITEMENT_TYPES, normalizeExcitementType } from "../excitement-type.js";
4
+ test("normalizeExcitementType accepts every supported enum value", () => {
5
+ for (const value of EXCITEMENT_TYPES) {
6
+ assert.equal(normalizeExcitementType(value), value);
7
+ }
8
+ });
9
+ test("normalizeExcitementType treats blank and null-like inputs as null", () => {
10
+ assert.equal(normalizeExcitementType(""), null);
11
+ assert.equal(normalizeExcitementType(" "), null);
12
+ assert.equal(normalizeExcitementType("null"), null);
13
+ assert.equal(normalizeExcitementType(null), null);
14
+ assert.equal(normalizeExcitementType(undefined), null);
15
+ });
16
+ test("normalizeExcitementType returns undefined for unsupported non-string or unknown values", () => {
17
+ assert.equal(normalizeExcitementType(true), undefined);
18
+ assert.equal(normalizeExcitementType(42), undefined);
19
+ assert.equal(normalizeExcitementType({ value: "setup" }), undefined);
20
+ assert.equal(normalizeExcitementType("galaxy_brain"), undefined);
21
+ });
@@ -1,33 +1,36 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { computeGateDecision, detectHighConfidenceViolation } from "../gate-decision.js";
3
+ import { computeGateDecision, detectGoldenChapterGateFailure, detectHighConfidenceViolation, evaluateGateDecisionFromEval } from "../gate-decision.js";
4
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");
5
+ assert.equal(computeGateDecision({ overall_final: 4.0, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "pass");
6
+ assert.equal(computeGateDecision({ overall_final: 3.9, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "polish");
7
+ assert.equal(computeGateDecision({ overall_final: 3.5, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "polish");
8
+ assert.equal(computeGateDecision({ overall_final: 3.4, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "revise");
9
+ assert.equal(computeGateDecision({ overall_final: 3.0, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "revise");
10
+ assert.equal(computeGateDecision({ overall_final: 2.9, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "pause_for_user");
11
+ assert.equal(computeGateDecision({ overall_final: 2.0, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "pause_for_user");
12
+ assert.equal(computeGateDecision({ overall_final: 1.99, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "pause_for_user_force_rewrite");
13
13
  });
14
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");
15
+ assert.equal(computeGateDecision({ overall_final: 4.8, revision_count: 0, has_high_confidence_violation: true, has_golden_chapter_gate_failure: false }), "revise");
16
16
  });
17
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");
18
+ assert.equal(computeGateDecision({ overall_final: 4.8, revision_count: 2, has_high_confidence_violation: true, has_golden_chapter_gate_failure: false }), "pause_for_user");
19
+ });
20
+ test("computeGateDecision forces revise on golden chapter gate failures", () => {
21
+ assert.equal(computeGateDecision({ overall_final: 4.8, revision_count: 0, has_high_confidence_violation: false, has_golden_chapter_gate_failure: true }), "revise");
19
22
  });
20
23
  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");
24
+ assert.equal(computeGateDecision({ overall_final: 3.2, revision_count: 2, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "force_passed");
22
25
  });
23
26
  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");
27
+ assert.equal(computeGateDecision({ overall_final: 3.6, revision_count: 2, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false }), "force_passed");
25
28
  });
26
29
  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");
30
+ assert.equal(computeGateDecision({ overall_final: 3.6, revision_count: 1, has_high_confidence_violation: false, has_golden_chapter_gate_failure: false, max_revisions: 1 }), "force_passed");
28
31
  });
29
32
  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");
33
+ assert.equal(computeGateDecision({ overall_final: 1.0, revision_count: 0, has_high_confidence_violation: true, has_golden_chapter_gate_failure: true, force_pass: true }), "force_passed");
31
34
  });
32
35
  test("detectHighConfidenceViolation returns false when contract_verification is missing", () => {
33
36
  assert.deepEqual(detectHighConfidenceViolation({ overall: 4.0, recommendation: "pass" }), {
@@ -64,3 +67,47 @@ test("detectHighConfidenceViolation marks inferred constraint_type for ls_checks
64
67
  assert.equal(res.high_confidence_violations.length, 1);
65
68
  assert.equal(res.high_confidence_violations[0].constraint_type_inferred, true);
66
69
  });
70
+ test("detectGoldenChapterGateFailure returns false when gates are missing", () => {
71
+ assert.deepEqual(detectGoldenChapterGateFailure({ overall: 4.0, recommendation: "pass" }), {
72
+ has_golden_chapter_gate_failure: false,
73
+ failed_checks: []
74
+ });
75
+ });
76
+ test("detectGoldenChapterGateFailure deduplicates repeated failed gate ids", () => {
77
+ const res = detectGoldenChapterGateFailure({
78
+ golden_chapter_gates: {
79
+ activated: true,
80
+ passed: false,
81
+ failed_gate_ids: ["hook_present", "protagonist_within_200_words"],
82
+ checks: [{ id: "hook_present", status: "fail", detail: "no hook" }]
83
+ }
84
+ });
85
+ assert.equal(res.has_golden_chapter_gate_failure, true);
86
+ assert.equal(res.failed_checks.length, 2);
87
+ assert.deepEqual(res.failed_checks.map((item) => item.id), ["hook_present", "protagonist_within_200_words"]);
88
+ });
89
+ test("detectGoldenChapterGateFailure accepts failed and violation statuses", () => {
90
+ const res = detectGoldenChapterGateFailure({
91
+ golden_chapter_gates: {
92
+ activated: true,
93
+ checks: [
94
+ { id: "a", status: "failed" },
95
+ { id: "b", status: "violation" }
96
+ ]
97
+ }
98
+ });
99
+ assert.equal(res.has_golden_chapter_gate_failure, true);
100
+ assert.deepEqual(res.failed_checks.map((item) => item.id), ["a", "b"]);
101
+ });
102
+ test("evaluateGateDecisionFromEval rejects non-object eval payloads", () => {
103
+ assert.deepEqual(evaluateGateDecisionFromEval({ evalRaw: null, revision_count: 0 }), {
104
+ ok: false,
105
+ reason: "eval_invalid"
106
+ });
107
+ });
108
+ test("evaluateGateDecisionFromEval rejects missing overall scores", () => {
109
+ assert.deepEqual(evaluateGateDecisionFromEval({ evalRaw: { chapter: 1 }, revision_count: 0 }), {
110
+ ok: false,
111
+ reason: "eval_missing_overall"
112
+ });
113
+ });