TOOL RELEASE

verifact:声明对齐证据

2026-09-27 · 文章 GPL-3.0 / 工具 Apache-2.0 · GitHub

一篇文档在发布前被合并——内容里的数字是过时的。改代码的人不会记得去更新文档;写文档的人不会知道代码变了。哨兵不解决这个问题的根源,但它让「忘了更新」变成「不让你合」。于无声处。——题记

FIBEMATE 系列。此前讲过 CBOM(盘点用了哪些算法)、密码学时间账本(证据在何时存在)、密码时间机器(查 git 历史)。这一篇解决最后一个问题:文档里写的数字,和产出物对不对得上。 这不是抽查,是把文档传唤到证据面前——每条声明要么对上产出物,要么交代自己为什么对不上。

工具叫 verifact,事实哨兵。已开源,Apache-2.0,零依赖(Node ≥ 18)。

问题:文档会漂移

仓库里到处是硬数字:性能基准、测试计数、状态数、文件行数。文档写在某个时刻,产出物在另一个时刻重测,两边的数字就开始各走各的。

真实例子,2026-09-27 实测:

前两条是文档落后于代码。第三条更麻烦:一个没有出处的数字,挂在最显眼的位置。

人工核对这类问题靠 grep 和肉眼。文档 341 份、声明 2251 条,肉眼核对每轮发稿都来一遍不现实。这就是工具要占的位置。

设计:抽声明、绑产出物、六态判定

工作分三步。

抽取。扫描 markdown(默认也查代码块外的正文),把带单位的数字连同键名抽出来:103µs、26,115 states、654 行。每条声明记文件、行号、键名、数值。

绑定。把声明和产出物对接。产出物来源在配置里登记:benchmark 的 JSON/日志、主仓 scripts、时间戳目录、核心库。绑定按键名相似度和数值容差打分(阈值 minScore: 0.45,相对容差 0.5%)。

判定。六种结果,只有一种是红的:

判定含义处置
verified文档值和产出物在容差内一致绿
drift绑定成功但数值超出容差红,门禁拦截
ambiguous多个产出物候选冲突,或跨模块同名键交人工
unbound找不到产出物依据交人工
引用存在 / 缺失声明引用 lg-XXX 这类实体,查它在不在扫描范围存在绿 / 缺失交人工

这套分级的关键是不把「没证据」当「错」。unbound 1836 条不等于文档在说谎——可能证据没导出成机器可读格式,可能是叙述性文字。把 unbound 也算失败,工具会在第一天就被噪音淹没。红只留给一种情况:找到了依据,但数字对不上。

实测

2026-09-27,对 fibemate 全仓 + 待发布文章 + benchmark 产出物跑全量:

文档 341 份 产出物 5369 条 声明 2251 条
verified 145 drift 24 ambiguous 227 unbound 1836
引用型 19(存在 17 / 缺失 2) 另有 17 条声明经 ignore 规则收敛、不计入六态
核验率 7.2%
用语线索 high 5 medium 117 low 10
耗时 4.1s(workers=6,冷缓存)

核验率的计算口径:(verified 145 + 引用存在 17)÷ 可判定声明 2251。可判定 = 六态合计,被 ignore 规则收敛的 17 条不计入。计算在 bin/verifact.js:117,读者可以自己验。

核验率 7.2%,不高。这是诚实的上限:能自动核验的前提是产出物机器可读,而大多数声明(叙事、对比、非数值)没有对应格式。工具的作用是守住有依据的那部分,不是假装能核全部。

回归测试 19 项(test/run.js),全部 PASS。

在 CI 里:sentinel.yml

本地跑工具只是第一步——人不一定记得跑。真正的门禁在 CI。

主仓新增 sentinel.yml:任何 PR 改了 www/docs/**、packages/pqc-kem/**、packages/fml-dsa/**,自动触发。作业内容一条命令:

node bin/verifact.js verify --docs ../../fibemate/www/docs --fail-on drift --no-cache

双仓 checkout(主仓 + fibemate-tools),两个 Action 都用 40 位 SHA 钉死版本;--no-cache 保证每次全量、不吃缓存红利;--fail-on drift 把红锁定在唯一一种该红的判定上。比成功,CI 绿;比出 drift,CI 红——不让你合。

ignore 机制在这里发挥作用:当前 12 条规则、收敛 17 条声明,每条规则带 note 说明为什么——比如「RSA-2048 那一行的 60ms,与 ML-KEM keygen 无关」「文档引的是 JS 基准,verifact 查到的是硬件基准,两套数据都对,口径不同」。ignore 不是掩盖,是把「口径差异」和「真过期」分开登记,将来口径统一了再撤。

一个插曲:xdiff

哨兵本来还有一个作业——xdiff(实现差分检测:同一个算法,两个实现各跑一遍,看输出是否逐字节一致)。它在本地跑是对的,但 CI 里硬编码了本地绝对路径 D:/FIBEMATE/...,CI 上找不到实现文件,一直假红。

两个选项:

选了后者。xdiff 从 sentinel.yml 拆除(2026-09-27,workflow 里留了注释说明原因),等它支持相对路径再挂回来。有的门禁挂一个 continue-on-error: true 看起来体面,实际上更不体面。CI 门禁要么拦,要么不存在,中间状态是噪音。

三条守门线

三条守门线,各自不重叠。

抓到过什么

工具投产两周,实际拦下的:

  1. 头条数字无出处。p95=78.5ms 出现在主仓 README 和官网 pqc-readiness、FAQ、architecture 四处,产出物里无依据。处理方式二选一:补实测证据,或改写口径——发稿前必须处理。
  2. 15-31x 性能声明的底数漂移。PROGRESS.md 的 103µs 对不上产出物的 262μs,「提升 15-31x」的倍数声明跟着动摇。
  3. TLA+ 不变式 3 vs 7。发布物和模型文件各说各话。
  4. lg-079 引用缺失。两个文档副本指向同一份本地查不到的时间戳实体,收敛成 1 条待办。

用语线索单独说。抽取器顺带查绝对化措辞,今天的 high 级 5 条:「全球唯一」「绝对安全」「零风险」。这类词在任何审计语境都站不住,位置和上下文一起列出来,改稿时逐条清。

边界

使用

git clone https://github.com/Lennonhaha/fibemate-tools
cd fibemate-tools/verifact

node bin/verifact.js init                          # 生成配置样例
node bin/verifact.js verify                         # 全量核验,exit 0=过 / 1=有drift / 2=配置错
node bin/verifact.js verify --fail-on unbound       # 收紧门禁——慎用
node bin/verifact.js explain <文件>:<行>:<键>        # 单条溯源
node bin/verifact.js diff                           # 和上一轮对比
node bin/verifact.js serve                          # 本地看板

配置里 deny 和 ignore 两段是人工复核后的沉淀:deny 拦已知误绑,ignore 收敛同名键的语义冲突。每条带 note,说明为什么。

参考实践