dash-devtools 1.0.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.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件產生工具
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
__all__ = ['generate_claude_md', 'get_release_status', 'publish_release']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_claude_md(project_path):
|
|
12
|
+
"""產生 CLAUDE.md"""
|
|
13
|
+
project = Path(project_path)
|
|
14
|
+
claude_dir = project / '.claude'
|
|
15
|
+
claude_md = claude_dir / 'CLAUDE.md'
|
|
16
|
+
|
|
17
|
+
if not project.exists():
|
|
18
|
+
return {'success': False, 'error': '專案不存在'}
|
|
19
|
+
|
|
20
|
+
# 基本模板
|
|
21
|
+
template = f"""# {project.name}
|
|
22
|
+
|
|
23
|
+
## 專案概述
|
|
24
|
+
|
|
25
|
+
[待補充]
|
|
26
|
+
|
|
27
|
+
## 技術堆疊
|
|
28
|
+
|
|
29
|
+
| 類別 | 技術 |
|
|
30
|
+
|------|------|
|
|
31
|
+
| 前端 | [待補充] |
|
|
32
|
+
| 後端 | [待補充] |
|
|
33
|
+
| 資料庫 | [待補充] |
|
|
34
|
+
|
|
35
|
+
## 開發規範
|
|
36
|
+
|
|
37
|
+
遵循 DashAI 開發規範,詳見全域 CLAUDE.md
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
claude_dir.mkdir(exist_ok=True)
|
|
42
|
+
claude_md.write_text(template, encoding='utf-8')
|
|
43
|
+
return {'success': True}
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return {'success': False, 'error': str(e)}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_release_status():
|
|
49
|
+
"""取得版本狀態"""
|
|
50
|
+
projects_dir = Path('/Users/dash/Documents/github')
|
|
51
|
+
status = {}
|
|
52
|
+
|
|
53
|
+
for project in projects_dir.iterdir():
|
|
54
|
+
if not project.is_dir():
|
|
55
|
+
continue
|
|
56
|
+
pkg_path = project / 'package.json'
|
|
57
|
+
if pkg_path.exists():
|
|
58
|
+
try:
|
|
59
|
+
pkg = json.loads(pkg_path.read_text())
|
|
60
|
+
status[project.name] = {
|
|
61
|
+
'version': pkg.get('version', 'N/A'),
|
|
62
|
+
'last_update': 'N/A'
|
|
63
|
+
}
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return status
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def publish_release(project_path, version):
|
|
71
|
+
"""發布版本"""
|
|
72
|
+
project = Path(project_path)
|
|
73
|
+
pkg_path = project / 'package.json'
|
|
74
|
+
|
|
75
|
+
if not pkg_path.exists():
|
|
76
|
+
return {'success': False, 'error': 'package.json 不存在'}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
pkg = json.loads(pkg_path.read_text())
|
|
80
|
+
pkg['version'] = version
|
|
81
|
+
pkg_path.write_text(json.dumps(pkg, indent=2, ensure_ascii=False))
|
|
82
|
+
return {'success': True}
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return {'success': False, 'error': str(e)}
|
dash_devtools/health.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""
|
|
2
|
+
專案健康評分系統
|
|
3
|
+
|
|
4
|
+
類似 Lighthouse 的評分機制,量化專案品質:
|
|
5
|
+
- 安全性 (Security): 機敏資料、依賴漏洞
|
|
6
|
+
- 品質 (Quality): 程式碼規範、檔案結構
|
|
7
|
+
- 維護性 (Maintainability): 技術債務、文件完整度
|
|
8
|
+
- 效能 (Performance): Bundle 大小、載入時間
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Dict, List
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.progress import Progress, BarColumn, TextColumn
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class HealthScore:
|
|
26
|
+
"""健康評分結果"""
|
|
27
|
+
category: str
|
|
28
|
+
score: int # 0-100
|
|
29
|
+
max_score: int = 100
|
|
30
|
+
issues: List[str] = field(default_factory=list)
|
|
31
|
+
recommendations: List[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def percentage(self) -> float:
|
|
35
|
+
return (self.score / self.max_score) * 100
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def grade(self) -> str:
|
|
39
|
+
"""轉換為等級"""
|
|
40
|
+
if self.score >= 90:
|
|
41
|
+
return 'A'
|
|
42
|
+
elif self.score >= 80:
|
|
43
|
+
return 'B'
|
|
44
|
+
elif self.score >= 70:
|
|
45
|
+
return 'C'
|
|
46
|
+
elif self.score >= 60:
|
|
47
|
+
return 'D'
|
|
48
|
+
else:
|
|
49
|
+
return 'F'
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def color(self) -> str:
|
|
53
|
+
"""根據分數決定顏色"""
|
|
54
|
+
if self.score >= 90:
|
|
55
|
+
return 'green'
|
|
56
|
+
elif self.score >= 70:
|
|
57
|
+
return 'yellow'
|
|
58
|
+
elif self.score >= 50:
|
|
59
|
+
return 'orange1'
|
|
60
|
+
else:
|
|
61
|
+
return 'red'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class HealthChecker:
|
|
65
|
+
"""專案健康檢查器"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, project_path: str):
|
|
68
|
+
self.project_path = Path(project_path)
|
|
69
|
+
self.project_name = self.project_path.name
|
|
70
|
+
|
|
71
|
+
def check_all(self) -> Dict[str, HealthScore]:
|
|
72
|
+
"""執行完整健康檢查"""
|
|
73
|
+
scores = {
|
|
74
|
+
'security': self._check_security(),
|
|
75
|
+
'quality': self._check_quality(),
|
|
76
|
+
'maintainability': self._check_maintainability(),
|
|
77
|
+
'performance': self._check_performance(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# 如果有 openspec 目錄,加入規格健康評分
|
|
81
|
+
openspec_dir = self.project_path / 'openspec'
|
|
82
|
+
if openspec_dir.exists():
|
|
83
|
+
scores['spec'] = self._check_spec()
|
|
84
|
+
|
|
85
|
+
return scores
|
|
86
|
+
|
|
87
|
+
def _check_security(self) -> HealthScore:
|
|
88
|
+
"""安全性檢查"""
|
|
89
|
+
score = 100
|
|
90
|
+
issues = []
|
|
91
|
+
recommendations = []
|
|
92
|
+
|
|
93
|
+
# 檢查 .env 是否在 .gitignore
|
|
94
|
+
gitignore = self.project_path / '.gitignore'
|
|
95
|
+
if gitignore.exists():
|
|
96
|
+
content = gitignore.read_text()
|
|
97
|
+
if '.env' not in content:
|
|
98
|
+
score -= 20
|
|
99
|
+
issues.append('.env 未加入 .gitignore')
|
|
100
|
+
recommendations.append('將 .env 加入 .gitignore')
|
|
101
|
+
else:
|
|
102
|
+
score -= 10
|
|
103
|
+
issues.append('缺少 .gitignore 檔案')
|
|
104
|
+
|
|
105
|
+
# 檢查是否有 .env 檔案被追蹤
|
|
106
|
+
env_file = self.project_path / '.env'
|
|
107
|
+
if env_file.exists():
|
|
108
|
+
# 檢查是否在 git 中
|
|
109
|
+
git_dir = self.project_path / '.git'
|
|
110
|
+
if git_dir.exists():
|
|
111
|
+
import subprocess
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
['git', 'ls-files', '.env'],
|
|
114
|
+
cwd=self.project_path,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True
|
|
117
|
+
)
|
|
118
|
+
if result.stdout.strip():
|
|
119
|
+
score -= 30
|
|
120
|
+
issues.append('.env 檔案被 git 追蹤')
|
|
121
|
+
|
|
122
|
+
# 檢查硬編碼的機敏資料
|
|
123
|
+
sensitive_patterns = [
|
|
124
|
+
('password', -15, '發現硬編碼密碼'),
|
|
125
|
+
('api_key', -15, '發現硬編碼 API Key'),
|
|
126
|
+
('secret', -10, '發現硬編碼 Secret'),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
for ext in ['*.js', '*.ts', '*.py']:
|
|
130
|
+
for file_path in self.project_path.rglob(ext):
|
|
131
|
+
if 'node_modules' in str(file_path) or '.git' in str(file_path):
|
|
132
|
+
continue
|
|
133
|
+
try:
|
|
134
|
+
content = file_path.read_text().lower()
|
|
135
|
+
for pattern, penalty, message in sensitive_patterns:
|
|
136
|
+
if f'{pattern} = "' in content or f"{pattern} = '" in content:
|
|
137
|
+
score += penalty
|
|
138
|
+
if message not in issues:
|
|
139
|
+
issues.append(message)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
# 檢查依賴漏洞(簡化版)
|
|
144
|
+
package_lock = self.project_path / 'package-lock.json'
|
|
145
|
+
if package_lock.exists():
|
|
146
|
+
recommendations.append('建議定期執行 npm audit')
|
|
147
|
+
|
|
148
|
+
return HealthScore(
|
|
149
|
+
category='安全性',
|
|
150
|
+
score=max(0, score),
|
|
151
|
+
issues=issues,
|
|
152
|
+
recommendations=recommendations
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _check_quality(self) -> HealthScore:
|
|
156
|
+
"""程式碼品質檢查"""
|
|
157
|
+
score = 100
|
|
158
|
+
issues = []
|
|
159
|
+
recommendations = []
|
|
160
|
+
|
|
161
|
+
# 檢查 ESLint/Prettier 設定
|
|
162
|
+
has_linter = any([
|
|
163
|
+
(self.project_path / '.eslintrc.js').exists(),
|
|
164
|
+
(self.project_path / '.eslintrc.json').exists(),
|
|
165
|
+
(self.project_path / 'eslint.config.js').exists(),
|
|
166
|
+
])
|
|
167
|
+
if not has_linter:
|
|
168
|
+
score -= 10
|
|
169
|
+
recommendations.append('建議加入 ESLint 設定')
|
|
170
|
+
|
|
171
|
+
has_prettier = any([
|
|
172
|
+
(self.project_path / '.prettierrc').exists(),
|
|
173
|
+
(self.project_path / '.prettierrc.json').exists(),
|
|
174
|
+
])
|
|
175
|
+
if not has_prettier:
|
|
176
|
+
score -= 5
|
|
177
|
+
recommendations.append('建議加入 Prettier 設定')
|
|
178
|
+
|
|
179
|
+
# 檢查 TypeScript
|
|
180
|
+
package_json = self.project_path / 'package.json'
|
|
181
|
+
if package_json.exists():
|
|
182
|
+
try:
|
|
183
|
+
pkg = json.loads(package_json.read_text())
|
|
184
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
185
|
+
if 'typescript' not in deps:
|
|
186
|
+
score -= 5
|
|
187
|
+
recommendations.append('建議使用 TypeScript')
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
# 檢查檔案行數
|
|
192
|
+
large_files = []
|
|
193
|
+
for ext in ['*.js', '*.ts', '*.py']:
|
|
194
|
+
for file_path in self.project_path.rglob(ext):
|
|
195
|
+
if 'node_modules' in str(file_path) or '.git' in str(file_path):
|
|
196
|
+
continue
|
|
197
|
+
try:
|
|
198
|
+
lines = len(file_path.read_text().splitlines())
|
|
199
|
+
if lines > 500:
|
|
200
|
+
large_files.append(f'{file_path.name} ({lines} 行)')
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
if large_files:
|
|
205
|
+
score -= min(len(large_files) * 5, 20)
|
|
206
|
+
issues.append(f'{len(large_files)} 個檔案超過 500 行')
|
|
207
|
+
|
|
208
|
+
return HealthScore(
|
|
209
|
+
category='品質',
|
|
210
|
+
score=max(0, score),
|
|
211
|
+
issues=issues,
|
|
212
|
+
recommendations=recommendations
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _check_maintainability(self) -> HealthScore:
|
|
216
|
+
"""維護性檢查"""
|
|
217
|
+
score = 100
|
|
218
|
+
issues = []
|
|
219
|
+
recommendations = []
|
|
220
|
+
|
|
221
|
+
# 檢查 README
|
|
222
|
+
readme = self.project_path / 'README.md'
|
|
223
|
+
if not readme.exists():
|
|
224
|
+
score -= 15
|
|
225
|
+
issues.append('缺少 README.md')
|
|
226
|
+
else:
|
|
227
|
+
content = readme.read_text()
|
|
228
|
+
if len(content) < 200:
|
|
229
|
+
score -= 5
|
|
230
|
+
recommendations.append('README 內容過短,建議補充')
|
|
231
|
+
|
|
232
|
+
# 檢查 CLAUDE.md
|
|
233
|
+
claude_md = self.project_path / 'CLAUDE.md'
|
|
234
|
+
if not claude_md.exists():
|
|
235
|
+
score -= 10
|
|
236
|
+
recommendations.append('建議加入 CLAUDE.md (dash docs claude)')
|
|
237
|
+
|
|
238
|
+
# 檢查 package.json 完整度
|
|
239
|
+
package_json = self.project_path / 'package.json'
|
|
240
|
+
if package_json.exists():
|
|
241
|
+
try:
|
|
242
|
+
pkg = json.loads(package_json.read_text())
|
|
243
|
+
if not pkg.get('description'):
|
|
244
|
+
score -= 5
|
|
245
|
+
recommendations.append('package.json 缺少 description')
|
|
246
|
+
if not pkg.get('scripts'):
|
|
247
|
+
score -= 5
|
|
248
|
+
issues.append('package.json 缺少 scripts')
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
# 檢查測試設定
|
|
253
|
+
has_tests = any([
|
|
254
|
+
(self.project_path / 'tests').exists(),
|
|
255
|
+
(self.project_path / '__tests__').exists(),
|
|
256
|
+
(self.project_path / 'spec').exists(),
|
|
257
|
+
any(self.project_path.rglob('*.test.ts')),
|
|
258
|
+
any(self.project_path.rglob('*.spec.ts')),
|
|
259
|
+
])
|
|
260
|
+
if not has_tests:
|
|
261
|
+
score -= 15
|
|
262
|
+
recommendations.append('建議加入測試')
|
|
263
|
+
|
|
264
|
+
# 檢查 Git hooks
|
|
265
|
+
husky = self.project_path / '.husky'
|
|
266
|
+
if not husky.exists():
|
|
267
|
+
score -= 5
|
|
268
|
+
recommendations.append('建議使用 Git hooks (dash hooks install)')
|
|
269
|
+
|
|
270
|
+
return HealthScore(
|
|
271
|
+
category='維護性',
|
|
272
|
+
score=max(0, score),
|
|
273
|
+
issues=issues,
|
|
274
|
+
recommendations=recommendations
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _check_performance(self) -> HealthScore:
|
|
278
|
+
"""效能檢查"""
|
|
279
|
+
score = 100
|
|
280
|
+
issues = []
|
|
281
|
+
recommendations = []
|
|
282
|
+
|
|
283
|
+
# 檢查 node_modules 大小(簡化版)
|
|
284
|
+
node_modules = self.project_path / 'node_modules'
|
|
285
|
+
if node_modules.exists():
|
|
286
|
+
# 計算依賴數量
|
|
287
|
+
package_json = self.project_path / 'package.json'
|
|
288
|
+
if package_json.exists():
|
|
289
|
+
try:
|
|
290
|
+
pkg = json.loads(package_json.read_text())
|
|
291
|
+
deps_count = len(pkg.get('dependencies', {}))
|
|
292
|
+
if deps_count > 50:
|
|
293
|
+
score -= 10
|
|
294
|
+
issues.append(f'依賴數量過多 ({deps_count} 個)')
|
|
295
|
+
recommendations.append('審視並移除不必要的依賴')
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
# 檢查是否有未使用的依賴(簡化版)
|
|
300
|
+
package_json = self.project_path / 'package.json'
|
|
301
|
+
if package_json.exists():
|
|
302
|
+
recommendations.append('建議定期執行 depcheck 檢查未使用依賴')
|
|
303
|
+
|
|
304
|
+
# 檢查圖片優化
|
|
305
|
+
large_images = []
|
|
306
|
+
for ext in ['*.png', '*.jpg', '*.jpeg']:
|
|
307
|
+
for file_path in self.project_path.rglob(ext):
|
|
308
|
+
if 'node_modules' in str(file_path):
|
|
309
|
+
continue
|
|
310
|
+
try:
|
|
311
|
+
size_kb = file_path.stat().st_size / 1024
|
|
312
|
+
if size_kb > 500:
|
|
313
|
+
large_images.append(f'{file_path.name} ({size_kb:.0f}KB)')
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
if large_images:
|
|
318
|
+
score -= min(len(large_images) * 5, 15)
|
|
319
|
+
issues.append(f'{len(large_images)} 個圖片超過 500KB')
|
|
320
|
+
recommendations.append('建議壓縮大型圖片')
|
|
321
|
+
|
|
322
|
+
return HealthScore(
|
|
323
|
+
category='效能',
|
|
324
|
+
score=max(0, score),
|
|
325
|
+
issues=issues,
|
|
326
|
+
recommendations=recommendations
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _check_spec(self) -> HealthScore:
|
|
330
|
+
"""OpenSpec 規格健康檢查"""
|
|
331
|
+
import time
|
|
332
|
+
|
|
333
|
+
score = 100
|
|
334
|
+
issues = []
|
|
335
|
+
recommendations = []
|
|
336
|
+
|
|
337
|
+
openspec_dir = self.project_path / 'openspec'
|
|
338
|
+
specs_dir = openspec_dir / 'specs'
|
|
339
|
+
changes_dir = openspec_dir / 'changes'
|
|
340
|
+
|
|
341
|
+
# 檢查目錄結構
|
|
342
|
+
if not specs_dir.exists():
|
|
343
|
+
score -= 10
|
|
344
|
+
recommendations.append('建議建立 openspec/specs/ 目錄')
|
|
345
|
+
|
|
346
|
+
if not changes_dir.exists():
|
|
347
|
+
score -= 10
|
|
348
|
+
recommendations.append('建議建立 openspec/changes/ 目錄')
|
|
349
|
+
|
|
350
|
+
# 統計規格數量
|
|
351
|
+
specs_count = 0
|
|
352
|
+
if specs_dir.exists():
|
|
353
|
+
specs_count = len(list(specs_dir.glob('*.md')))
|
|
354
|
+
|
|
355
|
+
if specs_count == 0:
|
|
356
|
+
score -= 15
|
|
357
|
+
recommendations.append('尚未建立任何功能規格')
|
|
358
|
+
|
|
359
|
+
# 統計活動變更
|
|
360
|
+
changes_count = 0
|
|
361
|
+
stale_changes = []
|
|
362
|
+
if changes_dir.exists():
|
|
363
|
+
now = time.time()
|
|
364
|
+
seven_days = 7 * 24 * 60 * 60
|
|
365
|
+
|
|
366
|
+
for change_file in changes_dir.glob('*.md'):
|
|
367
|
+
changes_count += 1
|
|
368
|
+
mtime = change_file.stat().st_mtime
|
|
369
|
+
if now - mtime > seven_days:
|
|
370
|
+
stale_changes.append(change_file.stem)
|
|
371
|
+
|
|
372
|
+
# 過期變更扣分
|
|
373
|
+
if stale_changes:
|
|
374
|
+
penalty = min(len(stale_changes) * 10, 30)
|
|
375
|
+
score -= penalty
|
|
376
|
+
issues.append(f'{len(stale_changes)} 個變更超過 7 天未處理')
|
|
377
|
+
for change in stale_changes[:3]:
|
|
378
|
+
recommendations.append(f'處理過期變更: {change}')
|
|
379
|
+
|
|
380
|
+
# 檢查規格格式 (簡化版)
|
|
381
|
+
invalid_specs = 0
|
|
382
|
+
if specs_dir.exists():
|
|
383
|
+
for spec_file in specs_dir.glob('*.md'):
|
|
384
|
+
try:
|
|
385
|
+
content = spec_file.read_text(encoding='utf-8')
|
|
386
|
+
if not content.startswith('---'):
|
|
387
|
+
invalid_specs += 1
|
|
388
|
+
except Exception:
|
|
389
|
+
invalid_specs += 1
|
|
390
|
+
|
|
391
|
+
if invalid_specs > 0:
|
|
392
|
+
score -= invalid_specs * 5
|
|
393
|
+
issues.append(f'{invalid_specs} 個規格檔案格式不正確')
|
|
394
|
+
|
|
395
|
+
return HealthScore(
|
|
396
|
+
category='規格',
|
|
397
|
+
score=max(0, score),
|
|
398
|
+
issues=issues,
|
|
399
|
+
recommendations=recommendations
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def render_health_report(project_name: str, scores: Dict[str, HealthScore]):
|
|
404
|
+
"""渲染健康報告"""
|
|
405
|
+
|
|
406
|
+
# 計算總分
|
|
407
|
+
total_score = sum(s.score for s in scores.values()) // len(scores)
|
|
408
|
+
|
|
409
|
+
# 標題
|
|
410
|
+
grade_colors = {'A': 'green', 'B': 'cyan', 'C': 'yellow', 'D': 'orange1', 'F': 'red'}
|
|
411
|
+
grade = 'A' if total_score >= 90 else 'B' if total_score >= 80 else 'C' if total_score >= 70 else 'D' if total_score >= 60 else 'F'
|
|
412
|
+
|
|
413
|
+
title = Text()
|
|
414
|
+
title.append(f"\n {project_name} ", style="bold white")
|
|
415
|
+
title.append("健康報告\n", style="dim")
|
|
416
|
+
|
|
417
|
+
console.print(Panel(title, border_style="cyan"))
|
|
418
|
+
|
|
419
|
+
# 總分顯示
|
|
420
|
+
score_display = Text()
|
|
421
|
+
score_display.append(" 總分: ", style="dim")
|
|
422
|
+
score_display.append(f"{total_score}", style=f"bold {grade_colors[grade]}")
|
|
423
|
+
score_display.append(f"/100 ", style="dim")
|
|
424
|
+
score_display.append(f"[{grade}]", style=f"bold {grade_colors[grade]}")
|
|
425
|
+
console.print(score_display)
|
|
426
|
+
console.print()
|
|
427
|
+
|
|
428
|
+
# 各項評分進度條
|
|
429
|
+
for key, score in scores.items():
|
|
430
|
+
bar_width = 30
|
|
431
|
+
filled = int((score.score / 100) * bar_width)
|
|
432
|
+
|
|
433
|
+
bar = Text()
|
|
434
|
+
bar.append(f" {score.category: <6} ", style="white")
|
|
435
|
+
bar.append("█" * filled, style=score.color)
|
|
436
|
+
bar.append("░" * (bar_width - filled), style="dim")
|
|
437
|
+
bar.append(f" {score.score:3d}%", style=score.color)
|
|
438
|
+
|
|
439
|
+
console.print(bar)
|
|
440
|
+
|
|
441
|
+
console.print()
|
|
442
|
+
|
|
443
|
+
# 問題和建議
|
|
444
|
+
all_issues = []
|
|
445
|
+
all_recommendations = []
|
|
446
|
+
|
|
447
|
+
for score in scores.values():
|
|
448
|
+
all_issues.extend(score.issues)
|
|
449
|
+
all_recommendations.extend(score.recommendations)
|
|
450
|
+
|
|
451
|
+
if all_issues:
|
|
452
|
+
console.print(" [red]問題[/red]")
|
|
453
|
+
for issue in all_issues[:5]:
|
|
454
|
+
console.print(f" [red]•[/red] {issue}")
|
|
455
|
+
console.print()
|
|
456
|
+
|
|
457
|
+
if all_recommendations:
|
|
458
|
+
console.print(" [yellow]建議[/yellow]")
|
|
459
|
+
for rec in all_recommendations[:5]:
|
|
460
|
+
console.print(f" [yellow]•[/yellow] {rec}")
|
|
461
|
+
|
|
462
|
+
console.print()
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
'project': project_name,
|
|
466
|
+
'total_score': total_score,
|
|
467
|
+
'grade': grade,
|
|
468
|
+
'scores': {k: {'score': v.score, 'issues': v.issues} for k, v in scores.items()}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def run_health_check(project_path: str) -> dict:
|
|
473
|
+
"""執行健康檢查並輸出報告"""
|
|
474
|
+
checker = HealthChecker(project_path)
|
|
475
|
+
scores = checker.check_all()
|
|
476
|
+
return render_health_report(checker.project_name, scores)
|