Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions targetintel/functional_dependency/presentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Pure, read-only presentation of portable DepMap dependency evidence.

This module consumes only :class:`DependencyReportEvidence`. It does not load
snapshots, profiles, matrices, or upstream workflow state.
"""
from __future__ import annotations

from html import escape as html_escape
import json
import math
import re
from typing import Any, Mapping

from .report_contract import DependencyReportEvidence


class DependencyPresentationError(ValueError):
"""Raised when a report-evidence boundary is unsuitable for rendering."""


_FIXED_LIMITATIONS = (
"DepMap cell-line dependency is not clinical anti-PD-1 response evidence.",
"Absence of tumor-cell dependency does not invalidate an immune target.",
"Broad dependency may reflect general essentiality.",
"Cell lines do not reproduce the complete tumor microenvironment.",
"Candidate activation requires explicit human review.",
)


def _evidence(value: Any) -> DependencyReportEvidence:
if not isinstance(value, DependencyReportEvidence):
raise DependencyPresentationError("invalid_dependency_report_evidence")
if (value.baseline_preserved is not True
or value.production_activation_enabled is not False
or value.approved_authorization_emitted is not False
or value.human_review_required is not True):
raise DependencyPresentationError("invalid_dependency_activation_state")
return value


def _release_name(identifier: str) -> str:
"""Convert a machine identifier to the deliberately small display form."""
return identifier.replace("_", " ")


def _markdown_text(value: Any) -> str:
"""Keep untrusted text to one Markdown line and out of Markdown syntax."""
text = str(value).replace("\r", " ").replace("\n", " ")
return (text.replace("\\", "\\\\").replace("`", "\\`")
.replace("<", "&lt;").replace(">", "&gt;"))


def _json(value: Any) -> str:
"""Produce a stable representation of contract-approved structured values."""
try:
return json.dumps(_plain(value), sort_keys=True, separators=(",", ":"),
ensure_ascii=False, allow_nan=False)
except (TypeError, ValueError) as exc:
raise DependencyPresentationError("unsupported_structured_value") from exc


def _plain(value: Any) -> Any:
"""Convert immutable contract containers to JSON-compatible containers."""
if isinstance(value, Mapping):
return {key: _plain(item) for key, item in value.items()}
if isinstance(value, tuple):
return [_plain(item) for item in value]
return value


def _number(value: int | float | None, *, unavailable: bool = False) -> str:
if value is None:
return "not available" if unavailable else "not reported"
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise DependencyPresentationError("unsupported_structured_value")
if isinstance(value, float) and not math.isfinite(value):
raise DependencyPresentationError("unsupported_structured_value")
return _json(value)


def _text(value: str | None, *, unavailable: bool = False) -> str:
if value is None:
return "not available" if unavailable else "not reported"
return _markdown_text(value)


def _markdown_mapping(value: Mapping[str, Any] | None) -> str:
return "not reported" if value is None else _markdown_text(_json(value))


def _limitations(evidence: DependencyReportEvidence) -> tuple[str, ...]:
retained = tuple(sorted(evidence.limitations))
return retained + tuple(item for item in _FIXED_LIMITATIONS if item not in retained)


def render_dependency_markdown(evidence: DependencyReportEvidence) -> str:
"""Render deterministic Markdown without modifying or deriving evidence."""
evidence = _evidence(evidence)
unavailable = not evidence.profile_available
title = _release_name(evidence.release_identifier)
lines = [
f"## Functional dependency — {_markdown_text(title)}", "",
"### Coverage", "",
f"- **Profile available:** {'yes' if evidence.profile_available else 'no'}",
f"- **Coverage status:** {_markdown_text(evidence.coverage_status)}",
f"- **Total model count:** {_number(evidence.model_count, unavailable=unavailable)}",
f"- **Context model count:** {_number(evidence.context_model_count, unavailable=unavailable)}",
f"- **Reference model count:** {_number(evidence.reference_model_count, unavailable=unavailable)}",
f"- **Available context observations:** {_number(evidence.available_context_observations, unavailable=unavailable)}",
f"- **Available reference observations:** {_number(evidence.available_reference_observations, unavailable=unavailable)}",
f"- **Coverage fraction:** {_number(evidence.coverage_fraction, unavailable=unavailable)}",
f"- **Missing-value state:** {_text(evidence.missing_value_state, unavailable=unavailable)}",
f"- **Unavailable reason:** {_text(evidence.unavailable_reason, unavailable=unavailable)}",
"", "### Dependency profile", "",
]
if unavailable:
lines.extend([
"No validated DepMap profile is available for this target in the portable evidence bundle. "
"No dependency conclusion is drawn.",
])
else:
lines.extend([
f"- **Gene-effect summary:** {_markdown_mapping(evidence.gene_effect)}",
f"- **Dependency-probability summary:** {_markdown_mapping(evidence.dependency_probability)}",
f"- **Context-versus-reference comparison:** {_markdown_mapping(evidence.context_reference_comparison)}",
f"- **Selectivity:** {_markdown_mapping(evidence.selectivity)}",
f"- **Dependency interpretation state:** {_text(evidence.dependency_interpretation_state)}",
])
lines.extend([
"", "### Integration", "",
f"- **Baseline rank:** {_number(evidence.baseline_rank)}",
f"- **Dependency-aware candidate rank:** {_number(evidence.dependency_aware_candidate_rank)}",
f"- **Rank delta:** {_number(evidence.rank_delta)}",
"- **Rank-delta convention:** dependency-aware candidate rank minus baseline rank.",
"- **Negative rank delta:** movement toward a lower numerical rank.",
f"- **Integration state:** {_text(evidence.integration_state)}",
"- **Baseline preserved:** yes",
"- **Production activation enabled:** disabled",
"- **Approved authorization emitted:** not emitted",
f"- **Candidate activation readiness:** {_text(evidence.candidate_activation_readiness)}",
"- **Human review required:** required",
"", "### Release provenance", "",
f"- **Evidence ID:** `{_markdown_text(evidence.evidence_id)}`",
f"- **Release identifier:** `{_markdown_text(evidence.release_identifier)}`",
f"- **Release manifest ID:** `{_markdown_text(evidence.release_manifest_id)}`",
f"- **Configuration ID:** `{_markdown_text(evidence.configuration_id)}`",
f"- **Scientific closure identity:** `{_markdown_text(evidence.scientific_closure_identity)}`",
f"- **Context identity:** `{_markdown_text(evidence.context_identity)}`",
f"- **Canonical gene identity:** `{_text(evidence.canonical_gene_identity, unavailable=unavailable)}`",
f"- **Contract format version:** `{_markdown_text(evidence.format_version)}`",
"- **Portable source artifacts:** " + ", ".join(
f"`{_markdown_text(name)}`" for name in evidence.provenance["source_artifact_names"]
),
"", "### Limitations", "",
])
lines.extend(f"- {_markdown_text(item)}" for item in _limitations(evidence))
return "\n".join(lines) + "\n"


def render_dependency_html(evidence: DependencyReportEvidence) -> str:
"""Render the same escaped content in a semantic dedicated HTML section."""
markdown = render_dependency_markdown(evidence)
_, body = markdown.split("\n\n", 1)
sections = re.split(r"\n(?=### )", body.rstrip("\n"))
rendered: list[str] = [
'<section class="card functional-dependency">',
f'<h2>Functional dependency — {html_escape(_release_name(evidence.release_identifier), quote=True)}</h2>',
]
for section in sections:
heading, content = section.split("\n", 1)
if not heading.startswith("### "):
raise DependencyPresentationError("invalid_dependency_presentation_structure")
rendered.append(f"<h3>{html_escape(heading[4:], quote=True)}</h3>")
rendered.append(f'<pre class="note">{html_escape(content, quote=True)}</pre>')
rendered.append("</section>")
return "\n".join(rendered) + "\n"
19 changes: 18 additions & 1 deletion targetintel/html_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
make_feasibility_report_section,
render_feasibility_html,
)
from targetintel.functional_dependency.presentation import (
DependencyPresentationError,
render_dependency_html,
)
from targetintel.functional_dependency.report_contract import DependencyReportEvidence


DEFAULT_HTML_REPORT_DIR = Path("results/html_reports")
Expand Down Expand Up @@ -374,6 +379,7 @@ def make_target_html_report(
evidence_card: EvidenceCard | None = None,
feasibility_annotations: tuple[object, ...] | list[object] | None = None,
feasibility_target_identifier_type: str | None = None,
dependency_evidence: DependencyReportEvidence | None = None,
) -> str:
"""
Generate one standalone HTML target report.
Expand Down Expand Up @@ -401,6 +407,13 @@ def make_target_html_report(
annotations=feasibility_annotations,
))
feasibility_suffix = f"\n\n{feasibility_html}" if feasibility_html else ""
dependency_suffix = ""
if dependency_evidence is not None:
if not isinstance(dependency_evidence, DependencyReportEvidence):
raise DependencyPresentationError("invalid_dependency_report_evidence")
if dependency_evidence.gene_symbol != symbol:
raise DependencyPresentationError("dependency_evidence_target_mismatch")
dependency_suffix = f"\n\n{render_dependency_html(dependency_evidence)}"

