novel-writer-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/agents/chapter-writer.md +142 -0
- package/agents/character-weaver.md +117 -0
- package/agents/consistency-auditor.md +85 -0
- package/agents/plot-architect.md +128 -0
- package/agents/quality-judge.md +232 -0
- package/agents/style-analyzer.md +109 -0
- package/agents/style-refiner.md +97 -0
- package/agents/summarizer.md +128 -0
- package/agents/world-builder.md +161 -0
- package/dist/__tests__/character-voice.test.js +445 -0
- package/dist/__tests__/commit-prototype-pollution.test.js +45 -0
- package/dist/__tests__/engagement.test.js +382 -0
- package/dist/__tests__/foreshadow-visibility.test.js +131 -0
- package/dist/__tests__/hook-ledger.test.js +1028 -0
- package/dist/__tests__/naming-lint.test.js +132 -0
- package/dist/__tests__/narrative-health-injection.test.js +359 -0
- package/dist/__tests__/next-step-prejudge-guardrails.test.js +325 -0
- package/dist/__tests__/next-step-title-fix.test.js +153 -0
- package/dist/__tests__/platform-profile.test.js +274 -0
- package/dist/__tests__/promise-ledger.test.js +189 -0
- package/dist/__tests__/readability-lint.test.js +209 -0
- package/dist/__tests__/text-utils.test.js +39 -0
- package/dist/__tests__/title-policy.test.js +147 -0
- package/dist/advance.js +75 -0
- package/dist/character-voice.js +805 -0
- package/dist/checkpoint.js +126 -0
- package/dist/cli.js +563 -0
- package/dist/cliche-lint.js +515 -0
- package/dist/commit.js +1460 -0
- package/dist/consistency-auditor.js +684 -0
- package/dist/engagement.js +687 -0
- package/dist/errors.js +7 -0
- package/dist/fingerprint.js +16 -0
- package/dist/foreshadow-visibility.js +214 -0
- package/dist/fs-utils.js +68 -0
- package/dist/hook-ledger.js +721 -0
- package/dist/hook-policy.js +107 -0
- package/dist/instruction-gates.js +51 -0
- package/dist/instructions.js +406 -0
- package/dist/latest-summary-loader.js +29 -0
- package/dist/lock.js +121 -0
- package/dist/naming-lint.js +531 -0
- package/dist/ner.js +73 -0
- package/dist/next-step.js +408 -0
- package/dist/novel-ask.js +270 -0
- package/dist/output.js +9 -0
- package/dist/platform-constraints.js +518 -0
- package/dist/platform-profile.js +325 -0
- package/dist/prejudge-guardrails.js +370 -0
- package/dist/project.js +40 -0
- package/dist/promise-ledger.js +723 -0
- package/dist/readability-lint.js +555 -0
- package/dist/safe-parse.js +36 -0
- package/dist/safe-path.js +29 -0
- package/dist/scoring-weights.js +290 -0
- package/dist/steps.js +60 -0
- package/dist/text-utils.js +18 -0
- package/dist/title-policy.js +251 -0
- package/dist/type-guards.js +6 -0
- package/dist/validate.js +131 -0
- package/docs/user/README.md +17 -0
- package/docs/user/guardrails.md +179 -0
- package/docs/user/interactive-gates.md +124 -0
- package/docs/user/novel-cli.md +289 -0
- package/docs/user/ops.md +123 -0
- package/docs/user/quick-start.md +97 -0
- package/docs/user/spec-system.md +166 -0
- package/docs/user/storylines.md +144 -0
- package/package.json +48 -0
- package/schemas/README.md +18 -0
- package/schemas/character-voice-drift.schema.json +135 -0
- package/schemas/character-voice-profiles.schema.json +141 -0
- package/schemas/engagement-metrics.schema.json +38 -0
- package/schemas/hook-ledger.schema.json +108 -0
- package/schemas/platform-profile.schema.json +235 -0
- package/schemas/promise-ledger.schema.json +97 -0
- package/scripts/calibrate-quality-judge.sh +91 -0
- package/scripts/compare-regression-runs.sh +86 -0
- package/scripts/lib/_common.py +131 -0
- package/scripts/lib/calibrate_quality_judge.py +312 -0
- package/scripts/lib/compare_regression_runs.py +142 -0
- package/scripts/lib/run_regression.py +621 -0
- package/scripts/lint-blacklist.sh +201 -0
- package/scripts/lint-cliche.sh +370 -0
- package/scripts/lint-readability.sh +404 -0
- package/scripts/query-foreshadow.sh +252 -0
- package/scripts/run-ner.sh +669 -0
- package/scripts/run-regression.sh +122 -0
- package/skills/cli-step/SKILL.md +158 -0
- package/skills/continue/SKILL.md +348 -0
- package/skills/continue/references/context-contracts.md +169 -0
- package/skills/continue/references/continuity-checks.md +187 -0
- package/skills/continue/references/file-protocols.md +64 -0
- package/skills/continue/references/foreshadowing.md +130 -0
- package/skills/continue/references/gate-decision.md +53 -0
- package/skills/continue/references/periodic-maintenance.md +46 -0
- package/skills/novel-writing/SKILL.md +77 -0
- package/skills/novel-writing/references/quality-rubric.md +140 -0
- package/skills/novel-writing/references/style-guide.md +145 -0
- package/skills/start/SKILL.md +458 -0
- package/skills/start/references/quality-review.md +86 -0
- package/skills/start/references/setting-update.md +44 -0
- package/skills/start/references/vol-planning.md +61 -0
- package/skills/start/references/vol-review.md +58 -0
- package/skills/status/SKILL.md +116 -0
- package/skills/status/references/sample-output.md +60 -0
- package/templates/ai-blacklist.json +79 -0
- package/templates/brief-template.md +46 -0
- package/templates/genre-weight-profiles.json +90 -0
- package/templates/novel-ask/example.answer.json +12 -0
- package/templates/novel-ask/example.question.json +51 -0
- package/templates/platform-profile.json +148 -0
- package/templates/style-profile-template.json +58 -0
- package/templates/web-novel-cliche-lint.json +41 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { NovelCliError } from "./errors.js";
|
|
2
|
+
import { isPlainObject } from "./type-guards.js";
|
|
3
|
+
const SNAKE_CASE_ID = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
|
|
4
|
+
const RFC3339_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
5
|
+
export function isSnakeCaseId(id) {
|
|
6
|
+
return SNAKE_CASE_ID.test(id);
|
|
7
|
+
}
|
|
8
|
+
function fail(context, message) {
|
|
9
|
+
throw new NovelCliError(`Invalid ${context}: ${message}`, 2);
|
|
10
|
+
}
|
|
11
|
+
function assertNoUnknownKeys(obj, allowed, context) {
|
|
12
|
+
for (const key of Object.keys(obj)) {
|
|
13
|
+
if (key.startsWith("_"))
|
|
14
|
+
continue;
|
|
15
|
+
if (!allowed.has(key))
|
|
16
|
+
fail(context, `Unknown field '${key}'.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function requirePlainObject(value, context) {
|
|
20
|
+
if (!isPlainObject(value))
|
|
21
|
+
fail(context, "Expected an object.");
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function requireNonEmptyString(value, context) {
|
|
25
|
+
if (typeof value !== "string")
|
|
26
|
+
fail(context, "Expected a string.");
|
|
27
|
+
if (value.trim().length === 0)
|
|
28
|
+
fail(context, "Expected a non-empty string.");
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function requireBoolean(value, context) {
|
|
32
|
+
if (typeof value !== "boolean")
|
|
33
|
+
fail(context, "Expected a boolean.");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function requireInt(value, context) {
|
|
37
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
38
|
+
fail(context, "Expected an integer.");
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function requireNonEmptyArray(value, context) {
|
|
42
|
+
if (!Array.isArray(value))
|
|
43
|
+
fail(context, "Expected an array.");
|
|
44
|
+
if (value.length === 0)
|
|
45
|
+
fail(context, "Expected a non-empty array.");
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function requireIsoDateString(value, context) {
|
|
49
|
+
const s = requireNonEmptyString(value, context);
|
|
50
|
+
if (!RFC3339_DATE_TIME.test(s))
|
|
51
|
+
fail(context, "Expected an ISO-8601 / RFC3339 date-time string (with timezone).");
|
|
52
|
+
if (!Number.isFinite(Date.parse(s)))
|
|
53
|
+
fail(context, "Expected a valid ISO-8601 / RFC3339 date-time string.");
|
|
54
|
+
return s;
|
|
55
|
+
}
|
|
56
|
+
function validateOptions(raw, context) {
|
|
57
|
+
const optionsRaw = requireNonEmptyArray(raw, context);
|
|
58
|
+
const labels = new Set();
|
|
59
|
+
const options = [];
|
|
60
|
+
for (let i = 0; i < optionsRaw.length; i++) {
|
|
61
|
+
const optCtx = `${context}[${i}]`;
|
|
62
|
+
const optObj = requirePlainObject(optionsRaw[i], optCtx);
|
|
63
|
+
assertNoUnknownKeys(optObj, new Set(["label", "description"]), optCtx);
|
|
64
|
+
const label = requireNonEmptyString(optObj.label, `${optCtx}.label`);
|
|
65
|
+
const description = requireNonEmptyString(optObj.description, `${optCtx}.description`);
|
|
66
|
+
if (labels.has(label))
|
|
67
|
+
fail(`${optCtx}.label`, `Duplicate option label '${label}'.`);
|
|
68
|
+
labels.add(label);
|
|
69
|
+
options.push({ label, description });
|
|
70
|
+
}
|
|
71
|
+
return options;
|
|
72
|
+
}
|
|
73
|
+
function validateSingleChoiceDefault(raw, optionLabels, context) {
|
|
74
|
+
if (raw === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const d = requireNonEmptyString(raw, context);
|
|
77
|
+
if (!optionLabels.has(d))
|
|
78
|
+
fail(context, `Default must be one of the option labels.`);
|
|
79
|
+
return d;
|
|
80
|
+
}
|
|
81
|
+
function validateMultiChoiceDefault(raw, optionLabels, context) {
|
|
82
|
+
if (raw === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (!Array.isArray(raw))
|
|
85
|
+
fail(context, "Default must be an array of strings.");
|
|
86
|
+
if (raw.length === 0)
|
|
87
|
+
fail(context, "Default must be a non-empty array of strings.");
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const out = [];
|
|
90
|
+
for (let i = 0; i < raw.length; i++) {
|
|
91
|
+
const itemCtx = `${context}[${i}]`;
|
|
92
|
+
const v = requireNonEmptyString(raw[i], itemCtx);
|
|
93
|
+
if (!optionLabels.has(v))
|
|
94
|
+
fail(itemCtx, "Default entry must be one of the option labels.");
|
|
95
|
+
if (seen.has(v))
|
|
96
|
+
fail(itemCtx, `Duplicate default entry '${v}'.`);
|
|
97
|
+
seen.add(v);
|
|
98
|
+
out.push(v);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
export function parseNovelAskQuestionSpec(raw) {
|
|
103
|
+
const ctx = "NOVEL_ASK";
|
|
104
|
+
const obj = requirePlainObject(raw, ctx);
|
|
105
|
+
assertNoUnknownKeys(obj, new Set(["version", "topic", "questions"]), ctx);
|
|
106
|
+
const version = requireInt(obj.version, `${ctx}.version`);
|
|
107
|
+
if (version < 1)
|
|
108
|
+
fail(`${ctx}.version`, "Expected an integer >= 1.");
|
|
109
|
+
const topic = requireNonEmptyString(obj.topic, `${ctx}.topic`);
|
|
110
|
+
const questionsRaw = requireNonEmptyArray(obj.questions, `${ctx}.questions`);
|
|
111
|
+
const questions = [];
|
|
112
|
+
const questionIds = new Set();
|
|
113
|
+
for (let i = 0; i < questionsRaw.length; i++) {
|
|
114
|
+
const qCtx = `${ctx}.questions[${i}]`;
|
|
115
|
+
const qObj = requirePlainObject(questionsRaw[i], qCtx);
|
|
116
|
+
const id = requireNonEmptyString(qObj.id, `${qCtx}.id`);
|
|
117
|
+
if (!isSnakeCaseId(id))
|
|
118
|
+
fail(`${qCtx}.id`, "Question id must be stable snake_case.");
|
|
119
|
+
if (questionIds.has(id))
|
|
120
|
+
fail(`${qCtx}.id`, `Duplicate question id '${id}'.`);
|
|
121
|
+
questionIds.add(id);
|
|
122
|
+
const header = requireNonEmptyString(qObj.header, `${qCtx}.header`);
|
|
123
|
+
const question = requireNonEmptyString(qObj.question, `${qCtx}.question`);
|
|
124
|
+
const kind = qObj.kind;
|
|
125
|
+
if (kind !== "single_choice" && kind !== "multi_choice" && kind !== "free_text") {
|
|
126
|
+
fail(`${qCtx}.kind`, `Expected one of: single_choice, multi_choice, free_text.`);
|
|
127
|
+
}
|
|
128
|
+
const required = requireBoolean(qObj.required, `${qCtx}.required`);
|
|
129
|
+
if (kind === "free_text") {
|
|
130
|
+
assertNoUnknownKeys(qObj, new Set(["id", "header", "question", "kind", "required"]), qCtx);
|
|
131
|
+
questions.push({ id, header, question, kind, required });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const options = validateOptions(qObj.options, `${qCtx}.options`);
|
|
135
|
+
const optionLabels = new Set(options.map((o) => o.label));
|
|
136
|
+
const allowOtherRaw = qObj.allow_other;
|
|
137
|
+
const allow_other = allowOtherRaw === undefined ? undefined : requireBoolean(allowOtherRaw, `${qCtx}.allow_other`);
|
|
138
|
+
if (kind === "single_choice") {
|
|
139
|
+
assertNoUnknownKeys(qObj, new Set(["id", "header", "question", "kind", "required", "options", "allow_other", "default"]), qCtx);
|
|
140
|
+
const def = validateSingleChoiceDefault(qObj.default, optionLabels, `${qCtx}.default`);
|
|
141
|
+
questions.push({
|
|
142
|
+
id,
|
|
143
|
+
header,
|
|
144
|
+
question,
|
|
145
|
+
kind,
|
|
146
|
+
required,
|
|
147
|
+
options,
|
|
148
|
+
...(allow_other === undefined ? {} : { allow_other }),
|
|
149
|
+
...(def === undefined ? {} : { default: def })
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
assertNoUnknownKeys(qObj, new Set(["id", "header", "question", "kind", "required", "options", "allow_other", "default"]), qCtx);
|
|
154
|
+
const def = validateMultiChoiceDefault(qObj.default, optionLabels, `${qCtx}.default`);
|
|
155
|
+
questions.push({
|
|
156
|
+
id,
|
|
157
|
+
header,
|
|
158
|
+
question,
|
|
159
|
+
kind,
|
|
160
|
+
required,
|
|
161
|
+
options,
|
|
162
|
+
...(allow_other === undefined ? {} : { allow_other }),
|
|
163
|
+
...(def === undefined ? {} : { default: def })
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return { version, topic, questions };
|
|
167
|
+
}
|
|
168
|
+
export function parseNovelAskAnswerSpec(raw) {
|
|
169
|
+
const ctx = "AnswerSpec";
|
|
170
|
+
const obj = requirePlainObject(raw, ctx);
|
|
171
|
+
assertNoUnknownKeys(obj, new Set(["version", "topic", "answers", "answered_at", "answered_by"]), ctx);
|
|
172
|
+
const version = requireInt(obj.version, `${ctx}.version`);
|
|
173
|
+
if (version < 1)
|
|
174
|
+
fail(`${ctx}.version`, "Expected an integer >= 1.");
|
|
175
|
+
const topic = requireNonEmptyString(obj.topic, `${ctx}.topic`);
|
|
176
|
+
const answered_at = requireIsoDateString(obj.answered_at, `${ctx}.answered_at`);
|
|
177
|
+
const answered_by = requireNonEmptyString(obj.answered_by, `${ctx}.answered_by`);
|
|
178
|
+
const answersObj = requirePlainObject(obj.answers, `${ctx}.answers`);
|
|
179
|
+
const answers = {};
|
|
180
|
+
for (const [questionId, value] of Object.entries(answersObj)) {
|
|
181
|
+
const entryCtx = `${ctx}.answers.${questionId}`;
|
|
182
|
+
if (!isSnakeCaseId(questionId))
|
|
183
|
+
fail(entryCtx, "Answer key must be stable snake_case.");
|
|
184
|
+
if (typeof value === "string") {
|
|
185
|
+
if (value.trim().length === 0)
|
|
186
|
+
fail(entryCtx, "Expected a non-empty string.");
|
|
187
|
+
answers[questionId] = value;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
if (value.length === 0)
|
|
192
|
+
fail(entryCtx, "Expected a non-empty string array.");
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
const out = [];
|
|
195
|
+
for (let i = 0; i < value.length; i++) {
|
|
196
|
+
const itemCtx = `${entryCtx}[${i}]`;
|
|
197
|
+
const v = requireNonEmptyString(value[i], itemCtx);
|
|
198
|
+
if (seen.has(v))
|
|
199
|
+
fail(itemCtx, `Duplicate answer entry '${v}'.`);
|
|
200
|
+
seen.add(v);
|
|
201
|
+
out.push(v);
|
|
202
|
+
}
|
|
203
|
+
answers[questionId] = out;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
fail(entryCtx, "Expected a string or string array.");
|
|
207
|
+
}
|
|
208
|
+
return { version, topic, answers, answered_at, answered_by };
|
|
209
|
+
}
|
|
210
|
+
export function validateNovelAskAnswerAgainstQuestionSpec(questionSpec, answerSpec) {
|
|
211
|
+
const qs = questionSpec;
|
|
212
|
+
const ans = answerSpec;
|
|
213
|
+
if (ans.version !== qs.version) {
|
|
214
|
+
fail("AnswerSpec.version", `Must match NOVEL_ASK.version (${qs.version}).`);
|
|
215
|
+
}
|
|
216
|
+
if (ans.topic !== qs.topic) {
|
|
217
|
+
fail("AnswerSpec.topic", `Must match NOVEL_ASK.topic (${JSON.stringify(qs.topic)}).`);
|
|
218
|
+
}
|
|
219
|
+
const questionById = new Map();
|
|
220
|
+
for (const q of qs.questions)
|
|
221
|
+
questionById.set(q.id, q);
|
|
222
|
+
for (const answerId of Object.keys(ans.answers)) {
|
|
223
|
+
if (!questionById.has(answerId))
|
|
224
|
+
fail(`AnswerSpec.answers.${answerId}`, "No matching question id in NOVEL_ASK.");
|
|
225
|
+
}
|
|
226
|
+
for (const q of qs.questions) {
|
|
227
|
+
const value = ans.answers[q.id];
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
if (q.required)
|
|
230
|
+
fail(`AnswerSpec.answers.${q.id}`, "Missing required answer.");
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (q.kind === "free_text") {
|
|
234
|
+
if (typeof value !== "string")
|
|
235
|
+
fail(`AnswerSpec.answers.${q.id}`, "Expected a string for free_text.");
|
|
236
|
+
if (value.trim().length === 0)
|
|
237
|
+
fail(`AnswerSpec.answers.${q.id}`, "Expected a non-empty string for free_text.");
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const optionLabels = new Set(q.options.map((o) => o.label));
|
|
241
|
+
const allowOther = q.allow_other === true;
|
|
242
|
+
if (q.kind === "single_choice") {
|
|
243
|
+
if (typeof value !== "string")
|
|
244
|
+
fail(`AnswerSpec.answers.${q.id}`, "Expected a string for single_choice.");
|
|
245
|
+
if (value.trim().length === 0)
|
|
246
|
+
fail(`AnswerSpec.answers.${q.id}`, "Expected a non-empty string for single_choice.");
|
|
247
|
+
if (!allowOther && !optionLabels.has(value))
|
|
248
|
+
fail(`AnswerSpec.answers.${q.id}`, "Answer must be one of the option labels.");
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!Array.isArray(value))
|
|
252
|
+
fail(`AnswerSpec.answers.${q.id}`, "Expected a string array for multi_choice.");
|
|
253
|
+
if (value.length === 0)
|
|
254
|
+
fail(`AnswerSpec.answers.${q.id}`, "Expected a non-empty string array for multi_choice.");
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
for (let i = 0; i < value.length; i++) {
|
|
257
|
+
const item = value[i];
|
|
258
|
+
const itemCtx = `AnswerSpec.answers.${q.id}[${i}]`;
|
|
259
|
+
if (typeof item !== "string")
|
|
260
|
+
fail(itemCtx, "Expected a string.");
|
|
261
|
+
if (item.trim().length === 0)
|
|
262
|
+
fail(itemCtx, "Expected a non-empty string.");
|
|
263
|
+
if (seen.has(item))
|
|
264
|
+
fail(itemCtx, `Duplicate answer entry '${item}'.`);
|
|
265
|
+
seen.add(item);
|
|
266
|
+
if (!allowOther && !optionLabels.has(item))
|
|
267
|
+
fail(itemCtx, "Answer entry must be one of the option labels.");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function okJson(command, data = {}) {
|
|
2
|
+
return { ok: true, command, data };
|
|
3
|
+
}
|
|
4
|
+
export function errJson(command, message, code) {
|
|
5
|
+
return { ok: false, command, error: { message, code } };
|
|
6
|
+
}
|
|
7
|
+
export function printJson(payload) {
|
|
8
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
9
|
+
}
|