"""Render scb-check's JSON report as a Markdown block.
Used by .github/workflows/code-erosion.yml. Reads the report path, the
scb-check exit code, and (optionally) the ast-grep rule-hits path from argv,
writes Markdown to stdout. The workflow feeds that Markdown both into the PR
comment and the job summary.
This never raises on a missing/garbled report or a renamed key: the workflow
is informational and must not fail. See docs/dev/ci-code-erosion.md.
"""
from __future__ import annotations
import json
import sys
from collections import Counter
MARKER = "<!-- code-erosion-report -->"
def band(value: float, human_hi: float, agent_lo: float) -> str:
"""Bucket a metric against the SlopCodeBench reference bands."""
if value <= human_hi:
return "human"
if value >= agent_lo:
return "agent"
return "between"
def rule_hits_section(path: str) -> list[str]:
"""Render the independent ast-grep TS-verbosity layer as Markdown.
Reads ast-grep's `--json=stream` output (one JSON object per line).
Returns an empty list when the file is missing or unreadable, so the
section is simply omitted rather than ever failing the report.
"""
try:
with open(path, encoding="utf-8") as handle:
hits = [json.loads(line) for line in handle if line.strip()]
except (OSError, ValueError):
return []
lines = [
"",
"### Rule hits (TS verbosity layer)",
"",
"_Standalone `ast-grep`, separate from the number above._",
"",
]
if not hits:
lines.append("No rule hits. 🎉")
return lines
by_rule = Counter(h.get("ruleId", "?") for h in hits)
distinct = {
(h.get("file"), h.get("range", {}).get("start", {}).get("line"))
for h in hits
}
lines.append("| Rule | Hits |")
lines.append("|---|---|")
for rule, count in by_rule.most_common():
lines.append(f"| `{rule}` | {count} |")
lines.append("")
lines.append(f"{len(distinct)} distinct lines flagged across `src/`.")
return lines
def main() -> int:
report_path = sys.argv[1]
exit_code = sys.argv[2] if len(sys.argv) > 2 else "0"
rule_hits_path = sys.argv[3] if len(sys.argv) > 3 else ""
out: list[str] = [
MARKER,
"## Code Erosion Report",
"",
"_Informational — **never blocks the merge**. `scb-check==0.2.0` "
"SlopCodeBench metrics; method & caveats in `docs/dev/ci-code-erosion.md`._",
"",
]
try:
with open(report_path, encoding="utf-8") as handle:
data = json.load(handle)
except (OSError, ValueError) as error:
out.append(
f"> scb-check produced no parseable report (exit {exit_code}): "
f"`{error}`",
)
print("\n".join(out))
return 0
if exit_code == "2":
out.append(
"> **Warning:** scb-check exited 2 (path/config error). "
"Numbers below may be incomplete.",
)
out.append("")
try:
verbosity = float(data["verbosity"])
erosion = float(data["erosion"])
cog_erosion = float(data["cog_erosion"])
files_scanned = data["files_scanned"]
total_loc = data["total_loc"]
high_cc = data["high_cc_functions"]
total_functions = data["total_functions"]
except (KeyError, TypeError, ValueError) as error:
out.append(
f"> scb-check report is missing an expected field (`{error}`). "
"The tool version may have changed its JSON schema; update "
"`.github/scripts/erosion-summary.py`.",
)
print("\n".join(out))
return 0
out.extend(
[
"| Metric | Value | Human band | Agent band | Reading |",
"|---|---|---|---|---|",
f"| Verbosity\\* | {verbosity:.3f} | 0.15 | 0.33 | "
f"{band(verbosity, 0.21, 0.23)} |",
f"| Erosion | {erosion:.3f} | 0.31 | 0.68 | "
f"{band(erosion, 0.48, 0.48)} |",
f"| Cognitive erosion | {cog_erosion:.3f} | – | – | – |",
"",
f"Scanned {files_scanned} files / {total_loc} SLOC "
f"· high-CC functions {high_cc}/{total_functions}",
"",
"\\* On TypeScript, `verbosity` is partial and the bands are "
"Python-calibrated.",
],
)
if rule_hits_path:
out.extend(rule_hits_section(rule_hits_path))
print("\n".join(out))
return 0
if __name__ == "__main__":
raise SystemExit(main())