实现本地清洗实验与产物保存

This commit is contained in:
2026-08-22 17:25:44 +08:00
parent eed2119016
commit 6fcc7d5736
13 changed files with 2701 additions and 20 deletions
@@ -54,7 +54,7 @@ arXiv:<新版数字编号和版本> [<ASCII 分类>] <日> <英文月份缩写>
合成测试覆盖严格匹配、反向引用、首行/中间/末行、三种行尾、多个命中、围栏中仍删除、审计字段、确定性和
第二次运行零修改。测试只使用短小的虚构字符串,不含真实论文片段。
2026-08-22 对本地 5 份 ClinDB-ReviewBench Markdown 做了只读、纯内存复核:
2026-08-22 对本地 5 份 ClinDB-ReviewBench Markdown 做了只读、纯内存复核:
| 复核项 | 结果 |
| --- | --- |
@@ -64,13 +64,20 @@ arXiv:<新版数字编号和版本> [<ASCII 分类>] <日> <英文月份缩写>
| 第二次运行 | 5 份合计 0 条修改 |
| 源文件复读 | 5/5 与处理前内存内容一致,没有回写 |
本次没有保存清洗后 Markdown,没有把真实原文复制进测试、日志或仓库。安装、静态检查和完整测试命令仍只在根目录
这次早期复核没有保存清洗后 Markdown没有把真实原文复制进测试、日志或仓库。它是 `0004` 当时验证边界的
历史事实。
`0005` 批准本地产物机制后,同日又完成一次保存型实验:5 份文档全部为 `success`,仍然只修改 sim 和 springer
各一处;输出哈希全部匹配,输入运行前后字节不变,Springer 两处合法引用仍保留。产物位于
`artifacts/2026-08-22/runs/clindb-arxiv-stamp-artifacts-v1/`,只在本机保留并受 Git 忽略。具体产物结构和验证结果见
[`local-experiment-artifacts.md`](local-experiment-artifacts.md)。安装、静态检查和完整测试命令仍只在根目录
[`README.md`](../../README.md#当前可用检查) 维护。
## 6. 剩余边界
这个组件只证明第一条严格删除规则能够在公共核心上闭环,不表示论文已经清洗完成。HTML 实体、Word 批注、手稿
行号、断词、表格和参考文献间距仍未实现;文件输出、profile 和批处理也不存在。
行号、断词、表格和参考文献间距仍未实现。当前只有固定输入和固定组件的本地实验输出;通用文件接口、profile、
公共 CLI 和通用批处理仍不存在。
如果出现新的提交戳格式,默认行为是保留。必须先补充真实证据、反向样例和 design,再决定是否放宽模式,不能为了
提高命中数量直接修改正则表达式。
@@ -128,8 +128,10 @@
同一候选修改中的多条记录共享候选引用,同一组件批次中的所有记录共享批次前后哈希。记录只描述已经发生的修改;
验证失败或最终复查中没有执行的候选不会冒充实际改动。
这些内容当前只存在于内存返回值中。仓库没有 reporter、审计文件格式或日志持久化,调用方也不能默认把失败结果中
的部分文本写回原文件。
这些内容在核心中只存在于内存返回值中。核心外已经有一个获批的本地实验 reporter,可以校验修改链并把审计、
成功 Markdown 和 diff 保存到私有产物目录;机制见
[`local-experiment-artifacts.md`](local-experiment-artifacts.md)。这没有改变核心接口,也不允许把失败结果中的部分文本
写成正式输出或写回原文件。
## 8. 当前验证和剩余边界
@@ -146,7 +148,8 @@
- 除严格删除 arXiv 提交边栏戳外的其他论文、GovDoc 或 HTML 表格清洗组件;
- 独立文档检查、人工建议或审核流程;
- Markdown parser、AST 或共享业务中间表示;
- 文件读写、CLI、批处理、项目 profile 格式和生产集成;
- 审计结果的长期存储或脱敏输出协议。
- 通用文件输入、公共 CLI、通用批处理、项目 profile 格式和生产集成;
- 审计结果的长期存储、自动清理或脱敏输出协议。
这些边界中的任何一项要进入实现,都需要先用新的 design 明确语义、代价和验收方式。
当前只有一个固定数据和组件组合的本地实验脚本,不构成上述公共能力。这些边界中的任何一项要进入实现,都需要先用
新的 design 明确语义、代价和验收方式。
@@ -0,0 +1,156 @@
# 本地清洗实验如何保存 Markdown、审计和 diff
## 1. 它解决什么问题
内存流水线可以安全地产生 `TransformResult`,但进程结束后,评审者仍需要打开清洗后的完整 Markdown、查看总 diff
并追溯每条修改属于哪个组件、为什么修改、修改前后是什么。
当前本地实验层把这些结果保存到独立目录,同时继续保持三个边界:
- 组件和 `Pipeline` 仍然不读写文件;
- 输入文件永远不被覆盖;
- 只有 `success` 文档才产生正式的清洗后 Markdown。
已经实现的范围来自已批准的
[`0005-local-experiment-runner-and-artifacts.md`](../design/0005-local-experiment-runner-and-artifacts.md)。
精确字段、校验和函数签名以 `src/mdpolish/` 中的代码与测试为准。
## 2. 三层怎样解耦
```text
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 组件组合只存在于仓库内实验脚本,通用模块没有硬编码论文名。
## 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
└── 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 从输入开始,按组件批次重放修改:
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 和其他真实数据没有因此获得输出授权。
## 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)。