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..f5fdab9 --- /dev/null +++ b/docs/handoff/2026-08-04-target-safety-therapeutic-window-prescreen.zh-CN.md @@ -0,0 +1,69 @@ +# Target Safety and Therapeutic-Window Pre-screen Handoff + +## Status + +- 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 + +## 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. +- 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. +- `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. +- Module tests: 21 pass; full suite: 228 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. + +## 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.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. + +## 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 a9dd969..543f2c5 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.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. ## 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..f193c2e --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/README.md @@ -0,0 +1,57 @@ +# 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. + +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. 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 + +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..e4175d1 --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/__init__.py @@ -0,0 +1,31 @@ +"""Public-evidence target safety pre-screen contracts and conservative rules.""" + +from .contracts import ( + AssessmentRequest, + AssessmentResult, + Criticality, + Decision, + DifferentialStatus, + EvidenceAxis, + EvidenceClaim, + EvidenceLevel, + FatalFlag, + RiskDirection, + TargetProfile, +) +from .engine import assess_target + +__all__ = [ + "AssessmentRequest", + "AssessmentResult", + "Criticality", + "Decision", + "DifferentialStatus", + "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..f0b5277 --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/contracts.py @@ -0,0 +1,259 @@ +"""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 +from enum import StrEnum +import re +from typing import Final + + +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-]*$") + + +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 DifferentialStatus(StrEnum): + PRESENT = "present" + ABSENT = "absent" + UNKNOWN = "unknown" + NOT_ASSESSED = "not_assessed" + + +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 + 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 + 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") + 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) + + +@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 = 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) +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, ...] + material_risk_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.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 new file mode 100644 index 0000000..2a6b293 --- /dev/null +++ b/genmodules/target_safety_therapeutic_window_prescreen/engine.py @@ -0,0 +1,280 @@ +"""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 typing import Hashable + +from .contracts import ( + AssessmentRequest, + AssessmentResult, + AxisSummary, + Criticality, + Decision, + DifferentialStatus, + 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] = [] + 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 + 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 + 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 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 + 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( + 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 + 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 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 + ): + 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 + or claim.direction == RiskDirection.UNKNOWN + ) + 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 + ) + 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) + 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 { + EvidenceAxis.SURFACE_ACCESSIBILITY, + EvidenceAxis.ANTIGEN_DENSITY, + EvidenceAxis.TISSUE_CONSEQUENCE, + } + and claim.level in {EvidenceLevel.B, EvidenceLevel.C} + ) + 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 ( + not request.claims + or critical_unknown + or conflict_refs + or unresolved_refs + or incomplete_axes + ): + decision = Decision.HOLD + elif material_risk_refs: + decision = ( + Decision.CONDITIONAL_GO + if all_material_risks_covered + else Decision.HOLD + ) + 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.4.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, + 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..a59a85b --- /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.4.0 + module_type: GenModule + name: Public-Evidence Target Safety and Therapeutic-Window Pre-screen Engine + input_contract: TargetSafetyAssessmentRequest@0.4.0 + output_contract: TargetSafetyAssessmentResult@0.4.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..fb3b1fc 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2261,3 +2261,56 @@ 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 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. + +### 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. + +### 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. + +### 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 new file mode 100644 index 0000000..593eafa --- /dev/null +++ b/tests/test_target_safety_therapeutic_window_prescreen.py @@ -0,0 +1,449 @@ +import unittest + +from genmodules.target_safety_therapeutic_window_prescreen import ( + AssessmentRequest, + Criticality, + Decision, + DifferentialStatus, + 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", + ) + + +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( + 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, + hazard_context_ref="external:hazard/heart", + ), + 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_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( + claim_ref="external:claim/surface-differential", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + 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", + axis=EvidenceAxis.ANTIGEN_DENSITY, + level=EvidenceLevel.C, + 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( + claim_ref="external:claim/conditional-material-risk", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.REGENERATIVE, + hazard_context_ref="external:hazard/conditional", + ) + ) + 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-risk", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + surface_exposed=True, + hazard_context_ref="external:hazard/heart", + ), + claim( + claim_ref="external:claim/critical-tissue-risk", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, + hazard_context_ref="external:hazard/heart", + ), + ] + ) + ) + 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.B, + direction=RiskDirection.SUPPORTS_RISK, + normal_density_relation="higher", + hazard_context_ref="external:hazard/kidney", + ), + claim( + claim_ref="external:claim/inaccessible-surface", + axis=EvidenceAxis.SURFACE_ACCESSIBILITY, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + surface_exposed=False, + hazard_context_ref="external:hazard/kidney", + ), + claim( + claim_ref="external:claim/critical-tissue", + axis=EvidenceAxis.NORMAL_TISSUE_EXPRESSION, + level=EvidenceLevel.B, + direction=RiskDirection.SUPPORTS_RISK, + criticality=Criticality.CRITICAL_NON_REGENERATIVE, + hazard_context_ref="external:hazard/kidney", + ), + ] + ) + ) + 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, + 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) + 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") + + 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()