8.1 KiB
本地清洗实验如何保存 Markdown、审计和 diff
1. 它解决什么问题
内存流水线可以安全地产生 TransformResult,但进程结束后,评审者仍需要打开清洗后的完整 Markdown、查看总 diff,
并追溯每条修改属于哪个组件、为什么修改、修改前后是什么。
当前本地实验层把这些结果保存到独立目录,同时继续保持三个边界:
- 组件和
Pipeline仍然不读写文件; - 输入文件永远不被覆盖;
- 只有
success文档才产生正式的清洗后 Markdown。
已经实现的范围来自已批准的
0005-local-experiment-runner-and-artifacts.md。
精确字段、校验和函数签名以 src/mdpolish/ 中的代码与测试为准。
2. 三层怎样解耦
experiment.py
├── pipeline.py 只负责内存清洗
├── reporting.py 只负责 JSON、行列和 unified diff
└── artifact_store.py 只负责日期目录、权限和原子发布
experiment.py严格读取调用方显式列出的 UTF-8 Markdown,逐份调用同一个Pipeline;reporting.py重放并校验Change的快照链,再生成机器可读审计和人可读 diff;artifact_store.py不理解清洗规则,只把已经生成的字节写入私有临时目录,校验后一次性发布。
因此,新增组件不会改变文件层;调整目录布局不会影响清洗和报告;修改 JSON 或 diff 时也不需要碰流水线。 ClinDB 的 5 份论文、历史 arXiv 单组件组合和当前 first-batch 组合只存在于两个仓库内实验脚本,通用模块没有硬编码 论文名或业务组件。
3. 输入怎样保持原样
实验层先完成整批预检:
- 验证日期、运行 ID、文档 ID 和来源标签;
- 确认所有路径存在、是普通文件且没有重复;
- 以二进制读取全部输入;
- 使用严格 UTF-8 解码;
- 比较原始字节 SHA-256 与核心 Markdown SHA-256。
读取过程不剔除 BOM,不规范化 Unicode,不转换 \n、\r\n 或文件末尾换行。全部文档运行结束后,
实验层再次读取每份输入并比较原始字节和哈希;任何变化都会阻止产物发布。
输入缺失、不是普通文件、不是有效 UTF-8 或清单冲突属于整批预检失败。这时不调用流水线,也不创建最终运行目录。
4. 状态怎样决定产物
每份文档独立运行,某一份发生核心错误不会阻止其他已经预检的文档继续产生结果。
| 文档状态 | result.json |
cleaned.md |
changes.diff |
|---|---|---|---|
success |
有 | 有 | 有;零修改时为空文件 |
failed |
有 | 无 | 无 |
unstable |
有 | 无 | 无 |
failed 和 unstable 的审计仍保留已经实际发生的 Change、错误或残留候选,但不持久化
partial_markdown,避免半成品看起来像正式结果。
整批状态按 failed、unstable、success 的优先级汇总。即使其他文档有成功产物,只要一份失败,
manifest.json 就会把整批标为 failed。
5. 产物怎样组织
artifacts/
└── <YYYY-MM-DD>/
└── runs/
└── <run_id>/
├── manifest.json
└── documents/
└── <document_id>/
├── result.json
├── cleaned.md
└── changes.diff
日期取实验启动时本机时区中的日历日期。运行 ID 由调用方显式提供;同一日期下已经存在同名目录时拒绝覆盖。
manifest.json 是整次运行的索引,记录:
- 开始、完成和到期时间;
- 本机日期及 UTC 偏移;
- mdpolish、Python、平台和 Git 状态;
- 实际组件顺序、版本和参数;
- 每份输入的来源标签、前后哈希、状态、修改数量和产物相对路径;
- 整批成功、失败、不稳定和修改数量。
每份 result.json 保存实际修改、错误与残留候选。每条修改包含组件、候选引用、理由、Python 字符范围、
1-based 行列、before、after 和批次前后哈希。完整成功文本只存在于 cleaned.md。
changes.diff 是原始输入到最终成功输出的 unified diff,只用于人工查看。它不包含绝对路径或时间戳,
也不是修改重放的权威;机器审计仍以 result.json 为准。
6. 为什么 reporter 要重放修改
第二个组件看到的是第一个组件修改后的快照,因此后续 Change.span 不一定对应最初输入。为了生成准确行列,
reporter 从输入开始,按组件批次重放修改:
- 当前文本哈希必须等于该批次
before_sha256; - 每条范围内的原文必须等于
before; - 同一批次按核心相同的从后向前顺序应用;
- 结果哈希必须等于
after_sha256; - 全部批次完成后必须等于
TransformResult.current_sha256。
任何一步不一致都说明内存结果、reporter 或调用方式违反契约,整次运行不会发布最终目录。行列只是方便人查看的 派生信息,修改权威仍是快照绑定的 Python 字符范围。
7. 文件怎样安全发布
artifact store 先在同一日期的 runs/ 下建立本次专用临时目录。所有文件写入后都会重新读取校验,
成功 Markdown 还要再次核对输出 SHA-256,manifest 的身份、状态、计数和路径也必须与各文档审计及实际文件一致。
只有全部文件、清单和权限都通过,临时目录才会在 runs/ 目录协作锁内重新检查目标,并原子重命名为最终运行 ID。
当前 Linux 本地实现使用:
- 目录权限
0700; - 文件权限
0600; - 已存在的目标目录拒绝覆盖;
- 写入中途失败时不发布最终目录。
这保证不会发布已知不完整的结果,但不承诺跨平台断电耐久性或网络文件系统语义。
8. 隐私和保留边界
cleaned.md、diff 和 JSON 审计都可能包含真实原文,因此整个 artifacts/ 都是本地敏感数据:
- 受 Git 忽略;
- 不进入 Wiki、提交、推送或外部系统;
- 终端只显示状态、计数和目录;
- 默认保留 30 个日历日;
- manifest 记录
retention_until; - 第一版不自动删除,到期后仍需用户确认具体目录再清理。
当前只批准对 data/md/ 中 5 份论文副本保存产物。仓库外 GovDoc 和其他真实数据没有因此获得输出授权。
9. 已完成的真实验证
2026-08-22 使用 paper.arxiv_submission_stamp 1.0.0 对 5 份本地论文副本完成一次保存型实验:
| 项目 | 结果 |
|---|---|
| 运行 ID | clindb-arxiv-stamp-artifacts-v1 |
| 输出位置 | artifacts/2026-08-22/runs/clindb-arxiv-stamp-artifacts-v1/ |
| 文档状态 | 5/5 success |
| 实际修改 | sim 1 条、springer 1 条,其余 0 条 |
| 修改位置 | sim 1:1、springer 18:1 |
| 合法反向样例 | Springer 两处 arXiv preprint arXiv: 均保留 |
| 输出校验 | 5/5 cleaned.md 与 current_sha256 一致 |
| 输入只读 | 5/5 运行前后字节和哈希不变 |
| 权限 | 全部运行目录 0700,产物文件 0600 |
实际运行方法见
run-local-clindb-arxiv-experiment.md。
同日又使用 design/0006 的 8 组件流水线完成 ClinDB 第一批保存型实验:
| 项目 | 结果 |
|---|---|
| 运行 ID | clindb-first-batch-v1 |
| 输出位置 | artifacts/2026-08-22/runs/clindb-first-batch-v1/ |
| 文档状态 | 5/5 success |
| 实际修改 | dmp 47、ejhf 9、jama 79、sim 3、springer 17,共 155 条 |
| 第二次运行 | 5/5 success,合计 0 条修改 |
| 输出校验 | 5/5 cleaned.md 与 current_sha256 一致 |
| 输入只读 | 5/5 运行前后字节和哈希不变 |
| 内容反例 | JAMA Abstract 前内容不变;Springer 两条合法 arXiv 引用保留;图片引用文字不变 |
| 权限 | 运行目录 0700,产物文件 0600 |
当前完整运行方法见
run-local-clindb-first-batch-experiment.md。实验层的文件、
JSON、diff 和权限契约没有因组件增多而改变。