# 本地清洗实验如何保存 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/ └── / └── runs/ └── / ├── manifest.json ├── review-locator.json └── documents/ └── / ├── 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)。