style: Format code with ruff formatter

- Auto-format 11 files to comply with ruff formatting standards
- Fixes CI/CD formatter check failures

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
yusyus
2026-02-03 21:37:54 +03:00
parent b01dfc5251
commit 4e8ad835ed
12 changed files with 271 additions and 231 deletions

View File

@@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
# Import config manager for settings # Import config manager for settings
try: try:
from skill_seekers.cli.config_manager import get_config_manager from skill_seekers.cli.config_manager import get_config_manager
CONFIG_AVAILABLE = True CONFIG_AVAILABLE = True
except ImportError: except ImportError:
CONFIG_AVAILABLE = False CONFIG_AVAILABLE = False
@@ -107,7 +108,9 @@ class AIEnhancer:
logger.warning("⚠️ anthropic package not installed, falling back to LOCAL mode") logger.warning("⚠️ anthropic package not installed, falling back to LOCAL mode")
self.mode = "local" self.mode = "local"
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Failed to initialize API client: {e}, falling back to LOCAL mode") logger.warning(
f"⚠️ Failed to initialize API client: {e}, falling back to LOCAL mode"
)
self.mode = "local" self.mode = "local"
if self.mode == "local" and self.enabled: if self.mode == "local" and self.enabled:
@@ -212,7 +215,8 @@ DO NOT include any explanation - just write the JSON file.
except json.JSONDecodeError: except json.JSONDecodeError:
# Try to find JSON in the response # Try to find JSON in the response
import re import re
json_match = re.search(r'\[[\s\S]*\]|\{[\s\S]*\}', response_text)
json_match = re.search(r"\[[\s\S]*\]|\{[\s\S]*\}", response_text)
if json_match: if json_match:
return json_match.group() return json_match.group()
logger.warning("⚠️ Could not parse JSON from LOCAL response") logger.warning("⚠️ Could not parse JSON from LOCAL response")

View File

