From 0fac7c9ec4ca0bd841be32ee45caf8b7443c0728 Mon Sep 17 00:00:00 2001 From: Zhixin Eason Li Date: Tue, 4 Aug 2026 16:57:41 -0400 Subject: [PATCH] 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 | 12 + ...get_safety_therapeutic_window_prescreen.py | 121 ++++++++++ 9 files changed, 684 insertions(+) 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..b4592bf 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2261,3 +2261,15 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - 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 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()