from tht.execute import ExecResult, PlanSummary from tht.sqlcheck import CheckResult NOTE_HEADING = "## Note del reviewer" # Bound del report (D16): celle di dati clinici (referti, anamnesi) possono essere # enormi; senza limiti il report.md esplode (il caso 344KB segnalato) e i '|'/newline # nei dati corrompono la tabella markdown. _MAX_CELL_CHARS = 200 _MAX_TABLE_ROWS = 50 def _escape_cell(value: object) -> str: """Rende una cella sicura per il markdown: niente '|' o newline grezzi, larghezza limitata. Evita corruzione della tabella e injection di markdown da dati DB.""" s = str(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ") if len(s) > _MAX_CELL_CHARS: s = s[:_MAX_CELL_CHARS] + "…" return s def _markdown_table(columns: list[str], rows: list[tuple]) -> str: lines = [ "| " + " | ".join(_escape_cell(c) for c in columns) + " |", "|" + "---|" * len(columns), ] for row in rows[:_MAX_TABLE_ROWS]: lines.append("| " + " | ".join(_escape_cell(v) for v in row) + " |") if len(rows) > _MAX_TABLE_ROWS: lines.append(f"| _… {len(rows) - _MAX_TABLE_ROWS} righe in più non mostrate …_ |") return "\n".join(lines) def render_validation_report( *, session_id: str, check: CheckResult, plan: PlanSummary, plan_warnings: list[str], result: ExecResult, runtime_warnings: list[str], static_warnings: list[str], limit: int, reviewer_notes: str = "", ) -> str: """Sezioni fattuali del validation report. La sezione note e' di Pi/reviewer: se gia' scritta, va passata in reviewer_notes per preservarla.""" def warn(ws: list[str]) -> str: return "\n".join(f"- warning: {w}" for w in ws) if ws else "- nessun warning" parts = [ f"# Validation report — {session_id}", "", "> Avvertenza: questo report contiene sample di dati del database.", "", "## Parsing e read-only", "", "- parsing: OK" if check.ok else "- parsing: ERRORI", "- statement read-only per struttura: OK" if check.ok else "\n".join(f"- ERRORE: {e}" for e in check.errors), warn(check.warnings + static_warnings), "", "## Piano di esecuzione", "", f"- costo totale stimato: {plan.total_cost}", f"- righe stimate: {plan.plan_rows}", f"- nodi: {', '.join(plan.node_types)}", warn(plan_warnings), "", "## Preview / aggregato", "", f"- tempo di esecuzione: {result.execution_ms} ms (limit {limit})", "", _markdown_table(result.columns, result.rows), "", warn(runtime_warnings), "", NOTE_HEADING, "", reviewer_notes.strip(), "", ] return "\n".join(parts) def extract_reviewer_notes(report_md: str) -> str: """Estrae il contenuto della sezione note da un report esistente. rsplit sull'ULTIMA occorrenza dell'heading: la sezione note e' sempre in coda, e se per caso la stringa dell'heading comparisse nei dati di preview (ora comunque con newline escapati, quindi mai a inizio riga) non corromperebbe l'estrazione.""" if NOTE_HEADING not in report_md: return "" return report_md.rsplit(NOTE_HEADING, 1)[1].strip()