Every llm/agent stage that can die persistently now falls back to a machine-readable PIPELINE-FAULT sentinel instead of killing the graph: - review_domain -> branch-local domain_fault marker (banner-prefixed domain_report the map collects; completeness_gate precedent) - verify -> verdict (verifier fault renders findings unverified + a PIPELINE-FAULT attention line) - parse -> parse_fault -> render (complete NEEDS-HUMAN verdict_out; render's no-changes stub is bypassed for fault verdicts) - refine_groups -> cover_gate (deterministic-proposal fallback absorbs the failure; degradation noted in groups_note) - aux_lanes -> aux_fault -> synthesize (aux_note fault marker; visible, never blocking - aux is enrichment) - synthesize -> verdict (synth_failure marker; keyed off the failure prefix + non-empty diff, never findings==[], so clean zero-finding reviews stay MERGE-READY) verdict.py gains unified fault accounting (R2): any PIPELINE-FAULT in domain_reports / verifier_output / state fault keys forces NEEDS-HUMAN, prefix-anchored so a report merely quoting the marker mid-text never trips it; old 'PIPELINE FAULT' spelling migrated. Faults are prepended to attention so the [:5] cap can never hide one. Dedicated *_failure state keys capture engine failure text on every fallback+output_schema node, keeping the bundled warning baseline at zero. 15 python3-guarded R5 tests: fault paths, quoted-marker precedence, happy-path byte-compat regression, marker normalization, crash-path fail-closed (verdict/cover_gate/domain_fault), verifier-fault-on-empty- diff render, and the no-changes stub regression.
117 lines
4.6 KiB
Python
117 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Deterministic report assembly for the code-reviewer graph.
|
|
|
|
Builds the standard report format from the structured findings and prose
|
|
sections. Pure templating — no judgment, no re-summarization, no finding
|
|
ever added or removed here.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
|
|
def load_state():
|
|
if path := os.environ.get("GRAPH_STATE_FILE"):
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
|
|
|
|
|
def main():
|
|
state = load_state()
|
|
v = state.get("verdict_out") or {}
|
|
findings = v.get("findings_final") or []
|
|
counts = v.get("counts") or {}
|
|
changed = state.get("changed_files") or []
|
|
rigor = state.get("resolved_rigor") or "production"
|
|
|
|
# A fault can leave changed_files empty (a parse fault dies before the
|
|
# diff is resolved; a verifier fault fires even on an empty diff) — a
|
|
# fault-degraded verdict must still render as a full report. parse_fault
|
|
# puts the marker in the reason; verdict.py prepends it to attention.
|
|
faulted = str(v.get("reason") or "").startswith("PIPELINE-FAULT:") or any(
|
|
isinstance(a, str) and a.startswith("PIPELINE-FAULT:") for a in v.get("attention") or []
|
|
)
|
|
if not changed and not faulted:
|
|
print(json.dumps({"final_report": "# Code Review Summary\n\nNo changes to review."}))
|
|
return
|
|
|
|
verdict = v.get("verdict", "NEEDS-HUMAN")
|
|
lines = [
|
|
"# Code Review Summary",
|
|
"",
|
|
f"**Verdict: {verdict}** — {v.get('reason', '')}",
|
|
"",
|
|
"## Walkthrough",
|
|
state.get("walkthrough") or "(no walkthrough provided)",
|
|
"",
|
|
"## Changes",
|
|
"",
|
|
"| File | Changes | Findings |",
|
|
"|------|---------|----------|",
|
|
]
|
|
|
|
desc_by_file = {}
|
|
for row in state.get("changes_rows") or []:
|
|
if isinstance(row, dict) and row.get("file"):
|
|
desc_by_file[row["file"]] = row.get("desc", "")
|
|
blocking = [f for f in findings if f.get("section") == "blocking"]
|
|
deferred = [f for f in findings if f.get("section") == "deferred"]
|
|
for f in changed:
|
|
per = [x for x in blocking if x.get("file") == f]
|
|
tally = " ".join(
|
|
f"{icon} {n}" for icon, n in
|
|
((i, sum(1 for x in per if x.get("icon") == i)) for i in ("🔴", "🟡", "🟢", "💡"))
|
|
if n
|
|
) or "—"
|
|
lines.append(f"| `{f}` | {desc_by_file.get(f, '')} | {tally} |")
|
|
|
|
lines += ["", "## Detailed Findings", ""]
|
|
by_file = {}
|
|
for x in blocking:
|
|
by_file.setdefault(x.get("file", "(unattributed)"), []).append(x)
|
|
if by_file:
|
|
for f in sorted(by_file):
|
|
lines += [f"### `{f}`", ""]
|
|
for x in by_file[f]:
|
|
lines += [x.get("block", ""), ""]
|
|
else:
|
|
lines += ["No blocking findings.", ""]
|
|
|
|
lines += ["## Cross-File Concerns", state.get("cross_file") or "None", ""]
|
|
if (state.get("operational_history") or "").strip():
|
|
lines += ["## Operational history", state["operational_history"], ""]
|
|
if (state.get("org_context_section") or "").strip():
|
|
lines += ["## Org context", state["org_context_section"], ""]
|
|
if deferred and rigor != "production":
|
|
lines += ["## Deferred by quality bar", ""]
|
|
for x in deferred:
|
|
lines += [x.get("block", ""), ""]
|
|
attention = v.get("attention") or []
|
|
if verdict == "NEEDS-HUMAN" and attention:
|
|
lines += ["## Human attention required", ""]
|
|
lines += [f"- {a}" for a in attention]
|
|
lines += [""]
|
|
|
|
miss_ids = sorted({
|
|
m for x in findings for m in
|
|
__import__("re").findall(r"MISS-\d+", (x.get("title") or "") + (x.get("block") or ""))
|
|
})
|
|
lines += [
|
|
"---",
|
|
f"*Reviewed {len(changed)} files, found {counts.get('🔴', 0)} critical, "
|
|
f"{counts.get('🟡', 0)} warnings, {counts.get('🟢', 0)} suggestions, "
|
|
f"{counts.get('💡', 0)} nitpicks ({v.get('deferred_count', 0)} deferred by quality bar)*",
|
|
f"*Quality bar: {rigor} — provenance: {state.get('bar_provenance', '?')}; "
|
|
f"surfaces: {state.get('resolved_surfaces') or 'none'}*",
|
|
f"*Verdict: {verdict}; verifier: dropped {v.get('dropped_count', 0)} provably-false"
|
|
+ (f" ({', '.join(v.get('dropped_titles', [])[:5])})" if v.get("dropped_count") else "")
|
|
+ f"; review-miss patterns applied: {', '.join(miss_ids) if miss_ids else 'none relevant'}*",
|
|
]
|
|
print(json.dumps({"final_report": "\n".join(lines)}))
|
|
|
|
|
|
try:
|
|
main()
|
|
except Exception as e: # noqa: BLE001
|
|
print(json.dumps({"final_report": f"# Code Review Summary\n\n**Verdict: NEEDS-HUMAN** — report rendering error: {e}"}))
|