From 4521a7df96695c661c0b92a24431485d9f6f693b Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 16 Jun 2026 09:41:20 -0400 Subject: [PATCH] feat(scripts): add --summary and --by-size modes to exception_handling audit --- scripts/audit_exception_handling.py | 114 ++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/scripts/audit_exception_handling.py b/scripts/audit_exception_handling.py index 3306a3a1..1f91467a 100644 --- a/scripts/audit_exception_handling.py +++ b/scripts/audit_exception_handling.py @@ -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: