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/gate-decision.js
CHANGED
|
@@ -38,10 +38,10 @@ export function detectHighConfidenceViolation(evalRaw) {
|
|
|
38
38
|
return { has_high_confidence_violation: hardChecks.length > 0, high_confidence_violations: hardChecks };
|
|
39
39
|
}
|
|
40
40
|
export function computeGateDecision(args) {
|
|
41
|
-
const maxRevisions =
|
|
41
|
+
const maxRevisions = normalizeMaxRevisions(args.max_revisions);
|
|
42
42
|
if (args.force_pass)
|
|
43
43
|
return "force_passed";
|
|
44
|
-
if (args.has_high_confidence_violation) {
|
|
44
|
+
if (args.has_high_confidence_violation || args.has_golden_chapter_gate_failure) {
|
|
45
45
|
return args.revision_count >= maxRevisions ? "pause_for_user" : "revise";
|
|
46
46
|
}
|
|
47
47
|
const score = args.overall_final;
|
|
@@ -57,3 +57,99 @@ export function computeGateDecision(args) {
|
|
|
57
57
|
return "pause_for_user";
|
|
58
58
|
return "pause_for_user_force_rewrite";
|
|
59
59
|
}
|
|
60
|
+
function normalizeMaxRevisions(value) {
|
|
61
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 2;
|
|
62
|
+
}
|
|
63
|
+
export function normalizeGateRevisionCount(value) {
|
|
64
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
65
|
+
}
|
|
66
|
+
export function normalizeGateMaxRevisions(value) {
|
|
67
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
68
|
+
}
|
|
69
|
+
function isGoldenChapterGateFailureStatus(value) {
|
|
70
|
+
return value === "fail" || value === "failed" || value === "violation";
|
|
71
|
+
}
|
|
72
|
+
export function detectGoldenChapterGateFailure(evalRaw) {
|
|
73
|
+
if (!isPlainObject(evalRaw))
|
|
74
|
+
return { has_golden_chapter_gate_failure: false, failed_checks: [] };
|
|
75
|
+
const evalObj = evalRaw;
|
|
76
|
+
const raw = evalObj.golden_chapter_gates;
|
|
77
|
+
if (!isPlainObject(raw))
|
|
78
|
+
return { has_golden_chapter_gate_failure: false, failed_checks: [] };
|
|
79
|
+
const gates = raw;
|
|
80
|
+
if (gates.activated === false)
|
|
81
|
+
return { has_golden_chapter_gate_failure: false, failed_checks: [] };
|
|
82
|
+
const failedChecks = [];
|
|
83
|
+
const seenIds = new Set();
|
|
84
|
+
const checksRaw = gates.checks;
|
|
85
|
+
if (Array.isArray(checksRaw)) {
|
|
86
|
+
for (const item of checksRaw) {
|
|
87
|
+
if (!isPlainObject(item))
|
|
88
|
+
continue;
|
|
89
|
+
const check = item;
|
|
90
|
+
if (!isGoldenChapterGateFailureStatus(check.status))
|
|
91
|
+
continue;
|
|
92
|
+
const id = typeof check.id === "string" && check.id.trim().length > 0 ? check.id.trim() : null;
|
|
93
|
+
if (id) {
|
|
94
|
+
if (seenIds.has(id))
|
|
95
|
+
continue;
|
|
96
|
+
seenIds.add(id);
|
|
97
|
+
failedChecks.push({ ...check, id });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
failedChecks.push(check);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const failedGateIdsRaw = gates.failed_gate_ids;
|
|
104
|
+
if (Array.isArray(failedGateIdsRaw)) {
|
|
105
|
+
for (const item of failedGateIdsRaw) {
|
|
106
|
+
if (typeof item !== "string" || item.trim().length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
const id = item.trim();
|
|
109
|
+
if (seenIds.has(id))
|
|
110
|
+
continue;
|
|
111
|
+
seenIds.add(id);
|
|
112
|
+
failedChecks.push({ id, status: "fail" });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const hasFailure = gates.passed === false || failedChecks.length > 0;
|
|
116
|
+
return { has_golden_chapter_gate_failure: hasFailure, failed_checks: failedChecks };
|
|
117
|
+
}
|
|
118
|
+
export function evaluateGateDecisionFromEval(args) {
|
|
119
|
+
if (!isPlainObject(args.evalRaw))
|
|
120
|
+
return { ok: false, reason: "eval_invalid" };
|
|
121
|
+
const evalObj = args.evalRaw;
|
|
122
|
+
const overall = typeof evalObj.overall_final === "number"
|
|
123
|
+
? evalObj.overall_final
|
|
124
|
+
: typeof evalObj.overall === "number"
|
|
125
|
+
? evalObj.overall
|
|
126
|
+
: null;
|
|
127
|
+
if (overall === null || !Number.isFinite(overall)) {
|
|
128
|
+
return { ok: false, reason: "eval_missing_overall" };
|
|
129
|
+
}
|
|
130
|
+
const maxRevisions = normalizeGateMaxRevisions(args.max_revisions);
|
|
131
|
+
const violation = detectHighConfidenceViolation(evalObj);
|
|
132
|
+
const goldenGateFailure = detectGoldenChapterGateFailure(evalObj);
|
|
133
|
+
const decision = computeGateDecision({
|
|
134
|
+
overall_final: overall,
|
|
135
|
+
revision_count: args.revision_count,
|
|
136
|
+
has_high_confidence_violation: violation.has_high_confidence_violation,
|
|
137
|
+
has_golden_chapter_gate_failure: goldenGateFailure.has_golden_chapter_gate_failure,
|
|
138
|
+
...(maxRevisions === null ? {} : { max_revisions: maxRevisions }),
|
|
139
|
+
...(args.force_pass ? { force_pass: true } : {})
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
gate: {
|
|
144
|
+
overall_final: overall,
|
|
145
|
+
decision,
|
|
146
|
+
revision_count: args.revision_count,
|
|
147
|
+
max_revisions: maxRevisions,
|
|
148
|
+
has_high_confidence_violation: violation.has_high_confidence_violation,
|
|
149
|
+
high_confidence_violations: violation.high_confidence_violations,
|
|
150
|
+
has_golden_chapter_gate_failure: goldenGateFailure.has_golden_chapter_gate_failure,
|
|
151
|
+
golden_chapter_gate_failures: goldenGateFailure.failed_checks,
|
|
152
|
+
recommendation: typeof evalObj.recommendation === "string" ? evalObj.recommendation : null
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { NovelCliError } from "./errors.js";
|
|
3
|
+
import { pathExists, readJsonFile } from "./fs-utils.js";
|
|
4
|
+
import { CANONICAL_PLATFORM_IDS, canonicalPlatformId } from "./platform-profile.js";
|
|
5
|
+
import { isPlainObject } from "./type-guards.js";
|
|
6
|
+
const VALID_THRESHOLD_OPERATORS = ["<", "<=", ">", ">=", "==", "!="];
|
|
7
|
+
function requireString(value, file, field) {
|
|
8
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
9
|
+
throw new NovelCliError(`Invalid ${file}: '${field}' must be a non-empty string.`, 2);
|
|
10
|
+
}
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
function requireOptionalStringArray(value, file, field) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim().length > 0)) {
|
|
17
|
+
throw new NovelCliError(`Invalid ${file}: '${field}' must be a string array.`, 2);
|
|
18
|
+
}
|
|
19
|
+
return value.map((item) => item.trim());
|
|
20
|
+
}
|
|
21
|
+
function parseRule(raw, file, field) {
|
|
22
|
+
if (!isPlainObject(raw))
|
|
23
|
+
throw new NovelCliError(`Invalid ${file}: '${field}' must be an object.`, 2);
|
|
24
|
+
const obj = raw;
|
|
25
|
+
const out = {
|
|
26
|
+
id: requireString(obj.id, file, `${field}.id`),
|
|
27
|
+
requirement: requireString(obj.requirement, file, `${field}.requirement`)
|
|
28
|
+
};
|
|
29
|
+
if (obj.evaluation_hint !== undefined) {
|
|
30
|
+
out.evaluation_hint = requireString(obj.evaluation_hint, file, `${field}.evaluation_hint`);
|
|
31
|
+
}
|
|
32
|
+
if (obj.threshold !== undefined) {
|
|
33
|
+
if (!isPlainObject(obj.threshold))
|
|
34
|
+
throw new NovelCliError(`Invalid ${file}: '${field}.threshold' must be an object.`, 2);
|
|
35
|
+
const threshold = obj.threshold;
|
|
36
|
+
const value = threshold.value;
|
|
37
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
38
|
+
throw new NovelCliError(`Invalid ${file}: '${field}.threshold.value' must be string|number|boolean.`, 2);
|
|
39
|
+
}
|
|
40
|
+
const operator = requireString(threshold.operator, file, `${field}.threshold.operator`);
|
|
41
|
+
if (!VALID_THRESHOLD_OPERATORS.includes(operator)) {
|
|
42
|
+
throw new NovelCliError(`Invalid ${file}: '${field}.threshold.operator' must be one of: ${VALID_THRESHOLD_OPERATORS.join(", ")}.`, 2);
|
|
43
|
+
}
|
|
44
|
+
out.threshold = {
|
|
45
|
+
metric: requireString(threshold.metric, file, `${field}.threshold.metric`),
|
|
46
|
+
operator,
|
|
47
|
+
value
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const allowed_values = requireOptionalStringArray(obj.allowed_values, file, `${field}.allowed_values`);
|
|
51
|
+
if (allowed_values)
|
|
52
|
+
out.allowed_values = allowed_values;
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function parseChapterConfig(raw, file, field) {
|
|
56
|
+
if (!isPlainObject(raw))
|
|
57
|
+
throw new NovelCliError(`Invalid ${file}: '${field}' must be an object.`, 2);
|
|
58
|
+
const obj = raw;
|
|
59
|
+
const gatesRaw = obj.gates;
|
|
60
|
+
if (!Array.isArray(gatesRaw) || gatesRaw.length === 0) {
|
|
61
|
+
throw new NovelCliError(`Invalid ${file}: '${field}.gates' must be a non-empty array.`, 2);
|
|
62
|
+
}
|
|
63
|
+
const gates = gatesRaw.map((item, index) => parseRule(item, file, `${field}.gates[${index}]`));
|
|
64
|
+
const notes = requireOptionalStringArray(obj.notes, file, `${field}.notes`);
|
|
65
|
+
return notes ? { gates, notes } : { gates };
|
|
66
|
+
}
|
|
67
|
+
export function parseGoldenChapterGates(raw, file) {
|
|
68
|
+
if (!isPlainObject(raw))
|
|
69
|
+
throw new NovelCliError(`Invalid ${file}: expected a JSON object.`, 2);
|
|
70
|
+
const obj = raw;
|
|
71
|
+
if (obj.schema_version !== 1)
|
|
72
|
+
throw new NovelCliError(`Invalid ${file}: 'schema_version' must be 1.`, 2);
|
|
73
|
+
if (!isPlainObject(obj.platforms))
|
|
74
|
+
throw new NovelCliError(`Invalid ${file}: 'platforms' must be an object.`, 2);
|
|
75
|
+
const platformsRaw = obj.platforms;
|
|
76
|
+
const platforms = {};
|
|
77
|
+
for (const platformId of CANONICAL_PLATFORM_IDS) {
|
|
78
|
+
const platformRaw = platformsRaw[platformId];
|
|
79
|
+
if (!isPlainObject(platformRaw)) {
|
|
80
|
+
throw new NovelCliError(`Invalid ${file}: missing 'platforms.${platformId}' object.`, 2);
|
|
81
|
+
}
|
|
82
|
+
const platformObj = platformRaw;
|
|
83
|
+
if (!isPlainObject(platformObj.chapters)) {
|
|
84
|
+
throw new NovelCliError(`Invalid ${file}: 'platforms.${platformId}.chapters' must be an object.`, 2);
|
|
85
|
+
}
|
|
86
|
+
const chaptersRaw = platformObj.chapters;
|
|
87
|
+
const chapters = {};
|
|
88
|
+
for (const chapter of ["1", "2", "3"]) {
|
|
89
|
+
chapters[chapter] = parseChapterConfig(chaptersRaw[chapter], file, `platforms.${platformId}.chapters.${chapter}`);
|
|
90
|
+
}
|
|
91
|
+
platforms[platformId] = { chapters };
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(obj.invalid_combinations)) {
|
|
94
|
+
throw new NovelCliError(`Invalid ${file}: 'invalid_combinations' must be an array.`, 2);
|
|
95
|
+
}
|
|
96
|
+
const invalid_combinations = obj.invalid_combinations.map((item, index) => {
|
|
97
|
+
if (!isPlainObject(item)) {
|
|
98
|
+
throw new NovelCliError(`Invalid ${file}: 'invalid_combinations[${index}]' must be an object.`, 2);
|
|
99
|
+
}
|
|
100
|
+
const entry = item;
|
|
101
|
+
const platform = requireString(entry.platform, file, `invalid_combinations[${index}].platform`);
|
|
102
|
+
if (!CANONICAL_PLATFORM_IDS.includes(platform)) {
|
|
103
|
+
throw new NovelCliError(`Invalid ${file}: 'invalid_combinations[${index}].platform' must be one of ${CANONICAL_PLATFORM_IDS.join(", ")}.`, 2);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
genre: requireString(entry.genre, file, `invalid_combinations[${index}].genre`),
|
|
107
|
+
platform: platform,
|
|
108
|
+
warning: requireString(entry.warning, file, `invalid_combinations[${index}].warning`)
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const out = { schema_version: 1, invalid_combinations, platforms };
|
|
112
|
+
if (typeof obj.description === "string" && obj.description.trim().length > 0)
|
|
113
|
+
out.description = obj.description.trim();
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
export async function loadGoldenChapterGates(rootDir) {
|
|
117
|
+
const relPath = "golden-chapter-gates.json";
|
|
118
|
+
const absPath = join(rootDir, relPath);
|
|
119
|
+
if (!(await pathExists(absPath)))
|
|
120
|
+
return null;
|
|
121
|
+
const raw = await readJsonFile(absPath);
|
|
122
|
+
return { relPath, config: parseGoldenChapterGates(raw, relPath) };
|
|
123
|
+
}
|
|
124
|
+
export function selectGoldenChapterGatesForPlatform(args) {
|
|
125
|
+
if (!Number.isInteger(args.chapter) || args.chapter < 1 || args.chapter > 3)
|
|
126
|
+
return null;
|
|
127
|
+
const platform = canonicalPlatformId(args.platformId);
|
|
128
|
+
const platformConfig = args.config.platforms[platform];
|
|
129
|
+
if (!platformConfig)
|
|
130
|
+
return null;
|
|
131
|
+
const current_chapter = platformConfig.chapters[String(args.chapter)];
|
|
132
|
+
if (!current_chapter)
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
platform,
|
|
136
|
+
chapter: args.chapter,
|
|
137
|
+
current_chapter,
|
|
138
|
+
chapters: platformConfig.chapters,
|
|
139
|
+
invalid_combination_warnings: args.config.invalid_combinations
|
|
140
|
+
.filter((item) => item.platform === platform)
|
|
141
|
+
.map((item) => ({ genre: item.genre, warning: item.warning }))
|
|
142
|
+
};
|
|
143
|
+
}
|
package/dist/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { canonicalPlatformId } from "./platform-profile.js";
|
|
4
5
|
import { createDefaultCheckpoint } from "./checkpoint.js";
|
|
5
6
|
import { NovelCliError } from "./errors.js";
|
|
6
7
|
import { ensureDir, pathExists, readJsonFile, readTextFile, writeJsonFile, writeTextFile } from "./fs-utils.js";
|
|
@@ -14,9 +15,9 @@ export function resolveInitRootDir(args) {
|
|
|
14
15
|
return resolve(cwdAbs, args.projectOverride);
|
|
15
16
|
}
|
|
16
17
|
export function normalizePlatformId(value) {
|
|
17
|
-
if (value === "qidian" || value === "tomato")
|
|
18
|
+
if (value === "qidian" || value === "tomato" || value === "fanqie" || value === "jinjiang")
|
|
18
19
|
return value;
|
|
19
|
-
throw new NovelCliError(`Invalid --platform: ${String(value)} (expected qidian|tomato).`, 2);
|
|
20
|
+
throw new NovelCliError(`Invalid --platform: ${String(value)} (expected qidian|tomato|fanqie|jinjiang).`, 2);
|
|
20
21
|
}
|
|
21
22
|
function moduleRootDir() {
|
|
22
23
|
// src/init.ts → <repo_root>; dist/init.js → <package_root>
|
|
@@ -40,7 +41,21 @@ async function ensureRootIsDirectory(absPath) {
|
|
|
40
41
|
}
|
|
41
42
|
async function writeIfMissingOrForce(args) {
|
|
42
43
|
const abs = join(args.rootDir, args.relPath);
|
|
43
|
-
|
|
44
|
+
let exists = false;
|
|
45
|
+
try {
|
|
46
|
+
const current = await stat(abs);
|
|
47
|
+
if (!current.isFile()) {
|
|
48
|
+
throw new NovelCliError(`Cannot initialize ${args.relPath}: existing path is not a file. Remove it or choose a different project root.`, 2);
|
|
49
|
+
}
|
|
50
|
+
exists = true;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err instanceof NovelCliError)
|
|
54
|
+
throw err;
|
|
55
|
+
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : null;
|
|
56
|
+
if (code !== "ENOENT")
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
44
59
|
if (exists && !args.force) {
|
|
45
60
|
args.result.skipped.push(args.relPath);
|
|
46
61
|
return;
|
|
@@ -93,9 +108,32 @@ async function loadPlatformProfileTemplate(platform) {
|
|
|
93
108
|
}
|
|
94
109
|
return selected;
|
|
95
110
|
}
|
|
111
|
+
async function tryLoadPlatformWritingGuide(platform) {
|
|
112
|
+
const canonicalPlatform = canonicalPlatformId(platform);
|
|
113
|
+
const relPath = `platforms/${canonicalPlatform}.md`;
|
|
114
|
+
const absPath = join(TEMPLATE_DIR, relPath);
|
|
115
|
+
if (!(await pathExists(absPath))) {
|
|
116
|
+
return {
|
|
117
|
+
text: null,
|
|
118
|
+
warning: `Missing optional platform writing guide template: templates/${relPath}. Init continued without platform-writing-guide.md.`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return { text: await readTextFile(absPath), warning: null };
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
126
|
+
return {
|
|
127
|
+
text: null,
|
|
128
|
+
warning: `Failed to read optional platform writing guide template: templates/${relPath}. ${message}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
96
132
|
const DEFAULT_TEMPLATES = [
|
|
97
133
|
{ relPath: "brief.md", templateName: "brief-template.md", kind: "text" },
|
|
98
134
|
{ relPath: "style-profile.json", templateName: "style-profile-template.json", kind: "json" },
|
|
135
|
+
{ relPath: "genre-excitement-map.json", templateName: "genre-excitement-map.json", kind: "json" },
|
|
136
|
+
{ relPath: "genre-golden-standards.json", templateName: "genre-golden-standards.json", kind: "json" },
|
|
99
137
|
{ relPath: "ai-blacklist.json", templateName: "ai-blacklist.json", kind: "json" },
|
|
100
138
|
{ relPath: "web-novel-cliche-lint.json", templateName: "web-novel-cliche-lint.json", kind: "json" }
|
|
101
139
|
];
|
|
@@ -120,7 +158,8 @@ export async function initProject(args) {
|
|
|
120
158
|
ensuredDirs: [],
|
|
121
159
|
created: [],
|
|
122
160
|
overwritten: [],
|
|
123
|
-
skipped: []
|
|
161
|
+
skipped: [],
|
|
162
|
+
warnings: []
|
|
124
163
|
};
|
|
125
164
|
await ensureRootIsDirectory(args.rootDir);
|
|
126
165
|
for (const relDir of STAGING_SUBDIRS) {
|
|
@@ -138,9 +177,19 @@ export async function initProject(args) {
|
|
|
138
177
|
});
|
|
139
178
|
if (!minimal) {
|
|
140
179
|
for (const tmpl of DEFAULT_TEMPLATES) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
180
|
+
let contents;
|
|
181
|
+
if (tmpl.kind === "text") {
|
|
182
|
+
contents = { kind: "text", text: await loadTemplateText(tmpl.templateName) };
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const json = await loadTemplateJson(tmpl.templateName);
|
|
186
|
+
if (tmpl.relPath === "style-profile.json" && platform) {
|
|
187
|
+
contents = { kind: "json", json: { ...json, platform } };
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
contents = { kind: "json", json };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
144
193
|
await writeIfMissingOrForce({ rootDir: args.rootDir, relPath: tmpl.relPath, contents, force, result });
|
|
145
194
|
}
|
|
146
195
|
}
|
|
@@ -161,6 +210,26 @@ export async function initProject(args) {
|
|
|
161
210
|
force,
|
|
162
211
|
result
|
|
163
212
|
});
|
|
213
|
+
await writeIfMissingOrForce({
|
|
214
|
+
rootDir: args.rootDir,
|
|
215
|
+
relPath: "golden-chapter-gates.json",
|
|
216
|
+
contents: { kind: "json", json: await loadTemplateJson("golden-chapter-gates.json") },
|
|
217
|
+
force,
|
|
218
|
+
result
|
|
219
|
+
});
|
|
220
|
+
const guide = await tryLoadPlatformWritingGuide(platform);
|
|
221
|
+
if (guide.text !== null) {
|
|
222
|
+
await writeIfMissingOrForce({
|
|
223
|
+
rootDir: args.rootDir,
|
|
224
|
+
relPath: "platform-writing-guide.md",
|
|
225
|
+
contents: { kind: "text", text: guide.text },
|
|
226
|
+
force,
|
|
227
|
+
result
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else if (guide.warning) {
|
|
231
|
+
result.warnings.push(guide.warning);
|
|
232
|
+
}
|
|
164
233
|
}
|
|
165
234
|
return result;
|
|
166
235
|
}
|