html = f"""<!doctype html>
<html lang="en">
Expand Down Expand Up @@ -443,7 +456,7 @@ def make_target_html_report(
</div>
</section>

{_make_evidence_html(evidence_card)}{feasibility_suffix}
{_make_evidence_html(evidence_card)}{feasibility_suffix}{dependency_suffix}

<section class="card">
<h2>Stable TargetIntel-IO classification</h2>
Expand Down Expand Up @@ -588,6 +601,7 @@ def write_target_html_report(
evidence_card: EvidenceCard | None = None,
feasibility_annotations: tuple[object, ...] | list[object] | None = None,
feasibility_target_identifier_type: str | None = None,
dependency_evidence: DependencyReportEvidence | None = None,
) -> Path:
"""
Write one standalone HTML report.
Expand All @@ -603,6 +617,7 @@ def write_target_html_report(
row, evidence_card=evidence_card,
feasibility_annotations=feasibility_annotations,
feasibility_target_identifier_type=feasibility_target_identifier_type,
dependency_evidence=dependency_evidence,
),
encoding="utf-8",
)
Expand Down Expand Up @@ -779,6 +794,7 @@ def write_top_html_reports(
evidence_cards: Mapping[str, EvidenceCard] | None = None,
feasibility_annotations: Mapping[str, tuple[object, ...] | list[object]] | None = None,
feasibility_target_identifier_type: str | None = None,
dependency_evidence_by_symbol: Mapping[str, DependencyReportEvidence] | None = None,
) -> list[Path]:
"""
Write HTML reports for the union of top-N targets across all modes.
Expand Down Expand Up @@ -828,6 +844,7 @@ def write_top_html_reports(
evidence_card=(evidence_cards or {}).get(str(row["target_symbol"])),
feasibility_annotations=target_annotations,
feasibility_target_identifier_type=feasibility_target_identifier_type,
dependency_evidence=(dependency_evidence_by_symbol or {}).get(str(row["target_symbol"])),
))

index_path = write_html_index(
Expand Down
19 changes: 18 additions & 1 deletion targetintel/hypothesis_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
make_feasibility_report_section,
render_feasibility_markdown,
)
from targetintel.functional_dependency.presentation import (
DependencyPresentationError,
render_dependency_markdown,
)
from targetintel.functional_dependency.report_contract import DependencyReportEvidence


DEFAULT_CARD_DIR = Path("results/target_cards")
Expand Down Expand Up @@ -201,6 +206,7 @@ def make_target_card(
evidence_card: EvidenceCard | None = None,
feasibility_annotations: tuple[object, ...] | list[object] | None = None,
feasibility_target_identifier_type: str | None = None,
dependency_evidence: DependencyReportEvidence | None = None,
) -> str:
"""
Generate one Markdown target hypothesis card.
Expand All @@ -226,6 +232,13 @@ def make_target_card(
)
)
feasibility_suffix = f"\n{feasibility_section}" if feasibility_section else ""
dependency_suffix = ""
if dependency_evidence is not None:
if not isinstance(dependency_evidence, DependencyReportEvidence):
raise DependencyPresentationError("invalid_dependency_report_evidence")
if dependency_evidence.gene_symbol != symbol:
raise DependencyPresentationError("dependency_evidence_target_mismatch")
dependency_suffix = f"\n{render_dependency_markdown(dependency_evidence)}"

