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

tymict/icu-platform Private

[참조 MVP] ICU 실행 플랫폼 — 센서 프레임과 A-SW 목표값을 ICU 액추에이터 명령 봉투로 변환합니다.

https://git.agrithing.ai/tymict/icu-platform.git
6 commits
COMMIT DETAIL

feat: adopt cpp ACU SDV reference implementation

A-SW Reference Factory <[email protected]> · 2026. 8. 2. 오후 9:25:54
9d25865adffb79bd367c37d8c45c77cba62cb986
15 changed files +112 −128 parent 6da7869
modified .asw/quality-profile.json +7−5 View file
@@ -2,21 +2,23 @@
2 2 "schemaVersion": "asw.quality-profile/v1",
3 3 "repository": "tymict/icu-platform",
4 4 "scope": "runtime-component",
5 "language": "python",
6 "runtime": "python>=3.11",
5 + "language": "cpp",
6 + "runtime": "C++17",
7 + "executionTarget": "acu",
8 + "buildSystem": "g++",
7 9 "staticAnalysis": {
8 "adapter": "python-ast-policy",
10 + "adapter": "g++-warning-policy",
9 11 "command": "python tools/quality_scan.py static --output dist/quality/static-analysis.json",
10 12 "reportFormat": "asw.quality-result/v1",
11 13 "required": true,
12 14 "thresholds": {
13 15 "critical": 0,
14 16 "high": 0,
15 "complexityWarning": 12
17 + "warnings": 0
16 18 }
17 19 },
18 20 "dynamicAnalysis": {
19 "adapter": "unittest-scenario",
21 + "adapter": "g++-test-scenario",
20 22 "command": "python tools/quality_scan.py dynamic --output dist/quality/dynamic-analysis.json",
21 23 "reportFormat": "asw.quality-result/v1",
22 24 "required": true,
added .asw/sdv-component.json +17−0 View file
@@ -0,0 +1,17 @@
1 + {
2 + "schemaVersion": "asw.sdv-component/v1",
3 + "repository": "tymict/icu-platform",
4 + "executionTarget": "acu",
5 + "language": "cpp",
6 + "runtime": "C++17",
7 + "buildSystem": "g++",
8 + "transport": "CAN-FD",
9 + "lifecycle": [
10 + "source",
11 + "static-analysis",
12 + "build",
13 + "dynamic-test",
14 + "package",
15 + "release-gate"
16 + ]
17 + }
modified .gitea/workflows/verify.yml +6−5 View file
@@ -1,4 +1,4 @@
1 name: Verify reference MVP
1 + name: Verify SDV reference MVP
2 2
3 3 on:
4 4 push:
@@ -10,19 +10,20 @@ jobs:
10 10 runs-on: ubuntu-latest
11 11 steps:
12 12 - uses: actions/checkout@v4
13 - name: Validate quality profile and interface contract
13 + - name: Validate SDV metadata and interface contract
14 14 run: |
15 + python -m json.tool .asw/sdv-component.json >/dev/null
15 16 python -m json.tool .asw/quality-profile.json >/dev/null
16 17 python -m json.tool contracts/component.interface.json >/dev/null
17 - name: Static analysis · Python AST policy
18 + - name: Static analysis · g++-warning-policy
18 19 run: python tools/quality_scan.py static --output dist/quality/static-analysis.json
19 - name: Dynamic analysis · unittest and scenario
20 + - name: Dynamic analysis · g++-test-scenario
20 21 run: python tools/quality_scan.py dynamic --output dist/quality/dynamic-analysis.json
21 22 - name: Assemble quality summary
22 23 run: python tools/quality_scan.py summary --output dist/quality/summary.json
23 24 - name: Build evidence package
24 25 run: |
25 tar -czf dist/reference-mvp.tar.gz asw_component contracts examples release .asw README.md run.py
26 + tar -czf dist/reference-mvp.tar.gz src include tests Makefile contracts examples release .asw README.md
26 27 sha256sum dist/reference-mvp.tar.gz > dist/checksums.sha256
27 28 - name: Upload evidence
28 29 uses: christopherhx/gitea-upload-artifact@v4
added Makefile +5−0 View file
@@ -0,0 +1,5 @@
1 + all:
2 + g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -Iinclude src/component.cpp tests/component_test.cpp -o component_test
3 +
4 + check: all
5 + ./component_test
modified README.md +13−3 View file
@@ -9,7 +9,7 @@
9 9 - 사업 역할 기관: **티와이엠아이씨티**
10 10 - 기술 영역: **Runtime**
11 11 - 성숙도: 인터페이스 협의 및 파이프라인 검증용 MVP
12 - 실행환경: Python 3.11 이상, 외부 패키지 없음
12 + - 실행환경: C++17
13 13
14 14 ## 실행
15 15
@@ -22,13 +22,13 @@ CLI는 JSON 입력 하나를 받아 JSON 결과 하나를 출력합니다. 실
22 22
23 23 ## 저장소 구성
24 24
25 - `asw_component/component.py`: 최소 실행 로직
25 + - `src/component.cpp``include/component.hpp`: ACU 대상 최소 실행 로직
26 26 - `examples/input.json`: 재현 가능한 입력
27 27 - `contracts/`: 기계 판독형 인터페이스 계약
28 28 - `tests/`: 기준 동작 시험
29 29 - `release/manifest.json`: 소유기관·버전·호환성 선언
30 30 - `.asw/quality-profile.json`: 언어별 정적·동적 분석 정책
31 - `tools/quality_scan.py`: Python AST 정책 검사와 unittest·시나리오 동적분석
31 + - `tools/quality_scan.py`: cpp 정적 정책과 동적 시나리오 어댑터
32 32 - `.gitea/workflows/verify.yml`: 계약→정적분석→동적분석→증빙 패키지 CI
33 33
34 34 ## 완료 기준
@@ -37,3 +37,13 @@ CLI는 JSON 입력 하나를 받아 JSON 결과 하나를 출력합니다. 실
37 37 2. 계약 JSON과 단위시험이 통과합니다.
38 38 3. Gitea Actions가 정적·동적 분석 결과, 증빙 패키지와 SHA-256을 생성합니다.
39 39 4. 실제 기관 코드는 동일 계약을 구현한 뒤 별도 승인·MIL/HIL 검증을 거칩니다.
40 +
41 + ## SDV 실행 프로파일
42 +
43 + - 실행 노드: **ACU**
44 + - 구현 언어: **cpp**
45 + - 빌드 체계: **g++**
46 + - 연동 규약: **CAN-FD**
47 + - 로컬 검증: `make check`
48 +
49 + 이 프로파일은 실제 기관 구현의 기술 선택을 강제하지 않으며, A-SW Hub가 서로 다른 언어와 실행 노드의 품질 결과를 동일한 릴리스 게이트로 집계하는 방법을 보여줍니다.
deleted asw_component/__init__.py +0−3
@@ -1,3 +0,0 @@
1 from .component import execute
2
3 __all__ = ["execute"]
deleted asw_component/component.py +0−13
@@ -1,13 +0,0 @@
1 from __future__ import annotations
2
3 def _clamp(value, limit):
4 return max(-limit, min(limit, value))
5
6 def execute(payload: dict) -> dict:
7 desired, limits = payload["desired"], payload["limits"]
8 safe = payload.get("safety_state") == "RUN"
9 command = {
10 "steering_rad": _clamp(float(desired.get("steering_rad", 0)), float(limits["steering_rad"])) if safe else 0.0,
11 "speed_mps": max(0.0, min(float(desired.get("speed_mps", 0)), float(limits["speed_mps"]))) if safe else 0.0,
12 }
13 return {"sequence": int(payload["sequence"]), "accepted": safe, "actuator_command": command, "transport": "CAN-FD"}
added include/component.hpp +2−0 View file
@@ -0,0 +1,2 @@
1 + #pragma once
2 + namespace asw { struct ActuatorCommand { double steering_rad; double speed_mps; bool accepted; }; ActuatorCommand make_command(double steering, double speed, double steering_limit, double speed_limit, bool safe); }
deleted pyproject.toml +0−8
@@ -1,8 +0,0 @@
1 [project]
2 name = "icu_platform"
3 version = "0.1.0"
4 description = "센서 프레임과 A-SW 목표값을 ICU 액추에이터 명령 봉투로 변환합니다."
5 requires-python = ">=3.11"
6
7 [tool.unittest]
8 test-path = "tests"
modified release/manifest.json +9−4 View file
@@ -4,7 +4,7 @@
4 4 "displayName": "ICU 실행 플랫폼",
5 5 "owner": "티와이엠아이씨티",
6 6 "implementationAuthor": "주식회사 비아 (연동 검증용 참조 구현)",
7 "version": "0.1.0-mvp",
7 + "version": "0.2.0-multilang-mvp",
8 8 "domain": "Runtime",
9 9 "status": "REFERENCE_MVP",
10 10 "compatibility": {
@@ -12,9 +12,14 @@
12 12 "api": "asw-component/v1"
13 13 },
14 14 "evidence": [
15 "unit-test",
15 + "static-analysis",
16 + "dynamic-test",
16 17 "interface-contract",
17 "example-run",
18 + "quality-summary",
18 19 "sha256"
19 ]
20 + ],
21 + "language": "cpp",
22 + "executionTarget": "acu",
23 + "buildSystem": "g++",
24 + "transport": "CAN-FD"
20 25 }
deleted run.py +0−10
@@ -1,10 +0,0 @@
1 from __future__ import annotations
2 import json
3 import sys
4 from asw_component import execute
5
6 if __name__ == "__main__":
7 path = sys.argv[1] if len(sys.argv) > 1 else None
8 with open(path, encoding="utf-8") if path else sys.stdin as source:
9 payload = json.load(source)
10 print(json.dumps(execute(payload), ensure_ascii=False, indent=2))
added src/component.cpp +8−0 View file
@@ -0,0 +1,8 @@
1 + #include "component.hpp"
2 + #include <algorithm>
3 + namespace asw {
4 + ActuatorCommand make_command(double steering, double speed, double steering_limit, double speed_limit, bool safe) {
5 + if (!safe) return {0, 0, false};
6 + return {std::clamp(steering, -steering_limit, steering_limit), std::clamp(speed, 0.0, speed_limit), true};
7 + }
8 + }
added tests/component_test.cpp +3−0 View file
@@ -0,0 +1,3 @@
1 + #include "component.hpp"
2 + #include <cassert>
3 + int main() { const auto command = asw::make_command(0.8, 1.5, 0.55, 4, true); assert(command.accepted); assert(command.steering_rad == 0.55); assert(command.speed_mps == 1.5); return 0; }
deleted tests/test_component.py +0−18
@@ -1,18 +0,0 @@
1 from __future__ import annotations
2 import json
3 import unittest
4 from pathlib import Path
5 from asw_component import execute
6
7 ROOT = Path(__file__).resolve().parents[1]
8
9 class ComponentTest(unittest.TestCase):
10 def test_reference_scenario(self):
11 payload = json.loads((ROOT / "examples/input.json").read_text(encoding="utf-8"))
12 result = execute(payload)
13 self.assertTrue(result["accepted"])
14 self.assertEqual(result["sequence"], 42)
15 self.assertEqual(result["actuator_command"]["speed_mps"], 1.5)
16
17 if __name__ == "__main__":
18 unittest.main()
modified tools/quality_scan.py +42−59 View file
@@ -1,14 +1,18 @@
1 1 from __future__ import annotations
2 2
3 3 import argparse
4 import ast
5 4 import json
6 5 import subprocess
7 import sys
8 6 from pathlib import Path
9 7
10 8 ROOT = Path(__file__).resolve().parents[1]
11 SCHEMA = "asw.quality-result/v1"
9 + LANGUAGE = "cpp"
10 + STATIC_ADAPTER = "g++-warning-policy"
11 + DYNAMIC_ADAPTER = "g++-test-scenario"
12 + COMPILER = "g++"
13 + STANDARD = "c++17"
14 + EXTENSION = "cpp"
15 + BANNED = [["NATIVE-GETS","gets("],["NATIVE-STRCPY","strcpy("],["NATIVE-SPRINTF","sprintf("],["NATIVE-SYSTEM","system("]]
12 16
13 17
14 18 def write_result(path: str, payload: dict) -> None:
@@ -17,81 +21,60 @@ def write_result(path: str, payload: dict) -> None:
17 21 destination.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
18 22
19 23
24 + def run(command: list[str]) -> dict:
25 + completed = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
26 + return {"command": " ".join(command), "exitCode": completed.returncode, "stdout": completed.stdout[-4000:], "stderr": completed.stderr[-4000:]}
27 +
28 +
20 29 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))
30 + patterns = ["*.ts", "*.mjs"] if LANGUAGE == "typescript" else [f"*.{EXTENSION}", "*.h", "*.hpp"]
31 + files: set[Path] = set()
32 + for pattern in patterns:
33 + files.update((ROOT / "src").rglob(pattern))
34 + if (ROOT / "include").exists():
35 + files.update((ROOT / "include").rglob(pattern))
36 + return sorted(files)
23 37
24 38
25 39 def static_analysis() -> dict:
26 40 issues: list[dict] = []
27 41 files = source_files()
28 lines = functions = maximum_complexity = 0
42 + lines = 0
29 43 for path in files:
30 relative = str(path.relative_to(ROOT))
31 44 source = path.read_text(encoding="utf-8")
32 45 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:]}
46 + for rule, token in BANNED:
47 + if token in source:
48 + issues.append({"rule": rule, "severity": "HIGH", "path": str(path.relative_to(ROOT)), "line": source[:source.index(token)].count("\n") + 1, "message": f"forbidden token: {token}"})
49 + if LANGUAGE == "typescript":
50 + command = run(["node", "--check", "src/component.mjs"])
51 + else:
52 + command = run([COMPILER, f"-std={STANDARD}", "-Wall", "-Wextra", "-Wpedantic", "-Werror", "-Iinclude", "-fsyntax-only", f"src/component.{EXTENSION}"])
53 + if command["exitCode"] != 0:
54 + issues.append({"rule": "COMPILE-SYNTAX", "severity": "CRITICAL", "path": f"src/component.{EXTENSION}", "line": 0, "message": command["stderr"][-1000:]})
55 + blocking = [issue for issue in issues if issue["severity"] in {"CRITICAL", "HIGH"}]
56 + return {"schemaVersion": "asw.quality-result/v1", "kind": "static-analysis", "adapter": STATIC_ADAPTER, "language": LANGUAGE, "state": "PASSED" if not blocking else "FAILED", "metrics": {"files": len(files), "lines": lines, "blockingIssues": len(blocking), "warnings": len(issues) - len(blocking)}, "issues": issues, "commands": [command]}
62 57
63 58
64 59 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"]))
60 + output = ROOT / "dist" / "component_test"
61 + output.parent.mkdir(parents=True, exist_ok=True)
62 + if LANGUAGE == "typescript":
63 + commands = [run(["node", "--test", "tests/component.test.mjs"])]
64 + else:
65 + compile_command = [COMPILER, f"-std={STANDARD}", "-Wall", "-Wextra", "-Wpedantic", "-Werror", "-Iinclude", f"src/component.{EXTENSION}", f"tests/component_test.{EXTENSION}", "-o", str(output)]
66 + commands = [run(compile_command)]
67 + if commands[0]["exitCode"] == 0:
68 + commands.append(run([str(output)]))
72 69 passed = sum(item["exitCode"] == 0 for item in commands)
73 70 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 }
71 + return {"schemaVersion": "asw.quality-result/v1", "kind": "dynamic-analysis", "adapter": DYNAMIC_ADAPTER, "language": LANGUAGE, "state": "PASSED" if rate == 1 else "FAILED", "metrics": {"testCases": 1, "commands": len(commands), "passedCommands": passed, "passRate": rate}, "commands": commands}
83 72
84 73
85 74 def summary() -> dict:
86 75 static = json.loads((ROOT / "dist/quality/static-analysis.json").read_text(encoding="utf-8"))
87 76 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 }
77 + return {"schemaVersion": "asw.quality-summary/v1", "language": LANGUAGE, "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"]}}
95 78
96 79
97 80 parser = argparse.ArgumentParser()