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.
- package/README.md +1 -1
- package/agents/chapter-writer.md +43 -14
- package/agents/character-weaver.md +7 -1
- package/agents/plot-architect.md +20 -7
- package/agents/quality-judge.md +199 -20
- package/agents/style-analyzer.md +14 -8
- package/agents/style-refiner.md +10 -3
- package/agents/world-builder.md +8 -1
- package/dist/__tests__/agent-prompts-anti-ai-upgrade.test.js +194 -6
- package/dist/__tests__/agent-prompts-platform-expansion.test.js +33 -0
- package/dist/__tests__/anti-ai-infrastructure.test.js +548 -0
- package/dist/__tests__/anti-ai-templates.test.js +2 -2
- package/dist/__tests__/canon-status-lifecycle.test.js +481 -0
- package/dist/__tests__/commit-gate-decision.test.js +65 -0
- package/dist/__tests__/commit-prototype-pollution.test.js +1 -1
- package/dist/__tests__/excitement-type-annotation.test.js +240 -0
- package/dist/__tests__/excitement-type.test.js +21 -0
- package/dist/__tests__/gate-decision.test.js +62 -15
- package/dist/__tests__/genre-excitement-mapping.test.js +355 -0
- package/dist/__tests__/golden-chapter-gates.test.js +79 -0
- package/dist/__tests__/golden-chapter-mini-planning.test.js +485 -0
- package/dist/__tests__/helpers/quickstart-mini-planning.js +61 -0
- package/dist/__tests__/init.test.js +57 -5
- package/dist/__tests__/instructions-platform-expansion.test.js +125 -0
- package/dist/__tests__/next-step-gate-decision-routing.test.js +98 -0
- package/dist/__tests__/orchestrator-state-write-path.test.js +1 -1
- package/dist/__tests__/platform-profile.test.js +57 -1
- package/dist/__tests__/quickstart-pipeline.test.js +73 -6
- package/dist/__tests__/scoring-weights.test.js +193 -0
- package/dist/__tests__/steps-id.test.js +2 -0
- package/dist/__tests__/validate-quickstart-prereqs.test.js +2 -0
- package/dist/advance.js +27 -2
- package/dist/anti-ai-context.js +535 -0
- package/dist/cli.js +3 -1
- package/dist/commit.js +22 -0
- package/dist/excitement-type.js +12 -0
- package/dist/gate-decision.js +98 -2
- package/dist/golden-chapter-gates.js +143 -0
- package/dist/init.js +76 -7
- package/dist/instructions.js +552 -6
- package/dist/next-step.js +124 -88
- package/dist/platform-profile.js +20 -8
- package/dist/quickstart-mini-planning.js +30 -0
- package/dist/scoring-weights.js +38 -3
- package/dist/steps.js +1 -1
- package/dist/validate.js +293 -214
- package/dist/volume-commit.js +271 -5
- package/dist/volume-planning.js +78 -3
- package/docs/user/README.md +1 -0
- package/docs/user/migration-guide.md +166 -0
- package/docs/user/novel-cli.md +4 -3
- package/docs/user/quick-start.md +354 -57
- package/package.json +1 -1
- package/schemas/platform-profile.schema.json +2 -2
- package/scripts/lint-blacklist.sh +221 -76
- package/scripts/lint-structural.sh +538 -0
- package/skills/continue/SKILL.md +6 -0
- package/skills/continue/references/context-contracts.md +71 -6
- package/skills/continue/references/periodic-maintenance.md +12 -1
- package/skills/novel-writing/references/quality-rubric.md +79 -26
- package/skills/novel-writing/references/style-guide.md +129 -19
- package/skills/start/SKILL.md +23 -3
- package/skills/start/references/vol-planning.md +12 -3
- package/templates/ai-blacklist.json +1024 -246
- package/templates/ai-sentence-patterns.json +167 -0
- package/templates/genre-excitement-map.json +48 -0
- package/templates/genre-golden-standards.json +80 -0
- package/templates/genre-weight-profiles.json +15 -0
- package/templates/golden-chapter-gates.json +230 -0
- package/templates/novel-ask/example.question.json +3 -2
- package/templates/platform-profile.json +141 -1
- package/templates/platforms/fanqie.md +35 -0
- package/templates/platforms/jinjiang.md +35 -0
- package/templates/platforms/qidian.md +35 -0
- package/templates/style-profile-template.json +3 -0
package/dist/next-step.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { tryResolveVolumeChapterRange } from "./consistency-auditor.js";
|
|
4
4
|
import { NovelCliError } from "./errors.js";
|
|
5
5
|
import { pathExists, readJsonFile, readTextFile } from "./fs-utils.js";
|
|
6
|
-
import {
|
|
6
|
+
import { evaluateGateDecisionFromEval, normalizeGateMaxRevisions, normalizeGateRevisionCount } from "./gate-decision.js";
|
|
7
7
|
import { checkHookPolicy } from "./hook-policy.js";
|
|
8
8
|
import { loadPlatformProfile } from "./platform-profile.js";
|
|
9
9
|
import { QUICKSTART_STAGING_RELS } from "./quickstart.js";
|
|
@@ -15,7 +15,84 @@ import { summarizeReadabilityIssues } from "./readability-lint.js";
|
|
|
15
15
|
import { computeTitlePolicyReport } from "./title-policy.js";
|
|
16
16
|
import { QUICKSTART_PHASES, chapterRelPaths, formatStepId } from "./steps.js";
|
|
17
17
|
import { isPlainObject } from "./type-guards.js";
|
|
18
|
-
import { computeVolumeNextStep } from "./volume-planning.js";
|
|
18
|
+
import { computeVolumeNextStep, hasQuickstartMiniPlanningArtifacts, volumeFinalRelPaths } from "./volume-planning.js";
|
|
19
|
+
function resolveMaxRevisions(loadedProfile) {
|
|
20
|
+
return normalizeGateMaxRevisions(loadedProfile?.profile.scoring?.max_revisions);
|
|
21
|
+
}
|
|
22
|
+
function routeGateDrivenNextStep(args) {
|
|
23
|
+
const evaluated = evaluateGateDecisionFromEval({
|
|
24
|
+
evalRaw: args.evalRaw,
|
|
25
|
+
revision_count: args.revisionCount,
|
|
26
|
+
...(args.maxRevisions === null ? {} : { max_revisions: args.maxRevisions })
|
|
27
|
+
});
|
|
28
|
+
if (!evaluated.ok) {
|
|
29
|
+
return {
|
|
30
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "judge" }),
|
|
31
|
+
reason: `${args.stagePrefix}:${evaluated.reason}`,
|
|
32
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
33
|
+
evidence: { ...args.evidence }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const gate = evaluated.gate;
|
|
37
|
+
const gateEvidence = {
|
|
38
|
+
...args.evidence,
|
|
39
|
+
gate: {
|
|
40
|
+
decision: gate.decision,
|
|
41
|
+
overall_final: gate.overall_final,
|
|
42
|
+
revision_count: gate.revision_count,
|
|
43
|
+
max_revisions: gate.max_revisions,
|
|
44
|
+
has_high_confidence_violation: gate.has_high_confidence_violation,
|
|
45
|
+
high_confidence_violations: gate.high_confidence_violations.slice(0, 10),
|
|
46
|
+
has_golden_chapter_gate_failure: gate.has_golden_chapter_gate_failure,
|
|
47
|
+
golden_chapter_gate_failures: gate.golden_chapter_gate_failures.slice(0, 10)
|
|
48
|
+
},
|
|
49
|
+
quality_judge: {
|
|
50
|
+
recommendation: gate.recommendation
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
if (gate.decision === "pass") {
|
|
54
|
+
return {
|
|
55
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "commit" }),
|
|
56
|
+
reason: `${args.stagePrefix}:gate:pass`,
|
|
57
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
58
|
+
evidence: gateEvidence
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (gate.decision === "force_passed") {
|
|
62
|
+
return {
|
|
63
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "commit" }),
|
|
64
|
+
reason: `${args.stagePrefix}:gate:force_passed`,
|
|
65
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
66
|
+
evidence: gateEvidence
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (gate.decision === "polish") {
|
|
70
|
+
return {
|
|
71
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "refine" }),
|
|
72
|
+
reason: `${args.stagePrefix}:gate:polish`,
|
|
73
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
74
|
+
evidence: gateEvidence
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (gate.decision === "revise") {
|
|
78
|
+
return {
|
|
79
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "draft" }),
|
|
80
|
+
reason: `${args.stagePrefix}:gate:revise`,
|
|
81
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
82
|
+
evidence: gateEvidence
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (gate.decision === "pause_for_user" || gate.decision === "pause_for_user_force_rewrite") {
|
|
86
|
+
return {
|
|
87
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
|
|
88
|
+
reason: `${args.stagePrefix}:gate:${gate.decision}`,
|
|
89
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
90
|
+
evidence: gateEvidence
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const _exhaustive = gate.decision;
|
|
94
|
+
throw new NovelCliError(`Unsupported gate decision: ${String(_exhaustive)}`, 2);
|
|
95
|
+
}
|
|
19
96
|
function normalizeStage(stage) {
|
|
20
97
|
if (stage === null || stage === undefined)
|
|
21
98
|
return null;
|
|
@@ -368,12 +445,29 @@ async function computeChapterNextStep(projectRootDir, checkpoint) {
|
|
|
368
445
|
});
|
|
369
446
|
if (guardrailsGate)
|
|
370
447
|
return guardrailsGate;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
448
|
+
let evalRaw;
|
|
449
|
+
try {
|
|
450
|
+
evalRaw = await readJsonFile(join(projectRootDir, rel.staging.evalJson));
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
454
|
+
return {
|
|
455
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
|
|
456
|
+
reason: "refined:eval_read_failed",
|
|
457
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
458
|
+
evidence: { ...evidence, error: message }
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const revisionCount = normalizeGateRevisionCount(checkpoint.revision_count);
|
|
462
|
+
return routeGateDrivenNextStep({
|
|
463
|
+
stagePrefix: "refined",
|
|
464
|
+
inflightChapter,
|
|
465
|
+
pipelineStage: stage,
|
|
466
|
+
evidence,
|
|
467
|
+
evalRaw,
|
|
468
|
+
revisionCount,
|
|
469
|
+
maxRevisions: resolveMaxRevisions(loadedProfile)
|
|
470
|
+
});
|
|
377
471
|
}
|
|
378
472
|
if (stage === "judged") {
|
|
379
473
|
if (!hasChapter) {
|
|
@@ -451,87 +545,16 @@ async function computeChapterNextStep(projectRootDir, checkpoint) {
|
|
|
451
545
|
evidence: { ...evidence }
|
|
452
546
|
};
|
|
453
547
|
}
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
const revisionCount = typeof checkpoint.revision_count === "number" && Number.isInteger(checkpoint.revision_count) && checkpoint.revision_count >= 0
|
|
465
|
-
? checkpoint.revision_count
|
|
466
|
-
: 0;
|
|
467
|
-
const violation = detectHighConfidenceViolation(evalRaw);
|
|
468
|
-
const maxRevisions = typeof loadedProfile?.profile.scoring?.max_revisions === "number" &&
|
|
469
|
-
Number.isInteger(loadedProfile.profile.scoring.max_revisions) &&
|
|
470
|
-
loadedProfile.profile.scoring.max_revisions >= 0
|
|
471
|
-
? loadedProfile.profile.scoring.max_revisions
|
|
472
|
-
: null;
|
|
473
|
-
const gateDecision = computeGateDecision({
|
|
474
|
-
overall_final: overall,
|
|
475
|
-
revision_count: revisionCount,
|
|
476
|
-
has_high_confidence_violation: violation.has_high_confidence_violation,
|
|
477
|
-
...(maxRevisions === null ? {} : { max_revisions: maxRevisions })
|
|
548
|
+
const revisionCount = normalizeGateRevisionCount(checkpoint.revision_count);
|
|
549
|
+
return routeGateDrivenNextStep({
|
|
550
|
+
stagePrefix: "judged",
|
|
551
|
+
inflightChapter,
|
|
552
|
+
pipelineStage: stage,
|
|
553
|
+
evidence,
|
|
554
|
+
evalRaw,
|
|
555
|
+
revisionCount,
|
|
556
|
+
maxRevisions: resolveMaxRevisions(loadedProfile)
|
|
478
557
|
});
|
|
479
|
-
const gateEvidence = {
|
|
480
|
-
...evidence,
|
|
481
|
-
gate: {
|
|
482
|
-
decision: gateDecision,
|
|
483
|
-
overall_final: overall,
|
|
484
|
-
revision_count: revisionCount,
|
|
485
|
-
max_revisions: maxRevisions,
|
|
486
|
-
has_high_confidence_violation: violation.has_high_confidence_violation,
|
|
487
|
-
high_confidence_violations: violation.high_confidence_violations.slice(0, 10)
|
|
488
|
-
},
|
|
489
|
-
quality_judge: {
|
|
490
|
-
recommendation: typeof evalObj.recommendation === "string" ? evalObj.recommendation : null
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
if (gateDecision === "pass") {
|
|
494
|
-
return {
|
|
495
|
-
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
|
|
496
|
-
reason: "judged:gate:pass",
|
|
497
|
-
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
498
|
-
evidence: gateEvidence
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
if (gateDecision === "force_passed") {
|
|
502
|
-
return {
|
|
503
|
-
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
|
|
504
|
-
reason: "judged:gate:force_passed",
|
|
505
|
-
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
506
|
-
evidence: gateEvidence
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
if (gateDecision === "polish") {
|
|
510
|
-
return {
|
|
511
|
-
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "refine" }),
|
|
512
|
-
reason: "judged:gate:polish",
|
|
513
|
-
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
514
|
-
evidence: gateEvidence
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
if (gateDecision === "revise") {
|
|
518
|
-
return {
|
|
519
|
-
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
520
|
-
reason: "judged:gate:revise",
|
|
521
|
-
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
522
|
-
evidence: gateEvidence
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
if (gateDecision === "pause_for_user" || gateDecision === "pause_for_user_force_rewrite") {
|
|
526
|
-
return {
|
|
527
|
-
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "review" }),
|
|
528
|
-
reason: `judged:gate:${gateDecision}`,
|
|
529
|
-
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
530
|
-
evidence: gateEvidence
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
const _exhaustive = gateDecision;
|
|
534
|
-
throw new NovelCliError(`Unsupported gate decision: ${String(_exhaustive)}`, 2);
|
|
535
558
|
}
|
|
536
559
|
// Unknown stage: upstream parseCheckpoint validates enum so this should be unreachable.
|
|
537
560
|
throw new NovelCliError(`Checkpoint has unexpected pipeline_stage=${stage}. This should not happen; repair .checkpoint.json and rerun.`, 2);
|
|
@@ -576,6 +599,7 @@ async function computeQuickStartNextStep(projectRootDir, checkpoint) {
|
|
|
576
599
|
const rulesExists = await pathExists(rulesAbs);
|
|
577
600
|
const contracts = await countContractArtifacts(projectRootDir);
|
|
578
601
|
const styleExists = await pathExists(styleAbs);
|
|
602
|
+
const miniPlanningExists = await hasQuickstartMiniPlanningArtifacts(projectRootDir);
|
|
579
603
|
const trialExists = await pathExists(trialAbs);
|
|
580
604
|
const evalExists = await pathExists(evalAbs);
|
|
581
605
|
let rulesOk = false;
|
|
@@ -648,6 +672,7 @@ async function computeQuickStartNextStep(projectRootDir, checkpoint) {
|
|
|
648
672
|
styleExists,
|
|
649
673
|
styleOk,
|
|
650
674
|
...(styleError ? { styleError } : {}),
|
|
675
|
+
miniPlanningExists,
|
|
651
676
|
trialExists,
|
|
652
677
|
trialOk,
|
|
653
678
|
...(trialError ? { trialError } : {}),
|
|
@@ -685,6 +710,15 @@ async function computeQuickStartNextStep(projectRootDir, checkpoint) {
|
|
|
685
710
|
evidence
|
|
686
711
|
};
|
|
687
712
|
}
|
|
713
|
+
else if (!miniPlanningExists) {
|
|
714
|
+
selectedPhase = "f0";
|
|
715
|
+
selected = {
|
|
716
|
+
step: formatStepId({ kind: "quickstart", phase: "f0" }),
|
|
717
|
+
reason: "quickstart:f0",
|
|
718
|
+
inflight: { chapter: null, pipeline_stage: null },
|
|
719
|
+
evidence
|
|
720
|
+
};
|
|
721
|
+
}
|
|
688
722
|
else if (!trialOk) {
|
|
689
723
|
selectedPhase = "trial";
|
|
690
724
|
selected = {
|
|
@@ -728,6 +762,8 @@ async function computeQuickStartNextStep(projectRootDir, checkpoint) {
|
|
|
728
762
|
return QUICKSTART_STAGING_RELS.contractsDir;
|
|
729
763
|
case "style":
|
|
730
764
|
return QUICKSTART_STAGING_RELS.styleProfileJson;
|
|
765
|
+
case "f0":
|
|
766
|
+
return volumeFinalRelPaths(1).outlineMd;
|
|
731
767
|
case "trial":
|
|
732
768
|
return QUICKSTART_STAGING_RELS.trialChapterMd;
|
|
733
769
|
case "results":
|
package/dist/platform-profile.js
CHANGED
|
@@ -2,6 +2,8 @@ import { join } from "node:path";
|
|
|
2
2
|
import { NovelCliError } from "./errors.js";
|
|
3
3
|
import { pathExists, readJsonFile } from "./fs-utils.js";
|
|
4
4
|
import { isPlainObject } from "./type-guards.js";
|
|
5
|
+
export const PLATFORM_IDS = ["qidian", "tomato", "fanqie", "jinjiang"];
|
|
6
|
+
export const CANONICAL_PLATFORM_IDS = ["qidian", "fanqie", "jinjiang"];
|
|
5
7
|
function requireIntField(obj, field, file) {
|
|
6
8
|
const v = obj[field];
|
|
7
9
|
if (typeof v !== "number" || !Number.isInteger(v))
|
|
@@ -33,9 +35,14 @@ function requireStringField(obj, field, file) {
|
|
|
33
35
|
return v;
|
|
34
36
|
}
|
|
35
37
|
function requirePlatformId(value, file) {
|
|
36
|
-
if (value === "qidian" || value === "tomato")
|
|
38
|
+
if (value === "qidian" || value === "tomato" || value === "fanqie" || value === "jinjiang")
|
|
37
39
|
return value;
|
|
38
|
-
throw new NovelCliError(`Invalid ${file}: 'platform' must be one of:
|
|
40
|
+
throw new NovelCliError(`Invalid ${file}: 'platform' must be one of: ${PLATFORM_IDS.join(", ")}.`, 2);
|
|
41
|
+
}
|
|
42
|
+
export function canonicalPlatformId(id) {
|
|
43
|
+
if (id === "tomato")
|
|
44
|
+
return "fanqie";
|
|
45
|
+
return id;
|
|
39
46
|
}
|
|
40
47
|
function requireSeverityPolicy(value, file, field) {
|
|
41
48
|
if (value === "warn" || value === "soft" || value === "hard")
|
|
@@ -46,12 +53,17 @@ function parseWordCountPolicy(raw, file) {
|
|
|
46
53
|
if (!isPlainObject(raw))
|
|
47
54
|
throw new NovelCliError(`Invalid ${file}: 'word_count' must be an object.`, 2);
|
|
48
55
|
const obj = raw;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
const target_min = requireIntField(obj, "target_min", file);
|
|
57
|
+
const target_max = requireIntField(obj, "target_max", file);
|
|
58
|
+
const hard_min = requireIntField(obj, "hard_min", file);
|
|
59
|
+
const hard_max = requireIntField(obj, "hard_max", file);
|
|
60
|
+
if (target_min > target_max) {
|
|
61
|
+
throw new NovelCliError(`Invalid ${file}: 'word_count.target_min' must be <= 'word_count.target_max'.`, 2);
|
|
62
|
+
}
|
|
63
|
+
if (hard_min > hard_max) {
|
|
64
|
+
throw new NovelCliError(`Invalid ${file}: 'word_count.hard_min' must be <= 'word_count.hard_max'.`, 2);
|
|
65
|
+
}
|
|
66
|
+
return { target_min, target_max, hard_min, hard_max };
|
|
55
67
|
}
|
|
56
68
|
function parseInfoLoadPolicy(raw, file) {
|
|
57
69
|
if (!isPlainObject(raw))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const QUICKSTART_MINI_PLANNING_RANGE = { start: 1, end: 3 };
|
|
2
|
+
export function quickstartMiniPlanningChapters(range = QUICKSTART_MINI_PLANNING_RANGE) {
|
|
3
|
+
const chapters = [];
|
|
4
|
+
for (let chapter = range.start; chapter <= range.end; chapter++) {
|
|
5
|
+
chapters.push(chapter);
|
|
6
|
+
}
|
|
7
|
+
return chapters;
|
|
8
|
+
}
|
|
9
|
+
export function extractOutlineChapterNumbers(text) {
|
|
10
|
+
const chapterHeadingRe = /^###\s*第\s*(\d+)\s*章/u;
|
|
11
|
+
const chapters = [];
|
|
12
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
13
|
+
const match = chapterHeadingRe.exec(line);
|
|
14
|
+
if (!match)
|
|
15
|
+
continue;
|
|
16
|
+
const chapter = Number.parseInt(match[1] ?? "", 10);
|
|
17
|
+
if (!Number.isInteger(chapter) || chapter < 1)
|
|
18
|
+
continue;
|
|
19
|
+
chapters.push(chapter);
|
|
20
|
+
}
|
|
21
|
+
return chapters;
|
|
22
|
+
}
|
|
23
|
+
export function matchesQuickstartMiniPlanningSeedSequence(chapters) {
|
|
24
|
+
const expectedChapters = quickstartMiniPlanningChapters();
|
|
25
|
+
return chapters.length === expectedChapters.length && chapters.every((chapter, index) => chapter === expectedChapters[index]);
|
|
26
|
+
}
|
|
27
|
+
export function startsWithQuickstartMiniPlanningSeedSequence(chapters) {
|
|
28
|
+
const expectedChapters = quickstartMiniPlanningChapters();
|
|
29
|
+
return chapters.length >= expectedChapters.length && expectedChapters.every((chapter, index) => chapters[index] === chapter);
|
|
30
|
+
}
|
package/dist/scoring-weights.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { NovelCliError } from "./errors.js";
|
|
3
3
|
import { pathExists, readJsonFile, writeJsonFile } from "./fs-utils.js";
|
|
4
|
+
import { CANONICAL_PLATFORM_IDS, canonicalPlatformId } from "./platform-profile.js";
|
|
4
5
|
import { isPlainObject } from "./type-guards.js";
|
|
5
6
|
const CORE_DIMENSIONS = [
|
|
6
7
|
"plot_logic",
|
|
@@ -13,7 +14,11 @@ const CORE_DIMENSIONS = [
|
|
|
13
14
|
"storyline_coherence",
|
|
14
15
|
];
|
|
15
16
|
const OPTIONAL_DIMENSIONS = ["hook_strength"];
|
|
16
|
-
const
|
|
17
|
+
export const KNOWN_SCORING_DIMENSIONS = [...CORE_DIMENSIONS, ...OPTIONAL_DIMENSIONS];
|
|
18
|
+
const KNOWN_DIMENSIONS = new Set(KNOWN_SCORING_DIMENSIONS);
|
|
19
|
+
export function isKnownScoringDimension(value) {
|
|
20
|
+
return KNOWN_DIMENSIONS.has(value);
|
|
21
|
+
}
|
|
17
22
|
function requireIntField(obj, field, file) {
|
|
18
23
|
const v = obj[field];
|
|
19
24
|
if (typeof v !== "number" || !Number.isInteger(v))
|
|
@@ -90,6 +95,7 @@ export function parseGenreWeightProfiles(raw, file) {
|
|
|
90
95
|
throw new NovelCliError(`Invalid ${file}: 'profiles' must be an object.`, 2);
|
|
91
96
|
const profilesRaw = obj.profiles;
|
|
92
97
|
const profiles = {};
|
|
98
|
+
const canonicalPlatforms = new Set(CANONICAL_PLATFORM_IDS);
|
|
93
99
|
const allowedDims = new Set(dimensions);
|
|
94
100
|
for (const [profileId, profileRaw] of Object.entries(profilesRaw)) {
|
|
95
101
|
if (!isPlainObject(profileRaw))
|
|
@@ -129,6 +135,31 @@ export function parseGenreWeightProfiles(raw, file) {
|
|
|
129
135
|
default_profile_by_drive_type,
|
|
130
136
|
profiles
|
|
131
137
|
};
|
|
138
|
+
if (obj.platform_multipliers !== undefined) {
|
|
139
|
+
if (!isPlainObject(obj.platform_multipliers)) {
|
|
140
|
+
throw new NovelCliError(`Invalid ${file}: 'platform_multipliers' must be an object.`, 2);
|
|
141
|
+
}
|
|
142
|
+
const multipliersRaw = obj.platform_multipliers;
|
|
143
|
+
const platform_multipliers = {};
|
|
144
|
+
for (const [platformId, platformRaw] of Object.entries(multipliersRaw)) {
|
|
145
|
+
if (!canonicalPlatforms.has(platformId)) {
|
|
146
|
+
throw new NovelCliError(`Invalid ${file}: unknown platform_multipliers key '${platformId}' (allowed canonical ids: ${CANONICAL_PLATFORM_IDS.join(", ")}).`, 2);
|
|
147
|
+
}
|
|
148
|
+
if (!isPlainObject(platformRaw)) {
|
|
149
|
+
throw new NovelCliError(`Invalid ${file}: 'platform_multipliers.${platformId}' must be an object.`, 2);
|
|
150
|
+
}
|
|
151
|
+
const weightsRaw = platformRaw;
|
|
152
|
+
const parsedWeights = {};
|
|
153
|
+
for (const [dim, value] of Object.entries(weightsRaw)) {
|
|
154
|
+
if (!allowedDims.has(dim)) {
|
|
155
|
+
throw new NovelCliError(`Invalid ${file}: unknown dimension '${dim}' in platform_multipliers.${platformId} (allowed: ${dimensions.join(", ")}).`, 2);
|
|
156
|
+
}
|
|
157
|
+
parsedWeights[dim] = requireFiniteNonNegativeNumber(value, file, `platform_multipliers.${platformId}.${dim}`);
|
|
158
|
+
}
|
|
159
|
+
platform_multipliers[platformId] = parsedWeights;
|
|
160
|
+
}
|
|
161
|
+
out.platform_multipliers = platform_multipliers;
|
|
162
|
+
}
|
|
132
163
|
if (typeof obj.description === "string" && obj.description.trim().length > 0)
|
|
133
164
|
out.description = obj.description.trim();
|
|
134
165
|
if (typeof obj.last_updated === "string" && obj.last_updated.trim().length > 0)
|
|
@@ -188,6 +219,7 @@ export function computeEffectiveScoringWeights(args) {
|
|
|
188
219
|
const dims = args.hookPolicy?.required ? configDims : configDims.filter((d) => d !== "hook_strength");
|
|
189
220
|
const overridesRaw = args.scoring.weight_overrides ?? null;
|
|
190
221
|
const overrides = overridesRaw ? { ...overridesRaw } : null;
|
|
222
|
+
const platformMultipliers = args.platformId !== undefined ? args.config.platform_multipliers?.[canonicalPlatformId(args.platformId)] ?? null : null;
|
|
191
223
|
const allowedDims = new Set(configDims);
|
|
192
224
|
const effectiveDims = new Set(dims);
|
|
193
225
|
if (overrides) {
|
|
@@ -207,7 +239,9 @@ export function computeEffectiveScoringWeights(args) {
|
|
|
207
239
|
}
|
|
208
240
|
const rawWeights = {};
|
|
209
241
|
for (const dim of dims) {
|
|
210
|
-
|
|
242
|
+
const baseWeight = typeof overrides?.[dim] === "number" ? overrides[dim] : profile.weights[dim];
|
|
243
|
+
const multiplier = platformMultipliers?.[dim] ?? 1.0;
|
|
244
|
+
rawWeights[dim] = baseWeight * multiplier;
|
|
211
245
|
}
|
|
212
246
|
const normalization = normalizeWeights({
|
|
213
247
|
dimensions: dims,
|
|
@@ -247,7 +281,8 @@ export async function attachScoringWeightsToEval(args) {
|
|
|
247
281
|
const effective = computeEffectiveScoringWeights({
|
|
248
282
|
config: args.genreWeightProfiles.config,
|
|
249
283
|
scoring,
|
|
250
|
-
hookPolicy: args.platformProfile.hook_policy
|
|
284
|
+
hookPolicy: args.platformProfile.hook_policy,
|
|
285
|
+
platformId: args.platformProfile.platform
|
|
251
286
|
});
|
|
252
287
|
const raw = await readJsonFile(args.evalAbsPath);
|
|
253
288
|
if (!isPlainObject(raw))
|
package/dist/steps.js
CHANGED
|
@@ -10,7 +10,7 @@ export const ORCHESTRATOR_STATES = [
|
|
|
10
10
|
];
|
|
11
11
|
export const CHAPTER_STAGES = ["draft", "summarize", "refine", "judge", "title-fix", "hook-fix", "review", "commit"];
|
|
12
12
|
export const VOLUME_PHASES = ["outline", "validate", "commit"];
|
|
13
|
-
export const QUICKSTART_PHASES = ["world", "characters", "style", "trial", "results"];
|
|
13
|
+
export const QUICKSTART_PHASES = ["world", "characters", "style", "f0", "trial", "results"];
|
|
14
14
|
export const REVIEW_PHASES = ["collect", "audit", "report", "cleanup", "transition"];
|
|
15
15
|
export function pad3(n) {
|
|
16
16
|
return String(n).padStart(3, "0");
|