diff --git a/contexts/melanoma_anti_pd1/release/real_run_config.template.json b/contexts/melanoma_anti_pd1/release/real_run_config.template.json new file mode 100644 index 0000000..bfdb57f --- /dev/null +++ b/contexts/melanoma_anti_pd1/release/real_run_config.template.json @@ -0,0 +1 @@ +{"configuration_format_version":"v0.5.0","evidence_classification":"local_real_public_release","release_manifest":"REPLACE_WITH_LOCAL_RELEASE_MANIFEST.json","data_root":"REPLACE_WITH_LOCAL_DATA_DIRECTORY","target_subset":"REPLACE_WITH_TARGET_SUBSET.tsv","benchmark":"REPLACE_WITH_BENCHMARK.tsv","discovery_sources":"REPLACE_WITH_DISCOVERY_SOURCES.tsv","discovery_policy":"REPLACE_WITH_DISCOVERY_POLICY.json","universe_context":"REPLACE_WITH_UNIVERSE_CONTEXT.json","profile_context":"REPLACE_WITH_PROFILE_CONTEXT.json","profile_policy":"REPLACE_WITH_PROFILE_POLICY.json","baseline_ranking":"REPLACE_WITH_EXPLICIT_REAL_BASELINE.tsv","benchmark_policy":"REPLACE_WITH_BENCHMARK_POLICY.json","integration_policy":"REPLACE_WITH_INTEGRATION_POLICY.json","integration_context":"REPLACE_WITH_INTEGRATION_CONTEXT.json","release_policy":"REPLACE_WITH_RELEASE_POLICY.json","expected_context_identity":"melanoma_anti_pd1:v1","limitations":["Template only: replace every placeholder after human provenance review.","This file is not a completed real-data run."]} diff --git a/docs/audits/v0.5.0_release_closure_current_state.md b/docs/audits/v0.5.0_release_closure_current_state.md new file mode 100644 index 0000000..8ed7486 --- /dev/null +++ b/docs/audits/v0.5.0_release_closure_current_state.md @@ -0,0 +1,20 @@ +# v0.5.0 release-closure current-state audit + +Reviewed surfaces: the Issue 501–506 specifications and audits, canonical +`targetintel.functional_dependency` modules, melanoma context package, +existing release documentation, benchmark baseline fixture, and existing +DepMap local-layout and checksum contracts. + +Findings: ingestion is local-only and validates release-manifest bytes; +profiles, universe freezing, benchmark evaluation, and integration are +separate APIs. Issue 506 already keeps its overlay diagnostic and defaults to +the baseline, but its activation state is not a module-release decision. +Fixture manifests are visibly synthetic in their release identifiers and +limitations, but a release-level classification boundary was previously +absent. No existing module creates tags or changes production scoring. + +Issue 507 addresses the gap with explicit evidence classification, strict +configuration and policy contracts, preflight, stage orchestration, baseline +preservation checks, and separately retained module and activation readiness. +The included execution is synthetic fixture validation only; it is not a real +DepMap release or melanoma finding. diff --git a/docs/releases/v0.5.0.md b/docs/releases/v0.5.0.md new file mode 100644 index 0000000..78eab40 --- /dev/null +++ b/docs/releases/v0.5.0.md @@ -0,0 +1,21 @@ +# v0.5.0 — functional-dependency release closure architecture + +Implemented architecture: local-only release preflight, immutable run and +policy identities, deterministic orchestration of Issues 501–506, explicit +release criteria, baseline-preservation recording, and deterministic artifact +comparison. + +Fixture validation: compact synthetic fixtures exercise the complete workflow +and end in `blocked_fixture_evidence`. They do not establish real melanoma +findings, real-data readiness, or candidate activation. + +Real-data execution status: no completed real-data run is included in this +repository. A valid human-reviewed local public release may become eligible +for research-preview human review only after its required criteria and +reproducibility review pass. Candidate activation remains separately governed +by Issue 506 and is never automatic. + +Remaining human actions include provenance and licensing review, benchmark and +holdout review, unresolved-target review, reproducibility review, a separate +module-release decision, a separate candidate decision, and any manual tag or +later public release metadata update. diff --git a/docs/specs/v0.5.0_real_data_release_closure.md b/docs/specs/v0.5.0_real_data_release_closure.md new file mode 100644 index 0000000..1073a95 --- /dev/null +++ b/docs/specs/v0.5.0_real_data_release_closure.md @@ -0,0 +1,22 @@ +# v0.5.0 real-data release closure + +Issue 507 adds an offline orchestration boundary around the Issue 501–506 +functional-dependency contracts. It requires an immutable, checksum-derived +run configuration and a local release manifest before any scientific stage is +run. Local paths are operational references and are excluded from scientific +identity. + +The module release decision is distinct from the Issue 506 candidate activation +decision. A real, release-pinned and reproducible run may be submitted for +human research-preview review while its candidate overlay remains blocked. +Neither state activates production ranking, creates authorization, creates a +tag, or changes package metadata. + +The only evidence classifications are `synthetic_fixture`, +`local_real_public_release`, and `externally_validated_real_release`. Fixture +manifest identities cannot be relabelled as real. Missing, malformed, remote, +credential-bearing, or hidden-reasoning configuration inputs fail closed. + +Use `examples/depmap/run_v0_5_release_closure.py` with an explicit reviewed +configuration. The supplied context template is intentionally incomplete and +is not evidence of a completed real-data run. diff --git a/examples/depmap/README.md b/examples/depmap/README.md index 1cfda02..6e8292e 100644 --- a/examples/depmap/README.md +++ b/examples/depmap/README.md @@ -84,3 +84,14 @@ python examples/depmap/run_dependency_integration_gate.py \ --evidence-scope synthetic_fixture \ --output-dir /tmp/targetintel-dependency-integration ``` +## v0.5.0 release closure + +The local-only closure runner orchestrates the isolated functional-dependency +stages and never changes production ranking or creates a release. The compact +fixture is deliberately blocked as synthetic evidence: + +```bash +python examples/depmap/run_v0_5_release_closure.py \ + --run-config tests/fixtures/depmap/release_closure/run_config.json \ + --evidence-classification synthetic_fixture --output-dir /tmp/targetintel-v050-closure +``` diff --git a/examples/depmap/run_v0_5_release_closure.py b/examples/depmap/run_v0_5_release_closure.py new file mode 100644 index 0000000..792693c --- /dev/null +++ b/examples/depmap/run_v0_5_release_closure.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Run the local-only v0.5.0 DepMap release-closure workflow.""" +from __future__ import annotations +import argparse +from pathlib import Path +import sys + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from targetintel.functional_dependency import ReleaseClosureError, V050ReleaseRunConfiguration, run_release_closure + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--run-config", required=True) + parser.add_argument("--evidence-classification", required=True) + parser.add_argument("--output-dir", required=True) + args = parser.parse_args() + try: + configuration = V050ReleaseRunConfiguration.from_file(args.run_config) + result = run_release_closure(configuration, args.evidence_classification, Path(args.output_dir)) + except ReleaseClosureError as error: + parser.error(str(error)) + print(result["terminal_state"]) + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/targetintel/functional_dependency/__init__.py b/targetintel/functional_dependency/__init__.py index a5ea855..9d4de21 100644 --- a/targetintel/functional_dependency/__init__.py +++ b/targetintel/functional_dependency/__init__.py @@ -38,6 +38,11 @@ validate_evidence_scope, validate_integration_state, write_dependency_integration_artifacts, ) +from .release_closure import ( + ReleaseClosureError, V050ReleaseClosurePolicy, V050ReleaseRunConfiguration, + compare_release_runs, preflight_release, run_release_closure, validate_evidence_classification, + validate_release_state, +) __all__ = [ "DepMapFileManifest", @@ -81,4 +86,12 @@ "validate_evidence_scope", "validate_integration_state", "write_dependency_integration_artifacts", + "ReleaseClosureError", + "V050ReleaseClosurePolicy", + "V050ReleaseRunConfiguration", + "preflight_release", + "compare_release_runs", + "run_release_closure", + "validate_evidence_classification", + "validate_release_state", ] diff --git a/targetintel/functional_dependency/dependency_integration.py b/targetintel/functional_dependency/dependency_integration.py index 28fa21b..537c3b3 100644 --- a/targetintel/functional_dependency/dependency_integration.py +++ b/targetintel/functional_dependency/dependency_integration.py @@ -325,6 +325,7 @@ def build_dependency_integration(benchmark_dir: Path | str, baseline_ranking: Pa mandatory_bad = any(row["mandatory"] and row["result"] != "pass" for row in criteria) if not compatible: status = "blocked_incompatible_inputs" elif evidence_scope == "synthetic_fixture": status = "blocked_fixture_evidence" + elif any(row["criterion_id"] in {"benchmark_coverage", "holdout_coverage", "eligible_target_count", "permitted_missing_profile_fraction"} and row["mandatory"] and row["result"] != "pass" for row in criteria): status = "blocked_insufficient_evidence" elif mandatory_bad: status = "blocked_policy_failure" else: status = "eligible_for_human_activation" candidate = DependencyAwareProfileCandidate(FORMAT_VERSION, "dependency_aware_melanoma_anti_pd1_candidate_v1", context["context_identity"], baseline_id, str(manifest["dependency_profile_run_id"]), str(manifest["candidate_ranking_ids"]["bounded_overlay"]), policy.policy_id, policy.permitted_candidate_construction_method, policy.fixed_rank_band_size, policy.tie_handling_rule, policy.minimum_dependency_component_count, policy.missing_profile_fallback, "dependency_aware_melanoma_anti_pd1_candidate_v1", status, ("Experimental analysis-only candidate; it is not clinically validated.", "Explicit future human authorization is required.")) diff --git a/targetintel/functional_dependency/depmap_models.py b/targetintel/functional_dependency/depmap_models.py index d2a94fe..1560489 100644 --- a/targetintel/functional_dependency/depmap_models.py +++ b/targetintel/functional_dependency/depmap_models.py @@ -7,6 +7,7 @@ from hashlib import sha256 import json from pathlib import Path, PurePosixPath +import re from types import MappingProxyType from typing import Any, Mapping from urllib.parse import urlparse @@ -72,15 +73,26 @@ def _safe_release_directory(value: str) -> bool: def _forbidden_nested(value: Any) -> bool: - forbidden = ("credential", "password", "secret", "token", "authorization", "reasoning", "thinking", "chain_of_thought") + # Keys, rather than ordinary domain prose, are controlled. In particular + # legitimate limitations may mention future human authorization without + # becoming a credential-bearing artifact. + forbidden = frozenset({"credential", "credentials", "password", "secret", "token", "api_key", "apikey", "access_key", "access_token", "private_key", "reasoning", "hidden_reasoning", "thinking", "chain_of_thought"}) if isinstance(value, Mapping): return any( - _forbidden_nested(str(key)) or _forbidden_nested(item) + str(key).casefold() in forbidden or _forbidden_nested(item) for key, item in value.items() ) if isinstance(value, (tuple, list)): return any(_forbidden_nested(item) for item in value) - return isinstance(value, str) and any(marker in value.casefold() for marker in forbidden) + if not isinstance(value, str): + return False + # Detect structured secret/reasoning disclosures, while allowing normal + # domain language such as "future authorization is required". + return bool(re.search( + r"(?:authorization\s*:\s*bearer\b|(?:secret|token|password|api[_-]?key|access[_-]?token)\s*[:=]|(?:chain_of_thought|hidden_reasoning)\s*:|-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----)", + value, + flags=re.IGNORECASE, + )) def _require(condition: bool, message: str) -> None: diff --git a/targetintel/functional_dependency/release_closure.py b/targetintel/functional_dependency/release_closure.py new file mode 100644 index 0000000..e786733 --- /dev/null +++ b/targetintel/functional_dependency/release_closure.py @@ -0,0 +1,359 @@ +"""Offline, fail-closed v0.5.0 functional-dependency release closure. + +This module is deliberately an orchestration and evidence-accounting boundary. +It does not alter TargetIntel scoring, rankings, roles, or defaults. +""" +from __future__ import annotations + +import csv +from dataclasses import dataclass +from hashlib import sha256 +import json +from pathlib import Path +import tempfile +from typing import Any, Mapping +from urllib.parse import urlparse + +from .depmap_models import (DepMapLocalLayoutRequest, DepMapReleaseManifest, + LOCAL_LAYOUT_REQUEST_FORMAT_VERSION, canonical_json, _forbidden_nested, _identity) +from .depmap_validation import validate_local_release +from .depmap_ingestion import (INGESTION_REQUEST_FORMAT_VERSION, + DepMapIngestionRequest, DepMapTargetRequest, ingest_local_release) +from .depmap_profiles import (DepMapModelContextDefinition, + FunctionalDependencyProfilePolicy, build_dependency_profiles, + write_dependency_profile_artifacts) +from .target_universes import freeze_universes +from .depmap_benchmark import (DependencyBenchmarkPolicy, + evaluate_dependency_benchmark, write_dependency_benchmark_artifacts, + load_baseline_ranking) +from .dependency_integration import (DependencyIntegrationPolicy, + build_dependency_integration, write_dependency_integration_artifacts) + +FORMAT_VERSION = "v0.5.0" +EVIDENCE_CLASSIFICATIONS = frozenset({"synthetic_fixture", "local_real_public_release", "externally_validated_real_release"}) +RELEASE_STATES = frozenset({"blocked_fixture_evidence", "blocked_missing_real_data", "blocked_invalid_real_data", "blocked_incompatible_artifacts", "blocked_incomplete_universe", "blocked_benchmark_failure", "blocked_nonreproducible", "ready_research_preview_human_review", "ready_optional_candidate_human_review", "explicitly_rejected"}) +_REQUIRED_CONFIG = frozenset({"configuration_format_version", "evidence_classification", "release_manifest", "data_root", "target_subset", "benchmark", "discovery_sources", "discovery_policy", "universe_context", "profile_context", "profile_policy", "baseline_ranking", "benchmark_policy", "integration_policy", "integration_context", "release_policy", "expected_context_identity", "limitations"}) +_SELF_REFERENTIAL_ARTIFACTS = ("reproducibility_summary.json", "output_checksums.tsv", "release_closure_manifest.json") +_TOP_LEVEL_SCIENTIFIC_ARTIFACTS = ("release_preflight.json", "stage_manifest_index.json", "artifact_compatibility.json", "release_criteria.tsv", "release_readiness.json", "activation_readiness_summary.json", "input_checksums.tsv", "limitations.tsv", "human_release_actions.json", "release_report.md") + + +class ReleaseClosureError(ValueError): + """Sanitized terminal error; callers must not serialize tracebacks.""" + + +def validate_release_state(value: str) -> str: + if value not in RELEASE_STATES: + raise ReleaseClosureError("unknown release state") + return value + + +def validate_evidence_classification(value: str) -> str: + if value not in EVIDENCE_CLASSIFICATIONS: + raise ReleaseClosureError("unknown evidence classification") + return value + + +def _safe_nested(value: Any) -> bool: + """Use the canonical DepMap validator for keys and scalar disclosures.""" + return not _forbidden_nested(value) + + +def _path(value: str, base: Path) -> Path: + if not isinstance(value, str) or not value or urlparse(value).scheme: + raise ReleaseClosureError("release configuration requires local file references") + path = Path(value) + if path.is_absolute(): + return path.resolve() + resolved = (base / path).resolve() + try: + resolved.relative_to(base.resolve()) + except ValueError as error: + raise ReleaseClosureError("relative release configuration reference escapes its directory") from error + return resolved + + +def _sha(path: Path) -> str: + return sha256(path.read_bytes()).hexdigest() + + +def _json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise ReleaseClosureError("configured JSON input could not be read") from error + if not isinstance(value, dict): + raise ReleaseClosureError("configured JSON input must be an object") + return value + + +@dataclass(frozen=True) +class V050ReleaseRunConfiguration: + configuration_format_version: str + evidence_classification: str + references: Mapping[str, Path] + expected_context_identity: str + limitations: tuple[str, ...] + + @property + def configuration_id(self) -> str: + # Paths are operational only. Bytes and declared evidence classification + # are scientific identity inputs. + files = {key: _sha(path) for key, path in sorted(self.references.items()) if path.is_file()} + return _identity("v050rc", {"configuration_format_version": self.configuration_format_version, + "evidence_classification": self.evidence_classification, "input_checksums": files, + "expected_context_identity": self.expected_context_identity, "limitations": list(self.limitations)}) + + @classmethod + def from_file(cls, path: Path | str) -> "V050ReleaseRunConfiguration": + config_path = Path(path).resolve(); data = _json(config_path) + if set(data) != _REQUIRED_CONFIG: + raise ReleaseClosureError("unknown or missing release configuration fields") + if not _safe_nested(data): + raise ReleaseClosureError("release configuration contains controlled credential or hidden-reasoning fields") + if data["configuration_format_version"] != FORMAT_VERSION: + raise ReleaseClosureError("unsupported release configuration format version") + validate_evidence_classification(data["evidence_classification"]) + if not isinstance(data["expected_context_identity"], str) or not data["expected_context_identity"]: + raise ReleaseClosureError("expected context identity is required") + if not isinstance(data["limitations"], list) or not all(isinstance(x, str) and x for x in data["limitations"]): + raise ReleaseClosureError("limitations must be explicit non-empty strings") + refs = {key: _path(str(data[key]), config_path.parent) for key in _REQUIRED_CONFIG - {"configuration_format_version", "evidence_classification", "expected_context_identity", "limitations"}} + return cls(FORMAT_VERSION, data["evidence_classification"], refs, data["expected_context_identity"], tuple(sorted(set(data["limitations"])))) + + +@dataclass(frozen=True) +class V050ReleaseClosurePolicy: + policy_format_version: str + allowed_evidence_classifications: tuple[str, ...] + required_pipeline_stages: tuple[str, ...] + minimum_benchmark_count: int + minimum_discovery_count: int + minimum_benchmark_coverage: float + minimum_holdout_coverage: float + maximum_unresolved_fraction: float + reproducibility_required: bool + baseline_preservation_required: bool + release_ready_states: tuple[str, ...] + human_review_required: bool + candidate_activation_separate: bool + limitations: tuple[str, ...] + + @property + def policy_id(self) -> str: + return _identity("v050rp", self.to_dict()) + def to_dict(self) -> dict[str, Any]: + return {"policy_format_version": self.policy_format_version, "allowed_evidence_classifications": list(self.allowed_evidence_classifications), "required_pipeline_stages": list(self.required_pipeline_stages), "minimum_benchmark_count": self.minimum_benchmark_count, "minimum_discovery_count": self.minimum_discovery_count, "minimum_benchmark_coverage": self.minimum_benchmark_coverage, "minimum_holdout_coverage": self.minimum_holdout_coverage, "maximum_unresolved_fraction": self.maximum_unresolved_fraction, "reproducibility_required": self.reproducibility_required, "baseline_preservation_required": self.baseline_preservation_required, "release_ready_states": list(self.release_ready_states), "human_review_required": self.human_review_required, "candidate_activation_separate": self.candidate_activation_separate, "limitations": list(self.limitations)} + @classmethod + def from_file(cls, path: Path) -> "V050ReleaseClosurePolicy": + data = _json(path); required = set(cls.__dataclass_fields__) + if set(data) != required or not _safe_nested(data): raise ReleaseClosureError("unknown, missing, or unsafe release policy fields") + try: policy = cls(**{key: tuple(sorted(data[key])) if key in {"allowed_evidence_classifications", "required_pipeline_stages", "release_ready_states", "limitations"} else data[key] for key in required}) + except (TypeError, ValueError) as error: raise ReleaseClosureError("invalid release policy") from error + if policy.policy_format_version != FORMAT_VERSION or not set(policy.allowed_evidence_classifications).issubset(EVIDENCE_CLASSIFICATIONS) or not policy.allowed_evidence_classifications: raise ReleaseClosureError("invalid release policy classification") + if not set(policy.release_ready_states).issubset(RELEASE_STATES) or not all(isinstance(getattr(policy, k), (int, float)) and not isinstance(getattr(policy, k), bool) for k in ("minimum_benchmark_count", "minimum_discovery_count", "minimum_benchmark_coverage", "minimum_holdout_coverage", "maximum_unresolved_fraction")): raise ReleaseClosureError("invalid release policy thresholds") + return policy + + +def _write_json(path: Path, value: Any) -> None: path.write_text(canonical_json(value) + "\n", encoding="utf-8", newline="") +def _write_tsv(path: Path, rows: list[Mapping[str, Any]]) -> None: + fields = ["criterion_id", "description", "source_artifact", "source_field", "observed_value", "comparison_operator", "threshold", "result", "mandatory", "limitations"] + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fields, delimiter="\t"); writer.writeheader() + for row in sorted(rows, key=lambda x: str(x["criterion_id"])): writer.writerow({k: json.dumps(row[k], sort_keys=True) if isinstance(row[k], (list, dict)) else row[k] for k in fields}) + + +def _criterion(identifier: str, desc: str, artifact: str, field: str, observed: Any, operator: str, threshold: Any, mandatory: bool = True) -> dict[str, Any]: + if observed is None: result = "unavailable" + elif operator == ">=": result = "pass" if observed >= threshold else "fail" + elif operator == "<=": result = "pass" if observed <= threshold else "fail" + elif operator == "==": result = "pass" if observed == threshold else "fail" + else: raise ReleaseClosureError("unsupported release criterion operator") + return {"criterion_id": identifier, "description": desc, "source_artifact": artifact, "source_field": field, "observed_value": observed, "comparison_operator": operator, "threshold": threshold, "result": result, "mandatory": mandatory, "limitations": []} + + +def preflight_release(config: V050ReleaseRunConfiguration, requested_classification: str) -> dict[str, Any]: + validate_evidence_classification(requested_classification) + failures: list[str] = [] + if requested_classification != config.evidence_classification: failures.append("command evidence classification does not match configuration") + for name, path in config.references.items(): + if name == "data_root": + if not path.is_dir(): failures.append("data root is absent or not a directory") + elif not path.is_file() or not path.stat().st_size: failures.append(f"required input is absent, not regular, or empty: {name}") + manifest = None + if not failures: + try: manifest = DepMapReleaseManifest.from_dict(_json(config.references["release_manifest"])) + except (ValueError, KeyError, TypeError, ReleaseClosureError): failures.append("release manifest is invalid") + if manifest is not None: + try: + layout = DepMapLocalLayoutRequest(LOCAL_LAYOUT_REQUEST_FORMAT_VERSION, config.references["data_root"], manifest.release_identifier, config.references["data_root"], "read_only", "release_closure") + validation = validate_local_release(manifest, layout, source_root=config.references["data_root"]) + if not validation.is_valid: + failures.append("release manifest local-file validation failed") + except (ValueError, KeyError, TypeError, OSError): + failures.append("release manifest local-file validation failed") + fixture_markers = "" + if manifest is not None: + fixture_markers = " ".join([manifest.release_identifier, manifest.source_name, *manifest.release_limitations]).casefold() + if config.evidence_classification != "synthetic_fixture" and ("fixture" in fixture_markers or "synthetic" in fixture_markers): failures.append("known fixture release identity cannot be claimed as real evidence") + if config.evidence_classification == "synthetic_fixture" and "fixture" not in fixture_markers and "synthetic" not in fixture_markers: failures.append("synthetic evidence classification requires fixture-identifiable manifest") + return {"preflight_format_version": FORMAT_VERSION, "configuration_id": config.configuration_id, "evidence_classification": config.evidence_classification, "status": "passed" if not failures else "failed", "failures": sorted(failures), "release_manifest_id": None if manifest is None else manifest.manifest_id, "release_identifier": None if manifest is None else manifest.release_identifier, "input_checksums": {key: _sha(path) for key, path in sorted(config.references.items()) if path.is_file()}} + + +def _targets(path: Path) -> list[DepMapTargetRequest]: + with path.open(encoding="utf-8", newline="") as handle: + rows = list(csv.DictReader(handle, delimiter="\t")) + if not rows: raise ReleaseClosureError("target subset is empty") + return [DepMapTargetRequest(row.get("requested_identifier", ""), row.get("requested_identifier_type", "")) for row in rows] + + +def _release_state_before_reproducibility(policy: V050ReleaseClosurePolicy, classification: str, criteria: list[dict[str, Any]], integration: Mapping[str, Any]) -> str: + """Return the module decision without treating candidate activation as release approval.""" + if classification == "synthetic_fixture": + return "blocked_fixture_evidence" + if classification not in policy.allowed_evidence_classifications: + return "explicitly_rejected" + # Candidate-policy outcomes remain separate from module readiness, but an + # Issue 506 incompatibility means this closure did not operate on a + # compatible chain of scientific artifacts and must fail closed. + if (integration.get("decision_state") == "blocked_incompatible_inputs" or + any(c["criterion_id"] == "integration_artifact_compatibility" and c["mandatory"] and c["result"] != "pass" for c in criteria)): + return "blocked_incompatible_artifacts" + if any(c["criterion_id"] in {"benchmark_count", "discovery_count", "unresolved_fraction"} and c["mandatory"] and c["result"] != "pass" for c in criteria): + return "blocked_incomplete_universe" + if any(c["mandatory"] and c["result"] != "pass" for c in criteria): + return "blocked_benchmark_failure" + # Candidate activation is intentionally separate. A blocked candidate can + # still leave a reproducible module ready for research-preview review. + if integration.get("decision_state") == "eligible_for_human_activation" and "ready_optional_candidate_human_review" in policy.release_ready_states: + return "ready_optional_candidate_human_review" + if "ready_research_preview_human_review" in policy.release_ready_states: + return "ready_research_preview_human_review" + return "explicitly_rejected" + + +def run_release_closure(config: V050ReleaseRunConfiguration, requested_classification: str, output_dir: Path | str, *, _skip_reproducibility_check: bool = False) -> dict[str, Any]: + output = Path(output_dir).resolve(); output.mkdir(parents=True, exist_ok=True) + preflight = preflight_release(config, requested_classification); _write_json(output / "release_preflight.json", preflight) + policy = V050ReleaseClosurePolicy.from_file(config.references["release_policy"]) + if preflight["status"] != "passed": + state = "blocked_fixture_evidence" if config.evidence_classification == "synthetic_fixture" else "blocked_missing_real_data" if any("absent" in x for x in preflight["failures"]) else "blocked_invalid_real_data" + return _finalize(output, config, policy, state, [], {}, {"status": "not_run", "human_review_required": True}, preflight) + try: + manifest = DepMapReleaseManifest.from_dict(_json(config.references["release_manifest"])) + targets = _targets(config.references["target_subset"]) + full_dir, subset_dir = output / "ingestion_full", output / "ingestion_subset" + ingest_local_release(DepMapIngestionRequest(INGESTION_REQUEST_FORMAT_VERSION, manifest, "full_matrix", config.references["data_root"], full_dir)) + ingest_local_release(DepMapIngestionRequest(INGESTION_REQUEST_FORMAT_VERSION, manifest, "target_subset", config.references["data_root"], subset_dir, target_universe=targets)) + universe_context = _json(config.references["universe_context"]) + if universe_context.get("context_identity") != config.expected_context_identity or universe_context.get("release_manifest_id") != manifest.manifest_id: raise ReleaseClosureError("universe context identities are incompatible") + freeze_universes(config.references["benchmark"], config.references["discovery_sources"], config.references["discovery_policy"], full_dir / "gene_index.tsv", universe_context, output / "universes") + profile_context = DepMapModelContextDefinition.from_dict(_json(config.references["profile_context"])); profile_policy = FunctionalDependencyProfilePolicy.from_dict(_json(config.references["profile_policy"])) + profiles, assignments = build_dependency_profiles(subset_dir, profile_context, profile_policy); write_dependency_profile_artifacts(output / "profiles", profiles, assignments) + benchmark_policy = DependencyBenchmarkPolicy.from_dict(_json(config.references["benchmark_policy"])) + baseline_bytes = config.references["baseline_ranking"].read_bytes(); baseline_rows, baseline_id, baseline_fp = load_baseline_ranking(config.references["baseline_ranking"]) + evaluation = evaluate_dependency_benchmark(output / "universes", output / "profiles", config.references["baseline_ranking"], benchmark_policy); write_dependency_benchmark_artifacts(output / "benchmark", evaluation, benchmark_policy) + if baseline_bytes != config.references["baseline_ranking"].read_bytes(): raise ReleaseClosureError("baseline input was modified") + integration_context = _json(config.references["integration_context"]) + if not _safe_nested(integration_context): + raise ReleaseClosureError("integration context contains controlled credential or hidden-reasoning fields") + # Mandatory Issue 506 compatibility anchors for the real-release route. + integration_context.update({"context_identity": config.expected_context_identity, "benchmark_context_identity": config.expected_context_identity, "freeze_id": json.loads((output / "universes/universe_freeze_manifest.json").read_text())["freeze_id"], "dependency_profile_run_id": profiles.run_id, "benchmark_policy_id": benchmark_policy.policy_id}) + integration_policy = DependencyIntegrationPolicy.from_dict(_json(config.references["integration_policy"])) + issue506_scope = "synthetic_fixture" if config.evidence_classification == "synthetic_fixture" else "local_real_data" + integration = build_dependency_integration(output / "benchmark", config.references["baseline_ranking"], integration_policy, integration_context, issue506_scope); write_dependency_integration_artifacts(output / "integration", integration) + metrics = _collect_metrics(output, baseline_id, baseline_fp, profiles.run_id, integration) + criteria = _criteria(policy, metrics) + state = _release_state_before_reproducibility(policy, config.evidence_classification, criteria, integration["decision"]) + reproducibility: Mapping[str, Any] | None = None + if state.startswith("ready") and policy.reproducibility_required and not _skip_reproducibility_check: + # The comparison is a second, direct API execution in an isolated + # temporary directory. No external process, download, or operational + # path enters the scientific identity. + # Finalize once before comparison so the primary and replica both + # contain every non-self-referential top-level scientific artifact. + _finalize(output, config, policy, state, criteria, metrics, integration["decision"], preflight) + with tempfile.TemporaryDirectory(prefix="targetintel-v050-repro-") as temporary: + replica = run_release_closure(config, requested_classification, Path(temporary), _skip_reproducibility_check=True) + reproducibility = compare_release_runs(output, temporary) + if not replica["successful_closure"]: + reproducibility = {**reproducibility, "differing_artifacts": sorted(set(reproducibility["differing_artifacts"] + ["replica_terminal_state"])), "result": "nonreproducible"} + if reproducibility["result"] != "reproducible": + state = "blocked_nonreproducible" + return _finalize(output, config, policy, state, criteria, metrics, integration["decision"], preflight, reproducibility) + except (ValueError, OSError, ReleaseClosureError) as error: + # A sanitized blocked record, never a raw traceback or false success. + state = "blocked_fixture_evidence" if config.evidence_classification == "synthetic_fixture" else "blocked_invalid_real_data" + return _finalize(output, config, policy, state, [], {"failure": str(error), "failure_category": "pipeline_execution_failure"}, {"status": "not_run", "human_review_required": True}, preflight) + + +def _collect_metrics(output: Path, baseline_id: str, baseline_fp: str, profile_run_id: str, integration: Mapping[str, Any]) -> dict[str, Any]: + benchmark = _json(output / "benchmark/benchmark_coverage.json"); universe = _json(output / "universes/universe_overlap.json") + unresolved = max(0, universe["counts"].get("unresolved", 0)); discovery = sum(1 for _ in csv.DictReader((output / "universes/discovery_universe.tsv").open(), delimiter="\t")); background = sum(1 for _ in csv.DictReader((output / "universes/background_universe.tsv").open(), delimiter="\t")); + return {"benchmark_count": benchmark.get("total_benchmark_targets"), "development_count": benchmark.get("development_benchmark_targets"), "holdout_count": benchmark.get("holdout_benchmark_targets"), "discovery_count": discovery, "background_count": background, "benchmark_coverage": benchmark.get("profiled_target_count", 0) / benchmark.get("total_benchmark_targets", 1), "holdout_coverage": next((x.get("observed") for x in _json(output / "benchmark/integration_evidence.json").get("criteria", []) if x.get("criterion") == "minimum_holdout_coverage"), None), "unresolved_count": unresolved, "unresolved_fraction": unresolved / max(discovery, 1), "baseline_ranking_id": baseline_id, "baseline_fingerprint": baseline_fp, "profile_run_id": profile_run_id, "integration_state": integration.get("decision_state"), "integration_artifacts_compatible": integration.get("compatibility", {}).get("compatible"), "baseline_preserved": integration.get("criteria", [])[-1].get("result") == "pass" if integration.get("criteria") else False} + + +def _criteria(policy: V050ReleaseClosurePolicy, m: Mapping[str, Any]) -> list[dict[str, Any]]: + return [_criterion("benchmark_count", "Actual benchmark count meets the immutable minimum.", "benchmark_coverage.json", "total_benchmark_targets", m.get("benchmark_count"), ">=", policy.minimum_benchmark_count), _criterion("discovery_count", "Discovery universe count meets the immutable minimum.", "discovery_universe.tsv", "row_count", m.get("discovery_count"), ">=", policy.minimum_discovery_count), _criterion("benchmark_coverage", "Benchmark profile coverage meets the immutable minimum.", "benchmark_coverage.json", "profiled/total", m.get("benchmark_coverage"), ">=", policy.minimum_benchmark_coverage), _criterion("holdout_coverage", "Holdout coverage meets the immutable minimum.", "integration_evidence.json", "minimum_holdout_coverage", m.get("holdout_coverage"), ">=", policy.minimum_holdout_coverage), _criterion("unresolved_fraction", "Unresolved target fraction remains within policy.", "universe_overlap.json", "unresolved/discovery", m.get("unresolved_fraction"), "<=", policy.maximum_unresolved_fraction), _criterion("integration_artifact_compatibility", "Issue 506 accepted the same intermediate artifact identities and overlay recipe.", "input_compatibility.json", "compatible", m.get("integration_artifacts_compatible"), "==", True), _criterion("baseline_preservation", "Baseline bytes, scores, and ranks remain unchanged.", "baseline_preservation.json", "all preservation checks", m.get("baseline_preserved"), "==", True)] + + +def compare_release_runs(first: Path | str, second: Path | str) -> dict[str, Any]: + """Compare scientific closure artifacts while excluding operational location.""" + left, right = Path(first), Path(second) + scientific_roots = ("ingestion_full", "ingestion_subset", "universes", "profiles", "benchmark", "integration") + names = list(_TOP_LEVEL_SCIENTIFIC_ARTIFACTS) + names.extend(sorted({str(path.relative_to(left)) for root in scientific_roots for path in (left / root).rglob("*") if path.is_file()} | {str(path.relative_to(right)) for root in scientific_roots for path in (right / root).rglob("*") if path.is_file()})) + differing = [] + for name in names: + a, b = left / name, right / name + if not a.is_file() or not b.is_file() or a.read_bytes() != b.read_bytes(): differing.append(name) + configuration_id = None + if (left / "release_closure_manifest.json").is_file(): configuration_id = _json(left / "release_closure_manifest.json").get("configuration_id") + inventories = {"first": _output_inventory_is_valid(left), "second": _output_inventory_is_valid(right)} + manifest_ids = {"first": _closure_identity(left), "second": _closure_identity(right)} + return {"reproducibility_format_version": FORMAT_VERSION, "configuration_id": configuration_id, "compared_artifacts": list(names), "differing_artifacts": differing, "excluded_artifacts": list(_SELF_REFERENTIAL_ARTIFACTS), "excluded_artifact_invariants": {"output_checksum_inventory_valid": inventories, "closure_scientific_identity": manifest_ids}, "excluded_operational_fields": ["output_path", "timestamp", "hostname", "username", "runtime_duration"], "result": "reproducible" if not differing and all(inventories.values()) and manifest_ids["first"] == manifest_ids["second"] else "nonreproducible"} + + +def _output_inventory_is_valid(directory: Path) -> bool: + table = directory / "output_checksums.tsv" + if not table.is_file(): + return False + try: + rows = list(csv.DictReader(table.open(encoding="utf-8", newline=""), delimiter="\t")) + return all(set(row) == {"name", "sha256"} and (directory / row["name"]).is_file() and _sha(directory / row["name"]) == row["sha256"] for row in rows) + except (OSError, KeyError, TypeError): + return False + + +def _closure_identity(directory: Path) -> str | None: + try: + closure = _json(directory / "release_closure_manifest.json") + return _identity("v050closure", {key: closure.get(key) for key in ("configuration_id", "policy_id", "evidence_classification")}) + except ReleaseClosureError: + return None + + +def _finalize(output: Path, config: V050ReleaseRunConfiguration, policy: V050ReleaseClosurePolicy, state: str, criteria: list[dict[str, Any]], metrics: Mapping[str, Any], activation: Mapping[str, Any], preflight: Mapping[str, Any], reproducibility: Mapping[str, Any] | None = None) -> dict[str, Any]: + validate_release_state(state); stage_names = ["ingestion_full", "ingestion_subset", "universes", "profiles", "benchmark", "integration"] + stages = [{"stage": name, "manifest_present": any((output / name).glob("*manifest*.json")), "status": "completed" if (output / name).is_dir() else "not_completed"} for name in stage_names] + _write_json(output / "stage_manifest_index.json", {"configuration_id": config.configuration_id, "stages": stages}) + _write_json(output / "artifact_compatibility.json", {"compatible": all(x["status"] == "completed" for x in stages) if state.startswith("ready") else False, "expected_context_identity": config.expected_context_identity, "metrics": dict(metrics)}) + _write_tsv(output / "release_criteria.tsv", criteria) + input_rows = [{"name": k, "sha256": v} for k, v in sorted(preflight.get("input_checksums", {}).items())] + (output / "input_checksums.tsv").write_text("name\tsha256\n" + "".join(f"{r['name']}\t{r['sha256']}\n" for r in input_rows), encoding="utf-8", newline="") + readiness = {"release_readiness_format_version": FORMAT_VERSION, "release_state": state, "human_review_required": True, "production_activation_enabled": False, "configuration_id": config.configuration_id, "policy_id": policy.policy_id, "criteria": criteria, "limitations": list(config.limitations)} + _write_json(output / "release_readiness.json", readiness) + _write_json(output / "activation_readiness_summary.json", {"module_release_state": state, "integration_state": activation.get("decision_state", activation.get("status")), "candidate_activation_readiness": "blocked" if str(activation.get("decision_state", "")).startswith("blocked") or state == "blocked_fixture_evidence" else "eligible_for_human_review", "human_review_required": True, "approved_authorization_emitted": False}) + (output / "limitations.tsv").write_text("limitation\n" + "".join(x + "\n" for x in sorted(set(config.limitations + ("No automatic tag, version change, or production activation.",)))), encoding="utf-8", newline="") + _write_json(output / "human_release_actions.json", {"human_review_required": True, "automatic_tag_created": False, "actions": ["Inspect real-data source licensing and provenance.", "Inspect benchmark and holdout behaviour.", "Inspect unresolved targets and reproducibility results.", "Decide module research-preview release separately from candidate activation.", "Manually create any release tag and later update public release metadata."]}) + (output / "release_report.md").write_text("# v0.5.0 release closure\n\n- Module release state: `" + state + "`\n- Candidate activation remains a separate human decision.\n- No production activation, tag, or version change is performed.\n", encoding="utf-8", newline="") + # The closure manifest, checksum table, and reproducibility summary are + # intentionally excluded to avoid a self-referential checksum cycle. All + # other release-decision artifacts must exist before this inventory is made. + outputs = [{"name": str(p.relative_to(output)), "sha256": _sha(p)} for p in sorted(output.rglob("*")) if p.is_file() and p.name not in {"output_checksums.tsv", "release_closure_manifest.json", "reproducibility_summary.json"}] + (output / "output_checksums.tsv").write_text("name\tsha256\n" + "".join(f"{r['name']}\t{r['sha256']}\n" for r in outputs), encoding="utf-8", newline="") + reproducibility = dict(reproducibility or {"reproducibility_format_version": FORMAT_VERSION, "configuration_id": config.configuration_id, "differing_artifacts": [], "excluded_artifacts": list(_SELF_REFERENTIAL_ARTIFACTS), "excluded_artifact_invariants": {}, "result": "not_compared"}) + reproducibility.update({"scientific_artifact_checksums": outputs, "excluded_operational_fields": ["output_path", "timestamp", "hostname", "username", "runtime_duration"]}) + _write_json(output / "reproducibility_summary.json", reproducibility) + closure = {"release_closure_format_version": FORMAT_VERSION, "configuration_id": config.configuration_id, "policy_id": policy.policy_id, "evidence_classification": config.evidence_classification, "terminal_state": state, "successful_closure": state.startswith("ready"), "human_review_required": True, "production_activation_enabled": False} + _write_json(output / "release_closure_manifest.json", closure) + return closure diff --git a/tests/fixtures/depmap/release_closure/benchmark/baseline_ranking.tsv b/tests/fixtures/depmap/release_closure/benchmark/baseline_ranking.tsv new file mode 100644 index 0000000..8cd7a9c --- /dev/null +++ b/tests/fixtures/depmap/release_closure/benchmark/baseline_ranking.tsv @@ -0,0 +1,5 @@ +original_target_identifier canonical_target_identity baseline_rank baseline_score primary_intent role selected_state +BRAF symbol:BRAF|entrez:673 1 0.9 small_molecule tumor_intrinsic_driver selected +NRAS symbol:NRAS|entrez:4893 2 0.8 small_molecule mechanism selected +PTEN symbol:PTEN|entrez:5728 3 0.7 resistance_biomarker biomarker "" +CDK4 symbol:CDK4|entrez:1019 4 0.6 small_molecule tumor_intrinsic_driver "" diff --git a/tests/fixtures/depmap/release_closure/benchmark/evaluation_policy.json b/tests/fixtures/depmap/release_closure/benchmark/evaluation_policy.json new file mode 100644 index 0000000..5025010 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/benchmark/evaluation_policy.json @@ -0,0 +1 @@ +{"policy_format_version":"v0.5.0","policy_id_label":"synthetic-dependency-benchmark-v1","partitions":["development","holdout","combined"],"positive_classes":["known_positive"],"negative_classes":["negative_control"],"descriptive_classes":["challenging_control","biomarker_not_intervention_target","context_dependent","mechanism_control"],"excluded_classes":["unknown_or_holdout"],"primary_k_values":[1,2],"secondary_k_values":[3],"minimum_benchmark_coverage":0.5,"minimum_holdout_coverage":0.5,"minimum_eligible_targets":2,"exact_enrichment":{"method":"one_sided_hypergeometric","minimum_population":2},"multiple_testing_family":"all_partition_ranking_k_enrichment_tests","candidate_rankings":{"dependency_only":{"minimum_component_count":1,"components":["gene_effect_contrast","dependency_probability_contrast","lineage_position"]},"bounded_overlay":{"band_size":2}},"source_ablations":{"all_components":["gene_effect_contrast","dependency_probability_contrast","lineage_position"],"without_gene_effect_contrast":["dependency_probability_contrast","lineage_position"],"without_dependency_probability_contrast":["gene_effect_contrast","lineage_position"],"without_lineage_position":["gene_effect_contrast","dependency_probability_contrast"]},"rank_stability_thresholds":{"minimum_spearman":0.0,"maximum_band_violations":0},"missing_profile_policy":"retain_baseline_order_ineligible","tie_handling_policy":"midrank_signal_baseline_then_identity","limitations":["Synthetic fixture only; human review is required."]} diff --git a/tests/fixtures/depmap/release_closure/ingestion/README.txt b/tests/fixtures/depmap/release_closure/ingestion/README.txt new file mode 100644 index 0000000..6195601 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/README.txt @@ -0,0 +1 @@ +Synthetic non-biological DepMap ingestion fixture. diff --git a/tests/fixtures/depmap/release_closure/ingestion/common_essential.tsv b/tests/fixtures/depmap/release_closure/ingestion/common_essential.tsv new file mode 100644 index 0000000..a3db94b --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/common_essential.tsv @@ -0,0 +1,2 @@ +gene_label +BRAF (673) diff --git a/tests/fixtures/depmap/release_closure/ingestion/dependency_probability.csv b/tests/fixtures/depmap/release_closure/ingestion/dependency_probability.csv new file mode 100644 index 0000000..922371c --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/dependency_probability.csv @@ -0,0 +1,7 @@ +ModelID,BRAF (673),NRAS (4893),PTEN (5728),BAD LABEL,CDK4 (1019) +ACH-001,0.9,0.2,,0.1,0.4 +ACH-002,0.8,0.3,0.4,0.2,0.5 +ACH-003,0.7,0.4,0.5,0.3,0.6 +ACH-005,0.6,0.5,0.6,0.4,0.7 +ACH-006,0.5,0.6,0.7,0.5,0.8 +ACH-007,0.4,0.7,0.8,0.6,0.9 diff --git a/tests/fixtures/depmap/release_closure/ingestion/gene_effect.csv b/tests/fixtures/depmap/release_closure/ingestion/gene_effect.csv new file mode 100644 index 0000000..6e8a3b2 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/gene_effect.csv @@ -0,0 +1,8 @@ +ModelID,BRAF (673),NRAS (4893),PTEN (5728),BAD LABEL,TP53 (7157) +ACH-001,-0.5,-0.2,,0.1,-0.8 +ACH-002,-0.6,-0.1,-0.3,0.2,-0.7 +ACH-003,-0.4,-0.3,-0.2,0.3,-0.9 +ACH-004,-0.2,-0.4,-0.1,0.4,-0.6 +ACH-005,-0.1,-0.5,-0.2,0.5,-0.5 +ACH-006,-0.3,-0.6,-0.3,0.6,-0.4 +ACH-007,-0.8,-0.7,-0.4,0.7,-0.3 diff --git a/tests/fixtures/depmap/release_closure/ingestion/model_metadata.csv b/tests/fixtures/depmap/release_closure/ingestion/model_metadata.csv new file mode 100644 index 0000000..e81f869 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/model_metadata.csv @@ -0,0 +1,7 @@ +ModelID,OncotreeLineage,PrimaryDisease,Subtype +ACH-001,Skin,Melanoma,Synthetic +ACH-002,Skin,Melanoma,Synthetic +ACH-003,Skin,Melanoma,Synthetic +ACH-005,Lung,Synthetic,Synthetic +ACH-006,Breast,Synthetic,Synthetic +ACH-007,Liver,,Synthetic diff --git a/tests/fixtures/depmap/release_closure/ingestion/pan_dependency.tsv b/tests/fixtures/depmap/release_closure/ingestion/pan_dependency.tsv new file mode 100644 index 0000000..95f4fef --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/pan_dependency.tsv @@ -0,0 +1,2 @@ +gene_label +NRAS (4893) diff --git a/tests/fixtures/depmap/release_closure/ingestion/release_manifest.json b/tests/fixtures/depmap/release_closure/ingestion/release_manifest.json new file mode 100644 index 0000000..4a98c65 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/release_manifest.json @@ -0,0 +1,19 @@ +{ + "declaration_state": "declared", + "file_manifests": [ + {"dataset_role":"crispr_gene_effect","expected_size_bytes":285,"file_format":"csv","file_manifest_format_version":"v0.5.0","limitations":["Synthetic non-biological fixture."],"relative_filename":"gene_effect.csv","required":true,"schema_fingerprint":{"canonical_required_columns":["ModelID"],"dataset_role":"crispr_gene_effect","gene_column_naming_contract":"depmap_symbol_entrez_label","identifier_orientation":"models_by_genes","model_identifier_contract":"ModelID","nullable_field_policy":"values_may_be_missing","primitive_value_type":"float","required_identifier_fields":["ModelID"],"schema_fingerprint_format_version":"v0.5.0","schema_mapping_version":"depmap-header-v1"},"sha256_checksum":"6d27b494d289d2c6c365d6d9faa08041418ebc8afcd2c0818dda4f6f9927e41b","source_description":"Synthetic CI fixture"}, + {"dataset_role":"crispr_dependency_probability","expected_size_bytes":230,"file_format":"csv","file_manifest_format_version":"v0.5.0","limitations":["Synthetic non-biological fixture."],"relative_filename":"dependency_probability.csv","required":false,"schema_fingerprint":{"canonical_required_columns":["ModelID"],"dataset_role":"crispr_dependency_probability","gene_column_naming_contract":"depmap_symbol_entrez_label","identifier_orientation":"models_by_genes","model_identifier_contract":"ModelID","nullable_field_policy":"values_may_be_missing","primitive_value_type":"float","required_identifier_fields":["ModelID"],"schema_fingerprint_format_version":"v0.5.0","schema_mapping_version":"depmap-header-v1"},"sha256_checksum":"60a63f15ab36afc8a830812c10f3c2abae07865452e044da0cf7bfda7a77a389","source_description":"Synthetic CI fixture"}, + {"dataset_role":"model_metadata","expected_size_bytes":236,"file_format":"csv","file_manifest_format_version":"v0.5.0","limitations":["Synthetic non-biological fixture."],"relative_filename":"model_metadata.csv","required":true,"schema_fingerprint":{"canonical_required_columns":["ModelID","OncotreeLineage","PrimaryDisease"],"dataset_role":"model_metadata","gene_column_naming_contract":null,"identifier_orientation":"model_metadata_rows","model_identifier_contract":"ModelID","nullable_field_policy":"values_may_be_missing","primitive_value_type":"string","required_identifier_fields":["ModelID"],"schema_fingerprint_format_version":"v0.5.0","schema_mapping_version":"depmap-header-v1"},"sha256_checksum":"a10a0b430c9e7fa1cf0d5a75c24231db249d4f69e24f91caab0bb692818ff4b3","source_description":"Synthetic CI fixture"}, + {"dataset_role":"common_essential_reference","expected_size_bytes":22,"file_format":"tsv","file_manifest_format_version":"v0.5.0","limitations":["Synthetic non-biological fixture."],"relative_filename":"common_essential.tsv","required":false,"schema_fingerprint":{"canonical_required_columns":["gene_label"],"dataset_role":"common_essential_reference","gene_column_naming_contract":"depmap_symbol_entrez_label","identifier_orientation":"gene_reference_rows","model_identifier_contract":null,"nullable_field_policy":"values_may_be_missing","primitive_value_type":"string","required_identifier_fields":["gene_label"],"schema_fingerprint_format_version":"v0.5.0","schema_mapping_version":"depmap-header-v1"},"sha256_checksum":"91246e10ca44a74cdfe9feffca3c8437218928d5cfd2bd0a24913caa95d87066","source_description":"Synthetic CI fixture"}, + {"dataset_role":"pan_dependency_reference","expected_size_bytes":23,"file_format":"tsv","file_manifest_format_version":"v0.5.0","limitations":["Synthetic non-biological fixture."],"relative_filename":"pan_dependency.tsv","required":false,"schema_fingerprint":{"canonical_required_columns":["gene_label"],"dataset_role":"pan_dependency_reference","gene_column_naming_contract":"depmap_symbol_entrez_label","identifier_orientation":"gene_reference_rows","model_identifier_contract":null,"nullable_field_policy":"values_may_be_missing","primitive_value_type":"string","required_identifier_fields":["gene_label"],"schema_fingerprint_format_version":"v0.5.0","schema_mapping_version":"depmap-header-v1"},"sha256_checksum":"7847ed450c454f86d83ca32694272f5d1260a9a21d0dcfb1f156ba8b9f5c8738","source_description":"Synthetic CI fixture"}, + {"dataset_role":"release_readme","expected_size_bytes":51,"file_format":"text","file_manifest_format_version":"v0.5.0","limitations":["Synthetic non-biological fixture."],"relative_filename":"README.txt","required":true,"schema_fingerprint":{"canonical_required_columns":[],"dataset_role":"release_readme","gene_column_naming_contract":null,"identifier_orientation":"release_document","model_identifier_contract":null,"nullable_field_policy":"values_may_be_missing","primitive_value_type":"string","required_identifier_fields":[],"schema_fingerprint_format_version":"v0.5.0","schema_mapping_version":"depmap-header-v1"},"sha256_checksum":"0c9ab6074ea856621d596bc243403be52295f6672fa8f00cae6898930d474264","source_description":"Synthetic CI fixture"} + ], + "manifest_schema_id":"targetintel.depmap-release-manifest", + "manifest_schema_version":"v0.5.0", + "optional_dataset_roles":["crispr_dependency_probability","common_essential_reference","pan_dependency_reference"], + "release_identifier":"synthetic-fixture-502", + "release_limitations":["Synthetic non-biological fixture; no biological conclusion."], + "required_dataset_roles":["crispr_gene_effect","model_metadata","release_readme"], + "research_use_boundary":"Local ingestion only; no clinical or biological interpretation.", + "source_name":"Synthetic DepMap-like fixture" +} diff --git a/tests/fixtures/depmap/release_closure/ingestion/target_subset.tsv b/tests/fixtures/depmap/release_closure/ingestion/target_subset.tsv new file mode 100644 index 0000000..b77075b --- /dev/null +++ b/tests/fixtures/depmap/release_closure/ingestion/target_subset.tsv @@ -0,0 +1,5 @@ +requested_identifier requested_identifier_type +BRAF symbol +4893 entrez +CDK4 symbol +NOT_A_GENE symbol diff --git a/tests/fixtures/depmap/release_closure/integration/context.json b/tests/fixtures/depmap/release_closure/integration/context.json new file mode 100644 index 0000000..88aeeec --- /dev/null +++ b/tests/fixtures/depmap/release_closure/integration/context.json @@ -0,0 +1 @@ +{"context_identity":"melanoma_anti_pd1:v1"} diff --git a/tests/fixtures/depmap/release_closure/integration/integration_policy.json b/tests/fixtures/depmap/release_closure/integration/integration_policy.json new file mode 100644 index 0000000..f114e5d --- /dev/null +++ b/tests/fixtures/depmap/release_closure/integration/integration_policy.json @@ -0,0 +1 @@ +{"policy_format_version":"v0.5.0","policy_id_label":"synthetic-dependency-integration-v1","allowed_evidence_scopes":["local_real_data","externally_validated_real_data"],"required_issue505_status":"human_review_required","minimum_benchmark_coverage":0.5,"minimum_holdout_coverage":0.5,"minimum_eligible_target_count":2,"primary_k":1,"recall_non_degradation_required":true,"negative_control_non_worsening_required":true,"minimum_bounded_overlay_spearman":0.0,"maximum_median_rank_displacement":0.0,"zero_band_violations_required":true,"minimum_source_ablation_top_k_jaccard":0.0,"permitted_missing_profile_fraction":0.5,"permitted_candidate_construction_method":"bounded_overlay","fixed_rank_band_size":2,"tie_handling_rule":"midrank_signal_baseline_then_identity","minimum_dependency_component_count":1,"missing_profile_fallback":"retain_baseline_order","baseline_fallback_policy":"baseline_unless_explicit_authorized_opt_in","explicit_opt_in_required":true,"human_approval_required":true,"limitations":["Synthetic fixture policy; real evidence and human review are required."]} diff --git a/tests/fixtures/depmap/release_closure/profiles/melanoma_context.json b/tests/fixtures/depmap/release_closure/profiles/melanoma_context.json new file mode 100644 index 0000000..e89e140 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/profiles/melanoma_context.json @@ -0,0 +1 @@ +{"accepted_values":{"PrimaryDisease":["Melanoma"]},"context_definition_format_version":"v0.5.0","context_name":"melanoma","context_version":"fixture-v1","limitations":["Synthetic exact-metadata context only."],"metadata_mapping_version":"depmap-modelid-v1","minimum_context_model_count":2,"minimum_reference_model_count":1} diff --git a/tests/fixtures/depmap/release_closure/profiles/profile_policy.json b/tests/fixtures/depmap/release_closure/profiles/profile_policy.json new file mode 100644 index 0000000..a2d15c4 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/profiles/profile_policy.json @@ -0,0 +1 @@ +{"contradiction_observation_rules":{"high_probability_at_or_above":0.8,"low_probability_at_or_below":0.2,"strong_negative_gene_effect_at_or_below":-0.2},"dependency_probability_thresholds":[0.5,0.8],"lineage_ranking_method":"gene_effect_median_strength_percentile","limitations":["Thresholds are descriptive analytical conventions, not biological truth."],"minimum_eligible_lineages":2,"minimum_measured_context_models":2,"minimum_measured_reference_models":1,"minimum_models_per_lineage":1,"missing_value_policy":"exclude_and_report","profile_policy_format_version":"v0.5.0","quantile_method":"linear"} diff --git a/tests/fixtures/depmap/release_closure/release_policy.json b/tests/fixtures/depmap/release_closure/release_policy.json new file mode 100644 index 0000000..63c9f81 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/release_policy.json @@ -0,0 +1 @@ +{"policy_format_version":"v0.5.0","allowed_evidence_classifications":["local_real_public_release","externally_validated_real_release"],"required_pipeline_stages":["ingestion_full","ingestion_subset","universes","profiles","benchmark","integration"],"minimum_benchmark_count":4,"minimum_discovery_count":4,"minimum_benchmark_coverage":0.5,"minimum_holdout_coverage":0.5,"maximum_unresolved_fraction":1.0,"reproducibility_required":true,"baseline_preservation_required":true,"release_ready_states":["ready_research_preview_human_review","ready_optional_candidate_human_review"],"human_review_required":true,"candidate_activation_separate":true,"limitations":["Synthetic fixture policy cannot establish real release readiness."]} diff --git a/tests/fixtures/depmap/release_closure/run_config.json b/tests/fixtures/depmap/release_closure/run_config.json new file mode 100644 index 0000000..1f79c5f --- /dev/null +++ b/tests/fixtures/depmap/release_closure/run_config.json @@ -0,0 +1 @@ +{"configuration_format_version":"v0.5.0","evidence_classification":"synthetic_fixture","release_manifest":"ingestion/release_manifest.json","data_root":"ingestion","target_subset":"ingestion/target_subset.tsv","benchmark":"universes/benchmark.tsv","discovery_sources":"universes/discovery_sources.tsv","discovery_policy":"universes/discovery_policy.json","universe_context":"universe_context.json","profile_context":"profiles/melanoma_context.json","profile_policy":"profiles/profile_policy.json","baseline_ranking":"benchmark/baseline_ranking.tsv","benchmark_policy":"benchmark/evaluation_policy.json","integration_policy":"integration/integration_policy.json","integration_context":"integration/context.json","release_policy":"release_policy.json","expected_context_identity":"melanoma_anti_pd1:v1","limitations":["Synthetic CI fixture only; no melanoma finding or real release claim."]} diff --git a/tests/fixtures/depmap/release_closure/universe_context.json b/tests/fixtures/depmap/release_closure/universe_context.json new file mode 100644 index 0000000..bb1b402 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/universe_context.json @@ -0,0 +1 @@ +{"canonical_mapping_version":"depmap-gene-label-v2","context_identity":"melanoma_anti_pd1:v1","disease_identity":"MONDO:0005105","release_manifest_id":"dmrm_292d2a2f9a7addd60366b2d82b2497449b13a33854f15a32016eec40947cc190"} diff --git a/tests/fixtures/depmap/release_closure/universes/benchmark.tsv b/tests/fixtures/depmap/release_closure/universes/benchmark.tsv new file mode 100644 index 0000000..846ad18 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/universes/benchmark.tsv @@ -0,0 +1,5 @@ +original_gene_symbol canonical_identity benchmark_class resistance_axes expected_qualitative_behaviour role evidence_source_key curation_rationale partition curation_version entry_limitations +BRAF symbol:BRAF|entrez:673 known_positive tumor_intrinsic_driver known curated melanoma driver control target internal:benchmark-v1 Existing internal benchmark target. development v1 Internal curation only +NRAS symbol:NRAS|entrez:4893 challenging_control melanoma_plasticity context-dependent curated control mechanism internal:benchmark-v1 Existing internal benchmark target. holdout v1 Internal curation only +PTEN symbol:PTEN|entrez:5728 biomarker_not_intervention_target antigen_presentation_loss biomarker or mechanism control biomarker internal:benchmark-v1 Existing internal benchmark target. development v1 Internal curation only +TP53 symbol:TP53|entrez:7157 negative_control other_unresolved negative direct intervention control mechanism internal:benchmark-v1 Existing internal benchmark target. holdout v1 Internal curation only diff --git a/tests/fixtures/depmap/release_closure/universes/context.json b/tests/fixtures/depmap/release_closure/universes/context.json new file mode 100644 index 0000000..e232f03 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/universes/context.json @@ -0,0 +1 @@ +{"canonical_mapping_version":"depmap-gene-label-v2","context_identity":"melanoma_anti_pd1:v1","disease_identity":"MONDO:0005105","release_manifest_id":"dmrm_synthetic_fixture_502"} diff --git a/tests/fixtures/depmap/release_closure/universes/discovery_policy.json b/tests/fixtures/depmap/release_closure/universes/discovery_policy.json new file mode 100644 index 0000000..5746635 --- /dev/null +++ b/tests/fixtures/depmap/release_closure/universes/discovery_policy.json @@ -0,0 +1 @@ +{"policy_format_version":"v0.5.0","policy_id_label":"synthetic-discovery-v1","approved_source_classes":["benchmark_union","resistance_axis"],"source_specific_inclusion_rules":{"benchmark_union":"union every curated benchmark canonical identity","resistance_axis":"include exact curated resistance-axis source record"},"canonical_identity_mapping_version":"depmap-gene-label-v2","benchmark_union_required":true,"exclusion_rules":["No DepMap outcomes or ranking fields."],"advisory_maximum_size":1000,"limitations":["Synthetic operational fixture; not biological curation."]} diff --git a/tests/fixtures/depmap/release_closure/universes/discovery_sources.tsv b/tests/fixtures/depmap/release_closure/universes/discovery_sources.tsv new file mode 100644 index 0000000..20167cd --- /dev/null +++ b/tests/fixtures/depmap/release_closure/universes/discovery_sources.tsv @@ -0,0 +1,4 @@ +original_gene_symbol canonical_identity source_class source_record_id source_dataset_version inclusion_rule resistance_axes role_annotation resolution_status inclusion_status rejection_reason limitations +BRAF symbol:BRAF|entrez:673 resistance_axis fixture:axis:BRAF fixture-v1 exact curated axis source tumor_intrinsic_driver tumor intrinsic candidate resolved_exact included Synthetic fixture +NRAS symbol:NRAS|entrez:4893 resistance_axis fixture:axis:NRAS fixture-v1 exact curated axis source melanoma_plasticity tumor intrinsic candidate resolved_exact included Synthetic fixture +UNRES unresolved:UNRES resistance_axis fixture:axis:UNRES fixture-v1 exact curated axis source other_unresolved unresolved unresolved No exact canonical identity Synthetic fixture diff --git a/tests/test_dependency_integration.py b/tests/test_dependency_integration.py index 1280f1b..04f2e27 100644 --- a/tests/test_dependency_integration.py +++ b/tests/test_dependency_integration.py @@ -139,3 +139,32 @@ def test_gate_rejects_overlay_recipe_divergence(tmp_path): result = build_dependency_integration(benchmark_dir, BASELINE, policy, {"context_identity": "melanoma_anti_pd1:v1"}, "local_real_data") assert result["decision"]["decision_state"] == "blocked_incompatible_inputs" assert "bounded_overlay_recipe_mismatch" in result["compatibility"]["reasons"] + + +def test_gate_distinguishes_insufficient_evidence_from_policy_failure(tmp_path): + """Coverage and eligible-target shortages retain the controlled evidence state.""" + benchmark_dir = tmp_path / "benchmark"; benchmark_dir.mkdir() + _write_issue505_fixture(benchmark_dir) + (benchmark_dir / "benchmark_coverage.json").write_text(json.dumps({ + "total_benchmark_targets": 4, "profiled_target_count": 0, + })) + policy = DependencyIntegrationPolicy.from_dict(json.loads(FIXTURE.read_text())) + result = build_dependency_integration( + benchmark_dir, BASELINE, policy, {"context_identity": "melanoma_anti_pd1:v1"}, "local_real_data", + ) + assert result["decision"]["decision_state"] == "blocked_insufficient_evidence" + + +def test_bounded_overlay_permits_a_genuine_within_band_reorder() -> None: + """A lower baseline target may move only inside its fixed rank band.""" + from targetintel.functional_dependency.depmap_benchmark import _rank + rows = [ + {"canonical_identity": "top", "baseline_rank": 1, "dependency_signal": .1}, + {"canonical_identity": "lower", "baseline_rank": 2, "dependency_signal": .9}, + {"canonical_identity": "next", "baseline_rank": 3, "dependency_signal": .8}, + {"canonical_identity": "last", "baseline_rank": 4, "dependency_signal": .2}, + ] + _rank(rows, "bounded_overlay_rank", overlay=True, band_size=2) + assert rows[1]["bounded_overlay_rank"] == 1 + assert rows[0]["bounded_overlay_rank"] == 2 + assert all((row["baseline_rank"] - 1) // 2 == (row["bounded_overlay_rank"] - 1) // 2 for row in rows) diff --git a/tests/test_release_closure.py b/tests/test_release_closure.py new file mode 100644 index 0000000..9273b9b --- /dev/null +++ b/tests/test_release_closure.py @@ -0,0 +1,226 @@ +"""Offline checks for the v0.5.0 release-closure boundary.""" +from __future__ import annotations +import csv +from dataclasses import replace +from hashlib import sha256 +import json +from pathlib import Path +import shutil +import pytest +from targetintel.functional_dependency import (ReleaseClosureError, + V050ReleaseClosurePolicy, V050ReleaseRunConfiguration, compare_release_runs, + preflight_release, run_release_closure, validate_evidence_classification, + validate_release_state) +from targetintel.functional_dependency.release_closure import ( + _criterion, _release_state_before_reproducibility, _safe_nested, +) + +ROOT = Path("tests/fixtures/depmap/release_closure") + +def config() -> V050ReleaseRunConfiguration: + return V050ReleaseRunConfiguration.from_file(ROOT / "run_config.json") + +def test_controlled_release_enums_fail_closed() -> None: + assert validate_release_state("blocked_fixture_evidence") == "blocked_fixture_evidence" + assert validate_evidence_classification("synthetic_fixture") == "synthetic_fixture" + with pytest.raises(ReleaseClosureError): validate_release_state("ready") + with pytest.raises(ReleaseClosureError): validate_evidence_classification("real") + +def test_config_identity_is_path_independent_and_checksum_sensitive(tmp_path: Path) -> None: + first = config(); copied = tmp_path / "config.json"; copied.write_text((ROOT / "run_config.json").read_text()) + # Relative paths intentionally point elsewhere and are therefore unsuitable; + # identity itself is derived from bytes, not the configuration filename. + assert first.configuration_id == config().configuration_id + data = json.loads((ROOT / "run_config.json").read_text()); data["limitations"].append("changed") + changed = tmp_path / "changed.json"; changed.write_text(json.dumps(data)) + assert V050ReleaseRunConfiguration.from_file(changed).configuration_id != first.configuration_id + + +def test_run_configuration_rejects_url_reference(tmp_path: Path) -> None: + payload = json.loads((ROOT / "run_config.json").read_text()) + payload["release_manifest"] = "https://example.invalid/release_manifest.json" + path = tmp_path / "url-config.json" + path.write_text(json.dumps(payload)) + with pytest.raises(ReleaseClosureError, match="local file references"): + V050ReleaseRunConfiguration.from_file(path) + +def test_fixture_preflight_and_closure_are_blocked_but_complete(tmp_path: Path) -> None: + result = run_release_closure(config(), "synthetic_fixture", tmp_path / "one") + assert result["terminal_state"] == "blocked_fixture_evidence" + assert not result["successful_closure"] + for name in ("release_preflight.json", "stage_manifest_index.json", "release_criteria.tsv", "release_readiness.json", "activation_readiness_summary.json", "human_release_actions.json"): + assert (tmp_path / "one" / name).is_file() + activation = json.loads((tmp_path / "one" / "activation_readiness_summary.json").read_text()) + assert activation["candidate_activation_readiness"] == "blocked" + assert activation["approved_authorization_emitted"] is False + + +def test_closure_retains_unchanged_baseline_bytes_fingerprint_ranks_and_scores(tmp_path: Path) -> None: + baseline = config().references["baseline_ranking"] + baseline_bytes = baseline.read_bytes() + baseline_rows = list(csv.DictReader(baseline.open(encoding="utf-8", newline=""), delimiter="\t")) + run_release_closure(config(), "synthetic_fixture", tmp_path / "closure") + + preservation = json.loads((tmp_path / "closure" / "integration" / "baseline_preservation.json").read_text()) + benchmark_manifest = json.loads((tmp_path / "closure" / "benchmark" / "dependency_benchmark_manifest.json").read_text()) + overlay_rows = list(csv.DictReader((tmp_path / "closure" / "integration" / "candidate_overlay.tsv").open(encoding="utf-8", newline=""), delimiter="\t")) + assert baseline.read_bytes() == baseline_bytes + assert preservation["baseline_file_bytes_unchanged"] is True + assert preservation["baseline_fingerprint_before"] == preservation["baseline_fingerprint_after"] == sha256(baseline_bytes).hexdigest() + assert benchmark_manifest["baseline_fingerprint"] == preservation["baseline_fingerprint_before"] + assert preservation["baseline_ranks_retained_exactly"] is True + assert preservation["baseline_scores_retained_exactly"] is True + assert [(row["canonical_target_identity"], row["baseline_rank"], row["baseline_score"]) for row in overlay_rows] == [ + (row["canonical_target_identity"], row["baseline_rank"], row["baseline_score"]) + for row in baseline_rows + ] + +def test_fixture_cannot_be_relabelled_real(tmp_path: Path) -> None: + raw = json.loads((ROOT / "run_config.json").read_text()) + raw["evidence_classification"] = "local_real_public_release" + for key, value in list(raw.items()): + if key not in {"configuration_format_version", "evidence_classification", "expected_context_identity", "limitations"}: + raw[key] = str(config().references[key]) + path = tmp_path / "real-label.json"; path.write_text(json.dumps(raw)) + result = run_release_closure(V050ReleaseRunConfiguration.from_file(path), "local_real_public_release", tmp_path / "bad") + assert result["terminal_state"] == "blocked_invalid_real_data" + +def test_policy_identity_changes_with_threshold() -> None: + first = V050ReleaseClosurePolicy.from_file(ROOT / "release_policy.json") + payload = json.loads((ROOT / "release_policy.json").read_text()); payload["minimum_benchmark_count"] = 5 + changed = ROOT / "policy-copy.json" + try: + changed.write_text(json.dumps(payload)); assert V050ReleaseClosurePolicy.from_file(changed).policy_id != first.policy_id + finally: + changed.unlink(missing_ok=True) + +def test_equivalent_fixture_runs_compare_reproducible(tmp_path: Path) -> None: + run_release_closure(config(), "synthetic_fixture", tmp_path / "one") + run_release_closure(config(), "synthetic_fixture", tmp_path / "two") + assert compare_release_runs(tmp_path / "one", tmp_path / "two")["result"] == "reproducible" + + +def test_reproducibility_comparison_detects_nested_scientific_artifact_change(tmp_path: Path) -> None: + run_release_closure(config(), "synthetic_fixture", tmp_path / "one") + run_release_closure(config(), "synthetic_fixture", tmp_path / "two") + artifact = tmp_path / "two" / "integration" / "integration_gate_decision.json" + artifact.write_text(artifact.read_text() + "\n") + comparison = compare_release_runs(tmp_path / "one", tmp_path / "two") + assert comparison["result"] == "nonreproducible" + assert "integration/integration_gate_decision.json" in comparison["differing_artifacts"] + + +def test_reproducibility_comparison_detects_top_level_scientific_artifact_change(tmp_path: Path) -> None: + run_release_closure(config(), "synthetic_fixture", tmp_path / "one") + run_release_closure(config(), "synthetic_fixture", tmp_path / "two") + artifact = tmp_path / "two" / "release_readiness.json" + artifact.write_text(artifact.read_text() + "\n") + comparison = compare_release_runs(tmp_path / "one", tmp_path / "two") + assert comparison["result"] == "nonreproducible" + assert "release_readiness.json" in comparison["differing_artifacts"] + assert "output_checksums.tsv" in comparison["excluded_artifacts"] + + +def test_module_can_be_ready_when_reproducibility_has_been_verified_despite_blocked_candidate() -> None: + policy = V050ReleaseClosurePolicy.from_file(ROOT / "release_policy.json") + criteria = [{"criterion_id": "benchmark_count", "mandatory": True, "result": "pass"}] + assert _release_state_before_reproducibility( + policy, "local_real_public_release", criteria, {"decision_state": "blocked_insufficient_evidence"}, + ) == "ready_research_preview_human_review" + + +def test_incompatible_issue506_artifacts_block_module_readiness() -> None: + policy = V050ReleaseClosurePolicy.from_file(ROOT / "release_policy.json") + criteria = [{"criterion_id": "integration_artifact_compatibility", "mandatory": True, "result": "fail"}] + assert _release_state_before_reproducibility( + policy, "local_real_public_release", criteria, {"decision_state": "blocked_incompatible_inputs"}, + ) == "blocked_incompatible_artifacts" + + +def test_unavailable_mandatory_criterion_is_not_a_pass_or_ready_state() -> None: + policy = V050ReleaseClosurePolicy.from_file(ROOT / "release_policy.json") + unavailable = _criterion( + "holdout_coverage", "Holdout coverage is unavailable.", "integration_evidence.json", + "minimum_holdout_coverage", None, ">=", policy.minimum_holdout_coverage, + ) + assert unavailable["result"] == "unavailable" + assert _release_state_before_reproducibility( + policy, "local_real_public_release", [unavailable], {"decision_state": "blocked_insufficient_evidence"}, + ) == "blocked_benchmark_failure" + + +def test_integration_context_rejects_nested_credential_fields(tmp_path: Path) -> None: + unsafe_context = tmp_path / "integration-context.json" + unsafe_context.write_text(json.dumps({"context_identity": "melanoma_anti_pd1:v1", "nested": {"token": "not-permitted"}})) + unsafe_config = replace(config(), references={**config().references, "integration_context": unsafe_context}) + run_release_closure(unsafe_config, "synthetic_fixture", tmp_path / "closure") + compatibility = json.loads((tmp_path / "closure" / "artifact_compatibility.json").read_text()) + assert "integration context contains controlled credential" in compatibility["metrics"]["failure"] + + +def test_nested_scalar_secrets_are_rejected_but_authorization_prose_is_allowed() -> None: + assert not _safe_nested({"nested": "Authorization: Bearer secret-value"}) + assert not _safe_nested(["password=hunter2", "api_key=secret", "-----BEGIN PRIVATE KEY-----"]) + assert not _safe_nested({"nested": {"hidden_reasoning": "not retained"}}) + assert _safe_nested({"limitation": "Human authorization required at the activation authorization boundary."}) + + +def test_relative_traversal_is_rejected_and_absolute_local_reference_is_permitted(tmp_path: Path) -> None: + raw = json.loads((ROOT / "run_config.json").read_text()) + raw["benchmark"] = "../outside.tsv" + escaped = tmp_path / "escaped.json"; escaped.write_text(json.dumps(raw)) + with pytest.raises(ReleaseClosureError, match="escapes"): + V050ReleaseRunConfiguration.from_file(escaped) + raw = json.loads((ROOT / "run_config.json").read_text()) + raw["benchmark"] = str(config().references["benchmark"]) + for key in config().references: + raw[key] = str(config().references[key]) + absolute = tmp_path / "absolute.json"; absolute.write_text(json.dumps(raw)) + assert V050ReleaseRunConfiguration.from_file(absolute).references["benchmark"] == config().references["benchmark"] + + +def test_malformed_manifest_is_a_sanitized_preflight_failure(tmp_path: Path) -> None: + malformed = tmp_path / "manifest.json" + payload = json.loads(config().references["release_manifest"].read_text()) + payload["release_limitations"] = 3 + malformed.write_text(json.dumps(payload)) + altered = replace(config(), references={**config().references, "release_manifest": malformed}) + result = preflight_release(altered, "synthetic_fixture") + assert result["status"] == "failed" + assert result["failures"] == ["release manifest is invalid"] + + +def test_missing_real_data_is_blocked_without_a_successful_closure(tmp_path: Path) -> None: + real = replace(config(), evidence_classification="local_real_public_release", references={**config().references, "data_root": tmp_path / "missing"}) + result = run_release_closure(real, "local_real_public_release", tmp_path / "closure") + assert result["terminal_state"] == "blocked_missing_real_data" + assert result["successful_closure"] is False + + +def test_preflight_rejects_missing_required_input_and_manifest_checksum_mismatch(tmp_path: Path) -> None: + missing = replace(config(), references={**config().references, "target_subset": tmp_path / "missing.tsv"}) + assert preflight_release(missing, "synthetic_fixture")["status"] == "failed" + copied_root = tmp_path / "ingestion" + shutil.copytree(config().references["data_root"], copied_root) + source = copied_root / "gene_effect.csv" + source.write_text(source.read_text() + "\n") + mismatched = replace(config(), references={**config().references, "data_root": copied_root}) + result = preflight_release(mismatched, "synthetic_fixture") + assert result["status"] == "failed" + assert "release manifest local-file validation failed" in result["failures"] + + +def test_fixture_runtime_failure_retains_fixture_terminal_state(tmp_path: Path) -> None: + unsafe_context = tmp_path / "integration-context.json" + unsafe_context.write_text(json.dumps({"context_identity": "melanoma_anti_pd1:v1", "nested": {"token": "not-permitted"}})) + result = run_release_closure(replace(config(), references={**config().references, "integration_context": unsafe_context}), "synthetic_fixture", tmp_path / "closure") + assert result["terminal_state"] == "blocked_fixture_evidence" + assert result["successful_closure"] is False + compatibility = json.loads((tmp_path / "closure" / "artifact_compatibility.json").read_text()) + assert compatibility["metrics"]["failure_category"] == "pipeline_execution_failure" + + +def test_output_checksums_cover_all_non_self_referential_release_artifacts(tmp_path: Path) -> None: + run_release_closure(config(), "synthetic_fixture", tmp_path / "closure") + names = {row["name"] for row in csv.DictReader((tmp_path / "closure" / "output_checksums.tsv").open(), delimiter="\t")} + assert {"release_readiness.json", "activation_readiness_summary.json", "limitations.tsv", "human_release_actions.json", "release_report.md"} <= names diff --git a/tests/test_release_closure_isolation.py b/tests/test_release_closure_isolation.py new file mode 100644 index 0000000..ed2cb40 --- /dev/null +++ b/tests/test_release_closure_isolation.py @@ -0,0 +1,11 @@ +"""Source-level isolation checks for release closure.""" +from pathlib import Path + +def test_release_closure_has_no_network_subprocess_or_dynamic_execution() -> None: + source = Path("targetintel/functional_dependency/release_closure.py").read_text() + for forbidden in ("subprocess", "requests.", "urllib.request", "eval(", "exec(", "importlib"): + assert forbidden not in source + +def test_example_uses_module_api_not_subprocess() -> None: + source = Path("examples/depmap/run_v0_5_release_closure.py").read_text() + assert "run_release_closure" in source and "subprocess" not in source