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
package/dist/validate.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { NovelCliError } from "./errors.js";
|
|
3
|
+
import { pathExists, readJsonFile, readTextFile } from "./fs-utils.js";
|
|
4
|
+
import { checkHookPolicy } from "./hook-policy.js";
|
|
5
|
+
import { loadPlatformProfile } from "./platform-profile.js";
|
|
6
|
+
import { rejectPathTraversalInput } from "./safe-path.js";
|
|
7
|
+
import { chapterRelPaths, formatStepId, titleFixSnapshotRel } from "./steps.js";
|
|
8
|
+
import { assertTitleFixOnlyChangedTitleLine, extractChapterTitleFromMarkdown } from "./title-policy.js";
|
|
9
|
+
import { isPlainObject } from "./type-guards.js";
|
|
10
|
+
function requireFile(exists, relPath) {
|
|
11
|
+
if (!exists)
|
|
12
|
+
throw new NovelCliError(`Missing required file: ${relPath}`, 2);
|
|
13
|
+
}
|
|
14
|
+
function requireStringField(obj, field, file) {
|
|
15
|
+
const v = obj[field];
|
|
16
|
+
if (typeof v !== "string" || v.length === 0)
|
|
17
|
+
throw new NovelCliError(`Invalid ${file}: missing string field '${field}'.`, 2);
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
function requireNumberField(obj, field, file) {
|
|
21
|
+
const v = obj[field];
|
|
22
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
23
|
+
throw new NovelCliError(`Invalid ${file}: missing number field '${field}'.`, 2);
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
export async function validateStep(args) {
|
|
27
|
+
const warnings = [];
|
|
28
|
+
const stepId = formatStepId(args.step);
|
|
29
|
+
if (args.step.kind !== "chapter") {
|
|
30
|
+
throw new NovelCliError(`Unsupported step: ${stepId}`, 2);
|
|
31
|
+
}
|
|
32
|
+
const rel = chapterRelPaths(args.step.chapter);
|
|
33
|
+
if (args.step.stage === "draft") {
|
|
34
|
+
const absChapter = join(args.rootDir, rel.staging.chapterMd);
|
|
35
|
+
const exists = await pathExists(absChapter);
|
|
36
|
+
requireFile(exists, rel.staging.chapterMd);
|
|
37
|
+
const content = await readTextFile(absChapter);
|
|
38
|
+
if (content.trim().length === 0)
|
|
39
|
+
throw new NovelCliError(`Empty draft file: ${rel.staging.chapterMd}`, 2);
|
|
40
|
+
return { ok: true, step: stepId, warnings };
|
|
41
|
+
}
|
|
42
|
+
if (args.step.stage === "summarize") {
|
|
43
|
+
requireFile(await pathExists(join(args.rootDir, rel.staging.chapterMd)), rel.staging.chapterMd);
|
|
44
|
+
requireFile(await pathExists(join(args.rootDir, rel.staging.summaryMd)), rel.staging.summaryMd);
|
|
45
|
+
requireFile(await pathExists(join(args.rootDir, rel.staging.deltaJson)), rel.staging.deltaJson);
|
|
46
|
+
requireFile(await pathExists(join(args.rootDir, rel.staging.crossrefJson)), rel.staging.crossrefJson);
|
|
47
|
+
const deltaRaw = await readJsonFile(join(args.rootDir, rel.staging.deltaJson));
|
|
48
|
+
if (!isPlainObject(deltaRaw))
|
|
49
|
+
throw new NovelCliError(`Invalid delta JSON: ${rel.staging.deltaJson} must be an object.`, 2);
|
|
50
|
+
const delta = deltaRaw;
|
|
51
|
+
const chapter = requireNumberField(delta, "chapter", rel.staging.deltaJson);
|
|
52
|
+
if (chapter !== args.step.chapter)
|
|
53
|
+
warnings.push(`Delta.chapter is ${chapter}, expected ${args.step.chapter}.`);
|
|
54
|
+
const storylineId = requireStringField(delta, "storyline_id", rel.staging.deltaJson);
|
|
55
|
+
rejectPathTraversalInput(storylineId, "delta.storyline_id");
|
|
56
|
+
const memoryRel = chapterRelPaths(args.step.chapter, storylineId).staging.storylineMemoryMd;
|
|
57
|
+
if (!memoryRel)
|
|
58
|
+
throw new NovelCliError(`Internal error: storyline memory path is null`, 2);
|
|
59
|
+
requireFile(await pathExists(join(args.rootDir, memoryRel)), memoryRel);
|
|
60
|
+
// Crossref sanity.
|
|
61
|
+
const crossrefRaw = await readJsonFile(join(args.rootDir, rel.staging.crossrefJson));
|
|
62
|
+
if (!isPlainObject(crossrefRaw))
|
|
63
|
+
throw new NovelCliError(`Invalid crossref JSON: ${rel.staging.crossrefJson} must be an object.`, 2);
|
|
64
|
+
return { ok: true, step: stepId, warnings };
|
|
65
|
+
}
|
|
66
|
+
if (args.step.stage === "refine") {
|
|
67
|
+
requireFile(await pathExists(join(args.rootDir, rel.staging.chapterMd)), rel.staging.chapterMd);
|
|
68
|
+
const changesExists = await pathExists(join(args.rootDir, rel.staging.styleRefinerChangesJson));
|
|
69
|
+
if (!changesExists)
|
|
70
|
+
warnings.push(`Missing optional changes log: ${rel.staging.styleRefinerChangesJson}`);
|
|
71
|
+
return { ok: true, step: stepId, warnings };
|
|
72
|
+
}
|
|
73
|
+
if (args.step.stage === "judge") {
|
|
74
|
+
requireFile(await pathExists(join(args.rootDir, rel.staging.chapterMd)), rel.staging.chapterMd);
|
|
75
|
+
requireFile(await pathExists(join(args.rootDir, rel.staging.evalJson)), rel.staging.evalJson);
|
|
76
|
+
const evalRaw = await readJsonFile(join(args.rootDir, rel.staging.evalJson));
|
|
77
|
+
if (!isPlainObject(evalRaw))
|
|
78
|
+
throw new NovelCliError(`Invalid eval JSON: ${rel.staging.evalJson} must be an object.`, 2);
|
|
79
|
+
const evalObj = evalRaw;
|
|
80
|
+
const chapter = requireNumberField(evalObj, "chapter", rel.staging.evalJson);
|
|
81
|
+
if (chapter !== args.step.chapter)
|
|
82
|
+
warnings.push(`Eval.chapter is ${chapter}, expected ${args.step.chapter}.`);
|
|
83
|
+
requireNumberField(evalObj, "overall", rel.staging.evalJson);
|
|
84
|
+
requireStringField(evalObj, "recommendation", rel.staging.evalJson);
|
|
85
|
+
const loadedProfile = await loadPlatformProfile(args.rootDir);
|
|
86
|
+
const hookPolicy = loadedProfile?.profile.hook_policy;
|
|
87
|
+
if (hookPolicy?.required) {
|
|
88
|
+
const check = checkHookPolicy({ hookPolicy, evalRaw });
|
|
89
|
+
if (check.status === "invalid_eval") {
|
|
90
|
+
throw new NovelCliError(`Hook policy enabled but eval is missing required hook fields (${rel.staging.evalJson}): ${check.reason}. Re-run QualityJudge with the updated contract.`, 2);
|
|
91
|
+
}
|
|
92
|
+
if (check.status === "fail")
|
|
93
|
+
warnings.push(`Hook policy failing: ${check.reason}`);
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, step: stepId, warnings };
|
|
96
|
+
}
|
|
97
|
+
if (args.step.stage === "hook-fix") {
|
|
98
|
+
const absChapter = join(args.rootDir, rel.staging.chapterMd);
|
|
99
|
+
const exists = await pathExists(absChapter);
|
|
100
|
+
requireFile(exists, rel.staging.chapterMd);
|
|
101
|
+
const content = await readTextFile(absChapter);
|
|
102
|
+
if (content.trim().length === 0)
|
|
103
|
+
throw new NovelCliError(`Empty draft file: ${rel.staging.chapterMd}`, 2);
|
|
104
|
+
return { ok: true, step: stepId, warnings };
|
|
105
|
+
}
|
|
106
|
+
if (args.step.stage === "title-fix") {
|
|
107
|
+
const absChapter = join(args.rootDir, rel.staging.chapterMd);
|
|
108
|
+
requireFile(await pathExists(absChapter), rel.staging.chapterMd);
|
|
109
|
+
const content = await readTextFile(absChapter);
|
|
110
|
+
if (content.trim().length === 0)
|
|
111
|
+
throw new NovelCliError(`Empty draft file: ${rel.staging.chapterMd}`, 2);
|
|
112
|
+
const snapshotRel = titleFixSnapshotRel(args.step.chapter);
|
|
113
|
+
const snapshotAbs = join(args.rootDir, snapshotRel);
|
|
114
|
+
requireFile(await pathExists(snapshotAbs), snapshotRel);
|
|
115
|
+
const before = await readTextFile(snapshotAbs);
|
|
116
|
+
assertTitleFixOnlyChangedTitleLine({ before, after: content, file: rel.staging.chapterMd });
|
|
117
|
+
const title = extractChapterTitleFromMarkdown(content);
|
|
118
|
+
if (!title.has_h1 || !title.title_text) {
|
|
119
|
+
throw new NovelCliError(`Invalid ${rel.staging.chapterMd}: title-fix must produce a non-empty Markdown H1 title line.`, 2);
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, step: stepId, warnings };
|
|
122
|
+
}
|
|
123
|
+
if (args.step.stage === "review") {
|
|
124
|
+
warnings.push("Review step has no machine-validated outputs; resolve issues manually and re-run judge.");
|
|
125
|
+
return { ok: true, step: stepId, warnings };
|
|
126
|
+
}
|
|
127
|
+
if (args.step.stage === "commit") {
|
|
128
|
+
throw new NovelCliError(`Use 'novel commit --chapter ${args.step.chapter}' for commit.`, 2);
|
|
129
|
+
}
|
|
130
|
+
throw new NovelCliError(`Unsupported step: ${stepId}`, 2);
|
|
131
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 用户手册
|
|
2
|
+
|
|
3
|
+
本目录是 **novel-writer-cli** 的用户文档集合。这里的文档同时覆盖两层使用方式:
|
|
4
|
+
|
|
5
|
+
- **CLI 层**:`novel ...`(确定性编排,不调用任何 LLM)
|
|
6
|
+
- **执行器/Skill 层**:在 Claude Code / Codex 中使用 `/novel:*` 等入口命令(底层会调用 CLI,并运行各类 subagent)
|
|
7
|
+
|
|
8
|
+
## 入口索引
|
|
9
|
+
|
|
10
|
+
- 快速上手(Skill 入口):[快速起步指南](quick-start.md)
|
|
11
|
+
- CLI 手册(最重要):[`novel` CLI](novel-cli.md)
|
|
12
|
+
- 常用操作(Skill 入口):[常用操作](ops.md)
|
|
13
|
+
- 规范体系(文件/契约/平台画像):[规范体系](spec-system.md)
|
|
14
|
+
- Guardrails(留存/可读性/命名):[Guardrails](guardrails.md)
|
|
15
|
+
- 交互式门控(NOVEL_ASK):[交互式门控](interactive-gates.md)
|
|
16
|
+
- 故事线(storylines):[故事线](storylines.md)
|
|
17
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Guardrails(留存 / 可读性 / 命名)
|
|
2
|
+
|
|
3
|
+
本项目的 Guardrails 是一组**确定性**检查:由 `platform-profile.json` 驱动,在 `novel next` 的关键节点生成可审计的 JSON 报告。当出现配置为 blocking 的问题时,`novel next` 会返回人工介入步骤(如 `...:review` / `...:title-fix`),阻止流水线推进到下一阶段。
|
|
4
|
+
|
|
5
|
+
> **注意**:Guardrails 的判定发生在 CLI 层(`novel next`/`novel commit`)。当返回 `...:title-fix` / `...:review` 等步骤时,需要执行器按 instruction packet 约定运行对应 subagent,并在 `novel validate`/`novel advance` 后继续流水线。
|
|
6
|
+
|
|
7
|
+
## 配置入口:`platform-profile.json`
|
|
8
|
+
|
|
9
|
+
Guardrails 的配置都在项目根目录的 `platform-profile.json`:
|
|
10
|
+
|
|
11
|
+
- **Retention(留存)**:`retention.title_policy`、`retention.hook_ledger`
|
|
12
|
+
- **Readability(移动端可读性)**:`readability.mobile`
|
|
13
|
+
- **Naming(命名冲突)**:`naming`
|
|
14
|
+
|
|
15
|
+
参考配置(字段说明见下):
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"retention": {
|
|
20
|
+
"title_policy": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"min_chars": 2,
|
|
23
|
+
"max_chars": 30,
|
|
24
|
+
"forbidden_patterns": ["^\\s*$", "^(?:无题|未命名|待定)$"],
|
|
25
|
+
"auto_fix": false
|
|
26
|
+
},
|
|
27
|
+
"hook_ledger": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"fulfillment_window_chapters": 12,
|
|
30
|
+
"diversity_window_chapters": 5,
|
|
31
|
+
"max_same_type_streak": 2,
|
|
32
|
+
"min_distinct_types_in_window": 2,
|
|
33
|
+
"overdue_policy": "warn"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"readability": {
|
|
37
|
+
"mobile": {
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"max_paragraph_chars": 320,
|
|
40
|
+
"max_consecutive_exposition_paragraphs": 3,
|
|
41
|
+
"blocking_severity": "hard_only"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"naming": {
|
|
45
|
+
"enabled": true,
|
|
46
|
+
"near_duplicate_threshold": 0.88,
|
|
47
|
+
"blocking_conflict_types": ["duplicate"],
|
|
48
|
+
"exemptions": {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> 上述示例仅展示 guardrails 相关字段。实际 `platform-profile.json` 还包含 `compliance`(含 `banned_words` 等 schema 必填字段)、`scoring` 等其他顶级段落,详见 [规范体系 — 平台画像](spec-system.md#平台画像与不可变绑定)。
|
|
54
|
+
>
|
|
55
|
+
> Readability lint 脚本路径可通过 `compliance.script_paths.lint_readability` 配置(可选),缺省使用 `scripts/lint-readability.sh`。
|
|
56
|
+
|
|
57
|
+
### 枚举值说明
|
|
58
|
+
|
|
59
|
+
**`overdue_policy`**(hook ledger 超期策略):
|
|
60
|
+
|
|
61
|
+
| 值 | 含义 |
|
|
62
|
+
|------|------|
|
|
63
|
+
| `warn` | 仅警告,不阻断 |
|
|
64
|
+
| `soft` | 需修订,但可被覆盖 |
|
|
65
|
+
| `hard` | 阻断流水线推进 |
|
|
66
|
+
|
|
67
|
+
**`blocking_severity`**(readability 阻断级别):
|
|
68
|
+
|
|
69
|
+
| 值 | 含义 |
|
|
70
|
+
|------|------|
|
|
71
|
+
| `hard_only` | 仅 severity=hard 的 issue 算 blocking |
|
|
72
|
+
| `soft_and_hard` | soft 和 hard 均算 blocking |
|
|
73
|
+
|
|
74
|
+
### 默认/降级行为
|
|
75
|
+
|
|
76
|
+
- 若 `platform-profile.json` **文件不存在**:所有 guardrails 跳过(产生 warning 提示)。
|
|
77
|
+
- 若 `retention`/`readability`/`naming` **字段缺失**或显式为 `null`:对应检查视为"未启用"(`status:"skipped"`)。
|
|
78
|
+
- 若 `*.enabled` 为 `false`:同样视为"未启用"。
|
|
79
|
+
- Readability lint 脚本缺失或执行失败时进入 `mode:"fallback"`,**只产生 warn 级问题**。这意味着 fallback 模式下 `blocking_severity` 设置实际无效——只有自定义 lint 脚本(`mode:"script"`)才能产出 `soft`/`hard` 级别的 issue。
|
|
80
|
+
|
|
81
|
+
> **注意**:`retention` 和 `readability` 若为非 null 对象,则其子字段(如 `title_policy` + `hook_ledger`)均为 schema required。不可只提供其中一个。
|
|
82
|
+
|
|
83
|
+
## 日志解读
|
|
84
|
+
|
|
85
|
+
> `logs/` 的完整目录清单(SSOT)见 [09-logs-index.md](../dr-workflow/novel-writer-tool/final/prd/09-logs-index.md)。
|
|
86
|
+
|
|
87
|
+
### Retention(留存)— `logs/retention/*`
|
|
88
|
+
|
|
89
|
+
Retention 主要包含两类输出:
|
|
90
|
+
|
|
91
|
+
1) **Hook ledger(章末钩子台账 + 窗口/多样性报告)**
|
|
92
|
+
|
|
93
|
+
- 台账文件:`hook-ledger.json`(项目根目录)
|
|
94
|
+
- 最新报告:`logs/retention/latest.json`
|
|
95
|
+
- 历史报告:`logs/retention/retention-report-vol-{V:02d}-ch{start:03d}-ch{end:03d}.json`
|
|
96
|
+
|
|
97
|
+
重点字段(`logs/retention/latest.json`):
|
|
98
|
+
|
|
99
|
+
- `has_blocking_issues`:为 `true` 时阻断流水线推进(取决于 `overdue_policy` 等策略)
|
|
100
|
+
- `issues[]`:本窗口内的具体问题(`severity` 为 `warn|soft|hard`)
|
|
101
|
+
- `debt.open[] / debt.lapsed[]`:仍未兑现/已超期的承诺条目摘要
|
|
102
|
+
- `diversity.*`:最近窗口内 hook 类型分布(连击 streak、窗口内 distinct types 等)
|
|
103
|
+
|
|
104
|
+
2) **Title policy(章节标题策略报告)**
|
|
105
|
+
|
|
106
|
+
- 最新报告:`logs/retention/title-policy/latest.json`
|
|
107
|
+
- 每章历史:`logs/retention/title-policy/title-policy-chapter-{C:03d}.json`
|
|
108
|
+
|
|
109
|
+
重点字段:
|
|
110
|
+
|
|
111
|
+
- `title.has_h1`:首个非空行是否为 `# H1` 标题
|
|
112
|
+
- `policy`:生效的标题策略(来自 `retention.title_policy`;未启用时为 `null`)
|
|
113
|
+
- `status`:`pass|warn|violation|skipped`
|
|
114
|
+
- `has_hard_violations`:存在 hard 违规则通常会触发 `novel next` 的 `...:title-fix`/`...:review`
|
|
115
|
+
|
|
116
|
+
> **注意**:标题检查还会使用 `compliance.banned_words`(M6 基线字段)做标题禁词检测。若标题被拒但 `forbidden_patterns` 无匹配,请检查 `banned_words` 配置。
|
|
117
|
+
|
|
118
|
+
### Readability(移动端可读性 lint)— `logs/readability/*`
|
|
119
|
+
|
|
120
|
+
- 最新报告:`logs/readability/latest.json`
|
|
121
|
+
- 每章历史:`logs/readability/readability-report-chapter-{C:03d}.json`
|
|
122
|
+
|
|
123
|
+
重点字段:
|
|
124
|
+
|
|
125
|
+
- `mode`:`script` 或 `fallback`
|
|
126
|
+
- `script.rel_path` / `script_error`:脚本路径与降级原因(若脚本缺失/失败)
|
|
127
|
+
- `issues[]`:问题列表,fallback 模式下可检测的 issue 类型包括:
|
|
128
|
+
- `overlong_paragraph`:段落超过 `max_paragraph_chars`
|
|
129
|
+
- `exposition_run_too_long`:连续说明段超过阈值
|
|
130
|
+
- `dialogue_dense_paragraph`:单段内对话过密
|
|
131
|
+
- `mixed_quote_styles`、`mixed_ellipsis_styles`、`mixed_comma_styles`、`mixed_period_styles`、`mixed_question_mark_styles`、`mixed_exclamation_styles`:各类标点风格混用
|
|
132
|
+
- `has_blocking_issues`:为 `true` 时阻断流水线推进(由 `blocking_severity` 决定"soft 是否算 blocking")
|
|
133
|
+
|
|
134
|
+
### Naming(命名冲突 lint)— `logs/naming/*`
|
|
135
|
+
|
|
136
|
+
- 最新报告:`logs/naming/latest.json`
|
|
137
|
+
- 每章历史:`logs/naming/naming-report-chapter-{C:03d}.json`
|
|
138
|
+
|
|
139
|
+
重点字段:
|
|
140
|
+
|
|
141
|
+
- `registry.total_characters / total_names`:当前角色档案规模(来自 `characters/active/*.json` + aliases)
|
|
142
|
+
- `issues[]`:冲突列表,`conflict_type` 可选值:
|
|
143
|
+
- `duplicate`:重名(同名不同人)
|
|
144
|
+
- `near_duplicate`:近似名(相似度 ≥ `near_duplicate_threshold`)
|
|
145
|
+
- `alias_collision`:别名与他人 canonical/alias 冲突
|
|
146
|
+
- `unknown_entity_confusion`:NER 检测到的未知实体与已有角色名相似(始终为 warn,依赖 NER 预计算)
|
|
147
|
+
- `has_blocking_issues`:为 `true` 时阻断流水线推进(由 `naming.blocking_conflict_types` 决定哪些冲突属于 hard)
|
|
148
|
+
|
|
149
|
+
## 常见修复速查
|
|
150
|
+
|
|
151
|
+
### Title policy
|
|
152
|
+
|
|
153
|
+
- 确保首个非空行为 `# 标题`(H1)
|
|
154
|
+
- 调整标题长度、避免命中 `forbidden_patterns`(同时注意 `compliance.banned_words` 也会影响标题检查)
|
|
155
|
+
- 若 `auto_fix` 设为 `true`:`novel next` 可能先返回 `chapter:NNN:title-fix`(只允许改标题行;最多一次)
|
|
156
|
+
|
|
157
|
+
### Readability
|
|
158
|
+
|
|
159
|
+
- 将超长段落拆分为更短段落(或增加对话/动作打断)
|
|
160
|
+
- 避免同章内混用标点风格(例如 `...` vs `……`、`,` vs `,`、`"` vs `""`)
|
|
161
|
+
- 若你有自定义 lint 脚本:配置 `compliance.script_paths.lint_readability` 指向它;脚本失败会自动降级到 fallback(不阻断)
|
|
162
|
+
|
|
163
|
+
### Naming
|
|
164
|
+
|
|
165
|
+
- 在 `characters/active/*.json` 里重命名冲突角色,或为角色添加/整理 `aliases`
|
|
166
|
+
- 使用 `naming.exemptions` 做白名单(当前仅支持 `ignore_names` 和 `allow_pairs` 两个字段,其余字段会被忽略):
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"naming": {
|
|
171
|
+
"exemptions": {
|
|
172
|
+
"ignore_names": ["老王"],
|
|
173
|
+
"allow_pairs": [["张三", "张山"]]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> `ignore_names` 中的名字会经过规范化(trim + 去空白 + toLowerCase)再匹配。`allow_pairs` 顺序无关——`["张三", "张山"]` 与 `["张山", "张三"]` 等价。
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# 交互式门控(`NOVEL_ASK`)
|
|
2
|
+
|
|
3
|
+
`NOVEL_ASK` 是一种**工具无关**的问卷/提问中间表示(IR),用于把“必须由用户确认/选择的信息”从自然语言模板中抽离出来,变成:
|
|
4
|
+
|
|
5
|
+
- 可审计:落盘为标准化 AnswerSpec JSON
|
|
6
|
+
- 可恢复:重跑执行器时若答案已存在且校验通过,跳过重复提问
|
|
7
|
+
- 可跨执行器:Claude Code / Codex 以不同交互能力呈现,但产出相同 AnswerSpec
|
|
8
|
+
|
|
9
|
+
## 端到端最短示例(InstructionPacket → AnswerSpec → main step)
|
|
10
|
+
|
|
11
|
+
### 1) InstructionPacket 里声明 gate
|
|
12
|
+
|
|
13
|
+
当 instruction packet 同时包含 `novel_ask` + `answer_path` 时,表示该 step 在执行 agent 之前有一个前置 gate:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"version": 1,
|
|
18
|
+
"step": "chapter:048:draft",
|
|
19
|
+
"agent": { "kind": "subagent", "name": "chapter-writer" },
|
|
20
|
+
"manifest": { "mode": "paths", "inline": {}, "paths": {} },
|
|
21
|
+
"novel_ask": {
|
|
22
|
+
"version": 1,
|
|
23
|
+
"topic": "platform binding",
|
|
24
|
+
"questions": [
|
|
25
|
+
{
|
|
26
|
+
"id": "platform",
|
|
27
|
+
"header": "Platform",
|
|
28
|
+
"question": "你准备发布到哪个平台?",
|
|
29
|
+
"kind": "single_choice",
|
|
30
|
+
"required": true,
|
|
31
|
+
"options": [
|
|
32
|
+
{ "label": "qidian", "description": "起点" },
|
|
33
|
+
{ "label": "jjwxc", "description": "晋江" },
|
|
34
|
+
{ "label": "web", "description": "自建站/博客" }
|
|
35
|
+
],
|
|
36
|
+
"default": "qidian"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"answer_path": "staging/novel-ask/chapter-048-draft.answers.json",
|
|
41
|
+
"expected_outputs": [
|
|
42
|
+
{
|
|
43
|
+
"path": "staging/novel-ask/chapter-048-draft.answers.json",
|
|
44
|
+
"required": true,
|
|
45
|
+
"note": "AnswerSpec JSON record for the NOVEL_ASK gate (written before main step execution)."
|
|
46
|
+
},
|
|
47
|
+
{ "path": "staging/chapters/chapter-048.md", "required": true }
|
|
48
|
+
],
|
|
49
|
+
"next_actions": [
|
|
50
|
+
{ "kind": "command", "command": "novel validate chapter:048:draft" },
|
|
51
|
+
{ "kind": "command", "command": "novel advance chapter:048:draft" }
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2) 执行器完成提问后,写入 AnswerSpec 到 answer_path
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"version": 1,
|
|
61
|
+
"topic": "platform binding",
|
|
62
|
+
"answers": {
|
|
63
|
+
"platform": "qidian"
|
|
64
|
+
},
|
|
65
|
+
"answered_at": "2026-02-27T10:12:34.000Z",
|
|
66
|
+
"answered_by": "claude_code"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> `answered_by` 是审计字段:建议在 Claude Code 适配器写入 `claude_code`,在 Codex 适配器写入 `codex`(也可以用 `human` 等)。
|
|
71
|
+
|
|
72
|
+
### 3) gate 通过后再执行主 agent
|
|
73
|
+
|
|
74
|
+
- 若 `answer_path` 已存在且校验通过:直接执行主 agent(可恢复语义)
|
|
75
|
+
- 若缺失:执行器必须先完成提问并写入 AnswerSpec
|
|
76
|
+
- 若存在但无效:该 step 视为 **blocked**,不得继续执行主 agent(需修复/删除 AnswerSpec 后重试)
|
|
77
|
+
|
|
78
|
+
## `answer_path` 约定与校验行为
|
|
79
|
+
|
|
80
|
+
### `answer_path` 约定(推荐)
|
|
81
|
+
|
|
82
|
+
`answer_path` 由编排器提供,且必须是 **project-relative** 路径。
|
|
83
|
+
|
|
84
|
+
推荐放在 `staging/novel-ask/` 下,并在文件名中包含 step 信息,便于审计与回放,例如:
|
|
85
|
+
|
|
86
|
+
- `staging/novel-ask/chapter-048-draft.answers.json`
|
|
87
|
+
- `staging/novel-ask/novel-init.answers.json`
|
|
88
|
+
|
|
89
|
+
> 需要重做 gate 时:删除该 AnswerSpec 文件,重新运行执行器即可触发再次提问。
|
|
90
|
+
|
|
91
|
+
### 校验行为(摘要)
|
|
92
|
+
|
|
93
|
+
执行器在进入主 agent 之前必须确保:
|
|
94
|
+
|
|
95
|
+
- `answer_path` 必须是非空的项目相对路径:不允许绝对路径、`..` 段,且解析后不得逃出项目根目录
|
|
96
|
+
- AnswerSpec 是合法 JSON object
|
|
97
|
+
- `version/topic` 与 QuestionSpec 一致
|
|
98
|
+
- `answers` 的 key 必须是 `snake_case`,且必须能在 QuestionSpec 里找到同名 question id(不允许多写未知字段)
|
|
99
|
+
- required=true 的问题必须回答
|
|
100
|
+
- `allow_other`(可选,默认 false):设为 `true` 时允许 choice 问题回答不在 option labels 内的自定义字符串
|
|
101
|
+
- choice 问题:当 `allow_other` 不是 `true` 时,答案必须落在 option labels 内
|
|
102
|
+
- multi_choice:
|
|
103
|
+
- required=true:至少选 1 个
|
|
104
|
+
- required=false:若 0 选择,**必须完全不写入该 question id**(写空数组会被校验拒绝)
|
|
105
|
+
- 不允许重复选项(数组内 duplicate 会被校验拒绝)
|
|
106
|
+
|
|
107
|
+
## Claude Code vs Codex:交互差异与降级策略
|
|
108
|
+
|
|
109
|
+
目标是:**交互体验可以不同,但最终落盘的 AnswerSpec 必须一致**。
|
|
110
|
+
|
|
111
|
+
### Claude Code(`AskUserQuestion`)
|
|
112
|
+
|
|
113
|
+
- 优先编译为一个或多个 `AskUserQuestion`
|
|
114
|
+
- `options[]` 每次有数量上限(2-4),选项过多时需要分页/循环
|
|
115
|
+
- `free_text` 可能需要依赖 UI 的 “Other” 输入;若不可用可提示用户用普通消息输入再由适配器写入 AnswerSpec
|
|
116
|
+
|
|
117
|
+
详见:`skills/cli-step/SKILL.md`
|
|
118
|
+
|
|
119
|
+
### Codex(Plan Mode `request_user_input` + JSON fallback)
|
|
120
|
+
|
|
121
|
+
- Plan Mode 下优先用 `request_user_input`(每题选项上限更低,且为互斥选项)
|
|
122
|
+
- 若工具不可用或无法可靠表达语义(例如 `free_text`):退化为严格 JSON 回答格式,由适配器构造 + 校验 AnswerSpec 后落盘
|
|
123
|
+
|
|
124
|
+
详见:`.codex/skills/novel-cli-step/SKILL.md`
|