@@ -89,7 +89,13 @@ class ArchitecturalPatternDetector:
# Framework detection patterns # Framework detection patterns
FRAMEWORK_MARKERS = { FRAMEWORK_MARKERS = {
# Game Engines (checked first to avoid false positives) # Game Engines (checked first to avoid false positives)
"Unity": ["Assembly-CSharp.csproj", "UnityEngine.dll", "ProjectSettings/ProjectVersion.txt", ".unity", "Library/"], "Unity": [
"Assembly-CSharp.csproj",
"UnityEngine.dll",
"ProjectSettings/ProjectVersion.txt",
".unity",
"Library/",
],
"Unreal": ["Source/", ".uproject", "Config/DefaultEngine.ini", "Binaries/", "Content/"], "Unreal": ["Source/", ".uproject", "Config/DefaultEngine.ini", "Binaries/", "Content/"],
"Godot": ["project.godot", ".godot", ".tscn", ".tres", ".gd"], "Godot": ["project.godot", ".godot", ".tscn", ".tres", ".gd"],
# Web Frameworks # Web Frameworks

View File

@@ -1429,7 +1429,6 @@ class CodeAnalyzer:
return comments return comments
def _analyze_godot_scene(self, content: str, file_path: str) -> dict[str, Any]: def _analyze_godot_scene(self, content: str, file_path: str) -> dict[str, Any]:
""" """
Analyze Godot .tscn scene file. Analyze Godot .tscn scene file.
@@ -1445,34 +1444,29 @@ class CodeAnalyzer:
scripts = [] scripts = []
# Extract external resources # Extract external resources
for match in re.finditer(r'\[ext_resource.*?type="(.+?)".*?path="(.+?)".*?id="(.+?)"\]', content): for match in re.finditer(
r'\[ext_resource.*?type="(.+?)".*?path="(.+?)".*?id="(.+?)"\]', content
):
res_type, path, res_id = match.groups() res_type, path, res_id = match.groups()
resources.append({ resources.append({"type": res_type, "path": path, "id": res_id})
"type": res_type,
"path": path,
"id": res_id
})
# Track scripts separately # Track scripts separately
if res_type == "Script": if res_type == "Script":
scripts.append({ scripts.append({"path": path, "id": res_id})
"path": path,
"id": res_id
})
# Extract nodes # Extract nodes
for match in re.finditer(r'\[node name="(.+?)".*?type="(.+?)".*?\]', content): for match in re.finditer(r'\[node name="(.+?)".*?type="(.+?)".*?\]', content):
node_name, node_type = match.groups() node_name, node_type = match.groups()
# Check if node has a script attached # Check if node has a script attached
script_match = re.search(rf'\[node name="{re.escape(node_name)}".*?script = ExtResource\("(.+?)"\)', content, re.DOTALL) script_match = re.search(
rf'\[node name="{re.escape(node_name)}".*?script = ExtResource\("(.+?)"\)',
content,
re.DOTALL,
)
attached_script = script_match.group(1) if script_match else None attached_script = script_match.group(1) if script_match else None
nodes.append({ nodes.append({"name": node_name, "type": node_type, "script": attached_script})
"name": node_name,
"type": node_type,
"script": attached_script
})
return { return {
"file": file_path, "file": file_path,
@@ -1482,8 +1476,8 @@ class CodeAnalyzer:
"scene_metadata": { "scene_metadata": {
"node_count": len(nodes), "node_count": len(nodes),
"script_count": len(scripts), "script_count": len(scripts),
"resource_count": len(resources) "resource_count": len(resources),
} },
} }
def _analyze_godot_resource(self, content: str, file_path: str) -> dict[str, Any]: def _analyze_godot_resource(self, content: str, file_path: str) -> dict[str, Any]:
@@ -1503,35 +1497,32 @@ class CodeAnalyzer:
script_path = None script_path = None
# Extract resource header # Extract resource header
header_match = re.search(r'\[gd_resource type="(.+?)"(?:\s+script_class="(.+?)")?\s+', content) header_match = re.search(
r'\[gd_resource type="(.+?)"(?:\s+script_class="(.+?)")?\s+', content
)
if header_match: if header_match:
resource_type = header_match.group(1) resource_type = header_match.group(1)
script_class = header_match.group(2) script_class = header_match.group(2)
# Extract external resources # Extract external resources
for match in re.finditer(r'\[ext_resource.*?type="(.+?)".*?path="(.+?)".*?id="(.+?)"\]', content): for match in re.finditer(
r'\[ext_resource.*?type="(.+?)".*?path="(.+?)".*?id="(.+?)"\]', content
):
res_type, path, res_id = match.groups() res_type, path, res_id = match.groups()
resources.append({ resources.append({"type": res_type, "path": path, "id": res_id})
"type": res_type,
"path": path,
"id": res_id
})
if res_type == "Script": if res_type == "Script":
script_path = path script_path = path
# Extract properties from [resource] section # Extract properties from [resource] section
resource_section = re.search(r'\[resource\](.*?)(?:\n\[|$)', content, re.DOTALL) resource_section = re.search(r"\[resource\](.*?)(?:\n\[|$)", content, re.DOTALL)
if resource_section: if resource_section:
prop_text = resource_section.group(1) prop_text = resource_section.group(1)
for line in prop_text.strip().split('\n'): for line in prop_text.strip().split("\n"):
if '=' in line: if "=" in line:
key, value = line.split('=', 1) key, value = line.split("=", 1)
properties.append({ properties.append({"name": key.strip(), "value": value.strip()})
"name": key.strip(),
"value": value.strip()
})
return { return {
"file": file_path, "file": file_path,
@@ -1542,8 +1533,8 @@ class CodeAnalyzer:
"resources": resources, "resources": resources,
"resource_metadata": { "resource_metadata": {
"property_count": len(properties), "property_count": len(properties),
"dependency_count": len(resources) "dependency_count": len(resources),
} },
} }
def _analyze_godot_shader(self, content: str, file_path: str) -> dict[str, Any]: def _analyze_godot_shader(self, content: str, file_path: str) -> dict[str, Any]:
@@ -1562,35 +1553,26 @@ class CodeAnalyzer:
shader_type = None shader_type = None
# Extract shader type # Extract shader type
type_match = re.search(r'shader_type\s+(\w+)', content) type_match = re.search(r"shader_type\s+(\w+)", content)
if type_match: if type_match:
shader_type = type_match.group(1) shader_type = type_match.group(1)
# Extract uniforms # Extract uniforms
for match in re.finditer(r'uniform\s+(\w+)\s+(\w+)(?:\s*:\s*(.+?))?(?:\s*=\s*(.+?))?;', content): for match in re.finditer(
r"uniform\s+(\w+)\s+(\w+)(?:\s*:\s*(.+?))?(?:\s*=\s*(.+?))?;", content
):
uniform_type, name, hint, default = match.groups() uniform_type, name, hint, default = match.groups()
uniforms.append({ uniforms.append({"name": name, "type": uniform_type, "hint": hint, "default": default})
"name": name,
"type": uniform_type,
"hint": hint,
"default": default
})
# Extract varying variables # Extract varying variables
for match in re.finditer(r'varying\s+(\w+)\s+(\w+)', content): for match in re.finditer(r"varying\s+(\w+)\s+(\w+)", content):
var_type, name = match.groups() var_type, name = match.groups()
varyings.append({ varyings.append({"name": name, "type": var_type})
"name": name,
"type": var_type
})
# Extract functions # Extract functions
for match in re.finditer(r'void\s+(\w+)\s*\(([^)]*)\)', content): for match in re.finditer(r"void\s+(\w+)\s*\(([^)]*)\)", content):
func_name, params = match.groups() func_name, params = match.groups()
functions.append({ functions.append({"name": func_name, "parameters": params.strip() if params else ""})
"name": func_name,
"parameters": params.strip() if params else ""
})
return { return {
"file": file_path, "file": file_path,
@@ -1598,10 +1580,7 @@ class CodeAnalyzer:
"uniforms": uniforms, "uniforms": uniforms,
"varyings": varyings, "varyings": varyings,
"functions": functions, "functions": functions,
"shader_metadata": { "shader_metadata": {"uniform_count": len(uniforms), "function_count": len(functions)},
"uniform_count": len(uniforms),
"function_count": len(functions)
}
} }
def _analyze_gdscript(self, content: str, file_path: str) -> dict[str, Any]: def _analyze_gdscript(self, content: str, file_path: str) -> dict[str, Any]:
@@ -1621,113 +1600,131 @@ class CodeAnalyzer:
exports = [] exports = []
# Extract class definition # Extract class definition
class_match = re.search(r'class_name\s+(\w+)(?:\s+extends\s+(\w+))?', content) class_match = re.search(r"class_name\s+(\w+)(?:\s+extends\s+(\w+))?", content)
if class_match: if class_match:
class_name = class_match.group(1) class_name = class_match.group(1)
extends = class_match.group(2) extends = class_match.group(2)
classes.append({ classes.append(
"name": class_name, {
"bases": [extends] if extends else [], "name": class_name,
"methods": [], "bases": [extends] if extends else [],
"line_number": content[: class_match.start()].count("\n") + 1 "methods": [],
}) "line_number": content[: class_match.start()].count("\n") + 1,
}
)
# Extract functions # Extract functions
for match in re.finditer(r'func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?:', content): for match in re.finditer(r"func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?:", content):
func_name, params, return_type = match.groups() func_name, params, return_type = match.groups()
# Parse parameters # Parse parameters
param_list = [] param_list = []
if params.strip(): if params.strip():
for param in params.split(','): for param in params.split(","):
param = param.strip() param = param.strip()
if ':' in param: if ":" in param:
# param_name: Type = default # param_name: Type = default
parts = param.split(':') parts = param.split(":")
name = parts[0].strip() name = parts[0].strip()
type_and_default = parts[1].strip() type_and_default = parts[1].strip()
param_type = type_and_default.split('=')[0].strip() if '=' in type_and_default else type_and_default param_type = (
default = type_and_default.split('=')[1].strip() if '=' in type_and_default else None type_and_default.split("=")[0].strip()
if "=" in type_and_default
else type_and_default
)
default = (
type_and_default.split("=")[1].strip()
if "=" in type_and_default
else None
)
param_list.append({ param_list.append(
"name": name, {"name": name, "type_hint": param_type, "default": default}
"type_hint": param_type, )
"default": default
})
else: else:
param_list.append({ param_list.append({"name": param, "type_hint": None, "default": None})
"name": param,
"type_hint": None,
"default": None
})
functions.append({ functions.append(
"name": func_name, {
"parameters": param_list, "name": func_name,
"return_type": return_type, "parameters": param_list,
"line_number": content[: match.start()].count("\n") + 1 "return_type": return_type,
}) "line_number": content[: match.start()].count("\n") + 1,
}
)
# Extract signals with documentation # Extract signals with documentation
signal_connections = [] signal_connections = []
signal_emissions = [] signal_emissions = []
for match in re.finditer(r'signal\s+(\w+)(?:\(([^)]*)\))?', content): for match in re.finditer(r"signal\s+(\w+)(?:\(([^)]*)\))?", content):
signal_name, params = match.groups() signal_name, params = match.groups()
line_number = content[: match.start()].count("\n") + 1 line_number = content[: match.start()].count("\n") + 1
# Extract documentation comment above signal (## or #) # Extract documentation comment above signal (## or #)
doc_comment = None doc_comment = None
lines = content[:match.start()].split('\n') lines = content[: match.start()].split("\n")
if len(lines) >= 2: if len(lines) >= 2:
prev_line = lines[-1].strip() prev_line = lines[-1].strip()
if prev_line.startswith('##') or prev_line.startswith('#'): if prev_line.startswith("##") or prev_line.startswith("#"):
doc_comment = prev_line.lstrip('#').strip() doc_comment = prev_line.lstrip("#").strip()
signals.append({ signals.append(
"name": signal_name, {
"parameters": params if params else "", "name": signal_name,
"line_number": line_number, "parameters": params if params else "",
"documentation": doc_comment "line_number": line_number,
}) "documentation": doc_comment,
}
)
# Extract signal connections (.connect() calls) # Extract signal connections (.connect() calls)
for match in re.finditer(r'(\w+(?:\.\w+)*)\.connect\(([^)]+)\)', content): for match in re.finditer(r"(\w+(?:\.\w+)*)\.connect\(([^)]+)\)", content):
signal_path, handler = match.groups() signal_path, handler = match.groups()
signal_connections.append({ signal_connections.append(
"signal": signal_path, {
"handler": handler.strip(), "signal": signal_path,
"line_number": content[: match.start()].count("\n") + 1 "handler": handler.strip(),
}) "line_number": content[: match.start()].count("\n") + 1,
}
)
# Extract signal emissions (.emit() calls) # Extract signal emissions (.emit() calls)
for match in re.finditer(r'(\w+(?:\.\w+)*)\.emit\(([^)]*)\)', content): for match in re.finditer(r"(\w+(?:\.\w+)*)\.emit\(([^)]*)\)", content):
signal_path, args = match.groups() signal_path, args = match.groups()
signal_emissions.append({ signal_emissions.append(
"signal": signal_path, {
"arguments": args.strip() if args else "", "signal": signal_path,
"line_number": content[: match.start()].count("\n") + 1 "arguments": args.strip() if args else "",
}) "line_number": content[: match.start()].count("\n") + 1,
}
)
# Extract @export variables # Extract @export variables
for match in re.finditer(r'@export(?:\(([^)]+)\))?\s+var\s+(\w+)(?:\s*:\s*(\w+))?(?:\s*=\s*(.+?))?(?:\n|$)', content): for match in re.finditer(
r"@export(?:\(([^)]+)\))?\s+var\s+(\w+)(?:\s*:\s*(\w+))?(?:\s*=\s*(.+?))?(?:\n|$)",
content,
):
hint, var_name, var_type, default = match.groups() hint, var_name, var_type, default = match.groups()
exports.append({ exports.append(
"name": var_name, {
"type": var_type, "name": var_name,
"default": default, "type": var_type,
"export_hint": hint, "default": default,
"line_number": content[: match.start()].count("\n") + 1 "export_hint": hint,
}) "line_number": content[: match.start()].count("\n") + 1,
}
)
# Detect test framework # Detect test framework
test_framework = None test_framework = None
test_functions = [] test_functions = []
# GUT (Godot Unit Test) - extends "res://addons/gut/test.gd" or extends GutTest # GUT (Godot Unit Test) - extends "res://addons/gut/test.gd" or extends GutTest
if re.search(r'extends\s+["\']?res://addons/gut/test\.gd["\']?', content) or \ if re.search(r'extends\s+["\']?res://addons/gut/test\.gd["\']?', content) or re.search(
re.search(r'extends\s+GutTest', content): r"extends\s+GutTest", content
):
test_framework = "GUT" test_framework = "GUT"
# Extract test functions (test_* functions) # Extract test functions (test_* functions)
@@ -1736,23 +1733,23 @@ class CodeAnalyzer:
test_functions.append(func) test_functions.append(func)
# gdUnit4 - @suite class annotation # gdUnit4 - @suite class annotation
elif re.search(r'@suite', content): elif re.search(r"@suite", content):
test_framework = "gdUnit4" test_framework = "gdUnit4"
# Extract test functions (@test annotated or test_* prefix) # Extract test functions (@test annotated or test_* prefix)
for i, func in enumerate(functions): for i, func in enumerate(functions):
# Check for @test annotation above function # Check for @test annotation above function
func_line = func["line_number"] func_line = func["line_number"]
lines = content.split('\n') lines = content.split("\n")
if func_line > 1: if func_line > 1:
prev_line = lines[func_line - 2].strip() prev_line = lines[func_line - 2].strip()
if prev_line.startswith('@test'): if prev_line.startswith("@test"):
test_functions.append(func) test_functions.append(func)
elif func["name"].startswith("test_"): elif func["name"].startswith("test_"):
test_functions.append(func) test_functions.append(func)
# WAT (WizAds Test) - less common # WAT (WizAds Test) - less common
elif re.search(r'extends\s+WAT\.Test', content): elif re.search(r"extends\s+WAT\.Test", content):
test_framework = "WAT" test_framework = "WAT"
for func in functions: for func in functions:
if func["name"].startswith("test_"): if func["name"].startswith("test_"):
@@ -1815,4 +1812,3 @@ def create_sprite(texture: str) -> Node2D:
] ]
) )
print(f" {method['name']}({params}) -> {method['return_type']}") print(f" {method['name']}({params}) -> {method['return_type']}")

