Files
pebrel/scripts/check_architecture.py
kuddev a3d25c913d build: enforce shared architecture and translation contracts
Adopt the shared source budgets, local dependency rules and tested Python
checkers. Add the architecture workflow and metadata-only cross-target
tools, and make existing filesystem test waits deterministic.
Document contributor checks in both unreleased notes without claiming
server-side branch protection or native platform validation.
2026-09-06 21:14:18 +08:00

79 lines
3.3 KiB
Python

import argparse
from pathlib import Path
import subprocess
import sys
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from scripts.architecture import budgets, dependencies
ROOT = Path(__file__).resolve().parents[1]
class Revision:
def __init__(self, root: Path, reference: str):
self.root = root
result = subprocess.run(
["git", "rev-parse", "--verify", "--end-of-options", reference + "^{commit}"],
cwd=root, capture_output=True, text=True, encoding="utf-8", check=True,
)
self.commit = result.stdout.strip()
def read(self, path: str):
result = subprocess.run(
["git", "--literal-pathspecs", "ls-tree", "--name-only", "-z", self.commit, "--", path],
cwd=self.root, capture_output=True, check=True,
)
if not result.stdout:
return None
return subprocess.run(
["git", "show", f"{self.commit}:{path}"],
cwd=self.root, capture_output=True, check=True,
).stdout
def run(root: Path, base: str | None = None, report: bool = False) -> int:
budget = budgets.Budget.parse((root / budgets.BUDGET_PATH).read_text(encoding="utf-8"))
revision = Revision(root, base) if base else None
previous_source = revision.read(budgets.BUDGET_PATH) if revision else None
previous = budgets.Budget.parse(previous_source.decode("utf-8")) if previous_source is not None else None
errors, notices, counts = budgets.check(root, budget, previous, revision.read if revision else None)
dependency_errors, members = dependencies.check(
root, dependencies.load(root / dependencies.POLICY_PATH), budget.roots, set(counts)
)
errors.extend(dependency_errors)
for member in members:
if member not in budget.roots:
errors.append(f"{member}: workspace member missing from line-budget roots")
print(f"Architecture: {len(counts)} source files, {len(members)} crates, {len(budget.exceptions)} legacy exceptions")
advisory = sorted(((name, count) for name, count in counts.items() if count > 800), key=lambda item: -item[1])
print(f"Review guidance only: {len(advisory)} files exceed the 800-line soft target")
if revision and previous is None:
print("Initial policy adoption: no budget in base; review the initial debt inventory")
for notice in notices:
print(f"NOTICE: {notice}")
if report:
for name, count in advisory:
print(f"REVIEW: {name}: {count} lines")
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return int(bool(errors))
def main() -> int:
parser = argparse.ArgumentParser(description="Offline first-party size and dependency contracts (Python 3.11+)")
parser.add_argument("--base", help="PR base commit; rejects new/increased debt and regrowth")
parser.add_argument("--report", action="store_true", help="also list advisory-size files")
options = parser.parse_args()
try:
return run(ROOT, options.base, options.report)
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
print(f"Architecture check could not complete: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())