Skip to content

Commit 84f140f

Browse files
authored
Merge pull request #66 from kc3hack/feature/issue-64-skill-tree-integration-test
Issue #64: スキルツリー生成の統合テスト
2 parents e740661 + 9b213c7 commit 84f140f

File tree

9 files changed

+927
-50
lines changed

9 files changed

+927
-50
lines changed

.github/decisions/010-skill-tree-test-strategy-after-ai-migration.md renamed to .github/decisions/011-skill-tree-test-strategy-after-ai-migration.md

File renamed without changes.
File renamed without changes.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,5 @@ $RECYCLE.BIN/
405405
*.lnk
406406

407407
# End of https://www.toptal.com/developers/gitignore/api/python,node,linux,macos,windows,visualstudiocode
408+
# Custom: Generated skill tree JSON files (contains user personal data)
409+
backend/scripts/skill_tree_*.json

backend/app/services/github_service.py

Lines changed: 216 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,27 @@ async def analyze_github_profile(username: str | None) -> dict[str, Any]:
5353

5454
try:
5555
async with httpx.AsyncClient(timeout=10.0) as client:
56+
headers = _get_github_headers()
57+
is_authenticated = "Authorization" in headers
58+
59+
# 認証されている場合、認証ユーザー情報を取得
60+
authenticated_username = None
61+
if is_authenticated:
62+
try:
63+
auth_user_response = await client.get(
64+
"https://api.github.com/user",
65+
headers=headers,
66+
)
67+
if auth_user_response.status_code == 200:
68+
authenticated_username = auth_user_response.json().get("login")
69+
logger.info(f"Authenticated as: {authenticated_username}")
70+
except Exception as e:
71+
logger.warning(f"Failed to get authenticated user info: {e}")
72+
5673
# ユーザー情報取得
5774
user_response = await client.get(
5875
f"https://api.github.com/users/{username}",
59-
headers=_get_github_headers(),
76+
headers=headers,
6077
)
6178

6279
if user_response.status_code == 404:
@@ -70,26 +87,57 @@ async def analyze_github_profile(username: str | None) -> dict[str, Any]:
7087
user_response.raise_for_status()
7188
user_data = user_response.json()
7289

73-
# リポジトリ一覧取得(最大100件)
74-
repos_response = await client.get(
75-
f"https://api.github.com/users/{username}/repos",
76-
params={"sort": "updated", "per_page": 100},
77-
headers=_get_github_headers(),
78-
)
90+
# リポジトリ一覧取得
91+
# 認証ユーザーと一致する場合はプライベートリポジトリも取得
92+
if (
93+
is_authenticated
94+
and authenticated_username
95+
and authenticated_username.lower() == username.lower()
96+
):
97+
logger.info(
98+
f"Fetching private repos for authenticated user: {username}"
99+
)
100+
repos_response = await client.get(
101+
"https://api.github.com/user/repos",
102+
params={
103+
"affiliation": "owner",
104+
"visibility": "all",
105+
"sort": "updated",
106+
"per_page": 100,
107+
},
108+
headers=headers,
109+
)
110+
else:
111+
logger.info(f"Fetching public repos only for user: {username}")
112+
repos_response = await client.get(
113+
f"https://api.github.com/users/{username}/repos",
114+
params={"sort": "updated", "per_page": 100},
115+
headers=headers,
116+
)
117+
79118
repos_response.raise_for_status()
80119
repos = repos_response.json()
81120

121+
logger.info(f"Fetched {len(repos)} repositories for {username}")
122+
82123
# 言語分析
83124
languages = _analyze_languages(repos)
125+
logger.info(f"Detected languages: {languages}")
84126

85127
# 技術スタック検出
86128
tech_stack = await _detect_tech_stack(client, username, repos)
129+
logger.info(f"Detected tech stack: {tech_stack}")
87130

88131
# 最近の活動(簡易版)
89132
recent_activity = _analyze_recent_activity(user_data, repos)
90133

91134
# スキル完了シグナル
92-
completion_signals = _generate_completion_signals(languages, tech_stack)
135+
completion_signals = _generate_completion_signals(
136+
languages, tech_stack, repos
137+
)
138+
logger.info(
139+
f"Generated {len(completion_signals)} completion signals: {list(completion_signals.keys())}"
140+
)
93141

