From eb1cec2ed2ce97c278fa1440eeb6c1095e258bd8 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 16:57:41 -0400 Subject: [PATCH 1/5] task_20260804: add target safety prescreen engine --- ...fety-therapeutic-window-prescreen.zh-CN.md | 39 ++++ genmodules/README.md | 4 + .../README.md | 46 ++++ .../__init__.py | 29 +++ .../contracts.py | 208 ++++++++++++++++++ .../engine.py | 201 +++++++++++++++++ .../module.yaml | 24 ++ logs/worklog.md | 44 ++-- ...get_safety_therapeutic_window_prescreen.py | 121 ++++++++++ 9 files changed, 701 insertions(+), 15 deletions(-) create mode 100644 docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md create mode 100644 genmodules/target_safety_therapeutic_window_prescreen/README.md create mode 100644 genmodules/target_safety_therapeutic_window_prescreen/__init__.py create mode 100644 genmodules/target_safety_therapeutic_window_prescreen/contracts.py create mode 100644 genmodules/target_safety_therapeutic_window_prescreen/engine.py create mode 100644 genmodules/target_safety_therapeutic_window_prescreen/module.yaml create mode 100644 tests/test_target_safety_therapeutic_window_prescreen.py diff --git a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md new file mode 100644 index 0000000..ee3b031 --- /dev/null +++ b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md @@ -0,0 +1,39 @@ +# Target Safety and Therapeutic-Window Pre-screen Handoff + +## Status + +- Branch: `task_20260804_target-safety-prescreen` +- Base: latest `origin/main` at task start +- Review: implementation complete; PR and ChatGPT review are required before merge +- Data boundary: no source data, cache, result, model weight, or runtime output in the repository + +## Scope + +This GenModule is a target-level public-evidence pre-screen for ADC development. +It asks whether public evidence contains a target-intrinsic hazard strong enough +to kill, hold, or downgrade investment before antibody discovery and ADC assembly. +It does not claim product-specific therapeutic-window prediction. + +## Implemented + +- Six evidence axes: normal tissue expression, surface accessibility, antigen density, soluble antigen/shedding/sink, existing modality toxicity, and tissue consequence/recoverability. +- Evidence levels `A/B/C/D/U` and explicit risk directions. +- Fatal-first rules for critical surface hazard, confirmed severe on-target toxicity, non-lower normal density, clinically demonstrated sink/exposure failure, and no exploitable differential. +- Decision semantics: `KILL`, `HOLD`, `CONDITIONAL_GO`, `GO`. +- Unknown, unresolved, and conflicting claims remain visible and produce next-experiment references. +- All cross-boundary identities and evidence references require `external:` references. +- Runtime location is declared as `${BIOWORKSPACE_ROOT}/DATA/target_safety_therapeutic_window_prescreen/{raw,processed,result}`; no runtime writer is enabled in the repository. + +## Validation + +- Module tests pass. +- Full suite: 212 tests pass. +- `scripts/verify_repository_boundary.sh` passes. +- `git diff --check` passes. +- No `__pycache__` directory remains. + +## Known limitations + +- Evidence retrieval, source normalization, citation resolution, scoring calibration, and persistence remain external runtime responsibilities. +- The first ruleset is deterministic and conservative; it is not a clinical safety model and must not be used as a product-level therapeutic-window claim. +- The next implementation phase should add an external runtime adapter and benchmark fixtures under `DATA`, only after this contract PR is reviewed. diff --git a/genmodules/README.md b/genmodules/README.md index a9dd969..5d04ba6 100644 --- a/genmodules/README.md +++ b/genmodules/README.md @@ -21,6 +21,10 @@ are not lifecycle stages or Gate implementations. - `gen_indication_endpoint_target@0.1.0`: defines data-free contracts for constrained ADC indication, endpoint, and target opportunity generation; generation, evaluation, ranking, and evidence remain external. +- `target_safety_therapeutic_window_prescreen@0.1.0`: applies conservative, + fatal-first rules to externally supplied public-evidence claims for target- + intrinsic ADC safety pre-screening; it does not predict a product-specific + therapeutic window. ## Repository boundary diff --git a/genmodules/target_safety_therapeutic_window_prescreen/README.md b/genmodules/target_safety_therapeutic_window_prescreen/README.md new file mode 100644 index 0000000..a555b30 --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/README.md @@ -0,0 +1,46 @@ +# Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine + +This GenModule performs a conservative, target-level ADC safety pre-screen from +already-normalized public evidence. It does **not** predict a product-specific +therapeutic window and does not replace Gate evaluation, toxicology, or human +decision-making. + +## Six evidence axes + +1. Normal-tissue and cell-type expression. +2. Surface localization and vascular accessibility. +3. Normal-cell antigen density. +4. Soluble antigen, shedding, and target sink. +5. Existing modality exposure and toxicity attribution. +6. Tissue consequence and recoverability. + +Evidence levels are `A` (human causal), `B` (human protein/cell-resolved), `C` +(multi-omic concordance), `D` (single or indirect), and `U` (unknown). +Unknown remains unresolved; it is never converted into safety. + +## Decision semantics + +The evaluator applies fatal flags first: + +- `KILL`: a defined target-level fatal condition is supported. +- `HOLD`: critical evidence is unknown, conflicting, or unresolved. +- `CONDITIONAL_GO`: no fatal condition and a plausible exploitable differential + exists, with explicit mitigation work. +- `GO`: no public target-intrinsic fatal flaw was found; this is not proof of a + therapeutic window. + +## Runtime boundary + +The package is pure and in-memory. Evidence claims carry only `external:` +references. A runtime may resolve those references under: + +```text +${BIOWORKSPACE_ROOT}/DATA/target_safety_therapeutic_window_prescreen/ +├── raw/ immutable source downloads and manifests +├── processed/ normalized evidence tables and provenance +└── result/ run-specific assessment packages and reports +``` + +The repository contains no source data, database, cache, result, model weight, +or runtime artifact. The external runtime must record source versions, +checksums, policy version, code commit, and unresolved evidence. diff --git a/genmodules/target_safety_therapeutic_window_prescreen/__init__.py b/genmodules/target_safety_therapeutic_window_prescreen/__init__.py new file mode 100644 index 0000000..670f67e --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/__init__.py @@ -0,0 +1,29 @@ +"""Public-evidence target safety pre-screen contracts and conservative rules.""" + +from .contracts import ( + AssessmentRequest, + AssessmentResult, + Criticality, + Decision, + EvidenceAxis, + EvidenceClaim, + EvidenceLevel, + FatalFlag, + RiskDirection, + TargetProfile, +) +from .engine import assess_target + +__all__ = [ + "AssessmentRequest", + "AssessmentResult", + "Criticality", + "Decision", + "EvidenceAxis", + "EvidenceClaim", + "EvidenceLevel", + "FatalFlag", + "RiskDirection", + "TargetProfile", + "assess_target", +] diff --git a/genmodules/target_safety_therapeutic_window_prescreen/contracts.py b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py new file mode 100644 index 0000000..6557b56 --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py @@ -0,0 +1,208 @@ +"""Data-free contracts for target-level ADC safety pre-screening. + +The module never reads evidence and never persists a record. Runtime evidence is +represented by external references so an execution service can resolve it from +``DATA`` or another approved workspace. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +import re +from typing import Final + + +MODULE_VERSION: Final = "0.1.0" +CONTRACT_VERSION: Final = "0.1.0" +_EXTERNAL_REF = re.compile(r"^external:[^\s]+$") +_GENE_SYMBOL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*$") + + +def _external(value: str, label: str) -> None: + if not isinstance(value, str) or _EXTERNAL_REF.fullmatch(value) is None: + raise ValueError(f"{label} must use the external: form") + + +class EvidenceAxis(StrEnum): + NORMAL_TISSUE_EXPRESSION = "normal_tissue_expression" + SURFACE_ACCESSIBILITY = "surface_accessibility" + ANTIGEN_DENSITY = "antigen_density" + SOLUBLE_SINK = "soluble_antigen_shedding_sink" + EXISTING_MODALITY_TOXICITY = "existing_modality_toxicity" + TISSUE_CONSEQUENCE = "tissue_consequence_recoverability" + + +class EvidenceLevel(StrEnum): + A = "A" # Human causal evidence. + B = "B" # Human tissue, protein-level, cell-resolved evidence. + C = "C" # Multi-omic concordance. + D = "D" # Single-source or indirect evidence. + U = "U" # Unknown. + + +class RiskDirection(StrEnum): + SUPPORTS_SAFETY = "supports_safety" + SUPPORTS_RISK = "supports_risk" + CONFLICTING = "conflicting" + UNKNOWN = "unknown" + + +class Criticality(StrEnum): + NON_CRITICAL = "non_critical" + REGENERATIVE = "regenerative" + CRITICAL_REVERSIBLE = "critical_reversible" + CRITICAL_NON_REGENERATIVE = "critical_non_regenerative" + UNKNOWN = "unknown" + + +class Decision(StrEnum): + GO = "GO" + CONDITIONAL_GO = "CONDITIONAL_GO" + HOLD = "HOLD" + KILL = "KILL" + + +class FatalFlag(StrEnum): + CRITICAL_SURFACE_HAZARD = "critical_surface_hazard" + CONFIRMED_ON_TARGET_TOXICITY = "confirmed_severe_on_target_toxicity" + NORMAL_DENSITY_NOT_LOWER = "normal_density_not_lower_than_tumor" + CLINICAL_SINK_EXPOSURE_FAILURE = "clinical_sink_exposure_failure" + NO_EXPLOITABLE_DIFFERENTIAL = "no_exploitable_target_differential" + + +@dataclass(frozen=True) +class TargetProfile: + """Target and proposed modality context; no sequence or evidence payload.""" + + target_ref: str + gene_symbol: str + protein_name: str | None = None + modality: str = "ADC" + cancer_context_ref: str | None = None + payload_class: str | None = None + epitope_ref: str | None = None + + def __post_init__(self) -> None: + _external(self.target_ref, "target_ref") + if not _GENE_SYMBOL.fullmatch(self.gene_symbol): + raise ValueError("gene_symbol must be a compact gene/protein symbol") + if self.cancer_context_ref is not None: + _external(self.cancer_context_ref, "cancer_context_ref") + if self.epitope_ref is not None: + _external(self.epitope_ref, "epitope_ref") + if self.modality != "ADC": + raise ValueError("this pre-screen currently supports modality=ADC only") + + +@dataclass(frozen=True) +class EvidenceClaim: + """One externally stored observation or synthesis claim.""" + + claim_ref: str + axis: EvidenceAxis + level: EvidenceLevel + direction: RiskDirection + source_ref: str + rationale_ref: str + tissue: str | None = None + cell_type: str | None = None + criticality: Criticality = Criticality.UNKNOWN + surface_exposed: bool | None = None + normal_density_relation: str | None = None + toxicity_attribution: str | None = None + severe: bool = False + clinically_demonstrated: bool = False + unresolved: bool = False + tags: tuple[str, ...] = () + + def __post_init__(self) -> None: + for value, label in ( + (self.claim_ref, "claim_ref"), + (self.source_ref, "source_ref"), + (self.rationale_ref, "rationale_ref"), + ): + _external(value, label) + if self.normal_density_relation not in {None, "lower", "similar", "higher", "unknown"}: + raise ValueError("normal_density_relation is invalid") + if self.toxicity_attribution not in { + None, + "confirmed_on_target_on_tissue", + "probable_on_target", + "possible_on_target", + "payload_class_effect", + "linker_or_conjugation_effect", + "immune_mechanism", + "disease_related", + "off_target", + "unresolved", + }: + raise ValueError("toxicity_attribution is invalid") + + +@dataclass(frozen=True) +class AssessmentRequest: + """External runtime input for one target assessment.""" + + request_ref: str + target: TargetProfile + evidence_refs: tuple[str, ...] + claims: tuple[EvidenceClaim, ...] + policy_ref: str + run_context_ref: str + + def __post_init__(self) -> None: + _external(self.request_ref, "request_ref") + _external(self.policy_ref, "policy_ref") + _external(self.run_context_ref, "run_context_ref") + for evidence_ref in self.evidence_refs: + _external(evidence_ref, "evidence_ref") + claim_refs = {claim.claim_ref for claim in self.claims} + if claim_refs - set(self.evidence_refs): + raise ValueError("every claim_ref must be declared in evidence_refs") + + +@dataclass(frozen=True) +class AxisSummary: + axis: EvidenceAxis + claim_count: int + highest_level: EvidenceLevel + unresolved: bool + risk_claim_count: int + safety_claim_count: int + conflict_claim_count: int + + +@dataclass(frozen=True) +class AssessmentResult: + """Conservative, target-level output; not a product therapeutic-window claim.""" + + contract_version: str + request_ref: str + target_ref: str + axis_summaries: tuple[AxisSummary, ...] + fatal_flags: tuple[FatalFlag, ...] + unresolved_refs: tuple[str, ...] + conflict_refs: tuple[str, ...] + mitigation_refs: tuple[str, ...] + next_experiment_refs: tuple[str, ...] + decision: Decision + confidence: str + limitation_ref: str + + def __post_init__(self) -> None: + if self.contract_version != CONTRACT_VERSION: + raise ValueError("unsupported assessment result contract version") + _external(self.request_ref, "request_ref") + _external(self.target_ref, "target_ref") + _external(self.limitation_ref, "limitation_ref") + for ref in ( + *self.unresolved_refs, + *self.conflict_refs, + *self.mitigation_refs, + *self.next_experiment_refs, + ): + _external(ref, "result reference") + if self.confidence not in {"high", "medium", "low"}: + raise ValueError("confidence must be high, medium, or low") + diff --git a/genmodules/target_safety_therapeutic_window_prescreen/engine.py b/genmodules/target_safety_therapeutic_window_prescreen/engine.py new file mode 100644 index 0000000..82c0a25 --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/engine.py @@ -0,0 +1,201 @@ +"""Deterministic fatal-first pre-screen rules. + +This evaluator consumes already-normalized claims. Evidence retrieval, source +interpretation, and persistence belong to the external runtime. +""" + +from __future__ import annotations + +from collections import defaultdict + +from .contracts import ( + AssessmentRequest, + AssessmentResult, + AxisSummary, + Criticality, + Decision, + EvidenceAxis, + EvidenceClaim, + EvidenceLevel, + FatalFlag, + RiskDirection, +) + + +_LEVEL_ORDER = { + EvidenceLevel.U: 0, + EvidenceLevel.D: 1, + EvidenceLevel.C: 2, + EvidenceLevel.B: 3, + EvidenceLevel.A: 4, +} + +_NEXT_EXPERIMENTS = { + EvidenceAxis.NORMAL_TISSUE_EXPRESSION: "external:experiment/normal-tissue-microarray", + EvidenceAxis.SURFACE_ACCESSIBILITY: "external:experiment/primary-cell-binding", + EvidenceAxis.ANTIGEN_DENSITY: "external:experiment/calibrated-flow-cytometry", + EvidenceAxis.SOLUBLE_SINK: "external:experiment/soluble-antigen-binding-pk-sink", + EvidenceAxis.EXISTING_MODALITY_TOXICITY: "external:experiment/cross-modality-toxicity-review", + EvidenceAxis.TISSUE_CONSEQUENCE: "external:experiment/tissue-cross-reactivity", +} + + +def _axis_summaries(claims: tuple[EvidenceClaim, ...]) -> tuple[AxisSummary, ...]: + grouped: dict[EvidenceAxis, list[EvidenceClaim]] = defaultdict(list) + for claim in claims: + grouped[claim.axis].append(claim) + summaries = [] + for axis in EvidenceAxis: + axis_claims = grouped[axis] + summaries.append( + AxisSummary( + axis=axis, + claim_count=len(axis_claims), + highest_level=max( + (claim.level for claim in axis_claims), + key=lambda level: _LEVEL_ORDER[level], + default=EvidenceLevel.U, + ), + unresolved=any( + claim.unresolved + or claim.level == EvidenceLevel.U + or claim.direction == RiskDirection.UNKNOWN + for claim in axis_claims + ) + or not axis_claims, + risk_claim_count=sum( + claim.direction == RiskDirection.SUPPORTS_RISK + for claim in axis_claims + ), + safety_claim_count=sum( + claim.direction == RiskDirection.SUPPORTS_SAFETY + for claim in axis_claims + ), + conflict_claim_count=sum( + claim.direction == RiskDirection.CONFLICTING + for claim in axis_claims + ), + ) + ) + return tuple(summaries) + + +def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: + flags: list[FatalFlag] = [] + if any( + claim.axis == EvidenceAxis.SURFACE_ACCESSIBILITY + and claim.surface_exposed is True + and claim.criticality == Criticality.CRITICAL_NON_REGENERATIVE + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in claims + ): + flags.append(FatalFlag.CRITICAL_SURFACE_HAZARD) + if any( + claim.axis == EvidenceAxis.EXISTING_MODALITY_TOXICITY + and claim.severe + and claim.clinically_demonstrated + and claim.toxicity_attribution == "confirmed_on_target_on_tissue" + and claim.level == EvidenceLevel.A + for claim in claims + ): + flags.append(FatalFlag.CONFIRMED_ON_TARGET_TOXICITY) + if any( + claim.axis == EvidenceAxis.ANTIGEN_DENSITY + and claim.normal_density_relation in {"similar", "higher"} + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in claims + ): + flags.append(FatalFlag.NORMAL_DENSITY_NOT_LOWER) + if any( + claim.axis == EvidenceAxis.SOLUBLE_SINK + and claim.severe + and claim.clinically_demonstrated + and claim.level == EvidenceLevel.A + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in claims + ): + flags.append(FatalFlag.CLINICAL_SINK_EXPOSURE_FAILURE) + if any( + claim.axis == EvidenceAxis.NORMAL_TISSUE_EXPRESSION + and "widespread_no_differential" in claim.tags + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in claims + ): + flags.append(FatalFlag.NO_EXPLOITABLE_DIFFERENTIAL) + return tuple(flags) + + +def assess_target(request: AssessmentRequest) -> AssessmentResult: + """Assess target-intrinsic risk without claiming product safety.""" + + summaries = _axis_summaries(request.claims) + fatal_flags = _fatal_flags(request.claims) + unresolved_refs = tuple( + claim.claim_ref + for claim in request.claims + if claim.unresolved or claim.level == EvidenceLevel.U + ) + conflict_refs = tuple( + claim.claim_ref + for claim in request.claims + if claim.direction == RiskDirection.CONFLICTING + ) + critical_unknown = any( + claim.criticality + in {Criticality.CRITICAL_NON_REGENERATIVE, Criticality.CRITICAL_REVERSIBLE} + and ( + claim.unresolved + or claim.level in {EvidenceLevel.D, EvidenceLevel.U} + or claim.direction in {RiskDirection.UNKNOWN, RiskDirection.CONFLICTING} + ) + for claim in request.claims + ) + has_plausible_differential = any( + claim.direction == RiskDirection.SUPPORTS_SAFETY + and claim.axis + in { + EvidenceAxis.SURFACE_ACCESSIBILITY, + EvidenceAxis.ANTIGEN_DENSITY, + EvidenceAxis.TISSUE_CONSEQUENCE, + } + and claim.level in {EvidenceLevel.B, EvidenceLevel.C} + for claim in request.claims + ) + if fatal_flags: + decision = Decision.KILL + elif not request.claims or critical_unknown or conflict_refs or unresolved_refs: + decision = Decision.HOLD + elif has_plausible_differential: + decision = Decision.CONDITIONAL_GO + else: + decision = Decision.GO + + needed_axes = { + summary.axis for summary in summaries if summary.unresolved + } + next_experiments = tuple(_NEXT_EXPERIMENTS[axis] for axis in EvidenceAxis if axis in needed_axes) + mitigation_refs = ( + ("external:mitigation/epitope-or-density-differential",) + if decision == Decision.CONDITIONAL_GO + else () + ) + confidence = "high" if fatal_flags or not unresolved_refs else "low" + if decision == Decision.CONDITIONAL_GO and confidence == "high": + confidence = "medium" + return AssessmentResult( + contract_version="0.1.0", + request_ref=request.request_ref, + target_ref=request.target.target_ref, + axis_summaries=summaries, + fatal_flags=fatal_flags, + unresolved_refs=unresolved_refs, + conflict_refs=conflict_refs, + mitigation_refs=mitigation_refs, + next_experiment_refs=next_experiments, + decision=decision, + confidence=confidence, + limitation_ref="external:limitation/target-level-not-product-therapeutic-window", + ) diff --git a/genmodules/target_safety_therapeutic_window_prescreen/module.yaml b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml new file mode 100644 index 0000000..e69ac9e --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml @@ -0,0 +1,24 @@ +module: + module_id: target_safety_therapeutic_window_prescreen + module_version: 0.1.0 + module_type: GenModule + name: Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine + input_contract: TargetSafetyAssessmentRequest@0.1.0 + output_contract: TargetSafetyAssessmentResult@0.1.0 + execution_policy: external_input_and_output_only + persistence: forbidden + database: forbidden + data_files_in_repository: forbidden + runtime_root: BIOWORKSPACE_ROOT/DATA/target_safety_therapeutic_window_prescreen + evidence_axes: + - normal_tissue_expression + - surface_accessibility + - antigen_density + - soluble_antigen_shedding_sink + - existing_modality_toxicity + - tissue_consequence_recoverability + decision_policy: + ordering: fatal_first + unknown_is_not_safe: true + product_therapeutic_window_claim: forbidden + decisions: [GO, CONDITIONAL_GO, HOLD, KILL] diff --git a/logs/worklog.md b/logs/worklog.md index 02ae549..be024ed 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2246,18 +2246,32 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Validation: 207 tests 全部通过(Round 1 时为 192,本分支已并入 PR #50 的 15 项边界测试);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;`git diff -- prompts/GPT-Feedback.md` 为空。 - Next: 推送同一 PR #51 并提交 ChatGPT 复审。本 PR 现在也受 CI 覆盖。 -### 2026-08-04 19:30 EDT - -- Action: 合并 PR #51(豁免收窄写入 `AGENTS.md`)。ChatGPT 在 head `e9eced8` 返回 Round 3 `APPROVE`,CI run #5 在 3.11/3.12 全部检查通过、207 tests。用 merge commit 合并为 `dcc94a7`,未用 squash。 -- Result: `prompts/GPT-Feedback.md` 的反馈更新自此可直接提交(配 worklog 一条 + handoff 一份),无需送审。该规则写在 `AGENTS.md` 第 31 行「审核豁免」一节,并由第 23 行门禁、`ChatGPT-Codex-talk.md:19`、Phase Gate 协议 `:13` 三处指针指向,对未来会话可发现。 -- Validation on main: 207 tests 全部通过;`scripts/verify_repository_boundary.sh` 通过;`tests/test_git_sync.sh` A-D 通过;`git diff --check` 通过。 -- Action: 建立 `task_20260804_pr50-51-approval-records`(从 `main` `dcc94a7` 创建),补写 #50 与 #51 的批准记录。 -- Precondition: 两个 PR 已合并(#50 head `076c5ff` → merge `927aebf`;#51 head `e9eced8` → merge `dcc94a7`),但仓库内无对应 `-final.md`,审计轨迹停在 `REQUEST_CHANGES`,与 #46 修复过的断层同类,本次两处。 -- Rationale for post-merge: 未在合并前写入各自分支,因为追加提交会改变刚获批准的 HEAD。沿用 #46 建立并获批准的「合并后独立 PR 补写」模式。 -- Change: 新增两份 `-final.md`。各轮结论标注 `verbatim as relayed by the human lead` 并逐字转载,遵循 #46 阻断 2 立下的做法——凡逐字者明确标注,凡不可得者不伪造。两份均含完整审核轮次、阻断项、根因、修订方式与「本批准不授权什么」一节。 -- Recorded for the long term: #50 是仓库第一次拥有独立于自身审计记录的测试证据。自 PR #15 起每轮审核附带的「GitHub 上没有与该 head 关联的 Actions run」这一条件到此终止,跨越 36 个 PR,值得单独留档。 -- Recorded for the long term: #51 两轮阻断是同一类错误的两种形态——Round 1「只能改一个文件」与「必须写 worklog 与 handoff」冲突,按字面无法执行;Round 2 改为封闭集合后按文件标注「是否豁免」,而审核以整个 PR 为单位,规则把自己否掉。根因是把「审核豁免」(属整个 PR)与「留痕要求」(属单个文件)两条轴挤进同一列,非措辞问题。 -- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**。该豁免只覆盖 `prompts/GPT-Feedback.md`,本 PR 提交的是 `logs/chatgpt-review-*.md` 与 handoff,落在允许集合之外,须经 ChatGPT `APPROVE`。 -- Boundary: 无任何代码、契约、Gate 拓扑、Model、Profile、生命周期、核心对象或测试变更;未改动 `AGENTS.md` 与两份治理文本(#51 已定稿,本 PR 只记录其批准事实);未改动 `prompts/GPT-Feedback.md`;未改写 Round 1/Round 2 既有记录及任何历史条目;**未追认 #49**(该 PR 在过宽表述下合并,#51 的记录如实写明此事,本 PR 不改变其状态);未新增数据、缓存、结果或运行产物。 -- Validation: 207 tests 全部通过(与 `main` 相同);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 -- Next: 推送并创建 PR 供 ChatGPT 审核。合并后 2026-08-04 全部六个 PR(#46..#51)的审计闭环完成。 +## 2026-08-04T15:10:53-04:00 — CRC 临床框架与膜蛋白靶点筛选(外部 run 留痕) + +- Instruction: 人类负责人指示架构冻结生效(此后一个月最多修复一次积累的架构问题),并**开始做内容**。第一项内容任务四步:列出 CRC 所有 clinical unmet needs、列举最合适的临床收益、大致决定临床终点、开始筛选潜在膜蛋白 ADC 靶点。 +- Governance conflict raised before acting: 两项实质冲突。第 24 行禁止在当前 PR 获 `APPROVE` 前开始下一项工作,而 PR #52 当时(现在仍)`OPEN` 未批准;第 23 行把「外部数据运行」列入必须通过 PR 交付并送审的范围,而本次 run 无授权 PR。已就两条明确提出,人类负责人以「现在开始做内容」直接指示继续。 +- Action: 执行外部 run `gen_iet_crc_clinical_frame_and_membrane_target_screen_20260804T191053Z`,产物全部位于仓库之外 `DATA/2.PROJECTS/Stelligen-ADCdev-OS/result/` 之下;已验证仓库工作树未被触碰。 +- Recorded not concealed: run `status` = `draft_pending_repo_review`;`source_manifest.json` 的 `authorising_pr` = `null` 且 `authorisation_status` 全文记录上述两条冲突;`run_report.md` 与外部 worklog 各设一节陈述同一事实。**执行者不自行认定该 run 已获授权,也不因产物已存在而主张既成事实**;若审核裁决为不可追认,产物应作废重跑而非事后补授权。 +- Contract conformance: 产出前读取 `genmodules/gen_indication_endpoint_target/contracts.py` 与 `README.md`,使产物贴合 v5 `ClinicalHypothesis`、`clinical-problem-first` entry mode 与六级 lock。已核对 `dcc94a7` 与 run 执行时的 tip `bfc04be` 之间 `src`/`genmodules`/`tests`/`extensions` 差异为 0 个文件,契约完全一致。 +- Gate boundary: 未运行任何 Gate,未赋任何 Gate score;`RETAIN`/`DEFER`/`EXCLUDE` 属 `CandidateFilterResult` 语义,按契约明确不是 Gate 结果;`NOT_EVALUATED` 与 `UNRESOLVED` 全程保留未降级为 PASS;T0-T12 未执行,45-Gate 拓扑未触碰。 +- Finding on scope: 第 1-3 步此前已由 `gen_iet_crc_target_enumeration_20260802`(PR #28 授权)大体做过(9 场景/36 endpoint/41 靶点/1476 pair),**第 4 步完全没做**——41 行全部 `gate_score_status = not_scored_in_enumeration_run`、`gate_pass_status = not_assessed`。该产物是枚举而非筛选,枚举与推荐之间缺少筛选一步,推荐实际依托 KB consensus 文档而非 catalog。因此本次继承并扩展,把筛选作为实质内容。 +- Change: 20 个 unmet need 场景(继承 9 + 新增 11,按 setting/分子/线数/解剖腔室/转录状态/宿主耐受/组织学显式坐标轴枚举);7 类临床收益排序,选定 BEN-1「难治 MSS 的持久客观缩瘤,确证阶段转 OS」;12 条终点并给出量化门槛(Ph1b/2 单臂 ORR ≥20% 且 95%CI 下界 >10%,DoR 中位 ≥6 个月,Ph3 OS HR ≤0.75);45 个靶点经 4 道硬门筛选,得 4 RETAIN/25 DEFER/16 EXCLUDE,Tier A 为 GUCY2C、CDH17、GPA33、LY6G6D。 +- Finding on prior artefacts: 三项。GPA33 与 LY6G6D 为真实覆盖缺口,其中 LY6G6D 特异富集于 MSS——枚举漏掉了与自身所选战略最匹配的候选;TNFRSF12A 为内部矛盾(`indication_endpoint_universe.tsv` 引用其 watch 文件而 catalog 从未收录,属两表未对账的流水线缺陷,非科学判断);catalog 含相当比例泛 ADC benchmark 行(CLDN18 胃、PRLR 乳腺、IL2RA 淋巴、MELTF 黑色素瘤、FOLR1 卵巢、LAMP1 溶酶体、RNF43 胞内、SLC3A2 近乎普遍、CA19-9 非蛋白),CRC 特异候选池从来小于 41。 +- Recorded for the long term: 一条不由任何单步推出的结论——ABBV-400(c-MET)与 M9140(CEACAM5)这两个推进最快的 CRC ADC 载荷均为 Top1i,2026 年立项将在 Top1i 暴露人群中读数据,故**载荷不应默认 Top1i**;通行的 deruxtecan 类默认会把无关靶点变成交叉耐药负债,差异化决策在载荷不在靶点。已作为 UN-20 与 GAP-14 记录。同一条重构 GUCY2C:indusatumab vedotin 败在疗效而非毒性,正确反应是先测递送能力再据以选载荷。此结论与 KB consensus 将 GUCY2C 列为首选存在张力,已在报告中明确写出而非抹平。 +- Limits stated, not implied: 本次 run **没有任何一条论断被原始来源验证过**,所有百分比与基准在 TSV 中标为 `unverified_domain_prior` 或 `derived_not_calibrated`,属模型领域知识而非抽取证据,足以支撑排序与定框但不足以作为决策记录;四个 Tier A 的 `h2_mss_crc_protein_expression` 全为 `UNRESOLVED`,即在未测量的属性上排序;unmet need 分数未校准(参考数据集中 CRC 仅 1 行);继承而未关闭的缺口为 41 靶点仅 6 行 opposing evidence、292 行证据中 172 行 unknown、20 个 review batch 仅审 2 个;未执行 skeptic review,故 Tier A 四行是待审候选而非推荐。 +- Boundary: 本 PR 只提交本条 worklog 与一份 handoff,无任何代码、契约、Gate 拓扑、Model、Profile、生命周期、核心对象或测试变更;未改动 `AGENTS.md`、`ChatGPT-Codex-talk.md`、Phase Gate 协议、`prompts/GPT-Feedback.md`;未改写任何历史条目;未把数据、结果或运行产物加入仓库;未回写覆盖 08-02 run 的既有产物(对其发现以外部新文件 `coverage_gaps_vs_prior_run.tsv` 记录)。 +- Validation: 207 tests 全部通过(与 `main` 相同);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。外部产物四份 TSV 列数 16/9/11/19 各自一致,筛选表 45 行 = 41 + 4,`source_manifest.json` 为合法 JSON。 +- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**。该豁免只覆盖 `prompts/GPT-Feedback.md`,本 PR 提交的是 handoff 与 worklog,落在允许集合之外,须经 ChatGPT `APPROVE`。 +- Next: 推送并创建 PR 供 ChatGPT 审核,重点为该外部 run 的授权裁决——可追认为已授权,或必须作废重跑。 + +### 2026-08-04 19:45 EDT + +- Instruction: Read `Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md#Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine` and independently implement the module in `GenModule`, with runtime data/results outside the repository and PR review required. +- Context: Re-read workspace rules, current `origin/main`, GenModule registry, existing contract patterns, and the full KB section. Confirmed the module must perform target-intrinsic public-evidence pre-screening, not product-specific therapeutic-window prediction. +- Branch: Created `task_20260804_target-safety-prescreen` from latest `origin/main`. +- Change: Added `genmodules/target_safety_therapeutic_window_prescreen/` with data-free contracts, six-axis evidence ontology, fatal-first deterministic evaluator, module manifest, and README. +- Change: Added five regression tests covering fatal precedence, unknown/conflicting HOLD, conditional GO, empty-evidence HOLD, and rejection of non-external references. +- Boundary: Added no source data, database, cache, results, model weights, or runtime artifacts. Runtime location is declared under `${BIOWORKSPACE_ROOT}/DATA/target_safety_therapeutic_window_prescreen/`. +- Bug found and fixed: Initial empty-evidence evaluation incorrectly returned `GO`; the rule was corrected so empty evidence returns `HOLD` and requests all six next experiments. +- Validation: module tests passed; full suite passed with 212 tests; repository boundary check passed; `git diff --check` passed; no `__pycache__` remains. +- Next: update the PR with the implementation, push the branch, create the PR, and submit the complete PR review request to ChatGPT before any merge. diff --git a/tests/test_target_safety_therapeutic_window_prescreen.py b/tests/test_target_safety_therapeutic_window_prescreen.py new file mode 100644 index 0000000..fe92bbe --- /dev/null +++ b/tests/test_target_safety_therapeutic_window_prescreen.py @@ -0,0 +1,121 @@ +import unittest + +from genmodules.target_safety_therapeutic_window_prescreen import ( + AssessmentRequest, + Criticality, + Decision, + EvidenceAxis, + EvidenceClaim, + EvidenceLevel, + RiskDirection, + TargetProfile, + assess_target, +) + + +def claim(**overrides): + values = { + "claim_ref": "external:claim/1", + "axis": EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + "level": EvidenceLevel.C, + "direction": RiskDirection.SUPPORTS_SAFETY, + "source_ref": "external:source/1", + "rationale_ref": "external:rationale/1", + } + values.update(overrides) + return EvidenceClaim(**values) + + +def request(claims): + return AssessmentRequest( + request_ref="external:request/1", + target=TargetProfile(target_ref="external:target/1", gene_symbol="GUCY2C"), + evidence_refs=tuple(item.claim_ref for item in claims), + claims=tuple(claims), + policy_ref="external:policy/target-safety-v0.1", + run_context_ref="external:run/context-1", + ) + + +class TargetSafetyPreScreenTests(unittest.TestCase): + def test_fatal_critical_surface_hazard_wins(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/critical-surface", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, + surface_exposed=True, + ), + claim( + claim_ref="external:claim/unknown-density", + axis=EvidenceAxis.ANTIGEN_DENSITY, + level=EvidenceLevel.U, + direction=RiskDirection.UNKNOWN, + unresolved=True, + ), + ] + ) + ) + self.assertEqual(result.decision, Decision.KILL) + self.assertEqual(result.fatal_flags[0].value, "critical_surface_hazard") + self.assertIn("external:claim/unknown-density", result.unresolved_refs) + + def test_unknown_or_conflicting_critical_evidence_holds(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/critical-conflict", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.D, + direction=RiskDirection.CONFLICTING, + criticality=Criticality.CRITICAL_REVERSIBLE, + ) + ] + ) + ) + self.assertEqual(result.decision, Decision.HOLD) + self.assertIn("external:claim/critical-conflict", result.conflict_refs) + + def test_plausible_differential_is_conditional_go(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/surface-differential", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_SAFETY, + surface_exposed=False, + ), + claim( + claim_ref="external:claim/density-differential", + axis=EvidenceAxis.ANTIGEN_DENSITY, + level=EvidenceLevel.C, + direction=RiskDirection.SUPPORTS_SAFETY, + normal_density_relation="lower", + ), + ] + ) + ) + self.assertEqual(result.decision, Decision.CONDITIONAL_GO) + self.assertEqual(result.confidence, "medium") + self.assertTrue(result.mitigation_refs) + + def test_empty_evidence_is_hold_and_requests_next_experiments(self): + result = assess_target(request([])) + self.assertEqual(result.decision, Decision.HOLD) + self.assertEqual(len(result.axis_summaries), 6) + self.assertEqual(len(result.next_experiment_refs), 6) + + def test_contract_rejects_non_external_evidence(self): + with self.assertRaises(ValueError): + claim(source_ref="local:source/1") + + +if __name__ == "__main__": + unittest.main() From d36e4a4f93655b2bcde91b4c78c935ff6eb3b805 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 17:12:47 -0400 Subject: [PATCH 2/5] task_20260804: close target safety prescreen review blockers --- ...fety-therapeutic-window-prescreen.zh-CN.md | 9 +- genmodules/README.md | 2 +- .../README.md | 5 + .../__init__.py | 2 + .../contracts.py | 21 ++- .../engine.py | 62 +++++-- .../module.yaml | 6 +- logs/worklog.md | 56 +++---- ...get_safety_therapeutic_window_prescreen.py | 158 ++++++++++++++++-- 9 files changed, 262 insertions(+), 59 deletions(-) diff --git a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md index ee3b031..f07a702 100644 --- a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md +++ b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md @@ -4,7 +4,7 @@ - Branch: `task_20260804_target-safety-prescreen` - Base: latest `origin/main` at task start -- Review: implementation complete; PR and ChatGPT review are required before merge +- Review: Round 1 returned `REQUEST_CHANGES`; remediation is in progress on a clean replacement PR - Data boundary: no source data, cache, result, model weight, or runtime output in the repository ## Scope @@ -19,6 +19,8 @@ It does not claim product-specific therapeutic-window prediction. - Six evidence axes: normal tissue expression, surface accessibility, antigen density, soluble antigen/shedding/sink, existing modality toxicity, and tissue consequence/recoverability. - Evidence levels `A/B/C/D/U` and explicit risk directions. - Fatal-first rules for critical surface hazard, confirmed severe on-target toxicity, non-lower normal density, clinically demonstrated sink/exposure failure, and no exploitable differential. +- Non-fatal material risk is explicitly retained and produces `HOLD`; `GO` requires all six axes to be resolved with no material risk or conflict. +- Unknown direction auto-propagates to unresolved; surface, tissue criticality, and density evidence are aggregated across claims; differential status is structured rather than tag-driven. - Decision semantics: `KILL`, `HOLD`, `CONDITIONAL_GO`, `GO`. - Unknown, unresolved, and conflicting claims remain visible and produce next-experiment references. - All cross-boundary identities and evidence references require `external:` references. @@ -37,3 +39,8 @@ It does not claim product-specific therapeutic-window prediction. - Evidence retrieval, source normalization, citation resolution, scoring calibration, and persistence remain external runtime responsibilities. - The first ruleset is deterministic and conservative; it is not a clinical safety model and must not be used as a product-level therapeutic-window claim. - The next implementation phase should add an external runtime adapter and benchmark fixtures under `DATA`, only after this contract PR is reviewed. + +## Round 1 remediation + +- Rebuilt the PR from `origin/main` so the CRC clinical-frame commit is not in scope. +- Bumped the module and contract version to `0.2.0` for the new structured differential and material-risk output fields. diff --git a/genmodules/README.md b/genmodules/README.md index 5d04ba6..cf67195 100644 --- a/genmodules/README.md +++ b/genmodules/README.md @@ -21,7 +21,7 @@ are not lifecycle stages or Gate implementations. - `gen_indication_endpoint_target@0.1.0`: defines data-free contracts for constrained ADC indication, endpoint, and target opportunity generation; generation, evaluation, ranking, and evidence remain external. -- `target_safety_therapeutic_window_prescreen@0.1.0`: applies conservative, +- `target_safety_therapeutic_window_prescreen@0.2.0`: applies conservative, fatal-first rules to externally supplied public-evidence claims for target- intrinsic ADC safety pre-screening; it does not predict a product-specific therapeutic window. diff --git a/genmodules/target_safety_therapeutic_window_prescreen/README.md b/genmodules/target_safety_therapeutic_window_prescreen/README.md index a555b30..9a8f39f 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/README.md +++ b/genmodules/target_safety_therapeutic_window_prescreen/README.md @@ -18,6 +18,11 @@ Evidence levels are `A` (human causal), `B` (human protein/cell-resolved), `C` (multi-omic concordance), `D` (single or indirect), and `U` (unknown). Unknown remains unresolved; it is never converted into safety. +Material risk claims that do not meet a fatal threshold produce `HOLD`, not +`GO`. A target reaches `GO` only when all six axes are represented and resolved +with no material risk or conflict. Differential status is a structured field; +free-form tags cannot create a fatal flag. + ## Decision semantics The evaluator applies fatal flags first: diff --git a/genmodules/target_safety_therapeutic_window_prescreen/__init__.py b/genmodules/target_safety_therapeutic_window_prescreen/__init__.py index 670f67e..e4175d1 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/__init__.py +++ b/genmodules/target_safety_therapeutic_window_prescreen/__init__.py @@ -5,6 +5,7 @@ AssessmentResult, Criticality, Decision, + DifferentialStatus, EvidenceAxis, EvidenceClaim, EvidenceLevel, @@ -19,6 +20,7 @@ "AssessmentResult", "Criticality", "Decision", + "DifferentialStatus", "EvidenceAxis", "EvidenceClaim", "EvidenceLevel", diff --git a/genmodules/target_safety_therapeutic_window_prescreen/contracts.py b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py index 6557b56..32271ce 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/contracts.py +++ b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py @@ -7,14 +7,14 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import StrEnum import re from typing import Final -MODULE_VERSION: Final = "0.1.0" -CONTRACT_VERSION: Final = "0.1.0" +MODULE_VERSION: Final = "0.2.0" +CONTRACT_VERSION: Final = "0.2.0" _EXTERNAL_REF = re.compile(r"^external:[^\s]+$") _GENE_SYMBOL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*$") @@ -48,6 +48,13 @@ class RiskDirection(StrEnum): UNKNOWN = "unknown" +class DifferentialStatus(StrEnum): + PRESENT = "present" + ABSENT = "absent" + UNKNOWN = "unknown" + NOT_ASSESSED = "not_assessed" + + class Criticality(StrEnum): NON_CRITICAL = "non_critical" REGENERATIVE = "regenerative" @@ -110,6 +117,7 @@ class EvidenceClaim: criticality: Criticality = Criticality.UNKNOWN surface_exposed: bool | None = None normal_density_relation: str | None = None + differential_status: DifferentialStatus = DifferentialStatus.NOT_ASSESSED toxicity_attribution: str | None = None severe: bool = False clinically_demonstrated: bool = False @@ -138,6 +146,10 @@ def __post_init__(self) -> None: "unresolved", }: raise ValueError("toxicity_attribution is invalid") + if not isinstance(self.differential_status, DifferentialStatus): + raise ValueError("differential_status is invalid") + if self.direction == RiskDirection.UNKNOWN and not self.unresolved: + object.__setattr__(self, "unresolved", True) @dataclass(frozen=True) @@ -184,6 +196,7 @@ class AssessmentResult: fatal_flags: tuple[FatalFlag, ...] unresolved_refs: tuple[str, ...] conflict_refs: tuple[str, ...] + material_risk_refs: tuple[str, ...] mitigation_refs: tuple[str, ...] next_experiment_refs: tuple[str, ...] decision: Decision @@ -199,10 +212,10 @@ def __post_init__(self) -> None: for ref in ( *self.unresolved_refs, *self.conflict_refs, + *self.material_risk_refs, *self.mitigation_refs, *self.next_experiment_refs, ): _external(ref, "result reference") if self.confidence not in {"high", "medium", "low"}: raise ValueError("confidence must be high, medium, or low") - diff --git a/genmodules/target_safety_therapeutic_window_prescreen/engine.py b/genmodules/target_safety_therapeutic_window_prescreen/engine.py index 82c0a25..ebd6edb 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/engine.py +++ b/genmodules/target_safety_therapeutic_window_prescreen/engine.py @@ -14,6 +14,7 @@ AxisSummary, Criticality, Decision, + DifferentialStatus, EvidenceAxis, EvidenceClaim, EvidenceLevel, @@ -82,15 +83,33 @@ def _axis_summaries(claims: tuple[EvidenceClaim, ...]) -> tuple[AxisSummary, ... def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: flags: list[FatalFlag] = [] - if any( + high_quality_surface_risk = any( claim.axis == EvidenceAxis.SURFACE_ACCESSIBILITY and claim.surface_exposed is True - and claim.criticality == Criticality.CRITICAL_NON_REGENERATIVE and claim.level in {EvidenceLevel.A, EvidenceLevel.B} and claim.direction == RiskDirection.SUPPORTS_RISK for claim in claims + ) + high_quality_critical_tissue_risk = any( + claim.criticality + in {Criticality.CRITICAL_NON_REGENERATIVE, Criticality.CRITICAL_REVERSIBLE} + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in claims + ) + if any( + ( + claim.axis == EvidenceAxis.SURFACE_ACCESSIBILITY + and claim.surface_exposed is True + and claim.criticality == Criticality.CRITICAL_NON_REGENERATIVE + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + ) + for claim in claims ): flags.append(FatalFlag.CRITICAL_SURFACE_HAZARD) + elif high_quality_surface_risk and high_quality_critical_tissue_risk: + flags.append(FatalFlag.CRITICAL_SURFACE_HAZARD) if any( claim.axis == EvidenceAxis.EXISTING_MODALITY_TOXICITY and claim.severe @@ -100,13 +119,14 @@ def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: for claim in claims ): flags.append(FatalFlag.CONFIRMED_ON_TARGET_TOXICITY) - if any( + high_quality_density_risk = any( claim.axis == EvidenceAxis.ANTIGEN_DENSITY and claim.normal_density_relation in {"similar", "higher"} and claim.level in {EvidenceLevel.A, EvidenceLevel.B} and claim.direction == RiskDirection.SUPPORTS_RISK for claim in claims - ): + ) + if high_quality_density_risk and high_quality_surface_risk and high_quality_critical_tissue_risk: flags.append(FatalFlag.NORMAL_DENSITY_NOT_LOWER) if any( claim.axis == EvidenceAxis.SOLUBLE_SINK @@ -119,7 +139,7 @@ def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: flags.append(FatalFlag.CLINICAL_SINK_EXPOSURE_FAILURE) if any( claim.axis == EvidenceAxis.NORMAL_TISSUE_EXPRESSION - and "widespread_no_differential" in claim.tags + and claim.differential_status == DifferentialStatus.ABSENT and claim.level in {EvidenceLevel.A, EvidenceLevel.B} and claim.direction == RiskDirection.SUPPORTS_RISK for claim in claims @@ -136,7 +156,9 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: unresolved_refs = tuple( claim.claim_ref for claim in request.claims - if claim.unresolved or claim.level == EvidenceLevel.U + if claim.unresolved + or claim.level == EvidenceLevel.U + or claim.direction == RiskDirection.UNKNOWN ) conflict_refs = tuple( claim.claim_ref @@ -153,8 +175,15 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: ) for claim in request.claims ) + material_risk_refs = tuple( + claim.claim_ref + for claim in request.claims + if claim.direction == RiskDirection.SUPPORTS_RISK + ) + incomplete_axes = any(summary.unresolved for summary in summaries) has_plausible_differential = any( - claim.direction == RiskDirection.SUPPORTS_SAFETY + claim.differential_status == DifferentialStatus.PRESENT + and claim.direction == RiskDirection.SUPPORTS_SAFETY and claim.axis in { EvidenceAxis.SURFACE_ACCESSIBILITY, @@ -166,10 +195,20 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: ) if fatal_flags: decision = Decision.KILL - elif not request.claims or critical_unknown or conflict_refs or unresolved_refs: + elif ( + not request.claims + or critical_unknown + or conflict_refs + or unresolved_refs + or incomplete_axes + ): decision = Decision.HOLD - elif has_plausible_differential: - decision = Decision.CONDITIONAL_GO + elif material_risk_refs: + decision = ( + Decision.CONDITIONAL_GO + if has_plausible_differential + else Decision.HOLD + ) else: decision = Decision.GO @@ -186,13 +225,14 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: if decision == Decision.CONDITIONAL_GO and confidence == "high": confidence = "medium" return AssessmentResult( - contract_version="0.1.0", + contract_version="0.2.0", request_ref=request.request_ref, target_ref=request.target.target_ref, axis_summaries=summaries, fatal_flags=fatal_flags, unresolved_refs=unresolved_refs, conflict_refs=conflict_refs, + material_risk_refs=material_risk_refs, mitigation_refs=mitigation_refs, next_experiment_refs=next_experiments, decision=decision, diff --git a/genmodules/target_safety_therapeutic_window_prescreen/module.yaml b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml index e69ac9e..5dc386f 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/module.yaml +++ b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml @@ -1,10 +1,10 @@ module: module_id: target_safety_therapeutic_window_prescreen - module_version: 0.1.0 + module_version: 0.2.0 module_type: GenModule name: Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine - input_contract: TargetSafetyAssessmentRequest@0.1.0 - output_contract: TargetSafetyAssessmentResult@0.1.0 + input_contract: TargetSafetyAssessmentRequest@0.2.0 + output_contract: TargetSafetyAssessmentResult@0.2.0 execution_policy: external_input_and_output_only persistence: forbidden database: forbidden diff --git a/logs/worklog.md b/logs/worklog.md index be024ed..6e230e8 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2246,32 +2246,30 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Validation: 207 tests 全部通过(Round 1 时为 192,本分支已并入 PR #50 的 15 项边界测试);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;`git diff -- prompts/GPT-Feedback.md` 为空。 - Next: 推送同一 PR #51 并提交 ChatGPT 复审。本 PR 现在也受 CI 覆盖。 -## 2026-08-04T15:10:53-04:00 — CRC 临床框架与膜蛋白靶点筛选(外部 run 留痕) - -- Instruction: 人类负责人指示架构冻结生效(此后一个月最多修复一次积累的架构问题),并**开始做内容**。第一项内容任务四步:列出 CRC 所有 clinical unmet needs、列举最合适的临床收益、大致决定临床终点、开始筛选潜在膜蛋白 ADC 靶点。 -- Governance conflict raised before acting: 两项实质冲突。第 24 行禁止在当前 PR 获 `APPROVE` 前开始下一项工作,而 PR #52 当时(现在仍)`OPEN` 未批准;第 23 行把「外部数据运行」列入必须通过 PR 交付并送审的范围,而本次 run 无授权 PR。已就两条明确提出,人类负责人以「现在开始做内容」直接指示继续。 -- Action: 执行外部 run `gen_iet_crc_clinical_frame_and_membrane_target_screen_20260804T191053Z`,产物全部位于仓库之外 `DATA/2.PROJECTS/Stelligen-ADCdev-OS/result/` 之下;已验证仓库工作树未被触碰。 -- Recorded not concealed: run `status` = `draft_pending_repo_review`;`source_manifest.json` 的 `authorising_pr` = `null` 且 `authorisation_status` 全文记录上述两条冲突;`run_report.md` 与外部 worklog 各设一节陈述同一事实。**执行者不自行认定该 run 已获授权,也不因产物已存在而主张既成事实**;若审核裁决为不可追认,产物应作废重跑而非事后补授权。 -- Contract conformance: 产出前读取 `genmodules/gen_indication_endpoint_target/contracts.py` 与 `README.md`,使产物贴合 v5 `ClinicalHypothesis`、`clinical-problem-first` entry mode 与六级 lock。已核对 `dcc94a7` 与 run 执行时的 tip `bfc04be` 之间 `src`/`genmodules`/`tests`/`extensions` 差异为 0 个文件,契约完全一致。 -- Gate boundary: 未运行任何 Gate,未赋任何 Gate score;`RETAIN`/`DEFER`/`EXCLUDE` 属 `CandidateFilterResult` 语义,按契约明确不是 Gate 结果;`NOT_EVALUATED` 与 `UNRESOLVED` 全程保留未降级为 PASS;T0-T12 未执行,45-Gate 拓扑未触碰。 -- Finding on scope: 第 1-3 步此前已由 `gen_iet_crc_target_enumeration_20260802`(PR #28 授权)大体做过(9 场景/36 endpoint/41 靶点/1476 pair),**第 4 步完全没做**——41 行全部 `gate_score_status = not_scored_in_enumeration_run`、`gate_pass_status = not_assessed`。该产物是枚举而非筛选,枚举与推荐之间缺少筛选一步,推荐实际依托 KB consensus 文档而非 catalog。因此本次继承并扩展,把筛选作为实质内容。 -- Change: 20 个 unmet need 场景(继承 9 + 新增 11,按 setting/分子/线数/解剖腔室/转录状态/宿主耐受/组织学显式坐标轴枚举);7 类临床收益排序,选定 BEN-1「难治 MSS 的持久客观缩瘤,确证阶段转 OS」;12 条终点并给出量化门槛(Ph1b/2 单臂 ORR ≥20% 且 95%CI 下界 >10%,DoR 中位 ≥6 个月,Ph3 OS HR ≤0.75);45 个靶点经 4 道硬门筛选,得 4 RETAIN/25 DEFER/16 EXCLUDE,Tier A 为 GUCY2C、CDH17、GPA33、LY6G6D。 -- Finding on prior artefacts: 三项。GPA33 与 LY6G6D 为真实覆盖缺口,其中 LY6G6D 特异富集于 MSS——枚举漏掉了与自身所选战略最匹配的候选;TNFRSF12A 为内部矛盾(`indication_endpoint_universe.tsv` 引用其 watch 文件而 catalog 从未收录,属两表未对账的流水线缺陷,非科学判断);catalog 含相当比例泛 ADC benchmark 行(CLDN18 胃、PRLR 乳腺、IL2RA 淋巴、MELTF 黑色素瘤、FOLR1 卵巢、LAMP1 溶酶体、RNF43 胞内、SLC3A2 近乎普遍、CA19-9 非蛋白),CRC 特异候选池从来小于 41。 -- Recorded for the long term: 一条不由任何单步推出的结论——ABBV-400(c-MET)与 M9140(CEACAM5)这两个推进最快的 CRC ADC 载荷均为 Top1i,2026 年立项将在 Top1i 暴露人群中读数据,故**载荷不应默认 Top1i**;通行的 deruxtecan 类默认会把无关靶点变成交叉耐药负债,差异化决策在载荷不在靶点。已作为 UN-20 与 GAP-14 记录。同一条重构 GUCY2C:indusatumab vedotin 败在疗效而非毒性,正确反应是先测递送能力再据以选载荷。此结论与 KB consensus 将 GUCY2C 列为首选存在张力,已在报告中明确写出而非抹平。 -- Limits stated, not implied: 本次 run **没有任何一条论断被原始来源验证过**,所有百分比与基准在 TSV 中标为 `unverified_domain_prior` 或 `derived_not_calibrated`,属模型领域知识而非抽取证据,足以支撑排序与定框但不足以作为决策记录;四个 Tier A 的 `h2_mss_crc_protein_expression` 全为 `UNRESOLVED`,即在未测量的属性上排序;unmet need 分数未校准(参考数据集中 CRC 仅 1 行);继承而未关闭的缺口为 41 靶点仅 6 行 opposing evidence、292 行证据中 172 行 unknown、20 个 review batch 仅审 2 个;未执行 skeptic review,故 Tier A 四行是待审候选而非推荐。 -- Boundary: 本 PR 只提交本条 worklog 与一份 handoff,无任何代码、契约、Gate 拓扑、Model、Profile、生命周期、核心对象或测试变更;未改动 `AGENTS.md`、`ChatGPT-Codex-talk.md`、Phase Gate 协议、`prompts/GPT-Feedback.md`;未改写任何历史条目;未把数据、结果或运行产物加入仓库;未回写覆盖 08-02 run 的既有产物(对其发现以外部新文件 `coverage_gaps_vs_prior_run.tsv` 记录)。 -- Validation: 207 tests 全部通过(与 `main` 相同);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。外部产物四份 TSV 列数 16/9/11/19 各自一致,筛选表 45 行 = 41 + 4,`source_manifest.json` 为合法 JSON。 -- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**。该豁免只覆盖 `prompts/GPT-Feedback.md`,本 PR 提交的是 handoff 与 worklog,落在允许集合之外,须经 ChatGPT `APPROVE`。 -- Next: 推送并创建 PR 供 ChatGPT 审核,重点为该外部 run 的授权裁决——可追认为已授权,或必须作废重跑。 - -### 2026-08-04 19:45 EDT - -- Instruction: Read `Zhixins-KB/2.Biotech/Asset-Generation-OS-architecture.md#Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine` and independently implement the module in `GenModule`, with runtime data/results outside the repository and PR review required. -- Context: Re-read workspace rules, current `origin/main`, GenModule registry, existing contract patterns, and the full KB section. Confirmed the module must perform target-intrinsic public-evidence pre-screening, not product-specific therapeutic-window prediction. -- Branch: Created `task_20260804_target-safety-prescreen` from latest `origin/main`. -- Change: Added `genmodules/target_safety_therapeutic_window_prescreen/` with data-free contracts, six-axis evidence ontology, fatal-first deterministic evaluator, module manifest, and README. -- Change: Added five regression tests covering fatal precedence, unknown/conflicting HOLD, conditional GO, empty-evidence HOLD, and rejection of non-external references. -- Boundary: Added no source data, database, cache, results, model weights, or runtime artifacts. Runtime location is declared under `${BIOWORKSPACE_ROOT}/DATA/target_safety_therapeutic_window_prescreen/`. -- Bug found and fixed: Initial empty-evidence evaluation incorrectly returned `GO`; the rule was corrected so empty evidence returns `HOLD` and requests all six next experiments. -- Validation: module tests passed; full suite passed with 212 tests; repository boundary check passed; `git diff --check` passed; no `__pycache__` remains. -- Next: update the PR with the implementation, push the branch, create the PR, and submit the complete PR review request to ChatGPT before any merge. +### 2026-08-04 19:30 EDT + +- Action: 合并 PR #51(豁免收窄写入 `AGENTS.md`)。ChatGPT 在 head `e9eced8` 返回 Round 3 `APPROVE`,CI run #5 在 3.11/3.12 全部检查通过、207 tests。用 merge commit 合并为 `dcc94a7`,未用 squash。 +- Result: `prompts/GPT-Feedback.md` 的反馈更新自此可直接提交(配 worklog 一条 + handoff 一份),无需送审。该规则写在 `AGENTS.md` 第 31 行「审核豁免」一节,并由第 23 行门禁、`ChatGPT-Codex-talk.md:19`、Phase Gate 协议 `:13` 三处指针指向,对未来会话可发现。 +- Validation on main: 207 tests 全部通过;`scripts/verify_repository_boundary.sh` 通过;`tests/test_git_sync.sh` A-D 通过;`git diff --check` 通过。 +- Action: 建立 `task_20260804_pr50-51-approval-records`(从 `main` `dcc94a7` 创建),补写 #50 与 #51 的批准记录。 +- Precondition: 两个 PR 已合并(#50 head `076c5ff` → merge `927aebf`;#51 head `e9eced8` → merge `dcc94a7`),但仓库内无对应 `-final.md`,审计轨迹停在 `REQUEST_CHANGES`,与 #46 修复过的断层同类,本次两处。 +- Rationale for post-merge: 未在合并前写入各自分支,因为追加提交会改变刚获批准的 HEAD。沿用 #46 建立并获批准的「合并后独立 PR 补写」模式。 +- Change: 新增两份 `-final.md`。各轮结论标注 `verbatim as relayed by the human lead` 并逐字转载,遵循 #46 阻断 2 立下的做法——凡逐字者明确标注,凡不可得者不伪造。两份均含完整审核轮次、阻断项、根因、修订方式与「本批准不授权什么」一节。 +- Recorded for the long term: #50 是仓库第一次拥有独立于自身审计记录的测试证据。自 PR #15 起每轮审核附带的「GitHub 上没有与该 head 关联的 Actions run」这一条件到此终止,跨越 36 个 PR,值得单独留档。 +- Recorded for the long term: #51 两轮阻断是同一类错误的两种形态——Round 1「只能改一个文件」与「必须写 worklog 与 handoff」冲突,按字面无法执行;Round 2 改为封闭集合后按文件标注「是否豁免」,而审核以整个 PR 为单位,规则把自己否掉。根因是把「审核豁免」(属整个 PR)与「留痕要求」(属单个文件)两条轴挤进同一列,非措辞问题。 +- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**。该豁免只覆盖 `prompts/GPT-Feedback.md`,本 PR 提交的是 `logs/chatgpt-review-*.md` 与 handoff,落在允许集合之外,须经 ChatGPT `APPROVE`。 +- Boundary: 无任何代码、契约、Gate 拓扑、Model、Profile、生命周期、核心对象或测试变更;未改动 `AGENTS.md` 与两份治理文本(#51 已定稿,本 PR 只记录其批准事实);未改动 `prompts/GPT-Feedback.md`;未改写 Round 1/Round 2 既有记录及任何历史条目;**未追认 #49**(该 PR 在过宽表述下合并,#51 的记录如实写明此事,本 PR 不改变其状态);未新增数据、缓存、结果或运行产物。 +- Validation: 207 tests 全部通过(与 `main` 相同);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 +- Next: 推送并创建 PR 供 ChatGPT 审核。合并后 2026-08-04 全部六个 PR(#46..#51)的审计闭环完成。 + +### 2026-08-04 21:20 EDT + +- Instruction: Apply ChatGPT Round 1 `REQUEST_CHANGES` feedback for the target-safety prescreen PR. +- Scope correction: Rebuilt the implementation from `origin/main` in a clean worktree; the CRC clinical-frame handoff is not included in the replacement PR. +- Change: Non-fatal material risk now produces `HOLD`; `GO` requires all six axes resolved, no material risk, and no conflict. `CONDITIONAL_GO` requires material risk plus an explicit structured exploitable differential. +- Change: `direction=UNKNOWN` automatically propagates to `unresolved`; unresolved and incomplete axes force `HOLD`. +- Change: Fatal evaluation aggregates high-quality surface risk with critical-tissue risk across claims. Density fatality now requires density, accessible surface, and critical tissue evidence together. +- Change: Replaced free-form `widespread_no_differential` tag logic with structured `DifferentialStatus`; added `material_risk_refs` to the result and bumped module/contracts to `0.2.0`. +- Tests: Added regression coverage for non-fatal risk, unknown direction, cross-claim aggregation, density false-positive prevention, GO completeness, free-tag rejection, and structured differential fatality. +- Validation: 12 module tests and 219 full-suite tests passed; repository boundary check passed; `git diff --check` passed; no `__pycache__` remains. +- Next: commit and push the clean replacement branch, create a replacement PR, and submit the remediation for ChatGPT review. diff --git a/tests/test_target_safety_therapeutic_window_prescreen.py b/tests/test_target_safety_therapeutic_window_prescreen.py index fe92bbe..caf627c 100644 --- a/tests/test_target_safety_therapeutic_window_prescreen.py +++ b/tests/test_target_safety_therapeutic_window_prescreen.py @@ -4,6 +4,7 @@ AssessmentRequest, Criticality, Decision, + DifferentialStatus, EvidenceAxis, EvidenceClaim, EvidenceLevel, @@ -37,6 +38,18 @@ def request(claims): ) +def complete_resolved_claims(): + return [ + claim( + claim_ref=f"external:claim/complete-{index}", + axis=axis, + level=EvidenceLevel.C, + direction=RiskDirection.SUPPORTS_SAFETY, + ) + for index, axis in enumerate(EvidenceAxis, start=1) + ] + + class TargetSafetyPreScreenTests(unittest.TestCase): def test_fatal_critical_surface_hazard_wins(self): result = assess_target( @@ -82,29 +95,154 @@ def test_unknown_or_conflicting_critical_evidence_holds(self): self.assertIn("external:claim/critical-conflict", result.conflict_refs) def test_plausible_differential_is_conditional_go(self): + claims = complete_resolved_claims() + claims[1] = claim( + claim_ref="external:claim/surface-differential", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_SAFETY, + surface_exposed=False, + differential_status=DifferentialStatus.PRESENT, + ) + claims[2] = claim( + claim_ref="external:claim/density-differential", + axis=EvidenceAxis.ANTIGEN_DENSITY, + level=EvidenceLevel.C, + direction=RiskDirection.SUPPORTS_SAFETY, + normal_density_relation="lower", + differential_status=DifferentialStatus.PRESENT, + ) + claims.append( + claim( + claim_ref="external:claim/conditional-material-risk", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.REGENERATIVE, + ) + ) + result = assess_target( + request(claims) + ) + self.assertEqual(result.decision, Decision.CONDITIONAL_GO) + self.assertEqual(result.confidence, "medium") + self.assertTrue(result.mitigation_refs) + + def test_go_requires_all_axes_resolved_and_no_material_risk(self): + result = assess_target(request(complete_resolved_claims())) + self.assertEqual(result.decision, Decision.GO) + self.assertEqual(result.material_risk_refs, ()) + + def test_nonfatal_material_risk_is_hold_not_go(self): + claims = complete_resolved_claims() + claims.append( + claim( + claim_ref="external:claim/nonfatal-risk", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.REGENERATIVE, + ) + ) + result = assess_target(request(claims)) + self.assertEqual(result.decision, Decision.HOLD) + self.assertIn("external:claim/nonfatal-risk", result.material_risk_refs) + + def test_unknown_direction_is_unresolved_and_holds(self): + claims = complete_resolved_claims() + claims[0] = claim( + claim_ref="external:claim/unknown-direction", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.UNKNOWN, + ) + result = assess_target(request(claims)) + self.assertEqual(result.decision, Decision.HOLD) + self.assertIn("external:claim/unknown-direction", result.unresolved_refs) + + def test_surface_and_critical_tissue_claims_aggregate_to_fatal(self): result = assess_target( request( [ claim( - claim_ref="external:claim/surface-differential", + claim_ref="external:claim/surface-risk", axis=EvidenceAxis.SURFACE_ACCESSIBILITY, level=EvidenceLevel.B, - direction=RiskDirection.SUPPORTS_SAFETY, - surface_exposed=False, + direction=RiskDirection.SUPPORTS_RISK, + surface_exposed=True, ), claim( - claim_ref="external:claim/density-differential", + claim_ref="external:claim/critical-tissue-risk", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, + ), + ] + ) + ) + self.assertIn("critical_surface_hazard", [flag.value for flag in result.fatal_flags]) + + def test_density_alone_does_not_kill_when_surface_is_inaccessible(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/density-risk", axis=EvidenceAxis.ANTIGEN_DENSITY, - level=EvidenceLevel.C, - direction=RiskDirection.SUPPORTS_SAFETY, - normal_density_relation="lower", + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + normal_density_relation="higher", + ), + claim( + claim_ref="external:claim/inaccessible-surface", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + surface_exposed=False, + ), + claim( + claim_ref="external:claim/critical-tissue", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, ), ] ) ) - self.assertEqual(result.decision, Decision.CONDITIONAL_GO) - self.assertEqual(result.confidence, "medium") - self.assertTrue(result.mitigation_refs) + self.assertNotIn("normal_density_not_lower_than_tumor", [flag.value for flag in result.fatal_flags]) + self.assertEqual(result.decision, Decision.HOLD) + + def test_free_tag_cannot_create_no_differential_fatal(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/free-tag", + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + tags=("widespread_no_differential",), + ) + ] + ) + ) + self.assertNotIn("no_exploitable_target_differential", [flag.value for flag in result.fatal_flags]) + + def test_structured_absent_differential_can_create_fatal(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/structured-no-differential", + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + differential_status=DifferentialStatus.ABSENT, + ) + ] + ) + ) + self.assertIn("no_exploitable_target_differential", [flag.value for flag in result.fatal_flags]) def test_empty_evidence_is_hold_and_requests_next_experiments(self): result = assess_target(request([])) From 6a280781b6cd1a7a710b96f5a76cde5995f78ec2 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 17:13:33 -0400 Subject: [PATCH 3/5] task_20260804: record clean replacement PR --- ...04-target-safety-therapeutic-window-prescreen.zh-CN.md | 3 ++- logs/worklog.md | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md index f07a702..0dc0df2 100644 --- a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md +++ b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md @@ -4,7 +4,7 @@ - Branch: `task_20260804_target-safety-prescreen` - Base: latest `origin/main` at task start -- Review: Round 1 returned `REQUEST_CHANGES`; remediation is in progress on a clean replacement PR +- Review: Round 1 returned `REQUEST_CHANGES`; remediation is in clean replacement PR #56 - Data boundary: no source data, cache, result, model weight, or runtime output in the repository ## Scope @@ -44,3 +44,4 @@ It does not claim product-specific therapeutic-window prediction. - Rebuilt the PR from `origin/main` so the CRC clinical-frame commit is not in scope. - Bumped the module and contract version to `0.2.0` for the new structured differential and material-risk output fields. +- Replacement PR: `https://github.com/leezx/StelligenOS/pull/56`, head `d36e4a4`. diff --git a/logs/worklog.md b/logs/worklog.md index 6e230e8..53f655c 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2273,3 +2273,11 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Tests: Added regression coverage for non-fatal risk, unknown direction, cross-claim aggregation, density false-positive prevention, GO completeness, free-tag rejection, and structured differential fatality. - Validation: 12 module tests and 219 full-suite tests passed; repository boundary check passed; `git diff --check` passed; no `__pycache__` remains. - Next: commit and push the clean replacement branch, create a replacement PR, and submit the remediation for ChatGPT review. + +### 2026-08-04 21:35 EDT + +- Action: Pushed the clean remediation branch `task_20260804_target-safety-prescreen-fix` and created replacement PR #56: `https://github.com/leezx/StelligenOS/pull/56`. +- Scope verification: final PR diff is based on `origin/main` and contains only the target-safety module, its tests, GenModule registry/docs, handoff, and worklog; no CRC clinical-frame handoff is present. +- Head: `d36e4a4`. +- Boundary: No data, cache, result, model weight, or runtime artifact was added. Original PR #55 was not force-rewritten. +- Next: submit PR #56 remediation to ChatGPT manually for another review cycle. From 402cd19e765fb2203a62f1590d572782c81a1523 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 17:22:40 -0400 Subject: [PATCH 4/5] task_20260804: align fatal evidence contexts --- ...fety-therapeutic-window-prescreen.zh-CN.md | 9 +- genmodules/README.md | 2 +- .../README.md | 5 +- .../contracts.py | 21 +++- .../engine.py | 95 ++++++++++----- .../module.yaml | 6 +- logs/worklog.md | 11 ++ ...get_safety_therapeutic_window_prescreen.py | 114 ++++++++++++++++++ 8 files changed, 227 insertions(+), 36 deletions(-) diff --git a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md index 0dc0df2..1c3f35b 100644 --- a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md +++ b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md @@ -43,5 +43,12 @@ It does not claim product-specific therapeutic-window prediction. ## Round 1 remediation - Rebuilt the PR from `origin/main` so the CRC clinical-frame commit is not in scope. -- Bumped the module and contract version to `0.2.0` for the new structured differential and material-risk output fields. +- Bumped the module and contract version to `0.3.0` for the structured differential, material-risk output, and hazard-context fields. - Replacement PR: `https://github.com/leezx/StelligenOS/pull/56`, head `d36e4a4`. + +## Round 2 remediation + +- Fatal aggregation is now context-aligned by shared `hazard_context_ref` or `(tissue, cell_type)`; unscoped claims cannot combine into a fatal decision. +- `CONDITIONAL_GO` now requires every material-risk claim to be covered by a context-matched differential or explicit `mitigates_claim_refs`. +- `NO_EXPLOITABLE_DIFFERENTIAL` requires an external comprehensive assessment reference; a single observation is downgraded to unresolved. +- Added cross-context, unrelated-differential, partial-coverage, and unscoped-fatal regression tests. diff --git a/genmodules/README.md b/genmodules/README.md index cf67195..af21bb3 100644 --- a/genmodules/README.md +++ b/genmodules/README.md @@ -21,7 +21,7 @@ are not lifecycle stages or Gate implementations. - `gen_indication_endpoint_target@0.1.0`: defines data-free contracts for constrained ADC indication, endpoint, and target opportunity generation; generation, evaluation, ranking, and evidence remain external. -- `target_safety_therapeutic_window_prescreen@0.2.0`: applies conservative, +- `target_safety_therapeutic_window_prescreen@0.3.0`: applies conservative, fatal-first rules to externally supplied public-evidence claims for target- intrinsic ADC safety pre-screening; it does not predict a product-specific therapeutic window. diff --git a/genmodules/target_safety_therapeutic_window_prescreen/README.md b/genmodules/target_safety_therapeutic_window_prescreen/README.md index 9a8f39f..89e0bc7 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/README.md +++ b/genmodules/target_safety_therapeutic_window_prescreen/README.md @@ -21,7 +21,10 @@ Unknown remains unresolved; it is never converted into safety. Material risk claims that do not meet a fatal threshold produce `HOLD`, not `GO`. A target reaches `GO` only when all six axes are represented and resolved with no material risk or conflict. Differential status is a structured field; -free-form tags cannot create a fatal flag. +free-form tags cannot create a fatal flag. Fatal aggregation requires a shared +`hazard_context_ref` or `(tissue, cell_type)` context; unscoped evidence cannot +be combined into a fatal decision. A differential must either share that +context or explicitly list the risk claims it mitigates. ## Decision semantics diff --git a/genmodules/target_safety_therapeutic_window_prescreen/contracts.py b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py index 32271ce..f3ddfbc 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/contracts.py +++ b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py @@ -13,8 +13,8 @@ from typing import Final -MODULE_VERSION: Final = "0.2.0" -CONTRACT_VERSION: Final = "0.2.0" +MODULE_VERSION: Final = "0.3.0" +CONTRACT_VERSION: Final = "0.3.0" _EXTERNAL_REF = re.compile(r"^external:[^\s]+$") _GENE_SYMBOL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*$") @@ -118,6 +118,9 @@ class EvidenceClaim: surface_exposed: bool | None = None normal_density_relation: str | None = None differential_status: DifferentialStatus = DifferentialStatus.NOT_ASSESSED + differential_assessment_ref: str | None = None + hazard_context_ref: str | None = None + mitigates_claim_refs: tuple[str, ...] = () toxicity_attribution: str | None = None severe: bool = False clinically_demonstrated: bool = False @@ -148,6 +151,20 @@ def __post_init__(self) -> None: raise ValueError("toxicity_attribution is invalid") if not isinstance(self.differential_status, DifferentialStatus): raise ValueError("differential_status is invalid") + for value, label in ( + (self.differential_assessment_ref, "differential_assessment_ref"), + (self.hazard_context_ref, "hazard_context_ref"), + ): + if value is not None: + _external(value, label) + for value in self.mitigates_claim_refs: + _external(value, "mitigates_claim_ref") + if ( + self.differential_status == DifferentialStatus.ABSENT + and self.differential_assessment_ref is None + ): + # A single observation cannot establish a cross-axis absence. + object.__setattr__(self, "differential_status", DifferentialStatus.UNKNOWN) if self.direction == RiskDirection.UNKNOWN and not self.unresolved: object.__setattr__(self, "unresolved", True) diff --git a/genmodules/target_safety_therapeutic_window_prescreen/engine.py b/genmodules/target_safety_therapeutic_window_prescreen/engine.py index ebd6edb..60d05d7 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/engine.py +++ b/genmodules/target_safety_therapeutic_window_prescreen/engine.py @@ -7,6 +7,7 @@ from __future__ import annotations from collections import defaultdict +from typing import Hashable from .contracts import ( AssessmentRequest, @@ -83,20 +84,28 @@ def _axis_summaries(claims: tuple[EvidenceClaim, ...]) -> tuple[AxisSummary, ... def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: flags: list[FatalFlag] = [] - high_quality_surface_risk = any( - claim.axis == EvidenceAxis.SURFACE_ACCESSIBILITY - and claim.surface_exposed is True - and claim.level in {EvidenceLevel.A, EvidenceLevel.B} - and claim.direction == RiskDirection.SUPPORTS_RISK - for claim in claims - ) - high_quality_critical_tissue_risk = any( - claim.criticality - in {Criticality.CRITICAL_NON_REGENERATIVE, Criticality.CRITICAL_REVERSIBLE} - and claim.level in {EvidenceLevel.A, EvidenceLevel.B} - and claim.direction == RiskDirection.SUPPORTS_RISK - for claim in claims - ) + grouped: dict[Hashable, list[EvidenceClaim]] = defaultdict(list) + for claim in claims: + if claim.hazard_context_ref is not None: + grouped[("ref", claim.hazard_context_ref)].append(claim) + elif claim.tissue and claim.cell_type: + grouped[("cell", claim.tissue, claim.cell_type)].append(claim) + + def context_has_surface_and_critical(context_claims: list[EvidenceClaim]) -> bool: + return any( + claim.axis == EvidenceAxis.SURFACE_ACCESSIBILITY + and claim.surface_exposed is True + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in context_claims + ) and any( + claim.criticality + in {Criticality.CRITICAL_NON_REGENERATIVE, Criticality.CRITICAL_REVERSIBLE} + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in context_claims + ) + if any( ( claim.axis == EvidenceAxis.SURFACE_ACCESSIBILITY @@ -104,11 +113,12 @@ def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: and claim.criticality == Criticality.CRITICAL_NON_REGENERATIVE and claim.level in {EvidenceLevel.A, EvidenceLevel.B} and claim.direction == RiskDirection.SUPPORTS_RISK + and (claim.hazard_context_ref is not None or (claim.tissue and claim.cell_type)) ) for claim in claims ): flags.append(FatalFlag.CRITICAL_SURFACE_HAZARD) - elif high_quality_surface_risk and high_quality_critical_tissue_risk: + elif any(context_has_surface_and_critical(items) for items in grouped.values()): flags.append(FatalFlag.CRITICAL_SURFACE_HAZARD) if any( claim.axis == EvidenceAxis.EXISTING_MODALITY_TOXICITY @@ -119,14 +129,30 @@ def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: for claim in claims ): flags.append(FatalFlag.CONFIRMED_ON_TARGET_TOXICITY) - high_quality_density_risk = any( - claim.axis == EvidenceAxis.ANTIGEN_DENSITY - and claim.normal_density_relation in {"similar", "higher"} - and claim.level in {EvidenceLevel.A, EvidenceLevel.B} - and claim.direction == RiskDirection.SUPPORTS_RISK - for claim in claims - ) - if high_quality_density_risk and high_quality_surface_risk and high_quality_critical_tissue_risk: + if any( + any( + claim.axis == EvidenceAxis.ANTIGEN_DENSITY + and claim.normal_density_relation in {"similar", "higher"} + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in items + ) + and any( + claim.axis == EvidenceAxis.SURFACE_ACCESSIBILITY + and claim.surface_exposed is True + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in items + ) + and any( + claim.criticality + in {Criticality.CRITICAL_NON_REGENERATIVE, Criticality.CRITICAL_REVERSIBLE} + and claim.level in {EvidenceLevel.A, EvidenceLevel.B} + and claim.direction == RiskDirection.SUPPORTS_RISK + for claim in items + ) + for items in grouped.values() + ): flags.append(FatalFlag.NORMAL_DENSITY_NOT_LOWER) if any( claim.axis == EvidenceAxis.SOLUBLE_SINK @@ -140,6 +166,7 @@ def _fatal_flags(claims: tuple[EvidenceClaim, ...]) -> tuple[FatalFlag, ...]: if any( claim.axis == EvidenceAxis.NORMAL_TISSUE_EXPRESSION and claim.differential_status == DifferentialStatus.ABSENT + and claim.differential_assessment_ref is not None and claim.level in {EvidenceLevel.A, EvidenceLevel.B} and claim.direction == RiskDirection.SUPPORTS_RISK for claim in claims @@ -181,8 +208,10 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: if claim.direction == RiskDirection.SUPPORTS_RISK ) incomplete_axes = any(summary.unresolved for summary in summaries) - has_plausible_differential = any( - claim.differential_status == DifferentialStatus.PRESENT + differential_claims = tuple( + claim + for claim in request.claims + if claim.differential_status == DifferentialStatus.PRESENT and claim.direction == RiskDirection.SUPPORTS_SAFETY and claim.axis in { @@ -191,8 +220,18 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: EvidenceAxis.TISSUE_CONSEQUENCE, } and claim.level in {EvidenceLevel.B, EvidenceLevel.C} - for claim in request.claims ) + covered_risk_refs = { + risk.claim_ref + for differential in differential_claims + for risk in request.claims + if risk.claim_ref in differential.mitigates_claim_refs + or ( + differential.hazard_context_ref is not None + and differential.hazard_context_ref == risk.hazard_context_ref + ) + } + all_material_risks_covered = bool(material_risk_refs) and set(material_risk_refs) <= covered_risk_refs if fatal_flags: decision = Decision.KILL elif ( @@ -206,7 +245,7 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: elif material_risk_refs: decision = ( Decision.CONDITIONAL_GO - if has_plausible_differential + if all_material_risks_covered else Decision.HOLD ) else: @@ -225,7 +264,7 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: if decision == Decision.CONDITIONAL_GO and confidence == "high": confidence = "medium" return AssessmentResult( - contract_version="0.2.0", + contract_version="0.3.0", request_ref=request.request_ref, target_ref=request.target.target_ref, axis_summaries=summaries, diff --git a/genmodules/target_safety_therapeutic_window_prescreen/module.yaml b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml index 5dc386f..9563baf 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/module.yaml +++ b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml @@ -1,10 +1,10 @@ module: module_id: target_safety_therapeutic_window_prescreen - module_version: 0.2.0 + module_version: 0.3.0 module_type: GenModule name: Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine - input_contract: TargetSafetyAssessmentRequest@0.2.0 - output_contract: TargetSafetyAssessmentResult@0.2.0 + input_contract: TargetSafetyAssessmentRequest@0.3.0 + output_contract: TargetSafetyAssessmentResult@0.3.0 execution_policy: external_input_and_output_only persistence: forbidden database: forbidden diff --git a/logs/worklog.md b/logs/worklog.md index 53f655c..22801f0 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2281,3 +2281,14 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Head: `d36e4a4`. - Boundary: No data, cache, result, model weight, or runtime artifact was added. Original PR #55 was not force-rewritten. - Next: submit PR #56 remediation to ChatGPT manually for another review cycle. + +### 2026-08-04 22:05 EDT + +- Action: Read ChatGPT Round 2 review for PR #56; result remained `REQUEST_CHANGES` with two blockers and one additional risk. +- Blocker 1 fix: Added `hazard_context_ref` and context-aware aggregation. Surface, criticality, and density evidence only combine within the same hazard context or `(tissue, cell_type)`; unscoped claims cannot trigger fatal. +- Blocker 2 fix: Added `mitigates_claim_refs` and context matching. `CONDITIONAL_GO` now requires all material-risk claims to be covered by relevant structured differentials; unrelated or partial coverage remains `HOLD`. +- Additional fix: `NO_EXPLOITABLE_DIFFERENTIAL` now requires `differential_assessment_ref`; an unreferenced single observation becomes `UNKNOWN` rather than fatal. +- Contract: Bumped module and contract versions from `0.2.0` to `0.3.0`. +- Tests: Added context mismatch, unscoped fatal, unrelated differential, and partial coverage cases. Full suite now passes 223 tests. +- Validation: 16 module tests passed; 223 full-suite tests passed; repository boundary check passed; `git diff --check` passed; no `__pycache__` remains. +- Next: push the Round 2 remediation to PR #56 and request another ChatGPT review. diff --git a/tests/test_target_safety_therapeutic_window_prescreen.py b/tests/test_target_safety_therapeutic_window_prescreen.py index caf627c..e897c5a 100644 --- a/tests/test_target_safety_therapeutic_window_prescreen.py +++ b/tests/test_target_safety_therapeutic_window_prescreen.py @@ -62,6 +62,7 @@ def test_fatal_critical_surface_hazard_wins(self): direction=RiskDirection.SUPPORTS_RISK, criticality=Criticality.CRITICAL_NON_REGENERATIVE, surface_exposed=True, + hazard_context_ref="external:hazard/heart", ), claim( claim_ref="external:claim/unknown-density", @@ -94,6 +95,24 @@ def test_unknown_or_conflicting_critical_evidence_holds(self): self.assertEqual(result.decision, Decision.HOLD) self.assertIn("external:claim/critical-conflict", result.conflict_refs) + def test_fatal_claim_without_hazard_context_holds(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/unscoped-fatal", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, + surface_exposed=True, + ) + ] + ) + ) + self.assertEqual(result.fatal_flags, ()) + self.assertEqual(result.decision, Decision.HOLD) + def test_plausible_differential_is_conditional_go(self): claims = complete_resolved_claims() claims[1] = claim( @@ -103,6 +122,7 @@ def test_plausible_differential_is_conditional_go(self): direction=RiskDirection.SUPPORTS_SAFETY, surface_exposed=False, differential_status=DifferentialStatus.PRESENT, + hazard_context_ref="external:hazard/conditional", ) claims[2] = claim( claim_ref="external:claim/density-differential", @@ -111,6 +131,8 @@ def test_plausible_differential_is_conditional_go(self): direction=RiskDirection.SUPPORTS_SAFETY, normal_density_relation="lower", differential_status=DifferentialStatus.PRESENT, + hazard_context_ref="external:hazard/conditional", + mitigates_claim_refs=("external:claim/conditional-material-risk",), ) claims.append( claim( @@ -119,6 +141,7 @@ def test_plausible_differential_is_conditional_go(self): level=EvidenceLevel.B, direction=RiskDirection.SUPPORTS_RISK, criticality=Criticality.REGENERATIVE, + hazard_context_ref="external:hazard/conditional", ) ) result = assess_target( @@ -170,6 +193,7 @@ def test_surface_and_critical_tissue_claims_aggregate_to_fatal(self): level=EvidenceLevel.B, direction=RiskDirection.SUPPORTS_RISK, surface_exposed=True, + hazard_context_ref="external:hazard/heart", ), claim( claim_ref="external:claim/critical-tissue-risk", @@ -177,6 +201,7 @@ def test_surface_and_critical_tissue_claims_aggregate_to_fatal(self): level=EvidenceLevel.B, direction=RiskDirection.SUPPORTS_RISK, criticality=Criticality.CRITICAL_NON_REGENERATIVE, + hazard_context_ref="external:hazard/heart", ), ] ) @@ -193,6 +218,7 @@ def test_density_alone_does_not_kill_when_surface_is_inaccessible(self): level=EvidenceLevel.B, direction=RiskDirection.SUPPORTS_RISK, normal_density_relation="higher", + hazard_context_ref="external:hazard/kidney", ), claim( claim_ref="external:claim/inaccessible-surface", @@ -200,6 +226,7 @@ def test_density_alone_does_not_kill_when_surface_is_inaccessible(self): level=EvidenceLevel.B, direction=RiskDirection.SUPPORTS_RISK, surface_exposed=False, + hazard_context_ref="external:hazard/kidney", ), claim( claim_ref="external:claim/critical-tissue", @@ -207,6 +234,7 @@ def test_density_alone_does_not_kill_when_surface_is_inaccessible(self): level=EvidenceLevel.B, direction=RiskDirection.SUPPORTS_RISK, criticality=Criticality.CRITICAL_NON_REGENERATIVE, + hazard_context_ref="external:hazard/kidney", ), ] ) @@ -238,12 +266,98 @@ def test_structured_absent_differential_can_create_fatal(self): level=EvidenceLevel.B, direction=RiskDirection.SUPPORTS_RISK, differential_status=DifferentialStatus.ABSENT, + differential_assessment_ref="external:assessment/no-differential", ) ] ) ) self.assertIn("no_exploitable_target_differential", [flag.value for flag in result.fatal_flags]) + def test_cross_context_surface_and_criticality_do_not_kill(self): + result = assess_target( + request( + [ + claim( + claim_ref="external:claim/liver-surface", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + surface_exposed=True, + hazard_context_ref="external:hazard/liver", + ), + claim( + claim_ref="external:claim/heart-critical", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, + hazard_context_ref="external:hazard/heart", + ), + ] + ) + ) + self.assertNotIn("critical_surface_hazard", [flag.value for flag in result.fatal_flags]) + self.assertEqual(result.decision, Decision.HOLD) + + def test_unrelated_differential_does_not_cover_material_risk(self): + claims = complete_resolved_claims() + claims[1] = claim( + claim_ref="external:claim/unrelated-differential", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_SAFETY, + surface_exposed=False, + differential_status=DifferentialStatus.PRESENT, + hazard_context_ref="external:hazard/gut", + ) + claims.append( + claim( + claim_ref="external:claim/heart-material-risk", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, + hazard_context_ref="external:hazard/heart", + ) + ) + result = assess_target(request(claims)) + self.assertEqual(result.decision, Decision.HOLD) + + def test_partial_differential_coverage_does_not_conditionally_go(self): + claims = complete_resolved_claims() + claims[1] = claim( + claim_ref="external:claim/covered-differential", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_SAFETY, + surface_exposed=False, + differential_status=DifferentialStatus.PRESENT, + hazard_context_ref="external:hazard/shared", + mitigates_claim_refs=("external:claim/risk-a",), + ) + claims.extend( + [ + claim( + claim_ref="external:claim/risk-a", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.REGENERATIVE, + hazard_context_ref="external:hazard/shared", + ), + claim( + claim_ref="external:claim/risk-b", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.REGENERATIVE, + hazard_context_ref="external:hazard/other", + ), + ] + ) + result = assess_target(request(claims)) + self.assertEqual(result.decision, Decision.HOLD) + def test_empty_evidence_is_hold_and_requests_next_experiments(self): result = assess_target(request([])) self.assertEqual(result.decision, Decision.HOLD) From ef5c9071527d2b326c24d79a834ad8a76359eed7 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 17:29:33 -0400 Subject: [PATCH 5/5] task_20260804: enforce evidence reference integrity --- ...fety-therapeutic-window-prescreen.zh-CN.md | 19 ++++- genmodules/README.md | 2 +- .../README.md | 3 + .../contracts.py | 31 ++++++-- .../engine.py | 2 +- .../module.yaml | 6 +- logs/worklog.md | 22 ++++++ ...get_safety_therapeutic_window_prescreen.py | 76 +++++++++++++++++++ 8 files changed, 149 insertions(+), 12 deletions(-) diff --git a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md index 1c3f35b..f5fdab9 100644 --- a/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md +++ b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md @@ -2,7 +2,7 @@ ## Status -- Branch: `task_20260804_target-safety-prescreen` +- Branch: `task_20260804_target-safety-prescreen-fix` - Base: latest `origin/main` at task start - Review: Round 1 returned `REQUEST_CHANGES`; remediation is in clean replacement PR #56 - Data boundary: no source data, cache, result, model weight, or runtime output in the repository @@ -24,12 +24,14 @@ It does not claim product-specific therapeutic-window prediction. - Decision semantics: `KILL`, `HOLD`, `CONDITIONAL_GO`, `GO`. - Unknown, unresolved, and conflicting claims remain visible and produce next-experiment references. - All cross-boundary identities and evidence references require `external:` references. +- `claim_ref` and `evidence_refs` are unique and must match exactly; mitigation + references must point to a `SUPPORTS_RISK` claim in the same request. - Runtime location is declared as `${BIOWORKSPACE_ROOT}/DATA/target_safety_therapeutic_window_prescreen/{raw,processed,result}`; no runtime writer is enabled in the repository. ## Validation - Module tests pass. -- Full suite: 212 tests pass. +- Module tests: 21 pass; full suite: 228 tests pass. - `scripts/verify_repository_boundary.sh` passes. - `git diff --check` passes. - No `__pycache__` directory remains. @@ -52,3 +54,16 @@ It does not claim product-specific therapeutic-window prediction. - `CONDITIONAL_GO` now requires every material-risk claim to be covered by a context-matched differential or explicit `mitigates_claim_refs`. - `NO_EXPLOITABLE_DIFFERENTIAL` requires an external comprehensive assessment reference; a single observation is downgraded to unresolved. - Added cross-context, unrelated-differential, partial-coverage, and unscoped-fatal regression tests. + +## Round 3 remediation + +- Read ChatGPT Round 3 `REQUEST_CHANGES`; the remaining blocker was that duplicate + claim/evidence references and invalid mitigation references could corrupt risk + coverage and permit a false `CONDITIONAL_GO`. +- Tightened `AssessmentRequest` to require unique claim references, unique + evidence references, exact equality between the two reference sets, and + mitigation references that resolve to in-request `SUPPORTS_RISK` claims. +- Added five contract-integrity regression tests covering duplicate claims, + duplicate evidence, missing mitigation targets, non-risk mitigation targets, + and the duplicate-reference conditional-go path. +- Bumped module and contract versions from `0.3.0` to `0.4.0`. diff --git a/genmodules/README.md b/genmodules/README.md index af21bb3..543f2c5 100644 --- a/genmodules/README.md +++ b/genmodules/README.md @@ -21,7 +21,7 @@ are not lifecycle stages or Gate implementations. - `gen_indication_endpoint_target@0.1.0`: defines data-free contracts for constrained ADC indication, endpoint, and target opportunity generation; generation, evaluation, ranking, and evidence remain external. -- `target_safety_therapeutic_window_prescreen@0.3.0`: applies conservative, +- `target_safety_therapeutic_window_prescreen@0.4.0`: applies conservative, fatal-first rules to externally supplied public-evidence claims for target- intrinsic ADC safety pre-screening; it does not predict a product-specific therapeutic window. diff --git a/genmodules/target_safety_therapeutic_window_prescreen/README.md b/genmodules/target_safety_therapeutic_window_prescreen/README.md index 89e0bc7..f193c2e 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/README.md +++ b/genmodules/target_safety_therapeutic_window_prescreen/README.md @@ -25,6 +25,9 @@ free-form tags cannot create a fatal flag. Fatal aggregation requires a shared `hazard_context_ref` or `(tissue, cell_type)` context; unscoped evidence cannot be combined into a fatal decision. A differential must either share that context or explicitly list the risk claims it mitigates. +Request contracts require unique claim and evidence references, exact equality +between those reference sets, and mitigation references to resolve to +in-request risk claims. ## Decision semantics diff --git a/genmodules/target_safety_therapeutic_window_prescreen/contracts.py b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py index f3ddfbc..f0b5277 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/contracts.py +++ b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py @@ -13,8 +13,8 @@ from typing import Final -MODULE_VERSION: Final = "0.3.0" -CONTRACT_VERSION: Final = "0.3.0" +MODULE_VERSION: Final = "0.4.0" +CONTRACT_VERSION: Final = "0.4.0" _EXTERNAL_REF = re.compile(r"^external:[^\s]+$") _GENE_SYMBOL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*$") @@ -186,9 +186,30 @@ def __post_init__(self) -> None: _external(self.run_context_ref, "run_context_ref") for evidence_ref in self.evidence_refs: _external(evidence_ref, "evidence_ref") - claim_refs = {claim.claim_ref for claim in self.claims} - if claim_refs - set(self.evidence_refs): - raise ValueError("every claim_ref must be declared in evidence_refs") + claim_refs = tuple(claim.claim_ref for claim in self.claims) + if len(set(claim_refs)) != len(claim_refs): + raise ValueError("claim_ref values must be unique") + if len(set(self.evidence_refs)) != len(self.evidence_refs): + raise ValueError("evidence_refs must be unique") + if set(self.evidence_refs) != set(claim_refs): + raise ValueError("evidence_refs must exactly match claim_refs") + + claim_ref_set = set(claim_refs) + risk_refs = { + claim.claim_ref + for claim in self.claims + if claim.direction == RiskDirection.SUPPORTS_RISK + } + for claim in self.claims: + for mitigated_ref in claim.mitigates_claim_refs: + if mitigated_ref not in claim_ref_set: + raise ValueError( + "mitigates_claim_ref must reference a claim in the request" + ) + if mitigated_ref not in risk_refs: + raise ValueError( + "mitigates_claim_ref must reference a SUPPORTS_RISK claim" + ) @dataclass(frozen=True) diff --git a/genmodules/target_safety_therapeutic_window_prescreen/engine.py b/genmodules/target_safety_therapeutic_window_prescreen/engine.py index 60d05d7..2a6b293 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/engine.py +++ b/genmodules/target_safety_therapeutic_window_prescreen/engine.py @@ -264,7 +264,7 @@ def assess_target(request: AssessmentRequest) -> AssessmentResult: if decision == Decision.CONDITIONAL_GO and confidence == "high": confidence = "medium" return AssessmentResult( - contract_version="0.3.0", + contract_version="0.4.0", request_ref=request.request_ref, target_ref=request.target.target_ref, axis_summaries=summaries, diff --git a/genmodules/target_safety_therapeutic_window_prescreen/module.yaml b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml index 9563baf..a59a85b 100644 --- a/genmodules/target_safety_therapeutic_window_prescreen/module.yaml +++ b/genmodules/target_safety_therapeutic_window_prescreen/module.yaml @@ -1,10 +1,10 @@ module: module_id: target_safety_therapeutic_window_prescreen - module_version: 0.3.0 + module_version: 0.4.0 module_type: GenModule name: Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine - input_contract: TargetSafetyAssessmentRequest@0.3.0 - output_contract: TargetSafetyAssessmentResult@0.3.0 + input_contract: TargetSafetyAssessmentRequest@0.4.0 + output_contract: TargetSafetyAssessmentResult@0.4.0 execution_policy: external_input_and_output_only persistence: forbidden database: forbidden diff --git a/logs/worklog.md b/logs/worklog.md index 22801f0..fb3b1fc 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2292,3 +2292,25 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Tests: Added context mismatch, unscoped fatal, unrelated differential, and partial coverage cases. Full suite now passes 223 tests. - Validation: 16 module tests passed; 223 full-suite tests passed; repository boundary check passed; `git diff --check` passed; no `__pycache__` remains. - Next: push the Round 2 remediation to PR #56 and request another ChatGPT review. + +### 2026-08-04 17:28 EDT + +- Instruction: Apply ChatGPT Round 3 `REQUEST_CHANGES` feedback for PR #56. +- Finding: `AssessmentRequest` did not enforce unique `claim_ref` values or + `evidence_refs`, did not define the evidence-to-claim relation strictly, and + accepted mitigation references that were missing or not risk claims. These + gaps could make risk coverage sets misleading and permit a false + `CONDITIONAL_GO` path. +- Change: Enforced unique claim references and evidence references, requiring + `evidence_refs` to exactly match the request claim references. Enforced that + every `mitigates_claim_ref` resolves to a `SUPPORTS_RISK` claim in the same + request. +- Tests: Added five contract-integrity tests for duplicate claims, duplicate + evidence, missing mitigation targets, non-risk mitigation targets, and the + duplicate-reference conditional-go path. +- Contract: Bumped module and contract versions from `0.3.0` to `0.4.0`. +- Boundary: Changed only the target-safety module contract/version metadata, + its tests and documentation, handoff, and this worklog; no data, cache, + result, model weight, or runtime artifact was added. +- Next: Run the module/full-suite/boundary checks, push the same PR #56, and + request the next ChatGPT review. Do not merge until `APPROVE`. diff --git a/tests/test_target_safety_therapeutic_window_prescreen.py b/tests/test_target_safety_therapeutic_window_prescreen.py index e897c5a..593eafa 100644 --- a/tests/test_target_safety_therapeutic_window_prescreen.py +++ b/tests/test_target_safety_therapeutic_window_prescreen.py @@ -368,6 +368,82 @@ def test_contract_rejects_non_external_evidence(self): with self.assertRaises(ValueError): claim(source_ref="local:source/1") + def test_contract_rejects_duplicate_claim_ref(self): + with self.assertRaises(ValueError): + request( + [ + claim(claim_ref="external:claim/duplicate"), + claim( + claim_ref="external:claim/duplicate", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + ), + ] + ) + + def test_contract_rejects_duplicate_evidence_ref(self): + item = claim() + with self.assertRaises(ValueError): + AssessmentRequest( + request_ref="external:request/duplicate-evidence", + target=TargetProfile( + target_ref="external:target/duplicate-evidence", + gene_symbol="GUCY2C", + ), + evidence_refs=(item.claim_ref, item.claim_ref), + claims=(item,), + policy_ref="external:policy/target-safety-v0.1", + run_context_ref="external:run/context-duplicate-evidence", + ) + + def test_contract_rejects_mitigation_for_missing_claim(self): + with self.assertRaises(ValueError): + request( + [ + claim( + claim_ref="external:claim/differential-missing", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_SAFETY, + differential_status=DifferentialStatus.PRESENT, + mitigates_claim_refs=("external:claim/missing",), + ) + ] + ) + + def test_contract_rejects_mitigation_for_nonrisk_claim(self): + with self.assertRaises(ValueError): + request( + [ + claim(claim_ref="external:claim/safety"), + claim( + claim_ref="external:claim/differential-nonrisk", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_SAFETY, + differential_status=DifferentialStatus.PRESENT, + mitigates_claim_refs=("external:claim/safety",), + ), + ] + ) + + def test_duplicate_refs_cannot_produce_conditional_go(self): + with self.assertRaises(ValueError): + request( + [ + claim( + claim_ref="external:claim/reused", + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.REGENERATIVE, + ), + claim( + claim_ref="external:claim/reused", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + direction=RiskDirection.SUPPORTS_SAFETY, + differential_status=DifferentialStatus.PRESENT, + ), + ] + ) + if __name__ == "__main__": unittest.main()