forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck_package_architecture_maps.py
More file actions
262 lines (223 loc) · 9.6 KB
/
Copy pathcheck_package_architecture_maps.py
File metadata and controls
262 lines (223 loc) · 9.6 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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/env python3
"""Enforce architecture maps for oversized source packages.
Existing mapless packages are grandfathered at their committed source-file
count. They emit warnings until documented. A new oversized package, or growth
past a grandfathered count, fails with guidance to add a package-root map.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Mapping
ISSUE_URL = "https://github.com/BasedHardware/omi/issues/9446"
DEFAULT_THRESHOLD = 12
MAP_NAMES = ("ARCHITECTURE.md", "README.md")
SOURCE_EXTENSIONS = {
".c",
".cc",
".cpp",
".cxx",
".dart",
".h",
".hpp",
".js",
".jsx",
".kt",
".m",
".mm",
".py",
".rs",
".swift",
".ts",
".tsx",
}
SKIP_SUFFIXES = (".gen.dart", ".g.dart", ".lock", ".min.js")
SKIP_PARTS = {".git", ".next", ".venv", "__pycache__", "build", "dist", "node_modules", "target"}
@dataclass(frozen=True)
class Finding:
package: str
source_count: int
baseline_count: int | None
level: str
message: str
def source_file(path: Path) -> bool:
if path.suffix not in SOURCE_EXTENSIONS:
return False
if path.name.endswith(SKIP_SUFFIXES):
return False
return not any(part in SKIP_PARTS for part in path.parts)
def package_roots(repo_root: Path) -> list[Path]:
"""Return intentionally non-overlapping package roots covered by the ratchet."""
backend_utils = repo_root / "backend" / "utils"
packages = sorted(path for path in backend_utils.iterdir() if path.is_dir()) if backend_utils.is_dir() else []
agent_source = repo_root / "desktop" / "macos" / "agent" / "src"
if agent_source.is_dir():
packages.append(agent_source)
return packages
def source_count(package_root: Path) -> int:
return sum(1 for path in package_root.rglob("*") if path.is_file() and source_file(path))
def has_architecture_map(package_root: Path) -> bool:
return any((package_root / name).is_file() for name in MAP_NAMES)
def load_baseline(path: Path) -> dict[str, int]:
try:
raw = path.read_text(encoding="utf-8")
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"cannot read package architecture baseline {path}: {exc}") from exc
return parse_baseline(raw, label=str(path))
def parse_baseline(raw: str, *, label: str) -> dict[str, int]:
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"cannot parse package architecture baseline {label}: {exc}") from exc
if not isinstance(payload, dict) or payload.get("version") != 1 or not isinstance(payload.get("packages"), dict):
raise ValueError(f"invalid package architecture baseline schema: {label}")
baseline: dict[str, int] = {}
for raw_package, raw_count in payload["packages"].items():
if not isinstance(raw_package, str) or isinstance(raw_count, bool) or not isinstance(raw_count, int):
raise ValueError(f"invalid package architecture baseline entry: {raw_package!r}: {raw_count!r}")
baseline[raw_package] = raw_count
return baseline
def load_baseline_at_ref(repo_root: Path, ref: str) -> dict[str, int] | None:
"""Load the trusted baseline from a PR base; None means first-time bootstrap."""
verify = subprocess.run(
["git", "rev-parse", "--verify", f"{ref}^{{commit}}"],
cwd=repo_root,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
if verify.returncode:
raise ValueError(f"cannot resolve package architecture baseline ref {ref}: {verify.stderr.strip()}")
baseline_path = ".github/scripts/package_architecture_baseline.json"
result = subprocess.run(
["git", "show", f"{ref}:{baseline_path}"],
cwd=repo_root,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if result.returncode:
return None
return parse_baseline(result.stdout, label=f"{ref}:{baseline_path}")
def baseline_change_findings(current: Mapping[str, int], previous: Mapping[str, int] | None) -> list[Finding]:
"""Reject self-grandfathering while allowing baseline shrinkage and removal."""
if previous is None:
return []
findings: list[Finding] = []
for package, current_count in sorted(current.items()):
previous_count = previous.get(package)
if previous_count is not None and current_count <= previous_count:
continue
if previous_count is None:
message = (
f"{package} was added to the architecture-map baseline at {current_count}. New oversized packages "
f"must add ARCHITECTURE.md or README.md instead of grandfathering themselves. See {ISSUE_URL}"
)
else:
message = (
f"{package}'s architecture-map baseline increased from {previous_count} to {current_count}. "
f"Add ARCHITECTURE.md or README.md instead of raising the baseline. See {ISSUE_URL}"
)
findings.append(Finding(package, current_count, previous_count, "error", message))
return findings
def evaluate_packages(repo_root: Path, baseline: Mapping[str, int], threshold: int) -> list[Finding]:
findings: list[Finding] = []
for package_root in package_roots(repo_root):
relative = package_root.relative_to(repo_root).as_posix()
count = source_count(package_root)
if count <= threshold or has_architecture_map(package_root):
continue
baseline_count = baseline.get(relative)
if baseline_count is None:
message = (
f"{relative} has {count} source files (threshold: {threshold}) and no package-root "
f"ARCHITECTURE.md or README.md. Add a map before this package grows further. See {ISSUE_URL}"
)
level = "error"
elif count > baseline_count:
message = (
f"{relative} grew from its grandfathered {baseline_count} source files to {count} without a "
f"package-root ARCHITECTURE.md or README.md. Add a map or revert the growth. See {ISSUE_URL}"
)
level = "error"
else:
message = (
f"{relative} remains grandfathered without an architecture map at {count} source files "
f"(baseline: {baseline_count}); add ARCHITECTURE.md or README.md opportunistically."
)
level = "warning"
findings.append(Finding(relative, count, baseline_count, level, message))
return findings
def annotation_escape(value: object) -> str:
return str(value).replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
def emit_finding(finding: Finding) -> None:
title = "Package architecture map required" if finding.level == "error" else "Package needs architecture map"
print(
f"::{finding.level} file={annotation_escape(finding.package)},line=1,title={annotation_escape(title)}::"
f"{annotation_escape(finding.message)}"
)
def write_summary(findings: list[Finding], threshold: int) -> None:
lines = [
"## Package architecture maps",
"",
f"Packages over {threshold} source files require a package-root `ARCHITECTURE.md` or `README.md`.",
"",
]
if not findings:
lines.append("All oversized packages have maps or remain below the threshold.")
else:
lines.extend(["| Level | Package | Source files |", "| --- | --- | ---: |"])
lines.extend(f"| {item.level} | {item.package} | {item.source_count} |" for item in findings)
summary = "\n".join(lines) + "\n"
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if summary_path:
with open(summary_path, "a", encoding="utf-8") as handle:
handle.write(summary)
else:
print(summary, end="")
def run(
*,
repo_root: Path,
baseline_path: Path,
threshold: int = DEFAULT_THRESHOLD,
previous_baseline: Mapping[str, int] | None = None,
) -> int:
try:
baseline = load_baseline(baseline_path)
except ValueError as exc:
print(f"::error title=Invalid package architecture baseline::{annotation_escape(exc)}")
return 1
findings = baseline_change_findings(baseline, previous_baseline)
findings.extend(evaluate_packages(repo_root, baseline, threshold))
for finding in findings:
emit_finding(finding)
write_summary(findings, threshold)
return 1 if any(finding.level == "error" for finding in findings) else 0
def main() -> int:
default_root = Path(__file__).resolve().parents[2]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=default_root)
parser.add_argument("--baseline", type=Path)
parser.add_argument("--base", help="Git revision whose baseline cannot be increased or extended")
parser.add_argument("--threshold", type=int, default=DEFAULT_THRESHOLD)
args = parser.parse_args()
repo_root = args.root.resolve()
baseline_path = args.baseline or repo_root / ".github" / "scripts" / "package_architecture_baseline.json"
try:
previous_baseline = load_baseline_at_ref(repo_root, args.base) if args.base else None
except ValueError as exc:
print(f"::error title=Invalid package architecture baseline::{annotation_escape(exc)}")
return 1
return run(
repo_root=repo_root,
baseline_path=baseline_path,
threshold=args.threshold,
previous_baseline=previous_baseline,
)
if __name__ == "__main__":
raise SystemExit(main())