card = f"""# Target hypothesis card: {symbol}

Expand Down Expand Up @@ -280,7 +293,7 @@ def make_target_card(

This card is generated by TargetIntel-IO as a transparent, rule-based target triage summary. It is intended for hypothesis generation and portfolio demonstration only. It does not represent clinical advice or validated therapeutic evidence.

{make_evidence_card_section(evidence_card)}{feasibility_suffix}
{make_evidence_card_section(evidence_card)}{feasibility_suffix}{dependency_suffix}
"""

return card
Expand All @@ -292,6 +305,7 @@ def write_target_card(
evidence_card: EvidenceCard | None = None,
feasibility_annotations: tuple[object, ...] | list[object] | None = None,
feasibility_target_identifier_type: str | None = None,
dependency_evidence: DependencyReportEvidence | None = None,
) -> Path:
"""
Write one target card to Markdown.
Expand All @@ -307,6 +321,7 @@ def write_target_card(
row, evidence_card=evidence_card,
feasibility_annotations=feasibility_annotations,
feasibility_target_identifier_type=feasibility_target_identifier_type,
dependency_evidence=dependency_evidence,
),
encoding="utf-8",
)
Expand All @@ -321,6 +336,7 @@ def write_top_target_cards(
evidence_cards: Mapping[str, EvidenceCard] | None = None,
feasibility_annotations: Mapping[str, tuple[object, ...] | list[object]] | None = None,
feasibility_target_identifier_type: str | None = None,
dependency_evidence_by_symbol: Mapping[str, DependencyReportEvidence] | None = None,
) -> list[Path]:
"""
Write Markdown target cards for the top targets across all modes.
Expand Down Expand Up @@ -370,6 +386,7 @@ def write_top_target_cards(
evidence_card=(evidence_cards or {}).get(str(row["target_symbol"])),
feasibility_annotations=target_annotations,
feasibility_target_identifier_type=feasibility_target_identifier_type,
dependency_evidence=(dependency_evidence_by_symbol or {}).get(str(row["target_symbol"])),
))

return written_paths
Loading
Loading