@cloud411716/fancy-webnovel 2.2.0 → 3.0.3
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 +65 -0
- package/client.js +201 -15
- package/cordis.patch.yml +5 -1
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +12 -7
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +11 -11
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +31 -31
- package/lib/infra.js +29 -4
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +140 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +31 -7
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/index.js +1128 -77
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +355 -107
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/summary-progress.js +71 -7
- package/plugins/fancy-analyze/templates.js +437 -139
- package/plugins/fancy-bootstrap/index.js +276 -131
- package/plugins/fancy-bootstrap/scripts/init.js +59 -25
- package/plugins/fancy-bootstrap/templates.js +35 -18
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-driver.js +29 -6
- package/plugins/fancy-import/index.js +13 -5
- package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
- package/plugins/fancy-import/templates.js +17 -8
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +145 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +410 -21
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +169 -22
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +545 -146
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/flesh-output.js +19 -6
- package/plugins/fancy-story-builder/index.js +4105 -396
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
- package/plugins/fancy-story-builder/purpose-template.md +42 -66
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +15 -6
- package/plugins/fancy-story-builder/references/index.md +97 -6
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +201 -293
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
- package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
- package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
- package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +267 -44
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +121 -19
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +129 -21
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +76 -17
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +56 -8
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +63 -13
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +34 -6
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +81 -18
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +202 -42
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +160 -27
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +217 -41
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +215 -41
- package/plugins/fancy-story-builder/templates.js +272 -74
- package/plugins/fancy-story-builder/wiki-references.js +429 -93
- package/plugins/fancy-story-builder/writing-driver.js +336 -9
- package/plugins/fancy-topic/index.js +414 -61
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +106 -63
- package/plugins/fancy-topic/templates.js +225 -122
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/lib/safe-rm.js +0 -94
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* brief-validate.js —— 校验“每章一份细纲”是否齐全、引用是否成立
|
|
4
|
+
*
|
|
5
|
+
* 这一层不是替代 rhythm-validate.js:聚合 JSON 仍由 rhythm-validate 判结构。
|
|
6
|
+
* 这里只判**写作阶段真正会读的每章细纲**:
|
|
7
|
+
* - 1..T 每章是否都有文件;
|
|
8
|
+
* - 必须字段是否存在;
|
|
9
|
+
* - cast/地点/道具/冲突/伏笔/高潮/爽点/信息差引用是否都能在上游找到;
|
|
10
|
+
* - must_write / must_not_write 是否为空。
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const SRC = ['设定', 'raw', 'sources'];
|
|
16
|
+
|
|
17
|
+
function readJson(file, fallback = null) {
|
|
18
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf-8')); } catch { return fallback; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function idsOf(list) {
|
|
22
|
+
return new Set((list || []).filter((x) => x && x.id).map((x) => x.id));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateChapterBriefs(projectRoot) {
|
|
26
|
+
const skeletonDir = path.join(projectRoot, ...SRC, '骨架');
|
|
27
|
+
const fleshDir = path.join(projectRoot, ...SRC, '血肉');
|
|
28
|
+
const rhythmDir = path.join(projectRoot, ...SRC, '节奏');
|
|
29
|
+
const briefDir = path.join(rhythmDir, '细纲');
|
|
30
|
+
const framework = readJson(path.join(skeletonDir, '框架.json'), {}) || {};
|
|
31
|
+
const entities = readJson(path.join(skeletonDir, '实体.json'), {}) || {};
|
|
32
|
+
const flesh = readJson(path.join(fleshDir, '冲突.json'), {}) || {};
|
|
33
|
+
const plan = readJson(path.join(rhythmDir, '章节规划.json'), {}) || {};
|
|
34
|
+
const T = Number(framework?.capacity?.target_chapters) || (plan.chapters || []).length || 0;
|
|
35
|
+
const errors = [];
|
|
36
|
+
const warnings = [];
|
|
37
|
+
|
|
38
|
+
const ids = {
|
|
39
|
+
characters: idsOf(entities.characters),
|
|
40
|
+
locations: idsOf(entities.locations),
|
|
41
|
+
items: idsOf(entities.items),
|
|
42
|
+
conflicts: idsOf(entities.conflicts),
|
|
43
|
+
foreshadows: idsOf(entities.foreshadows),
|
|
44
|
+
climaxes: idsOf(entities.climaxes),
|
|
45
|
+
pleasures: idsOf(entities.pleasures),
|
|
46
|
+
endings: idsOf(entities.endings),
|
|
47
|
+
};
|
|
48
|
+
const infoGapIds = new Set();
|
|
49
|
+
const gapDir = path.join(projectRoot, ...SRC, '档案', '信息差');
|
|
50
|
+
try {
|
|
51
|
+
for (const name of fs.readdirSync(gapDir)) {
|
|
52
|
+
if (name.endsWith('.json')) infoGapIds.add(name.slice(0, -5));
|
|
53
|
+
}
|
|
54
|
+
} catch { /* ignore */ }
|
|
55
|
+
|
|
56
|
+
const files = new Map();
|
|
57
|
+
try {
|
|
58
|
+
for (const name of fs.readdirSync(briefDir)) {
|
|
59
|
+
const match = /^第(\d+)章\.json$/.exec(name);
|
|
60
|
+
if (!match) continue;
|
|
61
|
+
files.set(Number(match[1]), path.join(briefDir, name));
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
errors.push('细纲目录不存在:设定/raw/sources/节奏/细纲');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (T > 0) {
|
|
68
|
+
for (let ch = 1; ch <= T; ch += 1) {
|
|
69
|
+
if (!files.has(ch)) errors.push('缺第 ' + ch + ' 章细纲文件');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const ch of files.keys()) {
|
|
73
|
+
if (T > 0 && (ch < 1 || ch > T)) warnings.push('细纲章号越界:第 ' + ch + ' 章');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const [ch, file] of [...files.entries()].sort((a, b) => a[0] - b[0])) {
|
|
77
|
+
const brief = readJson(file);
|
|
78
|
+
if (!brief) {
|
|
79
|
+
errors.push('第 ' + ch + ' 章细纲不是合法 JSON');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const key of ['ch', 'title', 'volume', 'scene_focus', 'pov', 'cast', 'must_write', 'must_not_write']) {
|
|
83
|
+
if (brief[key] === undefined || brief[key] === null || brief[key] === '') {
|
|
84
|
+
errors.push('第 ' + ch + ' 章缺字段:' + key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(brief.cast) || brief.cast.length === 0) {
|
|
88
|
+
warnings.push('第 ' + ch + ' 章没有 cast(可能只剩主角或上游时间线过窄)');
|
|
89
|
+
}
|
|
90
|
+
for (const c of (brief.cast || [])) {
|
|
91
|
+
if (c && c.id && !ids.characters.has(c.id)) errors.push('第 ' + ch + ' 章 cast 引用了不存在的人物:' + c.id);
|
|
92
|
+
}
|
|
93
|
+
if (brief.location && brief.location.id && !ids.locations.has(brief.location.id)) {
|
|
94
|
+
errors.push('第 ' + ch + ' 章地点引用了不存在的地点:' + brief.location.id);
|
|
95
|
+
}
|
|
96
|
+
for (const item of (brief.items || [])) {
|
|
97
|
+
if (item && item.id && !ids.items.has(item.id)) errors.push('第 ' + ch + ' 章道具引用了不存在的道具:' + item.id);
|
|
98
|
+
}
|
|
99
|
+
for (const conflict of (brief.conflicts || [])) {
|
|
100
|
+
if (conflict && conflict.id && !ids.conflicts.has(conflict.id)) {
|
|
101
|
+
errors.push('第 ' + ch + ' 章冲突引用了不存在的冲突:' + conflict.id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const rel of (brief.relationship_changes || [])) {
|
|
105
|
+
if (rel && rel.from && !ids.characters.has(rel.from)) {
|
|
106
|
+
errors.push('第 ' + ch + ' 章关系变化引用了不存在的人物:' + rel.from);
|
|
107
|
+
}
|
|
108
|
+
if (rel && rel.to && !ids.characters.has(rel.to)) {
|
|
109
|
+
errors.push('第 ' + ch + ' 章关系变化引用了不存在的人物:' + rel.to);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const f of (brief.foreshadow_actions || [])) {
|
|
113
|
+
if (f && f.id && !ids.foreshadows.has(f.id)) errors.push('第 ' + ch + ' 章伏笔引用了不存在的伏笔:' + f.id);
|
|
114
|
+
}
|
|
115
|
+
if (brief.climax && brief.climax.id && !ids.climaxes.has(brief.climax.id)) {
|
|
116
|
+
errors.push('第 ' + ch + ' 章高潮引用了不存在的高潮:' + brief.climax.id);
|
|
117
|
+
}
|
|
118
|
+
if (brief.pleasure && brief.pleasure.id && !ids.pleasures.has(brief.pleasure.id)) {
|
|
119
|
+
errors.push('第 ' + ch + ' 章爽点引用了不存在的爽点:' + brief.pleasure.id);
|
|
120
|
+
}
|
|
121
|
+
if (brief.ending && brief.ending.id && !ids.endings.has(brief.ending.id)) {
|
|
122
|
+
errors.push('第 ' + ch + ' 章结局引用了不存在的结局:' + brief.ending.id);
|
|
123
|
+
}
|
|
124
|
+
for (const gap of (brief.information_gaps || [])) {
|
|
125
|
+
if (gap && gap.id && !infoGapIds.has(gap.id)) {
|
|
126
|
+
errors.push('第 ' + ch + ' 章信息差引用了不存在的档案:' + gap.id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(brief.must_write) && brief.must_write.length === 0) {
|
|
130
|
+
warnings.push('第 ' + ch + ' 章 must_write 为空');
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(brief.must_not_write) && brief.must_not_write.length === 0) {
|
|
133
|
+
warnings.push('第 ' + ch + ' 章 must_not_write 为空');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
status: errors.length ? 1 : 0,
|
|
139
|
+
stage: '章节细纲',
|
|
140
|
+
total: T,
|
|
141
|
+
files: files.size,
|
|
142
|
+
errors: errors.slice(0, 200),
|
|
143
|
+
warnings: warnings.slice(0, 200),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function main() {
|
|
148
|
+
const args = process.argv.slice(2);
|
|
149
|
+
const rootArg = args.find((a) => a.startsWith('--project-root='));
|
|
150
|
+
const dirArg = args.find((a) => a.startsWith('--stage-dir='));
|
|
151
|
+
let projectRoot = rootArg ? rootArg.slice('--project-root='.length) : '';
|
|
152
|
+
if (!projectRoot && dirArg) {
|
|
153
|
+
projectRoot = path.resolve(dirArg.slice('--stage-dir='.length), '..', '..', '..', '..');
|
|
154
|
+
}
|
|
155
|
+
if (!projectRoot) {
|
|
156
|
+
process.stderr.write('需要 --project-root=<项目根> 或 --stage-dir=<节奏目录>\n');
|
|
157
|
+
process.exit(2);
|
|
158
|
+
}
|
|
159
|
+
const result = validateChapterBriefs(projectRoot);
|
|
160
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
161
|
+
process.exitCode = result.status;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (process.argv[1] && process.argv[1].endsWith('brief-validate.js')) main();
|
|
@@ -69,7 +69,8 @@ const SHORT_VALUE_EXEMPT_FIELDS = new Set(['exception', 'limit']);
|
|
|
69
69
|
// 这条错就永远修不好、还挂在报告里(tt5 实测 2026-10-25:
|
|
70
70
|
// "genre 字段内容过短(有效字符 2,要求至少 3):'历史'")。
|
|
71
71
|
const LENGTH_CHECK_EXEMPT_FIELDS = new Set([
|
|
72
|
-
'name', 'target_name', 'id', 'type', 'stage', 'gender', 'age_group',
|
|
72
|
+
'name', 'target_name', 'id', 'type', 'stage', 'timing', 'gender', 'age_group',
|
|
73
|
+
'kind', 'relation_label', 'state', 'traits', 'abilities', 'limits',
|
|
73
74
|
'catchphrase', 'behavior_repeat_points',
|
|
74
75
|
'position_level', 'theme_short', 'stance', 'core_emotion', 'info_name',
|
|
75
76
|
'focus_points', 'key_event_descriptions', 'memorable_scenes', 'epilogue_hints',
|
|
@@ -83,6 +84,8 @@ const LENGTH_CHECK_EXEMPT_FIELDS = new Set([
|
|
|
83
84
|
// 这些字段承载的是叙述性内容,几个字表达不了一个完整信息。
|
|
84
85
|
const LONG_PROSE_FIELDS = new Set([
|
|
85
86
|
'description', 'content', 'core_summary', 'core_conflict', 'conflict_summary',
|
|
87
|
+
'positioning', 'reader_value', 'core_setting', 'payoff_model', 'emotion_curve',
|
|
88
|
+
'opening_hook', 'pacing_plan', 'character_design', 'differentiation', 'risk_validation',
|
|
86
89
|
'conflict_solution', 'core_goal', 'core_drive', 'influence_scope',
|
|
87
90
|
'surface_contradiction', 'deep_contradiction', 'ultimate_contradiction',
|
|
88
91
|
'internal_structure', 'suppression_method', 'resource_seizure_path',
|
|
@@ -136,6 +136,98 @@ function fleshSelfRelationCheck(data, fileType, errors) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// ============================================================
|
|
140
|
+
// 个性化特征与关系阶段检查
|
|
141
|
+
// ============================================================
|
|
142
|
+
// `traits` 是角色的"独有特征",不是性格形容词堆砌。
|
|
143
|
+
// 模板只能要求字段存在,数量和重复要靠这里兜住。
|
|
144
|
+
function traitsAndChangesCheck(data, fileType, errors, warnings) {
|
|
145
|
+
const checkTraits = (item, path, label, min, max) => {
|
|
146
|
+
if (!item || item.traits === undefined) {
|
|
147
|
+
if (label === '角色') {
|
|
148
|
+
errors.push({
|
|
149
|
+
code: 'FLESH_TRAITS_MISSING',
|
|
150
|
+
path: path + '.traits',
|
|
151
|
+
message: '角色缺少 traits(独有特征/习惯)',
|
|
152
|
+
hint: '写 3-5 条能直接写进场景的具体特征,例如"紧张时反复摸腕环"'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!Array.isArray(item.traits)) return; // TYPE 已报
|
|
158
|
+
const values = item.traits.map((x) => (typeof x === 'string' ? x.trim() : '')).filter(Boolean);
|
|
159
|
+
if (values.length < min || values.length > max) {
|
|
160
|
+
errors.push({
|
|
161
|
+
code: 'FLESH_TRAITS_COUNT',
|
|
162
|
+
path: path + '.traits',
|
|
163
|
+
message: label + '的 traits 应为 ' + min + '-' + max + ' 条,实际 ' + values.length + ' 条',
|
|
164
|
+
hint: '特征要少而准:每条都能在场景里演出来,不要用"善良""勇敢"这类评价词凑数'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (new Set(values).size !== values.length) {
|
|
168
|
+
errors.push({
|
|
169
|
+
code: 'FLESH_TRAITS_DUPLICATE',
|
|
170
|
+
path: path + '.traits',
|
|
171
|
+
message: label + '的 traits 有重复条目',
|
|
172
|
+
hint: '删掉重复项,换成不同的习惯/感官锚点'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const checkChanges = (rel, path, label) => {
|
|
178
|
+
if (!rel || !Array.isArray(rel.changes) || rel.changes.length === 0) return;
|
|
179
|
+
for (const [k, ch] of rel.changes.entries()) {
|
|
180
|
+
if (!ch || typeof ch !== 'object') continue;
|
|
181
|
+
for (const key of ['stage', 'from', 'to', 'trigger', 'description']) {
|
|
182
|
+
const v = ch[key];
|
|
183
|
+
if (typeof v !== 'string' || !v.trim()) {
|
|
184
|
+
errors.push({
|
|
185
|
+
code: 'REL_CHANGE_INCOMPLETE',
|
|
186
|
+
path: path + '.changes[' + k + '].' + key,
|
|
187
|
+
message: label + '的关系变化缺少 ' + key + ',无法判断这一阶段到底发生了什么',
|
|
188
|
+
hint: '每条变化至少写清:阶段名、变化前状态、变化后状态、触发事件、变化描述'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const key of ['stage', 'from', 'to', 'trigger', 'description', 'timing']) {
|
|
193
|
+
if (typeof ch[key] === 'string' && /第\s*(?:\d+|[一二三四五六七八九十百千万]+)\s*章/.test(ch[key])) {
|
|
194
|
+
errors.push({
|
|
195
|
+
code: 'FLESH_FREETEXT_CHAPTER_REF',
|
|
196
|
+
path: path + '.changes[' + k + '].' + key,
|
|
197
|
+
message: '关系变化里出现"第 N 章"引用;章节号属于节奏阶段',
|
|
198
|
+
hint: '这里只写"前期/转折/后期/某卷"或事件本身,章节落点由节奏阶段 relationship_timeline 决定'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (fileType === '角色') {
|
|
206
|
+
for (const [i, c] of (data.characters || []).entries()) {
|
|
207
|
+
if (!c) continue;
|
|
208
|
+
checkTraits(c, 'characters[' + i + ']', '角色', 3, 5);
|
|
209
|
+
for (const [j, rel] of (c.relationships || []).entries()) {
|
|
210
|
+
if (!rel) continue;
|
|
211
|
+
if (rel.relationship_type === '其他' && !String(rel.relation_label || '').trim()) {
|
|
212
|
+
errors.push({
|
|
213
|
+
code: 'REL_LABEL_REQUIRED',
|
|
214
|
+
path: 'characters[' + i + '].relationships[' + j + '].relation_label',
|
|
215
|
+
message: 'relationship_type="其他" 时必须填 relation_label,写清具体关系(如 附身/寄生/契约/共生)',
|
|
216
|
+
hint: 'relation_label 是词表外关系的唯一语义来源'
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
checkChanges(rel, 'characters[' + i + '].relationships[' + j + ']', c.name || c.id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (fileType === '其他实体') {
|
|
224
|
+
for (const [i, o] of (data.other_entities || []).entries()) {
|
|
225
|
+
if (!o) continue;
|
|
226
|
+
checkTraits(o, 'other_entities[' + i + ']', '其他实体', 2, 6);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
139
231
|
// ============================================================
|
|
140
232
|
// 跨阶段:血肉关系 ↔ 骨架关系一致性
|
|
141
233
|
// ============================================================
|
|
@@ -169,11 +261,14 @@ function skeletonRelationConsistencyCheck(fleshData, sk, errors, warnings, opts)
|
|
|
169
261
|
}
|
|
170
262
|
const resolve = (v) => (v && idByKey.has(v) ? idByKey.get(v) : null);
|
|
171
263
|
|
|
172
|
-
// 骨架关系:'A→B' →
|
|
264
|
+
// 骨架关系:'A→B' → { kind, label }
|
|
173
265
|
const skRel = new Map();
|
|
174
266
|
for (const r of (sk.relations.character_relations || [])) {
|
|
175
267
|
if (!r || !r.from || !r.to) continue;
|
|
176
|
-
skRel.set(r.from + '→' + r.to,
|
|
268
|
+
skRel.set(r.from + '→' + r.to, {
|
|
269
|
+
kind: r.relation_kind,
|
|
270
|
+
label: typeof r.relation_label === 'string' ? r.relation_label.trim() : ''
|
|
271
|
+
});
|
|
177
272
|
}
|
|
178
273
|
|
|
179
274
|
// 血肉关系:有序对 → { kind, path },并记录血肉已覆盖的无序对
|
|
@@ -194,19 +289,36 @@ function skeletonRelationConsistencyCheck(fleshData, sk, errors, warnings, opts)
|
|
|
194
289
|
|
|
195
290
|
const fwd = skRel.get(key);
|
|
196
291
|
if (fwd !== undefined) {
|
|
197
|
-
if (r.relationship_type !== fwd) {
|
|
292
|
+
if (r.relationship_type !== fwd.kind) {
|
|
198
293
|
errors.push({
|
|
199
294
|
code: 'REL_TYPE_MISMATCH',
|
|
200
295
|
path: p + '.relationship_type',
|
|
201
|
-
message: '骨架 ' + srcId + '→' + tgtId + ' 的关系类型是 "' + fwd + '",血肉写的是 "' + r.relationship_type + '"',
|
|
296
|
+
message: '骨架 ' + srcId + '→' + tgtId + ' 的关系类型是 "' + fwd.kind + '",血肉写的是 "' + r.relationship_type + '"',
|
|
202
297
|
hint: '血肉阶段的关系类型必须与骨架一致;若语义确实变了,请在 description 里写清"关系在剧情中如何演变",而不是换掉类型'
|
|
203
298
|
});
|
|
204
299
|
}
|
|
300
|
+
const fleshLabel = typeof r.relation_label === 'string' ? r.relation_label.trim() : '';
|
|
301
|
+
if (fwd.label && fleshLabel && fwd.label !== fleshLabel) {
|
|
302
|
+
errors.push({
|
|
303
|
+
code: 'REL_LABEL_MISMATCH',
|
|
304
|
+
path: p + '.relation_label',
|
|
305
|
+
message: '骨架 ' + srcId + '→' + tgtId + ' 的关系标签是 "' + fwd.label + '",血肉写的是 "' + fleshLabel + '"',
|
|
306
|
+
hint: '关系标签必须与骨架完全一致'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (fwd.kind === '其他' && !fleshLabel) {
|
|
310
|
+
errors.push({
|
|
311
|
+
code: 'REL_LABEL_REQUIRED',
|
|
312
|
+
path: p + '.relation_label',
|
|
313
|
+
message: '骨架把 ' + srcId + '→' + tgtId + ' 标为"其他",血肉必须写 relation_label',
|
|
314
|
+
hint: '照抄骨架的 relation_label,或补齐具体关系名'
|
|
315
|
+
});
|
|
316
|
+
}
|
|
205
317
|
continue;
|
|
206
318
|
}
|
|
207
319
|
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
320
|
+
const revRow = skRel.get(tgtId + '→' + srcId);
|
|
321
|
+
if (revRow === undefined) {
|
|
210
322
|
errors.push({
|
|
211
323
|
code: 'REL_NOT_IN_SKELETON',
|
|
212
324
|
path: p,
|
|
@@ -216,6 +328,7 @@ function skeletonRelationConsistencyCheck(fleshData, sk, errors, warnings, opts)
|
|
|
216
328
|
continue;
|
|
217
329
|
}
|
|
218
330
|
|
|
331
|
+
const rev = revRow.kind;
|
|
219
332
|
const meta = RELATION_KIND_META[rev];
|
|
220
333
|
if (meta && meta.direction === 'symmetric') {
|
|
221
334
|
if (r.relationship_type !== rev) {
|
|
@@ -246,7 +359,7 @@ function skeletonRelationConsistencyCheck(fleshData, sk, errors, warnings, opts)
|
|
|
246
359
|
warnings.push({
|
|
247
360
|
code: 'REL_UNDECLARED',
|
|
248
361
|
path: '骨架关系 ' + key,
|
|
249
|
-
message: '骨架 ' + key + '("' + kind + '")在血肉阶段没有对应的 relationships 条目',
|
|
362
|
+
message: '骨架 ' + key + '("' + kind.kind + '")在血肉阶段没有对应的 relationships 条目',
|
|
250
363
|
hint: '该对角色的关系尚未血肉化;若刻意留白可忽略'
|
|
251
364
|
});
|
|
252
365
|
}
|
|
@@ -459,6 +572,7 @@ function charsetAndForbiddenCheck(data, errors) {
|
|
|
459
572
|
// ============================================================
|
|
460
573
|
const FLESH_ID_ARRAYS = {
|
|
461
574
|
'角色': ['characters'],
|
|
575
|
+
'其他实体': ['other_entities'],
|
|
462
576
|
'冲突': ['conflicts'],
|
|
463
577
|
'势力': ['factions'],
|
|
464
578
|
'剧情': ['main_lines', 'branches'],
|
|
@@ -488,6 +602,7 @@ function idUniquenessCheck(data, fileType, errors) {
|
|
|
488
602
|
// ============================================================
|
|
489
603
|
const FLESH_TO_SKELETON = {
|
|
490
604
|
'角色': { fleshKey: 'characters', skeletonKey: 'characters' },
|
|
605
|
+
'其他实体': { fleshKey: 'other_entities', skeletonKey: 'other_entities' },
|
|
491
606
|
'冲突': { fleshKey: 'conflicts', skeletonKey: 'conflicts' },
|
|
492
607
|
'势力': { fleshKey: 'factions', skeletonKey: 'factions' },
|
|
493
608
|
'地点': { fleshKey: 'locations', skeletonKey: 'locations' },
|
|
@@ -545,7 +660,7 @@ function foreshadowRefCheck(data, skeletonEntity, errors) {
|
|
|
545
660
|
code: 'FLESH_FORESHADOW_REF_MISSING',
|
|
546
661
|
path: 'endings[' + i + '].foreshadow_recovery_plan[' + j + '].foreshadow_id',
|
|
547
662
|
message: '伏笔 id "' + fid + '" 不存在于骨架的 foreshadows',
|
|
548
|
-
hint: '伏笔 id 必须来自骨架 实体.json 的 foreshadows(可用 upstream-query.js --stage=骨架 --file=实体
|
|
663
|
+
hint: '伏笔 id 必须来自骨架 实体.json 的 foreshadows(可用 node references/upstream-query.js --stage=骨架 --file=实体 --list 查全部 id 名单)'
|
|
549
664
|
});
|
|
550
665
|
}
|
|
551
666
|
}
|
|
@@ -733,7 +848,7 @@ function xrefCheck(data, fileType, skeletonEntity, errors) {
|
|
|
733
848
|
message: '引用' + label + ' "' + name + '" 不存在于骨架',
|
|
734
849
|
hint: guess
|
|
735
850
|
? '疑似拼错:骨架里有 "' + guess.name + '",请改成它'
|
|
736
|
-
: '引用必须用骨架里已有的' + label + '名字;可用 upstream-query.js --stage=骨架 --file=实体 查全量名单'
|
|
851
|
+
: '引用必须用骨架里已有的' + label + '名字;可用 node references/upstream-query.js --stage=骨架 --file=实体 --list 查全量名单'
|
|
737
852
|
});
|
|
738
853
|
}
|
|
739
854
|
};
|
|
@@ -843,6 +958,7 @@ function checkEntityCoverage(skeletonIds, fleshIds, label, errors, warnings) {
|
|
|
843
958
|
// P0-3:数组名 → 文件类型 的映射
|
|
844
959
|
const DIRECT_FILE_TYPE_MAP = {
|
|
845
960
|
characters: '角色',
|
|
961
|
+
other_entities: '其他实体',
|
|
846
962
|
conflicts: '冲突',
|
|
847
963
|
factions: '势力',
|
|
848
964
|
locations: '地点',
|
|
@@ -852,6 +968,7 @@ const DIRECT_FILE_TYPE_MAP = {
|
|
|
852
968
|
function completenessCheck(fleshData, skeletonEntity, errors, warnings) {
|
|
853
969
|
const directChecks = [
|
|
854
970
|
{ fleshKey: 'characters', skKey: 'characters', label: 'character' },
|
|
971
|
+
{ fleshKey: 'other_entities', skKey: 'other_entities', label: 'other_entity' },
|
|
855
972
|
{ fleshKey: 'conflicts', skKey: 'conflicts', label: 'conflict' },
|
|
856
973
|
{ fleshKey: 'factions', skKey: 'factions', label: 'faction' },
|
|
857
974
|
{ fleshKey: 'locations', skKey: 'locations', label: 'location' },
|
|
@@ -1017,7 +1134,7 @@ function validateSingle(args) {
|
|
|
1017
1134
|
safeCheck('freeTextChapterCheck', () => freeTextChapterCheck(data, warnings), errors);
|
|
1018
1135
|
safeCheck('idUniquenessCheck', () => idUniquenessCheck(data, file, errors), errors);
|
|
1019
1136
|
|
|
1020
|
-
if (file === '角色' || file === '冲突' || file === '势力' || file === '地点' || file === '结局') {
|
|
1137
|
+
if (file === '角色' || file === '其他实体' || file === '冲突' || file === '势力' || file === '地点' || file === '结局') {
|
|
1021
1138
|
safeCheck('entityRefCheck', () => entityRefCheck(data, file, sk.entity, errors), errors);
|
|
1022
1139
|
} else if (file === '剧情') {
|
|
1023
1140
|
safeCheck('storyRefCheck', () => storyRefCheck(data, sk.entity, errors), errors);
|
|
@@ -1035,6 +1152,7 @@ function validateSingle(args) {
|
|
|
1035
1152
|
safeCheck('xrefCheck', () => xrefCheck(data, file, sk.entity, errors), errors);
|
|
1036
1153
|
safeCheck('fleshSelfRelationCheck', () => fleshSelfRelationCheck(data, file, errors), errors);
|
|
1037
1154
|
safeCheck('contentQualityCheck', () => runContentQualityChecks(data, errors, { enumLeaves: collectEnumLeaves(tplRes.data.structure) }), errors);
|
|
1155
|
+
safeCheck('traitsAndChangesCheck', () => traitsAndChangesCheck(data, file, errors, warnings), errors);
|
|
1038
1156
|
|
|
1039
1157
|
if (file === '角色') {
|
|
1040
1158
|
safeCheck('arcConsistencyCheck', () => arcConsistencyCheck(data, warnings, sk.hierarchy), errors);
|
|
@@ -1091,6 +1209,7 @@ function validateAll(args) {
|
|
|
1091
1209
|
return emit(2, 'ALL', [{ code: 'UPSTREAM_NOT_PASSED', path: '', message: gate.reason, hint: '请先对骨架阶段执行全量校验并通过' }], []);
|
|
1092
1210
|
}
|
|
1093
1211
|
|
|
1212
|
+
const otherEntityCount = Array.isArray(sk.entity.other_entities) ? sk.entity.other_entities.length : 0;
|
|
1094
1213
|
const FILES = [
|
|
1095
1214
|
{ type: '角色', name: '角色.json', tpl: '角色模板.json' },
|
|
1096
1215
|
{ type: '冲突', name: '冲突.json', tpl: '冲突模板.json' },
|
|
@@ -1099,7 +1218,10 @@ function validateAll(args) {
|
|
|
1099
1218
|
{ type: '地点', name: '地点.json', tpl: '地点模板.json' },
|
|
1100
1219
|
{ type: '结局', name: '结局.json', tpl: '结局模板.json' },
|
|
1101
1220
|
{ type: '世界观规则', name: '世界观规则.json', tpl: '世界观规则模板.json' },
|
|
1102
|
-
{ type: '关键道具', name: '关键道具.json', tpl: '关键道具模板.json' }
|
|
1221
|
+
{ type: '关键道具', name: '关键道具.json', tpl: '关键道具模板.json' },
|
|
1222
|
+
...(otherEntityCount > 0 || fs.existsSync(path.join(stageDir, '其他实体.json'))
|
|
1223
|
+
? [{ type: '其他实体', name: '其他实体.json', tpl: '其他实体模板.json' }]
|
|
1224
|
+
: [])
|
|
1103
1225
|
];
|
|
1104
1226
|
|
|
1105
1227
|
const loaded = {};
|
|
@@ -1128,7 +1250,7 @@ function validateAll(args) {
|
|
|
1128
1250
|
safeCheck('freeTextChapterCheck', () => freeTextChapterCheck(data, warnings), errors);
|
|
1129
1251
|
safeCheck('idUniquenessCheck', () => idUniquenessCheck(data, f.type, errors), errors);
|
|
1130
1252
|
|
|
1131
|
-
if (f.type === '角色' || f.type === '冲突' || f.type === '势力' || f.type === '地点' || f.type === '结局') {
|
|
1253
|
+
if (f.type === '角色' || f.type === '其他实体' || f.type === '冲突' || f.type === '势力' || f.type === '地点' || f.type === '结局') {
|
|
1132
1254
|
safeCheck('entityRefCheck', () => entityRefCheck(data, f.type, sk.entity, errors), errors);
|
|
1133
1255
|
} else if (f.type === '剧情') {
|
|
1134
1256
|
safeCheck('storyRefCheck', () => storyRefCheck(data, sk.entity, errors), errors);
|
|
@@ -1146,6 +1268,7 @@ function validateAll(args) {
|
|
|
1146
1268
|
safeCheck('xrefCheck', () => xrefCheck(data, f.type, sk.entity, errors), errors);
|
|
1147
1269
|
safeCheck('fleshSelfRelationCheck', () => fleshSelfRelationCheck(data, f.type, errors), errors);
|
|
1148
1270
|
safeCheck('contentQualityCheck', () => runContentQualityChecks(data, errors, { enumLeaves: collectEnumLeaves(tplRes.data.structure) }), errors);
|
|
1271
|
+
safeCheck('traitsAndChangesCheck', () => traitsAndChangesCheck(data, f.type, errors, warnings), errors);
|
|
1149
1272
|
|
|
1150
1273
|
if (f.type === '角色') safeCheck('arcConsistencyCheck', () => arcConsistencyCheck(data, warnings, sk.hierarchy), errors);
|
|
1151
1274
|
}
|