feat(scripts): add --summary and --by-size modes to exception_handling audit

This commit is contained in:
ed
2026-06-16 09:41:20 -04:00
parent 01fbd62a3f
commit 4521a7df96
+109 -5
View File
@@ -43,11 +43,21 @@ the fix could look like (e.g., "return Result(data=NIL_T, errors=[...])").
Usage:
uv run python scripts/audit_exception_handling.py # human report
uv run python python scripts/audit_exception_handling.py --json # JSON output
uv run python python scripts/audit_exception_handling.py --src src
uv run python python scripts/audit_exception_handling.py --top 20
uv run python python scripts/audit_exception_handling.py --verbose
uv run python python scripts/audit_exception_handling.py --strict
uv run python scripts/audit_exception_handling.py --json # JSON output
uv run python scripts/audit_exception_handling.py --src src # source dir
uv run python scripts/audit_exception_handling.py --top 20 # top N files
uv run python scripts/audit_exception_handling.py --verbose # every site
uv run python scripts/audit_exception_handling.py --strict # exit 1 on violation
uv run python scripts/audit_exception_handling.py --summary # per-file summary table
uv run python scripts/audit_exception_handling.py --by-size # group by migration effort
Output modes (mutually exclusive; --json / --summary / --by-size override
the default human-readable report):
--summary: per-file table sorted by V+S descending. Use this for
"which files have the most violations" planning questions.
--by-size: groups files into small/medium/large/baseline buckets.
Use this for "how many migration tracks do I need" planning.
(default): top-N files with per-site breakdown and 1-line hints.
Exit codes:
0 - audit ran (informational mode; findings don't fail the script)
@@ -734,6 +744,90 @@ def render_json(reports: list[FileReport], files_scanned: int, top: int, verbose
return json.dumps(output, indent=2)
def render_summary(reports: list[FileReport], files_scanned: int) -> str:
"""Per-file summary table. Used for planning migration tracks.
Columns: file, total, V (violations), S (suspicious), ? (unclear), C (compliant).
Sorted by V+S descending so the highest-impact files are at the top.
"""
lines: list[str] = []
lines.append("=== Exception Handling Audit: Per-File Summary ===\n")
lines.append(f"Files scanned: {files_scanned}")
lines.append(f"Files with findings: {len(reports)}\n")
lines.append(f"{'file':<38} {'total':>6} {'V':>5} {'S':>5} {'?':>4} {'C':>5} baseline?")
lines.append("-" * 90)
for f in sorted(reports, key=lambda r: -(r.violation_count + r.suspicious_count)):
total = f.violation_count + f.suspicious_count + f.unclear_count + f.compliant_count
if total == 0:
continue
name = f.filename.replace("src/", "").replace("\\", "/")
base = "*BASELINE*" if f.is_refactored_baseline else ""
lines.append(f"{name:<38} {total:>6} {f.violation_count:>5} {f.suspicious_count:>5} {f.unclear_count:>4} {f.compliant_count:>5} {base}")
lines.append("-" * 90)
total_v = sum(r.violation_count for r in reports)
total_s = sum(r.suspicious_count for r in reports)
total_u = sum(r.unclear_count for r in reports)
total_c = sum(r.compliant_count for r in reports)
lines.append(f"{'TOTAL':<38} {total_v + total_s + total_u + total_c:>6} {total_v:>5} {total_s:>5} {total_u:>4} {total_c:>5}")
return "\n".join(lines) + "\n"
def render_by_size(reports: list[FileReport], files_scanned: int) -> str:
"""Group files by violation+suspicious count bucket for migration planning.
Buckets: small (<=5), medium (6-15), large (>=16). Plus the 3 refactored
baseline files as a separate bucket (the convention reference; remaining
gaps should be closed to make them pure compliant).
"""
lines: list[str] = []
lines.append("=== Exception Handling Audit: Files Grouped by Migration Effort ===\n")
lines.append(f"Files scanned: {files_scanned}")
lines.append(f"Files with findings: {len(reports)}\n")
baseline = [r for r in reports if r.is_refactored_baseline]
large = [r for r in reports if not r.is_refactored_baseline and r.violation_count + r.suspicious_count >= 16]
medium = [r for r in reports if not r.is_refactored_baseline and 6 <= r.violation_count + r.suspicious_count <= 15]
small = [r for r in reports if not r.is_refactored_baseline and r.violation_count + r.suspicious_count <= 5]
def _bucket(name: str, files: list[FileReport], note: str) -> None:
if not files:
return
v = sum(r.violation_count for r in files)
s = sum(r.suspicious_count for r in files)
u = sum(r.unclear_count for r in files)
c = sum(r.compliant_count for r in files)
total = v + s + u + c
lines.append(f"--- {name} ({len(files)} files, V+S={v+s}, V={v}, S={s}, ?={u}, C={c}, total={total}) ---")
if note:
lines.append(f" {note}")
for r in sorted(files, key=lambda x: -(x.violation_count + x.suspicious_count)):
name = r.filename.replace("src/", "").replace("\\", "/")
lines.append(f" {name:<36} V={r.violation_count:>3} S={r.suspicious_count:>2} ?={r.unclear_count:>2} C={r.compliant_count:>3} total={len(r.findings)}")
lines.append("")
_bucket(
"LARGE (>=16 V+S; dedicated track per file)",
large,
"Each file is too big for a batched track. 1 track per file; 2-3 days Tier 2 each.",
)
_bucket(
"MEDIUM (6-15 V+S; can group 2-3 files per track)",
medium,
"Each file is independent; can be batched in 1 track per group. 0.5-1 day Tier 2 each.",
)
_bucket(
"SMALL (<=5 V+S; batched in one 'small files' track)",
small,
"Each file is small enough for a single batched track. 0.5-1 day Tier 2 for the whole batch.",
)
_bucket(
"BASELINE (3 refactored files; the convention reference)",
baseline,
"These files ARE the convention. Remaining violations are gaps to close (deferred work from the parent track).",
)
return "\n".join(lines) + "\n"
def main() -> int:
parser = argparse.ArgumentParser(
description=__doc__,
@@ -746,6 +840,8 @@ def main() -> int:
parser.add_argument("--include-tests", action="store_true", help="Also scan tests/ and scripts/")
parser.add_argument("--strict", action="store_true", help="Exit 1 if any violations are found (for CI use)")
parser.add_argument("--include-baseline", action="store_true", help="Include the 3 refactored files in the violation count (default: exclude)")
parser.add_argument("--summary", action="store_true", help="Per-file summary table (for migration planning)")
parser.add_argument("--by-size", action="store_true", help="Group files by migration effort bucket (small/medium/large/baseline)")
parser.add_argument("--exclude", action="append", default=[], help="Additional path components to exclude (can repeat)")
args = parser.parse_args()
@@ -776,6 +872,14 @@ def main() -> int:
total_violations = sum(r.violation_count for r in reports if not r.is_refactored_baseline)
return 1 if (args.strict and total_violations > 0) else 0
if args.summary:
print(render_summary(reports, len(files)))
return 0
if args.by_size:
print(render_by_size(reports, len(files)))
return 0
print(render_human(reports, len(files), args.top, args.verbose))
if args.include_baseline: