A-SW Hub / Repositories
Local Gitea · READ ONLY
← 저장소 Repositories
REPOSITORY

jbnu/environment-adaptive-perception Private

[참조 MVP] 환경 적응형 인지 A-SW — 조도·강우·먼지 조건에 따라 탐지 임계값과 센서 우선순위를 조정합니다.

https://git.agrithing.ai/jbnu/environment-adaptive-perception.git
7 commits
COMMIT DETAIL

ci: add language-specific static and dynamic analysis

A-SW Reference Factory <[email protected]> · 2026. 8. 2. 오후 3:53:27
ae4151f3880e8116f4555cb4bb89ded54024481f
3 changed files +147 −8 parent 2ed89a8
added .asw/quality-profile.json +32−0 View file
@@ -0,0 +1,32 @@
1 + {
2 + "schemaVersion": "asw.quality-profile/v1",
3 + "repository": "jbnu/environment-adaptive-perception",
4 + "scope": "runtime-component",
5 + "language": "python",
6 + "runtime": "python>=3.11",
7 + "staticAnalysis": {
8 + "adapter": "python-ast-policy",
9 + "command": "python tools/quality_scan.py static --output dist/quality/static-analysis.json",
10 + "reportFormat": "asw.quality-result/v1",
11 + "required": true,
12 + "thresholds": {
13 + "critical": 0,
14 + "high": 0,
15 + "complexityWarning": 12
16 + }
17 + },
18 + "dynamicAnalysis": {
19 + "adapter": "unittest-scenario",
20 + "command": "python tools/quality_scan.py dynamic --output dist/quality/dynamic-analysis.json",
21 + "reportFormat": "asw.quality-result/v1",
22 + "required": true,
23 + "thresholds": {
24 + "minimumPassRate": 1
25 + }
26 + },
27 + "evidence": [
28 + "quality/static-analysis.json",
29 + "quality/dynamic-analysis.json",
30 + "quality/summary.json"
31 + ]
32 + }
modified .gitea/workflows/verify.yml +11−8 View file
@@ -10,16 +10,19 @@ jobs:
10 10 runs-on: ubuntu-latest
11 11 steps:
12 12 - uses: actions/checkout@v4
13 - name: Validate interface contract
14 run: python -m json.tool contracts/component.interface.json >/dev/null
15 - name: Run component tests
16 run: python -m unittest discover -s tests -v
17 - name: Exercise example
18 run: python run.py examples/input.json | python -m json.tool >/dev/null
13 + - name: Validate quality profile and interface contract
14 + run: |
15 + python -m json.tool .asw/quality-profile.json >/dev/null
16 + python -m json.tool contracts/component.interface.json >/dev/null
17 + - name: Static analysis · Python AST policy
18 + run: python tools/quality_scan.py static --output dist/quality/static-analysis.json
19 + - name: Dynamic analysis · unittest and scenario
20 + run: python tools/quality_scan.py dynamic --output dist/quality/dynamic-analysis.json
21 + - name: Assemble quality summary
22 + run: python tools/quality_scan.py summary --output dist/quality/summary.json
19 23 - name: Build evidence package
20 24 run: |
21 mkdir -p dist
22 tar -czf dist/reference-mvp.tar.gz asw_component contracts examples release README.md run.py
25 + tar -czf dist/reference-mvp.tar.gz asw_component contracts examples release .asw README.md run.py
23 26 sha256sum dist/reference-mvp.tar.gz > dist/checksums.sha256
24 27 - name: Upload evidence
25 28 uses: christopherhx/gitea-upload-artifact@v4
added tools/quality_scan.py +104−0 View file
@@ -0,0 +1,104 @@
1 + from __future__ import annotations
2 +
3 + import argparse
4 + import ast
5 + import json
6 + import subprocess
7 + import sys
8 + from pathlib import Path
9 +
10 + ROOT = Path(__file__).resolve().parents[1]
11 + SCHEMA = "asw.quality-result/v1"
12 +
13 +
14 + def write_result(path: str, payload: dict) -> None:
15 + destination = ROOT / path
16 + destination.parent.mkdir(parents=True, exist_ok=True)
17 + destination.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
18 +
19 +
20 + def source_files() -> list[Path]:
21 + ignored = {".git", "dist", "tools", "tests", "__pycache__"}
22 + return sorted(path for path in ROOT.rglob("*.py") if not any(part in ignored for part in path.relative_to(ROOT).parts))
23 +
24 +
25 + def static_analysis() -> dict:
26 + issues: list[dict] = []
27 + files = source_files()
28 + lines = functions = maximum_complexity = 0
29 + for path in files:
30 + relative = str(path.relative_to(ROOT))
31 + source = path.read_text(encoding="utf-8")
32 + lines += len(source.splitlines())
33 + try:
34 + tree = ast.parse(source, filename=relative)
35 + except SyntaxError as error:
36 + issues.append({"rule": "PY-SYNTAX", "severity": "CRITICAL", "path": relative, "line": error.lineno or 0, "message": error.msg})
37 + continue
38 + for node in ast.walk(tree):
39 + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
40 + functions += 1
41 + complexity = 1 + sum(isinstance(item, (ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ast.BoolOp, ast.Match)) for item in ast.walk(node))
42 + maximum_complexity = max(maximum_complexity, complexity)
43 + if complexity > 12:
44 + issues.append({"rule": "PY-COMPLEXITY", "severity": "MEDIUM", "path": relative, "line": node.lineno, "message": f"{node.name} complexity {complexity} exceeds warning threshold 12"})
45 + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in {"eval", "exec"}:
46 + 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"})
47 + blocking = [item for item in issues if item["severity"] in {"CRITICAL", "HIGH"}]
48 + return {
49 + "schemaVersion": SCHEMA,
50 + "kind": "static-analysis",
51 + "adapter": "python-ast-policy",
52 + "language": "python",
53 + "state": "PASSED" if not blocking else "FAILED",
54 + "metrics": {"files": len(files), "lines": lines, "functions": functions, "maximumComplexity": maximum_complexity, "blockingIssues": len(blocking), "warnings": len(issues) - len(blocking)},
55 + "issues": issues,
56 + }
57 +
58 +
59 + def command_result(command: list[str]) -> dict:
60 + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
61 + return {"command": " ".join(command), "exitCode": result.returncode, "stdout": result.stdout[-4000:], "stderr": result.stderr[-4000:]}
62 +
63 +
64 + def dynamic_analysis() -> dict:
65 + test_cases = 0
66 + for path in (ROOT / "tests").glob("test_*.py") if (ROOT / "tests").exists() else []:
67 + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
68 + test_cases += sum(isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_") for node in ast.walk(tree))
69 + commands = [command_result([sys.executable, "-m", "unittest", "discover", "-s", "tests", "-v"])]
70 + if (ROOT / "run.py").exists() and (ROOT / "examples" / "input.json").exists():
71 + commands.append(command_result([sys.executable, "run.py", "examples/input.json"]))
72 + passed = sum(item["exitCode"] == 0 for item in commands)
73 + rate = passed / len(commands) if commands else 0
74 + return {
75 + "schemaVersion": SCHEMA,
76 + "kind": "dynamic-analysis",
77 + "adapter": "unittest-scenario",
78 + "language": "python",
79 + "state": "PASSED" if rate == 1 else "FAILED",
80 + "metrics": {"testCases": test_cases, "commands": len(commands), "passedCommands": passed, "passRate": rate},
81 + "commands": commands,
82 + }
83 +
84 +
85 + def summary() -> dict:
86 + static = json.loads((ROOT / "dist/quality/static-analysis.json").read_text(encoding="utf-8"))
87 + dynamic = json.loads((ROOT / "dist/quality/dynamic-analysis.json").read_text(encoding="utf-8"))
88 + return {
89 + "schemaVersion": "asw.quality-summary/v1",
90 + "language": "python",
91 + "state": "PASSED" if static["state"] == dynamic["state"] == "PASSED" else "FAILED",
92 + "results": {"staticAnalysis": static["state"], "dynamicAnalysis": dynamic["state"]},
93 + "metrics": {"blockingIssues": static["metrics"]["blockingIssues"], "testCases": dynamic["metrics"]["testCases"], "passRate": dynamic["metrics"]["passRate"]},
94 + }
95 +
96 +
97 + parser = argparse.ArgumentParser()
98 + parser.add_argument("mode", choices=["static", "dynamic", "summary"])
99 + parser.add_argument("--output", required=True)
100 + arguments = parser.parse_args()
101 + result = static_analysis() if arguments.mode == "static" else dynamic_analysis() if arguments.mode == "dynamic" else summary()
102 + write_result(arguments.output, result)
103 + print(json.dumps({"kind": result.get("kind", "summary"), "state": result["state"], "metrics": result.get("metrics", {})}, ensure_ascii=False))
104 + raise SystemExit(0 if result["state"] == "PASSED" else 1)