moai-adk 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +8 -0
- moai_adk/__main__.py +86 -0
- moai_adk/cli/__init__.py +2 -0
- moai_adk/cli/commands/__init__.py +16 -0
- moai_adk/cli/commands/backup.py +56 -0
- moai_adk/cli/commands/doctor.py +184 -0
- moai_adk/cli/commands/init.py +284 -0
- moai_adk/cli/commands/restore.py +77 -0
- moai_adk/cli/commands/status.py +79 -0
- moai_adk/cli/commands/update.py +133 -0
- moai_adk/cli/main.py +12 -0
- moai_adk/cli/prompts/__init__.py +5 -0
- moai_adk/cli/prompts/init_prompts.py +159 -0
- moai_adk/core/__init__.py +2 -0
- moai_adk/core/git/__init__.py +24 -0
- moai_adk/core/git/branch.py +26 -0
- moai_adk/core/git/branch_manager.py +137 -0
- moai_adk/core/git/checkpoint.py +140 -0
- moai_adk/core/git/commit.py +68 -0
- moai_adk/core/git/event_detector.py +81 -0
- moai_adk/core/git/manager.py +127 -0
- moai_adk/core/project/__init__.py +2 -0
- moai_adk/core/project/backup_utils.py +84 -0
- moai_adk/core/project/checker.py +302 -0
- moai_adk/core/project/detector.py +105 -0
- moai_adk/core/project/initializer.py +174 -0
- moai_adk/core/project/phase_executor.py +297 -0
- moai_adk/core/project/validator.py +118 -0
- moai_adk/core/quality/__init__.py +6 -0
- moai_adk/core/quality/trust_checker.py +441 -0
- moai_adk/core/quality/validators/__init__.py +6 -0
- moai_adk/core/quality/validators/base_validator.py +19 -0
- moai_adk/core/template/__init__.py +8 -0
- moai_adk/core/template/backup.py +95 -0
- moai_adk/core/template/config.py +95 -0
- moai_adk/core/template/languages.py +44 -0
- moai_adk/core/template/merger.py +117 -0
- moai_adk/core/template/processor.py +310 -0
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +474 -0
- moai_adk/templates/.claude/agents/alfred/code-builder.md +534 -0
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +302 -0
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +175 -0
- moai_adk/templates/.claude/agents/alfred/git-manager.md +200 -0
- moai_adk/templates/.claude/agents/alfred/project-manager.md +152 -0
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +256 -0
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +247 -0
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +332 -0
- moai_adk/templates/.claude/commands/alfred/0-project.md +523 -0
- moai_adk/templates/.claude/commands/alfred/1-spec.md +531 -0
- moai_adk/templates/.claude/commands/alfred/2-build.md +413 -0
- moai_adk/templates/.claude/commands/alfred/3-sync.md +552 -0
- moai_adk/templates/.claude/hooks/alfred/README.md +238 -0
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +165 -0
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +79 -0
- moai_adk/templates/.claude/hooks/alfred/core/checkpoint.py +271 -0
- moai_adk/templates/.claude/hooks/alfred/core/context.py +110 -0
- moai_adk/templates/.claude/hooks/alfred/core/project.py +284 -0
- moai_adk/templates/.claude/hooks/alfred/core/tags.py +244 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +23 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/compact.py +51 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +25 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +80 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +71 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +41 -0
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +635 -0
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +691 -0
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +469 -0
- moai_adk/templates/.claude/settings.json +135 -0
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +68 -0
- moai_adk/templates/.github/workflows/moai-gitflow.yml +255 -0
- moai_adk/templates/.gitignore +41 -0
- moai_adk/templates/.moai/config.json +89 -0
- moai_adk/templates/.moai/memory/development-guide.md +367 -0
- moai_adk/templates/.moai/memory/spec-metadata.md +277 -0
- moai_adk/templates/.moai/project/product.md +121 -0
- moai_adk/templates/.moai/project/structure.md +150 -0
- moai_adk/templates/.moai/project/tech.md +221 -0
- moai_adk/templates/CLAUDE.md +733 -0
- moai_adk/templates/__init__.py +2 -0
- moai_adk/utils/__init__.py +8 -0
- moai_adk/utils/banner.py +42 -0
- moai_adk/utils/logger.py +152 -0
- moai_adk-0.3.0.dist-info/METADATA +20 -0
- moai_adk-0.3.0.dist-info/RECORD +87 -0
- moai_adk-0.3.0.dist-info/WHEEL +4 -0
- moai_adk-0.3.0.dist-info/entry_points.txt +2 -0
- moai_adk-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# @CODE:TRUST-001 | SPEC: SPEC-TRUST-001.md | TEST: tests/unit/core/quality/test_trust_checker.py
|
|
2
|
+
"""
|
|
3
|
+
TRUST 원칙 통합 검증 시스템
|
|
4
|
+
|
|
5
|
+
TRUST 5원칙:
|
|
6
|
+
- T: Test First (테스트 커버리지 ≥85%)
|
|
7
|
+
- R: Readable (파일 ≤300 LOC, 함수 ≤50 LOC, 매개변수 ≤5개)
|
|
8
|
+
- U: Unified (타입 안전성)
|
|
9
|
+
- S: Secured (보안 취약점 스캔)
|
|
10
|
+
- T: Trackable (TAG 체인 무결성)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from moai_adk.core.quality.validators.base_validator import ValidationResult
|
|
19
|
+
|
|
20
|
+
# ========================================
|
|
21
|
+
# 상수 정의 (의도를 드러내는 이름)
|
|
22
|
+
# ========================================
|
|
23
|
+
MIN_TEST_COVERAGE_PERCENT = 85
|
|
24
|
+
MAX_FILE_LINES_OF_CODE = 300
|
|
25
|
+
MAX_FUNCTION_LINES_OF_CODE = 50
|
|
26
|
+
MAX_FUNCTION_PARAMETERS = 5
|
|
27
|
+
MAX_CYCLOMATIC_COMPLEXITY = 10
|
|
28
|
+
|
|
29
|
+
# 파일 인코딩
|
|
30
|
+
DEFAULT_FILE_ENCODING = "utf-8"
|
|
31
|
+
|
|
32
|
+
# TAG 접두사
|
|
33
|
+
TAG_PREFIX_SPEC = "@SPEC:"
|
|
34
|
+
TAG_PREFIX_CODE = "@CODE:"
|
|
35
|
+
TAG_PREFIX_TEST = "@TEST:"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TrustChecker:
|
|
39
|
+
"""TRUST 원칙 통합 검증기"""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
"""TrustChecker 초기화"""
|
|
43
|
+
self.results: dict[str, ValidationResult] = {}
|
|
44
|
+
|
|
45
|
+
# ========================================
|
|
46
|
+
# T: Test First - Coverage Validation
|
|
47
|
+
# ========================================
|
|
48
|
+
|
|
49
|
+
def validate_coverage(self, project_path: Path, coverage_data: dict[str, Any]) -> ValidationResult:
|
|
50
|
+
"""
|
|
51
|
+
테스트 커버리지 검증 (≥85%)
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
project_path: 프로젝트 경로
|
|
55
|
+
coverage_data: 커버리지 데이터 (total_coverage, low_coverage_files)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ValidationResult: 검증 결과
|
|
59
|
+
"""
|
|
60
|
+
total_coverage = coverage_data.get("total_coverage", 0)
|
|
61
|
+
|
|
62
|
+
if total_coverage >= MIN_TEST_COVERAGE_PERCENT:
|
|
63
|
+
return ValidationResult(
|
|
64
|
+
passed=True, message=f"Test coverage: {total_coverage}% (Target: {MIN_TEST_COVERAGE_PERCENT}%)"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 실패 시 상세 정보 생성
|
|
68
|
+
low_files = coverage_data.get("low_coverage_files", [])
|
|
69
|
+
details = f"Current coverage: {total_coverage}% (Target: {MIN_TEST_COVERAGE_PERCENT}%)\n"
|
|
70
|
+
details += "Low coverage files:\n"
|
|
71
|
+
for file_info in low_files:
|
|
72
|
+
details += f" - {file_info['file']}: {file_info['coverage']}%\n"
|
|
73
|
+
details += "\nRecommended: Add more test cases to increase coverage."
|
|
74
|
+
|
|
75
|
+
return ValidationResult(
|
|
76
|
+
passed=False,
|
|
77
|
+
message=f"Test coverage: {total_coverage}% (Target: {MIN_TEST_COVERAGE_PERCENT}%)",
|
|
78
|
+
details=details,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# ========================================
|
|
82
|
+
# R: Readable - Code Constraints
|
|
83
|
+
# ========================================
|
|
84
|
+
|
|
85
|
+
def validate_file_size(self, src_path: Path) -> ValidationResult:
|
|
86
|
+
"""
|
|
87
|
+
파일 크기 검증 (≤300 LOC)
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
src_path: 소스 코드 디렉토리 경로
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
ValidationResult: 검증 결과
|
|
94
|
+
"""
|
|
95
|
+
# 입력 검증 (보안 강화)
|
|
96
|
+
if not src_path.exists():
|
|
97
|
+
return ValidationResult(passed=False, message=f"Source path does not exist: {src_path}", details="")
|
|
98
|
+
|
|
99
|
+
if not src_path.is_dir():
|
|
100
|
+
return ValidationResult(passed=False, message=f"Source path is not a directory: {src_path}", details="")
|
|
101
|
+
|
|
102
|
+
violations = []
|
|
103
|
+
|
|
104
|
+
for py_file in src_path.rglob("*.py"):
|
|
105
|
+
# 가드절 적용 (가독성 향상)
|
|
106
|
+
if py_file.name.startswith("test_"):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
lines = py_file.read_text(encoding="utf-8").splitlines()
|
|
111
|
+
loc = len(lines)
|
|
112
|
+
|
|
113
|
+
if loc > MAX_FILE_LINES_OF_CODE:
|
|
114
|
+
violations.append(f"{py_file.name}: {loc} LOC (Limit: {MAX_FILE_LINES_OF_CODE})")
|
|
115
|
+
except (UnicodeDecodeError, PermissionError):
|
|
116
|
+
# 보안: 파일 접근 오류 처리
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if not violations:
|
|
120
|
+
return ValidationResult(passed=True, message="All files within 300 LOC")
|
|
121
|
+
|
|
122
|
+
details = "Files exceeding 300 LOC:\n" + "\n".join(f" - {v}" for v in violations)
|
|
123
|
+
details += "\n\nRecommended: Refactor large files into smaller modules."
|
|
124
|
+
|
|
125
|
+
return ValidationResult(passed=False, message=f"{len(violations)} files exceed 300 LOC", details=details)
|
|
126
|
+
|
|
127
|
+
def validate_function_size(self, src_path: Path) -> ValidationResult:
|
|
128
|
+
"""
|
|
129
|
+
함수 크기 검증 (≤50 LOC)
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
src_path: 소스 코드 디렉토리 경로
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
ValidationResult: 검증 결과
|
|
136
|
+
"""
|
|
137
|
+
violations = []
|
|
138
|
+
|
|
139
|
+
for py_file in src_path.rglob("*.py"):
|
|
140
|
+
if py_file.name.startswith("test_"):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
content = py_file.read_text()
|
|
145
|
+
tree = ast.parse(content)
|
|
146
|
+
lines = content.splitlines()
|
|
147
|
+
|
|
148
|
+
for node in ast.walk(tree):
|
|
149
|
+
if isinstance(node, ast.FunctionDef):
|
|
150
|
+
# AST 라인 번호는 1-based
|
|
151
|
+
start_line = node.lineno
|
|
152
|
+
end_line = node.end_lineno if node.end_lineno else start_line # type: ignore
|
|
153
|
+
|
|
154
|
+
# 실제 함수 라인 수 계산 (데코레이터 제외)
|
|
155
|
+
func_lines = lines[start_line - 1:end_line]
|
|
156
|
+
func_loc = len(func_lines)
|
|
157
|
+
|
|
158
|
+
if func_loc > MAX_FUNCTION_LINES_OF_CODE:
|
|
159
|
+
violations.append(
|
|
160
|
+
f"{py_file.name}::{node.name}(): {func_loc} LOC (Limit: {MAX_FUNCTION_LINES_OF_CODE})"
|
|
161
|
+
)
|
|
162
|
+
except SyntaxError:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if not violations:
|
|
166
|
+
return ValidationResult(passed=True, message="All functions within 50 LOC")
|
|
167
|
+
|
|
168
|
+
details = "Functions exceeding 50 LOC:\n" + "\n".join(f" - {v}" for v in violations)
|
|
169
|
+
details += "\n\nRecommended: Extract complex functions into smaller ones."
|
|
170
|
+
|
|
171
|
+
return ValidationResult(passed=False, message=f"{len(violations)} functions exceed 50 LOC", details=details)
|
|
172
|
+
|
|
173
|
+
def validate_param_count(self, src_path: Path) -> ValidationResult:
|
|
174
|
+
"""
|
|
175
|
+
매개변수 개수 검증 (≤5개)
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
src_path: 소스 코드 디렉토리 경로
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
ValidationResult: 검증 결과
|
|
182
|
+
"""
|
|
183
|
+
violations = []
|
|
184
|
+
|
|
185
|
+
for py_file in src_path.rglob("*.py"):
|
|
186
|
+
if py_file.name.startswith("test_"):
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
tree = ast.parse(py_file.read_text())
|
|
191
|
+
for node in ast.walk(tree):
|
|
192
|
+
if isinstance(node, ast.FunctionDef):
|
|
193
|
+
param_count = len(node.args.args)
|
|
194
|
+
if param_count > MAX_FUNCTION_PARAMETERS:
|
|
195
|
+
violations.append(
|
|
196
|
+
f"{py_file.name}::{node.name}(): {param_count} parameters "
|
|
197
|
+
f"(Limit: {MAX_FUNCTION_PARAMETERS})"
|
|
198
|
+
)
|
|
199
|
+
except SyntaxError:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
if not violations:
|
|
203
|
+
return ValidationResult(passed=True, message="All functions within 5 parameters")
|
|
204
|
+
|
|
205
|
+
details = "Functions exceeding 5 parameters:\n" + "\n".join(f" - {v}" for v in violations)
|
|
206
|
+
details += "\n\nRecommended: Use data classes or parameter objects."
|
|
207
|
+
|
|
208
|
+
return ValidationResult(
|
|
209
|
+
passed=False, message=f"{len(violations)} functions exceed 5 parameters", details=details
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def validate_complexity(self, src_path: Path) -> ValidationResult:
|
|
213
|
+
"""
|
|
214
|
+
순환 복잡도 검증 (≤10)
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
src_path: 소스 코드 디렉토리 경로
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
ValidationResult: 검증 결과
|
|
221
|
+
"""
|
|
222
|
+
violations = []
|
|
223
|
+
|
|
224
|
+
for py_file in src_path.rglob("*.py"):
|
|
225
|
+
if py_file.name.startswith("test_"):
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
tree = ast.parse(py_file.read_text())
|
|
230
|
+
for node in ast.walk(tree):
|
|
231
|
+
if isinstance(node, ast.FunctionDef):
|
|
232
|
+
complexity = self._calculate_complexity(node)
|
|
233
|
+
if complexity > MAX_CYCLOMATIC_COMPLEXITY:
|
|
234
|
+
violations.append(
|
|
235
|
+
f"{py_file.name}::{node.name}(): complexity {complexity} "
|
|
236
|
+
f"(Limit: {MAX_CYCLOMATIC_COMPLEXITY})"
|
|
237
|
+
)
|
|
238
|
+
except SyntaxError:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if not violations:
|
|
242
|
+
return ValidationResult(passed=True, message="All functions within complexity 10")
|
|
243
|
+
|
|
244
|
+
details = "Functions exceeding complexity 10:\n" + "\n".join(f" - {v}" for v in violations)
|
|
245
|
+
details += "\n\nRecommended: Simplify complex logic using guard clauses."
|
|
246
|
+
|
|
247
|
+
return ValidationResult(
|
|
248
|
+
passed=False, message=f"{len(violations)} functions exceed complexity 10", details=details
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _calculate_complexity(self, node: ast.FunctionDef) -> int:
|
|
252
|
+
"""
|
|
253
|
+
순환 복잡도 계산 (McCabe complexity)
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
node: 함수 AST 노드
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
int: 순환 복잡도
|
|
260
|
+
"""
|
|
261
|
+
complexity = 1
|
|
262
|
+
for child in ast.walk(node):
|
|
263
|
+
# 분기문마다 +1
|
|
264
|
+
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler, ast.With)):
|
|
265
|
+
complexity += 1
|
|
266
|
+
# and/or 연산자마다 +1
|
|
267
|
+
elif isinstance(child, ast.BoolOp):
|
|
268
|
+
complexity += len(child.values) - 1
|
|
269
|
+
# elif는 이미 ast.If로 카운트되므로 별도 처리 불필요
|
|
270
|
+
return complexity
|
|
271
|
+
|
|
272
|
+
# ========================================
|
|
273
|
+
# T: Trackable - TAG Chain Validation
|
|
274
|
+
# ========================================
|
|
275
|
+
|
|
276
|
+
def validate_tag_chain(self, project_path: Path) -> ValidationResult:
|
|
277
|
+
"""
|
|
278
|
+
TAG 체인 완전성 검증
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
project_path: 프로젝트 경로
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
ValidationResult: 검증 결과
|
|
285
|
+
"""
|
|
286
|
+
specs_dir = project_path / ".moai" / "specs"
|
|
287
|
+
src_dir = project_path / "src"
|
|
288
|
+
|
|
289
|
+
# TAG 스캔
|
|
290
|
+
spec_tags = self._scan_tags(specs_dir, "@SPEC:")
|
|
291
|
+
code_tags = self._scan_tags(src_dir, "@CODE:")
|
|
292
|
+
|
|
293
|
+
# 체인 검증
|
|
294
|
+
broken_chains = []
|
|
295
|
+
for code_tag in code_tags:
|
|
296
|
+
tag_id = code_tag.split(":")[-1]
|
|
297
|
+
if not any(tag_id in spec_tag for spec_tag in spec_tags):
|
|
298
|
+
broken_chains.append(f"@CODE:{tag_id} (no @SPEC:{tag_id})")
|
|
299
|
+
|
|
300
|
+
if not broken_chains:
|
|
301
|
+
return ValidationResult(passed=True, message="TAG chain complete")
|
|
302
|
+
|
|
303
|
+
details = "broken tag chains:\n" + "\n".join(f" - {chain.lower()}" for chain in broken_chains)
|
|
304
|
+
details += "\n\nrecommended: add missing spec documents or fix tag references."
|
|
305
|
+
|
|
306
|
+
return ValidationResult(passed=False, message=f"{len(broken_chains)} broken TAG chains", details=details)
|
|
307
|
+
|
|
308
|
+
def detect_orphan_tags(self, project_path: Path) -> list[str]:
|
|
309
|
+
"""
|
|
310
|
+
고아 TAG 탐지
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
project_path: 프로젝트 경로
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
list[str]: 고아 TAG 목록
|
|
317
|
+
"""
|
|
318
|
+
specs_dir = project_path / ".moai" / "specs"
|
|
319
|
+
src_dir = project_path / "src"
|
|
320
|
+
|
|
321
|
+
spec_tags = self._scan_tags(specs_dir, "@SPEC:")
|
|
322
|
+
code_tags = self._scan_tags(src_dir, "@CODE:")
|
|
323
|
+
|
|
324
|
+
orphans = []
|
|
325
|
+
for code_tag in code_tags:
|
|
326
|
+
tag_id = code_tag.split(":")[-1]
|
|
327
|
+
if not any(tag_id in spec_tag for spec_tag in spec_tags):
|
|
328
|
+
orphans.append(code_tag)
|
|
329
|
+
|
|
330
|
+
return orphans
|
|
331
|
+
|
|
332
|
+
def _scan_tags(self, directory: Path, tag_prefix: str) -> list[str]:
|
|
333
|
+
"""
|
|
334
|
+
디렉토리에서 TAG 스캔
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
directory: 스캔할 디렉토리
|
|
338
|
+
tag_prefix: TAG 접두사 (예: "@SPEC:", "@CODE:")
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
list[str]: 발견된 TAG 목록
|
|
342
|
+
"""
|
|
343
|
+
if not directory.exists():
|
|
344
|
+
return []
|
|
345
|
+
|
|
346
|
+
tags = []
|
|
347
|
+
for file in directory.rglob("*"):
|
|
348
|
+
if file.is_file():
|
|
349
|
+
try:
|
|
350
|
+
content = file.read_text()
|
|
351
|
+
for line in content.splitlines():
|
|
352
|
+
if tag_prefix in line:
|
|
353
|
+
tags.append(line.strip())
|
|
354
|
+
except (UnicodeDecodeError, PermissionError):
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
return tags
|
|
358
|
+
|
|
359
|
+
# ========================================
|
|
360
|
+
# Report Generation
|
|
361
|
+
# ========================================
|
|
362
|
+
|
|
363
|
+
def generate_report(self, results: dict[str, Any], format: str = "markdown") -> str:
|
|
364
|
+
"""
|
|
365
|
+
검증 결과 보고서 생성
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
results: 검증 결과 딕셔너리
|
|
369
|
+
format: 보고서 형식 ("markdown" 또는 "json")
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
str: 보고서 문자열
|
|
373
|
+
"""
|
|
374
|
+
if format == "json":
|
|
375
|
+
return json.dumps(results, indent=2)
|
|
376
|
+
|
|
377
|
+
# Markdown 형식
|
|
378
|
+
report = "# TRUST Validation Report\n\n"
|
|
379
|
+
|
|
380
|
+
for category, result in results.items():
|
|
381
|
+
status = "✅ PASS" if result.get("passed", False) else "❌ FAIL"
|
|
382
|
+
value = result.get('value', 'N/A')
|
|
383
|
+
# 숫자인 경우 % 기호 추가
|
|
384
|
+
if isinstance(value, (int, float)):
|
|
385
|
+
value_str = f"{value}%"
|
|
386
|
+
else:
|
|
387
|
+
value_str = str(value)
|
|
388
|
+
|
|
389
|
+
report += f"## {category.upper()}\n"
|
|
390
|
+
report += f"**Status**: {status}\n"
|
|
391
|
+
report += f"**Value**: {value_str}\n\n"
|
|
392
|
+
|
|
393
|
+
return report
|
|
394
|
+
|
|
395
|
+
# ========================================
|
|
396
|
+
# Tool Selection
|
|
397
|
+
# ========================================
|
|
398
|
+
|
|
399
|
+
def select_tools(self, project_path: Path) -> dict[str, str]:
|
|
400
|
+
"""
|
|
401
|
+
언어별 도구 자동 선택
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
project_path: 프로젝트 경로
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
dict[str, str]: 선택된 도구 딕셔너리
|
|
408
|
+
"""
|
|
409
|
+
config_path = project_path / ".moai" / "config.json"
|
|
410
|
+
if not config_path.exists():
|
|
411
|
+
return {
|
|
412
|
+
"test_framework": "pytest",
|
|
413
|
+
"coverage_tool": "coverage.py",
|
|
414
|
+
"linter": "ruff",
|
|
415
|
+
"type_checker": "mypy",
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
config = json.loads(config_path.read_text())
|
|
419
|
+
language = config.get("project", {}).get("language", "python")
|
|
420
|
+
|
|
421
|
+
if language == "python":
|
|
422
|
+
return {
|
|
423
|
+
"test_framework": "pytest",
|
|
424
|
+
"coverage_tool": "coverage.py",
|
|
425
|
+
"linter": "ruff",
|
|
426
|
+
"type_checker": "mypy",
|
|
427
|
+
}
|
|
428
|
+
elif language == "typescript":
|
|
429
|
+
return {
|
|
430
|
+
"test_framework": "vitest",
|
|
431
|
+
"linter": "biome",
|
|
432
|
+
"type_checker": "tsc",
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# 기본값 (Python)
|
|
436
|
+
return {
|
|
437
|
+
"test_framework": "pytest",
|
|
438
|
+
"coverage_tool": "coverage.py",
|
|
439
|
+
"linter": "ruff",
|
|
440
|
+
"type_checker": "mypy",
|
|
441
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @CODE:TRUST-001:VALIDATOR | SPEC: SPEC-TRUST-001.md
|
|
2
|
+
"""Base validator class and validation result"""
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ValidationResult:
|
|
10
|
+
"""검증 결과 데이터 클래스"""
|
|
11
|
+
|
|
12
|
+
passed: bool
|
|
13
|
+
message: str
|
|
14
|
+
details: str = ""
|
|
15
|
+
metadata: dict[str, Any] | None = None
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
if self.metadata is None:
|
|
19
|
+
self.metadata = {}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @CODE:TEMPLATE-001 | SPEC: SPEC-INIT-003.md | Chain: TEMPLATE-001
|
|
2
|
+
"""Template management module."""
|
|
3
|
+
|
|
4
|
+
from moai_adk.core.template.backup import TemplateBackup
|
|
5
|
+
from moai_adk.core.template.merger import TemplateMerger
|
|
6
|
+
from moai_adk.core.template.processor import TemplateProcessor
|
|
7
|
+
|
|
8
|
+
__all__ = ["TemplateProcessor", "TemplateBackup", "TemplateMerger"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @CODE:TEMPLATE-001 | SPEC: SPEC-INIT-003.md | Chain: TEMPLATE-001
|
|
2
|
+
"""Template backup manager (SPEC-INIT-003 v0.3.0).
|
|
3
|
+
|
|
4
|
+
Creates and manages backups to protect user data during template updates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TemplateBackup:
|
|
15
|
+
"""Create and manage template backups."""
|
|
16
|
+
|
|
17
|
+
# Paths excluded from backups (protect user data)
|
|
18
|
+
BACKUP_EXCLUDE_DIRS = [
|
|
19
|
+
"specs", # User SPEC documents
|
|
20
|
+
"reports", # User reports
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def __init__(self, target_path: Path) -> None:
|
|
24
|
+
"""Initialize the backup manager.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
target_path: Project path (absolute).
|
|
28
|
+
"""
|
|
29
|
+
self.target_path = target_path.resolve()
|
|
30
|
+
|
|
31
|
+
def has_existing_files(self) -> bool:
|
|
32
|
+
"""Check whether backup-worthy files already exist.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True when any tracked file exists.
|
|
36
|
+
"""
|
|
37
|
+
return any(
|
|
38
|
+
(self.target_path / item).exists()
|
|
39
|
+
for item in [".moai", ".claude", "CLAUDE.md"]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def create_backup(self) -> Path:
|
|
43
|
+
"""Create a timestamped backup.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Backup path (for example, .moai-backups/20250110-143025/).
|
|
47
|
+
"""
|
|
48
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
49
|
+
backup_path = self.target_path / ".moai-backups" / timestamp
|
|
50
|
+
backup_path.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
# Copy backup targets
|
|
53
|
+
for item in [".moai", ".claude", "CLAUDE.md"]:
|
|
54
|
+
src = self.target_path / item
|
|
55
|
+
if not src.exists():
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
dst = backup_path / item
|
|
59
|
+
|
|
60
|
+
if item == ".moai":
|
|
61
|
+
# Copy while skipping protected paths
|
|
62
|
+
self._copy_exclude_protected(src, dst)
|
|
63
|
+
elif src.is_dir():
|
|
64
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
65
|
+
else:
|
|
66
|
+
shutil.copy2(src, dst)
|
|
67
|
+
|
|
68
|
+
return backup_path
|
|
69
|
+
|
|
70
|
+
def _copy_exclude_protected(self, src: Path, dst: Path) -> None:
|
|
71
|
+
"""Copy backup content while excluding protected paths.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
src: Source directory.
|
|
75
|
+
dst: Destination directory.
|
|
76
|
+
"""
|
|
77
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
for item in src.rglob("*"):
|
|
80
|
+
rel_path = item.relative_to(src)
|
|
81
|
+
rel_path_str = str(rel_path)
|
|
82
|
+
|
|
83
|
+
# Skip excluded paths
|
|
84
|
+
if any(
|
|
85
|
+
rel_path_str.startswith(exclude_dir)
|
|
86
|
+
for exclude_dir in self.BACKUP_EXCLUDE_DIRS
|
|
87
|
+
):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
dst_item = dst / rel_path
|
|
91
|
+
if item.is_file():
|
|
92
|
+
dst_item.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
shutil.copy2(item, dst_item)
|
|
94
|
+
elif item.is_dir():
|
|
95
|
+
dst_item.mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @CODE:PY314-001 | SPEC: SPEC-PY314-001.md | TEST: tests/unit/test_config_manager.py
|
|
2
|
+
"""Configuration Manager
|
|
3
|
+
|
|
4
|
+
Manage .moai/config.json:
|
|
5
|
+
- Read and write configuration files
|
|
6
|
+
- Support deep merges
|
|
7
|
+
- Preserve UTF-8 content
|
|
8
|
+
- Create directories automatically
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigManager:
|
|
17
|
+
"""Read and write .moai/config.json."""
|
|
18
|
+
|
|
19
|
+
DEFAULT_CONFIG = {
|
|
20
|
+
"mode": "personal",
|
|
21
|
+
"locale": "ko",
|
|
22
|
+
"moai": {
|
|
23
|
+
"version": "0.3.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def __init__(self, config_path: Path) -> None:
|
|
28
|
+
"""Initialize the ConfigManager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config_path: Path to config.json.
|
|
32
|
+
"""
|
|
33
|
+
self.config_path = config_path
|
|
34
|
+
|
|
35
|
+
def load(self) -> dict[str, Any]:
|
|
36
|
+
"""Load the configuration file.
|
|
37
|
+
|
|
38
|
+
Returns default values when the file is missing.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Configuration dictionary.
|
|
42
|
+
"""
|
|
43
|
+
if not self.config_path.exists():
|
|
44
|
+
return self.DEFAULT_CONFIG.copy()
|
|
45
|
+
|
|
46
|
+
with open(self.config_path, encoding="utf-8") as f:
|
|
47
|
+
data: dict[str, Any] = json.load(f)
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
def save(self, config: dict[str, Any]) -> None:
|
|
51
|
+
"""Persist the configuration file.
|
|
52
|
+
|
|
53
|
+
Creates directories when missing and preserves UTF-8 content.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
config: Configuration dictionary to save.
|
|
57
|
+
"""
|
|
58
|
+
# Ensure the directory exists
|
|
59
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# Write while preserving UTF-8 characters
|
|
62
|
+
with open(self.config_path, "w", encoding="utf-8") as f:
|
|
63
|
+
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
64
|
+
|
|
65
|
+
def update(self, updates: dict[str, Any]) -> None:
|
|
66
|
+
"""Update the configuration using a deep merge.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
updates: Dictionary of updates to apply.
|
|
70
|
+
"""
|
|
71
|
+
current = self.load()
|
|
72
|
+
merged = self._deep_merge(current, updates)
|
|
73
|
+
self.save(merged)
|
|
74
|
+
|
|
75
|
+
def _deep_merge(self, base: dict[str, Any], updates: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
"""Recursively deep-merge dictionaries.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
base: Base dictionary.
|
|
80
|
+
updates: Dictionary with updates.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Merged dictionary.
|
|
84
|
+
"""
|
|
85
|
+
result = base.copy()
|
|
86
|
+
|
|
87
|
+
for key, value in updates.items():
|
|
88
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
89
|
+
# When both sides are dicts, merge recursively
|
|
90
|
+
result[key] = self._deep_merge(result[key], value)
|
|
91
|
+
else:
|
|
92
|
+
# Otherwise, overwrite the value
|
|
93
|
+
result[key] = value
|
|
94
|
+
|
|
95
|
+
return result
|