|
| 1 | +"""Immutable, portable evidence for optional DepMap report rendering. |
| 2 | +
|
| 3 | +This contract consumes already-loaded, small portable records only. It does |
| 4 | +not read files, discover an environment, calculate profiles, or alter ranks. |
| 5 | +""" |
| 6 | +from __future__ import annotations |
| 7 | + |
| 8 | +from dataclasses import dataclass |
| 9 | +from hashlib import sha256 |
| 10 | +import json |
| 11 | +import math |
| 12 | +from pathlib import PurePosixPath |
| 13 | +import re |
| 14 | +from types import MappingProxyType |
| 15 | +from typing import Any, Mapping |
| 16 | + |
| 17 | + |
| 18 | +REPORT_EVIDENCE_FORMAT_VERSION = "v1" |
| 19 | +_COVERAGE_STATUSES = frozenset({ |
| 20 | + "not_available", "sufficient_complete_coverage", "sufficient_partial_coverage", |
| 21 | + "no_context_models", "context_models_all_values_missing", |
| 22 | + "insufficient_measured_context_models", "insufficient_measured_reference_models", |
| 23 | + "target_unresolved", "target_present_only_gene_effect", |
| 24 | + "target_present_only_dependency_probability", |
| 25 | +}) |
| 26 | +_LOCAL_PATH = re.compile(r"(?:/home/|/media/|/mnt/|/tmp/|/Users/|/Volumes/|(?:^|[^A-Za-z])[A-Za-z]:[\\/])") |
| 27 | + |
| 28 | + |
| 29 | +def _fail(field: str, message: str) -> None: |
| 30 | + raise ValueError(f"{field} {message}") |
| 31 | + |
| 32 | + |
| 33 | +def _text(value: Any, field: str) -> str: |
| 34 | + if not isinstance(value, str) or not value.strip(): |
| 35 | + _fail(field, "must be a non-empty string") |
| 36 | + return value |
| 37 | + |
| 38 | + |
| 39 | +def _freeze(value: Any, field: str = "structured value") -> Any: |
| 40 | + if isinstance(value, Mapping): |
| 41 | + frozen: dict[str, Any] = {} |
| 42 | + for key, item in value.items(): |
| 43 | + if not isinstance(key, str): |
| 44 | + _fail(field, "mapping keys must be strings") |
| 45 | + frozen[key] = _freeze(item, field) |
| 46 | + return MappingProxyType({key: frozen[key] for key in sorted(frozen)}) |
| 47 | + if isinstance(value, (list, tuple)): |
| 48 | + return tuple(_freeze(item, field) for item in value) |
| 49 | + if isinstance(value, bool) or value is None or isinstance(value, str) or isinstance(value, int): |
| 50 | + return value |
| 51 | + if isinstance(value, float): |
| 52 | + if not math.isfinite(value): |
| 53 | + _fail(field, "must not contain NaN or infinity") |
| 54 | + return value |
| 55 | + _fail(field, f"contains unsupported value type {type(value).__name__}") |
| 56 | + |
| 57 | + |
| 58 | +def _plain(value: Any) -> Any: |
| 59 | + if isinstance(value, Mapping): |
| 60 | + return {key: _plain(item) for key, item in value.items()} |
| 61 | + if isinstance(value, tuple): |
| 62 | + return [_plain(item) for item in value] |
| 63 | + return value |
| 64 | + |
| 65 | + |
| 66 | +def _canonical(value: Any) -> str: |
| 67 | + return json.dumps(_plain(_freeze(value)), sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False) |
| 68 | + |
| 69 | + |
| 70 | +def _integer(value: Any, field: str) -> int | None: |
| 71 | + if value is None: |
| 72 | + return None |
| 73 | + if not isinstance(value, int) or isinstance(value, bool) or value < 0: |
| 74 | + _fail(field, "must be a non-negative integer or null") |
| 75 | + return value |
| 76 | + |
| 77 | + |
| 78 | +def _rank(value: Any, field: str) -> int | None: |
| 79 | + result = _integer(value, field) |
| 80 | + if result == 0: |
| 81 | + _fail(field, "must be a positive integer or null") |
| 82 | + return result |
| 83 | + |
| 84 | + |
| 85 | +def _number(value: Any, field: str) -> float | int | None: |
| 86 | + if value is None: |
| 87 | + return None |
| 88 | + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value): |
| 89 | + _fail(field, "must be a finite number or null") |
| 90 | + return value |
| 91 | + |
| 92 | + |
| 93 | +def _portable(value: Any, field: str) -> None: |
| 94 | + if isinstance(value, Mapping): |
| 95 | + for key, item in value.items(): |
| 96 | + _portable(key, field); _portable(item, field) |
| 97 | + elif isinstance(value, (tuple, list)): |
| 98 | + for item in value: |
| 99 | + _portable(item, field) |
| 100 | + elif isinstance(value, str) and _LOCAL_PATH.search(value): |
| 101 | + _fail(field, "must not contain a local path") |
| 102 | + |
| 103 | + |
| 104 | +def _artifact_name(value: Any) -> str: |
| 105 | + value = _text(value, "provenance.source_artifact_names") |
| 106 | + if "\\" in value or "://" in value: |
| 107 | + _fail("provenance.source_artifact_names", "must contain portable relative artifact names") |
| 108 | + path = PurePosixPath(value) |
| 109 | + if path.is_absolute() or ".." in path.parts or path.name in {"", "."}: |
| 110 | + _fail("provenance.source_artifact_names", "must contain portable relative artifact names") |
| 111 | + return value |
| 112 | + |
| 113 | + |
| 114 | +@dataclass(frozen=True) |
| 115 | +class DependencyReportEvidence: |
| 116 | + """Read-only report boundary for one target's validated DepMap evidence. |
| 117 | +
|
| 118 | + ``rank_delta`` follows the bounded-overlay convention as |
| 119 | + ``dependency_aware_candidate_rank - baseline_rank``: a negative value is |
| 120 | + an observed improvement. It is validated only; no rank is calculated. |
| 121 | + """ |
| 122 | + |
| 123 | + format_version: str |
| 124 | + evidence_id: str |
| 125 | + release_identifier: str |
| 126 | + release_manifest_id: str |
| 127 | + configuration_id: str |
| 128 | + scientific_closure_identity: str |
| 129 | + context_identity: str |
| 130 | + gene_symbol: str |
| 131 | + canonical_gene_identity: str | None |
| 132 | + profile_available: bool |
| 133 | + coverage_status: str |
| 134 | + model_count: int | None |
| 135 | + context_model_count: int | None |
| 136 | + reference_model_count: int | None |
| 137 | + available_context_observations: int | None |
| 138 | + available_reference_observations: int | None |
| 139 | + coverage_fraction: float | int | None |
| 140 | + missing_value_state: str | None |
| 141 | + unavailable_reason: str | None |
| 142 | + gene_effect: Mapping[str, Any] | None |
| 143 | + dependency_probability: Mapping[str, Any] | None |
| 144 | + context_reference_comparison: Mapping[str, Any] | None |
| 145 | + selectivity: Mapping[str, Any] | None |
| 146 | + dependency_interpretation_state: str | None |
| 147 | + baseline_rank: int | None |
| 148 | + dependency_aware_candidate_rank: int | None |
| 149 | + rank_delta: int | None |
| 150 | + integration_state: str | None |
| 151 | + baseline_preserved: bool |
| 152 | + production_activation_enabled: bool |
| 153 | + approved_authorization_emitted: bool |
| 154 | + candidate_activation_readiness: str | None |
| 155 | + human_review_required: bool |
| 156 | + limitations: tuple[str, ...] | list[str] |
| 157 | + provenance: Mapping[str, Any] |
| 158 | + |
| 159 | + def __post_init__(self) -> None: |
| 160 | + if self.format_version != REPORT_EVIDENCE_FORMAT_VERSION: |
| 161 | + _fail("format_version", "is unsupported") |
| 162 | + for name in ("release_identifier", "release_manifest_id", "configuration_id", "scientific_closure_identity", "context_identity", "gene_symbol"): |
| 163 | + _text(getattr(self, name), name) |
| 164 | + if not isinstance(self.profile_available, bool): _fail("profile_available", "must be boolean") |
| 165 | + if self.coverage_status not in _COVERAGE_STATUSES: _fail("coverage_status", "is invalid") |
| 166 | + for name in ("model_count", "context_model_count", "reference_model_count", "available_context_observations", "available_reference_observations"): |
| 167 | + object.__setattr__(self, name, _integer(getattr(self, name), name)) |
| 168 | + object.__setattr__(self, "coverage_fraction", _number(self.coverage_fraction, "coverage_fraction")) |
| 169 | + if self.coverage_fraction is not None and not 0 <= self.coverage_fraction <= 1: |
| 170 | + _fail("coverage_fraction", "must be between 0 and 1") |
| 171 | + for observations, models, label in ((self.available_context_observations, self.context_model_count, "available_context_observations"), (self.available_reference_observations, self.reference_model_count, "available_reference_observations")): |
| 172 | + if observations is not None and models is not None and observations > models: |
| 173 | + _fail(label, "must not exceed its model count") |
| 174 | + if self.canonical_gene_identity is not None: _text(self.canonical_gene_identity, "canonical_gene_identity") |
| 175 | + for name in ("missing_value_state", "unavailable_reason", "dependency_interpretation_state", "integration_state", "candidate_activation_readiness"): |
| 176 | + value = getattr(self, name) |
| 177 | + if value is not None: _text(value, name) |
| 178 | + structured = ("gene_effect", "dependency_probability", "context_reference_comparison", "selectivity") |
| 179 | + for name in structured: |
| 180 | + value = getattr(self, name) |
| 181 | + if value is not None and not isinstance(value, Mapping): _fail(name, "must be a mapping or null") |
| 182 | + object.__setattr__(self, name, None if value is None else _freeze(value, name)) |
| 183 | + unavailable = not self.profile_available or self.coverage_status == "not_available" |
| 184 | + if unavailable: |
| 185 | + if self.profile_available or self.coverage_status != "not_available": |
| 186 | + _fail("coverage_status", "not_available must describe an unavailable profile") |
| 187 | + if any(getattr(self, name) is not None for name in structured): _fail("profile_available", "unavailable profiles must not contain dependency metrics") |
| 188 | + if self.unavailable_reason is None: _fail("unavailable_reason", "is required for an unavailable profile") |
| 189 | + if any(getattr(self, name) is not None for name in ("canonical_gene_identity", "model_count", "context_model_count", "reference_model_count", "available_context_observations", "available_reference_observations", "coverage_fraction")): |
| 190 | + _fail("profile_available", "unavailable profiles must use explicit absent coverage values") |
| 191 | + else: |
| 192 | + if not self.canonical_gene_identity: _fail("canonical_gene_identity", "is required for an available profile") |
| 193 | + if self.model_count is None or self.context_model_count is None or self.reference_model_count is None or self.available_context_observations is None or self.available_reference_observations is None or self.coverage_fraction is None: |
| 194 | + _fail("coverage", "available profiles require counts and coverage_fraction") |
| 195 | + if self.model_count < self.context_model_count + self.reference_model_count: |
| 196 | + _fail("model_count", "must cover context and reference model counts") |
| 197 | + for name in ("baseline_rank", "dependency_aware_candidate_rank"): |
| 198 | + object.__setattr__(self, name, _rank(getattr(self, name), name)) |
| 199 | + if self.rank_delta is not None and (not isinstance(self.rank_delta, int) or isinstance(self.rank_delta, bool)): |
| 200 | + _fail("rank_delta", "must be an integer or null") |
| 201 | + if self.baseline_rank is not None and self.dependency_aware_candidate_rank is not None: |
| 202 | + expected = self.dependency_aware_candidate_rank - self.baseline_rank |
| 203 | + if self.rank_delta != expected: _fail("rank_delta", "does not equal dependency_aware_candidate_rank minus baseline_rank") |
| 204 | + elif self.rank_delta is not None: _fail("rank_delta", "requires both ranks") |
| 205 | + if self.baseline_preserved is not True: _fail("baseline_preserved", "must be true") |
| 206 | + if self.production_activation_enabled is not False: _fail("production_activation_enabled", "must be false") |
| 207 | + if self.approved_authorization_emitted is not False: _fail("approved_authorization_emitted", "must be false") |
| 208 | + if self.human_review_required is not True: _fail("human_review_required", "must be true") |
| 209 | + limitations = tuple(self.limitations) |
| 210 | + if not limitations or any(not isinstance(item, str) or not item.strip() for item in limitations): _fail("limitations", "must contain non-empty strings") |
| 211 | + object.__setattr__(self, "limitations", tuple(sorted(set(limitations)))) |
| 212 | + if not isinstance(self.provenance, Mapping): _fail("provenance", "must be a mapping") |
| 213 | + frozen_provenance = _freeze(self.provenance, "provenance"); _portable(frozen_provenance, "provenance") |
| 214 | + artifacts = frozen_provenance.get("source_artifact_names") |
| 215 | + if not isinstance(artifacts, tuple) or not artifacts: |
| 216 | + _fail("provenance.source_artifact_names", "must be a non-empty sequence") |
| 217 | + normalized_artifacts = tuple(sorted(set(_artifact_name(item) for item in artifacts))) |
| 218 | + object.__setattr__(self, "provenance", MappingProxyType({ |
| 219 | + **{key: value for key, value in frozen_provenance.items() if key != "source_artifact_names"}, |
| 220 | + "source_artifact_names": normalized_artifacts, |
| 221 | + })) |
| 222 | + if self.evidence_id != self._identity(): _fail("evidence_id", "does not match deterministic scientific payload") |
| 223 | + |
| 224 | + def identity_payload(self) -> dict[str, Any]: |
| 225 | + return {name: _plain(getattr(self, name)) for name in self.__dataclass_fields__ if name not in {"evidence_id", "provenance"}} |
| 226 | + |
| 227 | + def _identity(self) -> str: |
| 228 | + return "drep_" + sha256(_canonical(self.identity_payload()).encode("utf-8")).hexdigest() |
| 229 | + |
| 230 | + @classmethod |
| 231 | + def create(cls, **values: Any) -> "DependencyReportEvidence": |
| 232 | + values = dict(values) |
| 233 | + values.pop("evidence_id", None) |
| 234 | + if "limitations" in values: |
| 235 | + values["limitations"] = tuple(sorted(set(values["limitations"]))) |
| 236 | + provisional = dict(values) |
| 237 | + provisional["evidence_id"] = "" |
| 238 | + # Freeze/normalize through a temporary identity payload without |
| 239 | + # retaining caller-owned values. |
| 240 | + payload = {name: provisional[name] for name in cls.__dataclass_fields__ if name not in {"evidence_id", "provenance"}} |
| 241 | + values["evidence_id"] = "drep_" + sha256(_canonical(payload).encode("utf-8")).hexdigest() |
| 242 | + return cls(**values) |
| 243 | + |
| 244 | + def to_dict(self) -> dict[str, Any]: |
| 245 | + return {name: _plain(getattr(self, name)) for name in self.__dataclass_fields__} |
| 246 | + |
| 247 | + def canonical_json(self) -> str: |
| 248 | + return _canonical(self.to_dict()) |
| 249 | + |
| 250 | + @classmethod |
| 251 | + def from_dict(cls, data: Mapping[str, Any]) -> "DependencyReportEvidence": |
| 252 | + if not isinstance(data, Mapping): _fail("data", "must be a mapping") |
| 253 | + fields = set(cls.__dataclass_fields__) |
| 254 | + if set(data) != fields: _fail("data", "has unknown or missing contract fields") |
| 255 | + return cls(**dict(data)) |
| 256 | + |
| 257 | + |
| 258 | +def build_dependency_report_evidence(*, release_summary: Mapping[str, Any], profile_record: Mapping[str, Any] | None, |
| 259 | + overlay_record: Mapping[str, Any] | None, provenance: Mapping[str, Any]) -> DependencyReportEvidence: |
| 260 | + """Pure adapter from explicit Issue 508-style portable records.""" |
| 261 | + if not isinstance(release_summary, Mapping) or not isinstance(provenance, Mapping): _fail("input", "must be mappings") |
| 262 | + profile = {} if profile_record is None else dict(profile_record) |
| 263 | + overlay = {} if overlay_record is None else dict(overlay_record) |
| 264 | + for field in ("release_manifest_id", "context_identity"): |
| 265 | + profile_value = profile.get(field) |
| 266 | + summary_value = release_summary.get(field) |
| 267 | + if profile_value is not None and summary_value is not None and profile_value != summary_value: |
| 268 | + _fail(field, "is incompatible between profile_record and release_summary") |
| 269 | + payload = profile.get("payload", profile) |
| 270 | + if not isinstance(payload, Mapping): _fail("profile_record", "payload must be a mapping") |
| 271 | + target = profile.get("target_identity", {}) |
| 272 | + if not isinstance(target, Mapping): target = {} |
| 273 | + status = payload.get("coverage_status", "not_available") |
| 274 | + available = status != "not_available" |
| 275 | + coverage = payload.get("model_coverage", {}) if isinstance(payload.get("model_coverage", {}), Mapping) else {} |
| 276 | + summaries = payload.get("summaries", {}) if isinstance(payload.get("summaries", {}), Mapping) else {} |
| 277 | + context = summaries.get("context", {}) if isinstance(summaries.get("context", {}), Mapping) else {} |
| 278 | + reference = summaries.get("non_context", {}) if isinstance(summaries.get("non_context", {}), Mapping) else {} |
| 279 | + effect = context.get("gene_effect") if isinstance(context, Mapping) else None |
| 280 | + probability = context.get("dependency_probability") if isinstance(context, Mapping) else None |
| 281 | + context_observations = effect.get("measured_model_count") if isinstance(effect, Mapping) else None |
| 282 | + reference_effect = reference.get("gene_effect") if isinstance(reference, Mapping) else None |
| 283 | + reference_observations = reference_effect.get("measured_model_count") if isinstance(reference_effect, Mapping) else None |
| 284 | + context_models = coverage.get("context_model_count") |
| 285 | + coverage_fraction = None if context_models in (None, 0) or context_observations is None else context_observations / context_models |
| 286 | + gene_symbol = target.get("normalized_request") or profile.get("target") or release_summary.get("gene_symbol") |
| 287 | + canonical = target.get("canonical_identity") or target.get("matched_original_source_label") or profile.get("canonical_gene_identity") |
| 288 | + overlay_canonical = overlay.get("canonical_target_identity") |
| 289 | + if overlay_canonical is not None and canonical is not None and overlay_canonical != canonical: |
| 290 | + _fail("overlay_record.canonical_target_identity", "is incompatible with profile_record target identity") |
| 291 | + overlay_original = overlay.get("original_target_identifier") |
| 292 | + if overlay_original is not None and gene_symbol is not None and overlay_original != gene_symbol: |
| 293 | + _fail("overlay_record.original_target_identifier", "is incompatible with profile_record target identity") |
| 294 | + values = { |
| 295 | + "format_version": REPORT_EVIDENCE_FORMAT_VERSION, "release_identifier": release_summary.get("release_identifier"), "release_manifest_id": release_summary.get("release_manifest_id"), "configuration_id": release_summary.get("configuration_id"), "scientific_closure_identity": release_summary.get("scientific_closure_identity"), "context_identity": release_summary.get("context_identity"), "gene_symbol": gene_symbol, "canonical_gene_identity": canonical if available else None, "profile_available": available, "coverage_status": status, "model_count": coverage.get("pan_cancer_model_count"), "context_model_count": context_models, "reference_model_count": coverage.get("non_context_model_count"), "available_context_observations": context_observations, "available_reference_observations": reference_observations, "coverage_fraction": coverage_fraction, "missing_value_state": payload.get("matrix_coverage_status"), "unavailable_reason": None if available else payload.get("target_resolution_status", "profile_not_available"), "gene_effect": effect if available else None, "dependency_probability": probability if available else None, "context_reference_comparison": payload.get("contrasts") if available else None, "selectivity": payload.get("empirical_context_lineage_position") if available else None, "dependency_interpretation_state": profile.get("terminal_status"), "baseline_rank": overlay.get("baseline_rank"), "dependency_aware_candidate_rank": overlay.get("candidate_rank"), "rank_delta": None, "integration_state": release_summary.get("integration_state"), "baseline_preserved": release_summary.get("baseline_preserved"), "production_activation_enabled": release_summary.get("production_activation_enabled"), "approved_authorization_emitted": release_summary.get("approved_authorization_emitted"), "candidate_activation_readiness": release_summary.get("candidate_activation_readiness"), "human_review_required": release_summary.get("human_review_required"), "limitations": tuple(release_summary.get("limitations", ())) + tuple(payload.get("limitations", ())), "provenance": provenance, |
| 296 | + } |
| 297 | + if values["baseline_rank"] is not None and values["dependency_aware_candidate_rank"] is not None: |
| 298 | + values["rank_delta"] = values["dependency_aware_candidate_rank"] - values["baseline_rank"] |
| 299 | + return DependencyReportEvidence.create(**values) |
0 commit comments