From 570562ce8db3fb9becce5d9bb745ad850b8069c8 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 22:16:27 -0400 Subject: [PATCH 1/3] task_20260804: contract for the EVGAP-01 surface localization extraction Freezes one evidence-extraction run that would discharge EVGAP-01, the gap registered in PR #58 that blocks LOCK-01. Nothing is executed and Level 01 remains unauthorised, because EVGAP-02 is still open. Governance finding stated up front: the data LOCK-01 needs already exists locally in ADC_surfaceome_reference v0.3.0, but that database has never been reviewed or approved. No review record in the repository mentions surfaceome, and the approved evidence extraction in PR #31 declared ADC_internalization_ reference as its source without wiring this one in. That is why the approved layer carries only transmembrane annotation: not because the data was missing, but because it was never connected. This PR therefore asks to admit exactly one pinned version, with the snapshot id, the raw manifest digest and four file checksums recorded, aborting the run on any mismatch. The case for admitting it is that its own build manifest already hard-codes the guards this repository keeps enforcing, two of which are literally the blockers from earlier rounds: membrane_topology_is_independent_surface_localization is false, and absence_is_negative_evidence is false. It also excludes an RNA FPKM source for not being a protein measurement, caps T7 confidence, and states that it establishes no malignant-cell positive fraction or ADC accessibility. The guards are built in, not bolted on by this contract. RQ-01 keys on independent_evidence_family_count with a floor of two, over curated knowledge, imaging and cell-surface capture MS. Topology and generic membrane are already excluded from that count, so it cannot silently readmit transmembrane annotation as localization. RQ-02 needs two paths. UniProt derives its extracellular-domain field from the TOPO_DOM records of transmembrane proteins, so GPI-anchored proteins with zero TM segments read false regardless of biology. With only the topology path, CEACAM5, MSLN, FOLR1 and MELTF would hold on a representation artefact while being confirmed_surface with both a signal peptide and a GPI anchor. The second path corrects that artefact rather than relaxing the bar. LAMP1 is the control: transmembrane with a signal peptide but a lumenal domain and no extracellular TOPO_DOM, so it holds under both paths, which is the organelle-membrane case the reviewer required. Five rules cover all 41 targets with no discretion and no exclusion: 22 eligible, 19 hold, 0 killed. not_surface_target and identity_unresolved stay unavailable. MF-01 is recorded as a mandatory finding and tested rather than merely asserted: GUCY2C holds, on one independent family and supported_surface rather than confirmed_surface, which contradicts the earlier multi-model consensus and the quarantined run's Tier A. 298 tests pass. Ten mutations caught and rolled back exactly. Co-Authored-By: Claude Opus 5 --- ...-01-surface-localization-contract.zh-CN.md | 104 ++++++ ...ap_01_surface_localization_extraction.yaml | 351 ++++++++++++++++++ ..._LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md | 139 +++++++ logs/worklog.md | 18 + tests/test_evgap_01_surface_localization.py | 266 +++++++++++++ 5 files changed, 878 insertions(+) create mode 100644 docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md create mode 100644 docs/pools/evgap_01_surface_localization_extraction.yaml create mode 100644 docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md create mode 100644 tests/test_evgap_01_surface_localization.py diff --git a/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md b/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md new file mode 100644 index 0000000..2ab0792 --- /dev/null +++ b/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md @@ -0,0 +1,104 @@ +# Handoff:EVGAP-01 target surface localization 证据抽取契约 + +- 日期:`2026-08-04` +- 任务分支:`task_20260804_evgap-01-surface-localization-contract` +- 基线:`main` @ `cd0e041` +- 前置:PR #57、PR #58,均已 `APPROVE` 并合并 +- 交付物类型:**contract-only** +- 外部运行:**无。没有执行抽取,没有产生任何证据、判定或候选。** +- 授权范围:**一次抽取运行;不授权执行 Level 01** +- 架构变更:`NO_ARCHITECTURE_CHANGE`(依据是 diff 范围,可由 `git diff --stat` 核验) +- 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** + +## 一、本次范围 + +人类负责人指示继续起 `EVGAP-01` 契约。`EVGAP-01` 是 PR #58 登记的两个缺口之一,阻断 `LOCK-01`:已批准证据层没有蛋白层面的质膜定位与细胞外结构域证据,导致 41 个靶点全部 DEFER、`eligible = 0`、Level 01 无法录入任何 pair。 + +本次交付该缺口的抽取契约。**不执行抽取。** + +## 二、关键发现:所需数据已在本地,但那个数据库从未被批准 + +`DATA/1.Databases/ADC_surfaceome_reference/processed/v0.3.0` 恰好覆盖三项 RQ 要求。但治理状态必须先讲清楚: + +> **该数据库从未被审核批准。** 仓库内 `logs/chatgpt-review-*.md` 无任何一条提及 surfaceome;worklog 唯一提及是 2026-08-01 的 mock 运行。已获批的证据抽取(PR #31)在 `source_manifest.json` 中声明的来源是 `ADC_internalization_reference`,**没有接入本库**。 + +这解释了一件之前没解释的事:**为什么已批准层只有跨膜段注释。** 不是数据不存在,是当时没接。 + +因此本 PR 请求把这一个版本纳入已批准来源:`0.3.0` / snapshot `2026-07-29-quant-topology-mm` / `raw_manifest_sha256 884f4191…`,四个文件逐一记录 SHA-256。 + +### 主张纳入它的核心理由 + +该库 `build_manifest.json` 的 `consensus_semantics` 已经写死了本仓库反复要求的守卫,其中两条正是前几轮审核的阻断本身: + +- `membrane_topology_is_independent_surface_localization: false` ← **PR #58 第二轮阻断** +- `absence_is_negative_evidence: false` ← 缺失一律 DEFER +- `generic_membrane_is_surface_confirmation: false`、`cci_receptor_role_is_surface_confirmation: false`、`tumor_ihc_is_surface_density: false` +- 排除 `GSE160572_MM_surfaceome.csv.gz`,理由是它是 RNA FPKM 而非蛋白测量 ← RNA 规则 + +它还自设 `full_t7_gate_confidence_cap: 0.55`,并声明自己不建立 malignant-cell positive fraction、isoform usage、calibrated treatment stability 或 ADC accessibility——即它自己就划开了 Level 02 边界。 + +守卫是构建时写死的,不是本契约事后附加的。这是主张纳入它、而不是另起一次网络抽取的核心理由。 + +## 三、仓库内交付了什么 + +| 文件 | 作用 | +|---|---| +| `docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md` | 抽取契约(面向操作者,中文) | +| `docs/pools/evgap_01_surface_localization_extraction.yaml` | 机器可读绑定:来源纳入请求+SHA-256、字段白名单、禁读清单、RQ 映射、五条判据规则、输出 schema 与十条验证 | +| `tests/test_evgap_01_surface_localization.py` | 15 项校验 | + +## 四、确定性映射与实测结果 + +`RQ-01` 用 `independent_evidence_family_count ≥ 2`。该库的三个独立家族是 `curated_knowledge`/`imaging`/`cell_surface_capture_ms`,**拓扑与泛膜已被排除在家族计数之外**,所以家族计数在结构上不可能把跨膜段当定位证据。 + +`RQ-02` 有两条路径。**`ECD-b` 是必需的,理由是修正数据表示假象。** UniProt 的 extracellular domain 字段由跨膜蛋白的 TOPO_DOM 推导;GPI 锚定蛋白零跨膜段、无 TOPO_DOM,该字段一律 `false`。只用 `ECD-a` 会让 `CEACAM5`、`MSLN`、`FOLR1`、`MELTF` 因假象落 hold——四者在库中都是 `confirmed_surface`、都带信号肽与 GPI 锚。GPI 锚 + 信号肽 + 零跨膜段在结构上即意味着全长成熟蛋白位于胞外。 + +`LAMP1` 是对照:有跨膜段与信号肽,但结构域朝向溶酶体腔内、无胞外 TOPO_DOM,两条路径都不满足,落 hold。规则自然落在正确一侧,无需特判。 + +| ID | 条件 | outcome | 数量 | +|---|---|---|---| +| `E1-01` | 三项 RQ 全满足且无冲突 | `eligible_surface_target` RETAIN | **22** | +| `E1-02` | 独立家族数 < 2 | `possible_surface_target` DEFER | **6** | +| `E1-03` | 两条 ECD 路径都不满足 | `possible_surface_target` DEFER | **3** | +| `E1-04` | `discordance_flags` 非空 | `possible_surface_target` DEFER | **6** | +| `E1-05` | 不在参考库中 | `possible_surface_target` DEFER | **4** | + +22 + 6 + 3 + 6 + 4 = 41,零自由裁量、零排除。`not_surface_target` 与 `identity_unresolved` 仍不可用。 + +## 五、三条必须写进结果报告的发现 + +- **`MF-01`:`GUCY2C` 落 hold。** 只有 `curated_knowledge` 一个独立家族,`consensus_class` 是 `supported_surface` 而非 `confirmed_surface`。**这与此前多模型共识把 GUCY2C 列为首选、以及被隔离运行的 Tier A 选择相反。** 必须原样写出,不得因与既有偏好冲突而弱化。测试会检查 `MF-01` 声称 hold 的靶点确实出现在某条 DEFER 规则的 `measured_targets` 里,防止这条发现被改成空话。 +- **`MF-02`:`eligible` 只是身份与拓扑层面的结论**,不表示在 CRC 肿瘤细胞表面可得——那是 Level 02 的 T7。 +- **`MF-03`:零排除。** hold 不是淘汰,是待证据。 + +## 六、明确没有做什么 + +- **没有执行抽取**,没有产生任何证据行、判定或候选。 +- 没有执行 Level 01,也不授权执行——`EVGAP-02` 仍未解除。 +- 没有读取被禁的四个文件(`tumor_surface_measurement`、`tumor_protein_context`、`treatment_surface_response`、`receptor_evidence`)作为判据;本 handoff 中引用的覆盖率与计数只来自白名单字段。 +- 没有解除 `EVGAP-01` 本身——解除要等抽取执行、结果 PR 获批,再另开 PR 更新输入绑定。 +- 没有引用被隔离运行(PR #53、#54)的任何产物。 +- 没有新增靶点或 clinical context。 +- **没有补 #52/#53/#54/#57/#58 的批准记录**(现在是五份)。事实已查全,未写文件。 +- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 298)。属无关改动。 + +## 七、验证结果 + +- `Ran 298 tests` 全部通过(`main` 基线 283 + 本次新增 15)。 +- `scripts/verify_repository_boundary.sh`:`Repository boundary check passed.` +- `git diff --check`:通过;零 `__pycache__`。 +- 所有计数由脚本读取外部数据库实测,非估计:41 靶点中 37 覆盖 / 4 未覆盖;`consensus_class` 为 29 `confirmed_surface` / 7 `supported_surface` / 1 `no_surface_support`;独立家族数分布 3 家族 10 个、2 家族 19 个、1 家族 7 个、0 家族 1 个;`discordance_flags` 非空 6 个;最终 22 eligible(`ECD-a` 18 + `ECD-b` 4)/ 19 hold / 0 killed。 +- 四个数据库文件的 SHA-256 由 `shasum -a 256` 实算。 + +## 八、后续顺序 + +1. 本契约 `APPROVE`。 +2. 执行抽取 → 结果 PR → `APPROVE`。 +3. 另开 PR 更新 `adc_pool_level_01_input_binding.yaml`,绑定抽取产物并解除 `EVGAP-01`。 +4. `EVGAP-02` 需其独立契约。**两个缺口都解除后,Level 01 才能执行。** + +## 九、当前阻断 + +- 本契约获 `APPROVE` 前,不得执行抽取。 +- 抽取完成也**不**解除 `EVGAP-02`,Level 01 仍不可执行。 +- 本仓库不得写入证据、候选、快照、cache、result 或 weights。 diff --git a/docs/pools/evgap_01_surface_localization_extraction.yaml b/docs/pools/evgap_01_surface_localization_extraction.yaml new file mode 100644 index 0000000..1ea5f7b --- /dev/null +++ b/docs/pools/evgap_01_surface_localization_extraction.yaml @@ -0,0 +1,351 @@ +# EVGAP-01:target surface localization 证据抽取绑定(machine-readable) +# +# 目的:解除 PR #58 登记的 EVGAP-01,使 Level 01 的 LOCK-01 可以真正产出 +# eligible_surface_target。本文件不含任何证据数据,只含来源身份、字段白名单、 +# 到 RQ 要求的映射、输出 schema 与验证规则。 +# +# 由 tests/test_evgap_01_surface_localization.py 校验。 +# LOCK-01 的 RQ-01/RQ-02/RQ-03 定义见 docs/pools/adc_pool_level_01_input_binding.yaml。 + +extraction: + extraction_id: evgap_01_surface_localization + extraction_version: 0.1.0 + discharges_gap: EVGAP-01 + gap_source: docs/pools/adc_pool_level_01_input_binding.yaml + serves_lock: LOCK-01 + status: contract_only_pending_review + execution_status: not_authorized_not_executed + # 本契约只授权一次抽取运行;不授权执行 Level 01。 + authorises_extraction_run: true + authorises_level_01_execution: false + # 抽取完成并经结果 PR 接受后,需再开一个 PR 更新 Level 01 输入绑定以解除 EVGAP-01。 + requires_followup_binding_pr: true + +# 本契约请求把这一个数据库版本纳入已批准来源集合。 +# 治理事实:该数据库此前从未被审核批准,仓库内无任何 APPROVE 记录提及它; +# 已获批的证据抽取(PR #31)的 source_manifest 用的是 ADC_internalization_reference, +# 没有接入本库——这正是已批准层只有跨膜段注释、没有质膜定位证据的原因。 +source_admission_request: + source_id: adc_surfaceome_reference_v0_3_0 + dataset_id: ADC_surfaceome_reference + dataset_version: 0.3.0 + snapshot_id: 2026-07-29-quant-topology-mm + root: external:databases/ADC_surfaceome_reference/processed/v0.3.0 + builder: AssetGenOS/scripts/build_t7_surfaceome_reference.py + builder_version: 0.3.0 + raw_manifest_sha256: 884f419118302ae39c3e50292d03295ff676434868e1061b39ead50f9cc977bb + previously_approved: false + previously_approved_evidence: 仓库内 logs/chatgpt-review-*.md 无任何一条提及 surfaceome;worklog 仅有一次 2026-08-01 mock 运行提及。 + files: + - path: surfaceome_consensus.tsv + sha256: c7baa2ac86e1d173ef555e92036c2841efb5d106c4931bcf71507fbd5cbc1e7b + rows: 20797 + role: 每基因共识与字段来源 + - path: membrane_topology_evidence.tsv + sha256: d2680b961793f8ed83decbc92cdf04d5818f4cdbcb1e241c0ef34c4f06e77eca + rows: 4863 + role: 拓扑逐条证据与 provenance + - path: source_evidence.tsv + sha256: a61222f03f2763af7733793d0bc85e19185515683a8fa1956d1559667cd662ab + rows: 41204 + role: 逐来源证据行,供 RQ-03 provenance + - path: build_manifest.json + sha256: 9872dacd2f30532364576f29ce8c7428a8df9c6bc5abea408472bb3f79069fe2 + role: 构建语义与版本声明 + + # 该库自身的 consensus_semantics 已经编码了本仓库反复要求的守卫。 + # 这是主张纳入它的核心理由:语义不是本契约事后附加的,是它构建时就写死的。 + inherited_semantics_guards: + - guard: absence_is_negative_evidence + value: false + matches_repo_rule: 缺失证据一律 DEFER,永不 EXCLUDE + - guard: membrane_topology_is_independent_surface_localization + value: false + matches_repo_rule: PR #58 第二轮阻断——跨膜段不足以判定细胞外可及 + - guard: generic_membrane_is_surface_confirmation + value: false + matches_repo_rule: 泛膜注释不等于质膜定位 + - guard: cci_receptor_role_is_surface_confirmation + value: false + matches_repo_rule: 受体角色推断不等于定位证据 + - guard: tumor_ihc_is_surface_density + value: false + matches_repo_rule: 表达强度不等于表面密度,属 Level 02 T7 + - guard: excluded_rna_source + value: GSE160572_MM_surfaceome.csv.gz 因其为 RNA FPKM 而被排除 + matches_repo_rule: RNA 不得当作蛋白层面验证,无例外 + +# 抽取范围:只处理已批准枚举里的 41 个靶点,不新增任何靶点。 +scope: + target_axis_source: external:result/gen_iet_crc_target_enumeration_20260802 + target_axis_file: target_evidence_catalog.tsv + target_axis_sha256: 27bb81ebe5f1048b732f62c530095e0007212f818f3844e004118bb94525cc28 + target_count: 41 + join_key: gene_symbol + new_targets_allowed: false + measured_coverage_in_reference: 37 + measured_absent_from_reference: 4 + measured_absent_targets: + - AG7 + - CA19-9 + - EDBN + - Undisclosed + +# 只允许读取这些字段。白名单之外一律不得进入推导,防止把 Level 02 的量化 +# 与肿瘤上下文数据洗进 LOCK-01。 +allowed_fields: + from_surfaceome_consensus: + - gene_symbol + - uniprot_accessions + - uniprot_signal_peptide + - uniprot_gpi_anchor + - uniprot_transmembrane_segment_count + - uniprot_extracellular_domain + - uniprot_extracellular_domain_count + - uniprot_extracellular_max_length_aa + - uniprot_ecd_meets_min_length + - independent_evidence_family_count + - independent_evidence_families + - consensus_class + - discordance_flags + - dataset_version + - snapshot_id + - row_checksum + from_source_evidence: + - gene_symbol + - source_id + - source_release + - evidence_family + - evidence_kind + - evidence_codes + - source_url + - license + +# 明确禁止读取的文件与字段:它们属 Level 02 / T7,读入即重演 PR #58 的越界。 +barred_files: + - path: tumor_surface_measurement.tsv + reason: 肿瘤表面定量属 Level 02 T7,不属 LOCK-01。 + - path: tumor_protein_context.tsv + reason: 肿瘤上下文蛋白组属 Level 02 T7。 + - path: treatment_surface_response.tsv + reason: 治疗后表面变化属 T5/T7,Level 01 不评估。 + - path: receptor_evidence.tsv + reason: 受体角色推断不构成定位证据(该库自身亦声明 cci_receptor_role_is_surface_confirmation=false)。 +barred_fields: + - field: cci_receptor_role + reason: 受体角色不是定位证据。 + - field: uniprot_generic_membrane + reason: 泛膜注释不等于质膜定位。 + - field: full_t7_gate_confidence_cap + reason: T7 置信上限属 Level 02,Level 01 不得引用。 + +# RQ-01:质膜定位。要求至少两个独立证据家族。 +# 该库定义的三个独立家族为 curated_knowledge / imaging / cell_surface_capture_ms, +# 且已把拓扑与泛膜排除在家族计数之外,故家族计数天然不会把拓扑当定位。 +rq_01_plasma_membrane_localization: + decisive_field: independent_evidence_family_count + minimum_independent_families: 2 + independent_families: + - curated_knowledge + - imaging + - cell_surface_capture_ms + topology_counts_as_family: false + generic_membrane_counts_as_family: false + rna_may_satisfy: false + +# RQ-02:细胞外结构域。两条合格路径,任一成立即满足。 +rq_02_extracellular_domain: + paths: + - path_id: ECD-a + condition: uniprot_ecd_meets_min_length = true + applies_to: 有跨膜段且带 TOPO_DOM Extracellular 注释的蛋白 + measured_count: 18 + # 必需的第二条路径。UniProt 的 extracellular domain 字段由跨膜蛋白的 + # TOPO_DOM 推导,GPI 锚定蛋白零跨膜段、无 TOPO_DOM,故该字段一律为 false。 + # 若只用 ECD-a,CEACAM5、MSLN、FOLR1、MELTF 会因数据表示假象落入 hold, + # 而四者都是 confirmed_surface 且带信号肽与 GPI 锚——成熟蛋白整体位于胞外。 + - path_id: ECD-b + condition: uniprot_gpi_anchor = true 且 uniprot_signal_peptide = true 且 uniprot_transmembrane_segment_count = 0 + applies_to: GPI 锚定、带信号肽、零跨膜段的蛋白,成熟蛋白整体位于胞外 + rationale_note: >- + 这是修正数据表示假象,不是放宽标准:GPI 锚定加信号肽加零跨膜段在结构上 + 即意味着全长成熟蛋白位于细胞外侧,其胞外长度等于成熟蛋白长度。 + measured_count: 4 + measured_targets: + - CEACAM5 + - FOLR1 + - MELTF + - MSLN + luminal_domain_is_not_extracellular: true + luminal_domain_note: >- + 溶酶体/内体膜蛋白的大结构域朝向腔内而非胞外。LAMP1 即属此类: + 有跨膜段与信号肽,但无胞外 TOPO_DOM,故两条路径都不满足,落 hold。 + 这正是审核方要求的「细胞器膜定位必须 DEFER」。 + +# RQ-03:蛋白层面 provenance。 +rq_03_protein_level_provenance: + requires_rows_in: source_evidence.tsv + required_fields: + - source_id + - source_release + - evidence_family + - source_url + - license + rna_derived_rows_admissible: false + +# 确定性映射。五条规则覆盖全部 41 个靶点,无自由裁量,零排除。 +derivation_rules: + - id: E1-01 + condition: RQ-01 满足且 RQ-02 任一路径满足且 RQ-03 满足,且 discordance_flags 为空 + lock_01_outcome: eligible_surface_target + disposition: RETAIN + resulting_state: eligible + expected_count: 22 + - id: E1-02 + condition: RQ-02 满足但独立证据家族数 < 2 + lock_01_outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + expected_count: 6 + measured_targets: + - CLDN18 + - GUCY2C + - LGR5 + - PRLR + - RNF43 + - SLC44A4 + - id: E1-03 + condition: 两条 RQ-02 路径都不满足(含细胞器腔内结构域) + lock_01_outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + expected_count: 3 + measured_targets: + - LAMP1 + - TDGF1 + - TM4SF1 + - id: E1-04 + condition: discordance_flags 非空 + lock_01_outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: 定位证据冲突一律 DEFER,不得由抽取者裁决。 + expected_count: 6 + measured_targets: + - CD276 + - F3 + - IL2RA + - MET + - MST1R + - TACSTD2 + - id: E1-05 + condition: 靶点不在参考库中 + lock_01_outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: 缺失是 DEFER,永不 EXCLUDE;该库自身 absence_is_negative_evidence 亦为 false。 + expected_count: 4 + measured_targets: + - AG7 + - CA19-9 + - EDBN + - Undisclosed + +# not_surface_target 仍然不可用。 +unavailable_outcomes: + - outcome: not_surface_target + reason: >- + 本库明确 absence_is_negative_evidence=false,不产出「该靶点不是表面蛋白」 + 这类阳性否定断言。consensus_class = no_surface_support 只表示无支持证据, + 不等于已证伪。故本次抽取仍不得排除任何靶点。 + - outcome: identity_unresolved + reason: 本库无身份解析结论字段;未覆盖的 4 个靶点一律落 possible_surface_target。 + +# 抽取后的预期结果,逐项可核对。 +predicted_result_shape: + targets_total: 41 + eligible: 22 + hold: 19 + killed: 0 + hold_breakdown: + E1-02_no_independent_localization: 6 + E1-03_no_extracellular_domain: 3 + E1-04_conflict: 6 + E1-05_not_in_reference: 4 + eligible_via_path: + ECD-a: 18 + ECD-b: 4 + # 执行结果必须逐项等于上列数字,任一项不符即视为偏离本契约。 + +# 必须写进结果报告的发现,避免被误读。 +mandatory_findings: + - id: MF-01 + finding: >- + GUCY2C 落 hold。它在本参考库中只有 curated_knowledge 一个独立证据家族, + 未达两家族门槛,consensus_class 为 supported_surface 而非 confirmed_surface。 + 这与此前多模型共识把 GUCY2C 列为首选、以及被隔离运行的 Tier A 选择相反。 + 结果报告必须原样写出,不得因与既有偏好冲突而弱化。 + - id: MF-02 + finding: >- + eligible 只表示身份与拓扑层面存在有合理依据的细胞外可及蛋白形式, + 不表示该靶点在 CRC 肿瘤细胞表面可得——后者是 Level 02 的 T7,本次未评估。 + - id: MF-03 + finding: >- + 零排除。41 个靶点中没有任何一个被判为 not_surface_target。 + hold 不是淘汰,是待证据。 + +output_schema: + per_target_columns: + - gene_symbol + - uniprot_accessions + - rule_id + - lock_01_outcome + - disposition + - evaluation_status + - rq_01_satisfied + - rq_01_family_count + - rq_01_families + - rq_02_satisfied + - rq_02_path + - rq_03_satisfied + - consensus_class + - discordance_flags + - source_ids + - source_releases + - source_urls + - licenses + - dataset_version + - snapshot_id + - row_checksum + - extracted_at + +output_validation: + - id: VAL-E01 + rule: 输出必须恰好 41 行,每行命中且仅命中一条 derivation_rules 规则,并记录 rule_id。 + - id: VAL-E02 + rule: 计数必须等于 predicted_result_shape:22 eligible + 19 hold + 0 killed,且 hold_breakdown 逐项相符。 + - id: VAL-E03 + rule: 输出中不得出现 not_surface_target 或 identity_unresolved。 + - id: VAL-E04 + rule: 每个 eligible 行必须记录 rq_02_path,取值限于 ECD-a 或 ECD-b。 + - id: VAL-E05 + rule: 每行必须有非空 source_ids、source_releases、source_urls、licenses,缺失即该行降为 hold。 + - id: VAL-E06 + rule: 四个输入文件与靶点轴文件的 SHA-256 必须与本契约记录一致,不一致即中止执行。 + - id: VAL-E07 + rule: 不得读取 barred_files 或 barred_fields 中的任何文件或字段。 + - id: VAL-E08 + rule: 输出中不得出现任何 Gate 分数、Gate 状态、T7 判定或肿瘤表面定量。 + - id: VAL-E09 + rule: mandatory_findings 的三条必须原样出现在结果报告中。 + - id: VAL-E10 + rule: 每个产物文件必须逐文件记录 SHA-256 并写入结果 PR 的 handoff。 + +not_authorised: + - 执行 Level 01 + - 评估 T7 或任何肿瘤细胞表面可得性 + - 新增任何靶点或 clinical context + - 任何筛选排序、Tier 划分、资产推荐或实验建议 + - 任何 Gate 执行或评分 + - 读取被禁文件或字段 + - 把被隔离运行(PR #53、#54)的任何产物引入 diff --git a/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md b/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md new file mode 100644 index 0000000..96e0584 --- /dev/null +++ b/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md @@ -0,0 +1,139 @@ +# EVGAP-01:target surface localization 证据抽取契约 + +- 任务分支:`task_20260804_evgap-01-surface-localization-contract` +- 前置工作包:PR #57(Level 01 判据定义)、PR #58(输入绑定与缺口登记),均已 `APPROVE` 并合并 +- 机器可读绑定:[`../pools/evgap_01_surface_localization_extraction.yaml`](../pools/evgap_01_surface_localization_extraction.yaml),由 `tests/test_evgap_01_surface_localization.py` 校验 +- 当前状态:**contract-only,未执行,等待 ChatGPT 审核** +- 授权范围:**一次证据抽取运行。不授权执行 Level 01。** + +## 目的 + +解除 PR #58 登记的 `EVGAP-01`,使 Level 01 的 `LOCK-01` 能够真正产出 `eligible_surface_target`。 + +PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracellular`/`localization`/`signal peptide`/`GPI` 关键词命中数**均为 0**,只有 `transmembrane_segment_count`,而跨膜拓扑不足以判定细胞外可及。因此 41 个靶点全部落 `possible_surface_target`,`eligible = 0`,Level 01 无法录入任何 pair。 + +本契约冻结一次抽取运行的范围、来源、字段白名单、判据映射与输出验证。**不执行抽取,不执行 Level 01。** + +## 一、需要审核方裁决的治理事实:本次请求纳入一个从未被批准的数据库 + +所需数据**已经存在于本地**:`DATA/1.Databases/ADC_surfaceome_reference/processed/v0.3.0`。但必须先讲清楚它的治理状态: + +> **`ADC_surfaceome_reference` 从未被审核批准过。** 仓库内 `logs/chatgpt-review-*.md` 没有任何一条提及 surfaceome;`logs/worklog.md` 里唯一的提及是 2026-08-01 一次 mock 运行。已获批的证据抽取(PR #31)在其 `source_manifest.json` 中声明的来源是 `ADC_internalization_reference`,**没有接入本库**——这正是已批准层只有跨膜段注释、没有质膜定位证据的直接原因。 + +因此本 PR 请求把**这一个版本**纳入已批准来源集合:`dataset_version 0.3.0`、`snapshot_id 2026-07-29-quant-topology-mm`、`raw_manifest_sha256 884f4191…`,四个文件逐一记录 SHA-256(`surfaceome_consensus.tsv` 20,797 行、`membrane_topology_evidence.tsv` 4,863 行、`source_evidence.tsv` 41,204 行、`build_manifest.json`)。执行前必须逐个校验,任一不一致即中止(`VAL-E06`)。 + +### 主张纳入它的核心理由:守卫是它构建时就写死的,不是本契约事后附加的 + +该库自身的 `build_manifest.json` → `consensus_semantics` 已经编码了本仓库反复要求的语义: + +| 该库的声明 | 对应仓库规则 | +|---|---| +| `absence_is_negative_evidence: false` | 缺失证据一律 DEFER,永不 EXCLUDE | +| `membrane_topology_is_independent_surface_localization: false` | **PR #58 第二轮阻断本身**——跨膜段不足以判定细胞外可及 | +| `generic_membrane_is_surface_confirmation: false` | 泛膜注释不等于质膜定位 | +| `cci_receptor_role_is_surface_confirmation: false` | 受体角色推断不等于定位证据 | +| `tumor_ihc_is_surface_density: false` | 表达强度不等于表面密度(属 Level 02 T7) | +| 排除 `GSE160572_MM_surfaceome.csv.gz`,理由是「processed values are RNA FPKM, not direct surface-protein measurements」 | RNA 不得当作蛋白层面验证,无例外 | + +该库还自设 `full_t7_gate_confidence_cap: 0.55`,并在 note 中明确它「does not establish a general malignant-cell positive fraction, isoform usage, calibrated treatment stability, or ADC accessibility」——即它自己就把 Level 02 的边界划开了。 + +## 二、抽取范围 + +只处理已批准枚举的 41 个靶点(`target_evidence_catalog.tsv`,SHA-256 `27bb81eb…`),**不新增任何靶点**,按 `gene_symbol` 连接。 + +实测覆盖:**37 覆盖 / 4 未覆盖**。未覆盖的是 `AG7`、`CA19-9`、`EDBN`、`Undisclosed`——两个占位符、一个碳水化合物抗原、一个非标准符号。四者一律落 `possible_surface_target` DEFER(`E1-05`)。 + +## 三、字段白名单与明确禁止读取的内容 + +只允许读 `surfaceome_consensus.tsv` 的 16 个字段与 `source_evidence.tsv` 的 8 个字段(清单见 YAML `allowed_fields`)。 + +**禁止读取以下文件**,读入即重演 PR #58 被阻断的越界: + +| 文件 | 理由 | +|---|---| +| `tumor_surface_measurement.tsv` | 肿瘤表面定量属 Level 02 T7 | +| `tumor_protein_context.tsv` | 肿瘤上下文蛋白组属 Level 02 T7 | +| `treatment_surface_response.tsv` | 治疗后表面变化属 T5/T7 | +| `receptor_evidence.tsv` | 受体角色推断不构成定位证据(该库自身亦声明 `cci_receptor_role_is_surface_confirmation: false`) | + +**禁止读取以下字段**:`cci_receptor_role`(角色不是定位)、`uniprot_generic_membrane`(泛膜不等于质膜)、`full_t7_gate_confidence_cap`(T7 置信属 Level 02)。 + +## 四、判据映射 + +### `RQ-01` 质膜定位:要求 ≥ 2 个独立证据家族 + +判决字段是 `independent_evidence_family_count`。该库定义的三个独立家族为 `curated_knowledge`/`imaging`/`cell_surface_capture_ms`,且**已把拓扑与泛膜排除在家族计数之外**——所以家族计数在结构上不可能把跨膜段当成定位证据。RNA 不得满足。 + +### `RQ-02` 细胞外结构域:两条合格路径 + +| 路径 | 条件 | 适用 | 实测 | +|---|---|---|---| +| `ECD-a` | `uniprot_ecd_meets_min_length = true` | 有跨膜段且带 TOPO_DOM Extracellular 注释 | **18** | +| `ECD-b` | `uniprot_gpi_anchor = true` 且 `uniprot_signal_peptide = true` 且 `uniprot_transmembrane_segment_count = 0` | GPI 锚定、带信号肽、零跨膜段 | **4** | + +**`ECD-b` 是必需的,理由是修正数据表示假象而不是放宽标准。** UniProt 的 extracellular domain 字段由跨膜蛋白的 TOPO_DOM 推导;GPI 锚定蛋白零跨膜段、没有 TOPO_DOM,该字段一律读成 `false`。若只用 `ECD-a`,`CEACAM5`、`MSLN`、`FOLR1`、`MELTF` 会因此落 hold——而四者在本库中都是 `confirmed_surface`,都带信号肽与 GPI 锚,成熟蛋白整体位于细胞外侧。GPI 锚 + 信号肽 + 零跨膜段在结构上即意味着全长成熟蛋白朝向胞外,其胞外长度等于成熟蛋白长度。 + +**腔内结构域不算胞外。** `LAMP1` 即属此类:有跨膜段与信号肽,但无胞外 TOPO_DOM,两条路径都不满足,落 hold。这正是审核方要求的「细胞器膜定位必须 DEFER」,规则自然落在正确一侧。 + +### `RQ-03` 蛋白层面 provenance + +要求 `source_evidence.tsv` 中存在对应行,且 `source_id`、`source_release`、`evidence_family`、`source_url`、`license` 均非空。RNA 来源行不可采纳。 + +## 五、确定性映射:五条规则覆盖 41 个靶点,零排除 + +| ID | 条件 | outcome | disposition | 数量 | +|---|---|---|---|---| +| `E1-01` | `RQ-01`+`RQ-02`+`RQ-03` 全满足且无冲突 | `eligible_surface_target` | RETAIN | **22** | +| `E1-02` | `RQ-02` 满足但独立家族数 < 2 | `possible_surface_target` | DEFER | **6** | +| `E1-03` | 两条 `RQ-02` 路径都不满足(含腔内结构域) | `possible_surface_target` | DEFER | **3** | +| `E1-04` | `discordance_flags` 非空 | `possible_surface_target` | DEFER | **6** | +| `E1-05` | 靶点不在参考库中 | `possible_surface_target` | DEFER | **4** | + +22 + 6 + 3 + 6 + 4 = 41。**零自由裁量、零排除。** + +- `E1-02`:`CLDN18`、`GUCY2C`、`LGR5`、`PRLR`、`RNF43`、`SLC44A4` +- `E1-03`:`LAMP1`、`TDGF1`、`TM4SF1` +- `E1-04`:`CD276`、`F3`、`IL2RA`、`MET`、`MST1R`、`TACSTD2` +- `E1-05`:`AG7`、`CA19-9`、`EDBN`、`Undisclosed` + +`not_surface_target` 与 `identity_unresolved` **仍然不可用**:该库明确 `absence_is_negative_evidence: false`,不产出「该靶点不是表面蛋白」这类阳性否定断言;`consensus_class = no_surface_support` 只表示无支持证据,不等于已证伪。**本次抽取不得排除任何靶点。** + +## 六、抽取后的预期结果,逐项可核对 + +| 量 | 值 | +|---|---| +| 靶点总数 | 41 | +| eligible | **22**(`ECD-a` 18 + `ECD-b` 4) | +| hold | **19**(6 + 3 + 6 + 4) | +| killed | **0** | + +**执行结果必须逐项等于上表,任一项不符即视为偏离本契约。** + +## 七、必须写进结果报告的三条发现 + +- **`MF-01`:`GUCY2C` 落 hold。** 它在本参考库中只有 `curated_knowledge` 一个独立证据家族,未达两家族门槛,`consensus_class` 是 `supported_surface` 而非 `confirmed_surface`。**这与此前多模型共识把 GUCY2C 列为首选、以及被隔离运行的 Tier A 选择相反。** 结果报告必须原样写出,不得因与既有偏好冲突而弱化。 +- **`MF-02`:`eligible` 只表示身份与拓扑层面存在有合理依据的细胞外可及蛋白形式**,不表示该靶点在 CRC 肿瘤细胞表面可得。后者是 Level 02 的 T7,本次未评估。 +- **`MF-03`:零排除。** 41 个靶点中没有任何一个被判为 `not_surface_target`。hold 不是淘汰,是待证据。 + +## 八、输出与验证 + +输出为每靶点一行、21 列(见 YAML `output_schema`),必含 `rule_id`、`rq_02_path`、`evaluation_status`、provenance 与 `row_checksum`。十条验证规则 `VAL-E01`..`VAL-E10` 见 YAML,要点:41 行且每行命中且仅命中一条规则;计数等于第六节;不得出现 `not_surface_target` 或 `identity_unresolved`;每个 eligible 必记 `rq_02_path`;provenance 缺失即降为 hold;输入 SHA-256 不一致即中止;不得读取被禁文件或字段;输出中不得出现任何 Gate 分数、Gate 状态、T7 判定或肿瘤表面定量;三条 `mandatory_findings` 必须原样出现;每个产物文件逐文件记录 SHA-256。 + +## 九、本契约授权与不授权 + +**授权:** 把 `ADC_surfaceome_reference@0.3.0`(指定 snapshot 与校验和)纳入已批准来源;按本契约执行**一次**抽取运行。 + +**不授权:** 执行 Level 01;评估 T7 或任何肿瘤细胞表面可得性;新增任何靶点或 clinical context;任何筛选排序、Tier 划分、资产推荐或实验建议;任何 Gate 执行或评分;读取被禁文件或字段;把被隔离运行(PR #53、#54)的任何产物引入。 + +## 十、后续顺序 + +1. 本契约获 `APPROVE`。 +2. 执行抽取 → 结果 PR → `APPROVE`。 +3. **另开一个 PR** 更新 `adc_pool_level_01_input_binding.yaml`,把抽取产物绑为 `LOCK-01` 的来源并解除 `EVGAP-01`。该 PR 不在本契约授权范围内。 +4. `EVGAP-02`(`LOCK-03` 的源级 CRC linkage 证据)仍未解除,需其独立契约。**两个缺口都解除后,Level 01 才能执行。** + +## 十一、当前阻断 + +- 本契约获 ChatGPT `APPROVE` 前,不得执行抽取。 +- 抽取完成也**不**解除 `EVGAP-02`,Level 01 仍不可执行。 +- 本仓库不得写入证据、候选、快照、cache、result 或 weights。 diff --git a/logs/worklog.md b/logs/worklog.md index 690b289..6ff1b2d 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2508,3 +2508,21 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - 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 描述,请求复审。 + +## 2026-08-04T21:45:00-04:00 — EVGAP-01 target surface localization 抽取契约(contract-only,未执行) + +- Instruction: 人类负责人指示继续起 `EVGAP-01` 契约。该缺口由 PR #58 登记,阻断 `LOCK-01`——已批准证据层无蛋白层面质膜定位与细胞外结构域证据,41 个靶点全部 DEFER、`eligible = 0`、Level 01 无法录入任何 pair。 +- Finding (high), governance: **所需数据已在本地 `DATA/1.Databases/ADC_surfaceome_reference/processed/v0.3.0`,但该数据库从未被审核批准。** `logs/chatgpt-review-*.md` 无任何一条提及 surfaceome;worklog 唯一提及是 2026-08-01 mock 运行;已获批的证据抽取(PR #31)`source_manifest.json` 声明的来源是 `ADC_internalization_reference`,**没有接入本库**。这解释了此前未解释的事:已批准层只有跨膜段注释,不是数据不存在,是当时没接。 +- Action: 本 PR 请求把该单一版本纳入已批准来源——`0.3.0`/snapshot `2026-07-29-quant-topology-mm`/`raw_manifest_sha256 884f4191…`,四个文件逐一记录 SHA-256(`surfaceome_consensus.tsv` 20,797 行、`membrane_topology_evidence.tsv` 4,863 行、`source_evidence.tsv` 41,204 行、`build_manifest.json`),执行前逐个校验、不一致即中止(`VAL-E06`)。 +- Rationale for admission: 该库 `consensus_semantics` 已把本仓库反复要求的守卫写死在构建时,其中两条正是前几轮审核的阻断本身——`membrane_topology_is_independent_surface_localization: false`(PR #58 第二轮阻断)、`absence_is_negative_evidence: false`(缺失一律 DEFER);另有 `generic_membrane_is_surface_confirmation: false`、`cci_receptor_role_is_surface_confirmation: false`、`tumor_ihc_is_surface_density: false`,并因 RNA FPKM 排除 `GSE160572_MM_surfaceome.csv.gz`。它还自设 `full_t7_gate_confidence_cap: 0.55` 并声明不建立 malignant-cell positive fraction/isoform usage/calibrated treatment stability/ADC accessibility,即自行划开 Level 02 边界。守卫是构建时写死的、不是本契约事后附加的——这是主张纳入它而非另起网络抽取的核心理由。 +- Scope frozen: 只处理已批准枚举的 41 靶点(`target_evidence_catalog.tsv`,SHA-256 `27bb81eb…`),不新增靶点,按 `gene_symbol` 连接。实测覆盖 37/未覆盖 4(`AG7`、`CA19-9`、`EDBN`、`Undisclosed`,两个占位符、一个碳水化合物抗原、一个非标准符号)。 +- Barred reads: 禁止读取 `tumor_surface_measurement.tsv`、`tumor_protein_context.tsv`、`treatment_surface_response.tsv`、`receptor_evidence.tsv`(前三属 Level 02 T7/T5,第四项该库自身即声明受体角色不构成定位证据);禁止字段 `cci_receptor_role`、`uniprot_generic_membrane`、`full_t7_gate_confidence_cap`。读入即重演 PR #58 被阻断的越界。 +- `RQ-01` mapping: 判决字段 `independent_evidence_family_count ≥ 2`。三个独立家族为 `curated_knowledge`/`imaging`/`cell_surface_capture_ms`,**拓扑与泛膜已被该库排除在家族计数之外**,故家族计数在结构上不可能把跨膜段当定位证据。RNA 不得满足。 +- `RQ-02` mapping, two paths: `ECD-a` = `uniprot_ecd_meets_min_length = true`(18);`ECD-b` = `uniprot_gpi_anchor` 且 `uniprot_signal_peptide` 且 `transmembrane_segment_count = 0`(4)。**`ECD-b` 是修正数据表示假象而非放宽标准**:UniProt 的 extracellular domain 字段由跨膜蛋白 TOPO_DOM 推导,GPI 锚定蛋白零跨膜段、无 TOPO_DOM,该字段一律 `false`;只用 `ECD-a` 会让 `CEACAM5`、`MSLN`、`FOLR1`、`MELTF` 因假象落 hold,而四者在库中皆为 `confirmed_surface` 且带信号肽与 GPI 锚,成熟蛋白整体位于胞外。对照:`LAMP1` 有跨膜段与信号肽但结构域朝向溶酶体腔内、无胞外 TOPO_DOM,两条路径都不满足、落 hold——审核方要求的「细胞器膜定位必须 DEFER」由规则自然落在正确一侧,无需特判。 +- Derivation, total and exclusion-free: `E1-01` 三项 RQ 全满足且无冲突 → RETAIN **22**;`E1-02` 家族数 < 2 → DEFER **6**(CLDN18、GUCY2C、LGR5、PRLR、RNF43、SLC44A4);`E1-03` 两条 ECD 路径都不满足 → DEFER **3**(LAMP1、TDGF1、TM4SF1);`E1-04` `discordance_flags` 非空 → DEFER **6**(CD276、F3、IL2RA、MET、MST1R、TACSTD2);`E1-05` 不在库中 → DEFER **4**。22+6+3+6+4 = 41,零自由裁量、零排除。`not_surface_target` 与 `identity_unresolved` 仍不可用(该库 `absence_is_negative_evidence: false`,`no_surface_support` 只表示无支持证据、不等于已证伪)。 +- Mandatory findings recorded: **`MF-01` GUCY2C 落 hold**——只有 `curated_knowledge` 一个独立家族,`consensus_class` 为 `supported_surface` 而非 `confirmed_surface`,**与此前多模型共识首选及被隔离运行的 Tier A 相反**,必须原样写入结果报告、不得弱化;测试会检查该靶点确实出现在某条 DEFER 规则的 `measured_targets` 中,防止这条发现被改成空话。`MF-02` eligible 只是身份与拓扑层面结论,不代表 CRC 肿瘤细胞表面可得(属 Level 02 T7)。`MF-03` 零排除,hold 不是淘汰。 +- Validation: `Ran 298 tests` 全部通过(`main` 基线 283 + 新增 15);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。实测计数:37/4 覆盖;`consensus_class` 29 confirmed/7 supported/1 no_support;独立家族数 3 家族 10 个、2 家族 19 个、1 家族 7 个、0 家族 1 个;`discordance_flags` 非空 6 个;最终 22 eligible(ECD-a 18 + ECD-b 4)/19 hold/0 killed。四个数据库文件 SHA-256 由 `shasum -a 256` 实算。 +- Deliberately not done: 未执行抽取;未执行 Level 01 也不授权执行(`EVGAP-02` 仍未解除);未解除 `EVGAP-01` 本身(须待抽取执行、结果 PR 获批后另开 PR 更新输入绑定);未读取被禁文件或字段作为判据;未引用被隔离运行任何产物;未新增靶点或 clinical context;**未补 #52/#53/#54/#57/#58 的批准记录**(现为五份,事实已查全未写文件)。 +- Noticed, not fixed: `requirements.txt` 注释仍写「207 tests」,实测 298。属无关改动,只记录。 +- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。 +- Next: 推送并创建 PR 送审;请审核方裁决是否接受把 `ADC_surfaceome_reference@0.3.0` 纳入已批准来源,以及 `ECD-b` 路径是否成立。 diff --git a/tests/test_evgap_01_surface_localization.py b/tests/test_evgap_01_surface_localization.py new file mode 100644 index 0000000..abf419c --- /dev/null +++ b/tests/test_evgap_01_surface_localization.py @@ -0,0 +1,266 @@ +"""Verify docs/pools/evgap_01_surface_localization_extraction.yaml. + +The extraction contract discharges EVGAP-01 by admitting one pinned database +version as an approved source and freezing how its fields map onto LOCK-01's +RQ-01/RQ-02/RQ-03. These tests check internal consistency and agreement with the +merged Level 01 contracts. They never read the external database: it lives +outside the repository, so the recorded checksums are audit metadata. +""" + +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] +POOLS = REPO_ROOT / "docs" / "pools" +EXTRACTION_PATH = POOLS / "evgap_01_surface_localization_extraction.yaml" +BINDING_PATH = POOLS / "adc_pool_level_01_input_binding.yaml" +LEVEL_CONTRACT_PATH = POOLS / "adc_pool_gate_usage.yaml" + +# Level 02 / T7 material that must never enter LOCK-01. +FORBIDDEN_FILES = frozenset( + { + "tumor_surface_measurement.tsv", + "tumor_protein_context.tsv", + "treatment_surface_response.tsv", + "receptor_evidence.tsv", + } +) + + +def _load(path: Path) -> dict: + with path.open(encoding="utf-8") as handle: + return yaml.safe_load(handle) + + +class Evgap01ExtractionContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.doc = _load(EXTRACTION_PATH) + cls.binding = _load(BINDING_PATH) + cls.level = { + entry["level"]: entry for entry in _load(LEVEL_CONTRACT_PATH)["levels"] + }["01"] + + def test_contract_targets_the_registered_gap(self) -> None: + head = self.doc["extraction"] + self.assertEqual(head["discharges_gap"], "EVGAP-01") + self.assertEqual(head["serves_lock"], "LOCK-01") + gaps = {g["id"]: g for g in self.binding["evidence_gaps"]} + self.assertIn("EVGAP-01", gaps) + self.assertEqual(gaps["EVGAP-01"]["blocks"], "LOCK-01") + self.assertEqual(head["execution_status"], "not_authorized_not_executed") + self.assertIs(head["authorises_extraction_run"], True) + self.assertIs(head["authorises_level_01_execution"], False) + self.assertIs(head["requires_followup_binding_pr"], True) + + def test_source_admission_is_declared_as_not_previously_approved(self) -> None: + request = self.doc["source_admission_request"] + self.assertIs(request["previously_approved"], False) + self.assertTrue(request["previously_approved_evidence"].strip()) + self.assertTrue(request["root"].startswith("external:")) + self.assertEqual(request["dataset_version"], "0.3.0") + for item in request["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)) + + def test_inherited_guards_cover_the_repository_rules(self) -> None: + guards = {g["guard"]: g for g in + self.doc["source_admission_request"]["inherited_semantics_guards"]} + # The two that previous review rounds turned on must be present and false. + for name in ("absence_is_negative_evidence", + "membrane_topology_is_independent_surface_localization"): + with self.subTest(guard=name): + self.assertIn(name, guards) + self.assertIs(guards[name]["value"], False) + self.assertTrue(guards[name]["matches_repo_rule"].strip()) + self.assertIn("excluded_rna_source", guards) + + def test_scope_is_the_approved_target_axis_only(self) -> None: + scope = self.doc["scope"] + self.assertIs(scope["new_targets_allowed"], False) + self.assertEqual( + scope["target_count"], self.binding["scope_consequences"]["raw_targets"] + ) + self.assertEqual( + scope["target_axis_sha256"], + next( + f["sha256"] + for s in self.binding["accepted_sources"] + for f in s["files"] + if f["path"] == "target_evidence_catalog.tsv" + ), + ) + self.assertEqual( + scope["measured_coverage_in_reference"] + + scope["measured_absent_from_reference"], + scope["target_count"], + ) + self.assertEqual( + len(scope["measured_absent_targets"]), scope["measured_absent_from_reference"] + ) + + def test_level_02_material_is_barred(self) -> None: + barred = {item["path"] for item in self.doc["barred_files"]} + self.assertEqual(barred, FORBIDDEN_FILES) + for item in self.doc["barred_files"]: + self.assertTrue(item["reason"].strip()) + allowed = set(self.doc["allowed_fields"]["from_surfaceome_consensus"]) | set( + self.doc["allowed_fields"]["from_source_evidence"] + ) + for item in self.doc["barred_fields"]: + with self.subTest(field=item["field"]): + self.assertNotIn(item["field"], allowed) + self.assertTrue(item["reason"].strip()) + # Nothing capped for T7 may leak into the field whitelist. + self.assertNotIn("full_t7_gate_confidence_cap", allowed) + + def test_rq_01_never_counts_topology_or_rna(self) -> None: + rq1 = self.doc["rq_01_plasma_membrane_localization"] + self.assertGreaterEqual(rq1["minimum_independent_families"], 2) + self.assertIs(rq1["topology_counts_as_family"], False) + self.assertIs(rq1["generic_membrane_counts_as_family"], False) + self.assertIs(rq1["rna_may_satisfy"], False) + self.assertEqual(len(rq1["independent_families"]), 3) + + def test_rq_02_has_both_paths_and_excludes_luminal_domains(self) -> None: + rq2 = self.doc["rq_02_extracellular_domain"] + paths = {p["path_id"]: p for p in rq2["paths"]} + self.assertEqual(set(paths), {"ECD-a", "ECD-b"}) + # The GPI path exists to correct a representation artefact, and must say so. + self.assertTrue(paths["ECD-b"]["rationale_note"].strip()) + self.assertEqual(len(paths["ECD-b"]["measured_targets"]), + paths["ECD-b"]["measured_count"]) + self.assertIs(rq2["luminal_domain_is_not_extracellular"], True) + self.assertIn("LAMP1", rq2["luminal_domain_note"]) + eligible = next(r for r in self.doc["derivation_rules"] if r["id"] == "E1-01") + self.assertEqual( + sum(p["measured_count"] for p in rq2["paths"]), eligible["expected_count"] + ) + + def test_rq_03_requires_protein_level_provenance(self) -> None: + rq3 = self.doc["rq_03_protein_level_provenance"] + self.assertIs(rq3["rna_derived_rows_admissible"], False) + for field in ("source_id", "source_release", "source_url", "license"): + self.assertIn(field, rq3["required_fields"]) + allowed = set(self.doc["allowed_fields"]["from_source_evidence"]) + self.assertTrue(set(rq3["required_fields"]) <= allowed) + + def test_derivation_rules_are_total_and_never_exclude(self) -> None: + rules = self.doc["derivation_rules"] + ids = [r["id"] for r in rules] + self.assertEqual(len(ids), len(set(ids))) + outcomes = { + o["outcome"]: o + for lock in self.level["locks"] + if lock["lock_id"] == "LOCK-01" + for o in lock["outcomes"] + } + for rule in rules: + with self.subTest(rule=rule["id"]): + self.assertIn(rule["lock_01_outcome"], outcomes) + self.assertNotEqual( + rule["disposition"], CandidateDisposition.EXCLUDE.value + ) + self.assertEqual( + rule["resulting_state"], + outcomes[rule["lock_01_outcome"]]["resulting_state"], + ) + self.assertEqual( + sum(r["expected_count"] for r in rules), self.doc["scope"]["target_count"] + ) + # Every rule that names targets must name exactly as many as it counts. + for rule in rules: + if "measured_targets" in rule: + with self.subTest(rule=rule["id"]): + self.assertEqual( + len(rule["measured_targets"]), rule["expected_count"] + ) + + def test_exclusion_outcomes_remain_unavailable(self) -> None: + unavailable = {u["outcome"]: u for u in self.doc["unavailable_outcomes"]} + self.assertIn("not_surface_target", unavailable) + self.assertIn("identity_unresolved", unavailable) + for item in unavailable.values(): + self.assertTrue(item["reason"].strip()) + produced = {r["lock_01_outcome"] for r in self.doc["derivation_rules"]} + self.assertEqual(produced & set(unavailable), set()) + forbidding = [ + r for r in self.doc["output_validation"] + if "not_surface_target" in r["rule"] + ] + self.assertTrue(forbidding) + + def test_predicted_shape_reconciles_with_the_rules(self) -> None: + shape = self.doc["predicted_result_shape"] + self.assertEqual( + shape["eligible"] + shape["hold"] + shape["killed"], shape["targets_total"] + ) + self.assertEqual(shape["targets_total"], self.doc["scope"]["target_count"]) + self.assertEqual(shape["killed"], 0) + by_id = {r["id"]: r for r in self.doc["derivation_rules"]} + self.assertEqual(by_id["E1-01"]["expected_count"], shape["eligible"]) + breakdown = shape["hold_breakdown"] + self.assertEqual(sum(breakdown.values()), shape["hold"]) + for key, count in breakdown.items(): + with self.subTest(key=key): + self.assertEqual(by_id[key.split("_")[0]]["expected_count"], count) + self.assertEqual(sum(shape["eligible_via_path"].values()), shape["eligible"]) + + def test_mandatory_findings_preserve_the_uncomfortable_ones(self) -> None: + findings = {f["id"]: f["finding"] for f in self.doc["mandatory_findings"]} + self.assertEqual(set(findings), {"MF-01", "MF-02", "MF-03"}) + # The finding that contradicts the earlier consensus must be kept. + self.assertIn("GUCY2C", findings["MF-01"]) + deferred = [ + r for r in self.doc["derivation_rules"] + if "GUCY2C" in r.get("measured_targets", []) + ] + self.assertTrue(deferred, "MF-01 claims GUCY2C defers; no rule shows it") + self.assertEqual( + deferred[0]["disposition"], CandidateDisposition.DEFER.value + ) + for text in findings.values(): + self.assertTrue(text.strip()) + + def test_output_schema_carries_rule_and_path_provenance(self) -> None: + columns = self.doc["output_schema"]["per_target_columns"] + self.assertEqual(len(columns), len(set(columns))) + for required in ("rule_id", "rq_02_path", "evaluation_status", "row_checksum", + "source_urls", "licenses", "dataset_version", "snapshot_id"): + with self.subTest(column=required): + self.assertIn(required, columns) + + def test_validation_rules_are_unique_and_bar_the_forbidden_reads(self) -> None: + rules = self.doc["output_validation"] + ids = [r["id"] for r in rules] + self.assertEqual(len(ids), len(set(ids))) + text = " ".join(r["rule"] for r in rules) + for phrase in ("SHA-256", "barred_files", "Gate", "mandatory_findings"): + with self.subTest(phrase=phrase): + self.assertIn(phrase, text) + + def test_authorisation_stops_short_of_level_01_and_t7(self) -> None: + not_authorised = " ".join(self.doc["not_authorised"]) + for phrase in ("执行 Level 01", "T7", "Gate"): + with self.subTest(phrase=phrase): + self.assertIn(phrase, not_authorised) + self.assertIs( + self.doc["extraction"]["authorises_level_01_execution"], False + ) + # Level 01 itself must still be unauthorised until the follow-up PR. + self.assertIs( + self.binding["binding"]["authorises_level_01_execution"], False + ) + + +if __name__ == "__main__": + unittest.main() From f236287dd955302f9381683318d5fd1cbc7b14b6 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Wed, 5 Aug 2026 11:14:56 -0400 Subject: [PATCH 2/3] task_20260804: address PR #59 review, defer source admission and freeze precedence All four blockers accepted and fixed in this PR. Blocker 1: a derived database cannot be admitted by self-declaration plus hashes. The draft recorded the snapshot, checksums, builder path and the database's own semantic guards, but the tests deliberately never read the external database, so nothing was actually audited: not the builder, the raw manifest, the source list, the licenses, whether the evidence families are genuinely independent, the dedup logic, how discordance flags are produced, row-level traceback, or whether the snapshot rebuilds. The reviewer is right that a database declaring membrane_topology_is_independent_surface_localization false is not the same as that database having been verified to obey it. I treated "its semantics are written correctly" as "it has been verified". Admission is now split out as dependency SRCADM-01 with a null record ref this contract may not fill in, the extraction is blocked on it, nine audit items are registered, the six self-declared guards are marked claim_pending_audit, and the checksums are demoted to integrity pins. Blocker 2: the ECD path counts conflated satisfying a path with being eligible. E1-02's six targets satisfy RQ-02 by construction, so path satisfaction cannot total 22. Measured and split: ECD-a satisfied 30 of which 18 eligible, ECD-b satisfied 4 of which 4 eligible, no overlap, and the decomposition 34 RQ-02 positives = 22 eligible + 6 E1-02 + 6 discordant is now an asserted identity. Blocker 3: VAL-E05 required source provenance on every row, which the four reference-absent targets cannot supply, so it would have failed legitimate holds or pushed the executor to fabricate provenance. Provenance is now split: covered rows need source provenance, absent rows need absence provenance and may leave the source fields empty, fabrication is forbidden, and a reference_absent row carrying source ids is a validation failure. Blocker 4: the five conditions are not mutually exclusive - TM4SF1 and TDGF1 each match both E1-03 and E1-02 - and only the pre-written totals were checked, not one-and-only-one. Precedence is now frozen as E1-05, E1-04, E1-03, E1-02, E1-01 with its rationale, the two multi-condition targets are recorded as resolving to E1-03, and tests prove unique assignment over every overlap combination and that a resolved target is not also counted under the suppressed rule. The totals are unchanged at 22/6/3/6/4, because the original script already applied this order implicitly; it simply was not written into the contract. 306 tests pass. Twelve mutations caught and rolled back exactly. Co-Authored-By: Claude Opus 5 --- ...-01-surface-localization-contract.zh-CN.md | 69 ++++- ...ap_01_surface_localization_extraction.yaml | 217 ++++++++++++---- ..._LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md | 114 ++++++--- logs/worklog.md | 18 ++ tests/test_evgap_01_surface_localization.py | 235 +++++++++++++++--- 5 files changed, 531 insertions(+), 122 deletions(-) diff --git a/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md b/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md index 2ab0792..6d20ff2 100644 --- a/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md +++ b/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md @@ -6,7 +6,7 @@ - 前置:PR #57、PR #58,均已 `APPROVE` 并合并 - 交付物类型:**contract-only** - 外部运行:**无。没有执行抽取,没有产生任何证据、判定或候选。** -- 授权范围:**一次抽取运行;不授权执行 Level 01** +- 授权范围:**只冻结抽取边界;抽取本身尚未获授权(待 `SRCADM-01`);不授权执行 Level 01** - 架构变更:`NO_ARCHITECTURE_CHANGE`(依据是 diff 范围,可由 `git diff --stat` 核验) - 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** @@ -24,7 +24,9 @@ 这解释了一件之前没解释的事:**为什么已批准层只有跨膜段注释。** 不是数据不存在,是当时没接。 -因此本 PR 请求把这一个版本纳入已批准来源:`0.3.0` / snapshot `2026-07-29-quant-topology-mm` / `raw_manifest_sha256 884f4191…`,四个文件逐一记录 SHA-256。 +初稿据此请求把这一个版本纳入已批准来源。 + +> **本段与下一小节已被第十节阻断 1 推翻。** 派生数据库不能靠自声明 + 哈希纳入;admission 已剥离为独立依赖项 `SRCADM-01`,本契约只引用不批准。原文保留以对照裁决前后差异。 ### 主张纳入它的核心理由 @@ -39,13 +41,15 @@ 守卫是构建时写死的,不是本契约事后附加的。这是主张纳入它、而不是另起一次网络抽取的核心理由。 +> **更正(第十节阻断 1)**:以上六条现一律标记 `status: claim_pending_audit`。它们是主张 admission 的**理由**,不是 admission 已完成的**证据**——库自己声明遵守某条规则,不等于已验证它确实遵守。 + ## 三、仓库内交付了什么 | 文件 | 作用 | |---|---| | `docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md` | 抽取契约(面向操作者,中文) | -| `docs/pools/evgap_01_surface_localization_extraction.yaml` | 机器可读绑定:来源纳入请求+SHA-256、字段白名单、禁读清单、RQ 映射、五条判据规则、输出 schema 与十条验证 | -| `tests/test_evgap_01_surface_localization.py` | 15 项校验 | +| `docs/pools/evgap_01_surface_localization_extraction.yaml` | 机器可读绑定:来源依赖项 `SRCADM-01`+完整性校验和、字段白名单、禁读清单、RQ 映射、求值优先级、五条判据规则、输出 schema 与 15 条验证 | +| `tests/test_evgap_01_surface_localization.py` | 23 项校验 | ## 四、确定性映射与实测结果 @@ -58,6 +62,7 @@ | ID | 条件 | outcome | 数量 | |---|---|---|---| | `E1-01` | 三项 RQ 全满足且无冲突 | `eligible_surface_target` RETAIN | **22** | +(按冻结的优先级 `E1-05` → `E1-04` → `E1-03` → `E1-02` → `E1-01` 求值,见第十节阻断 4) | `E1-02` | 独立家族数 < 2 | `possible_surface_target` DEFER | **6** | | `E1-03` | 两条 ECD 路径都不满足 | `possible_surface_target` DEFER | **3** | | `E1-04` | `discordance_flags` 非空 | `possible_surface_target` DEFER | **6** | @@ -80,11 +85,11 @@ - 没有引用被隔离运行(PR #53、#54)的任何产物。 - 没有新增靶点或 clinical context。 - **没有补 #52/#53/#54/#57/#58 的批准记录**(现在是五份)。事实已查全,未写文件。 -- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 298)。属无关改动。 +- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 306)。属无关改动。 ## 七、验证结果 -- `Ran 298 tests` 全部通过(`main` 基线 283 + 本次新增 15)。 +- `Ran 306 tests` 全部通过(`main` 基线 283 + 本次新增 23)。 - `scripts/verify_repository_boundary.sh`:`Repository boundary check passed.` - `git diff --check`:通过;零 `__pycache__`。 - 所有计数由脚本读取外部数据库实测,非估计:41 靶点中 37 覆盖 / 4 未覆盖;`consensus_class` 为 29 `confirmed_surface` / 7 `supported_surface` / 1 `no_surface_support`;独立家族数分布 3 家族 10 个、2 家族 19 个、1 家族 7 个、0 家族 1 个;`discordance_flags` 非空 6 个;最终 22 eligible(`ECD-a` 18 + `ECD-b` 4)/ 19 hold / 0 killed。 @@ -93,12 +98,58 @@ ## 八、后续顺序 1. 本契约 `APPROVE`。 -2. 执行抽取 → 结果 PR → `APPROVE`。 -3. 另开 PR 更新 `adc_pool_level_01_input_binding.yaml`,绑定抽取产物并解除 `EVGAP-01`。 -4. `EVGAP-02` 需其独立契约。**两个缺口都解除后,Level 01 才能执行。** +2. **`SRCADM-01` 独立 source admission PR** 审计 `AUD-01`..`AUD-09` → `APPROVE`,在 `logs/` 留记录。 +3. 把 `admission_record_ref` 指向该记录,执行抽取 → 结果 PR → `APPROVE`。 +4. 另开 PR 更新 `adc_pool_level_01_input_binding.yaml`,绑定抽取产物并解除 `EVGAP-01`。 +5. `EVGAP-02` 需其独立契约。**两个缺口都解除后,Level 01 才能执行。** ## 九、当前阻断 - 本契约获 `APPROVE` 前,不得执行抽取。 +- **获 `APPROVE` 后仍不得执行抽取**,直到 `SRCADM-01` 取得独立 `APPROVE` 记录(`VAL-E13`)。 - 抽取完成也**不**解除 `EVGAP-02`,Level 01 仍不可执行。 - 本仓库不得写入证据、候选、快照、cache、result 或 weights。 + +## 十、第一轮审核裁决与修订(`REQUEST_CHANGES`,四条阻断全部接受) + +ChatGPT 对 PR #59(HEAD `570562c`)返回 `REQUEST_CHANGES`。四条阻断**全部接受**,已在同一 PR 内做最小修订。 + +### 阻断 1(接受):未批准数据库不能靠自声明升级为 approved source + +`ADC_surfaceome_reference@0.3.0` 是**派生数据库**,不是原始公开数据源。初稿只记录了 snapshot、SHA-256、builder 路径与该库自己的语义守卫,而测试明确不读外部数据库,因此实际上**没有审计** builder 实现、raw manifest、原始来源清单、license、evidence family 是否真正独立、去重与冲突处理、代表性行回溯、snapshot 可重建性。 + +审核方的判断是对的:**该库自己声明 `membrane_topology_is_independent_surface_localization = false`,不等于它确实遵守这条规则。** 我把「它的语义写得对」当成了「它已被验证」。 + +修订:把 source admission 从本契约剥离,改为依赖项 `SRCADM-01`——`admission_status: pending_separate_admission_pr`、`admission_record_ref: null`(本契约不得代填)、`authorises_extraction_run: false`、`extraction_blocked_by: [SRCADM-01]`。登记该 admission PR 必须逐项审计的 `AUD-01`..`AUD-09`(builder 与版本、raw manifest 对应、来源清单与 release、license 与再分发、family 独立性、去重逻辑、discordance 生成、代表性靶点回溯、可复现构建)。六条自声明守卫一律标 `status: claim_pending_audit`——它们是主张 admission 的理由,不是 admission 已完成的证据。四个 SHA-256 保留但角色降为 `files_pinned_for_integrity_only`。新增 `VAL-E13`:抽取前 `admission_record_ref` 必须指向实际存在的独立 `APPROVE` 记录,为空即不得执行。 + +### 阻断 2(接受):RQ-02 路径计数与 E1-02 自相矛盾 + +初稿写 `ECD-a measured_count = 18`、`ECD-b = 4`,并让测试强制 18 + 4 = 22 eligible。但 `E1-02` 的 6 个靶点是「`RQ-02` 满足但家族数 < 2」——它们必然也命中某条 ECD 路径。**我把「满足 ECD 路径」错等于「最终 eligible」。** + +实测后修订为两个分开的计数:`ECD-a` 路径命中 **30**、其中最终 eligible **18**;`ECD-b` 路径命中 **4**、其中 eligible **4**;两条路径无重叠。并写入分解恒等式:**34 个 RQ-02 阳性 = 22 eligible + 6 `E1-02` + 6 `E1-04` 中 RQ-02 阳性者**。新增 `VAL-E12` 与两条测试:eligible 不得超过路径命中数;`eligible_via_path` 之和必须等于 `E1-01`,而路径命中之和减去重叠必须等于 RQ-02 阳性总数。 + +### 阻断 3(接受):VAL-E05 与 reference-absent 靶点冲突 + +`E1-05` 的 4 个靶点不在参考库中,`source_evidence.tsv` 里本就没有它们的行,而初稿的 `VAL-E05` 要求**每行**都有非空 `source_ids`/`source_releases`/`source_urls`/`licenses`。后果正如审核方所说:合法的 hold 行无法通过验证,或迫使执行者伪造 provenance。 + +修订:provenance 拆成两类。覆盖行须有完整来源 provenance;未覆盖行的 `source_*` 允许为空,但必须有完整**缺失 provenance**——`reference_dataset_id`、`reference_dataset_version`、`reference_snapshot_id`、`target_axis_ref`、`absence_reason`(只能取 `gene_symbol_not_present_in_reference`)、`lookup_at`。新增 `VAL-E05b`/`VAL-E05c`:**禁止伪造 source evidence,禁止把缺失表述为 source-supported**——`provenance_kind = reference_absent` 的行若出现非空 `source_ids` 即为验证失败。输出列 21 → 26,新增 `provenance_kind`、`absence_reason`、`target_axis_ref`、`lookup_at`。 + +### 阻断 4(接受):五条规则没有冻结优先级 + +条件并非天然互斥,而 `VAL-E01` 要求「命中且仅命中一条」。实测本 snapshot 下有 **2 个**靶点同时满足 `E1-03` 与 `E1-02`:`TM4SF1`、`TDGF1`。初稿只验证了预写计数之和等于 41,**没有证明真实条件下的 one-and-only-one**。 + +修订:冻结 `derivation_precedence` = `E1-05` → `E1-04` → `E1-03` → `E1-02` → `E1-01`,并写明理由;登记 `measured_multi_condition_targets`,两个靶点解析到 `E1-03`。新增 `VAL-E11`。测试新增三项:优先级覆盖全部规则且首尾正确;用等价 fixture 逐例证明九种条件组合(含 absent+conflict、conflict+low-family、conflict+no-ECD、no-ECD+low-family)各有唯一结果;重叠靶点必须落在优先级更高的规则、且**不得被计入被压制的那条规则的 `measured_targets`**。 + +**计数未变**:按冻结后的优先级重算仍是 22/6/3/6/4,与初稿一致——因为初稿的计算脚本已隐含同一顺序,只是没把顺序写进契约。 + +### 本轮变异检验 + +12 个变异全部被捕获后精确回滚,与备份 `diff -q` 一致、测试恢复 `OK`:自行填入 admission 记录并放行、把自声明守卫标为已验证、删掉 license 审计项、让 eligible 超过路径命中数、用 eligible 冒充路径命中数、破坏分解恒等式、要求未覆盖行也有 source 字段、允许伪造 source evidence、删掉 `absence_reason` 要求、把 `E1-01` 提到优先级最前、把冲突优先级降到最后、让重叠靶点解析到被压制的规则。 + +### 审核方认可、本轮未改动的部分 + +`ECD-b` 路径(在 GPI 注释可靠、同时有信号肽、零跨膜段与蛋白级 provenance 的前提下)可作为 extracellular topology 路径;跨膜段不再单独满足 `RQ-01`;GPI 路径没被当成普通跨膜蛋白规则;`LAMP1` 的腔内结构域不被当成胞外;缺失与冲突均 DEFER 不 EXCLUDE;`not_surface_target` 仍不可用;T7 与肿瘤定量文件明确禁止;不执行 Level 01;不新增 target/context;仓库不存 evidence/result;`EVGAP-02` 继续未解除。 + +### 审核回写状态 + +审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本节与 `logs/worklog.md`。 diff --git a/docs/pools/evgap_01_surface_localization_extraction.yaml b/docs/pools/evgap_01_surface_localization_extraction.yaml index 1ea5f7b..ce3bdf5 100644 --- a/docs/pools/evgap_01_surface_localization_extraction.yaml +++ b/docs/pools/evgap_01_surface_localization_extraction.yaml @@ -15,65 +15,96 @@ extraction: serves_lock: LOCK-01 status: contract_only_pending_review execution_status: not_authorized_not_executed - # 本契约只授权一次抽取运行;不授权执行 Level 01。 - authorises_extraction_run: true + # 本契约冻结抽取边界,但**尚不授权执行抽取**:所依赖的数据库 snapshot + # 必须先通过独立的 source admission PR 取得自己的 APPROVE 记录。见 + # source_admission_dependency。 + authorises_extraction_run: false authorises_level_01_execution: false + extraction_blocked_by: + - SRCADM-01 # 抽取完成并经结果 PR 接受后,需再开一个 PR 更新 Level 01 输入绑定以解除 EVGAP-01。 requires_followup_binding_pr: true -# 本契约请求把这一个数据库版本纳入已批准来源集合。 -# 治理事实:该数据库此前从未被审核批准,仓库内无任何 APPROVE 记录提及它; -# 已获批的证据抽取(PR #31)的 source_manifest 用的是 ADC_internalization_reference, -# 没有接入本库——这正是已批准层只有跨膜段注释、没有质膜定位证据的原因。 -source_admission_request: +# 依赖的数据库 snapshot。**本契约不批准它。** +# +# 该库是派生数据库,不是原始公开数据源。哈希与它自己的语义声明只能证明 +# 「读到的就是这个版本」,不能证明该版本确实遵守它所声明的规则。 +# 因此纳入已批准来源必须由独立的 source admission PR 完成,本契约只引用其结论。 +source_admission_dependency: + id: SRCADM-01 source_id: adc_surfaceome_reference_v0_3_0 dataset_id: ADC_surfaceome_reference dataset_version: 0.3.0 snapshot_id: 2026-07-29-quant-topology-mm root: external:databases/ADC_surfaceome_reference/processed/v0.3.0 - builder: AssetGenOS/scripts/build_t7_surfaceome_reference.py - builder_version: 0.3.0 - raw_manifest_sha256: 884f419118302ae39c3e50292d03295ff676434868e1061b39ead50f9cc977bb + is_derived_database: true previously_approved: false previously_approved_evidence: 仓库内 logs/chatgpt-review-*.md 无任何一条提及 surfaceome;worklog 仅有一次 2026-08-01 mock 运行提及。 - files: + admission_status: pending_separate_admission_pr + admission_record_ref: null + admission_record_required: true + # 抽取执行前必须存在的独立 APPROVE 记录路径形态;本契约不得代填。 + admission_record_path_pattern: logs/chatgpt-review--adc-surfaceome-reference-v0-3-0-admission-final.md + # 该 admission PR 必须逐项审计以下内容。哈希与自声明不在其列。 + required_audit_items: + - id: AUD-01 + item: builder 实现与版本(AssetGenOS/scripts/build_t7_surfaceome_reference.py@0.3.0) + - id: AUD-02 + item: raw manifest 与 raw_manifest_sha256 884f4191… 的对应关系 + - id: AUD-03 + item: 原始来源清单与各自的 release 版本 + - id: AUD-04 + item: 每个原始来源的 license 与再分发条件 + - id: AUD-05 + item: independent_evidence_family 的独立性是否真实成立,而非同源数据的重复计数 + - id: AUD-06 + item: 来源去重逻辑 + - id: AUD-07 + item: discordance_flags 的生成规则与覆盖范围 + - id: AUD-08 + item: 代表性靶点的逐行来源回溯 + - id: AUD-09 + item: snapshot 是否可由 builder 与 raw manifest 复现 + # 自声明的语义守卫在 admission 中只能作为待验证项,不能作为已验证结论。 + self_declared_guards_are_claims_not_verification: true + self_declared_guards: + - guard: absence_is_negative_evidence + declared_value: false + status: claim_pending_audit + relates_to_repo_rule: 缺失证据一律 DEFER,永不 EXCLUDE + - guard: membrane_topology_is_independent_surface_localization + declared_value: false + status: claim_pending_audit + relates_to_repo_rule: PR #58 第二轮阻断——跨膜段不足以判定细胞外可及 + - guard: generic_membrane_is_surface_confirmation + declared_value: false + status: claim_pending_audit + relates_to_repo_rule: 泛膜注释不等于质膜定位 + - guard: cci_receptor_role_is_surface_confirmation + declared_value: false + status: claim_pending_audit + relates_to_repo_rule: 受体角色推断不等于定位证据 + - guard: tumor_ihc_is_surface_density + declared_value: false + status: claim_pending_audit + relates_to_repo_rule: 表达强度不等于表面密度,属 Level 02 T7 + - guard: excluded_rna_source + declared_value: GSE160572_MM_surfaceome.csv.gz 因其为 RNA FPKM 而被排除 + status: claim_pending_audit + relates_to_repo_rule: RNA 不得当作蛋白层面验证,无例外 + # 抽取执行时仍须校验这些校验和;它们是版本固定手段,不是批准依据。 + files_pinned_for_integrity_only: - path: surfaceome_consensus.tsv sha256: c7baa2ac86e1d173ef555e92036c2841efb5d106c4931bcf71507fbd5cbc1e7b rows: 20797 - role: 每基因共识与字段来源 - path: membrane_topology_evidence.tsv sha256: d2680b961793f8ed83decbc92cdf04d5818f4cdbcb1e241c0ef34c4f06e77eca rows: 4863 - role: 拓扑逐条证据与 provenance - path: source_evidence.tsv sha256: a61222f03f2763af7733793d0bc85e19185515683a8fa1956d1559667cd662ab rows: 41204 - role: 逐来源证据行,供 RQ-03 provenance - path: build_manifest.json sha256: 9872dacd2f30532364576f29ce8c7428a8df9c6bc5abea408472bb3f79069fe2 - role: 构建语义与版本声明 - - # 该库自身的 consensus_semantics 已经编码了本仓库反复要求的守卫。 - # 这是主张纳入它的核心理由:语义不是本契约事后附加的,是它构建时就写死的。 - inherited_semantics_guards: - - guard: absence_is_negative_evidence - value: false - matches_repo_rule: 缺失证据一律 DEFER,永不 EXCLUDE - - guard: membrane_topology_is_independent_surface_localization - value: false - matches_repo_rule: PR #58 第二轮阻断——跨膜段不足以判定细胞外可及 - - guard: generic_membrane_is_surface_confirmation - value: false - matches_repo_rule: 泛膜注释不等于质膜定位 - - guard: cci_receptor_role_is_surface_confirmation - value: false - matches_repo_rule: 受体角色推断不等于定位证据 - - guard: tumor_ihc_is_surface_density - value: false - matches_repo_rule: 表达强度不等于表面密度,属 Level 02 T7 - - guard: excluded_rna_source - value: GSE160572_MM_surfaceome.csv.gz 因其为 RNA FPKM 而被排除 - matches_repo_rule: RNA 不得当作蛋白层面验证,无例外 # 抽取范围:只处理已批准枚举里的 41 个靶点,不新增任何靶点。 scope: @@ -155,11 +186,14 @@ rq_01_plasma_membrane_localization: # RQ-02:细胞外结构域。两条合格路径,任一成立即满足。 rq_02_extracellular_domain: + # 两个计数必须分开:满足某条路径 != 最终 eligible。 + # 满足 RQ-02 的靶点还可能因独立家族数不足或定位证据冲突而落 hold。 paths: - path_id: ECD-a condition: uniprot_ecd_meets_min_length = true applies_to: 有跨膜段且带 TOPO_DOM Extracellular 注释的蛋白 - measured_count: 18 + measured_path_satisfied_count: 30 + eligible_via_path_count: 18 # 必需的第二条路径。UniProt 的 extracellular domain 字段由跨膜蛋白的 # TOPO_DOM 推导,GPI 锚定蛋白零跨膜段、无 TOPO_DOM,故该字段一律为 false。 # 若只用 ECD-a,CEACAM5、MSLN、FOLR1、MELTF 会因数据表示假象落入 hold, @@ -170,28 +204,83 @@ rq_02_extracellular_domain: rationale_note: >- 这是修正数据表示假象,不是放宽标准:GPI 锚定加信号肽加零跨膜段在结构上 即意味着全长成熟蛋白位于细胞外侧,其胞外长度等于成熟蛋白长度。 - measured_count: 4 + measured_path_satisfied_count: 4 + eligible_via_path_count: 4 measured_targets: - CEACAM5 - FOLR1 - MELTF - MSLN + paths_are_mutually_exclusive_in_this_snapshot: true + measured_path_overlap: 0 + # RQ-02 阳性总数的分解恒等式。执行结果必须满足它。 + rq_02_positive_decomposition: + total: 34 + final_eligible: 22 + hold_insufficient_localization_families: 6 + hold_discordance: 6 + identity: "|RQ-02 positive| = |E1-01| + |E1-02| + |E1-04 中 RQ-02 阳性者|" luminal_domain_is_not_extracellular: true luminal_domain_note: >- 溶酶体/内体膜蛋白的大结构域朝向腔内而非胞外。LAMP1 即属此类: 有跨膜段与信号肽,但无胞外 TOPO_DOM,故两条路径都不满足,落 hold。 这正是审核方要求的「细胞器膜定位必须 DEFER」。 -# RQ-03:蛋白层面 provenance。 +# RQ-03:蛋白层面 provenance。分两类,不得混用。 rq_03_protein_level_provenance: - requires_rows_in: source_evidence.tsv - required_fields: - - source_id - - source_release - - evidence_family - - source_url - - license rna_derived_rows_admissible: false + # 库中覆盖的靶点:必须有完整的来源 provenance。 + covered_rows: + requires_rows_in: source_evidence.tsv + required_fields: + - source_id + - source_release + - evidence_family + - source_url + - license + # 库中未覆盖的靶点(E1-05):source_evidence.tsv 里本就没有它们的行, + # 因此不得要求来源 provenance——否则合法的 hold 行无法通过验证, + # 或迫使执行者伪造 provenance。改为要求「缺失 provenance」。 + absent_rows: + requires_rows_in: null + source_provenance_fields_may_be_empty: true + fabricating_source_evidence_forbidden: true + required_absence_fields: + - reference_dataset_id + - reference_dataset_version + - reference_snapshot_id + - target_axis_ref + - absence_reason + - lookup_at + absence_reason_allowed_values: + - gene_symbol_not_present_in_reference + never_present_absence_as_source_supported: true + +# 规则求值优先级。五条规则的条件并非天然互斥——同一靶点可能同时满足 +# 「两条 ECD 路径都不满足」与「独立家族数 < 2」。实测本 snapshot 下有 2 个 +# 这样的靶点(TM4SF1、TDGF1)。因此必须冻结优先级,按此顺序取第一个命中者, +# 每个靶点恰好命中一条规则。 +derivation_precedence: + - E1-05 + - E1-04 + - E1-03 + - E1-02 + - E1-01 +precedence_rationale: >- + 先判「不在库中」,因为无数据时其余条件都无法求值;再判定位证据冲突, + 因为冲突使后续判据不可信;再判细胞外结构域,再判独立家族数; + 全部通过才 eligible。 +measured_multi_condition_targets: + - gene_symbol: TM4SF1 + conditions_matched: + - E1-03 + - E1-02 + resolved_to: E1-03 + - gene_symbol: TDGF1 + conditions_matched: + - E1-03 + - E1-02 + resolved_to: E1-03 # 确定性映射。五条规则覆盖全部 41 个靶点,无自由裁量,零排除。 derivation_rules: @@ -318,6 +407,10 @@ output_schema: - snapshot_id - row_checksum - extracted_at + - provenance_kind + - absence_reason + - target_axis_ref + - lookup_at output_validation: - id: VAL-E01 @@ -329,7 +422,19 @@ output_validation: - id: VAL-E04 rule: 每个 eligible 行必须记录 rq_02_path,取值限于 ECD-a 或 ECD-b。 - id: VAL-E05 - rule: 每行必须有非空 source_ids、source_releases、source_urls、licenses,缺失即该行降为 hold。 + rule: >- + provenance_kind = source_supported 的行必须有非空 source_ids、source_releases、 + source_urls、licenses;缺失即该行降为 hold。 + - id: VAL-E05b + rule: >- + provenance_kind = reference_absent 的行(即 E1-05)允许 source_* 字段为空, + 但必须有完整的缺失 provenance:reference_dataset_id、reference_dataset_version、 + reference_snapshot_id、target_axis_ref、absence_reason、lookup_at。 + absence_reason 只能取 gene_symbol_not_present_in_reference。 + - id: VAL-E05c + rule: >- + 禁止为未覆盖靶点伪造 source evidence,也禁止把缺失表述为 source-supported: + provenance_kind = reference_absent 的行若出现非空 source_ids 即为验证失败。 - id: VAL-E06 rule: 四个输入文件与靶点轴文件的 SHA-256 必须与本契约记录一致,不一致即中止执行。 - id: VAL-E07 @@ -340,8 +445,24 @@ output_validation: rule: mandatory_findings 的三条必须原样出现在结果报告中。 - id: VAL-E10 rule: 每个产物文件必须逐文件记录 SHA-256 并写入结果 PR 的 handoff。 + - id: VAL-E11 + rule: >- + 规则求值必须按 derivation_precedence 顺序取第一个命中者;每个靶点恰好命中 + 一条规则。同时满足多条条件的靶点必须落在优先级更高的那条,并在结果中 + 记录其余被压制的条件。 + - id: VAL-E12 + rule: >- + RQ-02 阳性靶点数必须满足 rq_02_positive_decomposition 的恒等式; + 每条路径必须同时报出 path-satisfied 与 eligible-via-path 两个计数, + 不得只报其一或用后者冒充前者。 + - id: VAL-E13 + rule: >- + 抽取执行前,source_admission_dependency.admission_record_ref 必须指向一份 + 实际存在的独立 APPROVE 记录;为空即不得执行。 not_authorised: + - 在 SRCADM-01 取得独立 APPROVE 记录前执行抽取 + - 把 ADC_surfaceome_reference 纳入已批准来源(本契约只引用,不批准) - 执行 Level 01 - 评估 T7 或任何肿瘤细胞表面可得性 - 新增任何靶点或 clinical context diff --git a/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md b/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md index 96e0584..7632438 100644 --- a/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md +++ b/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md @@ -4,7 +4,7 @@ - 前置工作包:PR #57(Level 01 判据定义)、PR #58(输入绑定与缺口登记),均已 `APPROVE` 并合并 - 机器可读绑定:[`../pools/evgap_01_surface_localization_extraction.yaml`](../pools/evgap_01_surface_localization_extraction.yaml),由 `tests/test_evgap_01_surface_localization.py` 校验 - 当前状态:**contract-only,未执行,等待 ChatGPT 审核** -- 授权范围:**一次证据抽取运行。不授权执行 Level 01。** +- 授权范围:**只冻结抽取边界。抽取本身尚未获授权**——所依赖的数据库 snapshot 必须先通过独立的 source admission PR 取得自己的 `APPROVE`(`SRCADM-01`)。也不授权执行 Level 01。 ## 目的 @@ -12,30 +12,46 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracellular`/`localization`/`signal peptide`/`GPI` 关键词命中数**均为 0**,只有 `transmembrane_segment_count`,而跨膜拓扑不足以判定细胞外可及。因此 41 个靶点全部落 `possible_surface_target`,`eligible = 0`,Level 01 无法录入任何 pair。 -本契约冻结一次抽取运行的范围、来源、字段白名单、判据映射与输出验证。**不执行抽取,不执行 Level 01。** +本契约冻结一次抽取运行的范围、字段白名单、判据映射与输出验证。**不执行抽取,不执行 Level 01,也不批准所依赖的数据库。** -## 一、需要审核方裁决的治理事实:本次请求纳入一个从未被批准的数据库 +## 一、所依赖的数据库不由本契约批准(`SRCADM-01`) -所需数据**已经存在于本地**:`DATA/1.Databases/ADC_surfaceome_reference/processed/v0.3.0`。但必须先讲清楚它的治理状态: +所需数据**已经存在于本地**:`DATA/1.Databases/ADC_surfaceome_reference/processed/v0.3.0`。它的治理状态是: -> **`ADC_surfaceome_reference` 从未被审核批准过。** 仓库内 `logs/chatgpt-review-*.md` 没有任何一条提及 surfaceome;`logs/worklog.md` 里唯一的提及是 2026-08-01 一次 mock 运行。已获批的证据抽取(PR #31)在其 `source_manifest.json` 中声明的来源是 `ADC_internalization_reference`,**没有接入本库**——这正是已批准层只有跨膜段注释、没有质膜定位证据的直接原因。 +> **该数据库从未被审核批准。** 仓库内 `logs/chatgpt-review-*.md` 没有任何一条提及 surfaceome;`logs/worklog.md` 里唯一的提及是 2026-08-01 一次 mock 运行。已获批的证据抽取(PR #31)在其 `source_manifest.json` 中声明的来源是 `ADC_internalization_reference`,**没有接入本库**——这正是已批准层只有跨膜段注释、没有质膜定位证据的直接原因。 -因此本 PR 请求把**这一个版本**纳入已批准来源集合:`dataset_version 0.3.0`、`snapshot_id 2026-07-29-quant-topology-mm`、`raw_manifest_sha256 884f4191…`,四个文件逐一记录 SHA-256(`surfaceome_consensus.tsv` 20,797 行、`membrane_topology_evidence.tsv` 4,863 行、`source_evidence.tsv` 41,204 行、`build_manifest.json`)。执行前必须逐个校验,任一不一致即中止(`VAL-E06`)。 +**初稿试图在本契约内直接把它升级为 approved source,那是错的,已按审核裁决更正。** 它是**派生数据库**,不是原始公开数据源。哈希与它自己的语义声明只能证明「读到的就是这个版本」,**不能证明该版本确实遵守它所声明的规则**。 -### 主张纳入它的核心理由:守卫是它构建时就写死的,不是本契约事后附加的 +因此纳入已批准来源改由**独立的 source admission PR** 完成,本契约只引用其结论: -该库自身的 `build_manifest.json` → `consensus_semantics` 已经编码了本仓库反复要求的语义: +- `admission_status: pending_separate_admission_pr` +- `admission_record_ref: null`(本契约不得代填) +- `authorises_extraction_run: false`,`extraction_blocked_by: [SRCADM-01]` +- `VAL-E13`:抽取执行前 `admission_record_ref` 必须指向一份实际存在的独立 `APPROVE` 记录,为空即不得执行 -| 该库的声明 | 对应仓库规则 | +### 那个 admission PR 必须逐项审计的内容 + +哈希与自声明**不在其列**。 + +| ID | 审计项 | |---|---| -| `absence_is_negative_evidence: false` | 缺失证据一律 DEFER,永不 EXCLUDE | -| `membrane_topology_is_independent_surface_localization: false` | **PR #58 第二轮阻断本身**——跨膜段不足以判定细胞外可及 | -| `generic_membrane_is_surface_confirmation: false` | 泛膜注释不等于质膜定位 | -| `cci_receptor_role_is_surface_confirmation: false` | 受体角色推断不等于定位证据 | -| `tumor_ihc_is_surface_density: false` | 表达强度不等于表面密度(属 Level 02 T7) | -| 排除 `GSE160572_MM_surfaceome.csv.gz`,理由是「processed values are RNA FPKM, not direct surface-protein measurements」 | RNA 不得当作蛋白层面验证,无例外 | +| `AUD-01` | builder 实现与版本(`build_t7_surfaceome_reference.py@0.3.0`) | +| `AUD-02` | raw manifest 与 `raw_manifest_sha256 884f4191…` 的对应关系 | +| `AUD-03` | 原始来源清单与各自 release 版本 | +| `AUD-04` | 每个原始来源的 license 与再分发条件 | +| `AUD-05` | `independent_evidence_family` 的独立性是否真实成立,而非同源数据的重复计数 | +| `AUD-06` | 来源去重逻辑 | +| `AUD-07` | `discordance_flags` 的生成规则与覆盖范围 | +| `AUD-08` | 代表性靶点的逐行来源回溯 | +| `AUD-09` | snapshot 是否可由 builder 与 raw manifest 复现 | + +### 该库的自声明守卫:待验证项,不是已验证结论 + +以下六条在本契约中一律标记 `status: claim_pending_audit`。它们是主张 admission 的**理由**,不是 admission 已完成的**证据**: + +`membrane_topology_is_independent_surface_localization: false`(正是 PR #58 第二轮阻断本身)、`absence_is_negative_evidence: false`、`generic_membrane_is_surface_confirmation: false`、`cci_receptor_role_is_surface_confirmation: false`、`tumor_ihc_is_surface_density: false`、以及因 RNA FPKM 排除 `GSE160572_MM_surfaceome.csv.gz`。 -该库还自设 `full_t7_gate_confidence_cap: 0.55`,并在 note 中明确它「does not establish a general malignant-cell positive fraction, isoform usage, calibrated treatment stability, or ADC accessibility」——即它自己就把 Level 02 的边界划开了。 +四个文件的 SHA-256 仍记录,但角色降为 `files_pinned_for_integrity_only`——版本固定手段,不是批准依据。 ## 二、抽取范围 @@ -66,10 +82,18 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracell ### `RQ-02` 细胞外结构域:两条合格路径 -| 路径 | 条件 | 适用 | 实测 | +**两个计数必须分开:满足某条路径 ≠ 最终 eligible。** 满足 `RQ-02` 的靶点还可能因独立家族数不足或定位证据冲突而落 hold。初稿把二者混为一谈,已按审核裁决更正。 + +| 路径 | 条件 | 路径命中 | 其中最终 eligible | |---|---|---|---| -| `ECD-a` | `uniprot_ecd_meets_min_length = true` | 有跨膜段且带 TOPO_DOM Extracellular 注释 | **18** | -| `ECD-b` | `uniprot_gpi_anchor = true` 且 `uniprot_signal_peptide = true` 且 `uniprot_transmembrane_segment_count = 0` | GPI 锚定、带信号肽、零跨膜段 | **4** | +| `ECD-a` | `uniprot_ecd_meets_min_length = true` | **30** | **18** | +| `ECD-b` | `uniprot_gpi_anchor` 且 `uniprot_signal_peptide` 且 `transmembrane_segment_count = 0` | **4** | **4** | + +两条路径在本 snapshot 下无重叠(`measured_path_overlap: 0`)。`RQ-02` 阳性总数的分解恒等式: + +> **34 个 RQ-02 阳性 = 22 最终 eligible + 6 独立家族数不足(`E1-02`)+ 6 定位证据冲突(`E1-04` 中 RQ-02 阳性者)** + +`VAL-E12` 要求每条路径同时报出两个计数,不得只报其一或用后者冒充前者。 **`ECD-b` 是必需的,理由是修正数据表示假象而不是放宽标准。** UniProt 的 extracellular domain 字段由跨膜蛋白的 TOPO_DOM 推导;GPI 锚定蛋白零跨膜段、没有 TOPO_DOM,该字段一律读成 `false`。若只用 `ECD-a`,`CEACAM5`、`MSLN`、`FOLR1`、`MELTF` 会因此落 hold——而四者在本库中都是 `confirmed_surface`,都带信号肽与 GPI 锚,成熟蛋白整体位于细胞外侧。GPI 锚 + 信号肽 + 零跨膜段在结构上即意味着全长成熟蛋白朝向胞外,其胞外长度等于成熟蛋白长度。 @@ -77,9 +101,27 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracell ### `RQ-03` 蛋白层面 provenance -要求 `source_evidence.tsv` 中存在对应行,且 `source_id`、`source_release`、`evidence_family`、`source_url`、`license` 均非空。RNA 来源行不可采纳。 +分两类,不得混用。RNA 来源行一概不可采纳。 + +**库中覆盖的靶点**:要求 `source_evidence.tsv` 有对应行,且 `source_id`、`source_release`、`evidence_family`、`source_url`、`license` 均非空。 -## 五、确定性映射:五条规则覆盖 41 个靶点,零排除 +**库中未覆盖的靶点(`E1-05`)**:`source_evidence.tsv` 里本就没有它们的行。**初稿要求所有行都有来源 provenance,那会让合法的 `E1-05` hold 行无法通过验证,或迫使执行者伪造 provenance**——已按审核裁决更正。改为要求「缺失 provenance」:`reference_dataset_id`、`reference_dataset_version`、`reference_snapshot_id`、`target_axis_ref`、`absence_reason`(只能取 `gene_symbol_not_present_in_reference`)、`lookup_at`。 + +`source_*` 字段允许为空,但**禁止伪造 source evidence**,也**禁止把缺失表述为 source-supported**:`provenance_kind = reference_absent` 的行若出现非空 `source_ids` 即为验证失败(`VAL-E05b`/`VAL-E05c`)。 + +## 五、规则求值优先级(必须冻结) + +五条规则的条件**并非天然互斥**——同一靶点可能同时满足「两条 ECD 路径都不满足」与「独立家族数 < 2」。实测本 snapshot 下有 **2 个**这样的靶点:`TM4SF1` 与 `TDGF1`。初稿没有定义优先级,`VAL-E01` 的「命中且仅命中一条」无从保证,已按审核裁决更正。 + +冻结顺序,取第一个命中者: + +> `E1-05` 不在库中 → `E1-04` 定位证据冲突 → `E1-03` 无细胞外结构域 → `E1-02` 独立家族数不足 → `E1-01` eligible + +理由:先判「不在库中」,因为无数据时其余条件都无法求值;再判冲突,因为冲突使后续判据不可信;再判细胞外结构域,再判独立家族数;全部通过才 eligible。 + +`TM4SF1` 与 `TDGF1` 同时命中 `E1-03` 与 `E1-02`,按优先级解析到 **`E1-03`**,并须在结果中记录被压制的条件(`VAL-E11`)。测试用等价 fixture 覆盖了每一种重叠组合,逐例证明恰好命中一条。 + +## 六、确定性映射:五条规则覆盖 41 个靶点,零排除 | ID | 条件 | outcome | disposition | 数量 | |---|---|---|---|---| @@ -98,42 +140,46 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracell `not_surface_target` 与 `identity_unresolved` **仍然不可用**:该库明确 `absence_is_negative_evidence: false`,不产出「该靶点不是表面蛋白」这类阳性否定断言;`consensus_class = no_surface_support` 只表示无支持证据,不等于已证伪。**本次抽取不得排除任何靶点。** -## 六、抽取后的预期结果,逐项可核对 +## 七、抽取后的预期结果,逐项可核对 | 量 | 值 | |---|---| | 靶点总数 | 41 | -| eligible | **22**(`ECD-a` 18 + `ECD-b` 4) | +| eligible | **22**(经 `ECD-a` 18 + 经 `ECD-b` 4) | | hold | **19**(6 + 3 + 6 + 4) | | killed | **0** | **执行结果必须逐项等于上表,任一项不符即视为偏离本契约。** -## 七、必须写进结果报告的三条发现 +## 八、必须写进结果报告的三条发现 - **`MF-01`:`GUCY2C` 落 hold。** 它在本参考库中只有 `curated_knowledge` 一个独立证据家族,未达两家族门槛,`consensus_class` 是 `supported_surface` 而非 `confirmed_surface`。**这与此前多模型共识把 GUCY2C 列为首选、以及被隔离运行的 Tier A 选择相反。** 结果报告必须原样写出,不得因与既有偏好冲突而弱化。 - **`MF-02`:`eligible` 只表示身份与拓扑层面存在有合理依据的细胞外可及蛋白形式**,不表示该靶点在 CRC 肿瘤细胞表面可得。后者是 Level 02 的 T7,本次未评估。 - **`MF-03`:零排除。** 41 个靶点中没有任何一个被判为 `not_surface_target`。hold 不是淘汰,是待证据。 -## 八、输出与验证 +## 九、输出与验证 + +输出为每靶点一行、**26 列**(见 YAML `output_schema`),必含 `rule_id`、`rq_02_path`、`evaluation_status`、`provenance_kind`、`absence_reason`、`target_axis_ref`、`lookup_at` 与 `row_checksum`。 -输出为每靶点一行、21 列(见 YAML `output_schema`),必含 `rule_id`、`rq_02_path`、`evaluation_status`、provenance 与 `row_checksum`。十条验证规则 `VAL-E01`..`VAL-E10` 见 YAML,要点:41 行且每行命中且仅命中一条规则;计数等于第六节;不得出现 `not_surface_target` 或 `identity_unresolved`;每个 eligible 必记 `rq_02_path`;provenance 缺失即降为 hold;输入 SHA-256 不一致即中止;不得读取被禁文件或字段;输出中不得出现任何 Gate 分数、Gate 状态、T7 判定或肿瘤表面定量;三条 `mandatory_findings` 必须原样出现;每个产物文件逐文件记录 SHA-256。 +**15 条**验证规则 `VAL-E01`..`VAL-E13` 见 YAML。要点:41 行且每行按优先级命中且仅命中一条规则(`VAL-E01`/`VAL-E11`);计数等于第七节且 `RQ-02` 分解恒等式成立(`VAL-E02`/`VAL-E12`);不得出现 `not_surface_target` 或 `identity_unresolved`;每个 eligible 必记 `rq_02_path`;覆盖行须有完整来源 provenance、未覆盖行须有完整缺失 provenance 且禁止伪造(`VAL-E05`/`E05b`/`E05c`);输入 SHA-256 不一致即中止;不得读取被禁文件或字段;输出中不得出现任何 Gate 分数、Gate 状态、T7 判定或肿瘤表面定量;三条 `mandatory_findings` 必须原样出现;每个产物文件逐文件记录 SHA-256;**抽取前 `admission_record_ref` 必须指向实际存在的独立 `APPROVE` 记录,为空即不得执行(`VAL-E13`)**。 -## 九、本契约授权与不授权 +## 十、本契约授权与不授权 -**授权:** 把 `ADC_surfaceome_reference@0.3.0`(指定 snapshot 与校验和)纳入已批准来源;按本契约执行**一次**抽取运行。 +**授权:** 冻结本次抽取的范围、字段白名单、判据映射、求值优先级与输出验证。 -**不授权:** 执行 Level 01;评估 T7 或任何肿瘤细胞表面可得性;新增任何靶点或 clinical context;任何筛选排序、Tier 划分、资产推荐或实验建议;任何 Gate 执行或评分;读取被禁文件或字段;把被隔离运行(PR #53、#54)的任何产物引入。 +**不授权:** 在 `SRCADM-01` 取得独立 `APPROVE` 记录前执行抽取;把 `ADC_surfaceome_reference` 纳入已批准来源(本契约只引用,不批准);执行 Level 01;评估 T7 或任何肿瘤细胞表面可得性;新增任何靶点或 clinical context;任何筛选排序、Tier 划分、资产推荐或实验建议;任何 Gate 执行或评分;读取被禁文件或字段;把被隔离运行(PR #53、#54)的任何产物引入。 -## 十、后续顺序 +## 十一、后续顺序 1. 本契约获 `APPROVE`。 -2. 执行抽取 → 结果 PR → `APPROVE`。 -3. **另开一个 PR** 更新 `adc_pool_level_01_input_binding.yaml`,把抽取产物绑为 `LOCK-01` 的来源并解除 `EVGAP-01`。该 PR 不在本契约授权范围内。 -4. `EVGAP-02`(`LOCK-03` 的源级 CRC linkage 证据)仍未解除,需其独立契约。**两个缺口都解除后,Level 01 才能执行。** +2. **`SRCADM-01` 独立 source admission PR** 审计 `AUD-01`..`AUD-09` → `APPROVE`,并在 `logs/` 留下记录。 +3. 把 `admission_record_ref` 指向该记录,执行抽取 → 结果 PR → `APPROVE`。 +4. **另开一个 PR** 更新 `adc_pool_level_01_input_binding.yaml`,把抽取产物绑为 `LOCK-01` 的来源并解除 `EVGAP-01`。该 PR 不在本契约授权范围内。 +5. `EVGAP-02`(`LOCK-03` 的源级 CRC linkage 证据)仍未解除,需其独立契约。**两个缺口都解除后,Level 01 才能执行。** -## 十一、当前阻断 +## 十二、当前阻断 - 本契约获 ChatGPT `APPROVE` 前,不得执行抽取。 +- **本契约获 `APPROVE` 后仍不得执行抽取**,直到 `SRCADM-01` 取得独立 `APPROVE` 记录。 - 抽取完成也**不**解除 `EVGAP-02`,Level 01 仍不可执行。 - 本仓库不得写入证据、候选、快照、cache、result 或 weights。 diff --git a/logs/worklog.md b/logs/worklog.md index 6ff1b2d..2b5ed08 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2526,3 +2526,21 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Noticed, not fixed: `requirements.txt` 注释仍写「207 tests」,实测 298。属无关改动,只记录。 - Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。 - Next: 推送并创建 PR 送审;请审核方裁决是否接受把 `ADC_surfaceome_reference@0.3.0` 纳入已批准来源,以及 `ECD-b` 路径是否成立。 + +## 2026-08-05T00:20:00-04:00 — PR #59 第一轮审核裁决与修订(四条阻断全部接受) + +- Review: ChatGPT 对 PR #59(HEAD `570562c`,可合并、CI 成功)返回 `REQUEST_CHANGES`,四条阻断。**全部接受**,同一 PR 内最小修订。 +- Finding 1 accepted: **未批准的派生数据库不能靠自声明 + 哈希升级为 approved source。** 初稿只记录 snapshot/SHA-256/builder 路径/该库自己的语义守卫,而测试明确不读外部数据库,故实际未审计 builder 实现、raw manifest、原始来源清单、license、evidence family 独立性、去重与冲突处理、代表性行回溯、snapshot 可重建性。**执行者错误:把「它的语义写得对」当成了「它已被验证」。** 该库声明 `membrane_topology_is_independent_surface_localization = false` 不等于它确实遵守该规则。 +- Fix 1: source admission 从本契约剥离为依赖项 `SRCADM-01`——`admission_status: pending_separate_admission_pr`、`admission_record_ref: null`(不得代填)、`authorises_extraction_run: false`、`extraction_blocked_by: [SRCADM-01]`;登记 `AUD-01`..`AUD-09` 九项必审内容;六条自声明守卫一律标 `status: claim_pending_audit`;四个 SHA-256 角色降为 `files_pinned_for_integrity_only`;新增 `VAL-E13`(抽取前 `admission_record_ref` 必须指向实际存在的独立 `APPROVE` 记录)。 +- Finding 2 accepted: **RQ-02 路径计数与 `E1-02` 自相矛盾。** 初稿写 `ECD-a=18`/`ECD-b=4` 并强制 18+4=22 eligible,但 `E1-02` 的 6 个靶点是「RQ-02 满足但家族数<2」,必然也命中某条 ECD 路径。**执行者把「满足 ECD 路径」错等于「最终 eligible」。** +- Fix 2: 实测后拆为两个计数——`ECD-a` 路径命中 **30**/其中 eligible **18**;`ECD-b` 命中 **4**/eligible **4**;两路径无重叠。写入分解恒等式 **34 RQ-02 阳性 = 22 eligible + 6 `E1-02` + 6 `E1-04` 中 RQ-02 阳性者**。新增 `VAL-E12` 与两条测试(eligible 不得超过路径命中;`eligible_via_path` 之和等于 `E1-01`,路径命中之和减重叠等于 RQ-02 阳性总数)。 +- Finding 3 accepted: **`VAL-E05` 与 reference-absent 靶点冲突。** `E1-05` 的 4 个靶点在 `source_evidence.tsv` 里本就没有行,而初稿要求每行都有非空 source provenance——会让合法 hold 行无法通过验证,或迫使执行者伪造 provenance。 +- Fix 3: provenance 拆两类。覆盖行须完整来源 provenance;未覆盖行 `source_*` 允许为空但须完整**缺失 provenance**(`reference_dataset_id`/`reference_dataset_version`/`reference_snapshot_id`/`target_axis_ref`/`absence_reason` 只能取 `gene_symbol_not_present_in_reference`/`lookup_at`)。新增 `VAL-E05b`/`VAL-E05c`:禁止伪造 source evidence,禁止把缺失表述为 source-supported,`provenance_kind = reference_absent` 行出现非空 `source_ids` 即验证失败。输出列 21 → 26。 +- Finding 4 accepted: **五条规则没有冻结优先级**,而 `VAL-E01` 要求命中且仅命中一条。实测本 snapshot 下 **2 个**靶点同时满足 `E1-03` 与 `E1-02`(`TM4SF1`、`TDGF1`)。初稿只验证预写计数之和等于 41,未证明真实条件下的 one-and-only-one。 +- Fix 4: 冻结 `derivation_precedence` = `E1-05` → `E1-04` → `E1-03` → `E1-02` → `E1-01` 并写明理由;登记 `measured_multi_condition_targets`(两靶点解析到 `E1-03`);新增 `VAL-E11`;新增三条测试——优先级覆盖全部规则且首尾正确、用等价 fixture 逐例证明九种条件组合(含 absent+conflict、conflict+low-family、conflict+no-ECD、no-ECD+low-family)各有唯一结果、重叠靶点必须落在优先级更高规则且不得计入被压制规则的 `measured_targets`。 +- Counts unchanged: 按冻结后优先级重算仍为 22/6/3/6/4,与初稿一致——初稿的计算脚本已隐含同一顺序,只是没把顺序写进契约。 +- Validation: `Ran 306 tests` 全部通过(`main` 基线 283 + 新增 23,由 15 增至 23);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 +- Mutation-tested (本轮 12 个,全部 `FAILED` 后精确回滚,与备份 `diff -q` 一致、恢复 `OK`): 自行填入 admission 记录并放行、自声明守卫标为已验证、删掉 license 审计项、eligible 超过路径命中数、用 eligible 冒充路径命中、破坏分解恒等式、要求未覆盖行也有 source 字段、允许伪造 source evidence、删掉 `absence_reason` 要求、`E1-01` 提到优先级最前、冲突优先级降到最后、重叠靶点解析到被压制规则。 +- Accepted by reviewer, unchanged: `ECD-b` 路径可作为 extracellular topology 路径(前提是 GPI 注释可靠、有信号肽、零跨膜段、蛋白级 provenance);跨膜段不再单独满足 `RQ-01`;GPI 路径未被当成普通跨膜蛋白规则;`LAMP1` 腔内结构域不被当成胞外;缺失与冲突均 DEFER 不 EXCLUDE;`not_surface_target` 仍不可用;T7 与肿瘤定量文件明确禁止;不执行 Level 01;不新增 target/context;仓库不存 evidence/result;`EVGAP-02` 继续未解除。 +- Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十节。 +- Next: 推送同一 PR 并同步 PR 描述请求复审。此后欠着的工作变为三项:`SRCADM-01` 数据库 admission PR、`EVGAP-02` 契约、五份批准记录。 diff --git a/tests/test_evgap_01_surface_localization.py b/tests/test_evgap_01_surface_localization.py index abf419c..4e481ca 100644 --- a/tests/test_evgap_01_surface_localization.py +++ b/tests/test_evgap_01_surface_localization.py @@ -1,10 +1,10 @@ """Verify docs/pools/evgap_01_surface_localization_extraction.yaml. -The extraction contract discharges EVGAP-01 by admitting one pinned database -version as an approved source and freezing how its fields map onto LOCK-01's -RQ-01/RQ-02/RQ-03. These tests check internal consistency and agreement with the -merged Level 01 contracts. They never read the external database: it lives -outside the repository, so the recorded checksums are audit metadata. +The extraction contract freezes how one pinned database version would map onto +LOCK-01's RQ-01/RQ-02/RQ-03. It does not admit that database: admission needs its +own review, tracked as SRCADM-01. These tests check internal consistency and +agreement with the merged Level 01 contracts. They never read the external +database, so the recorded checksums are integrity pins, not verification. """ from __future__ import annotations @@ -56,34 +56,66 @@ def test_contract_targets_the_registered_gap(self) -> None: self.assertIn("EVGAP-01", gaps) self.assertEqual(gaps["EVGAP-01"]["blocks"], "LOCK-01") self.assertEqual(head["execution_status"], "not_authorized_not_executed") - self.assertIs(head["authorises_extraction_run"], True) + # The extraction is frozen but not yet authorised: SRCADM-01 blocks it. + self.assertIs(head["authorises_extraction_run"], False) self.assertIs(head["authorises_level_01_execution"], False) self.assertIs(head["requires_followup_binding_pr"], True) - def test_source_admission_is_declared_as_not_previously_approved(self) -> None: - request = self.doc["source_admission_request"] - self.assertIs(request["previously_approved"], False) - self.assertTrue(request["previously_approved_evidence"].strip()) - self.assertTrue(request["root"].startswith("external:")) - self.assertEqual(request["dataset_version"], "0.3.0") - for item in request["files"]: + def test_database_admission_is_deferred_to_its_own_approval(self) -> None: + """Blocker 1: a derived database cannot be admitted by self-declaration.""" + + dep = self.doc["source_admission_dependency"] + self.assertEqual(dep["id"], "SRCADM-01") + self.assertIs(dep["is_derived_database"], True) + self.assertIs(dep["previously_approved"], False) + self.assertEqual(dep["admission_status"], "pending_separate_admission_pr") + self.assertIsNone(dep["admission_record_ref"]) + self.assertIs(dep["admission_record_required"], True) + # The extraction must be blocked while the admission is pending. + head = self.doc["extraction"] + self.assertIs(head["authorises_extraction_run"], False) + self.assertIn("SRCADM-01", head["extraction_blocked_by"]) + # Admitting the database is explicitly outside this contract. + not_authorised = " ".join(self.doc["not_authorised"]) + self.assertIn("SRCADM-01", not_authorised) + self.assertIn("纳入已批准来源", not_authorised) + + def test_admission_audit_covers_more_than_hashes(self) -> None: + dep = self.doc["source_admission_dependency"] + items = {a["id"]: a["item"] for a in dep["required_audit_items"]} + self.assertGreaterEqual(len(items), 9) + joined = " ".join(items.values()) + for topic in ("builder", "raw manifest", "license", "去重", + "discordance_flags", "回溯", "复现"): + with self.subTest(topic=topic): + self.assertIn(topic, joined) + # Independence of the evidence families must be audited, not assumed. + self.assertIn("独立性", joined) + + def test_self_declared_guards_are_claims_not_verification(self) -> None: + dep = self.doc["source_admission_dependency"] + self.assertIs(dep["self_declared_guards_are_claims_not_verification"], True) + guards = dep["self_declared_guards"] + self.assertGreaterEqual(len(guards), 6) + for guard in guards: + with self.subTest(guard=guard["guard"]): + self.assertEqual(guard["status"], "claim_pending_audit") + self.assertTrue(guard["relates_to_repo_rule"].strip()) + names = {g["guard"] for g in guards} + for required in ("absence_is_negative_evidence", + "membrane_topology_is_independent_surface_localization"): + self.assertIn(required, names) + + def test_checksums_are_integrity_pins_not_approval(self) -> None: + dep = self.doc["source_admission_dependency"] + pinned = dep["files_pinned_for_integrity_only"] + self.assertEqual(len(pinned), 4) + for item in pinned: 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)) - def test_inherited_guards_cover_the_repository_rules(self) -> None: - guards = {g["guard"]: g for g in - self.doc["source_admission_request"]["inherited_semantics_guards"]} - # The two that previous review rounds turned on must be present and false. - for name in ("absence_is_negative_evidence", - "membrane_topology_is_independent_surface_localization"): - with self.subTest(guard=name): - self.assertIn(name, guards) - self.assertIs(guards[name]["value"], False) - self.assertTrue(guards[name]["matches_repo_rule"].strip()) - self.assertIn("excluded_rna_source", guards) - def test_scope_is_the_approved_target_axis_only(self) -> None: scope = self.doc["scope"] self.assertIs(scope["new_targets_allowed"], False) @@ -135,24 +167,91 @@ def test_rq_02_has_both_paths_and_excludes_luminal_domains(self) -> None: rq2 = self.doc["rq_02_extracellular_domain"] paths = {p["path_id"]: p for p in rq2["paths"]} self.assertEqual(set(paths), {"ECD-a", "ECD-b"}) - # The GPI path exists to correct a representation artefact, and must say so. self.assertTrue(paths["ECD-b"]["rationale_note"].strip()) self.assertEqual(len(paths["ECD-b"]["measured_targets"]), - paths["ECD-b"]["measured_count"]) + paths["ECD-b"]["eligible_via_path_count"]) self.assertIs(rq2["luminal_domain_is_not_extracellular"], True) self.assertIn("LAMP1", rq2["luminal_domain_note"]) - eligible = next(r for r in self.doc["derivation_rules"] if r["id"] == "E1-01") + + def test_path_satisfaction_is_not_confused_with_eligibility(self) -> None: + """Blocker 2: satisfying an ECD path is not the same as being eligible.""" + + rq2 = self.doc["rq_02_extracellular_domain"] + by_id = {r["id"]: r for r in self.doc["derivation_rules"]} + for path in rq2["paths"]: + with self.subTest(path=path["path_id"]): + # Both counts must exist and eligibility cannot exceed satisfaction. + self.assertIn("measured_path_satisfied_count", path) + self.assertIn("eligible_via_path_count", path) + self.assertLessEqual( + path["eligible_via_path_count"], + path["measured_path_satisfied_count"], + ) + # eligible_via_path must sum to E1-01, not to the satisfied totals. + self.assertEqual( + sum(p["eligible_via_path_count"] for p in rq2["paths"]), + by_id["E1-01"]["expected_count"], + ) + satisfied = sum(p["measured_path_satisfied_count"] for p in rq2["paths"]) + self.assertEqual(satisfied - rq2["measured_path_overlap"], + rq2["rq_02_positive_decomposition"]["total"]) + + def test_rq_02_positive_decomposition_holds(self) -> None: + rq2 = self.doc["rq_02_extracellular_domain"] + decomposition = rq2["rq_02_positive_decomposition"] + by_id = {r["id"]: r for r in self.doc["derivation_rules"]} + self.assertEqual( + decomposition["final_eligible"] + + decomposition["hold_insufficient_localization_families"] + + decomposition["hold_discordance"], + decomposition["total"], + ) + self.assertEqual(decomposition["final_eligible"], by_id["E1-01"]["expected_count"]) self.assertEqual( - sum(p["measured_count"] for p in rq2["paths"]), eligible["expected_count"] + decomposition["hold_insufficient_localization_families"], + by_id["E1-02"]["expected_count"], ) + # RQ-02 positives must outnumber the eligible set: holds exist downstream. + self.assertGreater(decomposition["total"], decomposition["final_eligible"]) + self.assertTrue(decomposition["identity"].strip()) def test_rq_03_requires_protein_level_provenance(self) -> None: rq3 = self.doc["rq_03_protein_level_provenance"] self.assertIs(rq3["rna_derived_rows_admissible"], False) + covered = rq3["covered_rows"] for field in ("source_id", "source_release", "source_url", "license"): - self.assertIn(field, rq3["required_fields"]) + self.assertIn(field, covered["required_fields"]) allowed = set(self.doc["allowed_fields"]["from_source_evidence"]) - self.assertTrue(set(rq3["required_fields"]) <= allowed) + self.assertTrue(set(covered["required_fields"]) <= allowed) + + def test_reference_absent_rows_need_absence_provenance_not_fabrication(self) -> None: + """Blocker 3: absent targets have no source rows and must not invent any.""" + + rq3 = self.doc["rq_03_protein_level_provenance"] + absent = rq3["absent_rows"] + self.assertIsNone(absent["requires_rows_in"]) + self.assertIs(absent["source_provenance_fields_may_be_empty"], True) + self.assertIs(absent["fabricating_source_evidence_forbidden"], True) + self.assertIs(rq3["never_present_absence_as_source_supported"], True) + for field in ("reference_dataset_id", "reference_dataset_version", + "reference_snapshot_id", "target_axis_ref", + "absence_reason", "lookup_at"): + with self.subTest(field=field): + self.assertIn(field, absent["required_absence_fields"]) + # The absence path must not reuse the covered-row requirement. + self.assertEqual( + set(absent["required_absence_fields"]) + & set(rq3["covered_rows"]["required_fields"]), + set(), + ) + # Validation must carry all three provenance rules. + ids = {r["id"] for r in self.doc["output_validation"]} + for rule_id in ("VAL-E05", "VAL-E05b", "VAL-E05c"): + self.assertIn(rule_id, ids) + columns = set(self.doc["output_schema"]["per_target_columns"]) + for column in ("provenance_kind", "absence_reason", "target_axis_ref", "lookup_at"): + with self.subTest(column=column): + self.assertIn(column, columns) def test_derivation_rules_are_total_and_never_exclude(self) -> None: rules = self.doc["derivation_rules"] @@ -185,6 +284,80 @@ def test_derivation_rules_are_total_and_never_exclude(self) -> None: len(rule["measured_targets"]), rule["expected_count"] ) + def test_precedence_is_frozen_and_covers_every_rule(self) -> None: + """Blocker 4: the five conditions are not naturally mutually exclusive.""" + + precedence = self.doc["derivation_precedence"] + rule_ids = [r["id"] for r in self.doc["derivation_rules"]] + self.assertEqual(set(precedence), set(rule_ids)) + self.assertEqual(len(precedence), len(set(precedence))) + # Absence first, then conflict, then the RQ checks, eligible last. + self.assertEqual(precedence[0], "E1-05") + self.assertEqual(precedence[1], "E1-04") + self.assertEqual(precedence[-1], "E1-01") + self.assertTrue(self.doc["precedence_rationale"].strip()) + ids = {r["id"] for r in self.doc["output_validation"]} + self.assertIn("VAL-E11", ids) + + def _assign(self, target: dict) -> str: + """Reference implementation of the frozen precedence.""" + + conditions = { + "E1-05": target["absent"], + "E1-04": not target["absent"] and target["conflict"], + "E1-03": not target["absent"] and not target["rq2"], + "E1-02": not target["absent"] and target["families"] < 2, + "E1-01": True, + } + for rule_id in self.doc["derivation_precedence"]: + if conditions[rule_id]: + return rule_id + raise AssertionError("precedence did not cover the target") + + def test_precedence_gives_one_and_only_one_rule_per_target(self) -> None: + floor = self.doc["rq_01_plasma_membrane_localization"][ + "minimum_independent_families" + ] + # Cases chosen to exercise every overlap the reviewer named. + cases = [ + ({"absent": True, "conflict": False, "rq2": False, "families": 0}, "E1-05"), + ({"absent": True, "conflict": True, "rq2": False, "families": 0}, "E1-05"), + ({"absent": False, "conflict": True, "rq2": True, "families": 3}, "E1-04"), + ({"absent": False, "conflict": True, "rq2": True, "families": 1}, "E1-04"), + ({"absent": False, "conflict": True, "rq2": False, "families": 0}, "E1-04"), + ({"absent": False, "conflict": False, "rq2": False, "families": 3}, "E1-03"), + ({"absent": False, "conflict": False, "rq2": False, "families": 1}, "E1-03"), + ({"absent": False, "conflict": False, "rq2": True, "families": 1}, "E1-02"), + ({"absent": False, "conflict": False, "rq2": True, "families": floor}, "E1-01"), + ] + for target, expected in cases: + with self.subTest(**target): + self.assertEqual(self._assign(target), expected) + + def test_multi_condition_targets_resolve_to_the_higher_rule(self) -> None: + recorded = self.doc["measured_multi_condition_targets"] + self.assertTrue(recorded) + precedence = self.doc["derivation_precedence"] + by_id = {r["id"]: r for r in self.doc["derivation_rules"]} + for item in recorded: + with self.subTest(target=item["gene_symbol"]): + matched = item["conditions_matched"] + self.assertGreater(len(matched), 1, "not a multi-condition target") + winner = min(matched, key=precedence.index) + self.assertEqual(item["resolved_to"], winner) + # The recorded resolution must match where the rule counts it. + self.assertIn( + item["gene_symbol"], by_id[item["resolved_to"]]["measured_targets"] + ) + # And it must NOT be counted under a suppressed condition. + for suppressed in matched: + if suppressed == winner: + continue + self.assertNotIn( + item["gene_symbol"], + by_id[suppressed].get("measured_targets", []), + ) + def test_exclusion_outcomes_remain_unavailable(self) -> None: unavailable = {u["outcome"]: u for u in self.doc["unavailable_outcomes"]} self.assertIn("not_surface_target", unavailable) From cfc8c594c993d444b93683b37eed1ca71973a3d8 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Wed, 5 Aug 2026 11:38:30 -0400 Subject: [PATCH 3/3] task_20260804: close the RQ-03 rule gap and the absence-column gap in PR #59 Both blockers accepted. Both were holes I left. Blocker 1: a covered target whose RQ-03 provenance fails had no rule to land on. The reviewer's combination is real - in the reference, RQ-01 satisfied, RQ-02 satisfied, no discordance, but source_evidence.tsv missing required fields - and it matches none of the five rules, so VAL-E01's exactly-one-rule could not hold. VAL-E05 said such a row degrades to hold without naming the rule or a rule_id. E1-04b now covers it: in the reference but RQ-03 unsatisfied, giving possible_surface_target, DEFER, hold. It sits third in the precedence, after discordance and before both RQ checks, because when provenance fails the row's evidence cannot be cited at all and arguing about topology or family counts is moot. Its disposition can only be DEFER: RETAIN is impossible without a traceable source, and EXCLUDE is wrong because missing provenance is not negative evidence. Measured: all 37 covered targets satisfy RQ-03, so the rule is vacuous on this snapshot and the totals are unchanged at 22/6/3/6/0/4. It still has to exist, because provenance completeness is not something this contract guarantees. Blocker 2: VAL-E05b demanded six absence columns while per_target_columns carried only three of them, so an executor could not satisfy the schema and the validation rule at once. That was a miss when I added blocker 3's fix last round. The three reference columns are added, conditionally_required_columns now states which set is mandatory for each provenance_kind, and the three are pinned to the admission snapshot's dataset id, version and snapshot id rather than being free text, with VAL-E05d enforcing it. A test asserts the required absence fields are a subset of the schema columns and that the pinned values equal the admission dependency's own fields, so bumping the admission version without updating the pins fails. 309 tests pass. Eight mutations caught and rolled back exactly. Co-Authored-By: Claude Opus 5 --- ...-01-surface-localization-contract.zh-CN.md | 40 +++++++- ...ap_01_surface_localization_extraction.yaml | 69 ++++++++++++- ..._LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md | 19 ++-- logs/worklog.md | 13 +++ tests/test_evgap_01_surface_localization.py | 98 +++++++++++++++++-- 5 files changed, 218 insertions(+), 21 deletions(-) diff --git a/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md b/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md index 6d20ff2..7946835 100644 --- a/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md +++ b/docs/handoff/2026-08-04-evgap-01-surface-localization-contract.zh-CN.md @@ -49,7 +49,7 @@ |---|---| | `docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md` | 抽取契约(面向操作者,中文) | | `docs/pools/evgap_01_surface_localization_extraction.yaml` | 机器可读绑定:来源依赖项 `SRCADM-01`+完整性校验和、字段白名单、禁读清单、RQ 映射、求值优先级、五条判据规则、输出 schema 与 15 条验证 | -| `tests/test_evgap_01_surface_localization.py` | 23 项校验 | +| `tests/test_evgap_01_surface_localization.py` | 26 项校验 | ## 四、确定性映射与实测结果 @@ -85,11 +85,11 @@ - 没有引用被隔离运行(PR #53、#54)的任何产物。 - 没有新增靶点或 clinical context。 - **没有补 #52/#53/#54/#57/#58 的批准记录**(现在是五份)。事实已查全,未写文件。 -- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 306)。属无关改动。 +- 没有修 `requirements.txt` 注释里过期的「207 tests」(实测 309)。属无关改动。 ## 七、验证结果 -- `Ran 306 tests` 全部通过(`main` 基线 283 + 本次新增 23)。 +- `Ran 309 tests` 全部通过(`main` 基线 283 + 本次新增 26)。 - `scripts/verify_repository_boundary.sh`:`Repository boundary check passed.` - `git diff --check`:通过;零 `__pycache__`。 - 所有计数由脚本读取外部数据库实测,非估计:41 靶点中 37 覆盖 / 4 未覆盖;`consensus_class` 为 29 `confirmed_surface` / 7 `supported_surface` / 1 `no_surface_support`;独立家族数分布 3 家族 10 个、2 家族 19 个、1 家族 7 个、0 家族 1 个;`discordance_flags` 非空 6 个;最终 22 eligible(`ECD-a` 18 + `ECD-b` 4)/ 19 hold / 0 killed。 @@ -153,3 +153,37 @@ ChatGPT 对 PR #59(HEAD `570562c`)返回 `REQUEST_CHANGES`。四条阻断** ### 审核回写状态 审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本节与 `logs/worklog.md`。 + +## 十一、第二轮审核裁决与修订(`REQUEST_CHANGES`,两条阻断全部接受) + +ChatGPT 对 PR #59(HEAD `f236287`)返回 `REQUEST_CHANGES`。上一轮四条确认已基本修复;本轮两条**契约缺口**全部接受。两条都是我留下的洞。 + +### 阻断 1(接受):covered target 的 RQ-03 缺失没有对应 derivation rule + +审核方构造的组合是对的:一个在库中的靶点,`RQ-01` 满足、`RQ-02` 满足、无 discordance,但 `source_evidence.tsv` 字段不全导致 `RQ-03` 不满足——它不命中 `E1-01`(RQ-03 未满足)、不命中 `E1-02`(家族数不低)、不命中 `E1-03`(RQ-02 满足)、不命中 `E1-04`(无冲突)、不命中 `E1-05`(在库中)。**`VAL-E01` 的「恰好命中一条」因此无从满足**,而 `VAL-E05` 只写「缺失即该行降为 hold」,没说降到哪条规则,也没有对应 `rule_id`。 + +修订:新增 `E1-04b`——「在库中但 `RQ-03` provenance 不成立」→ `possible_surface_target` DEFER `hold`。插入优先级第三位:`E1-05` → `E1-04` → **`E1-04b`** → `E1-03` → `E1-02` → `E1-01`。理由写入契约:provenance 不成立时该行证据本身不可引用,再谈拓扑与家族数没有意义,所以排在两个 RQ 判据之前、冲突之后。其 disposition 只能是 DEFER——既不得 RETAIN(无可回溯来源),也不得 EXCLUDE(缺 provenance 不是否定证据)。 + +**实测 37 个覆盖靶点全部满足 `RQ-03`**,故 `E1-04b` 在本 snapshot 下 `expected_count: 0`、`vacuous_this_run: true`,**计数不变**(22/6/3/6/0/4 = 41)。但它必须存在:provenance 完整性不由本契约保证,抽取时可能失败。 + +测试新增按验收标准的组合:`RQ-01=true, RQ-02=true, RQ-03=false` → `E1-04b`;`discordance=true 且 RQ-03=false` → `E1-04`(冲突优先);`RQ-02=false 且 RQ-03=false` → `E1-04b`;共 13 种组合逐例证明恰好命中一条,且 provenance 缺失只能 DEFER。 + +### 阻断 2(接受):`VAL-E05b` 要求的字段没有全部进入 output schema + +`VAL-E05b` 要求六列,而 `per_target_columns` 初稿只加了 `absence_reason`、`target_axis_ref`、`lookup_at` 三列,缺 `reference_dataset_id`、`reference_dataset_version`、`reference_snapshot_id`。**执行者无法同时遵守 output schema 与 validation rule。** 这是我上一轮补 blocker 3 时漏改的。 + +修订:三列补入 `per_target_columns`(21 → 26 → **29** 列)。新增 `conditionally_required_columns` 明确条件必填——`provenance_kind = reference_absent` 时六列必填、`source_*` 可空;`provenance_kind = source_supported` 时 `source_*` 必填、这六列可空。并加 `pinned_to_admission_snapshot`:三列必须分别等于 `ADC_surfaceome_reference`/`0.3.0`/`2026-07-29-quant-topology-mm`,不得自由填写;新增 `VAL-E05d` 强制。 + +测试直接断言 `required_absence_fields ⊆ per_target_columns`,并断言 pinned 值与 `source_admission_dependency` 的 `dataset_id`/`dataset_version`/`snapshot_id` 逐项相等——admission 版本一变,pinned 值不同步就会失败。 + +### 本轮变异检验 + +8 个变异全部被捕获后精确回滚,与备份 `diff -q` 一致、测试恢复 `OK`:从优先级里删掉 `E1-04b`、把 `E1-04b` 改判 RETAIN、把 `E1-04b` 排到 `E1-01` 之后、让 RQ-03 不指向失败规则、删掉 `reference_dataset_id` 列、让 pinned 值与 admission 不符、让 `source_supported` 不要求来源字段、谎报 RQ-03 有失败靶点。 + +### 审核方认可、本轮未改动的部分 + +`AUD-01`..`AUD-09` 足以覆盖 builder、raw manifest、license、family independence、去重、discordance、行级 provenance 与重建;`admission_record_ref = null` 时不得执行抽取;自声明守卫仅作 pending claim;RQ-02 分解自洽(34 = 22 + 6 + 6);`ECD-b` 路径合理;reference-absent 靶点不再被迫伪造 source evidence;precedence 已解决 `TM4SF1`/`TDGF1` 的多条件命中;不执行 Level 01;不评估 T7;不新增 target/context;不读取被禁文件;`EVGAP-02` 仍未解除;仓库内无 evidence 或结果数据。 + +### 审核回写状态 + +审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本节与 `logs/worklog.md`。 diff --git a/docs/pools/evgap_01_surface_localization_extraction.yaml b/docs/pools/evgap_01_surface_localization_extraction.yaml index ce3bdf5..d5da347 100644 --- a/docs/pools/evgap_01_surface_localization_extraction.yaml +++ b/docs/pools/evgap_01_surface_localization_extraction.yaml @@ -255,6 +255,11 @@ rq_03_protein_level_provenance: absence_reason_allowed_values: - gene_symbol_not_present_in_reference never_present_absence_as_source_supported: true + # 覆盖靶点 RQ-03 不成立时的归属规则。 + covered_row_failure_rule: E1-04b + covered_row_failure_disposition: DEFER + measured_covered_targets_satisfying_rq_03: 37 + measured_covered_targets_failing_rq_03: 0 # 规则求值优先级。五条规则的条件并非天然互斥——同一靶点可能同时满足 # 「两条 ECD 路径都不满足」与「独立家族数 < 2」。实测本 snapshot 下有 2 个 @@ -263,13 +268,15 @@ rq_03_protein_level_provenance: derivation_precedence: - E1-05 - E1-04 + - E1-04b - E1-03 - E1-02 - E1-01 precedence_rationale: >- 先判「不在库中」,因为无数据时其余条件都无法求值;再判定位证据冲突, - 因为冲突使后续判据不可信;再判细胞外结构域,再判独立家族数; - 全部通过才 eligible。 + 因为冲突使后续判据不可信;再判 provenance 是否成立,因为 provenance 不成立时 + 该行的证据本身不可引用,再谈拓扑与家族数没有意义;再判细胞外结构域, + 再判独立家族数;全部通过才 eligible。 measured_multi_condition_targets: - gene_symbol: TM4SF1 conditions_matched: @@ -327,6 +334,20 @@ derivation_rules: - MET - MST1R - TACSTD2 + # 覆盖靶点的 RQ-03 不成立时必须有归属规则,否则 VAL-E01 的「恰好命中一条」 + # 无从满足。实测本 snapshot 下 37 个覆盖靶点全部满足 RQ-03,故本规则为空规则; + # 但它必须存在,因为 provenance 完整性不由本契约保证,抽取时可能失败。 + - id: E1-04b + condition: 靶点在参考库中,但 RQ-03 provenance 不成立 + lock_01_outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + reason: >- + provenance 不成立时该行证据不可引用,只能 DEFER; + 既不得 RETAIN(无可回溯来源),也不得 EXCLUDE(缺 provenance 不是否定证据)。 + expected_count: 0 + vacuous_this_run: true + vacuous_reason: 实测 37 个覆盖靶点在 source_evidence.tsv 中均有齐备的 source_id/source_release/evidence_family/source_url/license。 - id: E1-05 condition: 靶点不在参考库中 lock_01_outcome: possible_surface_target @@ -384,6 +405,38 @@ mandatory_findings: hold 不是淘汰,是待证据。 output_schema: + # provenance_kind 决定哪一组列必填。 + conditionally_required_columns: + - when_provenance_kind: reference_absent + required_columns: + - reference_dataset_id + - reference_dataset_version + - reference_snapshot_id + - absence_reason + - target_axis_ref + - lookup_at + may_be_empty_columns: + - source_ids + - source_releases + - source_urls + - licenses + # 这三列必须分别等于 admission snapshot 的对应值,不得自由填写。 + pinned_to_admission_snapshot: + reference_dataset_id: ADC_surfaceome_reference + reference_dataset_version: 0.3.0 + reference_snapshot_id: 2026-07-29-quant-topology-mm + - when_provenance_kind: source_supported + required_columns: + - source_ids + - source_releases + - source_urls + - licenses + may_be_empty_columns: + - reference_dataset_id + - reference_dataset_version + - reference_snapshot_id + - absence_reason + - lookup_at per_target_columns: - gene_symbol - uniprot_accessions @@ -408,6 +461,9 @@ output_schema: - row_checksum - extracted_at - provenance_kind + - reference_dataset_id + - reference_dataset_version + - reference_snapshot_id - absence_reason - target_axis_ref - lookup_at @@ -431,6 +487,12 @@ output_validation: 但必须有完整的缺失 provenance:reference_dataset_id、reference_dataset_version、 reference_snapshot_id、target_axis_ref、absence_reason、lookup_at。 absence_reason 只能取 gene_symbol_not_present_in_reference。 + 这六列全部在 output_schema.per_target_columns 之内。 + - id: VAL-E05d + rule: >- + reference_dataset_id、reference_dataset_version、reference_snapshot_id 必须分别 + 等于 admission snapshot 的 ADC_surfaceome_reference、0.3.0、 + 2026-07-29-quant-topology-mm,不得自由填写。 - id: VAL-E05c rule: >- 禁止为未覆盖靶点伪造 source evidence,也禁止把缺失表述为 source-supported: @@ -449,7 +511,8 @@ output_validation: rule: >- 规则求值必须按 derivation_precedence 顺序取第一个命中者;每个靶点恰好命中 一条规则。同时满足多条条件的靶点必须落在优先级更高的那条,并在结果中 - 记录其余被压制的条件。 + 记录其余被压制的条件。覆盖靶点若 RQ-03 不成立,必须归入 E1-04b, + 不得因无归属规则而落空。 - id: VAL-E12 rule: >- RQ-02 阳性靶点数必须满足 rq_02_positive_decomposition 的恒等式; diff --git a/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md b/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md index 7632438..07bf617 100644 --- a/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md +++ b/docs/tasks/EVGAP_01_SURFACE_LOCALIZATION_EXTRACTION_CONTRACT.zh-CN.md @@ -105,7 +105,9 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracell **库中覆盖的靶点**:要求 `source_evidence.tsv` 有对应行,且 `source_id`、`source_release`、`evidence_family`、`source_url`、`license` 均非空。 -**库中未覆盖的靶点(`E1-05`)**:`source_evidence.tsv` 里本就没有它们的行。**初稿要求所有行都有来源 provenance,那会让合法的 `E1-05` hold 行无法通过验证,或迫使执行者伪造 provenance**——已按审核裁决更正。改为要求「缺失 provenance」:`reference_dataset_id`、`reference_dataset_version`、`reference_snapshot_id`、`target_axis_ref`、`absence_reason`(只能取 `gene_symbol_not_present_in_reference`)、`lookup_at`。 +**覆盖靶点若 `RQ-03` 不成立** → 归入 `E1-04b`,DEFER(见第五、六节)。 + +**库中未覆盖的靶点(`E1-05`)**:`source_evidence.tsv` 里本就没有它们的行。**初稿要求所有行都有来源 provenance,那会让合法的 `E1-05` hold 行无法通过验证,或迫使执行者伪造 provenance**——已按审核裁决更正。改为要求「缺失 provenance」六列:`reference_dataset_id`、`reference_dataset_version`、`reference_snapshot_id`、`target_axis_ref`、`absence_reason`(只能取 `gene_symbol_not_present_in_reference`)、`lookup_at`。**这六列全部在 `output_schema.per_target_columns` 之内**(初稿只加了后三列,导致 `VAL-E05b` 要求的字段 schema 里没有——已按审核裁决补齐)。前三列必须分别等于 admission snapshot 的 `ADC_surfaceome_reference`/`0.3.0`/`2026-07-29-quant-topology-mm`,不得自由填写(`VAL-E05d`)。 `source_*` 字段允许为空,但**禁止伪造 source evidence**,也**禁止把缺失表述为 source-supported**:`provenance_kind = reference_absent` 的行若出现非空 `source_ids` 即为验证失败(`VAL-E05b`/`VAL-E05c`)。 @@ -115,9 +117,9 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracell 冻结顺序,取第一个命中者: -> `E1-05` 不在库中 → `E1-04` 定位证据冲突 → `E1-03` 无细胞外结构域 → `E1-02` 独立家族数不足 → `E1-01` eligible +> `E1-05` 不在库中 → `E1-04` 定位证据冲突 → `E1-04b` provenance 不成立 → `E1-03` 无细胞外结构域 → `E1-02` 独立家族数不足 → `E1-01` eligible -理由:先判「不在库中」,因为无数据时其余条件都无法求值;再判冲突,因为冲突使后续判据不可信;再判细胞外结构域,再判独立家族数;全部通过才 eligible。 +理由:先判「不在库中」,因为无数据时其余条件都无法求值;再判冲突,因为冲突使后续判据不可信;**再判 provenance 是否成立,因为 provenance 不成立时该行证据本身不可引用,再谈拓扑与家族数没有意义**;再判细胞外结构域,再判独立家族数;全部通过才 eligible。 `TM4SF1` 与 `TDGF1` 同时命中 `E1-03` 与 `E1-02`,按优先级解析到 **`E1-03`**,并须在结果中记录被压制的条件(`VAL-E11`)。测试用等价 fixture 覆盖了每一种重叠组合,逐例证明恰好命中一条。 @@ -129,9 +131,14 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracell | `E1-02` | `RQ-02` 满足但独立家族数 < 2 | `possible_surface_target` | DEFER | **6** | | `E1-03` | 两条 `RQ-02` 路径都不满足(含腔内结构域) | `possible_surface_target` | DEFER | **3** | | `E1-04` | `discordance_flags` 非空 | `possible_surface_target` | DEFER | **6** | +| `E1-04b` | 在库中但 `RQ-03` provenance 不成立 | `possible_surface_target` | DEFER | **0**(空规则) | | `E1-05` | 靶点不在参考库中 | `possible_surface_target` | DEFER | **4** | -22 + 6 + 3 + 6 + 4 = 41。**零自由裁量、零排除。** +22 + 6 + 3 + 6 + 0 + 4 = 41。**零自由裁量、零排除。** + +**`E1-04b` 是按审核裁决新增的。** 初稿漏了一种组合:覆盖靶点 `RQ-01` 与 `RQ-02` 满足、无冲突,但 `source_evidence.tsv` 字段不全导致 `RQ-03` 不满足——它不命中 `E1-01`(RQ-03 未满足)、不命中 `E1-02`(家族数不低)、不命中 `E1-03`(RQ-02 满足)、不命中 `E1-04`(无冲突)、不命中 `E1-05`(在库中),因此 `VAL-E01` 的「恰好命中一条」无从满足;`VAL-E05` 只写「降为 hold」却没说降到哪条规则、也没有对应 `rule_id`。 + +实测 **37 个覆盖靶点全部满足 `RQ-03`**,故本规则在本 snapshot 下为空规则,计数不变。但它必须存在——provenance 完整性不由本契约保证,抽取时可能失败。其 disposition 只能是 DEFER:既不得 RETAIN(无可回溯来源),也不得 EXCLUDE(缺 provenance 不是否定证据)。 - `E1-02`:`CLDN18`、`GUCY2C`、`LGR5`、`PRLR`、`RNF43`、`SLC44A4` - `E1-03`:`LAMP1`、`TDGF1`、`TM4SF1` @@ -159,9 +166,9 @@ PR #58 的实测结论是:已批准证据层里 `plasma membrane`/`extracell ## 九、输出与验证 -输出为每靶点一行、**26 列**(见 YAML `output_schema`),必含 `rule_id`、`rq_02_path`、`evaluation_status`、`provenance_kind`、`absence_reason`、`target_axis_ref`、`lookup_at` 与 `row_checksum`。 +输出为每靶点一行、**29 列**(见 YAML `output_schema`),必含 `rule_id`、`rq_02_path`、`evaluation_status`、`provenance_kind`、`absence_reason`、`target_axis_ref`、`lookup_at` 与 `row_checksum`。 -**15 条**验证规则 `VAL-E01`..`VAL-E13` 见 YAML。要点:41 行且每行按优先级命中且仅命中一条规则(`VAL-E01`/`VAL-E11`);计数等于第七节且 `RQ-02` 分解恒等式成立(`VAL-E02`/`VAL-E12`);不得出现 `not_surface_target` 或 `identity_unresolved`;每个 eligible 必记 `rq_02_path`;覆盖行须有完整来源 provenance、未覆盖行须有完整缺失 provenance 且禁止伪造(`VAL-E05`/`E05b`/`E05c`);输入 SHA-256 不一致即中止;不得读取被禁文件或字段;输出中不得出现任何 Gate 分数、Gate 状态、T7 判定或肿瘤表面定量;三条 `mandatory_findings` 必须原样出现;每个产物文件逐文件记录 SHA-256;**抽取前 `admission_record_ref` 必须指向实际存在的独立 `APPROVE` 记录,为空即不得执行(`VAL-E13`)**。 +**16 条**验证规则 `VAL-E01`..`VAL-E13`(含 `VAL-E05b`/`c`/`d`) 见 YAML。要点:41 行且每行按优先级命中且仅命中一条规则(`VAL-E01`/`VAL-E11`);计数等于第七节且 `RQ-02` 分解恒等式成立(`VAL-E02`/`VAL-E12`);不得出现 `not_surface_target` 或 `identity_unresolved`;每个 eligible 必记 `rq_02_path`;覆盖行须有完整来源 provenance、未覆盖行须有完整缺失 provenance 且禁止伪造(`VAL-E05`/`E05b`/`E05c`);输入 SHA-256 不一致即中止;不得读取被禁文件或字段;输出中不得出现任何 Gate 分数、Gate 状态、T7 判定或肿瘤表面定量;三条 `mandatory_findings` 必须原样出现;每个产物文件逐文件记录 SHA-256;**抽取前 `admission_record_ref` 必须指向实际存在的独立 `APPROVE` 记录,为空即不得执行(`VAL-E13`)**。 ## 十、本契约授权与不授权 diff --git a/logs/worklog.md b/logs/worklog.md index 2b5ed08..e65854a 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2544,3 +2544,16 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Accepted by reviewer, unchanged: `ECD-b` 路径可作为 extracellular topology 路径(前提是 GPI 注释可靠、有信号肽、零跨膜段、蛋白级 provenance);跨膜段不再单独满足 `RQ-01`;GPI 路径未被当成普通跨膜蛋白规则;`LAMP1` 腔内结构域不被当成胞外;缺失与冲突均 DEFER 不 EXCLUDE;`not_surface_target` 仍不可用;T7 与肿瘤定量文件明确禁止;不执行 Level 01;不新增 target/context;仓库不存 evidence/result;`EVGAP-02` 继续未解除。 - Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十节。 - Next: 推送同一 PR 并同步 PR 描述请求复审。此后欠着的工作变为三项:`SRCADM-01` 数据库 admission PR、`EVGAP-02` 契约、五份批准记录。 + +## 2026-08-05T01:05:00-04:00 — PR #59 第二轮审核裁决与修订(两条契约缺口全部接受) + +- Review: ChatGPT 对 PR #59(HEAD `f236287`,可合并、CI 成功)返回 `REQUEST_CHANGES`。上一轮四条确认已基本修复(source admission 拆为 `SRCADM-01`、当前契约不再授权抽取、RQ-02 两个计数分开、reference-absent provenance 与 source provenance 区分、precedence 已冻结并覆盖多条件重叠)。本轮两条**契约缺口**全部接受,两条都是执行者留下的洞。 +- Finding 1 accepted: **覆盖靶点的 `RQ-03` 缺失没有对应 derivation rule。** 审核方构造的组合成立:在库中、`RQ-01` 满足、`RQ-02` 满足、无 discordance,但 `source_evidence.tsv` 字段不全导致 `RQ-03` 不满足——不命中 `E1-01`(RQ-03 未满足)/`E1-02`(家族数不低)/`E1-03`(RQ-02 满足)/`E1-04`(无冲突)/`E1-05`(在库中),故 `VAL-E01` 的「恰好命中一条」无从满足;`VAL-E05` 只写「降为 hold」,未说降到哪条规则、无对应 `rule_id`。 +- Fix 1: 新增 `E1-04b`「在库中但 RQ-03 provenance 不成立」→ `possible_surface_target` DEFER `hold`,插入优先级第三位(`E1-05` → `E1-04` → **`E1-04b`** → `E1-03` → `E1-02` → `E1-01`)。理由写入契约:provenance 不成立时该行证据不可引用,再谈拓扑与家族数无意义,故排在两个 RQ 判据之前、冲突之后。disposition 只能 DEFER——不得 RETAIN(无可回溯来源),不得 EXCLUDE(缺 provenance 不是否定证据)。`rq_03` 增 `covered_row_failure_rule: E1-04b`。**实测 37 个覆盖靶点全部满足 RQ-03**,故 `expected_count: 0`、`vacuous_this_run: true`,计数不变(22/6/3/6/0/4 = 41);但规则必须存在,provenance 完整性不由本契约保证。测试按验收标准新增组合:`RQ-01=true,RQ-02=true,RQ-03=false` → `E1-04b`;`discordance=true 且 RQ-03=false` → `E1-04`(冲突优先);`RQ-02=false 且 RQ-03=false` → `E1-04b`;共 13 种组合逐例证明恰好命中一条,且 provenance 缺失只能 DEFER。 +- Finding 2 accepted: **`VAL-E05b` 要求的六列没有全部进入 output schema。** `per_target_columns` 上一轮只加了 `absence_reason`/`target_axis_ref`/`lookup_at`,缺 `reference_dataset_id`/`reference_dataset_version`/`reference_snapshot_id`,执行者无法同时遵守 schema 与 validation rule。**这是执行者上一轮补 blocker 3 时的漏改。** +- Fix 2: 三列补入 `per_target_columns`(21 → 26 → **29**)。新增 `conditionally_required_columns`:`reference_absent` 时六列必填、`source_*` 可空;`source_supported` 时 `source_*` 必填、六列可空。新增 `pinned_to_admission_snapshot`——三列必须分别等于 `ADC_surfaceome_reference`/`0.3.0`/`2026-07-29-quant-topology-mm`,不得自由填写;新增 `VAL-E05d` 强制。测试直接断言 `required_absence_fields ⊆ per_target_columns`,并断言 pinned 值与 `source_admission_dependency` 的 `dataset_id`/`dataset_version`/`snapshot_id` 逐项相等——admission 版本一变、pinned 不同步即失败。 +- Validation: `Ran 309 tests` 全部通过(`main` 基线 283 + 新增 26,由 23 增至 26);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 +- Mutation-tested (本轮 8 个,全部 `FAILED` 后精确回滚,与备份 `diff -q` 一致、恢复 `OK`): 从优先级删掉 `E1-04b`、`E1-04b` 改判 RETAIN、`E1-04b` 排到 `E1-01` 之后、RQ-03 不指向失败规则、删掉 `reference_dataset_id` 列、pinned 值与 admission 不符、`source_supported` 不要求来源字段、谎报 RQ-03 有失败靶点。 +- Accepted by reviewer, unchanged: `AUD-01`..`AUD-09` 足以覆盖 builder/raw manifest/license/family independence/去重/discordance/行级 provenance/重建;`admission_record_ref = null` 时不得执行抽取;自声明守卫仅作 pending claim;RQ-02 分解自洽(34 = 22 + 6 + 6);`ECD-b` 路径合理;reference-absent 靶点不再被迫伪造 source evidence;precedence 已解决 `TM4SF1`/`TDGF1`;不执行 Level 01;不评估 T7;不新增 target/context;不读取被禁文件;`EVGAP-02` 仍未解除;仓库内无 evidence 或结果数据。 +- Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十一节。 +- Next: 推送同一 PR 并同步 PR 描述请求复审。 diff --git a/tests/test_evgap_01_surface_localization.py b/tests/test_evgap_01_surface_localization.py index 4e481ca..f882a25 100644 --- a/tests/test_evgap_01_surface_localization.py +++ b/tests/test_evgap_01_surface_localization.py @@ -305,6 +305,7 @@ def _assign(self, target: dict) -> str: conditions = { "E1-05": target["absent"], "E1-04": not target["absent"] and target["conflict"], + "E1-04b": not target["absent"] and not target["rq3"], "E1-03": not target["absent"] and not target["rq2"], "E1-02": not target["absent"] and target["families"] < 2, "E1-01": True, @@ -320,20 +321,99 @@ def test_precedence_gives_one_and_only_one_rule_per_target(self) -> None: ] # Cases chosen to exercise every overlap the reviewer named. cases = [ - ({"absent": True, "conflict": False, "rq2": False, "families": 0}, "E1-05"), - ({"absent": True, "conflict": True, "rq2": False, "families": 0}, "E1-05"), - ({"absent": False, "conflict": True, "rq2": True, "families": 3}, "E1-04"), - ({"absent": False, "conflict": True, "rq2": True, "families": 1}, "E1-04"), - ({"absent": False, "conflict": True, "rq2": False, "families": 0}, "E1-04"), - ({"absent": False, "conflict": False, "rq2": False, "families": 3}, "E1-03"), - ({"absent": False, "conflict": False, "rq2": False, "families": 1}, "E1-03"), - ({"absent": False, "conflict": False, "rq2": True, "families": 1}, "E1-02"), - ({"absent": False, "conflict": False, "rq2": True, "families": floor}, "E1-01"), + ({"absent": True, "conflict": False, "rq2": False, "rq3": False, "families": 0}, "E1-05"), + ({"absent": True, "conflict": True, "rq2": False, "rq3": False, "families": 0}, "E1-05"), + ({"absent": False, "conflict": True, "rq2": True, "rq3": True, "families": 3}, "E1-04"), + ({"absent": False, "conflict": True, "rq2": True, "rq3": True, "families": 1}, "E1-04"), + ({"absent": False, "conflict": True, "rq2": False, "rq3": True, "families": 0}, "E1-04"), + # discordance outranks a provenance failure. + ({"absent": False, "conflict": True, "rq2": True, "rq3": False, "families": 3}, "E1-04"), + # The gap the reviewer found: RQ-01 and RQ-02 hold, no conflict, RQ-03 fails. + ({"absent": False, "conflict": False, "rq2": True, "rq3": False, "families": 3}, "E1-04b"), + ({"absent": False, "conflict": False, "rq2": False, "rq3": False, "families": 3}, "E1-04b"), + ({"absent": False, "conflict": False, "rq2": True, "rq3": False, "families": 1}, "E1-04b"), + ({"absent": False, "conflict": False, "rq2": False, "rq3": True, "families": 3}, "E1-03"), + ({"absent": False, "conflict": False, "rq2": False, "rq3": True, "families": 1}, "E1-03"), + ({"absent": False, "conflict": False, "rq2": True, "rq3": True, "families": 1}, "E1-02"), + ({"absent": False, "conflict": False, "rq2": True, "rq3": True, "families": floor}, "E1-01"), ] for target, expected in cases: with self.subTest(**target): self.assertEqual(self._assign(target), expected) + def test_provenance_failure_on_a_covered_target_has_its_own_rule(self) -> None: + """Blocker 1: RQ-03 failure on a covered target must land somewhere.""" + + rules = {r["id"]: r for r in self.doc["derivation_rules"]} + self.assertIn("E1-04b", rules) + rule = rules["E1-04b"] + self.assertEqual(rule["lock_01_outcome"], "possible_surface_target") + self.assertEqual(rule["disposition"], CandidateDisposition.DEFER.value) + self.assertEqual(rule["resulting_state"], "hold") + # Vacuous on this snapshot, but it must be declared and justified. + self.assertEqual(rule["expected_count"], 0) + self.assertIs(rule["vacuous_this_run"], True) + self.assertTrue(rule["vacuous_reason"].strip()) + # It must sit in the precedence, after discordance. + precedence = self.doc["derivation_precedence"] + self.assertIn("E1-04b", precedence) + self.assertLess(precedence.index("E1-04"), precedence.index("E1-04b")) + self.assertLess(precedence.index("E1-04b"), precedence.index("E1-01")) + # RQ-03 must name this rule as the failure route. + rq3 = self.doc["rq_03_protein_level_provenance"] + self.assertEqual(rq3["covered_row_failure_rule"], "E1-04b") + self.assertEqual( + rq3["covered_row_failure_disposition"], CandidateDisposition.DEFER.value + ) + self.assertEqual(rq3["measured_covered_targets_failing_rq_03"], 0) + self.assertEqual( + rq3["measured_covered_targets_satisfying_rq_03"], + self.doc["scope"]["measured_coverage_in_reference"], + ) + + def test_absence_fields_are_all_present_in_the_output_schema(self) -> None: + """Blocker 2: VAL-E05b cannot demand columns the schema does not have.""" + + required = set( + self.doc["rq_03_protein_level_provenance"]["absent_rows"][ + "required_absence_fields" + ] + ) + columns = set(self.doc["output_schema"]["per_target_columns"]) + self.assertTrue(required <= columns, f"missing columns: {required - columns}") + for field in ("reference_dataset_id", "reference_dataset_version", + "reference_snapshot_id"): + with self.subTest(field=field): + self.assertIn(field, columns) + + def test_conditional_columns_pin_the_admission_snapshot(self) -> None: + blocks = { + b["when_provenance_kind"]: b + for b in self.doc["output_schema"]["conditionally_required_columns"] + } + self.assertEqual(set(blocks), {"reference_absent", "source_supported"}) + absent = blocks["reference_absent"] + required = set( + self.doc["rq_03_protein_level_provenance"]["absent_rows"][ + "required_absence_fields" + ] + ) + self.assertEqual(set(absent["required_columns"]), required) + for field in ("source_ids", "source_releases", "source_urls", "licenses"): + self.assertIn(field, absent["may_be_empty_columns"]) + # The three reference columns must equal the admission snapshot, not be free text. + dep = self.doc["source_admission_dependency"] + pinned = absent["pinned_to_admission_snapshot"] + self.assertEqual(pinned["reference_dataset_id"], dep["dataset_id"]) + self.assertEqual(str(pinned["reference_dataset_version"]), str(dep["dataset_version"])) + self.assertEqual(pinned["reference_snapshot_id"], dep["snapshot_id"]) + ids = {r["id"] for r in self.doc["output_validation"]} + self.assertIn("VAL-E05d", ids) + # And source-supported rows must still carry real source provenance. + supported = blocks["source_supported"] + for field in ("source_ids", "source_releases", "source_urls", "licenses"): + self.assertIn(field, supported["required_columns"]) + def test_multi_condition_targets_resolve_to_the_higher_rule(self) -> None: recorded = self.doc["measured_multi_condition_targets"] self.assertTrue(recorded)