from __future__ import annotations from collections import Counter from claridoc.models import Brief, PipelineConfig, RoundResult def render_run_report( brief: Brief, config: PipelineConfig, rounds: list[RoundResult], warnings: list[str], ) -> str: final = rounds[-1] lines = [ "# ClariDoc quality report", "", f"- Document: **{brief.title}**", f"- Type: `{brief.document_type.value}`", f"- Language: `{brief.language}`", f"- Gate: **{'PASS' if final.passed else 'FAIL'}**", f"- Final composite score: **{final.composite_score:.1f}/100**", f"- Rounds: **{len(rounds)}**", "", "## Provider topology", "", f"- Planner: `{config.planner.provider}`{_model_suffix(config.planner.model)}", f"- Writer: `{config.writer.provider}`{_model_suffix(config.writer.model)}", f"- Reviser: `{config.reviser.provider}`{_model_suffix(config.reviser.model)}", "- Reviewers: " + ", ".join( f"`{reviewer.role}` → `{reviewer.provider.provider}`{_model_suffix(reviewer.provider.model)}" for reviewer in config.reviewers ), "", "## Quality-gate configuration", "", f"- Minimum score: {config.quality_gate.minimum_score:.1f}", f"- Maximum blockers: {config.quality_gate.max_blockers}", f"- Maximum errors: {config.quality_gate.max_errors}", f"- Maximum revisions: {config.quality_gate.max_revisions}", f"- Weights: deterministic {config.quality_gate.deterministic_weight:.0%}, model reviews {config.quality_gate.model_weight:.0%}", "", "## Round history", "", "| Round | Deterministic | Model mean | Composite | Blockers | Errors | Gate |", "|---:|---:|---:|---:|---:|---:|---|", ] for item in rounds: model_mean = sum(review.score for review in item.reviews) / len(item.reviews) if item.reviews else item.lint_report.score lines.append( f"| {item.round_number} | {item.lint_report.score:.1f} | {model_mean:.1f} | " f"{item.composite_score:.1f} | {item.blocker_count} | {item.error_count} | " f"{'PASS' if item.passed else 'FAIL'} |" ) lines.extend(["", "## Final deterministic findings", ""]) if not final.lint_report.issues: lines.append("No deterministic findings.\n") else: counts = Counter(issue.severity.value for issue in final.lint_report.issues) lines.append( ", ".join(f"{name}: {counts.get(name, 0)}" for name in ("blocker", "error", "warning", "info")) ) lines.extend(["", "| Severity | Code | Location | Finding |", "|---|---|---|---|"]) for issue in final.lint_report.issues: location = f"line {issue.line}" if issue.line else (issue.section or "—") message = _escape_table_cell(issue.message) lines.append( f"| {issue.severity.value} | `{issue.code}` | {location} | {message} |" ) lines.extend(["", "## Final independent reviews", ""]) for review in final.reviews: lines.extend([ f"### {review.role} — {review.provider}", "", f"Score: **{review.score:.1f}/100**", "", ]) if review.strengths: lines.append("Strengths: " + "; ".join(review.strengths)) lines.append("") if review.issues: lines.extend(["| Severity | Section | Problem | Correction |", "|---|---|---|---|"]) for issue in review.issues: problem = _escape_table_cell(issue.problem) fix = _escape_table_cell(issue.fix) lines.append( f"| {issue.severity} | {issue.section or '—'} | {problem} | {fix} |" ) lines.append("") else: lines.append("No material issues reported.\n") if warnings: lines.extend(["## Harness warnings", ""]) lines.extend(f"- {warning}" for warning in warnings) lines.append("") lines.extend([ "## Interpretation", "", "A PASS means this run met the configured structural, lint, and model-review gate. It does not replace domain-owner verification, executable code testing, legal review, security review, or independent validation of source truth.", "", ]) return "\n".join(lines) def _model_suffix(model: str) -> str: return f" (`{model}`)" if model else "" def _escape_table_cell(value: str) -> str: return value.replace("|", "\\|").replace("\n", "
")