#!/usr/bin/env python3 from __future__ import annotations import argparse import json from pathlib import Path def load_json(path: Path) -> dict: return json.loads(path.read_text()) def locale_rows(payload: dict) -> list[tuple[str, dict]]: summary = payload.get("summary", {}) return [(locale, data) for locale, data in summary.items() if locale != "snippets"] def print_error(payload: dict) -> int: error = payload.get("error") if error: print(f"AUDIT ERROR: {error}") return 2 return 0 def print_summary(payload: dict) -> tuple[int, int]: total_block = 0 total_warn = 0 for locale, data in locale_rows(payload): sev = data.get("by_severity", {}) block = int(sev.get("block", 0) or 0) warn = int(sev.get("warn", 0) or 0) log = int(sev.get("log", 0) or 0) total_block += block total_warn += warn print( f"LOCALE {locale}: issues_found={data.get('issues_found', 0)} " f"issues_remaining={data.get('remaining_issues', 0)} " f"block={block} warn={warn} log={log}" ) return total_block, total_warn def _cta_issue_is_allowed_now(locale: str, issue: dict) -> bool: if issue.get("issue_type") != "cta_language_mismatch": return False if issue.get("severity") != "block": return False try: from mandelblog_content_guard.validators.rules.cta import validate_cta except Exception: return False bad_value = issue.get("bad_value") or "" field_paths = issue.get("field_paths") or [] if not field_paths: return False for field_path in field_paths: if validate_cta(locale, field_path, bad_value): return False return True def effective_block_count(payload: dict) -> tuple[int, int]: """Return (effective_block, ignored_block) after applying allowlists.""" ignored = 0 block = 0 issues = payload.get("issues") or {} for locale, data in locale_rows(payload): locale_issues = issues.get(locale) or [] for issue in locale_issues: if issue.get("severity") != "block": continue if _cta_issue_is_allowed_now(locale, issue): ignored += int(issue.get("count") or 1) continue block += int(issue.get("count") or 1) return block, ignored def print_regressions(current: dict, previous: dict) -> None: prev_summary = {locale: data for locale, data in locale_rows(previous)} regressions = [] for locale, data in locale_rows(current): prev = prev_summary.get(locale, {}) cur_remaining = int(data.get("remaining_issues", 0) or 0) prev_remaining = int(prev.get("remaining_issues", 0) or 0) cur_sev = data.get("by_severity", {}) prev_sev = prev.get("by_severity", {}) delta = { "remaining": cur_remaining - prev_remaining, "block": int(cur_sev.get("block", 0) or 0) - int(prev_sev.get("block", 0) or 0), "warn": int(cur_sev.get("warn", 0) or 0) - int(prev_sev.get("warn", 0) or 0), "log": int(cur_sev.get("log", 0) or 0) - int(prev_sev.get("log", 0) or 0), } if any(value > 0 for value in delta.values()): regressions.append((locale, delta)) if regressions: print("REGRESSIONS:") for locale, delta in regressions: print( f"- {locale}: remaining={delta['remaining']:+d} block={delta['block']:+d} " f"warn={delta['warn']:+d} log={delta['log']:+d}" ) else: print("REGRESSIONS: none") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--json", required=True, help="Current multilingual audit JSON file") parser.add_argument("--previous-json", help="Optional previous audit JSON file for regression comparison") args = parser.parse_args() current = load_json(Path(args.json)) error_status = print_error(current) if error_status: return error_status total_block, total_warn = print_summary(current) effective_block, ignored_block = effective_block_count(current) if ignored_block: print(f"IGNORED: {ignored_block} block issue(s) now allowed by current rules") if args.previous_json: prev_path = Path(args.previous_json) if prev_path.exists(): print_regressions(current, load_json(prev_path)) else: print("REGRESSIONS: previous artifact not found") if effective_block > 0: return 2 if total_warn > 0: return 1 return 0 if __name__ == "__main__": raise SystemExit(main())