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>
152 lines
4.8 KiB
Python
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())
|