diff --git a/src/skill_seekers/cli/ai_enhancer.py b/src/skill_seekers/cli/ai_enhancer.py index f5217b9..9fbfcd8 100644 --- a/src/skill_seekers/cli/ai_enhancer.py +++ b/src/skill_seekers/cli/ai_enhancer.py @@ -36,6 +36,7 @@ logger = logging.getLogger(__name__) # Import config manager for settings try: from skill_seekers.cli.config_manager import get_config_manager + CONFIG_AVAILABLE = True except ImportError: CONFIG_AVAILABLE = False @@ -107,7 +108,9 @@ class AIEnhancer: logger.warning("⚠️ anthropic package not installed, falling back to LOCAL mode") self.mode = "local" 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" if self.mode == "local" and self.enabled: @@ -212,7 +215,8 @@ DO NOT include any explanation - just write the JSON file. except json.JSONDecodeError: # Try to find JSON in the response 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: return json_match.group() logger.warning("⚠️ Could not parse JSON from LOCAL response") diff --git a/src/skill_seekers/cli/architectural_pattern_detector.py b/src/skill_seekers/cli/architectural_pattern_detector.py index a41af6a..3608204 100644 --- a/src/skill_seekers/cli/architectural_pattern_detector.py +++ b/src/skill_seekers/cli/architectural_pattern_detector.py @@ -89,7 +89,13 @@ class ArchitecturalPatternDetector: # Framework detection patterns FRAMEWORK_MARKERS = { # 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/"], "Godot": ["project.godot", ".godot", ".tscn", ".tres", ".gd"], # Web Frameworks diff --git a/src/skill_seekers/cli/code_analyzer.py b/src/skill_seekers/cli/code_analyzer.py index ec87424..f7d44b9 100644 --- a/src/skill_seekers/cli/code_analyzer.py +++ b/src/skill_seekers/cli/code_analyzer.py @@ -1429,7 +1429,6 @@ class CodeAnalyzer: return comments - def _analyze_godot_scene(self, content: str, file_path: str) -> dict[str, Any]: """ Analyze Godot .tscn scene file. @@ -1445,34 +1444,29 @@ class CodeAnalyzer: scripts = [] # 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() - resources.append({ - "type": res_type, - "path": path, - "id": res_id - }) + resources.append({"type": res_type, "path": path, "id": res_id}) # Track scripts separately if res_type == "Script": - scripts.append({ - "path": path, - "id": res_id - }) + scripts.append({"path": path, "id": res_id}) # Extract nodes for match in re.finditer(r'\[node name="(.+?)".*?type="(.+?)".*?\]', content): node_name, node_type = match.groups() # 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 - nodes.append({ - "name": node_name, - "type": node_type, - "script": attached_script - }) + nodes.append({"name": node_name, "type": node_type, "script": attached_script}) return { "file": file_path, @@ -1482,8 +1476,8 @@ class CodeAnalyzer: "scene_metadata": { "node_count": len(nodes), "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]: @@ -1503,35 +1497,32 @@ class CodeAnalyzer: script_path = None # 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: resource_type = header_match.group(1) script_class = header_match.group(2) # 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() - resources.append({ - "type": res_type, - "path": path, - "id": res_id - }) + resources.append({"type": res_type, "path": path, "id": res_id}) if res_type == "Script": script_path = path # 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: prop_text = resource_section.group(1) - for line in prop_text.strip().split('\n'): - if '=' in line: - key, value = line.split('=', 1) - properties.append({ - "name": key.strip(), - "value": value.strip() - }) + for line in prop_text.strip().split("\n"): + if "=" in line: + key, value = line.split("=", 1) + properties.append({"name": key.strip(), "value": value.strip()}) return { "file": file_path, @@ -1542,8 +1533,8 @@ class CodeAnalyzer: "resources": resources, "resource_metadata": { "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]: @@ -1562,35 +1553,26 @@ class CodeAnalyzer: shader_type = None # 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: shader_type = type_match.group(1) # 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() - uniforms.append({ - "name": name, - "type": uniform_type, - "hint": hint, - "default": default - }) + uniforms.append({"name": name, "type": uniform_type, "hint": hint, "default": default}) # 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() - varyings.append({ - "name": name, - "type": var_type - }) + varyings.append({"name": name, "type": var_type}) # 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() - functions.append({ - "name": func_name, - "parameters": params.strip() if params else "" - }) + functions.append({"name": func_name, "parameters": params.strip() if params else ""}) return { "file": file_path, @@ -1598,10 +1580,7 @@ class CodeAnalyzer: "uniforms": uniforms, "varyings": varyings, "functions": functions, - "shader_metadata": { - "uniform_count": len(uniforms), - "function_count": len(functions) - } + "shader_metadata": {"uniform_count": len(uniforms), "function_count": len(functions)}, } def _analyze_gdscript(self, content: str, file_path: str) -> dict[str, Any]: @@ -1621,113 +1600,131 @@ class CodeAnalyzer: exports = [] # 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: class_name = class_match.group(1) extends = class_match.group(2) - classes.append({ - "name": class_name, - "bases": [extends] if extends else [], - "methods": [], - "line_number": content[: class_match.start()].count("\n") + 1 - }) + classes.append( + { + "name": class_name, + "bases": [extends] if extends else [], + "methods": [], + "line_number": content[: class_match.start()].count("\n") + 1, + } + ) # 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() # Parse parameters param_list = [] if params.strip(): - for param in params.split(','): + for param in params.split(","): param = param.strip() - if ':' in param: + if ":" in param: # param_name: Type = default - parts = param.split(':') + parts = param.split(":") name = parts[0].strip() type_and_default = parts[1].strip() - param_type = 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_type = ( + 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({ - "name": name, - "type_hint": param_type, - "default": default - }) + param_list.append( + {"name": name, "type_hint": param_type, "default": default} + ) else: - param_list.append({ - "name": param, - "type_hint": None, - "default": None - }) + param_list.append({"name": param, "type_hint": None, "default": None}) - functions.append({ - "name": func_name, - "parameters": param_list, - "return_type": return_type, - "line_number": content[: match.start()].count("\n") + 1 - }) + functions.append( + { + "name": func_name, + "parameters": param_list, + "return_type": return_type, + "line_number": content[: match.start()].count("\n") + 1, + } + ) # Extract signals with documentation signal_connections = [] 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() line_number = content[: match.start()].count("\n") + 1 # Extract documentation comment above signal (## or #) doc_comment = None - lines = content[:match.start()].split('\n') + lines = content[: match.start()].split("\n") if len(lines) >= 2: prev_line = lines[-1].strip() - if prev_line.startswith('##') or prev_line.startswith('#'): - doc_comment = prev_line.lstrip('#').strip() + if prev_line.startswith("##") or prev_line.startswith("#"): + doc_comment = prev_line.lstrip("#").strip() - signals.append({ - "name": signal_name, - "parameters": params if params else "", - "line_number": line_number, - "documentation": doc_comment - }) + signals.append( + { + "name": signal_name, + "parameters": params if params else "", + "line_number": line_number, + "documentation": doc_comment, + } + ) # 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_connections.append({ - "signal": signal_path, - "handler": handler.strip(), - "line_number": content[: match.start()].count("\n") + 1 - }) + signal_connections.append( + { + "signal": signal_path, + "handler": handler.strip(), + "line_number": content[: match.start()].count("\n") + 1, + } + ) # 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_emissions.append({ - "signal": signal_path, - "arguments": args.strip() if args else "", - "line_number": content[: match.start()].count("\n") + 1 - }) + signal_emissions.append( + { + "signal": signal_path, + "arguments": args.strip() if args else "", + "line_number": content[: match.start()].count("\n") + 1, + } + ) # 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() - exports.append({ - "name": var_name, - "type": var_type, - "default": default, - "export_hint": hint, - "line_number": content[: match.start()].count("\n") + 1 - }) + exports.append( + { + "name": var_name, + "type": var_type, + "default": default, + "export_hint": hint, + "line_number": content[: match.start()].count("\n") + 1, + } + ) # Detect test framework test_framework = None test_functions = [] # 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 \ - re.search(r'extends\s+GutTest', content): + if re.search(r'extends\s+["\']?res://addons/gut/test\.gd["\']?', content) or re.search( + r"extends\s+GutTest", content + ): test_framework = "GUT" # Extract test functions (test_* functions) @@ -1736,23 +1733,23 @@ class CodeAnalyzer: test_functions.append(func) # gdUnit4 - @suite class annotation - elif re.search(r'@suite', content): + elif re.search(r"@suite", content): test_framework = "gdUnit4" # Extract test functions (@test annotated or test_* prefix) for i, func in enumerate(functions): # Check for @test annotation above function func_line = func["line_number"] - lines = content.split('\n') + lines = content.split("\n") if func_line > 1: prev_line = lines[func_line - 2].strip() - if prev_line.startswith('@test'): + if prev_line.startswith("@test"): test_functions.append(func) elif func["name"].startswith("test_"): test_functions.append(func) # 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" for func in functions: if func["name"].startswith("test_"): @@ -1815,4 +1812,3 @@ def create_sprite(texture: str) -> Node2D: ] ) print(f" {method['name']}({params}) -> {method['return_type']}") - diff --git a/src/skill_seekers/cli/codebase_scraper.py b/src/skill_seekers/cli/codebase_scraper.py index cdd55a4..cfc7f8c 100644 --- a/src/skill_seekers/cli/codebase_scraper.py +++ b/src/skill_seekers/cli/codebase_scraper.py @@ -401,11 +401,13 @@ def extract_markdown_structure(content: str) -> dict[str, Any]: if header_match: level = len(header_match.group(1)) text = header_match.group(2).strip() - structure["headers"].append({ - "level": level, - "text": text, - "line": i + 1, - }) + structure["headers"].append( + { + "level": level, + "text": text, + "line": i + 1, + } + ) # First h1 is the title if level == 1 and structure["title"] is None: structure["title"] = text @@ -416,24 +418,30 @@ def extract_markdown_structure(content: str) -> dict[str, Any]: language = match.group(1) or "text" code = match.group(2).strip() if len(code) > 0: - structure["code_blocks"].append({ - "language": language, - "code": code[:500], # Truncate long code blocks - "full_length": len(code), - }) + structure["code_blocks"].append( + { + "language": language, + "code": code[:500], # Truncate long code blocks + "full_length": len(code), + } + ) # Extract links link_pattern = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") for match in link_pattern.finditer(content): - structure["links"].append({ - "text": match.group(1), - "url": match.group(2), - }) + structure["links"].append( + { + "text": match.group(1), + "url": match.group(2), + } + ) 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. @@ -546,12 +554,14 @@ def process_markdown_docs( structure = extract_markdown_structure(content) summary = generate_markdown_summary(content, structure) - doc_data.update({ - "title": structure.get("title") or md_path.stem, - "structure": structure, - "summary": summary, - "content": content if depth == "full" else None, - }) + doc_data.update( + { + "title": structure.get("title") or md_path.stem, + "structure": structure, + "summary": summary, + "content": content if depth == "full" else None, + } + ) processed_docs.append(doc_data) # Track categories @@ -587,6 +597,7 @@ def process_markdown_docs( # Copy file to category folder dest_path = category_dir / doc["filename"] import shutil + shutil.copy2(src_path, dest_path) except Exception as 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: 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}") return index_data @@ -636,18 +649,22 @@ def _enhance_docs_api(docs: list[dict], api_key: str) -> list[dict]: """Enhance docs using Claude API.""" try: import anthropic + client = anthropic.Anthropic(api_key=api_key) # Batch documents for efficiency batch_size = 10 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 - 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") - ]) + 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") + ] + ) if not docs_text: continue @@ -666,12 +683,13 @@ Return JSON with format: response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2000, - messages=[{"role": "user", "content": prompt}] + messages=[{"role": "user", "content": prompt}], ) # Parse response and merge enhancements try: import re + json_match = re.search(r"\{.*\}", response.content[0].text, re.DOTALL) if json_match: enhancements = json.loads(json_match.group()) @@ -700,10 +718,12 @@ def _enhance_docs_local(docs: list[dict]) -> list[dict]: if not docs_with_summary: return docs - 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 - ]) + 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 + ] + ) 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: import re + json_match = re.search(r"\{.*\}", result.stdout, re.DOTALL) if json_match: enhancements = json.loads(json_match.group()) @@ -801,7 +822,9 @@ def analyze_codebase( if enhance_level > 0: 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 directory = Path(directory).resolve() @@ -1411,7 +1434,9 @@ Use this skill when you need to: skill_content += "- **Architecture**: `references/architecture/` - Architectural patterns\n" refs_added = True 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 if not refs_added: @@ -1691,10 +1716,7 @@ def _format_signal_flow_section(output_dir: Path, results: dict[str, Any]) -> st content += "\n" # Test framework detection - test_files = [ - f for f in results.get("files", []) - if f.get("test_framework") - ] + test_files = [f for f in results.get("files", []) if f.get("test_framework")] if test_files: frameworks = {} @@ -1732,7 +1754,15 @@ def _format_documentation_section(_output_dir: Path, docs_data: dict[str, Any]) content += f"**Categories:** {len(categories)}\n\n" # 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 sorted_categories = [] @@ -1779,6 +1809,7 @@ def _format_documentation_section(_output_dir: Path, docs_data: dict[str, Any]) if all_topics: # Deduplicate and count from collections import Counter + topic_counts = Counter(all_topics) top_topics = [t for t, _ in topic_counts.most_common(10)] content += f"**Key Topics:** {', '.join(top_topics)}\n\n" diff --git a/src/skill_seekers/cli/config_enhancer.py b/src/skill_seekers/cli/config_enhancer.py index 59d9d84..67b32b7 100644 --- a/src/skill_seekers/cli/config_enhancer.py +++ b/src/skill_seekers/cli/config_enhancer.py @@ -167,9 +167,7 @@ class ConfigEnhancer: for setting in cf.get("settings", [])[:5]: # First 5 settings per file # Support both "type" (from config_extractor) and "value_type" (legacy) value_type = setting.get("type", setting.get("value_type", "unknown")) - settings_summary.append( - f" - {setting['key']}: {setting['value']} ({value_type})" - ) + settings_summary.append(f" - {setting['key']}: {setting['value']} ({value_type})") # Support both "type" (from config_extractor) and "config_type" (legacy) 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")) settings_preview = [] 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""" ### {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) # Try to load the most recent JSON file with expected structure - for json_file in sorted( - potential_files, key=lambda f: f.stat().st_mtime, reverse=True - ): + for json_file in sorted(potential_files, key=lambda f: f.stat().st_mtime, reverse=True): try: with open(json_file) as f: data = json.load(f) diff --git a/src/skill_seekers/cli/config_extractor.py b/src/skill_seekers/cli/config_extractor.py index 02163a2..a43f8fd 100644 --- a/src/skill_seekers/cli/config_extractor.py +++ b/src/skill_seekers/cli/config_extractor.py @@ -429,7 +429,9 @@ class ConfigParser: # JSON array at root - extract from each dict item for idx, item in enumerate(data): 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: # Primitive value at root (string, number, etc.) - skip 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 for idx, item in enumerate(data): 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: config_file.parse_errors.append(f"YAML parse error: {str(e)}") diff --git a/src/skill_seekers/cli/dependency_analyzer.py b/src/skill_seekers/cli/dependency_analyzer.py index 1960075..055eab5 100644 --- a/src/skill_seekers/cli/dependency_analyzer.py +++ b/src/skill_seekers/cli/dependency_analyzer.py @@ -272,7 +272,7 @@ class DependencyAnalyzer: # Extract extends with class name: extends MyBaseClass # 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): class_name = match.group(1) line_num = content[: match.start()].count("\n") + 1 diff --git a/src/skill_seekers/cli/main.py b/src/skill_seekers/cli/main.py index 341c537..80cf8e9 100644 --- a/src/skill_seekers/cli/main.py +++ b/src/skill_seekers/cli/main.py @@ -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( - "--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( "--enhance-level", type=int, choices=[0, 1, 2, 3], 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( @@ -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-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("--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) if args.quick: # Quick = surface depth + skip advanced features + no AI - sys.argv.extend([ - "--depth", "surface", - "--skip-patterns", - "--skip-test-examples", - "--skip-how-to-guides", - "--skip-config-patterns", - ]) + sys.argv.extend( + [ + "--depth", + "surface", + "--skip-patterns", + "--skip-test-examples", + "--skip-how-to-guides", + "--skip-config-patterns", + ] + ) elif args.comprehensive: # Comprehensive = full depth + all features (AI level is separate) sys.argv.extend(["--depth", "full"]) @@ -588,6 +595,7 @@ def main(argv: list[str] | None = None) -> int: # Use default from config (default: 1) try: from skill_seekers.cli.config_manager import get_config_manager + config = get_config_manager() enhance_level = config.get_default_enhance_level() except Exception: diff --git a/src/skill_seekers/cli/signal_flow_analyzer.py b/src/skill_seekers/cli/signal_flow_analyzer.py index 5cff9e1..f1395eb 100644 --- a/src/skill_seekers/cli/signal_flow_analyzer.py +++ b/src/skill_seekers/cli/signal_flow_analyzer.py @@ -144,9 +144,7 @@ class SignalFlowAnalyzer: # Observer pattern - signals with multiple connections multi_connected = { - sig: len(conns) - for sig, conns in self.signal_connections.items() - if len(conns) >= 3 + sig: len(conns) for sig, conns in self.signal_connections.items() if len(conns) >= 3 } if multi_connected: @@ -170,9 +168,7 @@ class SignalFlowAnalyzer: def _calculate_statistics(self) -> dict[str, Any]: """Calculate signal usage statistics.""" total_signals = len(self.signal_declarations) - total_connections = sum( - len(conns) for conns in self.signal_connections.values() - ) + total_connections = sum(len(conns) for conns in self.signal_connections.values()) total_emissions = sum(len(emits) for emits in self.signal_emissions.items()) # Find most connected signals @@ -181,17 +177,13 @@ class SignalFlowAnalyzer: )[:5] # Find most emitted signals - most_emitted = sorted( - self.signal_emissions.items(), key=lambda x: len(x[1]), reverse=True - )[:5] + most_emitted = sorted(self.signal_emissions.items(), key=lambda x: len(x[1]), reverse=True)[ + :5 + ] # Signal density (signals per GDScript file) - gdscript_files = sum( - 1 for f in self.files if f.get("language") == "GDScript" - ) - signal_density = ( - total_signals / gdscript_files if gdscript_files > 0 else 0 - ) + gdscript_files = sum(1 for f in self.files if f.get("language") == "GDScript") + signal_density = total_signals / gdscript_files if gdscript_files > 0 else 0 return { "total_signals": total_signals, @@ -200,12 +192,10 @@ class SignalFlowAnalyzer: "signal_density": round(signal_density, 2), "gdscript_files": gdscript_files, "most_connected_signals": [ - {"signal": sig, "connection_count": len(conns)} - for sig, conns in most_connected + {"signal": sig, "connection_count": len(conns)} for sig, conns in most_connected ], "most_emitted_signals": [ - {"signal": sig, "emission_count": len(emits)} - for sig, emits in most_emitted + {"signal": sig, "emission_count": len(emits)} for sig, emits in most_emitted ], } @@ -272,9 +262,7 @@ class SignalFlowAnalyzer: return patterns[:10] # Top 10 most used signals - def generate_how_to_guides( - self, output_dir: Path, ai_mode: str = "LOCAL" - ) -> str: + def generate_how_to_guides(self, output_dir: Path, ai_mode: str = "LOCAL") -> str: """ Generate signal-based how-to guides using AI. @@ -297,7 +285,9 @@ class SignalFlowAnalyzer: for i, pattern in enumerate(patterns, 1): 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" @@ -313,9 +303,7 @@ class SignalFlowAnalyzer: return str(guide_file) - def _generate_signal_guide( - self, pattern: dict[str, Any], ai_mode: str - ) -> str: + def _generate_signal_guide(self, pattern: dict[str, Any], ai_mode: str) -> str: """ 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) if guide_file: import logging + logger = logging.getLogger(__name__) logger.info(f"📚 Generated signal how-to guides: {guide_file}") except Exception as e: import logging + logger = logging.getLogger(__name__) 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 Connections**: {stats['total_connections']}") lines.append(f"- **Total Emissions**: {stats['total_emissions']}") - lines.append( - f"- **Signal Density**: {stats['signal_density']} signals per file\n" - ) + lines.append(f"- **Signal Density**: {stats['signal_density']} signals per file\n") # Patterns if analysis["patterns"]: @@ -480,9 +468,7 @@ class SignalFlowAnalyzer: if stats["most_connected_signals"]: lines.append("## Most Connected Signals\n") for item in stats["most_connected_signals"]: - lines.append( - f"- **{item['signal']}**: {item['connection_count']} connections" - ) + lines.append(f"- **{item['signal']}**: {item['connection_count']} connections") lines.append("") with open(output_dir / "signal_reference.md", "w") as f: diff --git a/src/skill_seekers/cli/test_example_extractor.py b/src/skill_seekers/cli/test_example_extractor.py index c2c8ca4..b42740e 100644 --- a/src/skill_seekers/cli/test_example_extractor.py +++ b/src/skill_seekers/cli/test_example_extractor.py @@ -816,7 +816,11 @@ class GenericTestAnalyzer: # Find next method (setup or test) next_pattern = patterns.get("setup", patterns["test_function"]) 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] example = self._create_example( diff --git a/tests/test_analyze_command.py b/tests/test_analyze_command.py index 2463ae9..913a81b 100644 --- a/tests/test_analyze_command.py +++ b/tests/test_analyze_command.py @@ -63,17 +63,20 @@ class TestAnalyzeSubcommand(unittest.TestCase): def test_all_skip_flags(self): """Test all skip flags are properly parsed.""" - args = self.parser.parse_args([ - "analyze", - "--directory", ".", - "--skip-api-reference", - "--skip-dependency-graph", - "--skip-patterns", - "--skip-test-examples", - "--skip-how-to-guides", - "--skip-config-patterns", - "--skip-docs" - ]) + args = self.parser.parse_args( + [ + "analyze", + "--directory", + ".", + "--skip-api-reference", + "--skip-dependency-graph", + "--skip-patterns", + "--skip-test-examples", + "--skip-how-to-guides", + "--skip-config-patterns", + "--skip-docs", + ] + ) self.assertTrue(args.skip_api_reference) self.assertTrue(args.skip_dependency_graph) self.assertTrue(args.skip_patterns) diff --git a/uv.lock b/uv.lock index 3cd5525..d7128f9 100644 --- a/uv.lock +++ b/uv.lock @@ -1846,7 +1846,7 @@ wheels = [ [[package]] name = "skill-seekers" -version = "2.7.4" +version = "2.9.0" source = { editable = "." } dependencies = [ { name = "anthropic" },