@cloud411716/fancy-webnovel 1.0.45 → 2.2.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/client.js +183 -0
- package/cordis.patch.yml +0 -14
- package/index.js +21 -37
- package/lib/chinese-num.js +111 -0
- package/lib/deployed-state.js +141 -0
- package/lib/error-codes.js +104 -0
- package/lib/infra.js +569 -0
- package/lib/llm-wiki.js +109 -0
- package/lib/safe-rm.js +94 -0
- package/lib/scraper-utils.js +165 -0
- package/package.json +37 -5
- package/plugins/fancy-analyze/command-input.js +65 -0
- package/plugins/fancy-analyze/index.js +973 -0
- package/plugins/fancy-analyze/schema-analyze.md +379 -0
- package/plugins/fancy-analyze/schema-write.md +178 -0
- package/plugins/fancy-analyze/scripts/split_source.js +292 -0
- package/plugins/fancy-analyze/summary-progress.js +431 -0
- package/plugins/fancy-analyze/templates.js +881 -0
- package/plugins/fancy-bootstrap/index.js +168 -0
- package/plugins/fancy-bootstrap/scripts/init.js +143 -0
- package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
- package/plugins/fancy-bootstrap/templates.js +54 -0
- package/plugins/fancy-import/import-core.js +1118 -0
- package/plugins/fancy-import/import-driver.js +581 -0
- package/plugins/fancy-import/index.js +730 -0
- package/plugins/fancy-import/prompts/01-import-prompt.md +106 -0
- package/plugins/fancy-import/templates.js +49 -0
- package/plugins/fancy-scan/index.js +319 -0
- package/plugins/fancy-scan/scripts/run-scan.js +520 -0
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +794 -0
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +339 -0
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +611 -0
- package/plugins/fancy-scan/templates.js +295 -0
- package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
- package/plugins/fancy-story-builder/deslop-driver.js +821 -0
- package/plugins/fancy-story-builder/flesh-output.js +155 -0
- package/plugins/fancy-story-builder/index.js +3152 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +553 -0
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +540 -0
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +504 -0
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +188 -0
- package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
- package/plugins/fancy-story-builder/purpose-template.md +107 -0
- package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -0
- package/plugins/fancy-story-builder/references/glossary.md +585 -0
- package/plugins/fancy-story-builder/references/index.md +490 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +163 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +239 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +181 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +177 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +208 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +142 -0
- package/plugins/fancy-story-builder/rhythm-output.js +140 -0
- package/plugins/fancy-story-builder/schema-setting.md +324 -0
- package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
- package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +413 -0
- package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
- package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +1189 -0
- package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
- package/plugins/fancy-story-builder/scripts/graph-utils.js +1385 -0
- package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
- package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
- package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1216 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +377 -0
- package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1832 -0
- package/plugins/fancy-story-builder/scripts/stage-batches.js +1371 -0
- package/plugins/fancy-story-builder/scripts/stage-contract.js +417 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
- package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
- package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
- package/plugins/fancy-story-builder/skeleton-output.js +199 -0
- package/plugins/fancy-story-builder/stage-driver.js +1049 -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 +204 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
- 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 +66 -0
- 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 +76 -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 +72 -0
- 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 +37 -0
- 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 +53 -0
- 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 +40 -0
- 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 +79 -0
- 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 +132 -0
- 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 +133 -0
- 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 +160 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
- 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 +134 -0
- package/plugins/fancy-story-builder/templates.js +1248 -0
- package/plugins/fancy-story-builder/wiki-references.js +204 -0
- package/plugins/fancy-story-builder/writing-driver.js +1061 -0
- package/plugins/fancy-topic/index.js +704 -0
- package/plugins/fancy-topic/scripts/select-topic.js +274 -0
- package/plugins/fancy-topic/templates.js +373 -0
- package/commands/bootstrap/index.js +0 -160
- package/commands/bootstrap/init-project.js +0 -121
- package/commands/scan/index.js +0 -275
- package/commands/scan/platforms/fanqie.js +0 -146
- package/commands/scan/platforms/jinjiang.js +0 -143
- package/commands/scan/platforms/qidian.js +0 -102
- package/commands/scan/platforms/qimao.js +0 -127
- package/commands/scan/scraper.js +0 -195
- package/infra.js +0 -160
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* label-coverage.js —— 模板字段 → 中文标签 的覆盖闸门
|
|
4
|
+
*
|
|
5
|
+
* 背景(2026-10 血肉阶段产物复查):
|
|
6
|
+
* 产物 md 里每个字段的中文标签来自 graph-utils.js 的 FIELD_CN / CONTEXTUAL_LABEL。
|
|
7
|
+
* 那两张表是手工维护的,模板加字段时没人同步加标签,结果产物里直接显示
|
|
8
|
+
* 英文 key(势力.md 的 goal、suppression_method、function_detail 全是英文)。
|
|
9
|
+
* 实测:283 个模板字段里有 103 个查不到中文标签。
|
|
10
|
+
*
|
|
11
|
+
* 本脚本把"漏一个就失败"变成硬约束:
|
|
12
|
+
* 模板新增字段 → 忘了在 FIELD_CN / CONTEXTUAL_LABEL 里加中文标签 → 本脚本立刻报错。
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* node scripts/label-coverage.js --plugin-root=<插件目录>
|
|
16
|
+
* (也可被 prompt-drift-check.js 内部调用,作为仓库级一致性闸门的一环)
|
|
17
|
+
*
|
|
18
|
+
* 输出:stdout 为 JSON
|
|
19
|
+
* { "status": 0, "file": "LABEL_COVERAGE", "errors": [...], "warnings": [...], "report": {...} }
|
|
20
|
+
* status: 0=全覆盖 1=有字段缺中文标签
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
import { FIELD_CN, CONTEXTUAL_LABEL } from './graph-utils.js';
|
|
26
|
+
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
|
|
29
|
+
// 模板目录名(相对插件根)
|
|
30
|
+
const TEMPLATE_DIRS = ['骨架', '血肉', '节奏'];
|
|
31
|
+
|
|
32
|
+
// ============================================================
|
|
33
|
+
// 模板字段收集
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 递归收集一个 JSON Schema 的 properties 里的所有字段名。
|
|
38
|
+
* 数组的 items.properties 也要收(模板里 members / details / relationships 都是这种)。
|
|
39
|
+
*/
|
|
40
|
+
function collectProps(props, acc) {
|
|
41
|
+
if (!props || typeof props !== 'object') return;
|
|
42
|
+
for (const [k, v] of Object.entries(props)) {
|
|
43
|
+
if (!acc.has(k)) acc.set(k, []);
|
|
44
|
+
acc.set(k, acc.get(k));
|
|
45
|
+
if (v && typeof v === 'object') {
|
|
46
|
+
collectProps(v.properties, acc);
|
|
47
|
+
if (v.items && typeof v.items === 'object') collectProps(v.items.properties, acc);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 收集插件模板目录里出现过的全部字段名。
|
|
54
|
+
* 返回 Map<field, Set<templateFile>>。
|
|
55
|
+
*/
|
|
56
|
+
export function collectTemplateFields(pluginRoot) {
|
|
57
|
+
const out = new Map();
|
|
58
|
+
for (const dirName of TEMPLATE_DIRS) {
|
|
59
|
+
const dir = path.join(pluginRoot, 'templates', dirName);
|
|
60
|
+
if (!fs.existsSync(dir)) continue;
|
|
61
|
+
for (const f of fs.readdirSync(dir)) {
|
|
62
|
+
if (!f.endsWith('.json')) continue;
|
|
63
|
+
let j;
|
|
64
|
+
try {
|
|
65
|
+
j = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
66
|
+
} catch (e) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const props = (j.structure || {}).properties || {};
|
|
70
|
+
const tmp = new Map();
|
|
71
|
+
collectProps(props, tmp);
|
|
72
|
+
for (const [k] of tmp) {
|
|
73
|
+
if (!out.has(k)) out.set(k, new Set());
|
|
74
|
+
out.get(k).add(dirName + '/' + f);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================
|
|
82
|
+
// 覆盖检查
|
|
83
|
+
// ============================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 检查模板字段是否都有中文标签。
|
|
87
|
+
*
|
|
88
|
+
* 判定"有标签"的两种情况:
|
|
89
|
+
* 1. FIELD_CN 里有这个 key(全局映射)
|
|
90
|
+
* 2. CONTEXTUAL_LABEL 的某一层里有这个 key(上下文相关映射)
|
|
91
|
+
* —— 例如 conflicts 在冲突档案里是"冲突",在道具归属里是"涉及冲突",
|
|
92
|
+
* 这类同名不同义的字段靠上下文表覆盖。
|
|
93
|
+
*/
|
|
94
|
+
export function runLabelCoverage(pluginRoot) {
|
|
95
|
+
const errors = [];
|
|
96
|
+
const warnings = [];
|
|
97
|
+
const report = {};
|
|
98
|
+
|
|
99
|
+
const fields = collectTemplateFields(pluginRoot);
|
|
100
|
+
const globalKeys = new Set(Object.keys(FIELD_CN));
|
|
101
|
+
const ctxKeys = new Set();
|
|
102
|
+
for (const m of Object.values(CONTEXTUAL_LABEL || {})) {
|
|
103
|
+
if (!m || typeof m !== 'object') continue;
|
|
104
|
+
for (const k of Object.keys(m)) ctxKeys.add(k);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const missing = [];
|
|
108
|
+
const viaContext = [];
|
|
109
|
+
for (const [k, sources] of fields) {
|
|
110
|
+
if (globalKeys.has(k)) continue;
|
|
111
|
+
if (ctxKeys.has(k)) {
|
|
112
|
+
viaContext.push(k);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
missing.push({ field: k, sources: [...sources] });
|
|
116
|
+
}
|
|
117
|
+
missing.sort((a, b) => (a.field < b.field ? -1 : a.field > b.field ? 1 : 0));
|
|
118
|
+
viaContext.sort();
|
|
119
|
+
|
|
120
|
+
report.labelCoverage = {
|
|
121
|
+
templateFieldCount: fields.size,
|
|
122
|
+
globalLabelCount: globalKeys.size,
|
|
123
|
+
contextLabelCount: ctxKeys.size,
|
|
124
|
+
coveredByContext: viaContext.length,
|
|
125
|
+
missingCount: missing.length,
|
|
126
|
+
missingFields: missing.map((m) => m.field)
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const m of missing) {
|
|
130
|
+
errors.push({
|
|
131
|
+
code: 'LABEL_FIELD_UNCOVERED',
|
|
132
|
+
path: 'plugins/fancy-story-builder/scripts/graph-utils.js',
|
|
133
|
+
message: '模板字段 ' + m.field + ' 没有中文标签(出现在 ' + m.sources.join('、') + ')',
|
|
134
|
+
hint: '在 FIELD_CN 里加一条 ' + m.field + ' 键值对;若该字段在不同父级下语义不同,改用 CONTEXTUAL_LABEL'
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { errors, warnings, report };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================
|
|
142
|
+
// CLI
|
|
143
|
+
// ============================================================
|
|
144
|
+
|
|
145
|
+
function parseArgs(argv) {
|
|
146
|
+
const args = {};
|
|
147
|
+
for (const a of argv.slice(2)) {
|
|
148
|
+
const m = a.match(/^--([\w-]+)(?:=(.*))?$/);
|
|
149
|
+
if (m) args[m[1]] = m[2] === undefined ? true : m[2];
|
|
150
|
+
}
|
|
151
|
+
return args;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function main() {
|
|
155
|
+
const args = parseArgs(process.argv);
|
|
156
|
+
const pluginRoot = args['plugin-root'] || path.resolve(__dirname, '..');
|
|
157
|
+
const r = runLabelCoverage(pluginRoot);
|
|
158
|
+
const payload = {
|
|
159
|
+
status: r.errors.length === 0 ? 0 : 1,
|
|
160
|
+
file: 'LABEL_COVERAGE',
|
|
161
|
+
errors: r.errors,
|
|
162
|
+
warnings: r.warnings,
|
|
163
|
+
report: r.report
|
|
164
|
+
};
|
|
165
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
166
|
+
return r.errors.length === 0 ? 0 : 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
170
|
+
process.exit(main());
|
|
171
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* prompt-drift-check.js
|
|
4
|
+
* 提示词 ↔ 规则文件 / 脚本 漂移检查
|
|
5
|
+
*
|
|
6
|
+
* 背景:提示词里把三样东西硬编码了,且各自独立维护,没有任何机制保证一致:
|
|
7
|
+
* 1. 公式清单与拓扑层级(骨架 30 条 / 节奏 7 条)
|
|
8
|
+
* 2. 错误代码速查表
|
|
9
|
+
* 3. 阶段边界的字段清单
|
|
10
|
+
* 一旦底层文件变化,提示词会静默过期:LLM 按过期清单算数字、
|
|
11
|
+
* 拿到没在表里解释过的错误码无从下手,产出的 derivations 缺键多键也没有任何报错。
|
|
12
|
+
*
|
|
13
|
+
* 本脚本检查:
|
|
14
|
+
* A. 公式清单双向比对 + 条数声明核对
|
|
15
|
+
* B. 约束清单双向比对 + 条数声明核对
|
|
16
|
+
* C. 错误代码双向比对:脚本会发出的码,提示词里必须能查到;
|
|
17
|
+
* 提示词里提到的码,脚本必须真的会发
|
|
18
|
+
*
|
|
19
|
+
* 用法:
|
|
20
|
+
* node prompt-drift-check.js
|
|
21
|
+
* node prompt-drift-check.js --plugin-root=<插件目录>
|
|
22
|
+
* 退出码:0=一致 1=存在漂移 2=脚本异常
|
|
23
|
+
*
|
|
24
|
+
* 源码中零反引号。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { runLabelCoverage } from './label-coverage.js';
|
|
31
|
+
import { INFRA_CODES as INFRA_CODE_SET } from './error-classes.js';
|
|
32
|
+
|
|
33
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
|
|
35
|
+
const BT_CHAR = String.fromCharCode(96);
|
|
36
|
+
const BT_RE = new RegExp(BT_CHAR + '([^' + BT_CHAR + ']+)' + BT_CHAR, 'g');
|
|
37
|
+
|
|
38
|
+
// ============================================================
|
|
39
|
+
// 参数
|
|
40
|
+
// ============================================================
|
|
41
|
+
function parseArgs() {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const arg of args) {
|
|
45
|
+
// 参数名允许连字符(--plugin-root),否则该参数会被静默忽略
|
|
46
|
+
const m = arg.match(/^--([\w-]+)=(.+)$/);
|
|
47
|
+
if (m) result[m[1]] = m[2];
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================
|
|
53
|
+
// 抽取
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 从 markdown 表格行里抽「序号 + 反引号包裹的名字」。
|
|
58
|
+
* 形如: | 1 | `structure.volume_count` |
|
|
59
|
+
*
|
|
60
|
+
* 返回 { names, firstLine }:firstLine 是第一行命中行在原文中的行号,
|
|
61
|
+
* 用来往上找「这份清单有 N 条」的引入句。
|
|
62
|
+
*/
|
|
63
|
+
export function extractTableNames(mdText) {
|
|
64
|
+
const out = [];
|
|
65
|
+
const lines = mdText.split('\n');
|
|
66
|
+
const re = new RegExp('^\\s*\\|\\s*\\d+\\s*\\|\\s*' + BT_CHAR + '([^' + BT_CHAR + ']+)' + BT_CHAR + '\\s*\\|');
|
|
67
|
+
let firstLine = -1;
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const m = lines[i].match(re);
|
|
70
|
+
if (!m) continue;
|
|
71
|
+
if (firstLine < 0) firstLine = i;
|
|
72
|
+
out.push(m[1].trim());
|
|
73
|
+
}
|
|
74
|
+
return { names: out, firstLine };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 从 markdown 的无序列表里抽反引号包裹的名字。
|
|
79
|
+
* 形如: - `branch_chapter_feasible`
|
|
80
|
+
*/
|
|
81
|
+
export function extractListNames(mdText) {
|
|
82
|
+
const out = [];
|
|
83
|
+
const re = new RegExp('^\\s*[-*]\\s+' + BT_CHAR + '([^' + BT_CHAR + ']+)' + BT_CHAR, 'gm');
|
|
84
|
+
let m;
|
|
85
|
+
while ((m = re.exec(mdText)) !== null) {
|
|
86
|
+
out.push(m[1].trim());
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 从指定的一行文本里抽「N 条公式 / N 条约束」这类条数声明。
|
|
93
|
+
*
|
|
94
|
+
* 只看一行,不做全文扫描:全文扫描会命中「核心铁律(本阶段最重要的 6 条约束)」
|
|
95
|
+
* 这类与公式清单无关的计数,造成误报。
|
|
96
|
+
*/
|
|
97
|
+
export function extractCountInLine(line, keyword) {
|
|
98
|
+
if (!line) return null;
|
|
99
|
+
const m = line.match(new RegExp('(\\d+)\\s*条' + keyword));
|
|
100
|
+
return m ? Number(m[1]) : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 从第 startLine 行往上最多 lookback 行,找含「N 条 keyword」的引入句。
|
|
105
|
+
*/
|
|
106
|
+
export function findCountClaimAbove(lines, startLine, keyword, lookback) {
|
|
107
|
+
const lb = lookback === undefined ? 6 : lookback;
|
|
108
|
+
for (let i = startLine; i >= 0 && i > startLine - lb; i--) {
|
|
109
|
+
const c = extractCountInLine(lines[i], keyword);
|
|
110
|
+
if (c !== null) return { count: c, line: i, text: lines[i].trim() };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 找到引入清单的那一行,并返回它之后、下一个同级标题之前的内容。
|
|
117
|
+
* anchor 是一个正则,匹配到的那一行即引入句(条数声明通常就写在这行里)。
|
|
118
|
+
*/
|
|
119
|
+
export function extractSectionAfter(mdText, anchorRe) {
|
|
120
|
+
const lines = mdText.split('\n');
|
|
121
|
+
let start = -1;
|
|
122
|
+
let anchorText = '';
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
if (anchorRe.test(lines[i])) { start = i + 1; anchorText = lines[i]; break; }
|
|
125
|
+
}
|
|
126
|
+
if (start < 0) return { section: '', anchor: '', anchorLine: -1 };
|
|
127
|
+
const out = [];
|
|
128
|
+
for (let i = start; i < lines.length; i++) {
|
|
129
|
+
const line = lines[i];
|
|
130
|
+
// 遇到新的标题行就停
|
|
131
|
+
if (/^#{1,6}\s/.test(line)) break;
|
|
132
|
+
out.push(line);
|
|
133
|
+
}
|
|
134
|
+
return { section: out.join('\n'), anchor: anchorText, anchorLine: start - 1 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// 比对
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
function diffSets(declared, actual) {
|
|
142
|
+
const declaredSet = new Set(declared);
|
|
143
|
+
const actualSet = new Set(actual);
|
|
144
|
+
const missing = actual.filter((x) => !declaredSet.has(x)); // 文件里有、提示词没列
|
|
145
|
+
const extra = declared.filter((x) => !actualSet.has(x)); // 提示词列了、文件里没有
|
|
146
|
+
return { missing, extra };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compareList(label, declared, actual, errors) {
|
|
150
|
+
const d = diffSets(declared, actual);
|
|
151
|
+
for (const name of d.missing) {
|
|
152
|
+
errors.push({
|
|
153
|
+
code: 'FORMULA_LIST_MISSING',
|
|
154
|
+
path: label,
|
|
155
|
+
message: label + ' 在提示词里没有列出:' + name,
|
|
156
|
+
hint: '提示词的公式清单与公式文件脱节,LLM 会按过期清单计算'
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const name of d.extra) {
|
|
160
|
+
errors.push({
|
|
161
|
+
code: 'FORMULA_LIST_EXTRA',
|
|
162
|
+
path: label,
|
|
163
|
+
message: label + ' 在提示词里列了但公式文件里没有:' + name,
|
|
164
|
+
hint: '提示词里残留了已删除或改名的公式'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return d;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================
|
|
171
|
+
// 错误代码比对
|
|
172
|
+
// ============================================================
|
|
173
|
+
|
|
174
|
+
// 提示词里可能出现的"大写 token",但不是错误代码
|
|
175
|
+
const NON_ERROR_CAPS = new Set([
|
|
176
|
+
'JSON', 'ID', 'TBD', 'TODO', 'PASS', 'BLOCKED', 'OK', 'TRUE', 'FALSE',
|
|
177
|
+
'CSV', 'YAML', 'MD', 'HTML', 'UTF', 'ASCII', 'URL', 'API', 'LLM', 'N/A'
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
// 错误代码的形态:全大写、至少 2 个字符("ENUM" / "G_ISOLATED" 都覆盖)
|
|
181
|
+
const ERROR_CODE_RE = /^[A-Z][A-Z0-9_]{1,}$/;
|
|
182
|
+
// 反向比对时只看带下划线的码:单字母/数字组合的 token 多半是实体 id 示例(C001 / V002)
|
|
183
|
+
const UNDERSCORE_CODE_RE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
|
|
184
|
+
|
|
185
|
+
// 基础设施类错误码:全部对应 status 2(脚本异常),提示词对这类只需说"按 BLOCKED 处理",
|
|
186
|
+
// 不需要逐条解释,因此不纳入"必须在速查表里查到"的要求。
|
|
187
|
+
//
|
|
188
|
+
// 定义在 error-classes.js —— 插件的驱动层用同一份判定来决定
|
|
189
|
+
// "哪些错误不该甩给模型重投",两边必须一致,不能各写一份。
|
|
190
|
+
const INFRA_CODES = INFRA_CODE_SET;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 从脚本源码里抽出所有 code: 'XXX' 的错误码。
|
|
194
|
+
*/
|
|
195
|
+
export function extractScriptCodes(scriptPath) {
|
|
196
|
+
const text = readText(scriptPath);
|
|
197
|
+
if (text === null) return null;
|
|
198
|
+
const out = new Set();
|
|
199
|
+
const re = /code\s*:\s*'([A-Z][A-Z0-9_]*)'/g;
|
|
200
|
+
let m;
|
|
201
|
+
while ((m = re.exec(text)) !== null) out.add(m[1]);
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 从提示词里抽出所有被反引号包裹的全大写 token(错误代码的候选)。
|
|
207
|
+
*/
|
|
208
|
+
export function extractPromptCodes(mdText) {
|
|
209
|
+
const out = new Set();
|
|
210
|
+
const re = new RegExp(BT_CHAR + '([A-Z][A-Z0-9_]*)' + BT_CHAR, 'g');
|
|
211
|
+
let m;
|
|
212
|
+
while ((m = re.exec(mdText)) !== null) {
|
|
213
|
+
const token = m[1];
|
|
214
|
+
if (NON_ERROR_CAPS.has(token)) continue;
|
|
215
|
+
if (!ERROR_CODE_RE.test(token)) continue;
|
|
216
|
+
out.add(token);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 每个阶段:提示词文件、校验脚本、以及共享的错误码来源。
|
|
223
|
+
*/
|
|
224
|
+
const STAGE_CODE_SOURCES = [
|
|
225
|
+
{
|
|
226
|
+
stage: '骨架',
|
|
227
|
+
prompt: 'prompts/01-skeleton-prompt.md',
|
|
228
|
+
script: 'scripts/skeleton-validate.js',
|
|
229
|
+
// 驱动层(插件根目录的 stage-driver.js)的错误码也要扫进来:
|
|
230
|
+
// 它们全在 INFRA_CODES 里(模型改不了),所以不会要求进提示词速查表 ——
|
|
231
|
+
// 但必须被扫到,否则新增插件级码时没人知道它没被登记。
|
|
232
|
+
shared: ['scripts/graph-utils.js', 'scripts/content-quality.js', 'stage-driver.js']
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
stage: '血肉',
|
|
236
|
+
prompt: 'prompts/02-flesh-prompt.md',
|
|
237
|
+
script: 'scripts/flesh-validate.js',
|
|
238
|
+
shared: ['scripts/graph-utils.js', 'scripts/content-quality.js']
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
stage: '节奏',
|
|
242
|
+
prompt: 'prompts/03-rhythm-prompt.md',
|
|
243
|
+
script: 'scripts/rhythm-validate.js',
|
|
244
|
+
shared: ['scripts/graph-utils.js']
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
stage: '写作',
|
|
248
|
+
prompt: 'prompts/04-writing-prompt.md',
|
|
249
|
+
script: 'scripts/writing-validate.js',
|
|
250
|
+
shared: ['scripts/graph-utils.js']
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
stage: '去味',
|
|
254
|
+
prompt: 'prompts/05-deslop-prompt.md',
|
|
255
|
+
script: 'scripts/deslop-validate.js',
|
|
256
|
+
shared: ['scripts/ai-patterns.js']
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
function checkErrorCodes(pluginRoot, errors, report) {
|
|
261
|
+
for (const src of STAGE_CODE_SOURCES) {
|
|
262
|
+
const promptPath = path.join(pluginRoot, src.prompt);
|
|
263
|
+
const scriptPath = path.join(pluginRoot, src.script);
|
|
264
|
+
const promptText = readText(promptPath);
|
|
265
|
+
if (promptText === null) {
|
|
266
|
+
errors.push({ code: 'PROMPT_LOAD', path: promptPath, message: '读取' + src.stage + '阶段提示词失败', hint: null });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const actual = extractScriptCodes(scriptPath);
|
|
270
|
+
if (actual === null) {
|
|
271
|
+
errors.push({ code: 'SCRIPT_LOAD', path: scriptPath, message: '读取' + src.stage + '阶段校验脚本失败', hint: null });
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
for (const s of src.shared) {
|
|
275
|
+
const shared = extractScriptCodes(path.join(pluginRoot, s));
|
|
276
|
+
if (shared) for (const c of shared) actual.add(c);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const mentioned = extractPromptCodes(promptText);
|
|
280
|
+
const undocumented = [...actual].filter((c) => !mentioned.has(c) && !INFRA_CODES.has(c)).sort();
|
|
281
|
+
const stale = [...mentioned].filter((c) => UNDERSCORE_CODE_RE.test(c) && !actual.has(c)).sort();
|
|
282
|
+
|
|
283
|
+
report[src.stage + '错误码'] = {
|
|
284
|
+
scriptCount: actual.size,
|
|
285
|
+
documentedCount: [...actual].filter((c) => mentioned.has(c)).length,
|
|
286
|
+
undocumented: undocumented,
|
|
287
|
+
stale: stale
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
for (const c of undocumented) {
|
|
291
|
+
errors.push({
|
|
292
|
+
code: 'ERROR_CODE_UNDOCUMENTED',
|
|
293
|
+
path: src.prompt,
|
|
294
|
+
message: src.stage + '校验脚本会发出错误码 ' + c + ',但提示词里查不到',
|
|
295
|
+
hint: '在提示词的「常见错误代码速查」里补上这条,否则 LLM 拿到这个码无从下手'
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
for (const c of stale) {
|
|
299
|
+
errors.push({
|
|
300
|
+
code: 'ERROR_CODE_UNKNOWN',
|
|
301
|
+
path: src.prompt,
|
|
302
|
+
message: src.stage + '提示词里提到错误码 ' + c + ',但该阶段脚本不会发出这个码',
|
|
303
|
+
hint: '提示词残留了过期或写错的错误码(也可能是串到了别的阶段的码)'
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================
|
|
310
|
+
// 主流程
|
|
311
|
+
// ============================================================
|
|
312
|
+
|
|
313
|
+
function readText(p) {
|
|
314
|
+
try {
|
|
315
|
+
return fs.readFileSync(p, 'utf-8');
|
|
316
|
+
} catch (e) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readJson(p) {
|
|
322
|
+
const t = readText(p);
|
|
323
|
+
if (t === null) return null;
|
|
324
|
+
try {
|
|
325
|
+
return JSON.parse(t);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function runDriftCheck(pluginRoot) {
|
|
332
|
+
const errors = [];
|
|
333
|
+
const warnings = [];
|
|
334
|
+
const report = {};
|
|
335
|
+
|
|
336
|
+
// ---------- 骨架:公式清单 ----------
|
|
337
|
+
const skFormulaPath = path.join(pluginRoot, 'references', 'formulas-skeleton.json');
|
|
338
|
+
const skPromptPath = path.join(pluginRoot, 'prompts', '01-skeleton-prompt.md');
|
|
339
|
+
const skFormulas = readJson(skFormulaPath);
|
|
340
|
+
const skPrompt = readText(skPromptPath);
|
|
341
|
+
if (!skFormulas) {
|
|
342
|
+
errors.push({ code: 'FORMULAS_LOAD', path: skFormulaPath, message: '读取或解析 formulas-skeleton.json 失败', hint: null });
|
|
343
|
+
} else if (!skPrompt) {
|
|
344
|
+
errors.push({ code: 'PROMPT_LOAD', path: skPromptPath, message: '读取骨架提示词失败', hint: null });
|
|
345
|
+
} else {
|
|
346
|
+
const actual = Object.keys(skFormulas.formulas || {});
|
|
347
|
+
const table = extractTableNames(skPrompt);
|
|
348
|
+
const declared = table.names;
|
|
349
|
+
report.skeleton = { declaredCount: declared.length, actualCount: actual.length };
|
|
350
|
+
if (declared.length === 0) {
|
|
351
|
+
errors.push({
|
|
352
|
+
code: 'FORMULA_LIST_UNPARSED',
|
|
353
|
+
path: skPromptPath,
|
|
354
|
+
message: '没能从骨架提示词里解析出公式清单表格',
|
|
355
|
+
hint: '提示词的公式清单表格结构可能变了(形如 | 1 | `name` | 的行)'
|
|
356
|
+
});
|
|
357
|
+
} else {
|
|
358
|
+
compareList('骨架公式', declared, actual, errors);
|
|
359
|
+
// 条数声明:只看清单表格正上方那几行里"N 条公式"的声明
|
|
360
|
+
const claim = findCountClaimAbove(skPrompt.split('\n'), table.firstLine, '公式');
|
|
361
|
+
if (claim && claim.count !== actual.length) {
|
|
362
|
+
errors.push({
|
|
363
|
+
code: 'FORMULA_COUNT_MISMATCH',
|
|
364
|
+
path: skPromptPath,
|
|
365
|
+
message: '提示词声明「' + claim.count + ' 条公式」,但 formulas-skeleton.json 里实际是 ' + actual.length + ' 条',
|
|
366
|
+
hint: '同步提示词里的条数声明与公式清单'
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------- 节奏:约束清单 ----------
|
|
373
|
+
const rhFormulaPath = path.join(pluginRoot, 'references', 'formulas-rhythm.json');
|
|
374
|
+
const rhPromptPath = path.join(pluginRoot, 'prompts', '03-rhythm-prompt.md');
|
|
375
|
+
const rhFormulas = readJson(rhFormulaPath);
|
|
376
|
+
const rhPrompt = readText(rhPromptPath);
|
|
377
|
+
if (!rhFormulas) {
|
|
378
|
+
errors.push({ code: 'FORMULAS_LOAD', path: rhFormulaPath, message: '读取或解析 formulas-rhythm.json 失败', hint: null });
|
|
379
|
+
} else if (!rhPrompt) {
|
|
380
|
+
errors.push({ code: 'PROMPT_LOAD', path: rhPromptPath, message: '读取节奏提示词失败', hint: null });
|
|
381
|
+
} else {
|
|
382
|
+
const actual = Object.keys(rhFormulas.constraints || {});
|
|
383
|
+
const anchor = extractSectionAfter(rhPrompt, /constraints.*节中的\s*\d+\s*条约束名/);
|
|
384
|
+
const declared = extractListNames(anchor.section);
|
|
385
|
+
report.rhythm = { declaredCount: declared.length, actualCount: actual.length };
|
|
386
|
+
if (declared.length === 0) {
|
|
387
|
+
errors.push({
|
|
388
|
+
code: 'CONSTRAINT_LIST_UNPARSED',
|
|
389
|
+
path: rhPromptPath,
|
|
390
|
+
message: '没能从节奏提示词里解析出约束名清单',
|
|
391
|
+
hint: '提示词的约束清单结构可能变了(形如 - `constraint_name` 的行)'
|
|
392
|
+
});
|
|
393
|
+
} else {
|
|
394
|
+
compareList('节奏约束', declared, actual, errors);
|
|
395
|
+
// 条数声明写在引入句里
|
|
396
|
+
const claim = extractCountInLine(anchor.anchor, '约束');
|
|
397
|
+
if (claim !== null && claim !== actual.length) {
|
|
398
|
+
errors.push({
|
|
399
|
+
code: 'CONSTRAINT_COUNT_MISMATCH',
|
|
400
|
+
path: rhPromptPath,
|
|
401
|
+
message: '提示词声明「' + claim + ' 条约束」,但 formulas-rhythm.json 里实际是 ' + actual.length + ' 条',
|
|
402
|
+
hint: '同步提示词里的条数声明与约束清单'
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------- 错误代码 ----------
|
|
409
|
+
checkErrorCodes(pluginRoot, errors, report);
|
|
410
|
+
|
|
411
|
+
// ---------- 模板字段中文标签覆盖 ----------
|
|
412
|
+
// 模板加了新字段却忘了在 FIELD_CN / CONTEXTUAL_LABEL 里加中文标签,
|
|
413
|
+
// 产物 md 里就会直接显示英文 key(2026-10 血肉阶段产物复查发现过 103 个这种字段)。
|
|
414
|
+
{
|
|
415
|
+
const cov = runLabelCoverage(pluginRoot);
|
|
416
|
+
for (const e of cov.errors) errors.push(e);
|
|
417
|
+
for (const w of cov.warnings) warnings.push(w);
|
|
418
|
+
report.模板字段标签覆盖 = cov.report.labelCoverage;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { errors, warnings, report };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function main() {
|
|
425
|
+
const args = parseArgs();
|
|
426
|
+
const pluginRoot = args['plugin-root'] || path.resolve(__dirname, '..');
|
|
427
|
+
const r = runDriftCheck(pluginRoot);
|
|
428
|
+
const payload = {
|
|
429
|
+
status: r.errors.length === 0 ? 0 : 1,
|
|
430
|
+
file: 'PROMPT_DRIFT',
|
|
431
|
+
errors: r.errors,
|
|
432
|
+
warnings: r.warnings,
|
|
433
|
+
report: r.report
|
|
434
|
+
};
|
|
435
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
436
|
+
return r.errors.length === 0 ? 0 : 1;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
440
|
+
process.exit(main());
|
|
441
|
+
}
|