REPOSITORY
metafarmers/fruit-work-target-perception Private
[참조 MVP] 과채류 작업대상 인식 — RGB-D 관측에서 과실과 생장점을 식별하고 로봇 작업 후보를 구조화하는 참조 MVP입니다.
https://git.agrithing.ai/metafarmers/fruit-work-target-perception.git 54a9587 feat: 과채류 작업대상 인식 구체 MVP 구현
bb3f276 ci: add static dynamic and evidence gates
66d6ea9 feat: add executable institution reference MVP
bee1524 docs: define product role and interface contract
1 items · feat/concrete-mvp-v2
·/ tools/quality_scan.py
python · 5.2 KB · 66753f4 Download
from __future__ import annotationsimport argparseimport astimport jsonimport subprocessimport sysfrom pathlib import PathROOT = Path(__file__).resolve().parents[1]SCHEMA = "asw.quality-result/v1"BRANCH_NODES = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ast.BoolOp)if hasattr(ast, "Match"): BRANCH_NODES += (ast.Match,)def write_result(path: str, payload: dict) -> None: destination = ROOT / path destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")def source_files() -> list[Path]: ignored = {".git", "dist", "tools", "tests", "__pycache__"} return sorted(path for path in ROOT.rglob("*.py") if not any(part in ignored for part in path.relative_to(ROOT).parts))def static_analysis() -> dict: issues: list[dict] = [] files = source_files() lines = functions = maximum_complexity = 0 for path in files: relative = str(path.relative_to(ROOT)) source = path.read_text(encoding="utf-8") lines += len(source.splitlines()) try: tree = ast.parse(source, filename=relative) except SyntaxError as error: issues.append({"rule": "PY-SYNTAX", "severity": "CRITICAL", "path": relative, "line": error.lineno or 0, "message": error.msg}) continue for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): functions += 1 complexity = 1 + sum(isinstance(item, BRANCH_NODES) for item in ast.walk(node)) maximum_complexity = max(maximum_complexity, complexity) if complexity > 12: issues.append({"rule": "PY-COMPLEXITY", "severity": "MEDIUM", "path": relative, "line": node.lineno, "message": f"{node.name} complexity {complexity} exceeds warning threshold 12"}) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in {"eval", "exec"}: issues.append({"rule": "PY-DYNAMIC-EXEC", "severity": "HIGH", "path": relative, "line": node.lineno, "message": f"{node.func.id}() is not allowed in the reference policy"}) blocking = [item for item in issues if item["severity"] in {"CRITICAL", "HIGH"}] return { "schemaVersion": SCHEMA, "kind": "static-analysis", "adapter": "python-ast-policy", "language": "python", "state": "PASSED" if not blocking else "FAILED", "metrics": {"files": len(files), "lines": lines, "functions": functions, "maximumComplexity": maximum_complexity, "blockingIssues": len(blocking), "warnings": len(issues) - len(blocking)}, "issues": issues, }def command_result(command: list[str]) -> dict: result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False) return {"command": " ".join(command), "exitCode": result.returncode, "stdout": result.stdout[-4000:], "stderr": result.stderr[-4000:]}def dynamic_analysis() -> dict: test_cases = 0 for path in (ROOT / "tests").glob("test_*.py") if (ROOT / "tests").exists() else []: tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) test_cases += sum(isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_") for node in ast.walk(tree)) commands = [command_result([sys.executable, "-m", "unittest", "discover", "-s", "tests", "-v"])] if (ROOT / "run.py").exists() and (ROOT / "examples" / "input.json").exists(): commands.append(command_result([sys.executable, "run.py", "examples/input.json"])) passed = sum(item["exitCode"] == 0 for item in commands) rate = passed / len(commands) if commands else 0 return { "schemaVersion": SCHEMA, "kind": "dynamic-analysis", "adapter": "unittest-scenario", "language": "python", "state": "PASSED" if rate == 1 else "FAILED", "metrics": {"testCases": test_cases, "commands": len(commands), "passedCommands": passed, "passRate": rate}, "commands": commands, }def summary() -> dict: static = json.loads((ROOT / "dist/quality/static-analysis.json").read_text(encoding="utf-8")) dynamic = json.loads((ROOT / "dist/quality/dynamic-analysis.json").read_text(encoding="utf-8")) return { "schemaVersion": "asw.quality-summary/v1", "language": "python", "state": "PASSED" if static["state"] == dynamic["state"] == "PASSED" else "FAILED", "results": {"staticAnalysis": static["state"], "dynamicAnalysis": dynamic["state"]}, "metrics": {"blockingIssues": static["metrics"]["blockingIssues"], "testCases": dynamic["metrics"]["testCases"], "passRate": dynamic["metrics"]["passRate"]}, }parser = argparse.ArgumentParser()parser.add_argument("mode", choices=["static", "dynamic", "summary"])parser.add_argument("--output", required=True)arguments = parser.parse_args()result = static_analysis() if arguments.mode == "static" else dynamic_analysis() if arguments.mode == "dynamic" else summary()write_result(arguments.output, result)print(json.dumps({"kind": result.get("kind", "summary"), "state": result["state"], "metrics": result.get("metrics", {})}, ensure_ascii=False))raise SystemExit(0 if result["state"] == "PASSED" else 1)