diff --git a/docs/handoff/2026-08-04-adc-pool-level-01-input-binding.zh-CN.md b/docs/handoff/2026-08-04-adc-pool-level-01-input-binding.zh-CN.md new file mode 100644 index 0000000..33bf6c4 --- /dev/null +++ b/docs/handoff/2026-08-04-adc-pool-level-01-input-binding.zh-CN.md @@ -0,0 +1,235 @@ +# Handoff:ADC Pool Level 01 输入绑定与执行契约 + +- 日期:`2026-08-04` +- 任务分支:`task_20260804_adc-pool-level-01-input-binding` +- 基线:`main` @ `5e0458b` +- 前置:PR #57(Level 01 判据定义),已 `APPROVE` 并合并 +- 交付物类型:**contract-only** +- 授权范围:**仅绑定 raw 轴,不授权执行 Level 01**(第二轮审核后降级,见第十二节) +- 外部运行:**无。没有执行任何运行,没有产生任何 context、target、pair、disposition 或排序。** +- 架构变更:`NO_ARCHITECTURE_CHANGE`(依据是 diff 范围,可由 `git diff --stat` 核验) +- 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** + +## 一、本次范围 + +人类负责人要求给出 Level 01 的最终 output。核实后发现 Level 01 从未执行——PR #57 合并的是定义,不是结果,且 `BLOCK-02` 挡着执行。人类负责人选择「先起 CRC context 契约 PR」。 + +本次交付那个契约。但核实过程改变了它的形状,见下一节。 + +## 二、关键发现:不需要重跑枚举,`BLOCK-02` 的表述本身不准确 + +PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 2026-08-04 运行」。**这句话不准确,本 PR 予以更正。** + +核实结果:2026-08-02 的枚举运行早已通过 **PR #29 `APPROVE`**,批准记录 `logs/chatgpt-review-2026-08-02-crc-target-enumeration-results-final.md` 明确写着「Authorized: use external enumeration output as input to a new target-level evidence extraction task」;target 级证据抽取又通过 **PR #31 `APPROVE`**。所以存在一条完整的、未被隔离的输入链。 + +`BLOCK-02` 的正确表述是「不得使用 2026-08-04 那次运行的产物」,而不是「没有可用的 context」。我原先把「被隔离」误推为「无可用输入」,那是一次过度推广,与我在 PR #53 上犯过的「把针对 `ff943e7` 的检查推广成全局结论」是同一类错误。 + +**后果:Level 01 不需要任何新的外部枚举运行。** 原先我告诉人类负责人的「两个契约 + 两次运行」缩减为「一个契约 + 一次 Level 01 执行」。 + +## 三、仓库内交付了什么 + +| 文件 | 作用 | +|---|---| +| `docs/tasks/ADC_POOL_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md` | 输入绑定与执行契约(面向操作者,中文) | +| `docs/pools/adc_pool_level_01_input_binding.yaml` | 机器可读绑定:允许来源+SHA-256、禁止来源、状态上限、linkage 规则、输出验证 | +| `tests/test_adc_pool_level_01_input_binding.py` | 32 项校验,把绑定钉在已合并的 Level 01 契约与实际存在的批准记录上 | + +## 四、绑定的输入 + +| 来源 | 授权 PR | 提供 | 实测规模 | +|---|---|---|---| +| `gen_iet_crc_target_enumeration_20260802` | #29 `APPROVE` | raw contexts、raw targets | 9 indications(36 endpoint 行)、41 targets | +| `gen_iet_crc_target_evidence_20260801T2235EDT` | #31 `APPROVE` | linkage 证据 | 292 units、41 genes | + +10 个输入文件的 SHA-256 逐一记录,执行前必须校验,任一不一致即中止(`VAL-B05`)。 + +`indication_endpoint_target_pairs.tsv`(1,476 行)**不作为输入**——它按旧的 `indication + endpoint + target` 单元构建,而 Level 01 的单元是 `clinical context × target` 且 endpoint 不锁定。 + +两次被隔离的运行(#53、#54)明确列为 `barred_sources`,逐条写出禁止内容;`VAL-B06` 禁止那 4 个仅存在于被隔离运行的靶点出现在输出里。 + +## 五、三条实测得到的硬约束 + +**1. LOCK-02:最多只有 1 个 context 能 `eligible`。** +实测 9 个 context 的来源状态是 1 个 `canonical_c0`(confidence 0.93)、7 个 `derived_strategy`(`not_calibrated`)、1 个 `benchmark_subgroup`(`benchmark_only`)。PR #28 契约自身禁止「把 derived strategy 自动升级为 canonical clinical fact」,此处继承为 outcome 上限:未校准来源**强制 DEFER**,由测试机械保证不可能得到 RETAIN。但 LOCK-01 给出 0 个 eligible target(见第十二节),故 Eligible Universe Index 为 1 × 0 = **0**。 + +**2. LOCK-01:既有 `disposition` 列不可继承。** +`target_evidence_catalog.tsv` 的 `disposition` 取值是 `benchmark`(19)/`candidate`(16)/`hold`(6)——不是 `CandidateFilterResult`。它们由 PR #28 的五条最小筛选层产生,判据与 LOCK-01 不同,且 41 行的 `gate_score_status` 全为 `not_scored_in_enumeration_run`、`gate_pass_status` 全为 `not_assessed`。测试断言这三个标签与 `CandidateDisposition` 取值无交集,防止混读。 + +**3. LOCK-03:证据只有 target 级、疾病级,不是 pair 级、亚群级。** +实测 292 units = 41 genes × 7 dimensions + 5 opposing,**没有 indication/context 列**;方向为 supporting 88/opposing 32/unknown 172;**292 个单元全部 `machine_extracted_requires_human_review`**,20 个专家复核批次只完成 2 个、覆盖 4 个靶点。因此疾病级证据只能支撑 canonical context,不能建立亚群特异 linkage(`LNK-02`)。`no_known_linkage_after_complete_search` 本次不可用,因为检索范围未闭合(`VAL-B03` 禁止其出现)。 + +## 六、`DECISION-02`(请审核方裁决) + +**未经专家复核的 machine-extracted 证据是否满足 LOCK-03 的存在性?** 冻结为**满足**,附两条硬约束:每个 pair 必须携带 `linkage_evidence_review_status`;只有 machine-extracted 证据的 pair 可进 Level 01 active pool 但**不得晋级 Level 02**,直到通过专家复核。 + +理由:LOCK-03 问存在性不问有效性,每个单元都有 `source_id`/`source_path_or_url`/`evidence_locator` 可回溯,且 Level 01 召回优先。被否决的替代方案是「只有专家复核通过才算」——那 41 个靶点只剩 4 个可用,Level 01 报出接近空池,而每个靶点其实都有可回溯来源,反而失真。质量判断留给 Level 02,届时 review status 就在表里。 + +若审核方要严格方案,只需把 `machine_extracted_evidence_satisfies_existence` 置 `false`,规则与测试都已就位。 + +## 七、可以预见的结果形状(预先写明,避免误读) + +**本节已被第十二节取代。** 第二轮审核后的推算是:Raw Enumeration Matrix **369**;context 资格 eligible **1**/hold **8**;target 资格 eligible **0**/hold **41**/killed **0**;Eligible Universe Index **0**;Pool Level 01 active **0**。`CNT-02` 对账 1 × 0 = 0,`CNT-03` 对账 0 = 0 + 0 + 0。这不是一次被授权运行的预期产物,而是**不授权执行的理由**。 + +池子很小,真正的瓶颈不在漏斗设计,在**剩余 18 个专家复核批次**。这与 PR #54 M6「portfolio 受数据集限制而非 Gate 限制」一致,只是这次建立在已批准证据上。预先写明是为了避免结果出来后被误读为 Level 01 失效。 + +## 八、明确没有做什么 + +- **没有执行 Level 01**,没有任何 context、target、pair、disposition、排序或推荐。 +- 没有新的枚举运行;没有抓取文献、下载数据或运行分析。 +- 没有引用两次被隔离运行的任何产物。 +- 没有定义 Level 02/03;没有实现 PR #57 记录的六条缺口(`GAP-P01`..`GAP-P06`)。 +- 没有改 `BLOCK-02` 在 PR #57 文件里的原文——那是已获批准的历史记录,更正写在本 PR,不回写历史。 +- **没有补 #52/#53/#54/#57 的批准记录。** 人类负责人先要 Level 01,那件事中断在事实收集阶段、未写任何文件。已查明的事实一并留在这里,避免重做:#52 也没有记录(原以为只缺三份);**四个 PR 在 GitHub 上都没有 review 记录**(`/reviews` 返回空);已批准 head 与 merge commit 分别是 #52 `bfc04be`/`985edf8`、#53 `5318eca`/`09990c8`、#54 `8992563`/`58984e7`、#57 `6036c01`/`5e0458b`,其中 #54 与 #57 的合并 head 与获批 head 不同,差异都只是 main 经合并进入。四轮转述评审的逐字文本可从会话 transcript 恢复。 +- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 283)。仍属无关改动。 + +## 九、验证结果 + +- `Ran 283 tests` 全部通过(`main` 基线 251 + 本次新增 32)。 +- `scripts/verify_repository_boundary.sh`:`Repository boundary check passed.` +- `git diff --check`:通过;零 `__pycache__`。 +- 所有规模数字均由脚本读取外部产物实测:9 contexts、41 targets、369 pairs、36 endpoint 行、292 evidence units、41 genes、7 dimensions、supporting/opposing/unknown = 88/32/172、专家复核 2/20 批次覆盖 4 靶点、`cost_tier = low` 的 Gate 7 个。 +- 10 个输入文件的 SHA-256 由 `shasum -a 256` 实算。 + +## 十、当前阻断 + +- 本契约获 ChatGPT `APPROVE` 前,不得执行 Level 01。 +- `DECISION-02` 未获裁决前,执行者不得自行改用严格方案。 +- 本仓库不得写入候选池、快照、证据、cache、result 或 weights。 + +## 十一、第一轮审核裁决与修订(`REQUEST_CHANGES`,2026-08-04) + +ChatGPT 对 PR #58(HEAD `8f5c85d`)返回 `REQUEST_CHANGES`,两条阻断**全部接受**。已在同一 PR 内做最小修订,未夹带无关改动。 + +### 阻断 1(接受):LOCK-01 没有真正绑定到可执行证据 + +原契约只写「LOCK-01 必须独立推导」,没说怎么推导。审核方指出后果是实质的:41 个靶点最终有多少 eligible/hold/killed 取决于执行者自由解释,不可复现;而本 PR 又不授权新的检索,所以绑定只证明了「有 41 个 raw targets」,没证明能完成 LOCK-01。 + +先核实已批准层里到底有没有蛋白层面表面证据,再决定是补 mapping 还是降级授权。**结论是补 mapping**:`surface_reachability` 有 32 条来自 `transmembrane_segment_count` 的蛋白拓扑注释(direction `supporting`),9 条 `not_available`。 + +新增 `lock_01_derivation`:单一来源 `target_evidence_units.tsv`/`dimension = surface_reachability`/按 `gene_symbol` 连接/判决字段 `evidence_locator`;`barred_fields` 列出 7 个禁止参与推导的字段(含 `disposition`、`gate_score_status`、`gate_pass_status`);RETAIN 白名单只有 `transmembrane_segment_count`,`rna_derived_locators_may_retain: false`;四条规则 `L1-01`..`L1-04`,缺失、冲突、RNA 一律 DEFER。 + +**两个 outcome 声明为本次不可用**:`not_surface_target` 需要阳性的 negative topology 证据,已批准层没有任何一条断言某靶点不是表面蛋白,故**本次不得排除任何靶点**;`identity_unresolved` 需要身份解析结论字段,已批准层没有。完备性 32 + 9 = 41,零自由裁量、零排除。另加 `VAL-B07`/`VAL-B08`。 + + > **本段的 32 eligible 已被第十二节推翻。** 第二轮审核裁定跨膜段不足以判 eligible,现值为 **0 eligible + 41 hold + 0 killed**。原文保留以对照裁决前后差异。 + +### 阻断 2(接受):36 endpoint rows → 9 clinical contexts 的转换规则未冻结 + +审核方指出 369 的算术没问题但语义不唯一——不同执行者都能产出 9 个 context 而 identity 与字段内容不同。 + +新增 `clinical_context_projection`:身份只由 `indication_id` 决定;`context_ref_template = external:clinical-context/crc/{indication_id}`;6 个 context 级字段必须组内一致(实测 9 个分组全部一致);4 个 `endpoint_role` 折叠为 `endpoint_candidates` 且 `endpoint_maturity = not_locked_at_level_01`,`endpoint`/`endpoint_role`/`rationale` 不进入身份;排序键与去重键固定(实测重复 role 对为 0);`CTX-01`/`CTX-02` 冲突与残缺一律 `undefined_context` DEFER;每个 context 记录全部 `source_row_keys`。另加 `VAL-B09`/`VAL-B10`。 + +测试用与真实 schema 同构的合成 fixture 实现这些规则,验证 36 行必得 9 个 context、正序/逆序/旋转结果完全相同、追加重复行不改结果、改字段值走 `undefined_context`、删 endpoint 行同样走该路径且不影响其他 context、36 行与引用集合一一对应、改 endpoint 值不改任何 `clinical_context_ref`。 + +### 自查发现的第三个错误:LOCK-03 只绑一个 dimension 会保证空池 + +修订过程中实测:**41 条 `crc_prevalence` 全部 `direction = unknown`、`locator = not_available`**,原始 statement 自述「CRC prevalence 未在该运行中调和」。而初稿把 LOCK-03 只绑到 `crc_prevalence`,那会让 active pool 恒为 **0**。 + +这是执行者的错误,不是审核方提出的。来源文档 Lock 3 列出的合格 linkage 形式本就包含「已有 CRC preclinical 或 clinical targeting evidence」,只绑表达类证据是漏读来源文档。改为两类依据:`LB-expression`(`crc_prevalence`,实测 0,声明为本次空)与 `LB-precedent`(`adc_precedent` supporting 且 locator 为 `clinical_adc_names;clinical_stage_max`,实测 33)。新增测试断言**至少存在一个非空依据**,且每个依据的 `vacuous_this_run` 必须与实测计数一致——将来若所有依据都变空,测试直接失败,而不是静默产出空池。 + +若不做这次实测,本契约会以「预期 active pool 上限 41」的说法通过审核,而真实结果是 0。 + +### 预期结果形状改为逐项精确值 + +原写「active pool 上限 41 个 pair」——技术上是上界但严重误导。改为算出的精确值并加入 `predicted_result_shape` 与对账测试:Raw Matrix **369**、context eligible **1**/hold **8**、target eligible **32**/hold **9**/killed **0**、Eligible Universe Index **32**、active **27**/hold **5**/reactivation-eligible **0**,`CNT-03` 对账 32 = 27 + 5 + 0。 + + > **本段数字已被第十二节推翻**,现值为 target eligible **0**/hold **41**、Eligible Universe Index **0**、active **0**。原文保留以对照。 + +同时写入必须出现在结果报告里的结构性限制:**27 个 active pair 的 linkage 全部只有「已有临床 ADC 针对该靶点」这一类,没有任何一条 CRC 表达证据**,`adc_precedent` 原始 statement 也自述不建立 CRC 疗效或安全窗。因此本次的 `active` 仅表示「存在一条可回溯的 CRC-scoped ADC precedent」。 + +### 本轮变异检验 + +10 个变异全部被捕获后精确回滚,回滚后与备份 `diff -q` 一致、测试恢复 `OK`:允许 RNA locator RETAIN、把缺失证据改判 EXCLUDE/killed、把 `not_surface_target` 从不可用清单移除、从 `barred_fields` 删掉 `disposition`、把 `expected_count` 改成不对账的值、把 `endpoint_role` 塞进 context 身份、让 ref 模板依赖 `endpoint_role`、把冲突路径改判 EXCLUDE、从去重键里删掉 endpoint 字段、把 `endpoint_locked` 改成 `true`。 + +### 审核方认可、本轮未改动的部分 + +#53/#54 列为 barred sources;输入文件 SHA-256 固定且不一致即中止;旧 `indication_endpoint_target_pairs.tsv` 不作输入;`no_known_linkage_after_complete_search` 本轮禁用;疾病级证据不用于 derived/benchmark 亚群特异 linkage;`DECISION-02` 获接受;不执行 Gate、不评分、不排序;仓库不存候选、证据或结果;contract-only 范围与 GenModule/Gate 边界无污染。 + +### 审核回写状态 + +审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403,未写回 GitHub。裁决以人类负责人转述为准,已完整记录于本节与 `logs/worklog.md`。 + +## 十二、第二轮审核裁决与修订(`REQUEST_CHANGES`,2026-08-04) + +ChatGPT 对 PR #58(HEAD `8ac045e`)返回 `REQUEST_CHANGES`。上一轮两个工程问题被确认已修复;本轮暴露两个**更基础的科学语义问题**。两条**全部接受**,并导致本 PR 的授权范围被降级。 + +### 阻断 1(接受):跨膜段证据不足以判定 `eligible_surface_target` + +`transmembrane_segment_count` 只证明蛋白具有跨膜拓扑,不能单独证明质膜定位、细胞外结构域存在、表位可被抗体接近、位于 CRC tumor-cell surface,也不能排除内质网/高尔基/线粒体等细胞器膜蛋白。而 PR #57 冻结的 LOCK-01 问题是「是否存在有合理依据的**细胞外可及蛋白形式**」。把 32 个靶点判为 eligible 超过了输入证据能支持的强度。 + +原始 statement 本身就写着「supports a membrane-associated target hypothesis but **does not prove tumor-cell surface exposure**」——我读了这句话,还是把它升级成了 RETAIN。这是本轮最直接的错误。 + +修订:`eligible_surface_target` 现在要求同时满足 `RQ-01` plasma-membrane localization、`RQ-02` extracellular domain/topology、`RQ-03` protein-level provenance,三者缺一不可且全部要求蛋白层面来源。跨膜段单独 → `L1-02` `possible_surface_target` DEFER(32);无注释 → `L1-03` DEFER(9);细胞器定位或定位冲突 → `L1-04` DEFER。 + +**实测:已批准层无法满足 `RQ-01` 与 `RQ-02`。** `target_evidence_units.tsv` 中 `plasma membrane`/`extracellular`/`localization`/`signal peptide`/`GPI` 关键词命中数**均为 0**。故 `retain_requirements_satisfiable_by_approved_inputs: false`,**eligible = 0**。 + +### 阻断 2(接受):泛癌 ADC precedent 不能直接证明 CRC linkage + +「某靶点已有临床 ADC」可能发生在任何癌种,最多证明该 target 具有 ADC modality precedent,不能自动证明与 CRC clinical context 存在 linkage;而 `indication_fit` 若只是 catalog 派生或模型判断,也不能替代源级 CRC 证据。 + +修订:`LB-precedent` 现在要求源证据本身包含 CRC/colorectal indication,或 CRC 细胞系/PDO/PDX/动物模型的 ADC/preclinical targeting 证据,并记录 `precedent_indication` 与 `source_locator`。仅其他癌种的 precedent → `LNK-02b` DEFER,保留为 target/modality metadata。仅 `indication_fit` → `LNK-02c` DEFER。 + +**实测:`measured_source_level_crc_units = 0`。** 33 条 `adc_precedent` supporting 单元的实质主张都是「Local ADC Index contains ADC precedent for〈药名〉」,不附任何 indication。 + +**一个必须记录的实测陷阱**:这 33 条 statement 全部含 "CRC" 字样,但只出现在免责句「precedent does not establish CRC efficacy or a safe therapeutic window」里。我上一轮就是按「statement 是否包含 CRC」计数得到 33/33 的,那是假阳性。契约已写入 `measurement_trap` 禁止该判据。 + +### 两条阻断的合并后果:本 PR 降级为只绑定 raw 轴 + +两条修订各自独立地把可 RETAIN 的数量归零: + +| 量 | 修订前 | 修订后 | +|---|---|---| +| target eligible | 32 | **0** | +| Eligible Universe Index | 32 | **0** | +| Pool Level 01 active | 27 | **0** | + +因此**已批准证据包无法支撑 Level 01 执行**。执行只会产出空的 Eligible Universe Index 与空的 pool 快照,既无候选价值,又有被误读为「已筛完」的风险。 + +按审核方上一轮给出的退路,本 PR 降级:`scope_of_authorisation: raw_axis_binding_only`、`authorises_level_01_execution: false`,并登记两个证据缺口: + +- **`EVGAP-01`**(阻断 `LOCK-01`):缺蛋白层面的 plasma-membrane 定位与 extracellular domain/topology 证据。需要一次受控的 target-surface localization evidence extraction。 +- **`EVGAP-02`**(阻断 `LOCK-03`):缺源级 CRC-specific linkage 证据。需要一次受控的 CRC-specific target-context linkage evidence extraction。 + +两者各自需要独立的 contract-only PR 与 `APPROVE`,都不在本 PR 授权范围内。 + +### 测试的守卫方式改了 + +原有一条测试断言「至少存在一个非空 linkage 依据」。现在两类依据都空,那条断言若保留就会阻止提交一个**诚实**的绑定。改为:每个依据的 `vacuous_this_run` 必须与其**合格**计数一致(不是 supporting 计数——泛癌 precedent 是 supporting 但不合格),且**当没有任何依据合格时,`authorises_level_01_execution` 必须为 `false`**。守卫的对象从「必须有货」变成「没货就不许执行」,这才是正确的不变式。 + +### 本轮变异检验 + +10 个变异全部被捕获后精确回滚,回滚后与备份 `diff -q` 一致、测试恢复 `OK`:让跨膜段单独恢复 RETAIN、把 RETAIN 条件削减为只要 `RQ-01`、声明已批准输入可满足 RETAIN 要求、把细胞器定位改判 EXCLUDE、让 `indication_fit` 可替代源级证据、把泛癌 precedent 声明为合格、去掉 `requires_source_level_crc_indication`、在空池情况下声明授权执行、把 coverage 改回 32 eligible、把 `LNK-02b` 改判 RETAIN。 + +### 审核方认可、本轮未改动的部分 + +context identity 只由 `indication_id` 决定;endpoint 不进入 identity 也未被锁定;顺序变化与重复行不改投影;冲突与残缺 context 进入 DEFER;每个 source row 都有 provenance;#53/#54 来源明确禁止;输入 SHA-256 固定;`no_known_linkage_after_complete_search` 本轮不可用;machine-extracted 证据可满足存在性但不得直接晋级 Level 02;不运行 Gate、不评分、不排序;仓库未写入候选、证据或运行结果。 + +### 审核回写状态 + +审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403,未写回 GitHub。裁决以人类负责人转述为准,已完整记录于本节与 `logs/worklog.md`。 + +## 十三、第三轮审核裁决与修订(`REQUEST_CHANGES`,2026-08-04) + +ChatGPT 对 PR #58(HEAD `6720edb`)返回 `REQUEST_CHANGES`。第二轮两个科学语义问题确认已正确修复,降级为 `raw_axis_binding_only` 被认可,`EVGAP-01`/`EVGAP-02` 被确认足以作为后续两个受控抽取契约的范围依据。仅剩一处残留矛盾。 + +### 阻断(接受):`VAL-B07` 仍保留旧计数 + +主体契约已改为 `eligible = 0`/`hold = 41`/`killed = 0`,`lock_01_derivation.coverage` 与 `predicted_result_shape` 都用这组值,但 `output_validation.additional_rules.VAL-B07` 仍写着 `32 eligible + 9 hold + 0 killed`。同一份机器可读契约同时规定两套互斥结果,未来验证无法确定权威值。**这是第二轮修订的漏改,执行者的错误。** + +修订: + +1. `VAL-B07` 的散文改为 `0 eligible + 41 hold + 0 killed`,并注明 `41 hold = 32 (L1-02,仅有跨膜拓扑) + 9 (L1-03,无任何注释)`。 +2. 新增结构化字段 `expected_target_eligibility`,使计数可被机械比对而不必解析散文;并加 `validates: evidence_insufficient_binding_state` 与 `authorises_result_generation: false`,明确该规则用于验证「证据不足导致无法执行」的绑定状态,不代表授权生成 Level 01 结果。 +3. 新增 `test_target_eligibility_counts_agree_in_all_three_places`,直接断言 `VAL-B07`、`lock_01_derivation.coverage`、`predicted_result_shape.target_eligibility` 三处逐键相等,并断言散文与结构化计数不矛盾。 +4. 新增 `test_no_validation_rule_asserts_eligible_targets_while_blocked`:在未授权执行时,任何验证规则都不得要求存在 eligible target,且每条非空 LOCK-01 规则都必须 DEFER。 + +### 全文扫描结果 + +按验收标准逐处核对,仓库内已不存在任何把跨膜段对应的 32 个靶点写成 eligible 的**执行或验证要求**。剩余提到「32」的位置全部是描述性的:绑定 YAML 第 318 行说明 `41 hold = 32 + 9`;契约第 81 行把旧映射描述为错误;本 handoff 第十一节是第一轮的历史记录,已加取代标记指向第十二节;第十二节的对照表与变异清单本身就是记录修订的。 + +### 本轮变异检验 + +5 个变异全部被捕获后精确回滚,与备份 `diff -q` 一致、测试恢复 `OK`:把 `VAL-B07` 的结构化计数改回 32、只把散文改回 32 使其与结构化字段矛盾、把 `coverage` 改成与 `VAL-B07` 不一致、把 `predicted_result_shape` 改成与 `VAL-B07` 不一致、声明该规则可授权生成结果。 + +### 审核回写状态 + +审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本节与 `logs/worklog.md`。 diff --git a/docs/pools/adc_pool_level_01_input_binding.yaml b/docs/pools/adc_pool_level_01_input_binding.yaml new file mode 100644 index 0000000..6cdae76 --- /dev/null +++ b/docs/pools/adc_pool_level_01_input_binding.yaml @@ -0,0 +1,554 @@ +# ADC Pool Level 01 输入绑定(machine-readable) +# +# 这份文件把 Level 01 的两条原始轴与 linkage 证据绑定到**已获批准的**外部产物上, +# 并冻结「来源状态 → LOCK 输出上限」的映射。它不含任何候选、证据或结果数据。 +# +# 由 tests/test_adc_pool_level_01_input_binding.py 校验。 +# Level 01 的判据定义见 docs/pools/adc_pool_gate_usage.yaml(PR #57,已 APPROVE)。 + +binding: + binding_id: adc_pool_level_01_input_binding + binding_version: 0.1.0 + level: "01" + level_contract_ref: docs/pools/adc_pool_gate_usage.yaml + status: contract_only_pending_review + execution_status: not_authorized_not_executed + # 本绑定不需要任何新的枚举运行。上游产物已经存在并已获批准。 + requires_new_enumeration_run: false + # 但已批准层不含 plasma-membrane 定位证据,也不含源级 CRC-specific precedent, + # 因此本绑定**只**授权绑定 raw 轴,不授权执行 Level 01。见 scope_of_authorisation。 + scope_of_authorisation: raw_axis_binding_only + authorises_level_01_execution: false + level_01_execution_blocked_by: + - EVGAP-01 + - EVGAP-02 + +# 允许作为 Level 01 输入的外部产物。每一项都必须有自己的 APPROVE 记录。 +accepted_sources: + - source_id: crc_enumeration_20260802 + run_dir: external:result/gen_iet_crc_target_enumeration_20260802 + authorising_pr: 29 + authorising_record: logs/chatgpt-review-2026-08-02-crc-target-enumeration-results-final.md + decision: APPROVE + supplies_axis: + - raw_clinical_contexts + - raw_targets + files: + - path: indication_endpoint_universe.tsv + sha256: b0b9506644df5453b97982f8c934b10795890b9c602342d7716124948c55d727 + rows: 36 + distinct_indications: 9 + - path: target_evidence_catalog.tsv + sha256: 27bb81ebe5f1048b732f62c530095e0007212f818f3844e004118bb94525cc28 + rows: 41 + - path: opposing_evidence.tsv + sha256: 99ccf54440b3ac52d7fb37feabdcd5c9608f6f608e2068433d2e3944bd214efd + - path: source_manifest.json + sha256: 9f7fa1a3d627172a10de7e1c3a5b652ceb3c42f42aa0c5165484d703bd15469e + - path: run_report.md + sha256: 2579c8f0ef95e45655a0c83b5159de7b2adade5b0b1329d76f968edd93b3944b + - path: external_run_worklog.md + sha256: 83f318d2b550ad2cc20e7374b7c1ff8e1e78c8c1572e92aa33c1019c5fe53442 + - path: indication_endpoint_target_pairs.tsv + sha256: c6d9f77d8c56541129ec7d6a2dcd01a8ca74325c320152b5bd2e75671ee85c73 + used_by_level_01: false + reason: Level 01 自己生成 pair;旧 pair 表按旧 indication+endpoint+target 单元构建,不作为输入。 + + - source_id: crc_target_evidence_20260801 + run_dir: external:result/gen_iet_crc_target_evidence_20260801T2235EDT + authorising_pr: 31 + authorising_record: logs/chatgpt-review-2026-08-02-crc-target-evidence-results-final.md + decision: APPROVE + supplies_axis: + - linkage_evidence + files: + - path: target_evidence_units.tsv + sha256: adc8fff738d9747f413aa2bcec7d95034782ed8cb9806bd5d32b6fe7de35124d + rows: 292 + distinct_genes: 41 + - path: target_evidence_summary.tsv + sha256: 6426fac0c23a91ccccaa68ac4fcd13078e4535470eae5e3fbd2cdcd0256e6bb6 + - path: source_manifest.json + sha256: 59e17e1285b25864386e2955d9a9027f9a5bf2a7843c87e566e1130274e683d3 + # 实测的关键限制,必须原样传递给执行者,不得掩盖。 + measured_limits: + granularity: target_level_only + has_clinical_context_column: false + all_units_review_status: machine_extracted_requires_human_review + expert_review_batches_passed: 2 + expert_review_batches_total: 20 + expert_review_targets_covered: 4 + direction_counts: + supporting: 88 + opposing: 32 + unknown: 172 + +# 明确禁止作为输入的来源。 +barred_sources: + - source_id: crc_clinical_frame_and_membrane_target_screen_20260804 + run_dir: external:result/gen_iet_crc_clinical_frame_and_membrane_target_screen_20260804T191053Z + status: UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED + related_pr: 53 + barred_content: + - 20 个 unmet-need 场景(其中 11 个为该运行新增) + - 45 个靶点(其中 4 个为该运行新增:GPA33、LY6G6D、TNFRSF12A、CEACAM6) + - 全部 RETAIN/DEFER/EXCLUDE disposition 与 Tier A 选择 + - benefit ranking、endpoint 定量门槛、payload 类别结论 + - source_id: adc_seed_playbook_v0_1_20260804 + run_dir: external:result/gen_iet_adc_seed_playbook_v0.1_20260804T201605Z + status: UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED + related_pr: 54 + barred_content: + - Seed Admission Standard 与全部决策规则 + - 17 靶点 disposition、抗体进入条件、压力测试判定 + - 全部实验建议与架构结论 + +# 范围收缩,这是使用已批准来源的直接后果,必须写明而不是回避。 +scope_consequences: + raw_clinical_contexts: 9 + raw_targets: 41 + raw_enumeration_matrix_pairs: 369 + note: >- + 被隔离运行扩大到 20 场景 / 45 靶点,那部分不可用。Level 01 首次执行的范围 + 因此小于该运行,这是正确结果而不是退步。 + +# 按本绑定的规则算出的预期结果。预先写明,使执行结果可被逐项核对, +# 而不是执行完才知道形状。任一项与实际不符即视为执行偏离本契约。 +predicted_result_shape: + # 本表是「若按本绑定执行会得到什么」的推算,用于说明为何不授权执行, + # 不是一次被授权运行的预期产物。 + is_authorised_to_execute: false + raw_enumeration_matrix: 369 + context_eligibility: + eligible: 1 + hold: 8 + superseded: 0 + target_eligibility: + eligible: 0 + hold: 41 + killed: 0 + eligible_universe_index: 0 + pool_level_01: + active: 0 + hold: 0 + reactivation_eligible: 0 + # CNT-02 对账:1 x 0 = 0。CNT-03 对账:0 = 0 + 0 + 0。 + derivation_note: >- + LOCK-02 给出 1 个 eligible context,但 LOCK-01 给出 0 个 eligible target—— + 跨膜拓扑单独不足以判定细胞外可及,而已批准层不含质膜定位与细胞外结构域证据。 + 故 Eligible Universe Index = 1 x 0 = 0,Level 01 不产生任何 pair 行。 + LOCK-03 两类依据也都为空,即使 LOCK-01 通过也不会有 active。 + conclusion: >- + 已批准证据包无法支撑 Level 01 执行。执行只会产出一份空的 Eligible Universe + Index 与空的 pool 快照,既无候选价值,又有被误读为「已筛完」的风险。 + 因此本绑定只授权绑定 raw 轴,不授权执行 Level 01。 + +# 阻断 Level 01 执行的证据缺口。每一条都需要一次受控的证据抽取运行, +# 并各自走 contract-only PR 与 APPROVE,不在本 PR 授权范围内。 +evidence_gaps: + - id: EVGAP-01 + blocks: LOCK-01 + missing: plasma-membrane 定位证据与 extracellular domain/topology 证据(蛋白层面) + measured: >- + target_evidence_units.tsv 中 plasma membrane / extracellular / localization / + signal peptide / GPI 关键词命中数均为 0;surface_reachability 只有 + transmembrane_segment_count(32) 与 not_available(9)。 + consequence: 41 个 target 全部落 possible_surface_target,eligible = 0。 + required_next_run: 受控的 target-surface localization evidence extraction + - id: EVGAP-02 + blocks: LOCK-03 + missing: 源级 CRC-specific linkage 证据(CRC 表达,或附 indication 的 CRC ADC/preclinical targeting 证据) + measured: >- + crc_prevalence 41 条全部 unknown/not_available;adc_precedent 的 33 条 + supporting 单元均不附 indication,其 statement 中的 CRC 字样仅出现在免责句; + indication_fit 为 catalog 派生标签且对 41 行全部成立,不区分靶点。 + consequence: 两类 linkage 依据全空,即使 LOCK-01 通过也不会有 active pair。 + required_next_run: 受控的 CRC-specific target-context linkage evidence extraction + +# LOCK-02:来源状态决定 outcome 上限。禁止把 derived strategy 自动升级为 +# canonical clinical fact——这是 PR #28 契约自身的禁令,此处继承。 +lock_02_status_ceiling: + - source_status: canonical_c0 + calibration: calibrated + max_outcome: validated_unmet_context + count: 1 + - source_status: derived_strategy + calibration: not_calibrated + max_outcome: plausible_unmet_context + forced_disposition: DEFER + count: 7 + - source_status: benchmark_subgroup + calibration: benchmark_only + max_outcome: weak_context + forced_disposition: DEFER + reason: benchmark 情境用于校准,不是开发情境。 + count: 1 + +# LOCK-01:既有 disposition 列是分类标签,不是筛选判决,禁止当作 LOCK-01 输出。 +lock_01_input_semantics: + catalog_disposition_values: + - benchmark + - candidate + - hold + catalog_disposition_is_candidate_filter_result: false + may_be_inherited_as_lock_01_outcome: false + reason: >- + 这些标签由 PR #28 契约的五条最小筛选层产生,判据与 LOCK-01 不同; + 且 gate_score_status 全部为 not_scored_in_enumeration_run、 + gate_pass_status 全部为 not_assessed。LOCK-01 必须独立推导。 + +# LOCK-01 的确定性推导规则。 +# +# PR #57 冻结的 LOCK-01 问题是「是否存在有合理依据的**细胞外可及蛋白形式**」。 +# transmembrane_segment_count 只证明蛋白有跨膜拓扑,不能单独证明位于质膜、 +# 存在细胞外结构域、表位可被抗体接近,也不能排除内质网/高尔基/线粒体等 +# 细胞器膜蛋白。故跨膜段注释**单独不足以** RETAIN,只能 DEFER。 +lock_01_derivation: + source_id: crc_target_evidence_20260801 + source_file: target_evidence_units.tsv + dimension: surface_reachability + join_key: gene_symbol + decisive_field: evidence_locator + # 禁止参与推导的字段,防止把旧判决或 Gate 标签洗成 LOCK-01 结果。 + barred_fields: + - disposition + - gate_score_status + - gate_pass_status + - evidence_class + - clinical_stage_max + - internalization_status + - normal_tissue_risk_status + # RNA 证据不得满足 LOCK-01,无例外。 + rna_derived_locators_may_retain: false + # RETAIN 的三项必要条件,缺一不可。全部要求蛋白层面来源。 + retain_requirements: + - requirement_id: RQ-01 + claim: plasma_membrane_localization + must_be_protein_level: true + - requirement_id: RQ-02 + claim: extracellular_domain_or_topology + must_be_protein_level: true + - requirement_id: RQ-03 + claim: protein_level_provenance + must_be_protein_level: true + retain_requires_all_of: + - RQ-01 + - RQ-02 + - RQ-03 + # 实测:已批准层不含任何一条 plasma-membrane 定位或 extracellular domain 证据。 + # 关键词 plasma membrane / extracellular / localization / signal peptide / GPI + # 在 target_evidence_units.tsv 中命中数均为 0。故 RQ-01 与 RQ-02 无法满足。 + retain_requirements_satisfiable_by_approved_inputs: false + rules: + - id: L1-01 + condition: 同时满足 RQ-01、RQ-02、RQ-03 + outcome: eligible_surface_target + disposition: RETAIN + resulting_state: eligible + evidence_basis: plasma_membrane_localization_and_extracellular_topology + required_provenance: + - source_id + - source_path_or_url + - evidence_locator + - retrieved_at + - review_status + expected_count: 0 + vacuous_this_run: true + vacuous_reason: 已批准层不提供 plasma-membrane 定位与 extracellular domain 证据。 + - id: L1-02 + condition: evidence_locator = transmembrane_segment_count 且 evidence_direction = supporting,但无质膜定位与细胞外结构域证据 + outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: >- + 跨膜拓扑只支持「膜相关」假设,不证明质膜定位或细胞外可及; + 原始 statement 亦自述 does not prove tumor-cell surface exposure。 + expected_count: 32 + - id: L1-03 + condition: evidence_locator = not_available + outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: 已批准层未提供任何拓扑注释;缺失是 DEFER,永不 EXCLUDE。 + expected_count: 9 + - id: L1-04 + condition: 注释指向内质网/高尔基/线粒体等细胞器膜,或定位证据相互冲突 + outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: 细胞器膜定位与定位冲突都不构成细胞外可及,一律 DEFER。 + vacuous_this_run: true + vacuous_reason: 已批准层无细胞器定位字段,故本规则本次无命中。 + - id: L1-05 + condition: 同一 gene_symbol 同时存在 supporting 与 opposing 的 surface_reachability 单元 + outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: 证据冲突是 DEFER,永不 EXCLUDE。 + vacuous_this_run: true + vacuous_reason: 实测 surface_reachability 方向只有 supporting(32) 与 unknown(9),无 opposing。 + - id: L1-06 + condition: 证据来源为 RNA 层面 + outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: RNA 不得当作蛋白层面验证,仓库硬规则,无例外。 + vacuous_this_run: true + vacuous_reason: 已批准层的 surface_reachability 全部来自蛋白注释,无 RNA 来源单元。 + # 本次不可用的 outcome,与 LOCK-03 的 no_known_linkage_after_complete_search 同理。 + unavailable_outcomes: + - outcome: not_surface_target + reason: >- + 该 outcome 要求阳性的 negative protein/topology 证据(例如注释为纯胞内、 + 零跨膜段且无信号肽或 GPI 锚)。已批准层只有 transmembrane_segment_count 与 + not_available 两种 locator,没有任何一条断言某靶点不是表面蛋白。 + 因此本次执行不得排除任何靶点。 + - outcome: identity_unresolved + reason: >- + 该 outcome 要求一个身份解析结论字段。已批准层没有该字段, + 无法区分「身份未解析」与「注释不可得」,故一律落 possible_surface_target。 + # 完备性:两条规则覆盖全部 41 个靶点,无自由裁量空间。 + coverage: + total_targets: 41 + eligible: 0 + hold: 41 + killed: 0 + determinate_without_free_judgement: true + note: >- + 41 = 32 (L1-02,仅有跨膜拓扑) + 9 (L1-03,无任何注释)。零 eligible、零排除。 + Level 01 无法从已批准输入录入任何 target,见 evidence_gaps EVGAP-01。 + +# 36 行 indication_endpoint_universe.tsv → 9 个 clinical context 的确定性投影。 +# +# PR #57 规定 Level 01 的单元是 clinical context x target 且 endpoint 不锁定。 +# 输入是每 indication 4 行 endpoint。若不冻结投影规则,不同执行者都能产出 +# 「9 个 context」但 identity 与字段内容不同。以下规则消除该自由度。 +clinical_context_projection: + source_id: crc_enumeration_20260802 + source_file: indication_endpoint_universe.tsv + input_rows: 36 + output_contexts: 9 + group_by: indication_id + # context 身份只由 indication_id 决定,不含 endpoint,故 endpoint 变化不改身份。 + identity_fields: + - indication_id + context_ref_template: "external:clinical-context/crc/{indication_id}" + context_ref_depends_only_on: indication_id + # 这些字段必须在同一 indication 的所有行内取值一致;否则走冲突路径。 + context_level_fields_required_constant: + - label + - status + - source + - clinical_need + - confidence + - priority + # 实测:9 个分组中每个分组的上述 6 个字段都一致,无冲突行。 + measured_all_groups_constant: true + # endpoint 字段的处理:折叠为 metadata,绝不锁定。 + endpoint_handling: + endpoint_locked: false + required_endpoint_roles: + - regulatory_ultimate + - pivotal_supporting + - early_adc_proof + - supportive_exploratory + collapse_into: endpoint_candidates + collapse_element_fields: + - endpoint_role + - endpoint + retained_metadata: + - endpoint_class + - endpoint_maturity + - unresolved_endpoint_assumptions + endpoint_maturity_value: not_locked_at_level_01 + dropped_from_context_identity: + - endpoint + - endpoint_role + - rationale + determinism: + sort_rows_by: + - indication_id + - endpoint_role + dedupe_key: + - indication_id + - endpoint_role + - endpoint + order_independent: true + measured_duplicate_role_pairs: 0 + # 冲突与残缺一律走 DEFER,不排除。 + conflict_handling: + - id: CTX-01 + condition: 同一 indication 的 context_level_fields_required_constant 中任一字段取值不唯一 + outcome: undefined_context + disposition: DEFER + resulting_state: hold + vacuous_this_run: true + - id: CTX-02 + condition: 同一 indication 的 endpoint_role 不足 4 种 + outcome: undefined_context + disposition: DEFER + resulting_state: hold + vacuous_this_run: true + provenance: + each_context_records: source_row_keys + source_row_key_fields: + - indication_id + - endpoint_role + every_input_row_must_be_referenced: true + +# LOCK-03:证据只有 target 级,没有 context 列。这决定了 pair 级 linkage 的上限。 +lock_03_linkage_rules: + # 疾病级证据可支撑 canonical CRC 情境,但不能区分 8 个亚群情境。 + evidence_granularity: disease_level_not_subgroup_level + # 两类可接受的 linkage 依据。来源文档 Lock 3 明确列出的合格形式包括 + # 「target 在该人群的肿瘤样本中表达」与「已有 CRC preclinical 或 clinical + # targeting evidence」,故不能只绑表达类证据。 + accepted_linkage_bases: + - basis_id: LB-expression + dimension: crc_prevalence + required_direction: supporting + # 实测:41 条 crc_prevalence 全部 direction=unknown、locator=not_available, + # 原始 statement 自述「CRC prevalence 未在该运行中调和」。故本依据本次为空。 + measured_supporting_units: 0 + measured_qualifying_units: 0 + vacuous_this_run: true + - basis_id: LB-precedent + dimension: adc_precedent + required_direction: supporting + required_locator: clinical_adc_names;clinical_stage_max + # 「某靶点已有临床 ADC」可能发生在任何癌种。要满足 LOCK-03,源证据本身 + # 必须写明 CRC/colorectal indication,或 CRC 细胞系/PDO/PDX/动物模型的 + # ADC/preclinical targeting 证据,并记录 indication 与 source locator。 + requires_source_level_crc_indication: true + accepted_source_level_forms: + - crc_or_colorectal_clinical_indication_named_in_source + - crc_cell_line_pdo_pdx_or_animal_model_adc_or_preclinical_targeting + required_recorded_fields: + - precedent_indication + - source_locator + # indication_fit 是 catalog 派生标签,不能替代源级 CRC 证据。 + indication_fit_may_substitute: false + indication_fit_reason: >- + indication_fit 取值只有两种 CRC-scoped 标签,对 41 行全部成立, + 不区分任何靶点,且属 catalog 派生/模型判断。PR #57 已明确模型领域知识 + 单独不足以录入 pair,故它不构成源级 CRC 证据。 + # 泛癌 precedent 保留为 metadata,但不满足 LOCK-03。 + other_cancer_precedent_disposition: metadata_only_hold + measured_supporting_units: 33 + measured_source_level_crc_units: 0 + measured_qualifying_units: 0 + # 实测陷阱,记录以免复现:33/33 条 statement 都含 "CRC" 字样,但只出现在 + # 免责句 "precedent does not establish CRC efficacy or a safe therapeutic + # window" 里;实质主张是 "Local ADC Index contains ADC precedent for <药名>", + # 不附任何 indication。按 statement 里是否出现 CRC 计数会得到 33,是假阳性。 + measurement_trap: >- + 不得用「statement 是否包含 CRC」作为判据:该字样来自免责句, + 非 indication 断言。必须要求源证据显式记录 precedent_indication。 + vacuous_this_run: true + vacuous_reason: 已批准层无任何一条附 CRC indication 的源级 ADC precedent。 + rules: + - id: LNK-01 + condition: 命中任一 accepted_linkage_bases,且该情境为 canonical_c0 + outcome: linkage_evidence_exists + disposition: RETAIN + resulting_state: active + - id: LNK-02 + condition: 命中任一 accepted_linkage_bases,但该情境为 derived 或 benchmark 亚群 + outcome: linkage_unassessed + disposition: DEFER + resulting_state: hold + reason: 疾病级证据不能建立亚群特异 linkage。 + - id: LNK-02b + condition: 仅有其他癌种的 ADC precedent,无源级 CRC indication + outcome: linkage_unassessed + disposition: DEFER + resulting_state: hold + reason: 泛癌 precedent 只证明 ADC modality precedent,不证明与 CRC context 的 linkage。 + retained_as: target_modality_metadata + - id: LNK-02c + condition: 仅有 catalog 派生的 indication_fit 标签,无源级 CRC 证据 + outcome: linkage_unassessed + disposition: DEFER + resulting_state: hold + reason: 模型或 catalog 派生判断单独不足以录入 pair。 + - id: LNK-03 + condition: 未命中任何 accepted_linkage_bases + outcome: linkage_unassessed + disposition: DEFER + resulting_state: hold + - id: LNK-04 + condition: 未按 LOCK-03 规定范围完成检索 + outcome: linkage_evidence_missing + disposition: DEFER + resulting_state: hold + # DECISION-02,明示待审核方裁决。 + machine_extracted_evidence_satisfies_existence: true + requires_review_status_column: true + carry_forward_constraint: >- + linkage 证据仅为 machine_extracted_requires_human_review 的 pair 可以进入 + Level 01 active pool,但**不得晋级到 Level 02**,直到该证据通过专家复核。 + no_known_linkage_after_complete_search_available: false + no_known_linkage_unavailable_reason: >- + 该 outcome 要求 search_complete=true 与完整检索记录。既有证据包为 + machine_extracted、且 20 个专家复核批次只完成 2 个,检索范围未闭合, + 因此本次执行不得输出该 outcome。 + +# 本绑定授权什么、不授权什么。 +authorises: + - 把 raw clinical context 轴与 raw target 轴绑定到 PR #29 / #31 已批准的产物,并固定其 SHA-256。 + - 冻结 LOCK-01 推导规则、LOCK-02 状态上限、LOCK-03 linkage 依据与 clinical context 投影。 + - 记录 EVGAP-01 与 EVGAP-02 两个证据缺口及其所需的后续受控抽取。 +not_authorised: + - 执行 Level 01(被 EVGAP-01 与 EVGAP-02 阻断) + - 任何证据抽取或检索运行 + - 任何新的 clinical context 或 target 枚举 + - 任何靶点筛选排序、Tier 划分、资产推荐或实验建议 + - 任何 Gate 执行或 Gate 评分 + - endpoint 锁定或定量门槛 + - Level 02 与 Level 03 + - 把被隔离运行的任何产物重新引入 + +output_validation: + inherits_from: docs/pools/adc_pool_gate_usage.yaml + additional_rules: + - id: VAL-B01 + rule: 每个 clinical context 行必须携带 source_status 与 calibration,且 LOCK-02 outcome 不得超过 lock_02_status_ceiling 规定的上限。 + - id: VAL-B02 + rule: 每个 pair 行必须携带 linkage_evidence_review_status;仅 machine_extracted 的行必须同时标记不得晋级 Level 02。 + - id: VAL-B03 + rule: 输出中不得出现 no_known_linkage_after_complete_search。 + - id: VAL-B04 + rule: raw_enumeration_matrix 必须恰好 369 行(9 x 41)。 + - id: VAL-B05 + rule: 每个输入文件的 SHA-256 必须与本绑定记录一致,不一致即中止执行。 + - id: VAL-B06 + rule: 输出中不得出现任何被隔离运行独有的场景 ID 或靶点(GPA33、LY6G6D、TNFRSF12A、CEACAM6)。 + - id: VAL-B07 + # 本规则当前用于验证「证据不足导致无法执行」的绑定状态, + # 不代表授权生成 Level 01 结果。三处计数必须一致:本规则、 + # lock_01_derivation.coverage、predicted_result_shape.target_eligibility。 + validates: evidence_insufficient_binding_state + authorises_result_generation: false + expected_target_eligibility: + eligible: 0 + hold: 41 + killed: 0 + rule: >- + 每个 LOCK-01 结果行必须记录命中的 lock_01_derivation 规则 ID 与 + required_provenance 全部字段;不得出现 not_surface_target 或 + identity_unresolved;41 个靶点必须恰好分为 0 eligible + 41 hold + 0 killed, + 其中 41 hold = 32 (L1-02,仅有跨膜拓扑) + 9 (L1-03,无任何注释)。 + - id: VAL-B08 + rule: >- + LOCK-01 推导不得读取 barred_fields 中的任何字段;结果不得与 + target_evidence_catalog.tsv 的 disposition 列存在函数依赖。 + - id: VAL-B09 + rule: >- + clinical context 必须恰好 9 个,每个 clinical_context_ref 由 + context_ref_template 生成且只依赖 indication_id;36 个输入行必须全部被 + 某个 context 的 source_row_keys 引用,无遗漏无重复。 + - id: VAL-B10 + rule: >- + 每个 context 必须携带 endpoint_candidates(4 项)与 + endpoint_maturity = not_locked_at_level_01;不得出现任何被锁定的 + protocol endpoint 或定量门槛。 diff --git a/docs/tasks/ADC_POOL_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md b/docs/tasks/ADC_POOL_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md new file mode 100644 index 0000000..f5791ac --- /dev/null +++ b/docs/tasks/ADC_POOL_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md @@ -0,0 +1,219 @@ +# ADC Pool Level 01 输入绑定与执行契约 + +- 任务分支:`task_20260804_adc-pool-level-01-input-binding` +- 前置工作包:PR #57(Level 01 判据定义),ChatGPT `APPROVE` +- 机器可读绑定:[`../pools/adc_pool_level_01_input_binding.yaml`](../pools/adc_pool_level_01_input_binding.yaml),由 `tests/test_adc_pool_level_01_input_binding.py` 校验 +- 当前状态:**contract-only,未执行,等待 ChatGPT 审核** +- **授权范围:仅绑定 raw 轴。本契约不授权执行 Level 01**——被 `EVGAP-01`/`EVGAP-02` 两个证据缺口阻断,见第十节。 + +## 目的 + +解除 PR #57 的 `BLOCK-02`,把 Level 01 的两条原始轴与 linkage 证据绑定到**已获独立批准**的外部产物上,并冻结「来源状态 → LOCK 输出上限」的映射。 + +本文件**不执行 Level 01**,也**不授权任何新的枚举运行**。 + +**并且,经第二轮审核核实,本契约也不授权执行 Level 01。** 已批准证据包既不含 plasma-membrane 定位证据,也不含源级 CRC-specific linkage 证据,`LOCK-01` 与 `LOCK-03` 都无法产出任何 RETAIN。执行只会得到一份空的 Eligible Universe Index 与空的 pool 快照。缺口与所需的后续受控抽取见第十节。 + +## 一、关键发现:不需要重跑枚举 + +PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 2026-08-04 运行」。**这句话不准确,本契约予以更正。** + +核实结果:2026-08-02 的枚举运行早已通过 **PR #29 `APPROVE`**,其批准记录明确写着 + +> Authorized: use external enumeration output as input to a new target-level evidence extraction task. + +而 target 级证据抽取又通过 **PR #31 `APPROVE`**。因此存在一条完整的、未被隔离的输入链。`BLOCK-02` 的正确表述应是「不得使用 2026-08-04 那次运行的产物」,而不是「没有可用的 context」。 + +**后果:Level 01 不需要任何新的外部枚举运行,只需要一次 Level 01 自身的执行。** 原先估计的「两个契约 + 两次运行」缩减为「一个契约 + 一次运行」。 + +## 二、允许的输入 + +| 来源 | 授权 PR | 提供 | 规模 | +|---|---|---|---| +| `gen_iet_crc_target_enumeration_20260802` | #29 `APPROVE` | raw clinical contexts、raw targets | 9 indications(36 endpoint 行)、41 targets | +| `gen_iet_crc_target_evidence_20260801T2235EDT` | #31 `APPROVE` | linkage 证据 | 292 evidence units、41 genes | + +7 + 3 个输入文件的 SHA-256 已逐一记录在绑定 YAML 中。**执行前必须逐个校验,任一不一致即中止。** + +`indication_endpoint_target_pairs.tsv`(1,476 行)**不作为输入**:它按旧的 `indication + endpoint + target` 单元构建,而 Level 01 的单元是 `clinical context × target` 且 endpoint 不锁定。Level 01 自己生成 pair。 + +## 三、明确禁止的输入 + +| 来源 | 状态 | 禁止内容 | +|---|---|---| +| `gen_iet_crc_clinical_frame_and_membrane_target_screen_20260804T191053Z`(PR #53) | `UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED` | 20 个场景(11 个为新增)、45 个靶点(4 个为新增:GPA33、LY6G6D、TNFRSF12A、CEACAM6)、全部 disposition 与 Tier A、benefit ranking、endpoint 定量门槛、payload 类别结论 | +| `gen_iet_adc_seed_playbook_v0.1_20260804T201605Z`(PR #54) | `UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED` | Seed Admission Standard、17 靶点 disposition、抗体进入条件、压力测试判定、全部实验建议与架构结论 | + +输出验证规则 `VAL-B06` 明确禁止上述四个仅存在于被隔离运行的靶点出现在结果中。 + +## 四、范围收缩,写明而不回避 + +| | 被隔离运行 | 本契约 | +|---|---|---| +| clinical contexts | 20 | **9** | +| targets | 45 | **41** | +| Raw Enumeration Matrix | — | **369 pairs**(9 × 41) | + +Level 01 首次执行的范围因此小于那次被隔离的运行。**这是正确结果,不是退步**——差额正是未经授权扩大的那部分。 + +## 五、LOCK-02:来源状态决定 outcome 上限 + +实测 9 个 context 的来源状态:1 个 `canonical_c0`(confidence 0.93)、7 个 `derived_strategy`(`not_calibrated`)、1 个 `benchmark_subgroup`(`benchmark_only`)。 + +| 来源状态 | 校准 | outcome 上限 | disposition | 数量 | +|---|---|---|---|---| +| `canonical_c0` | calibrated | `validated_unmet_context` | RETAIN 可用 | 1 | +| `derived_strategy` | `not_calibrated` | `plausible_unmet_context` | **强制 DEFER** | 7 | +| `benchmark_subgroup` | `benchmark_only` | `weak_context` | **强制 DEFER** | 1 | + +依据是 PR #28 契约自身的禁令:「不得将 derived strategy 自动升级为 canonical clinical fact」。此处继承,并由测试机械保证未校准来源不可能得到 RETAIN。 + +**直接后果:只有 1 个 context 可以 `eligible`。** 但 LOCK-01 给出 0 个 eligible target,故 Eligible Universe Index 为 1 × 0 = **0**,见第六节与第十节。 + +## 六、LOCK-01:既有 disposition 列不可继承 + +`target_evidence_catalog.tsv` 已有 `disposition` 列,取值为 `benchmark`(19)/`candidate`(16)/`hold`(6)。 + +**这些不是 `CandidateFilterResult`,不得当作 LOCK-01 输出。** 它们由 PR #28 契约的五条最小筛选层产生,判据与 LOCK-01 不同;且全部 41 行的 `gate_score_status = not_scored_in_enumeration_run`、`gate_pass_status = not_assessed`。测试断言这三个标签与 `CandidateDisposition` 的取值无交集,避免混读。 + +### LOCK-01 的确定性推导 + +**第二轮审核裁决:跨膜段证据不足以判定 `eligible_surface_target`,本契约已据此更正。** `transmembrane_segment_count` 只证明蛋白具有跨膜拓扑,不能单独证明位于质膜、存在细胞外结构域、表位可被抗体接近,也不能排除内质网/高尔基/线粒体等细胞器膜蛋白。而 PR #57 冻结的 LOCK-01 问题是「是否存在有合理依据的**细胞外可及蛋白形式**」。原映射把 32 个靶点判为 eligible,超过输入证据能支持的强度。 + +`eligible_surface_target` 现在要求**同时**满足三项,缺一不可,且全部要求蛋白层面来源: + +| 要求 | 内容 | +|---|---| +| `RQ-01` | plasma-membrane localization | +| `RQ-02` | extracellular domain / topology | +| `RQ-03` | protein-level provenance | + +| ID | 条件 | outcome | disposition | 数量 | +|---|---|---|---|---| +| `L1-01` | 同时满足 `RQ-01`+`RQ-02`+`RQ-03` | `eligible_surface_target` | RETAIN | **0**(本次空规则) | +| `L1-02` | 只有 `transmembrane_segment_count` supporting,无质膜定位与细胞外结构域证据 | `possible_surface_target` | DEFER | **32** | +| `L1-03` | `evidence_locator = not_available` | `possible_surface_target` | DEFER | **9** | +| `L1-04` | 注释指向细胞器膜,或定位证据冲突 | `possible_surface_target` | DEFER | 0(本次空规则) | +| `L1-05` | 同 gene 同时 supporting 与 opposing | `possible_surface_target` | DEFER | 0(本次空规则) | +| `L1-06` | 证据来源为 RNA 层面 | `possible_surface_target` | DEFER | 0(本次空规则) | + +- 来源固定为 `target_evidence_units.tsv`/`dimension = surface_reachability`/按 `gene_symbol` 连接 +- **禁止参与推导的字段**:`disposition`、`gate_score_status`、`gate_pass_status`、`evidence_class`、`clinical_stage_max`、`internalization_status`、`normal_tissue_risk_status` +- `retain_requirements_satisfiable_by_approved_inputs: false`。实测依据:`target_evidence_units.tsv` 中 `plasma membrane`/`extracellular`/`localization`/`signal peptide`/`GPI` 关键词命中数**均为 0**;`surface_reachability` 只有 `transmembrane_segment_count`(32) 与 `not_available`(9)。 +- 原始 statement 自述「supports a membrane-associated target hypothesis but **does not prove tumor-cell surface exposure**」——这句话本身就说明它不足以 RETAIN。 + +**两个 outcome 仍然不可用**:`not_surface_target` 要求阳性的 negative protein/topology 证据,已批准层没有任何一条断言某靶点不是表面蛋白;`identity_unresolved` 要求身份解析结论字段,已批准层没有。 + +**完备性:41 = 32 (`L1-02`) + 9 (`L1-03`),eligible = 0,killed = 0。** 零自由裁量、零排除,但也**零录入**。 + +## 六之二、36 行 → 9 个 clinical context 的确定性投影 + +PR #57 规定 Level 01 的单元是 `clinical context × target` 且 endpoint 不锁定,而输入是每 indication 4 行 endpoint。若不冻结投影规则,不同执行者都能产出「9 个 context」但 identity 与字段内容不同。以下规则消除该自由度。 + +- **身份**:`indication_id`,仅此一项。`clinical_context_ref = external:clinical-context/crc/{indication_id}`,只依赖 `indication_id`,故 endpoint 变化不改身份。 +- **必须组内一致的 context 级字段**:`label`、`status`、`source`、`clinical_need`、`confidence`、`priority`。实测 9 个分组全部一致,无冲突行。 +- **endpoint 折叠为 metadata,绝不锁定**:4 个 `endpoint_role`(`regulatory_ultimate`/`pivotal_supporting`/`early_adc_proof`/`supportive_exploratory`)折叠成 `endpoint_candidates`,保留 `endpoint_class`、`endpoint_maturity = not_locked_at_level_01`、`unresolved_endpoint_assumptions`;`endpoint`、`endpoint_role`、`rationale` 不进入 context 身份。 +- **确定性**:按 `(indication_id, endpoint_role)` 排序,按 `(indication_id, endpoint_role, endpoint)` 去重。实测重复 role 对为 0。行序变化不改结果。 +- **冲突与残缺**:`CTX-01` 组内字段取值不唯一、`CTX-02` `endpoint_role` 不足 4 种,两者都 → `undefined_context` DEFER `hold`。**不排除。** +- **provenance**:每个 context 记录其全部 `source_row_keys`,36 行必须全部被引用、无遗漏无重复(`VAL-B09`)。 + +测试用一份与真实 schema 同构的合成 fixture 实现上述规则并验证:36 行必得 9 个 context;正序、逆序、旋转输入结果完全相同;追加重复行不改结果;改一个 context 级字段值即走 `undefined_context`;删一个 endpoint 行同样走该路径且不影响其他 context;36 行与引用集合一一对应;改 endpoint 值不改任何 `clinical_context_ref`。 + +## 七、LOCK-03:证据只有 target 级,这是硬上限 + +实测:292 个 evidence unit = 41 genes × 7 dimensions + 5 opposing。**没有 indication/context 列**,证据是 target 级、疾病级,不是 pair 级、亚群级。方向分布为 supporting 88/opposing 32/unknown 172,且 **292 个单元全部为 `machine_extracted_requires_human_review`**;20 个专家复核批次只完成 2 个,覆盖 4 个靶点。 + +### 两类可接受的 linkage 依据 + +**实测发现,并据此更正了本契约初稿:41 条 `crc_prevalence` 单元全部 `direction = unknown`、`locator = not_available`**,原始 statement 自述「Target-specific CRC prevalence and malignant-cell/state prevalence were not harmonized in this run」。初稿把 LOCK-03 只绑到 `crc_prevalence` 一个 dimension,那会**保证 active pool 为空**。这是执行者的错误,由实测自查发现。 + +来源文档 Lock 3 列出的合格 linkage 形式本就包含「**已有 CRC preclinical 或 clinical targeting evidence**」,因此只绑表达类证据本身就是漏读来源文档。冻结两类依据: + +| 依据 | dimension | 判据 | 实测 supporting | 实测**合格** | +|---|---|---|---|---| +| `LB-expression` | `crc_prevalence` | `direction = supporting` | 0 | **0** | +| `LB-precedent` | `adc_precedent` | `supporting` 且 `locator = clinical_adc_names;clinical_stage_max` **且源证据本身写明 CRC/colorectal indication** | 33 | **0** | + +**第二轮审核裁决:泛癌 ADC precedent 不能直接证明 CRC linkage,本契约已据此收紧。** 「某靶点已有临床 ADC」可能发生在任何癌种,它最多证明该 target 具有 ADC modality precedent。`LB-precedent` 现在要求源证据本身包含 CRC/colorectal indication,或 CRC 细胞系/PDO/PDX/动物模型的 ADC/preclinical targeting 证据,并记录 `precedent_indication` 与 `source_locator`。仅在其他癌种中的 precedent 保留为 target/modality metadata(`LNK-02b`),**不满足 LOCK-03**。 + +`indication_fit` **不得替代**源级 CRC 证据(`LNK-02c`):它只有两种 CRC-scoped 标签、对 41 行全部成立、不区分任何靶点,且属 catalog 派生判断;PR #57 已明确模型领域知识单独不足以录入 pair。 + +**实测结果:`measured_source_level_crc_units = 0`。** `adc_precedent` 的 33 条 supporting 单元实质主张都是「Local ADC Index contains ADC precedent for〈药名〉」,**不附任何 indication**。 + +> **一个必须记录的实测陷阱**:这 33 条 statement 全部含 "CRC" 字样,但只出现在免责句「precedent does not establish CRC efficacy or a safe therapeutic window」里。按「statement 是否包含 CRC」计数会得到 33/33,是假阳性。契约已写入 `measurement_trap`,禁止用该判据。 + +| ID | 条件 | outcome | disposition | +|---|---|---|---| +| `LNK-01` | 命中任一合格依据,且 context 为 `canonical_c0` | `linkage_evidence_exists` | RETAIN | +| `LNK-02` | 命中任一合格依据,但 context 为 derived/benchmark 亚群 | `linkage_unassessed` | DEFER | +| `LNK-02b` | 仅有其他癌种 ADC precedent,无源级 CRC indication | `linkage_unassessed` | DEFER | +| `LNK-02c` | 仅有 catalog 派生的 `indication_fit`,无源级 CRC 证据 | `linkage_unassessed` | DEFER | +| `LNK-03` | 未命中任何依据 | `linkage_unassessed` | DEFER | +| `LNK-04` | 未按规定范围完成检索 | `linkage_evidence_missing` | DEFER | + +**两类依据本次都为空。** 测试据此断言:每个依据的 `vacuous_this_run` 必须与其**合格**计数一致(不是 supporting 计数——泛癌 precedent 是 supporting 但不合格);且当没有任何依据合格时,`authorises_level_01_execution` **必须为 `false`**。这样全空的绑定会明确阻断执行,而不是静默产出空池。 + +**`no_known_linkage_after_complete_search` 本次不可用。** 该 outcome 要求 `search_complete = true` 与完整检索记录;既有证据包为 machine-extracted、专家复核只完成 2/20,检索范围未闭合。`VAL-B03` 禁止其出现在输出中。 + +### `DECISION-02`(请裁决) + +**未经专家复核的 machine-extracted 证据,是否满足 LOCK-03 的「存在性」?** 本契约冻结为**满足**,但附两个硬约束: + +1. 每个 pair 行必须携带 `linkage_evidence_review_status`; +2. linkage 证据仅为 `machine_extracted_requires_human_review` 的 pair 可进入 Level 01 active pool,但**不得晋级 Level 02**,直到该证据通过专家复核。 + +理由:LOCK-03 问的是「是否存在一项公开证据表明相关」,不问有效性,且每个单元都有 `source_id`、`source_path_or_url` 与 `evidence_locator` 可回溯;Level 01 的错误偏好是召回优先。被否决的替代方案是「只有专家复核通过的证据才满足」——那会让 41 个靶点里只剩 4 个可用,Level 01 报出接近空池,而每个靶点其实都有可回溯来源,反而失真。真正的质量判断留给 Level 02,届时 review status 就在表里。 + +若审核方认为应采用严格方案,改动只是把 `machine_extracted_evidence_satisfies_existence` 置为 `false`,规则与测试都已就位。 + +## 八、本契约授权与不授权 + +**授权:** 把 raw clinical context 轴与 raw target 轴绑定到 PR #29/#31 已批准的产物并固定 SHA-256;冻结 LOCK-01 推导、LOCK-02 状态上限、LOCK-03 linkage 依据与 clinical context 投影;记录 `EVGAP-01`/`EVGAP-02` 及其所需的后续受控抽取。 + +**不授权:** **执行 Level 01**(被 `EVGAP-01`/`EVGAP-02` 阻断);任何证据抽取或检索运行;任何新的 context 或 target 枚举;任何靶点筛选排序、Tier 划分、资产推荐或实验建议;任何 Gate 执行或评分;endpoint 锁定或定量门槛;Level 02 与 Level 03;把被隔离运行的任何产物重新引入。 + +## 九、输出验证 + +继承 PR #57 契约的 10 条验证规则,另加 6 条: + +| ID | 规则 | +|---|---| +| `VAL-B01` | 每个 context 行必须携带 `source_status` 与 `calibration`,LOCK-02 outcome 不得超过第五节上限 | +| `VAL-B02` | 每个 pair 行必须携带 `linkage_evidence_review_status`;仅 machine-extracted 的行必须标记不得晋级 Level 02 | +| `VAL-B03` | 输出中不得出现 `no_known_linkage_after_complete_search` | +| `VAL-B04` | Raw Enumeration Matrix 必须恰好 369 行 | +| `VAL-B05` | 每个输入文件的 SHA-256 必须与绑定记录一致,不一致即中止 | +| `VAL-B06` | 输出中不得出现 GPA33、LY6G6D、TNFRSF12A、CEACAM6 | + +执行后每个产物文件仍须逐文件记录 SHA-256,并通过独立结果 PR 审核;`APPROVE` 前不得发布任何排序、推荐或资产决策。 + +## 十、按本绑定推算的结果,以及为什么不授权执行 + +| 量 | 值 | +|---|---| +| Raw Enumeration Matrix | **369** | +| context 资格 | eligible **1**/hold **8**/superseded **0** | +| target 资格 | eligible **0**/hold **41**/killed **0** | +| Eligible Universe Index | **0**(1 × 0) | +| Pool Level 01 | active **0**/hold **0**/reactivation-eligible **0** | + +`CNT-02` 对账 1 × 0 = 0;`CNT-03` 对账 0 = 0 + 0 + 0。 + +LOCK-02 给出 1 个 eligible context,但 LOCK-01 给出 **0** 个 eligible target,故 Eligible Universe Index 为空,Level 01 不产生任何 pair 行。LOCK-03 的两类依据也都为空,即使 LOCK-01 通过也不会有 active。 + +**结论:已批准证据包无法支撑 Level 01 执行。** 执行只会产出一份空的 Eligible Universe Index 与空的 pool 快照,既无候选价值,又有被误读为「已筛完」的风险。因此本契约**只授权绑定 raw 轴**。 + +### 阻断执行的两个证据缺口 + +| ID | 阻断 | 缺什么 | 实测依据 | 所需后续运行 | +|---|---|---|---|---| +| `EVGAP-01` | `LOCK-01` | plasma-membrane 定位与 extracellular domain/topology 证据(蛋白层面) | `plasma membrane`/`extracellular`/`localization`/`signal peptide`/`GPI` 关键词命中数均为 0 | 受控的 target-surface localization evidence extraction | +| `EVGAP-02` | `LOCK-03` | 源级 CRC-specific linkage 证据 | `crc_prevalence` 41 条全 `not_available`;33 条 `adc_precedent` 均不附 indication;`indication_fit` 为 catalog 派生且对 41 行全部成立 | 受控的 CRC-specific target-context linkage evidence extraction | + +两条缺口各自需要一次受控证据抽取运行,**各自走 contract-only PR 与 `APPROVE`**,都不在本 PR 的授权范围内。 + +## 十一、当前阻断 + +- **本契约获 `APPROVE` 后仍不得执行 Level 01**,直到 `EVGAP-01` 与 `EVGAP-02` 各自通过受控抽取补齐并被接受。 +- 本仓库不得写入候选池、快照、证据、cache、result 或 weights。 +- `DECISION-02` 未获裁决前,执行者不得自行改用严格方案。 diff --git a/logs/worklog.md b/logs/worklog.md index e851361..690b289 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2443,3 +2443,68 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Review write-back: 审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403 未写回 GitHub。裁决以人类负责人转述为准,已完整记录于本条与 `docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md` 第十二节。 - Scope: 本轮只改 `docs/pools/adc_pool_gate_usage.yaml`、`docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md`、`tests/test_adc_pool_gate_usage.py`、本条 worklog 与该 handoff。仍未触碰 `src/`、`src/contracts/`、`genmodules/`、`extensions/`、`docs/architecture/`、`AGENTS.md`、`prompts/`;仍未新增 Gate、未改 45-Gate 拓扑。`NO_ARCHITECTURE_CHANGE` 判断不变。 - Next: 推送同一 PR 并同步 PR 描述,请求复审。 + +## 2026-08-04T19:35:00-04:00 — ADC Pool Level 01 输入绑定与执行契约(contract-only,未执行) + +- Instruction: 人类负责人要求「直接给我 ADC Pool Level 01,这个最终的 output」。核实后说明 Level 01 从未执行——PR #57 合并的是定义不是结果,`BLOCK-02` 挡着执行。人类负责人选择「先起 CRC context 契约 PR」。本条记录该契约。 +- Correction (executor error): PR #57 的 `BLOCK-02` 写「唯一的 context 枚举来自被隔离的 2026-08-04 运行」,**这句话不准确**。核实发现 2026-08-02 枚举早已通过 **PR #29 `APPROVE`**(记录明写「Authorized: use external enumeration output as input to a new target-level evidence extraction task」),target 级证据抽取又通过 **PR #31 `APPROVE`**,存在完整的未被隔离输入链。正确表述是「不得使用 2026-08-04 那次运行的产物」,不是「没有可用 context」。执行者把「被隔离」过度推广为「无可用输入」,与 PR #53 上「把针对 `ff943e7` 的检查推广成全局结论」是同一类错误。**后果:不需要任何新的枚举运行**,原先告知的「两个契约 + 两次运行」缩减为「一个契约 + 一次 Level 01 执行」。未回写 PR #57 已获批准的原文,更正写在本 PR。 +- Delivered: `docs/tasks/ADC_POOL_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md`、`docs/pools/adc_pool_level_01_input_binding.yaml`、`tests/test_adc_pool_level_01_input_binding.py`(13 项校验)。 +- Bound inputs: `gen_iet_crc_target_enumeration_20260802`(PR #29,9 indications/36 endpoint 行/41 targets)与 `gen_iet_crc_target_evidence_20260801T2235EDT`(PR #31,292 units/41 genes)。10 个输入文件的 SHA-256 用 `shasum -a 256` 实算并逐一记录,执行前必须校验、任一不一致即中止(`VAL-B05`)。`indication_endpoint_target_pairs.tsv`(1,476 行)不作输入,因其按旧 `indication+endpoint+target` 单元构建。 +- Barred inputs: #53、#54 两次被隔离运行列为 `barred_sources` 并逐条写出禁止内容;`VAL-B06` 禁止 GPA33/LY6G6D/TNFRSF12A/CEACAM6 出现在输出。 +- Scope consequence, stated not hidden: contexts 20 → **9**、targets 45 → **41**、Raw Enumeration Matrix **369 pairs**。首次执行范围小于被隔离运行,差额正是未经授权扩大的部分,属正确结果而非退步。 +- Finding (high): **LOCK-02 最多只有 1 个 context 能 `eligible`。** 实测 9 个 context 为 1 `canonical_c0`(conf 0.93)/7 `derived_strategy`(`not_calibrated`)/1 `benchmark_subgroup`(`benchmark_only`)。继承 PR #28 契约自身禁令「不得将 derived strategy 自动升级为 canonical clinical fact」为 outcome 上限,未校准来源**强制 DEFER**,由测试机械保证不可能 RETAIN。Eligible Universe Index 上限 1 × |eligible_targets| ≤ 41 pairs。 +- Finding (high): **既有 `disposition` 列不可继承为 LOCK-01 输出。** 取值为 `benchmark`(19)/`candidate`(16)/`hold`(6),不是 `CandidateFilterResult`;由 PR #28 五条最小筛选层产生、判据与 LOCK-01 不同;41 行 `gate_score_status` 全为 `not_scored_in_enumeration_run`、`gate_pass_status` 全为 `not_assessed`。测试断言三个标签与 `CandidateDisposition` 取值无交集。 +- Finding (high): **linkage 证据只有 target 级、疾病级。** 实测 292 units = 41 genes × 7 dimensions + 5 opposing,**无 indication/context 列**;direction supporting 88/opposing 32/unknown 172;**292 个单元全部 `machine_extracted_requires_human_review`**;20 个专家复核批次只完成 2 个、覆盖 4 靶点。故疾病级证据只能支撑 canonical context,不能建立亚群特异 linkage(`LNK-02`)。`no_known_linkage_after_complete_search` 本次不可用(检索范围未闭合,`VAL-B03` 禁止输出)。 +- `DECISION-02` recorded for adjudication: 未经专家复核的 machine-extracted 证据**满足** LOCK-03 存在性,附两条硬约束——每个 pair 必须带 `linkage_evidence_review_status`;仅 machine-extracted 的 pair 可进 active pool 但**不得晋级 Level 02**。理由:LOCK-03 问存在性不问有效性,每单元有 `source_id`/`source_path_or_url`/`evidence_locator` 可回溯,Level 01 召回优先。被否决的严格方案会让 41 靶点只剩 4 个可用、报出接近空池而失真。若审核方要严格方案,只需把 `machine_extracted_evidence_satisfies_existence` 置 `false`。 +- Predicted result shape, written in advance: 1 个 `eligible` context、8 个 `hold`;active pool 上限 41 pair 全部集中在 canonical MSS/pMMR mCRC 3L+;其余 328 pair 落 `hold`。真正瓶颈在**剩余 18 个专家复核批次**,不在漏斗设计,与 PR #54 M6「受数据集限制而非 Gate 限制」一致但这次建立在已批准证据上。预先写明以免结果被误读为 Level 01 失效。 +- Validation: `Ran 264 tests` 全部通过(`main` 基线 251 + 新增 13);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。所有规模数字由脚本读取外部产物实测,非估计。 +- Deliberately not done: 未执行 Level 01;无新枚举运行;未抓取文献/下载数据/运行分析;未引用被隔离运行任何产物;未定义 Level 02/03;未实现 `GAP-P01`..`GAP-P06`;未回写 PR #57 历史原文。 +- Interrupted task, facts preserved: #52/#53/#54/#57 的批准记录 PR 中断在事实收集阶段、**未写任何文件**。已查明:#52 也缺记录(原以为只缺三份);**四个 PR 在 GitHub 上都没有 review 记录**(`/reviews` 返回空);获批 head/merge commit 为 #52 `bfc04be`/`985edf8`、#53 `5318eca`/`09990c8`、#54 `8992563`/`58984e7`、#57 `6036c01`/`5e0458b`,其中 #54、#57 合并 head 与获批 head 不同、差异仅为 main 经合并进入;四轮转述评审逐字文本可从会话 transcript 恢复。 +- Noticed, not fixed: `requirements.txt` 注释仍写「207 tests」,实测 264。属无关改动,只记录。 +- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。 +- Next: 推送并创建 PR 送审;请审核方裁决 `DECISION-02`,以及确认对 `BLOCK-02` 的更正是否成立。 + +## 2026-08-04T20:05:00-04:00 — PR #58 第一轮审核裁决与最小修订(两条阻断全部接受,另自查出第三个错误) + +- Review: ChatGPT 对 PR #58(HEAD `8f5c85d`,可合并、CI 成功)返回 `REQUEST_CHANGES`,两条阻断。**全部接受**,同一 PR 内最小修订,未夹带无关改动。 +- Finding 1 accepted: **LOCK-01 没有真正绑定到可执行证据。** 原契约只写「LOCK-01 必须独立推导」,没说怎么推导。后果是实质的:41 个靶点最终多少 eligible/hold/killed 取决于执行者自由解释、不可复现;而本 PR 又不授权新检索,故绑定只证明「有 41 个 raw targets」,没证明能完成 LOCK-01。 +- Fix 1: 先核实已批准层有无蛋白层面表面证据再决定补 mapping 还是降级授权。**结论是补 mapping**——`surface_reachability` 有 32 条来自 `transmembrane_segment_count` 的蛋白拓扑注释(`supporting`)、9 条 `not_available`。新增 `lock_01_derivation`:单一来源 `target_evidence_units.tsv`/`dimension=surface_reachability`/按 `gene_symbol` 连接/判决字段 `evidence_locator`;`barred_fields` 列 7 个禁止字段(含 `disposition`、`gate_score_status`、`gate_pass_status`);RETAIN 白名单仅 `transmembrane_segment_count`、`rna_derived_locators_may_retain: false`;`L1-01`..`L1-04` 四条规则,缺失/冲突/RNA 一律 DEFER。**`not_surface_target` 与 `identity_unresolved` 声明为本次不可用**——前者需阳性 negative topology 证据而已批准层没有任何一条断言某靶点不是表面蛋白,故**本次不得排除任何靶点**;后者需身份解析结论字段而已批准层没有。完备性 32+9=41,零自由裁量、零排除。加 `VAL-B07`/`VAL-B08`。 +- Finding 2 accepted: **36 endpoint rows → 9 contexts 的转换规则未冻结。** 369 的算术没问题但语义不唯一,不同执行者都能产出 9 个 context 而 identity 与字段内容不同。 +- Fix 2: 新增 `clinical_context_projection`——身份只由 `indication_id` 决定;`context_ref_template = external:clinical-context/crc/{indication_id}`;6 个 context 级字段必须组内一致(实测 9 组全部一致);4 个 `endpoint_role` 折叠为 `endpoint_candidates`、`endpoint_maturity = not_locked_at_level_01`,`endpoint`/`endpoint_role`/`rationale` 不进身份;排序键与去重键固定(实测重复 role 对 0);`CTX-01`/`CTX-02` 冲突与残缺一律 `undefined_context` DEFER;每个 context 记录全部 `source_row_keys`。加 `VAL-B09`/`VAL-B10`。测试用与真实 schema 同构的合成 fixture 实现规则并验证 36→9、正序/逆序/旋转结果相同、重复行不改结果、改字段走 `undefined_context`、删 endpoint 行走同路径且不影响其他 context、36 行与引用一一对应、改 endpoint 值不改任何 ref。 +- Executor error, self-caught by measurement (第三个错误,非审核方提出): **LOCK-03 只绑一个 dimension 会保证空池。** 实测 **41 条 `crc_prevalence` 全部 `direction=unknown`、`locator=not_available`**,原始 statement 自述「CRC prevalence 未在该运行中调和」。初稿把 LOCK-03 只绑 `crc_prevalence`,active pool 会恒为 **0**。来源文档 Lock 3 的合格 linkage 形式本就包含「已有 CRC preclinical 或 clinical targeting evidence」,只绑表达类证据是漏读来源文档。改为两类依据:`LB-expression`(实测 0,声明本次空)与 `LB-precedent`(`adc_precedent` supporting 且 locator 为 `clinical_adc_names;clinical_stage_max`,实测 33)。新增测试断言**至少存在一个非空依据**,且每个依据 `vacuous_this_run` 必须与实测计数一致——将来全部变空时测试直接失败,不静默产出空池。**若不做这次实测,本契约会以「预期 active pool 上限 41」通过审核,而真实结果是 0。** +- Prediction corrected: 原写「active pool 上限 41 个 pair」,技术上是上界但严重误导。改为算出的精确值并加 `predicted_result_shape` 与对账测试:Raw Matrix **369**、context eligible **1**/hold **8**、target eligible **32**/hold **9**/killed **0**、Eligible Universe Index **32**、active **27**/hold **5**/reactivation-eligible **0**,`CNT-03` 对账 32=27+5+0。执行结果必须逐项相等,任一项不符即视为偏离契约。 +- Structural limit written into the contract: **27 个 active pair 的 linkage 全部只有「已有临床 ADC 针对该靶点」这一类,没有任何一条 CRC 表达证据**;`adc_precedent` 原始 statement 自述不建立 CRC 疗效或安全窗。故本次 `active` 仅表示「存在一条可回溯的 CRC-scoped ADC precedent」,此句必须原样出现在结果报告,否则 active 会被误读。 +- Validation: `Ran 277 tests` 全部通过(`main` 基线 251 + 新增 26,由 13 增至 26);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 +- Mutation-tested (本轮 10 个,全部 `FAILED` 后精确回滚,与备份 `diff -q` 一致、恢复 `OK`): 允许 RNA locator RETAIN、缺失证据改判 EXCLUDE/killed、把 `not_surface_target` 从不可用清单移除、从 `barred_fields` 删 `disposition`、`expected_count` 改成不对账值、把 `endpoint_role` 塞进 context 身份、ref 模板依赖 `endpoint_role`、冲突路径改判 EXCLUDE、去重键删掉 endpoint 字段、`endpoint_locked` 改 `true`。 +- Accepted by reviewer, unchanged: #53/#54 列为 barred sources;输入 SHA-256 固定且不一致即中止;旧 `indication_endpoint_target_pairs.tsv` 不作输入;`no_known_linkage_after_complete_search` 本轮禁用;疾病级证据不用于亚群特异 linkage;**`DECISION-02` 获接受**;不执行 Gate、不评分、不排序;仓库不存候选/证据/结果;contract-only 范围与 GenModule/Gate 边界无污染。 +- Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十一节。 +- Next: 推送同一 PR 并同步 PR 描述,请求复审。 + +## 2026-08-04T20:45:00-04:00 — PR #58 第二轮审核裁决与修订(两条科学语义阻断,本 PR 降级为只绑 raw 轴) + +- Review: ChatGPT 对 PR #58(HEAD `8ac045e`,可合并、CI 成功)返回 `REQUEST_CHANGES`。确认上一轮两个工程问题已修复(36→9 投影确定化、LOCK-01 不再继承旧 disposition 并补了 mapping、输入隔离/SHA/DATA 边界/测试均正确),但暴露两个**更基础的科学语义问题**。两条**全部接受**。 +- Finding 1 accepted: **跨膜段证据不足以判定 `eligible_surface_target`。** `transmembrane_segment_count` 只证明跨膜拓扑,不能单独证明质膜定位、细胞外结构域存在、表位可被抗体接近、位于 CRC tumor-cell surface,也不能排除内质网/高尔基/线粒体等细胞器膜蛋白。PR #57 冻结的 LOCK-01 问题是「是否存在有合理依据的**细胞外可及蛋白形式**」,把 32 个靶点判 eligible 超过了输入证据强度。**执行者错误尤为直接:原始 statement 本身就写着 does not prove tumor-cell surface exposure,读到了却仍升级为 RETAIN。** +- Fix 1: `eligible_surface_target` 改为必须同时满足 `RQ-01` plasma-membrane localization、`RQ-02` extracellular domain/topology、`RQ-03` protein-level provenance,三者缺一不可且全部要求蛋白层面来源。跨膜段单独 → `L1-02` DEFER(32);无注释 → `L1-03` DEFER(9);细胞器定位或定位冲突 → `L1-04` DEFER。实测已批准层 `plasma membrane`/`extracellular`/`localization`/`signal peptide`/`GPI` 关键词命中数**均为 0**,故 `retain_requirements_satisfiable_by_approved_inputs: false`,**eligible = 0**。 +- Finding 2 accepted: **泛癌 ADC precedent 不能直接证明 CRC linkage。** 「某靶点已有临床 ADC」可发生在任何癌种,最多证明 ADC modality precedent;`indication_fit` 若为 catalog 派生或模型判断也不能替代源级 CRC 证据(PR #57 已明确模型领域知识单独不足以录入 pair)。 +- Fix 2: `LB-precedent` 改为要求源证据本身含 CRC/colorectal indication,或 CRC 细胞系/PDO/PDX/动物模型的 ADC/preclinical targeting 证据,并记录 `precedent_indication` 与 `source_locator`;仅其他癌种 precedent → `LNK-02b` DEFER 并保留为 target/modality metadata;仅 `indication_fit` → `LNK-02c` DEFER。实测 `measured_source_level_crc_units = 0`——33 条 supporting 单元实质主张均为「Local ADC Index contains ADC precedent for〈药名〉」,不附 indication。 +- Measurement trap recorded: 33 条 statement 全含 "CRC" 字样,但只出现在免责句「precedent does not establish CRC efficacy or a safe therapeutic window」里。**执行者上一轮正是按「statement 是否包含 CRC」计数得到 33/33,属假阳性。** 已写入 `measurement_trap` 禁止该判据。 +- Consequence, PR downgraded: 两条修订各自独立把可 RETAIN 数量归零——target eligible 32→**0**、Eligible Universe Index 32→**0**、active 27→**0**。**已批准证据包无法支撑 Level 01 执行**;执行只会产出空的 Eligible Universe Index 与空快照,既无候选价值又有被误读为「已筛完」的风险。按审核方上一轮给出的退路降级:`scope_of_authorisation: raw_axis_binding_only`、`authorises_level_01_execution: false`。 +- Evidence gaps registered: **`EVGAP-01`**(阻断 LOCK-01,缺蛋白层面 plasma-membrane 定位与 extracellular domain/topology 证据,需受控 target-surface localization evidence extraction);**`EVGAP-02`**(阻断 LOCK-03,缺源级 CRC-specific linkage 证据,需受控 CRC-specific target-context linkage evidence extraction)。两者各需独立 contract-only PR 与 `APPROVE`,均不在本 PR 授权范围。 +- Test invariant changed: 原「至少存在一个非空 linkage 依据」的断言若保留会阻止提交一个**诚实**的绑定。改为——每个依据的 `vacuous_this_run` 必须与其**合格**计数一致(不是 supporting 计数,泛癌 precedent 是 supporting 但不合格),且**无任何依据合格时 `authorises_level_01_execution` 必须为 `false`**。守卫对象由「必须有货」改为「没货就不许执行」。 +- Validation: `Ran 281 tests` 全部通过(`main` 基线 251 + 新增 30);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 +- Mutation-tested (本轮 10 个,全部 `FAILED` 后精确回滚,与备份 `diff -q` 一致、恢复 `OK`): 跨膜段单独恢复 RETAIN、RETAIN 条件削减为只要 `RQ-01`、声明已批准输入可满足 RETAIN、细胞器定位改判 EXCLUDE、`indication_fit` 可替代源级证据、泛癌 precedent 声明为合格、去掉 `requires_source_level_crc_indication`、空池却声明授权执行、coverage 改回 32 eligible、`LNK-02b` 改判 RETAIN。 +- Accepted by reviewer, unchanged: context identity 只由 `indication_id` 决定;endpoint 不进 identity 也未锁定;顺序与重复行不改投影;冲突与残缺 context 进 DEFER;每个 source row 有 provenance;#53/#54 来源禁止;输入 SHA-256 固定;`no_known_linkage_after_complete_search` 本轮不可用;machine-extracted 证据可满足存在性但不得直接晋级 Level 02;不运行 Gate、不评分、不排序;仓库未写入候选/证据/结果。 +- Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十二节。 +- Next: 推送同一 PR 并同步 PR 描述,请求复审。后续两个证据抽取契约需人类负责人决定先做哪个。 + +## 2026-08-04T21:10:00-04:00 — PR #58 第三轮审核裁决与修订(VAL-B07 旧计数残留) + +- Review: ChatGPT 对 PR #58(HEAD `6720edb`,可合并、CI 成功)返回 `REQUEST_CHANGES`。第二轮两个科学语义问题确认已正确修复;降级为 `raw_axis_binding_only`、`authorises_level_01_execution: false` 被认可;`EVGAP-01`/`EVGAP-02` 被确认足以作为后续两个受控抽取契约的范围依据。仅剩一处残留矛盾。 +- Finding accepted: **`VAL-B07` 仍保留旧计数 `32 eligible + 9 hold + 0 killed`**,而主体契约、`lock_01_derivation.coverage` 与 `predicted_result_shape` 都已改为 `0 eligible + 41 hold + 0 killed`。同一份机器可读契约同时规定两套互斥结果,未来验证无法确定权威值。**这是第二轮修订的漏改,执行者错误。** +- Fix: `VAL-B07` 散文改为 `0 eligible + 41 hold + 0 killed` 并注明 `41 hold = 32 (L1-02) + 9 (L1-03)`;新增结构化字段 `expected_target_eligibility` 使计数可机械比对而不依赖解析散文;新增 `validates: evidence_insufficient_binding_state` 与 `authorises_result_generation: false`,明确该规则验证「证据不足导致无法执行」的绑定状态、不授权生成 Level 01 结果。 +- Tests: 新增 `test_target_eligibility_counts_agree_in_all_three_places`——断言 `VAL-B07`、`lock_01_derivation.coverage`、`predicted_result_shape.target_eligibility` 三处逐键相等,且散文与结构化计数不矛盾。新增 `test_no_validation_rule_asserts_eligible_targets_while_blocked`——未授权执行时任何验证规则都不得要求存在 eligible target,且每条非空 LOCK-01 规则必须 DEFER。测试 30 → 32。 +- Full-text sweep per acceptance criterion: 仓库内已不存在任何把跨膜段对应的 32 个靶点写成 eligible 的**执行或验证要求**。剩余提到「32」的位置全部为描述性:绑定 YAML 第 318 行说明 `41 hold = 32 + 9`;契约第 81 行把旧映射描述为错误;handoff 第十一节为第一轮历史记录并已加取代标记指向第十二节;第十二节的对照表与变异清单本身即修订记录。 +- Validation: `Ran 283 tests` 全部通过(`main` 基线 251 + 新增 32);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 +- Mutation-tested (本轮 5 个,全部 `FAILED` 后精确回滚,与备份 `diff -q` 一致、恢复 `OK`): `VAL-B07` 结构化计数改回 32、只改散文回 32 使其与结构化字段矛盾、`coverage` 改成与 `VAL-B07` 不一致、`predicted_result_shape` 改成与 `VAL-B07` 不一致、声明该规则可授权生成结果。 +- Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十三节。 +- Next: 推送同一 PR 并同步 PR 描述,请求复审。 diff --git a/tests/test_adc_pool_level_01_input_binding.py b/tests/test_adc_pool_level_01_input_binding.py new file mode 100644 index 0000000..93833ef --- /dev/null +++ b/tests/test_adc_pool_level_01_input_binding.py @@ -0,0 +1,647 @@ +"""Verify docs/pools/adc_pool_level_01_input_binding.yaml. + +The binding pins Level 01's inputs to externally stored, separately approved +run artefacts. These tests check the binding's internal consistency and its +agreement with the merged Level 01 contract. They never read the external +artefacts: those live outside the repository, so the checksums recorded here +are audit metadata, not something this suite can or should resolve. +""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +import yaml + +from genmodules.gen_indication_endpoint_target.contracts import CandidateDisposition + + +REPO_ROOT = Path(__file__).resolve().parents[1] +BINDING_PATH = REPO_ROOT / "docs" / "pools" / "adc_pool_level_01_input_binding.yaml" +LEVEL_CONTRACT_PATH = REPO_ROOT / "docs" / "pools" / "adc_pool_gate_usage.yaml" + +QUARANTINED_STATUS = "UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED" +# Targets that exist only in the quarantined 2026-08-04 run. +QUARANTINE_ONLY_TARGETS = ("GPA33", "LY6G6D", "TNFRSF12A", "CEACAM6") + + +def _load(path: Path) -> dict: + with path.open(encoding="utf-8") as handle: + return yaml.safe_load(handle) + + +class InputBindingTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.binding = _load(BINDING_PATH) + cls.level = { + entry["level"]: entry for entry in _load(LEVEL_CONTRACT_PATH)["levels"] + }["01"] + + def test_binding_targets_the_merged_level_01_contract(self) -> None: + header = self.binding["binding"] + self.assertEqual(header["level"], "01") + self.assertEqual( + header["level_contract_ref"], "docs/pools/adc_pool_gate_usage.yaml" + ) + self.assertTrue(LEVEL_CONTRACT_PATH.exists()) + self.assertEqual(header["execution_status"], "not_authorized_not_executed") + self.assertIs(header["requires_new_enumeration_run"], False) + + def test_every_accepted_source_names_its_approval(self) -> None: + sources = self.binding["accepted_sources"] + self.assertTrue(sources) + for source in sources: + with self.subTest(source=source["source_id"]): + self.assertEqual(source["decision"], "APPROVE") + self.assertIsInstance(source["authorising_pr"], int) + record = REPO_ROOT / source["authorising_record"] + self.assertTrue(record.exists(), f"missing record: {record}") + self.assertIn("APPROVE", record.read_text(encoding="utf-8")) + self.assertTrue(source["run_dir"].startswith("external:")) + + def test_every_pinned_file_has_a_sha256(self) -> None: + for source in self.binding["accepted_sources"]: + for item in source["files"]: + with self.subTest(path=item["path"]): + digest = item["sha256"] + self.assertEqual(len(digest), 64, digest) + self.assertTrue( + all(c in "0123456789abcdef" for c in digest), digest + ) + + def test_quarantined_runs_are_barred_not_merely_unlisted(self) -> None: + barred = self.binding["barred_sources"] + self.assertEqual(len(barred), 2) + accepted_dirs = {s["run_dir"] for s in self.binding["accepted_sources"]} + for source in barred: + with self.subTest(source=source["source_id"]): + self.assertEqual(source["status"], QUARANTINED_STATUS) + self.assertTrue(source["barred_content"]) + self.assertNotIn(source["run_dir"], accepted_dirs) + self.assertEqual({s["related_pr"] for s in barred}, {53, 54}) + + def test_scope_consequence_arithmetic_is_stated_and_correct(self) -> None: + scope = self.binding["scope_consequences"] + self.assertEqual( + scope["raw_enumeration_matrix_pairs"], + scope["raw_clinical_contexts"] * scope["raw_targets"], + ) + enumeration = next( + s + for s in self.binding["accepted_sources"] + if s["source_id"] == "crc_enumeration_20260802" + ) + universe = next( + f for f in enumeration["files"] + if f["path"] == "indication_endpoint_universe.tsv" + ) + catalog = next( + f for f in enumeration["files"] + if f["path"] == "target_evidence_catalog.tsv" + ) + self.assertEqual(scope["raw_clinical_contexts"], universe["distinct_indications"]) + self.assertEqual(scope["raw_targets"], catalog["rows"]) + + def test_lock_02_ceiling_covers_every_context_and_forbids_upgrades(self) -> None: + ceiling = self.binding["lock_02_status_ceiling"] + self.assertEqual( + sum(entry["count"] for entry in ceiling), + self.binding["scope_consequences"]["raw_clinical_contexts"], + ) + outcomes = { + outcome["outcome"]: outcome + for lock in self.level["locks"] + if lock["lock_id"] == "LOCK-02" + for outcome in lock["outcomes"] + } + for entry in ceiling: + with self.subTest(status=entry["source_status"]): + self.assertIn(entry["max_outcome"], outcomes) + # Anything not calibrated must be forced to DEFER, never RETAIN. + if entry["calibration"] != "calibrated": + self.assertEqual( + entry["forced_disposition"], CandidateDisposition.DEFER.value + ) + self.assertEqual( + outcomes[entry["max_outcome"]]["disposition"], + CandidateDisposition.DEFER.value, + ) + calibrated = [e for e in ceiling if e["calibration"] == "calibrated"] + self.assertEqual(len(calibrated), 1) + self.assertEqual(calibrated[0]["max_outcome"], "validated_unmet_context") + + def test_prior_disposition_labels_are_not_reusable_as_lock_01_output(self) -> None: + semantics = self.binding["lock_01_input_semantics"] + self.assertIs(semantics["catalog_disposition_is_candidate_filter_result"], False) + self.assertIs(semantics["may_be_inherited_as_lock_01_outcome"], False) + # The prior labels must not collide with CandidateDisposition values. + prior = {v.lower() for v in semantics["catalog_disposition_values"]} + contract = {item.value.lower() for item in CandidateDisposition} + self.assertEqual(prior & contract, set()) + + def test_linkage_rules_never_exclude(self) -> None: + rules = self.binding["lock_03_linkage_rules"] + lock_03 = next(l for l in self.level["locks"] if l["lock_id"] == "LOCK-03") + valid = {o["outcome"]: o for o in lock_03["outcomes"]} + for rule in rules["rules"]: + with self.subTest(rule=rule["id"]): + self.assertIn(rule["outcome"], valid) + self.assertIn( + rule["disposition"], + {CandidateDisposition.RETAIN.value, CandidateDisposition.DEFER.value}, + ) + self.assertEqual( + rule["resulting_state"], valid[rule["outcome"]]["resulting_state"] + ) + self.assertEqual( + rules["evidence_granularity"], "disease_level_not_subgroup_level" + ) + + def test_vacuous_linkage_bases_must_block_execution(self) -> None: + """If no linkage basis qualifies, the binding must not authorise a run.""" + + bases = self.binding["lock_03_linkage_rules"]["accepted_linkage_bases"] + self.assertGreaterEqual(len(bases), 2) + ids = [b["basis_id"] for b in bases] + self.assertEqual(len(ids), len(set(ids))) + for basis in bases: + with self.subTest(basis=basis["basis_id"]): + self.assertEqual(basis["required_direction"], "supporting") + # Vacuity is decided by QUALIFYING units, not merely supporting + # ones: pan-cancer precedent is supporting but does not qualify. + self.assertIsInstance(basis["measured_qualifying_units"], int) + self.assertEqual( + basis["vacuous_this_run"], basis["measured_qualifying_units"] == 0 + ) + live = [b for b in bases if not b["vacuous_this_run"]] + if not live: + self.assertIs(self.binding["binding"]["authorises_level_01_execution"], False) + + def test_pan_cancer_precedent_cannot_establish_crc_linkage(self) -> None: + basis = next( + b for b in self.binding["lock_03_linkage_rules"]["accepted_linkage_bases"] + if b["basis_id"] == "LB-precedent" + ) + self.assertIs(basis["requires_source_level_crc_indication"], True) + self.assertIs(basis["indication_fit_may_substitute"], False) + self.assertEqual(basis["other_cancer_precedent_disposition"], "metadata_only_hold") + for field in ("precedent_indication", "source_locator"): + self.assertIn(field, basis["required_recorded_fields"]) + # Supporting-but-not-qualifying must be visible, not collapsed. + self.assertGreater(basis["measured_supporting_units"], 0) + self.assertEqual(basis["measured_qualifying_units"], 0) + self.assertTrue(basis["measurement_trap"].strip()) + + rules = {r["id"]: r for r in self.binding["lock_03_linkage_rules"]["rules"]} + for rule_id in ("LNK-02b", "LNK-02c"): + with self.subTest(rule=rule_id): + self.assertEqual( + rules[rule_id]["disposition"], CandidateDisposition.DEFER.value + ) + self.assertEqual(rules[rule_id]["resulting_state"], "hold") + + def test_transmembrane_topology_alone_cannot_retain(self) -> None: + derivation = self.binding["lock_01_derivation"] + self.assertIs(derivation["retain_requirements_satisfiable_by_approved_inputs"], False) + required = {r["requirement_id"] for r in derivation["retain_requirements"]} + self.assertEqual(required, {"RQ-01", "RQ-02", "RQ-03"}) + self.assertEqual(set(derivation["retain_requires_all_of"]), required) + claims = {r["requirement_id"]: r for r in derivation["retain_requirements"]} + self.assertEqual(claims["RQ-01"]["claim"], "plasma_membrane_localization") + self.assertEqual(claims["RQ-02"]["claim"], "extracellular_domain_or_topology") + for requirement in derivation["retain_requirements"]: + self.assertIs(requirement["must_be_protein_level"], True) + + rules = {r["id"]: r for r in derivation["rules"]} + # The transmembrane-only rule must defer, and the RETAIN rule must + # demand all three requirements rather than a bare locator. + tm_rule = rules["L1-02"] + self.assertIn("transmembrane_segment_count", tm_rule["condition"]) + self.assertEqual(tm_rule["disposition"], CandidateDisposition.DEFER.value) + self.assertEqual(tm_rule["resulting_state"], "hold") + retain = rules["L1-01"] + self.assertEqual(retain["disposition"], CandidateDisposition.RETAIN.value) + for requirement_id in required: + self.assertIn(requirement_id, retain["condition"]) + self.assertNotIn("transmembrane_segment_count", retain["condition"]) + + def test_organelle_or_conflicting_localization_defers(self) -> None: + rules = {r["id"]: r for r in self.binding["lock_01_derivation"]["rules"]} + organelle = rules["L1-04"] + self.assertEqual(organelle["disposition"], CandidateDisposition.DEFER.value) + self.assertEqual(organelle["resulting_state"], "hold") + self.assertEqual(organelle["outcome"], "possible_surface_target") + + def test_evidence_gaps_block_execution_and_name_their_next_run(self) -> None: + gaps = {g["id"]: g for g in self.binding["evidence_gaps"]} + self.assertEqual(set(gaps), {"EVGAP-01", "EVGAP-02"}) + self.assertEqual( + set(self.binding["binding"]["level_01_execution_blocked_by"]), set(gaps) + ) + self.assertEqual( + self.binding["binding"]["scope_of_authorisation"], "raw_axis_binding_only" + ) + self.assertIs(self.binding["binding"]["authorises_level_01_execution"], False) + self.assertIs(self.binding["predicted_result_shape"]["is_authorised_to_execute"], False) + for gap in gaps.values(): + with self.subTest(gap=gap["id"]): + self.assertIn(gap["blocks"], {"LOCK-01", "LOCK-03"}) + for field in ("missing", "measured", "consequence", "required_next_run"): + self.assertTrue(str(gap[field]).strip()) + not_authorised = " ".join(self.binding["not_authorised"]) + self.assertIn("执行 Level 01", not_authorised) + + def test_predicted_shape_reconciles_with_the_counting_identities(self) -> None: + shape = self.binding["predicted_result_shape"] + scope = self.binding["scope_consequences"] + ctx, tgt = shape["context_eligibility"], shape["target_eligibility"] + pool = shape["pool_level_01"] + + # CNT-01 + self.assertEqual(shape["raw_enumeration_matrix"], scope["raw_enumeration_matrix_pairs"]) + # CNT-04 and CNT-05 + self.assertEqual( + ctx["eligible"] + ctx["hold"] + ctx["superseded"], scope["raw_clinical_contexts"] + ) + self.assertEqual( + tgt["eligible"] + tgt["hold"] + tgt["killed"], scope["raw_targets"] + ) + # CNT-02 + self.assertEqual( + shape["eligible_universe_index"], ctx["eligible"] * tgt["eligible"] + ) + # CNT-03 + self.assertEqual( + pool["active"] + pool["hold"] + pool["reactivation_eligible"], + shape["eligible_universe_index"], + ) + # Consistent with the LOCK-01 derivation and the LOCK-02 ceiling. + self.assertEqual(tgt["eligible"], self.binding["lock_01_derivation"]["coverage"]["eligible"]) + self.assertEqual(tgt["hold"], self.binding["lock_01_derivation"]["coverage"]["hold"]) + self.assertEqual(tgt["killed"], 0) + # An empty universe must not be presented as an authorised run. + if shape["eligible_universe_index"] == 0: + self.assertIs(shape["is_authorised_to_execute"], False) + self.assertTrue(shape["conclusion"].strip()) + calibrated = [ + e for e in self.binding["lock_02_status_ceiling"] + if e["calibration"] == "calibrated" + ] + self.assertEqual(ctx["eligible"], sum(e["count"] for e in calibrated)) + self.assertTrue(shape["derivation_note"].strip()) + + def test_complete_search_exclusion_is_unavailable_with_this_input(self) -> None: + rules = self.binding["lock_03_linkage_rules"] + self.assertIs(rules["no_known_linkage_after_complete_search_available"], False) + self.assertTrue(rules["no_known_linkage_unavailable_reason"].strip()) + forbidding = [ + r for r in self.binding["output_validation"]["additional_rules"] + if "no_known_linkage_after_complete_search" in r["rule"] + ] + self.assertTrue(forbidding, "no validation rule forbids the outcome") + + def test_unreviewed_evidence_may_enter_level_01_but_not_advance(self) -> None: + rules = self.binding["lock_03_linkage_rules"] + self.assertIs(rules["machine_extracted_evidence_satisfies_existence"], True) + self.assertIs(rules["requires_review_status_column"], True) + constraint = rules["carry_forward_constraint"] + self.assertIn("Level 02", constraint) + self.assertTrue(constraint.strip()) + + def test_measured_limits_of_the_evidence_package_are_recorded(self) -> None: + evidence = next( + s + for s in self.binding["accepted_sources"] + if s["source_id"] == "crc_target_evidence_20260801" + ) + limits = evidence["measured_limits"] + self.assertEqual(limits["granularity"], "target_level_only") + self.assertIs(limits["has_clinical_context_column"], False) + self.assertEqual( + limits["all_units_review_status"], + "machine_extracted_requires_human_review", + ) + self.assertLess( + limits["expert_review_batches_passed"], + limits["expert_review_batches_total"], + ) + + def test_authorisation_scope_is_explicit_and_bounded(self) -> None: + self.assertTrue(self.binding["authorises"]) + not_authorised = " ".join(self.binding["not_authorised"]) + for phrase in ("枚举", "Gate", "Level 02", "endpoint"): + with self.subTest(phrase=phrase): + self.assertIn(phrase, not_authorised) + + # ---- Blocker 1: LOCK-01 must be derivable without free judgement ---- + + def test_lock_01_derivation_maps_every_outcome_uniquely(self) -> None: + derivation = self.binding["lock_01_derivation"] + lock_01 = next(l for l in self.level["locks"] if l["lock_id"] == "LOCK-01") + declared = {o["outcome"]: o for o in lock_01["outcomes"]} + + produced = {r["outcome"] for r in derivation["rules"]} + unavailable = {u["outcome"] for u in derivation["unavailable_outcomes"]} + # Every outcome the level declares is either produced or explicitly barred. + self.assertEqual(produced | unavailable, set(declared)) + self.assertEqual(produced & unavailable, set()) + + for rule in derivation["rules"]: + with self.subTest(rule=rule["id"]): + outcome = declared[rule["outcome"]] + self.assertEqual(rule["disposition"], outcome["disposition"]) + self.assertEqual(rule["resulting_state"], outcome["resulting_state"]) + for barred in derivation["unavailable_outcomes"]: + with self.subTest(outcome=barred["outcome"]): + self.assertTrue(barred["reason"].strip()) + + def test_lock_01_rna_evidence_can_never_retain(self) -> None: + derivation = self.binding["lock_01_derivation"] + self.assertIs(derivation["rna_derived_locators_may_retain"], False) + retaining = [ + r for r in derivation["rules"] + if r["disposition"] == CandidateDisposition.RETAIN.value + ] + self.assertTrue(retaining) + for rule in retaining: + with self.subTest(rule=rule["id"]): + # RETAIN must rest on localization plus extracellular topology, + # never on a bare annotation basis. + self.assertEqual( + rule["evidence_basis"], + "plasma_membrane_localization_and_extracellular_topology", + ) + # Any rule whose reason invokes RNA must defer. + for rule in derivation["rules"]: + if "RNA" in rule.get("reason", ""): + self.assertEqual(rule["disposition"], CandidateDisposition.DEFER.value) + + def test_lock_01_missing_or_conflicting_evidence_never_excludes(self) -> None: + derivation = self.binding["lock_01_derivation"] + for rule in derivation["rules"]: + with self.subTest(rule=rule["id"]): + self.assertNotEqual( + rule["disposition"], CandidateDisposition.EXCLUDE.value + ) + self.assertIn( + "not_surface_target", + {u["outcome"] for u in derivation["unavailable_outcomes"]}, + ) + self.assertEqual(derivation["coverage"]["killed"], 0) + + def test_lock_01_cannot_read_prior_dispositions_or_gate_labels(self) -> None: + derivation = self.binding["lock_01_derivation"] + barred = set(derivation["barred_fields"]) + for field in ("disposition", "gate_score_status", "gate_pass_status"): + self.assertIn(field, barred) + # The decisive field must not itself be a barred field. + self.assertNotIn(derivation["decisive_field"], barred) + semantics = self.binding["lock_01_input_semantics"] + self.assertIs(semantics["may_be_inherited_as_lock_01_outcome"], False) + + def test_lock_01_coverage_accounts_for_every_target(self) -> None: + derivation = self.binding["lock_01_derivation"] + coverage = derivation["coverage"] + self.assertEqual( + coverage["eligible"] + coverage["hold"] + coverage["killed"], + coverage["total_targets"], + ) + self.assertEqual( + coverage["total_targets"], + self.binding["scope_consequences"]["raw_targets"], + ) + self.assertIs(coverage["determinate_without_free_judgement"], True) + # Non-vacuous rules must carry an expected count, and those must reconcile. + counted = sum( + r["expected_count"] for r in derivation["rules"] if "expected_count" in r + ) + self.assertEqual(counted, coverage["total_targets"]) + for rule in derivation["rules"]: + if "expected_count" not in rule: + with self.subTest(rule=rule["id"]): + self.assertIs(rule["vacuous_this_run"], True) + self.assertTrue(rule["vacuous_reason"].strip()) + + # ---- Blocker 2: the context projection must be deterministic ---- + + def _project(self, rows: list[dict]) -> dict: + """Reference implementation of the declared projection rules.""" + + projection = self.binding["clinical_context_projection"] + key_fields = projection["determinism"]["dedupe_key"] + deduped = {tuple(row[f] for f in key_fields): row for row in rows} + ordered = sorted( + deduped.values(), + key=lambda r: tuple(r[f] for f in projection["determinism"]["sort_rows_by"]), + ) + groups: dict[str, list[dict]] = {} + for row in ordered: + groups.setdefault(row[projection["group_by"]], []).append(row) + + contexts = {} + for indication_id, members in sorted(groups.items()): + constant = projection["context_level_fields_required_constant"] + conflicted = any( + len({m[field] for m in members}) != 1 for field in constant + ) + roles = {m["endpoint_role"] for m in members} + incomplete = roles != set( + projection["endpoint_handling"]["required_endpoint_roles"] + ) + contexts[indication_id] = { + "clinical_context_ref": projection["context_ref_template"].format( + indication_id=indication_id + ), + "endpoint_candidates": [ + (m["endpoint_role"], m["endpoint"]) for m in members + ], + "endpoint_maturity": projection["endpoint_handling"][ + "endpoint_maturity_value" + ], + "source_row_keys": [ + tuple(m[f] for f in projection["provenance"]["source_row_key_fields"]) + for m in members + ], + "outcome": "undefined_context" if (conflicted or incomplete) else None, + } + return contexts + + @staticmethod + def _fixture() -> list[dict]: + """Synthetic rows mirroring the approved file's schema. Not real data.""" + + roles = ( + ("regulatory_ultimate", "OS"), + ("pivotal_supporting", "PFS"), + ("early_adc_proof", "ORR"), + ("supportive_exploratory", "DCR"), + ) + rows = [] + for n in range(1, 10): + for role, endpoint in roles: + rows.append( + { + "indication_id": f"ctx_{n:02d}", + "label": f"context {n}", + "status": "derived_strategy", + "source": "pilot", + "clinical_need": "need", + "confidence": "not_calibrated", + "priority": str(n), + "endpoint_role": role, + "endpoint": endpoint, + "rationale": f"rationale {role}", + } + ) + return rows + + def test_projection_turns_36_rows_into_exactly_9_contexts(self) -> None: + projection = self.binding["clinical_context_projection"] + self.assertEqual(projection["input_rows"], 36) + self.assertEqual( + projection["output_contexts"], + self.binding["scope_consequences"]["raw_clinical_contexts"], + ) + rows = self._fixture() + self.assertEqual(len(rows), projection["input_rows"]) + contexts = self._project(rows) + self.assertEqual(len(contexts), projection["output_contexts"]) + self.assertTrue(all(c["outcome"] is None for c in contexts.values())) + + def test_projection_is_independent_of_input_row_order(self) -> None: + rows = self._fixture() + forward = self._project(rows) + backward = self._project(list(reversed(rows))) + # Deterministic: reversing the input changes nothing, including provenance. + self.assertEqual(forward, backward) + rotated = self._project(rows[7:] + rows[:7]) + self.assertEqual(forward, rotated) + + def test_duplicate_endpoint_rows_do_not_change_context_identity(self) -> None: + rows = self._fixture() + baseline = self._project(rows) + with_dupes = self._project(rows + rows[:5]) + self.assertEqual(baseline, with_dupes) + self.assertEqual( + self.binding["clinical_context_projection"]["determinism"][ + "measured_duplicate_role_pairs" + ], + 0, + ) + + def test_conflicting_or_incomplete_rows_defer_and_never_exclude(self) -> None: + projection = self.binding["clinical_context_projection"] + for rule in projection["conflict_handling"]: + with self.subTest(rule=rule["id"]): + self.assertEqual(rule["disposition"], CandidateDisposition.DEFER.value) + self.assertEqual(rule["resulting_state"], "hold") + self.assertEqual(rule["outcome"], "undefined_context") + + # A varying context-level field must take the conflict path. + rows = self._fixture() + rows[0] = dict(rows[0], confidence="calibrated") + conflicted = self._project(rows) + self.assertEqual(conflicted["ctx_01"]["outcome"], "undefined_context") + + # A missing endpoint role must take the same path. + rows = [r for r in self._fixture() if not ( + r["indication_id"] == "ctx_02" and r["endpoint_role"] == "early_adc_proof" + )] + incomplete = self._project(rows) + self.assertEqual(incomplete["ctx_02"]["outcome"], "undefined_context") + self.assertIsNone(incomplete["ctx_01"]["outcome"]) + + def test_every_input_row_is_traceable_to_its_context(self) -> None: + projection = self.binding["clinical_context_projection"] + self.assertIs(projection["provenance"]["every_input_row_must_be_referenced"], True) + rows = self._fixture() + contexts = self._project(rows) + referenced = [key for c in contexts.values() for key in c["source_row_keys"]] + expected = [ + tuple(r[f] for f in projection["provenance"]["source_row_key_fields"]) + for r in rows + ] + self.assertEqual(sorted(referenced), sorted(expected)) + self.assertEqual(len(referenced), len(set(referenced)), "row referenced twice") + + def test_context_identity_excludes_endpoint_and_endpoint_stays_unlocked(self) -> None: + projection = self.binding["clinical_context_projection"] + self.assertEqual(projection["identity_fields"], ["indication_id"]) + self.assertEqual(projection["context_ref_depends_only_on"], "indication_id") + handling = projection["endpoint_handling"] + self.assertIs(handling["endpoint_locked"], False) + for dropped in ("endpoint", "endpoint_role"): + self.assertIn(dropped, handling["dropped_from_context_identity"]) + self.assertEqual(handling["endpoint_maturity_value"], "not_locked_at_level_01") + # Changing an endpoint value must not change any context ref. + rows = self._fixture() + baseline = self._project(rows) + rows[1] = dict(rows[1], endpoint="EFS") + altered = self._project(rows) + self.assertEqual( + {k: v["clinical_context_ref"] for k, v in baseline.items()}, + {k: v["clinical_context_ref"] for k, v in altered.items()}, + ) + + def test_target_eligibility_counts_agree_in_all_three_places(self) -> None: + """VAL-B07, the derivation coverage and the predicted shape must match.""" + + rule = next( + r for r in self.binding["output_validation"]["additional_rules"] + if r["id"] == "VAL-B07" + ) + expected = rule["expected_target_eligibility"] + coverage = self.binding["lock_01_derivation"]["coverage"] + predicted = self.binding["predicted_result_shape"]["target_eligibility"] + + keys = ("eligible", "hold", "killed") + self.assertEqual(set(expected), set(keys)) + for key in keys: + with self.subTest(key=key): + self.assertEqual(expected[key], coverage[key]) + self.assertEqual(expected[key], predicted[key]) + # The prose of the rule must not contradict its structured counts. + for key in keys: + self.assertIn(f"{expected[key]} {key}", rule["rule"]) + # This rule validates a blocked state; it authorises no result. + self.assertIs(rule["authorises_result_generation"], False) + self.assertEqual(rule["validates"], "evidence_insufficient_binding_state") + + def test_no_validation_rule_asserts_eligible_targets_while_blocked(self) -> None: + """No rule may require eligible targets when execution is not authorised.""" + + if self.binding["binding"]["authorises_level_01_execution"]: + self.skipTest("execution is authorised; this guard does not apply") + self.assertEqual( + self.binding["lock_01_derivation"]["coverage"]["eligible"], 0 + ) + for rule in self.binding["output_validation"]["additional_rules"]: + counts = rule.get("expected_target_eligibility") + if counts is None: + continue + with self.subTest(rule=rule["id"]): + self.assertEqual(counts["eligible"], 0) + # Every non-vacuous LOCK-01 rule must defer while requirements are unmet. + for lock_rule in self.binding["lock_01_derivation"]["rules"]: + if lock_rule.get("vacuous_this_run"): + continue + with self.subTest(rule=lock_rule["id"]): + self.assertEqual( + lock_rule["disposition"], CandidateDisposition.DEFER.value + ) + + def test_validation_rules_bar_quarantine_only_targets(self) -> None: + rules = " ".join( + r["rule"] for r in self.binding["output_validation"]["additional_rules"] + ) + for target in QUARANTINE_ONLY_TARGETS: + with self.subTest(target=target): + self.assertIn(target, rules) + ids = [r["id"] for r in self.binding["output_validation"]["additional_rules"]] + self.assertEqual(len(ids), len(set(ids))) + + +if __name__ == "__main__": + unittest.main()