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
package/dist/next-step.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { tryResolveVolumeChapterRange } from "./consistency-auditor.js";
|
|
3
|
+
import { NovelCliError } from "./errors.js";
|
|
2
4
|
import { pathExists, readJsonFile, readTextFile } from "./fs-utils.js";
|
|
5
|
+
import { computeGateDecision, detectHighConfidenceViolation } from "./gate-decision.js";
|
|
3
6
|
import { checkHookPolicy } from "./hook-policy.js";
|
|
4
7
|
import { loadPlatformProfile } from "./platform-profile.js";
|
|
8
|
+
import { computeReviewNext } from "./volume-review.js";
|
|
5
9
|
import { computePrejudgeGuardrailsReport, loadPrejudgeGuardrailsReportIfFresh, prejudgeGuardrailsRelPath } from "./prejudge-guardrails.js";
|
|
6
10
|
import { summarizeNamingIssues } from "./naming-lint.js";
|
|
7
11
|
import { summarizeReadabilityIssues } from "./readability-lint.js";
|
|
8
12
|
import { computeTitlePolicyReport } from "./title-policy.js";
|
|
9
13
|
import { chapterRelPaths, formatStepId } from "./steps.js";
|
|
14
|
+
import { isPlainObject } from "./type-guards.js";
|
|
15
|
+
import { computeVolumeNextStep } from "./volume-planning.js";
|
|
10
16
|
function normalizeStage(stage) {
|
|
11
17
|
if (stage === null || stage === undefined)
|
|
12
18
|
return null;
|
|
@@ -15,8 +21,7 @@ function normalizeStage(stage) {
|
|
|
15
21
|
return null;
|
|
16
22
|
}
|
|
17
23
|
async function checkHookPolicyForStage(args) {
|
|
18
|
-
const
|
|
19
|
-
const hookPolicy = loadedProfile?.profile.hook_policy;
|
|
24
|
+
const hookPolicy = args.loadedProfile?.profile.hook_policy;
|
|
20
25
|
if (!hookPolicy?.required)
|
|
21
26
|
return null;
|
|
22
27
|
let evalRaw;
|
|
@@ -60,10 +65,9 @@ async function checkHookPolicyForStage(args) {
|
|
|
60
65
|
return null;
|
|
61
66
|
}
|
|
62
67
|
async function checkTitlePolicyForStage(args) {
|
|
63
|
-
|
|
64
|
-
if (!loadedProfile)
|
|
68
|
+
if (!args.loadedProfile)
|
|
65
69
|
return null;
|
|
66
|
-
const titlePolicy = loadedProfile.profile.retention?.title_policy;
|
|
70
|
+
const titlePolicy = args.loadedProfile.profile.retention?.title_policy;
|
|
67
71
|
if (!titlePolicy?.enabled)
|
|
68
72
|
return null;
|
|
69
73
|
if (!args.hasChapter) {
|
|
@@ -87,7 +91,7 @@ async function checkTitlePolicyForStage(args) {
|
|
|
87
91
|
evidence: { ...args.evidence, titleFixCount: args.titleFixCount, error: message }
|
|
88
92
|
};
|
|
89
93
|
}
|
|
90
|
-
const report = computeTitlePolicyReport({ chapter: args.inflightChapter, chapterText, platformProfile: loadedProfile.profile });
|
|
94
|
+
const report = computeTitlePolicyReport({ chapter: args.inflightChapter, chapterText, platformProfile: args.loadedProfile.profile });
|
|
91
95
|
if (report.status === "pass" || report.status === "skipped")
|
|
92
96
|
return null;
|
|
93
97
|
if (!report.has_hard_violations && !titlePolicy.auto_fix)
|
|
@@ -118,8 +122,7 @@ async function checkTitlePolicyForStage(args) {
|
|
|
118
122
|
};
|
|
119
123
|
}
|
|
120
124
|
async function checkPrejudgeGuardrailsForStage(args) {
|
|
121
|
-
|
|
122
|
-
if (!loadedProfile)
|
|
125
|
+
if (!args.loadedProfile)
|
|
123
126
|
return null;
|
|
124
127
|
const chapterAbsPath = join(args.projectRootDir, args.chapterRelPath);
|
|
125
128
|
const cacheRelPath = prejudgeGuardrailsRelPath(args.inflightChapter);
|
|
@@ -128,8 +131,8 @@ async function checkPrejudgeGuardrailsForStage(args) {
|
|
|
128
131
|
rootDir: args.projectRootDir,
|
|
129
132
|
chapter: args.inflightChapter,
|
|
130
133
|
chapterAbsPath,
|
|
131
|
-
platformProfileRelPath: loadedProfile.relPath,
|
|
132
|
-
platformProfile: loadedProfile.profile
|
|
134
|
+
platformProfileRelPath: args.loadedProfile.relPath,
|
|
135
|
+
platformProfile: args.loadedProfile.profile
|
|
133
136
|
});
|
|
134
137
|
if (report)
|
|
135
138
|
cacheStatus = "hit";
|
|
@@ -139,8 +142,8 @@ async function checkPrejudgeGuardrailsForStage(args) {
|
|
|
139
142
|
rootDir: args.projectRootDir,
|
|
140
143
|
chapter: args.inflightChapter,
|
|
141
144
|
chapterAbsPath,
|
|
142
|
-
platformProfileRelPath: loadedProfile.relPath,
|
|
143
|
-
platformProfile: loadedProfile.profile
|
|
145
|
+
platformProfileRelPath: args.loadedProfile.relPath,
|
|
146
|
+
platformProfile: args.loadedProfile.profile
|
|
144
147
|
});
|
|
145
148
|
}
|
|
146
149
|
catch (err) {
|
|
@@ -193,13 +196,33 @@ async function checkPrejudgeGuardrailsForStage(args) {
|
|
|
193
196
|
}
|
|
194
197
|
};
|
|
195
198
|
}
|
|
196
|
-
|
|
199
|
+
async function computeChapterNextStep(projectRootDir, checkpoint) {
|
|
197
200
|
const inflightChapter = typeof checkpoint.inflight_chapter === "number" ? checkpoint.inflight_chapter : null;
|
|
198
201
|
const stage = normalizeStage(checkpoint.pipeline_stage);
|
|
199
202
|
const hookFixCount = typeof checkpoint.hook_fix_count === "number" ? checkpoint.hook_fix_count : 0;
|
|
200
203
|
const titleFixCount = typeof checkpoint.title_fix_count === "number" ? checkpoint.title_fix_count : 0;
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
if (inflightChapter !== null && inflightChapter < 1) {
|
|
205
|
+
throw new NovelCliError(".checkpoint.json.inflight_chapter must be an int >= 1 (or null).", 2);
|
|
206
|
+
}
|
|
207
|
+
if (stage === null || stage === "committed") {
|
|
208
|
+
if (inflightChapter !== null) {
|
|
209
|
+
throw new NovelCliError(`Checkpoint inconsistent: pipeline_stage=${stage ?? "null"} but inflight_chapter=${inflightChapter}. Set inflight_chapter to null.`, 2);
|
|
210
|
+
}
|
|
211
|
+
// Volume-end: enter deterministic volume review pipeline (issue #144).
|
|
212
|
+
if (checkpoint.last_completed_chapter > 0) {
|
|
213
|
+
let range = null;
|
|
214
|
+
try {
|
|
215
|
+
range = await tryResolveVolumeChapterRange({ rootDir: projectRootDir, volume: checkpoint.current_volume });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Best-effort: if we can't resolve range, fall back to chapter pipeline.
|
|
219
|
+
range = null;
|
|
220
|
+
}
|
|
221
|
+
if (range && checkpoint.last_completed_chapter === range.end) {
|
|
222
|
+
const next = await computeReviewNext(projectRootDir, checkpoint);
|
|
223
|
+
return { ...next, reason: `volume_end:${next.reason}` };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
203
226
|
const nextChapter = checkpoint.last_completed_chapter + 1;
|
|
204
227
|
return {
|
|
205
228
|
step: formatStepId({ kind: "chapter", chapter: nextChapter, stage: "draft" }),
|
|
@@ -207,6 +230,9 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
207
230
|
inflight: { chapter: null, pipeline_stage: stage }
|
|
208
231
|
};
|
|
209
232
|
}
|
|
233
|
+
if (inflightChapter === null) {
|
|
234
|
+
throw new NovelCliError(`Checkpoint inconsistent: pipeline_stage=${stage} requires inflight_chapter. Repair .checkpoint.json and rerun.`, 2);
|
|
235
|
+
}
|
|
210
236
|
const rel = chapterRelPaths(inflightChapter);
|
|
211
237
|
const hasChapter = await pathExists(join(projectRootDir, rel.staging.chapterMd));
|
|
212
238
|
const hasSummary = await pathExists(join(projectRootDir, rel.staging.summaryMd));
|
|
@@ -281,6 +307,7 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
281
307
|
evidence
|
|
282
308
|
};
|
|
283
309
|
}
|
|
310
|
+
const loadedProfile = await loadPlatformProfile(projectRootDir);
|
|
284
311
|
if (!hasEval) {
|
|
285
312
|
const titleGate = await checkTitlePolicyForStage({
|
|
286
313
|
projectRootDir,
|
|
@@ -290,7 +317,8 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
290
317
|
evidence,
|
|
291
318
|
titleFixCount,
|
|
292
319
|
hasChapter,
|
|
293
|
-
chapterRelPath: rel.staging.chapterMd
|
|
320
|
+
chapterRelPath: rel.staging.chapterMd,
|
|
321
|
+
loadedProfile
|
|
294
322
|
});
|
|
295
323
|
if (titleGate)
|
|
296
324
|
return titleGate;
|
|
@@ -309,7 +337,8 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
309
337
|
evidence,
|
|
310
338
|
titleFixCount,
|
|
311
339
|
hasChapter,
|
|
312
|
-
chapterRelPath: rel.staging.chapterMd
|
|
340
|
+
chapterRelPath: rel.staging.chapterMd,
|
|
341
|
+
loadedProfile
|
|
313
342
|
});
|
|
314
343
|
if (titleGate)
|
|
315
344
|
return titleGate;
|
|
@@ -320,7 +349,8 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
320
349
|
pipelineStage: stage,
|
|
321
350
|
evidence,
|
|
322
351
|
hookFixCount,
|
|
323
|
-
evalRelPath: rel.staging.evalJson
|
|
352
|
+
evalRelPath: rel.staging.evalJson,
|
|
353
|
+
loadedProfile
|
|
324
354
|
});
|
|
325
355
|
if (hookGate)
|
|
326
356
|
return hookGate;
|
|
@@ -330,7 +360,8 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
330
360
|
inflightChapter,
|
|
331
361
|
pipelineStage: stage,
|
|
332
362
|
evidence,
|
|
333
|
-
chapterRelPath: rel.staging.chapterMd
|
|
363
|
+
chapterRelPath: rel.staging.chapterMd,
|
|
364
|
+
loadedProfile
|
|
334
365
|
});
|
|
335
366
|
if (guardrailsGate)
|
|
336
367
|
return guardrailsGate;
|
|
@@ -358,6 +389,7 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
358
389
|
evidence
|
|
359
390
|
};
|
|
360
391
|
}
|
|
392
|
+
const loadedProfile = await loadPlatformProfile(projectRootDir);
|
|
361
393
|
const titleGate = await checkTitlePolicyForStage({
|
|
362
394
|
projectRootDir,
|
|
363
395
|
stagePrefix: "judged",
|
|
@@ -366,7 +398,8 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
366
398
|
evidence,
|
|
367
399
|
titleFixCount,
|
|
368
400
|
hasChapter,
|
|
369
|
-
chapterRelPath: rel.staging.chapterMd
|
|
401
|
+
chapterRelPath: rel.staging.chapterMd,
|
|
402
|
+
loadedProfile
|
|
370
403
|
});
|
|
371
404
|
if (titleGate)
|
|
372
405
|
return titleGate;
|
|
@@ -377,7 +410,8 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
377
410
|
pipelineStage: stage,
|
|
378
411
|
evidence,
|
|
379
412
|
hookFixCount,
|
|
380
|
-
evalRelPath: rel.staging.evalJson
|
|
413
|
+
evalRelPath: rel.staging.evalJson,
|
|
414
|
+
loadedProfile
|
|
381
415
|
});
|
|
382
416
|
if (hookGate)
|
|
383
417
|
return hookGate;
|
|
@@ -387,22 +421,152 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
387
421
|
inflightChapter,
|
|
388
422
|
pipelineStage: stage,
|
|
389
423
|
evidence,
|
|
390
|
-
chapterRelPath: rel.staging.chapterMd
|
|
424
|
+
chapterRelPath: rel.staging.chapterMd,
|
|
425
|
+
loadedProfile
|
|
391
426
|
});
|
|
392
427
|
if (guardrailsGate)
|
|
393
428
|
return guardrailsGate;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
429
|
+
// Gate decision: deterministic mapping from QualityJudge outputs → next action.
|
|
430
|
+
let evalRaw;
|
|
431
|
+
try {
|
|
432
|
+
evalRaw = await readJsonFile(join(projectRootDir, rel.staging.evalJson));
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
436
|
+
return {
|
|
437
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
|
|
438
|
+
reason: `judged:eval_read_failed`,
|
|
439
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
440
|
+
evidence: { ...evidence, error: message }
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (!isPlainObject(evalRaw)) {
|
|
444
|
+
return {
|
|
445
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
|
|
446
|
+
reason: `judged:eval_invalid`,
|
|
447
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
448
|
+
evidence: { ...evidence }
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const evalObj = evalRaw;
|
|
452
|
+
const overall = typeof evalObj.overall_final === "number" ? evalObj.overall_final : typeof evalObj.overall === "number" ? evalObj.overall : null;
|
|
453
|
+
if (overall === null || !Number.isFinite(overall)) {
|
|
454
|
+
return {
|
|
455
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
|
|
456
|
+
reason: `judged:eval_missing_overall`,
|
|
457
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
458
|
+
evidence: { ...evidence }
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const revisionCount = typeof checkpoint.revision_count === "number" && Number.isInteger(checkpoint.revision_count) && checkpoint.revision_count >= 0
|
|
462
|
+
? checkpoint.revision_count
|
|
463
|
+
: 0;
|
|
464
|
+
const violation = detectHighConfidenceViolation(evalRaw);
|
|
465
|
+
const maxRevisions = typeof loadedProfile?.profile.scoring?.max_revisions === "number" &&
|
|
466
|
+
Number.isInteger(loadedProfile.profile.scoring.max_revisions) &&
|
|
467
|
+
loadedProfile.profile.scoring.max_revisions >= 0
|
|
468
|
+
? loadedProfile.profile.scoring.max_revisions
|
|
469
|
+
: null;
|
|
470
|
+
const gateDecision = computeGateDecision({
|
|
471
|
+
overall_final: overall,
|
|
472
|
+
revision_count: revisionCount,
|
|
473
|
+
has_high_confidence_violation: violation.has_high_confidence_violation,
|
|
474
|
+
...(maxRevisions === null ? {} : { max_revisions: maxRevisions })
|
|
475
|
+
});
|
|
476
|
+
const gateEvidence = {
|
|
477
|
+
...evidence,
|
|
478
|
+
gate: {
|
|
479
|
+
decision: gateDecision,
|
|
480
|
+
overall_final: overall,
|
|
481
|
+
revision_count: revisionCount,
|
|
482
|
+
max_revisions: maxRevisions,
|
|
483
|
+
has_high_confidence_violation: violation.has_high_confidence_violation,
|
|
484
|
+
high_confidence_violations: violation.high_confidence_violations.slice(0, 10)
|
|
485
|
+
},
|
|
486
|
+
quality_judge: {
|
|
487
|
+
recommendation: typeof evalObj.recommendation === "string" ? evalObj.recommendation : null
|
|
488
|
+
}
|
|
399
489
|
};
|
|
490
|
+
if (gateDecision === "pass") {
|
|
491
|
+
return {
|
|
492
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
|
|
493
|
+
reason: "judged:gate:pass",
|
|
494
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
495
|
+
evidence: gateEvidence
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (gateDecision === "force_passed") {
|
|
499
|
+
return {
|
|
500
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
|
|
501
|
+
reason: "judged:gate:force_passed",
|
|
502
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
503
|
+
evidence: gateEvidence
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (gateDecision === "polish") {
|
|
507
|
+
return {
|
|
508
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "refine" }),
|
|
509
|
+
reason: "judged:gate:polish",
|
|
510
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
511
|
+
evidence: gateEvidence
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
if (gateDecision === "revise") {
|
|
515
|
+
return {
|
|
516
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
517
|
+
reason: "judged:gate:revise",
|
|
518
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
519
|
+
evidence: gateEvidence
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (gateDecision === "pause_for_user" || gateDecision === "pause_for_user_force_rewrite") {
|
|
523
|
+
return {
|
|
524
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "review" }),
|
|
525
|
+
reason: `judged:gate:${gateDecision}`,
|
|
526
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
527
|
+
evidence: gateEvidence
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const _exhaustive = gateDecision;
|
|
531
|
+
throw new NovelCliError(`Unsupported gate decision: ${String(_exhaustive)}`, 2);
|
|
532
|
+
}
|
|
533
|
+
// Unknown stage: upstream parseCheckpoint validates enum so this should be unreachable.
|
|
534
|
+
throw new NovelCliError(`Checkpoint has unexpected pipeline_stage=${stage}. This should not happen; repair .checkpoint.json and rerun.`, 2);
|
|
535
|
+
}
|
|
536
|
+
function notImplementedState(state) {
|
|
537
|
+
throw new NovelCliError(`Not implemented: orchestrator_state=${state}`, 2);
|
|
538
|
+
}
|
|
539
|
+
export async function computeNextStep(projectRootDir, checkpoint) {
|
|
540
|
+
switch (checkpoint.orchestrator_state) {
|
|
541
|
+
case "WRITING":
|
|
542
|
+
case "CHAPTER_REWRITE":
|
|
543
|
+
return await computeChapterNextStep(projectRootDir, checkpoint);
|
|
544
|
+
case "ERROR_RETRY": {
|
|
545
|
+
const stage = normalizeStage(checkpoint.pipeline_stage);
|
|
546
|
+
const inflight = typeof checkpoint.inflight_chapter === "number" ? checkpoint.inflight_chapter : null;
|
|
547
|
+
let normalizedCheckpoint = checkpoint;
|
|
548
|
+
let healPrefix = "";
|
|
549
|
+
// Only auto-heal invariants when explicitly in ERROR_RETRY.
|
|
550
|
+
if ((stage === null || stage === "committed") && inflight !== null) {
|
|
551
|
+
normalizedCheckpoint = { ...checkpoint, inflight_chapter: null };
|
|
552
|
+
healPrefix = "healed_drop_inflight:";
|
|
553
|
+
}
|
|
554
|
+
else if (stage !== null && stage !== "committed" && inflight === null) {
|
|
555
|
+
normalizedCheckpoint = { ...checkpoint, inflight_chapter: checkpoint.last_completed_chapter + 1 };
|
|
556
|
+
healPrefix = "healed_infer_inflight:";
|
|
557
|
+
}
|
|
558
|
+
const next = await computeChapterNextStep(projectRootDir, normalizedCheckpoint);
|
|
559
|
+
return { ...next, reason: `error_retry:${healPrefix}${next.reason}` };
|
|
560
|
+
}
|
|
561
|
+
case "INIT":
|
|
562
|
+
return notImplementedState(checkpoint.orchestrator_state);
|
|
563
|
+
case "QUICK_START":
|
|
564
|
+
return notImplementedState(checkpoint.orchestrator_state);
|
|
565
|
+
case "VOL_PLANNING":
|
|
566
|
+
return await computeVolumeNextStep(projectRootDir, checkpoint);
|
|
567
|
+
case "VOL_REVIEW":
|
|
568
|
+
return await computeReviewNext(projectRootDir, checkpoint);
|
|
569
|
+
default:
|
|
570
|
+
return notImplementedState(checkpoint.orchestrator_state);
|
|
400
571
|
}
|
|
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
572
|
}
|
package/dist/platform-profile.js
CHANGED
|
@@ -122,6 +122,9 @@ function parseScoringPolicy(raw, file) {
|
|
|
122
122
|
genre_drive_type: requireStringField(obj, "genre_drive_type", file),
|
|
123
123
|
weight_profile_id: requireStringField(obj, "weight_profile_id", file)
|
|
124
124
|
};
|
|
125
|
+
if (obj.max_revisions !== undefined) {
|
|
126
|
+
out.max_revisions = requireNonNegativeIntValue(obj.max_revisions, file, "scoring.max_revisions");
|
|
127
|
+
}
|
|
125
128
|
if (obj.weight_overrides !== undefined) {
|
|
126
129
|
if (!isPlainObject(obj.weight_overrides))
|
|
127
130
|
throw new NovelCliError(`Invalid ${file}: 'scoring.weight_overrides' must be an object.`, 2);
|
package/dist/steps.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { NovelCliError } from "./errors.js";
|
|
2
|
+
export const ORCHESTRATOR_STATES = [
|
|
3
|
+
"INIT",
|
|
4
|
+
"QUICK_START",
|
|
5
|
+
"VOL_PLANNING",
|
|
6
|
+
"WRITING",
|
|
7
|
+
"CHAPTER_REWRITE",
|
|
8
|
+
"VOL_REVIEW",
|
|
9
|
+
"ERROR_RETRY"
|
|
10
|
+
];
|
|
2
11
|
export const CHAPTER_STAGES = ["draft", "summarize", "refine", "judge", "title-fix", "hook-fix", "review", "commit"];
|
|
12
|
+
export const VOLUME_PHASES = ["outline", "validate", "commit"];
|
|
13
|
+
export const QUICKSTART_PHASES = ["world", "characters", "style", "trial", "results"];
|
|
14
|
+
export const REVIEW_PHASES = ["collect", "audit", "report", "cleanup", "transition"];
|
|
3
15
|
export function pad3(n) {
|
|
4
16
|
return String(n).padStart(3, "0");
|
|
5
17
|
}
|
|
@@ -10,29 +22,60 @@ export function titleFixSnapshotRel(chapter) {
|
|
|
10
22
|
return `staging/logs/title-fix-chapter-${pad3(chapter)}-before.md`;
|
|
11
23
|
}
|
|
12
24
|
export function formatStepId(step) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
switch (step.kind) {
|
|
26
|
+
case "chapter":
|
|
27
|
+
return `chapter:${pad3(step.chapter)}:${step.stage}`;
|
|
28
|
+
case "volume":
|
|
29
|
+
return `volume:${step.phase}`;
|
|
30
|
+
case "quickstart":
|
|
31
|
+
return `quickstart:${step.phase}`;
|
|
32
|
+
case "review":
|
|
33
|
+
return `review:${step.phase}`;
|
|
34
|
+
default:
|
|
35
|
+
throw new NovelCliError(`Unsupported step kind: ${step.kind}`, 2);
|
|
36
|
+
}
|
|
16
37
|
}
|
|
17
38
|
export function parseStepId(input) {
|
|
18
39
|
const trimmed = input.trim();
|
|
19
40
|
const parts = trimmed.split(":");
|
|
20
|
-
if (parts.length
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
if (parts.length === 3) {
|
|
42
|
+
const [kind, chapterRaw, stageRaw] = parts;
|
|
43
|
+
if (kind !== "chapter")
|
|
44
|
+
throw new NovelCliError(`Invalid step id: ${input}. Expected kind 'chapter'.`, 2);
|
|
45
|
+
if (!/^\d+$/.test(chapterRaw))
|
|
46
|
+
throw new NovelCliError(`Invalid step id: ${input}. Chapter must be a number.`, 2);
|
|
47
|
+
const chapter = Number.parseInt(chapterRaw, 10);
|
|
48
|
+
if (!Number.isInteger(chapter) || chapter <= 0) {
|
|
49
|
+
throw new NovelCliError(`Invalid step id: ${input}. Chapter must be an int >= 1.`, 2);
|
|
50
|
+
}
|
|
51
|
+
if (!CHAPTER_STAGES.includes(stageRaw)) {
|
|
52
|
+
throw new NovelCliError(`Invalid step id: ${input}. Stage must be one of: ${CHAPTER_STAGES.join(", ")}`, 2);
|
|
53
|
+
}
|
|
54
|
+
return { kind: "chapter", chapter, stage: stageRaw };
|
|
31
55
|
}
|
|
32
|
-
if (
|
|
33
|
-
|
|
56
|
+
if (parts.length === 2) {
|
|
57
|
+
const [kind, phaseRaw] = parts;
|
|
58
|
+
if (kind === "volume") {
|
|
59
|
+
if (!VOLUME_PHASES.includes(phaseRaw)) {
|
|
60
|
+
throw new NovelCliError(`Invalid step id: ${input}. Phase must be one of: ${VOLUME_PHASES.join(", ")}`, 2);
|
|
61
|
+
}
|
|
62
|
+
return { kind: "volume", phase: phaseRaw };
|
|
63
|
+
}
|
|
64
|
+
if (kind === "quickstart") {
|
|
65
|
+
if (!QUICKSTART_PHASES.includes(phaseRaw)) {
|
|
66
|
+
throw new NovelCliError(`Invalid step id: ${input}. Phase must be one of: ${QUICKSTART_PHASES.join(", ")}`, 2);
|
|
67
|
+
}
|
|
68
|
+
return { kind: "quickstart", phase: phaseRaw };
|
|
69
|
+
}
|
|
70
|
+
if (kind === "review") {
|
|
71
|
+
if (!REVIEW_PHASES.includes(phaseRaw)) {
|
|
72
|
+
throw new NovelCliError(`Invalid step id: ${input}. Phase must be one of: ${REVIEW_PHASES.join(", ")}`, 2);
|
|
73
|
+
}
|
|
74
|
+
return { kind: "review", phase: phaseRaw };
|
|
75
|
+
}
|
|
76
|
+
throw new NovelCliError(`Invalid step id: ${input}. Supported kinds: chapter, volume, quickstart, review.`, 2);
|
|
34
77
|
}
|
|
35
|
-
|
|
78
|
+
throw new NovelCliError(`Invalid step id: ${input}. Expected format: chapter:048:draft (or quickstart:world).`, 2);
|
|
36
79
|
}
|
|
37
80
|
export function chapterRelPaths(chapter, storylineId) {
|
|
38
81
|
const id = pad3(chapter);
|