From 0e39ef58ad462813d507f70bbd49cd8b5b5bb8f2 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 18:41:53 -0400 Subject: [PATCH 1/2] task_20260804: define ADC Pool funnel Level 01 (contract-only, not executed) Freeze the definition, criteria, ordering, semantics, evidence standard and output validation for ADC Pool Level 01, and record which criteria the level uses in a machine-readable registry. Nothing is executed: no clinical context is enumerated, no target is enumerated, no pair, disposition, ranking or recommendation is produced. This ordering is what the 2026-08-04 rulings on PR #53 and #54 require: a contract-only PR freezes scope and semantics first, then APPROVE, then the run. This PR is that prerequisite for Level 01. Level 01 executes zero Gates. It runs three eligibility locks that produce CandidateFilterResult, which the contract states explicitly is not a Gate result. Each lock borrows one Gate's responsibility without constituting that Gate's PASS. There is also a hard reason, measured from the repository: the dependency chain is T7 -> T2 -> {T0, T1}, and the source document rules T1 out of Level 01-03, so Level 01 cannot produce a legitimate T7 result at all. Locks are the only expression compatible with the frozen topology. Cost is defined as marginal cost per candidate eliminated, which granularity determines: eliminating a clinical context removes a whole column, a target a whole row, a pair-level criterion one cell. Hence context -> target -> pair, enforced mechanically by test. Cheapness alone is insufficient: C42/C44/C45 are all cost_tier low, cheaper than T2/T7/T11, but competitive crowding must not KILL alone, so they cannot run first. GAP-P01..GAP-P06 are recorded, not resolved. GAP-P05 is a direct conflict between EarlyReductionSchedule, which forces T2 before T7, and the source document, which wants T7 first at Level 02. BLOCK-02: LOCK-02 needs a CRC clinical context list, and the only enumeration available comes from the quarantined PR #53 run, so even with APPROVE Level 01 cannot execute until that upstream run is re-executed and accepted. No architecture change: src/, src/contracts/, genmodules/, extensions/, docs/architecture/ and AGENTS.md are untouched; no Gate added and the 45-Gate topology is unchanged. 242 tests pass (228 baseline + 14 new). The new tests were mutation-tested, with all five mutations caught and rolled back exactly. Co-Authored-By: Claude Opus 5 --- .../2026-08-04-adc-pool-level-01.zh-CN.md | 123 ++++++++ docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md | 199 ++++++++++++ docs/pools/adc_pool_gate_usage.yaml | 286 ++++++++++++++++++ logs/worklog.md | 27 ++ tests/test_adc_pool_gate_usage.py | 193 ++++++++++++ 5 files changed, 828 insertions(+) create mode 100644 docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md create mode 100644 docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md create mode 100644 docs/pools/adc_pool_gate_usage.yaml create mode 100644 tests/test_adc_pool_gate_usage.py diff --git a/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md b/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md new file mode 100644 index 0000000..fb4eccd --- /dev/null +++ b/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md @@ -0,0 +1,123 @@ +# Handoff:ADC Pool 漏斗 Level 01 定义与执行契约 + +- 日期:`2026-08-04` +- 任务分支:`task_20260804_adc-pool-level-01` +- 基线:`main` @ `e7092d5` +- 来源:`Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md` 的 `# ADC pool漏斗gating`(第 2–602 行,只读取,未修改) +- 交付物类型:**contract-only** +- 外部运行:**无。本次没有执行任何外部运行,没有产生任何候选、pair、disposition、排序或推荐。** +- 架构变更:`NO_ARCHITECTURE_CHANGE`(依据是 diff 范围,可由 `git diff --stat` 核验;见第六节) +- 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** + +## 一、本次范围 + +人类负责人的指示是:读取来源文档的 ADC pool 漏斗 gating 一节,开始构建 ADC Pool,**先做 Level 01,审核完了再做下一个**;核心原理是先用一些 gate 锁定漏斗的最大可能性集合,再逐级加层、加入新 gate 逐级下筛;**每一个 Level 用了什么 gate 都要记录下来**,且先用最低成本的高可信 gate,较难的 gate 放到后面。 + +本次交付 Level 01 的**定义与执行契约**,不执行 Level 01。这个先后顺序是 2026-08-04 对 PR #53 与 #54 的审核裁决所要求的:先用 contract-only PR 预先冻结范围、语义、证据标准与输出验证,获 `APPROVE` 后再运行,重跑产物才是被接受的产物。本 PR 就是 Level 01 的那个前置契约。 + +候选池本身是数据,按仓库硬边界必须留在外部工作区;本仓库只冻结判据身份、顺序与语义。 + +## 二、仓库内交付了什么 + +三个文件,全部数据自由(data-free): + +| 文件 | 作用 | +|---|---| +| `docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md` | Level 01 的完整定义与执行契约(面向操作者,中文) | +| `docs/pools/adc_pool_gate_usage.yaml` | **每层用了什么判据的权威机器可读记录**,即人类负责人要求「记录下来」的那份记录。每新增一层在 `levels` 下追加一个条目 | +| `tests/test_adc_pool_gate_usage.py` | 14 项校验,把上面那份记录钉在冻结的 45-Gate 拓扑与 `CandidateFilterResult` 语义上 | + +## 三、Level 01 的核心结论 + +**Level 01 运行的 Gate 数量是零。** 它只运行三把 eligibility lock,产出 `CandidateFilterResult`——按契约明确不是 Gate 结果。三把锁各自借用一个 Gate 的**职责**,但都不构成该 Gate 的 PASS。 + +| 顺序 | Lock | 粒度 | 借用职责 | +|---|---|---|---| +| 1 | `LOCK-02` crc_clinical_context_eligibility | clinical context 级 | `clinical_context_endpoint`(T0,`low`) | +| 2 | `LOCK-01` target_identity_eligibility | target 级 | `tumor_cell_surface_availability`(T7,`medium`) | +| 3 | `LOCK-03` target_context_linkage_existence | pair 级 | `target_population_mapping`(T2,`medium`) | + +`Universe Index`(完整笛卡尔积,用于证明覆盖范围)与 `Pool Level 01 active`(只有至少一项 linkage 证据的 pair)分成两层;未录入的 pair 不删除,状态为 `reactivation-eligible`。 + +## 四、关键设计决策 + +**1. 把「成本」定义为每淘汰一个候选的边际成本,而不是 catalogue 的 `cost_tier`。** +边际成本由判据的作用粒度决定:淘汰一个 clinical context 一次性移除候选矩阵的一整列,淘汰一个 target 移除一整行,pair 级判据每次只处理一格。因此顺序必然是 context 级 → target 级 → pair 级。这与来源文档第四节给出的 Level 01 顺序独立地一致,并解释了它为什么对。该约束由 `test_locks_run_cheapest_granularity_first` 机械保证,顺序退化即失败。 + +**2. 明确「便宜本身不够,必须同时具备否决力」。** +这条来自实测反例而非推理:45 个 Gate 中 `cost_tier = low` 的只有 7 个——T0 与 C40–C45。`competitive_position_entry_window`、`patent_landscape`、`preliminary_technical_fto` 全是 `low`,**比 T2/T7/T11 的 `medium` 更便宜**;但来源文档第二节明确竞争拥挤不得单独 KILL(已有成功竞争者同时也是靶点与 modality 可行的证据)。所以它们尽管最便宜也不能排在最前。完整表述是:**在阴性结果具有否决力的判据里,先跑边际成本最低的那个。** + +**3. 每个锁输出必须同时记录 `pool_state`。** +`CandidateDisposition` 只有三值,来源文档的锁输出是四值,且第七节要求「无 linkage 的 pair 留在 Universe Index 不删除」——这是「未录入但仍存活」,既不是 EXCLUDE(已淘汰)也不是 DEFER(等证据)。只写 disposition 会丢信息,所以 12 个 outcome 全部同时写 `disposition` 与 `pool_state`,并登记为 `GAP-P04`。 + +**4. 每个 EXCLUDE 都必须写明排除依据,且只有定义性依据才允许排除。** +Level 01 的错误偏好是召回优先。因此只有 `not_surface_target`(纯胞内蛋白不可能是 ADC 靶点)与 `redundant_context`(可判定的集合包含关系)允许排除;`no_known_linkage` 虽记为 EXCLUDE,但 `pool_state` 强制为 `reactivation-eligible`,因为无关联证据不等于已证伪。由 `test_every_exclusion_declares_its_basis` 强制。 + +**5. `DEVIATION-01`(唯一的实质偏离,明示待审核方裁决)。** +来源文档把 weak 与 redundant 合成一个 `weak_or_redundant_context` 输出,本契约拆成 `redundant_context`(EXCLUDE)与 `weak_context`(DEFER)。理由:`redundant` 可判定,`weak` 是价值判断;若合并并映射为 EXCLUDE,本层就会因价值判断丢候选,直接违反它自己声明的错误偏好。明示以便审核方接受或否决,而不是静默改写来源文档。 + +## 五、对照仓库实测得到的发现 + +**发现 1(高):在不运行 T1 的前提下,Level 01 在结构上不可能产生一个合法的 T7 结果。** +实测依赖链为 `tumor_cell_surface_availability → target_population_mapping → {clinical_context_endpoint, endpoint_driving_population}`。来源文档第五节明确 T1 不适合作 Level 01–03 的普遍筛选器,而冻结拓扑要求按依赖顺序执行、不得跳过前置 Gate。所以把三把锁写成 `CandidateFilterResult` 不是图省事,而是唯一与冻结拓扑相容的表达方式。这给来源文档「Level 01 不要跑完整 Gate」的设计取舍补上了一个硬理由。 + +**发现 2(高,属 Level 02,登记为 `GAP-P05`):既有内核与来源文档在 Level 02 的 Gate 顺序上直接冲突。** +`src/capabilities/early_t_gate_reduction.py` 的 `EarlyReductionSchedule.__post_init__` 硬性要求 `gate_ids[:2] == (target_population_mapping, tumor_cell_surface_availability)`,即 **T2 必须先于 T7**;而来源文档第四节要求 Level 02 **先跑 T7**。二者不能同时成立,必须在定义 Level 02 时解决。现在登记,避免下一层重新发现。 + +**发现 3(中,属 Level 02,登记为 `GAP-P06`):`EARLY_REDUCTION_GATE_IDS` 只含 T2/T7/T8/T9/T10/T11,不含任何 C Gate**,故来源文档 Level 02 的 C2/C4/C5 quick scan 无法通过既有能力调度。 + +**发现 4(中):Level 01 的三把锁不需要任何新契约。** +对照实际文件得到的对应关系:锁结果 → `CandidateFilterResult`(`filter_id` 承载 lock id);三值 → `CandidateDisposition`;未评估/未解决 → `EvaluationStatus`;clinical context → v5 `AnchorClinicalContext` + `IntendedBenefitHypothesis`;「至少一项 linkage 证据」→ `TargetCandidateGenerationPolicy.minimum_distinct_positive_evidence_groups`;笛卡尔积上限 → `maximum_candidates_per_clinical_frame` 与 `candidate_budget`;证据出处 → `EvidenceRecord`。 + +**发现 5(中):契约层已经禁止「模型单独生成候选」。** +`TargetCandidateGenerationPolicy.__post_init__` 对 `permit_model_only_generation` 与 `permit_rule_only_generation` 一律抛错。这正是 PR #53 被阻断的那一点,已经是契约级强制,不只是 policy 约定。本契约据此把「模型领域知识单独不足以录入一个 pair」写入证据标准。 + +**发现 6(低):`filter_policy_ref` 必须是 `external:`**,因此锁的具体数值门槛按契约设计属于外部 policy。本仓库只冻结判据身份、顺序与语义,这是设计使然而非让步。 + +## 六、架构影响 + +本 PR 改动五个文件:上述三个 + 本 handoff + 一条 `logs/worklog.md`。 + +未触碰:`src/`、`src/contracts/`、`genmodules/`、`genmodules/assetgenos_catalog/`、`extensions/`、`docs/architecture/`、`AGENTS.md`、`prompts/`。未新增 Gate、未改 45-Gate 拓扑与身份、未改四阶段生命周期、未改八类核心对象、未改 `GateInputEnvelope@2.0.0` 与 `GateModelOutput@2.0.0`、未改任何 Model 或 Profile。可由 `git diff --stat main...HEAD` 核验。 + +据此本 PR 不构成架构变更,不消耗 2026 年 8 月的月度架构修复额度。第七节的六条缺口确实是架构问题,**它们仍未解决**,需要独立任务与额度;执行者不代为裁决。 + +## 七、已记录但未解决的缺口 + +| ID | 缺口 | 影响层 | +|---|---|---| +| `GAP-P01` | 内核没有 pool level 身份或层级快照对象 | 01 | +| `GAP-P02` | 内核没有 `active`/`hold`/`killed`/`superseded`/`reactivation-eligible` 这组 pool 生命周期状态 | 01 | +| `GAP-P03` | 内核没有与 Pool Level 01 相区分的 Universe Index 对象 | 01 | +| `GAP-P04` | `CandidateDisposition` 三值不足以表达四值 lock 输出、Level 02 五值输出,以及「未录入但可复活」 | 01 | +| `GAP-P05` | `EarlyReductionSchedule` 强制 T2 先于 T7,与来源文档 Level 02 冲突 | 02 | +| `GAP-P06` | `EARLY_REDUCTION_GATE_IDS` 不含任何 C Gate | 02 | + +## 八、明确没有做什么 + +- **没有执行 Level 01。** 没有枚举 clinical context、没有枚举 target、没有生成任何 pair、没有给出任何 disposition、没有排序、没有推荐。 +- **没有定义 Level 02 与 Level 03。** `defined_levels` 只有 `"01"`,由测试断言与 `levels` 条目一致。来源文档第八节的 Level 02/03 Gate 清单只作为 `GAP-P05`/`GAP-P06` 的上下文被引用,未被冻结为定义。 +- **没有实现第七节的六条缺口**,也没有为了让 Level 01「看起来完整」而私自扩展 `CandidateDisposition` 或新增 pool 生命周期枚举。 +- **没有使用被隔离运行的任何产物。** PR #53 与 #54 的两次外部运行仍是 `UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED`,其 20 个 unmet-need 场景、45 个靶点、Tier A 选择、payload 结论、Seed Admission Standard 与靶点 disposition **一条都没有被引用为输入或依据**。 +- **没有修正一处已发现的无关缺陷。** `requirements.txt` 的注释写「the full suite (207 tests)」,实测为 228。这属于本次范围之外的改动,按第 25 条不在本 PR 内顺手改;此处记录,留待相关 PR 处理。 +- 没有创建仍然欠着的两个 contract-only PR(#53 上游 CRC clinical frame 的、Playbook 六模块的),也没有补 #53/#54 的批准记录。二者都是独立范围。 + +## 九、验证结果 + +- `Ran 242 tests` 全部通过(`main` 基线 228 + 本次新增 14)。 +- `scripts/verify_repository_boundary.sh`:`Repository boundary check passed.` +- `git diff --check`:通过。 +- 零 `__pycache__`(全程 `PYTHONDONTWRITEBYTECODE=1`)。 +- **新增测试做过变异检验**,不是只看绿灯。5 个变异各自被捕获、随后精确回滚:把 `LOCK-02` 的 `borrowed_gate_cost_tier` 由 `low` 改成 `medium`(`FAILED`);允许 RNA 满足 `LOCK-01`(`FAILED`);从 `gates_not_run` 删掉 `transaction_readiness`(`FAILED`);把 pair 级锁的 `run_order` 提到第一(`FAILED`);删掉一个 EXCLUDE 的 `exclusion_basis`(`FAILED`)。回滚后与备份 `diff -q` 一致、测试恢复 `OK`。 +- 数值对账:`snapshot_columns` 25 列、`gates_not_run` 45 项、3 把锁 12 个 outcome、6 条缺口、catalogue 中 `cost_tier = low` 的 Gate 7 个——全部由脚本实测,不是估计。 + +## 十、当前阻断 + +- `BLOCK-01`:本契约获 ChatGPT `APPROVE` 前,**不得执行 Level 01**。 +- `BLOCK-02`:`LOCK-02` 需要一份 CRC clinical context 清单,而目前唯一的枚举来自被隔离的 PR #53 运行,按裁决不得作为任何后续工作的输入。**因此即使本契约获批,Level 01 仍不能执行**,必须先按裁决重跑 CRC clinical frame 并被接受。这是隔离裁决的直接后果,此处明示而非绕过。 + +## 十一、请审核方重点看的三点 + +1. `DEVIATION-01` 是否接受:把来源文档的 `weak_or_redundant_context` 拆成 `redundant_context`(EXCLUDE)与 `weak_context`(DEFER)。 +2. 第六节的 `NO_ARCHITECTURE_CHANGE` 判断是否成立。这一次的依据是 diff 范围而非推导结论,与 PR #54 被降级的那个断言性质不同;若仍认为它只能是待审假设,请指出。 +3. `BLOCK-02` 的处理是否正确:Level 01 的定义可以先冻结,但执行必须等 CRC clinical frame 重跑被接受。 diff --git a/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md b/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md new file mode 100644 index 0000000..bee7117 --- /dev/null +++ b/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md @@ -0,0 +1,199 @@ +# ADC Pool 漏斗 Level 01 定义与执行契约 + +- 任务分支:`task_20260804_adc-pool-level-01` +- 来源:`Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md` 的 `# ADC pool漏斗gating` 一节(第 2–602 行,只读取未修改) +- 机器可读记录:[`adc_pool_gate_usage.yaml`](./adc_pool_gate_usage.yaml),由 `tests/test_adc_pool_gate_usage.py` 校验 +- 当前状态:**contract-only,未执行,等待 ChatGPT 审核** + +## 目的 + +冻结 ADC Pool 漏斗 Level 01 的**定义、判据、顺序、语义、证据标准和输出验证**,并把本层使用了哪些判据写成可复核的机器可读记录。 + +本文件**不执行 Level 01**,不生成候选、不生成 pair、不生成 disposition、不排序、不推荐靶点。获得 `APPROVE` 后才可执行;执行产物全部留在外部工作区。 + +这一顺序是 2026-08-04 对 PR #53 与 #54 的审核裁决所要求的:先用 contract-only PR 预先冻结范围与语义,获批后再运行,**重跑产物才是被接受的产物**。本 PR 就是 Level 01 的那个前置契约。 + +## 一、Level 01 是什么 + +> **CRC ADC Candidate Universe v0.1:由公开可枚举膜蛋白 × 明确 CRC clinical contexts 构成的高召回候选池。** + +必须按来源文档收紧口径:**Level 01 只能宣称是「当前公开知识下、按既定定义构建的高覆盖候选宇宙」,不得宣称是理论上所有 CRC indication–target pair。** 至少以下六类未来仍可能出现且不可由 gene-level surfaceome 枚举:新发现的膜蛋白或蛋白异构体、非经典膜相关抗原、肿瘤特异糖基化/剪切/构象表位、治疗诱导后才暴露的 target、正常组织存在但肿瘤中可达性异常的 target、复合物或非蛋白抗原。 + +Level 01 的目标不是准确,而是:尽量不漏、每个 pair 可追溯、可增量更新、后续由低成本判据快速压缩。因此本层的错误偏好明确是 **recall 优先于 precision**:宁可保留可疑候选,不要过早漏掉。 + +### 最小单元是 Clinical context × Target,不是 indication × target + +Level 01 的一个候选是一个 pair,其 clinical context 至少包含 disease setting、line of therapy、molecular subgroup、treatment history、metastatic site 或 resistant context。 + +此时 **endpoint 不锁定**,只记录 intended clinical benefit、endpoint class、endpoint maturity、unresolved endpoint assumptions。若在 Level 01 就要求 protocol endpoint,本层会退化成临床方案设计,而不是候选枚举。这与 v5 的六级递进锁一致:Level 01 对应 `exploratory`/`provisional`,不对应 `protocol-locked`。 + +### Universe Index 与 Pool Level 01 是两层,不是一层 + +| | 内容 | 用途 | +|---|---|---| +| **Universe Index** | 所有可枚举 CRC clinical contexts × 所有合格 surface targets 的完整笛卡尔积 | 证明覆盖范围 | +| **Pool Level 01(active)** | 只有**至少存在一项 target–context linkage 证据**的 pair | 真正进入后续层的候选 | + +不做这个区分,就会把算力花在成千上万个毫无生物学关联的组合上,并让 `unknown` 淹没有意义的候选。**未进入 active pool 的 pair 不删除**,留在 Universe Index,状态为 `reactivation-eligible`。 + +## 二、Level 01 运行的判据:三把锁,零个 Gate + +**Level 01 运行的 Gate 数量是零。** 这不是省略,是本层的定义。本层只运行三把 eligibility lock,产出 `CandidateFilterResult`,按契约(`genmodules/gen_indication_endpoint_target/contracts.py`)**明确不是 Gate 结果**。本层不写任何 Gate 分数、不写任何 Gate 状态、不推进任何 Gate。 + +三把锁各自借用一个 Gate 的**职责**,但都不是该 Gate 的简化执行,也**不构成该 Gate 的 PASS**——来源文档第二节对 T7 亦明确「不能把它当成完整 T7 PASS」。 + +| 顺序 | Lock | 粒度 | 借用职责 | 该 Gate 的 catalogue cost_tier | 问的问题 | +|---|---|---|---|---|---| +| 1 | `LOCK-02` crc_clinical_context_eligibility | clinical context 级 | `clinical_context_endpoint`(T0) | `low` | 这个 CRC 临床情境是否真实存在、当前疗效或人群覆盖是否有明确不足、人群是否可界定、是否有大致 intended benefit | +| 2 | `LOCK-01` target_identity_eligibility | target 级 | `tumor_cell_surface_availability`(T7) | `medium` | 这个靶点是否存在有合理依据的细胞外可及蛋白形式,身份是否能映射到统一 gene/protein identifier | +| 3 | `LOCK-03` target_context_linkage_existence | pair 级 | `target_population_mapping`(T2) | `medium` | 是否至少存在一项公开证据表明该靶点与该 CRC 情境有关(不要求证明有效) | + +### 为什么必须是锁而不是 Gate:一个可验证的依赖链理由 + +除了「Level 01 不该跑重 Gate」这个设计取舍之外,还有一个由仓库实测得到的硬理由: + +``` +tumor_cell_surface_availability (T7) + └─ 依赖 target_population_mapping (T2) + └─ 依赖 clinical_context_endpoint (T0), endpoint_driving_population (T1) +``` + +来源文档第五节明确要求 **T1 不适合作为 Level 01–03 的普遍筛选器**(代价高、定义困难,且会把系统拖回 cell-state-first)。而冻结拓扑要求按依赖顺序执行、不得跳过前置 Gate。因此**在不运行 T1 的前提下,Level 01 在结构上就不可能产生一个合法的 T7 结果**。把三把锁写成 `CandidateFilterResult` 不是为了图省事,而是唯一与冻结拓扑相容的表达方式。 + +### 每把锁的输出与 disposition 映射 + +`CandidateDisposition` 只有 `RETAIN`/`EXCLUDE`/`DEFER` 三值,来源文档的锁输出是四值,因此每一行都必须同时记录 `pool_state`,否则会丢信息(见 `GAP-P04`)。 + +**LOCK-02 clinical context 级** + +| 输出 | disposition | pool_state | 排除依据 | +|---|---|---|---| +| `validated_unmet_context` | RETAIN | `active` | — | +| `plausible_unmet_context` | DEFER | `hold` | — | +| `redundant_context` | EXCLUDE | `superseded` | 定义性(集合包含关系),必须写出取代它的 context 引用 | +| `weak_context` | DEFER | `hold` | — | +| `undefined_context` | DEFER | `hold` | — | + +> **DEVIATION-01(需审核确认)**:来源文档把 weak 与 redundant 合成一个 `weak_or_redundant_context` 输出。本契约把它拆成两个。理由是二者的排除依据强度不同:`redundant` 是可判定的集合包含关系;`weak` 是价值判断,在一个以召回为先的层里不足以据此排除。若合并保留并映射为 EXCLUDE,本层就会因为价值判断而丢候选,直接违反它自己声明的错误偏好。这是本契约对来源文档唯一的实质偏离,明示以便审核方接受或否决。 + +**LOCK-01 target 级** + +| 输出 | disposition | pool_state | 排除依据 | +|---|---|---|---| +| `eligible_surface_target` | RETAIN | `active` | — | +| `possible_surface_target` | DEFER | `hold` | — | +| `not_surface_target` | EXCLUDE | `killed` | 定义性(纯胞内蛋白不可能是 ADC 靶点) | +| `identity_unresolved` | DEFER | `hold` | — | + +**LOCK-03 pair 级** + +| 输出 | disposition | pool_state | 排除依据 | +|---|---|---|---| +| `linkage_evidence_exists` | RETAIN | `active` | — | +| `no_known_linkage` | EXCLUDE | `reactivation-eligible` | 无关联证据不等于已证伪;**留在 Universe Index,不删除** | +| `linkage_unassessed` | DEFER | `hold` | — | + +### 录入规则 + +- pair 进入 active pool 的条件是**三把锁全部 RETAIN**。 +- 任一锁 DEFER → `hold`。 +- 任一锁 EXCLUDE → 未录入 active pool;按上表决定是 `killed`、`superseded` 还是 `reactivation-eligible`。 + +## 三、顺序原则 + +人类负责人的原则是「先用最低成本的高可信 gate 去筛,比较难的 gate 放到后面」。本契约把它落成两条可执行的判据。 + +**第一条:成本按「每淘汰一个候选的边际成本」衡量,而边际成本由作用粒度决定。** + +淘汰一个 clinical context 会一次性移除候选矩阵的一整列,淘汰一个 target 移除一整行,pair 级判据每次只处理一格。因此顺序必然是 `clinical_context_level → target_level → pair_level`。这与来源文档第四节给出的 Level 01 顺序(clinical context eligibility → surface-target identity → minimal linkage)独立地一致,也解释了它为什么是对的。该约束由测试 `test_locks_run_cheapest_granularity_first` 机械保证,顺序退化会直接失败。 + +**第二条:便宜本身不够,必须同时具备否决力。** + +这一条来自实测的反例,不是推理:45 个 Gate 中 cost_tier 为 `low` 的只有 T0 与 C40–C45 共 7 个,`competitive_position_entry_window`、`patent_landscape`、`preliminary_technical_fto` 全部是 `low`,**比 T2/T7/T11 的 `medium` 更便宜**。但来源文档第二节明确指出竞争拥挤不得单独 KILL——已有成功竞争者同时也是靶点与 modality 可行的证据。所以它们尽管最便宜,也不应排在最前,只能作 `deprioritize`/`conditional retain`/`differentiation required`。 + +因此「便宜优先」的完整表述是:**在阴性结果具有否决力的判据里,先跑边际成本最低的那个。** + +## 四、Level 01 明确不做什么 + +`adc_pool_gate_usage.yaml` 的 `gates_not_run` 逐一列出全部 45 个 Gate,测试断言该清单与冻结拓扑完全相等。按来源文档第五节,其中以下几类**即使到 Level 02–03 也不适合作普遍筛选器**,此处一并记录,避免下一层重新讨论: + +- `endpoint_driving_population`(T1):代价高、定义困难,会把系统拖回 cell-state-first;只对进入 Level 03 的优先候选运行。 +- `treatment_induced_state_response`(T5):高度依赖具体 treatment context 与 longitudinal data,早期公开数据通常不足。 +- `net_endpoint_benefit`(T6):多个前置 Gate 的综合结果,不能作早期独立过滤器。 +- `target_opportunity_decision`(T12):阶段性聚合决策,不是 evidence Gate。既有内核的 `EarlyReductionSchedule` 也已硬性禁止调度 T12。 +- **全部 16 个 P Gate**:需要具体 antibody/ADC construct。Level 01–03 还没有产品对象,不得提前假装运行;最多只能记录 anticipated product risk、design hypothesis、unresolved product requirement。 +- **C46–C55(`blocking_claim_severity` 起共 10 个)**:进入真实资产开发后的商业/IP 深化,不属 universe filtering 阶段。 + +同时,Level 01 也不做:T1 的 cell population 精细建模、T3 intervention causality、T5 treatment-induced state、任何 P 系列、任何深度 IP/FTO。 + +## 五、证据标准 + +- **只用公开证据。** +- **RNA 证据不得满足 `LOCK-01`。** 这是仓库硬规则,无例外:RNA 不得当作蛋白层面验证。RNA 可以支持 `LOCK-03`——该锁只问「是否存在关联」,不问蛋白是否在表面——但必须标注 `rna_only`,且不得据此给出 `eligible_surface_target`。 +- **模型领域知识单独不足以录入一个 pair。** 这与 `src/capabilities/target_candidate_generation.py` 的 `TargetCandidateGenerationPolicy` 一致,该契约已把 `permit_model_only_generation` 与 `permit_rule_only_generation` 硬编码为禁止。这也正是 PR #53 被阻断的那一点:未经原始来源验证的模型领域知识只能形成待验证假设,不支撑正式筛选排序。 +- **证据缺失一律 DEFER,永不 EXCLUDE。** `null` 不得转为 0,缺失信息必须显式,`NOT_EVALUATED` 与 `UNRESOLVED` 全程保留、不得静默转成 PASS。 + +## 六、输出与验证 + +Level 01 的输出是外部工作区的一份层级快照,字段见 `adc_pool_gate_usage.yaml` 的 `snapshot_columns`(25 列),涵盖来源文档第六节要求的全部状态历史字段。**候选即使被淘汰也不删除**,只改 `pool_state`,取值限于 `active`/`hold`/`killed`/`superseded`/`reactivation-eligible`。 + +执行后必须逐条验证,任一条不通过即不得提交结果 PR: + +1. 每个 pair 都有三把锁各自的 outcome、disposition 与 `evaluation_status`;未评估的必须显式写 `NOT_EVALUATED`,不得留空。 +2. `decision = RETAIN` 的行,三把锁必须全部 RETAIN。 +3. 任何 EXCLUDE 行都必须有非空 `decision_reason_refs`,且其 `pool_state` 与本契约第二节的映射表一致。 +4. 所有证据引用都是可追溯的外部引用;仓库内不出现任何证据数据。 +5. 计数对账:`|Universe Index| = |active| + |hold| + |killed| + |superseded| + |reactivation-eligible|`,且 Universe Index 的 pair 数等于合格 context 数 × 合格 target 数。 +6. 输出中不出现任何 Gate 分数、Gate 状态或 Gate PASS/FAIL 字样。 +7. 每个产物文件的 **SHA-256 逐文件记录**并写入结果 PR 的 handoff。这是 PR #53/#54 审核的第 3/4 条要求,此后为常规要求。 +8. 结果 PR 在获得 ChatGPT `APPROVE` 前,不得发布任何排序、推荐或资产决策。 + +## 七、已记录但本次不解决的契约缺口 + +以下六条都是真实缺口。它们**没有在本 PR 中被实现**,因为架构已于 2026-08-04 冻结、每月最多一次积累修复,顺手实现正是前两轮被阻断的越界行为。 + +| ID | 缺口 | 影响层 | +|---|---|---| +| `GAP-P01` | 内核没有 pool level 身份或层级快照对象,来源文档第六节的状态历史目前只能以外部 TSV 表达 | 01 | +| `GAP-P02` | 内核没有 `active`/`hold`/`killed`/`superseded`/`reactivation-eligible` 这组 pool 生命周期状态 | 01 | +| `GAP-P03` | 内核没有与 Pool Level 01 相区分的 Universe Index 对象,「未录入但仍存活」只能靠 `pool_state` 字段承载 | 01 | +| `GAP-P04` | `CandidateDisposition` 只有三值,无法表达来源文档四值 lock 输出与 Level 02 五值输出,也无法区分「淘汰」与「未录入但可复活」 | 01 | +| `GAP-P05` | `src/capabilities/early_t_gate_reduction.py` 的 `EarlyReductionSchedule` **强制 T2 先于 T7**,而来源文档第四节要求 Level 02 先跑 T7。既有内核与来源文档直接冲突 | 02 | +| `GAP-P06` | `EARLY_REDUCTION_GATE_IDS` 只含 T2/T7/T8/T9/T10/T11,不含任何 C Gate,故来源文档 Level 02 的 C2/C4/C5 quick scan 无法通过既有能力调度 | 02 | + +`GAP-P05` 与 `GAP-P06` 属 Level 02,现在记录是因为它们在读取 Level 01 依赖时被发现,提前登记可避免下一层重新发现。 + +## 八、与既有仓库对象的对应关系 + +Level 01 的三把锁不需要任何新契约,可完全由既有对象表达。以下对应关系对照实际文件得出,可独立复核: + +| Level 01 概念 | 既有仓库对象 | +|---|---| +| 锁的判定结果 | `CandidateFilterResult`(`filter_id` 承载 lock id;`filter_policy_ref` 必须是 `external:`) | +| 三值 disposition | `CandidateDisposition` | +| 未评估/未解决 | `EvaluationStatus` | +| clinical context | v5 `AnchorClinicalContext` + `IntendedBenefitHypothesis`,锁状态 `exploratory`/`provisional` | +| 「至少一项 linkage 证据」 | `TargetCandidateGenerationPolicy.minimum_distinct_positive_evidence_groups` | +| 笛卡尔积上限 | `TargetCandidateGenerationPolicy.maximum_candidates_per_clinical_frame`、`TargetCandidateGenerationRequest.candidate_budget` | +| 禁止模型单独生成 | `TargetCandidateGenerationPolicy.permit_model_only_generation = False`(契约层强制) | +| 证据出处 | `EvidenceRecord` | + +按契约设计,`filter_policy_ref` 必须是 `external:`,因此**锁的具体数值门槛属于外部 policy,本仓库只冻结判据身份、顺序与语义**。本文件与 `adc_pool_gate_usage.yaml` 冻结的正是后者。 + +## 九、架构影响 + +本 PR 的改动只有五个文件:本文件、`adc_pool_gate_usage.yaml`、`tests/test_adc_pool_gate_usage.py`、一份 `docs/handoff/`、一条 `logs/worklog.md`。 + +未触碰:`src/`、`src/contracts/`、`genmodules/`、`genmodules/assetgenos_catalog/`、`extensions/`、`docs/architecture/`、`AGENTS.md`。未新增 Gate、未改 45-Gate 拓扑与身份、未改四阶段生命周期、未改八类核心对象、未改 `GateInputEnvelope@2.0.0` 与 `GateModelOutput@2.0.0`、未改任何 Model 或 Profile。可由 `git diff --stat main...HEAD` 直接核验。 + +据此,本 PR 不构成架构变更、不消耗 2026 年 8 月的月度架构修复额度。第七节六条缺口则确实是架构问题,**它们仍未解决**,需要独立任务与额度;本 PR 不代为裁决。 + +## 十、当前阻断 + +- `BLOCK-01`:本契约获得 ChatGPT `APPROVE` 前,**不得执行 Level 01**。 +- `BLOCK-02`:`LOCK-02` 需要一份 CRC clinical context 清单,而目前唯一的枚举来自 2026-08-04 被隔离的运行(PR #53,`UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED`),**按裁决不得作为任何后续工作的输入**。因此即使本契约获批,Level 01 仍不能执行,必须先按裁决重跑 CRC clinical frame 并被接受。这是隔离裁决的直接后果,此处明示而非绕过。 +- 本仓库不得写入候选池、快照、证据、cache、result 或 weights。 + +## 十一、Level 02 与 Level 03 不在本 PR 范围内 + +人类负责人的指示是「先做 Level 01,审核完了再做下一个」。因此本 PR **不定义 Level 02 与 Level 03**,`adc_pool_gate_usage.yaml` 的 `defined_levels` 只有 `"01"`,并由测试断言 `defined_levels` 与 `levels` 条目一致。来源文档第八节给出的 Level 02/03 Gate 清单在本 PR 中**只作为 `GAP-P05`/`GAP-P06` 的上下文被引用,未被冻结为定义**。 diff --git a/docs/pools/adc_pool_gate_usage.yaml b/docs/pools/adc_pool_gate_usage.yaml new file mode 100644 index 0000000..6302907 --- /dev/null +++ b/docs/pools/adc_pool_gate_usage.yaml @@ -0,0 +1,286 @@ +# ADC Pool 漏斗——每层判据使用记录(machine-readable) +# +# 这份文件是 ADC Pool 漏斗每一层「用了什么」的权威机器可读记录。 +# 它只记录判据身份、顺序、语义和排除清单;不含任何候选、证据、分数或结果。 +# 候选池本身是数据,按仓库边界必须留在外部工作区。 +# +# 由 tests/test_adc_pool_gate_usage.py 对照 src/capabilities/gates.py 的冻结 +# 45-Gate 拓扑与 genmodules/assetgenos_catalog/gates/adc/v0.2 的 cost_tier 校验。 +# 每新增一层都必须在 levels 下追加一个条目,并同时通过该测试。 + +registry: + registry_id: adc_pool_gate_usage + registry_version: 0.1.0 + modality: ADC + disease_scope: CRC + source_ref: "Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md#ADC pool漏斗gating" + gate_topology_ref: src/contracts/gate_system.yaml + gate_catalog_ref: genmodules/assetgenos_catalog/gates/adc/v0.2 + gate_count: 45 + topology_change: none + status: contract_only_pending_review + defined_levels: + - "01" + +# 分层顺序原则。 +# +# 人类负责人的原则是「先用最低成本的高可信 gate 去筛,比较难的 gate 放到后面」。 +# 本注册表把「成本」定义为每淘汰一个候选的边际成本,而不是 catalogue 的 cost_tier, +# 因为边际成本主要由判据的作用粒度决定:淘汰一个 clinical context 会一次性移除 +# 候选矩阵的一整列,淘汰一个 target 会移除一整行,而 pair 级判据每次只处理一格。 +ordering_principle: + cost_metric: marginal_cost_per_candidate_eliminated + granularity_order: + - clinical_context_level + - target_level + - pair_level + cheapness_alone_is_insufficient: true + # 反例,由 catalogue 实测得到:C40-C45 的 cost_tier 全部是 low,比 T0 之外所有 + # T Gate 的 medium 更便宜,但它们不得单独 KILL——竞争拥挤同时也是靶点与 modality + # 可行的证据。因此「便宜」必须与「阴性结果具有否决力」同时成立才排在前面。 + cheap_but_non_decisive_examples: + - competitive_position_entry_window + - patent_landscape + - preliminary_technical_fto + +levels: + - level: "01" + level_id: adc_pool_level_01 + name: Evidence-linked seeds + purpose: 在当前公开知识下高召回枚举可定义的 clinical context x target seed,并只排除定义性不可能项。 + error_preference: recall_over_precision + contract_ref: docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md + + # 本层运行的 Gate 数量为零。这不是省略,而是本层的定义。 + # Level 01 只运行 eligibility lock,产出 CandidateFilterResult, + # 按契约明确不是 Gate 结果,因此不得写入任何 Gate 分数或状态。 + gates_executed: [] + gates_executed_count: 0 + result_contract: CandidateFilterResult + result_is_gate_result: false + gate_scores_written: none + evaluation_status_preserved: + - EVALUATED + - NOT_EVALUATED + - UNRESOLVED + + # 三把锁各自借用一个 Gate 的「职责」,但都不是该 Gate 的简化执行, + # 更不构成该 Gate 的 PASS。来源文档第二节亦明确「不能把它当成完整 T7 PASS」。 + locks: + - lock_id: LOCK-01 + run_order: 2 + name: target_identity_eligibility + granularity: target_level + gate_responsibility_borrowed: tumor_cell_surface_availability + borrowed_gate_cost_tier: medium + borrowed_gate_is_hard_gate: true + constitutes_gate_pass: false + question: 这个靶点是否存在有合理依据的细胞外可及蛋白形式,且身份能映射到统一 gene/protein identifier。 + outcomes: + - outcome: eligible_surface_target + disposition: RETAIN + pool_state: active + - outcome: possible_surface_target + disposition: DEFER + pool_state: hold + - outcome: not_surface_target + disposition: EXCLUDE + pool_state: killed + exclusion_basis: definitional + - outcome: identity_unresolved + disposition: DEFER + pool_state: hold + + - lock_id: LOCK-02 + run_order: 1 + name: crc_clinical_context_eligibility + granularity: clinical_context_level + gate_responsibility_borrowed: clinical_context_endpoint + borrowed_gate_cost_tier: low + borrowed_gate_is_hard_gate: true + constitutes_gate_pass: false + endpoint_locked: false + question: 这个 CRC 临床情境是否真实存在、当前疗效或人群覆盖是否有明确不足、人群是否可界定、是否有大致 intended benefit。 + outcomes: + - outcome: validated_unmet_context + disposition: RETAIN + pool_state: active + - outcome: plausible_unmet_context + disposition: DEFER + pool_state: hold + # DEVIATION-01:来源文档把 weak 与 redundant 合为一个 weak_or_redundant_context + # 输出。本注册表把它拆成两个,因为二者的排除依据强度不同:redundant 是集合 + # 包含关系,可判定;weak 是价值判断,在一个以召回为先的层里不足以据此排除。 + - outcome: redundant_context + disposition: EXCLUDE + pool_state: superseded + exclusion_basis: definitional + requires_superseding_context_ref: true + - outcome: weak_context + disposition: DEFER + pool_state: hold + - outcome: undefined_context + disposition: DEFER + pool_state: hold + + - lock_id: LOCK-03 + run_order: 3 + name: target_context_linkage_existence + granularity: pair_level + gate_responsibility_borrowed: target_population_mapping + borrowed_gate_cost_tier: medium + borrowed_gate_is_hard_gate: true + constitutes_gate_pass: false + question: 是否至少存在一项公开证据表明该靶点与该 CRC 情境有关,不要求证明有效。 + outcomes: + - outcome: linkage_evidence_exists + disposition: RETAIN + pool_state: active + # 来源文档第七节要求无 linkage 的 pair 留在 Universe Index,不进入 + # active pool,也不删除。三值 CandidateDisposition 无法表达 + # 「未录入但仍存活」,故此处必须同时写 pool_state 才不丢信息。见 GAP-P04。 + - outcome: no_known_linkage + disposition: EXCLUDE + pool_state: reactivation-eligible + exclusion_basis: absence_of_linkage_not_disproof + retained_in_universe_index: true + - outcome: linkage_unassessed + disposition: DEFER + pool_state: hold + + # 本层不运行的 Gate:全部 45 个。逐一列出而不写「其余」, + # 以便测试能断言这份清单与冻结拓扑完全相等。 + gates_not_run: + - clinical_context_endpoint + - endpoint_driving_population + - target_population_mapping + - intervention_causality + - baseline_coverage_and_escape + - treatment_induced_state_response + - net_endpoint_benefit + - tumor_cell_surface_availability + - intratumoral_antigen_accessibility + - antibody_dependent_internalization + - antibody_epitope_realizability + - on_target_therapeutic_index + - target_opportunity_decision + - product_design_objective + - epitope_landscape + - epitope_function + - binding_geometry_kinetics + - antibody_format_fc_design + - antibody_sequence_developability + - productive_internalization_trafficking + - conjugation_platform_site + - dar_molecular_property_balance + - payload_cell_state_match + - linker_release_match + - bystander_tumor_coverage + - integrated_pk_stability_exposure + - construct_therapeutic_index + - biomarker_clinical_assay_codesign + - integrated_adc_product_decision + - commercial_opportunity_threshold + - regulatory_development_path + - competitive_position_entry_window + - product_claim_decomposition + - patent_landscape + - preliminary_technical_fto + - blocking_claim_severity + - design_around_opportunity + - access_strategy + - fto_product_configuration + - own_ip_inventive_concept + - patentability_enablement + - claim_architecture_patent_estate + - disclosure_filing_strategy + - lifecycle_exclusivity_strategy + - transaction_readiness + + evidence_standard: + public_evidence_only: true + # 仓库硬规则,无例外:RNA 证据不得当作蛋白层面验证。 + rna_may_satisfy: + - LOCK-03 + rna_may_not_satisfy: + - LOCK-01 + model_domain_knowledge_alone_admits_pair: false + absent_evidence_disposition: DEFER + absent_evidence_may_exclude: false + null_is_not_zero: true + + admission_rule: + pair_enters_active_pool_when: 三把锁全部 RETAIN + any_lock_defer_result: hold + any_lock_exclude_result: not_admitted + + snapshot_columns: + - pair_id + - clinical_context_ref + - target_ref + - pool_entry_level + - current_pool_level + - pool_state + - lock_01_outcome + - lock_01_disposition + - lock_01_evaluation_status + - lock_02_outcome + - lock_02_disposition + - lock_02_evaluation_status + - lock_03_outcome + - lock_03_disposition + - lock_03_evaluation_status + - decision + - decision_reason_refs + - fatal_flags + - material_risks + - unknowns + - conflicts + - next_required_evidence + - evidence_refs + - last_assessed_at + - policy_version + + pool_states: + - active + - hold + - killed + - superseded + - reactivation-eligible + + execution_status: not_authorized_not_executed + execution_blockers: + - id: BLOCK-01 + statement: 本契约未获 ChatGPT APPROVE 前不得执行 Level 01。 + - id: BLOCK-02 + statement: >- + LOCK-02 需要 CRC clinical context 清单,而目前唯一的枚举来自被隔离的 + 2026-08-04 运行(PR #53,UNAUTHORIZED_QUARANTINED_NOT_ACCEPTED), + 不得作为输入。必须先按裁决重跑并被接受。 + +# 已记录但本次不解决的契约缺口。每一条都需要独立的架构任务与月度额度, +# 不得在本 PR 内顺手实现。 +recorded_gaps: + - id: GAP-P01 + statement: 内核没有 pool level 身份或层级快照对象,来源文档第六节的状态历史目前只能以外部 TSV 表达。 + affects_level: "01" + - id: GAP-P02 + statement: 内核没有 active/hold/killed/superseded/reactivation-eligible 这组 pool 生命周期状态。 + affects_level: "01" + - id: GAP-P03 + statement: 内核没有与 Pool Level 01 相区分的 Universe Index 对象,故「未录入但仍存活」只能靠 pool_state 字段承载。 + affects_level: "01" + - id: GAP-P04 + statement: CandidateDisposition 只有三值,无法表达来源文档四值 lock 输出与 Level 02 五值输出,也无法区分「淘汰」与「未录入但可复活」。 + affects_level: "01" + - id: GAP-P05 + statement: >- + src/capabilities/early_t_gate_reduction.py 的 EarlyReductionSchedule 强制 + target_population_mapping 先于 tumor_cell_surface_availability,而来源文档第四节 + 要求 Level 02 先跑 T7。这是既有内核与来源文档的直接冲突,须在定义 Level 02 时解决。 + affects_level: "02" + - id: GAP-P06 + statement: >- + EARLY_REDUCTION_GATE_IDS 只含 T2/T7/T8/T9/T10/T11,不含任何 C Gate, + 故来源文档 Level 02 的 C2/C4/C5 quick scan 无法通过既有能力调度。 + affects_level: "02" diff --git a/logs/worklog.md b/logs/worklog.md index 66eab59..99ee4c8 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2393,3 +2393,30 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Validation: `#53` 分支 207 tests 通过;`#55` 分支 212 tests 通过;两者 `scripts/verify_repository_boundary.sh` 与 `git diff --check` 均通过;零 `__pycache__`。 - Deliberately not done: 未事后追认任何 run;未撤回 M1 映射内容(只降级效力);未删除任何科学内容;**未创建两个 contract-only PR**(#53 上游的、Playbook 六模块的),它们需预先冻结范围与语义、属新范围须另立任务授权。 - Next: 重新提交 PR #53 与 #54 复审;#55 由人类负责人决定是否单独送审及如何处理其架构问题。 + +## 2026-08-04T18:32:39-04:00 — ADC Pool 漏斗 Level 01 定义与执行契约(contract-only,未执行) + +- Instruction: 人类负责人指示读取 `2.Biotech/Asset-Generation-OS-architecture#ADC pool漏斗gating`,开始构建 ADC Pool,**先做 Level 01,审核完了再做下一个**;核心原理是先用一些 gate 锁定漏斗的最大可能性集合,再逐级加层加入新 gate 下筛;**每层用了什么 gate 都要记录下来**;先用最低成本的高可信 gate,较难的放后面。 +- Read: 解析 Obsidian heading link,目标为 `Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md` 第 2–602 行(`# ADC pool漏斗gating` 至下一个一级标题前),全文读取,未修改来源文件。 +- Decision: 交付 Level 01 的**定义与执行契约**,不执行 Level 01。此顺序即 2026-08-04 对 PR #53/#54 的裁决所要求的「contract-only PR 预先冻结范围与语义 → `APPROVE` → 再运行」,本 PR 就是 Level 01 的那个前置契约。候选池是数据,按硬边界留在外部工作区;仓库只冻结判据身份、顺序与语义。 +- Measured: 用脚本抽取全部 45 个 `gate.yaml` 的 `cost_tier`/`hard_gate`/`priority`/`dependencies`,不靠假设。`cost_tier = low` 的只有 7 个(T0 与 C40–C45)。 +- Finding (high): 实测依赖链 `tumor_cell_surface_availability → target_population_mapping → {clinical_context_endpoint, endpoint_driving_population}`。来源文档第五节明确 T1 不适合作 Level 01–03 的普遍筛选器,冻结拓扑又要求按依赖顺序执行、不得跳过前置 Gate,故**在不跑 T1 的前提下 Level 01 结构上不可能产生合法的 T7 结果**。三把锁写成 `CandidateFilterResult` 是唯一与冻结拓扑相容的表达,不是图省事。 +- Finding (high, Level 02, `GAP-P05`): `src/capabilities/early_t_gate_reduction.py` 的 `EarlyReductionSchedule.__post_init__` 硬性要求 `gate_ids[:2] == (T2, T7)`,即 **T2 必须先于 T7**;来源文档第四节要求 Level 02 **先跑 T7**。既有内核与来源文档直接冲突,须在定义 Level 02 时解决。现在登记以免下一层重新发现。 +- Finding (medium, Level 02, `GAP-P06`): `EARLY_REDUCTION_GATE_IDS` 只含 T2/T7/T8/T9/T10/T11,不含任何 C Gate,来源文档 Level 02 的 C2/C4/C5 quick scan 无法通过既有能力调度。 +- Finding (medium): Level 01 三把锁不需要任何新契约。对照实际文件的映射:锁结果→`CandidateFilterResult`(`filter_id` 承载 lock id);三值→`CandidateDisposition`;未评估/未解决→`EvaluationStatus`;context→v5 `AnchorClinicalContext`+`IntendedBenefitHypothesis`;「至少一项 linkage 证据」→`TargetCandidateGenerationPolicy.minimum_distinct_positive_evidence_groups`;笛卡尔积上限→`maximum_candidates_per_clinical_frame` 与 `candidate_budget`;证据出处→`EvidenceRecord`。 +- Finding (medium): `TargetCandidateGenerationPolicy.__post_init__` 已对 `permit_model_only_generation` 与 `permit_rule_only_generation` 一律抛错,即**契约层已禁止模型单独生成候选**——正是 PR #53 被阻断的那一点。据此把「模型领域知识单独不足以录入一个 pair」写入证据标准。 +- Design 1: 把「成本」定义为**每淘汰一个候选的边际成本**,而非 catalogue `cost_tier`;边际成本由作用粒度决定(淘汰一个 context 移除一整列,一个 target 移除一整行,pair 级每次一格),故顺序必为 context 级 → target 级 → pair 级。与来源文档第四节独立一致,并解释了它为什么对。由测试机械保证,顺序退化即失败。 +- Design 2: 明确**便宜本身不够,必须同时具备否决力**。反例来自实测:C42/C44/C45 全是 `low`,比 T2/T7/T11 的 `medium` 更便宜,但竞争拥挤不得单独 KILL(已有成功竞争者同时也是靶点与 modality 可行的证据),故不能排在最前。完整表述:在阴性结果具有否决力的判据里,先跑边际成本最低的那个。 +- Design 3: 12 个 lock outcome 全部同时写 `disposition` 与 `pool_state`。因为 `CandidateDisposition` 只有三值、来源文档锁输出是四值,且第七节要求无 linkage 的 pair 留在 Universe Index 不删除——那是「未录入但仍存活」,既非 EXCLUDE 也非 DEFER。只写 disposition 会丢信息,登记为 `GAP-P04`。 +- Design 4: Level 01 召回优先,故只有定义性依据允许排除——`not_surface_target`(纯胞内蛋白不可能是 ADC 靶点)与 `redundant_context`(可判定的集合包含关系);`no_known_linkage` 虽记 EXCLUDE 但 `pool_state` 强制 `reactivation-eligible`,因无关联证据不等于已证伪。由测试强制每个 EXCLUDE 写明 `exclusion_basis`。 +- Deviation recorded, not silent (`DEVIATION-01`): 把来源文档的 `weak_or_redundant_context` 拆成 `redundant_context`(EXCLUDE)与 `weak_context`(DEFER)。理由:`redundant` 可判定、`weak` 是价值判断;合并并映射为 EXCLUDE 会让本层因价值判断丢候选,直接违反它自己声明的错误偏好。明示待审核方接受或否决,不静默改写来源文档。 +- Delivered: `docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md`(定义与执行契约)、`docs/pools/adc_pool_gate_usage.yaml`(每层判据使用的权威机器可读记录,即人类负责人要求「记录下来」的那份)、`tests/test_adc_pool_gate_usage.py`(14 项校验)。 +- Gate boundary: **Level 01 运行的 Gate 数量是零**,`gates_not_run` 逐一列出全部 45 个并由测试断言与冻结拓扑完全相等;`result_is_gate_result: false`;`gate_scores_written: none`;`EVALUATED`/`NOT_EVALUATED`/`UNRESOLVED` 全域保留。三把锁只借用 T0/T7/T2 的**职责**,`constitutes_gate_pass` 全为 `false`。 +- Validation: `Ran 242 tests` 全部通过(`main` 基线 228 + 新增 14);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。数值全部脚本实测:`snapshot_columns` 25 列、`gates_not_run` 45 项、12 个 outcome、6 条缺口、`cost_tier = low` 的 Gate 7 个。 +- Mutation-tested, not just green: 5 个变异各自被捕获后精确回滚——`LOCK-02` 的 `borrowed_gate_cost_tier` 由 `low` 改 `medium`、允许 RNA 满足 `LOCK-01`、从 `gates_not_run` 删掉 `transaction_readiness`、把 pair 级锁 `run_order` 提到第一、删掉一个 EXCLUDE 的 `exclusion_basis`,全部 `FAILED`;回滚后与备份 `diff -q` 一致、恢复 `OK`。 +- Blockers recorded: `BLOCK-01` 本契约获 `APPROVE` 前不得执行 Level 01。`BLOCK-02` `LOCK-02` 需要 CRC clinical context 清单,而唯一的枚举来自被隔离的 PR #53 运行,按裁决不得作为后续输入,**故即使本契约获批 Level 01 仍不能执行**,必须先重跑 CRC clinical frame 并被接受。这是隔离裁决的直接后果,明示而非绕过。 +- Architecture: 改动五个文件(上述三个+本条 worklog+一份 handoff)。未触碰 `src/`、`src/contracts/`、`genmodules/`、`genmodules/assetgenos_catalog/`、`extensions/`、`docs/architecture/`、`AGENTS.md`、`prompts/`;未新增 Gate、未改 45-Gate 拓扑与身份、四阶段生命周期、八类核心对象、`GateInputEnvelope@2.0.0`、`GateModelOutput@2.0.0`、任何 Model 或 Profile。可由 `git diff --stat main...HEAD` 核验。据此不构成架构变更、不消耗 8 月月度额度;六条缺口确实是架构问题且**仍未解决**,需独立任务与额度,执行者不代为裁决。 +- Deliberately not done: 未执行 Level 01(无候选、无 pair、无 disposition、无排序、无推荐);未定义 Level 02/03(`defined_levels` 只有 `"01"` 并由测试断言);未实现六条缺口,未为「看起来完整」私自扩展 `CandidateDisposition` 或新增 pool 生命周期枚举;**未引用被隔离运行的任何产物**(#53 的 20 场景/45 靶点/Tier A/payload 结论、#54 的 Seed Admission Standard 与靶点 disposition 一条都未作输入);未创建仍然欠着的两个 contract-only PR,未补 #53/#54 的批准记录。 +- Noticed, not fixed: `requirements.txt` 注释写「the full suite (207 tests)」,实测 228(本分支 242)。属本次范围外,按第 25 条不在本 PR 顺手改,只记录。 +- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。 +- Next: 推送并创建 PR 送审;请审核方重点看 `DEVIATION-01` 是否接受、`NO_ARCHITECTURE_CHANGE` 判断是否成立、`BLOCK-02` 的处理是否正确。 diff --git a/tests/test_adc_pool_gate_usage.py b/tests/test_adc_pool_gate_usage.py new file mode 100644 index 0000000..3f858e5 --- /dev/null +++ b/tests/test_adc_pool_gate_usage.py @@ -0,0 +1,193 @@ +"""Verify docs/pools/adc_pool_gate_usage.yaml against the frozen Gate topology. + +The registry records which criteria each ADC Pool level uses. These tests keep +that record from drifting away from the 45-Gate catalogue and from the +CandidateFilterResult semantics. They assert nothing about candidates, which +are data and live outside this repository. +""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +import yaml + +from genmodules.gen_indication_endpoint_target.contracts import ( + CandidateDisposition, + EvaluationStatus, +) +from src.capabilities.gates import GATE_IDS + + +REPO_ROOT = Path(__file__).resolve().parents[1] +REGISTRY_PATH = REPO_ROOT / "docs" / "pools" / "adc_pool_gate_usage.yaml" +GATE_CATALOG_ROOT = ( + REPO_ROOT / "genmodules" / "assetgenos_catalog" / "gates" / "adc" / "v0.2" +) + +# Fields the source document requires every pool candidate row to preserve. +# Source: Asset-Generation-OS-architecture.md#ADC pool漏斗gating, section 6. +REQUIRED_SNAPSHOT_FIELDS = ( + "pair_id", + "clinical_context_ref", + "target_ref", + "pool_entry_level", + "current_pool_level", + "decision", + "decision_reason_refs", + "fatal_flags", + "material_risks", + "unknowns", + "conflicts", + "next_required_evidence", + "last_assessed_at", + "policy_version", +) + +REQUIRED_POOL_STATES = frozenset( + {"active", "hold", "killed", "superseded", "reactivation-eligible"} +) + + +def _load_registry() -> dict: + with REGISTRY_PATH.open(encoding="utf-8") as handle: + return yaml.safe_load(handle) + + +def _catalog_cost_tiers() -> dict[str, str]: + tiers: dict[str, str] = {} + for path in sorted(GATE_CATALOG_ROOT.glob("*/*/*/gate.yaml")): + with path.open(encoding="utf-8") as handle: + document = yaml.safe_load(handle) + gate = document["gate"] + tiers[gate["gate_id"]] = gate["runtime"]["cost_tier"] + return tiers + + +class ADCPoolGateUsageRegistryTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.registry = _load_registry() + cls.levels = {entry["level"]: entry for entry in cls.registry["levels"]} + + def test_registry_binds_to_the_frozen_topology_without_changing_it(self) -> None: + header = self.registry["registry"] + self.assertEqual(header["gate_count"], len(GATE_IDS)) + self.assertEqual(header["gate_count"], 45) + self.assertEqual(header["topology_change"], "none") + self.assertEqual(header["modality"], "ADC") + + def test_declared_levels_match_the_level_entries(self) -> None: + self.assertEqual( + list(self.registry["registry"]["defined_levels"]), list(self.levels) + ) + self.assertEqual(len(self.levels), len(self.registry["levels"])) + + def test_level_01_executes_no_gate_and_produces_no_gate_result(self) -> None: + level = self.levels["01"] + self.assertEqual(level["gates_executed"], []) + self.assertEqual(level["gates_executed_count"], 0) + self.assertEqual(level["result_contract"], "CandidateFilterResult") + self.assertIs(level["result_is_gate_result"], False) + self.assertEqual(level["gate_scores_written"], "none") + + def test_level_01_lists_every_gate_as_not_run(self) -> None: + not_run = self.levels["01"]["gates_not_run"] + self.assertEqual(len(not_run), len(set(not_run)), "duplicate gate id") + self.assertEqual(set(not_run), set(GATE_IDS)) + + def test_level_01_preserves_the_full_evaluation_status_domain(self) -> None: + preserved = self.levels["01"]["evaluation_status_preserved"] + self.assertEqual( + set(preserved), {status.value for status in EvaluationStatus} + ) + + def test_borrowed_gate_responsibilities_are_real_gates_and_not_passes(self) -> None: + tiers = _catalog_cost_tiers() + self.assertEqual(len(tiers), len(GATE_IDS)) + for level in self.registry["levels"]: + for lock in level["locks"]: + gate_id = lock["gate_responsibility_borrowed"] + with self.subTest(lock=lock["lock_id"]): + self.assertIn(gate_id, GATE_IDS) + self.assertIs(lock["constitutes_gate_pass"], False) + self.assertEqual( + lock["borrowed_gate_cost_tier"], tiers[gate_id] + ) + + def test_lock_dispositions_and_pool_states_stay_inside_the_contracts(self) -> None: + valid_dispositions = {item.value for item in CandidateDisposition} + for level in self.registry["levels"]: + declared_states = set(level["pool_states"]) + self.assertEqual(declared_states, REQUIRED_POOL_STATES) + for lock in level["locks"]: + for outcome in lock["outcomes"]: + with self.subTest(outcome=outcome["outcome"]): + self.assertIn(outcome["disposition"], valid_dispositions) + self.assertIn(outcome["pool_state"], declared_states) + + def test_every_exclusion_declares_its_basis(self) -> None: + for level in self.registry["levels"]: + for lock in level["locks"]: + for outcome in lock["outcomes"]: + if outcome["disposition"] != CandidateDisposition.EXCLUDE.value: + continue + with self.subTest(outcome=outcome["outcome"]): + self.assertTrue(outcome.get("exclusion_basis")) + + def test_locks_run_cheapest_granularity_first(self) -> None: + granularity_order = self.registry["ordering_principle"]["granularity_order"] + for level in self.registry["levels"]: + locks = sorted(level["locks"], key=lambda lock: lock["run_order"]) + self.assertEqual( + [lock["run_order"] for lock in locks], + list(range(1, len(locks) + 1)), + "run_order must be a permutation of 1..n", + ) + indices = [granularity_order.index(lock["granularity"]) for lock in locks] + self.assertEqual(indices, sorted(indices), "granularity must not regress") + + def test_rna_evidence_never_satisfies_the_surface_protein_lock(self) -> None: + standard = self.levels["01"]["evidence_standard"] + self.assertIn("LOCK-01", standard["rna_may_not_satisfy"]) + self.assertNotIn("LOCK-01", standard["rna_may_satisfy"]) + self.assertIs(standard["public_evidence_only"], True) + self.assertIs(standard["model_domain_knowledge_alone_admits_pair"], False) + + def test_absent_evidence_defers_and_never_excludes(self) -> None: + standard = self.levels["01"]["evidence_standard"] + self.assertEqual( + standard["absent_evidence_disposition"], CandidateDisposition.DEFER.value + ) + self.assertIs(standard["absent_evidence_may_exclude"], False) + self.assertIs(standard["null_is_not_zero"], True) + + def test_snapshot_preserves_every_required_history_field(self) -> None: + columns = self.levels["01"]["snapshot_columns"] + self.assertEqual(len(columns), len(set(columns)), "duplicate column") + for field in REQUIRED_SNAPSHOT_FIELDS: + with self.subTest(field=field): + self.assertIn(field, columns) + + def test_level_01_is_recorded_as_not_yet_authorized(self) -> None: + level = self.levels["01"] + self.assertEqual(level["execution_status"], "not_authorized_not_executed") + self.assertTrue(level["execution_blockers"]) + for blocker in level["execution_blockers"]: + self.assertTrue(blocker["statement"].strip()) + + def test_recorded_gaps_are_not_silently_resolved(self) -> None: + gaps = self.registry["recorded_gaps"] + self.assertTrue(gaps) + ids = [gap["id"] for gap in gaps] + self.assertEqual(len(ids), len(set(ids)), "duplicate gap id") + known_levels = set(self.levels) | {"02", "03"} + for gap in gaps: + with self.subTest(gap=gap["id"]): + self.assertTrue(gap["statement"].strip()) + self.assertIn(gap["affects_level"], known_levels) + + +if __name__ == "__main__": + unittest.main() From 6036c014a7e9d3c14554aaa5253dffe940933194 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 18:59:46 -0400 Subject: [PATCH 2/2] task_20260804: address PR #57 review, both blockers accepted Blocker 1: no_known_linkage was EXCLUDE while the same contract stated absent evidence always DEFERs and absent_evidence_may_exclude was false. The reviewer is right that this is not cosmetic: "no linkage found" conflated "not yet assessed / search incomplete" with "searched the declared scope and found nothing", and an executor could encode ordinary missing evidence either way, which directly changes the size of Pool Level 01. LOCK-03 now has four outcomes, each carrying an explicit evidence_state. linkage_unassessed (not_assessed) and the new linkage_evidence_missing (absent_incomplete_search) both DEFER to hold. Exclusion is only available as no_known_linkage_after_complete_search, whose semantics are restricted to EXCLUDE_FROM_ACTIVE_POOL with is_scientific_disproof false, is_killed false and a permanent reactivation-eligible state. It requires six search-completeness fields; missing any one forces a fallback to linkage_evidence_missing. The snapshot carries those six columns, 25 -> 31. Blocker 2: the old identity summed killed and superseded into the Universe Index pair-state total, but a target ruled not_surface_target is by definition not an eligible surface target. Context-level, target-level and pair-level outcomes were mixed into one total, so no unique denominator existed at run time. I had not spotted this. Level 01 now produces three objects: Raw Enumeration Matrix (raw x raw, may contain excluded items), Eligible Universe Index (eligible contexts x eligible targets) and Pool Level 01 (pair states active/hold/reactivation-eligible), plus two eligibility audit artefacts. State vocabularies are split per granularity; killed and superseded are audit history and are excluded from pair reconciliation. CNT-01..CNT-05 are machine-readable and the tests evaluate them on a worked example, asserting that superseding a context removes exactly |eligible_targets| pairs and killing a target removes exactly |eligible_contexts|, while the raw matrix is unchanged. GAP-P03 and GAP-P04 are restated to match, and both remain unresolved: the three-object structure and the two EXCLUDE semantics are still external encoding conventions rather than contract support. Tests 14 -> 23, covering each acceptance criterion the reviewer specified. 251 tests pass. Ten further mutations were caught and rolled back exactly, fifteen across both rounds. Still no change under src/, genmodules/, extensions/, docs/architecture/ or AGENTS.md, and no Gate added. Co-Authored-By: Claude Opus 5 --- .../2026-08-04-adc-pool-level-01.zh-CN.md | 67 ++++- docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md | 104 +++++--- docs/pools/adc_pool_gate_usage.yaml | 202 ++++++++++++-- logs/worklog.md | 16 ++ tests/test_adc_pool_gate_usage.py | 249 +++++++++++++++++- 5 files changed, 561 insertions(+), 77 deletions(-) diff --git a/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md b/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md index fb4eccd..dcce17a 100644 --- a/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md +++ b/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md @@ -25,7 +25,7 @@ |---|---| | `docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md` | Level 01 的完整定义与执行契约(面向操作者,中文) | | `docs/pools/adc_pool_gate_usage.yaml` | **每层用了什么判据的权威机器可读记录**,即人类负责人要求「记录下来」的那份记录。每新增一层在 `levels` 下追加一个条目 | -| `tests/test_adc_pool_gate_usage.py` | 14 项校验,把上面那份记录钉在冻结的 45-Gate 拓扑与 `CandidateFilterResult` 语义上 | +| `tests/test_adc_pool_gate_usage.py` | 23 项校验,把上面那份记录钉在冻结的 45-Gate 拓扑与 `CandidateFilterResult` 语义上 | ## 三、Level 01 的核心结论 @@ -37,7 +37,7 @@ | 2 | `LOCK-01` target_identity_eligibility | target 级 | `tumor_cell_surface_availability`(T7,`medium`) | | 3 | `LOCK-03` target_context_linkage_existence | pair 级 | `target_population_mapping`(T2,`medium`) | -`Universe Index`(完整笛卡尔积,用于证明覆盖范围)与 `Pool Level 01 active`(只有至少一项 linkage 证据的 pair)分成两层;未录入的 pair 不删除,状态为 `reactivation-eligible`。 +Level 01 产出三个对象(见第四节决策 6):`Raw Enumeration Matrix`、`Eligible Universe Index`、`Pool Level 01`。未录入 active pool 的 pair 不删除,留在 Eligible Universe Index,状态为 `reactivation-eligible`。 ## 四、关键设计决策 @@ -47,11 +47,14 @@ **2. 明确「便宜本身不够,必须同时具备否决力」。** 这条来自实测反例而非推理:45 个 Gate 中 `cost_tier = low` 的只有 7 个——T0 与 C40–C45。`competitive_position_entry_window`、`patent_landscape`、`preliminary_technical_fto` 全是 `low`,**比 T2/T7/T11 的 `medium` 更便宜**;但来源文档第二节明确竞争拥挤不得单独 KILL(已有成功竞争者同时也是靶点与 modality 可行的证据)。所以它们尽管最便宜也不能排在最前。完整表述是:**在阴性结果具有否决力的判据里,先跑边际成本最低的那个。** -**3. 每个锁输出必须同时记录 `pool_state`。** -`CandidateDisposition` 只有三值,来源文档的锁输出是四值,且第七节要求「无 linkage 的 pair 留在 Universe Index 不删除」——这是「未录入但仍存活」,既不是 EXCLUDE(已淘汰)也不是 DEFER(等证据)。只写 disposition 会丢信息,所以 12 个 outcome 全部同时写 `disposition` 与 `pool_state`,并登记为 `GAP-P04`。 +**3. 每个锁输出必须同时记录 `disposition_semantics`、`resulting_state` 与 `evidence_state`。** +`CandidateDisposition` 只有三值,无法区分两种性质完全不同的 EXCLUDE:`EXCLUDE_DEFINITIONALLY_INELIGIBLE`(`killed`/`superseded`,不可复活)与 `EXCLUDE_FROM_ACTIVE_POOL`(`reactivation-eligible`,非科学证伪)。只写 disposition 会丢信息,所以 13 个 outcome 全部同时写这三个字段,并登记为 `GAP-P04`。 -**4. 每个 EXCLUDE 都必须写明排除依据,且只有定义性依据才允许排除。** -Level 01 的错误偏好是召回优先。因此只有 `not_surface_target`(纯胞内蛋白不可能是 ADC 靶点)与 `redundant_context`(可判定的集合包含关系)允许排除;`no_known_linkage` 虽记为 EXCLUDE,但 `pool_state` 强制为 `reactivation-eligible`,因为无关联证据不等于已证伪。由 `test_every_exclusion_declares_its_basis` 强制。 +**4. 每个 EXCLUDE 都必须写明排除依据与语义,且只有两类依据允许排除。** +Level 01 的错误偏好是召回优先。允许排除的只有:定义性不合格(`not_surface_target` 纯胞内蛋白不可能是 ADC 靶点、`redundant_context` 可判定的集合包含关系),以及**完整检索后确证无关联**(`no_known_linkage_after_complete_search`,语义仅限移出 active pool、强制 `reactivation-eligible`)。由 `test_every_exclusion_declares_its_basis`、`test_only_a_completed_search_may_remove_a_pair_from_the_active_pool` 与 `test_definitional_exclusions_are_never_reactivation_eligible` 三项强制。 + +**6. Level 01 产出三个对象,而不是两个。** +只分 Universe Index 与 Pool Level 01 会让计数失去唯一的 denominator——被判 `not_surface_target` 的靶点按定义已不属于「合格 surface targets」。因此拆成 `Raw Enumeration Matrix`(raw × raw,可含被排除项)、`Eligible Universe Index`(eligible contexts × eligible targets)、`Pool Level 01`(pair 级 `active`/`hold`/`reactivation-eligible`),状态词表按 context/target/pair 三种粒度分开,`killed` 与 `superseded` 属资格审计历史、不参与 pair 级对账。五条计数恒等式 `CNT-01`..`CNT-05` 由测试在算例上实际求值验证。 **5. `DEVIATION-01`(唯一的实质偏离,明示待审核方裁决)。** 来源文档把 weak 与 redundant 合成一个 `weak_or_redundant_context` 输出,本契约拆成 `redundant_context`(EXCLUDE)与 `weak_context`(DEFER)。理由:`redundant` 可判定,`weak` 是价值判断;若合并并映射为 EXCLUDE,本层就会因价值判断丢候选,直接违反它自己声明的错误偏好。明示以便审核方接受或否决,而不是静默改写来源文档。 @@ -104,12 +107,12 @@ Level 01 的错误偏好是召回优先。因此只有 `not_surface_target`( ## 九、验证结果 -- `Ran 242 tests` 全部通过(`main` 基线 228 + 本次新增 14)。 +- `Ran 251 tests` 全部通过(`main` 基线 228 + 本次新增 23)。 - `scripts/verify_repository_boundary.sh`:`Repository boundary check passed.` - `git diff --check`:通过。 - 零 `__pycache__`(全程 `PYTHONDONTWRITEBYTECODE=1`)。 -- **新增测试做过变异检验**,不是只看绿灯。5 个变异各自被捕获、随后精确回滚:把 `LOCK-02` 的 `borrowed_gate_cost_tier` 由 `low` 改成 `medium`(`FAILED`);允许 RNA 满足 `LOCK-01`(`FAILED`);从 `gates_not_run` 删掉 `transaction_readiness`(`FAILED`);把 pair 级锁的 `run_order` 提到第一(`FAILED`);删掉一个 EXCLUDE 的 `exclusion_basis`(`FAILED`)。回滚后与备份 `diff -q` 一致、测试恢复 `OK`。 -- 数值对账:`snapshot_columns` 25 列、`gates_not_run` 45 项、3 把锁 12 个 outcome、6 条缺口、catalogue 中 `cost_tier = low` 的 Gate 7 个——全部由脚本实测,不是估计。 +- **新增测试做过变异检验**,不是只看绿灯。首轮 5 个 + 复审轮 10 个,共 15 个变异各自被捕获、随后精确回滚,回滚后与备份 `diff -q` 一致、测试恢复 `OK`。首轮:`borrowed_gate_cost_tier` 由 `low` 改 `medium`、允许 RNA 满足 `LOCK-01`、从 `gates_not_run` 删掉 `transaction_readiness`、把 pair 级锁 `run_order` 提到第一、删掉一个 EXCLUDE 的 `exclusion_basis`。复审轮见第十二节。 +- 数值对账:`snapshot_columns` 31 列、`gates_not_run` 45 项、3 把锁 13 个 outcome、5 个 pool object、5 条计数恒等式、6 条缺口、catalogue 中 `cost_tier = low` 的 Gate 7 个——全部由脚本实测,不是估计。 ## 十、当前阻断 @@ -118,6 +121,50 @@ Level 01 的错误偏好是召回优先。因此只有 `not_surface_target`( ## 十一、请审核方重点看的三点 -1. `DEVIATION-01` 是否接受:把来源文档的 `weak_or_redundant_context` 拆成 `redundant_context`(EXCLUDE)与 `weak_context`(DEFER)。 +1. `DEVIATION-01` 是否接受:把来源文档的 `weak_or_redundant_context` 拆成 `redundant_context`(EXCLUDE)与 `weak_context`(DEFER)。**已于第一轮审核获接受。** 2. 第六节的 `NO_ARCHITECTURE_CHANGE` 判断是否成立。这一次的依据是 diff 范围而非推导结论,与 PR #54 被降级的那个断言性质不同;若仍认为它只能是待审假设,请指出。 3. `BLOCK-02` 的处理是否正确:Level 01 的定义可以先冻结,但执行必须等 CRC clinical frame 重跑被接受。 + +## 十二、第一轮审核裁决与修订(`REQUEST_CHANGES`,2026-08-04) + +ChatGPT 对 PR #57(HEAD `0e39ef5`)返回 `REQUEST_CHANGES`,两条阻断**全部接受**。两条都会改变实际运行语义,不是文字问题。已在同一 PR 内做最小修订,未夹带任何无关改动。 + +### 阻断 1(接受):`no_known_linkage` 的 disposition 与证据标准自相矛盾 + +契约同时写着「证据缺失一律 DEFER,永不 EXCLUDE」和 `absent_evidence_may_exclude: false`,却又把 `no_known_linkage` 定为 EXCLUDE。这是同一契约内的直接矛盾。审核方指出的后果是实质性的:「没有发现 linkage」混合了「尚未充分检索/未评估」与「已完成规定范围检索仍无发现」两种完全不同的情况,执行者可以任选一种编码,**直接影响 Pool Level 01 的规模**。 + +修订如下: + +- `LOCK-03` 的 outcome 由 3 个拆成 4 个,并给每个 outcome 增加 `evidence_state` 字段: + - `linkage_unassessed`(`not_assessed`)→ DEFER/`hold` + - `linkage_evidence_missing`(`absent_incomplete_search`)→ DEFER/`hold`(新增) + - `no_known_linkage_after_complete_search`(`absent_after_complete_search`)→ EXCLUDE/`reactivation-eligible`(由 `no_known_linkage` 更名并收紧) +- 明确该 EXCLUDE 的语义只能是 **`EXCLUDE_FROM_ACTIVE_POOL`**,并写入 `is_scientific_disproof: false`、`is_killed: false`、`retained_in_eligible_universe_index: true`。 +- 该 outcome 必须附六项检索完整性记录才允许输出:`search_complete`、`search_policy_ref`、`source_coverage_ref`、`search_scope`、`searched_at`、`search_policy_version`。**缺任何一项即必须退回 `linkage_evidence_missing`(DEFER)。** 快照相应新增 6 列(25 → 31)。 +- `evidence_standard` 显式列出 `absent_evidence_states: [not_assessed, absent_incomplete_search]`,并把「完整检索后无发现」单独列为不属于证据缺失的一类。 + +按审核方给出的验收标准新增测试:`test_missing_or_unassessed_evidence_can_never_exclude`(缺失与未评估都不能 EXCLUDE)、`test_only_a_completed_search_may_remove_a_pair_from_the_active_pool`(只有完整检索才允许该 outcome,且六项字段齐备)、`test_completeness_fields_are_carried_by_the_snapshot`、`test_definitional_exclusions_are_never_reactivation_eligible`(定义性排除不得被当成可复活,反向亦然)。 + +### 阻断 2(接受):Universe Index 的定义与计数公式不一致 + +原公式把 `killed`(含 `not_surface_target`)与 `superseded`(含 `redundant_context`)算进 Universe Index 的 pair-state 总和,但被判 `not_surface_target` 的靶点按定义已不属于「合格 surface targets」。**结果是 context 级资格结论、target 级资格结论与 pair 级池状态被混进同一个总和,运行时得不到唯一正确的 denominator。** 这条我原先没看出来,审核方是对的。 + +修订如下: + +- 拆成三个对象:`Raw Enumeration Matrix`(raw contexts × raw targets,**可以**含 `undefined_context`/`redundant_context`/`not_surface_target`/`identity_unresolved`)、`Eligible Universe Index`(eligible contexts × eligible surface targets)、`Pool Level 01`(pair 级 `active`/`hold`/`reactivation-eligible`)。另加两份资格审计产物,共五份,不得合并成一张表。 +- 状态词表按粒度分开:context 级 `eligible`/`hold`/`superseded`;target 级 `eligible`/`hold`/`killed`;pair 级 `active`/`hold`/`reactivation-eligible`。原来那份混合的五值 `pool_states` 已删除。 +- **`killed` 与 `superseded` 明确不参与 pair 级对账**,写入 `excluded_from_pair_reconciliation`。 +- 五条计数恒等式 `CNT-01`..`CNT-05` 写成机器可读形式(`lhs` + `rhs_product`/`rhs_sum`),测试**在算例上实际求值**,不是只检查字段存在:`test_counting_identities_are_consistent_on_a_worked_example`、`test_superseding_a_context_removes_exactly_one_column`(context 被 superseded 时 Eligible Universe Index 恰好减少 `|eligible_targets|`,Raw Matrix 不变)、`test_killing_a_target_removes_exactly_one_row`(恰好减少 `|eligible_contexts|`)、`test_pair_reconciliation_excludes_killed_and_superseded`。 +- `GAP-P03` 与 `GAP-P04` 的表述据此更新:三对象结构与两种 EXCLUDE 语义目前只能靠外部编码约定表达,**仍是未解决的契约缺口**,没有在本 PR 内实现。 + +### 复审轮的变异检验 + +10 个变异全部被捕获,随后精确回滚,回滚后与备份 `diff -q` 一致、测试恢复 `OK`:把 `linkage_unassessed` 改成 EXCLUDE、把 `linkage_evidence_missing` 改成 EXCLUDE、把完整检索排除的 `resulting_state` 由 `reactivation-eligible` 改成 `active`、把它声明为科学证伪且 `is_killed: true`、删掉一项检索完整性字段、删掉快照里一列检索完整性列、把 `killed_targets` 混进 `CNT-03` 的求和、把 `CNT-02` 的乘数由 `eligible_contexts` 换成 `raw_contexts`、把资格审计对象声明为产生 pair 状态、给 target 级 outcome 赋一个 pair 级状态值。 + +### 审核方认可、本轮未改动的部分 + +Level 01 运行零个正式 Gate、`CandidateFilterResult` 不构成 Gate PASS、三把锁按 context → target → pair 的粒度顺序、endpoint 不在 Level 01 锁死、Universe Index 与 evidence-linked active pool 需要区分、`DEVIATION-01` 获接受、45 个 Gate 全列为 `gates_not_run`、P Gate 未提前运行、数据与结果留在仓库外、contract-only PR 不授权执行 Level 01、被隔离的 #53 产物未被重新作为输入。 + +### 审核回写状态 + +审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403,未写回 GitHub。裁决内容以人类负责人转述为准,已完整记录于本节与 `logs/worklog.md`。 diff --git a/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md b/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md index bee7117..401c6b9 100644 --- a/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md +++ b/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md @@ -27,14 +27,27 @@ Level 01 的一个候选是一个 pair,其 clinical context 至少包含 disea 此时 **endpoint 不锁定**,只记录 intended clinical benefit、endpoint class、endpoint maturity、unresolved endpoint assumptions。若在 Level 01 就要求 protocol endpoint,本层会退化成临床方案设计,而不是候选枚举。这与 v5 的六级递进锁一致:Level 01 对应 `exploratory`/`provisional`,不对应 `protocol-locked`。 -### Universe Index 与 Pool Level 01 是两层,不是一层 +### Level 01 有三个对象,不是两个 -| | 内容 | 用途 | +来源文档区分了 Universe Index 与 Pool Level 01。但只分两层会让计数失去唯一的 denominator:被 `LOCK-01` 判为 `not_surface_target` 的靶点按定义就不属于「合格 surface targets」,被 `LOCK-02` 判为 `redundant_context` 的情境是否还算「合格 clinical contexts」也没有答案。**context 级资格结论、target 级资格结论与 pair 级池状态是三种不同粒度的对象,混进一个总和里就无法对账。** 因此本契约拆成三个: + +| 对象 | 内容 | 用途 | +|---|---|---| +| **A. Raw Enumeration Matrix** | 所有被枚举、准备评估的 raw contexts × raw targets。**可以**包含 `undefined_context`、`redundant_context`、`not_surface_target`、`identity_unresolved` | 记录评估范围本身 | +| **B. Eligible Universe Index** | 只有通过两把身份资格锁的 eligible contexts × eligible surface targets | 证明覆盖范围 | +| **C. Pool Level 01** | Eligible Universe Index 经 `LOCK-03` 后的 pair 级状态:`active`/`hold`/`reactivation-eligible` | 真正进入后续层的候选 | + +不做 B 与 C 的区分,就会把算力花在成千上万个毫无生物学关联的组合上,并让 `unknown` 淹没有意义的候选。**未进入 active pool 的 pair 不删除**,留在 Eligible Universe Index,状态为 `reactivation-eligible`。 + +### 状态词表按粒度分开 + +| 粒度 | 状态 | 由哪把锁赋值 | |---|---|---| -| **Universe Index** | 所有可枚举 CRC clinical contexts × 所有合格 surface targets 的完整笛卡尔积 | 证明覆盖范围 | -| **Pool Level 01(active)** | 只有**至少存在一项 target–context linkage 证据**的 pair | 真正进入后续层的候选 | +| clinical context 级 | `eligible`/`hold`/`superseded` | `LOCK-02` | +| target 级 | `eligible`/`hold`/`killed` | `LOCK-01` | +| pair 级 | `active`/`hold`/`reactivation-eligible` | `LOCK-03` | -不做这个区分,就会把算力花在成千上万个毫无生物学关联的组合上,并让 `unknown` 淹没有意义的候选。**未进入 active pool 的 pair 不删除**,留在 Universe Index,状态为 `reactivation-eligible`。 +**`killed` 与 `superseded` 属资格审计历史,从不产生 pair 行,因此不参与 pair 级对账。** 一个 context 被 `superseded`,Eligible Universe Index 减少 `|eligible_targets|` 个 pair;一个 target 被 `killed`,减少 `|eligible_contexts|` 个 pair;两种情况下 Raw Enumeration Matrix 都不变——排除是审计记录,不是删除。 ## 二、Level 01 运行的判据:三把锁,零个 Gate @@ -62,42 +75,55 @@ tumor_cell_surface_availability (T7) ### 每把锁的输出与 disposition 映射 -`CandidateDisposition` 只有 `RETAIN`/`EXCLUDE`/`DEFER` 三值,来源文档的锁输出是四值,因此每一行都必须同时记录 `pool_state`,否则会丢信息(见 `GAP-P04`)。 +`CandidateDisposition` 只有 `RETAIN`/`EXCLUDE`/`DEFER` 三值,无法区分两种性质完全不同的 EXCLUDE,因此每一行都必须同时记录 `disposition_semantics` 与 `resulting_state`,否则会丢信息(见 `GAP-P04`): + +- **`EXCLUDE_DEFINITIONALLY_INELIGIBLE`**:定义性不合格,落到 `killed` 或 `superseded`,**不可复活**。 +- **`EXCLUDE_FROM_ACTIVE_POOL`**:只是移出 active pool,落到 `reactivation-eligible`,**不构成科学证伪**。 **LOCK-02 clinical context 级** -| 输出 | disposition | pool_state | 排除依据 | -|---|---|---|---| -| `validated_unmet_context` | RETAIN | `active` | — | -| `plausible_unmet_context` | DEFER | `hold` | — | -| `redundant_context` | EXCLUDE | `superseded` | 定义性(集合包含关系),必须写出取代它的 context 引用 | -| `weak_context` | DEFER | `hold` | — | -| `undefined_context` | DEFER | `hold` | — | +| 输出 | disposition | resulting_state | evidence_state | 排除依据 | +|---|---|---|---|---| +| `validated_unmet_context` | RETAIN | `eligible` | `present` | — | +| `plausible_unmet_context` | DEFER | `hold` | `absent_incomplete_search` | — | +| `redundant_context` | EXCLUDE(定义性) | `superseded` | `present` | 可判定的集合包含关系,必须写出取代它的 context 引用 | +| `weak_context` | DEFER | `hold` | `present` | — | +| `undefined_context` | DEFER | `hold` | `not_assessed` | — | > **DEVIATION-01(需审核确认)**:来源文档把 weak 与 redundant 合成一个 `weak_or_redundant_context` 输出。本契约把它拆成两个。理由是二者的排除依据强度不同:`redundant` 是可判定的集合包含关系;`weak` 是价值判断,在一个以召回为先的层里不足以据此排除。若合并保留并映射为 EXCLUDE,本层就会因为价值判断而丢候选,直接违反它自己声明的错误偏好。这是本契约对来源文档唯一的实质偏离,明示以便审核方接受或否决。 **LOCK-01 target 级** -| 输出 | disposition | pool_state | 排除依据 | -|---|---|---|---| -| `eligible_surface_target` | RETAIN | `active` | — | -| `possible_surface_target` | DEFER | `hold` | — | -| `not_surface_target` | EXCLUDE | `killed` | 定义性(纯胞内蛋白不可能是 ADC 靶点) | -| `identity_unresolved` | DEFER | `hold` | — | +| 输出 | disposition | resulting_state | evidence_state | 排除依据 | +|---|---|---|---|---| +| `eligible_surface_target` | RETAIN | `eligible` | `present` | — | +| `possible_surface_target` | DEFER | `hold` | `absent_incomplete_search` | — | +| `not_surface_target` | EXCLUDE(定义性) | `killed` | `present` | 纯胞内蛋白不可能是 ADC 靶点 | +| `identity_unresolved` | DEFER | `hold` | `not_assessed` | — | **LOCK-03 pair 级** -| 输出 | disposition | pool_state | 排除依据 | -|---|---|---|---| -| `linkage_evidence_exists` | RETAIN | `active` | — | -| `no_known_linkage` | EXCLUDE | `reactivation-eligible` | 无关联证据不等于已证伪;**留在 Universe Index,不删除** | -| `linkage_unassessed` | DEFER | `hold` | — | +「没有发现 linkage」包含两种性质完全不同的情况,必须严格区分,否则执行者可以把普通的证据缺失编码成排除,直接改变 Pool Level 01 的规模。 + +| 输出 | disposition | resulting_state | evidence_state | 说明 | +|---|---|---|---|---| +| `linkage_evidence_exists` | RETAIN | `active` | `present` | — | +| `linkage_unassessed` | DEFER | `hold` | `not_assessed` | **尚未评估**,属证据缺失 | +| `linkage_evidence_missing` | DEFER | `hold` | `absent_incomplete_search` | **检索未达规定范围**,属证据缺失 | +| `no_known_linkage_after_complete_search` | EXCLUDE(仅 `EXCLUDE_FROM_ACTIVE_POOL`) | `reactivation-eligible` | `absent_after_complete_search` | **已按规定范围检索完毕仍无 linkage**,这是阳性检索结论,不是证据缺失 | + +`no_known_linkage_after_complete_search` 的约束是硬性的: + +- 它**只**表示移出 active pool,**不表示科学证伪**,`is_scientific_disproof: false`,`is_killed: false`。 +- pair **留在 Eligible Universe Index**,状态恒为 `reactivation-eligible`,不得解读为 `killed`。 +- 必须同时具备六项检索完整性记录:`search_complete`、`search_policy_ref`、`source_coverage_ref`、`search_scope`、`searched_at`、`search_policy_version`。**缺任何一项即不得输出本 outcome,必须退回 `linkage_evidence_missing`(DEFER)。** ### 录入规则 - pair 进入 active pool 的条件是**三把锁全部 RETAIN**。 - 任一锁 DEFER → `hold`。 -- 任一锁 EXCLUDE → 未录入 active pool;按上表决定是 `killed`、`superseded` 还是 `reactivation-eligible`。 +- 任一锁定义性排除 → 该 context 或 target 不进入 Eligible Universe Index(`superseded` 或 `killed`)。 +- `LOCK-03` 完整检索后无 linkage → pair 留在 Eligible Universe Index,状态 `reactivation-eligible`。 ## 三、顺序原则 @@ -131,22 +157,32 @@ tumor_cell_surface_availability (T7) - **只用公开证据。** - **RNA 证据不得满足 `LOCK-01`。** 这是仓库硬规则,无例外:RNA 不得当作蛋白层面验证。RNA 可以支持 `LOCK-03`——该锁只问「是否存在关联」,不问蛋白是否在表面——但必须标注 `rna_only`,且不得据此给出 `eligible_surface_target`。 - **模型领域知识单独不足以录入一个 pair。** 这与 `src/capabilities/target_candidate_generation.py` 的 `TargetCandidateGenerationPolicy` 一致,该契约已把 `permit_model_only_generation` 与 `permit_rule_only_generation` 硬编码为禁止。这也正是 PR #53 被阻断的那一点:未经原始来源验证的模型领域知识只能形成待验证假设,不支撑正式筛选排序。 -- **证据缺失一律 DEFER,永不 EXCLUDE。** `null` 不得转为 0,缺失信息必须显式,`NOT_EVALUATED` 与 `UNRESOLVED` 全程保留、不得静默转成 PASS。 +- **证据缺失一律 DEFER,永不 EXCLUDE。** 「证据缺失」在本契约中严格限于两种 `evidence_state`:`not_assessed`(尚未评估)与 `absent_incomplete_search`(检索未达规定范围)。二者一律 DEFER,落到 `hold`。`null` 不得转为 0,缺失信息必须显式,`NOT_EVALUATED` 与 `UNRESOLVED` 全程保留、不得静默转成 PASS。 +- **「已按规定范围检索完毕仍无 linkage」不属于证据缺失。** 它是一项阳性的检索结论(`evidence_state = absent_after_complete_search`),可以把 pair 移出 active pool,但**仅限于此**:语义只能是 `EXCLUDE_FROM_ACTIVE_POOL`,不构成科学证伪,不置为 `killed`,pair 留在 Eligible Universe Index 且恒为 `reactivation-eligible`,并必须附六项检索完整性记录。这条与上一条的界线是执行者唯一可以缩小 Pool Level 01 规模的地方,因此它由测试机械把守。 ## 六、输出与验证 -Level 01 的输出是外部工作区的一份层级快照,字段见 `adc_pool_gate_usage.yaml` 的 `snapshot_columns`(25 列),涵盖来源文档第六节要求的全部状态历史字段。**候选即使被淘汰也不删除**,只改 `pool_state`,取值限于 `active`/`hold`/`killed`/`superseded`/`reactivation-eligible`。 +Level 01 必须产出**五份**外部产物,不得合并成一张表:Raw Enumeration Matrix、Context Eligibility Audit、Target Eligibility Audit、Eligible Universe Index、Pool Level 01 Snapshot。快照字段见 `adc_pool_gate_usage.yaml` 的 `snapshot_columns`(31 列),涵盖来源文档第六节要求的全部状态历史字段,外加 `LOCK-03` 的六项检索完整性记录。**候选即使被排除也不删除**,只改状态,取值限于对应粒度的词表。 执行后必须逐条验证,任一条不通过即不得提交结果 PR: 1. 每个 pair 都有三把锁各自的 outcome、disposition 与 `evaluation_status`;未评估的必须显式写 `NOT_EVALUATED`,不得留空。 2. `decision = RETAIN` 的行,三把锁必须全部 RETAIN。 -3. 任何 EXCLUDE 行都必须有非空 `decision_reason_refs`,且其 `pool_state` 与本契约第二节的映射表一致。 +3. 任何 EXCLUDE 行都必须有非空 `decision_reason_refs`,且其 `disposition_semantics` 与 `resulting_state` 与本契约第二节的映射表一致。 4. 所有证据引用都是可追溯的外部引用;仓库内不出现任何证据数据。 -5. 计数对账:`|Universe Index| = |active| + |hold| + |killed| + |superseded| + |reactivation-eligible|`,且 Universe Index 的 pair 数等于合格 context 数 × 合格 target 数。 -6. 输出中不出现任何 Gate 分数、Gate 状态或 Gate PASS/FAIL 字样。 -7. 每个产物文件的 **SHA-256 逐文件记录**并写入结果 PR 的 handoff。这是 PR #53/#54 审核的第 3/4 条要求,此后为常规要求。 -8. 结果 PR 在获得 ChatGPT `APPROVE` 前,不得发布任何排序、推荐或资产决策。 +5. **凡 `evidence_state ∈ {not_assessed, absent_incomplete_search}` 的行,disposition 必须是 DEFER。** 出现任何一行以证据缺失为由 EXCLUDE,即为验证失败。 +6. **凡输出 `no_known_linkage_after_complete_search` 的行,六项检索完整性字段必须齐备且 `search_complete = true`。** 缺任何一项即必须改回 `linkage_evidence_missing`。 +7. 计数对账,五条恒等式全部成立: + - `CNT-01`:`|raw_matrix| = |raw_contexts| × |raw_targets|` + - `CNT-02`:`|eligible_universe_index| = |eligible_contexts| × |eligible_targets|` + - `CNT-03`:`|eligible_universe_index| = |active| + |hold_pairs| + |reactivation_eligible_pairs|` + - `CNT-04`:`|raw_contexts| = |eligible_contexts| + |hold_contexts| + |superseded_contexts|` + - `CNT-05`:`|raw_targets| = |eligible_targets| + |hold_targets| + |killed_targets|` + + `killed` 与 `superseded` **不得**出现在 `CNT-03` 的求和里——它们是 context 级与 target 级的资格审计历史,从不产生 pair 行。 +8. 输出中不出现任何 Gate 分数、Gate 状态或 Gate PASS/FAIL 字样。 +9. 每个产物文件的 **SHA-256 逐文件记录**并写入结果 PR 的 handoff。这是 PR #53/#54 审核的第 3/4 条要求,此后为常规要求。 +10. 结果 PR 在获得 ChatGPT `APPROVE` 前,不得发布任何排序、推荐或资产决策。 ## 七、已记录但本次不解决的契约缺口 @@ -156,8 +192,8 @@ Level 01 的输出是外部工作区的一份层级快照,字段见 `adc_pool_ |---|---|---| | `GAP-P01` | 内核没有 pool level 身份或层级快照对象,来源文档第六节的状态历史目前只能以外部 TSV 表达 | 01 | | `GAP-P02` | 内核没有 `active`/`hold`/`killed`/`superseded`/`reactivation-eligible` 这组 pool 生命周期状态 | 01 | -| `GAP-P03` | 内核没有与 Pool Level 01 相区分的 Universe Index 对象,「未录入但仍存活」只能靠 `pool_state` 字段承载 | 01 | -| `GAP-P04` | `CandidateDisposition` 只有三值,无法表达来源文档四值 lock 输出与 Level 02 五值输出,也无法区分「淘汰」与「未录入但可复活」 | 01 | +| `GAP-P03` | 内核没有 Raw Enumeration Matrix/Eligible Universe Index/Pool Level 01 这三个对象,也没有把 context 级与 target 级资格审计与 pair 级池状态分开的结构,故三对象与 `CNT-01`..`CNT-05` 只能靠外部产物加本注册表约束表达 | 01 | +| `GAP-P04` | `CandidateDisposition` 只有三值,无法表达来源文档四值 lock 输出与 Level 02 五值输出;尤其无法区分 `EXCLUDE_DEFINITIONALLY_INELIGIBLE`(`killed`/`superseded`)与 `EXCLUDE_FROM_ACTIVE_POOL`(`reactivation-eligible`,非科学证伪)。本契约靠 `disposition_semantics` 与 `resulting_state` 两个字段补足,属外部编码约定而非契约支持 | 01 | | `GAP-P05` | `src/capabilities/early_t_gate_reduction.py` 的 `EarlyReductionSchedule` **强制 T2 先于 T7**,而来源文档第四节要求 Level 02 先跑 T7。既有内核与来源文档直接冲突 | 02 | | `GAP-P06` | `EARLY_REDUCTION_GATE_IDS` 只含 T2/T7/T8/T9/T10/T11,不含任何 C Gate,故来源文档 Level 02 的 C2/C4/C5 quick scan 无法通过既有能力调度 | 02 | diff --git a/docs/pools/adc_pool_gate_usage.yaml b/docs/pools/adc_pool_gate_usage.yaml index 6302907..697970a 100644 --- a/docs/pools/adc_pool_gate_usage.yaml +++ b/docs/pools/adc_pool_gate_usage.yaml @@ -79,17 +79,23 @@ levels: outcomes: - outcome: eligible_surface_target disposition: RETAIN - pool_state: active + resulting_state: eligible + evidence_state: present - outcome: possible_surface_target disposition: DEFER - pool_state: hold + resulting_state: hold + evidence_state: absent_incomplete_search - outcome: not_surface_target disposition: EXCLUDE - pool_state: killed + resulting_state: killed + evidence_state: present exclusion_basis: definitional + disposition_semantics: EXCLUDE_DEFINITIONALLY_INELIGIBLE + is_scientific_disproof: true - outcome: identity_unresolved disposition: DEFER - pool_state: hold + resulting_state: hold + evidence_state: not_assessed - lock_id: LOCK-02 run_order: 1 @@ -104,24 +110,31 @@ levels: outcomes: - outcome: validated_unmet_context disposition: RETAIN - pool_state: active + resulting_state: eligible + evidence_state: present - outcome: plausible_unmet_context disposition: DEFER - pool_state: hold + resulting_state: hold + evidence_state: absent_incomplete_search # DEVIATION-01:来源文档把 weak 与 redundant 合为一个 weak_or_redundant_context # 输出。本注册表把它拆成两个,因为二者的排除依据强度不同:redundant 是集合 # 包含关系,可判定;weak 是价值判断,在一个以召回为先的层里不足以据此排除。 - outcome: redundant_context disposition: EXCLUDE - pool_state: superseded + resulting_state: superseded + evidence_state: present exclusion_basis: definitional + disposition_semantics: EXCLUDE_DEFINITIONALLY_INELIGIBLE + is_scientific_disproof: false requires_superseding_context_ref: true - outcome: weak_context disposition: DEFER - pool_state: hold + resulting_state: hold + evidence_state: present - outcome: undefined_context disposition: DEFER - pool_state: hold + resulting_state: hold + evidence_state: not_assessed - lock_id: LOCK-03 run_order: 3 @@ -135,18 +148,40 @@ levels: outcomes: - outcome: linkage_evidence_exists disposition: RETAIN - pool_state: active - # 来源文档第七节要求无 linkage 的 pair 留在 Universe Index,不进入 - # active pool,也不删除。三值 CandidateDisposition 无法表达 - # 「未录入但仍存活」,故此处必须同时写 pool_state 才不丢信息。见 GAP-P04。 - - outcome: no_known_linkage - disposition: EXCLUDE - pool_state: reactivation-eligible - exclusion_basis: absence_of_linkage_not_disproof - retained_in_universe_index: true + resulting_state: active + evidence_state: present + # 「未评估」与「检索不完整」都属于证据缺失,一律 DEFER。 + # 只有完成规定范围检索后仍无 linkage 才是一项阳性检索结论,见下一条。 - outcome: linkage_unassessed disposition: DEFER - pool_state: hold + resulting_state: hold + evidence_state: not_assessed + - outcome: linkage_evidence_missing + disposition: DEFER + resulting_state: hold + evidence_state: absent_incomplete_search + # 这一条不是证据缺失,而是「已按规定范围检索完毕、未发现任何 linkage」。 + # 它只把 pair 移出 active pool,不构成科学证伪,也不置为 killed。 + # 三值 CandidateDisposition 无法表达「移出 active pool 但仍存活」, + # 故必须同时写 disposition_semantics 与 resulting_state 才不丢信息。见 GAP-P04。 + - outcome: no_known_linkage_after_complete_search + disposition: EXCLUDE + disposition_semantics: EXCLUDE_FROM_ACTIVE_POOL + resulting_state: reactivation-eligible + evidence_state: absent_after_complete_search + exclusion_basis: complete_search_found_no_linkage + is_scientific_disproof: false + is_killed: false + retained_in_eligible_universe_index: true + requires_search_completeness_record: true + # 缺任何一项即不得输出本 outcome,必须退回 linkage_evidence_missing。 + required_search_fields: + - search_complete + - search_policy_ref + - source_coverage_ref + - search_scope + - searched_at + - search_policy_version # 本层不运行的 Gate:全部 45 个。逐一列出而不写「其余」, # 以便测试能断言这份清单与冻结拓扑完全相等。 @@ -205,9 +240,22 @@ levels: rna_may_not_satisfy: - LOCK-01 model_domain_knowledge_alone_admits_pair: false + null_is_not_zero: true + # 「证据缺失」在本契约中严格限于两种情况:未评估(not_assessed)与 + # 检索不完整(absent_incomplete_search)。二者一律 DEFER,永不 EXCLUDE。 + absent_evidence_states: + - not_assessed + - absent_incomplete_search absent_evidence_disposition: DEFER absent_evidence_may_exclude: false - null_is_not_zero: true + # 「已按规定范围检索完毕、仍未发现 linkage」不属于证据缺失。它是一项阳性的 + # 检索结论,可以把 pair 移出 active pool,但仅限于此:不构成科学证伪、 + # 不置为 killed、仍留在 Eligible Universe Index 且 reactivation-eligible。 + complete_search_absence_state: absent_after_complete_search + complete_search_absence_disposition: EXCLUDE + complete_search_absence_semantics: EXCLUDE_FROM_ACTIVE_POOL + complete_search_absence_is_scientific_disproof: false + complete_search_requires_completeness_record: true admission_rule: pair_enters_active_pool_when: 三把锁全部 RETAIN @@ -220,7 +268,7 @@ levels: - target_ref - pool_entry_level - current_pool_level - - pool_state + - pair_pool_state - lock_01_outcome - lock_01_disposition - lock_01_evaluation_status @@ -230,6 +278,14 @@ levels: - lock_03_outcome - lock_03_disposition - lock_03_evaluation_status + # LOCK-03 检索完整性记录。只有这六列齐备,才允许输出 + # no_known_linkage_after_complete_search;否则必须退回 linkage_evidence_missing。 + - lock_03_search_complete + - lock_03_search_policy_ref + - lock_03_source_coverage_ref + - lock_03_search_scope + - lock_03_searched_at + - lock_03_search_policy_version - decision - decision_reason_refs - fatal_flags @@ -241,12 +297,97 @@ levels: - last_assessed_at - policy_version - pool_states: - - active - - hold + # 状态词表按粒度分开。混用会让计数失去唯一的 denominator: + # context 级与 target 级的排除是资格审计历史,从不产生 pair 行; + # 只有通过两把身份资格锁的 context 与 target 才配对成 pair。 + state_vocabularies: + clinical_context_level: + - eligible + - hold + - superseded + target_level: + - eligible + - hold + - killed + pair_level: + - active + - hold + - reactivation-eligible + + # Level 01 的三个对象,必须分开产出,不得合并成一张表。 + pool_objects: + - id: raw_enumeration_matrix + definition: 所有被枚举、准备评估的 raw contexts x raw targets。 + may_contain: + - undefined_context + - redundant_context + - not_surface_target + - identity_unresolved + assigns_pair_state: false + - id: context_eligibility_audit + definition: LOCK-02 对每个 raw context 的判定与依据,含被 superseded 的 context。 + granularity: clinical_context_level + assigns_pair_state: false + - id: target_eligibility_audit + definition: LOCK-01 对每个 raw target 的判定与依据,含被 killed 的 target。 + granularity: target_level + assigns_pair_state: false + - id: eligible_universe_index + definition: eligible contexts x eligible surface targets。用于证明覆盖范围。 + granularity: pair_level + assigns_pair_state: true + - id: pool_level_01_snapshot + definition: Eligible Universe Index 经 LOCK-03 后的 pair 级状态快照。 + granularity: pair_level + assigns_pair_state: true + + # 计数恒等式。CNT-02 与 CNT-03 共同给出唯一 denominator。 + counting_identities: + - id: CNT-01 + subject: raw_enumeration_matrix + identity: "|raw_matrix| = |raw_contexts| * |raw_targets|" + lhs: raw_matrix + rhs_product: + - raw_contexts + - raw_targets + - id: CNT-02 + subject: eligible_universe_index + identity: "|eligible_universe_index| = |eligible_contexts| * |eligible_targets|" + lhs: eligible_universe_index + rhs_product: + - eligible_contexts + - eligible_targets + - id: CNT-03 + subject: pool_level_01_snapshot + identity: "|eligible_universe_index| = |active| + |hold_pairs| + |reactivation_eligible_pairs|" + lhs: eligible_universe_index + rhs_sum: + - active + - hold_pairs + - reactivation_eligible_pairs + - id: CNT-04 + subject: context_eligibility_audit + identity: "|raw_contexts| = |eligible_contexts| + |hold_contexts| + |superseded_contexts|" + lhs: raw_contexts + rhs_sum: + - eligible_contexts + - hold_contexts + - superseded_contexts + - id: CNT-05 + subject: target_eligibility_audit + identity: "|raw_targets| = |eligible_targets| + |hold_targets| + |killed_targets|" + lhs: raw_targets + rhs_sum: + - eligible_targets + - hold_targets + - killed_targets + + # killed 与 superseded 属资格审计历史,不参与 pair 级对账。 + # 一个 context 被 superseded,Eligible Universe Index 减少 |eligible_targets| 个 pair; + # 一个 target 被 killed,减少 |eligible_contexts| 个 pair。二者都不产生 pair 行。 + excluded_from_pair_reconciliation: - killed - superseded - - reactivation-eligible execution_status: not_authorized_not_executed execution_blockers: @@ -268,10 +409,17 @@ recorded_gaps: statement: 内核没有 active/hold/killed/superseded/reactivation-eligible 这组 pool 生命周期状态。 affects_level: "01" - id: GAP-P03 - statement: 内核没有与 Pool Level 01 相区分的 Universe Index 对象,故「未录入但仍存活」只能靠 pool_state 字段承载。 + statement: >- + 内核没有 Raw Enumeration Matrix / Eligible Universe Index / Pool Level 01 + 这三个对象,也没有把 context 级与 target 级资格审计与 pair 级池状态分开的结构, + 故三个对象与 CNT-01..CNT-05 目前只能以外部产物加本注册表约束表达。 affects_level: "01" - id: GAP-P04 - statement: CandidateDisposition 只有三值,无法表达来源文档四值 lock 输出与 Level 02 五值输出,也无法区分「淘汰」与「未录入但可复活」。 + statement: >- + CandidateDisposition 只有三值,无法表达来源文档四值 lock 输出与 Level 02 五值输出; + 尤其无法区分 EXCLUDE_DEFINITIONALLY_INELIGIBLE(killed/superseded)与 + EXCLUDE_FROM_ACTIVE_POOL(reactivation-eligible,非科学证伪)。 + 本契约靠 disposition_semantics 与 resulting_state 两个字段补足,属外部编码约定而非契约支持。 affects_level: "01" - id: GAP-P05 statement: >- diff --git a/logs/worklog.md b/logs/worklog.md index 99ee4c8..7364666 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2420,3 +2420,19 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Noticed, not fixed: `requirements.txt` 注释写「the full suite (207 tests)」,实测 228(本分支 242)。属本次范围外,按第 25 条不在本 PR 顺手改,只记录。 - Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。 - Next: 推送并创建 PR 送审;请审核方重点看 `DEVIATION-01` 是否接受、`NO_ARCHITECTURE_CHANGE` 判断是否成立、`BLOCK-02` 的处理是否正确。 + +## 2026-08-04T19:10:00-04:00 — PR #57 第一轮审核裁决与最小修订(两条阻断全部接受) + +- Review: ChatGPT 对 PR #57(HEAD `0e39ef5`,可合并、CI 成功)返回 `REQUEST_CHANGES`,两条阻断。**两条全部接受**,且都会改变实际运行语义,不是文字问题。已在同一 PR 内做最小修订,未夹带无关改动。 +- Finding 1 accepted: 契约同时写「证据缺失一律 DEFER,永不 EXCLUDE」与 `absent_evidence_may_exclude: false`,却把 `no_known_linkage` 定为 EXCLUDE,**同一契约内直接矛盾**。审核方指出的后果是实质的:「没有发现 linkage」混合了「未评估/检索不充分」与「已完成规定范围检索仍无发现」两种完全不同的情况,执行者可任选一种编码,**直接影响 Pool Level 01 的规模**。 +- Fix 1: `LOCK-03` outcome 由 3 拆成 4,每个 outcome 新增 `evidence_state`——`linkage_unassessed`(`not_assessed`)→DEFER/`hold`;`linkage_evidence_missing`(`absent_incomplete_search`)→DEFER/`hold`(新增);`no_known_linkage_after_complete_search`(`absent_after_complete_search`)→EXCLUDE/`reactivation-eligible`(更名并收紧)。该 EXCLUDE 的语义明确只能是 **`EXCLUDE_FROM_ACTIVE_POOL`**,写入 `is_scientific_disproof: false`、`is_killed: false`、`retained_in_eligible_universe_index: true`。必须附六项检索完整性记录(`search_complete`、`search_policy_ref`、`source_coverage_ref`、`search_scope`、`searched_at`、`search_policy_version`)才允许输出,**缺任何一项必须退回 `linkage_evidence_missing`**;快照相应 25 列 → 31 列。`evidence_standard` 显式列出 `absent_evidence_states`,并把「完整检索后无发现」单列为不属于证据缺失。 +- Finding 2 accepted: 原计数公式把 `killed`(含 `not_surface_target`)与 `superseded`(含 `redundant_context`)算进 Universe Index 的 pair-state 总和,但被判 `not_surface_target` 的靶点按定义已不属于「合格 surface targets」。**context 级资格结论、target 级资格结论与 pair 级池状态被混进同一总和,运行时得不到唯一正确的 denominator。** 这一条执行者原先没看出来,审核方是对的。 +- Fix 2: 拆成三个对象——`Raw Enumeration Matrix`(raw × raw,可含被排除项)、`Eligible Universe Index`(eligible contexts × eligible targets)、`Pool Level 01`(pair 级 `active`/`hold`/`reactivation-eligible`),另加两份资格审计产物共五份,不得合并成一张表。状态词表按粒度分开(context 级 `eligible`/`hold`/`superseded`;target 级 `eligible`/`hold`/`killed`;pair 级 `active`/`hold`/`reactivation-eligible`),原混合五值 `pool_states` 已删除。`killed` 与 `superseded` 写入 `excluded_from_pair_reconciliation`。五条恒等式 `CNT-01`..`CNT-05` 写成机器可读形式(`lhs` + `rhs_product`/`rhs_sum`)。 +- Tests: 由 14 项增至 23 项,按审核方给出的验收标准逐条覆盖。阻断 1:`test_missing_or_unassessed_evidence_can_never_exclude`、`test_only_a_completed_search_may_remove_a_pair_from_the_active_pool`、`test_completeness_fields_are_carried_by_the_snapshot`、`test_definitional_exclusions_are_never_reactivation_eligible`。阻断 2:`test_counting_identities_are_consistent_on_a_worked_example`、`test_superseding_a_context_removes_exactly_one_column`、`test_killing_a_target_removes_exactly_one_row`、`test_pair_reconciliation_excludes_killed_and_superseded`、`test_pool_objects_separate_eligibility_audit_from_pair_states`。计数测试**在算例上实际求值**,不是只检查字段存在。 +- Gaps updated, not closed: `GAP-P03` 改为「内核没有三对象结构、也没有把 context/target 级资格审计与 pair 级池状态分开的结构」;`GAP-P04` 改为「无法区分 `EXCLUDE_DEFINITIONALLY_INELIGIBLE` 与 `EXCLUDE_FROM_ACTIVE_POOL`,本契约靠 `disposition_semantics` 与 `resulting_state` 补足,属外部编码约定而非契约支持」。**两条仍未解决**,未在本 PR 内实现。 +- Validation: `Ran 251 tests` 全部通过(`main` 基线 228 + 新增 23);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。实测数值:`snapshot_columns` 31 列、`gates_not_run` 45 项、13 个 outcome、5 个 pool object、5 条计数恒等式、6 条缺口。 +- Mutation-tested (复审轮 10 个,全部 `FAILED` 后精确回滚,与备份 `diff -q` 一致、恢复 `OK`): 把 `linkage_unassessed` 改 EXCLUDE、把 `linkage_evidence_missing` 改 EXCLUDE、完整检索排除的 `resulting_state` 由 `reactivation-eligible` 改 `active`、把它声明为科学证伪且 `is_killed: true`、删掉一项检索完整性字段、删掉快照一列检索完整性列、把 `killed_targets` 混进 `CNT-03` 求和、`CNT-02` 乘数由 `eligible_contexts` 换 `raw_contexts`、资格审计对象声明为产生 pair 状态、给 target 级 outcome 赋 pair 级状态值。累计两轮 15 个变异。 +- Accepted by reviewer, unchanged this round: Level 01 运行零个正式 Gate、`CandidateFilterResult` 不构成 Gate PASS、三把锁 context → target → pair 粒度顺序、endpoint 不在 Level 01 锁死、Universe Index 与 evidence-linked active pool 需区分、**`DEVIATION-01` 获接受**、45 个 Gate 全列 `gates_not_run`、P Gate 未提前运行、数据与结果留在仓库外、contract-only PR 不授权执行 Level 01、被隔离的 #53 产物未被重新作为输入。 +- Review write-back: 审核方尝试通过 GitHub 连接器提交正式 `REQUEST_CHANGES` review,连接器返回 403 未写回 GitHub。裁决以人类负责人转述为准,已完整记录于本条与 `docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md` 第十二节。 +- Scope: 本轮只改 `docs/pools/adc_pool_gate_usage.yaml`、`docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md`、`tests/test_adc_pool_gate_usage.py`、本条 worklog 与该 handoff。仍未触碰 `src/`、`src/contracts/`、`genmodules/`、`extensions/`、`docs/architecture/`、`AGENTS.md`、`prompts/`;仍未新增 Gate、未改 45-Gate 拓扑。`NO_ARCHITECTURE_CHANGE` 判断不变。 +- Next: 推送同一 PR 并同步 PR 描述,请求复审。 diff --git a/tests/test_adc_pool_gate_usage.py b/tests/test_adc_pool_gate_usage.py index 3f858e5..d572a4b 100644 --- a/tests/test_adc_pool_gate_usage.py +++ b/tests/test_adc_pool_gate_usage.py @@ -45,8 +45,26 @@ "policy_version", ) -REQUIRED_POOL_STATES = frozenset( - {"active", "hold", "killed", "superseded", "reactivation-eligible"} +REQUIRED_STATE_VOCABULARIES = { + "clinical_context_level": frozenset({"eligible", "hold", "superseded"}), + "target_level": frozenset({"eligible", "hold", "killed"}), + "pair_level": frozenset({"active", "hold", "reactivation-eligible"}), +} + +# Evidence states that mean the evidence is simply not there yet. These must +# never exclude. "Searched completely and found nothing" is a separate state. +ABSENT_EVIDENCE_STATES = frozenset({"not_assessed", "absent_incomplete_search"}) +COMPLETE_SEARCH_ABSENCE_STATE = "absent_after_complete_search" + +REQUIRED_SEARCH_FIELDS = frozenset( + { + "search_complete", + "search_policy_ref", + "source_coverage_ref", + "search_scope", + "searched_at", + "search_policy_version", + } ) @@ -116,16 +134,20 @@ def test_borrowed_gate_responsibilities_are_real_gates_and_not_passes(self) -> N lock["borrowed_gate_cost_tier"], tiers[gate_id] ) - def test_lock_dispositions_and_pool_states_stay_inside_the_contracts(self) -> None: + def test_lock_states_are_scoped_to_their_granularity(self) -> None: valid_dispositions = {item.value for item in CandidateDisposition} for level in self.registry["levels"]: - declared_states = set(level["pool_states"]) - self.assertEqual(declared_states, REQUIRED_POOL_STATES) + vocabularies = level["state_vocabularies"] + self.assertEqual( + {key: frozenset(value) for key, value in vocabularies.items()}, + REQUIRED_STATE_VOCABULARIES, + ) for lock in level["locks"]: + allowed = set(vocabularies[lock["granularity"]]) for outcome in lock["outcomes"]: with self.subTest(outcome=outcome["outcome"]): self.assertIn(outcome["disposition"], valid_dispositions) - self.assertIn(outcome["pool_state"], declared_states) + self.assertIn(outcome["resulting_state"], allowed) def test_every_exclusion_declares_its_basis(self) -> None: for level in self.registry["levels"]: @@ -135,6 +157,93 @@ def test_every_exclusion_declares_its_basis(self) -> None: continue with self.subTest(outcome=outcome["outcome"]): self.assertTrue(outcome.get("exclusion_basis")) + self.assertTrue(outcome.get("disposition_semantics")) + + def test_missing_or_unassessed_evidence_can_never_exclude(self) -> None: + """Blocker 1: absent evidence defers; it must not shrink the pool.""" + + for level in self.registry["levels"]: + standard = level["evidence_standard"] + self.assertEqual( + set(standard["absent_evidence_states"]), ABSENT_EVIDENCE_STATES + ) + for lock in level["locks"]: + for outcome in lock["outcomes"]: + if outcome["evidence_state"] not in ABSENT_EVIDENCE_STATES: + continue + with self.subTest(outcome=outcome["outcome"]): + self.assertEqual( + outcome["disposition"], CandidateDisposition.DEFER.value + ) + self.assertEqual(outcome["resulting_state"], "hold") + + def test_only_a_completed_search_may_remove_a_pair_from_the_active_pool( + self, + ) -> None: + """Blocker 1: EXCLUDE on absence requires a completeness record.""" + + for level in self.registry["levels"]: + standard = level["evidence_standard"] + self.assertEqual( + standard["complete_search_absence_state"], + COMPLETE_SEARCH_ABSENCE_STATE, + ) + self.assertEqual( + standard["complete_search_absence_semantics"], + "EXCLUDE_FROM_ACTIVE_POOL", + ) + self.assertIs( + standard["complete_search_absence_is_scientific_disproof"], False + ) + self.assertIs(standard["complete_search_requires_completeness_record"], True) + + absence_exclusions = [ + outcome + for lock in level["locks"] + for outcome in lock["outcomes"] + if outcome["evidence_state"] == COMPLETE_SEARCH_ABSENCE_STATE + ] + self.assertTrue(absence_exclusions, "no complete-search outcome declared") + for outcome in absence_exclusions: + with self.subTest(outcome=outcome["outcome"]): + self.assertEqual( + outcome["disposition"], CandidateDisposition.EXCLUDE.value + ) + self.assertEqual( + outcome["disposition_semantics"], "EXCLUDE_FROM_ACTIVE_POOL" + ) + self.assertIs(outcome["is_scientific_disproof"], False) + self.assertIs(outcome["is_killed"], False) + self.assertEqual(outcome["resulting_state"], "reactivation-eligible") + self.assertIs(outcome["requires_search_completeness_record"], True) + self.assertEqual( + set(outcome["required_search_fields"]), REQUIRED_SEARCH_FIELDS + ) + + def test_completeness_fields_are_carried_by_the_snapshot(self) -> None: + columns = set(self.levels["01"]["snapshot_columns"]) + for field in REQUIRED_SEARCH_FIELDS: + with self.subTest(field=field): + self.assertIn(f"lock_03_{field}", columns) + + def test_definitional_exclusions_are_never_reactivation_eligible(self) -> None: + pair_states = set( + self.registry["levels"][0]["state_vocabularies"]["pair_level"] + ) + self.assertIn("reactivation-eligible", pair_states) + for level in self.registry["levels"]: + for lock in level["locks"]: + for outcome in lock["outcomes"]: + if ( + outcome.get("disposition_semantics") + != "EXCLUDE_DEFINITIONALLY_INELIGIBLE" + ): + continue + with self.subTest(outcome=outcome["outcome"]): + self.assertNotEqual( + outcome["resulting_state"], "reactivation-eligible" + ) + self.assertIn(outcome["resulting_state"], {"killed", "superseded"}) def test_locks_run_cheapest_granularity_first(self) -> None: granularity_order = self.registry["ordering_principle"]["granularity_order"] @@ -177,6 +286,134 @@ def test_level_01_is_recorded_as_not_yet_authorized(self) -> None: for blocker in level["execution_blockers"]: self.assertTrue(blocker["statement"].strip()) + def test_pool_objects_separate_eligibility_audit_from_pair_states(self) -> None: + """Blocker 2: only pair-level objects may carry a pair state.""" + + objects = {item["id"]: item for item in self.levels["01"]["pool_objects"]} + self.assertEqual( + set(objects), + { + "raw_enumeration_matrix", + "context_eligibility_audit", + "target_eligibility_audit", + "eligible_universe_index", + "pool_level_01_snapshot", + }, + ) + for name in ("raw_enumeration_matrix", "context_eligibility_audit", + "target_eligibility_audit"): + self.assertIs(objects[name]["assigns_pair_state"], False) + for name in ("eligible_universe_index", "pool_level_01_snapshot"): + self.assertIs(objects[name]["assigns_pair_state"], True) + self.assertEqual( + set(self.levels["01"]["excluded_from_pair_reconciliation"]), + {"killed", "superseded"}, + ) + + def test_counting_identities_are_consistent_on_a_worked_example(self) -> None: + """Blocker 2: the declared identities must yield one denominator.""" + + identities = {item["id"]: item for item in self.levels["01"]["counting_identities"]} + self.assertEqual(set(identities), {f"CNT-0{n}" for n in range(1, 6)}) + + counts = { + "raw_contexts": 10, + "eligible_contexts": 6, + "hold_contexts": 3, + "superseded_contexts": 1, + "raw_targets": 100, + "eligible_targets": 70, + "hold_targets": 20, + "killed_targets": 10, + "raw_matrix": 1000, + "eligible_universe_index": 420, + "active": 120, + "hold_pairs": 200, + "reactivation_eligible_pairs": 100, + } + self._assert_identities_hold(identities, counts) + + def test_superseding_a_context_removes_exactly_one_column(self) -> None: + """Blocker 2: how the universe changes when a context is superseded.""" + + identities = {item["id"]: item for item in self.levels["01"]["counting_identities"]} + before = { + "raw_contexts": 10, "eligible_contexts": 6, "hold_contexts": 3, + "superseded_contexts": 1, "raw_targets": 100, "eligible_targets": 70, + "hold_targets": 20, "killed_targets": 10, "raw_matrix": 1000, + "eligible_universe_index": 420, "active": 120, "hold_pairs": 200, + "reactivation_eligible_pairs": 100, + } + after = dict(before) + after["eligible_contexts"] -= 1 + after["superseded_contexts"] += 1 + after["eligible_universe_index"] = ( + after["eligible_contexts"] * after["eligible_targets"] + ) + after["hold_pairs"] -= before["eligible_targets"] + + self.assertEqual( + before["eligible_universe_index"] - after["eligible_universe_index"], + before["eligible_targets"], + ) + # The raw matrix is unchanged: superseding is audit history, not deletion. + self.assertEqual(after["raw_matrix"], before["raw_matrix"]) + self.assertEqual(after["raw_contexts"], before["raw_contexts"]) + self._assert_identities_hold(identities, after) + + def test_killing_a_target_removes_exactly_one_row(self) -> None: + """Blocker 2: how the universe changes when a target is killed.""" + + identities = {item["id"]: item for item in self.levels["01"]["counting_identities"]} + before = { + "raw_contexts": 10, "eligible_contexts": 6, "hold_contexts": 3, + "superseded_contexts": 1, "raw_targets": 100, "eligible_targets": 70, + "hold_targets": 20, "killed_targets": 10, "raw_matrix": 1000, + "eligible_universe_index": 420, "active": 120, "hold_pairs": 200, + "reactivation_eligible_pairs": 100, + } + after = dict(before) + after["eligible_targets"] -= 1 + after["killed_targets"] += 1 + after["eligible_universe_index"] = ( + after["eligible_contexts"] * after["eligible_targets"] + ) + after["hold_pairs"] -= before["eligible_contexts"] + + self.assertEqual( + before["eligible_universe_index"] - after["eligible_universe_index"], + before["eligible_contexts"], + ) + self.assertEqual(after["raw_matrix"], before["raw_matrix"]) + self.assertEqual(after["raw_targets"], before["raw_targets"]) + self._assert_identities_hold(identities, after) + + def test_pair_reconciliation_excludes_killed_and_superseded(self) -> None: + """Blocker 2: killed/superseded must not enter the pair-state sum.""" + + cnt3 = next( + item + for item in self.levels["01"]["counting_identities"] + if item["id"] == "CNT-03" + ) + self.assertEqual(cnt3["lhs"], "eligible_universe_index") + summands = set(cnt3["rhs_sum"]) + self.assertEqual(summands, {"active", "hold_pairs", "reactivation_eligible_pairs"}) + for forbidden in ("killed_targets", "superseded_contexts", "killed", "superseded"): + self.assertNotIn(forbidden, summands) + + def _assert_identities_hold(self, identities: dict, counts: dict) -> None: + for identity_id, identity in sorted(identities.items()): + with self.subTest(identity=identity_id): + lhs = counts[identity["lhs"]] + if "rhs_product" in identity: + rhs = 1 + for name in identity["rhs_product"]: + rhs *= counts[name] + else: + rhs = sum(counts[name] for name in identity["rhs_sum"]) + self.assertEqual(lhs, rhs, identity["identity"]) + def test_recorded_gaps_are_not_silently_resolved(self) -> None: gaps = self.registry["recorded_gaps"] self.assertTrue(gaps)