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/instructions.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
3
|
import { NovelCliError } from "./errors.js";
|
|
4
4
|
import { ensureDir, pathExists, readJsonFile, readTextFile, writeJsonFile, writeTextFileIfMissing } from "./fs-utils.js";
|
|
5
5
|
import { loadContinuityLatestSummary, tryResolveVolumeChapterRange } from "./consistency-auditor.js";
|
|
6
|
+
import { normalizeExcitementType } from "./excitement-type.js";
|
|
6
7
|
import { loadEngagementLatestSummary } from "./engagement.js";
|
|
7
8
|
import { computeForeshadowVisibilityReport, loadForeshadowGlobalItems } from "./foreshadow-visibility.js";
|
|
8
|
-
import {
|
|
9
|
+
import { loadGoldenChapterGates, selectGoldenChapterGatesForPlatform } from "./golden-chapter-gates.js";
|
|
10
|
+
import { computeEffectiveScoringWeights, isKnownScoringDimension, loadGenreWeightProfiles } from "./scoring-weights.js";
|
|
9
11
|
import { parseNovelAskQuestionSpec } from "./novel-ask.js";
|
|
10
12
|
import { loadPlatformProfile } from "./platform-profile.js";
|
|
13
|
+
import { loadAntiAiGenreOverrides, loadAntiAiJudgeContext, loadAntiAiStatisticalTargets } from "./anti-ai-context.js";
|
|
11
14
|
import { computePrejudgeGuardrailsReport, writePrejudgeGuardrailsReport } from "./prejudge-guardrails.js";
|
|
12
15
|
import { loadPromiseLedgerLatestSummary } from "./promise-ledger.js";
|
|
13
16
|
import { QUICKSTART_STAGING_RELS } from "./quickstart.js";
|
|
@@ -16,7 +19,7 @@ import { computeTitlePolicyReport } from "./title-policy.js";
|
|
|
16
19
|
import { chapterRelPaths, formatStepId, pad2, pad3, titleFixSnapshotRel } from "./steps.js";
|
|
17
20
|
import { isPlainObject } from "./type-guards.js";
|
|
18
21
|
import { VOL_REVIEW_RELS } from "./volume-review.js";
|
|
19
|
-
import {
|
|
22
|
+
import { QUICKSTART_MINI_PLANNING_RANGE, resolveVolumeChapterRange, volumeFinalRelPaths, volumeStagingRelPaths } from "./volume-planning.js";
|
|
20
23
|
function relIfExists(relPath, exists) {
|
|
21
24
|
return exists ? relPath : null;
|
|
22
25
|
}
|
|
@@ -27,6 +30,391 @@ function safeEmbedMode(mode) {
|
|
|
27
30
|
return "brief";
|
|
28
31
|
throw new NovelCliError(`Unsupported --embed mode: ${mode}. Supported: brief`, 2);
|
|
29
32
|
}
|
|
33
|
+
async function loadOutlineExcitementType(args) {
|
|
34
|
+
const outlineRel = `volumes/vol-${pad2(args.volume)}/outline.md`;
|
|
35
|
+
const outlineAbs = join(args.rootDir, outlineRel);
|
|
36
|
+
if (!(await pathExists(outlineAbs)))
|
|
37
|
+
return null;
|
|
38
|
+
const lines = (await readTextFile(outlineAbs)).split(/\r?\n/u);
|
|
39
|
+
const headingRe = /^###\s*第\s*(\d+)\s*章/u;
|
|
40
|
+
const excitementPrefix = "- **ExcitementType**:";
|
|
41
|
+
let startLine = -1;
|
|
42
|
+
let endLine = lines.length;
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const match = headingRe.exec(lines[i] ?? "");
|
|
45
|
+
if (!match)
|
|
46
|
+
continue;
|
|
47
|
+
const chapter = Number.parseInt(match[1] ?? "", 10);
|
|
48
|
+
if (chapter !== args.chapter) {
|
|
49
|
+
if (startLine >= 0) {
|
|
50
|
+
endLine = i;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
startLine = i;
|
|
56
|
+
}
|
|
57
|
+
if (startLine < 0)
|
|
58
|
+
return null;
|
|
59
|
+
for (const line of lines.slice(startLine, endLine)) {
|
|
60
|
+
if (line.startsWith(excitementPrefix)) {
|
|
61
|
+
return normalizeExcitementType(line.slice(excitementPrefix.length));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
async function loadChapterExcitementType(args) {
|
|
67
|
+
const contractRel = `volumes/vol-${pad2(args.volume)}/chapter-contracts/chapter-${pad3(args.chapter)}.json`;
|
|
68
|
+
const contractAbs = join(args.rootDir, contractRel);
|
|
69
|
+
if (await pathExists(contractAbs)) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await readJsonFile(contractAbs);
|
|
72
|
+
if (isPlainObject(raw) && Object.prototype.hasOwnProperty.call(raw, "excitement_type")) {
|
|
73
|
+
const normalized = normalizeExcitementType(raw.excitement_type);
|
|
74
|
+
return normalized === undefined ? null : normalized;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Fall through to outline-based backward-compatible parsing.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const outlineExcitementType = await loadOutlineExcitementType(args);
|
|
82
|
+
return outlineExcitementType === undefined ? null : outlineExcitementType;
|
|
83
|
+
}
|
|
84
|
+
const GENRE_ALIASES = {
|
|
85
|
+
xuanhuan: "xuanhuan",
|
|
86
|
+
"玄幻": "xuanhuan",
|
|
87
|
+
dushi: "dushi",
|
|
88
|
+
"都市": "dushi",
|
|
89
|
+
scifi: "scifi",
|
|
90
|
+
sci_fi: "scifi",
|
|
91
|
+
"sci-fi": "scifi",
|
|
92
|
+
"科幻": "scifi",
|
|
93
|
+
history: "history",
|
|
94
|
+
"历史": "history",
|
|
95
|
+
suspense: "suspense",
|
|
96
|
+
mystery: "suspense",
|
|
97
|
+
"悬疑": "suspense",
|
|
98
|
+
romance: "romance",
|
|
99
|
+
"言情": "romance"
|
|
100
|
+
};
|
|
101
|
+
function normalizeProjectGenre(raw) {
|
|
102
|
+
if (typeof raw !== "string")
|
|
103
|
+
return null;
|
|
104
|
+
const trimmed = raw.trim();
|
|
105
|
+
if (trimmed.length === 0)
|
|
106
|
+
return null;
|
|
107
|
+
const withoutParens = trimmed.replace(/[((].*$/u, "").trim();
|
|
108
|
+
if (withoutParens.length === 0)
|
|
109
|
+
return null;
|
|
110
|
+
const compact = withoutParens.replace(/\s+/gu, "");
|
|
111
|
+
return GENRE_ALIASES[withoutParens] ?? GENRE_ALIASES[compact] ?? GENRE_ALIASES[compact.toLowerCase()] ?? null;
|
|
112
|
+
}
|
|
113
|
+
function toNonEmptyStringArray(raw) {
|
|
114
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
115
|
+
return null;
|
|
116
|
+
const out = raw.map((item) => (typeof item === "string" ? item.trim() : "")).filter((item) => item.length > 0);
|
|
117
|
+
return out.length === raw.length ? out : null;
|
|
118
|
+
}
|
|
119
|
+
function toKnownDimensionArray(raw) {
|
|
120
|
+
const dimensions = toNonEmptyStringArray(raw);
|
|
121
|
+
if (!dimensions)
|
|
122
|
+
return null;
|
|
123
|
+
return dimensions.every((dimension) => isKnownScoringDimension(dimension)) ? dimensions : null;
|
|
124
|
+
}
|
|
125
|
+
function toNumericThresholds(raw) {
|
|
126
|
+
if (!isPlainObject(raw))
|
|
127
|
+
return null;
|
|
128
|
+
const entries = Object.entries(raw).map(([key, value]) => [key.trim(), value]);
|
|
129
|
+
if (entries.length === 0
|
|
130
|
+
|| entries.length !== Object.keys(raw).length
|
|
131
|
+
|| entries.some(([key, value]) => key.length === 0 || !isKnownScoringDimension(key) || typeof value !== "number" || !Number.isFinite(value))) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return Object.fromEntries(entries);
|
|
135
|
+
}
|
|
136
|
+
async function loadProjectGenre(rootDir) {
|
|
137
|
+
const briefAbs = join(rootDir, "brief.md");
|
|
138
|
+
if (!(await pathExists(briefAbs)))
|
|
139
|
+
return null;
|
|
140
|
+
try {
|
|
141
|
+
const lines = (await readTextFile(briefAbs)).split(/\r?\n/u);
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const match = /^\s*-\s*\*\*(?:题材|Genre)\*\*[::]\s*(.+?)\s*$/u.exec(line);
|
|
144
|
+
if (!match)
|
|
145
|
+
continue;
|
|
146
|
+
const normalized = normalizeProjectGenre(match[1] ?? "");
|
|
147
|
+
if (normalized)
|
|
148
|
+
return normalized;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
async function loadSelectedGenreExcitementMap(rootDir) {
|
|
157
|
+
const genre = await loadProjectGenre(rootDir);
|
|
158
|
+
if (!genre)
|
|
159
|
+
return null;
|
|
160
|
+
const relPath = "genre-excitement-map.json";
|
|
161
|
+
const absPath = join(rootDir, relPath);
|
|
162
|
+
if (!(await pathExists(absPath)))
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
const raw = await readJsonFile(absPath);
|
|
166
|
+
if (!isPlainObject(raw) || raw.schema_version !== 1 || !isPlainObject(raw.genres))
|
|
167
|
+
return null;
|
|
168
|
+
const entry = raw.genres[genre];
|
|
169
|
+
if (!isPlainObject(entry) || !isPlainObject(entry.chapters))
|
|
170
|
+
return null;
|
|
171
|
+
const chapter1 = normalizeExcitementType(entry.chapters["1"]);
|
|
172
|
+
const chapter2 = normalizeExcitementType(entry.chapters["2"]);
|
|
173
|
+
const chapter3 = normalizeExcitementType(entry.chapters["3"]);
|
|
174
|
+
if (!chapter1 || !chapter2 || !chapter3)
|
|
175
|
+
return null;
|
|
176
|
+
return {
|
|
177
|
+
genre,
|
|
178
|
+
chapters: { "1": chapter1, "2": chapter2, "3": chapter3 },
|
|
179
|
+
source: relPath
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function loadSelectedGenreGoldenStandards(rootDir) {
|
|
187
|
+
const genre = await loadProjectGenre(rootDir);
|
|
188
|
+
if (!genre)
|
|
189
|
+
return null;
|
|
190
|
+
const relPath = "genre-golden-standards.json";
|
|
191
|
+
const absPath = join(rootDir, relPath);
|
|
192
|
+
if (!(await pathExists(absPath)))
|
|
193
|
+
return null;
|
|
194
|
+
try {
|
|
195
|
+
const raw = await readJsonFile(absPath);
|
|
196
|
+
if (!isPlainObject(raw) || raw.schema_version !== 1 || !isPlainObject(raw.genres))
|
|
197
|
+
return null;
|
|
198
|
+
const entry = raw.genres[genre];
|
|
199
|
+
if (!isPlainObject(entry))
|
|
200
|
+
return null;
|
|
201
|
+
const focusDimensions = toKnownDimensionArray(entry.focus_dimensions);
|
|
202
|
+
const criteria = toNonEmptyStringArray(entry.criteria);
|
|
203
|
+
const minimumThresholds = toNumericThresholds(entry.minimum_thresholds);
|
|
204
|
+
if (!focusDimensions || !criteria || !minimumThresholds)
|
|
205
|
+
return null;
|
|
206
|
+
return {
|
|
207
|
+
genre,
|
|
208
|
+
focus_dimensions: focusDimensions,
|
|
209
|
+
criteria,
|
|
210
|
+
minimum_thresholds: minimumThresholds,
|
|
211
|
+
source: relPath
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function normalizeCanonStatus(raw, sourceLabel) {
|
|
219
|
+
if (raw == null)
|
|
220
|
+
return "established";
|
|
221
|
+
if (typeof raw !== "string") {
|
|
222
|
+
console.warn(`[canon_status] Invalid non-string canon_status${sourceLabel ? ` in ${sourceLabel}` : ""}; defaulting to "established".`);
|
|
223
|
+
return "established";
|
|
224
|
+
}
|
|
225
|
+
const normalized = raw.trim().toLowerCase();
|
|
226
|
+
if (normalized === "")
|
|
227
|
+
return "established";
|
|
228
|
+
if (normalized === "planned" || normalized === "deprecated" || normalized === "established")
|
|
229
|
+
return normalized;
|
|
230
|
+
console.warn(`[canon_status] Invalid canon_status "${raw}"${sourceLabel ? ` in ${sourceLabel}` : ""}; defaulting to "established".`);
|
|
231
|
+
return "established";
|
|
232
|
+
}
|
|
233
|
+
function asNonEmptyString(raw) {
|
|
234
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null;
|
|
235
|
+
}
|
|
236
|
+
function buildPlannedRuleInfo(args) {
|
|
237
|
+
const plannedRuleInfo = { canon_status: "planned", rule: args.rule };
|
|
238
|
+
if (args.id)
|
|
239
|
+
plannedRuleInfo.id = args.id;
|
|
240
|
+
if (args.category)
|
|
241
|
+
plannedRuleInfo.category = args.category;
|
|
242
|
+
if (args.constraintType)
|
|
243
|
+
plannedRuleInfo.constraint_type = args.constraintType;
|
|
244
|
+
return plannedRuleInfo;
|
|
245
|
+
}
|
|
246
|
+
async function loadRuleLifecycleContext(rootDir) {
|
|
247
|
+
const empty = { hardRulesList: [], plannedRulesInfo: [], degraded: false };
|
|
248
|
+
const rulesAbs = join(rootDir, "world/rules.json");
|
|
249
|
+
if (!(await pathExists(rulesAbs)))
|
|
250
|
+
return empty;
|
|
251
|
+
try {
|
|
252
|
+
const raw = await readJsonFile(rulesAbs);
|
|
253
|
+
if (!isPlainObject(raw))
|
|
254
|
+
return { ...empty, degraded: true };
|
|
255
|
+
const rules = raw.rules;
|
|
256
|
+
if (!Array.isArray(rules))
|
|
257
|
+
return { ...empty, degraded: true };
|
|
258
|
+
const hardRulesList = [];
|
|
259
|
+
const plannedRulesInfo = [];
|
|
260
|
+
for (const item of rules) {
|
|
261
|
+
if (!isPlainObject(item))
|
|
262
|
+
continue;
|
|
263
|
+
const rule = asNonEmptyString(item.rule);
|
|
264
|
+
if (!rule)
|
|
265
|
+
continue;
|
|
266
|
+
const status = normalizeCanonStatus(item.canon_status, `${relative(rootDir, rulesAbs)}${typeof item.id === "string" ? `#${item.id}` : ""}`);
|
|
267
|
+
const id = asNonEmptyString(item.id);
|
|
268
|
+
const category = asNonEmptyString(item.category);
|
|
269
|
+
const constraintType = asNonEmptyString(item.constraint_type);
|
|
270
|
+
if (status === "planned") {
|
|
271
|
+
plannedRulesInfo.push(buildPlannedRuleInfo({ id, category, constraintType, rule }));
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (status === "deprecated")
|
|
275
|
+
continue;
|
|
276
|
+
if (constraintType !== "hard")
|
|
277
|
+
continue;
|
|
278
|
+
hardRulesList.push(id ? `${id}: ${rule}` : rule);
|
|
279
|
+
}
|
|
280
|
+
return { hardRulesList, plannedRulesInfo, degraded: false };
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return { ...empty, degraded: true };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function selectFallbackCharacterCandidates(candidates, options) {
|
|
287
|
+
const nonDeprecatedCandidates = candidates.filter((candidate) => candidate.canonStatus !== "deprecated");
|
|
288
|
+
if (!options.includePlannedCharacters) {
|
|
289
|
+
return nonDeprecatedCandidates.filter((candidate) => candidate.canonStatus !== "planned").slice(0, 15);
|
|
290
|
+
}
|
|
291
|
+
if (!options.prioritizePlannedOnFallback)
|
|
292
|
+
return nonDeprecatedCandidates.slice(0, 15);
|
|
293
|
+
const plannedCandidates = nonDeprecatedCandidates.filter((candidate) => candidate.canonStatus === "planned");
|
|
294
|
+
const activeCandidates = nonDeprecatedCandidates.filter((candidate) => candidate.canonStatus !== "planned");
|
|
295
|
+
// Fallback uses a shared 15-slot budget across active + planned candidates.
|
|
296
|
+
// Planned entries go first so future-facing foreshadowing survives truncation;
|
|
297
|
+
// explicit chapter-contract matches bypass this fallback path entirely.
|
|
298
|
+
return [...plannedCandidates, ...activeCandidates].slice(0, 15);
|
|
299
|
+
}
|
|
300
|
+
async function loadExistingCharacterProfiles(rootDir, candidates) {
|
|
301
|
+
const pathsOrNull = await Promise.all(candidates.map(async (candidate) => ((await pathExists(join(rootDir, candidate.mdRel))) ? candidate.mdRel : null)));
|
|
302
|
+
return pathsOrNull.filter((path) => path !== null);
|
|
303
|
+
}
|
|
304
|
+
async function loadCharacterContext(args) {
|
|
305
|
+
const empty = {
|
|
306
|
+
activeCharacterContracts: [],
|
|
307
|
+
activeCharacterProfiles: [],
|
|
308
|
+
plannedCharacterContracts: [],
|
|
309
|
+
plannedCharacterProfiles: []
|
|
310
|
+
};
|
|
311
|
+
const charsDirRel = "characters/active";
|
|
312
|
+
const charsDirAbs = join(args.rootDir, charsDirRel);
|
|
313
|
+
if (!(await pathExists(charsDirAbs)))
|
|
314
|
+
return empty;
|
|
315
|
+
const desiredRefs = new Set();
|
|
316
|
+
const contractAbs = join(args.rootDir, args.chapterContractRel);
|
|
317
|
+
if (await pathExists(contractAbs)) {
|
|
318
|
+
try {
|
|
319
|
+
const raw = await readJsonFile(contractAbs);
|
|
320
|
+
if (isPlainObject(raw)) {
|
|
321
|
+
const preconditions = isPlainObject(raw.preconditions) ? raw.preconditions : null;
|
|
322
|
+
const characterStates = preconditions && isPlainObject(preconditions.character_states) ? preconditions.character_states : null;
|
|
323
|
+
if (characterStates) {
|
|
324
|
+
for (const key of Object.keys(characterStates)) {
|
|
325
|
+
const normalized = key.trim().toLowerCase();
|
|
326
|
+
if (normalized.length > 0)
|
|
327
|
+
desiredRefs.add(normalized);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Ignore malformed chapter contracts here; validateStep remains the source of truth.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const entries = await readdir(charsDirAbs, { withFileTypes: true });
|
|
338
|
+
const candidates = [];
|
|
339
|
+
const hasDesiredRefs = desiredRefs.size > 0;
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
342
|
+
continue;
|
|
343
|
+
const jsonRel = `${charsDirRel}/${entry.name}`;
|
|
344
|
+
try {
|
|
345
|
+
const raw = await readJsonFile(join(args.rootDir, jsonRel));
|
|
346
|
+
if (!isPlainObject(raw))
|
|
347
|
+
continue;
|
|
348
|
+
const id = asNonEmptyString(raw.id) ?? entry.name.replace(/\.json$/u, "");
|
|
349
|
+
const displayName = asNonEmptyString(raw.display_name) ?? id;
|
|
350
|
+
const canonStatus = normalizeCanonStatus(raw.canon_status, jsonRel);
|
|
351
|
+
const matched = hasDesiredRefs && (desiredRefs.has(id.toLowerCase()) || desiredRefs.has(displayName.toLowerCase()));
|
|
352
|
+
candidates.push({
|
|
353
|
+
id,
|
|
354
|
+
displayName,
|
|
355
|
+
canonStatus,
|
|
356
|
+
jsonRel,
|
|
357
|
+
mdRel: `${charsDirRel}/${entry.name.replace(/\.json$/u, '.md')}`,
|
|
358
|
+
matched
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
candidates.sort((left, right) => left.jsonRel.localeCompare(right.jsonRel));
|
|
366
|
+
const preferred = hasDesiredRefs
|
|
367
|
+
? candidates.filter((candidate) => candidate.matched &&
|
|
368
|
+
candidate.canonStatus !== "deprecated" &&
|
|
369
|
+
(args.options.includePlannedCharacters || candidate.canonStatus !== "planned"))
|
|
370
|
+
: [];
|
|
371
|
+
const selectedCandidates = preferred.length > 0 ? preferred : selectFallbackCharacterCandidates(candidates, args.options);
|
|
372
|
+
const activeCandidates = selectedCandidates.filter((candidate) => candidate.canonStatus !== "planned");
|
|
373
|
+
const plannedCandidates = args.options.includePlannedCharacters
|
|
374
|
+
? selectedCandidates.filter((candidate) => candidate.canonStatus === "planned")
|
|
375
|
+
: [];
|
|
376
|
+
const [activeCharacterProfiles, plannedCharacterProfiles] = await Promise.all([
|
|
377
|
+
loadExistingCharacterProfiles(args.rootDir, activeCandidates),
|
|
378
|
+
loadExistingCharacterProfiles(args.rootDir, plannedCandidates)
|
|
379
|
+
]);
|
|
380
|
+
return {
|
|
381
|
+
activeCharacterContracts: activeCandidates.map((candidate) => candidate.jsonRel),
|
|
382
|
+
activeCharacterProfiles,
|
|
383
|
+
plannedCharacterContracts: plannedCandidates.map((candidate) => candidate.jsonRel),
|
|
384
|
+
plannedCharacterProfiles
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return empty;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function attachCanonStatusContext(args) {
|
|
392
|
+
const ruleLifecycle = await loadRuleLifecycleContext(args.rootDir);
|
|
393
|
+
args.inline.hard_rules_list = ruleLifecycle.hardRulesList;
|
|
394
|
+
if (ruleLifecycle.degraded)
|
|
395
|
+
args.inline.world_rules_context_degraded = true;
|
|
396
|
+
if (args.options.includePlannedRulesInfo && ruleLifecycle.plannedRulesInfo.length > 0) {
|
|
397
|
+
args.inline.planned_rules_info = ruleLifecycle.plannedRulesInfo;
|
|
398
|
+
}
|
|
399
|
+
const characterContext = await loadCharacterContext({
|
|
400
|
+
rootDir: args.rootDir,
|
|
401
|
+
chapterContractRel: args.chapterContractRel,
|
|
402
|
+
options: {
|
|
403
|
+
includePlannedCharacters: args.options.includePlannedCharacters,
|
|
404
|
+
prioritizePlannedOnFallback: args.options.prioritizePlannedCharactersOnFallback
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
if (characterContext.activeCharacterContracts.length > 0)
|
|
408
|
+
args.paths.character_contracts = characterContext.activeCharacterContracts;
|
|
409
|
+
if (characterContext.activeCharacterProfiles.length > 0)
|
|
410
|
+
args.paths.character_profiles = characterContext.activeCharacterProfiles;
|
|
411
|
+
if (args.options.includePlannedCharacters && characterContext.plannedCharacterContracts.length > 0) {
|
|
412
|
+
args.paths.planned_character_contracts = characterContext.plannedCharacterContracts;
|
|
413
|
+
}
|
|
414
|
+
if (args.options.includePlannedCharacters && characterContext.plannedCharacterProfiles.length > 0) {
|
|
415
|
+
args.paths.planned_character_profiles = characterContext.plannedCharacterProfiles;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
30
418
|
async function buildReviewInstructionPacket(args) {
|
|
31
419
|
const stepId = formatStepId(args.step);
|
|
32
420
|
if (args.step.kind !== "review")
|
|
@@ -206,6 +594,8 @@ async function buildQuickStartInstructionPacket(args) {
|
|
|
206
594
|
}
|
|
207
595
|
}
|
|
208
596
|
const trialChapter = Math.max(1, args.checkpoint.last_completed_chapter + 1);
|
|
597
|
+
const miniPlanningStaging = volumeStagingRelPaths(1);
|
|
598
|
+
const miniPlanningFinal = volumeFinalRelPaths(1);
|
|
209
599
|
const isTrialMode = step.phase === "trial" || step.phase === "results";
|
|
210
600
|
const inline = {
|
|
211
601
|
quickstart_phase: step.phase,
|
|
@@ -221,19 +611,33 @@ async function buildQuickStartInstructionPacket(args) {
|
|
|
221
611
|
};
|
|
222
612
|
await maybeAddPath("project_brief", "brief.md");
|
|
223
613
|
await maybeAddPath("platform_profile", "platform-profile.json");
|
|
614
|
+
await maybeAddPath("platform_writing_guide", "platform-writing-guide.md");
|
|
615
|
+
await maybeAddPath("style_guide", "skills/novel-writing/references/style-guide.md");
|
|
224
616
|
await maybeAddPath("style_profile_template", "style-profile.json");
|
|
617
|
+
await maybeAddPath("storylines", "storylines/storylines.json");
|
|
225
618
|
// Attach staging quickstart artifacts when present (for resume/debug).
|
|
226
619
|
await maybeAddPath("quickstart_rules", QUICKSTART_STAGING_RELS.rulesJson);
|
|
227
620
|
await maybeAddPath("quickstart_contracts_dir", QUICKSTART_STAGING_RELS.contractsDir);
|
|
228
621
|
await maybeAddPath("quickstart_style_profile", QUICKSTART_STAGING_RELS.styleProfileJson);
|
|
229
622
|
await maybeAddPath("quickstart_trial_chapter", QUICKSTART_STAGING_RELS.trialChapterMd);
|
|
230
623
|
await maybeAddPath("quickstart_evaluation", QUICKSTART_STAGING_RELS.evaluationJson);
|
|
624
|
+
// Attach committed mini-planning artifacts when present.
|
|
625
|
+
await maybeAddPath("mini_volume_outline", miniPlanningFinal.outlineMd);
|
|
626
|
+
await maybeAddPath("mini_storyline_schedule", miniPlanningFinal.storylineScheduleJson);
|
|
627
|
+
await maybeAddPath("mini_volume_foreshadowing", miniPlanningFinal.foreshadowingJson);
|
|
628
|
+
if (trialChapter <= QUICKSTART_MINI_PLANNING_RANGE.end) {
|
|
629
|
+
await maybeAddPath("mini_chapter_contract", miniPlanningFinal.chapterContractJson(trialChapter));
|
|
630
|
+
}
|
|
231
631
|
const asString = (value) => (typeof value === "string" ? value : null);
|
|
232
632
|
const qsRules = asString(paths.quickstart_rules);
|
|
233
633
|
const qsContractsDir = asString(paths.quickstart_contracts_dir);
|
|
234
634
|
const qsStyleProfile = asString(paths.quickstart_style_profile);
|
|
235
635
|
const qsTrialChapter = asString(paths.quickstart_trial_chapter);
|
|
236
636
|
const styleTemplate = asString(paths.style_profile_template);
|
|
637
|
+
const miniOutline = asString(paths.mini_volume_outline);
|
|
638
|
+
const miniStorylineSchedule = asString(paths.mini_storyline_schedule);
|
|
639
|
+
const miniVolumeForeshadowing = asString(paths.mini_volume_foreshadowing);
|
|
640
|
+
const miniChapterContract = asString(paths.mini_chapter_contract);
|
|
237
641
|
// Provide canonical manifest keys in addition to quickstart-scoped aliases.
|
|
238
642
|
if (qsRules)
|
|
239
643
|
paths.world_rules = qsRules;
|
|
@@ -245,6 +649,14 @@ async function buildQuickStartInstructionPacket(args) {
|
|
|
245
649
|
paths.style_profile = styleTemplate;
|
|
246
650
|
if (qsTrialChapter)
|
|
247
651
|
paths.chapter_draft = qsTrialChapter;
|
|
652
|
+
if (miniOutline)
|
|
653
|
+
paths.volume_outline = miniOutline;
|
|
654
|
+
if (miniStorylineSchedule)
|
|
655
|
+
paths.storyline_schedule = miniStorylineSchedule;
|
|
656
|
+
if (miniVolumeForeshadowing)
|
|
657
|
+
paths.volume_foreshadowing = miniVolumeForeshadowing;
|
|
658
|
+
if (miniChapterContract)
|
|
659
|
+
paths.chapter_contract = miniChapterContract;
|
|
248
660
|
let agent;
|
|
249
661
|
const expected_outputs = [];
|
|
250
662
|
const next_actions = [];
|
|
@@ -279,7 +691,27 @@ async function buildQuickStartInstructionPacket(args) {
|
|
|
279
691
|
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
280
692
|
next_actions.push({ kind: "command", command: `novel advance ${stepId}` });
|
|
281
693
|
next_actions.push({ kind: "command", command: `novel next`, note: "Compute next deterministic step (skips already-generated artifacts)." });
|
|
282
|
-
next_actions.push({ kind: "command", command: `novel instructions quickstart:
|
|
694
|
+
next_actions.push({ kind: "command", command: `novel instructions quickstart:f0 --json`, note: "After advance, generate the opening mini-plan for chapters 1-3." });
|
|
695
|
+
}
|
|
696
|
+
else if (step.phase === "f0") {
|
|
697
|
+
agent = { kind: "subagent", name: "plot-architect" };
|
|
698
|
+
inline.quickstart_mini_planning = true;
|
|
699
|
+
inline.volume_plan = { volume: 1, chapter_range: [QUICKSTART_MINI_PLANNING_RANGE.start, QUICKSTART_MINI_PLANNING_RANGE.end] };
|
|
700
|
+
inline.expected_outputs_base_dir = miniPlanningStaging.dir;
|
|
701
|
+
const selectedGenreExcitementMap = await loadSelectedGenreExcitementMap(args.rootDir);
|
|
702
|
+
if (selectedGenreExcitementMap)
|
|
703
|
+
inline.genre_excitement_map = selectedGenreExcitementMap;
|
|
704
|
+
expected_outputs.push({ path: miniPlanningStaging.outlineMd, required: true });
|
|
705
|
+
expected_outputs.push({ path: miniPlanningStaging.storylineScheduleJson, required: true });
|
|
706
|
+
expected_outputs.push({ path: miniPlanningStaging.foreshadowingJson, required: true });
|
|
707
|
+
expected_outputs.push({ path: miniPlanningStaging.newCharactersJson, required: true });
|
|
708
|
+
for (let ch = QUICKSTART_MINI_PLANNING_RANGE.start; ch <= QUICKSTART_MINI_PLANNING_RANGE.end; ch++) {
|
|
709
|
+
expected_outputs.push({ path: miniPlanningStaging.chapterContractJson(ch), required: true });
|
|
710
|
+
}
|
|
711
|
+
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
712
|
+
next_actions.push({ kind: "command", command: `novel advance ${stepId}` });
|
|
713
|
+
next_actions.push({ kind: "command", command: `novel next`, note: "Compute next deterministic step (skips already-generated artifacts)." });
|
|
714
|
+
next_actions.push({ kind: "command", command: `novel instructions quickstart:trial --json`, note: "After mini-plan commit, proceed to trial chapter writing." });
|
|
283
715
|
}
|
|
284
716
|
else if (step.phase === "trial") {
|
|
285
717
|
agent = { kind: "subagent", name: "chapter-writer" };
|
|
@@ -291,6 +723,43 @@ async function buildQuickStartInstructionPacket(args) {
|
|
|
291
723
|
}
|
|
292
724
|
else if (step.phase === "results") {
|
|
293
725
|
agent = { kind: "subagent", name: "quality-judge" };
|
|
726
|
+
const loadedPlatform = await loadPlatformProfile(args.rootDir);
|
|
727
|
+
if (loadedPlatform?.profile.scoring) {
|
|
728
|
+
const loadedWeights = await loadGenreWeightProfiles(args.rootDir);
|
|
729
|
+
if (!loadedWeights) {
|
|
730
|
+
throw new NovelCliError("Missing required file: genre-weight-profiles.json (required when platform-profile.json.scoring is present). Copy it from templates/genre-weight-profiles.json.", 2);
|
|
731
|
+
}
|
|
732
|
+
inline.scoring_weights = {
|
|
733
|
+
...computeEffectiveScoringWeights({
|
|
734
|
+
config: loadedWeights.config,
|
|
735
|
+
scoring: loadedPlatform.profile.scoring,
|
|
736
|
+
hookPolicy: loadedPlatform.profile.hook_policy,
|
|
737
|
+
platformId: loadedPlatform.profile.platform
|
|
738
|
+
}),
|
|
739
|
+
source: { platform_profile: loadedPlatform.relPath, genre_weight_profiles: loadedWeights.relPath }
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
if (loadedPlatform && trialChapter <= 3) {
|
|
743
|
+
const loadedGoldenGates = await loadGoldenChapterGates(args.rootDir);
|
|
744
|
+
if (loadedGoldenGates) {
|
|
745
|
+
const selectedGoldenGates = selectGoldenChapterGatesForPlatform({
|
|
746
|
+
config: loadedGoldenGates.config,
|
|
747
|
+
platformId: loadedPlatform.profile.platform,
|
|
748
|
+
chapter: trialChapter
|
|
749
|
+
});
|
|
750
|
+
if (selectedGoldenGates) {
|
|
751
|
+
inline.golden_chapter_gates = {
|
|
752
|
+
...selectedGoldenGates,
|
|
753
|
+
source: loadedGoldenGates.relPath
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (trialChapter <= 3) {
|
|
759
|
+
const selectedGenreGoldenStandards = await loadSelectedGenreGoldenStandards(args.rootDir);
|
|
760
|
+
if (selectedGenreGoldenStandards)
|
|
761
|
+
inline.genre_golden_standards = selectedGenreGoldenStandards;
|
|
762
|
+
}
|
|
294
763
|
expected_outputs.push({
|
|
295
764
|
path: QUICKSTART_STAGING_RELS.evaluationJson,
|
|
296
765
|
required: true,
|
|
@@ -354,7 +823,7 @@ export async function buildInstructionPacket(args) {
|
|
|
354
823
|
if (args.step.kind === "volume") {
|
|
355
824
|
const step = args.step;
|
|
356
825
|
const volume = args.checkpoint.current_volume;
|
|
357
|
-
const range =
|
|
826
|
+
const range = await resolveVolumeChapterRange({ rootDir: args.rootDir, current_volume: volume, last_completed_chapter: args.checkpoint.last_completed_chapter });
|
|
358
827
|
const embedMode = safeEmbedMode(args.embedMode);
|
|
359
828
|
const embed = {};
|
|
360
829
|
if (embedMode === "brief") {
|
|
@@ -402,6 +871,13 @@ export async function buildInstructionPacket(args) {
|
|
|
402
871
|
const next_actions = [];
|
|
403
872
|
const staging = volumeStagingRelPaths(volume);
|
|
404
873
|
const final = volumeFinalRelPaths(volume);
|
|
874
|
+
if (range.start > 1) {
|
|
875
|
+
await maybeAddPath("existing_volume_outline", final.outlineMd);
|
|
876
|
+
await maybeAddPath("existing_storyline_schedule", final.storylineScheduleJson);
|
|
877
|
+
await maybeAddPath("existing_foreshadowing", final.foreshadowingJson);
|
|
878
|
+
await maybeAddPath("existing_chapter_contracts_dir", final.chapterContractsDir);
|
|
879
|
+
inline.volume_plan_seed_range = [1, range.start - 1];
|
|
880
|
+
}
|
|
405
881
|
const addPlanningOutputs = (base) => {
|
|
406
882
|
expected_outputs.push({ path: base.outlineMd, required: true });
|
|
407
883
|
expected_outputs.push({ path: base.storylineScheduleJson, required: true });
|
|
@@ -413,6 +889,11 @@ export async function buildInstructionPacket(args) {
|
|
|
413
889
|
};
|
|
414
890
|
if (step.phase === "outline") {
|
|
415
891
|
agent = { kind: "subagent", name: "plot-architect" };
|
|
892
|
+
if (range.start <= 3) {
|
|
893
|
+
const selectedGenreExcitementMap = await loadSelectedGenreExcitementMap(args.rootDir);
|
|
894
|
+
if (selectedGenreExcitementMap)
|
|
895
|
+
inline.genre_excitement_map = selectedGenreExcitementMap;
|
|
896
|
+
}
|
|
416
897
|
inline.expected_outputs_base_dir = staging.dir;
|
|
417
898
|
addPlanningOutputs(staging);
|
|
418
899
|
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
@@ -498,6 +979,7 @@ export async function buildInstructionPacket(args) {
|
|
|
498
979
|
await maybeAddPath(commonPaths, "project_brief", "brief.md");
|
|
499
980
|
await maybeAddPath(commonPaths, "style_profile", "style-profile.json");
|
|
500
981
|
await maybeAddPath(commonPaths, "platform_profile", "platform-profile.json");
|
|
982
|
+
await maybeAddPath(commonPaths, "platform_writing_guide", "platform-writing-guide.md");
|
|
501
983
|
await maybeAddPath(commonPaths, "ai_blacklist", "ai-blacklist.json");
|
|
502
984
|
await maybeAddPath(commonPaths, "web_novel_cliche_lint", "web-novel-cliche-lint.json");
|
|
503
985
|
await maybeAddPath(commonPaths, "genre_weight_profiles", "genre-weight-profiles.json");
|
|
@@ -613,6 +1095,23 @@ export async function buildInstructionPacket(args) {
|
|
|
613
1095
|
inline.engagement_report_summary_degraded = true;
|
|
614
1096
|
inline.promise_ledger_report_summary_degraded = true;
|
|
615
1097
|
}
|
|
1098
|
+
await attachCanonStatusContext({
|
|
1099
|
+
rootDir: args.rootDir,
|
|
1100
|
+
chapterContractRel,
|
|
1101
|
+
inline,
|
|
1102
|
+
paths,
|
|
1103
|
+
options: {
|
|
1104
|
+
includePlannedRulesInfo: true,
|
|
1105
|
+
includePlannedCharacters: true,
|
|
1106
|
+
prioritizePlannedCharactersOnFallback: true
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
const statisticalTargets = await loadAntiAiStatisticalTargets(args.rootDir);
|
|
1110
|
+
if (statisticalTargets)
|
|
1111
|
+
inline.statistical_targets = statisticalTargets;
|
|
1112
|
+
const genreOverrides = await loadAntiAiGenreOverrides(args.rootDir);
|
|
1113
|
+
if (genreOverrides)
|
|
1114
|
+
inline.genre_overrides = genreOverrides;
|
|
616
1115
|
// Optional: inject non-spoiler light-touch reminders for dormant foreshadowing items (best-effort).
|
|
617
1116
|
try {
|
|
618
1117
|
const loadedPlatform = await loadPlatformProfile(args.rootDir).catch(() => null);
|
|
@@ -686,6 +1185,30 @@ export async function buildInstructionPacket(args) {
|
|
|
686
1185
|
const chapterDraftRel = relIfExists(rel.staging.chapterMd, await pathExists(join(args.rootDir, rel.staging.chapterMd)));
|
|
687
1186
|
paths.chapter_draft = chapterDraftRel;
|
|
688
1187
|
paths.cross_references = relIfExists(rel.staging.crossrefJson, await pathExists(join(args.rootDir, rel.staging.crossrefJson)));
|
|
1188
|
+
if (chapterDraftRel) {
|
|
1189
|
+
const antiAiContext = await loadAntiAiJudgeContext({ rootDir: args.rootDir, chapterRel: chapterDraftRel });
|
|
1190
|
+
if (antiAiContext.blacklistLint)
|
|
1191
|
+
inline.blacklist_lint = antiAiContext.blacklistLint;
|
|
1192
|
+
else if (antiAiContext.degraded.blacklist_lint)
|
|
1193
|
+
inline.blacklist_lint_degraded = true;
|
|
1194
|
+
if (antiAiContext.statisticalProfile)
|
|
1195
|
+
inline.statistical_profile = antiAiContext.statisticalProfile;
|
|
1196
|
+
if (antiAiContext.structuralRuleViolations)
|
|
1197
|
+
inline.structural_rule_violations = antiAiContext.structuralRuleViolations;
|
|
1198
|
+
else if (antiAiContext.degraded.structural_rule_violations)
|
|
1199
|
+
inline.structural_rule_violations_degraded = true;
|
|
1200
|
+
}
|
|
1201
|
+
await attachCanonStatusContext({
|
|
1202
|
+
rootDir: args.rootDir,
|
|
1203
|
+
chapterContractRel,
|
|
1204
|
+
inline,
|
|
1205
|
+
paths,
|
|
1206
|
+
options: {
|
|
1207
|
+
includePlannedRulesInfo: false,
|
|
1208
|
+
includePlannedCharacters: false,
|
|
1209
|
+
prioritizePlannedCharactersOnFallback: false
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
689
1212
|
const loadedPlatform = await loadPlatformProfile(args.rootDir);
|
|
690
1213
|
if (loadedPlatform?.profile.scoring) {
|
|
691
1214
|
const loadedWeights = await loadGenreWeightProfiles(args.rootDir);
|
|
@@ -695,15 +1218,38 @@ export async function buildInstructionPacket(args) {
|
|
|
695
1218
|
const effective = computeEffectiveScoringWeights({
|
|
696
1219
|
config: loadedWeights.config,
|
|
697
1220
|
scoring: loadedPlatform.profile.scoring,
|
|
698
|
-
hookPolicy: loadedPlatform.profile.hook_policy
|
|
1221
|
+
hookPolicy: loadedPlatform.profile.hook_policy,
|
|
1222
|
+
platformId: loadedPlatform.profile.platform
|
|
699
1223
|
});
|
|
700
1224
|
inline.scoring_weights = {
|
|
701
1225
|
...effective,
|
|
702
1226
|
source: { platform_profile: loadedPlatform.relPath, genre_weight_profiles: loadedWeights.relPath }
|
|
703
1227
|
};
|
|
704
1228
|
}
|
|
1229
|
+
if (loadedPlatform && step.chapter <= 3) {
|
|
1230
|
+
const loadedGoldenGates = await loadGoldenChapterGates(args.rootDir);
|
|
1231
|
+
if (loadedGoldenGates) {
|
|
1232
|
+
const selectedGoldenGates = selectGoldenChapterGatesForPlatform({
|
|
1233
|
+
config: loadedGoldenGates.config,
|
|
1234
|
+
platformId: loadedPlatform.profile.platform,
|
|
1235
|
+
chapter: step.chapter
|
|
1236
|
+
});
|
|
1237
|
+
if (selectedGoldenGates) {
|
|
1238
|
+
inline.golden_chapter_gates = {
|
|
1239
|
+
...selectedGoldenGates,
|
|
1240
|
+
source: loadedGoldenGates.relPath
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (step.chapter <= 3) {
|
|
1246
|
+
const selectedGenreGoldenStandards = await loadSelectedGenreGoldenStandards(args.rootDir);
|
|
1247
|
+
if (selectedGenreGoldenStandards)
|
|
1248
|
+
inline.genre_golden_standards = selectedGenreGoldenStandards;
|
|
1249
|
+
}
|
|
705
1250
|
// Optional: inject compact continuity summary for LS-001 evidence (non-blocking).
|
|
706
1251
|
inline.continuity_report_summary = await loadContinuityLatestSummary(args.rootDir);
|
|
1252
|
+
inline.excitement_type = await loadChapterExcitementType({ rootDir: args.rootDir, volume, chapter: step.chapter });
|
|
707
1253
|
// Optional: pre-judge guardrails report (title/readability/naming). Non-blocking here; gate engine decides.
|
|
708
1254
|
inline.prejudge_guardrails = null;
|
|
709
1255
|
if (loadedPlatform && chapterDraftRel) {
|