View File

@@ -401,11 +401,13 @@ def extract_markdown_structure(content: str) -> dict[str, Any]:
if header_match: if header_match:
level = len(header_match.group(1)) level = len(header_match.group(1))
text = header_match.group(2).strip() text = header_match.group(2).strip()
structure["headers"].append({ structure["headers"].append(
"level": level, {
"text": text, "level": level,
"line": i + 1, "text": text,
}) "line": i + 1,
}
)
# First h1 is the title # First h1 is the title
if level == 1 and structure["title"] is None: if level == 1 and structure["title"] is None:
structure["title"] = text structure["title"] = text
@@ -416,24 +418,30 @@ def extract_markdown_structure(content: str) -> dict[str, Any]:
language = match.group(1) or "text" language = match.group(1) or "text"
code = match.group(2).strip() code = match.group(2).strip()
if len(code) > 0: if len(code) > 0:
structure["code_blocks"].append({ structure["code_blocks"].append(
"language": language, {
"code": code[:500], # Truncate long code blocks "language": language,
"full_length": len(code), "code": code[:500], # Truncate long code blocks
}) "full_length": len(code),
}
)
# Extract links # Extract links
link_pattern = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") link_pattern = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
for match in link_pattern.finditer(content): for match in link_pattern.finditer(content):
structure["links"].append({ structure["links"].append(
"text": match.group(1), {
"url": match.group(2), "text": match.group(1),
}) "url": match.group(2),
}
)
return structure return structure
def generate_markdown_summary(content: str, structure: dict[str, Any], max_length: int = 500) -> str: def generate_markdown_summary(
content: str, structure: dict[str, Any], max_length: int = 500
) -> str:
""" """
Generate a summary of markdown content. Generate a summary of markdown content.
@@ -546,12 +554,14 @@ def process_markdown_docs(
structure = extract_markdown_structure(content) structure = extract_markdown_structure(content)
summary = generate_markdown_summary(content, structure) summary = generate_markdown_summary(content, structure)
doc_data.update({ doc_data.update(
"title": structure.get("title") or md_path.stem, {
"structure": structure, "title": structure.get("title") or md_path.stem,
"summary": summary, "structure": structure,
"content": content if depth == "full" else None, "summary": summary,
}) "content": content if depth == "full" else None,
}
)
processed_docs.append(doc_data) processed_docs.append(doc_data)
# Track categories # Track categories
@@ -587,6 +597,7 @@ def process_markdown_docs(
# Copy file to category folder # Copy file to category folder
dest_path = category_dir / doc["filename"] dest_path = category_dir / doc["filename"]
import shutil import shutil
shutil.copy2(src_path, dest_path) shutil.copy2(src_path, dest_path)
except Exception as e: except Exception as e:
logger.debug(f"Failed to copy {doc['path']}: {e}") logger.debug(f"Failed to copy {doc['path']}: {e}")
@@ -602,7 +613,9 @@ def process_markdown_docs(
with open(index_json, "w", encoding="utf-8") as f: with open(index_json, "w", encoding="utf-8") as f:
json.dump(index_data, f, indent=2, default=str) json.dump(index_data, f, indent=2, default=str)
logger.info(f"✅ Processed {len(processed_docs)} documentation files in {len(categories)} categories") logger.info(
f"✅ Processed {len(processed_docs)} documentation files in {len(categories)} categories"
)
logger.info(f"📁 Saved to: {docs_output_dir}") logger.info(f"📁 Saved to: {docs_output_dir}")
return index_data return index_data
@@ -636,18 +649,22 @@ def _enhance_docs_api(docs: list[dict], api_key: str) -> list[dict]:
"""Enhance docs using Claude API.""" """Enhance docs using Claude API."""
try: try:
import anthropic import anthropic
client = anthropic.Anthropic(api_key=api_key) client = anthropic.Anthropic(api_key=api_key)
# Batch documents for efficiency # Batch documents for efficiency
batch_size = 10 batch_size = 10
for i in range(0, len(docs), batch_size): for i in range(0, len(docs), batch_size):
batch = docs[i:i + batch_size] batch = docs[i : i + batch_size]
# Create prompt for batch # Create prompt for batch
docs_text = "\n\n".join([ docs_text = "\n\n".join(
f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nSummary: {d.get('summary', 'N/A')}" [
for d in batch if d.get("summary") f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nSummary: {d.get('summary', 'N/A')}"
]) for d in batch
if d.get("summary")
]
)
if not docs_text: if not docs_text:
continue continue
@@ -666,12 +683,13 @@ Return JSON with format:
response = client.messages.create( response = client.messages.create(
model="claude-sonnet-4-20250514", model="claude-sonnet-4-20250514",
max_tokens=2000, max_tokens=2000,
messages=[{"role": "user", "content": prompt}] messages=[{"role": "user", "content": prompt}],
) )
# Parse response and merge enhancements # Parse response and merge enhancements
try: try:
import re import re
json_match = re.search(r"\{.*\}", response.content[0].text, re.DOTALL) json_match = re.search(r"\{.*\}", response.content[0].text, re.DOTALL)
if json_match: if json_match:
enhancements = json.loads(json_match.group()) enhancements = json.loads(json_match.group())
@@ -700,10 +718,12 @@ def _enhance_docs_local(docs: list[dict]) -> list[dict]:
if not docs_with_summary: if not docs_with_summary:
return docs return docs
docs_text = "\n\n".join([ docs_text = "\n\n".join(
f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nPath: {d['path']}\nSummary: {d.get('summary', 'N/A')}" [
for d in docs_with_summary[:20] # Limit to 20 docs f"## {d.get('title', d['filename'])}\nCategory: {d['category']}\nPath: {d['path']}\nSummary: {d.get('summary', 'N/A')}"
]) for d in docs_with_summary[:20] # Limit to 20 docs
]
)
prompt = f"""Analyze these documentation files from a codebase and provide insights. prompt = f"""Analyze these documentation files from a codebase and provide insights.
@@ -734,6 +754,7 @@ Output JSON only:
if result.returncode == 0 and result.stdout: if result.returncode == 0 and result.stdout:
import re import re
json_match = re.search(r"\{.*\}", result.stdout, re.DOTALL) json_match = re.search(r"\{.*\}", result.stdout, re.DOTALL)
if json_match: if json_match:
enhancements = json.loads(json_match.group()) enhancements = json.loads(json_match.group())
@@ -801,7 +822,9 @@ def analyze_codebase(
if enhance_level > 0: if enhance_level > 0:
level_names = {1: "SKILL.md only", 2: "SKILL.md+Architecture+Config", 3: "full"} level_names = {1: "SKILL.md only", 2: "SKILL.md+Architecture+Config", 3: "full"}
logger.info(f"🤖 AI Enhancement Level: {enhance_level} ({level_names.get(enhance_level, 'unknown')})") logger.info(
f"🤖 AI Enhancement Level: {enhance_level} ({level_names.get(enhance_level, 'unknown')})"
)
# Resolve directory to absolute path to avoid relative_to() errors # Resolve directory to absolute path to avoid relative_to() errors
directory = Path(directory).resolve() directory = Path(directory).resolve()
@@ -1411,7 +1434,9 @@ Use this skill when you need to:
skill_content += "- **Architecture**: `references/architecture/` - Architectural patterns\n" skill_content += "- **Architecture**: `references/architecture/` - Architectural patterns\n"
refs_added = True refs_added = True
if extract_docs and (output_dir / "documentation").exists(): if extract_docs and (output_dir / "documentation").exists():
skill_content += "- **Documentation**: `references/documentation/` - Project documentation\n" skill_content += (
"- **Documentation**: `references/documentation/` - Project documentation\n"
)
refs_added = True refs_added = True
if not refs_added: if not refs_added:
@@ -1691,10 +1716,7 @@ def _format_signal_flow_section(output_dir: Path, results: dict[str, Any]) -> st
content += "\n" content += "\n"
# Test framework detection # Test framework detection
test_files = [ test_files = [f for f in results.get("files", []) if f.get("test_framework")]
f for f in results.get("files", [])
if f.get("test_framework")
]
if test_files: if test_files:
frameworks = {} frameworks = {}
@@ -1732,7 +1754,15 @@ def _format_documentation_section(_output_dir: Path, docs_data: dict[str, Any])
content += f"**Categories:** {len(categories)}\n\n" content += f"**Categories:** {len(categories)}\n\n"
# List documents by category (most important first) # List documents by category (most important first)
priority_order = ["overview", "architecture", "guides", "workflows", "features", "api", "examples"] priority_order = [
"overview",
"architecture",
"guides",
"workflows",
"features",
"api",
"examples",
]
# Sort categories by priority # Sort categories by priority
sorted_categories = [] sorted_categories = []
@@ -1779,6 +1809,7 @@ def _format_documentation_section(_output_dir: Path, docs_data: dict[str, Any])
if all_topics: if all_topics:
# Deduplicate and count # Deduplicate and count
from collections import Counter from collections import Counter
topic_counts = Counter(all_topics) topic_counts = Counter(all_topics)
top_topics = [t for t, _ in topic_counts.most_common(10)] top_topics = [t for t, _ in topic_counts.most_common(10)]
content += f"**Key Topics:** {', '.join(top_topics)}\n\n" content += f"**Key Topics:** {', '.join(top_topics)}\n\n"

View File

@@ -167,9 +167,7 @@ class ConfigEnhancer:
for setting in cf.get("settings", [])[:5]: # First 5 settings per file for setting in cf.get("settings", [])[:5]: # First 5 settings per file
# Support both "type" (from config_extractor) and "value_type" (legacy) # Support both "type" (from config_extractor) and "value_type" (legacy)
value_type = setting.get("type", setting.get("value_type", "unknown")) value_type = setting.get("type", setting.get("value_type", "unknown"))
settings_summary.append( settings_summary.append(f" - {setting['key']}: {setting['value']} ({value_type})")
f" - {setting['key']}: {setting['value']} ({value_type})"
)
# Support both "type" (from config_extractor) and "config_type" (legacy) # Support both "type" (from config_extractor) and "config_type" (legacy)
config_type = cf.get("type", cf.get("config_type", "unknown")) config_type = cf.get("type", cf.get("config_type", "unknown"))
@@ -306,7 +304,9 @@ Focus on actionable insights that help developers understand and improve their c
config_type = cf.get("type", cf.get("config_type", "unknown")) config_type = cf.get("type", cf.get("config_type", "unknown"))
settings_preview = [] settings_preview = []
for s in cf.get("settings", [])[:3]: # Show first 3 settings for s in cf.get("settings", [])[:3]: # Show first 3 settings
settings_preview.append(f" - {s.get('key', 'unknown')}: {str(s.get('value', ''))[:50]}") settings_preview.append(
f" - {s.get('key', 'unknown')}: {str(s.get('value', ''))[:50]}"
)
config_data.append(f""" config_data.append(f"""
### {cf["relative_path"]} ({config_type}) ### {cf["relative_path"]} ({config_type})
@@ -431,9 +431,7 @@ DO NOT explain your work - just write the JSON file directly.
potential_files.append(json_file) potential_files.append(json_file)
# Try to load the most recent JSON file with expected structure # Try to load the most recent JSON file with expected structure
for json_file in sorted( for json_file in sorted(potential_files, key=lambda f: f.stat().st_mtime, reverse=True):
potential_files, key=lambda f: f.stat().st_mtime, reverse=True
):
try: try:
with open(json_file) as f: with open(json_file) as f:
data = json.load(f) data = json.load(f)

View File

@@ -429,7 +429,9 @@ class ConfigParser:
# JSON array at root - extract from each dict item # JSON array at root - extract from each dict item
for idx, item in enumerate(data): for idx, item in enumerate(data):
if isinstance(item, dict): if isinstance(item, dict):
self._extract_settings_from_dict(item, config_file, parent_path=[f"[{idx}]"]) self._extract_settings_from_dict(
item, config_file, parent_path=[f"[{idx}]"]
)
else: else:
# Primitive value at root (string, number, etc.) - skip # Primitive value at root (string, number, etc.) - skip
logger.debug(f"Skipping JSON with primitive root: {config_file.relative_path}") logger.debug(f"Skipping JSON with primitive root: {config_file.relative_path}")
@@ -452,7 +454,9 @@ class ConfigParser:
# YAML array at root - extract from each dict item # YAML array at root - extract from each dict item
for idx, item in enumerate(data): for idx, item in enumerate(data):
if isinstance(item, dict): if isinstance(item, dict):
self._extract_settings_from_dict(item, config_file, parent_path=[f"[{idx}]"]) self._extract_settings_from_dict(
item, config_file, parent_path=[f"[{idx}]"]
)
except yaml.YAMLError as e: except yaml.YAMLError as e:
config_file.parse_errors.append(f"YAML parse error: {str(e)}") config_file.parse_errors.append(f"YAML parse error: {str(e)}")

View File

@@ -272,7 +272,7 @@ class DependencyAnalyzer:
# Extract extends with class name: extends MyBaseClass # Extract extends with class name: extends MyBaseClass
# Note: This creates a symbolic dependency that may not resolve to a file # Note: This creates a symbolic dependency that may not resolve to a file
extends_class_pattern = r'extends\s+([A-Z]\w+)' extends_class_pattern = r"extends\s+([A-Z]\w+)"
for match in re.finditer(extends_class_pattern, content): for match in re.finditer(extends_class_pattern, content):
class_name = match.group(1) class_name = match.group(1)
line_num = content[: match.start()].count("\n") + 1 line_num = content[: match.start()].count("\n") + 1

View File

@@ -300,14 +300,16 @@ For more information: https://github.com/yusufkaraaslan/Skill_Seekers
) )
analyze_parser.add_argument("--file-patterns", help="Comma-separated file patterns") analyze_parser.add_argument("--file-patterns", help="Comma-separated file patterns")
analyze_parser.add_argument( analyze_parser.add_argument(
"--enhance", action="store_true", help="Enable AI enhancement (default level 1 = SKILL.md only)" "--enhance",
action="store_true",
help="Enable AI enhancement (default level 1 = SKILL.md only)",
) )
analyze_parser.add_argument( analyze_parser.add_argument(
"--enhance-level", "--enhance-level",
type=int, type=int,
choices=[0, 1, 2, 3], choices=[0, 1, 2, 3],
default=None, default=None,
help="AI enhancement level: 0=off, 1=SKILL.md only (default), 2=+Architecture+Config, 3=full" help="AI enhancement level: 0=off, 1=SKILL.md only (default), 2=+Architecture+Config, 3=full",
) )
analyze_parser.add_argument("--skip-api-reference", action="store_true", help="Skip API docs") analyze_parser.add_argument("--skip-api-reference", action="store_true", help="Skip API docs")
analyze_parser.add_argument( analyze_parser.add_argument(
@@ -321,7 +323,9 @@ For more information: https://github.com/yusufkaraaslan/Skill_Seekers
) )
analyze_parser.add_argument("--skip-how-to-guides", action="store_true", help="Skip guides") analyze_parser.add_argument("--skip-how-to-guides", action="store_true", help="Skip guides")
analyze_parser.add_argument("--skip-config-patterns", action="store_true", help="Skip config") analyze_parser.add_argument("--skip-config-patterns", action="store_true", help="Skip config")
analyze_parser.add_argument("--skip-docs", action="store_true", help="Skip project docs (README, docs/)") analyze_parser.add_argument(
"--skip-docs", action="store_true", help="Skip project docs (README, docs/)"
)
analyze_parser.add_argument("--no-comments", action="store_true", help="Skip comments") analyze_parser.add_argument("--no-comments", action="store_true", help="Skip comments")
analyze_parser.add_argument("--verbose", action="store_true", help="Verbose logging") analyze_parser.add_argument("--verbose", action="store_true", help="Verbose logging")
@@ -565,13 +569,16 @@ def main(argv: list[str] | None = None) -> int:
# Handle preset flags (depth and features) # Handle preset flags (depth and features)
if args.quick: if args.quick:
# Quick = surface depth + skip advanced features + no AI # Quick = surface depth + skip advanced features + no AI
sys.argv.extend([ sys.argv.extend(
"--depth", "surface", [
"--skip-patterns", "--depth",
"--skip-test-examples", "surface",
"--skip-how-to-guides", "--skip-patterns",
"--skip-config-patterns", "--skip-test-examples",
]) "--skip-how-to-guides",
"--skip-config-patterns",
]
)
elif args.comprehensive: elif args.comprehensive:
# Comprehensive = full depth + all features (AI level is separate) # Comprehensive = full depth + all features (AI level is separate)
sys.argv.extend(["--depth", "full"]) sys.argv.extend(["--depth", "full"])
@@ -588,6 +595,7 @@ def main(argv: list[str] | None = None) -> int:
# Use default from config (default: 1) # Use default from config (default: 1)
try: try:
from skill_seekers.cli.config_manager import get_config_manager from skill_seekers.cli.config_manager import get_config_manager
config = get_config_manager() config = get_config_manager()
enhance_level = config.get_default_enhance_level() enhance_level = config.get_default_enhance_level()
except Exception: except Exception:

View File

@@ -144,9 +144,7 @@ class SignalFlowAnalyzer:
# Observer pattern - signals with multiple connections # Observer pattern - signals with multiple connections
multi_connected = { multi_connected = {
sig: len(conns) sig: len(conns) for sig, conns in self.signal_connections.items() if len(conns) >= 3
for sig, conns in self.signal_connections.items()
if len(conns) >= 3
} }
if multi_connected: if multi_connected:
@@ -170,9 +168,7 @@ class SignalFlowAnalyzer:
def _calculate_statistics(self) -> dict[str, Any]: def _calculate_statistics(self) -> dict[str, Any]:
"""Calculate signal usage statistics.""" """Calculate signal usage statistics."""
total_signals = len(self.signal_declarations) total_signals = len(self.signal_declarations)
total_connections = sum( total_connections = sum(len(conns) for conns in self.signal_connections.values())
len(conns) for conns in self.signal_connections.values()
)
total_emissions = sum(len(emits) for emits in self.signal_emissions.items()) total_emissions = sum(len(emits) for emits in self.signal_emissions.items())
# Find most connected signals # Find most connected signals
@@ -181,17 +177,13 @@ class SignalFlowAnalyzer:
)[:5] )[:5]
# Find most emitted signals # Find most emitted signals
most_emitted = sorted( most_emitted = sorted(self.signal_emissions.items(), key=lambda x: len(x[1]), reverse=True)[
self.signal_emissions.items(), key=lambda x: len(x[1]), reverse=True :5
)[:5] ]
# Signal density (signals per GDScript file) # Signal density (signals per GDScript file)
gdscript_files = sum( gdscript_files = sum(1 for f in self.files if f.get("language") == "GDScript")
1 for f in self.files if f.get("language") == "GDScript" signal_density = total_signals / gdscript_files if gdscript_files > 0 else 0
)
signal_density = (
total_signals / gdscript_files if gdscript_files > 0 else 0
)
return { return {
"total_signals": total_signals, "total_signals": total_signals,
@@ -200,12 +192,10 @@ class SignalFlowAnalyzer:
"signal_density": round(signal_density, 2), "signal_density": round(signal_density, 2),
"gdscript_files": gdscript_files, "gdscript_files": gdscript_files,
"most_connected_signals": [ "most_connected_signals": [
{"signal": sig, "connection_count": len(conns)} {"signal": sig, "connection_count": len(conns)} for sig, conns in most_connected
for sig, conns in most_connected
], ],
"most_emitted_signals": [ "most_emitted_signals": [
{"signal": sig, "emission_count": len(emits)} {"signal": sig, "emission_count": len(emits)} for sig, emits in most_emitted
for sig, emits in most_emitted
], ],
} }
@@ -272,9 +262,7 @@ class SignalFlowAnalyzer:
return patterns[:10] # Top 10 most used signals return patterns[:10] # Top 10 most used signals
def generate_how_to_guides( def generate_how_to_guides(self, output_dir: Path, ai_mode: str = "LOCAL") -> str:
self, output_dir: Path, ai_mode: str = "LOCAL"
) -> str:
""" """
Generate signal-based how-to guides using AI. Generate signal-based how-to guides using AI.
@@ -297,7 +285,9 @@ class SignalFlowAnalyzer:
for i, pattern in enumerate(patterns, 1): for i, pattern in enumerate(patterns, 1):
signal_name = pattern["signal_name"] signal_name = pattern["signal_name"]
guide_content += f"{i}. [How to use `{signal_name}`](#{signal_name.lower().replace('_', '-')})\n" guide_content += (
f"{i}. [How to use `{signal_name}`](#{signal_name.lower().replace('_', '-')})\n"
)
guide_content += "\n---\n\n" guide_content += "\n---\n\n"
@@ -313,9 +303,7 @@ class SignalFlowAnalyzer:
return str(guide_file) return str(guide_file)
def _generate_signal_guide( def _generate_signal_guide(self, pattern: dict[str, Any], ai_mode: str) -> str:
self, pattern: dict[str, Any], ai_mode: str
) -> str:
""" """
Generate a how-to guide for a single signal using AI. Generate a how-to guide for a single signal using AI.
@@ -434,10 +422,12 @@ class SignalFlowAnalyzer:
guide_file = self.generate_how_to_guides(output_dir, ai_mode) guide_file = self.generate_how_to_guides(output_dir, ai_mode)
if guide_file: if guide_file:
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info(f"📚 Generated signal how-to guides: {guide_file}") logger.info(f"📚 Generated signal how-to guides: {guide_file}")
except Exception as e: except Exception as e:
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.warning(f"Failed to generate signal how-to guides: {e}") logger.warning(f"Failed to generate signal how-to guides: {e}")
@@ -453,9 +443,7 @@ class SignalFlowAnalyzer:
lines.append(f"- **Total Signals**: {stats['total_signals']}") lines.append(f"- **Total Signals**: {stats['total_signals']}")
lines.append(f"- **Total Connections**: {stats['total_connections']}") lines.append(f"- **Total Connections**: {stats['total_connections']}")
lines.append(f"- **Total Emissions**: {stats['total_emissions']}") lines.append(f"- **Total Emissions**: {stats['total_emissions']}")
lines.append( lines.append(f"- **Signal Density**: {stats['signal_density']} signals per file\n")
f"- **Signal Density**: {stats['signal_density']} signals per file\n"
)
# Patterns # Patterns
if analysis["patterns"]: if analysis["patterns"]:
@@ -480,9 +468,7 @@ class SignalFlowAnalyzer:
if stats["most_connected_signals"]: if stats["most_connected_signals"]:
lines.append("## Most Connected Signals\n") lines.append("## Most Connected Signals\n")
for item in stats["most_connected_signals"]: for item in stats["most_connected_signals"]:
lines.append( lines.append(f"- **{item['signal']}**: {item['connection_count']} connections")
f"- **{item['signal']}**: {item['connection_count']} connections"
)
lines.append("") lines.append("")
with open(output_dir / "signal_reference.md", "w") as f: with open(output_dir / "signal_reference.md", "w") as f:

