verifact 0.0.0-stage → 0.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/LICENSE +202 -0
- package/README.md +94 -2
- package/VERIFACT-DESIGN.md +337 -0
- package/bin/verifact.js +463 -0
- package/package.json +49 -4
- package/src/config.js +198 -0
- package/src/core/artifacts.js +275 -0
- package/src/core/cache.js +87 -0
- package/src/core/claims.js +329 -0
- package/src/core/crossdoc.js +97 -0
- package/src/core/dispositions.js +292 -0
- package/src/core/lint.js +111 -0
- package/src/core/normalize.js +178 -0
- package/src/core/resolve.js +307 -0
- package/src/core/stable-key.js +97 -0
- package/src/core/walk.js +99 -0
- package/src/engine.js +467 -0
- package/src/errors.js +35 -0
- package/src/history.js +91 -0
- package/src/logger.js +29 -0
- package/src/pooled-scan.js +109 -0
- package/src/server/api.js +167 -0
- package/src/server/www/app.js +203 -0
- package/src/server/www/index.html +83 -0
- package/src/server/www/style.css +101 -0
- package/src/worker.js +35 -0
- package/verifact.json.example +92 -0
package/bin/verifact.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { loadConfig } = require('../src/config');
|
|
8
|
+
const engine = require('../src/engine');
|
|
9
|
+
const logger = require('../src/logger');
|
|
10
|
+
const { readRuns, diffRuns } = require('../src/history');
|
|
11
|
+
const { UsageError } = require('../src/errors');
|
|
12
|
+
|
|
13
|
+
const HELP = `verifact — 事实哨兵:把文档里的硬数字声明绑定到真实产出物上逐条核验
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
verifact verify [--config <file>] [--cwd <dir>] [选项] 扫描并核验,按门禁策略给出退出码
|
|
17
|
+
verifact explain <claimId> [--config <file>] 打印单条声明的判定依据与候选
|
|
18
|
+
verifact diff [--since <n>] [--top <n>] 对比两次运行之间的状态迁移
|
|
19
|
+
verifact serve [--config <file>] [--port <n>] 启动本地看板与 API
|
|
20
|
+
verifact init [--dir <dir>] 生成一份 verifact.json 样例配置
|
|
21
|
+
|
|
22
|
+
verify 选项:
|
|
23
|
+
--cwd <dir> 配置文件查找 / 缓存 / --out 的基准目录,默认当前目录
|
|
24
|
+
--config <file> 配置文件路径,默认依次查找 verifact.json / .verifact.json
|
|
25
|
+
--docs <dir|file> 追加文档根(可多次,覆盖配置里的 docs.roots;相对路径以配置文件目录为基准)
|
|
26
|
+
--artifacts <dir> 追加产出物根(相对路径同样以配置文件目录为基准)
|
|
27
|
+
--allow-scan-errors 扫描崩溃时不中止(默认中止并 exit 1);被跳过的文件会逐条列出
|
|
28
|
+
--fail-on <policy> none | drift | review | lint-high (默认 drift)
|
|
29
|
+
--format <fmt> text | json | md (默认 text)
|
|
30
|
+
--out <file> 结果写入文件(json/md 时尤其有用)
|
|
31
|
+
--top <n> 文本摘要里展示的 drift 条数,默认 20
|
|
32
|
+
--deep 启用激进抽取(含裸大数字,误报率更高)
|
|
33
|
+
--no-cache 跳过产出物增量缓存,强制重建索引
|
|
34
|
+
--concurrency <n> worker 数上限
|
|
35
|
+
--quiet 不输出 stderr 结构化日志
|
|
36
|
+
|
|
37
|
+
退出码:
|
|
38
|
+
0 核验通过 / 无命中门禁条件
|
|
39
|
+
1 命中门禁条件(存在 drift 等)
|
|
40
|
+
2 用法或配置错误
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const out = { _: [], flags: {} };
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < argv.length) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
if (a === '--help' || a === '-h') { out.flags.help = true; i++; continue; }
|
|
49
|
+
if (a.startsWith('--')) {
|
|
50
|
+
const key = a.slice(2);
|
|
51
|
+
const next = argv[i + 1];
|
|
52
|
+
const booleanFlags = ['deep', 'no-cache', 'quiet', 'version', 'allow-scan-errors'];
|
|
53
|
+
if (booleanFlags.indexOf(key) >= 0 || next === undefined || next.startsWith('--')) {
|
|
54
|
+
out.flags[key] = true;
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
out.flags[key] = next;
|
|
59
|
+
i += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
out._.push(a);
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function collectRepeated(argv, name) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = 0; i < argv.length; i++) {
|
|
71
|
+
if (argv[i] === '--' + name && argv[i + 1]) out.push(argv[i + 1]);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildOverrides(rawArgv, flags) {
|
|
77
|
+
const o = {};
|
|
78
|
+
const docs = collectRepeated(rawArgv, 'docs');
|
|
79
|
+
const arts = collectRepeated(rawArgv, 'artifacts');
|
|
80
|
+
if (docs.length) o.docs = { roots: docs };
|
|
81
|
+
if (arts.length) o.artifacts = { roots: arts };
|
|
82
|
+
if (flags['fail-on']) o.gate = { failOn: flags['fail-on'] };
|
|
83
|
+
if (flags.port) o.server = { port: Number(flags.port) };
|
|
84
|
+
return o;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pad(s, n) {
|
|
88
|
+
const str = String(s == null ? '' : s);
|
|
89
|
+
const w = [...str].reduce((acc, ch) => acc + (/[\u4e00-\u9fff\uff00-\uffef]/.test(ch) ? 2 : 1), 0);
|
|
90
|
+
return str + ' '.repeat(Math.max(0, n - w));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function valueText(v) {
|
|
94
|
+
if (!v) return '-';
|
|
95
|
+
if (v.type === 'ref') return v.raw;
|
|
96
|
+
if (v.type === 'ratio') return v.raw;
|
|
97
|
+
return v.raw;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const STATUS_LABEL = {
|
|
101
|
+
verified: '✔ 已核验',
|
|
102
|
+
drift: '✖ 漂移',
|
|
103
|
+
ambiguous: '? 候选冲突',
|
|
104
|
+
unbound: '· 未绑定',
|
|
105
|
+
'ref-found': '✔ 产出物存在',
|
|
106
|
+
'ref-missing': '✖ 产出物缺失',
|
|
107
|
+
ignored: '- 已忽略',
|
|
108
|
+
denied: '- 已拒绑',
|
|
109
|
+
disposition: '- 已分类',
|
|
110
|
+
reference: '- 标准编号(不按比值)',
|
|
111
|
+
sealed: '- 哈希存证'
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function renderText(report, top) {
|
|
115
|
+
const L = [];
|
|
116
|
+
const c = report.counts;
|
|
117
|
+
L.push('verifact — 事实哨兵');
|
|
118
|
+
L.push('─'.repeat(78));
|
|
119
|
+
L.push(`生成时间 ${report.generatedAt} 耗时 ${report.durationMs}ms cache=${report.meta.cache}${report.meta.cacheReason ? '(' + report.meta.cacheReason + ')' : ''} workers=${report.meta.workers}`);
|
|
120
|
+
L.push(`文档 ${c.docs} 份 产出物条目 ${c.artifacts} 条 缓存 ${report.meta.cache}`);
|
|
121
|
+
L.push('');
|
|
122
|
+
// 覆盖率分母只取一个来源(counts.codeArtifactClaims),不再现场重算;
|
|
123
|
+
// 现场重算会与「处置分类」那一行各说一套(#102)。
|
|
124
|
+
const reviewable = c.codeArtifactClaims != null
|
|
125
|
+
? c.codeArtifactClaims
|
|
126
|
+
: c.verified + c.drift + c.ambiguous + c.unbound + c.refFound + c.refMissing;
|
|
127
|
+
const rate = reviewable ? ((c.verified + c.refFound) / reviewable * 100).toFixed(1) : '0.0';
|
|
128
|
+
// 双分母:换了分母的「覆盖率上升」不算上升(RFC §2.3)。
|
|
129
|
+
const allJudged = c.allJudgedClaims != null ? c.allJudgedClaims : reviewable;
|
|
130
|
+
const rateAll = allJudged ? ((c.verified + c.refFound) / allJudged * 100).toFixed(1) : '0.0';
|
|
131
|
+
L.push(`声明总数 ${pad(c.claims, 6)} 已核验 ${pad(c.verified, 5)} 漂移 ${pad(c.drift, 5)} 候选冲突 ${pad(c.ambiguous, 5)} 未绑定 ${pad(c.unbound, 5)}`);
|
|
132
|
+
L.push(`引用型 ${pad(c.refFound + c.refMissing, 6)} 存在 ${pad(c.refFound, 5)} 缺失 ${pad(c.refMissing, 5)} 忽略 ${pad(c.ignored, 5)} 拒绑 ${pad(c.denied, 5)}`);
|
|
133
|
+
if (c.dispositionCounts) {
|
|
134
|
+
const dc = c.dispositionCounts;
|
|
135
|
+
L.push(`处置分类 code-artifact ${pad(dc['code-artifact'] || 0, 5)} progress ${pad(dc.progress || 0, 5)} measured-once ${pad(dc['measured-once'] || 0, 5)} narrative ${pad(dc.narrative || 0, 5)} 已分类 ${pad(c.dispositioned || 0, 5)}`);
|
|
136
|
+
}
|
|
137
|
+
if (c.standardRefs) {
|
|
138
|
+
const refs = (report.claims || []).filter((x) => x.status === 'reference');
|
|
139
|
+
const shown = refs.slice(0, 3).map((x) => x.file + ':L' + x.line + ' ' + String(x.value && x.value.raw || '').trim());
|
|
140
|
+
L.push(`标准编号 ${pad(c.standardRefs, 5)} 条(不当比值解析,例:${shown.join(';')}${refs.length > 3 ? ' …' : ''})`);
|
|
141
|
+
}
|
|
142
|
+
if (c.sealed) {
|
|
143
|
+
// 存证目录里的声明既不是 unbound 也不是 ignored:它们从来就不该被整体评判,
|
|
144
|
+
// 但也不能凭空消失。单独一行,名字写清楚。
|
|
145
|
+
const files = new Set((report.claims || []).filter((x) => x.status === 'sealed').map((x) => x.file));
|
|
146
|
+
L.push(`Sealed (by hash): ${c.sealed} 条(${files.size} 个文件,不插注释、不参与判定)`);
|
|
147
|
+
}
|
|
148
|
+
const sw = report.counts.sealedWarnings;
|
|
149
|
+
if (sw && sw.emptyPatterns && sw.emptyPatterns.length) {
|
|
150
|
+
L.push(`警告:sealed 规则未命中任何声明(路径可能写错,存证目录实际未受保护):${sw.emptyPatterns.join(';')}`);
|
|
151
|
+
}
|
|
152
|
+
L.push(`核验率 ${rate}% (code-artifact ${reviewable} 条) ${rateAll}% (全部受判 ${allJudged} 条)`);
|
|
153
|
+
|
|
154
|
+
const skipped = (report.meta && report.meta.skippedFiles) || [];
|
|
155
|
+
if (skipped.length) {
|
|
156
|
+
const tolerated = skipped.filter((x) => x.tolerated);
|
|
157
|
+
if (tolerated.length) {
|
|
158
|
+
L.push(`警告:${tolerated.length} 个文件扫描失败,已按 --allow-scan-errors 跳过,它们的声明没有进入上面的数字:`);
|
|
159
|
+
for (const x of tolerated) L.push(` ${x.file} (${x.reason})`);
|
|
160
|
+
}
|
|
161
|
+
const tooLarge = skipped.filter((x) => x.reason === 'too_large');
|
|
162
|
+
if (tooLarge.length) {
|
|
163
|
+
const limit = tooLarge[0].limit ? ` > ${tooLarge[0].limit} B` : '';
|
|
164
|
+
L.push(`跳过(超限${limit}):${tooLarge.length} 个文件`);
|
|
165
|
+
for (const x of tooLarge.slice(0, 5)) L.push(` ${x.file} ${x.bytes != null ? x.bytes + ' B' : ''}`);
|
|
166
|
+
if (tooLarge.length > 5) L.push(` … 另有 ${tooLarge.length - 5} 个`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const dw = report.counts.dispositionWarnings;
|
|
171
|
+
if (dw) {
|
|
172
|
+
if (dw.shareExceedsHalf) {
|
|
173
|
+
L.push(`⚠ 全报告 ${(dw.share * 100).toFixed(1)}% 的声明被标为非 code-artifact(> 50%):换分母不等于覆盖率上升,请逐条看归属。`);
|
|
174
|
+
}
|
|
175
|
+
for (const f of dw.fullyDispositionedFiles || []) {
|
|
176
|
+
L.push(`⚠ ${f.file}:整份文件 ${f.claims} 条声明全部非 code-artifact,该文件对覆盖率零贡献。`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
L.push(`用语线索 high ${pad(report.lintCounts.high, 4)} medium ${pad(report.lintCounts.medium, 4)} low ${pad(report.lintCounts.low, 4)}`);
|
|
180
|
+
L.push('');
|
|
181
|
+
|
|
182
|
+
const drifts = report.claims.filter((x) => x.status === 'drift');
|
|
183
|
+
if (drifts.length) {
|
|
184
|
+
L.push(`漂移明细(${drifts.length} 条,展示前 ${top} 条)`);
|
|
185
|
+
L.push('─'.repeat(78));
|
|
186
|
+
for (const d of drifts.slice(0, top)) {
|
|
187
|
+
L.push(`${pad(d.file + ':' + d.line, 46)} ${pad(d.module || '-', 12)} ${d.key}`);
|
|
188
|
+
L.push(` 文档值 ${valueText(d.value)}`);
|
|
189
|
+
L.push(` 产出物 ${d.best ? d.best.source + ' :: ' + d.best.path : '-'}`);
|
|
190
|
+
L.push(` 实际值 ${d.best ? valueText(d.best.value) : '-'} score=${d.score != null ? Number(d.score).toFixed(2) : '-'} ${d.cmp ? d.cmp.reason : ''}`);
|
|
191
|
+
L.push(` 原文 ${d.sentence}`);
|
|
192
|
+
L.push('');
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
L.push('漂移明细:无');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const missing = report.claims.filter((x) => x.status === 'ref-missing');
|
|
199
|
+
if (missing.length) {
|
|
200
|
+
L.push('引用型产出物缺失');
|
|
201
|
+
L.push('─'.repeat(78));
|
|
202
|
+
for (const m of missing.slice(0, top)) {
|
|
203
|
+
L.push(` ${pad(m.file + ':' + m.line, 46)} ${valueText(m.value)}`);
|
|
204
|
+
}
|
|
205
|
+
L.push('');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const high = report.lint.filter((f) => f.severity === 'high');
|
|
209
|
+
if (high.length) {
|
|
210
|
+
L.push('用语线索 · high');
|
|
211
|
+
L.push('─'.repeat(78));
|
|
212
|
+
for (const f of high.slice(0, top)) {
|
|
213
|
+
L.push(` ${pad(f.file + ':' + f.line, 46)} ${pad(f.ruleId, 8)} ${f.matched}`);
|
|
214
|
+
}
|
|
215
|
+
L.push('');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const dups = report.duplicateClaims || [];
|
|
219
|
+
if (dups.length) {
|
|
220
|
+
L.push('重复声明(同一文件内完全相同的数字,身份用 #n 区分)');
|
|
221
|
+
L.push('─'.repeat(78));
|
|
222
|
+
for (const d of dups.slice(0, top)) {
|
|
223
|
+
L.push(` ${pad(d.file + ':' + d.lines.join(','), 46)} ${pad(d.kind, 8)} ${d.key} = ${d.value}`);
|
|
224
|
+
L.push(' 同一个值出现了 ' + d.lines.length + ' 次;不是错误,但请确认不是复制粘贴的残骸。');
|
|
225
|
+
}
|
|
226
|
+
L.push('');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
L.push('─'.repeat(78));
|
|
230
|
+
L.push(`门禁策略 ${report.gate.policy} → ${report.gate.failed ? 'FAILED' : 'PASSED'}`);
|
|
231
|
+
if (report.meta.history && report.meta.history.legacyDropped) {
|
|
232
|
+
L.push('提示:history 已升级到 schema ' + report.meta.history.schema
|
|
233
|
+
+ ',' + report.meta.history.legacyDropped + ' 条旧记录(行号 id)已丢弃;本次是新基线。');
|
|
234
|
+
}
|
|
235
|
+
if (report.meta.docsMissing.length) {
|
|
236
|
+
L.push(`注意:以下 root 不存在,已跳过 → ${report.meta.docsMissing.join(', ')}`);
|
|
237
|
+
}
|
|
238
|
+
return L.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderMarkdown(report, top) {
|
|
242
|
+
const L = [];
|
|
243
|
+
const c = report.counts;
|
|
244
|
+
L.push('# verifact 核验报告');
|
|
245
|
+
L.push('');
|
|
246
|
+
L.push(`- 生成时间:${report.generatedAt}`);
|
|
247
|
+
L.push(`- 耗时:${report.durationMs} ms(缓存 ${report.meta.cache},worker ${report.meta.workers})`);
|
|
248
|
+
L.push(`- 文档 ${c.docs} 份 / 产出物条目 ${c.artifacts} 条 / 声明 ${c.claims} 条`);
|
|
249
|
+
L.push(`- 已核验 ${c.verified} · 漂移 ${c.drift} · 候选冲突 ${c.ambiguous} · 未绑定 ${c.unbound} · 引用存在 ${c.refFound} · 引用缺失 ${c.refMissing}`);
|
|
250
|
+
L.push(`- 用语线索:high ${report.lintCounts.high} / medium ${report.lintCounts.medium} / low ${report.lintCounts.low}`);
|
|
251
|
+
L.push(`- 门禁:${report.gate.policy} → ${report.gate.failed ? '**FAILED**' : 'PASSED'}`);
|
|
252
|
+
L.push('');
|
|
253
|
+
const drifts = report.claims.filter((x) => x.status === 'drift');
|
|
254
|
+
L.push('## 漂移明细');
|
|
255
|
+
L.push('');
|
|
256
|
+
if (!drifts.length) {
|
|
257
|
+
L.push('无。');
|
|
258
|
+
} else {
|
|
259
|
+
L.push('| 位置 | 模块 | 键 | 文档值 | 产出物 | 实际值 | 评分 |');
|
|
260
|
+
L.push('|---|---|---|---|---|---|---|');
|
|
261
|
+
for (const d of drifts.slice(0, top)) {
|
|
262
|
+
L.push(`| ${d.file}:${d.line} | ${d.module || '-'} | \`${d.key}\` | ${valueText(d.value)} | \`${d.best ? d.best.source : '-'}\` | ${d.best ? valueText(d.best.value) : '-'} | ${d.score != null ? Number(d.score).toFixed(2) : '-'} |`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
L.push('');
|
|
266
|
+
L.push('## high 级用语线索');
|
|
267
|
+
L.push('');
|
|
268
|
+
const high = report.lint.filter((f) => f.severity === 'high');
|
|
269
|
+
if (!high.length) L.push('无。');
|
|
270
|
+
else {
|
|
271
|
+
L.push('| 位置 | 规则 | 命中 | 原文 |');
|
|
272
|
+
L.push('|---|---|---|---|');
|
|
273
|
+
for (const f of high.slice(0, top)) {
|
|
274
|
+
L.push(`| ${f.file}:${f.line} | ${f.ruleId} | ${f.matched} | ${String(f.sentence).replace(/[\\|]/g, c => '\\' + c).slice(0, 80)} |`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
L.push('');
|
|
278
|
+
L.push('> 本报告只陈述「文档写的值」与「产出物里的值」是否一致,以及两者的位置。');
|
|
279
|
+
L.push('> 它不判定哪一方正确,也不声明任何漏洞。');
|
|
280
|
+
return L.join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function cmdVerify(rawArgv, args) {
|
|
284
|
+
const flags = args.flags;
|
|
285
|
+
const cwd = path.resolve(flags.cwd || process.cwd());
|
|
286
|
+
const cfg = loadConfig({ cwd, configPath: flags.config, overrides: buildOverrides(rawArgv, flags) });
|
|
287
|
+
const log = logger.create({ quiet: !!flags.quiet });
|
|
288
|
+
const report = await engine.run(cfg, {
|
|
289
|
+
deep: !!flags.deep,
|
|
290
|
+
noCache: !!flags['no-cache'],
|
|
291
|
+
allowScanErrors: !!flags['allow-scan-errors'],
|
|
292
|
+
concurrency: flags.concurrency ? Number(flags.concurrency) : undefined,
|
|
293
|
+
logger: log
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const fmt = flags.format || 'text';
|
|
297
|
+
const top = flags.top ? Number(flags.top) : 20;
|
|
298
|
+
let rendered;
|
|
299
|
+
if (fmt === 'json') rendered = JSON.stringify(report, null, 2);
|
|
300
|
+
else if (fmt === 'md') rendered = renderMarkdown(report, top);
|
|
301
|
+
else rendered = renderText(report, top);
|
|
302
|
+
|
|
303
|
+
if (flags.out) {
|
|
304
|
+
fs.mkdirSync(path.dirname(path.resolve(cwd, flags.out)), { recursive: true });
|
|
305
|
+
fs.writeFileSync(path.resolve(cwd, flags.out), rendered, 'utf8');
|
|
306
|
+
if (fmt !== 'text') process.stdout.write('written: ' + flags.out + '\n');
|
|
307
|
+
else process.stdout.write(rendered + '\n');
|
|
308
|
+
} else {
|
|
309
|
+
process.stdout.write(rendered + '\n');
|
|
310
|
+
}
|
|
311
|
+
return report.gate.failed ? 1 : 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function cmdExplain(rawArgv, args) {
|
|
315
|
+
const id = args._[1];
|
|
316
|
+
if (!id) throw new UsageError('explain 需要声明 id,例如 README.md:L28:ratio:kat');
|
|
317
|
+
const flags = args.flags;
|
|
318
|
+
const cwd = path.resolve(flags.cwd || process.cwd());
|
|
319
|
+
const cfg = loadConfig({ cwd, configPath: flags.config, overrides: buildOverrides(rawArgv, flags) });
|
|
320
|
+
const report = await engine.run(cfg, { noCache: !!flags['no-cache'], logger: logger.create({ quiet: true }) });
|
|
321
|
+
const hit = report.claims.find((c) => c.id === id || (c.file + ':' + c.line) === id);
|
|
322
|
+
if (!hit) {
|
|
323
|
+
process.stdout.write('未找到声明 ' + id + '\n');
|
|
324
|
+
process.stdout.write('可用 id 示例:\n');
|
|
325
|
+
for (const c of report.claims.slice(0, 10)) process.stdout.write(' ' + c.id + '\n');
|
|
326
|
+
return 2;
|
|
327
|
+
}
|
|
328
|
+
const L = [];
|
|
329
|
+
L.push('声明 ' + hit.id);
|
|
330
|
+
L.push(' 文件行 ' + hit.file + ':' + hit.line);
|
|
331
|
+
L.push(' 模块 ' + (hit.module || '-'));
|
|
332
|
+
L.push(' 键 ' + hit.key + ' tokens=' + JSON.stringify(hit.keyTokens));
|
|
333
|
+
L.push(' 类型 ' + hit.kind + (hit.subtype ? '/' + hit.subtype : ''));
|
|
334
|
+
L.push(' 文档值 ' + valueText(hit.value) + ' ' + JSON.stringify(hit.value));
|
|
335
|
+
L.push(' 判定 ' + (STATUS_LABEL[hit.status] || hit.status) + ' score=' + (hit.score != null ? Number(hit.score).toFixed(3) : '-'));
|
|
336
|
+
L.push(' 依据 ' + (hit.note || '-'));
|
|
337
|
+
if (hit.best) {
|
|
338
|
+
L.push(' 命中产出物');
|
|
339
|
+
L.push(' source ' + hit.best.source);
|
|
340
|
+
L.push(' path ' + hit.best.path);
|
|
341
|
+
L.push(' value ' + valueText(hit.best.value) + ' ' + JSON.stringify(hit.best.value));
|
|
342
|
+
}
|
|
343
|
+
if (hit.alternatives && hit.alternatives.length) {
|
|
344
|
+
L.push(' 其他候选');
|
|
345
|
+
for (const a of hit.alternatives) {
|
|
346
|
+
L.push(' ' + Number(a.score).toFixed(3) + ' ' + a.source + ' :: ' + a.path + ' = ' + valueText(a.value));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
L.push(' 原文 ' + hit.sentence);
|
|
350
|
+
process.stdout.write(L.join('\n') + '\n');
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function cmdDiff(rawArgv, args) {
|
|
355
|
+
const flags = args.flags;
|
|
356
|
+
const cwd = path.resolve(flags.cwd || process.cwd());
|
|
357
|
+
const cfg = loadConfig({ cwd, configPath: flags.config, overrides: buildOverrides(rawArgv, flags) });
|
|
358
|
+
const runs = readRuns(cfg, cwd, 60);
|
|
359
|
+
if (runs.legacyDropped) {
|
|
360
|
+
process.stdout.write('注意:history 里 ' + runs.legacyDropped + ' 条旧 schema 1 记录已忽略'
|
|
361
|
+
+ '(行号 id 无法映射到内容寻址键,见 #96)。本次比较只使用 schema 2 记录。\n');
|
|
362
|
+
}
|
|
363
|
+
if (runs.length < 2) {
|
|
364
|
+
process.stdout.write('历史里不足两次运行,先跑两次 verify 再 diff。当前:' + runs.length + ' 次\n');
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
const back = flags.since ? Number(flags.since) : 1;
|
|
368
|
+
const curr = runs[runs.length - 1];
|
|
369
|
+
const prev = runs[Math.max(0, runs.length - 1 - back)];
|
|
370
|
+
const d = diffRuns(prev, curr);
|
|
371
|
+
|
|
372
|
+
const L = [];
|
|
373
|
+
L.push('verifact diff');
|
|
374
|
+
L.push('─'.repeat(78));
|
|
375
|
+
L.push(`基准 ${String(prev.generatedAt).replace('T', ' ').slice(0, 19)} 对比 ${String(curr.generatedAt).replace('T', ' ').slice(0, 19)}`);
|
|
376
|
+
L.push(`drift ${pad(prev.counts.drift, 5)} → ${pad(curr.counts.drift, 5)} verified ${pad(prev.counts.verified, 5)} → ${pad(curr.counts.verified, 5)} unbound ${pad(prev.counts.unbound, 5)} → ${pad(curr.counts.unbound, 5)}`);
|
|
377
|
+
if (!prev.states) {
|
|
378
|
+
L.push('注意:基准那次运行没有逐条状态快照(v0.1 之前写的,或首次运行)。');
|
|
379
|
+
L.push(' 未收录的条目一律按 unbound 解释,所以会显示一大批「新增」。本次结果即为新基线。');
|
|
380
|
+
}
|
|
381
|
+
L.push('');
|
|
382
|
+
|
|
383
|
+
const section = (title, list) => {
|
|
384
|
+
L.push(`${title}(${list.length})`);
|
|
385
|
+
if (!list.length) { L.push(' 无。'); L.push(''); return; }
|
|
386
|
+
for (const c of list.slice(0, Number(flags.top) || 30)) {
|
|
387
|
+
L.push(' ' + pad(c.from, 12) + ' → ' + pad(c.to, 12) + ' ' + c.key);
|
|
388
|
+
}
|
|
389
|
+
L.push('');
|
|
390
|
+
};
|
|
391
|
+
section('新增 drift', d.newDrift);
|
|
392
|
+
section('已消除的 drift', d.resolvedDrift);
|
|
393
|
+
section('新增 verified', d.newVerified);
|
|
394
|
+
section('verified 退化为其他', d.lostVerified);
|
|
395
|
+
section('新增 ref-missing', d.newMissingRef);
|
|
396
|
+
|
|
397
|
+
if (!d.all.length) L.push('两次运行之间没有状态变化。');
|
|
398
|
+
process.stdout.write(L.join('\n') + '\n');
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function cmdServe(rawArgv, args) {
|
|
403
|
+
const flags = args.flags;
|
|
404
|
+
const cwd = path.resolve(flags.cwd || process.cwd());
|
|
405
|
+
const cfg = loadConfig({ cwd, configPath: flags.config, overrides: buildOverrides(rawArgv, flags) });
|
|
406
|
+
const server = require('../src/server/api');
|
|
407
|
+
await server.start(cfg, logger.create({ quiet: !!flags.quiet }));
|
|
408
|
+
// 服务已在监听:这里必须挂住进程,否则 main() 立刻返回 0 会把进程退出
|
|
409
|
+
return new Promise(() => {});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function cmdInit(args) {
|
|
413
|
+
const dir = path.resolve(args.flags.dir || process.cwd());
|
|
414
|
+
const target = path.join(dir, 'verifact.json');
|
|
415
|
+
if (fs.existsSync(target)) {
|
|
416
|
+
process.stdout.write('已存在 ' + target + ',未覆盖\n');
|
|
417
|
+
return 0;
|
|
418
|
+
}
|
|
419
|
+
const sample = {
|
|
420
|
+
docs: { roots: ['../README.md', '../docs'] },
|
|
421
|
+
artifacts: { roots: ['../tools', '../reports'] },
|
|
422
|
+
modules: ['ML-KEM-768', 'SM2', 'SM3', 'SM4', 'VWZ'],
|
|
423
|
+
resolve: { minScore: 0.45 },
|
|
424
|
+
bindings: [],
|
|
425
|
+
ignore: [],
|
|
426
|
+
gate: { failOn: 'drift' }
|
|
427
|
+
};
|
|
428
|
+
fs.writeFileSync(target, JSON.stringify(sample, null, 2) + '\n', 'utf8');
|
|
429
|
+
process.stdout.write('written: ' + target + '\n');
|
|
430
|
+
return 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function main() {
|
|
434
|
+
const rawArgv = process.argv.slice(2);
|
|
435
|
+
const args = parseArgs(rawArgv);
|
|
436
|
+
if (args.flags.help || !args._.length) {
|
|
437
|
+
process.stdout.write(HELP);
|
|
438
|
+
return args._.length ? 0 : 2;
|
|
439
|
+
}
|
|
440
|
+
const cmd = args._[0];
|
|
441
|
+
try {
|
|
442
|
+
if (cmd === 'verify') return await cmdVerify(rawArgv, args);
|
|
443
|
+
if (cmd === 'explain') return await cmdExplain(rawArgv, args);
|
|
444
|
+
if (cmd === 'diff') return await cmdDiff(rawArgv, args);
|
|
445
|
+
if (cmd === 'serve') return await cmdServe(rawArgv, args);
|
|
446
|
+
if (cmd === 'init') return cmdInit(args);
|
|
447
|
+
if (cmd === 'version') { process.stdout.write(require('../package.json').version + '\n'); return 0; }
|
|
448
|
+
throw new UsageError('未知子命令: ' + cmd);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
if (e && e.code) {
|
|
451
|
+
process.stderr.write(JSON.stringify(e.toJSON ? e.toJSON() : { error: 'error', message: e.message }) + '\n');
|
|
452
|
+
return e.code === 'usage_error' || e.code === 'config_error' ? 2 : 1;
|
|
453
|
+
}
|
|
454
|
+
process.stderr.write(JSON.stringify({ error: 'internal_error', message: (e && e.message) || String(e) }) + '\n');
|
|
455
|
+
return 1;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (require.main === module) {
|
|
460
|
+
main().then((code) => process.exit(code)).catch(() => process.exit(1));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = { main, renderText, renderMarkdown, parseArgs };
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verifact",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "事实哨兵 — 把文档里的硬数字声明绑定到真实产出物上逐条核验,漂移即红。",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "刘天赫 <research@fibemate.net>",
|
|
7
|
+
"homepage": "https://github.com/Lennonhaha/fibemate-tools/tree/main/verifact#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Lennonhaha/fibemate-tools.git",
|
|
11
|
+
"directory": "verifact"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Lennonhaha/fibemate-tools/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"verification",
|
|
18
|
+
"documentation",
|
|
19
|
+
"drift-detection",
|
|
20
|
+
"audit",
|
|
21
|
+
"evidence",
|
|
22
|
+
"claims",
|
|
23
|
+
"supply-chain",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"type": "commonjs",
|
|
27
|
+
"main": "src/engine.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"verifact": "bin/verifact.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin/",
|
|
33
|
+
"src/",
|
|
34
|
+
"verifact.json.example",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"VERIFACT-DESIGN.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"verify": "node bin/verifact.js verify",
|
|
41
|
+
"serve": "node bin/verifact.js serve",
|
|
42
|
+
"test": "node test/run.js"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|