-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathaudit_documentation.py
More file actions
171 lines (134 loc) Β· 5.81 KB
/
audit_documentation.py
File metadata and controls
171 lines (134 loc) Β· 5.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/usr/bin/env python3
"""Documentation Audit Script.
Scans the repository for required documentation files (README.md, AGENTS.md, SPEC.md, PAI.md)
and reports on coverage and quality (stub detection).
"""
import os
import sys
from pathlib import Path
from typing import Any
# Ensure codomyrmex is in path
_project_root = Path(__file__).resolve().parents[2]
if str(_project_root / "src") not in sys.path:
sys.path.insert(0, str(_project_root / "src"))
from codomyrmex.utils import ScriptBase
class DocumentationAudit(ScriptBase):
def __init__(self):
super().__init__(
name="doc_audit",
description="Audits repository documentation coverage",
version="1.0.0",
)
self.required_files = ["README.md", "AGENTS.md", "SPEC.md", "PAI.md"]
self.stub_threshold = 500 # Bytes
def add_arguments(self, parser):
parser.add_argument(
"--target",
type=Path,
default=Path.cwd() / "src/codomyrmex",
help="Target directory to scan",
)
parser.add_argument(
"--fix",
action="store_true",
help="Attempt to create missing files (Dry run recommended first)",
)
def scan_directory(self, path: Path) -> dict[str, Any]:
"""Scan a single directory for documentation status."""
stats = {
"path": str(path.relative_to(self.root_dir)),
"files": {},
"missing": [],
"stubs": [],
}
for filename in self.required_files:
file_path = path / filename
if file_path.exists():
size = file_path.stat().st_size
stats["files"][filename] = size
if size < self.stub_threshold:
stats["stubs"].append(filename)
else:
stats["missing"].append(filename)
return stats
def calculate_score(self, stats: dict[str, Any]) -> float:
"""Calculate a compliance score (0-100)."""
total = len(self.required_files)
present = total - len(stats["missing"])
non_stubs = present - len(stats["stubs"])
# 50% for presence, 50% for quality
score = (present / total) * 50 + (non_stubs / total) * 50
return score
def generate_report(self, results: list[dict[str, Any]]) -> None:
"""Generate a markdown report."""
report_lines = [
"# Documentation Audit Report",
f"**Date**: {os.popen('date').read().strip()}",
f"**Target**: {self.target_dir}",
"",
"## Summary",
"",
"| Module | Score | Missing | Stubs |",
"| :--- | :---: | :--- | :--- |",
]
total_score = 0
for res in results:
score = self.calculate_score(res)
total_score += score
missing_str = ", ".join(res["missing"]) if res["missing"] else "β
"
stubs_str = ", ".join(res["stubs"]) if res["stubs"] else "β
"
# Highlight poor scores
icon = "π’" if score > 80 else "π‘" if score > 50 else "π΄"
report_lines.append(
f"| {icon} **{res['path']}** | {score:.0f}% | {missing_str} | {stubs_str} |"
)
avg_score = total_score / len(results) if results else 0
report_lines.insert(5, f"**Average Compliance**: {avg_score:.1f}%")
report_lines.insert(6, "")
report_content = "\n".join(report_lines)
# Save report
if self.output_path:
report_file = self.output_path / "audit_report.md"
with open(report_file, "w") as f:
f.write(report_content)
self.log_success(f"Report saved to {report_file}")
# Also print to console
print(report_content)
def run(self, args, config):
self.target_dir = args.target.resolve()
self.root_dir = Path.cwd()
self.log_info(f"Scanning target: {self.target_dir}")
results = []
# Directories that are not our code β skip entire subtrees
SKIP_DIRS = {"__pycache__", ".mypy_cache", ".git", "gitnexus", "node_modules"}
# Walk through directories
# We only care about modules (directories with __init__.py) or top-level dirs
for root, dirs, files in os.walk(self.target_dir):
path = Path(root)
# Skip hidden dirs, pycache, and external tool caches
if path.name.startswith(".") or path.name in SKIP_DIRS:
dirs.clear() # prune subtree β don't recurse further
continue
# Skip vendor/gitnexus subtree (git submodule β not our code)
path_parts = list(path.parts)
if "vendor" in path_parts:
vendor_idx = path_parts.index("vendor")
parts_after_vendor = path_parts[vendor_idx + 1 :]
if parts_after_vendor and parts_after_vendor[0] == "gitnexus":
dirs.clear()
continue
# Prune SKIP_DIRS from dirs so os.walk won't recurse into them
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
# Check if it's a python module or has significant content
if "__init__.py" in files or any(p.suffix == ".py" for p in path.iterdir()):
stats = self.scan_directory(path)
results.append(stats)
self.generate_report(results)
return {"scanned": len(results), "average_score": 0} # Simplified return
if __name__ == "__main__":
import sys
# Ensure src is in path for imports (file-relative for any working directory)
project_root = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(project_root / "src"))
audit = DocumentationAudit()
sys.exit(audit.execute())