115 lines
4.6 KiB
Python
115 lines
4.6 KiB
Python
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", "<br>")
|