View File

@@ -816,7 +816,11 @@ class GenericTestAnalyzer:
# Find next method (setup or test) # Find next method (setup or test)
next_pattern = patterns.get("setup", patterns["test_function"]) next_pattern = patterns.get("setup", patterns["test_function"])
next_setup = re.search(next_pattern, code[setup_start:]) next_setup = re.search(next_pattern, code[setup_start:])
setup_end = setup_start + next_setup.start() if next_setup else min(setup_start + 500, len(code)) setup_end = (
setup_start + next_setup.start()
if next_setup
else min(setup_start + 500, len(code))
)
setup_body = code[setup_start:setup_end] setup_body = code[setup_start:setup_end]
example = self._create_example( example = self._create_example(

View File

@@ -63,17 +63,20 @@ class TestAnalyzeSubcommand(unittest.TestCase):
def test_all_skip_flags(self): def test_all_skip_flags(self):
"""Test all skip flags are properly parsed.""" """Test all skip flags are properly parsed."""
args = self.parser.parse_args([ args = self.parser.parse_args(
"analyze", [
"--directory", ".", "analyze",
"--skip-api-reference", "--directory",
"--skip-dependency-graph", ".",
"--skip-patterns", "--skip-api-reference",
"--skip-test-examples", "--skip-dependency-graph",
"--skip-how-to-guides", "--skip-patterns",
"--skip-config-patterns", "--skip-test-examples",
"--skip-docs" "--skip-how-to-guides",
]) "--skip-config-patterns",
"--skip-docs",
]
)
self.assertTrue(args.skip_api_reference) self.assertTrue(args.skip_api_reference)
self.assertTrue(args.skip_dependency_graph) self.assertTrue(args.skip_dependency_graph)
self.assertTrue(args.skip_patterns) self.assertTrue(args.skip_patterns)

2
uv.lock generated
View File

@@ -1846,7 +1846,7 @@ wheels = [
[[package]] [[package]]
name = "skill-seekers" name = "skill-seekers"
version = "2.7.4" version = "2.9.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "anthropic" }, { name = "anthropic" },