LogTool::Course::Aggregator 按等级累计计数时,内部可以用散列;但报表协议不能暴露散列遍历顺序。当前 Course 入口固定 TOTAL 在第一行,出现过的等级按 rows 返回的排序行输出,行尾是 LF,字段隔符是 TAB,最后显式编码为 UTF-8。fixtures/course.tsv 通过 import 后有 4 条 accepted 记录,报表金标是 TOTAL<TAB>4、ERROR<TAB>1、INFO<TAB>2、WARN<TAB>1,每行后跟 LF。未出现的等级不输出零计数行,只有 TOTAL 总是存在。

examples/perl-language/bin/logtool report --input accepted.jsonl 不另造一套统计规则:它读取 JSON Lines,向 LogTool::Course::Aggregator 逐条 add,再把 TOTAL 与排序后的 rows 拼成 TSV 字符串。tools/replay.pl --case all 的 report case 逐字节比对该金标,并记录 stdout SHA-256:系统 Perl 5.34 与精确 5.38.2 复跑均得到同一个报表 SHA-256 505cc75323483dc3a7dcece7815c7bbc80623b5f06bab0ca2a892f7f265fde52。

图:JSON Lines 聚合成确定性报表字节

1
2
3
accepted JSONL -> read_jsonl -> Aggregator->add
|
+-> TOTAL + sorted rows -> encode UTF-8 -> STDOUT / --output

守恒反例也进入 replay:bad-json 不能生成部分报表,missing-file 不能假装空输入成功,input-output-alias 不能把输入文件也当输出目标。旧报表保护由 tools/replay.pl 的 bad-json case 写入 OLD 后复查;文件发布细节再由 cli-faults.py 和工程测试覆盖 close、rename、目录输出、缺父目录等边界。这里的结论只到“报表字节顺序和失败不覆盖旧目标已复跑”,不宣称断电持久化。

练习:若输入本来没有 DEBUG,还需要写 DEBUG<TAB>0 吗?依本工具金标不需要;要改这个决定,就必须改数据契约、replay 金标和报表出口,不能只给其中一个命令补行。复跑证据见 completion/evidence/semantics/*replay-all.*;参考 Perl 5.38.2 pod/perlfunc.pod 的 sort、rename,以及 pod/perlunicode.pod 的编码边界。