TIDE / src /report.py
hiasgnpsadgd's picture
Deploy TIDE Docker backend
33d7314 verified
Raw
History Blame Contribute Delete
7.76 kB
from __future__ import annotations
from pathlib import Path
from typing import Any
from module_runner import run_registry
from trial_profile import normalize_trial
DEFAULT_PROJECT_ROOT = Path(__file__).resolve().parents[1]
def build_report(raw_form: dict[str, Any], project_root: Path | None = None) -> dict[str, Any]:
profile = normalize_trial(raw_form)
validation = profile["validation"]
if validation["errors"]:
return {
"ok": False,
"trial_feature_profile": profile,
"modules": {},
"final_output": {
"status": "needs_input",
"message": "Complete required fields before running the evidence summary.",
"plain_language": [
"The protocol entry is incomplete.",
"Fix the required fields shown in validation errors, then run the report again.",
],
},
}
root = _resolve_project_root(project_root)
bundle = run_registry(profile, root)
modules = bundle["modules"]
capabilities = bundle["capabilities"]
pipeline = bundle["pipeline"]
evidence = modules["historical_comparator"]["result"]
final_output = _final_output(profile, modules, capabilities, pipeline)
return {
"ok": True,
"trial_feature_profile": profile,
"modules": modules,
"historical_evidence": evidence,
"final_output": final_output,
}
def _resolve_project_root(project_root: Path | None) -> Path:
if project_root is None:
return DEFAULT_PROJECT_ROOT
resolved = project_root.resolve()
if resolved.is_file() or resolved.suffix.lower() == ".zip":
return resolved.parent
return resolved
def _final_output(
profile: dict[str, Any],
modules: dict[str, Any],
capabilities: dict[str, Any],
pipeline: dict[str, Any],
) -> dict[str, Any]:
evidence = modules["historical_comparator"]["result"]
comparison = evidence["comparison"]
summary = evidence["summary"]
completeness = modules["protocol_completeness"]["result"]
outlook = capabilities.get("publication_outlook", {})
narrative = capabilities.get("narrative_summary", {})
title = profile.get("brief_title") or "Untitled trial"
module_statuses = {
name: {
"status": module.get("status"),
"language": module.get("language"),
"selection": module.get("selection"),
"capability": module.get("capability"),
"provenance": module.get("provenance"),
"reason": module.get("reason"),
"description": module.get("description"),
}
for name, module in modules.items()
}
review_flags = comparison["flags"]
display_flags = review_flags or ["No major review flags from this simple comparator pass."]
return {
"title": title,
"review_priority": comparison["review_priority"],
"takeaway": narrative.get("takeaway", ""),
"summary": narrative.get("summary", []),
"summary_source": narrative.get("source", "template"),
"summary_warning": narrative.get("warning"),
"headline": _headline(profile, evidence, outlook, completeness, comparison),
"actions": narrative.get("actions") or _actions(review_flags, completeness),
"key_numbers": {
"planned_enrollment": profile.get("enrollment"),
"comparator_median_enrollment": summary["median_enrollment"],
"planned_facilities": profile.get("number_of_facilities"),
"comparator_median_facilities": summary["median_facilities"],
"planned_arms": profile.get("number_of_arms"),
"comparator_median_arms": summary["median_arms"],
"planned_primary_outcomes": profile.get("number_of_primary_outcomes"),
"comparator_median_primary_outcomes": summary["median_primary_outcomes"],
"planned_secondary_outcomes": profile.get("number_of_secondary_outcomes"),
"comparator_median_secondary_outcomes": summary["median_secondary_outcomes"],
"comparator_publication_rate": summary["publication_rate"],
"comparator_results_reported_rate": summary["results_reported_rate"],
"comparator_median_time_to_publication_days": summary["median_time_to_publication_days"],
},
"review_flags": display_flags,
"module_statuses": module_statuses,
"pipeline": pipeline,
"plain_language": narrative.get("summary", []),
"model_status": outlook.get("provenance_detail", comparison["note"]),
}
def _headline(
profile: dict[str, Any],
evidence: dict[str, Any],
outlook: dict[str, Any],
completeness: dict[str, Any],
comparison: dict[str, Any],
) -> list[dict[str, Any]]:
"""The skimmable top-of-report numbers, most decision-relevant first."""
pub = outlook.get("publication_likelihood")
results = outlook.get("results_reporting_likelihood")
priority = comparison["review_priority"]
priority_tone = {"high": "alert", "focused": "watch", "standard": "good"}.get(priority, "neutral")
return [
{
"label": "Publication likelihood",
"value": _percent(pub),
"raw": pub,
"tone": _rate_tone(pub),
"provenance": outlook.get("provenance_label", "Historical comparator"),
"sub": f"among {outlook.get('basis_rows', evidence.get('used_rows'))} comparable trials",
},
{
"label": "Results reporting likelihood",
"value": _percent(results),
"raw": results,
"tone": _rate_tone(results),
"provenance": outlook.get("provenance_label", "Historical comparator"),
"sub": "of comparable trials posted results",
},
{
"label": "Review priority",
"value": priority.title(),
"raw": priority,
"tone": priority_tone,
"provenance": "Design flags",
"sub": f"{len(comparison['flags'])} flag(s) raised",
},
{
"label": "Protocol completeness",
"value": _percent(completeness["completion_ratio"]),
"raw": completeness["completion_ratio"],
"tone": _rate_tone(completeness["completion_ratio"], good=0.9, watch=0.7),
"provenance": "Protocol entry",
"sub": f"{completeness['filled_fields']}/{completeness['total_fields']} fields",
},
]
def _actions(review_flags: list[str], completeness: dict[str, Any]) -> list[dict[str, Any]]:
"""Actionable improvement items, design review first then completeness gaps."""
actions: list[dict[str, Any]] = [
{"severity": "review", "text": flag} for flag in review_flags
]
for section in completeness["weakest_sections"]:
if section["missing"]:
fields = ", ".join(field.replace("_", " ") for field in section["missing"])
actions.append(
{
"severity": "completeness",
"text": f"Complete the {section['section'].replace('_', ' ')} section — add: {fields}.",
}
)
if not actions:
actions.append({"severity": "ok", "text": "No design or completeness issues flagged in this pass."})
return actions
def _rate_tone(value: float | None, good: float = 0.6, watch: float = 0.35) -> str:
if value is None:
return "neutral"
if value >= good:
return "good"
if value >= watch:
return "watch"
return "alert"
def _percent(value: float | None) -> str:
if value is None:
return "NA"
return f"{round(value * 100)}%"