Skip to content

Commit dd998a3

Browse files
authored
feat(depmap): add immutable report evidence contract (#69)
1 parent d981e8c commit dd998a3

2 files changed

Lines changed: 423 additions & 0 deletions

File tree

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
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

Comments
 (0)