novel-writer-cli 0.0.1

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 (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/agents/chapter-writer.md +142 -0
  4. package/agents/character-weaver.md +117 -0
  5. package/agents/consistency-auditor.md +85 -0
  6. package/agents/plot-architect.md +128 -0
  7. package/agents/quality-judge.md +232 -0
  8. package/agents/style-analyzer.md +109 -0
  9. package/agents/style-refiner.md +97 -0
  10. package/agents/summarizer.md +128 -0
  11. package/agents/world-builder.md +161 -0
  12. package/dist/__tests__/character-voice.test.js +445 -0
  13. package/dist/__tests__/commit-prototype-pollution.test.js +45 -0
  14. package/dist/__tests__/engagement.test.js +382 -0
  15. package/dist/__tests__/foreshadow-visibility.test.js +131 -0
  16. package/dist/__tests__/hook-ledger.test.js +1028 -0
  17. package/dist/__tests__/naming-lint.test.js +132 -0
  18. package/dist/__tests__/narrative-health-injection.test.js +359 -0
  19. package/dist/__tests__/next-step-prejudge-guardrails.test.js +325 -0
  20. package/dist/__tests__/next-step-title-fix.test.js +153 -0
  21. package/dist/__tests__/platform-profile.test.js +274 -0
  22. package/dist/__tests__/promise-ledger.test.js +189 -0
  23. package/dist/__tests__/readability-lint.test.js +209 -0
  24. package/dist/__tests__/text-utils.test.js +39 -0
  25. package/dist/__tests__/title-policy.test.js +147 -0
  26. package/dist/advance.js +75 -0
  27. package/dist/character-voice.js +805 -0
  28. package/dist/checkpoint.js +126 -0
  29. package/dist/cli.js +563 -0
  30. package/dist/cliche-lint.js +515 -0
  31. package/dist/commit.js +1460 -0
  32. package/dist/consistency-auditor.js +684 -0
  33. package/dist/engagement.js +687 -0
  34. package/dist/errors.js +7 -0
  35. package/dist/fingerprint.js +16 -0
  36. package/dist/foreshadow-visibility.js +214 -0
  37. package/dist/fs-utils.js +68 -0
  38. package/dist/hook-ledger.js +721 -0
  39. package/dist/hook-policy.js +107 -0
  40. package/dist/instruction-gates.js +51 -0
  41. package/dist/instructions.js +406 -0
  42. package/dist/latest-summary-loader.js +29 -0
  43. package/dist/lock.js +121 -0
  44. package/dist/naming-lint.js +531 -0
  45. package/dist/ner.js +73 -0
  46. package/dist/next-step.js +408 -0
  47. package/dist/novel-ask.js +270 -0
  48. package/dist/output.js +9 -0
  49. package/dist/platform-constraints.js +518 -0
  50. package/dist/platform-profile.js +325 -0
  51. package/dist/prejudge-guardrails.js +370 -0
  52. package/dist/project.js +40 -0
  53. package/dist/promise-ledger.js +723 -0
  54. package/dist/readability-lint.js +555 -0
  55. package/dist/safe-parse.js +36 -0
  56. package/dist/safe-path.js +29 -0
  57. package/dist/scoring-weights.js +290 -0
  58. package/dist/steps.js +60 -0
  59. package/dist/text-utils.js +18 -0
  60. package/dist/title-policy.js +251 -0
  61. package/dist/type-guards.js +6 -0
  62. package/dist/validate.js +131 -0
  63. package/docs/user/README.md +17 -0
  64. package/docs/user/guardrails.md +179 -0
  65. package/docs/user/interactive-gates.md +124 -0
  66. package/docs/user/novel-cli.md +289 -0
  67. package/docs/user/ops.md +123 -0
  68. package/docs/user/quick-start.md +97 -0
  69. package/docs/user/spec-system.md +166 -0
  70. package/docs/user/storylines.md +144 -0
  71. package/package.json +48 -0
  72. package/schemas/README.md +18 -0
  73. package/schemas/character-voice-drift.schema.json +135 -0
  74. package/schemas/character-voice-profiles.schema.json +141 -0
  75. package/schemas/engagement-metrics.schema.json +38 -0
  76. package/schemas/hook-ledger.schema.json +108 -0
  77. package/schemas/platform-profile.schema.json +235 -0
  78. package/schemas/promise-ledger.schema.json +97 -0
  79. package/scripts/calibrate-quality-judge.sh +91 -0
  80. package/scripts/compare-regression-runs.sh +86 -0
  81. package/scripts/lib/_common.py +131 -0
  82. package/scripts/lib/calibrate_quality_judge.py +312 -0
  83. package/scripts/lib/compare_regression_runs.py +142 -0
  84. package/scripts/lib/run_regression.py +621 -0
  85. package/scripts/lint-blacklist.sh +201 -0
  86. package/scripts/lint-cliche.sh +370 -0
  87. package/scripts/lint-readability.sh +404 -0
  88. package/scripts/query-foreshadow.sh +252 -0
  89. package/scripts/run-ner.sh +669 -0
  90. package/scripts/run-regression.sh +122 -0
  91. package/skills/cli-step/SKILL.md +158 -0
  92. package/skills/continue/SKILL.md +348 -0
  93. package/skills/continue/references/context-contracts.md +169 -0
  94. package/skills/continue/references/continuity-checks.md +187 -0
  95. package/skills/continue/references/file-protocols.md +64 -0
  96. package/skills/continue/references/foreshadowing.md +130 -0
  97. package/skills/continue/references/gate-decision.md +53 -0
  98. package/skills/continue/references/periodic-maintenance.md +46 -0
  99. package/skills/novel-writing/SKILL.md +77 -0
  100. package/skills/novel-writing/references/quality-rubric.md +140 -0
  101. package/skills/novel-writing/references/style-guide.md +145 -0
  102. package/skills/start/SKILL.md +458 -0
  103. package/skills/start/references/quality-review.md +86 -0
  104. package/skills/start/references/setting-update.md +44 -0
  105. package/skills/start/references/vol-planning.md +61 -0
  106. package/skills/start/references/vol-review.md +58 -0
  107. package/skills/status/SKILL.md +116 -0
  108. package/skills/status/references/sample-output.md +60 -0
  109. package/templates/ai-blacklist.json +79 -0
  110. package/templates/brief-template.md +46 -0
  111. package/templates/genre-weight-profiles.json +90 -0
  112. package/templates/novel-ask/example.answer.json +12 -0
  113. package/templates/novel-ask/example.question.json +51 -0
  114. package/templates/platform-profile.json +148 -0
  115. package/templates/style-profile-template.json +58 -0
  116. package/templates/web-novel-cliche-lint.json +41 -0
package/dist/ner.js ADDED
@@ -0,0 +1,73 @@
1
+ import { execFile } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { promisify } from "node:util";
4
+ import { NovelCliError } from "./errors.js";
5
+ import { isPlainObject } from "./type-guards.js";
6
+ const execFileAsync = promisify(execFile);
7
+ export function parseNerOutput(raw) {
8
+ if (!isPlainObject(raw))
9
+ throw new NovelCliError(`Invalid NER output: expected JSON object.`, 2);
10
+ const obj = raw;
11
+ const schema = obj.schema_version;
12
+ if (typeof schema !== "number" || !Number.isInteger(schema))
13
+ throw new NovelCliError(`Invalid NER output: schema_version must be an int.`, 2);
14
+ if (schema !== 1)
15
+ throw new NovelCliError(`Invalid NER output: unsupported schema_version=${schema} (expected 1).`, 2);
16
+ const entitiesRaw = obj.entities;
17
+ if (!isPlainObject(entitiesRaw))
18
+ throw new NovelCliError(`Invalid NER output: missing entities object.`, 2);
19
+ const entitiesObj = entitiesRaw;
20
+ const parseList = (key) => {
21
+ const listRaw = entitiesObj[key];
22
+ if (!Array.isArray(listRaw))
23
+ return [];
24
+ const out = [];
25
+ for (const it of listRaw) {
26
+ if (!isPlainObject(it))
27
+ continue;
28
+ const rec = it;
29
+ const text = typeof rec.text === "string" ? rec.text.trim() : "";
30
+ const confidence = typeof rec.confidence === "string" ? rec.confidence : "unknown";
31
+ const mentionsRaw = rec.mentions;
32
+ const mentions = [];
33
+ if (Array.isArray(mentionsRaw)) {
34
+ for (const m of mentionsRaw) {
35
+ if (!isPlainObject(m))
36
+ continue;
37
+ const mo = m;
38
+ const line = typeof mo.line === "number" && Number.isInteger(mo.line) ? mo.line : null;
39
+ const snippet = typeof mo.snippet === "string" ? mo.snippet : null;
40
+ if (line !== null && snippet !== null)
41
+ mentions.push({ line, snippet });
42
+ }
43
+ }
44
+ if (text.length === 0)
45
+ continue;
46
+ out.push({ text, confidence, mentions });
47
+ }
48
+ return out;
49
+ };
50
+ return {
51
+ schema_version: 1,
52
+ entities: {
53
+ characters: parseList("characters"),
54
+ locations: parseList("locations"),
55
+ time_markers: parseList("time_markers"),
56
+ events: parseList("events")
57
+ }
58
+ };
59
+ }
60
+ function runNerScriptPath() {
61
+ return fileURLToPath(new URL("../scripts/run-ner.sh", import.meta.url));
62
+ }
63
+ export async function runNer(chapterAbs) {
64
+ const script = runNerScriptPath();
65
+ const { stdout } = await execFileAsync("bash", [script, chapterAbs], {
66
+ maxBuffer: 10 * 1024 * 1024,
67
+ timeout: 60_000,
68
+ killSignal: "SIGKILL"
69
+ });
70
+ const trimmed = stdout.trim();
71
+ const raw = JSON.parse(trimmed);
72
+ return parseNerOutput(raw);
73
+ }
@@ -0,0 +1,408 @@
1
+ import { join } from "node:path";
2
+ import { pathExists, readJsonFile, readTextFile } from "./fs-utils.js";
3
+ import { checkHookPolicy } from "./hook-policy.js";
4
+ import { loadPlatformProfile } from "./platform-profile.js";
5
+ import { computePrejudgeGuardrailsReport, loadPrejudgeGuardrailsReportIfFresh, prejudgeGuardrailsRelPath } from "./prejudge-guardrails.js";
6
+ import { summarizeNamingIssues } from "./naming-lint.js";
7
+ import { summarizeReadabilityIssues } from "./readability-lint.js";
8
+ import { computeTitlePolicyReport } from "./title-policy.js";
9
+ import { chapterRelPaths, formatStepId } from "./steps.js";
10
+ function normalizeStage(stage) {
11
+ if (stage === null || stage === undefined)
12
+ return null;
13
+ if (typeof stage === "string")
14
+ return stage;
15
+ return null;
16
+ }
17
+ async function checkHookPolicyForStage(args) {
18
+ const loadedProfile = await loadPlatformProfile(args.projectRootDir);
19
+ const hookPolicy = loadedProfile?.profile.hook_policy;
20
+ if (!hookPolicy?.required)
21
+ return null;
22
+ let evalRaw;
23
+ try {
24
+ evalRaw = await readJsonFile(join(args.projectRootDir, args.evalRelPath));
25
+ }
26
+ catch (err) {
27
+ const message = err instanceof Error ? err.message : String(err);
28
+ return {
29
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "judge" }),
30
+ reason: `${args.stagePrefix}:hook_eval_read_failed`,
31
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
32
+ evidence: { ...args.evidence, hookFixCount: args.hookFixCount, error: message }
33
+ };
34
+ }
35
+ const check = checkHookPolicy({ hookPolicy, evalRaw });
36
+ if (check.status === "invalid_eval") {
37
+ return {
38
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "judge" }),
39
+ reason: `${args.stagePrefix}:hook_eval_invalid:${check.reason}`,
40
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
41
+ evidence: { ...args.evidence, hookFixCount: args.hookFixCount, hook_check: check }
42
+ };
43
+ }
44
+ if (check.status === "fail") {
45
+ if (args.hookFixCount < 1) {
46
+ return {
47
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "hook-fix" }),
48
+ reason: `${args.stagePrefix}:hook_policy_fail:hook-fix:${check.reason}`,
49
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
50
+ evidence: { ...args.evidence, hookFixCount: args.hookFixCount, hook_check: check }
51
+ };
52
+ }
53
+ return {
54
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
55
+ reason: `${args.stagePrefix}:hook_policy_fail:manual_review:${check.reason}`,
56
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
57
+ evidence: { ...args.evidence, hookFixCount: args.hookFixCount, hook_check: check }
58
+ };
59
+ }
60
+ return null;
61
+ }
62
+ async function checkTitlePolicyForStage(args) {
63
+ const loadedProfile = await loadPlatformProfile(args.projectRootDir);
64
+ if (!loadedProfile)
65
+ return null;
66
+ const titlePolicy = loadedProfile.profile.retention?.title_policy;
67
+ if (!titlePolicy?.enabled)
68
+ return null;
69
+ if (!args.hasChapter) {
70
+ return {
71
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "draft" }),
72
+ reason: `${args.stagePrefix}:missing_chapter`,
73
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
74
+ evidence: { ...args.evidence, titleFixCount: args.titleFixCount }
75
+ };
76
+ }
77
+ let chapterText;
78
+ try {
79
+ chapterText = await readTextFile(join(args.projectRootDir, args.chapterRelPath));
80
+ }
81
+ catch (err) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ return {
84
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
85
+ reason: `${args.stagePrefix}:title_read_failed`,
86
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
87
+ evidence: { ...args.evidence, titleFixCount: args.titleFixCount, error: message }
88
+ };
89
+ }
90
+ const report = computeTitlePolicyReport({ chapter: args.inflightChapter, chapterText, platformProfile: loadedProfile.profile });
91
+ if (report.status === "pass" || report.status === "skipped")
92
+ return null;
93
+ if (!report.has_hard_violations && !titlePolicy.auto_fix)
94
+ return null;
95
+ const primaryIssue = report.issues.find((i) => i.severity === "hard") ?? report.issues[0] ?? null;
96
+ const issueSummary = primaryIssue?.summary ?? "title policy failing";
97
+ if (titlePolicy.auto_fix) {
98
+ if (args.titleFixCount < 1) {
99
+ return {
100
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "title-fix" }),
101
+ reason: `${args.stagePrefix}:title_policy_fail:title-fix`,
102
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
103
+ evidence: { ...args.evidence, titleFixCount: args.titleFixCount, title_policy: { status: report.status, issue: issueSummary } }
104
+ };
105
+ }
106
+ return {
107
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
108
+ reason: `${args.stagePrefix}:title_policy_fail:manual_review`,
109
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
110
+ evidence: { ...args.evidence, titleFixCount: args.titleFixCount, title_policy: { status: report.status, issue: issueSummary } }
111
+ };
112
+ }
113
+ return {
114
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
115
+ reason: `${args.stagePrefix}:title_policy_fail:manual_fix_required`,
116
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
117
+ evidence: { ...args.evidence, titleFixCount: args.titleFixCount, title_policy: { status: report.status, issue: issueSummary } }
118
+ };
119
+ }
120
+ async function checkPrejudgeGuardrailsForStage(args) {
121
+ const loadedProfile = await loadPlatformProfile(args.projectRootDir);
122
+ if (!loadedProfile)
123
+ return null;
124
+ const chapterAbsPath = join(args.projectRootDir, args.chapterRelPath);
125
+ const cacheRelPath = prejudgeGuardrailsRelPath(args.inflightChapter);
126
+ let cacheStatus = "miss";
127
+ let report = await loadPrejudgeGuardrailsReportIfFresh({
128
+ rootDir: args.projectRootDir,
129
+ chapter: args.inflightChapter,
130
+ chapterAbsPath,
131
+ platformProfileRelPath: loadedProfile.relPath,
132
+ platformProfile: loadedProfile.profile
133
+ });
134
+ if (report)
135
+ cacheStatus = "hit";
136
+ if (!report) {
137
+ try {
138
+ report = await computePrejudgeGuardrailsReport({
139
+ rootDir: args.projectRootDir,
140
+ chapter: args.inflightChapter,
141
+ chapterAbsPath,
142
+ platformProfileRelPath: loadedProfile.relPath,
143
+ platformProfile: loadedProfile.profile
144
+ });
145
+ }
146
+ catch (err) {
147
+ const message = err instanceof Error ? err.message : String(err);
148
+ return {
149
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
150
+ reason: `${args.stagePrefix}:prejudge_guardrails_error`,
151
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
152
+ evidence: { ...args.evidence, prejudge_guardrails: { cache: { status: cacheStatus, rel_path: cacheRelPath }, error: message } }
153
+ };
154
+ }
155
+ }
156
+ if (!report.has_blocking_issues)
157
+ return null;
158
+ const readabilityBlocking = report.readability_lint.has_blocking_issues;
159
+ const namingBlocking = report.naming_lint.has_blocking_issues;
160
+ const readabilitySummary = readabilityBlocking ? summarizeReadabilityIssues(report.readability_lint.issues, 3) : null;
161
+ const namingSummary = namingBlocking ? summarizeNamingIssues(report.naming_lint.issues, 3) : null;
162
+ const reasons = [];
163
+ if (readabilityBlocking)
164
+ reasons.push("readability_lint");
165
+ if (namingBlocking)
166
+ reasons.push("naming_lint");
167
+ const label = reasons.length > 0 ? reasons.join("+") : report.blocking_reasons.join("+");
168
+ return {
169
+ step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
170
+ reason: `${args.stagePrefix}:prejudge_guardrails_blocking:${label}`,
171
+ inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
172
+ evidence: {
173
+ ...args.evidence,
174
+ prejudge_guardrails: {
175
+ cache: { status: cacheStatus, rel_path: cacheRelPath },
176
+ status: report.status,
177
+ has_blocking_issues: report.has_blocking_issues,
178
+ blocking_reasons: report.blocking_reasons,
179
+ platform_profile: report.platform_profile,
180
+ readability: {
181
+ status: report.readability_lint.status,
182
+ issues_total: report.readability_lint.issues.length,
183
+ has_blocking_issues: report.readability_lint.has_blocking_issues,
184
+ ...(readabilitySummary ? { blocking_summary: readabilitySummary } : {})
185
+ },
186
+ naming: {
187
+ status: report.naming_lint.status,
188
+ issues_total: report.naming_lint.issues.length,
189
+ has_blocking_issues: report.naming_lint.has_blocking_issues,
190
+ ...(namingSummary ? { blocking_summary: namingSummary } : {})
191
+ }
192
+ }
193
+ }
194
+ };
195
+ }
196
+ export async function computeNextStep(projectRootDir, checkpoint) {
197
+ const inflightChapter = typeof checkpoint.inflight_chapter === "number" ? checkpoint.inflight_chapter : null;
198
+ const stage = normalizeStage(checkpoint.pipeline_stage);
199
+ const hookFixCount = typeof checkpoint.hook_fix_count === "number" ? checkpoint.hook_fix_count : 0;
200
+ const titleFixCount = typeof checkpoint.title_fix_count === "number" ? checkpoint.title_fix_count : 0;
201
+ // Fresh start.
202
+ if (inflightChapter === null || stage === null || stage === "committed") {
203
+ const nextChapter = checkpoint.last_completed_chapter + 1;
204
+ return {
205
+ step: formatStepId({ kind: "chapter", chapter: nextChapter, stage: "draft" }),
206
+ reason: "fresh",
207
+ inflight: { chapter: null, pipeline_stage: stage }
208
+ };
209
+ }
210
+ const rel = chapterRelPaths(inflightChapter);
211
+ const hasChapter = await pathExists(join(projectRootDir, rel.staging.chapterMd));
212
+ const hasSummary = await pathExists(join(projectRootDir, rel.staging.summaryMd));
213
+ const hasDelta = await pathExists(join(projectRootDir, rel.staging.deltaJson));
214
+ const hasCrossref = await pathExists(join(projectRootDir, rel.staging.crossrefJson));
215
+ const hasEval = await pathExists(join(projectRootDir, rel.staging.evalJson));
216
+ const evidence = { hasChapter, hasSummary, hasDelta, hasCrossref, hasEval };
217
+ // Resume rules (aligned with skills/continue).
218
+ // Revision loop: restart from ChapterWriter regardless of existing staging artifacts.
219
+ if (stage === "revising") {
220
+ return {
221
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
222
+ reason: "revising:restart_draft",
223
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
224
+ evidence
225
+ };
226
+ }
227
+ if (stage === "drafting") {
228
+ if (!hasChapter) {
229
+ return {
230
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
231
+ reason: `${stage}:missing_chapter`,
232
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
233
+ evidence
234
+ };
235
+ }
236
+ if (!hasSummary || !hasDelta || !hasCrossref) {
237
+ return {
238
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "summarize" }),
239
+ reason: `${stage}:missing_summary`,
240
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
241
+ evidence
242
+ };
243
+ }
244
+ return {
245
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "refine" }),
246
+ reason: `${stage}:ready_refine`,
247
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
248
+ evidence
249
+ };
250
+ }
251
+ if (stage === "drafted") {
252
+ if (!hasChapter) {
253
+ return {
254
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
255
+ reason: "drafted:missing_chapter",
256
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
257
+ evidence
258
+ };
259
+ }
260
+ if (!hasSummary || !hasDelta || !hasCrossref) {
261
+ return {
262
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "summarize" }),
263
+ reason: "drafted:missing_summary",
264
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
265
+ evidence
266
+ };
267
+ }
268
+ return {
269
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "refine" }),
270
+ reason: "drafted:resume_refine",
271
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
272
+ evidence
273
+ };
274
+ }
275
+ if (stage === "refined") {
276
+ if (!hasChapter) {
277
+ return {
278
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
279
+ reason: "refined:missing_chapter",
280
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
281
+ evidence
282
+ };
283
+ }
284
+ if (!hasEval) {
285
+ const titleGate = await checkTitlePolicyForStage({
286
+ projectRootDir,
287
+ stagePrefix: "refined",
288
+ inflightChapter,
289
+ pipelineStage: stage,
290
+ evidence,
291
+ titleFixCount,
292
+ hasChapter,
293
+ chapterRelPath: rel.staging.chapterMd
294
+ });
295
+ if (titleGate)
296
+ return titleGate;
297
+ return {
298
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
299
+ reason: "refined:missing_eval",
300
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
301
+ evidence
302
+ };
303
+ }
304
+ const titleGate = await checkTitlePolicyForStage({
305
+ projectRootDir,
306
+ stagePrefix: "refined",
307
+ inflightChapter,
308
+ pipelineStage: stage,
309
+ evidence,
310
+ titleFixCount,
311
+ hasChapter,
312
+ chapterRelPath: rel.staging.chapterMd
313
+ });
314
+ if (titleGate)
315
+ return titleGate;
316
+ const hookGate = await checkHookPolicyForStage({
317
+ projectRootDir,
318
+ stagePrefix: "refined",
319
+ inflightChapter,
320
+ pipelineStage: stage,
321
+ evidence,
322
+ hookFixCount,
323
+ evalRelPath: rel.staging.evalJson
324
+ });
325
+ if (hookGate)
326
+ return hookGate;
327
+ const guardrailsGate = await checkPrejudgeGuardrailsForStage({
328
+ projectRootDir,
329
+ stagePrefix: "refined",
330
+ inflightChapter,
331
+ pipelineStage: stage,
332
+ evidence,
333
+ chapterRelPath: rel.staging.chapterMd
334
+ });
335
+ if (guardrailsGate)
336
+ return guardrailsGate;
337
+ return {
338
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
339
+ reason: "refined:ready_commit",
340
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
341
+ evidence
342
+ };
343
+ }
344
+ if (stage === "judged") {
345
+ if (!hasChapter) {
346
+ return {
347
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
348
+ reason: "judged:missing_chapter",
349
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
350
+ evidence
351
+ };
352
+ }
353
+ if (!hasEval) {
354
+ return {
355
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
356
+ reason: "judged:missing_eval",
357
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
358
+ evidence
359
+ };
360
+ }
361
+ const titleGate = await checkTitlePolicyForStage({
362
+ projectRootDir,
363
+ stagePrefix: "judged",
364
+ inflightChapter,
365
+ pipelineStage: stage,
366
+ evidence,
367
+ titleFixCount,
368
+ hasChapter,
369
+ chapterRelPath: rel.staging.chapterMd
370
+ });
371
+ if (titleGate)
372
+ return titleGate;
373
+ const hookGate = await checkHookPolicyForStage({
374
+ projectRootDir,
375
+ stagePrefix: "judged",
376
+ inflightChapter,
377
+ pipelineStage: stage,
378
+ evidence,
379
+ hookFixCount,
380
+ evalRelPath: rel.staging.evalJson
381
+ });
382
+ if (hookGate)
383
+ return hookGate;
384
+ const guardrailsGate = await checkPrejudgeGuardrailsForStage({
385
+ projectRootDir,
386
+ stagePrefix: "judged",
387
+ inflightChapter,
388
+ pipelineStage: stage,
389
+ evidence,
390
+ chapterRelPath: rel.staging.chapterMd
391
+ });
392
+ if (guardrailsGate)
393
+ return guardrailsGate;
394
+ return {
395
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
396
+ reason: "judged:ready_commit",
397
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
398
+ evidence
399
+ };
400
+ }
401
+ // Unknown stage: fall back to safest.
402
+ return {
403
+ step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
404
+ reason: `unknown_stage:${stage}`,
405
+ inflight: { chapter: inflightChapter, pipeline_stage: stage },
406
+ evidence
407
+ };
408
+ }