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..dcce17a --- /dev/null +++ b/docs/handoff/2026-08-04-adc-pool-level-01.zh-CN.md @@ -0,0 +1,170 @@ +# 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` | 23 项校验,把上面那份记录钉在冻结的 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`) | + +Level 01 产出三个对象(见第四节决策 6):`Raw Enumeration Matrix`、`Eligible Universe Index`、`Pool Level 01`。未录入 active pool 的 pair 不删除,留在 Eligible Universe Index,状态为 `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. 每个锁输出必须同时记录 `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_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,本层就会因价值判断丢候选,直接违反它自己声明的错误偏好。明示以便审核方接受或否决,而不是静默改写来源文档。 + +## 五、对照仓库实测得到的发现 + +**发现 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 251 tests` 全部通过(`main` 基线 228 + 本次新增 23)。 +- `scripts/verify_repository_boundary.sh`:`Repository boundary check passed.` +- `git diff --check`:通过。 +- 零 `__pycache__`(全程 `PYTHONDONTWRITEBYTECODE=1`)。 +- **新增测试做过变异检验**,不是只看绿灯。首轮 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 个——全部由脚本实测,不是估计。 + +## 十、当前阻断 + +- `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 重跑被接受。 + +## 十二、第一轮审核裁决与修订(`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 new file mode 100644 index 0000000..401c6b9 --- /dev/null +++ b/docs/pools/ADC_POOL_FUNNEL_LEVEL_01.zh-CN.md @@ -0,0 +1,235 @@ +# 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`。 + +### 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`。 + +### 状态词表按粒度分开 + +| 粒度 | 状态 | 由哪把锁赋值 | +|---|---|---| +| clinical context 级 | `eligible`/`hold`/`superseded` | `LOCK-02` | +| target 级 | `eligible`/`hold`/`killed` | `LOCK-01` | +| pair 级 | `active`/`hold`/`reactivation-eligible` | `LOCK-03` | + +**`killed` 与 `superseded` 属资格审计历史,从不产生 pair 行,因此不参与 pair 级对账。** 一个 context 被 `superseded`,Eligible Universe Index 减少 `|eligible_targets|` 个 pair;一个 target 被 `killed`,减少 `|eligible_contexts|` 个 pair;两种情况下 Raw Enumeration Matrix 都不变——排除是审计记录,不是删除。 + +## 二、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` 三值,无法区分两种性质完全不同的 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 | 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 | 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 级** + +「没有发现 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`。 +- 任一锁定义性排除 → 该 context 或 target 不进入 Eligible Universe Index(`superseded` 或 `killed`)。 +- `LOCK-03` 完整检索后无 linkage → pair 留在 Eligible Universe Index,状态 `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。** 「证据缺失」在本契约中严格限于两种 `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 必须产出**五份**外部产物,不得合并成一张表: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`,且其 `disposition_semantics` 与 `resulting_state` 与本契约第二节的映射表一致。 +4. 所有证据引用都是可追溯的外部引用;仓库内不出现任何证据数据。 +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` 前,不得发布任何排序、推荐或资产决策。 + +## 七、已记录但本次不解决的契约缺口 + +以下六条都是真实缺口。它们**没有在本 PR 中被实现**,因为架构已于 2026-08-04 冻结、每月最多一次积累修复,顺手实现正是前两轮被阻断的越界行为。 + +| ID | 缺口 | 影响层 | +|---|---|---| +| `GAP-P01` | 内核没有 pool level 身份或层级快照对象,来源文档第六节的状态历史目前只能以外部 TSV 表达 | 01 | +| `GAP-P02` | 内核没有 `active`/`hold`/`killed`/`superseded`/`reactivation-eligible` 这组 pool 生命周期状态 | 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 | + +`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..697970a --- /dev/null +++ b/docs/pools/adc_pool_gate_usage.yaml @@ -0,0 +1,434 @@ +# 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 + resulting_state: eligible + evidence_state: present + - outcome: possible_surface_target + disposition: DEFER + resulting_state: hold + evidence_state: absent_incomplete_search + - outcome: not_surface_target + disposition: EXCLUDE + resulting_state: killed + evidence_state: present + exclusion_basis: definitional + disposition_semantics: EXCLUDE_DEFINITIONALLY_INELIGIBLE + is_scientific_disproof: true + - outcome: identity_unresolved + disposition: DEFER + resulting_state: hold + evidence_state: not_assessed + + - 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 + resulting_state: eligible + evidence_state: present + - outcome: plausible_unmet_context + disposition: DEFER + resulting_state: hold + evidence_state: absent_incomplete_search + # DEVIATION-01:来源文档把 weak 与 redundant 合为一个 weak_or_redundant_context + # 输出。本注册表把它拆成两个,因为二者的排除依据强度不同:redundant 是集合 + # 包含关系,可判定;weak 是价值判断,在一个以召回为先的层里不足以据此排除。 + - outcome: redundant_context + disposition: EXCLUDE + 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 + resulting_state: hold + evidence_state: present + - outcome: undefined_context + disposition: DEFER + resulting_state: hold + evidence_state: not_assessed + + - 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 + resulting_state: active + evidence_state: present + # 「未评估」与「检索不完整」都属于证据缺失,一律 DEFER。 + # 只有完成规定范围检索后仍无 linkage 才是一项阳性检索结论,见下一条。 + - outcome: linkage_unassessed + disposition: DEFER + 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 个。逐一列出而不写「其余」, + # 以便测试能断言这份清单与冻结拓扑完全相等。 + 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 + 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 + # 「已按规定范围检索完毕、仍未发现 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 + 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 + - pair_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 + # 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 + - material_risks + - unknowns + - conflicts + - next_required_evidence + - evidence_refs + - last_assessed_at + - policy_version + + # 状态词表按粒度分开。混用会让计数失去唯一的 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 + + 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: >- + 内核没有 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 五值输出; + 尤其无法区分 EXCLUDE_DEFINITIONALLY_INELIGIBLE(killed/superseded)与 + EXCLUDE_FROM_ACTIVE_POOL(reactivation-eligible,非科学证伪)。 + 本契约靠 disposition_semantics 与 resulting_state 两个字段补足,属外部编码约定而非契约支持。 + 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 e71cc50..e851361 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2393,9 +2393,53 @@ 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` 的处理是否正确。 + ### 2026-08-04 19:09 EDT - Instruction: Read `Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md#Biotech基础设施` and record which existing public infrastructure StelligenOS can reuse in the future without rebuilding it. - Action: Added `docs/architecture/BIOTECH_INFRASTRUCTURE_CATALOG.zh-CN.md` to the `main` worktree. The document records the provider groups, recommended implementation order, future provider interface names, current internal repository capabilities, and the boundary between reusable data infrastructure and StelligenOS-owned ADC semantics, evidence adjudication, Gate, Rule, FTO, and asset decisions. - Boundary: This was documentation-only. No public data, database, cache, result, runtime dependency, provider adapter, or execution code was added. The catalog explicitly states that future data and processing remain outside the repository under external `DATA`. - Workflow: Per the explicit instruction for this catalog, it is being committed directly to `main` without the normal PR review cycle; the required worklog trace is retained. + +## 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 new file mode 100644 index 0000000..d572a4b --- /dev/null +++ b/tests/test_adc_pool_gate_usage.py @@ -0,0 +1,430 @@ +"""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_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", + } +) + + +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_states_are_scoped_to_their_granularity(self) -> None: + valid_dispositions = {item.value for item in CandidateDisposition} + for level in self.registry["levels"]: + 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["resulting_state"], allowed) + + 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")) + 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"] + 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_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) + 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()