From 5d85220d64a56ed9aa5bee49b7f343e7c27058c4 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Wed, 5 Aug 2026 15:52:24 -0400 Subject: [PATCH 1/4] task_20260805: record the EVGAP-02 CRC linkage extraction run Executed the Tier 1 extraction authorised by PR #61, whose only blocker was its own approval. Every artefact is external; this PR carries a handoff and a worklog entry. Reachability was tested before executing, because the contract makes an unreachable source a recorded fact rather than a silent skip. All six required endpoints answered: PubMed and PMC via NCBI E-utilities, ClinicalTrials.gov API v2, GEO via E-utilities db=gds, TCGA via the GDC genes API, and the Human Protein Atlas search API. 451 searches, zero unreachable, 41 of 41 targets complete at endpoint granularity, and all 369 pairs complete on the class D layer. LOCK-03 came out at 168 L3-02 retain/active, 192 L3-03 defer/hold and 9 L3-05 EXCLUDE_FROM_ACTIVE_POOL/reactivation-eligible. L3-01 and L3-04 are vacuous this run: no pair had an incomplete search and no other-cancer-only evidence was collected. The canonical context retains 40 of 41 targets because it needs only A/B/C evidence; the eight subgroups retain between 6 and 33 because each also needs class D context enrichment, and that gap is exactly what the 192 holds are. Evidence strength, stated plainly rather than buried: all 7,067 rows are machine_retrieved_requires_human_review with evidence_direction unknown. They are public records retrieved by the frozen queries, with PMIDs, PMCIDs, NCT numbers and GEO accessions that resolve, but their contents were not read. Under DECISION-02, accepted in PR #58, machine-retrieved evidence satisfies LOCK-03 existence while barring the pair from Level 02. So the 168 active pairs mean a CRC-specific linkage record exists pending review, not that linkage is established. Only EDBN reached L3-05 across all nine contexts, and that outcome is reversible: not scientific disproof, not killed, reactivation-eligible, still in the Eligible Universe Index. One defect of mine, found by self-check and repaired: the first evidence table lacked pair_id, clinical_context_id, context_specific and linkage_outcome, and because A/B/C are searched once per target a single row could not carry a unique pair_id, leaving VAL-L16 unsatisfiable. I had not aligned "searched per target" with "recorded per pair". Fixed by conforming the output to the frozen schema rather than amending the contract: target-level records were expanded into 7,067 pair-level rows with no network call repeated and no retrieved fact altered. All twenty contract validation rules VAL-L01 through VAL-L20 were checked against the actual artefacts and all passed. The package is at external:result/gen_iet_evgap_02_crc_linkage_20260805T190453Z.zip, 148063 bytes, SHA-256 9c9c184e7b66e2999950831a18e059847c3b7dfd4a5b6f92ac78ac9dce259ece. No Gate run, no score, no T2 or T7 assessment, no ranking, no Tier, no asset or experimental recommendation, no target or context added, no Tier 2 database opened, no quarantined artefact used. EVGAP-02 is NOT lifted by this PR and EVGAP-01 is untouched, so Level 01 remains unexecutable. 338 tests pass. Co-Authored-By: Claude Opus 5 --- ...-08-05-evgap-02-extraction-result.zh-CN.md | 152 ++++++++++++++++++ logs/worklog.md | 20 +++ 2 files changed, 172 insertions(+) create mode 100644 docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md diff --git a/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md new file mode 100644 index 0000000..b2b8e60 --- /dev/null +++ b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md @@ -0,0 +1,152 @@ +# Handoff:EVGAP-02 CRC linkage 抽取结果审核 + +- 日期:`2026-08-05` +- 任务分支:`task_20260805_evgap-02-extraction-result` +- 基线:`main` @ `8aa7e87` +- 授权依据:PR #61(`docs/pools/evgap_02_crc_linkage_extraction.yaml`),已 `APPROVE` 并合并;该契约 `authorises_extraction_run_after_approve: true`、`blocked_by: [contract_approval]`,条件已满足 +- 外部运行:`gen_iet_evgap_02_crc_linkage_20260805T190453Z` +- 交付物类型:**结果审核(外部运行留痕)** +- 架构变更:`NO_ARCHITECTURE_CHANGE`(本 PR 只含本 handoff 与一条 worklog) +- 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** + +## 一、本次做了什么 + +按 PR #61 已获批的契约执行一次 Tier 1 抽取,解除 `EVGAP-02`(LOCK-03 的证据缺口)。 + +**这不解除 `EVGAP-01`,不授权执行 Level 01,不使任何 pair 进入 Level 02。** + +## 二、执行前先测可达性 + +契约把来源不可达定为**须记录的事实**而非可静默跳过,因此执行前逐个测试六个必查 endpoint,全部应答: + +| source class | endpoint | API | +|---|---|---| +| `peer_reviewed_literature` | PubMed、PMC | NCBI E-utilities `esearch` | +| `clinical_trial_registry` | ClinicalTrials.gov | API v2 `/studies` | +| `public_molecular_dataset` | TCGA、GEO、Human Protein Atlas | GDC genes API;E-utilities `db=gds`;HPA `search_download` | + +## 三、检索完整性 + +| 项 | 值 | +|---|---| +| 检索次数 | **451** | +| 不可达检索 | **0** | +| target 级检索完整 | **41 / 41** | +| pair 级 D 类检索完整 | **369 / 369** | + +target 级按 endpoint 判定覆盖(不是按 source class),A/B/C 用类别特异术语;D 类按 pair 判定,369 个 pair 全部记录六个完整性字段。 + +## 四、LOCK-03 结果 + +| 规则 | outcome | disposition | state | 数量 | +|---|---|---|---|---| +| `L3-02` | `linkage_evidence_exists` | RETAIN | active | **168** | +| `L3-03` | `linkage_unassessed` | DEFER | hold | **192** | +| `L3-05` | `no_known_linkage_after_complete_search` | `EXCLUDE_FROM_ACTIVE_POOL` | reactivation-eligible | **9** | + +168 + 192 + 9 = 369。**`L3-01` 与 `L3-04` 本次为空规则**——无检索不完整的 pair,也未采集到「仅其他癌种 precedent」的证据。 + +### active 的分布,以及 192 个 hold 的来由 + +| context | active | +|---|---| +| `crc_mss_pmMR_mcrc_3l_plus`(canonical) | **40** | +| `crc_cms4_emt_oncofetal_revCSC_high` | 33 | +| `crc_mss_ras_mut_1l_2l_post_chemo_bev` | 19 | +| `crc_msi_h_dmmr_io_resistant` | 17 | +| `crc_mss_ras_mut_3l_plus` | 17 | +| `crc_braf_v600e_post_targeted` | 14 | +| `crc_her2_positive_post_treatment` | 11 | +| `crc_kras_g12c_post_targeted` | 11 | +| `crc_mss_ras_wt_antiegfr_resistant` | 6 | + +canonical context 只要有 A/B/C 证据即可 RETAIN,故 40 个靶点全部 active(41 个中只有 `EDBN` 无 A/B/C 证据)。**8 个亚群 context 必须同时有 D 类情境特异证据才能 RETAIN**——这正是那 192 个 `L3-03` 的差别所在:有疾病级 CRC 证据,但该亚群没有情境特异富集证据。 + +### 证据行 + +**7,067 行**:A 2,808 / B 2,295 / C 1,746 / D 218。`context_specific = true` 仅 D 类的 218 行——A/B/C 按契约是疾病级检索,按构造不具情境特异性。 + +## 五、必须原样记录的四条 + +- **`MF-L01`**:LOCK-03 的 RETAIN 只表示**存在可回溯的 CRC-specific linkage 证据记录**,**不表示该靶点适合 ADC、不表示疗效、不表示治疗窗**。 +- **`MF-L02`**:C 类 1,746 行(naked antibody/CAR-T/bispecific/RIT/immunotoxin/imaging antibody)证明 target 在 CRC 中可接近或可干预,**不是 ADC 疗效证据**。全部 C 类行 `is_adc_efficacy_evidence = false`。 +- **`MF-L03`**:未使用任何派生本地数据库。**检索完整性只在 Tier 1 声明范围内成立。** ADCdb、CRC 文献库、CRC Atlas ledger、竞争格局库仍待 `SRCADM-02`..`05`;日后任一获准入需另开 PR 扩大范围并重跑。 +- **`MF-L04`**:RETAIN **不使 pair 进入 Level 02**。`EVGAP-01` 未解除前 `may_advance_to_level_02` 恒为 `false`,369 行全部如此。 + +## 六、一条必须写明的证据强度限制 + +**全部 7,067 行 `review_status = machine_retrieved_requires_human_review`,`evidence_direction = unknown`。** + +这些是**按冻结查询式检索到的公开记录**——PMID、PMCID、NCT 号、GEO 登记号都可点击回溯,但**其内容未被阅读、未被人工判读**。按 PR #58 已获接受的 `DECISION-02`,machine-retrieved 证据满足 LOCK-03 的**存在性**,但携带该状态的 pair **不得晋级 Level 02**。 + +因此 **168 个 active 的含义是「存在待复核的 CRC-specific linkage 记录」,不是「linkage 已确证」。** 这一句必须原样出现在任何引用本结果的地方。 + +## 七、只有一个靶点完整检索后无任何 linkage + +`EDBN`,9 个 pair 全部 `L3-05`。它是非标准符号(fibronectin EDB 结构域),在 2026-08-05 的 Level 01 Preview 中也是 `E1-05`(不在 surfaceome 参考库)。 + +`L3-05` 是**可逆**的:`is_scientific_disproof = false`、`is_killed = false`、状态 `reactivation-eligible`,仍留在 Eligible Universe Index。**不是科学证伪,不是淘汰。** + +## 八、一处执行者自查并修正的问题 + +首版 evidence 表缺少契约要求的 4 列(`pair_id`、`clinical_context_id`、`context_specific`、`linkage_outcome`);而且 A/B/C 按契约是**按 target 检索一次**,导致单条证据行无法携带唯一 `pair_id`,`VAL-L16`「引用行的 `pair_id` 必须与 disposition 一致」不可满足。 + +这是我在设计输出时没把「按 target 检索」与「按 pair 记录证据」这两件事对齐。修正方式是**让输出符合已冻结的 schema,而不是改契约**:把 target 级记录按 pair 展开为 7,067 行 pair 级证据行,**未重复任何网络调用、未改变任何已检索事实**,只改表示形式。 + +## 九、验证结果 + +**契约 20 条验证规则 `VAL-L01`..`VAL-L20` 逐条对实际产物核验,全部 `PASS`**,包括: + +- `VAL-L01` 369 行、每行命中且仅命中一条规则、`pair_id` 唯一 +- `VAL-L02` 每行 outcome/disposition/state 与其 `rule_id` 的契约声明逐项一致 +- `VAL-L04` 全部 C 类行 `is_adc_efficacy_evidence = false` +- `VAL-L06` 全部亚群 `L3-02` 行都有非空 `class_d_evidence_refs` +- `VAL-L07` 全部 `L3-05` 行六项完整性字段齐备、`search_complete = true`、语义为 `EXCLUDE_FROM_ACTIVE_POOL`、`reactivation-eligible` +- `VAL-L09` 两张表的列集合与契约**逐表**相等 +- `VAL-L11`/`VAL-L19` 41 个 target 全部覆盖六个必查 endpoint +- `VAL-L16` 全部引用 id 存在于 evidence 表且 `pair_id` 一致,7,067 个 id 无重复 +- `VAL-L17` 逐规则引用约束全部满足 +- `VAL-L18` 369 个 pair 全部有六个 D 类字段且 `class_d_search_complete = true` +- `VAL-L20` `evidence_row_count` 全部等于三组 refs 去重后总数 + +仓库侧:`Ran 338 tests` 全部通过;`scripts/verify_repository_boundary.sh` 通过;运行期间仓库零写入。 + +## 十、明确没有做什么 + +- 未运行任何 Gate、未赋任何分数、未评估 T2 或 T7。 +- 未排序、未划 Tier、未推荐资产、未给实验建议。 +- 未新增靶点或 clinical context(0/0)。 +- **未打开任何 Tier 2 派生本地数据库**;未把任何派生库纳入已批准来源。 +- 未引用被隔离运行(PR #53、#54)的任何产物。 +- **未解除 `EVGAP-01`;未更新 `adc_pool_level_01_input_binding.yaml`;Level 01 仍不可执行。** 解除 `EVGAP-02` 须待本结果获批后另开 PR。 +- 未生成 `ADC_POOL_LEVEL_01_ACCEPTED`。 +- 未补八份批准记录(#52/#53/#54/#57/#58/#59/#60/#61),事实已查全但未写文件。 + +## 十一、下一步 + +1. 本结果 `APPROVE`。 +2. **另开 PR** 更新 Level 01 输入绑定,把本抽取产物绑为 `LOCK-03` 的来源并**解除 `EVGAP-02`**。 +3. Track B:`SRCADM-01` → `EVGAP-01` 抽取 → 结果 → binding。 +4. **两个缺口都解除后**,才能生成 `ADC_POOL_LEVEL_01_ACCEPTED`。 + +## 附录 A:产物 SHA-256 + +| 文件 | SHA-256 | +|---|---| +| `pair_linkage_evidence.tsv` | `333a5c4132174075691f1952e839cd6737ccdc68c238e2b497e6f666951c8e09` | +| `pair_linkage_disposition.tsv` | `464d75d426921d73cc417c0c33d27ec0cf8408ba031fae75e2d8331b8e31a9cc` | +| `search_log.tsv` | `dd0569c572bfd09f74f034f1844811c918182c767118e963afc9ed0a14c7ce08` | +| `run_report.md` | `ef3914954f9fc9f7a0e78c50a1495dd045e79ebc1cbd4c2768a6ce8f2f05786b` | +| `external_run_worklog.md` | `878dd8f863e61467128321c2e4fd464fba163a1cdeeac48ada323e929f05a88a` | +| `source_manifest.json` | `a143d4b3e00de7d4cedb325f4abe49a00b62c27c22a8f91e68c153bc461194b9` | + +运行目录:`external:result/gen_iet_evgap_02_crc_linkage_20260805T190453Z` + +### 供上传审核的打包 + +`external:result/gen_iet_evgap_02_crc_linkage_20260805T190453Z.zip` + +- ZIP SHA-256:`9c9c184e7b66e2999950831a18e059847c3b7dfd4a5b6f92ac78ac9dce259ece` +- 148,063 bytes,含且仅含上表六个文件 + +(按 PR #60 第二轮裁决后我自我约束的规则:外部产物每次交付都必须同时产出带校验和的打包,只改 handoff 不算完成交付。) diff --git a/logs/worklog.md b/logs/worklog.md index e4f24d9..bfe499d 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2647,3 +2647,23 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Accepted by reviewer, unchanged: EVGAP-02 与 EVGAP-01/SRCADM-01 独立;覆盖全部 369 pairs;Tier 1/Tier 2 分层与 Tier 2 禁用;A/B/C/D 四类框架;RNA 可支持 linkage 但不满足 LOCK-01;C 类可作 linkage existence 但非 ADC efficacy;disease-level 不自动支持 subgroup;other-cancer 只作 metadata;`L3-05` 可逆非证伪非 killed;不执行 Gate/Level 01、不解除 `EVGAP-01`;不预写 discovery run 结果数量。 - Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十四节。 - Next: 推送同一 PR 并同步 PR 描述请求复审。 + +## 2026-08-05T19:20:00-04:00 — EVGAP-02 CRC linkage 抽取执行(外部运行留痕) + +- Instruction: 人类负责人指示「先执行 EVGAP-02 抽取,然后 SRCADM-01」。授权依据为 PR #61 已 `APPROVE` 并合并的 `docs/pools/evgap_02_crc_linkage_extraction.yaml`,其 `blocked_by: [contract_approval]` 已满足。 +- Reachability tested first: 契约把来源不可达定为须记录的事实而非可静默跳过,故执行前逐个测试六个必查 endpoint,全部应答——PubMed/PMC(NCBI E-utilities `esearch`)、ClinicalTrials.gov API v2、GEO(E-utilities `db=gds`)、TCGA(GDC genes API)、Human Protein Atlas(`search_download`)。 +- Run: `gen_iet_evgap_02_crc_linkage_20260805T190453Z`,六个产物全部写入外部 `DATA`,**仓库零写入**。 +- Search completeness: **451 次检索、0 不可达、41/41 target 检索完整、369/369 pair 的 D 类检索完整**。target 级按 endpoint 判定覆盖(非按 source class),A/B/C 用类别特异术语;D 类按 pair 判定并记录六个完整性字段。 +- Result: **`L3-02` 168 RETAIN/active;`L3-03` 192 DEFER/hold;`L3-05` 9 `EXCLUDE_FROM_ACTIVE_POOL`/reactivation-eligible**。168+192+9 = 369。`L3-01` 与 `L3-04` 本次为空规则——无检索不完整的 pair,也未采集到「仅其他癌种 precedent」的证据。 +- active by context: canonical `crc_mss_pmMR_mcrc_3l_plus` **40**(41 个靶点中仅 `EDBN` 无 A/B/C 证据);8 个亚群 context 分别 33/19/17/17/14/11/11/6。**亚群必须同时有 D 类情境特异证据才能 RETAIN**——这正是 192 个 `L3-03` 的差别:有疾病级 CRC 证据,但该亚群无情境特异富集证据。 +- Evidence: **7,067 行**,A 2,808/B 2,295/C 1,746/D 218。`context_specific = true` 仅 D 类 218 行,A/B/C 按契约是疾病级检索、按构造不具情境特异性。 +- Mandatory findings recorded: `MF-L01` RETAIN 只表示存在可回溯的 CRC-specific linkage 记录,**不表示适合 ADC、不表示疗效、不表示治疗窗**;`MF-L02` C 类 1,746 行全部 `is_adc_efficacy_evidence = false`;`MF-L03` 未使用任何派生本地库,**完整性只在 Tier 1 范围内成立**,`SRCADM-02`..`05` 仍待准入,日后扩大须另开 PR 重跑;`MF-L04` RETAIN 不使 pair 进入 Level 02,369 行 `may_advance_to_level_02` 全为 `false`。 +- Evidence strength stated plainly: **全部 7,067 行 `review_status = machine_retrieved_requires_human_review`、`evidence_direction = unknown`**。这些是按冻结查询式检索到的公开记录(PMID/PMCID/NCT/GEO 登记号均可回溯),**内容未被阅读、未被人工判读**。按 PR #58 已获接受的 `DECISION-02`,machine-retrieved 证据满足 LOCK-03 存在性但该 pair 不得晋级 Level 02。**故 168 个 active 的含义是「存在待复核的 CRC-specific linkage 记录」,不是「linkage 已确证」。** +- Only one target reached L3-05: `EDBN`(9 个 pair)。非标准符号(fibronectin EDB 结构域),在 Level 01 Preview 中亦为 `E1-05` 不在 surfaceome 参考库。`L3-05` 可逆——`is_scientific_disproof: false`、`is_killed: false`、`reactivation-eligible`,仍留在 Eligible Universe Index,**不是科学证伪、不是淘汰**。 +- Executor defect found by self-check and repaired: 首版 evidence 表缺契约要求的 4 列(`pair_id`/`clinical_context_id`/`context_specific`/`linkage_outcome`),且 A/B/C 按契约按 target 检索一次导致单条证据无法携带唯一 `pair_id`、`VAL-L16` 不可满足。**这是我没把「按 target 检索」与「按 pair 记录证据」对齐。** 修正方式是让输出符合已冻结 schema 而非改契约:把 target 级记录按 pair 展开为 7,067 行,**未重复任何网络调用、未改变任何已检索事实**,只改表示形式。 +- Validation: **契约 20 条 `VAL-L01`..`VAL-L20` 逐条对实际产物核验,全部 `PASS`**,含 `VAL-L09` 两张表列集合逐表相等、`VAL-L16` 全部引用 id 存在且 `pair_id` 一致且 7,067 个 id 无重复、`VAL-L17` 逐规则引用约束、`VAL-L18` 369 pair 六字段齐备、`VAL-L20` 计数等于三组 refs 去重总数。仓库侧 `Ran 338 tests` 通过、`scripts/verify_repository_boundary.sh` 通过。 +- Packaged for review: `external:result/gen_iet_evgap_02_crc_linkage_20260805T190453Z.zip`,148,063 bytes,ZIP SHA-256 `9c9c184e7b66e2999950831a18e059847c3b7dfd4a5b6f92ac78ac9dce259ece`,含且仅含六个产物文件。**按 PR #60 第二轮裁决后自我约束的规则执行:外部产物每次交付必须同时产出带校验和的打包。** +- Boundary: 未运行任何 Gate、未赋分数、未评估 T2/T7、未排序、未划 Tier、未推荐资产、未给实验建议、未新增靶点或 context(0/0)、**未打开任何 Tier 2 派生库**、未引用被隔离运行产物、未写入仓库。 +- Deliberately not done: **未解除 `EVGAP-02`**(须待本结果获批后另开 PR 更新绑定);未解除 `EVGAP-01`;未更新 `adc_pool_level_01_input_binding.yaml`;Level 01 仍不可执行;未生成 `ADC_POOL_LEVEL_01_ACCEPTED`;未补八份批准记录。 +- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。本 PR 只含本条 worklog 与一份 handoff。 +- Next: 送审本结果;获批后另开 PR 解除 `EVGAP-02`;随后按指示起 `SRCADM-01`。 From 3874c447e2cda2586da318d945352ff8c3d1a735 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Wed, 5 Aug 2026 17:29:43 -0400 Subject: [PATCH 2/4] task_20260805: downgrade EVGAP-02 run to retrieval layer, amend contract to v0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #62 review: the run registered search hits as linkage evidence. Verified against the actual files first — every point holds. All 7,067 rows carried evidence_direction=unknown, and no assertion-bearing field was populated anywhere: prevalence empty in all rows, is_adc_efficacy_evidence false in all, protein_or_rna unresolved in all 5,699 literature rows. Not one extracted assertion. Root cause is the contract. v0.1.0 required evidence_direction as a column but no rule required it to be resolved, and nothing constrained where linkage_class came from — so it came from the query category. A fully compliant run therefore produced 168 RETAIN. Contract v0.2.0 adds the missing layer: L-RETRIEVAL / L-ASSERTION / L-DISPOSITION, six assertion elements with assertion_direction=unknown banned, identity_resolution with rule L3-00, per-endpoint admissibility, and VAL-L21..VAL-L28. L3-00 reuses the frozen LOCK-03 outcome linkage_evidence_missing rather than introducing one, because identity_unresolved belongs to LOCK-01's vocabulary. The run is rebuilt as an L-RETRIEVAL product with no network call repeated and no record discarded: 979 distinct records (the 7,067 figure was 979 replicated across 9 contexts), zero assertions, and all 369 pairs deferred — 36 under L3-00 for the four unresolvable entities, 333 under L3-01. EVGAP-02 stays open. GAP-P07 recorded, not fixed: four of the 41 frozen targets are not resolvable protein entities. Fixing the axis here would be a silent axis change. Co-Authored-By: Claude Opus 5 --- ...-08-05-evgap-02-extraction-result.zh-CN.md | 230 ++++---- .../evgap_02_crc_linkage_extraction.yaml | 521 +++++++++++++++++- ...2_CRC_LINKAGE_EXTRACTION_CONTRACT.zh-CN.md | 99 +++- logs/worklog.md | 21 + tests/test_evgap_02_crc_linkage.py | 397 +++++++++++-- 5 files changed, 1087 insertions(+), 181 deletions(-) diff --git a/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md index b2b8e60..954e25b 100644 --- a/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md +++ b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md @@ -1,152 +1,172 @@ -# Handoff:EVGAP-02 CRC linkage 抽取结果审核 +# Handoff:EVGAP-02 Tier 1 检索候选层 + 契约 v0.2.0(revision 2) - 日期:`2026-08-05` - 任务分支:`task_20260805_evgap-02-extraction-result` - 基线:`main` @ `8aa7e87` -- 授权依据:PR #61(`docs/pools/evgap_02_crc_linkage_extraction.yaml`),已 `APPROVE` 并合并;该契约 `authorises_extraction_run_after_approve: true`、`blocked_by: [contract_approval]`,条件已满足 -- 外部运行:`gen_iet_evgap_02_crc_linkage_20260805T190453Z` -- 交付物类型:**结果审核(外部运行留痕)** -- 架构变更:`NO_ARCHITECTURE_CHANGE`(本 PR 只含本 handoff 与一条 worklog) +- 本修订依据:**PR #62 审核 `REQUEST_CHANGES`** +- 外部运行:`gen_iet_evgap_02_crc_linkage_20260805T190453Z` **revision 2** +- 交付物类型:**契约修订 + 结果降级(外部运行留痕)** - 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** +- **结论:`EVGAP-02` 未解除。本运行只完成 `L-RETRIEVAL` 层。** -## 一、本次做了什么 +## 一、审核意见成立,逐条实测复核 -按 PR #61 已获批的契约执行一次 Tier 1 抽取,解除 `EVGAP-02`(LOCK-03 的证据缺口)。 +审核方指出:这些是搜索命中,不是 linkage evidence。**在改动任何东西之前先对实际文件逐条核验,全部成立。** -**这不解除 `EVGAP-01`,不授权执行 Level 01,不使任何 pair 进入 Level 02。** +1. **7,067 行全部 `evidence_direction = unknown`、`review_status = machine_retrieved_requires_human_review`。** +2. **没有任何一行带有已解析的断言字段**——实测比审核描述更彻底: + `positive_fraction_or_prevalence` 在 7,067 行中**全为空**, + `is_adc_efficacy_evidence` 全为 `false`, + `malignant_cell_attribution` 全为 `unresolved`/`not_applicable`, + 5,699 条文献行的 `protein_or_rna` 全为 `unresolved`。**一条已抽取的断言都没有。** +3. **实体消歧从未做过。** +4. **`ClinicalTrials.gov` 只用宽查询**,未解析 intervention/target/modality/indication,未验证同臂。 +5. **`GEO` 的 `db=gds` 元数据命中被登记为 A 类**(666 行)。 +6. **D 类 218 行全部 `literature_record`,prevalence 空、attribution `unresolved`**——共现而非富集。 -## 二、执行前先测可达性 +### 一处需要更正的细节 -契约把来源不可达定为**须记录的事实**而非可静默跳过,因此执行前逐个测试六个必查 endpoint,全部应答: +审核意见说 TCGA 与 HPA 的命中「被算作 A 类证据」。**实测:它们没有产生任何证据行。** +证据表的 `source_ref` 前缀只有四种:`PMC` 3,240、`PubMed` 2,459、`ClinicalTrials.gov` 702、`GEO` 666。 +两者的检索被执行并计入覆盖,但未登记为证据。 -| source class | endpoint | API | -|---|---|---| -| `peer_reviewed_literature` | PubMed、PMC | NCBI E-utilities `esearch` | -| `clinical_trial_registry` | ClinicalTrials.gov | API v2 `/studies` | -| `public_molecular_dataset` | TCGA、GEO、Human Protein Atlas | GDC genes API;E-utilities `db=gds`;HPA `search_download` | - -## 三、检索完整性 - -| 项 | 值 | -|---|---| -| 检索次数 | **451** | -| 不可达检索 | **0** | -| target 级检索完整 | **41 / 41** | -| pair 级 D 类检索完整 | **369 / 369** | +**原则完全成立,实际的实例是 `GEO`。** v0.2.0 把三个 dataset endpoint 一并禁用为 A 类依据。 -target 级按 endpoint 判定覆盖(不是按 source class),A/B/C 用类别特异术语;D 类按 pair 判定,369 个 pair 全部记录六个完整性字段。 +### 一处比审核意见更严重的发现 -## 四、LOCK-03 结果 +**`Undisclosed` 不是实体,是缺失值占位符**,却被当作基因符号检索:`PMC/A` 返回 **1,384** 条,产出 1 个 RETAIN。 -| 规则 | outcome | disposition | state | 数量 | -|---|---|---|---|---| -| `L3-02` | `linkage_evidence_exists` | RETAIN | active | **168** | -| `L3-03` | `linkage_unassessed` | DEFER | hold | **192** | -| `L3-05` | `no_known_linkage_after_complete_search` | `EXCLUDE_FROM_ACTIVE_POOL` | reactivation-eligible | **9** | +`CA19-9`(糖类抗原,无 HGNC 符号):`PMC/A` **14,200** 条,9 个 pair 中 **8 个 RETAIN**。 -168 + 192 + 9 = 369。**`L3-01` 与 `L3-04` 本次为空规则**——无检索不完整的 pair,也未采集到「仅其他癌种 precedent」的证据。 +`EDBN`:11 个 endpoint **全部 0 命中**,9 个 pair 落 `L3-05` **EXCLUDE**。 +它疑指 fibronectin 的 extra domain B(标准符号 `FN1`)—— +**被排除的唯一原因是这个缩写本身不通行于文献。消歧失败被当成了完整检索后的阴性结论。** -### active 的分布,以及 192 个 hold 的来由 +### 一处自查发现、审核未提的缺陷 -| context | active | -|---|---| -| `crc_mss_pmMR_mcrc_3l_plus`(canonical) | **40** | -| `crc_cms4_emt_oncofetal_revCSC_high` | 33 | -| `crc_mss_ras_mut_1l_2l_post_chemo_bev` | 19 | -| `crc_msi_h_dmmr_io_resistant` | 17 | -| `crc_mss_ras_mut_3l_plus` | 17 | -| `crc_braf_v600e_post_targeted` | 14 | -| `crc_her2_positive_post_treatment` | 11 | -| `crc_kras_g12c_post_targeted` | 11 | -| `crc_mss_ras_wt_antiegfr_resistant` | 6 | +**未披露的检索截断。** 451 次检索报告命中合计 **718,140**,实际登记 **979** 条; +**333/451 次检索被截断**,绝大多数每组只留 **3** 条。revision 1 未声明此上限却宣称检索完整。 -canonical context 只要有 A/B/C 证据即可 RETAIN,故 40 个靶点全部 active(41 个中只有 `EDBN` 无 A/B/C 证据)。**8 个亚群 context 必须同时有 D 类情境特异证据才能 RETAIN**——这正是那 192 个 `L3-03` 的差别所在:有疾病级 CRC 证据,但该亚群没有情境特异富集证据。 +## 二、根因在契约,不只在执行 -### 证据行 +v0.1.0 把 `evidence_direction` 与 `review_status` 列为**必需列,却没有任何一条规则要求它们被解析**。 +`linkage_class` 也没有任何规则约束其来源,于是它由**查询类别**决定。 +**一次完全合规的执行因此产出了 168 条 RETAIN。** 这是契约漏洞,修必须修在契约上。 -**7,067 行**:A 2,808 / B 2,295 / C 1,746 / D 218。`context_specific = true` 仅 D 类的 218 行——A/B/C 按契约是疾病级检索,按构造不具情境特异性。 +## 三、契约 v0.2.0 -## 五、必须原样记录的四条 +### 三层结构 -- **`MF-L01`**:LOCK-03 的 RETAIN 只表示**存在可回溯的 CRC-specific linkage 证据记录**,**不表示该靶点适合 ADC、不表示疗效、不表示治疗窗**。 -- **`MF-L02`**:C 类 1,746 行(naked antibody/CAR-T/bispecific/RIT/immunotoxin/imaging antibody)证明 target 在 CRC 中可接近或可干预,**不是 ADC 疗效证据**。全部 C 类行 `is_adc_efficacy_evidence = false`。 -- **`MF-L03`**:未使用任何派生本地数据库。**检索完整性只在 Tier 1 声明范围内成立。** ADCdb、CRC 文献库、CRC Atlas ledger、竞争格局库仍待 `SRCADM-02`..`05`;日后任一获准入需另开 PR 扩大范围并重跑。 -- **`MF-L04`**:RETAIN **不使 pair 进入 Level 02**。`EVGAP-01` 未解除前 `may_advance_to_level_02` 恒为 `false`,369 行全部如此。 +| layer | 产物 | 可支撑 LOCK-03 | +|---|---|---| +| `L-RETRIEVAL` | `retrieval_candidates` | **否** | +| `L-ASSERTION` | `linkage_assertions` | 是 | +| `L-DISPOSITION` | `pair_linkage_disposition` | 只能引用 assertion | -## 六、一条必须写明的证据强度限制 +`assertion_requirements` 规定六个构成要件(target/CRC/context 实体消歧、`relationship_type`、 +`assertion_direction`、`supporting_text_or_structured_field`),并**硬性禁止 `assertion_direction = unknown`**—— +这正是 v0.1.0 漏掉 7,067 行的那道检查。 -**全部 7,067 行 `review_status = machine_retrieved_requires_human_review`,`evidence_direction = unknown`。** +`linkage_class` 由 assertion 内容判定;候选表改记 `query_class_label`,且**候选表不得含 `linkage_class` 列**。 -这些是**按冻结查询式检索到的公开记录**——PMID、PMCID、NCT 号、GEO 登记号都可点击回溯,但**其内容未被阅读、未被人工判读**。按 PR #58 已获接受的 `DECISION-02`,machine-retrieved 证据满足 LOCK-03 的**存在性**,但携带该状态的 pair **不得晋级 Level 02**。 +### 关于 `DECISION-02` -因此 **168 个 active 的含义是「存在待复核的 CRC-specific linkage 记录」,不是「linkage 已确证」。** 这一句必须原样出现在任何引用本结果的地方。 +PR #58 允许机器抽取的证据满足 LOCK-03 existence——**前提是机器已抽出一条具体、可审计的 assertion**。 +v0.1.0 停在候选。这一读法写进了 `L-ASSERTION.machine_extraction_permitted_basis`,人工复核要求不变。 -## 七、只有一个靶点完整检索后无任何 linkage +### 实体消歧与 `L3-00` -`EDBN`,9 个 pair 全部 `L3-05`。它是非标准符号(fibronectin EDB 结构域),在 2026-08-05 的 Level 01 Preview 中也是 `E1-05`(不在 surfaceome 参考库)。 +新增 `L3-00`,置于优先级最前。关键是**不对称**:未消歧实体既不得 RETAIN, +也**绝不得** EXCLUDE——`unresolved_may_not_exclude` 直接以 `EDBN` 为例写明。 -`L3-05` 是**可逆**的:`is_scientific_disproof = false`、`is_killed = false`、状态 `reactivation-eligible`,仍留在 Eligible Universe Index。**不是科学证伪,不是淘汰。** +`L3-00` **不引入新 outcome**:LOCK-03 的词表由 PR #57 冻结,其中没有 `identity_unresolved` +(该 outcome 只属于 LOCK-01)。故 `L3-00` 复用 `linkage_evidence_missing`, +其 `evidence_state = absent_incomplete_search` 恰好正确,身份信息另由 +`identity_resolution_status` 列承载。 -## 八、一处执行者自查并修正的问题 +四个实体在 target 轴上同属 `rq_01_family_count = 0`、规则 `E1-05`, +故识别它们**不依赖自由裁量**(`mechanical_precondition`)。 -首版 evidence 表缺少契约要求的 4 列(`pair_id`、`clinical_context_id`、`context_specific`、`linkage_outcome`);而且 A/B/C 按契约是**按 target 检索一次**,导致单条证据行无法携带唯一 `pair_id`,`VAL-L16`「引用行的 `pair_id` 必须与 disposition 一致」不可满足。 +### endpoint 命中证明了什么 -这是我在设计输出时没把「按 target 检索」与「按 pair 记录证据」这两件事对齐。修正方式是**让输出符合已冻结的 schema,而不是改契约**:把 target 级记录按 pair 展开为 7,067 行 pair 级证据行,**未重复任何网络调用、未改变任何已检索事实**,只改表示形式。 +新增 `endpoint_evidence_admissibility`,逐 endpoint 写明 `hit_proves` 与 `hit_does_not_prove`。 +TCGA/HPA/GEO 三者 `admissible_as_class_a: false`, +但**仍为必查**(服务于覆盖与身份消歧),其命中不计入 `linkage_classes_hit`。 +`ClinicalTrials.gov` 须记录五个结构化字段并满足**同臂**要求。 -## 九、验证结果 +### 检索完整性 -**契约 20 条验证规则 `VAL-L01`..`VAL-L20` 逐条对实际产物核验,全部 `PASS`**,包括: +`search_complete` 现在需要四层:身份消歧、endpoint 覆盖、pair 级 D 类、**assertion 抽取完成**。 +`retrieval_alone_is_not_search_complete: true`。 -- `VAL-L01` 369 行、每行命中且仅命中一条规则、`pair_id` 唯一 -- `VAL-L02` 每行 outcome/disposition/state 与其 `rule_id` 的契约声明逐项一致 -- `VAL-L04` 全部 C 类行 `is_adc_efficacy_evidence = false` -- `VAL-L06` 全部亚群 `L3-02` 行都有非空 `class_d_evidence_refs` -- `VAL-L07` 全部 `L3-05` 行六项完整性字段齐备、`search_complete = true`、语义为 `EXCLUDE_FROM_ACTIVE_POOL`、`reactivation-eligible` -- `VAL-L09` 两张表的列集合与契约**逐表**相等 -- `VAL-L11`/`VAL-L19` 41 个 target 全部覆盖六个必查 endpoint -- `VAL-L16` 全部引用 id 存在于 evidence 表且 `pair_id` 一致,7,067 个 id 无重复 -- `VAL-L17` 逐规则引用约束全部满足 -- `VAL-L18` 369 个 pair 全部有六个 D 类字段且 `class_d_search_complete = true` -- `VAL-L20` `evidence_row_count` 全部等于三组 refs 去重后总数 +新增 `VAL-L21`..`VAL-L28`。 -仓库侧:`Ran 338 tests` 全部通过;`scripts/verify_repository_boundary.sh` 通过;运行期间仓库零写入。 +## 四、结果降级(revision 2) -## 十、明确没有做什么 +| | revision 1 | revision 2 | +|---|---|---| +| 证据表 | `pair_linkage_evidence.tsv` 7,067 行 | 撤销 → `retrieval_candidates.tsv` **979** + `linkage_assertions.tsv` **0** | +| `L3-02` RETAIN | 168 | **0** | +| `L3-03` | 192 | 0 | +| `L3-05` EXCLUDE | 9 | **0** | +| 全部 369 pair | 三分 | **全部 DEFER / hold** | -- 未运行任何 Gate、未赋任何分数、未评估 T2 或 T7。 -- 未排序、未划 Tier、未推荐资产、未给实验建议。 -- 未新增靶点或 clinical context(0/0)。 -- **未打开任何 Tier 2 派生本地数据库**;未把任何派生库纳入已批准来源。 -- 未引用被隔离运行(PR #53、#54)的任何产物。 -- **未解除 `EVGAP-01`;未更新 `adc_pool_level_01_input_binding.yaml`;Level 01 仍不可执行。** 解除 `EVGAP-02` 须待本结果获批后另开 PR。 -- 未生成 `ADC_POOL_LEVEL_01_ACCEPTED`。 -- 未补八份批准记录(#52/#53/#54/#57/#58/#59/#60/#61),事实已查全但未写文件。 +**未重复任何网络调用,未丢弃任何已检索记录。** -## 十一、下一步 +979 与 7,067 的关系:A/B/C 按 target 检索一次,revision 1 把每条记录复制到 9 个 context 上。 +`A 2808/9=312`、`B 2295/9=255`、`C 1746/9=194`、`D 218`(pair 级不复制),合计 **979**。 +**「7,067 条证据」实为 979 条记录乘以 9。** -1. 本结果 `APPROVE`。 -2. **另开 PR** 更新 Level 01 输入绑定,把本抽取产物绑为 `LOCK-03` 的来源并**解除 `EVGAP-02`**。 -3. Track B:`SRCADM-01` → `EVGAP-01` 抽取 → 结果 → binding。 -4. **两个缺口都解除后**,才能生成 `ADC_POOL_LEVEL_01_ACCEPTED`。 +369 个 pair:`L3-00` **36**(4 个不可消歧实体 × 9)、`L3-01` **333**(assertion 层未执行)。 +三组 `*_evidence_refs` 在 369 行中全空。 -## 附录 A:产物 SHA-256 +## 五、上游缺陷 GAP-P07(登记,不修) -| 文件 | SHA-256 | -|---|---| -| `pair_linkage_evidence.tsv` | `333a5c4132174075691f1952e839cd6737ccdc68c238e2b497e6f666951c8e09` | -| `pair_linkage_disposition.tsv` | `464d75d426921d73cc417c0c33d27ec0cf8408ba031fae75e2d8331b8e31a9cc` | -| `search_log.tsv` | `dd0569c572bfd09f74f034f1844811c918182c767118e963afc9ed0a14c7ce08` | -| `run_report.md` | `ef3914954f9fc9f7a0e78c50a1495dd045e79ebc1cbd4c2768a6ce8f2f05786b` | -| `external_run_worklog.md` | `878dd8f863e61467128321c2e4fd464fba163a1cdeeac48ada323e929f05a88a` | -| `source_manifest.json` | `a143d4b3e00de7d4cedb325f4abe49a00b62c27c22a8f91e68c153bc461194b9` | +PR #58 冻结的 41 个 target 中至少四个不是可消歧的蛋白实体。 +`EVGAP-02` 无权改轴——在本契约内给 `Undisclosed` 编一个身份等于静默改轴。 -运行目录:`external:result/gen_iet_evgap_02_crc_linkage_20260805T190453Z` +**binding 本身已察觉这一点**:它把 `identity_unresolved` 列入 `unavailable_outcomes`, +理由是「已批准层没有身份解析结论字段」。四个实体因此以 `E1-05` 留在轴上。 +**缺陷在 PR #58 时已可见,只是当时无法表达。** -### 供上传审核的打包 +修复须另开 PR,且会改动 41/369 这两个冻结计数。 -`external:result/gen_iet_evgap_02_crc_linkage_20260805T190453Z.zip` +## 六、产物与校验 -- ZIP SHA-256:`9c9c184e7b66e2999950831a18e059847c3b7dfd4a5b6f92ac78ac9dce259ece` -- 148,063 bytes,含且仅含上表六个文件 +外部包 **revision 2**(仓库内无任何数据文件): -(按 PR #60 第二轮裁决后我自我约束的规则:外部产物每次交付都必须同时产出带校验和的打包,只改 handoff 不算完成交付。) +| 文件 | 行数 | SHA-256 | +|---|---|---| +| `retrieval_candidates.tsv` | 979 | `09a2aa75ee7885ed9be3807d8c074e8a31fb81bef6bfff27728181831e5c326a` | +| `linkage_assertions.tsv` | 0 | `f0c83e0e2fb0aa13e354babe00164d9287c226bd6d7229c29434d664f744ee8b` | +| `pair_linkage_disposition.tsv` | 369 | `a43b47c874f9a70e3e3b4de11941ef9c87765f06bce5098247ac87d06351598d` | +| `search_log.tsv` | 451 | `dd0569c572bfd09f74f034f1844811c918182c767118e963afc9ed0a14c7ce08` | +| `run_report.md` | — | `9359ac1cc0c25ea3a3e7de06f8f5bd23270bed390db1a6fe7bdbe246e4846059` | +| `external_run_worklog.md` | — | `028a7c2f11a2b6b15b5f651c9f5826f6ee3eeee6f05cd7ebc3c2488e7c361c96` | +| `source_manifest.json` | — | `9fb1f2f75429e438d343a097eabdedeff44ae0f819cc346f71212f64b1647202` | + +**打包件**(按 PR #60 审核后确立的规则,每个修订单独出包并各带自己的 SHA-256): + +- `gen_iet_evgap_02_crc_linkage_20260805T190453Z_rev2.zip` +- SHA-256 `e8f2a7f5ce9fae25265994f0d9a1fae1e371a1bb55ccafea2026835bd5120d3d` +- 41,734 bytes,8 个条目 + +仓库内变更:契约 v0.2.0、契约文档、测试、本 handoff、一条 worklog。 +`tests/test_evgap_02_crc_linkage.py` **44 个测试**通过;全库 `Ran 353 tests` OK。 +外部产物另经 25 项 v0.2.0 规则核验,全通过。 + +## 七、边界 + +未运行任何 Gate,无评分、无排序、无 Tier、无资产推荐、无实验建议; +未新增或修改靶点与 clinical context;未读取任何 Tier 2 派生库; +未引用被隔离运行(PR #53、#54)的任何产物;未写入任何数据文件到仓库。 +369 个 pair 的 `may_advance_to_level_02` 全部 `false`。 + +## 八、后续顺序 + +1. 本 PR `APPROVE`(契约 v0.2.0 + `L-RETRIEVAL` 层产物),在 `logs/` 留审核记录。 +2. **另开 PR** 处理 `GAP-P07`:四个实体解析为标准符号、定义为非蛋白抗原,或移出轴。 +3. `GAP-P07` 处理后执行 `L-ASSERTION` 抽取 → 结果 PR。 +4. 两层齐备后才谈解除 `EVGAP-02`。 +5. `EVGAP-01` 走另一条独立轨道(`SRCADM-01` 在 PR #63 待审)。 +6. **两个缺口都解除后**,才能生成 `ADC_POOL_LEVEL_01_ACCEPTED`。 diff --git a/docs/pools/evgap_02_crc_linkage_extraction.yaml b/docs/pools/evgap_02_crc_linkage_extraction.yaml index c44650d..fcd7815 100644 --- a/docs/pools/evgap_02_crc_linkage_extraction.yaml +++ b/docs/pools/evgap_02_crc_linkage_extraction.yaml @@ -7,15 +7,41 @@ # 由 tests/test_evgap_02_crc_linkage.py 校验。 # LOCK-03 的 outcome 词表见 docs/pools/adc_pool_gate_usage.yaml(PR #57)。 # 来源文档:Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md#EVGAP-02 应该具体抽取什么 +# +# v0.2.0 修订(PR #62 审核):v0.1.0 有一个承重漏洞——它把 evidence_direction 与 +# review_status 列为必需列,却没有任何一条规则要求它们被解析。结果 0.1.0 下的一次 +# 合规执行产出了 7,067 行 evidence_direction=unknown 的检索命中,并据此判出 168 条 +# RETAIN。检索命中不是 linkage 证据。v0.2.0 把抽取拆成三层,并要求 LOCK-03 只能 +# 引用已抽取的 assertion。 extraction: extraction_id: evgap_02_crc_linkage - extraction_version: 0.1.0 + extraction_version: 0.2.0 + supersedes_version: 0.1.0 + amended_after_review_of_pr: 62 + amendment_reason: >- + v0.1.0 允许「搜索命中」直接充当 A/B/C/D 类 linkage 证据:linkage_class 由 + query 类别决定,而非由记录内容决定。因此 target、CRC 词与 modality 词在同一篇 + 全文中共现即可被登记为 precedent。v0.2.0 引入 assertion 层,把「检索到候选」 + 与「已抽取断言」分开,并把实体消歧前置为 LOCK-03 可求值的前提。 discharges_gap: EVGAP-02 gap_source: docs/pools/adc_pool_level_01_input_binding.yaml serves_lock: LOCK-03 - status: contract_only_pending_review - execution_status: not_authorized_not_executed + status: amended_pending_review + # v0.1.0 已执行一次,其产物按 PR #62 审核结论降级为 L-RETRIEVAL 层产物, + # 不构成 EVGAP-02 结果。v0.2.0 的 L-ASSERTION 与 L-DISPOSITION 尚未执行。 + execution_status: retrieval_layer_executed_assertion_layer_not_executed + gap_discharged: false + executed_runs: + - run_id: external:result/gen_iet_evgap_02_crc_linkage_20260805T190453Z + under_contract_version: 0.1.0 + layers_completed: + - L-RETRIEVAL + layers_not_completed: + - L-ASSERTION + - L-DISPOSITION + downgraded_by_review_of_pr: 62 + discharges_gap: false # 与 EVGAP-01 不同:本抽取不依赖任何未获准入的派生数据库, # Tier 1 原始公开来源即可支撑,故获 APPROVE 后即可执行。 authorises_extraction_run_after_approve: true @@ -31,6 +57,215 @@ extraction: LOCK-03 问的是「target 为什么与这个 CRC clinical context 有关」, 与表面拓扑无关,因此不读取 surfaceome 参考库,也不受其准入状态影响。 +# ---------------------------------------------------------------- 三层结构 +# +# v0.1.0 只有「检索」与「判定」两步,中间缺一层。缺的这一层正是「这条记录到底 +# 断言了什么」。没有它,linkage_class 只能由 query 类别决定,于是分类器就是查询词本身。 +extraction_layers: + - layer_id: L-RETRIEVAL + name: candidate_retrieval + unit: target(A/B/C)与 pair(D) + produces: retrieval_candidates + # 这一层的产物永远不能单独支撑 LOCK-03。 + may_support_lock_03: false + may_be_referenced_by_disposition: false + what_it_proves: >- + 在该 endpoint 上,给定 query 表达式返回了这条记录。仅此而已。 + 它不证明该记录谈及本 target,也不证明其中的 CRC 语义与本 target 有关。 + known_false_positive_modes: + - 缩写碰撞(符号在正文中指代其他事物) + - 全文命中(target 与 CRC 词分别出现在正文不同段落、彼此无关) + - 参考文献命中(命中来自文末引文而非正文主张) + - 综述背景命中(modality 词属于其他 target 的 precedent) + - "免责声明命中(PR #58 已记录:33 条 statement 全含 CRC,但只出现在 + 「precedent does not establish CRC efficacy」这句免责声明里)" + - layer_id: L-ASSERTION + name: assertion_extraction + unit: assertion + consumes: retrieval_candidates + produces: linkage_assertions + may_support_lock_03: true + # 一条 assertion 必须已经把主体、癌种、关系、方向与出处落到具体字段上。 + requires_all_of: assertion_requirements + machine_extraction_permitted: true + machine_extraction_permitted_basis: >- + PR #58 的 DECISION-02 允许机器抽取的证据满足 LOCK-03 existence, + 但其前提是机器**已经抽出一条具体、可审计的 linkage assertion**, + 而不是「返回了一批候选记录」。v0.1.0 的执行停在候选,未做抽取。 + human_review_still_required: true + human_review_scope: 每条 assertion 的 review_status 仍为待人工复核 + - layer_id: L-DISPOSITION + name: lock_03_disposition + unit: pair + consumes: linkage_assertions + may_only_reference: linkage_assertions + may_reference_retrieval_candidates: false + reason: >- + supporting_evidence_refs 若可指向纯检索命中,L3-02 就退化为 + 「query 有返回」。这正是 v0.1.0 实际发生的事。 + +# --------------------------------------------------------- assertion 的构成要件 +# +# 任一要件缺失,该记录停留在 L-RETRIEVAL,不得升级为 assertion。 +assertion_requirements: + required_elements: + - element: target_entity_resolved + meaning: 记录中的主体已消歧到本 pair 的 target,而不仅是字符串匹配 + - element: crc_entity_resolved + meaning: 记录中的癌种已消歧为 CRC,而不是「正文出现 colorectal 一词」 + - element: context_entity_resolved + meaning: D 类必需——亚群语义已消歧到本 pair 的 clinical context + - element: relationship_type + meaning: target 与 CRC/context 之间的关系类型,取自 relationship_types + - element: assertion_direction + meaning: 关系的方向,不得为 unknown + - element: supporting_text_or_structured_field + meaning: 支撑该断言的具体原文段落,或具体结构化字段值 + relationship_types: + - expressed_in + - enriched_in + - retained_in + - targeted_by_modality_in + - clinically_evaluated_in + # 这是 v0.1.0 缺失的那一条硬性要求。 + direction_must_be_resolved: true + direction_unknown_forbidden_for_lock_03: true + direction_unknown_consequence: 该记录停留在 retrieval_candidate,不得进入任何 refs + # 共现不是断言。 + cooccurrence_is_not_an_assertion: true + cooccurrence_examples_rejected: + - target 与 CRC 词共现于同一全文 + - target、CRC 与 "ADC" 共现于同一全文 + - target 与 BRAF/HER2/CMS4 等亚群词共现于同一全文 + examples_of_valid_assertions: + - "CEACAM5 protein was detected in 72% of CRC tumours" + - "A CEACAM5 ADC was tested in a CRC xenograft model" + examples_of_invalid_assertions: + - "一篇综述同时提到本 target、colorectal 与 antibody-drug conjugate" + - "GDC gene index 中存在该 gene symbol" + +# ---------------------------------------------------------------- 实体消歧 +# +# v0.1.0 直接把 target 轴的字符串塞进 query。实测后果:字符串 "Undisclosed" +# 被当作基因符号检索,PMC 返回 1,384 条,并使 1 个 pair 判出 RETAIN。 +identity_resolution: + required_before_search: true + resolution_must_record: + - input_symbol + - resolved_identifier_kind + - resolved_identifier + - synonyms_used + - resolution_status + - resolution_basis + resolved_identifier_kinds: + - hgnc_gene_symbol + - uniprot_accession + - non_protein_antigen_definition + resolution_status_vocabulary: + - resolved + - resolved_as_non_protein_antigen + - unresolvable_placeholder + - unresolvable_ambiguous_abbreviation + # 未消歧的实体不得进入检索,更不得由检索结果推出任何 LOCK-03 结论。 + unresolved_may_not_be_searched: true + unresolved_lock_03_rule: L3-00 + # 关键不对称:未消歧实体既不得 RETAIN,也**绝不得** EXCLUDE。 + unresolved_may_not_retain: true + unresolved_may_not_exclude: true + unresolved_may_not_exclude_reason: >- + 零命中若源于符号无法消歧,那是消歧失败,不是「完整检索后无 linkage」。 + 实测 v0.1.0 正是在此处出错:EDBN 在全部 11 个 endpoint 返回 0 条, + 于是 9 个 pair 落 L3-05 EXCLUDE。而 EDBN 应指 fibronectin 的 extra domain B, + 其标准符号为 FN1;被排除的原因只是缩写本身不出现在文献里。 + # 本次 target 轴中实测四个必须单列的实体。 + known_unresolved_entities: + - input_symbol: Undisclosed + resolution_status: unresolvable_placeholder + note: 这不是实体,是缺失值占位符。v0.1.0 对其执行了 11 次检索。 + - input_symbol: EDBN + resolution_status: unresolvable_ambiguous_abbreviation + note: 疑指 fibronectin extra domain B(标准符号 FN1),须由来源轴确认后才可检索。 + - input_symbol: AG7 + resolution_status: unresolvable_ambiguous_abbreviation + note: 非标准靶点符号;PMC 返回 56 条,全部为缩写碰撞候选。 + - input_symbol: CA19-9 + resolution_status: resolved_as_non_protein_antigen + note: >- + sialyl-Lewis a 糖类抗原,非蛋白基因产物,无 HGNC 符号。 + 它可被消歧,但不能按基因符号检索;PMC 返回 14,200 条。 + # 消歧规则的可判定性:四个实体在 target 轴中的 LOCK-01 状态一致,可机械识别。 + mechanical_precondition: >- + 这四个实体在 PR #58 的 target 轴中同属 rq_01_family_count = 0、rule E1-05 + (参考库无对应行)。故「无任何表面注释」与「符号无法消歧」在本轴上高度相关, + 可先按该字段筛出候选再逐个消歧,不依赖自由裁量。 + +# ------------------------------------------------- 各 endpoint 的命中证明了什么 +# +# v0.1.0 未声明这一层,于是「endpoint 返回一条记录」被一律当作该类证据。 +endpoint_evidence_admissibility: + - endpoint: PubMed + hit_proves: 该 query 在题录索引中匹配到该条记录 + hit_does_not_prove: 该记录断言本 target 与 CRC 有关 + assertion_extraction_source: title_and_abstract + requires_assertion_extraction: true + - endpoint: PMC + hit_proves: 该 query 的词项出现在该全文中的某处 + hit_does_not_prove: 这些词项彼此相关,或都指代本 target + assertion_extraction_source: matched_passage + requires_assertion_extraction: true + additional_requirement: 必须记录命中所在段落,且该段落须同时含主体与癌种语义 + - endpoint: ClinicalTrials.gov + hit_proves: 该 query 匹配到该试验记录 + hit_does_not_prove: 本 target 是干预靶点,或 CRC 是实际纳入适应症 + requires_assertion_extraction: true + # 宽查询(" colorectal")返回的记录不得直接进入 B/C 类。 + required_structured_fields: + - intervention_name + - intervention_target + - intervention_modality + - conditions + - arm_group_assignment + same_arm_requirement: true + same_arm_requirement_note: target 与 indication 必须属于同一试验臂,否则不成立 + - endpoint: TCGA + query_kind: GDC gene index lookup + hit_proves: GDC gene index 中存在该 gene symbol + hit_does_not_prove: + - CRC 肿瘤表达 + - tumour > normal + - prevalence + - malignant-cell attribution + admissible_as_class_a: false + admissible_as_class_a_reason: gene 存在性与表达无关,属身份查询而非证据查询 + permitted_role: identity_resolution_support_only + - endpoint: Human Protein Atlas + query_kind: search index lookup + hit_proves: HPA 中存在该 target 的页面 + hit_does_not_prove: CRC 人体肿瘤表达 + admissible_as_class_a: false + permitted_role: identity_resolution_support_only + - endpoint: GEO + query_kind: db=gds metadata search + hit_proves: 该数据集的标题或摘要匹配了 query 词项 + hit_does_not_prove: 该数据集中本 target 在 CRC 中表达或富集 + admissible_as_class_a: false + admissible_as_class_a_reason: >- + 数据集元数据匹配不是表达测量。要作为 A 类,必须抽取该数据集内本 target 的 + 具体表达结论(含方向),并按 RNA 标注。 + permitted_role: candidate_dataset_identification_only + rna_labelling_still_required: true + +# 三个 dataset endpoint 仍必查——它们服务于检索完整性与身份消歧, +# 但其命中不得计入 linkage_classes_hit。 +dataset_endpoint_handling: + dataset_endpoints: + - TCGA + - Human Protein Atlas + - GEO + still_required_for_coverage: true + hits_excluded_from_linkage_classes_hit: true + hits_may_not_be_sole_basis_for_class_a: true + # ---------------------------------------------------------------- 来源分层 # # 治理事实:仓库内 logs/chatgpt-review-*.md 中**没有任何一条**提及过任何本地 @@ -154,6 +389,23 @@ scope: D 类情境特异性富集必须按 pair 判定。 # ------------------------------------------------- 四类 linkage 判据(来源文档) +# +# v0.2.0:四类都是 assertion 的分类,不是 query 的分类。 +linkage_class_common_requirements: + applies_to_all_classes: true + requires_assertion: true + classified_by: assertion_content + classified_by_query_category_forbidden: true + classified_by_query_category_forbidden_reason: >- + v0.1.0 用 query 类别给记录贴 class:命中「class B 查询」即记为 B 类 precedent。 + 于是「文章里同时出现 target、CRC 与 ADC」被当成「该 target 在 CRC 中有 ADC precedent」。 + 命中可能来自综述、参考文献、背景介绍、别的 target 的 ADC,或 ADC 是别的缩写。 + minimum_fields_per_assertion: + - relationship_type + - assertion_direction + - supporting_text_or_structured_field + direction_unknown_admissible: false + linkage_classes: - class_id: A name: crc_human_tumor_expression @@ -217,7 +469,7 @@ linkage_classes: # 未闭合。本节正是使该 outcome 变为可用的前提:范围一旦冻结, # 「是否完成规定检索」就成为可判定的事实,而不是执行者的自我声明。 declared_search_scope: - scope_version: 0.1.0 + scope_version: 0.2.0 per_target_required_source_classes: - peer_reviewed_literature - clinical_trial_registry @@ -262,14 +514,26 @@ declared_search_scope: # 「完成规定检索」的定义。任一条不满足即为未完成。 search_complete_definition: + - 每个 target 的 input_symbol 都已完成 identity_resolution 且 resolution_status = resolved + 或 resolved_as_non_protein_antigen - 每个 target 都对 per_target_required_source_classes 的每一类的**每一个 endpoint** 执行过检索 - 每次检索都记录了 query_expression、executed_at、result_count 与 reachable - 每个 pair 都完成了 per_pair_required_class_d_search 并记录其六个字段 - 所有 reachable = false 的 endpoint 与 D 类检索都被显式记录为未完成,而非静默跳过 - search_complete_requires_both_levels: true + - 该 pair 的全部 retrieval_candidates 都已过 L-ASSERTION 层,即每条候选都已判定为 + 「已抽出 assertion」或「无法抽出 assertion」,不得有未判定的候选 + search_complete_requires_both_levels: false + search_complete_requires_all_levels: true search_complete_levels: + - target_level_identity_resolution - target_level_source_class_and_endpoint_coverage - pair_level_class_d_coverage + - pair_level_assertion_extraction_completed + # v0.2.0 新增:仅完成检索不等于完成本抽取。 + retrieval_alone_is_not_search_complete: true + retrieval_alone_is_not_search_complete_reason: >- + L3-05 EXCLUDE 的前提是「已完整检索且四类均无命中」。若 assertion 层未做, + 「无命中」只是「未判定」。v0.1.0 的 9 条 L3-05 全部出自这一混淆。 unreachable_source_consequence: search_incomplete_for_that_target unreachable_class_d_consequence: search_incomplete_for_that_pair silent_skip_forbidden: true @@ -279,31 +543,55 @@ declared_search_scope: # 五条规则必须覆盖全部 pair 且互斥。优先级先判检索完整性, # 因为检索未完成时其余判据都不可信。 derivation_precedence: + - L3-00 - L3-01 - L3-02 - L3-03 - L3-04 - L3-05 precedence_rationale: >- - 先判检索是否完成——未完成时「没找到」无法与「不存在」区分; + 先判实体是否消歧——符号未消歧时检索结果既不能支持也不能排除; + 再判检索与 assertion 抽取是否完成——未完成时「没找到」无法与「不存在」区分; 再判是否存在与该 context 匹配的 CRC-specific 证据; 再判疾病级证据遇亚群 context 的降级;再判仅有其他癌种 precedent 的降级; 以上都不成立且检索已完成,才允许判定完整检索后无 linkage。 derivation_rules: + # v0.2.0 新增,且必须位于最前。 + - id: L3-00 + condition: >- + 该 pair 的 target input_symbol 的 identity_resolution 结果为 + unresolvable_placeholder 或 unresolvable_ambiguous_abbreviation + lock_03_outcome: linkage_evidence_missing + disposition: DEFER + resulting_state: hold + reason: >- + 符号未消歧时,检索命中可能来自缩写碰撞,零命中可能来自符号本身不通行。 + 两个方向都不可采信,故只能 DEFER。 + # LOCK-03 的 outcome 词表由 PR #57 冻结,其中没有 identity_unresolved + # (该 outcome 只属于 LOCK-01)。故本规则复用 linkage_evidence_missing, + # 其 evidence_state = absent_incomplete_search 恰好正确, + # 身份信息另由 identity_resolution_status 列承载,不新增 outcome。 + outcome_vocabulary_note: reuses_lock_03_frozen_vocabulary + does_not_introduce_new_outcome: true + distinguishing_column: identity_resolution_status + may_never_become_l3_05: true - id: L3-01 condition: >- - 未按 declared_search_scope 完成规定检索——target 级 endpoint 覆盖未齐, - **或**该 pair 的 D 类检索未完成 + 实体已消歧,但未按 declared_search_scope 完成规定工作——target 级 endpoint + 覆盖未齐,**或**该 pair 的 D 类检索未完成,**或**该 pair 的 assertion 抽取未完成 lock_03_outcome: linkage_evidence_missing disposition: DEFER resulting_state: hold - reason: 检索未完成时「没找到」不能与「不存在」区分。 - covers_both_completeness_levels: true + reason: 检索或 assertion 抽取未完成时「没找到」不能与「不存在」区分。 + covers_all_completeness_levels: true - id: L3-02 condition: >- - 存在 A/B/C 任一类的 CRC-specific 证据,且 context 为 canonical_c0, - 或 context 为亚群且同时存在 D 类情境特异性证据 + 存在 A/B/C 任一类的 CRC-specific **assertion**,且 context 为 canonical_c0, + 或 context 为亚群且同时存在 D 类情境特异性 assertion + requires_assertion: true + requires_direction_resolved: true + may_reference_retrieval_candidates: false lock_03_outcome: linkage_evidence_exists disposition: RETAIN resulting_state: active @@ -324,9 +612,14 @@ derivation_rules: retained_as: target_modality_metadata - id: L3-05 condition: >- - 已按 declared_search_scope 完成规定检索(含该 pair 的 D 类检索), - 且四类 linkage 均无命中 + 实体已消歧,已按 declared_search_scope 完成规定检索(含该 pair 的 D 类检索) + **且已完成 assertion 抽取**,四类 linkage 均无 assertion requires_class_d_search_complete: true + requires_identity_resolved: true + requires_assertion_extraction_complete: true + zero_hits_from_unresolved_symbol_forbidden: true + zero_hits_from_unresolved_symbol_forbidden_note: >- + 零命中若源于符号无法消歧,必须落 L3-00,不得落本规则。 lock_03_outcome: no_known_linkage_after_complete_search disposition: EXCLUDE disposition_semantics: EXCLUDE_FROM_ACTIVE_POOL @@ -360,31 +653,101 @@ predicted_result_shape: # ---------------------------------------------------------------- 输出 output_schema: - # 每条证据一行。来源文档规定的 13 列为必需最小集。 - # evidence_id 使每条 disposition 都能一对一回溯到支持它的具体证据行。 - evidence_row_key: evidence_id - evidence_id_unique: true - evidence_columns: - - evidence_id + # v0.2.0:三张表,对应三层。前两张表不可混用。 + tables: + - table: retrieval_candidates + layer: L-RETRIEVAL + may_be_referenced_by_disposition: false + - table: linkage_assertions + layer: L-ASSERTION + may_be_referenced_by_disposition: true + - table: pair_linkage_disposition + layer: L-DISPOSITION + + # 第一层产物。它是检索审计记录,不是证据表。 + retrieval_candidate_row_key: candidate_id + retrieval_candidate_columns: + - candidate_id + - target_input_symbol + - queried_symbol + - source_class + - endpoint + - query_expression + - query_class_label + - source_ref + - source_locator + - result_rank + - retrieved_at + # 这四列把「候选」与「证据」的界限写进 schema 本身。 + - record_status + - linkage_validated + - may_support_lock_03 + - assertion_extraction_status + retrieval_candidate_fixed_values: + record_status: retrieved_candidate + linkage_validated: false + may_support_lock_03: false + # query_class_label 只记录该候选由哪一类查询检出,**不是** linkage_class。 + query_class_label_is_not_linkage_class: true + assertion_extraction_status_vocabulary: + - not_attempted + - assertion_extracted + - no_assertion_extractable + # 第二层产物。只有这张表的行可以被 disposition 引用。 + assertion_row_key: assertion_id + assertion_id_unique: true + assertion_columns: + - assertion_id + - candidate_id - pair_id - target - clinical_context_id - evidence_type + - linkage_outcome + - target_entity_resolved + - crc_entity_resolved + - context_entity_resolved + - relationship_type + - assertion_direction + - supporting_text_or_structured_field + - source_section_or_field + - extraction_method + - extraction_confidence + - review_status + - linkage_class - crc_specific - context_specific - human_or_model - protein_or_rna - source_ref - source_locator - - evidence_direction - - review_status - - linkage_outcome - # 以下为可审计性所需的补充列。 - - linkage_class - is_adc_efficacy_evidence - positive_fraction_or_prevalence - malignant_cell_attribution - retrieved_at + assertion_forbidden_values: + assertion_direction: + - unknown + - "" + assertion_must_cite_its_candidate: true + assertion_must_cite_its_candidate_reason: >- + candidate_id 使每条 assertion 都能回溯到它是从哪一次检索的哪一条记录抽出的, + 审核方因此可以独立复核抽取是否成立。 + + # v0.2.0 起「evidence 表」即 linkage_assertions 表,其列为 assertion_columns。 + # 既有验证规则中的「evidence 行」一律指 assertion 行。 + evidence_table_is_assertion_table: true + evidence_table_name: linkage_assertions + evidence_columns_alias: assertion_columns + evidence_row_key: assertion_id + evidence_id_unique: true + # v0.1.0 → v0.2.0 的列改名,避免两版列名被混用。 + renamed_from_v0_1_0: + - from: evidence_id + to: assertion_id + - from: evidence_direction + to: assertion_direction + change: 不再允许 unknown # 每个 pair 一行的 disposition 表。 disposition_columns: - pair_id @@ -405,6 +768,12 @@ output_schema: - search_policy_version - provenance_kind - evidence_row_count + # v0.2.0 新增:承载 L3-00 与 assertion 层完整性,使二者可逐 pair 审计。 + - identity_resolution_status + - resolved_identifier + - assertion_extraction_complete + - retrieval_candidate_count + - assertion_count # 三组引用,使每条 disposition 可逐条审计其依据。 - supporting_evidence_refs - class_d_evidence_refs @@ -421,12 +790,23 @@ output_schema: - may_advance_to_level_02 # 每条规则必须引用什么、必须不引用什么。这是 disposition 与 evidence 之间的 # 一对一审计关系;仅有 evidence_row_count 无法证明某条 disposition 由哪些证据支持。 + # v0.2.0:所有 refs 一律指向 linkage_assertions 表的 assertion_id, + # 指向 retrieval_candidates 的 candidate_id 即为验证失败。 + refs_point_to: assertion_id + refs_pointing_to_candidate_id_forbidden: true evidence_reference_requirements: + - rule_id: L3-00 + supporting_evidence_refs: must_be_empty + class_d_evidence_refs: must_be_empty + other_cancer_evidence_refs: must_be_empty + note: >- + 实体未消歧时,任何命中都可能是缩写碰撞,不得引用为依据; + 零命中也不得据以排除。 - rule_id: L3-01 supporting_evidence_refs: must_be_empty class_d_evidence_refs: must_be_empty other_cancer_evidence_refs: may_be_empty - note: 检索未完成时不得伪造任何 evidence refs。 + note: 检索或 assertion 抽取未完成时不得伪造任何 evidence refs。 - rule_id: L3-02 context_kind: canonical supporting_evidence_refs: at_least_one_of_class_a_b_or_c @@ -527,9 +907,10 @@ output_validation: 不得用两张表列的并集代替逐表检查。 - id: VAL-L16 rule: >- - 每条 evidence 行必须有唯一 evidence_id;每条 disposition 行的 + 每条 assertion 行必须有唯一 assertion_id;每条 disposition 行的 supporting_evidence_refs、class_d_evidence_refs、other_cancer_evidence_refs - 引用的每个 id 都必须存在于 evidence 表,且所引用行的 pair_id 与该 disposition 一致。 + 引用的每个 id 都必须存在于 linkage_assertions 表, + 且所引用行的 pair_id 与该 disposition 一致。 - id: VAL-L17 rule: >- 每条 disposition 必须满足 evidence_reference_requirements 中其 rule_id 与 @@ -549,6 +930,47 @@ output_validation: rule: >- evidence_row_count 必须等于该 pair 三组 refs 去重后的总条数, 不得只报计数而不给 refs。 + # ---------------- v0.2.0 新增:三层结构与实体消歧的验证规则 + - id: VAL-L21 + rule: >- + 每条 assertion 行的 assertion_direction 不得为 unknown 或空。 + 出现一条即为验证失败——这是 v0.1.0 未设、并因此漏掉 7,067 行的那道检查。 + - id: VAL-L22 + rule: >- + 每条 assertion 行必须同时非空:target_entity_resolved、crc_entity_resolved、 + relationship_type、assertion_direction、supporting_text_or_structured_field、 + source_section_or_field;D 类另需 context_entity_resolved 非空。 + - id: VAL-L23 + rule: >- + supporting_evidence_refs、class_d_evidence_refs、other_cancer_evidence_refs + 引用的每个 id 都必须是 linkage_assertions 表的 assertion_id。 + 任一 id 只存在于 retrieval_candidates 表即为验证失败。 + - id: VAL-L24 + rule: >- + retrieval_candidates 表每行必须 record_status = retrieved_candidate、 + linkage_validated = false、may_support_lock_03 = false, + 且 assertion_extraction_status 取自其词表。 + - id: VAL-L25 + rule: >- + linkage_class 必须可由该 assertion 的 relationship_type 与内容判定; + 仅凭 query_class_label 赋 linkage_class 即为验证失败。 + retrieval_candidates 表不得含 linkage_class 列。 + - id: VAL-L26 + rule: >- + TCGA、Human Protein Atlas 与 GEO 三个 endpoint 的命中不得计入 + linkage_classes_hit,也不得作为 A 类 assertion 的唯一依据; + 但它们的检索记录仍必须齐备,缺任一仍触发 L3-01。 + - id: VAL-L27 + rule: >- + 每个 target 的 input_symbol 必须有 identity_resolution 记录。 + resolution_status 为 unresolvable_placeholder 或 + unresolvable_ambiguous_abbreviation 的 target,其全部 9 个 pair 必须落 L3-00, + 不得落 L3-02,**尤其不得落 L3-05**。 + - id: VAL-L28 + rule: >- + ClinicalTrials.gov 来源的 B/C 类 assertion 必须记录 + required_structured_fields 的五个字段,且 target 与 indication 属于同一试验臂; + 仅有「symbol + colorectal」宽查询命中不得成为 B/C 类 assertion。 - id: VAL-L10 rule: 不得读取 tier_2_derived_local_databases 中的任何数据库。 - id: VAL-L11 @@ -584,6 +1006,42 @@ mandatory_findings: finding: >- LOCK-03 RETAIN 不使 pair 进入 Level 02。EVGAP-01 未解除前 may_advance_to_level_02 恒为 false。 + - id: MF-L05 + finding: >- + 检索命中不是 linkage 证据。报告必须分别给出 retrieval_candidate_count 与 + assertion_count;只报候选数即冒充证据量。 + - id: MF-L06 + finding: >- + 落 L3-00 的 pair 既未被支持也未被排除。报告不得把它们与 L3-05 + 合并计数,也不得表述为「无 linkage」。 + +# --------------------------------------------- 上游缺陷:target 轴含不可消歧实体 +# +# 本节只登记,不修复——target 轴由 PR #58 的 binding 冻结, +# 修复须另开 PR,不得在 EVGAP-02 内静默改轴。 +upstream_defect: + id: GAP-P07 + defect: PR #58 冻结的 41 个 target 中至少四个不是可消歧的蛋白实体 + entities: + - Undisclosed + - EDBN + - AG7 + - CA19-9 + where: external:result/gen_iet_crc_target_enumeration_20260802#target_evidence_catalog.tsv + frozen_by_pr: 58 + why_not_fixed_here: >- + EVGAP-02 无权新增或修改 target 与 clinical context(见 not_authorised)。 + 在本契约内给 Undisclosed 编一个身份,等于静默改轴。 + binding_already_knew: >- + binding 的 lock_01 规则明确把 identity_unresolved 列为 unavailable_outcomes, + 理由是「已批准层没有身份解析结论字段,无法区分身份未解析与注释不可得」。 + 也就是说该缺陷在 PR #58 时已被察觉但无法表达,于是四个实体以 + rq_01_family_count = 0、rule E1-05 的形式留在轴上。 + interim_handling: 本抽取按 L3-00 处理,全部 DEFER,既不 RETAIN 也不 EXCLUDE + requires_separate_pr: true + separate_pr_scope: >- + 在 target 轴层面决定这四个实体的归属:解析为标准符号、定义为非蛋白抗原、 + 或从轴上移除。移除会改变 41/369 这两个冻结计数,故必须经审核。 not_authorised: - 执行 Level 01 @@ -594,3 +1052,10 @@ not_authorised: - 新增靶点或 clinical context - 任何筛选排序、Tier 划分、资产推荐或实验建议 - 把被隔离运行(PR #53、#54)的任何产物引入 + # v0.2.0 新增 + - 把 retrieval_candidates 表的行引用为 LOCK-03 的依据 + - 用 query 类别给记录赋 linkage_class + - 让 assertion_direction = unknown 的记录支撑任何 disposition + - 把 TCGA/HPA/GEO 的命中当作 CRC 表达证据 + - 为 target 轴上不可消歧的实体编造身份,或据其零命中作出排除 + - 修改 target 轴以修复 GAP-P07(须另开 PR) diff --git a/docs/tasks/EVGAP_02_CRC_LINKAGE_EXTRACTION_CONTRACT.zh-CN.md b/docs/tasks/EVGAP_02_CRC_LINKAGE_EXTRACTION_CONTRACT.zh-CN.md index adc7070..b74ce66 100644 --- a/docs/tasks/EVGAP_02_CRC_LINKAGE_EXTRACTION_CONTRACT.zh-CN.md +++ b/docs/tasks/EVGAP_02_CRC_LINKAGE_EXTRACTION_CONTRACT.zh-CN.md @@ -4,8 +4,67 @@ - 前置工作包:PR #57(Level 01 判据定义)、#58(输入绑定与缺口登记)、#59(EVGAP-01 抽取契约),均已 `APPROVE` 并合并 - 机器可读绑定:[`../pools/evgap_02_crc_linkage_extraction.yaml`](../pools/evgap_02_crc_linkage_extraction.yaml),由 `tests/test_evgap_02_crc_linkage.py` 校验 - 来源文档:`Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md` 的 `# EVGAP-02 应该具体抽取什么` 与 `# EVGAP-02 最小结果标准`(只读取,未修改) -- 当前状态:**contract-only,未执行,等待 ChatGPT 审核** -- 授权范围:**获 `APPROVE` 后即可执行一次抽取。不授权执行 Level 01,不解除 `EVGAP-01`。** +- 当前状态:**v0.2.0,`L-RETRIEVAL` 层已执行,`L-ASSERTION` 层未执行,等待 ChatGPT 审核** +- 授权范围:**不授权执行 Level 01,不解除 `EVGAP-01`,`EVGAP-02` 亦未解除。** + +## 〇、v0.2.0 修订(PR #62 审核) + +v0.1.0 有一个承重漏洞:它把 `evidence_direction` 与 `review_status` 列为**必需列, +却没有任何一条规则要求它们被解析**;`linkage_class` 也没有任何规则约束其来源。 +于是一次**完全合规**的执行产出了 7,067 行 `evidence_direction = unknown` 的检索命中, +并据此判出 **168 条 RETAIN、9 条 EXCLUDE**。 + +**检索命中不是 linkage 证据。** 修必须修在契约上,因为执行并未违约。 + +### 三层结构 + +| layer | 产物 | 可支撑 LOCK-03 | +|---|---|---| +| `L-RETRIEVAL` | `retrieval_candidates` | **否** | +| `L-ASSERTION` | `linkage_assertions` | 是 | +| `L-DISPOSITION` | `pair_linkage_disposition` | 只能引用 assertion | + +`assertion_requirements` 规定六个构成要件:target/CRC/context 实体消歧、 +`relationship_type`、`assertion_direction`、`supporting_text_or_structured_field`。 +**硬性禁止 `assertion_direction = unknown`**——这正是 v0.1.0 漏掉 7,067 行的那道检查(`VAL-L21`)。 + +`linkage_class` 由 assertion 内容判定;候选表改记 `query_class_label`, +且**候选表不得含 `linkage_class` 列**(`VAL-L25`)。 + +### `DECISION-02` 的正确读法 + +PR #58 允许机器抽取的证据满足 LOCK-03 existence,**前提是机器已抽出一条具体、可审计的 assertion**—— +主体、癌种、关系、方向、出处俱全。v0.1.0 停在候选检索。人工复核要求不变。 + +### 实体消歧与 `L3-00` + +新增 `L3-00` 置于优先级最前。关键在**不对称**:未消歧实体既不得 RETAIN,也**绝不得** EXCLUDE。 + +实测四个必须单列的实体:`Undisclosed`(缺失值占位符,被当基因符号检索,PMC 返回 1,384 条)、 +`CA19-9`(糖类抗原,无 HGNC 符号,PMC 14,200 条)、`AG7`、 +`EDBN`(11 个 endpoint 全部 0 命中,因而被 `L3-05` 排除—— +**排除的唯一原因是这个缩写不通行于文献**)。 + +`L3-00` **不引入新 outcome**:LOCK-03 的词表由 PR #57 冻结,其中没有 `identity_unresolved` +(该 outcome 只属于 LOCK-01)。故复用 `linkage_evidence_missing`, +身份信息另由 `identity_resolution_status` 列承载。 + +### endpoint 命中证明了什么 + +新增 `endpoint_evidence_admissibility`。TCGA/HPA/GEO 三者 `admissible_as_class_a: false` +(gene index 存在性、页面存在性、数据集元数据匹配都不是表达证据), +但**仍为必查**,服务于覆盖与身份消歧,其命中不计入 `linkage_classes_hit`(`VAL-L26`)。 +`ClinicalTrials.gov` 须记录五个结构化字段并满足**同臂**要求(`VAL-L28`)。 + +### 检索完整性 + +`search_complete` 现需四层:身份消歧、endpoint 覆盖、pair 级 D 类、**assertion 抽取完成**。 +`retrieval_alone_is_not_search_complete: true`。 + +### 上游缺陷 `GAP-P07` + +target 轴上四个实体不可消歧。`EVGAP-02` 无权改轴,故**只登记不修复**—— +在本契约内给 `Undisclosed` 编一个身份等于静默改轴。修复须另开 PR。 ## 目的 @@ -115,13 +174,15 @@ A/B/C 三类的疾病级检索对同一 target 在 9 个 context 下结果 ## 五、LOCK-03 求值规则与冻结优先级 -优先级:`L3-01` → `L3-02` → `L3-03` → `L3-04` → `L3-05`。 +优先级(v0.2.0):`L3-00` → `L3-01` → `L3-02` → `L3-03` → `L3-04` → `L3-05`。 -理由:**先判检索是否完成——未完成时「没找到」无法与「不存在」区分**;再判是否存在与该 context 匹配的 CRC-specific 证据;再判疾病级证据遇亚群 context 的降级;再判仅有其他癌种 precedent 的降级;以上都不成立且检索已完成,才允许判定完整检索后无 linkage。 +理由:**先判实体是否消歧——符号未消歧时检索结果既不能支持也不能排除**; +再判检索与 assertion 抽取是否完成——未完成时「没找到」无法与「不存在」区分;再判是否存在与该 context 匹配的 CRC-specific 证据;再判疾病级证据遇亚群 context 的降级;再判仅有其他癌种 precedent 的降级;以上都不成立且检索已完成,才允许判定完整检索后无 linkage。 | ID | 条件 | outcome | disposition | state | |---|---|---|---|---| -| `L3-01` | 未完成规定检索 | `linkage_evidence_missing` | DEFER | hold | +| `L3-00` | target 符号不可消歧 | `linkage_evidence_missing` | DEFER | hold | +| `L3-01` | 未完成规定检索或 assertion 抽取 | `linkage_evidence_missing` | DEFER | hold | | `L3-02` | 有 A/B/C 任一 CRC-specific 证据,且 context 为 canonical,或亚群且有 D 类证据 | `linkage_evidence_exists` | **RETAIN** | active | | `L3-03` | 有 CRC 疾病级证据,但 context 为亚群且无 D 类 | `linkage_unassessed` | DEFER | hold | | `L3-04` | 仅有其他癌种 precedent | `linkage_unassessed` | DEFER | hold | @@ -129,7 +190,9 @@ A/B/C 三类的疾病级检索对同一 target 在 9 个 context 下结果 **只有 `L3-02` 可以 RETAIN,只有 `L3-05` 可以 EXCLUDE**,测试断言各自恰好一条。`L3-05` 的 EXCLUDE 语义严格限定为 `EXCLUDE_FROM_ACTIVE_POOL`:`is_scientific_disproof: false`、`is_killed: false`、`retained_in_eligible_universe_index: true`,并须六项检索完整性字段齐备。 -测试用参考实现穷举 `search_complete × crc_specific × canonical × class_d × other_cancer` 全部 **32 种组合**,证明每种恰好命中一条规则,且五条规则都可达。 +测试用参考实现穷举 `identity_resolved × search_complete × crc_specific × canonical × class_d × other_cancer` +全部 **64 种组合**,证明每种恰好命中一条规则,且六条规则都可达。 +另有一条专门的测试断言:**未消歧实体在任何组合下都落 `L3-00`,永远无法被 `L3-05` 排除。** ## 六、本契约不给出预期结果形状 @@ -178,29 +241,35 @@ EVGAP-01 读取的是已固定的数据集,结果可以事先算出并逐项 **20 条**验证规则 `VAL-L01`..`VAL-L20` 见 YAML。 -## 八、必须写进结果报告的四条 +## 八、必须写进结果报告的六条 - **`MF-L01`**:LOCK-03 RETAIN 只表示存在可回溯的 CRC-specific linkage 证据,**不表示该靶点适合 ADC、不表示疗效、不表示治疗窗**。 - **`MF-L02`**:C 类证据证明 target 在 CRC 中可接近或可干预,**不是 ADC 疗效证据**,必须原样标注。 - **`MF-L03`**:本次抽取未使用任何派生本地数据库,检索完整性只在 Tier 1 声明范围内成立。 - **`MF-L04`**:LOCK-03 RETAIN **不使 pair 进入 Level 02**。`EVGAP-01` 未解除前 `may_advance_to_level_02` 恒为 `false`。 +- **`MF-L05`**(v0.2.0):检索命中不是 linkage 证据。报告必须分别给出 `retrieval_candidate_count` 与 `assertion_count`;只报候选数即冒充证据量。 +- **`MF-L06`**(v0.2.0):落 `L3-00` 的 pair 既未被支持也未被排除,**不得**与 `L3-05` 合并计数,也不得表述为「无 linkage」。 ## 九、授权与不授权 -**授权:** 获 `APPROVE` 后按本契约执行**一次**抽取,读取 Tier 1 来源,覆盖全部 369 个 pair。 +**授权:** 按本契约执行抽取,读取 Tier 1 来源,覆盖全部 369 个 pair。 +`L-RETRIEVAL` 层已执行一次(`gen_iet_evgap_02_crc_linkage_20260805T190453Z` revision 2)。 **不授权:** 执行 Level 01;解除 `EVGAP-01`;读取任何 Tier 2 派生本地数据库;把任何派生数据库纳入已批准来源;评估 T2、T7 或任何 Gate;新增靶点或 clinical context;任何筛选排序、Tier 划分、资产推荐或实验建议;引入被隔离运行(PR #53、#54)的任何产物。 ## 十、后续顺序 -1. 本契约 `APPROVE`。 -2. 执行抽取 → 结果 PR → `APPROVE`。 -3. **另开 PR** 更新 `adc_pool_level_01_input_binding.yaml`,绑定抽取产物并解除 `EVGAP-02`。 -4. `EVGAP-01` 由 Track B 独立推进(`SRCADM-01` → 抽取 → 结果 → binding)。 -5. **两个缺口都解除后,才能生成 `ADC_POOL_LEVEL_01_ACCEPTED`。** +1. v0.2.0 契约与 `L-RETRIEVAL` 层产物 `APPROVE`。 +2. **另开 PR** 处理 `GAP-P07`:四个实体解析为标准符号、定义为非蛋白抗原,或移出轴。 + 移出会改动 41/369 这两个冻结计数,故必须经审核。 +3. `GAP-P07` 处理后执行 `L-ASSERTION` 抽取 → 结果 PR → `APPROVE`。 +4. **再另开 PR** 更新 `adc_pool_level_01_input_binding.yaml`,绑定产物并解除 `EVGAP-02`。 +5. `EVGAP-01` 由 Track B 独立推进(`SRCADM-01` → 抽取 → 结果 → binding)。 +6. **两个缺口都解除后,才能生成 `ADC_POOL_LEVEL_01_ACCEPTED`。** ## 十一、当前阻断 -- 本契约获 ChatGPT `APPROVE` 前,不得执行抽取。 -- 抽取完成也**不**解除 `EVGAP-01`,Level 01 仍不可执行。 +- `L-ASSERTION` 层未执行,故 **`EVGAP-02` 未解除**。 +- `GAP-P07` 未处理前,36 个 pair 无法脱离 `L3-00`。 +- `EVGAP-01` 亦未解除,Level 01 仍不可执行。 - 本仓库不得写入证据、候选、快照、cache、result 或 weights。 diff --git a/logs/worklog.md b/logs/worklog.md index bfe499d..df0a851 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2667,3 +2667,24 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Deliberately not done: **未解除 `EVGAP-02`**(须待本结果获批后另开 PR 更新绑定);未解除 `EVGAP-01`;未更新 `adc_pool_level_01_input_binding.yaml`;Level 01 仍不可执行;未生成 `ADC_POOL_LEVEL_01_ACCEPTED`;未补八份批准记录。 - Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。本 PR 只含本条 worklog 与一份 handoff。 - Next: 送审本结果;获批后另开 PR 解除 `EVGAP-02`;随后按指示起 `SRCADM-01`。 + +### 2026-08-05 16:55 EDT — EVGAP-02:审核后降级为检索候选层,契约修订至 v0.2.0(PR #62 第二轮) + +- Trigger: ChatGPT 对 PR #62 返回 `REQUEST_CHANGES`——「这些是搜索命中,不是 linkage evidence」。 +- Verified before changing anything: 逐条对实际文件核验,**审核意见全部成立**。7,067 行确实全部 `evidence_direction = unknown`、`review_status = machine_retrieved_requires_human_review`。 +- Worse than described: **没有任何一行带有已解析的断言字段。** `positive_fraction_or_prevalence` 在 7,067 行中全为空;`is_adc_efficacy_evidence` 全为 `false`;`malignant_cell_attribution` 全为 `unresolved`/`not_applicable`;5,699 条文献行的 `protein_or_rna` 全为 `unresolved`。**一条已抽取的断言都没有。** +- Identity resolution never happened: `Undisclosed` **是缺失值占位符、不是实体**,却被当作基因符号检索,`PMC/A` 返回 1,384 条并产出 1 个 RETAIN;`CA19-9`(糖类抗原,无 HGNC 符号)`PMC/A` 14,200 条、9 个 pair 中 8 个 RETAIN;`EDBN` 在 11 个 endpoint 全部 0 命中,9 个 pair 落 `L3-05` **EXCLUDE**——它疑指 fibronectin 的 extra domain B(标准符号 `FN1`),**被排除的唯一原因是这个缩写不通行于文献。消歧失败被当成了完整检索后的阴性结论。** +- One review detail corrected on the facts: 审核说 TCGA 与 HPA 的命中被算作 A 类证据。**实测它们没有产生任何证据行**——证据表 `source_ref` 前缀只有 `PMC` 3,240/`PubMed` 2,459/`ClinicalTrials.gov` 702/`GEO` 666。**原则完全成立,实际的实例是 `GEO`** 的 `db=gds` 元数据命中被登记为 666 行 A 类。 +- Third defect found by self-check, not raised in review: **未披露的检索截断**。451 次检索报告命中合计 **718,140**,实际登记 **979** 条,**333/451 次被截断**(多数每组只留 3 条)。revision 1 未声明此上限却宣称检索完整。 +- Root cause is the contract, not only the run: v0.1.0 把 `evidence_direction` 与 `review_status` 列为**必需列却无任何规则要求其被解析**,`linkage_class` 也无规则约束其来源,于是由**查询类别**决定。**一次完全合规的执行因此产出 168 条 RETAIN。** 故修在契约。 +- Contract amended to v0.2.0: 三层结构 `L-RETRIEVAL`/`L-ASSERTION`/`L-DISPOSITION`;`assertion_requirements` 六要件并**硬性禁止 `assertion_direction = unknown`**;`identity_resolution` 与新规则 `L3-00`(置于优先级最前,**未消歧实体既不得 RETAIN 也绝不得 EXCLUDE**);`endpoint_evidence_admissibility` 逐 endpoint 写明命中证明什么与不证明什么;`search_complete` 扩为四层;新增 `VAL-L21`..`VAL-L28`。 +- Frozen vocabulary respected: LOCK-03 的 outcome 词表由 PR #57 冻结、其中没有 `identity_unresolved`(该 outcome 只属 LOCK-01),故 `L3-00` **复用 `linkage_evidence_missing`**,身份信息另由 `identity_resolution_status` 列承载,**不新增 outcome**。 +- Result downgraded (revision 2): 撤销 `pair_linkage_evidence.tsv`,改出 `retrieval_candidates.tsv` **979 行** + `linkage_assertions.tsv` **0 行**。**未重复任何网络调用、未丢弃任何已检索记录。** 「7,067 条证据」实为 **979 条记录乘以 9**(A 2808/9=312、B 2295/9=255、C 1746/9=194、D 218 不复制)。 +- Dispositions withdrawn: 369 个 pair 全部 DEFER/hold——`L3-00` **36**(4 个不可消歧实体 × 9 context)、`L3-01` **333**。**RETAIN 0、EXCLUDE 0**,三组 `*_evidence_refs` 全空。 +- Upstream defect recorded, not fixed: **`GAP-P07`**——PR #58 冻结的 41 个 target 中至少四个不是可消歧的蛋白实体。`EVGAP-02` 无权改轴,在本契约内给 `Undisclosed` 编身份等于静默改轴。**binding 本身已察觉这一点**:它把 `identity_unresolved` 列入 `unavailable_outcomes`,理由是「已批准层没有身份解析结论字段」,四个实体因此以 `E1-05` 留在轴上。修复须另开 PR,会改动 41/369 两个冻结计数。 +- Validation: 外部产物经 **25 项 v0.2.0 规则**核验全通过;`tests/test_evgap_02_crc_linkage.py` **44 tests**;全库 `Ran 353 tests` OK;`scripts/verify_repository_boundary.sh` 通过。 +- Mutation testing: **15 个变异全部被捕获**,逐个 `diff -q` 回滚干净。其中一个首轮「逃逸」实为我的替换串缩进写错、变异根本未生效;改正缩进后被捕获——记此以免把无效变异误记为测试覆盖。 +- Packaged: `gen_iet_evgap_02_crc_linkage_20260805T190453Z_rev2.zip`,41,734 bytes,ZIP SHA-256 `e8f2a7f5ce9fae25265994f0d9a1fae1e371a1bb55ccafea2026835bd5120d3d`,8 个条目。**按 PR #60 裁决后确立的规则,每个修订单独出包并各带自己的 SHA-256。** +- Boundary: 未运行任何 Gate、未赋分数、未评估 T2/T7、未排序、未划 Tier、未推荐资产、未给实验建议、**未新增或修改靶点与 context**、未打开任何 Tier 2 派生库、未引用被隔离运行产物、未写入任何数据文件到仓库。 +- Deliberately not done: **未解除 `EVGAP-02`**;未处理 `GAP-P07`;未执行 `L-ASSERTION` 抽取;未解除 `EVGAP-01`;未更新 binding;未生成 `ADC_POOL_LEVEL_01_ACCEPTED`;未补九份批准记录。 +- Next: 送审本修订;获批后另开 PR 处理 `GAP-P07`,再执行 `L-ASSERTION` 抽取。 diff --git a/tests/test_evgap_02_crc_linkage.py b/tests/test_evgap_02_crc_linkage.py index d11a2be..9431f0b 100644 --- a/tests/test_evgap_02_crc_linkage.py +++ b/tests/test_evgap_02_crc_linkage.py @@ -3,7 +3,15 @@ The contract freezes the four linkage classes, the declared search scope, the LOCK-03 evaluation precedence and the provenance split for the EVGAP-02 extraction. These tests check internal consistency and agreement with the merged -Level 01 contracts. They read no external source: the extraction has not run. +Level 01 contracts. They read no external source. + +v0.2.0 adds the layer the first version lacked. v0.1.0 required an +``evidence_direction`` column but no rule ever required it to be resolved, so a +compliant run registered 7,067 search hits with ``evidence_direction=unknown`` +and derived 168 RETAIN dispositions from them. The tests below pin the three +properties that failure needed: search hits and assertions live in separate +tables, a disposition may only cite assertions, and an unresolvable symbol can +neither retain nor exclude. """ from __future__ import annotations @@ -23,12 +31,20 @@ LEVEL_CONTRACT_PATH = POOLS / "adc_pool_gate_usage.yaml" LINKAGE_CLASSES = ("A", "B", "C", "D") -# Columns the source document requires for every evidence row. +# Columns the source document requires for every evidence row. v0.2.0 renames +# evidence_direction to assertion_direction, which may no longer be "unknown". REQUIRED_EVIDENCE_COLUMNS = ( "pair_id", "target", "clinical_context_id", "evidence_type", "crc_specific", "context_specific", "human_or_model", "protein_or_rna", "source_ref", - "source_locator", "evidence_direction", "review_status", "linkage_outcome", + "source_locator", "assertion_direction", "review_status", "linkage_outcome", ) +# The elements that turn a retrieved record into an assertion. +ASSERTION_ELEMENTS = ( + "target_entity_resolved", "crc_entity_resolved", "context_entity_resolved", + "relationship_type", "assertion_direction", "supporting_text_or_structured_field", +) +# Endpoints whose hit proves only that the entity has a page or an index row. +IDENTITY_ONLY_ENDPOINTS = ("TCGA", "Human Protein Atlas", "GEO") def _load(path: Path) -> dict: @@ -55,10 +71,35 @@ def test_contract_targets_the_registered_gap(self) -> None: self.assertEqual(head["serves_lock"], "LOCK-03") gaps = {g["id"]: g for g in self.binding["evidence_gaps"]} self.assertEqual(gaps["EVGAP-02"]["blocks"], "LOCK-03") - self.assertEqual(head["execution_status"], "not_authorized_not_executed") self.assertIs(head["authorises_level_01_execution"], False) self.assertIs(head["requires_followup_binding_pr"], True) + def test_the_executed_run_is_recorded_as_retrieval_only(self) -> None: + """The v0.1.0 run happened; it must not read as having discharged the gap.""" + + head = self.doc["extraction"] + self.assertEqual(head["extraction_version"], "0.2.0") + self.assertEqual(head["supersedes_version"], "0.1.0") + self.assertEqual(head["amended_after_review_of_pr"], 62) + self.assertIs(head["gap_discharged"], False) + self.assertEqual( + head["execution_status"], + "retrieval_layer_executed_assertion_layer_not_executed", + ) + runs = head["executed_runs"] + self.assertTrue(runs) + for run in runs: + with self.subTest(run=run["run_id"]): + self.assertEqual(run["under_contract_version"], "0.1.0") + self.assertEqual(run["layers_completed"], ["L-RETRIEVAL"]) + self.assertEqual(set(run["layers_not_completed"]), + {"L-ASSERTION", "L-DISPOSITION"}) + self.assertIs(run["discharges_gap"], False) + self.assertEqual(run["downgraded_by_review_of_pr"], 62) + # The amendment must say why, not merely bump the number. + self.assertTrue(head["amendment_reason"].strip()) + self.assertIn("query", head["amendment_reason"]) + def test_extraction_is_independent_of_evgap_01(self) -> None: """The two tracks must be runnable in parallel.""" @@ -199,6 +240,7 @@ def _assign(self, pair: dict) -> str: """Reference implementation of the frozen precedence.""" conditions = { + "L3-00": not pair["identity_resolved"], "L3-01": not pair["search_complete"], "L3-02": pair["crc_specific"] and ( pair["canonical"] or pair["class_d"]), @@ -217,34 +259,52 @@ def test_precedence_covers_every_rule_and_starts_with_completeness(self) -> None 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))) - self.assertEqual(precedence[0], "L3-01") + self.assertEqual(precedence[0], "L3-00") + self.assertEqual(precedence[1], "L3-01") self.assertEqual(precedence[-1], "L3-05") self.assertTrue(self.doc["precedence_rationale"].strip()) def test_every_combination_resolves_to_exactly_one_rule(self) -> None: cases = [ + # An unresolved symbol outranks everything, including a closed search + # with zero hits. This is the case v0.1.0 got wrong in both directions. + ({"identity_resolved": False, "search_complete": True, + "crc_specific": True, "canonical": True, "class_d": True, + "other_cancer": True}, "L3-00"), + ({"identity_resolved": False, "search_complete": True, + "crc_specific": False, "canonical": True, "class_d": False, + "other_cancer": False}, "L3-00"), # Incomplete search always wins, whatever else is true. - ({"search_complete": False, "crc_specific": True, "canonical": True, + ({"identity_resolved": True, "search_complete": False, + "crc_specific": True, "canonical": True, "class_d": True, "other_cancer": True}, "L3-01"), - ({"search_complete": False, "crc_specific": False, "canonical": False, + ({"identity_resolved": True, "search_complete": False, + "crc_specific": False, "canonical": False, "class_d": False, "other_cancer": False}, "L3-01"), # CRC-specific evidence on the canonical context retains. - ({"search_complete": True, "crc_specific": True, "canonical": True, + ({"identity_resolved": True, "search_complete": True, + "crc_specific": True, "canonical": True, "class_d": False, "other_cancer": False}, "L3-02"), # A subgroup retains only with class D enrichment. - ({"search_complete": True, "crc_specific": True, "canonical": False, + ({"identity_resolved": True, "search_complete": True, + "crc_specific": True, "canonical": False, "class_d": True, "other_cancer": False}, "L3-02"), - ({"search_complete": True, "crc_specific": True, "canonical": False, + ({"identity_resolved": True, "search_complete": True, + "crc_specific": True, "canonical": False, "class_d": False, "other_cancer": False}, "L3-03"), - ({"search_complete": True, "crc_specific": True, "canonical": False, + ({"identity_resolved": True, "search_complete": True, + "crc_specific": True, "canonical": False, "class_d": False, "other_cancer": True}, "L3-03"), # Other-cancer precedent only. - ({"search_complete": True, "crc_specific": False, "canonical": True, + ({"identity_resolved": True, "search_complete": True, + "crc_specific": False, "canonical": True, "class_d": False, "other_cancer": True}, "L3-04"), # Nothing at all, search closed. - ({"search_complete": True, "crc_specific": False, "canonical": True, + ({"identity_resolved": True, "search_complete": True, + "crc_specific": False, "canonical": True, "class_d": False, "other_cancer": False}, "L3-05"), - ({"search_complete": True, "crc_specific": False, "canonical": False, + ({"identity_resolved": True, "search_complete": True, + "crc_specific": False, "canonical": False, "class_d": False, "other_cancer": False}, "L3-05"), ] for pair, expected in cases: @@ -253,18 +313,32 @@ def test_every_combination_resolves_to_exactly_one_rule(self) -> None: def test_precedence_is_total_over_the_whole_condition_space(self) -> None: seen = set() - for search_complete in (True, False): - for crc_specific in (True, False): - for canonical in (True, False): - for class_d in (True, False): - for other_cancer in (True, False): - pair = dict(search_complete=search_complete, - crc_specific=crc_specific, - canonical=canonical, class_d=class_d, - other_cancer=other_cancer) - seen.add(self._assign(pair)) + for identity_resolved in (True, False): + for search_complete in (True, False): + for crc_specific in (True, False): + for canonical in (True, False): + for class_d in (True, False): + for other_cancer in (True, False): + pair = dict(identity_resolved=identity_resolved, + search_complete=search_complete, + crc_specific=crc_specific, + canonical=canonical, class_d=class_d, + other_cancer=other_cancer) + seen.add(self._assign(pair)) self.assertEqual(seen, set(self.doc["derivation_precedence"])) + def test_an_unresolved_symbol_can_never_be_excluded(self) -> None: + """v0.1.0 excluded EDBN for nine pairs because its abbreviation drew zero + hits. Zero hits from an unresolvable symbol is a resolution failure.""" + + for canonical in (True, False): + for class_d in (True, False): + pair = dict(identity_resolved=False, search_complete=True, + crc_specific=False, canonical=canonical, + class_d=class_d, other_cancer=False) + with self.subTest(**pair): + self.assertEqual(self._assign(pair), "L3-00") + def test_only_the_complete_search_rule_may_exclude(self) -> None: """L3-02 retains, L3-05 excludes from the active pool, the rest defer.""" @@ -309,7 +383,12 @@ def test_required_columns_exist_in_their_own_table(self) -> None: """Checking the union of both tables would mask a missing column.""" schema = self.doc["output_schema"] - evidence = set(schema["evidence_columns"]) + # v0.2.0: the evidence table IS the assertion table. + self.assertIs(schema["evidence_table_is_assertion_table"], True) + self.assertEqual(schema["evidence_columns_alias"], "assertion_columns") + self.assertNotIn("evidence_columns", schema, + "a live evidence_columns key would let both versions coexist") + evidence = set(schema["assertion_columns"]) disposition = set(schema["disposition_columns"]) for column in REQUIRED_EVIDENCE_COLUMNS: with self.subTest(column=column): @@ -333,9 +412,9 @@ def test_evidence_rows_are_individually_addressable(self) -> None: """Blocker 2: a disposition must be able to name its supporting rows.""" schema = self.doc["output_schema"] - self.assertEqual(schema["evidence_row_key"], "evidence_id") + self.assertEqual(schema["evidence_row_key"], "assertion_id") self.assertIs(schema["evidence_id_unique"], True) - self.assertIn("evidence_id", schema["evidence_columns"]) + self.assertIn("assertion_id", schema["assertion_columns"]) for column in ("supporting_evidence_refs", "class_d_evidence_refs", "other_cancer_evidence_refs"): with self.subTest(column=column): @@ -393,11 +472,13 @@ def test_class_d_completeness_is_pair_level_and_gates_l3_03_and_l3_05(self) -> N self.assertIn(field, block["must_record"]) # The field must actually be carried by the disposition table. self.assertIn(field, self.doc["output_schema"]["disposition_columns"]) - # Completeness must require both levels, not just the target level. - self.assertIs(scope["search_complete_requires_both_levels"], True) + # Completeness must require all four levels, not just the target level. + self.assertIs(scope["search_complete_requires_all_levels"], True) self.assertEqual(set(scope["search_complete_levels"]), - {"target_level_source_class_and_endpoint_coverage", - "pair_level_class_d_coverage"}) + {"target_level_identity_resolution", + "target_level_source_class_and_endpoint_coverage", + "pair_level_class_d_coverage", + "pair_level_assertion_extraction_completed"}) self.assertEqual(scope["unreachable_class_d_consequence"], "search_incomplete_for_that_pair") # L3-03 and L3-05 may only fire once the D-class search has closed. @@ -405,7 +486,7 @@ def test_class_d_completeness_is_pair_level_and_gates_l3_03_and_l3_05(self) -> N for rule_id in ("L3-03", "L3-05"): with self.subTest(rule=rule_id): self.assertIs(by_id[rule_id]["requires_class_d_search_complete"], True) - self.assertIs(by_id["L3-01"]["covers_both_completeness_levels"], True) + self.assertIs(by_id["L3-01"]["covers_all_completeness_levels"], True) ids = {r["id"] for r in self.doc["output_validation"]} self.assertIn("VAL-L18", ids) @@ -470,13 +551,263 @@ def test_retain_does_not_promote_to_level_02(self) -> None: def test_mandatory_findings_keep_the_uncomfortable_qualifiers(self) -> None: findings = {f["id"]: f["finding"] for f in self.doc["mandatory_findings"]} - self.assertEqual(set(findings), {"MF-L01", "MF-L02", "MF-L03", "MF-L04"}) + self.assertEqual(set(findings), + {"MF-L01", "MF-L02", "MF-L03", "MF-L04", "MF-L05", "MF-L06"}) # RETAIN must never be read as ADC suitability or efficacy. self.assertIn("ADC", findings["MF-L01"]) self.assertIn("ADC", findings["MF-L02"]) + # The report must not present candidate counts as evidence counts, and + # must not fold the unresolved pairs into "no linkage". + self.assertIn("retrieval_candidate_count", findings["MF-L05"]) + self.assertIn("assertion_count", findings["MF-L05"]) + self.assertIn("L3-00", findings["MF-L06"]) + self.assertIn("L3-05", findings["MF-L06"]) for text in findings.values(): self.assertTrue(text.strip()) + # ------------------------------------------------ v0.2.0: the missing layer + def test_retrieval_and_assertion_are_separate_layers(self) -> None: + layers = {l["layer_id"]: l for l in self.doc["extraction_layers"]} + self.assertEqual(set(layers), {"L-RETRIEVAL", "L-ASSERTION", "L-DISPOSITION"}) + retrieval, assertion = layers["L-RETRIEVAL"], layers["L-ASSERTION"] + # A search hit alone may never support LOCK-03. + self.assertIs(retrieval["may_support_lock_03"], False) + self.assertIs(retrieval["may_be_referenced_by_disposition"], False) + self.assertIs(assertion["may_support_lock_03"], True) + self.assertEqual(assertion["consumes"], "retrieval_candidates") + # The disposition layer may only read assertions. + disposition = layers["L-DISPOSITION"] + self.assertEqual(disposition["may_only_reference"], "linkage_assertions") + self.assertIs(disposition["may_reference_retrieval_candidates"], False) + # The known false-positive modes must be named, not gestured at. + modes = " ".join(retrieval["known_false_positive_modes"]) + for mode in ("缩写碰撞", "参考文献", "综述", "免责声明"): + with self.subTest(mode=mode): + self.assertIn(mode, modes) + + def test_decision_02_is_read_correctly_not_as_a_licence_for_hits(self) -> None: + """PR #58 allowed machine-extracted evidence — extracted, not retrieved.""" + + assertion = next(l for l in self.doc["extraction_layers"] + if l["layer_id"] == "L-ASSERTION") + self.assertIs(assertion["machine_extraction_permitted"], True) + self.assertIs(assertion["human_review_still_required"], True) + basis = assertion["machine_extraction_permitted_basis"] + self.assertIn("DECISION-02", basis) + self.assertIn("assertion", basis) + + def test_an_assertion_needs_every_element_and_a_resolved_direction(self) -> None: + req = self.doc["assertion_requirements"] + elements = {e["element"] for e in req["required_elements"]} + self.assertEqual(elements, set(ASSERTION_ELEMENTS)) + for entry in req["required_elements"]: + with self.subTest(element=entry["element"]): + self.assertTrue(entry["meaning"].strip()) + # The check v0.1.0 lacked. + self.assertIs(req["direction_must_be_resolved"], True) + self.assertIs(req["direction_unknown_forbidden_for_lock_03"], True) + self.assertIs(req["cooccurrence_is_not_an_assertion"], True) + self.assertTrue(req["relationship_types"]) + self.assertTrue(req["cooccurrence_examples_rejected"]) + # Both a valid and an invalid example must be given, so the boundary is + # demonstrated rather than asserted. + self.assertTrue(req["examples_of_valid_assertions"]) + self.assertTrue(req["examples_of_invalid_assertions"]) + # And the schema must forbid the value, not only the prose. + forbidden = self.doc["output_schema"]["assertion_forbidden_values"] + self.assertIn("unknown", forbidden["assertion_direction"]) + + def test_linkage_class_comes_from_content_not_from_the_query(self) -> None: + common = self.doc["linkage_class_common_requirements"] + self.assertIs(common["applies_to_all_classes"], True) + self.assertIs(common["requires_assertion"], True) + self.assertEqual(common["classified_by"], "assertion_content") + self.assertIs(common["classified_by_query_category_forbidden"], True) + self.assertIs(common["direction_unknown_admissible"], False) + self.assertTrue(common["classified_by_query_category_forbidden_reason"].strip()) + schema = self.doc["output_schema"] + # The candidate table records which query found it, and that label is + # explicitly not a linkage class. + self.assertIn("query_class_label", schema["retrieval_candidate_columns"]) + self.assertIs(schema["query_class_label_is_not_linkage_class"], True) + self.assertNotIn("linkage_class", schema["retrieval_candidate_columns"]) + self.assertIn("linkage_class", schema["assertion_columns"]) + + def test_candidate_table_marks_itself_unusable_for_lock_03(self) -> None: + schema = self.doc["output_schema"] + columns = set(schema["retrieval_candidate_columns"]) + for column in ("record_status", "linkage_validated", "may_support_lock_03", + "assertion_extraction_status"): + with self.subTest(column=column): + self.assertIn(column, columns) + fixed = schema["retrieval_candidate_fixed_values"] + self.assertEqual(fixed["record_status"], "retrieved_candidate") + self.assertIs(fixed["linkage_validated"], False) + self.assertIs(fixed["may_support_lock_03"], False) + # Every assertion must name the candidate it came from. + self.assertIn("candidate_id", schema["assertion_columns"]) + self.assertIs(schema["assertion_must_cite_its_candidate"], True) + + def test_refs_may_only_point_at_assertions(self) -> None: + schema = self.doc["output_schema"] + self.assertEqual(schema["refs_point_to"], "assertion_id") + self.assertIs(schema["refs_pointing_to_candidate_id_forbidden"], True) + tables = {t["table"]: t for t in schema["tables"]} + self.assertIs(tables["retrieval_candidates"]["may_be_referenced_by_disposition"], + False) + self.assertIs(tables["linkage_assertions"]["may_be_referenced_by_disposition"], + True) + ids = {r["id"] for r in self.doc["output_validation"]} + self.assertIn("VAL-L23", ids) + + def test_identity_resolution_precedes_search_and_blocks_both_verdicts(self) -> None: + block = self.doc["identity_resolution"] + self.assertIs(block["required_before_search"], True) + self.assertIs(block["unresolved_may_not_be_searched"], True) + self.assertEqual(block["unresolved_lock_03_rule"], "L3-00") + # The asymmetry that matters: no RETAIN and no EXCLUDE. + self.assertIs(block["unresolved_may_not_retain"], True) + self.assertIs(block["unresolved_may_not_exclude"], True) + reason = block["unresolved_may_not_exclude_reason"] + self.assertIn("EDBN", reason) + self.assertIn("FN1", reason) + for field in ("input_symbol", "resolved_identifier", "resolution_status", + "resolution_basis"): + with self.subTest(field=field): + self.assertIn(field, block["resolution_must_record"]) + + def test_the_four_unresolvable_entities_are_named_with_their_kind(self) -> None: + """Each was searched as a gene symbol by the v0.1.0 run.""" + + block = self.doc["identity_resolution"] + entities = {e["input_symbol"]: e for e in block["known_unresolved_entities"]} + self.assertEqual(set(entities), + {"Undisclosed", "EDBN", "AG7", "CA19-9"}) + vocabulary = set(block["resolution_status_vocabulary"]) + for symbol, entry in sorted(entities.items()): + with self.subTest(symbol=symbol): + self.assertIn(entry["resolution_status"], vocabulary) + self.assertTrue(entry["note"].strip()) + # The distinctions must be kept: a placeholder is not an abbreviation, + # and a glycan antigen is resolvable but has no gene symbol. + self.assertEqual(entities["Undisclosed"]["resolution_status"], + "unresolvable_placeholder") + self.assertEqual(entities["CA19-9"]["resolution_status"], + "resolved_as_non_protein_antigen") + self.assertEqual(entities["EDBN"]["resolution_status"], + "unresolvable_ambiguous_abbreviation") + # Recognising them must not require free judgement. + self.assertIn("E1-05", block["mechanical_precondition"]) + + def test_l3_00_reuses_the_frozen_outcome_vocabulary(self) -> None: + """LOCK-03 has no identity_unresolved outcome; that belongs to LOCK-01.""" + + rule = next(r for r in self.doc["derivation_rules"] if r["id"] == "L3-00") + outcomes = {o["outcome"]: o for o in self.lock_03["outcomes"]} + self.assertNotIn("identity_unresolved", outcomes) + self.assertEqual(rule["lock_03_outcome"], "linkage_evidence_missing") + self.assertIn(rule["lock_03_outcome"], outcomes) + self.assertIs(rule["does_not_introduce_new_outcome"], True) + self.assertEqual(rule["distinguishing_column"], "identity_resolution_status") + self.assertIn("identity_resolution_status", + self.doc["output_schema"]["disposition_columns"]) + self.assertIs(rule["may_never_become_l3_05"], True) + # L3-05 must carry the matching guard. + l3_05 = next(r for r in self.doc["derivation_rules"] if r["id"] == "L3-05") + self.assertIs(l3_05["requires_identity_resolved"], True) + self.assertIs(l3_05["zero_hits_from_unresolved_symbol_forbidden"], True) + ids = {r["id"] for r in self.doc["output_validation"]} + self.assertIn("VAL-L27", ids) + + def test_each_endpoint_declares_what_its_hit_does_not_prove(self) -> None: + entries = {e["endpoint"]: e for e in self.doc["endpoint_evidence_admissibility"]} + # Every declared tier-1 endpoint must appear. + declared = {endpoint + for source in self.doc["source_tiers"]["tier_1_primary_public"]["sources"] + if source["source_class"] in + self.doc["declared_search_scope"]["per_target_required_source_classes"] + for endpoint in source["endpoints"]} + self.assertEqual(set(entries), declared) + for endpoint, entry in sorted(entries.items()): + with self.subTest(endpoint=endpoint): + self.assertTrue(entry["hit_proves"].strip()) + self.assertTrue(entry["hit_does_not_prove"]) + # A gene-index or page-existence hit is never class A evidence. + for endpoint in IDENTITY_ONLY_ENDPOINTS: + with self.subTest(endpoint=endpoint): + self.assertIs(entries[endpoint]["admissible_as_class_a"], False) + # TCGA's disclaimer must name the four things it cannot show. + tcga = entries["TCGA"]["hit_does_not_prove"] + for claim in ("CRC 肿瘤表达", "prevalence", "malignant-cell attribution"): + with self.subTest(claim=claim): + self.assertIn(claim, " ".join(tcga)) + # They stay mandatory for coverage even though they prove no linkage. + handling = self.doc["dataset_endpoint_handling"] + self.assertEqual(set(handling["dataset_endpoints"]), set(IDENTITY_ONLY_ENDPOINTS)) + self.assertIs(handling["still_required_for_coverage"], True) + self.assertIs(handling["hits_excluded_from_linkage_classes_hit"], True) + ids = {r["id"] for r in self.doc["output_validation"]} + self.assertIn("VAL-L26", ids) + + def test_trial_records_need_structured_fields_and_one_arm(self) -> None: + entry = next(e for e in self.doc["endpoint_evidence_admissibility"] + if e["endpoint"] == "ClinicalTrials.gov") + for field in ("intervention_name", "intervention_target", + "intervention_modality", "conditions", "arm_group_assignment"): + with self.subTest(field=field): + self.assertIn(field, entry["required_structured_fields"]) + self.assertIs(entry["same_arm_requirement"], True) + self.assertIs(entry["requires_assertion_extraction"], True) + ids = {r["id"] for r in self.doc["output_validation"]} + self.assertIn("VAL-L28", ids) + + def test_retrieval_alone_does_not_close_the_search(self) -> None: + scope = self.doc["declared_search_scope"] + self.assertEqual(scope["scope_version"], "0.2.0") + self.assertIs(scope["retrieval_alone_is_not_search_complete"], True) + self.assertTrue(scope["retrieval_alone_is_not_search_complete_reason"].strip()) + definition = " ".join(scope["search_complete_definition"]) + for token in ("identity_resolution", "L-ASSERTION"): + with self.subTest(token=token): + self.assertIn(token, definition) + # The disposition table must carry the per-pair assertion tally, so a + # zero-assertion pair cannot be presented as a searched-and-empty one. + columns = set(self.doc["output_schema"]["disposition_columns"]) + for column in ("assertion_extraction_complete", "retrieval_candidate_count", + "assertion_count"): + with self.subTest(column=column): + self.assertIn(column, columns) + + def test_the_target_axis_defect_is_recorded_not_silently_patched(self) -> None: + """Fixing the axis inside EVGAP-02 would be an unauthorised axis change.""" + + defect = self.doc["upstream_defect"] + self.assertEqual(defect["id"], "GAP-P07") + self.assertEqual(set(defect["entities"]), + {"Undisclosed", "EDBN", "AG7", "CA19-9"}) + self.assertEqual(defect["frozen_by_pr"], 58) + self.assertIs(defect["requires_separate_pr"], True) + self.assertEqual(defect["interim_handling"], + "本抽取按 L3-00 处理,全部 DEFER,既不 RETAIN 也不 EXCLUDE") + self.assertTrue(defect["why_not_fixed_here"].strip()) + # The binding must in fact have declared identity_unresolved unavailable, + # which is what the record claims. + unavailable = {o["outcome"] for o in + self.doc_binding_lock_01_unavailable_outcomes()} + self.assertIn("identity_unresolved", unavailable) + # And this contract must forbid changing the axis. + not_authorised = " ".join(self.doc["not_authorised"]) + self.assertIn("GAP-P07", not_authorised) + self.assertIn("新增靶点", not_authorised) + + def doc_binding_lock_01_unavailable_outcomes(self) -> list: + for key, value in self.binding.items(): + if isinstance(value, dict) and "unavailable_outcomes" in value: + if any(o["outcome"] == "identity_unresolved" + for o in value["unavailable_outcomes"]): + return value["unavailable_outcomes"] + raise AssertionError("binding declares no unavailable identity outcome") + def test_authorisation_stops_short_of_level_01_and_other_gaps(self) -> None: not_authorised = " ".join(self.doc["not_authorised"]) for phrase in ("执行 Level 01", "EVGAP-01", "tier_2", "T7", "Gate"): From 8503d7e3ad4014605709db190ac8ce8a7e3ccba8 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Thu, 6 Aug 2026 15:25:38 -0400 Subject: [PATCH 3/4] task_20260805: key L3-00 on resolution_status, move CA19-9 to L3-01 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second PR #62 review: revision 2 assigned CA19-9 to L3-00 although the contract declares it resolved_as_non_protein_antigen — a status search_complete accepts. The contradiction is direct and the review is right. Root cause was mine: the contract table was named known_unresolved_entities but held one resolved entity, and the rebuild script keyed L3-00 on membership in that table rather than on resolution_status. Fixed at the failure point: table renamed known_identity_findings, l3_00_statuses added explicitly, l3_00_membership_test set to resolution_status, l3_00_membership_by_list_forbidden set true, and every entry now declares the lock_03_rule its status implies so the two cannot drift apart. CA19-9 keeps its resolved status and moves to L3-01. Its pairs defer because the assertion layer has not run and because a gene-symbol query is an invalid form for a glycan antigen — not because the identity is unknown. New section non_protein_antigen_search_requirements and rule VAL-L29 record that. Revision 3: L3-00 27, L3-01 342. Still zero RETAIN, zero EXCLUDE, 369 hold, empty assertion table, 979 candidates unchanged, no network call repeated. GAP-P07 sharpened: the four entities are not one kind of defect. Whether a resolved non-protein antigen belongs in a membrane-protein target universe is the actual upstream question. Co-Authored-By: Claude Opus 5 --- ...-08-05-evgap-02-extraction-result.zh-CN.md | 69 ++++++++++++------ .../evgap_02_crc_linkage_extraction.yaml | 70 +++++++++++++++--- logs/worklog.md | 17 +++++ tests/test_evgap_02_crc_linkage.py | 71 +++++++++++++++++-- 4 files changed, 193 insertions(+), 34 deletions(-) diff --git a/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md index 954e25b..96967a8 100644 --- a/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md +++ b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md @@ -1,10 +1,10 @@ -# Handoff:EVGAP-02 Tier 1 检索候选层 + 契约 v0.2.0(revision 2) +# Handoff:EVGAP-02 Tier 1 检索候选层 + 契约 v0.2.0(revision 3) - 日期:`2026-08-05` - 任务分支:`task_20260805_evgap-02-extraction-result` - 基线:`main` @ `8aa7e87` -- 本修订依据:**PR #62 审核 `REQUEST_CHANGES`** -- 外部运行:`gen_iet_evgap_02_crc_linkage_20260805T190453Z` **revision 2** +- 本修订依据:**PR #62 第一轮与第二轮 `REQUEST_CHANGES`** +- 外部运行:`gen_iet_evgap_02_crc_linkage_20260805T190453Z` **revision 3** - 交付物类型:**契约修订 + 结果降级(外部运行留痕)** - 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** - **结论:`EVGAP-02` 未解除。本运行只完成 `L-RETRIEVAL` 层。** @@ -85,7 +85,8 @@ v0.1.0 停在候选。这一读法写进了 `L-ASSERTION.machine_extraction_perm `identity_resolution_status` 列承载。 四个实体在 target 轴上同属 `rq_01_family_count = 0`、规则 `E1-05`, -故识别它们**不依赖自由裁量**(`mechanical_precondition`)。 +故**筛出**它们不依赖自由裁量。但该字段只用于筛候选,**不决定** `resolution_status`, +也不决定是否落 `L3-00`——`CA19-9` 同属 `E1-05`,却已消歧。这正是第二轮的教训。 ### endpoint 命中证明了什么 @@ -99,17 +100,17 @@ TCGA/HPA/GEO 三者 `admissible_as_class_a: false`, `search_complete` 现在需要四层:身份消歧、endpoint 覆盖、pair 级 D 类、**assertion 抽取完成**。 `retrieval_alone_is_not_search_complete: true`。 -新增 `VAL-L21`..`VAL-L28`。 +新增 `VAL-L21`..`VAL-L29`。 -## 四、结果降级(revision 2) +## 四、结果降级(revision 3) -| | revision 1 | revision 2 | +| | revision 1 | revision 3 | |---|---|---| | 证据表 | `pair_linkage_evidence.tsv` 7,067 行 | 撤销 → `retrieval_candidates.tsv` **979** + `linkage_assertions.tsv` **0** | | `L3-02` RETAIN | 168 | **0** | | `L3-03` | 192 | 0 | | `L3-05` EXCLUDE | 9 | **0** | -| 全部 369 pair | 三分 | **全部 DEFER / hold** | +| 全部 369 pair | 三分 | **全部 DEFER / hold**(`L3-00` 27 + `L3-01` 342) | **未重复任何网络调用,未丢弃任何已检索记录。** @@ -117,12 +118,38 @@ TCGA/HPA/GEO 三者 `admissible_as_class_a: false`, `A 2808/9=312`、`B 2295/9=255`、`C 1746/9=194`、`D 218`(pair 级不复制),合计 **979**。 **「7,067 条证据」实为 979 条记录乘以 9。** -369 个 pair:`L3-00` **36**(4 个不可消歧实体 × 9)、`L3-01` **333**(assertion 层未执行)。 +369 个 pair:`L3-00` **27**(3 个**不可消歧**实体 `Undisclosed`/`EDBN`/`AG7` × 9)、 +`L3-01` **342**(assertion 层未执行,含 `CA19-9` 的 9 个 pair)。 三组 `*_evidence_refs` 在 369 行中全空。 +### 第二轮审核指出的矛盾,以及我为什么会犯 + +revision 2 给出 36/333,把 `CA19-9` 也放进 `L3-00`。**这与契约直接冲突**: +契约把它定为 `resolved_as_non_protein_antigen`,而 `search_complete_definition` +明确接受该 status——**它是已消歧的实体,不是身份未解析**。 + +根因在我自己的命名与实现:契约那张表叫 `known_unresolved_entities`, +里面却有一个 **resolved** 的条目;重建脚本按**表成员身份**而非按 `resolution_status` +赋 `L3-00`。**名字招来了这个 bug,脚本接受了邀请。** + +修在失效点而非症状:表改名 `known_identity_findings`;新增 `l3_00_statuses`; +`l3_00_membership_test: resolution_status`;`l3_00_membership_by_list_forbidden: true`; +每个条目显式声明其 status 蕴含的 `lock_03_rule`。 + +采用审核建议的**方案一**:`CA19-9` 保持 `resolved_as_non_protein_antigen`,转入 `L3-01`。 +它 defer 的原因是 assertion 层未执行,**外加** v0.1.0 按基因符号检索糖类抗原的查询形式无效—— +**不是**身份不明。新增 `non_protein_antigen_search_requirements` 与 `VAL-L29` 固定这一区分。 + +验证脚本的 `L3-00` 期望集**由契约推导**(按 `resolution_status` 过滤),不是硬编码, +因此同类错误再犯会被捕获。 + ## 五、上游缺陷 GAP-P07(登记,不修) -PR #58 冻结的 41 个 target 中至少四个不是可消歧的蛋白实体。 +PR #58 冻结的 41 个 target 中至少四个的身份需单独裁定,且**四者性质不同**: +`Undisclosed` 不是实体;`EDBN`、`AG7` 是无法消歧的缩写; +**`CA19-9` 已消歧,只是不是蛋白**——非蛋白抗原是否属于「所有潜在 ADC 膜蛋白靶点」 +这一 target universe,才是 `GAP-P07` 真正要回答的问题。 + `EVGAP-02` 无权改轴——在本契约内给 `Undisclosed` 编一个身份等于静默改轴。 **binding 本身已察觉这一点**:它把 `identity_unresolved` 列入 `unavailable_outcomes`, @@ -133,27 +160,28 @@ PR #58 冻结的 41 个 target 中至少四个不是可消歧的蛋白实体。 ## 六、产物与校验 -外部包 **revision 2**(仓库内无任何数据文件): +外部包 **revision 3**(仓库内无任何数据文件): | 文件 | 行数 | SHA-256 | |---|---|---| | `retrieval_candidates.tsv` | 979 | `09a2aa75ee7885ed9be3807d8c074e8a31fb81bef6bfff27728181831e5c326a` | | `linkage_assertions.tsv` | 0 | `f0c83e0e2fb0aa13e354babe00164d9287c226bd6d7229c29434d664f744ee8b` | -| `pair_linkage_disposition.tsv` | 369 | `a43b47c874f9a70e3e3b4de11941ef9c87765f06bce5098247ac87d06351598d` | +| `pair_linkage_disposition.tsv` | 369 | `4c4def27c73853ab175c9610c58bcb41c6765276cec133e71f161ee9a7dfc2ac` | | `search_log.tsv` | 451 | `dd0569c572bfd09f74f034f1844811c918182c767118e963afc9ed0a14c7ce08` | | `run_report.md` | — | `9359ac1cc0c25ea3a3e7de06f8f5bd23270bed390db1a6fe7bdbe246e4846059` | -| `external_run_worklog.md` | — | `028a7c2f11a2b6b15b5f651c9f5826f6ee3eeee6f05cd7ebc3c2488e7c361c96` | -| `source_manifest.json` | — | `9fb1f2f75429e438d343a097eabdedeff44ae0f819cc346f71212f64b1647202` | +| `external_run_worklog.md` | — | `0db30b1e0e0fa5e98637619275d595729cb30c2b90fdcaad913ae7e8a1565cd7` | +| `source_manifest.json` | — | `0faac3f936b602f635c7dc64771b6a5faed2ce126cbf2d5d5612be952ac42bda` | **打包件**(按 PR #60 审核后确立的规则,每个修订单独出包并各带自己的 SHA-256): -- `gen_iet_evgap_02_crc_linkage_20260805T190453Z_rev2.zip` -- SHA-256 `e8f2a7f5ce9fae25265994f0d9a1fae1e371a1bb55ccafea2026835bd5120d3d` -- 41,734 bytes,8 个条目 +- `gen_iet_evgap_02_crc_linkage_20260805T190453Z_rev3.zip` +- SHA-256 `ef268fd2f6dcc0c056b0dd01c67da5e850a5e79972ae5858dd49b5f60b49faac` +- 43,443 bytes,8 个条目 +- **revision 2 从未上传**,故其 36/333 未被任何下游依赖;rev2 包已删除以免误取。 仓库内变更:契约 v0.2.0、契约文档、测试、本 handoff、一条 worklog。 -`tests/test_evgap_02_crc_linkage.py` **44 个测试**通过;全库 `Ran 353 tests` OK。 -外部产物另经 25 项 v0.2.0 规则核验,全通过。 +`tests/test_evgap_02_crc_linkage.py` **47 个测试**通过;全库 `Ran 356 tests` OK。 +外部产物另经 20 项 v0.2.0 规则核验,全通过——其中 `L3-00` 期望集由契约推导而非硬编码。 ## 七、边界 @@ -165,7 +193,8 @@ PR #58 冻结的 41 个 target 中至少四个不是可消歧的蛋白实体。 ## 八、后续顺序 1. 本 PR `APPROVE`(契约 v0.2.0 + `L-RETRIEVAL` 层产物),在 `logs/` 留审核记录。 -2. **另开 PR** 处理 `GAP-P07`:四个实体解析为标准符号、定义为非蛋白抗原,或移出轴。 +2. **另开 PR** 处理 `GAP-P07`:`Undisclosed`/`EDBN`/`AG7` 解析为标准符号或移出轴; + `CA19-9` 则须裁定非蛋白抗原是否属于本 target universe。 3. `GAP-P07` 处理后执行 `L-ASSERTION` 抽取 → 结果 PR。 4. 两层齐备后才谈解除 `EVGAP-02`。 5. `EVGAP-01` 走另一条独立轨道(`SRCADM-01` 在 PR #63 待审)。 diff --git a/docs/pools/evgap_02_crc_linkage_extraction.yaml b/docs/pools/evgap_02_crc_linkage_extraction.yaml index fcd7815..aa40f00 100644 --- a/docs/pools/evgap_02_crc_linkage_extraction.yaml +++ b/docs/pools/evgap_02_crc_linkage_extraction.yaml @@ -178,26 +178,59 @@ identity_resolution: 于是 9 个 pair 落 L3-05 EXCLUDE。而 EDBN 应指 fibronectin 的 extra domain B, 其标准符号为 FN1;被排除的原因只是缩写本身不出现在文献里。 # 本次 target 轴中实测四个必须单列的实体。 - known_unresolved_entities: + # 只有这两个 status 触发 L3-00。判据是 status,**不是**「是否出现在下面这张表里」。 + # v0.2.0 首版把该表命名为 known_unresolved_entities 并让执行者按表成员身份赋 + # L3-00,于是 CA19-9 虽已消歧仍落进 L3-00——契约与结果自相矛盾。表名与判据都已改。 + l3_00_statuses: + - unresolvable_placeholder + - unresolvable_ambiguous_abbreviation + l3_00_membership_test: resolution_status + l3_00_membership_by_list_forbidden: true + # 本表登记的是「身份需要单独裁定的实体」,其中既有未消歧的,也有已消歧但 + # 不能按基因符号检索的。两者的 LOCK-03 处置不同。 + known_identity_findings: - input_symbol: Undisclosed resolution_status: unresolvable_placeholder + lock_03_rule: L3-00 note: 这不是实体,是缺失值占位符。v0.1.0 对其执行了 11 次检索。 - input_symbol: EDBN resolution_status: unresolvable_ambiguous_abbreviation + lock_03_rule: L3-00 note: 疑指 fibronectin extra domain B(标准符号 FN1),须由来源轴确认后才可检索。 - input_symbol: AG7 resolution_status: unresolvable_ambiguous_abbreviation + lock_03_rule: L3-00 note: 非标准靶点符号;PMC 返回 56 条,全部为缩写碰撞候选。 - input_symbol: CA19-9 resolution_status: resolved_as_non_protein_antigen + # 已消歧,故**不**落 L3-00。它与其余靶点一样受 assertion 层未执行所阻。 + lock_03_rule: L3-01 + resolved_identifier_kind: non_protein_antigen_definition + resolved_identifier: sialyl-Lewis a(CA19-9 糖类抗原表位) note: >- - sialyl-Lewis a 糖类抗原,非蛋白基因产物,无 HGNC 符号。 - 它可被消歧,但不能按基因符号检索;PMC 返回 14,200 条。 - # 消歧规则的可判定性:四个实体在 target 轴中的 LOCK-01 状态一致,可机械识别。 + 糖类抗原,非蛋白基因产物,无 HGNC 符号——但这是一个可定义的实体, + 不等于身份未解析。它是否属于「所有潜在 ADC 膜蛋白靶点」这一 Level 01 + target universe,属 GAP-P07 的上游问题,不在本契约内裁定。 + invalid_query_form_in_v0_1_0: true + invalid_query_form_note: >- + v0.1.0 按基因符号检索它,PMC 返回 14,200 条。该检索形式无效, + 其候选记录不得计入检索完整性;但这使检索未完成(L3-01),不使身份未解析。 + # 已消歧为非蛋白抗原的实体必须换一种检索形式,不得按基因符号检索。 + non_protein_antigen_search_requirements: + gene_symbol_query_forbidden: true + required_query_basis: + - antigen_or_epitope_name_and_synonyms + - carrier_or_glycan_definition + invalid_query_form_consequence: L3-01 + invalid_query_form_consequence_reason: >- + 检索形式无效等同于该 target 的规定检索未完成, + 与「身份未解析」是两回事,不得混为一谈。 + # 可判定性:四个实体在 target 轴中的 LOCK-01 状态一致,可机械筛出**候选**。 mechanical_precondition: >- 这四个实体在 PR #58 的 target 轴中同属 rq_01_family_count = 0、rule E1-05 - (参考库无对应行)。故「无任何表面注释」与「符号无法消歧」在本轴上高度相关, - 可先按该字段筛出候选再逐个消歧,不依赖自由裁量。 + (参考库无对应行),故可先按该字段机械筛出需要单独裁定身份的候选。 + 但该字段只用于筛候选,**不决定** resolution_status,也不决定是否落 L3-00—— + CA19-9 同属 E1-05,却已消歧。 # ------------------------------------------------- 各 endpoint 的命中证明了什么 # @@ -963,9 +996,15 @@ output_validation: - id: VAL-L27 rule: >- 每个 target 的 input_symbol 必须有 identity_resolution 记录。 - resolution_status 为 unresolvable_placeholder 或 - unresolvable_ambiguous_abbreviation 的 target,其全部 9 个 pair 必须落 L3-00, - 不得落 L3-02,**尤其不得落 L3-05**。 + L3-00 的成员资格由 resolution_status 判定:当且仅当 status 属于 + l3_00_statuses 时落 L3-00,其全部 9 个 pair 不得落 L3-02,**尤其不得落 L3-05**。 + status 为 resolved 或 resolved_as_non_protein_antigen 的 target + **不得**落 L3-00,即便它出现在 known_identity_findings 表中。 + - id: VAL-L29 + rule: >- + resolution_status = resolved_as_non_protein_antigen 的 target + 不得按基因符号检索。其 v0.1.0 候选记录不得计入检索完整性, + 该 target 的 pair 落 L3-01。 - id: VAL-L28 rule: >- ClinicalTrials.gov 来源的 B/C 类 assertion 必须记录 @@ -1021,12 +1060,23 @@ mandatory_findings: # 修复须另开 PR,不得在 EVGAP-02 内静默改轴。 upstream_defect: id: GAP-P07 - defect: PR #58 冻结的 41 个 target 中至少四个不是可消歧的蛋白实体 + defect: PR #58 冻结的 41 个 target 中至少四个的身份需要单独裁定 entities: - Undisclosed - EDBN - AG7 - CA19-9 + # 四个实体的缺陷性质不同,不可混为一谈。 + entity_kinds: + - input_symbol: Undisclosed + kind: 不是实体 + - input_symbol: EDBN + kind: 缩写无法消歧 + - input_symbol: AG7 + kind: 缩写无法消歧 + - input_symbol: CA19-9 + # 这一条才是 GAP-P07 真正要回答的问题。 + kind: 已消歧,但不是蛋白——是否属于本 target universe 需上游裁定 where: external:result/gen_iet_crc_target_enumeration_20260802#target_evidence_catalog.tsv frozen_by_pr: 58 why_not_fixed_here: >- diff --git a/logs/worklog.md b/logs/worklog.md index df0a851..23f49c2 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2688,3 +2688,20 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Boundary: 未运行任何 Gate、未赋分数、未评估 T2/T7、未排序、未划 Tier、未推荐资产、未给实验建议、**未新增或修改靶点与 context**、未打开任何 Tier 2 派生库、未引用被隔离运行产物、未写入任何数据文件到仓库。 - Deliberately not done: **未解除 `EVGAP-02`**;未处理 `GAP-P07`;未执行 `L-ASSERTION` 抽取;未解除 `EVGAP-01`;未更新 binding;未生成 `ADC_POOL_LEVEL_01_ACCEPTED`;未补九份批准记录。 - Next: 送审本修订;获批后另开 PR 处理 `GAP-P07`,再执行 `L-ASSERTION` 抽取。 + +### 2026-08-05 17:30 EDT — EVGAP-02:修正 CA19-9 的 L3-00 误判(PR #62 第三轮) + +- Trigger: ChatGPT 对 PR #62 第二轮 `REQUEST_CHANGES`——revision 2 把 `CA19-9` 放进 `L3-00`,与契约直接冲突。**该意见成立。** +- The contradiction: 契约把 `CA19-9` 定为 `resolved_as_non_protein_antigen`,而 `search_complete_definition` 明确接受该 status。**它是已消歧的实体,不是身份未解析。** 只有 `unresolvable_placeholder` 与 `unresolvable_ambiguous_abbreviation` 才应触发 `L3-00`。 +- Root cause in my own work: 契约那张表命名为 `known_unresolved_entities`,**里面却有一个 resolved 的条目**;重建脚本按**表成员身份**而非按 `resolution_status` 赋 `L3-00`。**名字招来了这个 bug,脚本接受了邀请。** +- Fixed at the point of failure, not the symptom: 表改名 `known_identity_findings`;新增 `l3_00_statuses`(只含两个 unresolvable);`l3_00_membership_test: resolution_status`;`l3_00_membership_by_list_forbidden: true`;每个条目显式声明其 status 蕴含的 `lock_03_rule`,测试逐条比对二者是否自洽。 +- Option 1 taken: `CA19-9` 保持 `resolved_as_non_protein_antigen`,转 `L3-01`。新增 `non_protein_antigen_search_requirements`——非蛋白抗原**不得按基因符号检索**,v0.1.0 的 14,200 条 PMC 命中出自无效查询形式;`invalid_query_form_consequence: L3-01`,即**检索未完成,而非身份未解析**。新增 `VAL-L29`。 +- Result rebuilt (revision 3): `L3-00` **27**(`Undisclosed`/`EDBN`/`AG7` × 9),`L3-01` **342**。仍是 0 RETAIN、0 EXCLUDE、369 hold、三组 refs 全空、assertion 表空、候选 979 未变、**未重复任何网络调用**。 +- GAP-P07 sharpened: 四个实体**性质不同**,不可混为一谈。三个是不可消歧的符号;**`CA19-9` 已消歧、只是不是蛋白**——非蛋白抗原是否属于「所有潜在 ADC 膜蛋白靶点」这一 target universe,才是 `GAP-P07` 真正要回答的问题。契约新增 `entity_kinds` 分列。 +- Verification hardened: 外部核验脚本的 `L3-00` 期望集**由契约按 `resolution_status` 推导**,不是硬编码,故同类错误再犯会被捕获。20 项核验全通过。 +- Tests: `tests/test_evgap_02_crc_linkage.py` **47 tests**(新增 3 条:status 而非成员身份、非蛋白抗原检索形式、两处 completeness 定义互不矛盾);全库 `Ran 356 tests` OK;boundary check 通过。 +- Mutation testing: **10 个新变异全部被捕获**,`diff -q` 回滚干净。 +- Packaged: `gen_iet_evgap_02_crc_linkage_20260805T190453Z_rev3.zip`,43,443 bytes,ZIP SHA-256 `ef268fd2f6dcc0c056b0dd01c67da5e850a5e79972ae5858dd49b5f60b49faac`。**revision 2 从未上传**,其 36/333 未被任何下游依赖;rev2 包已删除以免误取。 +- Also done this turn: 按人工负责人指示关闭 PR #55 并注明 superseded by #56(#56 已于 2026-08-04 合并)。**分支保留不删**——删除分支须单独授权。 +- Deliberately not done: 未解除 `EVGAP-02`;未处理 `GAP-P07`;未执行 `L-ASSERTION` 抽取;未改 target 轴;未补九份批准记录。 +- Next: 送审 revision 3;并按第二轮审核要求为 PR #63 制作可独立复核的 SRCADM-01 audit bundle。 diff --git a/tests/test_evgap_02_crc_linkage.py b/tests/test_evgap_02_crc_linkage.py index 9431f0b..d4cca8d 100644 --- a/tests/test_evgap_02_crc_linkage.py +++ b/tests/test_evgap_02_crc_linkage.py @@ -676,11 +676,11 @@ def test_identity_resolution_precedes_search_and_blocks_both_verdicts(self) -> N with self.subTest(field=field): self.assertIn(field, block["resolution_must_record"]) - def test_the_four_unresolvable_entities_are_named_with_their_kind(self) -> None: + def test_the_four_flagged_entities_are_named_with_their_kind(self) -> None: """Each was searched as a gene symbol by the v0.1.0 run.""" block = self.doc["identity_resolution"] - entities = {e["input_symbol"]: e for e in block["known_unresolved_entities"]} + entities = {e["input_symbol"]: e for e in block["known_identity_findings"]} self.assertEqual(set(entities), {"Undisclosed", "EDBN", "AG7", "CA19-9"}) vocabulary = set(block["resolution_status_vocabulary"]) @@ -696,8 +696,71 @@ def test_the_four_unresolvable_entities_are_named_with_their_kind(self) -> None: "resolved_as_non_protein_antigen") self.assertEqual(entities["EDBN"]["resolution_status"], "unresolvable_ambiguous_abbreviation") - # Recognising them must not require free judgement. - self.assertIn("E1-05", block["mechanical_precondition"]) + # Recognising them must not require free judgement — but the field that + # finds them must not be mistaken for the field that classifies them. + precondition = block["mechanical_precondition"] + self.assertIn("E1-05", precondition) + self.assertIn("CA19-9", precondition) + + def test_l3_00_membership_follows_status_not_list_membership(self) -> None: + """The v0.2.0 first draft put CA19-9 in L3-00 although it is resolved. + + The table is a list of entities whose identity needed adjudicating; two + of its statuses mean unresolved and one means resolved. Keying L3-00 on + membership rather than status contradicts the contract.""" + + block = self.doc["identity_resolution"] + statuses = set(block["l3_00_statuses"]) + self.assertEqual(statuses, {"unresolvable_placeholder", + "unresolvable_ambiguous_abbreviation"}) + self.assertEqual(block["l3_00_membership_test"], "resolution_status") + self.assertIs(block["l3_00_membership_by_list_forbidden"], True) + # A resolved status may never be an L3-00 status. + for resolved in ("resolved", "resolved_as_non_protein_antigen"): + with self.subTest(status=resolved): + self.assertNotIn(resolved, statuses) + # Every flagged entity must declare the rule it lands on, and that rule + # must agree with its status. + for entry in block["known_identity_findings"]: + with self.subTest(symbol=entry["input_symbol"]): + expected = "L3-00" if entry["resolution_status"] in statuses \ + else "L3-01" + self.assertEqual(entry["lock_03_rule"], expected) + # CA19-9 specifically: resolved, so L3-01, and it must carry an identifier. + ca = next(e for e in block["known_identity_findings"] + if e["input_symbol"] == "CA19-9") + self.assertEqual(ca["lock_03_rule"], "L3-01") + self.assertTrue(ca["resolved_identifier"].strip()) + self.assertEqual(ca["resolved_identifier_kind"], "non_protein_antigen_definition") + self.assertIn(ca["resolved_identifier_kind"], + block["resolved_identifier_kinds"]) + + def test_a_non_protein_antigen_may_not_be_queried_by_gene_symbol(self) -> None: + block = self.doc["identity_resolution"]["non_protein_antigen_search_requirements"] + self.assertIs(block["gene_symbol_query_forbidden"], True) + self.assertTrue(block["required_query_basis"]) + # An invalid query form leaves the search incomplete; it does not make + # the identity unresolved. + self.assertEqual(block["invalid_query_form_consequence"], "L3-01") + self.assertTrue(block["invalid_query_form_consequence_reason"].strip()) + ids = {r["id"] for r in self.doc["output_validation"]} + self.assertIn("VAL-L29", ids) + + def test_the_two_completeness_definitions_agree_on_resolved_statuses(self) -> None: + """search_complete accepts both resolved statuses, so neither may be + treated as unresolved when the disposition is assigned.""" + + definition = " ".join( + self.doc["declared_search_scope"]["search_complete_definition"]) + self.assertIn("resolved_as_non_protein_antigen", definition) + statuses = set(self.doc["identity_resolution"]["l3_00_statuses"]) + # The statuses search_complete accepts and the statuses that force L3-00 + # must be disjoint, or a target could be both complete and unresolved. + accepted = {s for s in + self.doc["identity_resolution"]["resolution_status_vocabulary"] + if s in definition} + self.assertTrue(accepted) + self.assertEqual(accepted & statuses, set()) def test_l3_00_reuses_the_frozen_outcome_vocabulary(self) -> None: """LOCK-03 has no identity_unresolved outcome; that belongs to LOCK-01.""" From aa3583dc0bc1180504b08c56e7bbeee9a991dbf7 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Thu, 6 Aug 2026 15:59:28 -0400 Subject: [PATCH 4/4] No semantic or architectural changes. This revision only provides the exact external review package requested in the previous review. Co-Authored-By: Claude Opus 5 --- ...-08-05-evgap-02-extraction-result.zh-CN.md | 44 +++++++++++++++---- logs/worklog.md | 13 ++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md index 96967a8..0ea8346 100644 --- a/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md +++ b/docs/handoff/2026-08-05-evgap-02-extraction-result.zh-CN.md @@ -160,28 +160,54 @@ PR #58 冻结的 41 个 target 中至少四个的身份需单独裁定,且** ## 六、产物与校验 -外部包 **revision 3**(仓库内无任何数据文件): +外部包 **revision 3**(仓库内无任何数据文件)。**8 个文件**,ZIP 内 9 个条目(多出的一条是目录条目): | 文件 | 行数 | SHA-256 | |---|---|---| | `retrieval_candidates.tsv` | 979 | `09a2aa75ee7885ed9be3807d8c074e8a31fb81bef6bfff27728181831e5c326a` | | `linkage_assertions.tsv` | 0 | `f0c83e0e2fb0aa13e354babe00164d9287c226bd6d7229c29434d664f744ee8b` | -| `pair_linkage_disposition.tsv` | 369 | `4c4def27c73853ab175c9610c58bcb41c6765276cec133e71f161ee9a7dfc2ac` | +| `pair_linkage_disposition.tsv` | 369 | `33674913edf0d1efb82f4bc2a8303e55e18f2a39a32ba7b2dd3152c5f6734dad` | | `search_log.tsv` | 451 | `dd0569c572bfd09f74f034f1844811c918182c767118e963afc9ed0a14c7ce08` | -| `run_report.md` | — | `9359ac1cc0c25ea3a3e7de06f8f5bd23270bed390db1a6fe7bdbe246e4846059` | +| `run_report.md` | — | `4c4def27c73853ab175c9610c58bcb41c6765276cec133e71f161ee9a7dfc2ac` | | `external_run_worklog.md` | — | `0db30b1e0e0fa5e98637619275d595729cb30c2b90fdcaad913ae7e8a1565cd7` | -| `source_manifest.json` | — | `0faac3f936b602f635c7dc64771b6a5faed2ce126cbf2d5d5612be952ac42bda` | +| `verify_package.py` | — | `63bd46eb220a6d12be594ad809b03a933dbd3b2901d7d5d9a4d1e7d9b3701bcf` | +| `source_manifest.json` | — | `776677c571725060175e3e480936e8f17eeeebb3ee67d5472c2a52c114a5265f` | -**打包件**(按 PR #60 审核后确立的规则,每个修订单独出包并各带自己的 SHA-256): +**打包件(唯一正式受审包):** - `gen_iet_evgap_02_crc_linkage_20260805T190453Z_rev3.zip` -- SHA-256 `ef268fd2f6dcc0c056b0dd01c67da5e850a5e79972ae5858dd49b5f60b49faac` -- 43,443 bytes,8 个条目 -- **revision 2 从未上传**,故其 36/333 未被任何下游依赖;rev2 包已删除以免误取。 +- SHA-256 `81baa45f23f180c68b16d18c83284b60bdee725c017e668e590d4e80b04176e9` +- 46,292 bytes,8 个文件(ZIP 内 9 个条目) +- `revision = 3` + +**上一版声明的 ZIP SHA-256 `ef268fd2…` 作废**——包内新增了 `verify_package.py`, +内容变了哈希必然变。**以本表为准。** + +同时更正上一版 handoff 的一处笔误:`pair_linkage_disposition.tsv` 曾被写成 +`4c4def27…`,那其实是 `run_report.md` 的哈希。本表由 `source_manifest.json` 直接生成, +不再手抄。 + +### 包内自带验证脚本 + +``` +python3 verify_package.py . +python3 verify_package.py . --zip ../.zip --zip-sha256 +``` + +只读包内文件,无网络、无写入、不依赖包外路径,解压即可运行,退出码 0 表示全通过。 +逐项检查:文件数、每个文件的 SHA-256 与字节数、清单未遗漏文件、`revision = 3`、 +`979 / 0 / 369`、schema(候选表无 `linkage_class`、三个标记列齐备)、 +候选表三个固定值、`L3-00 27` / `L3-01 342` / `L3-02..L3-05` 全为 `0`、 +无 RETAIN 无 EXCLUDE、369 行全 DEFER/hold、三组 refs 全空、 +`may_advance_to_level_02` 全 `false`、三个不可消歧 target 各 9 个 pair 全 `L3-00`、 +`CA19-9` 9 个 pair 全 `L3-01` 且 `identity_resolution_status = +resolved_as_non_protein_antigen`、`EVGAP-02` 未解除。 + +**实测(从全新解压目录运行,含 ZIP 哈希校验):`65/65 MATCH`,退出码 0。** 仓库内变更:契约 v0.2.0、契约文档、测试、本 handoff、一条 worklog。 `tests/test_evgap_02_crc_linkage.py` **47 个测试**通过;全库 `Ran 356 tests` OK。 -外部产物另经 20 项 v0.2.0 规则核验,全通过——其中 `L3-00` 期望集由契约推导而非硬编码。 +外部产物另经包内 `verify_package.py` 核验:**65/65 MATCH**,退出码 0。 ## 七、边界 diff --git a/logs/worklog.md b/logs/worklog.md index 3dfd7b7..4c5c64a 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2713,3 +2713,16 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Versioning: Updated the version index to state that unapproved `v2-draft` was superseded without a snapshot; no `v3` snapshot is created before approval. Updated architecture/README navigation, added this repository handoff, and refreshed workspace `HANDOFF.md` with the active v3 review task. - Boundary: Documentation-only. No data, cache, result, model weight, provider execution, Gate execution, contract change, lifecycle change, or module implementation was added. - Validation: Final branch validation ran 338 tests successfully; `scripts/verify_repository_boundary.sh` and `git diff --check` passed; no `__pycache__` directory was generated. + +### 2026-08-06 16:10 EDT — EVGAP-02:冻结语义,只补正式受审包(PR #62 第四轮) + +- Instruction: 冻结当前 HEAD,不改架构/契约/规则/测试逻辑,只补齐实际受审包并使包内数据严格匹配 handoff。 +- **本轮未改任何语义**:`docs/pools/evgap_02_crc_linkage_extraction.yaml` 未动、`tests/` 未动、契约版本仍 v0.2.0、disposition 逻辑未动、未新增 mandatory finding、未改 target 轴、未补 admission binding、未执行 EVGAP-01、未解除 EVGAP-02。 +- Added to the package only: `verify_package.py`(只读验证脚本)。因包内容变化,ZIP 哈希必然变化,故三处声明同步更新。 +- Package: `gen_iet_evgap_02_crc_linkage_20260805T190453Z_rev3.zip`,SHA-256 `81baa45f23f180c68b16d18c83284b60bdee725c017e668e590d4e80b04176e9`,46,292 bytes,**8 个文件**(ZIP 内 9 个条目,多出的一条是目录条目),`revision = 3`。**上一版声明的 `ef268fd2…` 作废。** +- Three declarations aligned: handoff、PR body、`source_manifest.json` 现在写同一个文件名、同一个 ZIP SHA-256、同一个文件数、同一个 revision。 +- Corrected a transcription error in the previous handoff: `pair_linkage_disposition.tsv` 曾被写成 `4c4def27…`,那其实是 `run_report.md` 的哈希。真实值 `33674913…`。本轮的哈希表由 `source_manifest.json` 直接生成,不再手抄。 +- Verifier scope: 文件数、逐文件 SHA-256 与字节数、清单未遗漏文件、可选 ZIP 哈希、`revision = 3`、`979 / 0 / 369`、schema(候选表无 `linkage_class`、三个标记列齐备)、候选表三个固定值、`L3-00 27`/`L3-01 342`/`L3-02..L3-05` 全 `0`、无 RETAIN 无 EXCLUDE、369 行全 DEFER/hold、三组 refs 全空、`may_advance_to_level_02` 全 `false`、三个不可消歧 target 各 9 pair 全 `L3-00`、`CA19-9` 9 pair 全 `L3-01` 且 status 为 `resolved_as_non_protein_antigen`、`EVGAP-02` 未解除。 +- Verified from a clean extract in a temp directory (not the working copy), with the ZIP digest passed in: **`65/65 MATCH`,退出码 0**。 +- One self-caught slip: 脚本初版把「文件数」定为 9,实为 8——ZIP 的第 9 个条目是目录条目本身。已改正并在 handoff 中写明该区别。另注意 `python3 ... | tail` 会把 `tail` 的退出码当成脚本的,实际退出码须用 `PIPESTATUS`。 +- Repo-side changes this round: 仅本条 worklog 与 handoff 的包信息段。测试与契约零改动。