Files
iomgaa bcd5932a66 docs: 诊断编码改归解码开关管
负责人 2026-10-16「诊断编码归开关管,实现统一」:映射表加 Diagnosis 一项(17 项),
管三张表的诊断列;不再从报告表列名取编码含义;decode_codes 为 false 时答案库诊断名留空。

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

30 KiB
Raw Permalink Blame History

任务书 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

格式:每个编码列一项,列名映射到「数字 → 含义」:

Sex: {1: ..., 2: ...}
Symptom_Pain: {0: ..., 1: ...}

需要映射的编码共 17 项,名单是 delivery 里的常量:Sex;Diagnosis(同时管临床表的 Diagnosis 列和两张报告表的「病理类型」列,三列编码相同);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。

  • 映射表必须恰好覆盖这 17 项,多一项或少一项都整次构建停下(T5)。Sex 的含义必须是「男」「女」两个汉字,才能和报告表的性别比对(C10);Diagnosis 的含义必须恰好是 IPMN、SPN、MCN、SCN 四个,评价拿它和专家的结论比。
  • 这 17 项的名单定义在 delivery(它是交付包形状的一部分);T5 由 build 读配置时检查,因为映射表是配置。
  • 映射表还没拿到,这一步不提交 config/code_maps.yaml,不编造含义。解码路径只用测试里的样例映射表验证。
  • 诊断编码和其他编码列一样归 decode_codes 管(负责人 2026-10-16:「诊断编码归开关管,实现统一」)。报告表列名里写着「1 IPMN,2 SPN,3 MCN,4 SCN」,T3 只把它当表头原文核对,程序不从列名里取含义。诊断不进资料库,所以这个开关对诊断只影响答案库。

二、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 时,映射表恰好覆盖 17 项编码,Sex 映射到「男」「女」,Diagnosis 映射到四个亚型缩写(由 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(decode_codes 为 true 时还要在映射表的 Diagnosis 里);推注开始时间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 时,临床表各编码列里非空的值都要在映射表里。

几点说明:

  • 跳过清单和复核清单写在 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 表诊断编码、诊断名(`decode_codes` 为 true 时按映射表解码,为 false 时留空)
  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,diagnoses.csv 的诊断名留空;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. 规则不清楚的地方:哪里让你犹豫、哪里两份文件说法不一致、哪里是靠猜的。没有就写「没有」。