Files
CADx-4/research-wiki/任务书/01-装配两个库.md
T
iomgaa fa962a2672 docs: 任务书 01 装配两个库;待定问题 06 超声描述中段的造影提示
- 任务书 01:配置、delivery 的 T1–T5 与 C01–C13 检查、library/answers 布局、
  快照、build、测试、规模与审阅;已过一轮「硕士生阅读」并按意见修订
- C11 只认段首的结论标题;中段「造影提示:」按负责人 2026-10-16「先不进行任何处理」
  原样保留,记入待定问题 06 待问医生
- 结构.md 第五部分补「只有 library 引用 PIL」;WORK_LOG 更新下一步

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:14:04 -04:00

302 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 01:装配两个库(不含 CEUS 结果)
读入 40 例交付包,逐例检查,把检查全部通过的病例装配成两个库:给 AI 专家看的资料库(`library/`,化名、三路分开)和只给评价用的标准答案库(`answers/`,金标准、化名对照、跳过清单)。同时落地参数系统(`config/build.yaml`、`config/code_maps.yaml`)。这一步不处理 CEUS 视频:超声一路只放报告「所见」和医生截的关键图;程序从视频里算出的关键帧和灌注指标(「CEUS 结果」),等「CEUS 定量」那一步(`结构.md` 第七部分第 3 步)完成后,由 build 重新构建一次再加进去。
对应 `research-wiki/explanation/结构.md` 第七部分第 2 步。要写的 delivery、library、answers、runs、build 都是 `src/cadx/<模块名>/` 下的子包,各自职责见 `结构.md` 第三部分。这是本仓库第一份任务书,前提是骨架已完成(提交 5eb38df、1f7a6a3)。
按 `AGENTS.md`「两档标准」,这一步决定专家能看见什么、金标准怎么读,是**账本级**代码:写完要派两个互相独立的全新上下文子 Agent 审阅(见「审阅」)。
来源:
- 负责人 2026-10-16 定:先做「装配两个库」;数据格式和口径以 40 例交付包为准,原话「因为数据改过很多版,所以你们需要对齐“40例交付包”那个」(design 0002 决定 5)。
- 负责人 2026-10-16 定:配置「每段一份」;交付包里的数字编码列「晚点我会找他们要映射表」,解码做成一个可选的超参数,映射表是一份独立的配置文件(`结构.md` 第五部分「配置」)。
- 负责人 2026-10-16 定:同一个人在不同表里本应一样的值(金标准、年龄等)要逐例比对;检查没过的病例「都是遇到整个病例跳过,让医生核对去吧(可以记录是哪一个检查没过……因为遇到第一个就整个病例跳过了)」(`AGENTS.md`「错误怎么处理」、`结构.md` 第三部分)。
- 负责人 2026-10-16 定:用化名时,「保存金标准那个库中需要保存化名与真名的映射」。
- 其余做法(检查项的具体内容和顺序、文件格式、构建号、半成品目录、化名形式)是监督者 2026-10-16 提出的,负责人确认任务书时一并认可。
## 先认识几个词
- **四类胰腺囊性肿瘤**:IPMN(导管内乳头状黏液性肿瘤)、SPN(实性假乳头状肿瘤)、MCN(黏液性囊性肿瘤)、SCN(浆液性囊性肿瘤)。项目要 AI 专家判断病灶属于哪一类,所以资料里出现这些名字就等于泄露答案。**金标准**是术后病理给出的类别,交付表里叫「病理类型」或 `Diagnosis`。
- **CEUS、MR**:超声造影视频(往静脉打造影剂后录的超声);磁共振检查,本项目只有它的文字报告。
- **临床表的化验**:`CA199`(CA19-9)和 `CEA` 是肿瘤标志物,`Amylase` 淀粉酶、`Lipase` 脂肪酶反映胰腺炎症,`TBil` 总胆红素反映胆道是否梗阻。每项配一个 `*_Level` 档位列(0/1,看起来是正常/异常,未经临床方确认)。`Symptom_*` 是症状,`Hx_*` 是既往史(`Hx_Family_PC` 是胰腺癌家族史)。
- **推注开始时间s**:超声表里的一列,视频第 0 秒时造影剂已经推注了多少秒,「CEUS 定量」那一步挑关键帧要用。这一步只检查它是非负整数,不放进资料库。
- **监督者**:和负责人讨论方向、写任务书并验收执行结果的会话。执行者是拿到这份任务书写代码的你。
- **交付包**:临床方交来的一批原始资料,目前是 `data/交付包_20260908_40例/`,含三张汇总表(临床工作簿、超声报告文本、MR 报告文本)和 40 个病例目录(每个目录有一张医生截的超声关键图、一段 CEUS 视频、一份单例报告表)。只读,不进 git。
- **病例号**:临床表里是 `P001` 这种写法,两张报告表的「患者ID」是不补零的 `1`、`12`,病例目录名是 `001`,视频文件名用 `U001`。它们指同一个病人,规范化后统一成三位补零的 `001`。
- **编码列**:交付包里用数字表示类别的列,例如临床表的 `Sex` 是 1/2、症状列是 0/1。交付包没写这些数字代表什么,映射表要向临床方要。
- **构建**:运行一次 `scripts/build.py`,产出一对同号的 `library/<构建号>/` 和 `answers/<构建号>/`。构建完不再修改,要改就重新构建。
- **化名**:给专家的资料里用来代替病例号的随机名字。真实病例号会暴露交付顺序,而顺序可能和诊断有关(`AGENTS.md` 硬性约束 1)。
- **结论段**:报告里医生写的诊断倾向(「印象」「提示」之类)。它接近答案,不能给专家。
- **跳过清单**:检查没过、没进资料库的病例,以及各自没过的是哪一项。交负责人转给医生核对。
- **账本级**:`AGENTS.md`「两档标准」里出错会悄悄污染结论的那一档:每条规则要有独立预期值的测试,运行产物只追加。
## 开工前先读
- `AGENTS.md`:全部,尤其是「硬性约束」1–3、「不设默认值」「罕见情况怎么办」「错误怎么处理」「测试」。
- `research-wiki/explanation/结构.md`:第二部分(整体框架图)、第三部分(delivery、library、answers、runs、build 的职责,依赖表,「几条边界」)、第四部分(资料库布局与可见性表、答案库内容)、第五部分(目录、「配置」、文件格式、导入契约)、第六部分(规模)。
- `research-wiki/design/0002-四段流水线与两个库.md`:决定 5、7。
- `research-wiki/待定问题/03`、`04`。
- `data/交付包_20260908_40例/交付说明_40例_20260908.md`。
- 旧代码(只读,只借规则,按普通函数重写;行号以 `reference/CADx-4-CAD/` 为准):
- `app/providers/workbook.py:48-99`:按列白名单读表,先扫表头记位置,每行只取白名单列,报错时不回显原始行。照这个思路读。
- `app/providers/delivery.py:79-120`:病例号规范化。它不认不补零的写法,这里要认。
- `app/providers/modalities.py:87-91,135-141,216-274`:结论段和亚型名的识别。旧规则只认段首的标题;这里沿用,见下文 C11。
- `app/providers/clinical.py:118-130`:化验原值与档位成对检查。
- 旧代码在 40 例上读不进去的原因(工作表名写死 `Sheet1`、编号列写死「研究编号」、病例目录里的 xlsx 报错)都是因为它按旧样本 `1.2-TEST` 写死;这里按 40 例交付包写,不做兼容。
**病人资料不进任何文字**:报告、提交说明、测试、日志里只写结构和计数,不写报告原文、病例号、诊断值。终端输出同样只给计数。
## 一、配置
### 依赖
在 `pyproject.toml` 加:`pydantic`、`pyyaml`、`openpyxl`、`pillow`。用 `uv add` 加,版本下限取当前版本。
### `config/build.yaml`
字段定义写在 build 里(pydantic,`strict=True`、`extra="forbid"`、`frozen=True`,每个字段必填、无默认值;`结构.md` 第五部分「配置」):
YAML 里的路径是字符串,路径字段是唯一允许从字符串转成 `Path` 的地方(字段级关掉 strict),其余字段一律不转换类型。
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `delivery_dir` | 路径 | 交付包目录 |
| `library_root` | 路径 | 资料库的上级目录,构建产物放在它下面的 `<构建号>/` |
| `answers_root` | 路径 | 答案库的上级目录 |
| `alias_seed` | 整数 | 生成化名的随机种子 |
| `decode_codes` | 布尔 | 编码列是否按映射表解码 |
| `code_maps` | 路径或 null | 映射表位置。`decode_codes` 为 true 时必须是路径,为 false 时必须是 null,不符就报错 |
配置里的相对路径相对仓库根目录解析。提交一份真实可用的 `config/build.yaml`,`decode_codes: false`、`code_maps: null`。
### `config/code_maps.yaml`
格式:每个编码列一项,列名映射到「数字 → 含义」:
```yaml
Sex: {1: ..., 2: ...}
Symptom_Pain: {0: ..., 1: ...}
```
需要映射的编码列共 16 列,名单是 delivery 里的常量:`Sex`;`Symptom_Pain`、`Symptom_Jaundice`、`Symptom_WeightLoss`、`Symptom_Fever`、`Symptom_Asymptomatic`;`Hx_Pancreatitis`、`Hx_Alcohol`、`Hx_Gallstones`、`Hx_Malignancy`、`Hx_Family_PC`;`CA199_Level`、`CEA_Level`、`Amylase_Level`、`Lipase_Level`、`TBil_Level`。
- 映射表必须恰好覆盖这 16 列,多一列或少一列都整次构建停下(T5)。`Sex` 的含义必须是「男」「女」两个汉字,才能和报告表的性别比对(C10)。
- 这 16 列的名单定义在 delivery(它是交付包形状的一部分);T5 由 build 读配置时检查,因为映射表是配置。
- 映射表还没拿到,**这一步不提交 `config/code_maps.yaml`**,不编造含义。解码路径只用测试里的样例映射表验证。
- 诊断编码不在映射表里:两张报告表的列名「病理类型(1 IPMN,2 SPN,3 MCN,4 SCN)」已经写明了含义,delivery 把这个对应关系写成常量,同时核对列名原文和常量一致(T3),所以不受 `decode_codes` 控制。临床表的 `Diagnosis` 列没有说明,它和报告表逐例一致(C10)才进库,所以同样适用这组含义。
## 二、delivery:读交付包、逐例检查
delivery 只读交付包,交出每例读到的内容和检查结果,不决定谁能看见什么(`结构.md` 第三部分)。只有 delivery 引用 `openpyxl`。
### 交付包的形状(全部写成 delivery 里的常量)
| 文件 | 工作表 | 表头(第 1 行,顺序和名字都要一字不差) |
| --- | --- | --- |
| `Clinical_agent/临床工作簿_40例.xlsx` | `Sheet` | `Case_ID`、`Sex`、`Age`、`Diagnosis`、`Symptom_Pain`、`Symptom_Jaundice`、`Symptom_WeightLoss`、`Symptom_Fever`、`Symptom_Asymptomatic`、`Hx_Pancreatitis`、`Hx_Alcohol`、`Hx_Gallstones`、`Hx_Malignancy`、`Hx_Family_PC`、`CA199`、`CA199_Level`、`CEA`、`CEA_Level`、`Amylase`、`Amylase_Level`、`Lipase`、`Lipase_Level`、`TBil`、`TBil_Level`(共 24 列,监督者 2026-10-16 用只输出表头的脚本核过) |
| `CEUS_agent/超声报告文本_40例.xlsx` | `超声报告` | `患者ID`、`CEUS视频ID`、`性别`、`年龄`、`病理类型(1 IPMN,2 SPN,3 MCN,4 SCN)`、`推注开始时间s`、`超声描述` |
| `MR_agent/MR报告文本_40例.xlsx` | `MR报告` | `患者ID`、`性别`、`年龄`、`病理类型(1 IPMN,2 SPN,3 MCN,4 SCN)`、`MR描述` |
| `CEUS_agent/<三位病例号>/` | — | 病例目录,内容见 C04 |
| `CEUS_agent/<三位病例号>/U<号>_roi2_report.xlsx` | `Sheet1` | 与超声汇总表相同的 7 列 |
文件名里的 `roi2` 是交付方的版本标记(视频上带医生画的病灶框),这一步不用视频,只检查它在不在。
### 整张表的问题:整次构建停下
这些问题出现时一例也读不出来,没有「跳过这一例」可言。报错写清是哪个文件、哪一项不符。
| 编号 | 检查 |
| --- | --- |
| T1 | 三张汇总表都存在 |
| T2 | 每张汇总表恰好有上表那一个工作表,名字一致 |
| T3 | 表头与上表一字不差(含诊断列名里写的编码含义) |
| T4 | `Clinical_agent/`、`MR_agent/` 下各只有自己那张汇总表;`CEUS_agent/` 下只有超声汇总表和三位数字命名的病例目录。多出来的文件说明交付方式变了,不能悄悄忽略 |
| T5 | `decode_codes` 为 true 时,映射表恰好覆盖 16 个编码列,`Sex` 映射到「男」「女」(由 build 检查,见第一部分) |
交付包根目录的 `交付说明_*.md` 不读。
### 单个病例的问题:跳过这一例,记录第一项没过的检查
先找出编号认不出的行(C01):这种行不知道是哪个病人,在跳过清单里单独记一条,病例号一栏写「表名第 N 行」,不计入病例数。其余行规范化后,病例号取三张表和病例目录的并集,这就是「病例总数」。同一个病人如果在某张表里编号写坏了,他在 C01 记一条,在 C03(缺在那张表)再记一条,这是预期的。
类型按 openpyxl 读出的 Python 类型判断,不做转换:要求整数的格读出来是 `float`(如 `45.0`)也算不过;字符串不去首尾空格。
每一例按下表顺序检查,**遇到第一项没过就停止检查这一例**,跳过整例,在跳过清单里记下这一项(`AGENTS.md`「错误怎么处理」)。后面的检查没做过,不代表没问题;跳过清单的说明文字里要写明这一点。
| 编号 | 检查 | 记录的涉及值 |
| --- | --- | --- |
| C01 | 编号能规范化:临床表认 `P` 加三位数字;两张报告表认不补零或补零的纯数字(字符串或整数);病例目录认三位数字。规范化后在 1–999 之间 | 认不出的原始写法和所在表、行号 |
| C02 | 同一张表里这个编号只出现一次 | 出现的表和行号 |
| C03 | 三张表和病例目录里都有这一例 | 缺在哪里 |
| C04 | 病例目录恰好有三个文件:`U<号>_key.png`;一个视频,名字是 `U<号>_roi2.mp4` 或 `U<号>.mp4`(交付说明里写明 033 例是后一种);`U<号>_roi2_report.xlsx`。这里的 `<号>` 与目录号相同 | 实际的文件名列表 |
| C05 | 超声表的 `CEUS视频ID` 是 `U<号>`,与病例号一致 | 两者的值 |
| C06 | 单例报告表:工作表 `Sheet1`,表头同超声汇总表,恰好 1 行数据,7 格与汇总表对应行逐格相等 | 不相等的列和两边的值 |
| C07 | 临床表取值,见表下的分条 | 不合规的列和值 |
| C08 | 超声表取值:`性别` 是「男」或「女」;`年龄` 形如「数字岁」;诊断是 1–4;`推注开始时间s` 是非负整数;`超声描述` 是非空字符串 | 不合规的列和值 |
| C09 | MR 表取值:`性别`、`年龄`、诊断同 C08;`MR描述` 是非空字符串 | 不合规的列和值 |
| C10 | 跨表一致:诊断三张表两两相等;年龄三张表相等(报告表去掉「岁」后比);性别超声表与 MR 表相等;`decode_codes` 为 true 时,临床表解码后的 `Sex` 也要与超声表相等 | 对不上的字段和各表的值 |
| C11 | 报告文字(超声描述、MR 描述)里没有结论段标题:先做 NFKC(全角转半角)归一,再查「关键词 + 可选空白 + 冒号」,出现在全文开头或换行之后(段首)即不过。关键词:超声提示、超声诊断、影像学诊断、检查结论、诊断意见、印象、结论、诊断、意见、提示 | 哪份报告、命中的关键词、命中处所在的句子 |
| C12 | 报告文字里没有亚型名:NFKC 归一并转大写后,不含 `IPMN`、`SPN`、`MCN`、`SCN`、导管内乳头状、实性假乳头状、黏液性囊、粘液性囊、浆液性囊 | 哪份报告、命中的词 |
| C13 | 报告文字里没有人口学信息(影像两路不该看到性别、年龄,`结构.md` 第四部分):不含「数字岁」,不含「男」「女」两个字 | 哪份报告、命中的词 |
C07 的分条:
- `Sex` 是 1 或 2;`Age` 是正整数;`Diagnosis` 是 1–4;
- 5 个症状、5 个既往史是 0 或 1,不能空;
- 5 对化验(原值和档位):两格都空表示没测,可以;两格都有值时,原值是数字(整数或浮点),档位是 0 或 1;只有一格有值不过;
- `decode_codes` 为 true 时,16 个编码列里非空的值都要在映射表里。
几点说明:
- 跳过清单和复核清单写在 `answers/` 里,只在本机、不进 git,可以写原值和原句,医生核对要用。报告、终端、日志、提交说明里只写计数和检查编号。
- C13 禁单字「男」「女」:40 例实测两份报告文字都没有这两个字,描述病灶也用不到它们,出现了多半是写了患者性别。
- 第 C10 项是负责人点名要的:同一个人在不同表里本应一样的值对不上,大概率是录入错误,要拿去找医生改。`decode_codes` 为 false 时临床表的性别没法和报告表比,构建清单里写明「临床性别未比对」。
- C11 沿用旧代码的规则,只认段首。40 例的报告正文都没有换行,这条在 40 例上不会触发。监督者 2026-10-16 用只输出位置的脚本核过:40 份超声描述里有 1 份在正文中段有「造影提示:」(冒号后还有 46 个字),MR 描述里没有。这句是不是医生的诊断倾向、该不该给专家,负责人要先问医生(`待定问题/06`);在那之前按负责人 2026-10-16 的决定**不做任何处理**,这一例照常进库,原文不改。它会因为含「提示」出现在复核清单里。这一条定下来之前,不能用这次构建做正式会诊。
- C12 不查「浆液性」「黏液性」单独出现:这两个词在描述囊液性状时是正常用语。40 例实测上表的词都没出现。
- 报告里出现「考虑、可能、倾向、提示」(不带冒号)不算检查不过,只列进复核清单(见第四部分),由负责人过目。40 例实测超声 1 份、MR 3 份。
delivery 对外交出:每个通过的病例的读入内容(三张表各一行的白名单列、病例目录里关键图的路径),和每个没过的病例的跳过记录(编号、检查编号、检查说明、文件、列、涉及值)。读入时只取白名单列:临床表除 `Case_ID`、`Diagnosis` 外的列进临床一路,诊断三列单独交出,供答案库用。
## 三、library:给专家的资料库
格式按 `结构.md` 第四部分。这一步的内容:
```
library/<构建号>/
manifest.json
cases/<化名>/
clinical/clinical.json
ultrasound/report.txt
ultrasound/key.png
mr/report.txt
```
- `clinical.json`:`{"codes_decoded": true 或 false, "fields": {列名: 值, ...}}`。`fields` 的键是临床表除 `Case_ID`、`Diagnosis` 以外的 22 列列名。编码列在 `codes_decoded` 为 false 时写原始数字,为 true 时写映射表里的含义;化验没测写 null。字段名给模型时要不要改写成自然语言,由会诊任务书定。
- `report.txt`:超声描述、MR 描述原文,UTF-8,不加任何标题和编号。
- `key.png`:解码像素后按原来的颜色模式重新编码成 PNG,不带任何文本块、EXIF 和 ICC 等附加数据。超声帧上烧录的信息栏不遮蔽(`AGENTS.md` 硬性约束 3)。
- `manifest.json`:构建号、git 提交、配置文件的 SHA-256、`codes_decoded`、未比对项(如「临床性别未比对」)、所用 CEUS 结果的来源(这一步写 null)、病例化名列表(按化名排序)、每个文件的相对路径和 SHA-256。不写真实病例号、交付包里的文件名、诊断。
- `manifest.json` 里「配置文件的 SHA-256」:`decode_codes` 为 true 时同时记映射表的 SHA-256。
- **读取**:按路读出,一路一个对象(临床、超声、MR 各一个类型;超声对象里关键图给文件路径,怎么交给模型由会诊定),读时核对文件的 SHA-256 与清单一致。另提供列出一次构建的全部化名。拒绝读取名字以 `.partial` 结尾的目录。
只有 library 引用 `PIL`。
## 四、answers:标准答案库
```
answers/<构建号>/
manifest.json 构建号(与 library 同一个)、本目录其余全部文件(含 snapshot/ 下的)的 SHA-256
snapshot/ 运行快照(第五部分)
aliases.csv 化名、病例号(三位)、视频号(U 号,「CEUS 定量」的结果按视频号接回病例)
diagnoses.csv 化名、病例号、临床表诊断编码、超声表诊断编码、MR 表诊断编码、诊断名(按诊断编码常量)
skipped.csv 病例号或「表名第 N 行」、检查编号、检查说明、文件、列、涉及值(JSON 字符串)
review.csv 化名、路(超声/MR)、命中的词、所在句子
```
- `skipped.csv` 是交给医生核对的东西。文件开头不能加注释行(CSV),「每例只记第一项没过的检查,其余检查没做」这句说明写进 `manifest.json` 的 `skipped_note` 字段,构建结束时也在终端打印。
- `review.csv` 只扫进库的病例:报告文字做 NFKC 归一后,按「。」和「;」(NFKC 会把全角分号转成半角)切成句子,含「考虑」「可能」「倾向」「提示」任一词的句子各记一行。
- 读取:评价要用的读取函数(读化名对照、读诊断)在 answers 里提供,读时核对 SHA-256。其余读取等评价任务书再加。
## 五、runs:构建号与快照
runs 这一步只做快照部分,会诊运行目录等第 4 步再加。
- **构建号**:`YYYYMMDD-HHMMSS-<git 短哈希>`,工作区有未提交改动时末尾加 `-dirty`。时间用本地时间。
- **快照**(写进 `answers/<构建号>/snapshot/`):`snapshot.json` 记构建号、完整 git 提交号、工作区是否干净、`git diff HEAD` 输出的 SHA-256、Python 版本、`uv.lock` 的 SHA-256、开始和结束时间;同目录复制 `build.yaml` 原文,`decode_codes` 为 true 时同时复制映射表原文,各记 SHA-256。
- 工作区不干净照样允许构建,但必须如实记下(负责人 2026-10-16 认可)。
## 六、build 与 `scripts/build.py`
build 是唯一同时碰到原始资料和答案的地方,也是决定谁能看见什么的唯一地方(`结构.md` 第三部分)。
1. 读配置;检查 T5(编码列名单取自 delivery)。
2. 调 delivery 读交付包,拿到通过的病例和跳过记录。
3. 生成化名:用 `random.Random(alias_seed)` 为每个通过的病例生成化名:4 个字符,每个字符从 `a–z0–9` 里抽;和已有化名重复、或含 `ipmn`、`spn`、`mcn`、`scn` 子串的就重抽(否则会被泄露扫描误报)。先把病例号排好序再依次抽,同一个种子、同一批病例得到同样的化名。
4. 按 `结构.md` 第四部分的可见性表写资料库:临床一路只写 `clinical.json`;超声一路只写超声描述和关键图;MR 一路只写 MR 描述。性别、年龄只在临床一路。
5. 写答案库和快照。
6. 两个库先写进 `<根目录>/<构建号>.partial/`,全部写完、清单算完后再改名为 `<构建号>/`:先改答案库,最后改资料库。资料库的 `<构建号>/` 出现,才算这次构建完成;会诊只读资料库,所以答案库改名成功、资料库没改成的情况,会诊看不到这次构建。目标目录已存在就报错,不覆盖。中途出错留下的 `.partial` 不删,由人看。
7. 终端只打印:构建号、两个库的路径、病例总数、进库病例数、跳过病例数、认不出编号的行数、按检查编号分的跳过数、复核清单条数、跳过清单的说明句。不打印病例号和任何资料内容。
`scripts/build.py` 只接收 `--config <路径>` 一个参数(`结构.md` 第五部分「配置」)。
## 七、测试
预期值照本任务书的文字写,不从实现里推(`AGENTS.md`「两档标准」)。测试不读 `data/` 下的真实资料。
**测试用交付包**:写一个测试辅助函数,在 pytest 临时目录里用 openpyxl 生成一个形状与 40 例完全相同的假交付包(3–5 例,报告文字是编造的、不涉及真实病人;关键图用 Pillow 生成小图并带上一个 PNG 文本块)。各条测试在它的基础上改出要测的情况。
至少覆盖:
- **T1–T5**:各一条,整次构建停下,`library/`、`answers/` 下都没有留下非 `.partial` 的目录。
- **C01–C13**:各一条,只让一例违反这一项,断言这一例被跳过、跳过记录的检查编号正确、其余病例正常进库。C10 分诊断、年龄、性别三种各一条;C07 包括化验只空一列;C11 包括全角冒号、换行后的段首两种,另加一条:关键词加冒号出现在正文中段时不算不过、这一例正常进库。
- **只记第一项**:一例同时违反 C08 和 C10,跳过记录只有 C08。
- **可见性**:资料库里性别、年龄只出现在临床一路;超声、MR 两路的文件里不出现临床表的任何列名。
- **泄露扫描**:library 提供一个扫描函数:给定一次构建的目录、禁止出现的字符串列表、禁止的 JSON 键名列表,返回命中(文件、命中的词)。字符串按子串扫全部 `.txt`、`.json` 文件(含 `manifest.json`),不扫 PNG(PNG 的附加数据由「关键图」那条测试管);键名按整个键名、不分大小写比较。测试里用一份独立写的词表(不 import delivery 的常量)调用它,断言不出现:本次构建任一病例的病例号(`P001`、`U001` 两种写法)、交付包里的文件名、四种亚型的缩写和中文名、诊断列名、`Diagnosis`、`Case_ID`;键名不出现 `diagnosis`、`label`、`gold`、`pathology`、`name`、`row`。再写一条测试证明扫描函数本身管用:往资料库塞一个泄露,它能查出来。
- **化名**:同一个种子构建两次,化名相同;换种子,化名不同;化名都是 4 位小写字母加数字,互不相同。
- **解码开关**:false 时 `clinical.json` 是原始数字、`codes_decoded` 为 false;true 时按样例映射表解码;`decode_codes` 与 `code_maps` 不配套(true 配 null,false 配路径)时配置读取报错。
- **关键图**:资料库里的 `key.png` 颜色模式和像素与原图相同,PNG 文本块没有了。
- **清单与读取**:改坏资料库里的一个文件,library 读取时报错;`.partial` 目录读取时报错。
- **不覆盖**:同一个构建号目录已存在时报错。
**真实 40 例验收**(执行者跑,不进 `make check`):
- 用提交的 `config/build.yaml` 跑一次 `scripts/build.py`,报告终端输出。
- 写一个 `scripts/scan_library.py --library <资料库目录> --answers <答案库目录>`:从答案库读出这次构建的真实病例号和视频号,加上脚本里自己写的亚型名和列名词表,调 library 的扫描函数扫资料库,只输出命中的条数和类别,不输出命中的内容。这个脚本同时读两个库,只能放在 `scripts/`。
- 报告跳过数按检查编号分的计数。监督者事先知道的情况:C10 应该有 6 例左右(`待定问题/04`),C11 应该是 0 例,复核清单里有一条来自那份含「造影提示:」的超声描述(见第二部分说明)。实际数字和这里不一样就如实报告,不改规则去凑。
## 八、import-linter 契约
在 `pyproject.toml` 加(`结构.md` 第三部分依赖表、第五部分):
1. delivery、library、answers、runs 不引用其他 `cadx` 模块;
2. build 只引用 delivery、library、answers、runs(ceus 这一步还没有,不写);
3. 在 `cadx` 里只有 delivery 引用 `openpyxl`;
4. 在 `cadx` 里只有 library 引用 `PIL`。
顺手把 `pyproject.toml` 里「规划中的契约见结构.md 第四节」改成「第五部分」。
## 规模
预期(不含测试,`结构.md` 第六部分计数口径):
| 模块 | 预期行数 |
| --- | --- |
| delivery | 400–700 |
| library | 150–300 |
| answers | 100–200 |
| runs(只含快照部分) | 50–150 |
| build(含配置定义) | 300–500 |
| `scripts/build.py` | ≤ 100 |
| `scripts/scan_library.py` | ≤ 150 |
超过区间上限的两倍就停下检查:是需求本身复杂,还是专门处理罕见情况的分支在变多。
完成后更新:
- `research-wiki/explanation/结构.md`:第六部分填实际行数;第八部分补这五个模块对外的函数和类型,每项一句话;第五部分「配置」表里 `build.yaml` 一行如有出入,按实际写。
- `research-wiki/guides/装配两个库.md`:怎么改配置、怎么跑、产物在哪、跳过清单怎么看(常青文档,过一轮「硕士生阅读」)。
- `WORK_LOG.md`。
不新建其他 md 文件。
## 审阅
写完、`make check` 通过后,按顺序:
1. 派一个全新上下文的子 Agent 审阅。只给它:本任务书、`AGENTS.md`、`结构.md` 第三到第五部分、`git --no-pager diff` 的完整输出。不给它看你的思路。让它找:专家能看到答案、真实病例号、另一路资料的具体路径;检查没过的病例却进了库;跳过清单记错检查项;构建产物被覆盖或改动的具体场景。
2. 账本级,再派一个全新上下文的子 Agent 独立审一轮,材料相同,不给它看第一轮的意见。
3. Codex 可用时,再用 `/codex:rescue --fresh --wait` 做一轮对抗审查。
每条意见先问「日常使用中会不会真撞上」「不改会怎样」:会悄悄让答案漏进资料库或让病例进错库的,修,修了附测试;只会报错停下或一眼能看出来的,不加代码,在报告里列出来。子 Agent 不写 md 文件,结论直接回复;它们的 prompt 里写明本机 `grep`、`find` 用 `/usr/bin/` 下的版本。
## 报告
做完后本地提交(不推送),在回复里交一份报告,监督者会对着实际文件逐项核对。已验证的事实、推断、未验证的部分分开写。
1. 提交号;新增和修改的文件列表,每个模块的行数。
2. `make check` 的完整输出。
3. 真实 40 例构建:终端输出原文、构建号、两个库的路径、按检查编号分的跳过计数、复核清单条数、`scripts/scan_library.py` 的输出。不写病例号和资料内容。
4. 两轮审阅(和 Codex,如有)的意见,以及每条怎么处理:修了的附测试名,没修的写「不改会怎样」。
5. 你和本任务书、`结构.md`、`AGENTS.md` 不一致的地方,以及为什么。
6. 规则不清楚的地方:哪里让你犹豫、哪里两份文件说法不一致、哪里是靠猜的。没有就写「没有」。