From 8f5c85d1025f545d03f0c396eff6cea7412c6292 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 20:23:05 -0400 Subject: [PATCH 1/4] task_20260804: bind ADC Pool Level 01 inputs to approved runs (contract-only) Discharges PR #57's BLOCK-02 by binding Level 01's two raw axes and its linkage evidence to externally stored artefacts that already carry their own APPROVE. Nothing is executed: no context, target, pair, disposition or ranking is produced, and no new enumeration run is authorised. Correction to BLOCK-02 as written in PR #57. It said the only context enumeration came from the quarantined 2026-08-04 run. That was inaccurate. The 2026-08-02 enumeration was approved by PR #29, whose record explicitly authorises using its output as input to a further task, and the target-level evidence extraction was approved by PR #31. So an un-quarantined input chain exists, and the correct statement is that the 2026-08-04 artefacts must not be used, not that no usable context exists. I had generalised "quarantined" into "no usable input". Consequence: no new enumeration run is needed, so the previously stated "two contracts and two runs" reduces to one contract and one Level 01 execution. PR #57's approved text is not rewritten; the correction lives here. Three hard constraints measured from the bound artefacts: LOCK-02 admits at most one eligible context. The nine contexts are one canonical_c0 at confidence 0.93, seven not_calibrated derived strategies and one benchmark_only subgroup. PR #28's own prohibition on promoting a derived strategy to canonical fact is inherited as an outcome ceiling, so uncalibrated sources are forced to DEFER, enforced by test. The existing disposition column cannot be inherited as LOCK-01 output. Its values are benchmark/candidate/hold, produced under different criteria, and all 41 rows read not_scored_in_enumeration_run / not_assessed. Linkage evidence is target-level and disease-level: 292 units over 41 genes and 7 dimensions with no context column, all machine_extracted_requires_human_review, and only 2 of 20 expert-review batches complete. Disease-level evidence therefore cannot establish subgroup-specific linkage, and no_known_linkage_after_complete_search is unavailable because the search scope is not closed. DECISION-02 is recorded for adjudication: unreviewed machine-extracted evidence satisfies LOCK-03 existence, but such pairs must carry a review-status column and must not advance to Level 02 until expert review passes. Predicted shape is written in advance so a small pool is not misread as failure: one eligible context, active pool bounded by 41 pairs, 328 pairs on hold, and the real bottleneck is the remaining 18 expert-review batches. 264 tests pass. No change under src/, genmodules/, extensions/, docs/architecture/ or AGENTS.md, and no Gate added. Co-Authored-By: Claude Opus 5 --- ...4-adc-pool-level-01-input-binding.zh-CN.md | 96 ++++++++ .../adc_pool_level_01_input_binding.yaml | 200 ++++++++++++++++ ...L_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md | 135 +++++++++++ logs/worklog.md | 20 ++ tests/test_adc_pool_level_01_input_binding.py | 214 ++++++++++++++++++ 5 files changed, 665 insertions(+) create mode 100644 docs/handoff/2026-08-04-adc-pool-level-01-input-binding.zh-CN.md create mode 100644 docs/pools/adc_pool_level_01_input_binding.yaml create mode 100644 docs/tasks/ADC_POOL_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md create mode 100644 tests/test_adc_pool_level_01_input_binding.py 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..e7c8788 --- /dev/null +++ b/docs/handoff/2026-08-04-adc-pool-level-01-input-binding.zh-CN.md @@ -0,0 +1,96 @@ +# 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** +- 外部运行:**无。没有执行任何运行,没有产生任何 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` | 13 项校验,把绑定钉在已合并的 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。所以 Eligible Universe Index 上限是 1 × |eligible_targets| ≤ 41 pairs。 + +**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`,规则与测试都已就位。 + +## 七、可以预见的结果形状(预先写明,避免误读) + +按上述规则,执行后的预期是:**1 个 `eligible` context、8 个 `hold`;active pool 上限 41 个 pair,全部集中在 canonical MSS/pMMR mCRC 3L+;其余 328 个 pair 落 `hold`。** + +池子很小,真正的瓶颈不在漏斗设计,在**剩余 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」(实测 264)。仍属无关改动。 + +## 九、验证结果 + +- `Ran 264 tests` 全部通过(`main` 基线 251 + 本次新增 13)。 +- `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。 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..c0b1275 --- /dev/null +++ b/docs/pools/adc_pool_level_01_input_binding.yaml @@ -0,0 +1,200 @@ +# 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 + +# 允许作为 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 首次执行的范围 + 因此小于该运行,这是正确结果而不是退步。 + +# 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-03:证据只有 target 级,没有 context 列。这决定了 pair 级 linkage 的上限。 +lock_03_linkage_rules: + evidence_dimension_used: crc_prevalence + # 疾病级证据可支撑 canonical CRC 情境,但不能区分 8 个亚群情境。 + evidence_granularity: disease_level_not_subgroup_level + rules: + - id: LNK-01 + condition: crc_prevalence 单元存在且 direction=supporting,且该情境为 canonical_c0 + outcome: linkage_evidence_exists + disposition: RETAIN + - id: LNK-02 + condition: crc_prevalence 单元存在且 direction=supporting,但该情境为 derived 或 benchmark 亚群 + outcome: linkage_unassessed + disposition: DEFER + reason: 疾病级证据不能建立亚群特异 linkage。 + - id: LNK-03 + condition: crc_prevalence 单元 direction=unknown 或不存在 + outcome: linkage_unassessed + disposition: DEFER + - id: LNK-04 + condition: 未按 LOCK-03 规定范围完成检索 + outcome: linkage_evidence_missing + disposition: DEFER + # 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: + - 按本绑定执行 Level 01 一次,产出 docs/pools/adc_pool_gate_usage.yaml 规定的五份产物。 +not_authorised: + - 任何新的 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)。 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..96508c0 --- /dev/null +++ b/docs/tasks/ADC_POOL_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md @@ -0,0 +1,135 @@ +# 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 审核** + +## 目的 + +解除 PR #57 的 `BLOCK-02`,把 Level 01 的两条原始轴与 linkage 证据绑定到**已获独立批准**的外部产物上,并冻结「来源状态 → LOCK 输出上限」的映射。 + +本文件**不执行 Level 01**,也**不授权任何新的枚举运行**。 + +## 一、关键发现:不需要重跑枚举 + +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`。** Eligible Universe Index 因此上限为 1 × |eligible_targets| ≤ 41 pairs。这个数字小,但它是「只用已批准证据」的真实结果。 + +## 六、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`。LOCK-01 必须独立推导。测试断言这三个标签与 `CandidateDisposition` 的取值无交集,避免混读。 + +## 七、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 个靶点。 + +据此冻结四条规则: + +| ID | 条件 | outcome | disposition | +|---|---|---|---| +| `LNK-01` | `crc_prevalence` 单元存在且 `supporting`,且该 context 为 `canonical_c0` | `linkage_evidence_exists` | RETAIN | +| `LNK-02` | 同上但 context 为 derived 或 benchmark 亚群 | `linkage_unassessed` | DEFER | +| `LNK-03` | `crc_prevalence` 方向为 `unknown` 或不存在 | `linkage_unassessed` | DEFER | +| `LNK-04` | 未按规定范围完成检索 | `linkage_evidence_missing` | DEFER | + +`LNK-02` 的理由是:疾病级证据不能建立亚群特异 linkage。这与第五节的 LOCK-02 上限相互独立,但结论一致——目前只有 canonical context 能走到 RETAIN。 + +**`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`,规则与测试都已就位。 + +## 八、本契约授权与不授权 + +**授权:** 按本绑定执行 Level 01 一次,产出 PR #57 契约规定的五份产物。 + +**不授权:** 任何新的 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` 前不得发布任何排序、推荐或资产决策。 + +## 十、可以预见的结果形状 + +按第五至七节的规则,本次执行的预期结果是:**1 个 `eligible` context,8 个 `hold`;active pool 上限 41 个 pair,全部集中在 canonical MSS/pMMR mCRC 3L+ 情境;其余 328 个 pair 落在 `hold`。** + +这个池子很小,而且真正的瓶颈不在漏斗设计,在**剩余 18 个专家复核批次**——与 PR #54 M6「portfolio 受数据集限制而非 Gate 限制」的判断一致,只是这次结论建立在已批准证据上。此处预先写明,是为了避免结果出来后被误读为 Level 01 失效。 + +## 十一、当前阻断 + +- 本契约获 ChatGPT `APPROVE` 前,不得执行 Level 01。 +- 本仓库不得写入候选池、快照、证据、cache、result 或 weights。 +- `DECISION-02` 未获裁决前,执行者不得自行改用严格方案。 diff --git a/logs/worklog.md b/logs/worklog.md index e851361..e5f4d7c 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2443,3 +2443,23 @@ 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` 的更正是否成立。 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..faff2a1 --- /dev/null +++ b/tests/test_adc_pool_level_01_input_binding.py @@ -0,0 +1,214 @@ +"""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_outcomes = {o["outcome"] for o in lock_03["outcomes"]} + for rule in rules["rules"]: + with self.subTest(rule=rule["id"]): + self.assertIn(rule["outcome"], valid_outcomes) + self.assertIn( + rule["disposition"], + {CandidateDisposition.RETAIN.value, CandidateDisposition.DEFER.value}, + ) + self.assertEqual( + rules["evidence_granularity"], "disease_level_not_subgroup_level" + ) + + 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) + + 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() From 8ac045e2bb385d1cdc4376c69aba1aa0e196fc54 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 20:46:43 -0400 Subject: [PATCH 2/4] task_20260804: address PR #58 review, freeze LOCK-01 and context projection Both blockers accepted and fixed in this PR. A third error surfaced while fixing them and is corrected here too. Blocker 1: LOCK-01 was not bound to executable evidence. The contract only said it must be derived independently, which left how it is derived to the executor, so the eligible/hold/killed split over 41 targets was not reproducible. I first checked whether the approved layer contains protein-level surface evidence at all, since the alternative was to downgrade this PR to binding the raw target axis only. It does: surface_reachability carries 32 transmembrane_segment_count annotations with supporting direction, and 9 not_available. lock_01_derivation now fixes one source, one dimension, one join key and one decisive field, bars seven fields from the derivation including the prior disposition and both gate labels, whitelists a single locator for RETAIN, and defers on missing, conflicting and RNA-derived evidence. not_surface_target and identity_unresolved are declared unavailable this run: the approved layer contains no row asserting that a target is not a surface protein, and no identity-resolution field, so nothing may be excluded. 32 + 9 = 41 with no discretion and no exclusion. Blocker 2: the 36-row to 9-context projection was not frozen. The arithmetic was fine but the semantics were not unique. clinical_context_projection now fixes identity to indication_id alone, derives the context ref from it alone, requires six context-level fields to be constant within a group, collapses the four endpoint roles into unlocked metadata, fixes sort and dedupe keys, and routes conflicts and missing roles to undefined_context DEFER. Tests implement the declared rules over a synthetic fixture and prove order independence, duplicate tolerance, conflict routing and full provenance. Third error, self-caught by measurement: all 41 crc_prevalence units are unknown/not_available, so binding LOCK-03 to that dimension alone would have guaranteed an empty active pool. The source document's Lock 3 already lists existing CRC clinical targeting evidence as a valid linkage form, so binding only expression evidence was a misreading. LOCK-03 now accepts two bases, and a test asserts at least one is non-vacuous and that each basis's vacuity matches its measured count, so an all-empty binding fails loudly instead of silently producing an empty pool. The predicted shape is corrected from a misleading upper bound to exact values with a reconciliation test: 369 raw pairs, 1 eligible context, 32 eligible targets, 32 in the Eligible Universe Index, 27 active and 5 hold. The contract now also requires the result report to state that all 27 active pairs rest on ADC precedent alone, with no CRC expression evidence at all. 277 tests pass. Ten mutations caught and rolled back exactly. Co-Authored-By: Claude Opus 5 --- ...4-adc-pool-level-01-input-binding.zh-CN.md | 58 +++- .../adc_pool_level_01_input_binding.yaml | 240 +++++++++++++- ...L_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md | 86 ++++- logs/worklog.md | 16 + tests/test_adc_pool_level_01_input_binding.py | 313 +++++++++++++++++- 5 files changed, 693 insertions(+), 20 deletions(-) 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 index e7c8788..4d4d289 100644 --- 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 @@ -31,7 +31,7 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 |---|---| | `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` | 13 项校验,把绑定钉在已合并的 Level 01 契约与实际存在的批准记录上 | +| `tests/test_adc_pool_level_01_input_binding.py` | 26 项校验,把绑定钉在已合并的 Level 01 契约与实际存在的批准记录上 | ## 四、绑定的输入 @@ -49,7 +49,7 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 ## 五、三条实测得到的硬约束 **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。所以 Eligible Universe Index 上限是 1 × |eligible_targets| ≤ 41 pairs。 +实测 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。所以 Eligible Universe Index 恰为 1 × 32 = 32 pairs。 **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` 取值无交集,防止混读。 @@ -67,7 +67,7 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 ## 七、可以预见的结果形状(预先写明,避免误读) -按上述规则,执行后的预期是:**1 个 `eligible` context、8 个 `hold`;active pool 上限 41 个 pair,全部集中在 canonical MSS/pMMR mCRC 3L+;其余 328 个 pair 落 `hold`。** +按上述规则算出(不是估计):Raw Enumeration Matrix **369**;context 资格 eligible **1**/hold **8**;target 资格 eligible **32**/hold **9**/killed **0**;Eligible Universe Index **32**;Pool Level 01 active **27**/hold **5**/reactivation-eligible **0**。`CNT-03` 对账 32 = 27 + 5 + 0。**执行结果必须逐项等于这些数字**,任一项不符即视为偏离契约。 池子很小,真正的瓶颈不在漏斗设计,在**剩余 18 个专家复核批次**。这与 PR #54 M6「portfolio 受数据集限制而非 Gate 限制」一致,只是这次建立在已批准证据上。预先写明是为了避免结果出来后被误读为 Level 01 失效。 @@ -79,11 +79,11 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 - 没有定义 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」(实测 264)。仍属无关改动。 +- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 277)。仍属无关改动。 ## 九、验证结果 -- `Ran 264 tests` 全部通过(`main` 基线 251 + 本次新增 13)。 +- `Ran 277 tests` 全部通过(`main` 基线 251 + 本次新增 26)。 - `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 个。 @@ -94,3 +94,51 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 - 本契约获 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`。 + +### 阻断 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。 + +同时写入必须出现在结果报告里的结构性限制:**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`。 diff --git a/docs/pools/adc_pool_level_01_input_binding.yaml b/docs/pools/adc_pool_level_01_input_binding.yaml index c0b1275..53a701e 100644 --- a/docs/pools/adc_pool_level_01_input_binding.yaml +++ b/docs/pools/adc_pool_level_01_input_binding.yaml @@ -105,6 +105,35 @@ scope_consequences: 被隔离运行扩大到 20 场景 / 45 靶点,那部分不可用。Level 01 首次执行的范围 因此小于该运行,这是正确结果而不是退步。 +# 按本绑定的规则算出的预期结果。预先写明,使执行结果可被逐项核对, +# 而不是执行完才知道形状。任一项与实际不符即视为执行偏离本契约。 +predicted_result_shape: + raw_enumeration_matrix: 369 + context_eligibility: + eligible: 1 + hold: 8 + superseded: 0 + target_eligibility: + eligible: 32 + hold: 9 + killed: 0 + eligible_universe_index: 32 + pool_level_01: + active: 27 + hold: 5 + reactivation_eligible: 0 + # CNT-03 对账:32 = 27 + 5 + 0 + derivation_note: >- + 1 个 canonical context x 32 个 LOCK-01 eligible target = 32 pair;其中 27 个 target + 命中 LB-precedent(adc_precedent supporting 且 locator 为 + clinical_adc_names;clinical_stage_max),走 LNK-01 得 active;其余 5 个未命中任何 + linkage 依据,走 LNK-03 得 hold。LB-expression 本次为空,不贡献任何 active。 + bottleneck: >- + active pool 完全依赖 ADC precedent,因为已批准层没有任何一条 CRC prevalence + 证据。这意味着 27 个 active pair 全部只有「已有临床 ADC 针对该靶点」这一类 + linkage,没有任何一条 CRC 表达证据。这是本次执行最重要的结构性限制, + 必须写进结果报告,不得让读者误以为 active 代表已有表达支持。 + # LOCK-02:来源状态决定 outcome 上限。禁止把 derived strategy 自动升级为 # canonical clinical fact——这是 PR #28 契约自身的禁令,此处继承。 lock_02_status_ceiling: @@ -137,29 +166,213 @@ lock_01_input_semantics: 且 gate_score_status 全部为 not_scored_in_enumeration_run、 gate_pass_status 全部为 not_assessed。LOCK-01 必须独立推导。 +# LOCK-01 的确定性推导规则。 +# +# 只允许一个来源文件和一个 dimension,且判据只有一个字段(evidence_locator)。 +# 这不是为了简洁,而是因为已批准层里只有这一个可判别字段——它只能区分 +# 「拓扑注释存在」与「不可得」。据此 LOCK-01 只能产出两个 outcome, +# 另外两个 outcome 本次不可用,见 unavailable_outcomes。 +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 + allowed_retain_locators: + - transmembrane_segment_count + allowed_retain_evidence_basis: protein_topology_annotation + rules: + - id: L1-01 + condition: evidence_locator = transmembrane_segment_count 且 evidence_direction = supporting + outcome: eligible_surface_target + disposition: RETAIN + resulting_state: eligible + evidence_basis: protein_topology_annotation + required_provenance: + - source_id + - source_path_or_url + - evidence_locator + - retrieved_at + - review_status + expected_count: 32 + - id: L1-02 + condition: evidence_locator = not_available + outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: 已批准层未提供拓扑注释;缺失是 DEFER,永不 EXCLUDE。 + expected_count: 9 + - id: L1-03 + 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-04 + 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: 32 + hold: 9 + killed: 0 + determinate_without_free_judgement: true + +# 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: - evidence_dimension_used: crc_prevalence # 疾病级证据可支撑 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 + vacuous_this_run: true + - basis_id: LB-precedent + dimension: adc_precedent + required_direction: supporting + required_locator: clinical_adc_names;clinical_stage_max + # indication_fit 在 41 行中全部为 CRC 范围,故该字段用于确认 CRC 归属而非区分。 + corroborating_field: indication_fit + corroborating_field_source: target_evidence_catalog.tsv + measured_supporting_units: 33 + vacuous_this_run: false rules: - id: LNK-01 - condition: crc_prevalence 单元存在且 direction=supporting,且该情境为 canonical_c0 + condition: 命中任一 accepted_linkage_bases,且该情境为 canonical_c0 outcome: linkage_evidence_exists disposition: RETAIN + resulting_state: active - id: LNK-02 - condition: crc_prevalence 单元存在且 direction=supporting,但该情境为 derived 或 benchmark 亚群 + condition: 命中任一 accepted_linkage_bases,但该情境为 derived 或 benchmark 亚群 outcome: linkage_unassessed disposition: DEFER + resulting_state: hold reason: 疾病级证据不能建立亚群特异 linkage。 - id: LNK-03 - condition: crc_prevalence 单元 direction=unknown 或不存在 + 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 @@ -198,3 +411,22 @@ output_validation: rule: 每个输入文件的 SHA-256 必须与本绑定记录一致,不一致即中止执行。 - id: VAL-B06 rule: 输出中不得出现任何被隔离运行独有的场景 ID 或靶点(GPA33、LY6G6D、TNFRSF12A、CEACAM6)。 + - id: VAL-B07 + rule: >- + 每个 LOCK-01 结果行必须记录命中的 lock_01_derivation 规则 ID 与 + required_provenance 全部字段;不得出现 not_surface_target 或 + identity_unresolved;41 个靶点必须恰好分为 32 eligible + 9 hold + 0 killed。 + - 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 index 96508c0..f549d5e 100644 --- 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 @@ -65,29 +65,79 @@ Level 01 首次执行的范围因此小于那次被隔离的运行。**这是正 依据是 PR #28 契约自身的禁令:「不得将 derived strategy 自动升级为 canonical clinical fact」。此处继承,并由测试机械保证未校准来源不可能得到 RETAIN。 -**直接后果:本次执行最多只有 1 个 context 可以 `eligible`。** Eligible Universe Index 因此上限为 1 × |eligible_targets| ≤ 41 pairs。这个数字小,但它是「只用已批准证据」的真实结果。 +**直接后果:本次执行只有 1 个 context 可以 `eligible`。** Eligible Universe Index 因此恰为 1 × 32 = 32 pairs(32 见第六节 LOCK-01 推导)。这个数字小,但它是「只用已批准证据」的真实结果。 ## 六、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`。LOCK-01 必须独立推导。测试断言这三个标签与 `CandidateDisposition` 的取值无交集,避免混读。 +**这些不是 `CandidateFilterResult`,不得当作 LOCK-01 输出。** 它们由 PR #28 契约的五条最小筛选层产生,判据与 LOCK-01 不同;且全部 41 行的 `gate_score_status = not_scored_in_enumeration_run`、`gate_pass_status = not_assessed`。测试断言这三个标签与 `CandidateDisposition` 的取值无交集,避免混读。 + +### LOCK-01 的确定性推导 + +只允许一个来源、一个 dimension、一个判决字段。不是为了简洁,而是因为已批准层里只有这一个可判别字段。 + +- 来源:`target_evidence_units.tsv`,`dimension = surface_reachability`,按 `gene_symbol` 连接 +- 判决字段:`evidence_locator`,实测取值只有两种——`transmembrane_segment_count`(32)与 `not_available`(9) +- **禁止参与推导的字段**:`disposition`、`gate_score_status`、`gate_pass_status`、`evidence_class`、`clinical_stage_max`、`internalization_status`、`normal_tissue_risk_status` + +| ID | 条件 | outcome | disposition | 数量 | +|---|---|---|---|---| +| `L1-01` | `evidence_locator = transmembrane_segment_count` 且 `direction = supporting` | `eligible_surface_target` | RETAIN | **32** | +| `L1-02` | `evidence_locator = not_available` | `possible_surface_target` | DEFER | **9** | +| `L1-03` | 同一 gene 同时存在 supporting 与 opposing | `possible_surface_target` | DEFER | 0(本次空规则) | +| `L1-04` | 证据来源为 RNA 层面 | `possible_surface_target` | DEFER | 0(本次空规则) | + +RETAIN 的证据基础只能是 `protein_topology_annotation`,白名单只有 `transmembrane_segment_count` 一个 locator,`rna_derived_locators_may_retain: false`。原始 statement 自述「supports a membrane-associated target hypothesis but does not prove tumor-cell surface exposure」——这正合 LOCK-01 的职责(身份与拓扑),证明肿瘤细胞表面可得是 Level 02 的 T7,不在本层。 + +**两个 outcome 本次不可用:** + +- `not_surface_target`:要求阳性的 negative protein/topology 证据(注释为纯胞内、零跨膜段且无信号肽或 GPI 锚)。已批准层只有上述两种 locator,**没有任何一条断言某靶点不是表面蛋白**。因此本次执行不得排除任何靶点。 +- `identity_unresolved`:要求一个身份解析结论字段,已批准层没有,无法区分「身份未解析」与「注释不可得」,故一律落 `possible_surface_target`。 + +完备性:32 + 9 = 41,全部靶点得到确定状态,**零自由裁量、零排除**。`VAL-B07`/`VAL-B08` 强制记录命中规则 ID 与 provenance,并禁止结果与旧 `disposition` 列存在函数依赖。 + +## 六之二、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**(本次为空) | +| `LB-precedent` | `adc_precedent` | `direction = supporting` 且 `locator = clinical_adc_names;clinical_stage_max` | **33** | + +`indication_fit` 在 41 行中全部为 CRC 范围(19 个 `CRC clinical benchmark in local ADC Index`、22 个 `CRC public literature/landscape candidate`),故该字段用于确认 CRC 归属,不用于区分。 | ID | 条件 | outcome | disposition | |---|---|---|---| -| `LNK-01` | `crc_prevalence` 单元存在且 `supporting`,且该 context 为 `canonical_c0` | `linkage_evidence_exists` | RETAIN | -| `LNK-02` | 同上但 context 为 derived 或 benchmark 亚群 | `linkage_unassessed` | DEFER | -| `LNK-03` | `crc_prevalence` 方向为 `unknown` 或不存在 | `linkage_unassessed` | DEFER | +| `LNK-01` | 命中任一依据,且该 context 为 `canonical_c0` | `linkage_evidence_exists` | RETAIN | +| `LNK-02` | 命中任一依据,但 context 为 derived 或 benchmark 亚群 | `linkage_unassessed` | DEFER | +| `LNK-03` | 未命中任何依据 | `linkage_unassessed` | DEFER | | `LNK-04` | 未按规定范围完成检索 | `linkage_evidence_missing` | DEFER | `LNK-02` 的理由是:疾病级证据不能建立亚群特异 linkage。这与第五节的 LOCK-02 上限相互独立,但结论一致——目前只有 canonical context 能走到 RETAIN。 +测试断言**至少存在一个非空依据**,且每个依据的 `vacuous_this_run` 必须与其实测计数一致——若将来所有依据都变空,测试会直接失败,而不是静默产出空池。 + **`no_known_linkage_after_complete_search` 本次不可用。** 该 outcome 要求 `search_complete = true` 与完整检索记录;既有证据包为 machine-extracted、专家复核只完成 2/20,检索范围未闭合。`VAL-B03` 禁止其出现在输出中。 ### `DECISION-02`(请裁决) @@ -122,11 +172,29 @@ Level 01 首次执行的范围因此小于那次被隔离的运行。**这是正 执行后每个产物文件仍须逐文件记录 SHA-256,并通过独立结果 PR 审核;`APPROVE` 前不得发布任何排序、推荐或资产决策。 -## 十、可以预见的结果形状 +## 十、可以预见的结果形状(逐项可核对) + +按第五至七节的规则算出,不是估计: + +| 量 | 值 | +|---|---| +| Raw Enumeration Matrix | **369** | +| context 资格 | eligible **1**/hold **8**/superseded **0** | +| target 资格 | eligible **32**/hold **9**/killed **0** | +| Eligible Universe Index | **32**(1 × 32) | +| Pool Level 01 | active **27**/hold **5**/reactivation-eligible **0** | + +`CNT-03` 对账:32 = 27 + 5 + 0。推导:1 个 canonical context × 32 个 LOCK-01 eligible target = 32 pair;其中 27 个命中 `LB-precedent`,走 `LNK-01` 得 active;其余 5 个未命中任何 linkage 依据,走 `LNK-03` 得 hold。 + +**执行结果必须逐项等于上表。任一项不符即视为执行偏离本契约。** + +### 必须写进结果报告的结构性限制 + +**27 个 active pair 的 linkage 全部只有「已有临床 ADC 针对该靶点」这一类,没有任何一条 CRC 表达证据**——因为已批准层里 41 条 `crc_prevalence` 全是 `not_available`。`adc_precedent` 的原始 statement 也自述「precedent does not establish CRC efficacy or a safe therapeutic window for a new asset」。 -按第五至七节的规则,本次执行的预期结果是:**1 个 `eligible` context,8 个 `hold`;active pool 上限 41 个 pair,全部集中在 canonical MSS/pMMR mCRC 3L+ 情境;其余 328 个 pair 落在 `hold`。** +因此 `active` 在本次执行中的含义仅是「存在一条可回溯的 CRC-scoped ADC precedent」,**不代表该靶点在 CRC 上有表达支持**。这一句必须原样出现在结果报告里,否则 active 会被误读。 -这个池子很小,而且真正的瓶颈不在漏斗设计,在**剩余 18 个专家复核批次**——与 PR #54 M6「portfolio 受数据集限制而非 Gate 限制」的判断一致,只是这次结论建立在已批准证据上。此处预先写明,是为了避免结果出来后被误读为 Level 01 失效。 +真正的瓶颈是 CRC 表达证据缺口与**剩余 18 个专家复核批次**,不是漏斗设计——与 PR #54 M6「portfolio 受数据集限制而非 Gate 限制」一致,只是这次建立在已批准证据上。 ## 十一、当前阻断 diff --git a/logs/worklog.md b/logs/worklog.md index e5f4d7c..d3709e8 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2463,3 +2463,19 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - 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 描述,请求复审。 diff --git a/tests/test_adc_pool_level_01_input_binding.py b/tests/test_adc_pool_level_01_input_binding.py index faff2a1..9d6f58d 100644 --- a/tests/test_adc_pool_level_01_input_binding.py +++ b/tests/test_adc_pool_level_01_input_binding.py @@ -144,18 +144,74 @@ def test_prior_disposition_labels_are_not_reusable_as_lock_01_output(self) -> No 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_outcomes = {o["outcome"] for o in lock_03["outcomes"]} + 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_outcomes) + 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_linkage_has_a_non_vacuous_basis(self) -> None: + """A binding whose only linkage basis is empty guarantees an empty pool.""" + + 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") + self.assertIsInstance(basis["measured_supporting_units"], int) + # A vacuous basis must be declared vacuous, and vice versa. + self.assertEqual( + basis["vacuous_this_run"], + basis["measured_supporting_units"] == 0, + ) + live = [b for b in bases if not b["vacuous_this_run"]] + self.assertTrue(live, "every linkage basis is vacuous: pool would be empty") + + 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["killed"], 0) + 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["bottleneck"].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) @@ -199,6 +255,259 @@ def test_authorisation_scope_is_explicit_and_bounded(self) -> None: 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) + self.assertEqual( + derivation["allowed_retain_evidence_basis"], "protein_topology_annotation" + ) + allowed = set(derivation["allowed_retain_locators"]) + 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"]): + self.assertEqual(rule["evidence_basis"], "protein_topology_annotation") + self.assertTrue( + any(locator in rule["condition"] for locator in allowed), + f"RETAIN rule must cite an allowed locator: {rule['condition']}", + ) + # 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_validation_rules_bar_quarantine_only_targets(self) -> None: rules = " ".join( r["rule"] for r in self.binding["output_validation"]["additional_rules"] From 6720edb0b8f8aa572bd139343d6c1d942b4fc0ec Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 21:12:32 -0400 Subject: [PATCH 3/4] task_20260804: accept PR #58 round-2 review, downgrade to raw-axis binding only Both scientific-semantic blockers accepted. Together they reduce every possible RETAIN to zero, so this PR no longer authorises executing Level 01. Blocker 1: transmembrane segment count does not establish an extracellularly accessible protein form. It shows transmembrane topology only, not plasma membrane localization, not an extracellular domain, not epitope accessibility, and it does not exclude ER, Golgi or mitochondrial membrane proteins. Calling 32 targets eligible over-promoted the evidence. The source statement itself says it does not prove tumor-cell surface exposure; I read that line and still promoted it. eligible_surface_target now requires plasma-membrane localization, an extracellular domain or topology, and protein-level provenance, all three, all protein-level. Transmembrane-only defers, no annotation defers, and organelle or conflicting localization defers. Measured: plasma membrane, extracellular, localization, signal peptide and GPI all have zero occurrences in the approved units, so eligible becomes 0. Blocker 2: a pan-cancer ADC precedent does not establish CRC linkage. It shows ADC modality precedent only, and indication_fit is a catalog-derived label that holds for all 41 rows, so it cannot substitute for source-level CRC evidence. LB-precedent now requires the source to name a CRC/colorectal indication, or CRC cell line, PDO, PDX or animal-model targeting evidence, with the indication and source locator recorded. Other-cancer precedent is retained as target/modality metadata and holds. Measured source-level CRC units: 0. Measurement trap recorded: all 33 supporting adc_precedent statements contain "CRC", but only inside the disclaimer "precedent does not establish CRC efficacy or a safe therapeutic window". Counting on that substring is what produced last round's false 33/33, and the binding now forbids that criterion. Consequence: target eligible 32 -> 0, Eligible Universe Index 32 -> 0, active 27 -> 0. Executing would emit an empty index and an empty snapshot, which has no candidate value and invites being misread as "screening complete". Per the reviewer's own fallback from the previous round, the binding is downgraded to raw_axis_binding_only with authorises_level_01_execution false, and two gaps are registered: EVGAP-01 needs a controlled target-surface localization extraction, EVGAP-02 needs a controlled CRC-specific linkage extraction. Each needs its own contract-only PR and APPROVE. The "at least one non-vacuous basis" test would have blocked submitting an honest binding, so the invariant is inverted: vacuity is judged on qualifying rather than supporting units, and when nothing qualifies the binding must not authorise execution. 281 tests pass. Ten mutations caught and rolled back exactly. Co-Authored-By: Claude Opus 5 --- ...4-adc-pool-level-01-input-binding.zh-CN.md | 70 ++++++- .../adc_pool_level_01_input_binding.yaml | 184 ++++++++++++++---- ...L_LEVEL_01_INPUT_BINDING_CONTRACT.zh-CN.md | 98 ++++++---- logs/worklog.md | 17 ++ tests/test_adc_pool_level_01_input_binding.py | 109 +++++++++-- 5 files changed, 380 insertions(+), 98 deletions(-) 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 index 4d4d289..333de44 100644 --- 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 @@ -5,6 +5,7 @@ - 基线:`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`「审核豁免」。** @@ -31,7 +32,7 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 |---|---| | `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` | 26 项校验,把绑定钉在已合并的 Level 01 契约与实际存在的批准记录上 | +| `tests/test_adc_pool_level_01_input_binding.py` | 30 项校验,把绑定钉在已合并的 Level 01 契约与实际存在的批准记录上 | ## 四、绑定的输入 @@ -49,7 +50,7 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 ## 五、三条实测得到的硬约束 **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。所以 Eligible Universe Index 恰为 1 × 32 = 32 pairs。 +实测 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` 取值无交集,防止混读。 @@ -67,7 +68,7 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 ## 七、可以预见的结果形状(预先写明,避免误读) -按上述规则算出(不是估计):Raw Enumeration Matrix **369**;context 资格 eligible **1**/hold **8**;target 资格 eligible **32**/hold **9**/killed **0**;Eligible Universe Index **32**;Pool Level 01 active **27**/hold **5**/reactivation-eligible **0**。`CNT-03` 对账 32 = 27 + 5 + 0。**执行结果必须逐项等于这些数字**,任一项不符即视为偏离契约。 +**本节已被第十二节取代。** 第二轮审核后的推算是: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 失效。 @@ -79,11 +80,11 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 - 没有定义 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」(实测 277)。仍属无关改动。 +- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 281)。仍属无关改动。 ## 九、验证结果 -- `Ran 277 tests` 全部通过(`main` 基线 251 + 本次新增 26)。 +- `Ran 281 tests` 全部通过(`main` 基线 251 + 本次新增 30)。 - `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 个。 @@ -142,3 +143,62 @@ ChatGPT 对 PR #58(HEAD `8f5c85d`)返回 `REQUEST_CHANGES`,两条阻断** ### 审核回写状态 审核方尝试通过 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`。 diff --git a/docs/pools/adc_pool_level_01_input_binding.yaml b/docs/pools/adc_pool_level_01_input_binding.yaml index 53a701e..14db9b2 100644 --- a/docs/pools/adc_pool_level_01_input_binding.yaml +++ b/docs/pools/adc_pool_level_01_input_binding.yaml @@ -15,6 +15,13 @@ binding: 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: @@ -108,31 +115,55 @@ scope_consequences: # 按本绑定的规则算出的预期结果。预先写明,使执行结果可被逐项核对, # 而不是执行完才知道形状。任一项与实际不符即视为执行偏离本契约。 predicted_result_shape: + # 本表是「若按本绑定执行会得到什么」的推算,用于说明为何不授权执行, + # 不是一次被授权运行的预期产物。 + is_authorised_to_execute: false raw_enumeration_matrix: 369 context_eligibility: eligible: 1 hold: 8 superseded: 0 target_eligibility: - eligible: 32 - hold: 9 + eligible: 0 + hold: 41 killed: 0 - eligible_universe_index: 32 + eligible_universe_index: 0 pool_level_01: - active: 27 - hold: 5 + active: 0 + hold: 0 reactivation_eligible: 0 - # CNT-03 对账:32 = 27 + 5 + 0 + # CNT-02 对账:1 x 0 = 0。CNT-03 对账:0 = 0 + 0 + 0。 derivation_note: >- - 1 个 canonical context x 32 个 LOCK-01 eligible target = 32 pair;其中 27 个 target - 命中 LB-precedent(adc_precedent supporting 且 locator 为 - clinical_adc_names;clinical_stage_max),走 LNK-01 得 active;其余 5 个未命中任何 - linkage 依据,走 LNK-03 得 hold。LB-expression 本次为空,不贡献任何 active。 - bottleneck: >- - active pool 完全依赖 ADC precedent,因为已批准层没有任何一条 CRC prevalence - 证据。这意味着 27 个 active pair 全部只有「已有临床 ADC 针对该靶点」这一类 - linkage,没有任何一条 CRC 表达证据。这是本次执行最重要的结构性限制, - 必须写进结果报告,不得让读者误以为 active 代表已有表达支持。 + 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 契约自身的禁令,此处继承。 @@ -168,10 +199,10 @@ lock_01_input_semantics: # LOCK-01 的确定性推导规则。 # -# 只允许一个来源文件和一个 dimension,且判据只有一个字段(evidence_locator)。 -# 这不是为了简洁,而是因为已批准层里只有这一个可判别字段——它只能区分 -# 「拓扑注释存在」与「不可得」。据此 LOCK-01 只能产出两个 outcome, -# 另外两个 outcome 本次不可用,见 unavailable_outcomes。 +# PR #57 冻结的 LOCK-01 问题是「是否存在有合理依据的**细胞外可及蛋白形式**」。 +# transmembrane_segment_count 只证明蛋白有跨膜拓扑,不能单独证明位于质膜、 +# 存在细胞外结构域、表位可被抗体接近,也不能排除内质网/高尔基/线粒体等 +# 细胞器膜蛋白。故跨膜段注释**单独不足以** RETAIN,只能 DEFER。 lock_01_derivation: source_id: crc_target_evidence_20260801 source_file: target_evidence_units.tsv @@ -187,33 +218,68 @@ lock_01_derivation: - clinical_stage_max - internalization_status - normal_tissue_risk_status - # RNA 证据不得满足 LOCK-01,无例外。此处不仅声明,还枚举允许的证据类型白名单。 + # RNA 证据不得满足 LOCK-01,无例外。 rna_derived_locators_may_retain: false - allowed_retain_locators: - - transmembrane_segment_count - allowed_retain_evidence_basis: protein_topology_annotation + # 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: evidence_locator = transmembrane_segment_count 且 evidence_direction = supporting + condition: 同时满足 RQ-01、RQ-02、RQ-03 outcome: eligible_surface_target disposition: RETAIN resulting_state: eligible - evidence_basis: protein_topology_annotation + evidence_basis: plasma_membrane_localization_and_extracellular_topology required_provenance: - source_id - source_path_or_url - evidence_locator - retrieved_at - review_status - expected_count: 32 + 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。 + reason: 已批准层未提供任何拓扑注释;缺失是 DEFER,永不 EXCLUDE。 expected_count: 9 - - id: L1-03 + - 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 @@ -221,7 +287,7 @@ lock_01_derivation: reason: 证据冲突是 DEFER,永不 EXCLUDE。 vacuous_this_run: true vacuous_reason: 实测 surface_reachability 方向只有 supporting(32) 与 unknown(9),无 opposing。 - - id: L1-04 + - id: L1-06 condition: 证据来源为 RNA 层面 outcome: possible_surface_target disposition: DEFER @@ -244,10 +310,13 @@ lock_01_derivation: # 完备性:两条规则覆盖全部 41 个靶点,无自由裁量空间。 coverage: total_targets: 41 - eligible: 32 - hold: 9 + 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 的确定性投影。 # @@ -341,16 +410,42 @@ lock_03_linkage_rules: # 实测: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 - # indication_fit 在 41 行中全部为 CRC 范围,故该字段用于确认 CRC 归属而非区分。 - corroborating_field: indication_fit - corroborating_field_source: target_evidence_catalog.tsv + # 「某靶点已有临床 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 - vacuous_this_run: false + 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 @@ -363,6 +458,19 @@ lock_03_linkage_rules: 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 @@ -387,8 +495,12 @@ lock_03_linkage_rules: # 本绑定授权什么、不授权什么。 authorises: - - 按本绑定执行 Level 01 一次,产出 docs/pools/adc_pool_gate_usage.yaml 规定的五份产物。 + - 把 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 评分 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 index f549d5e..f5791ac 100644 --- 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 @@ -4,6 +4,7 @@ - 前置工作包: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` 两个证据缺口阻断,见第十节。 ## 目的 @@ -11,6 +12,8 @@ 本文件**不执行 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 运行」。**这句话不准确,本契约予以更正。** @@ -65,7 +68,7 @@ Level 01 首次执行的范围因此小于那次被隔离的运行。**这是正 依据是 PR #28 契约自身的禁令:「不得将 derived strategy 自动升级为 canonical clinical fact」。此处继承,并由测试机械保证未校准来源不可能得到 RETAIN。 -**直接后果:本次执行只有 1 个 context 可以 `eligible`。** Eligible Universe Index 因此恰为 1 × 32 = 32 pairs(32 见第六节 LOCK-01 推导)。这个数字小,但它是「只用已批准证据」的真实结果。 +**直接后果:只有 1 个 context 可以 `eligible`。** 但 LOCK-01 给出 0 个 eligible target,故 Eligible Universe Index 为 1 × 0 = **0**,见第六节与第十节。 ## 六、LOCK-01:既有 disposition 列不可继承 @@ -75,27 +78,33 @@ Level 01 首次执行的范围因此小于那次被隔离的运行。**这是正 ### LOCK-01 的确定性推导 -只允许一个来源、一个 dimension、一个判决字段。不是为了简洁,而是因为已批准层里只有这一个可判别字段。 +**第二轮审核裁决:跨膜段证据不足以判定 `eligible_surface_target`,本契约已据此更正。** `transmembrane_segment_count` 只证明蛋白具有跨膜拓扑,不能单独证明位于质膜、存在细胞外结构域、表位可被抗体接近,也不能排除内质网/高尔基/线粒体等细胞器膜蛋白。而 PR #57 冻结的 LOCK-01 问题是「是否存在有合理依据的**细胞外可及蛋白形式**」。原映射把 32 个靶点判为 eligible,超过输入证据能支持的强度。 -- 来源:`target_evidence_units.tsv`,`dimension = surface_reachability`,按 `gene_symbol` 连接 -- 判决字段:`evidence_locator`,实测取值只有两种——`transmembrane_segment_count`(32)与 `not_available`(9) -- **禁止参与推导的字段**:`disposition`、`gate_score_status`、`gate_pass_status`、`evidence_class`、`clinical_stage_max`、`internalization_status`、`normal_tissue_risk_status` +`eligible_surface_target` 现在要求**同时**满足三项,缺一不可,且全部要求蛋白层面来源: + +| 要求 | 内容 | +|---|---| +| `RQ-01` | plasma-membrane localization | +| `RQ-02` | extracellular domain / topology | +| `RQ-03` | protein-level provenance | | ID | 条件 | outcome | disposition | 数量 | |---|---|---|---|---| -| `L1-01` | `evidence_locator = transmembrane_segment_count` 且 `direction = supporting` | `eligible_surface_target` | RETAIN | **32** | -| `L1-02` | `evidence_locator = not_available` | `possible_surface_target` | DEFER | **9** | -| `L1-03` | 同一 gene 同时存在 supporting 与 opposing | `possible_surface_target` | DEFER | 0(本次空规则) | -| `L1-04` | 证据来源为 RNA 层面 | `possible_surface_target` | DEFER | 0(本次空规则) | - -RETAIN 的证据基础只能是 `protein_topology_annotation`,白名单只有 `transmembrane_segment_count` 一个 locator,`rna_derived_locators_may_retain: false`。原始 statement 自述「supports a membrane-associated target hypothesis but does not prove tumor-cell surface exposure」——这正合 LOCK-01 的职责(身份与拓扑),证明肿瘤细胞表面可得是 Level 02 的 T7,不在本层。 - -**两个 outcome 本次不可用:** +| `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。 -- `not_surface_target`:要求阳性的 negative protein/topology 证据(注释为纯胞内、零跨膜段且无信号肽或 GPI 锚)。已批准层只有上述两种 locator,**没有任何一条断言某靶点不是表面蛋白**。因此本次执行不得排除任何靶点。 -- `identity_unresolved`:要求一个身份解析结论字段,已批准层没有,无法区分「身份未解析」与「注释不可得」,故一律落 `possible_surface_target`。 +**两个 outcome 仍然不可用**:`not_surface_target` 要求阳性的 negative protein/topology 证据,已批准层没有任何一条断言某靶点不是表面蛋白;`identity_unresolved` 要求身份解析结论字段,已批准层没有。 -完备性:32 + 9 = 41,全部靶点得到确定状态,**零自由裁量、零排除**。`VAL-B07`/`VAL-B08` 强制记录命中规则 ID 与 provenance,并禁止结果与旧 `disposition` 列存在函数依赖。 +**完备性:41 = 32 (`L1-02`) + 9 (`L1-03`),eligible = 0,killed = 0。** 零自由裁量、零排除,但也**零录入**。 ## 六之二、36 行 → 9 个 clinical context 的确定性投影 @@ -120,23 +129,29 @@ PR #57 规定 Level 01 的单元是 `clinical context × target` 且 endpoint 来源文档 Lock 3 列出的合格 linkage 形式本就包含「**已有 CRC preclinical 或 clinical targeting evidence**」,因此只绑表达类证据本身就是漏读来源文档。冻结两类依据: -| 依据 | dimension | 判据 | 实测 supporting | -|---|---|---|---| -| `LB-expression` | `crc_prevalence` | `direction = supporting` | **0**(本次为空) | -| `LB-precedent` | `adc_precedent` | `direction = supporting` 且 `locator = clinical_adc_names;clinical_stage_max` | **33** | +| 依据 | 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**。 -`indication_fit` 在 41 行中全部为 CRC 范围(19 个 `CRC clinical benchmark in local ADC Index`、22 个 `CRC public literature/landscape candidate`),故该字段用于确认 CRC 归属,不用于区分。 +> **一个必须记录的实测陷阱**:这 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-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 | -`LNK-02` 的理由是:疾病级证据不能建立亚群特异 linkage。这与第五节的 LOCK-02 上限相互独立,但结论一致——目前只有 canonical context 能走到 RETAIN。 - -测试断言**至少存在一个非空依据**,且每个依据的 `vacuous_this_run` 必须与其实测计数一致——若将来所有依据都变空,测试会直接失败,而不是静默产出空池。 +**两类依据本次都为空。** 测试据此断言:每个依据的 `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` 禁止其出现在输出中。 @@ -153,9 +168,9 @@ PR #57 规定 Level 01 的单元是 `clinical context × target` 且 endpoint ## 八、本契约授权与不授权 -**授权:** 按本绑定执行 Level 01 一次,产出 PR #57 契约规定的五份产物。 +**授权:** 把 raw clinical context 轴与 raw target 轴绑定到 PR #29/#31 已批准的产物并固定 SHA-256;冻结 LOCK-01 推导、LOCK-02 状态上限、LOCK-03 linkage 依据与 clinical context 投影;记录 `EVGAP-01`/`EVGAP-02` 及其所需的后续受控抽取。 -**不授权:** 任何新的 context 或 target 枚举;任何靶点筛选排序、Tier 划分、资产推荐或实验建议;任何 Gate 执行或评分;endpoint 锁定或定量门槛;Level 02 与 Level 03;把被隔离运行的任何产物重新引入。 +**不授权:** **执行 Level 01**(被 `EVGAP-01`/`EVGAP-02` 阻断);任何证据抽取或检索运行;任何新的 context 或 target 枚举;任何靶点筛选排序、Tier 划分、资产推荐或实验建议;任何 Gate 执行或评分;endpoint 锁定或定量门槛;Level 02 与 Level 03;把被隔离运行的任何产物重新引入。 ## 九、输出验证 @@ -172,32 +187,33 @@ PR #57 规定 Level 01 的单元是 `clinical context × target` 且 endpoint 执行后每个产物文件仍须逐文件记录 SHA-256,并通过独立结果 PR 审核;`APPROVE` 前不得发布任何排序、推荐或资产决策。 -## 十、可以预见的结果形状(逐项可核对) - -按第五至七节的规则算出,不是估计: +## 十、按本绑定推算的结果,以及为什么不授权执行 | 量 | 值 | |---|---| | Raw Enumeration Matrix | **369** | | context 资格 | eligible **1**/hold **8**/superseded **0** | -| target 资格 | eligible **32**/hold **9**/killed **0** | -| Eligible Universe Index | **32**(1 × 32) | -| Pool Level 01 | active **27**/hold **5**/reactivation-eligible **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-03` 对账:32 = 27 + 5 + 0。推导:1 个 canonical context × 32 个 LOCK-01 eligible target = 32 pair;其中 27 个命中 `LB-precedent`,走 `LNK-01` 得 active;其余 5 个未命中任何 linkage 依据,走 `LNK-03` 得 hold。 +`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 轴**。 -**27 个 active pair 的 linkage 全部只有「已有临床 ADC 针对该靶点」这一类,没有任何一条 CRC 表达证据**——因为已批准层里 41 条 `crc_prevalence` 全是 `not_available`。`adc_precedent` 的原始 statement 也自述「precedent does not establish CRC efficacy or a safe therapeutic window for a new asset」。 +### 阻断执行的两个证据缺口 -因此 `active` 在本次执行中的含义仅是「存在一条可回溯的 CRC-scoped ADC precedent」,**不代表该靶点在 CRC 上有表达支持**。这一句必须原样出现在结果报告里,否则 active 会被误读。 +| 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 | -真正的瓶颈是 CRC 表达证据缺口与**剩余 18 个专家复核批次**,不是漏斗设计——与 PR #54 M6「portfolio 受数据集限制而非 Gate 限制」一致,只是这次建立在已批准证据上。 +两条缺口各自需要一次受控证据抽取运行,**各自走 contract-only PR 与 `APPROVE`**,都不在本 PR 的授权范围内。 ## 十一、当前阻断 -- 本契约获 ChatGPT `APPROVE` 前,不得执行 Level 01。 +- **本契约获 `APPROVE` 后仍不得执行 Level 01**,直到 `EVGAP-01` 与 `EVGAP-02` 各自通过受控抽取补齐并被接受。 - 本仓库不得写入候选池、快照、证据、cache、result 或 weights。 - `DECISION-02` 未获裁决前,执行者不得自行改用严格方案。 diff --git a/logs/worklog.md b/logs/worklog.md index d3709e8..6adc038 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2479,3 +2479,20 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - 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 描述,请求复审。后续两个证据抽取契约需人类负责人决定先做哪个。 diff --git a/tests/test_adc_pool_level_01_input_binding.py b/tests/test_adc_pool_level_01_input_binding.py index 9d6f58d..a6773ff 100644 --- a/tests/test_adc_pool_level_01_input_binding.py +++ b/tests/test_adc_pool_level_01_input_binding.py @@ -159,8 +159,8 @@ def test_linkage_rules_never_exclude(self) -> None: rules["evidence_granularity"], "disease_level_not_subgroup_level" ) - def test_linkage_has_a_non_vacuous_basis(self) -> None: - """A binding whose only linkage basis is empty guarantees an empty pool.""" + 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) @@ -169,14 +169,89 @@ def test_linkage_has_a_non_vacuous_basis(self) -> None: for basis in bases: with self.subTest(basis=basis["basis_id"]): self.assertEqual(basis["required_direction"], "supporting") - self.assertIsInstance(basis["measured_supporting_units"], int) - # A vacuous basis must be declared vacuous, and vice versa. + # 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_supporting_units"] == 0, + basis["vacuous_this_run"], basis["measured_qualifying_units"] == 0 ) live = [b for b in bases if not b["vacuous_this_run"]] - self.assertTrue(live, "every linkage basis is vacuous: pool would be empty") + 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"] @@ -204,13 +279,18 @@ def test_predicted_shape_reconciles_with_the_counting_identities(self) -> None: ) # 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["bottleneck"].strip()) + 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"] @@ -280,10 +360,6 @@ def test_lock_01_derivation_maps_every_outcome_uniquely(self) -> None: 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) - self.assertEqual( - derivation["allowed_retain_evidence_basis"], "protein_topology_annotation" - ) - allowed = set(derivation["allowed_retain_locators"]) retaining = [ r for r in derivation["rules"] if r["disposition"] == CandidateDisposition.RETAIN.value @@ -291,10 +367,11 @@ def test_lock_01_rna_evidence_can_never_retain(self) -> None: self.assertTrue(retaining) for rule in retaining: with self.subTest(rule=rule["id"]): - self.assertEqual(rule["evidence_basis"], "protein_topology_annotation") - self.assertTrue( - any(locator in rule["condition"] for locator in allowed), - f"RETAIN rule must cite an allowed locator: {rule['condition']}", + # 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"]: From fc87a620d8fabce2a0e4d6a79f1e38034dd2abea Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 21:20:48 -0400 Subject: [PATCH 4/4] task_20260804: fix VAL-B07 stale counts and lock the three-way agreement VAL-B07 still required 32 eligible + 9 hold + 0 killed while the body of the contract, lock_01_derivation.coverage and predicted_result_shape had all moved to 0 eligible + 41 hold + 0 killed. One machine-readable contract stated two mutually exclusive results, so validation had no authoritative value. This was a miss in the round-2 revision. VAL-B07's prose now reads 0 eligible + 41 hold + 0 killed and notes that the 41 hold split as 32 from L1-02, transmembrane topology only, plus 9 from L1-03, no annotation at all. A structured expected_target_eligibility field makes the counts machine-comparable instead of requiring prose parsing, and the rule now declares validates: evidence_insufficient_binding_state with authorises_result_generation false, so it cannot be read as licensing a Level 01 result. Two tests lock this down. One asserts VAL-B07, the derivation coverage and the predicted shape agree key by key, and that the prose does not contradict the structured counts. The other asserts that while execution is unauthorised no validation rule may require eligible targets and every non-vacuous LOCK-01 rule must defer. Swept the repository per the acceptance criterion: no execution or validation requirement anywhere still treats the 32 transmembrane-only targets as eligible. The remaining mentions of 32 are descriptive - the hold breakdown, the contract describing the old mapping as an error, and the round-1 handoff record, which now carries a supersession marker pointing at the round-2 section. 283 tests pass. Five mutations caught and rolled back exactly. Co-Authored-By: Claude Opus 5 --- ...4-adc-pool-level-01-input-binding.zh-CN.md | 37 +++++++++++++-- .../adc_pool_level_01_input_binding.yaml | 12 ++++- logs/worklog.md | 12 +++++ tests/test_adc_pool_level_01_input_binding.py | 47 +++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) 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 index 333de44..33bf6c4 100644 --- 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 @@ -32,7 +32,7 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 |---|---| | `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` | 30 项校验,把绑定钉在已合并的 Level 01 契约与实际存在的批准记录上 | +| `tests/test_adc_pool_level_01_input_binding.py` | 32 项校验,把绑定钉在已合并的 Level 01 契约与实际存在的批准记录上 | ## 四、绑定的输入 @@ -80,11 +80,11 @@ PR #57 的 `BLOCK-02` 写的是「唯一的 context 枚举来自被隔离的 202 - 没有定义 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」(实测 281)。仍属无关改动。 +- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 283)。仍属无关改动。 ## 九、验证结果 -- `Ran 281 tests` 全部通过(`main` 基线 251 + 本次新增 30)。 +- `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 个。 @@ -110,6 +110,8 @@ ChatGPT 对 PR #58(HEAD `8f5c85d`)返回 `REQUEST_CHANGES`,两条阻断** **两个 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 与字段内容不同。 @@ -130,6 +132,8 @@ ChatGPT 对 PR #58(HEAD `8f5c85d`)返回 `REQUEST_CHANGES`,两条阻断** 原写「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」。 ### 本轮变异检验 @@ -202,3 +206,30 @@ context identity 只由 `indication_id` 决定;endpoint 不进入 identity 也 ### 审核回写状态 审核方尝试通过 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 index 14db9b2..6cdae76 100644 --- a/docs/pools/adc_pool_level_01_input_binding.yaml +++ b/docs/pools/adc_pool_level_01_input_binding.yaml @@ -524,10 +524,20 @@ output_validation: - 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 个靶点必须恰好分为 32 eligible + 9 hold + 0 killed。 + 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 中的任何字段;结果不得与 diff --git a/logs/worklog.md b/logs/worklog.md index 6adc038..690b289 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2496,3 +2496,15 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - 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 index a6773ff..93833ef 100644 --- a/tests/test_adc_pool_level_01_input_binding.py +++ b/tests/test_adc_pool_level_01_input_binding.py @@ -585,6 +585,53 @@ def test_context_identity_excludes_endpoint_and_endpoint_stays_unlocked(self) -> {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"]