Files
cowork-local/scripts/run_quality_gate.py
T
anhtnm1andClaude Opus 5 e5c184ce07 feat(gates): thêm cổng CASAN thứ tư (Gate O) và mở cổng LOC ra cả cây mã
Gate O — module production phải có ít nhất một nơi import
---------------------------------------------------------
Ba cổng đang có đều không bắt được mã chết, đúng như 1.400 dòng ở commit
trước đã chứng minh. Gate O dựng đồ thị import bằng AST từ
`__init__`/`__main__`/`app`, theo cả import muộn trong thân hàm.

Hai ngoại lệ tự động để `ALLOWLIST` không phải chép lại cùng một lý do nhiều
lần: `__init__.py` của gói mà mọi thành viên đều dormant, và module chỉ được
chính mã dormant đã miễn trừ import.

Cổng cũng đếm tuổi 9 seam chưa nối dây (nhãn `SEAM · dựng <ngày>`) và nhắc
khi quá 30 ngày. Chỉ [WARN], không làm CI đỏ: để nó đỏ thì CI sẽ đỏ vào một
buổi sáng mà không ai sửa gì, và cách nhanh nhất để xanh lại là sửa ngày.

Cổng LOC — quét 366 file thay vì 191
------------------------------------
`DEFAULT_TARGET_DIRS` chỉ có 4 gói Clean Architecture, nên một file 944 dòng
trong `ui/` vẫn qua cổng. Nay quét cả `ui/`, `core/`, `providers/`,
`security/`, `mcp_servers/` và các module ở thư mục gốc.

18 file đã dài hơn 400 dòng từ trước nằm trong `LEGACY_ALLOWANCE` — bánh cóc
chỉ quay một chiều, và nó đo DÒNG MÃ chứ không đo dòng vật lý. Bánh cóc chỉ
hỏi một câu, "file này có đang để thêm việc vào không?", mà viết thêm một
docstring thì không. Đếm dòng vật lý ở đó biến cổng thành thứ phạt người viết
tài liệu, và cách dễ nhất để làm nó xanh lại sẽ là xoá bớt chú thích. Trần
400 vẫn đếm dòng vật lý — đó là hợp đồng đã chốt của cổng S.

CI
--
Ghim tên thư mục checkout là `cowork_local`: nhiều test characterization sinh
tiến trình con `python -c "from cowork_local... import ..."`, mà tiến trình
con chỉ import được khi trên sys.path có thư mục mang đúng tên gói. Checkout
vào thư mục tên khác làm 73 test đỏ vì lý do không liên quan tới mã.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:36:52 +09:00

152 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Unified CASAN Quality Gate Orchestrator (EPIC R10 - Quality Assurance).
Runs all verification gates to validate architecture, security, single responsibility,
and test suite compliance before merging PRs or cutting a release.
Verification Stages (CASAN):
1. [C] Clean Architecture Guard (scripts/check_imports.py)
2. [A] Atomic & Secrets Audit (scripts/audit_security.py)
3. [S] Single Responsibility / LOC Guard (scripts/check_loc.py)
4. [O] Orphan Module Guard (scripts/check_orphan_modules.py)
5. [A/N] Automated Tests & No-Regression Suite (pytest)
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import List, Tuple
# Ensure stdout handles UTF-8 on Windows consoles without codec crash
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
REPO_ROOT = Path(__file__).resolve().parent.parent
def run_stage(title: str, cmd: List[str], cwd: Path) -> Tuple[bool, float, str]:
"""Execute a single quality gate command and measure elapsed duration.
Returns:
A tuple of (success_boolean, elapsed_seconds, combined_output)
"""
print(f"\n>> Running Gate: {title} ...")
start_time = time.time()
try:
proc = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
elapsed = time.time() - start_time
success = proc.returncode == 0
output = proc.stdout + ("\n" + proc.stderr if proc.stderr else "")
return success, elapsed, output
except Exception as exc:
elapsed = time.time() - start_time
return False, elapsed, f"Exception occurred while running {cmd}: {exc}"
def main() -> int:
"""Main CLI orchestrator for CASAN quality gates."""
parser = argparse.ArgumentParser(description="Run CASAN Quality Gates on the repository.")
parser.add_argument(
"--skip-tests",
action="store_true",
help="Skip running pytest (run static and architectural guards only)",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Print detailed command output for passing gates as well",
)
args = parser.parse_args()
print("=" * 75)
print("COWORK LOCAL - CASAN QUALITY GATE RUNNER")
print("=" * 75)
stages = [
(
"C - Clean Architecture Boundary Check",
[sys.executable, str(REPO_ROOT / "scripts" / "check_imports.py")],
),
(
"A - Secrets & Plaintext Credentials Audit",
[sys.executable, str(REPO_ROOT / "scripts" / "audit_security.py")],
),
(
"S - Single Responsibility LOC Limit (<= 400 LOC)",
[sys.executable, str(REPO_ROOT / "scripts" / "check_loc.py"), "--max-lines", "400"],
),
# Ba cổng trên không cổng nào bắt được mã chết: một file không ai import
# vẫn đúng chiều phụ thuộc, vẫn không có credential, vẫn dưới 400 dòng.
# Cổng O đi tìm đúng khoảng trống đó.
(
"O - Orphan Module Guard (moi module phai co noi import)",
[sys.executable, str(REPO_ROOT / "scripts" / "check_orphan_modules.py")],
),
]
if not args.skip_tests:
stages.append(
(
"A/N - Automated Pytest Suite (No-Regression)",
[sys.executable, "-m", "pytest", "-q"],
)
)
results = []
all_passed = True
total_start = time.time()
for title, cmd in stages:
success, elapsed, output = run_stage(title, cmd, cwd=REPO_ROOT)
results.append((title, success, elapsed, output))
if success:
print(f" [PASS] {title} ({elapsed:.2f}s)")
if args.verbose:
print(output.strip())
else:
all_passed = False
print(f" [FAIL] {title} ({elapsed:.2f}s)")
print("\n--- Output ---")
print(output.strip())
print("--------------")
total_elapsed = time.time() - total_start
print("\n" + "=" * 75)
print("QUALITY GATE SUMMARY REPORT")
print("=" * 75)
for title, success, elapsed, _ in results:
status_str = "[PASS]" if success else "[FAIL]"
print(f" {status_str:<8} | {elapsed:>6.2f}s | {title}")
print("-" * 75)
print(f"Total Execution Time: {total_elapsed:.2f}s")
if all_passed:
print("\nALL CASAN QUALITY GATES PASSED! Ready for PR merge or release.")
return 0
else:
print("\nQUALITY GATE FAILED! Please resolve the issues above before proceeding.")
return 1
if __name__ == "__main__":
sys.exit(main())