audit_review.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #!/usr/bin/env python3
  2. """
  3. audit_review.py — List and group audit feedback by target file.
  4. Usage:
  5. python3 audit_review.py <wiki-root> [--open|--resolved|--all]
  6. Examples:
  7. python3 audit_review.py ~/wikis/ai-research --open
  8. python3 audit_review.py ~/wikis/ai-research --resolved
  9. python3 audit_review.py ~/wikis/ai-research --all
  10. Reads every file under `<wiki-root>/audit/` (open) and `<wiki-root>/audit/resolved/`
  11. (resolved), parses the YAML frontmatter, and prints a report grouped by target
  12. file. Use this at the start of an `audit` operation to decide processing order.
  13. Exit codes:
  14. 0 — done (always, regardless of audit count)
  15. """
  16. import os
  17. import re
  18. import sys
  19. from collections import defaultdict
  20. from pathlib import Path
  21. FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
  22. def parse_frontmatter(text: str) -> dict | None:
  23. m = FRONTMATTER_RE.match(text)
  24. if not m:
  25. return None
  26. body = m.group(1)
  27. result: dict = {}
  28. for line in body.split("\n"):
  29. if not line.strip() or line.lstrip().startswith("#"):
  30. continue
  31. if ":" not in line:
  32. continue
  33. key, _, rest = line.partition(":")
  34. key = key.strip()
  35. val = rest.strip()
  36. if val.startswith("[") and val.endswith("]"):
  37. inner = val[1:-1].strip()
  38. result[key] = [p.strip().strip('"').strip("'") for p in inner.split(",") if p.strip()]
  39. elif val.startswith('"') and val.endswith('"'):
  40. result[key] = val[1:-1].replace("\\n", "\n").replace('\\"', '"')
  41. elif val.startswith("'") and val.endswith("'"):
  42. result[key] = val[1:-1]
  43. else:
  44. result[key] = val
  45. return result
  46. def extract_comment_one_line(text: str) -> str:
  47. """Pull the first non-empty line of the # Comment section."""
  48. in_comment = False
  49. for line in text.splitlines():
  50. stripped = line.strip()
  51. if stripped.lower().startswith("# comment"):
  52. in_comment = True
  53. continue
  54. if not in_comment:
  55. continue
  56. if not stripped:
  57. continue
  58. if stripped.startswith("#"):
  59. break
  60. return stripped[:100]
  61. return "(no comment body)"
  62. SEVERITY_ORDER = {"error": 0, "warn": 1, "suggest": 2, "info": 3}
  63. def main(root: str, mode: str) -> int:
  64. root_path = Path(root)
  65. audit_dir = root_path / "audit"
  66. if not audit_dir.exists():
  67. print(f"ERROR: audit/ not found at {audit_dir}", file=sys.stderr)
  68. return 1
  69. files: list[Path] = []
  70. if mode in ("open", "all"):
  71. files.extend(sorted(p for p in audit_dir.glob("*.md") if p.name != ".gitkeep"))
  72. if mode in ("resolved", "all"):
  73. resolved = audit_dir / "resolved"
  74. if resolved.exists():
  75. files.extend(sorted(p for p in resolved.glob("*.md") if p.name != ".gitkeep"))
  76. if not files:
  77. print(f"No {mode} audit files found.")
  78. return 0
  79. grouped: dict[str, list[dict]] = defaultdict(list)
  80. for p in files:
  81. text = p.read_text(encoding="utf-8")
  82. fm = parse_frontmatter(text)
  83. if fm is None:
  84. print(f"⚠️ {p.relative_to(root_path)} — missing frontmatter", file=sys.stderr)
  85. continue
  86. fm["_path"] = str(p.relative_to(root_path))
  87. fm["_one_liner"] = extract_comment_one_line(text)
  88. grouped[fm.get("target", "(no-target)")].append(fm)
  89. total = sum(len(v) for v in grouped.values())
  90. print(f"{mode.upper()} audits: {total} across {len(grouped)} target files\n")
  91. for target in sorted(grouped.keys()):
  92. entries = grouped[target]
  93. entries.sort(key=lambda e: (
  94. SEVERITY_ORDER.get(e.get("severity", "info"), 99),
  95. e.get("created", ""),
  96. ))
  97. print(f"{target} ({len(entries)} {mode})")
  98. for e in entries:
  99. sev = e.get("severity", "?")
  100. aid = e.get("id", "?")
  101. author = e.get("author", "?")
  102. created = e.get("created", "?")[:10] # date only
  103. line = e.get("_one_liner", "")
  104. print(f" [{aid}] {sev}: {line} — {author}, {created}")
  105. print()
  106. return 0
  107. if __name__ == "__main__":
  108. if len(sys.argv) < 2:
  109. print(__doc__)
  110. sys.exit(1)
  111. root = sys.argv[1]
  112. mode = "open"
  113. for arg in sys.argv[2:]:
  114. if arg == "--open":
  115. mode = "open"
  116. elif arg == "--resolved":
  117. mode = "resolved"
  118. elif arg == "--all":
  119. mode = "all"
  120. else:
  121. print(f"Unknown flag: {arg}", file=sys.stderr)
  122. sys.exit(1)
  123. sys.exit(main(root, mode))