feat: add architecture maintainability review gate
This commit is contained in:
@@ -30,6 +30,7 @@ DEFAULT_TARGETS = [
|
||||
"description-optimization-check",
|
||||
"promotion-check",
|
||||
"python-compat-check",
|
||||
"architecture-maintainability-check",
|
||||
"yao-cli-check",
|
||||
"skill-overview-check",
|
||||
"skill-report-metrics-check",
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a maintainability audit for the skill code surface."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
EXCLUDED_PARTS = {
|
||||
".git",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
"__pycache__",
|
||||
"dist",
|
||||
".previews",
|
||||
}
|
||||
|
||||
|
||||
def iter_python_files(skill_dir: Path) -> list[Path]:
|
||||
roots = [skill_dir / "scripts", skill_dir / "tests"]
|
||||
files: list[Path] = []
|
||||
for root in roots:
|
||||
if not root.exists():
|
||||
continue
|
||||
for path in root.rglob("*.py"):
|
||||
rel_parts = path.relative_to(skill_dir).parts
|
||||
if any(part in EXCLUDED_PARTS for part in rel_parts):
|
||||
continue
|
||||
if len(rel_parts) >= 2 and rel_parts[0] == "tests" and rel_parts[1].startswith("tmp"):
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def rel(skill_dir: Path, path: Path) -> str:
|
||||
try:
|
||||
return str(path.resolve().relative_to(skill_dir.resolve()))
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def classify_python_file(path: Path, text: str) -> str:
|
||||
if 'SCRIPT_INTERFACE = "internal-module"' in text or "SCRIPT_INTERFACE = 'internal-module'" in text:
|
||||
return "internal-module"
|
||||
if "argparse.ArgumentParser" in text or ".add_argument(" in text or "argparse" in text:
|
||||
return "cli-script"
|
||||
if path.parts[-2:-1] == ("tests",) or "/tests/" in path.as_posix():
|
||||
return "test"
|
||||
return "module"
|
||||
|
||||
|
||||
def recommendation_for(path: str) -> str:
|
||||
if path == "scripts/yao.py":
|
||||
return "Split command handlers by domain while keeping scripts/yao.py as the thin CLI orchestrator."
|
||||
if path == "scripts/render_review_studio.py":
|
||||
return "Move data loading and large section renderers into focused review_studio_* modules."
|
||||
if path == "scripts/render_review_viewer.py":
|
||||
return "Split viewer data assembly from HTML section rendering."
|
||||
if path.startswith("tests/"):
|
||||
return "Break broad integration assertions into focused verifier helpers when the next behavior change lands."
|
||||
return "Watch this file before adding new responsibilities; extract a helper module when one concern dominates."
|
||||
|
||||
|
||||
def count_command_handlers(skill_dir: Path) -> int:
|
||||
path = skill_dir / "scripts" / "yao.py"
|
||||
if not path.exists():
|
||||
return 0
|
||||
count = 0
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if line.startswith("def command_"):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def build_report(skill_dir: Path, warn_lines: int, block_lines: int, generated_at: str) -> dict[str, Any]:
|
||||
files = iter_python_files(skill_dir)
|
||||
records: list[dict[str, Any]] = []
|
||||
internal_count = 0
|
||||
cli_count = 0
|
||||
test_count = 0
|
||||
script_count = 0
|
||||
for path in files:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
line_count = len(text.splitlines())
|
||||
kind = classify_python_file(path, text)
|
||||
rel_path = rel(skill_dir, path)
|
||||
if kind == "internal-module":
|
||||
internal_count += 1
|
||||
if kind == "cli-script":
|
||||
cli_count += 1
|
||||
if rel_path.startswith("tests/"):
|
||||
test_count += 1
|
||||
if rel_path.startswith("scripts/"):
|
||||
script_count += 1
|
||||
severity = "pass"
|
||||
if line_count >= block_lines:
|
||||
severity = "block"
|
||||
elif line_count >= warn_lines:
|
||||
severity = "warn"
|
||||
records.append(
|
||||
{
|
||||
"path": rel_path,
|
||||
"lines": line_count,
|
||||
"kind": kind,
|
||||
"severity": severity,
|
||||
"recommendation": recommendation_for(rel_path),
|
||||
}
|
||||
)
|
||||
records.sort(key=lambda item: (-int(item["lines"]), str(item["path"])))
|
||||
hotspots = [item for item in records if item["severity"] in {"warn", "block"}]
|
||||
blockers = [item for item in records if item["severity"] == "block"]
|
||||
summary = {
|
||||
"python_file_count": len(records),
|
||||
"script_file_count": script_count,
|
||||
"test_file_count": test_count,
|
||||
"internal_module_count": internal_count,
|
||||
"cli_script_count": cli_count,
|
||||
"command_handler_count": count_command_handlers(skill_dir),
|
||||
"warn_line_threshold": warn_lines,
|
||||
"block_line_threshold": block_lines,
|
||||
"largest_file_lines": records[0]["lines"] if records else 0,
|
||||
"hotspot_count": len(hotspots),
|
||||
"blocker_count": len(blockers),
|
||||
"decision": "block-maintainability" if blockers else ("watch-maintainability-hotspots" if hotspots else "pass"),
|
||||
}
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"ok": not blockers,
|
||||
"generated_at": generated_at,
|
||||
"skill_dir": ".",
|
||||
"summary": summary,
|
||||
"largest_files": records[:12],
|
||||
"hotspots": hotspots,
|
||||
"actions": [
|
||||
{
|
||||
"path": item["path"],
|
||||
"severity": item["severity"],
|
||||
"action": item["recommendation"],
|
||||
}
|
||||
for item in hotspots[:8]
|
||||
],
|
||||
"artifacts": {
|
||||
"json": "reports/architecture_maintainability.json",
|
||||
"markdown": "reports/architecture_maintainability.md",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(report: dict[str, Any]) -> str:
|
||||
summary = report["summary"]
|
||||
lines = [
|
||||
"# Architecture Maintainability",
|
||||
"",
|
||||
f"Generated at: `{report['generated_at']}`",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
f"- decision: `{summary['decision']}`",
|
||||
f"- python files: `{summary['python_file_count']}`",
|
||||
f"- scripts: `{summary['script_file_count']}`",
|
||||
f"- tests: `{summary['test_file_count']}`",
|
||||
f"- internal modules: `{summary['internal_module_count']}`",
|
||||
f"- CLI scripts: `{summary['cli_script_count']}`",
|
||||
f"- Yao CLI command handlers: `{summary['command_handler_count']}`",
|
||||
f"- largest file lines: `{summary['largest_file_lines']}`",
|
||||
f"- hotspots: `{summary['hotspot_count']}`",
|
||||
f"- blockers: `{summary['blocker_count']}`",
|
||||
"",
|
||||
"This report keeps maintainability risk visible before the Meta Skill grows more gates, renderers, and CLI commands.",
|
||||
"",
|
||||
"## Hotspots",
|
||||
"",
|
||||
]
|
||||
hotspots = report.get("hotspots", [])
|
||||
if hotspots:
|
||||
lines.extend(["| File | Lines | Kind | Severity | Recommended action |", "| --- | ---: | --- | --- | --- |"])
|
||||
for item in hotspots:
|
||||
lines.append(
|
||||
f"| `{item['path']}` | `{item['lines']}` | `{item['kind']}` | `{item['severity']}` | {item['recommendation']} |"
|
||||
)
|
||||
else:
|
||||
lines.append("No file-size hotspots found.")
|
||||
lines.extend(["", "## Largest Files", ""])
|
||||
if report.get("largest_files"):
|
||||
lines.extend(["| File | Lines | Kind | Severity |", "| --- | ---: | --- | --- |"])
|
||||
for item in report["largest_files"]:
|
||||
lines.append(f"| `{item['path']}` | `{item['lines']}` | `{item['kind']}` | `{item['severity']}` |")
|
||||
else:
|
||||
lines.append("No Python files found under scripts/ or tests/.")
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Release Rule",
|
||||
"",
|
||||
"- `block` hotspots should be split before governed release.",
|
||||
"- `warn` hotspots can ship only when Review Studio keeps them visible and a reviewer accepts the modularization plan.",
|
||||
"- Do not split a file only for line count; split when a stable responsibility boundary is clear.",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Render architecture maintainability evidence for a skill package.")
|
||||
parser.add_argument("skill_dir", nargs="?", default=".")
|
||||
parser.add_argument("--output-json")
|
||||
parser.add_argument("--output-md")
|
||||
parser.add_argument("--warn-lines", type=int, default=900)
|
||||
parser.add_argument("--block-lines", type=int, default=1500)
|
||||
parser.add_argument("--generated-at", default=date.today().isoformat())
|
||||
args = parser.parse_args()
|
||||
|
||||
skill_dir = Path(args.skill_dir).resolve()
|
||||
report = build_report(skill_dir, args.warn_lines, args.block_lines, args.generated_at)
|
||||
output_json = Path(args.output_json) if args.output_json else skill_dir / "reports" / "architecture_maintainability.json"
|
||||
output_md = Path(args.output_md) if args.output_md else skill_dir / "reports" / "architecture_maintainability.md"
|
||||
output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_md.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_json.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
output_md.write_text(render_markdown(report), encoding="utf-8")
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
raise SystemExit(0 if report["ok"] else 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -18,11 +18,14 @@ VALID_GATES = {
|
||||
"context-budget",
|
||||
"runtime-matrix",
|
||||
"trust-report",
|
||||
"python-compat",
|
||||
"architecture-maintainability",
|
||||
"permission-gates",
|
||||
"permission-runtime",
|
||||
"skill-atlas",
|
||||
"operations-loop",
|
||||
"review-waivers",
|
||||
"world-class-evidence",
|
||||
"registry-audit",
|
||||
"release-notes",
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ def evidence_paths(skill_dir: Path) -> dict[str, str]:
|
||||
"runtime_conformance": "reports/conformance_matrix.md",
|
||||
"trust_report": "reports/security_trust_report.md",
|
||||
"python_compatibility": "reports/python_compatibility.md",
|
||||
"architecture_maintainability": "reports/architecture_maintainability.md",
|
||||
"permission_policy": "security/permission_policy.md",
|
||||
"runtime_permissions": "reports/runtime_permission_probes.md",
|
||||
"skill_atlas": "reports/skill_atlas.html",
|
||||
@@ -167,6 +168,7 @@ def load_review_data(skill_dir: Path) -> dict[str, dict[str, Any]]:
|
||||
"runtime_permissions": load_json(reports / "runtime_permission_probes.json"),
|
||||
"trust": load_json(reports / "security_trust_report.json"),
|
||||
"python_compatibility": load_json(reports / "python_compatibility.json"),
|
||||
"architecture_maintainability": load_json(reports / "architecture_maintainability.json"),
|
||||
"context_budget": load_json(reports / "context_budget.json"),
|
||||
"promotion": load_json(reports / "promotion_decisions.json"),
|
||||
"atlas": load_json(reports / "skill_atlas.json"),
|
||||
@@ -198,6 +200,7 @@ def insight_cards(data: dict[str, dict[str, Any]]) -> list[dict[str, str]]:
|
||||
runtime_permissions = data["runtime_permissions"].get("summary", {})
|
||||
trust = data["trust"].get("summary", {})
|
||||
python_compat = data["python_compatibility"].get("summary", {})
|
||||
architecture = data["architecture_maintainability"].get("summary", {})
|
||||
atlas = data["atlas"].get("summary", {})
|
||||
adoption = data["adoption_drift"].get("summary", {})
|
||||
waivers = data["review_waivers"].get("summary", {})
|
||||
@@ -268,6 +271,14 @@ def insight_cards(data: dict[str, dict[str, Any]]) -> list[dict[str, str]]:
|
||||
"value": str(python_compat.get("issue_count", 0)),
|
||||
"detail": f"{python_compat.get('file_count', 0)} files scanned for Python {python_compat.get('target_python', '3.11')}",
|
||||
},
|
||||
{
|
||||
"label": "Arch Debt",
|
||||
"value": str(architecture.get("hotspot_count", 0)),
|
||||
"detail": (
|
||||
f"{architecture.get('largest_file_lines', 0)} largest lines; "
|
||||
f"{architecture.get('command_handler_count', 0)} CLI handlers"
|
||||
),
|
||||
},
|
||||
{
|
||||
"label": "Atlas",
|
||||
"value": str(atlas.get("route_collision_count", 0)),
|
||||
@@ -451,6 +462,18 @@ ACTION_GUIDANCE: dict[str, dict[str, str]] = {
|
||||
],
|
||||
"verification": "python3 scripts/yao.py python-compat .",
|
||||
},
|
||||
"architecture-maintainability": {
|
||||
"summary": "处理大文件和 CLI command surface 的维护性热点,优先拆分稳定职责边界。",
|
||||
"why": "Meta Skill 的门禁、报告和 CLI 会持续增长;如果不把架构债纳入审查,后续能力会越来越难验证和迁移。",
|
||||
"source_fix": "reports/architecture_maintainability.md + scripts/yao.py + scripts/render_review_studio.py",
|
||||
"source_paths": [
|
||||
{"path": "reports/architecture_maintainability.md", "label": "architecture maintainability", "kind": "report", "patterns": ["# Architecture"]},
|
||||
{"path": "scripts/yao.py", "label": "Yao CLI orchestrator", "kind": "source", "patterns": ["def command_"]},
|
||||
{"path": "scripts/render_review_studio.py", "label": "Review Studio renderer", "kind": "source", "patterns": ["ACTION_GUIDANCE"]},
|
||||
{"path": "scripts/render_review_viewer.py", "label": "review viewer renderer", "kind": "source", "patterns": ["def "]},
|
||||
],
|
||||
"verification": "python3 scripts/yao.py architecture-audit .",
|
||||
},
|
||||
"permission-gates": {
|
||||
"summary": "补齐高权限能力的 reviewer、scope、reason、expires_at 和目标端 enforcement 说明。",
|
||||
"why": "权限契约只有在批准人、有效期和目标端处置方式明确时,才能支撑 governed release。",
|
||||
|
||||
@@ -291,6 +291,37 @@ def build_gates(skill_dir: Path, output_html: Path, data: dict[str, dict[str, An
|
||||
)
|
||||
)
|
||||
|
||||
architecture = data["architecture_maintainability"]
|
||||
architecture_summary = architecture.get("summary", {})
|
||||
if not architecture:
|
||||
if maturity in {"library", "governed"}:
|
||||
architecture_status = "warn"
|
||||
architecture_detail = "architecture maintainability report is missing for a reusable package"
|
||||
else:
|
||||
architecture_status = "pass"
|
||||
architecture_detail = "architecture maintainability report is optional until code surface grows"
|
||||
else:
|
||||
hotspot_count = int(architecture_summary.get("hotspot_count", 0) or 0)
|
||||
blocker_count = int(architecture_summary.get("blocker_count", 0) or 0)
|
||||
architecture_status = "block" if not architecture.get("ok", True) or blocker_count else ("warn" if hotspot_count else "pass")
|
||||
architecture_detail = (
|
||||
f"{architecture_summary.get('python_file_count', 0)} Python files; "
|
||||
f"{hotspot_count} hotspots; "
|
||||
f"{blocker_count} blockers; "
|
||||
f"largest {architecture_summary.get('largest_file_lines', 0)} lines; "
|
||||
f"{architecture_summary.get('command_handler_count', 0)} CLI handlers"
|
||||
)
|
||||
gates.append(
|
||||
gate(
|
||||
"architecture-maintainability",
|
||||
"架构维护",
|
||||
architecture_status,
|
||||
architecture_detail,
|
||||
"reports/architecture_maintainability.json",
|
||||
_report_link(output_html, skill_dir, "reports/architecture_maintainability.md"),
|
||||
)
|
||||
)
|
||||
|
||||
permission_governance = trust.get("permission_governance", {}) if isinstance(trust.get("permission_governance", {}), dict) else {}
|
||||
if trust and not permission_governance:
|
||||
permission_governance = fallback_permission_governance(skill_dir)
|
||||
@@ -566,6 +597,7 @@ def weighted_score(gates: list[dict[str, str]]) -> int:
|
||||
"runtime-matrix": 10,
|
||||
"trust-report": 10,
|
||||
"python-compat": 10,
|
||||
"architecture-maintainability": 10,
|
||||
"permission-gates": 10,
|
||||
"permission-runtime": 10,
|
||||
"skill-atlas": 10,
|
||||
|
||||
@@ -455,6 +455,24 @@ def command_python_compat(args: argparse.Namespace) -> int:
|
||||
return 0 if result["ok"] else 2
|
||||
|
||||
|
||||
def command_architecture_audit(args: argparse.Namespace) -> int:
|
||||
skill_dir = str(Path(args.skill_dir).resolve())
|
||||
cmd = [skill_dir]
|
||||
if args.output_json:
|
||||
cmd.extend(["--output-json", args.output_json])
|
||||
if args.output_md:
|
||||
cmd.extend(["--output-md", args.output_md])
|
||||
if args.warn_lines is not None:
|
||||
cmd.extend(["--warn-lines", str(args.warn_lines)])
|
||||
if args.block_lines is not None:
|
||||
cmd.extend(["--block-lines", str(args.block_lines)])
|
||||
if args.generated_at:
|
||||
cmd.extend(["--generated-at", args.generated_at])
|
||||
result = run_script("render_architecture_maintainability.py", cmd)
|
||||
print(json.dumps(result["payload"] if result["payload"] is not None else result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["ok"] else 2
|
||||
|
||||
|
||||
def command_report(args: argparse.Namespace) -> int:
|
||||
steps = []
|
||||
if args.refresh_optimization:
|
||||
@@ -472,6 +490,7 @@ def command_report(args: argparse.Namespace) -> int:
|
||||
run_script("render_context_reports.py", []),
|
||||
run_script("render_portability_report.py", []),
|
||||
run_script("python_compat_check.py", [str(ROOT)]),
|
||||
run_script("render_architecture_maintainability.py", [str(ROOT)]),
|
||||
run_script("render_reference_synthesis.py", [str(ROOT)]),
|
||||
run_script("render_artifact_design_profile.py", [str(ROOT)]),
|
||||
run_script("render_prompt_quality_profile.py", [str(ROOT)]),
|
||||
@@ -506,6 +525,7 @@ def command_report(args: argparse.Namespace) -> int:
|
||||
"context_budget": "reports/context_budget.json",
|
||||
"portability_score": "reports/portability_score.json",
|
||||
"python_compatibility": "reports/python_compatibility.json",
|
||||
"architecture_maintainability": "reports/architecture_maintainability.json",
|
||||
"reference_synthesis": "reports/reference-synthesis.json",
|
||||
"artifact_design_profile": "reports/artifact-design-profile.json",
|
||||
"prompt_quality_profile": "reports/prompt-quality-profile.json",
|
||||
@@ -1283,6 +1303,7 @@ def command_workspace_flow(args: argparse.Namespace) -> int:
|
||||
{"phase": "report-refresh", "result": run_script("render_context_reports.py", [])},
|
||||
{"phase": "report-refresh", "result": run_script("render_portability_report.py", [])},
|
||||
{"phase": "report-refresh", "result": run_script("python_compat_check.py", [str(ROOT)])},
|
||||
{"phase": "report-refresh", "result": run_script("render_architecture_maintainability.py", [str(ROOT)])},
|
||||
{"phase": "report-refresh", "result": run_script("compile_skill.py", [str(ROOT)])},
|
||||
{"phase": "report-refresh", "result": run_script("render_adoption_drift_report.py", [str(ROOT)])},
|
||||
{"phase": "report-refresh", "result": run_script("render_telemetry_hook_recipes.py", [str(ROOT)])},
|
||||
|
||||
@@ -93,6 +93,18 @@ def build_parser(command_handlers: dict[str, Callable[[argparse.Namespace], int]
|
||||
python_compat_cmd.add_argument("--generated-at")
|
||||
python_compat_cmd.set_defaults(func=_handler(command_handlers, "command_python_compat"))
|
||||
|
||||
architecture_audit_cmd = subparsers.add_parser(
|
||||
"architecture-audit",
|
||||
help="Render maintainability evidence for large Python files and CLI command surface.",
|
||||
)
|
||||
architecture_audit_cmd.add_argument("skill_dir", nargs="?", default=".")
|
||||
architecture_audit_cmd.add_argument("--output-json")
|
||||
architecture_audit_cmd.add_argument("--output-md")
|
||||
architecture_audit_cmd.add_argument("--warn-lines", type=int, default=900)
|
||||
architecture_audit_cmd.add_argument("--block-lines", type=int, default=1500)
|
||||
architecture_audit_cmd.add_argument("--generated-at")
|
||||
architecture_audit_cmd.set_defaults(func=_handler(command_handlers, "command_architecture_audit"))
|
||||
|
||||
review_cmd = subparsers.add_parser("review", help="Locate the current bundle and human review stub for a target.")
|
||||
review_cmd.add_argument(
|
||||
"--target",
|
||||
@@ -448,10 +460,12 @@ def build_parser(command_handlers: dict[str, Callable[[argparse.Namespace], int]
|
||||
review_waivers_cmd.add_argument(
|
||||
"--gate-key",
|
||||
choices=[
|
||||
"architecture-maintainability",
|
||||
"context-budget",
|
||||
"intent-canvas",
|
||||
"operations-loop",
|
||||
"output-lab",
|
||||
"python-compat",
|
||||
"registry-audit",
|
||||
"release-notes",
|
||||
"runtime-matrix",
|
||||
@@ -489,10 +503,12 @@ def build_parser(command_handlers: dict[str, Callable[[argparse.Namespace], int]
|
||||
review_annotations_cmd.add_argument(
|
||||
"--gate-key",
|
||||
choices=[
|
||||
"architecture-maintainability",
|
||||
"context-budget",
|
||||
"intent-canvas",
|
||||
"operations-loop",
|
||||
"output-lab",
|
||||
"python-compat",
|
||||
"registry-audit",
|
||||
"release-notes",
|
||||
"review-waivers",
|
||||
@@ -500,6 +516,7 @@ def build_parser(command_handlers: dict[str, Callable[[argparse.Namespace], int]
|
||||
"skill-atlas",
|
||||
"trigger-lab",
|
||||
"trust-report",
|
||||
"world-class-evidence",
|
||||
"permission-gates",
|
||||
"permission-runtime",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user