94142
return {
95143
"languages": languages,
@@ -160,33 +208,61 @@ async def _detect_tech_stack(
160208
"""
161209
tech_stack = set()
162210

163-
# 主要リポジトリ(スター数順、最大10件)から検出
164-
sorted_repos = sorted(
165-
repos, key=lambda r: r.get("stargazers_count", 0), reverse=True
166-
)[:10]
211+
# 主要リポジトリ(更新日時順、最大20件)から検出
212+
sorted_repos = sorted(repos, key=lambda r: r.get("updated_at", ""), reverse=True)[
213+
:20
214+
]
167215

168216
for repo in sorted_repos:
169217
repo_name = repo.get("name", "")
170218
description = repo.get("description", "") or ""
219+
language = repo.get("language", "") or ""
171220

172-
# リポジトリ名・説明文から技術スタックを推定
221+
# リポジトリ名・説明文・言語から技術スタックを推定
173222
tech_keywords = {
174-
"FastAPI": ["fastapi"],
223+
# Web Backend
224+
"FastAPI": ["fastapi", "fast-api"],
175225
"Django": ["django"],
176226
"Flask": ["flask"],
227+
"Express": ["express", "expressjs"],
228+
"NestJS": ["nestjs"],
229+
# Web Frontend
177230
"React": ["react", "reactjs"],
178-
"Next.js": ["nextjs", "next.js"],
231+
"Next.js": ["nextjs", "next.js", "next-js"],
179232
"Vue": ["vue", "vuejs"],
233+
"Nuxt": ["nuxt", "nuxtjs"],
180234
"Angular": ["angular"],
181-
"TypeScript": ["typescript", "ts"],
182-
"Docker": ["docker", "dockerfile"],
235+
"Svelte": ["svelte"],
236+
# CSS/UI
237+
"Tailwind": ["tailwind"],
238+
"Bootstrap": ["bootstrap"],
239+
"Material-UI": ["material-ui", "mui"],
240+
# TypeScript
241+
"TypeScript": ["typescript"],
242+
# Infrastructure
243+
"Docker": ["docker", "dockerfile", "container"],
183244
"Kubernetes": ["kubernetes", "k8s"],
245+
"AWS": ["aws", "lambda", "ec2", "s3"],
246+
"GCP": ["gcp", "google cloud"],
247+
"Terraform": ["terraform"],
248+
# Database
249+
"PostgreSQL": ["postgres", "postgresql"],
250+
"MySQL": ["mysql"],
251+
"MongoDB": ["mongodb", "mongo"],
252+
"Redis": ["redis"],
253+
# AI/ML
184254
"TensorFlow": ["tensorflow"],
185255
"PyTorch": ["pytorch"],
186256
"Scikit-learn": ["scikit", "sklearn"],
257+
"Keras": ["keras"],
258+
"OpenAI": ["openai", "gpt"],
259+
# Testing
260+
"Jest": ["jest"],
261+
"Pytest": ["pytest"],
262+
"Vitest": ["vitest"],
187263
}
188264

189-
combined_text = f"{repo_name} {description}".lower()
265+
combined_text = f"{repo_name} {description} {language}".lower()
190266

191267
for tech, keywords in tech_keywords.items():
192268
if any(keyword in combined_text for keyword in keywords):
@@ -235,59 +311,155 @@ def _analyze_recent_activity(
235311

236312

237313
def _generate_completion_signals(
238-
languages: list[str], tech_stack: list[str]
314+
languages: list[str], tech_stack: list[str], repos: list[dict[str, Any]]
239315
) -> dict[str, bool]:
240316
"""
241317
習得済みスキルのシグナルを生成
242318
243319
Args:
244320
languages: 使用言語リスト
245321
tech_stack: 技術スタックリスト
322+
repos: リポジトリリスト
246323
247324
Returns:
248325
スキルID: 完了フラグのマッピング
249326
"""
250327
signals = {}
251328

252-
# 言語ベースのシグナル
329+
# 言語ベースのシグナル(大幅に拡張)
253330
language_mapping = {
254-
"HTML": ["web_html_css"],
255-
"CSS": ["web_html_css"],
256-
"JavaScript": ["web_js_basics"],
257-
"TypeScript": ["web_typescript"],
258-
"Python": ["ai_python_basics", "infrastructure_linux_basics"],
259-
"Java": ["infrastructure_container"],
260-
"Go": ["infrastructure_container"],
261-
"Rust": ["infrastructure_container"],
262-
"C": ["game_math"],
263-
"C++": ["game_math", "game_engine"],
264-
"C#": ["game_engine"],
331+
"HTML": ["web_html_css", "web_a11y_basics"],
332+
"CSS": ["web_html_css", "web_css_fw"],
333+
"JavaScript": ["web_js_basics", "web_js_advanced"],
334+
"TypeScript": ["web_typescript", "web_js_advanced"],
335+
"Python": ["ai_python_basics", "ai_data_processing", "infra_shell_scripting"],
336+
"Java": ["infra_virt_basics"],
337+
"Go": ["infra_docker", "infra_kubernetes"],
338+
"Rust": ["infra_linux_admin"],
339+
"C": ["game_math_basics", "game_physics_basics"],
340+
"C++": ["game_math_basics", "game_engine_basics"],
341+
"C#": ["game_engine_basics", "game_scripting"],
342+
"Ruby": ["web_backend_api"],
343+
"PHP": ["web_backend_api"],
344+
"Shell": ["infra_shell_scripting", "infra_linux_admin"],
345+
"Dockerfile": ["infra_docker"],
265346
}
266347

267348
for lang in languages:
268349
if lang in language_mapping:
269350
for skill_id in language_mapping[lang]:
270351
signals[skill_id] = True
271352

272-
# 技術スタックベースのシグナル
353+
# 技術スタックベースのシグナル(大幅に拡張)
273354
tech_mapping = {
274-
"React": ["web_react"],
275-
"Next.js": ["web_nextjs"],
276-
"Vue": ["web_vue"],
277-
"Angular": ["web_angular"],
278-
"FastAPI": ["web_api_design"],
279-
"Django": ["web_api_design"],
280-
"Flask": ["web_api_design"],
281-
"Docker": ["infrastructure_docker"],
282-
"Kubernetes": ["infrastructure_kubernetes"],
283-
"TensorFlow": ["ai_deep_learning"],
284-
"PyTorch": ["ai_deep_learning"],
285-
"Scikit-learn": ["ai_ml"],
355+
# Web Frontend
356+
"React": ["web_spa_fw", "web_js_advanced"],
357+
"Next.js": ["web_ssr_ssg", "web_spa_fw", "web_build_tools"],
358+
"Vue": ["web_spa_fw"],
359+
"Nuxt": ["web_ssr_ssg", "web_spa_fw"],
360+
"Angular": ["web_spa_fw"],
361+
"Svelte": ["web_spa_fw"],
362+
# Web Backend
363+
"FastAPI": ["web_backend_api", "web_http_basics"],
364+
"Django": ["web_backend_api", "web_db_orm"],
365+
"Flask": ["web_backend_api"],
366+
"Express": ["web_backend_api"],
367+
"NestJS": ["web_backend_api"],
368+
# CSS/UI
369+
"Tailwind": ["web_css_fw"],
370+
"Bootstrap": ["web_css_fw"],
371+
"Material-UI": ["web_css_fw"],
372+
# Infrastructure
373+
"Docker": ["infra_docker", "infra_virt_basics"],
374+
"Kubernetes": ["infra_kubernetes", "infra_docker"],
375+
"AWS": ["infra_cloud", "infra_cicd"],
376+
"GCP": ["infra_cloud"],
377+
"Terraform": ["infra_iac", "infra_cloud"],
378+
# Database
379+
"PostgreSQL": ["web_db_orm"],
380+
"MySQL": ["web_db_orm"],
381+
"MongoDB": ["web_db_orm"],
382+
"Redis": ["web_cache"],
383+
# AI/ML
384+
"TensorFlow": ["ai_deep_learning", "ai_ml_basics"],
385+
"PyTorch": ["ai_deep_learning", "ai_ml_basics"],
386+
"Scikit-learn": ["ai_ml_basics", "ai_statistics"],
387+
"Keras": ["ai_deep_learning"],
388+
"OpenAI": ["ai_llm", "ai_ml_basics"],
389+
# Testing
390+
"Jest": ["web_testing"],
391+
"Pytest": ["ai_python_basics"],
392+
"Vitest": ["web_testing"],
286393
}
287394

288395
for tech in tech_stack:
289396
if tech in tech_mapping:
290397
for skill_id in tech_mapping[tech]:
291398
signals[skill_id] = True
292399

400+
# リポジトリ名・説明からセキュリティ関連を検出
401+
security_keywords = [
402+
"security",
403+
"vulnerability",
404+
"pentest",
405+
"penetration",
406+
"ctf",
407+
"exploit",
408+
"hacking",
409+
"cryptography",
410+
"crypto",
411+
"ssl",
412+
"tls",
413+
"authentication",
414+
"authorization",
415+
"oauth",
416+
"jwt",
417+
"xss",
418+
"csrf",
419+
"sql injection",
420+
"injection",
421+
"firewall",
422+
"ids",
423+
"ips",
424+
"siem",
425+
"malware",
426+
"reverse",
427+
"forensics",
428+
"audit",
429+
"compliance",
430+
]
431+
432+
has_security_work = False
433+
for repo in repos:
434+
repo_name = repo.get("name", "").lower()
435+
description = (repo.get("description") or "").lower()
436+
combined = f"{repo_name} {description}"
437+
438+
if any(keyword in combined for keyword in security_keywords):
439+
has_security_work = True
440+
break
441+
442+
if has_security_work:
443+
signals["sec_net_os_basics"] = True
444+
signals["sec_web_vuln_basics"] = True
445+
logger.info("Security-related work detected in repositories")
446+
447+
# HTTPサーバー・API開発の形跡があればHTTP基礎はクリア
448+
if any(tech in tech_stack for tech in ["FastAPI", "Django", "Flask"]):
449+
signals["web_http_basics"] = True
450+
451+
# フロントエンド + バックエンドの両方があれば全体的な理解があると判断
452+
has_frontend = any(
453+
tech in tech_stack for tech in ["React", "Next.js", "Vue", "Angular"]
454+
)
455+
has_backend = any(tech in tech_stack for tech in ["FastAPI", "Django", "Flask"])
456+
457+
if has_frontend and has_backend:
458+
signals["web_build_tools"] = True
459+
460+
# Dockerがあればインフラ基礎もクリア
461+
if "Docker" in tech_stack:
462+
signals["infra_linux_admin"] = True
463+
signals["infra_net_routing"] = True
464+
293465
return signals

0 commit comments

Comments
 (0)