196 lines
9.5 KiB
Markdown
196 lines
9.5 KiB
Markdown
# 本地清洗实验如何保存 Markdown、审计和 diff
|
||
|
||
## 1. 它解决什么问题
|
||
|
||
内存流水线可以安全地产生 `TransformResult`,但进程结束后,评审者仍需要打开清洗后的完整 Markdown、查看总 diff,
|
||
并追溯每条修改属于哪个组件、为什么修改、修改前后是什么。
|
||
|
||
当前本地实验层把这些结果保存到独立目录,同时继续保持三个边界:
|
||
|
||
- 组件和 `Pipeline` 仍然不读写文件;
|
||
- 输入文件永远不被覆盖;
|
||
- 只有 `success` 文档才产生正式的清洗后 Markdown。
|
||
|
||
已经实现的范围来自已批准的
|
||
[`0005-local-experiment-runner-and-artifacts.md`](../design/0005-local-experiment-runner-and-artifacts.md) 和
|
||
[`0007-local-markdown-reviewer.md`](../design/0007-local-markdown-reviewer.md)。
|
||
精确字段、校验和函数签名以 `src/mdpolish/` 中的代码与测试为准。
|
||
|
||
## 2. 保存与评审怎样解耦
|
||
|
||
```text
|
||
experiment.py
|
||
├── pipeline.py 只负责内存清洗
|
||
├── reporting.py 只负责 JSON、行列和 unified diff
|
||
└── artifact_store.py 只负责日期目录、权限和原子发布
|
||
│
|
||
▼
|
||
已发布运行目录
|
||
│
|
||
▼
|
||
reviewer/ 只通过发布后的文件做本地只读评审
|
||
```
|
||
|
||
- `experiment.py` 严格读取调用方显式列出的 UTF-8 Markdown,逐份调用同一个 `Pipeline`;
|
||
- `reporting.py` 重放并校验 `Change` 的快照链,再生成机器可读审计和人可读 diff;
|
||
- `artifact_store.py` 不理解清洗规则,只把已经生成的字节写入私有临时目录,校验后一次性发布。
|
||
- 同仓库 `reviewer/server/` 只共用无文件 I/O 的 Python 快照重放模块,不导入组件或流水线;它读取 manifest、result、
|
||
成功输出和本机定位文件。
|
||
|
||
因此,新增组件不会改变文件层;调整目录布局不会影响清洗和报告;修改 JSON 或 diff 时也不需要碰流水线。
|
||
ClinDB 的 5 份论文、历史 arXiv 单组件组合和当前 first-batch 组合只存在于两个仓库内实验脚本,通用模块没有硬编码
|
||
论文名或业务组件。
|
||
|
||
## 3. 输入怎样保持原样
|
||
|
||
实验层先完成整批预检:
|
||
|
||
1. 验证日期、运行 ID、文档 ID 和来源标签;
|
||
2. 确认所有路径存在、是普通文件且没有重复;
|
||
3. 以二进制读取全部输入;
|
||
4. 使用严格 UTF-8 解码;
|
||
5. 比较原始字节 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. 产物怎样组织
|
||
|
||
```text
|
||
artifacts/
|
||
└── <YYYY-MM-DD>/
|
||
└── runs/
|
||
└── <run_id>/
|
||
├── manifest.json
|
||
├── review-locator.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` 为准。
|
||
|
||
`review-locator.json` 只记录本次运行目录、每份输入的绝对解析路径和输入哈希,供本地评审器重新找到完整原文。它不复制
|
||
原文,不替代 manifest 或 result,也不作为可移植运行身份。绝对路径可能泄露本机目录结构,因此该文件同样是本地敏感数据。
|
||
|
||
## 6. 为什么 reporter 要重放修改
|
||
|
||
第二个组件看到的是第一个组件修改后的快照,因此后续 `Change.span` 不一定对应最初输入。为了生成准确行列,
|
||
reporter 从输入开始,按组件批次重放修改:
|
||
|
||
1. 当前文本哈希必须等于该批次 `before_sha256`;
|
||
2. 每条范围内的原文必须等于 `before`;
|
||
3. 同一批次按核心相同的从后向前顺序应用;
|
||
4. 结果哈希必须等于 `after_sha256`;
|
||
5. 全部批次完成后必须等于 `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 和其他真实数据没有因此获得输出授权。
|
||
|
||
同仓库只读页面的路径验证、组件快照重放和使用边界见
|
||
[`local-markdown-reviewer.md`](local-markdown-reviewer.md)。
|
||
|
||
## 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`](../guides/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`](../guides/run-local-clindb-first-batch-experiment.md)。实验层的文件、
|
||
JSON、diff 和权限契约没有因组件增多而改变。
|
||
|
||
2026-08-23 又产生运行 `clindb-first-batch-reviewer-v1`,用于验证新定位文件和本地页面:5/5 文档为 `success`,合计
|
||
155 条修改,输入运行前后哈希不变。评审器 API 校验了 5 份原文、成功输出以及 8 个组件形成的 40 个阶段,所有文本哈希
|
||
均与审计一致。实际页面启动步骤见 [`review-local-cleaning-run.md`](../guides/review-local-cleaning-run.md)。
|