"""Tool: generic phase gate validation (DB-backed). Discovers check commands from project config files (package.json, Makefile, nx.json, pyproject.toml) instead of hardcoded tech-stack registries. Supports any tech stack: Python, Node.js, NestJS, React, Vue, Nx monorepos, etc. Persists every run to orch_quality_runs in patterns.db. Args (kwargs): action (str): 'run' | 'show_last' | 'list_runs' | 'show_config' Default: 'run' project_name (str): project name (preferred, e.g. 'rentman-clone') project_root (str): alternative — path to project root, name derived from basename gate_level (str): 'implementation' | 'quality-review' | 'deployment' | 'release' | 'ops-gate' | 'security-gate' Default: 'quality-review' triggered_by (str): 'manual' | 'pre-commit' | 'pre-deploy' | etc. Default: 'manual' dry_run (bool): if True, list checks that would run without executing Default: False Returns: JSON string with decision, checks, blockers, next_action. Persists to orch_quality_runs (one row per run). For action=show_last: returns last run's full record. For action=list_runs: returns summary of recent runs. For action=show_config: returns orch_quality_config row. """ from __future__ import annotations import json import os import subprocess import shlex import time from typing import Any, Dict, List, Optional from helpers.tool import Tool, Response from usr.plugins.a0_software_orchestrator.helpers.db_state_store import ( DB_PATH, get_project_id, get_kv, set_kv, ) from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError from usr.plugins.a0_software_orchestrator.helpers.safety_rules import redact_secrets # --------------------------------------------------------------------------- # Name resolution # --------------------------------------------------------------------------- def _resolve_name(project_name: str = "", project_root: str = "") -> str: """Resolve project name from explicit name, root path, or cwd.""" if project_name: return project_name.strip() if project_root: return os.path.basename(project_root.rstrip("/")) return os.path.basename(os.getcwd()) # --------------------------------------------------------------------------- # Path discovery — detect backend/frontend dirs generically # --------------------------------------------------------------------------- _BACKEND_CANDIDATES = ("backend", "apps/api", "apps/backend", "server", "api") _FRONTEND_CANDIDATES = ("frontend", "apps/web", "apps/frontend", "client", "web") def _detect_backend_dir(project_root: str) -> Optional[str]: """Return the backend source directory relative to project_root, or None. Checks common layouts: backend/, apps/api/, apps/backend/, server/, api/. """ for candidate in _BACKEND_CANDIDATES: if os.path.isdir(os.path.join(project_root, candidate)): return candidate return None def _detect_frontend_dir(project_root: str) -> Optional[str]: """Return the frontend source directory relative to project_root, or None. Checks common layouts: frontend/, apps/web/, apps/frontend/, client/, web/. """ for candidate in _FRONTEND_CANDIDATES: if os.path.isdir(os.path.join(project_root, candidate)): return candidate return None # --------------------------------------------------------------------------- # Config-file readers # --------------------------------------------------------------------------- def _read_package_json_scripts(dir_path: str) -> Dict[str, str]: """Return scripts dict from package.json in dir_path, or {} if missing.""" pkg_path = os.path.join(dir_path, "package.json") if not os.path.isfile(pkg_path): return {} try: with open(pkg_path, "r", encoding="utf-8") as f: data = json.load(f) return data.get("scripts", {}) except (json.JSONDecodeError, OSError): return {} def _parse_makefile_targets(project_root: str) -> List[str]: """Return list of Makefile target names, or [] if no Makefile.""" for fname in ("Makefile", "makefile", "GNUmakefile"): mk_path = os.path.join(project_root, fname) if not os.path.isfile(mk_path): continue targets: List[str] = [] try: with open(mk_path, "r", encoding="utf-8", errors="replace") as f: for line in f: line = line.rstrip("\n") stripped = line.strip() # target lines: "name: deps" (not indented, not comment, has colon) if ( stripped and not stripped.startswith("#") and not line.startswith("\t") and not line.startswith(" ") and ":" in stripped ): target = stripped.split(":")[0].strip() # skip variable assignments (VAR = value or VAR := value) if "=" not in target and target: targets.append(target) except OSError: pass return targets return [] def _has_pytest_config(project_root: str, backend_dir: Optional[str]) -> bool: """Check if pytest is configured (pytest.ini or [tool.pytest] in pyproject.toml).""" search_dirs = [project_root] if backend_dir: search_dirs.append(os.path.join(project_root, backend_dir)) for d in search_dirs: if os.path.isfile(os.path.join(d, "pytest.ini")): return True pyproject = os.path.join(d, "pyproject.toml") if os.path.isfile(pyproject): try: with open(pyproject, "r", encoding="utf-8", errors="replace") as f: content = f.read() if "[tool.pytest" in content or "[pytest]" in content: return True except OSError: pass return False def _has_python_deps(project_root: str, backend_dir: Optional[str]) -> bool: """Check if project has Python dependencies (requirements.txt or pyproject.toml).""" search_dirs = [project_root] if backend_dir: search_dirs.append(os.path.join(project_root, backend_dir)) for d in search_dirs: if os.path.isfile(os.path.join(d, "requirements.txt")): return True if os.path.isfile(os.path.join(d, "pyproject.toml")): # only count if it has [project] or [tool.poetry] or [tool.setuptools] try: with open(os.path.join(d, "pyproject.toml"), "r", encoding="utf-8", errors="replace") as f: content = f.read() if "[project]" in content or "[tool.poetry]" in content or "[tool.setuptools]" in content: return True except OSError: pass return False def _detect_python_import_target(py_dir: str) -> Optional[str]: """Detect the main Python import target from directory structure. Looks for common patterns: app/__init__.py, src/__init__.py, main.py, app/main.py. Returns the import path string or None. """ if os.path.isfile(os.path.join(py_dir, "app", "__init__.py")): return "app" if os.path.isfile(os.path.join(py_dir, "app", "main.py")): return "app.main" if os.path.isfile(os.path.join(py_dir, "src", "__init__.py")): return "src" if os.path.isfile(os.path.join(py_dir, "main.py")): return "main" return None def _detect_stack_descriptor(project_root: str, backend_dir: Optional[str], frontend_dir: Optional[str]) -> str: """Return a short stack descriptor for the config row (informational only). This is NOT used for check routing — checks are discovered generically. Examples: 'python', 'nodejs', 'python+nodejs', 'nx-monorepo', 'unknown'. """ has_py = _has_python_deps(project_root, backend_dir) has_pkg = os.path.isfile(os.path.join(project_root, "package.json")) has_nx = os.path.isfile(os.path.join(project_root, "nx.json")) if has_nx: parts = [] if has_py: parts.append("python") parts.append("nx-monorepo") return "+".join(parts) if parts else "nx-monorepo" parts = [] if has_py: parts.append("python") if has_pkg or frontend_dir: parts.append("nodejs") if not parts: return "unknown" return "+".join(parts) # --------------------------------------------------------------------------- # Check discovery — read project config files and build a generic check list # --------------------------------------------------------------------------- def _discover_checks(project_root: str) -> List[Dict[str, Any]]: """Discover available checks from project config files. Reads package.json scripts, Makefile targets, nx.json presence, and pyproject.toml/pytest config to build a generic check list. Each check dict: {name, cmd, expect, level, category} Categories: lint, build, import-test, test, ops, security Levels: L1 (fast, static), L2 (build/import), L3 (full test), L4 (ops/security) """ checks: List[Dict[str, Any]] = [] backend_dir = _detect_backend_dir(project_root) frontend_dir = _detect_frontend_dir(project_root) root_scripts = _read_package_json_scripts(project_root) mk_targets = _parse_makefile_targets(project_root) has_nx = os.path.isfile(os.path.join(project_root, "nx.json")) # ------------------------------------------------------------------ # L1: Lint / static checks # ------------------------------------------------------------------ # Python lint (ruff) if Python project if _has_python_deps(project_root, backend_dir): py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root checks.append({ "name": "lint_ruff_check", "cmd": f"ruff check {shlex.quote(py_dir)}", "expect": 0, "level": "L1", "category": "lint", }) checks.append({ "name": "lint_ruff_format", "cmd": f"ruff format --check {shlex.quote(py_dir)}", "expect": 0, "level": "L1", "category": "lint", }) # JS/TS lint — root level if "lint" in root_scripts: checks.append({ "name": "lint_npm_root", "cmd": f"cd {shlex.quote(project_root)} && npm run lint 2>&1 | tail -20", "expect": 0, "level": "L1", "category": "lint", }) # JS/TS lint — frontend if frontend_dir: fe_path = os.path.join(project_root, frontend_dir) fe_scripts = _read_package_json_scripts(fe_path) if "lint" in fe_scripts: checks.append({ "name": f"lint_{frontend_dir.replace('/', '_')}", "cmd": f"cd {shlex.quote(fe_path)} && npm run lint 2>&1 | tail -20", "expect": 0, "level": "L1", "category": "lint", }) # JS/TS lint — backend (if separate JS/TS backend) if backend_dir: be_path = os.path.join(project_root, backend_dir) be_scripts = _read_package_json_scripts(be_path) if "lint" in be_scripts: checks.append({ "name": f"lint_{backend_dir.replace('/', '_')}", "cmd": f"cd {shlex.quote(be_path)} && npm run lint 2>&1 | tail -20", "expect": 0, "level": "L1", "category": "lint", }) # Makefile lint target if "lint" in mk_targets: checks.append({ "name": "lint_makefile", "cmd": f"cd {shlex.quote(project_root)} && make lint 2>&1 | tail -20", "expect": 0, "level": "L1", "category": "lint", }) # ------------------------------------------------------------------ # L2: Build + import tests # ------------------------------------------------------------------ # Nx build (if nx.json exists, prefer nx for monorepo builds) if has_nx: checks.append({ "name": "build_nx", "cmd": f"cd {shlex.quote(project_root)} && npx nx run-many --target=build --all 2>&1 | tail -30", "expect": 0, "level": "L2", "category": "build", }) else: # Frontend build if frontend_dir: fe_path = os.path.join(project_root, frontend_dir) fe_scripts = _read_package_json_scripts(fe_path) if "build" in fe_scripts: checks.append({ "name": f"build_{frontend_dir.replace('/', '_')}", "cmd": f"cd {shlex.quote(fe_path)} && npm run build 2>&1 | tail -20", "expect": 0, "level": "L2", "category": "build", }) # Backend build (JS/TS) if backend_dir: be_path = os.path.join(project_root, backend_dir) be_scripts = _read_package_json_scripts(be_path) if "build" in be_scripts: checks.append({ "name": f"build_{backend_dir.replace('/', '_')}", "cmd": f"cd {shlex.quote(be_path)} && npm run build 2>&1 | tail -20", "expect": 0, "level": "L2", "category": "build", }) # Root build if "build" in root_scripts: checks.append({ "name": "build_root", "cmd": f"cd {shlex.quote(project_root)} && npm run build 2>&1 | tail -20", "expect": 0, "level": "L2", "category": "build", }) # Makefile build if "build" in mk_targets: checks.append({ "name": "build_makefile", "cmd": f"cd {shlex.quote(project_root)} && make build 2>&1 | tail -20", "expect": 0, "level": "L2", "category": "build", }) # Python import test if _has_python_deps(project_root, backend_dir): py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root import_target = _detect_python_import_target(py_dir) if import_target: checks.append({ "name": f"import_test_{import_target.replace('.', '_')}", "cmd": f"cd {shlex.quote(py_dir)} && python -c 'import {import_target}' 2>&1", "expect": 0, "level": "L2", "category": "import-test", }) # ------------------------------------------------------------------ # L3: Full test suite # ------------------------------------------------------------------ # Python pytest if _has_pytest_config(project_root, backend_dir): py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root checks.append({ "name": "test_pytest", "cmd": f"cd {shlex.quote(py_dir)} && python -m pytest -q --tb=no 2>&1 | tail -20", "expect": 0, "level": "L3", "category": "test", }) # Nx test if has_nx: checks.append({ "name": "test_nx", "cmd": f"cd {shlex.quote(project_root)} && npx nx run-many --target=test --all 2>&1 | tail -30", "expect": 0, "level": "L3", "category": "test", }) else: # JS/TS test — root if "test" in root_scripts: checks.append({ "name": "test_npm_root", "cmd": f"cd {shlex.quote(project_root)} && npm test 2>&1 | tail -30", "expect": 0, "level": "L3", "category": "test", }) # JS/TS test — frontend if frontend_dir: fe_path = os.path.join(project_root, frontend_dir) fe_scripts = _read_package_json_scripts(fe_path) if "test" in fe_scripts: checks.append({ "name": f"test_{frontend_dir.replace('/', '_')}", "cmd": f"cd {shlex.quote(fe_path)} && npm test 2>&1 | tail -30", "expect": 0, "level": "L3", "category": "test", }) # JS/TS test — backend if backend_dir: be_path = os.path.join(project_root, backend_dir) be_scripts = _read_package_json_scripts(be_path) if "test" in be_scripts: checks.append({ "name": f"test_{backend_dir.replace('/', '_')}", "cmd": f"cd {shlex.quote(be_path)} && npm test 2>&1 | tail -30", "expect": 0, "level": "L3", "category": "test", }) # Makefile test if "test" in mk_targets: checks.append({ "name": "test_makefile", "cmd": f"cd {shlex.quote(project_root)} && make test 2>&1 | tail -30", "expect": 0, "level": "L3", "category": "test", }) # ------------------------------------------------------------------ # L4: Ops + Security # ------------------------------------------------------------------ # Ops: docker compose config validation compose_file = None for cf in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"): if os.path.isfile(os.path.join(project_root, cf)): compose_file = cf break if compose_file: checks.append({ "name": "ops_docker_compose_config", "cmd": f"cd {shlex.quote(project_root)} && docker compose -f {compose_file} config --quiet 2>&1", "expect": 0, "level": "L4", "category": "ops", }) # Security: npm audit (root or any sub-project with package.json) if os.path.isfile(os.path.join(project_root, "package.json")): checks.append({ "name": "security_npm_audit", "cmd": f"cd {shlex.quote(project_root)} && npm audit --omit=dev 2>&1 | tail -20", "expect": 0, "level": "L4", "category": "security", }) # Security: pip-audit (if requirements.txt found) if _has_python_deps(project_root, backend_dir): py_dir = os.path.join(project_root, backend_dir) if backend_dir else project_root reqs_file = os.path.join(py_dir, "requirements.txt") if os.path.isfile(reqs_file): checks.append({ "name": "security_pip_audit", "cmd": f"pip-audit -r {shlex.quote(reqs_file)} 2>&1 | tail -20", "expect": 0, "level": "L4", "category": "security", }) return checks # --------------------------------------------------------------------------- # Gate-level filtering # --------------------------------------------------------------------------- def _filter_checks_by_gate( all_checks: List[Dict[str, Any]], gate_level: str, ) -> List[Dict[str, Any]]: """Filter discovered checks based on gate_level. implementation, quality-review → L1 + L2 (lint + build + import test) deployment, release → L1 + L2 + L3 (+ full test suite) ops-gate → L4 ops category only security-gate → L4 security category only """ gate_level = gate_level.lower().strip() if gate_level in ("implementation", "quality-review"): levels = {"L1", "L2"} return [c for c in all_checks if c.get("level") in levels] elif gate_level in ("deployment", "release"): levels = {"L1", "L2", "L3"} return [c for c in all_checks if c.get("level") in levels] elif gate_level == "ops-gate": return [c for c in all_checks if c.get("category") == "ops"] elif gate_level == "security-gate": return [c for c in all_checks if c.get("category") == "security"] else: # Unknown gate: run L1 only (safe default) return [c for c in all_checks if c.get("level") == "L1"] # --------------------------------------------------------------------------- # Check execution (generic — no hardcoded exit-code assumptions) # --------------------------------------------------------------------------- def _run_check(check: Dict[str, Any], project_root: str, timeout: int = 300) -> Dict[str, Any]: """Run a single check, return result dict. The command is already fully resolved (with absolute paths) by the discovery phase. We just execute it and compare the exit code to the expected value. No special-case exit code handling. """ name = check["name"] cmd = check["cmd"] expect = check.get("expect", 0) start = time.time() try: proc = subprocess.run( ["bash", "-lc", f"set -o pipefail; {cmd}"], shell=False, capture_output=True, text=True, timeout=timeout, ) duration_ms = int((time.time() - start) * 1000) stdout = redact_secrets(proc.stdout[-500:] if proc.stdout else "") stderr = redact_secrets(proc.stderr[-500:] if proc.stderr else "") detail = redact_secrets((stdout + "\n" + stderr).strip()[:300]) or f"exit={proc.returncode}" # Generic: pass if exit code matches expected, fail otherwise. # No special-case for exit code 5 or any other value. result = "pass" if proc.returncode == expect else "fail" return { "name": name, "level": check.get("level", "?"), "category": check.get("category", "?"), "result": result, "duration_ms": duration_ms, "detail": detail, "exit_code": proc.returncode, } except subprocess.TimeoutExpired: return { "name": name, "level": check.get("level", "?"), "category": check.get("category", "?"), "result": "fail", "duration_ms": timeout * 1000, "detail": f"timeout after {timeout}s", "exit_code": -1, } except FileNotFoundError as e: return { "name": name, "level": check.get("level", "?"), "category": check.get("category", "?"), "result": "skip", "duration_ms": 0, "detail": f"command not found: {e}", "exit_code": -1, } except Exception as e: # pragma: no cover return { "name": name, "level": check.get("level", "?"), "category": check.get("category", "?"), "result": "fail", "duration_ms": 0, "detail": f"exception: {e}", "exit_code": -1, } def _suggest_next_action(failed_checks: List[Dict[str, Any]]) -> str: """Generate a human-readable next-action suggestion from failed checks. Generic: uses the check category to suggest an action, not hardcoded check names. """ if not failed_checks: return "No actions needed" actions: List[str] = [] for c in failed_checks: category = c.get("category", "") name = c.get("name", "unknown") if category == "lint": actions.append(f"Fix linting issues reported by {name}") elif category == "build": actions.append(f"Fix build errors reported by {name}") elif category == "import-test": actions.append(f"Fix import errors reported by {name}") elif category == "test": actions.append(f"Fix failing tests reported by {name}") elif category == "ops": actions.append(f"Fix ops issue reported by {name}") elif category == "security": actions.append(f"Fix security vulnerability reported by {name}") else: actions.append(f"Investigate {name}") return "; ".join(actions) # --------------------------------------------------------------------------- # DB persistence (kept from original — unchanged) # --------------------------------------------------------------------------- def _persist_run( project_id: int, gate_level: str, decision: str, checks: List[Dict[str, Any]], blockers: List[str], next_action: str, duration_ms: int, triggered_by: str, ) -> int: """Insert a run into orch_quality_runs, return the new id.""" import sqlite3 conn = sqlite3.connect(DB_PATH) try: cur = conn.execute( """ INSERT INTO orch_quality_runs (project_id, gate_level, decision, checks_json, blockers_json, next_action, duration_ms, triggered_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( project_id, gate_level, decision, json.dumps(checks), json.dumps(blockers), next_action, duration_ms, triggered_by, ), ) conn.commit() return cur.lastrowid finally: conn.close() def _fetch_last_run(project_id: int) -> Optional[Dict[str, Any]]: import sqlite3 conn = sqlite3.connect(DB_PATH) try: row = conn.execute( "SELECT id, project_id, gate_level, decision, checks_json, blockers_json, next_action, duration_ms, triggered_by, created_at FROM orch_quality_runs WHERE project_id = ? ORDER BY id DESC LIMIT 1", (project_id,), ).fetchone() if not row: return None return { "id": row[0], "project_id": row[1], "gate_level": row[2], "decision": row[3], "checks": json.loads(row[4]) if row[4] else [], "blockers": json.loads(row[5]) if row[5] else [], "next_action": row[6], "duration_ms": row[7], "triggered_by": row[8], "created_at": row[9], } finally: conn.close() def _list_runs(project_id: int, limit: int = 10) -> List[Dict[str, Any]]: import sqlite3 conn = sqlite3.connect(DB_PATH) try: rows = conn.execute( "SELECT id, gate_level, decision, duration_ms, triggered_by, created_at FROM orch_quality_runs WHERE project_id = ? ORDER BY id DESC LIMIT ?", (project_id, limit), ).fetchall() return [ { "id": r[0], "gate_level": r[1], "decision": r[2], "duration_ms": r[3], "triggered_by": r[4], "created_at": r[5], } for r in rows ] finally: conn.close() def _show_config(project_id: int) -> Optional[Dict[str, Any]]: import sqlite3 conn = sqlite3.connect(DB_PATH) try: row = conn.execute( "SELECT project_id, tech_stack, coverage_min_pct, strict_types, require_security_scan, require_openapi_check, custom_checks_json, updated_at FROM orch_quality_config WHERE project_id = ?", (project_id,), ).fetchone() if not row: return None return { "project_id": row[0], "tech_stack": row[1], "coverage_min_pct": row[2], "strict_types": bool(row[3]), "require_security_scan": bool(row[4]), "require_openapi_check": bool(row[5]), "custom_checks_json": row[6], "updated_at": row[7], } finally: conn.close() def _ensure_config(project_id: int, tech_stack: str) -> None: """Create or update orch_quality_config for a project.""" import sqlite3 conn = sqlite3.connect(DB_PATH) try: conn.execute( """ INSERT INTO orch_quality_config (project_id, tech_stack, coverage_min_pct, strict_types, require_security_scan, require_openapi_check, updated_at) VALUES (?, ?, 70, 1, 1, 0, datetime('now')) ON CONFLICT(project_id) DO UPDATE SET tech_stack = excluded.tech_stack, updated_at = datetime('now') """, (project_id, tech_stack), ) conn.commit() finally: conn.close() # --------------------------------------------------------------------------- # Tool class # --------------------------------------------------------------------------- VALID_GATE_LEVELS = { "implementation", "quality-review", "deployment", "release", "ops-gate", "security-gate", } class QualityGate(Tool): """Generic phase gate validation (DB-backed). Discovers checks from project config files (package.json, Makefile, nx.json, pyproject.toml) — no hardcoded tech-stack assumptions. Actions: run: execute discovered checks, persist run, return decision show_last: return most recent run for project list_runs: return summary of recent runs (default 10) show_config: return orch_quality_config for project Gate levels: implementation, quality-review → lint + build + import test deployment, release → lint + build + import test + full test ops-gate → docker compose config validation security-gate → dependency audit (npm/pip) """ async def execute( self, project_name: str = "", project_root: str = "", action: str = "run", gate_level: str = "quality-review", triggered_by: str = "manual", dry_run: bool = False, **kwargs, ) -> Response: name = _resolve_name(project_name, project_root) if not name: return Response(message=json.dumps({"error": "no project_name or project_root given"}), break_loop=False) # resolve project_id (no auto-create; LookupError on unknown name) try: project_id = get_project_id(name) if project_id is None: raise LookupError(f"project '{name}' is not registered") except (ValueError, LookupError) as e: return Response(message=json.dumps({"error": str(e)}), break_loop=False) except PluginDBError as e: return Response(message=json.dumps({"error": f"Database error: {e}"}), break_loop=False) # Determine project_root: prefer explicit, else default to dev-projects dir if not project_root: project_root = f"/a0/usr/workdir/dev-projects/{name}/" if not os.path.isdir(project_root): return Response(message=json.dumps({"error": f"project_root {project_root!r} does not exist; clone the repo first"}), break_loop=False) # --- Non-run actions (same as original) --- if action == "show_last": run = _fetch_last_run(project_id) return Response(message=json.dumps(run or {"info": "no runs yet"}, indent=2), break_loop=False) if action == "list_runs": runs = _list_runs(project_id) return Response(message=json.dumps(runs, indent=2), break_loop=False) if action == "show_config": cfg = _show_config(project_id) return Response(message=json.dumps(cfg or {"info": "no config yet"}, indent=2), break_loop=False) # --- action == 'run' (default) --- # Validate gate_level if gate_level not in VALID_GATE_LEVELS: return Response(message=json.dumps({ "error": f"invalid gate_level {gate_level!r}; valid: {sorted(VALID_GATE_LEVELS)}" }), break_loop=False) # Discover all available checks from project config files all_checks = _discover_checks(project_root) if not all_checks: return Response(message=json.dumps({ "error": f"no checks discovered in {project_root}", "project_root": project_root, "hint": "ensure package.json, Makefile, nx.json, or pyproject.toml exists", }), break_loop=False) # Filter checks for this gate level checks_def = _filter_checks_by_gate(all_checks, gate_level) if not checks_def: return Response(message=json.dumps({ "error": f"no checks match gate_level={gate_level}", "project_root": project_root, "discovered_categories": sorted({c.get("category", "?") for c in all_checks}), }), break_loop=False) # Detect stack descriptor for config row (informational only) backend_dir = _detect_backend_dir(project_root) frontend_dir = _detect_frontend_dir(project_root) stack_desc = _detect_stack_descriptor(project_root, backend_dir, frontend_dir) _ensure_config(project_id, stack_desc) if dry_run: return Response(message=json.dumps({ "project": name, "project_id": project_id, "tech_stack": stack_desc, "gate_level": gate_level, "would_run": [{ "name": c["name"], "category": c.get("category", "?"), "level": c.get("level", "?"), } for c in checks_def], }, indent=2), break_loop=False) # Execute checks start = time.time() results: List[Dict[str, Any]] = [] for c in checks_def: results.append(_run_check(c, project_root)) duration_ms = int((time.time() - start) * 1000) failed = [r for r in results if r["result"] == "fail"] skipped = [r for r in results if r["result"] == "skip"] decision = "fail" if failed else ("warn" if skipped else "pass") blockers = [f"{r['name']}: {r['detail']}" for r in failed] next_action = _suggest_next_action(failed) run_id = _persist_run( project_id=project_id, gate_level=gate_level, decision=decision, checks=results, blockers=blockers, next_action=next_action, duration_ms=duration_ms, triggered_by=triggered_by, ) summary = { "run_id": run_id, "project": name, "project_id": project_id, "tech_stack": stack_desc, "gate_level": gate_level, "decision": decision, "checks": results, "blockers": blockers, "next_action": next_action, "duration_ms": duration_ms, "triggered_by": triggered_by, } return Response(message=json.dumps(summary, indent=2), break_loop=False)