diff --git a/.test_short_id_normalization.md b/.test_short_id_normalization.md index e8f2acac..1325b77d 100644 --- a/.test_short_id_normalization.md +++ b/.test_short_id_normalization.md @@ -50,6 +50,5 @@ short_to_fqdn = build_short_id_to_fqdn_map(components) ## Notes -- Dependency components (`is_from_deps=True`) are included in the mapping - The normalization priority is: exact FQDN match → short ID lookup → not found - Source: [`test_short_id_normalization.py`](https://github.com/flamingo-stack/CodeWiki/blob/main/test_short_id_normalization.py) \ No newline at end of file diff --git a/.verify_fqdn_implementation.md b/.verify_fqdn_implementation.md index 44e24144..c782e8a5 100644 --- a/.verify_fqdn_implementation.md +++ b/.verify_fqdn_implementation.md @@ -5,7 +5,7 @@ Verification utility that statically checks whether the FQDN (Fully Qualified Do | Function | Description | |---|---| -| `check_node_model()` | Scans `core.py` for required FQDN-related fields: `short_id`, `namespace`, `is_from_deps` | +| `check_node_model()` | Scans `core.py` for required FQDN-related fields: `short_id`, `namespace` | | `check_ast_parser()` | Scans `ast_parser.py` for correct FQDN construction patterns and `Node` instantiation arguments | | `main()` | Orchestrates both checks, prints a summary, and exits with code `0` (pass) or `1` (fail) | @@ -26,7 +26,6 @@ FQDN Implementation Verification ✅ Node Model FQDN Fields Check: ✓ short_id: ✓ namespace: - ✓ is_from_deps: ✅ AST Parser FQDN Implementation Check: ✓ fqdn = f"{namespace}.{original_id}" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d6241925 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## Unreleased + +### Breaking changes + +- **`--additional-paths` and `additional_source_paths` are removed.** CodeWiki documents only the repository it + runs in. Other repositories are read through the Flamingo hub's code-graph tools (`HUB_BASE_URL`, + `WEBHOOK_SECRET`, `GITHUB_REPOSITORY`), never copied beside the checkout. Passing `--additional-paths` now stops + with an error saying so; drop the option. + +### Changes + +- Agent models are called by their provider's own API: the OpenAI Responses API on `api.openai.com`, the native + Anthropic API (with prompt caching) on `api.anthropic.com`, chat completions on any other host. +- Passing provider errors (dropped connections, rate limits, outages) are retried, by the SDK and then per module. +- Every component is placed in a module; clustering no longer leaves "non-essential" components undocumented. +- Copied libraries and minified builds are left out of what is documented (minified by form, a `/*!` library + banner, vendored folders), and each run logs what it left out and why. +- Each run's model usage, per answering model and failed attempts included, is written to `CODEWIKI_USAGE_FILE`. diff --git a/CLI_MULTI_PATH_INTEGRATION.md b/CLI_MULTI_PATH_INTEGRATION.md deleted file mode 100644 index 73e2b57e..00000000 --- a/CLI_MULTI_PATH_INTEGRATION.md +++ /dev/null @@ -1,338 +0,0 @@ -# CLI Multi-Path Integration Summary - -## Overview -Successfully added `--additional-paths` CLI option and wired it through the entire pipeline to enable multi-path configuration for dependency analysis. - -## Changes Made - -### 1. CLI Command (`codewiki/cli/commands/generate.py`) - -**Added CLI Option:** -```python -@click.option( - "--additional-paths", - type=str, - default=None, - help="Comma-separated additional paths to include in dependency analysis (e.g., 'vendor/deps,external/libs')", -) -``` - -**Added Parameter Validation:** -```python -# Validate and parse additional paths -additional_paths_list = None -if additional_paths: - additional_paths_list = parse_patterns(additional_paths) - - # Validate that paths exist - invalid_paths = [] - for path in additional_paths_list: - full_path = repo_path / path - if not full_path.exists(): - invalid_paths.append(path) - - if invalid_paths: - raise RepositoryError( - f"Additional paths not found:\n" - f" {', '.join(invalid_paths)}\n\n" - f"Please ensure all paths exist relative to repository root:\n" - f" Repository: {repo_path}\n" - f" Invalid paths: {invalid_paths}" - ) -``` - -**Added to Config Dictionary:** -```python -# Additional paths for dependency analysis -'additional_paths': additional_paths_list, -``` - -**Added Usage Examples:** -```bash -# Include additional dependency paths (e.g., vendor, external libs) -$ codewiki generate --additional-paths "vendor/packages,external/deps" -``` - -### 2. CLI Adapter (`codewiki/cli/adapters/doc_generator.py`) - -**Added Type Import:** -```python -from typing import Dict, Any, List -``` - -**Wire Through to Backend:** -```python -# Additional paths for dependency analysis (convert to absolute paths) -additional_source_paths=[ - str((Path(self.repo_path) / path).resolve()) - for path in self.config.get('additional_paths', []) -] if self.config.get('additional_paths') else None -``` - -**Added Verbose Logging:** -```python -if backend_config.additional_source_paths: - print(f" ├─ Additional Paths ({len(backend_config.additional_source_paths)}):") - for path in backend_config.additional_source_paths: - print(f" │ └─ {path}") -``` - -### 3. Dependency Graph Builder (`codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py`) - -**Updated to Use Multi-Path from Config:** -```python -# Get custom include/exclude patterns from config -include_patterns = self.config.include_patterns if self.config.include_patterns else None -exclude_patterns = self.config.exclude_patterns if self.config.exclude_patterns else None - -# Build list of paths to analyze -repo_paths = self.config.all_source_paths # Property that returns List[str] - -# Log multi-path mode if enabled -if self.config.is_multi_path_mode(): - logger.info(f"🔀 Multi-path mode enabled: analyzing {len(repo_paths)} paths") - for i, path in enumerate(repo_paths, 1): - logger.info(f" {i}. {path}") - -parser = DependencyParser( - repo_paths if len(repo_paths) > 1 else repo_paths[0], - include_patterns=include_patterns, - exclude_patterns=exclude_patterns -) -``` - -## CLI Usage Examples - -### Basic Usage -```bash -# Single additional path -codewiki generate --additional-paths "vendor/deps" - -# Multiple paths (comma-separated) -codewiki generate --additional-paths "vendor/deps,external/libs,third_party/packages" -``` - -### Combined with Other Options -```bash -# With verbose output -codewiki generate --additional-paths "vendor,external" --verbose - -# With include/exclude patterns -codewiki generate --additional-paths "vendor" \ - --include "*.py,*.js" \ - --exclude "*test*,*spec*" - -# With focus modules -codewiki generate --additional-paths "deps" \ - --focus "src/core,src/api" - -# Full example -codewiki generate \ - --additional-paths "vendor/packages,external/deps" \ - --include "*.py,*.js" \ - --exclude "*Tests*,*Specs*" \ - --doc-type architecture \ - --verbose -``` - -## Error Handling - -### Path Validation -The CLI performs early validation of all additional paths: - -1. **Parse comma-separated paths** using existing `parse_patterns()` function -2. **Check each path exists** relative to repository root -3. **Raise RepositoryError** with helpful message if any path is invalid: - ``` - Additional paths not found: - vendor/missing, external/nonexistent - - Please ensure all paths exist relative to repository root: - Repository: /path/to/repo - Invalid paths: ['vendor/missing', 'external/nonexistent'] - ``` - -### Backend Validation -Backend Config validates all paths using `validate_source_paths()`: -- Checks path exists -- Checks path is a directory -- Checks read access permissions - -## Integration Points - -### Data Flow -``` -CLI Option (--additional-paths) - ↓ parse_patterns() -CLI Command (additional_paths: str) - ↓ validation + split -CLI Command (additional_paths_list: List[str]) - ↓ pass to generator config -CLIDocumentationGenerator (config['additional_paths']) - ↓ convert to absolute paths -BackendConfig.from_cli(additional_source_paths: List[str]) - ↓ validate + store -DependencyGraphBuilder (config.all_source_paths) - ↓ use in parser -DependencyParser(repo_paths: Union[str, List[str]]) -``` - -### Existing Backend Support -The backend already supported multi-path mode: - -- **Config class** has `additional_source_paths` field -- **Config.all_source_paths** property returns list of all paths -- **Config.is_multi_path_mode()** checks if multi-path enabled -- **Config.validate_source_paths()** validates all paths -- **DependencyParser** accepts `Union[str, List[str]]` for repo_path - -## Testing - -### CLI Help -```bash -$ codewiki generate --help -... - --additional-paths TEXT Comma-separated additional paths to include - in dependency analysis (e.g., - 'vendor/deps,external/libs') -``` - -### Type Checking -```bash -$ mypy codewiki/cli/commands/generate.py -# No type errors -``` - -### Manual Testing -```bash -# Test with valid paths -$ codewiki generate --additional-paths "src,tests" --verbose - -# Test with invalid paths (should fail with clear error) -$ codewiki generate --additional-paths "missing,nonexistent" -``` - -### Test Results - -**Test 1: Invalid Paths (Expected Failure)** -```bash -$ cd /tmp/test_repo -$ codewiki generate --additional-paths "missing,nonexistent" - -✗ Additional paths not found: - missing, nonexistent - -Please ensure all paths exist relative to repository root: - Repository: /private/tmp/test_repo - Invalid paths: ['missing', 'nonexistent'] -``` -✅ **Result**: Early validation catches invalid paths with clear error message - -**Test 2: Valid Multi-Path Configuration** -```bash -$ cd /tmp/test_repo -$ codewiki generate --additional-paths "vendor,external" --verbose - -[1/4] Validating configuration... -✓ Configuration valid -[2/4] Validating repository... -✓ Repository valid: test_repo - Detected languages: Python (2 files) -✓ Output directory: /private/tmp/test_repo/docs -[4/4] Generating documentation... - - Max tokens: 128000 - Max token/module: 36369 - Max token/leaf module: 16000 - Max depth: 2 - Additional paths: ['vendor', 'external'] - -📋 Generation Configuration: - ├─ Cluster Model: gpt-5.2 - │ └─ Base URL: https://api.openai.com/v1 - │ └─ Max Tokens: 128000 - ├─ Main Model: gpt-5.2-chat-latest - │ └─ Base URL: https://api.openai.com/v1 - │ └─ Max Tokens: 128000 - ├─ Fallback Model: claude-opus-4-5-20251101 - │ └─ Base URL: https://api.anthropic.com/v1 - │ └─ Max Tokens: 200000 - ├─ Module Settings: - │ ├─ Max tokens/module: 36369 - │ ├─ Max tokens/leaf: 16000 - │ └─ Max depth: 2 - ├─ Additional Paths (2): - │ └─ /private/tmp/test_repo/vendor - │ └─ /private/tmp/test_repo/external - -[00:00] Phase 1/5: Dependency Analysis -[00:00] Initializing dependency analyzer... - ├─ Repository: /private/tmp/test_repo - └─ Output: /private/tmp/test_repo/docs/temp -[00:00] Scanning repository for source files... -[00:00] Building dependency graph... -INFO 🔀 Multi-path mode enabled: analyzing 3 paths - 1. /private/tmp/test_repo - 2. /private/tmp/test_repo/vendor - 3. /private/tmp/test_repo/external -``` -✅ **Result**: All paths parsed, validated, converted to absolute paths, and passed to analyzer - -## Benefits - -1. **User-Friendly Syntax**: Comma-separated paths (e.g., `path1,path2,path3`) -2. **Early Validation**: Paths checked before backend processing starts -3. **Clear Error Messages**: Helpful feedback if paths don't exist -4. **Verbose Logging**: Shows all paths being analyzed in verbose mode -5. **Backward Compatible**: Optional parameter, no impact on existing usage -6. **Type Safe**: Full type annotations and type checking pass - -## Future Enhancements - -### Optional: Support Repeated Option -Could add support for repeated option syntax: -```bash -# Alternative syntax (requires additional implementation) -codewiki generate --additional-path vendor --additional-path external -``` - -Implementation would change option to: -```python -@click.option( - "--additional-path", - "additional_paths", - multiple=True, - help="Additional path to include (can be specified multiple times)" -) -``` - -Then handle both comma-separated and repeated options. - -## Files Modified - -1. `/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/cli/commands/generate.py` - - Added CLI option - - Added parameter validation - - Added usage examples - - Wired through to generator config - -2. `/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/cli/adapters/doc_generator.py` - - Added type import - - Convert relative paths to absolute - - Pass to backend config - - Added verbose logging - -3. `/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py` - - Use config.get_all_source_paths() - - Log multi-path mode info - - Pass list to DependencyParser - -## Conclusion - -The multi-path configuration is now fully exposed via CLI with: -- ✅ User-friendly comma-separated syntax -- ✅ Early validation with clear error messages -- ✅ Complete integration through all layers -- ✅ Verbose logging support -- ✅ Type safety -- ✅ Backward compatibility diff --git a/FQDN_CHANGES_VISUAL.md b/FQDN_CHANGES_VISUAL.md deleted file mode 100644 index 5bf95bb3..00000000 --- a/FQDN_CHANGES_VISUAL.md +++ /dev/null @@ -1,210 +0,0 @@ -# FQDN Implementation - Visual Changes - -## Node Model Changes - -### BEFORE -```python -class Node(BaseModel): - id: str # Could collide across namespaces - name: str - component_type: str - # ... other fields ... - component_id: Optional[str] = None -``` - -### AFTER -```python -class Node(BaseModel): - id: str # FQDN format: {namespace}.{original_id} - name: str - component_type: str - # ... other fields ... - component_id: Optional[str] = None - - # NEW: FQDN metadata fields (backward compatible) - short_id: str = "" # Original ID for display - namespace: str = "" # Source namespace - is_from_deps: bool = False # Dependency flag -``` - ---- - -## Multi-Path Node Creation Changes - -### BEFORE -```python -def _build_namespaced_components( - self, - call_graph_result: Dict, - namespace: str, - namespace_mapping: Dict[str, str] -) -> Dict[str, Node]: - for func_dict in functions: - original_id = func_dict.get("id", "") - namespaced_id = f"{namespace}.{original_id}" - - node = Node( - id=namespaced_id, - name=func_dict.get("name", ""), - # ... other fields ... - component_id=namespaced_id - ) - - components[namespaced_id] = node # ⚠️ Variable naming inconsistent -``` - -### AFTER -```python -def _build_namespaced_components( - self, - call_graph_result: Dict, - namespace: str, - namespace_mapping: Dict[str, str], - repo_index: int = 0 # ✨ NEW: Track main vs deps -) -> Dict[str, Node]: - for func_dict in functions: - original_id = func_dict.get("id", "") - fqdn = f"{namespace}.{original_id}" # ✨ Clear FQDN variable - - node = Node( - id=fqdn, # ✨ FQDN as primary ID - name=func_dict.get("name", ""), - # ... other fields ... - component_id=fqdn, - # ✨ NEW: FQDN metadata - short_id=original_id, - namespace=namespace, - is_from_deps=(repo_index > 0) - ) - - components[fqdn] = node # ✅ Consistent FQDN usage -``` - ---- - -## Single-Path Node Creation Changes - -### BEFORE -```python -def _build_components_from_analysis(self, call_graph_result: Dict): - for func_dict in functions: - component_id = func_dict.get("id", "") - - node = Node( - id=component_id, # ⚠️ No namespace - name=func_dict.get("name", ""), - # ... other fields ... - component_id=component_id - ) - - self.components[component_id] = node # ⚠️ Potential collisions - - # Modules stored without namespace - if "." in component_id: - module_path = ".".join(component_id.split(".")[:-1]) - self.modules.add(module_path) -``` - -### AFTER -```python -def _build_components_from_analysis(self, call_graph_result: Dict): - # ✨ NEW: Compute namespace from repo path - namespace = self._get_namespace_from_path(self.repo_path) - - for func_dict in functions: - original_id = func_dict.get("id", "") - fqdn = f"{namespace}.{original_id}" # ✨ Construct FQDN - - node = Node( - id=fqdn, # ✨ FQDN as primary ID - name=func_dict.get("name", ""), - # ... other fields ... - component_id=fqdn, - # ✨ NEW: FQDN metadata - short_id=original_id, - namespace=namespace, - is_from_deps=False # Single-path = main repo - ) - - self.components[fqdn] = node # ✅ FQDN key - - # ✨ Modules stored with namespace - if "." in original_id: - module_path = ".".join(original_id.split(".")[:-1]) - self.modules.add(f"{namespace}.{module_path}") -``` - ---- - -## Example Data Transformation - -### BEFORE (Potential ID Collision) -```python -# Main repo component -Node( - id="services.auth.login", - name="login", - component_id="services.auth.login" -) - -# Dependency repo component (COLLISION!) -Node( - id="services.auth.login", # ⚠️ Same ID! - name="login", - component_id="services.auth.login" -) -``` - -### AFTER (No Collision with FQDN) -```python -# Main repo component -Node( - id="openframe-frontend.services.auth.login", # ✅ FQDN - name="login", - component_id="openframe-frontend.services.auth.login", - short_id="services.auth.login", # ✨ Clean display name - namespace="openframe-frontend", - is_from_deps=False -) - -# Dependency repo component (NO COLLISION!) -Node( - id="ui-kit.services.auth.login", # ✅ Different FQDN - name="login", - component_id="ui-kit.services.auth.login", - short_id="services.auth.login", # ✨ Same clean name - namespace="ui-kit", - is_from_deps=True # ✨ Marked as dependency -) -``` - ---- - -## Key Improvements - -| Aspect | Before | After | -|--------|--------|-------| -| **ID Format** | `services.auth.login` | `openframe-frontend.services.auth.login` | -| **Collision Risk** | ⚠️ High (cross-repo) | ✅ None (FQDN guarantee) | -| **Source Tracking** | ❌ Not tracked | ✅ `namespace` + `is_from_deps` | -| **Display Name** | `id` (long) | `short_id` (clean) | -| **Dependency Filtering** | ❌ Not possible | ✅ Use `is_from_deps` flag | -| **Module Paths** | `services.auth` | `openframe-frontend.services.auth` | - ---- - -## Backward Compatibility - -All new fields have defaults, ensuring existing code continues to work: - -```python -# Old code creating Node without new fields -node = Node( - id="some_id", - name="some_name", - component_type="function", - file_path="/path/to/file.py", - relative_path="file.py" -) -# ✅ Works! New fields default to: short_id="", namespace="", is_from_deps=False -``` diff --git a/FQDN_FIX_SUMMARY.md b/FQDN_FIX_SUMMARY.md index 686d42c9..febbb997 100644 --- a/FQDN_FIX_SUMMARY.md +++ b/FQDN_FIX_SUMMARY.md @@ -183,7 +183,7 @@ python -m pytest test_fqdn_normalization.py -v ### Step 3: Integration Test ```bash -# Run full workflow with multi-path +# Run full workflow cd /private/tmp/test-codewiki-fqdn /tmp/test-codewiki-fqdn.sh diff --git a/FQDN_IMPLEMENTATION_SUMMARY.md b/FQDN_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index b9637fd4..00000000 --- a/FQDN_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,138 +0,0 @@ -# FQDN Implementation Summary - -## Overview -Successfully implemented FQDN (Fully Qualified Domain Name) fields in the Node model and updated all Node creation points to use FQDN consistently as the primary identifier. - -## Changes Made - -### 1. Node Model Updates (`codewiki/src/be/dependency_analyzer/models/core.py`) - -**Added FQDN metadata fields:** -```python -class Node(BaseModel): - id: str # Now FQDN format: {namespace}.{original_id} - - # ... existing fields ... - - # NEW: FQDN metadata fields (all have defaults for backward compatibility) - short_id: str = "" # Original ID without namespace (for display) - namespace: str = "" # Namespace (e.g., "main", "deps", "ui-kit") - is_from_deps: bool = False # True if from dependencies, False if from main repo -``` - -**Key Points:** -- `id` field is now FQDN (primary key) -- `short_id` stores the original ID for display purposes -- `namespace` tracks which repository/source the component comes from -- `is_from_deps` boolean flag for quick identification of dependency components -- All new fields have defaults for backward compatibility - -### 2. Multi-Path Node Creation (`ast_parser.py:_build_namespaced_components`) - -**Updated signature:** -```python -def _build_namespaced_components( - self, - call_graph_result: Dict, - namespace: str, - namespace_mapping: Dict[str, str], - repo_index: int = 0 # NEW: 0 = main, >0 = deps -) -> Dict[str, Node]: -``` - -**Updated Node creation:** -```python -# Create FQDN (namespaced component ID) -fqdn = f"{namespace}.{original_id}" - -# Create node with FQDN as id and metadata fields -node = Node( - id=fqdn, # FQDN as primary identifier - # ... existing fields ... - component_id=fqdn, - # FQDN metadata fields - short_id=original_id, - namespace=namespace, - is_from_deps=(repo_index > 0) # True for dependencies -) - -# Dictionary key is FQDN -components[fqdn] = node -``` - -**Updated caller:** -```python -# In _parse_multiple_repositories() -for idx, repo_path in enumerate(self.repo_paths): - # ... - repo_components = self._build_namespaced_components( - call_graph_result, - namespace, - namespace_mapping, - repo_index=idx # Pass index: 0=main, >0=deps - ) -``` - -### 3. Single-Path Node Creation (`ast_parser.py:_build_components_from_analysis`) - -**Updated implementation:** -```python -def _build_components_from_analysis(self, call_graph_result: Dict): - # Compute namespace from repo path - namespace = self._get_namespace_from_path(self.repo_path) - - for func_dict in functions: - original_id = func_dict.get("id", "") - - # Construct FQDN: {namespace}.{original_id} - fqdn = f"{namespace}.{original_id}" - - node = Node( - id=fqdn, # FQDN as primary identifier - # ... existing fields ... - component_id=fqdn, - # FQDN metadata fields - short_id=original_id, - namespace=namespace, - is_from_deps=False # Single-path = main repo - ) - - # Dictionary key is FQDN - self.components[fqdn] = node - - # Update mapping for dependency resolution - component_id_mapping[original_id] = fqdn - component_id_mapping[fqdn] = fqdn -``` - -## Key Principles Applied - -1. **`node.id` is ALWAYS FQDN** - Throughout the entire codebase -2. **Dictionary keys use FQDN** - `components[fqdn] = node` -3. **Metadata preserved** - `short_id`, `namespace`, `is_from_deps` for display/filtering -4. **Backward compatibility** - All new fields have defaults -5. **Consistent mapping** - `component_id_mapping` properly handles FQDN conversion - -## Benefits - -1. **No ID Collisions** - FQDNs guarantee unique identifiers across all sources -2. **Source Tracking** - `namespace` and `is_from_deps` enable filtering/grouping -3. **Display Flexibility** - `short_id` provides clean names for UI -4. **Dependency Resolution** - FQDN-based dependencies work across namespaces -5. **Backward Compatible** - Existing code continues to work with defaults - -## Testing Validation - -- ✅ Node model syntax validated -- ✅ ast_parser.py syntax validated -- ✅ All Node creation points updated to use FQDN -- ✅ Dependency resolution uses FQDN mapping -- ✅ Module tracking uses namespaced paths - -## Next Steps - -The FQDN implementation is complete. Future work may include: -1. Update display components to use `short_id` for cleaner UI -2. Add filtering/grouping by `namespace` and `is_from_deps` -3. Enhance cross-namespace dependency visualization -4. Document FQDN format in API documentation diff --git a/MULTI_PATH_CHANGES_SUMMARY.md b/MULTI_PATH_CHANGES_SUMMARY.md deleted file mode 100644 index 3df8cc21..00000000 --- a/MULTI_PATH_CHANGES_SUMMARY.md +++ /dev/null @@ -1,244 +0,0 @@ -# Multi-Path Implementation - Changes Summary - -## Overview -Implementation of multi-path support in DependencyParser and RepoAnalyzer, enabling analysis of multiple source directories with proper component namespacing and cross-repository dependency resolution. - -## Files Modified - -### 1. `/codewiki/src/be/dependency_analyzer/ast_parser.py` -**Lines**: 162 → 373 (+211 lines, +130% growth) - -#### Changes: -- **Constructor (`__init__`)**: Now accepts `Union[str, List[str]]` for repo_path -- **New attribute**: `self.repo_paths: List[str]` - stores all repository paths -- **Maintained**: `self.repo_path` for backward compatibility - -#### New Methods (6): -1. `_parse_single_repository(repo_path, filtered_folders)` - Single path parsing -2. `_parse_multiple_repositories(filtered_folders)` - Multi-path orchestration -3. `_get_namespace_from_path(repo_path)` - Namespace generation -4. `_build_namespaced_components(call_graph_result, namespace, namespace_mapping)` - Component namespacing -5. `_resolve_cross_namespace_dependencies(all_components, namespace_mapping)` - Cross-repo dependency resolution - -#### Modified Methods (1): -1. `parse_repository(filtered_folders)` - Now dispatches to single or multi-path logic - -### 2. `/codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py` -**Lines**: 129 → 213 (+84 lines, +65% growth) - -#### Changes: -- **Constructor (`__init__`)**: Added `self._namespace_roots: Dict[str, str]` tracking -- **Updated**: `analyze_repository_structure()` now accepts `Union[str, List[str]]` - -#### New Methods (2): -1. `_analyze_multiple_repositories(repo_dirs)` - Multi-path analysis and merging -2. `_get_namespace_from_path(repo_path)` - Namespace generation (consistent with DependencyParser) - -#### Modified Methods (1): -1. `analyze_repository_structure(repo_dir)` - Handles both single and multi-path modes - -## Documentation Created - -### 1. `MULTI_PATH_IMPLEMENTATION.md` (423 lines) -Complete technical documentation covering: -- Architecture and design decisions -- Component namespacing format -- Dependency resolution strategies -- API changes and backward compatibility -- Usage examples and edge cases -- Performance characteristics -- Testing guidelines -- Migration guide - -### 2. `MULTI_PATH_QUICK_REFERENCE.md` (334 lines) -Quick reference guide with: -- Basic usage patterns -- Component ID format table -- Key methods reference -- Common patterns and recipes -- File pattern examples -- Error handling -- Performance tips -- Troubleshooting - -### 3. `examples/multi_path_example.py` (283 lines) -Working examples demonstrating: -- Single path usage (backward compatible) -- Multiple path usage with namespacing -- RepoAnalyzer multi-path support -- Component detail access -- Cross-namespace dependency analysis -- Saving multi-path analysis to JSON - -## Key Features Implemented - -### ✅ Multi-Path Support -- Parse multiple repositories in a single operation -- List of paths: `["/repo1", "/repo2", "/repo3"]` - -### ✅ Component Namespacing -- Format: `{namespace}.{original_component_id}` -- Namespace derived from directory name -- Example: `ui-kit.components.Button.render` - -### ✅ Dependency Resolution -1. **Intra-namespace**: Direct ID matching within same repository -2. **Cross-namespace**: Name-based matching across repositories -3. **Fallback**: Preserve unresolved IDs for debugging - -### ✅ Backward Compatibility -- Single string path works exactly as before -- `self.repo_path` maintained for legacy code -- No breaking changes to existing functionality - -### ✅ Merged File Trees (RepoAnalyzer) -- Multiple repos merged into single tree structure -- Each repo wrapped with namespace prefix -- Combined summary statistics - -### ✅ Type Safety -- Type hints: `Union[str, List[str]]` -- All methods properly typed -- Compatible with mypy - -## Architecture Decisions - -| Decision | Rationale | -|----------|-----------| -| Namespace from directory name | Simple, predictable, human-readable | -| Backward compatibility preserved | No disruption to existing code | -| Cross-namespace resolution by name | Flexible, handles most real-world cases | -| No custom namespace mapping (yet) | Keep initial implementation simple | -| Maintain `self.repo_path` | Preserve legacy code compatibility | - -## Testing - -### Syntax Validation: ✅ Passed -```bash -python3 -c "compile('ast_parser.py')" # ✅ Valid -python3 -c "compile('repo_analyzer.py')" # ✅ Valid -``` - -### Method Verification: ✅ All Present -- DependencyParser: 9/9 methods ✓ -- RepoAnalyzer: 7/7 methods ✓ - -## Usage Examples - -### Before (Single Path) -```python -parser = DependencyParser("/path/to/repo") -components = parser.parse_repository() -# Component IDs: "src.module.Component" -``` - -### After (Multi-Path) -```python -parser = DependencyParser([ - "/path/to/openframe-frontend", - "/path/to/ui-kit" -]) -components = parser.parse_repository() -# Component IDs: "openframe-frontend.src.module.Component" -# "ui-kit.components.Button" -``` - -## Impact Analysis - -### Backward Compatibility: ✅ 100% -- All existing single-path code works unchanged -- No API breaking changes -- Deprecated: None -- Removed: None - -### Performance Impact -- Single path: No performance change -- Multi-path: O(n * m) where n = repos, m = avg components per repo -- Memory: Linear growth with number of repositories - -### Type Safety: ✅ Enhanced -- Added `Union[str, List[str]]` types -- Maintained type consistency -- No type regressions - -## Migration Path - -### Phase 1: No Changes Required (Current State) -Existing code continues to work: -```python -parser = DependencyParser("/single/path") -``` - -### Phase 2: Opt-In Multi-Path (When Ready) -Update to multi-path when needed: -```python -parser = DependencyParser(["/path1", "/path2"]) -``` - -### Phase 3: Update Consumers (Optional) -Update code that processes component IDs: -```python -# Old: Assumes non-namespaced IDs -comp_id.split('.') # ["src", "module", "Component"] - -# New: Handle namespaced IDs -parts = comp_id.split('.') -namespace = parts[0] # "ui-kit" -module = '.'.join(parts[1:-1]) # "src.module" -name = parts[-1] # "Component" -``` - -## Future Enhancements - -### Potential Additions -1. Custom namespace mapping (override directory name) -2. Component visibility controls (public/private) -3. Namespace aliases (shorter IDs) -4. More sophisticated cross-namespace resolution -5. Performance optimizations for large multi-repo setups -6. Namespace-specific include/exclude patterns - -### Not Implemented (By Design) -- Breaking changes to single-path mode -- Required migration for existing code -- Complex configuration files -- Automatic namespace inference from imports - -## Validation Checklist - -- [x] Syntax validation passed -- [x] All methods implemented -- [x] Type hints added -- [x] Backward compatibility maintained -- [x] Documentation complete -- [x] Examples provided -- [x] No breaking changes -- [x] Code compiles successfully -- [x] Method signatures correct -- [x] Error handling preserved - -## Implementation Statistics - -| Metric | Value | -|--------|-------| -| Files modified | 2 | -| Lines added | +295 | -| New methods | 8 | -| Modified methods | 2 | -| Documentation pages | 3 | -| Code examples | 5 | -| Test coverage | Manual validation | -| Breaking changes | 0 | - -## Summary - -Successfully implemented multi-path support with: -- ✅ Zero breaking changes -- ✅ Complete backward compatibility -- ✅ Comprehensive documentation -- ✅ Working examples -- ✅ Type safety -- ✅ Proper namespacing -- ✅ Cross-repository dependency resolution - -The implementation is production-ready and can be merged without impacting existing functionality. diff --git a/MULTI_PATH_IMPLEMENTATION.md b/MULTI_PATH_IMPLEMENTATION.md deleted file mode 100644 index 5c2201a7..00000000 --- a/MULTI_PATH_IMPLEMENTATION.md +++ /dev/null @@ -1,367 +0,0 @@ -# Multi-Path Support Implementation - -## Overview - -This document describes the implementation of multi-path support in `DependencyParser` and `RepoAnalyzer`, allowing the analysis of multiple source directories with proper component namespacing and cross-repository dependency resolution. - -## Architecture - -### Component Namespacing - -When parsing multiple repositories, each component is namespaced to prevent ID collisions: - -``` -Format: {namespace}.{original_component_id} - -Example: - Single path: "src.components.Button.render" - Multi-path: "openframe-frontend.src.components.Button.render" -``` - -The namespace is derived from the repository directory name (e.g., `openframe-frontend`, `ui-kit`). - -### Dependency Resolution - -Dependencies are resolved in three stages: - -1. **Intra-namespace**: Dependencies within the same repository use namespaced IDs -2. **Cross-namespace**: Dependencies between repositories are resolved by component name matching -3. **Fallback**: Unresolved dependencies retain their original ID for debugging - -## DependencyParser Changes - -### Constructor (`__init__`) - -**Before:** -```python -def __init__(self, repo_path: str, include_patterns: List[str] = None, exclude_patterns: List[str] = None): - self.repo_path = os.path.abspath(repo_path) -``` - -**After:** -```python -def __init__(self, repo_path: Union[str, List[str]], include_patterns: List[str] = None, exclude_patterns: List[str] = None): - if isinstance(repo_path, str): - self.repo_paths = [os.path.abspath(repo_path)] - self.repo_path = self.repo_paths[0] # Backward compatibility - else: - self.repo_paths = [os.path.abspath(p) for p in repo_path] - self.repo_path = self.repo_paths[0] -``` - -**Backward Compatibility:** -- Single string path: Works exactly as before -- List of paths: New multi-path functionality -- `self.repo_path` maintained for legacy code - -### New Methods - -#### `_parse_single_repository(repo_path, filtered_folders)` -- Extracts original `parse_repository` logic -- Maintains backward compatibility -- No changes to component IDs - -#### `_parse_multiple_repositories(filtered_folders)` -- Orchestrates parsing of multiple repositories -- Creates namespace for each repository -- Merges components with namespaced IDs -- Resolves cross-namespace dependencies - -#### `_get_namespace_from_path(repo_path)` -- Generates namespace from directory name -- Example: `/path/to/ui-kit` → `"ui-kit"` - -#### `_build_namespaced_components(call_graph_result, namespace, namespace_mapping)` -- Builds components with namespace prefix -- Maintains mapping of original → namespaced IDs -- Tracks modules with namespaced paths - -#### `_resolve_cross_namespace_dependencies(all_components, namespace_mapping)` -- Resolves dependencies across repository boundaries -- Uses name matching for cross-namespace resolution -- Preserves unresolved dependencies for debugging - -### Updated Method - -#### `parse_repository(filtered_folders)` -- Now dispatches to single or multi-path parsing -- Returns unified component dictionary - -## RepoAnalyzer Changes - -### Constructor (`__init__`) - -**Added:** -```python -self._namespace_roots: Dict[str, str] = {} # namespace -> repo_path -``` - -Tracks which namespace corresponds to which repository path. - -### Updated Methods - -#### `analyze_repository_structure(repo_dir)` - -**Before:** -```python -def analyze_repository_structure(self, repo_dir: str) -> Dict: - file_tree = self._build_file_tree(repo_dir) - return { - "file_tree": file_tree, - "summary": {...} - } -``` - -**After:** -```python -def analyze_repository_structure(self, repo_dir: Union[str, List[str]]) -> Dict: - if isinstance(repo_dir, str): - # Single path - backward compatible - return self._analyze_single_repository(repo_dir) - else: - # Multiple paths - return self._analyze_multiple_repositories(repo_dir) -``` - -### New Methods - -#### `_analyze_multiple_repositories(repo_dirs)` -- Analyzes each repository separately -- Wraps each tree with namespace prefix -- Merges trees into single structure -- Combines summary statistics - -#### `_get_namespace_from_path(repo_path)` -- Same as DependencyParser implementation -- Ensures consistent namespacing - -## Usage Examples - -### Single Path (Backward Compatible) - -```python -from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser - -# Old code continues to work -parser = DependencyParser("/path/to/repo") -components = parser.parse_repository() - -# Component IDs unchanged -# "src.utils.helpers.formatDate" -``` - -### Multiple Paths - -```python -from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser - -# New multi-path support -paths = [ - "/path/to/openframe-frontend", - "/path/to/ui-kit", - "/path/to/shared-lib" -] - -parser = DependencyParser(paths, include_patterns=["*.py", "*.ts"]) -components = parser.parse_repository() - -# Component IDs are namespaced -# "openframe-frontend.src.utils.helpers.formatDate" -# "ui-kit.components.Button.render" -# "shared-lib.utils.api.fetchData" -``` - -### With RepoAnalyzer - -```python -from codewiki.src.be.dependency_analyzer.analysis.repo_analyzer import RepoAnalyzer - -analyzer = RepoAnalyzer( - include_patterns=["*.py", "*.ts"], - exclude_patterns=["*test*", "*spec*"] -) - -# Single path -result = analyzer.analyze_repository_structure("/path/to/repo") - -# Multiple paths -result = analyzer.analyze_repository_structure([ - "/path/to/repo1", - "/path/to/repo2" -]) - -# File tree structure -{ - "file_tree": { - "type": "directory", - "name": "multi-repo", - "children": [ - { - "type": "directory", - "name": "repo1", - "_namespace": "repo1", - "children": [...] - }, - { - "type": "directory", - "name": "repo2", - "_namespace": "repo2", - "children": [...] - } - ] - }, - "summary": { - "total_files": 150, - "total_size_kb": 2048.5, - "repositories": 2, - "namespaces": ["repo1", "repo2"] - } -} -``` - -## Component ID Format - -### Single Path Mode -``` -{module_path}.{component_name} - -Examples: -- src.components.Button.render -- lib.utils.helpers.formatDate -- services.api.UserService.getUser -``` - -### Multi-Path Mode -``` -{namespace}.{module_path}.{component_name} - -Examples: -- openframe-frontend.src.components.Button.render -- ui-kit.lib.utils.helpers.formatDate -- shared-lib.services.api.UserService.getUser -``` - -## Cross-Namespace Dependencies - -When a component in one namespace depends on a component in another: - -```python -# Component in openframe-frontend -{ - "id": "openframe-frontend.src.App.main", - "depends_on": [ - "openframe-frontend.src.utils.logger", # Same namespace - "ui-kit.components.Button" # Cross-namespace - ] -} -``` - -Resolution process: -1. Check if dependency ID exists in `all_components` -2. If not found, extract component name and search by name across namespaces -3. If still not found, keep original ID for debugging - -## Data Structures - -### DependencyParser - -```python -class DependencyParser: - repo_paths: List[str] # All repository paths - repo_path: str # First path (backward compat) - components: Dict[str, Node] # All components (namespaced in multi-path) - modules: Set[str] # All module paths (namespaced in multi-path) - include_patterns: List[str] # File include patterns - exclude_patterns: List[str] # File exclude patterns - analysis_service: AnalysisService -``` - -### RepoAnalyzer - -```python -class RepoAnalyzer: - include_patterns: List[str] - exclude_patterns: List[str] - _namespace_roots: Dict[str, str] # Maps namespace to repo_path -``` - -## Edge Cases Handled - -1. **Empty repositories**: Skipped in multi-path mode -2. **Duplicate namespaces**: Last one wins (deterministic) -3. **Circular dependencies**: Preserved as-is, no cycle detection -4. **Missing dependencies**: Kept with original ID for debugging -5. **Relative paths**: Maintained relative to original repository - -## Performance Characteristics - -- **Time Complexity**: O(n * m) where n = number of repos, m = avg components per repo -- **Space Complexity**: O(n * m) for component storage -- **Namespace Mapping**: O(k) where k = total unique component IDs - -## Testing - -### Test Single Path Compatibility -```python -parser = DependencyParser("/path/to/repo") -assert len(parser.repo_paths) == 1 -assert parser.repo_path == parser.repo_paths[0] -``` - -### Test Multiple Paths -```python -paths = ["/repo1", "/repo2"] -parser = DependencyParser(paths) -assert len(parser.repo_paths) == 2 -components = parser.parse_repository() -# Check namespaced IDs -assert any("repo1." in comp_id for comp_id in components.keys()) -assert any("repo2." in comp_id for comp_id in components.keys()) -``` - -### Test Namespace Generation -```python -parser = DependencyParser(["/path/to/openframe-frontend"]) -namespace = parser._get_namespace_from_path("/path/to/openframe-frontend") -assert namespace == "openframe-frontend" -``` - -## Migration Guide - -### Existing Code -```python -# No changes needed - works as before -parser = DependencyParser(repo_path) -components = parser.parse_repository() -``` - -### New Multi-Path Code -```python -# Update to use list of paths -parser = DependencyParser([repo_path1, repo_path2]) -components = parser.parse_repository() - -# Component IDs now include namespace prefix -for comp_id, component in components.items(): - print(f"Component: {comp_id}") # e.g., "ui-kit.components.Button" -``` - -## Implementation Files - -- `/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/ast_parser.py` (355 lines) -- `/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py` (178 lines) - -## Key Decisions - -1. **Namespace from directory name**: Simple, predictable, human-readable -2. **Backward compatibility preserved**: Single string path works unchanged -3. **Cross-namespace resolution by name**: Flexible but may match wrong component in ambiguous cases -4. **No breaking changes**: All existing code continues to work - -## Future Enhancements - -1. Custom namespace mapping (instead of directory name) -2. More sophisticated cross-namespace dependency resolution -3. Namespace aliases for shorter IDs -4. Component visibility controls (public/private across namespaces) -5. Performance optimization for very large multi-repo setups diff --git a/MULTI_PATH_LOGGING_EXAMPLES.md b/MULTI_PATH_LOGGING_EXAMPLES.md deleted file mode 100644 index b7bd9ea5..00000000 --- a/MULTI_PATH_LOGGING_EXAMPLES.md +++ /dev/null @@ -1,451 +0,0 @@ -# Multi-Path Implementation - Example Log Outputs - -## Example 1: OpenFrame Frontend + UI-Kit (2 Paths) - -### Command -```bash -codewiki generate \ - --repo-path=/Users/michaelassraf/Documents/GitHub/openframe-oss-tenant/openframe/services/openframe-frontend/multi-platform-hub \ - --output-dir=output \ - --additional-paths=ui-kit \ - --verbose -``` - -### Expected Log Output - -``` -🔍 CLI received 1 additional path(s): - ├─ Path #1: ui-kit - │ └─ Normalized: /Users/michaelassraf/Documents/GitHub/openframe-oss-tenant/openframe/services/openframe-frontend/multi-platform-hub/ui-kit - └─ Normalized 1 additional path(s) - -📋 Generation Configuration: - ├─ Cluster Model: claude-sonnet-4 - ├─ Main Model: claude-sonnet-4 - ├─ Fallback Model: gpt-4o-mini - ├─ Module Settings: - │ ├─ Max tokens/module: 36369 - │ ├─ Max tokens/leaf: 16000 - │ └─ Max depth: 2 - ├─ Additional Paths (1): - │ └─ /Users/michaelassraf/Documents/GitHub/openframe-oss-tenant/openframe/services/openframe-frontend/multi-platform-hub/ui-kit - └─ Agent Instructions: Configured - -🔍 Validating source paths... - ├─ ✓ Primary path valid: /Users/.../multi-platform-hub - ├─ Validating 1 additional path(s)... - │ ├─ ✓ Additional path #1 valid: /Users/.../ui-kit - └─ ✓ All 1 additional path(s) validated - -===== Stage 1/5: Dependency Analysis ===== - -🔍 Multi-path mode enabled: analyzing 2 source paths - ├─ Primary: /Users/.../multi-platform-hub - └─ Additional #1: /Users/.../ui-kit - -🔍 Analyzing 2 repository paths... - ├─ [1/2] Building file tree for: /Users/.../multi-platform-hub - │ └─ Namespace: 'multi-platform-hub' - │ └─ Found 487 files (8542.35 KB) - ├─ [2/2] Building file tree for: /Users/.../ui-kit - │ └─ Namespace: 'ui-kit' - │ └─ Found 328 files (5123.89 KB) - └─ ✓ Merged 2 repositories: - ├─ Total files: 815 - ├─ Total size: 13666.24 KB - └─ Namespaces: multi-platform-hub, ui-kit - -🔍 Parsing repository files... - -🔍 Multi-path mode detected: analyzing 2 source directories - ├─ Primary: /Users/.../multi-platform-hub - └─ Additional #1: /Users/.../ui-kit - -📂 Analyzing path 1/2: /Users/.../multi-platform-hub - └─ Namespace: 'multi-platform-hub' -📊 Namespace 'multi-platform-hub': found 1243 components - └─ Component types: - • function: 789 - • class: 234 - • interface: 156 - • method: 64 - -📂 Analyzing path 2/2: /Users/.../ui-kit - └─ Namespace: 'ui-kit' -📊 Namespace 'ui-kit': found 856 components - └─ Component types: - • function: 512 - • class: 198 - • interface: 123 - • method: 23 - -🔗 Resolving cross-namespace dependencies... - ├─ Cross-namespace dependency: multi-platform-hub.app.components.Button → ui-kit.components.ui.Button - ├─ Cross-namespace dependency: multi-platform-hub.app.hooks.useToast → ui-kit.hooks.useToast - ├─ Cross-namespace dependency: multi-platform-hub.lib.utils.cn → ui-kit.utils.cn - ... (12 more) - └─ ✓ Resolved 15 cross-namespace dependencies - -📊 Multi-path analysis complete: - ├─ Total components: 2099 - ├─ Total modules: 45 - └─ Namespaces: 2 - - └─ Parsed 2099 components total - └─ Component types found: - • function: 1301 - • class: 432 - • interface: 279 - • method: 87 - - └─ Saved dependency graph to: output/temp/dependency_graphs/multi-platform-hub_dependency_graph.json - -🌿 Filtering leaf nodes (total: 2099)... - └─ Valid types for this codebase: class, interface - -📊 Leaf node filtering complete: - ├─ Kept: 711 nodes - ├─ Skipped (invalid identifier): 0 - ├─ Skipped (wrong type): 1301 - └─ Skipped (not found): 87 - - ├─ Total components: 2099 - ├─ Leaf nodes: 711 - └─ Files analyzed: 815 - -✓ Stage 1/5 complete (45.2s) - -===== Stage 2/5: Module Clustering ===== - - ├─ Leaf nodes to cluster: 711 - ├─ Using model: claude-sonnet-4 - └─ Calling LLM for clustering... - - ├─ Total modules: 18 - └─ Module names: auth, dashboard, devices, logs, admin ... (13 more) - -✓ Stage 2/5 complete (23.7s) -``` - ---- - -## Example 2: Single Path (Backward Compatibility) - -### Command -```bash -codewiki generate \ - --repo-path=/Users/michaelassraf/Documents/GitHub/CodeWiki \ - --output-dir=output \ - --verbose -``` - -### Expected Log Output - -``` -📋 Generation Configuration: - ├─ Cluster Model: claude-sonnet-4 - ├─ Main Model: claude-sonnet-4 - ├─ Fallback Model: gpt-4o-mini - ├─ Module Settings: - │ ├─ Max tokens/module: 36369 - │ ├─ Max tokens/leaf: 16000 - │ └─ Max depth: 2 - -🔍 Validating source paths... - ├─ ✓ Primary path valid: /Users/.../CodeWiki - └─ No additional paths to validate (single-path mode) - -===== Stage 1/5: Dependency Analysis ===== - -📁 Single-path mode: analyzing /Users/.../CodeWiki - - ├─ Repository: /Users/.../CodeWiki - └─ Output: output/temp - -🔍 Parsing repository files... - └─ Parsed 234 components total - └─ Component types found: - • function: 145 - • class: 67 - • method: 22 - - └─ Saved dependency graph to: output/temp/dependency_graphs/CodeWiki_dependency_graph.json - -🌿 Filtering leaf nodes (total: 234)... - └─ Valid types for this codebase: class, function - -📊 Leaf node filtering complete: - ├─ Kept: 212 nodes - ├─ Skipped (invalid identifier): 0 - ├─ Skipped (wrong type): 22 - └─ Skipped (not found): 0 - - ├─ Total components: 234 - ├─ Leaf nodes: 212 - └─ Files analyzed: 156 - -✓ Stage 1/5 complete (12.3s) -``` - ---- - -## Example 3: Three Paths (Complex Multi-Repo) - -### Command -```bash -codewiki generate \ - --repo-path=/app/backend \ - --output-dir=output \ - --additional-paths=../shared-lib,../ui-components \ - --verbose -``` - -### Expected Log Output - -``` -🔍 CLI received 2 additional path(s): - ├─ Path #1: ../shared-lib - │ └─ Normalized: /app/shared-lib - ├─ Path #2: ../ui-components - │ └─ Normalized: /app/ui-components - └─ Normalized 2 additional path(s) - -🔍 Validating source paths... - ├─ ✓ Primary path valid: /app/backend - ├─ Validating 2 additional path(s)... - │ ├─ ✓ Additional path #1 valid: /app/shared-lib - │ ├─ ✓ Additional path #2 valid: /app/ui-components - └─ ✓ All 2 additional path(s) validated - -🔍 Multi-path mode enabled: analyzing 3 source paths - ├─ Primary: /app/backend - ├─ Additional #1: /app/shared-lib - └─ Additional #2: /app/ui-components - -🔍 Analyzing 3 repository paths... - ├─ [1/3] Building file tree for: /app/backend - │ └─ Namespace: 'backend' - │ └─ Found 312 files (5623.12 KB) - ├─ [2/3] Building file tree for: /app/shared-lib - │ └─ Namespace: 'shared-lib' - │ └─ Found 89 files (1234.56 KB) - ├─ [3/3] Building file tree for: /app/ui-components - │ └─ Namespace: 'ui-components' - │ └─ Found 156 files (2345.67 KB) - └─ ✓ Merged 3 repositories: - ├─ Total files: 557 - ├─ Total size: 9203.35 KB - └─ Namespaces: backend, shared-lib, ui-components - -🔍 Multi-path mode detected: analyzing 3 source directories - ├─ Primary: /app/backend - ├─ Additional #1: /app/shared-lib - └─ Additional #2: /app/ui-components - -📂 Analyzing path 1/3: /app/backend - └─ Namespace: 'backend' -📊 Namespace 'backend': found 456 components - └─ Component types: - • class: 234 - • function: 145 - • interface: 67 - • method: 10 - -📂 Analyzing path 2/3: /app/shared-lib - └─ Namespace: 'shared-lib' -📊 Namespace 'shared-lib': found 123 components - └─ Component types: - • function: 78 - • class: 34 - • interface: 11 - -📂 Analyzing path 3/3: /app/ui-components - └─ Namespace: 'ui-components' -📊 Namespace 'ui-components': found 234 components - └─ Component types: - • function: 156 - • class: 67 - • interface: 11 - -🔗 Resolving cross-namespace dependencies... - ├─ Cross-namespace dependency: backend.api.UserController → shared-lib.models.User - ├─ Cross-namespace dependency: backend.api.AuthController → shared-lib.utils.jwt - ├─ Cross-namespace dependency: ui-components.Button → shared-lib.themes.colors - ├─ Cross-namespace dependency: ui-components.Modal → shared-lib.utils.portal - ... (18 more) - └─ ✓ Resolved 22 cross-namespace dependencies - -📊 Multi-path analysis complete: - ├─ Total components: 813 - ├─ Total modules: 28 - └─ Namespaces: 3 -``` - ---- - -## Example 4: Error Case - Invalid Path - -### Command -```bash -codewiki generate \ - --repo-path=/app/backend \ - --output-dir=output \ - --additional-paths=../nonexistent \ - --verbose -``` - -### Expected Log Output - -``` -🔍 CLI received 1 additional path(s): - ├─ Path #1: ../nonexistent - │ └─ Normalized: /app/nonexistent - └─ Normalized 1 additional path(s) - -🔍 Validating source paths... - ├─ ✓ Primary path valid: /app/backend - ├─ Validating 1 additional path(s)... - │ └─ Checking additional path #1: /app/nonexistent - -ERROR: Additional source path #1 does not exist: /app/nonexistent - -Validation failed. Please check your path configuration. -``` - ---- - -## Example 5: Warning - No Files Found - -### Command -```bash -codewiki generate \ - --repo-path=/app/backend \ - --output-dir=output \ - --additional-paths=../empty-dir \ - --include-patterns="*.py" \ - --verbose -``` - -### Expected Log Output - -``` -🔍 Analyzing 2 repository paths... - ├─ [1/2] Building file tree for: /app/backend - │ └─ Namespace: 'backend' - │ └─ Found 312 files (5623.12 KB) - ├─ [2/2] Building file tree for: /app/empty-dir - │ └─ Namespace: 'empty-dir' - │ └─ ⚠️ No files found matching patterns - └─ ✓ Merged 2 repositories: - ├─ Total files: 312 - ├─ Total size: 5623.12 KB - └─ Namespaces: backend, empty-dir - -⚠️ WARNING: Namespace 'empty-dir' has no components -``` - ---- - -## Debugging Tips - -### Enable Verbose Logging -```bash -# Show all DEBUG level logs -codewiki generate --repo-path=. --output-dir=output --verbose - -# Show all logs including component details -export CODEWIKI_LOG_LEVEL=DEBUG -codewiki generate --repo-path=. --output-dir=output -``` - -### Filter Logs by Layer -```bash -# Show only multi-path related logs -codewiki generate ... 2>&1 | grep "🔍\|📂\|📊\|🔗" - -# Show only errors and warnings -codewiki generate ... 2>&1 | grep "ERROR\|WARNING\|⚠️" - -# Show only namespace information -codewiki generate ... 2>&1 | grep "Namespace" -``` - -### Capture Logs to File -```bash -# Full log capture -codewiki generate --repo-path=. --output-dir=output --verbose 2>&1 | tee codewiki.log - -# Time-stamped logs -codewiki generate ... 2>&1 | ts '[%Y-%m-%d %H:%M:%S]' | tee codewiki.log -``` - ---- - -## Key Log Patterns to Look For - -### Success Pattern -``` -✓ Primary path valid -✓ All X additional path(s) validated -✓ Merged X repositories -✓ Resolved X cross-namespace dependencies -📊 Multi-path analysis complete -``` - -### Progress Pattern -``` -[1/3] Building file tree -[2/3] Building file tree -[3/3] Building file tree -📂 Analyzing path 1/3 -📂 Analyzing path 2/3 -📂 Analyzing path 3/3 -``` - -### Error Pattern -``` -ERROR: Additional source path #X does not exist -ERROR: Additional source path #X is not a directory -ERROR: Additional source path #X is not readable -``` - -### Warning Pattern -``` -⚠️ No files found matching patterns -⚠️ Namespace 'X' has no components -⚠️ Leaf node 'X' not found in components -``` - ---- - -## Performance Monitoring - -### Time Tracking -Each log line includes implicit timing through stage progress: -``` -===== Stage 1/5: Dependency Analysis ===== -... (operations) -✓ Stage 1/5 complete (45.2s) - -===== Stage 2/5: Module Clustering ===== -... (operations) -✓ Stage 2/5 complete (23.7s) -``` - -### Component Counting -Track component growth across namespaces: -``` -📊 Namespace 'primary': found 1243 components -📊 Namespace 'ui-kit': found 856 components -📊 Multi-path analysis complete: - ├─ Total components: 2099 ← Sum of all namespaces -``` - -### Memory Usage -Monitor file counts and sizes: -``` - │ └─ Found 487 files (8542.35 KB) ← Per namespace - └─ ✓ Merged 2 repositories: - ├─ Total files: 815 ← Combined - ├─ Total size: 13666.24 KB ← Combined -``` diff --git a/MULTI_PATH_LOGGING_SUMMARY.md b/MULTI_PATH_LOGGING_SUMMARY.md deleted file mode 100644 index a26a329a..00000000 --- a/MULTI_PATH_LOGGING_SUMMARY.md +++ /dev/null @@ -1,417 +0,0 @@ -# Multi-Path Implementation - Comprehensive Logging Summary - -## Overview -Added extensive logging throughout the multi-path implementation to provide complete visibility into the multi-source analysis pipeline. - -## Logging Locations by Layer - -### 1. Config Layer (`codewiki/src/config.py`) - -#### `all_source_paths` Property (Lines 132-145) -**Added Logging:** -```python -logger.debug(f"🔍 all_source_paths property accessed:") -logger.debug(f" ├─ Primary: {paths[0]}") -for i, path in enumerate(paths[1:], 1): - logger.debug(f" └─ Additional #{i}: {path}") -# OR (single-path mode) -logger.debug(f"🔍 all_source_paths property accessed (single-path mode): {paths[0]}") -``` - -**What It Shows:** -- When the property is accessed -- Primary repository path -- All additional paths with numbering -- Whether in single-path or multi-path mode - ---- - -#### `validate_source_paths()` Method (Lines 146-177) -**Added Logging:** -```python -logger.info("🔍 Validating source paths...") -logger.debug(f" ├─ Checking primary path: {self.repo_path}") -logger.info(f" ├─ ✓ Primary path valid: {self.repo_path}") - -logger.info(f" ├─ Validating {len(self.additional_source_paths)} additional path(s)...") -logger.debug(f" │ └─ Checking additional path #{i}: {path}") -logger.info(f" │ ├─ ✓ Additional path #{i} valid: {path}") -logger.info(f" └─ ✓ All {len(self.additional_source_paths)} additional path(s) validated") -# OR (single-path mode) -logger.debug(f" └─ No additional paths to validate (single-path mode)") -``` - -**What It Shows:** -- Validation start -- Each path being checked -- Validation success for each path -- Total count of validated paths -- Errors if paths don't exist or aren't accessible - ---- - -#### `is_multi_path_mode()` Method (Lines 172-184) -**Added Logging:** -```python -logger.debug(f"🔍 Multi-path mode: ENABLED ({len(self.additional_source_paths)} additional paths)") -# OR -logger.debug("🔍 Multi-path mode: DISABLED (single-path)") -``` - -**What It Shows:** -- Whether multi-path mode is enabled -- Count of additional paths when enabled - ---- - -### 2. Parser Layer (`codewiki/src/be/dependency_analyzer/ast_parser.py`) - -#### `_parse_multiple_repositories()` Method (Lines 89-146) -**Added Logging:** -```python -logger.info(f"🔍 Multi-path mode detected: analyzing {len(self.repo_paths)} source directories") -logger.info(f" ├─ Primary: {self.repo_paths[0]}") -for i, path in enumerate(self.repo_paths[1:], 1): - logger.info(f" └─ Additional #{i}: {path}") - -# For each repository: -logger.info(f"\n📂 Analyzing path {idx}/{len(self.repo_paths)}: {repo_path}") -logger.info(f" └─ Namespace: '{namespace}'") - -logger.info(f"📊 Namespace '{namespace}': found {len(repo_components)} components") -logger.info(f" └─ Component types:") -for comp_type, count in sorted(type_counts.items(), key=lambda x: x[1], reverse=True): - logger.info(f" • {comp_type}: {count}") - -logger.info(f"\n🔗 Resolving cross-namespace dependencies...") -logger.info(f" └─ ✓ Resolved {cross_deps_count} cross-namespace dependencies") -# OR -logger.info(f" └─ No cross-namespace dependencies found") - -logger.info(f"\n📊 Multi-path analysis complete:") -logger.info(f" ├─ Total components: {len(all_components)}") -logger.info(f" ├─ Total modules: {len(self.modules)}") -logger.info(f" └─ Namespaces: {len(self.repo_paths)}") -``` - -**What It Shows:** -- Multi-path detection -- List of all paths being analyzed -- Current path being processed with progress (1/3, 2/3, etc.) -- Namespace assignment for each path -- Component count per namespace -- Component type breakdown per namespace -- Cross-namespace dependency resolution -- Final summary statistics - ---- - -#### `_resolve_cross_namespace_dependencies()` Method (Lines 239-273) -**Added Logging:** -```python -logger.debug(f" ├─ Cross-namespace dependency: {component_id} → {other_id}") -``` - -**What It Shows:** -- Each cross-namespace dependency discovered -- Source and target component IDs with namespaces -- Returns total count of cross-namespace dependencies - ---- - -### 3. Analyzer Layer (`codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py`) - -#### `_analyze_multiple_repositories()` Method (Lines 60-114) -**Added Logging:** -```python -logger.info(f"🔍 Analyzing {len(repo_dirs)} repository paths...") - -# For each repository: -logger.info(f" ├─ [{idx}/{len(repo_dirs)}] Building file tree for: {repo_dir}") -logger.info(f" │ └─ Namespace: '{namespace}'") - -logger.info(f" │ └─ Found {files} files ({size:.2f} KB)") -# OR (if no files found) -logger.warning(f" │ └─ ⚠️ No files found matching patterns") - -logger.info(f" └─ ✓ Merged {len(repo_dirs)} repositories:") -logger.info(f" ├─ Total files: {total_files}") -logger.info(f" ├─ Total size: {total_size_kb:.2f} KB") -logger.info(f" └─ Namespaces: {', '.join(self._namespace_roots.keys())}") -``` - -**What It Shows:** -- Total number of repository paths -- Current path being analyzed with progress -- Namespace generation for each path -- File count and size for each path -- Warning if no files found -- Final merged statistics - ---- - -### 4. CLI Layer (`codewiki/cli/adapters/doc_generator.py`) - -#### `generate()` Method (Lines 140-187) -**Added Logging:** -```python -logger.info(f"🔍 CLI received {len(additional_paths_raw)} additional path(s):") - -for i, path in enumerate(additional_paths_raw, 1): - normalized = str((Path(self.repo_path) / path).resolve()) - logger.info(f" ├─ Path #{i}: {path}") - logger.info(f" │ └─ Normalized: {normalized}") - -logger.info(f" └─ Normalized {len(additional_paths_normalized)} additional path(s)") -``` - -**What It Shows:** -- Receipt of additional paths from CLI -- Original relative path from config -- Normalized absolute path -- Path count before and after normalization - ---- - -### 5. Graph Builder Layer (`codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py`) - -#### `build_dependency_graph()` Method (Lines 44-52) -**Added Logging:** -```python -logger.info(f"🔍 Multi-path mode enabled: analyzing {len(repo_paths)} source paths") -logger.info(f" ├─ Primary: {repo_paths[0]}") -for i, path in enumerate(repo_paths[1:], 1): - logger.info(f" └─ Additional #{i}: {path}") -# OR (single-path mode) -logger.info(f"📁 Single-path mode: analyzing {repo_paths[0]}") -``` - -**What It Shows:** -- Mode detection (multi-path vs single-path) -- All source paths being analyzed -- Primary vs additional path distinction - ---- - -## Complete Logging Flow - -### Startup Sequence -``` -1. CLI receives additional_paths from config - └─ Logs raw paths and normalized paths - -2. BackendConfig.from_cli() creates config - └─ Sets additional_source_paths - -3. Config.validate_source_paths() validates all paths - └─ Logs validation for primary + additional paths - -4. Config.is_multi_path_mode() checks mode - └─ Logs mode detection - -5. Config.all_source_paths property accessed - └─ Logs all paths being analyzed -``` - -### Analysis Sequence -``` -6. DependencyGraphBuilder.build_dependency_graph() - └─ Logs mode and all source paths - -7. RepoAnalyzer._analyze_multiple_repositories() - └─ Logs file tree building per path - └─ Logs file counts and sizes per namespace - └─ Logs merged statistics - -8. DependencyParser._parse_multiple_repositories() - └─ Logs multi-path detection - └─ Logs namespace generation per path - └─ Logs component counts per namespace - └─ Logs component types per namespace - -9. DependencyParser._resolve_cross_namespace_dependencies() - └─ Logs cross-namespace dependencies found - └─ Logs resolution summary -``` - ---- - -## Log Level Breakdown - -### INFO Level (Always Visible) -- Multi-path mode detection -- Path lists and counts -- Namespace assignments -- Component counts per namespace -- Component type breakdowns -- Cross-namespace dependency summaries -- Final statistics - -### DEBUG Level (Verbose Mode Only) -- Property access logs -- Detailed validation steps -- Individual cross-namespace dependencies -- Internal state transitions - -### WARNING Level (Errors/Issues) -- No files found in path -- Leaf nodes not found in components -- Invalid paths or permissions - ---- - -## Testing Recommendations - -### Test Multi-Path Mode -```bash -codewiki generate \ - --repo-path=/path/to/primary \ - --output-dir=output \ - --additional-paths=../ui-kit,../shared-lib \ - --verbose -``` - -**Expected Log Output:** -``` -🔍 CLI received 2 additional path(s): - ├─ Path #1: ../ui-kit - │ └─ Normalized: /absolute/path/to/ui-kit - ├─ Path #2: ../shared-lib - │ └─ Normalized: /absolute/path/to/shared-lib - └─ Normalized 2 additional path(s) - -🔍 Validating source paths... - ├─ ✓ Primary path valid: /path/to/primary - ├─ Validating 2 additional path(s)... - │ ├─ ✓ Additional path #1 valid: /absolute/path/to/ui-kit - │ ├─ ✓ Additional path #2 valid: /absolute/path/to/shared-lib - └─ ✓ All 2 additional path(s) validated - -🔍 Multi-path mode enabled: analyzing 3 source paths - ├─ Primary: /path/to/primary - ├─ Additional #1: /absolute/path/to/ui-kit - └─ Additional #2: /absolute/path/to/shared-lib - -🔍 Analyzing 3 repository paths... - ├─ [1/3] Building file tree for: /path/to/primary - │ └─ Namespace: 'primary' - │ └─ Found 150 files (2500.00 KB) - ├─ [2/3] Building file tree for: /absolute/path/to/ui-kit - │ └─ Namespace: 'ui-kit' - │ └─ Found 75 files (1200.00 KB) - ├─ [3/3] Building file tree for: /absolute/path/to/shared-lib - │ └─ Namespace: 'shared-lib' - │ └─ Found 50 files (800.00 KB) - └─ ✓ Merged 3 repositories: - ├─ Total files: 275 - ├─ Total size: 4500.00 KB - └─ Namespaces: primary, ui-kit, shared-lib - -🔍 Multi-path mode detected: analyzing 3 source directories - ├─ Primary: /path/to/primary - ├─ Additional #1: /absolute/path/to/ui-kit - └─ Additional #2: /absolute/path/to/shared-lib - -📂 Analyzing path 1/3: /path/to/primary - └─ Namespace: 'primary' -📊 Namespace 'primary': found 200 components - └─ Component types: - • function: 120 - • class: 50 - • method: 30 - -📂 Analyzing path 2/3: /absolute/path/to/ui-kit - └─ Namespace: 'ui-kit' -📊 Namespace 'ui-kit': found 100 components - └─ Component types: - • function: 60 - • class: 40 - -📂 Analyzing path 3/3: /absolute/path/to/shared-lib - └─ Namespace: 'shared-lib' -📊 Namespace 'shared-lib': found 50 components - └─ Component types: - • function: 30 - • class: 20 - -🔗 Resolving cross-namespace dependencies... - └─ ✓ Resolved 15 cross-namespace dependencies - -📊 Multi-path analysis complete: - ├─ Total components: 350 - ├─ Total modules: 12 - └─ Namespaces: 3 -``` - -### Test Single-Path Mode (Backward Compatibility) -```bash -codewiki generate \ - --repo-path=/path/to/single \ - --output-dir=output \ - --verbose -``` - -**Expected Log Output:** -``` -🔍 all_source_paths property accessed (single-path mode): /path/to/single - -🔍 Validating source paths... - ├─ ✓ Primary path valid: /path/to/single - └─ No additional paths to validate (single-path mode) - -🔍 Multi-path mode: DISABLED (single-path) - -📁 Single-path mode: analyzing /path/to/single - -🔍 Parsing repository files... - └─ Parsed 200 components total - └─ Component types found: - • function: 120 - • class: 50 - • method: 30 -``` - ---- - -## Verification Checklist - -- [x] Config layer logs path access and validation -- [x] Config layer logs mode detection -- [x] CLI layer logs path normalization -- [x] Graph builder logs source paths -- [x] Analyzer layer logs file tree merging -- [x] Parser layer logs namespace generation -- [x] Parser layer logs component counts per namespace -- [x] Parser layer logs component types per namespace -- [x] Parser layer logs cross-namespace dependencies -- [x] All syntax validated with py_compile -- [x] Backward compatibility maintained (single-path mode) - ---- - -## Benefits of This Logging - -1. **Debugging**: Easily identify which path is causing issues -2. **Progress Tracking**: See real-time progress for each repository -3. **Verification**: Confirm all paths are being analyzed -4. **Performance**: Monitor file counts and sizes per path -5. **Dependency Analysis**: Track cross-namespace dependencies -6. **Transparency**: Complete visibility into multi-path pipeline - ---- - -## Log Format Consistency - -All logs use consistent emoji prefixes: -- 🔍 = Detection/Analysis -- 📂 = File/Directory operations -- 📊 = Statistics/Summaries -- 🔗 = Dependencies -- ✓ = Success -- ⚠️ = Warning - -Tree structure formatting: -- `├─` = Branch -- `└─` = Last branch -- `│` = Continuation -- `•` = List item diff --git a/MULTI_PATH_QUICK_REFERENCE.md b/MULTI_PATH_QUICK_REFERENCE.md deleted file mode 100644 index c27b7884..00000000 --- a/MULTI_PATH_QUICK_REFERENCE.md +++ /dev/null @@ -1,291 +0,0 @@ -# Multi-Path Support - Quick Reference - -## Basic Usage - -### Single Path (Backward Compatible) -```python -from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser - -parser = DependencyParser("/path/to/repo") -components = parser.parse_repository() -# Component IDs: "src.module.Component" -``` - -### Multiple Paths -```python -parser = DependencyParser([ - "/path/to/openframe-frontend", - "/path/to/ui-kit" -]) -components = parser.parse_repository() -# Component IDs: "openframe-frontend.src.module.Component" -# "ui-kit.components.Button" -``` - -## Component ID Format - -| Mode | Format | Example | -|------|--------|---------| -| Single | `{module}.{component}` | `src.components.Button` | -| Multi | `{namespace}.{module}.{component}` | `ui-kit.src.components.Button` | - -## Key Methods - -### DependencyParser - -```python -parser = DependencyParser( - repo_path: Union[str, List[str]], # Single or multiple paths - include_patterns: List[str] = None, # ["*.py", "*.ts"] - exclude_patterns: List[str] = None # ["*test*"] -) - -# Parse repositories -components: Dict[str, Node] = parser.parse_repository() - -# Save to JSON -parser.save_dependency_graph("/path/to/output.json") - -# Access namespace info (multi-path only) -namespace = parser._get_namespace_from_path("/path/to/repo") -``` - -### RepoAnalyzer - -```python -analyzer = RepoAnalyzer( - include_patterns: List[str] = None, - exclude_patterns: List[str] = None -) - -# Single path -result = analyzer.analyze_repository_structure("/path/to/repo") - -# Multiple paths -result = analyzer.analyze_repository_structure([ - "/path/to/repo1", - "/path/to/repo2" -]) - -# Result structure: -{ - "file_tree": {...}, - "summary": { - "total_files": int, - "total_size_kb": float, - "repositories": int, # Multi-path only - "namespaces": List[str] # Multi-path only - } -} -``` - -## Component Object (Node) - -```python -component = components["namespace.module.Component"] - -# Core attributes -component.id # Namespaced ID -component.name # Original component name -component.component_type # "function", "class", "method" -component.file_path # Absolute path -component.relative_path # Relative to repo -component.depends_on # Set of dependency IDs (namespaced) - -# Code details -component.source_code # Full source code -component.start_line # Starting line number -component.end_line # Ending line number -component.docstring # Component docstring -component.parameters # List of parameters - -# Additional info -component.node_type # AST node type -component.class_name # Parent class (if method) -component.base_classes # Inheritance (if class) -``` - -## Dependencies - -### Intra-Namespace (Same Repository) -```python -{ - "id": "openframe-frontend.src.App.main", - "depends_on": [ - "openframe-frontend.src.utils.logger", - "openframe-frontend.src.config.settings" - ] -} -``` - -### Cross-Namespace (Different Repositories) -```python -{ - "id": "openframe-frontend.src.App.main", - "depends_on": [ - "openframe-frontend.src.utils.logger", # Same namespace - "ui-kit.components.Button", # Cross-namespace - "shared-lib.api.fetchData" # Cross-namespace - ] -} -``` - -## Common Patterns - -### Filtering Components by Namespace -```python -parser = DependencyParser(["/repo1", "/repo2"]) -components = parser.parse_repository() - -# Get components from specific namespace -repo1_components = { - comp_id: comp for comp_id, comp in components.items() - if comp_id.startswith("repo1.") -} -``` - -### Finding Cross-Namespace Dependencies -```python -for comp_id, component in components.items(): - comp_namespace = comp_id.split('.')[0] - cross_deps = [ - dep for dep in component.depends_on - if dep.split('.')[0] != comp_namespace - ] - if cross_deps: - print(f"{comp_id} has {len(cross_deps)} cross-namespace deps") -``` - -### Grouping by Namespace -```python -from collections import defaultdict - -by_namespace = defaultdict(list) -for comp_id in components.keys(): - namespace = comp_id.split('.')[0] - by_namespace[namespace].append(comp_id) - -for namespace, comp_ids in by_namespace.items(): - print(f"{namespace}: {len(comp_ids)} components") -``` - -## File Patterns - -### Include Patterns -```python -# Python only -include_patterns = ["*.py"] - -# TypeScript/JavaScript -include_patterns = ["*.ts", "*.tsx", "*.js", "*.jsx"] - -# Multiple languages -include_patterns = ["*.py", "*.ts", "*.tsx", "*.cs", "*.java"] -``` - -### Exclude Patterns -```python -exclude_patterns = [ - "*test*", # Test files - "*spec*", # Spec files - "node_modules/*", # Dependencies - "build/*", # Build output - "dist/*", # Distribution files - ".git/*", # Git directory - "__pycache__/*" # Python cache -] -``` - -## Error Handling - -```python -try: - parser = DependencyParser(["/repo1", "/repo2"]) - components = parser.parse_repository() -except FileNotFoundError as e: - print(f"Repository not found: {e}") -except PermissionError as e: - print(f"Permission denied: {e}") -except Exception as e: - print(f"Parsing error: {e}") -``` - -## Performance Tips - -1. **Use Specific Include Patterns**: Only analyze relevant file types -2. **Aggressive Exclude Patterns**: Skip test files, dependencies, build outputs -3. **Limit Number of Paths**: More paths = longer analysis time -4. **Filter Early**: Use `filtered_folders` parameter when possible - -## Output Format (JSON) - -```json -{ - "namespace.module.Component": { - "id": "namespace.module.Component", - "name": "Component", - "component_type": "class", - "file_path": "/absolute/path/to/file.py", - "relative_path": "src/module/file.py", - "source_code": "class Component:\n ...", - "start_line": 10, - "end_line": 50, - "depends_on": [ - "namespace.module.Dependency1", - "other-namespace.utils.Helper" - ], - "docstring": "Component docstring", - "parameters": [], - "node_type": "class", - "has_docstring": true - } -} -``` - -## Backward Compatibility - -All existing single-path code works without changes: - -```python -# ✅ Still works -parser = DependencyParser("/path/to/repo") -parser.repo_path # Still available -components = parser.parse_repository() # Same behavior -``` - -## Migration Checklist - -- [ ] Update `DependencyParser` constructor calls to use list of paths -- [ ] Update code that assumes non-namespaced component IDs -- [ ] Update dependency resolution logic for cross-namespace refs -- [ ] Update JSON output parsing if used downstream -- [ ] Test with actual multi-path repositories -- [ ] Update documentation/examples - -## Common Issues - -### Issue: "Component not found" -**Cause**: Cross-namespace dependency resolution failed -**Solution**: Check component names match exactly, verify both repos were parsed - -### Issue: "Duplicate component IDs" -**Cause**: Two repos with same directory name -**Solution**: Rename one repository directory or use unique paths - -### Issue: "Performance degradation" -**Cause**: Too many files being analyzed -**Solution**: Add more exclude patterns, use specific include patterns - -### Issue: "Missing dependencies" -**Cause**: Component exists but not in dependency graph -**Solution**: Check include/exclude patterns, verify file was parsed - -## Examples Location - -See `/examples/multi_path_example.py` for complete working examples. - -## Documentation - -- Full Implementation: `MULTI_PATH_IMPLEMENTATION.md` -- API Reference: Component docstrings in source files -- Examples: `examples/multi_path_example.py` diff --git a/SECURITY_ANALYSIS_ADDITIONAL_PATHS.md b/SECURITY_ANALYSIS_ADDITIONAL_PATHS.md deleted file mode 100644 index dd530058..00000000 --- a/SECURITY_ANALYSIS_ADDITIONAL_PATHS.md +++ /dev/null @@ -1,852 +0,0 @@ -# Security & Edge Case Analysis: Additional Paths Implementation - -**Analysis Date:** 2026-02-02 -**Analyzer:** Claude Sonnet 4.5 -**CodeWiki Version:** Latest (main branch) -**Feature:** `--additional-paths` CLI parameter and `additional_source_paths` config field - ---- - -## Executive Summary - -✅ **SECURITY STATUS: SAFE** - -The additional paths implementation is **secure and well-designed**. After comprehensive analysis of all code paths from CLI input through backend validation, NO security vulnerabilities were found. The implementation follows security best practices including: - -- Absolute path normalization prevents directory traversal -- Existence validation prevents symlink attacks -- Read permission checks prevent privilege escalation -- No shell command execution (pure Python path operations) -- Proper error handling with descriptive messages - -**Recommended Actions:** -1. ✅ Safe to use in production -2. 📝 Add integration tests for edge cases (see Test Plan section) -3. 🔒 Consider adding optional path whitelist for enterprise deployments - ---- - -## 1. Security Analysis - -### 1.1 Path Traversal Attacks ✅ SAFE - -**Attack Vector:** `../../etc/passwd` or similar relative paths to escape repository root - -**Code Analysis:** - -```python -# Location: codewiki/cli/commands/generate.py:392-422 -additional_paths_list = parse_patterns(additional_paths) - -# Validate that paths exist -for path in additional_paths_list: - full_path = repo_path / path # Joins with repo_path - if not full_path.exists(): - invalid_paths.append(path) -``` - -```python -# Location: codewiki/cli/adapters/doc_generator.py:150 -normalized = str((Path(self.repo_path) / path).resolve()) -``` - -**Protection Mechanisms:** -1. **Path.resolve()**: Automatically canonicalizes paths, resolving `.` and `..` -2. **Path joining**: Uses `/` operator which normalizes paths -3. **Existence validation**: Path must exist before processing - -**Test Results:** - -| Attack Input | Normalized Output | Result | -|--------------|------------------|--------| -| `../../etc/passwd` | `/etc/passwd` (if exists) | ✅ Blocked by validation if outside repo | -| `../../../` | Parent dirs resolved | ✅ Validated against existence | -| `vendor/../../../etc` | `/etc` | ✅ Must exist under repo_path | - -**Verdict:** ✅ **PROTECTED** - Path traversal automatically resolved and validated - ---- - -### 1.2 Symlink Attacks ✅ SAFE - -**Attack Vector:** Symlink to sensitive system files or directories - -**Code Analysis:** - -```python -# Location: codewiki/src/config.py:180-188 -if not os.path.exists(path): - raise ValueError(f"Additional source path #{i} does not exist: {path}") -if not os.path.isdir(path): - raise ValueError(f"Additional source path #{i} is not a directory: {path}") - -# Check for read access -if not os.access(path, os.R_OK): - raise OSError(f"Additional source path #{i} is not readable: {path}") -``` - -**Protection Mechanisms:** -1. **os.path.exists()**: Follows symlinks by default (checks target exists) -2. **os.path.isdir()**: Validates target is a directory (not a file) -3. **os.access(R_OK)**: Verifies read permissions on target - -**Attack Scenarios:** - -| Symlink Target | Validation Behavior | Result | -|----------------|---------------------|--------| -| `ln -s /etc/passwd secrets` | ❌ isdir() fails (file, not dir) | ✅ Blocked | -| `ln -s /root/.ssh config` | ❌ access(R_OK) fails (no perms) | ✅ Blocked | -| `ln -s /var/www/html external` | ✅ Passes if readable dir | ⚠️ User responsibility | - -**Verdict:** ✅ **PROTECTED** - Symlinks must point to readable directories. User is responsible for what they symlink. - ---- - -### 1.3 Command Injection ✅ SAFE - -**Attack Vector:** Shell metacharacters in paths like `; rm -rf /` or `$(whoami)` - -**Code Analysis:** - -```python -# ALL path operations use Python pathlib/os modules - NO shell execution -Path(self.repo_path) / path # Pure Python path joining -os.path.exists(path) # Pure syscall, no shell -os.path.isdir(path) # Pure syscall, no shell -os.access(path, os.R_OK) # Pure syscall, no shell -``` - -**Protection Mechanisms:** -1. **No subprocess calls**: Paths never passed to shell -2. **pathlib abstraction**: Safe object-oriented path handling -3. **os module syscalls**: Direct system calls, not shell commands - -**Test Results:** - -| Injection Attempt | Processing | Result | -|-------------------|------------|--------| -| `vendor; rm -rf /` | Treated as literal path | ✅ File not found error | -| `$(whoami)` | Literal filename | ✅ File not found error | -| `id` | Literal filename | ✅ File not found error | -| `external && malicious` | Literal path | ✅ File not found error | - -**Verdict:** ✅ **COMPLETELY SAFE** - No shell execution anywhere in code path - ---- - -### 1.4 Privilege Escalation ✅ SAFE - -**Attack Vector:** Access files/directories outside user's permissions - -**Code Analysis:** - -```python -# Location: codewiki/src/config.py:187-188 -if not os.access(path, os.R_OK): - raise OSError(f"Additional source path #{i} is not readable: {path}") -``` - -**Protection Mechanisms:** -1. **os.access(R_OK)**: Checks effective UID/GID permissions -2. **No SUID operations**: CodeWiki runs with user's permissions -3. **Fail-fast validation**: Errors before any processing begins - -**Verdict:** ✅ **SAFE** - Cannot escalate beyond user's existing permissions - ---- - -## 2. Edge Case Analysis - -### 2.1 Empty Environment Variable ✅ HANDLED - -**Test:** -```bash -CODEWIKI_ADDITIONAL_PATHS="" codewiki generate -``` - -**Behavior:** -```python -# codewiki/cli/commands/generate.py:142-145 -@click.option( - "--additional-paths", - type=str, - default=None, # ✅ Defaults to None, not empty string -``` - -**Verdict:** ✅ Treated as unset, no additional paths loaded - ---- - -### 2.2 Paths with Spaces ✅ HANDLED - -**Test:** -```bash -codewiki generate --additional-paths "vendor packages,external deps" -``` - -**Behavior:** -```python -# codewiki/cli/commands/generate.py:35-39 -def parse_patterns(patterns_str: str) -> List[str]: - if not patterns_str: - return [] - return [p.strip() for p in patterns_str.split(',') if p.strip()] - # ✅ Result: ["vendor packages", "external deps"] -``` - -**Verdict:** ✅ Spaces in paths work correctly (after strip()) - ---- - -### 2.3 Special Characters ✅ HANDLED - -**Test:** -```bash -codewiki generate --additional-paths "vendor-2.0,external_libs,deps@v3" -``` - -**Behavior:** -- Hyphens: ✅ Valid in filesystem paths -- Underscores: ✅ Valid in filesystem paths -- `@` symbol: ✅ Valid in filesystem paths - -**Verdict:** ✅ All common special chars work - ---- - -### 2.4 Non-Existent Directories ✅ HANDLED - -**Test:** -```bash -codewiki generate --additional-paths "vendor,nonexistent" -``` - -**Behavior:** -```python -# codewiki/cli/commands/generate.py:400-418 -invalid_paths = [] -for path in additional_paths_list: - full_path = repo_path / path - if not full_path.exists(): - invalid_paths.append(path) - -if invalid_paths: - raise RepositoryError( - f"Additional paths not found:\n" - f" {', '.join(invalid_paths)}\n\n" - f"Please ensure all paths exist relative to repository root:\n" - f" Repository: {repo_path}\n" - f" Invalid paths: {invalid_paths}" - ) -``` - -**Output:** -``` -Error: Additional paths not found: - nonexistent - -Please ensure all paths exist relative to repository root: - Repository: /Users/user/myrepo - Invalid paths: ['nonexistent'] -``` - -**Verdict:** ✅ Clear error message with helpful context - ---- - -### 2.5 Circular Symlinks ✅ HANDLED - -**Test:** -```bash -ln -s link1 link2 -ln -s link2 link1 -codewiki generate --additional-paths "link1" -``` - -**Behavior:** -```python -# Python's os.path.exists() handles circular symlinks -# Returns False for unresolvable symlinks -if not os.path.exists(path): # ✅ Returns False - raise ValueError(f"Additional source path does not exist: {path}") -``` - -**Verdict:** ✅ Detected as non-existent path - ---- - -### 2.6 Very Long Paths (> 4096 chars) ⚠️ SYSTEM LIMIT - -**Test:** -```bash -# Create deeply nested directory -long_path = "a/" * 2000 # 4000 characters -codewiki generate --additional-paths "$long_path" -``` - -**Behavior:** -- Linux PATH_MAX: 4096 bytes (including null terminator) -- macOS PATH_MAX: 1024 bytes -- Windows MAX_PATH: 260 characters (legacy) / 32,767 (Unicode) - -**Python Handling:** -```python -# Python pathlib raises OSError if path too long -try: - Path(long_path).resolve() -except OSError as e: - # "File name too long" error -``` - -**Verdict:** ⚠️ **SYSTEM LIMITATION** - Handled by OS, clear error message - -**Recommendation:** Add documentation note about PATH_MAX limits - ---- - -### 2.7 Multiple Comma-Separated Paths ✅ HANDLED - -**Test:** -```bash -codewiki generate --additional-paths "vendor,external,deps,third_party" -``` - -**Behavior:** -```python -# codewiki/cli/commands/generate.py:392 -additional_paths_list = parse_patterns(additional_paths) -# Result: ["vendor", "external", "deps", "third_party"] - -# All validated in loop: -for i, path in enumerate(additional_paths_list, 1): - logger.debug(f"│ ├─ Path #{i}: {path}") -``` - -**Verdict:** ✅ Fully supported, all paths validated - ---- - -### 2.8 Relative vs Absolute Paths ✅ NORMALIZED - -**Test:** -```bash -# Relative paths -codewiki generate --additional-paths "vendor,./external,../sibling" - -# Absolute paths -codewiki generate --additional-paths "/opt/libs,/usr/local/deps" -``` - -**Behavior:** -```python -# codewiki/cli/adapters/doc_generator.py:150 -normalized = str((Path(self.repo_path) / path).resolve()) - -# Relative: joined with repo_path, then resolved -# Absolute: resolve() keeps absolute if valid -``` - -**Results:** - -| Input | Repo Path | Normalized Output | -|-------|-----------|------------------| -| `vendor` | `/home/user/repo` | `/home/user/repo/vendor` | -| `./external` | `/home/user/repo` | `/home/user/repo/external` | -| `../sibling` | `/home/user/repo` | `/home/user/sibling` | -| `/opt/libs` | `/home/user/repo` | `/opt/libs` | - -**Verdict:** ✅ Both relative and absolute paths work correctly - ---- - -### 2.9 Paths with Trailing Slash ✅ NORMALIZED - -**Test:** -```bash -codewiki generate --additional-paths "vendor/,external" -``` - -**Behavior:** -```python -# pathlib normalizes trailing slashes automatically -Path("vendor/").resolve() # → /absolute/path/vendor (no trailing slash) -``` - -**Verdict:** ✅ Trailing slashes automatically removed - ---- - -## 3. Error Handling Analysis - -### 3.1 Path Validation Failure ✅ EXCELLENT - -**Location:** `codewiki/cli/commands/generate.py:408-418` - -```python -if invalid_paths: - raise RepositoryError( - f"Additional paths not found:\n" - f" {', '.join(invalid_paths)}\n\n" - f"Please ensure all paths exist relative to repository root:\n" - f" Repository: {repo_path}\n" - f" Invalid paths: {invalid_paths}" - ) -``` - -**Quality:** -- ✅ Clear error type (`RepositoryError`) -- ✅ Lists all invalid paths -- ✅ Shows repository context -- ✅ Suggests solution ("ensure paths exist") - ---- - -### 3.2 Permission Errors ✅ EXCELLENT - -**Location:** `codewiki/src/config.py:187-188` - -```python -if not os.access(path, os.R_OK): - raise OSError(f"Additional source path #{i} is not readable: {path}") -``` - -**Quality:** -- ✅ Clear error type (`OSError`) -- ✅ Identifies which path failed (by number and path) -- ✅ Specific issue ("not readable") - ---- - -### 3.3 Type Errors ✅ HANDLED - -**Scenario:** Non-string passed to parse_patterns - -```python -def parse_patterns(patterns_str: str) -> List[str]: - if not patterns_str: # ✅ Handles None - return [] - return [p.strip() for p in patterns_str.split(',') if p.strip()] -``` - -**Test:** -- `None` → `[]` (empty list) -- Empty string → `[]` (empty list) -- Integer → Would fail at `.split()` with AttributeError (handled by Click type validation) - -**Verdict:** ✅ Type hints + Click validation prevent bad input - ---- - -### 3.4 Graceful Exit on Validation Failure ✅ PERFECT - -**Code Flow:** -```python -# 1. Early validation in CLI command -if invalid_paths: - raise RepositoryError(...) # Exit code based on error type - -# 2. Exception handling -except RepositoryError as e: - logger.error(e.message) - sys.exit(e.exit_code) # ✅ Clean exit with proper code -``` - -**Verdict:** ✅ No partial state changes, clean exit - ---- - -### 3.5 Logging Coverage ✅ EXCELLENT - -**Verbose Mode Output:** -```python -# codewiki/cli/commands/generate.py:389-421 -if verbose: - logger.debug("🔍 Stage 1.3: Additional Paths Validation") - logger.debug(f"├─ Parsed {len(additional_paths_list)} additional path(s)") - for i, path in enumerate(additional_paths_list, 1): - logger.debug(f"│ ├─ Path #{i}: {path}") - # ... validation ... - logger.debug(f"│ └─ ✓ {path} (exists)") - logger.debug(f"└─ All additional paths validated") -``` - -**Verdict:** ✅ Comprehensive logging for debugging - ---- - -## 4. Test Plan - -### 4.1 Unit Tests (Recommended) - -**File:** `tests/test_additional_paths.py` - -```python -import pytest -from pathlib import Path -from codewiki.cli.commands.generate import parse_patterns -from codewiki.src.config import Config - -class TestParsePatterns: - def test_empty_string(self): - assert parse_patterns("") == [] - - def test_single_path(self): - assert parse_patterns("vendor") == ["vendor"] - - def test_multiple_paths(self): - assert parse_patterns("vendor,external,deps") == ["vendor", "external", "deps"] - - def test_spaces_stripped(self): - assert parse_patterns("vendor , external , deps") == ["vendor", "external", "deps"] - - def test_trailing_commas(self): - assert parse_patterns("vendor,external,") == ["vendor", "external"] - -class TestConfigValidation: - def test_nonexistent_path(self, tmp_path): - config = Config.from_cli( - repo_path=str(tmp_path), - output_dir=str(tmp_path / "output"), - main_model="test", - cluster_model="test", - fallback_model="test", - cluster_api_key="test", - main_api_key="test", - fallback_api_key="test", - cluster_base_url="http://test", - main_base_url="http://test", - fallback_base_url="http://test", - additional_source_paths=[str(tmp_path / "nonexistent")] - ) - - with pytest.raises(ValueError, match="does not exist"): - config.validate_source_paths() - - def test_valid_additional_paths(self, tmp_path): - # Create test directories - vendor = tmp_path / "vendor" - vendor.mkdir() - external = tmp_path / "external" - external.mkdir() - - config = Config.from_cli( - repo_path=str(tmp_path), - output_dir=str(tmp_path / "output"), - main_model="test", - cluster_model="test", - fallback_model="test", - cluster_api_key="test", - main_api_key="test", - fallback_api_key="test", - cluster_base_url="http://test", - main_base_url="http://test", - fallback_base_url="http://test", - additional_source_paths=[str(vendor), str(external)] - ) - - # Should not raise - config.validate_source_paths() - assert config.is_multi_path_mode() is True - assert len(config.all_source_paths) == 3 # repo + 2 additional -``` - ---- - -### 4.2 Integration Tests (Recommended) - -**File:** `tests/integration/test_cli_additional_paths.py` - -```python -import subprocess -import pytest -from pathlib import Path - -def test_cli_nonexistent_path(tmp_path): - """Test CLI with nonexistent path""" - result = subprocess.run( - ["codewiki", "generate", "--additional-paths", "nonexistent"], - cwd=str(tmp_path), - capture_output=True, - text=True - ) - - assert result.returncode != 0 - assert "Additional paths not found" in result.stderr - assert "nonexistent" in result.stderr - -def test_cli_special_characters(tmp_path): - """Test paths with special characters""" - special_dir = tmp_path / "vendor-2.0" - special_dir.mkdir() - - result = subprocess.run( - ["codewiki", "generate", "--additional-paths", "vendor-2.0", "--verbose"], - cwd=str(tmp_path), - capture_output=True, - text=True - ) - - assert "vendor-2.0" in result.stdout or result.returncode == 0 - -def test_cli_symlink_to_directory(tmp_path): - """Test symlink to valid directory""" - real_dir = tmp_path / "real" - real_dir.mkdir() - - link = tmp_path / "link" - link.symlink_to(real_dir) - - result = subprocess.run( - ["codewiki", "generate", "--additional-paths", "link", "--verbose"], - cwd=str(tmp_path), - capture_output=True, - text=True - ) - - # Should work (symlink to dir) - assert "All additional paths validated" in result.stdout - -def test_cli_symlink_to_file(tmp_path): - """Test symlink to file (should fail)""" - real_file = tmp_path / "file.txt" - real_file.write_text("test") - - link = tmp_path / "link" - link.symlink_to(real_file) - - result = subprocess.run( - ["codewiki", "generate", "--additional-paths", "link"], - cwd=str(tmp_path), - capture_output=True, - text=True - ) - - assert result.returncode != 0 - assert "not a directory" in result.stderr.lower() -``` - ---- - -### 4.3 Security Tests (Critical) - -**File:** `tests/security/test_path_traversal.py` - -```python -import pytest -from pathlib import Path -from codewiki.src.config import Config - -class TestPathTraversalProtection: - def test_relative_parent_traversal(self, tmp_path): - """Test ../../etc/passwd style attacks""" - config = Config.from_cli( - repo_path=str(tmp_path), - output_dir=str(tmp_path / "output"), - main_model="test", - cluster_model="test", - fallback_model="test", - cluster_api_key="test", - main_api_key="test", - fallback_api_key="test", - cluster_base_url="http://test", - main_base_url="http://test", - fallback_base_url="http://test", - additional_source_paths=["../../etc/passwd"] - ) - - with pytest.raises((ValueError, FileNotFoundError)): - config.validate_source_paths() - - def test_absolute_system_path(self, tmp_path): - """Test absolute path to system directory""" - config = Config.from_cli( - repo_path=str(tmp_path), - output_dir=str(tmp_path / "output"), - main_model="test", - cluster_model="test", - fallback_model="test", - cluster_api_key="test", - main_api_key="test", - fallback_api_key="test", - cluster_base_url="http://test", - main_base_url="http://test", - fallback_base_url="http://test", - additional_source_paths=["/etc"] - ) - - # This would work if /etc is readable by user - # The question is: should we allow this? - # Current implementation: YES (user is responsible) -``` - ---- - -## 5. Bugs Found - -### 🐛 None Found - -After comprehensive analysis, **no bugs were identified** in the additional paths implementation. - ---- - -## 6. Security Hardening Recommendations - -### 6.1 Optional Path Whitelist (LOW PRIORITY) - -**For enterprise deployments**, consider adding a configuration option to restrict additional paths to specific directories: - -```python -# config.py -class Config: - allowed_additional_paths_prefix: Optional[List[str]] = None - - def validate_source_paths(self) -> None: - if self.allowed_additional_paths_prefix: - for path in self.additional_source_paths: - if not any(path.startswith(prefix) for prefix in self.allowed_additional_paths_prefix): - raise ValueError( - f"Additional path {path} not allowed. " - f"Must start with one of: {self.allowed_additional_paths_prefix}" - ) -``` - -**Use Case:** Prevent users from accidentally including system directories - -**Priority:** LOW (current implementation is already safe) - ---- - -### 6.2 Document PATH_MAX Limits (DOCUMENTATION) - -Add to documentation: - -```markdown -## Path Length Limits - -Additional paths are subject to operating system PATH_MAX limits: -- **Linux:** 4,096 characters -- **macOS:** 1,024 characters -- **Windows:** 260 characters (legacy) or 32,767 (long path mode) - -If you encounter "File name too long" errors, reduce your directory nesting depth. -``` - ---- - -### 6.3 Add Warning for System Directories (UX IMPROVEMENT) - -Consider warning users if they add paths outside their home directory: - -```python -def validate_source_paths(self) -> None: - # ... existing validation ... - - # Warn for system paths - home_dir = Path.home() - for path in self.additional_source_paths: - abs_path = Path(path).resolve() - if not abs_path.is_relative_to(home_dir): - logger.warning( - f"⚠️ Additional path is outside your home directory: {path}\n" - f" Ensure you have proper permissions and this is intentional." - ) -``` - ---- - -## 7. Conclusion - -### ✅ Security: EXCELLENT - -The additional paths implementation demonstrates **strong security practices**: - -1. ✅ No command injection vulnerabilities (pure Python path operations) -2. ✅ Path traversal automatically resolved and validated -3. ✅ Symlink attacks mitigated by type and permission checks -4. ✅ Cannot escalate beyond user's existing permissions -5. ✅ Comprehensive validation with clear error messages - -### ✅ Edge Case Handling: ROBUST - -All edge cases are properly handled: - -1. ✅ Empty/missing values → default to None -2. ✅ Special characters → work correctly -3. ✅ Non-existent paths → clear error message -4. ✅ Circular symlinks → detected as non-existent -5. ✅ Multiple paths → all validated -6. ✅ Relative/absolute → both normalized correctly - -### ✅ Error Handling: EXCELLENT - -Error handling is comprehensive and user-friendly: - -1. ✅ Descriptive error messages with context -2. ✅ Proper exception types -3. ✅ Clean exit codes -4. ✅ Detailed logging in verbose mode -5. ✅ No partial state on failure - -### 📊 Overall Grade: A+ - -The additional paths feature is **production-ready** with excellent security and robustness. No critical issues found. - ---- - -## Appendix A: Code Flow Diagram - -``` -User Input: --additional-paths "vendor,external" - ↓ -CLI Command (generate.py:142-166) - ├─ Parse comma-separated string → ["vendor", "external"] - ├─ Validate paths exist (repo_path / path) - ├─ Raise RepositoryError if any missing - └─ Pass to doc_generator - ↓ -Doc Generator (doc_generator.py:140-155) - ├─ Normalize to absolute paths with resolve() - ├─ Log normalized paths in verbose mode - └─ Pass to BackendConfig.from_cli() - ↓ -Backend Config (config.py:326-576) - ├─ Store in additional_source_paths field - ├─ Call validate_source_paths() - │ ├─ Check os.path.exists() - │ ├─ Check os.path.isdir() - │ └─ Check os.access(R_OK) - └─ Use in all_source_paths property - ↓ -Documentation Generation - ├─ Iterate over all_source_paths - ├─ Analyze dependencies from each path - └─ Merge into unified documentation -``` - ---- - -## Appendix B: Test Coverage Summary - -| Test Category | Status | Priority | -|---------------|--------|----------| -| Path Traversal Security | ✅ Verified Safe | CRITICAL | -| Symlink Security | ✅ Verified Safe | CRITICAL | -| Command Injection | ✅ Verified Safe | CRITICAL | -| Privilege Escalation | ✅ Verified Safe | CRITICAL | -| Edge Cases | ✅ All Handled | HIGH | -| Error Handling | ✅ Excellent | HIGH | -| Unit Tests | 📝 Recommended | MEDIUM | -| Integration Tests | 📝 Recommended | MEDIUM | -| Security Tests | 📝 Recommended | HIGH | - -**Recommended Next Steps:** -1. Implement unit tests from Section 4.1 -2. Add integration tests from Section 4.2 -3. Run security tests from Section 4.3 -4. Add documentation note about PATH_MAX limits - ---- - -**Report Prepared By:** Claude Sonnet 4.5 -**Verification Status:** ✅ All code paths analyzed -**Confidence Level:** 99% (manual testing recommended for 100%) diff --git a/codewiki/cli/adapters/.doc_generator.md b/codewiki/cli/adapters/.doc_generator.md index 15a95850..16473fa0 100644 --- a/codewiki/cli/adapters/.doc_generator.md +++ b/codewiki/cli/adapters/.doc_generator.md @@ -8,7 +8,7 @@ CLI adapter that wraps the backend `DocumentationGenerator` to add CLI-specific - **`CLIDocumentationGenerator`** — Main adapter class orchestrating documentation generation from the CLI. - `__init__(repo_path, output_dir, config, verbose=False, generate_html=False, diagrams_dir=None)` — Initializes job metadata, `ProgressTracker`, and `DocumentationJob`; configures backend logging. - `_configure_backend_logging()` — Sets up `logging.getLogger('codewiki.src.be')` with a `ColoredFormatter`; verbose mode logs `INFO`+ to stdout, non-verbose suppresses to `WARNING`+ on stderr. Disables propagation to avoid duplicate log lines. - - `generate()` — Main entry point. Builds a `BackendConfig` from CLI options (per-provider models, API keys, base URLs, token limits, temperature settings, clustering parameters, additional source paths), runs backend generation via `asyncio.run`, optionally triggers HTML generation, finalizes the job, and returns the completed `DocumentationJob`. Raises `APIError` on LLM failures. + - `generate()` — Main entry point. Builds a `BackendConfig` from CLI options (per-provider models, API keys, base URLs, token limits, temperature settings, clustering parameters), runs backend generation via `asyncio.run`, optionally triggers HTML generation, finalizes the job, and returns the completed `DocumentationJob`. Raises `APIError` on LLM failures. - `_run_backend_generation(backend_config)` — Async method driving the multi-stage backend pipeline (dependency analysis, clustering, doc generation) with `ProgressTracker` updates per stage. ## Usage Example @@ -24,7 +24,6 @@ config = { "main_api_key": "sk-...", "cluster_api_key": "sk-...", "max_depth": 2, - "additional_paths": ["shared/lib"], } generator = CLIDocumentationGenerator( @@ -42,5 +41,4 @@ print(job.status, job.output_directory) ## Notes - Requires `set_cli_context(True)` from `codewiki.src.config` to signal CLI mode to the backend. -- Additional source paths supplied in `config['additional_paths']` are resolved to absolute paths relative to `repo_path` before being passed to `BackendConfig`. - Verbose mode prints a detailed configuration summary (models, base URLs, token limits, clustering settings) prior to generation. \ No newline at end of file diff --git a/codewiki/cli/adapters/doc_generator.py b/codewiki/cli/adapters/doc_generator.py index ff93642a..f816d981 100644 --- a/codewiki/cli/adapters/doc_generator.py +++ b/codewiki/cli/adapters/doc_generator.py @@ -130,24 +130,6 @@ def generate(self) -> DocumentationJob: # Set CLI context for backend set_cli_context(True) - # Process additional paths from config - additional_paths_raw = self.config.get('additional_paths', []) - additional_paths_normalized = None - - if additional_paths_raw: - logger = logging.getLogger(__name__) - logger.info(f"🔍 CLI received {len(additional_paths_raw)} additional path(s):") - - additional_paths_normalized = [] - for i, path in enumerate(additional_paths_raw, 1): - # Convert to absolute path (relative to repo_path) - normalized = str((Path(self.repo_path) / path).resolve()) - logger.info(f" ├─ Path #{i}: {path}") - logger.info(f" │ └─ Normalized: {normalized}") - additional_paths_normalized.append(normalized) - - logger.info(f" └─ Normalized {len(additional_paths_normalized)} additional path(s)") - # Create backend config with CLI settings backend_config = BackendConfig.from_cli( repo_path=str(self.repo_path), @@ -190,9 +172,7 @@ def generate(self) -> DocumentationJob: # Agent instructions agent_instructions=self.config.get('agent_instructions'), # Optional diagrams directory - diagrams_dir=str(self.diagrams_dir) if self.diagrams_dir else None, - # Additional paths for dependency analysis (normalized) - additional_source_paths=additional_paths_normalized + diagrams_dir=str(self.diagrams_dir) if self.diagrams_dir else None ) # Log configuration details in verbose mode @@ -212,10 +192,6 @@ def generate(self) -> DocumentationJob: print(f" │ ├─ Max tokens/module: {backend_config.max_token_per_module}") print(f" │ ├─ Max tokens/leaf: {backend_config.max_token_per_leaf_module}") print(f" │ └─ Max depth: {backend_config.max_depth}") - if backend_config.additional_source_paths: - print(f" ├─ Additional Paths ({len(backend_config.additional_source_paths)}):") - for path in backend_config.additional_source_paths: - print(f" │ └─ {path}") if backend_config.agent_instructions: print(f" └─ Agent Instructions: Configured") if backend_config.agent_instructions.get('custom_instructions'): @@ -263,11 +239,6 @@ async def _run_backend_generation(self, backend_config: BackendConfig): print(f" ├─ Repository: {backend_config.repo_path}") print(f" └─ Output: {backend_config.output_dir}") - if backend_config.additional_source_paths: - print(f" ├─ Additional paths: {len(backend_config.additional_source_paths)}") - for i, path in enumerate(backend_config.additional_source_paths, 1): - print(f" │ └─ Path #{i}: {path}") - # Create documentation generator if self.verbose: logger.info("├─ Initializing DocumentationGenerator...") diff --git a/codewiki/cli/commands/generate.py b/codewiki/cli/commands/generate.py index ba469ae7..41562659 100644 --- a/codewiki/cli/commands/generate.py +++ b/codewiki/cli/commands/generate.py @@ -39,6 +39,15 @@ def parse_patterns(patterns_str: str) -> List[str]: return [p.strip() for p in patterns_str.split(',') if p.strip()] +def _removed_additional_paths(value): + """`--additional-paths` was removed: say so and why, rather than click's bare "no such option".""" + if value: + raise click.UsageError( + "--additional-paths was removed: CodeWiki documents only the repository it runs in, and reads other " + "repositories through the hub's code-graph tools (HUB_BASE_URL, WEBHOOK_SECRET). Drop the option." + ) + + @click.command(name="generate") @click.option( "--output", @@ -140,9 +149,11 @@ def parse_patterns(patterns_str: str) -> List[str]: ) @click.option( "--additional-paths", - type=str, - default=None, - help="Comma-separated additional paths to include in dependency analysis (e.g., 'vendor/deps,external/libs')", + hidden=True, + expose_value=False, + is_eager=True, + callback=lambda ctx, param, value: _removed_additional_paths(value), + help="Removed: other repositories are read through the hub's code-graph tools.", ) @click.pass_context def generate_command( @@ -162,8 +173,7 @@ def generate_command( max_tokens: Optional[int], max_token_per_module: Optional[int], max_token_per_leaf_module: Optional[int], - max_depth: Optional[int], - additional_paths: Optional[str] + max_depth: Optional[int] ): """ Generate comprehensive documentation for a code repository. @@ -208,10 +218,6 @@ def generate_command( \b # Override max depth for hierarchical decomposition $ codewiki generate --max-depth 3 - - \b - # Include additional dependency paths (e.g., vendor, external libs) - $ codewiki generate --additional-paths "vendor/packages,external/deps" """ logger = create_logger(verbose=verbose) start_time = time.time() @@ -377,43 +383,6 @@ def generate_command( if instructions: logger.debug(f"Custom instructions: {instructions}") - # Validate and parse additional paths - additional_paths_list = None - if additional_paths: - if verbose: - logger.debug("🔍 Stage 1.3: Additional Paths Validation") - - additional_paths_list = parse_patterns(additional_paths) - - if verbose: - logger.debug(f"├─ Parsed {len(additional_paths_list)} additional path(s)") - for i, path in enumerate(additional_paths_list, 1): - logger.debug(f"│ ├─ Path #{i}: {path}") - - # Validate that paths exist - invalid_paths = [] - for path in additional_paths_list: - full_path = repo_path / path - if not full_path.exists(): - invalid_paths.append(path) - elif verbose: - logger.debug(f"│ └─ ✓ {path} (exists)") - - if invalid_paths: - if verbose: - for path in invalid_paths: - logger.debug(f"│ └─ ✗ {path} (not found)") - raise RepositoryError( - f"Additional paths not found:\n" - f" {', '.join(invalid_paths)}\n\n" - f"Please ensure all paths exist relative to repository root:\n" - f" Repository: {repo_path}\n" - f" Invalid paths: {invalid_paths}" - ) - - if verbose: - logger.debug(f"└─ All additional paths validated") - # Log max token settings if verbose (from upstream) if verbose: effective_max_tokens = max_tokens if max_tokens is not None else config.main_max_tokens @@ -424,8 +393,6 @@ def generate_command( logger.debug(f"Max token/module: {effective_max_token_per_module}") logger.debug(f"Max token/leaf module: {effective_max_token_per_leaf}") logger.debug(f"Max depth: {effective_max_depth}") - if additional_paths_list: - logger.debug(f"Additional paths: {additional_paths_list}") # Get agent instructions (merge runtime with persistent) (from upstream) agent_instructions_dict = None @@ -485,8 +452,6 @@ def generate_command( 'max_depth': max_depth if max_depth is not None else config.max_depth, # Agent instructions 'agent_instructions': agent_instructions_dict, - # Additional paths for dependency analysis - 'additional_paths': additional_paths_list, }, verbose=verbose, generate_html=github_pages, diff --git a/codewiki/src/.config.md b/codewiki/src/.config.md index 0f012db6..5304efae 100644 --- a/codewiki/src/.config.md +++ b/codewiki/src/.config.md @@ -1,16 +1,14 @@ ## config.py -Defines the central `Config` dataclass that drives the CodeWiki documentation-generation pipeline, encapsulating repository paths, output directories, per-provider LLM settings (models, API keys, base URLs, temperatures, token limits), and agent instruction customization (include/exclude patterns, focus modules, doc type, custom instructions). It also supports multi-path source analysis and safe serialization that excludes runtime secrets. +Defines the central `Config` dataclass that drives the CodeWiki documentation-generation pipeline, encapsulating repository paths, output directories, per-provider LLM settings (models, API keys, base URLs, temperatures, token limits), and agent instruction customization (include/exclude patterns, focus modules, doc type, custom instructions). It also validates the repository path and supports safe serialization that excludes runtime secrets. ## Key Components -- **`Config`** — Core dataclass holding repo/output paths, per-provider (`cluster`, `main`, `fallback`) model/API/temperature/token settings, `agent_instructions`, and optional `additional_source_paths` for multi-path mode. +- **`Config`** — Core dataclass holding repo/output paths, per-provider (`cluster`, `main`, `fallback`) model/API/temperature/token settings, and `agent_instructions`. - **`Config.to_dict()` / `Config.from_dict()`** — Serialize/deserialize a `Config`, excluding API key fields (`cluster_api_key`, `main_api_key`, `fallback_api_key`) by default via `_RUNTIME_ONLY_SECRET_FIELDS` to prevent leaking secrets into logs/caches. - **Properties** — `include_patterns`, `exclude_patterns`, `focus_modules`, `doc_type`, `custom_instructions`: convenience accessors reading from `agent_instructions`. -- **`all_source_paths`** — Returns absolute paths for `repo_path` plus any `additional_source_paths`. -- **`validate_source_paths()`** — Verifies primary and additional paths exist, are directories, and are readable; raises `ValueError`/`OSError` otherwise. -- **`is_multi_path_mode()`** — Returns whether multiple source paths are configured. +- **`validate_repo_path()`** — Verifies the repository path exists and is a directory; raises `ValueError` otherwise. - **`get_prompt_addition()`** — Builds LLM prompt text based on `doc_type` and other agent instructions. - **Constants** — `OUTPUT_BASE_DIR`, `DEPENDENCY_GRAPHS_DIR`, `DOCS_DIR`, `MAX_DEPTH`, default token limits (`DEFAULT_MAX_TOKENS`, `DEFAULT_MAX_TOKEN_PER_MODULE`, `DEFAULT_MAX_TOKEN_PER_LEAF_MODULE`), and filenames (`MODULE_TREE_FILENAME`, `OVERVIEW_FILENAME`). - **`set_cli_context()` / `is_cli_context()`** — Track whether the module is running in CLI vs. web-app context, affecting how LLM credentials are loaded. @@ -34,12 +32,8 @@ config = Config( fallback_api_key="sk-...", ) -# Validate all configured source paths exist and are readable -config.validate_source_paths() - -# Check multi-path mode -if config.is_multi_path_mode(): - print("Analyzing paths:", config.all_source_paths) +# Validate the repository path exists and is a directory +config.validate_repo_path() # Serialize safely (no secrets included) safe_dict = config.to_dict() diff --git a/codewiki/src/be/agent_orchestrator.py b/codewiki/src/be/agent_orchestrator.py index 5a2113b3..e6c8ab58 100644 --- a/codewiki/src/be/agent_orchestrator.py +++ b/codewiki/src/be/agent_orchestrator.py @@ -9,7 +9,18 @@ """ from pydantic_ai import Agent +from pydantic_ai.exceptions import ( + FallbackExceptionGroup, + ModelConnectionError, + ModelRateLimitError, + ModelRequestTimeout, + ModelServerError, + ModelUnavailableError, +) from pydantic_ai.usage import UsageLimits +import asyncio +import copy +from codewiki.src.be.usage_tracker import run_and_record # import logfire import logging import os @@ -20,6 +31,25 @@ logger = logging.getLogger(__name__) +# A module whose agent run fails on a passing provider problem (a dropped connection, a rate limit, an outage) is +# run again, after the SDK's own retries are spent: these waits outlast a short outage or a laptop's network coming +# back. Any other error fails the module at once. +MODULE_RUN_RETRY_DELAYS_S = (60, 300) +_TRANSIENT_MODEL_ERRORS = ( + ModelConnectionError, + ModelRateLimitError, + ModelRequestTimeout, + ModelServerError, + ModelUnavailableError, +) + + +def is_transient_model_error(error: BaseException) -> bool: + """Whether a failed agent run may succeed when run again: every model it tried failed on a passing problem.""" + if isinstance(error, FallbackExceptionGroup): + return all(is_transient_model_error(child) for child in error.exceptions) + return isinstance(error, _TRANSIENT_MODEL_ERRORS) + # try: # # Configure logfire with environment variables for Docker compatibility # logfire_token = os.getenv('LOGFIRE_TOKEN') @@ -50,6 +80,7 @@ from codewiki.src.be.agent_tools.deps import CodeWikiDeps from codewiki.src.be.agent_tools.read_code_components import read_code_components_tool from codewiki.src.be.agent_tools.str_replace_editor import str_replace_editor_tool +from codewiki.src.be.agent_tools.code_graph import code_graph_tools from codewiki.src.be.agent_tools.generate_sub_module_documentations import generate_sub_module_documentation_tool from codewiki.src.be.llm_services import create_fallback_models, reset_request_counter from codewiki.src.be.prompt_template import ( @@ -95,7 +126,8 @@ def create_agent(self, module_name: str, components: Dict[str, Any], tools=[ read_code_components_tool, str_replace_editor_tool, - generate_sub_module_documentation_tool + generate_sub_module_documentation_tool, + *code_graph_tools(), ], system_prompt=format_system_prompt(module_name, self.custom_instructions), retries=3, # From fork @@ -105,7 +137,7 @@ def create_agent(self, module_name: str, components: Dict[str, Any], self.fallback_models, name=module_name, deps_type=CodeWikiDeps, - tools=[read_code_components_tool, str_replace_editor_tool], + tools=[read_code_components_tool, str_replace_editor_tool, *code_graph_tools()], system_prompt=format_leaf_system_prompt(module_name, self.custom_instructions), retries=3, # From fork ) @@ -185,11 +217,31 @@ async def process_module(self, module_name: str, components: Dict[str, Node], logger.info("") try: - result = await agent.run( - user_prompt, - deps=deps, - usage_limits=UsageLimits(request_limit=1000), - ) + # A retry starts from the state the first attempt started with. Sub-modules a failed attempt finished stay + # finished (deps.finished_sub_modules), so they are neither written nor paid for twice. + start_state = (copy.deepcopy(deps.module_tree), list(deps.path_to_current_module), deps.current_module_name, deps.current_depth) + for attempt, delay in enumerate((*MODULE_RUN_RETRY_DELAYS_S, None), start=1): + if attempt > 1: + deps.module_tree = copy.deepcopy(start_state[0]) + deps.path_to_current_module[:] = start_state[1] + deps.current_module_name, deps.current_depth = start_state[2], start_state[3] + try: + result = await run_and_record( + agent, + module_name, + user_prompt, + deps=deps, + usage_limits=UsageLimits(request_limit=1000), + ) + break + except Exception as run_error: + if delay is None or not is_transient_model_error(run_error): + raise + logger.warning( + f"⏳ {module_name}: attempt {attempt} failed on a passing provider error " + f"({type(run_error).__name__}: {run_error}); running it again in {delay}s" + ) + await asyncio.sleep(delay) # Save updated module tree file_manager.save_json(deps.module_tree, module_tree_path) diff --git a/codewiki/src/be/agent_tools/code_graph.py b/codewiki/src/be/agent_tools/code_graph.py new file mode 100644 index 00000000..b9fcfe06 --- /dev/null +++ b/codewiki/src/be/agent_tools/code_graph.py @@ -0,0 +1,205 @@ +"""Code-graph agent tools served by the Flamingo hub. + +CodeWiki parses ONLY the repository it documents. Code that repository +imports from another repository is not on disk; the writing agents look it +up on demand through the hub's code-graph tools over HTTP: + +- ``GET {HUB_BASE_URL}/api/ci/code-documentation/tools?github=`` + lists the tools this repository may call (the set differs between public + and private repositories, so it is never hard-coded here), each with its + name, description and JSON Schema for its arguments. +- ``POST {HUB_BASE_URL}/api/ci/code-documentation/tools`` with + ``{"github", "tool", "args"}`` runs one and answers ``{"content": str}``. + The content is wrapped by the hub in a ``<<``. +Without ``HUB_BASE_URL``, ``WEBHOOK_SECRET`` or ``GITHUB_REPOSITORY``, or +when the list cannot be read, no graph tool is offered and documentation +runs on the repository alone. A failed call is reported to the model as a +failed tool result (``ToolFailed``), never raised into the agent run. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +from typing import Any, Dict, List, Optional +from urllib.parse import urlparse + +import httpx +from pydantic_ai import Tool +from pydantic_ai.exceptions import ToolFailed + +logger = logging.getLogger(__name__) + +TOOLS_PATH = "/api/ci/code-documentation/tools" +LIST_TIMEOUT_SECONDS = 30.0 +CALL_TIMEOUT_SECONDS = 60.0 +LOOPBACK_HOSTS = {"localhost", "127.0.0.1"} + +# Names of CodeWiki's own agent tools: a hub tool never replaces one. +LOCAL_TOOL_NAMES = {"read_code_components", "str_replace_editor", "generate_sub_module_documentation"} + +_cached_tools: Optional[List[Tool]] = None + + +class _HubSettings: + """What the hub calls need, read from the environment once.""" + + def __init__(self, base_url: str, secret: str, repository: str): + self.base_url = base_url.rstrip("/") + self.secret = secret + self.repository = repository + + @property + def endpoint(self) -> str: + return f"{self.base_url}{TOOLS_PATH}" + + @property + def headers(self) -> Dict[str, str]: + return {"Authorization": f"Bearer {self.secret}", "Accept": "application/json"} + + +def _read_settings() -> Optional[_HubSettings]: + base_url = (os.environ.get("HUB_BASE_URL") or "").strip() + secret = (os.environ.get("WEBHOOK_SECRET") or "").strip() + repository = (os.environ.get("GITHUB_REPOSITORY") or "").strip() + missing = [ + name + for name, value in ( + ("HUB_BASE_URL", base_url), + ("WEBHOOK_SECRET", secret), + ("GITHUB_REPOSITORY", repository), + ) + if not value + ] + if missing: + logger.info(f"Code-graph tools are off: {', '.join(missing)} not set") + return None + parsed = urlparse(base_url) + secure = parsed.scheme == "https" or (parsed.scheme == "http" and parsed.hostname in LOOPBACK_HOSTS) + if not secure or not parsed.hostname: + logger.warning( + "Code-graph tools are off: HUB_BASE_URL must be https " + "(http only for localhost or 127.0.0.1)" + ) + return None + return _HubSettings(base_url, secret, repository) + + +def _error_message(response: httpx.Response) -> str: + """The hub's `error` and `code`, else the HTTP status.""" + try: + body = response.json() + except ValueError: + body = None + if isinstance(body, dict) and (body.get("error") or body.get("code")): + code = body.get("code") or f"HTTP_{response.status_code}" + return f"{code}: {body.get('error') or 'request refused'}" + return f"HTTP_{response.status_code}: the hub refused the request" + + +def _make_tool(settings: _HubSettings, spec: Dict[str, Any]) -> Tool: + name = spec["name"] + + async def call_hub_tool(**args: Any) -> str: + payload = {"github": settings.repository, "tool": name, "args": args} + try: + async with httpx.AsyncClient(timeout=CALL_TIMEOUT_SECONDS) as client: + response = await client.post(settings.endpoint, json=payload, headers=settings.headers) + except httpx.TimeoutException: + raise ToolFailed(f"The code graph did not answer {name} within {int(CALL_TIMEOUT_SECONDS)} s. Continue without it.") + except httpx.HTTPError as error: + raise ToolFailed(f"The code graph could not be reached for {name} ({type(error).__name__}). Continue without it.") + if response.status_code >= 400: + message = _error_message(response) + logger.warning(f"Code-graph tool {name} failed: {message}") + raise ToolFailed(f"{name} failed: {message}") + try: + body = response.json() + except ValueError: + raise ToolFailed(f"{name} failed: the hub's answer was not JSON") + content = body.get("content") if isinstance(body, dict) else None + if not isinstance(content, str): + raise ToolFailed(f"{name} failed: the hub's answer had no content") + # One line per answered call, so a run's log shows what the writers looked up. + logger.info(f"Code-graph tool {name} answered ({len(content)} chars) for {json.dumps(args, sort_keys=True)[:200]}") + return content + + return Tool.from_schema( + function=call_hub_tool, + name=name, + description=spec.get("description") or None, + json_schema=spec["input_schema"], + ) + + +def _fetch_tools(settings: _HubSettings) -> List[Tool]: + try: + response = httpx.get( + settings.endpoint, + params={"github": settings.repository}, + headers=settings.headers, + timeout=LIST_TIMEOUT_SECONDS, + ) + except httpx.HTTPError as error: + logger.warning(f"Code-graph tools are off: the hub could not be reached ({type(error).__name__})") + return [] + if response.status_code >= 400: + logger.warning(f"Code-graph tools are off: listing them failed ({_error_message(response)})") + return [] + try: + body = response.json() + except ValueError: + logger.warning("Code-graph tools are off: the hub's tool list was not JSON") + return [] + specs = body.get("tools") if isinstance(body, dict) else None + if not isinstance(specs, list): + logger.warning("Code-graph tools are off: the hub's answer had no tool list") + return [] + + tools: List[Tool] = [] + for spec in specs: + if not ( + isinstance(spec, dict) + and isinstance(spec.get("name"), str) + and spec["name"] + and isinstance(spec.get("input_schema"), dict) + ): + logger.warning("Skipping a code-graph tool the hub described without a name or input schema") + continue + if spec["name"] in LOCAL_TOOL_NAMES: + logger.warning(f"Skipping code-graph tool {spec['name']}: CodeWiki has a tool of that name") + continue + tools.append(_make_tool(settings, spec)) + logger.info( + f"Code-graph tools on for {settings.repository}: " + f"{', '.join(tool.name for tool in tools) or 'none served'}" + ) + return tools + + +def code_graph_tools() -> List[Tool]: + """The hub's code-graph tools for this repository, read once per process.""" + global _cached_tools + if _cached_tools is None: + settings = _read_settings() + _cached_tools = _fetch_tools(settings) if settings else [] + return list(_cached_tools) + + +async def load_code_graph_tools() -> List[Tool]: + """Read the tool list once, in a worker thread, so the async pipeline never waits on the hub inside its loop. + + The pipeline calls this before it builds any agent; every later `code_graph_tools()` is a cache read. + """ + return await asyncio.to_thread(code_graph_tools) + + +def reset_code_graph_tools_cache() -> None: + """Forget the tool list read from the hub (tests).""" + global _cached_tools + _cached_tools = None diff --git a/codewiki/src/be/agent_tools/deps.py b/codewiki/src/be/agent_tools/deps.py index 6e51b9af..bc134b6e 100644 --- a/codewiki/src/be/agent_tools/deps.py +++ b/codewiki/src/be/agent_tools/deps.py @@ -5,7 +5,7 @@ that is passed to agent tools during the CodeWiki documentation generation pipeline. """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any from codewiki.src.be.dependency_analyzer.models.core import Node from codewiki.src.config import Config @@ -23,3 +23,6 @@ class CodeWikiDeps: current_depth: int config: Config # LLM configuration custom_instructions: str = None + # Sub-modules whose documentation run finished, by their path: a module run retried after a passing provider + # error does not write (and pay for) them again. + finished_sub_modules: set = field(default_factory=set) diff --git a/codewiki/src/be/agent_tools/generate_sub_module_documentations.py b/codewiki/src/be/agent_tools/generate_sub_module_documentations.py index aa4272e8..64d941b9 100644 --- a/codewiki/src/be/agent_tools/generate_sub_module_documentations.py +++ b/codewiki/src/be/agent_tools/generate_sub_module_documentations.py @@ -18,10 +18,12 @@ from pydantic_ai import RunContext, Tool, Agent from pydantic_ai.usage import UsageLimits +from codewiki.src.be.usage_tracker import run_and_record from codewiki.src.be.agent_tools.deps import CodeWikiDeps from codewiki.src.be.agent_tools.read_code_components import read_code_components_tool from codewiki.src.be.agent_tools.str_replace_editor import str_replace_editor_tool +from codewiki.src.be.agent_tools.code_graph import code_graph_tools from codewiki.src.be.llm_services import create_fallback_models from codewiki.src.be.prompt_template import SYSTEM_PROMPT, LEAF_SYSTEM_PROMPT, format_user_prompt, format_system_prompt, format_leaf_system_prompt from codewiki.src.be.utils import is_complex_module, count_tokens @@ -98,6 +100,10 @@ async def generate_sub_module_documentation( value[sub_module_name] = {"components": core_component_ids, "children": {}} for sub_module_name, core_component_ids in sub_module_specs.items(): + finished_key = (*deps.path_to_current_module, sub_module_name) + if finished_key in deps.finished_sub_modules: + logger.info(f" ✓ Sub-module {sub_module_name} was documented by an earlier attempt; not run again") + continue # Create visual indentation for nested modules indent = " " * deps.current_depth @@ -117,7 +123,7 @@ async def generate_sub_module_documentation( name=sub_module_name, deps_type=CodeWikiDeps, system_prompt=format_system_prompt(sub_module_name, ctx.deps.custom_instructions), - tools=[read_code_components_tool, str_replace_editor_tool, generate_sub_module_documentation_tool], + tools=[read_code_components_tool, str_replace_editor_tool, generate_sub_module_documentation_tool, *code_graph_tools()], retries=3, ) else: @@ -126,7 +132,7 @@ async def generate_sub_module_documentation( name=sub_module_name, deps_type=CodeWikiDeps, system_prompt=format_leaf_system_prompt(sub_module_name, ctx.deps.custom_instructions), - tools=[read_code_components_tool, str_replace_editor_tool], + tools=[read_code_components_tool, str_replace_editor_tool, *code_graph_tools()], retries=3, ) @@ -138,7 +144,9 @@ async def generate_sub_module_documentation( try: # FLAMINGO_PATCH: Added usage_limits to prevent "request_limit of 50" exceeded errors - result = await sub_agent.run( + await run_and_record( + sub_agent, + sub_module_name, format_user_prompt( module_name=deps.current_module_name, core_component_ids=core_component_ids, @@ -148,6 +156,7 @@ async def generate_sub_module_documentation( deps=ctx.deps, usage_limits=UsageLimits(request_limit=1000), ) + deps.finished_sub_modules.add(finished_key) finally: # remove the sub-module name from the path to current module and the module tree deps.path_to_current_module.pop() diff --git a/codewiki/src/be/agent_tools/str_replace_editor.py b/codewiki/src/be/agent_tools/str_replace_editor.py index b0319b4b..cc5da287 100644 --- a/codewiki/src/be/agent_tools/str_replace_editor.py +++ b/codewiki/src/be/agent_tools/str_replace_editor.py @@ -11,7 +11,6 @@ from collections import defaultdict from pathlib import Path from typing import List, Optional, Tuple, Literal -import io import logging @@ -26,9 +25,11 @@ # There are some super strange "ascii can't decode x" errors, -# that can be solved with setting the default encoding for stdout -# (note that python3.6 doesn't have the reconfigure method) -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") +# that can be solved with setting the default encoding for stdout. +# Reconfigure the stream in place: wrapping its buffer in a new TextIOWrapper +# closed the original stream when the wrapper was collected. +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") TRUNCATED_MESSAGE: str = "To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for." MAX_RESPONSE_LEN: int = 16000 diff --git a/codewiki/src/be/cluster_modules.py b/codewiki/src/be/cluster_modules.py index 7893cf71..b6755a60 100644 --- a/codewiki/src/be/cluster_modules.py +++ b/codewiki/src/be/cluster_modules.py @@ -67,7 +67,7 @@ def extract_package_hint(fqdn: str) -> str: Extract package/category from FQDN for human readability. Examples: - "...src.main.java.com.openframe.api.controller.Class" → "controller" + "...src.main.java.com.example.api.controller.Class" → "controller" "main-repo.src/models/device.py::DeviceModel" → "models" """ if '::' not in fqdn: @@ -331,6 +331,135 @@ def normalize_component_ids_by_lookup( return module_tree +# How many left-out components one placement call names, so its prompt stays small whatever the repository's size. +PLACEMENT_BATCH_SIZE = 400 + + +def _component_dir(fqdn: str, components: Dict[str, Node]) -> List[str]: + """The directory a component's file is in, as path segments.""" + node = components.get(fqdn) + path = getattr(node, 'relative_path', None) or fqdn.split('::')[0] + return [part for part in path.replace('\\', '/').split('/')[:-1] if part] + + +def _shared_prefix(a: List[str], b: List[str]) -> int: + n = 0 + for x, y in zip(a, b): + if x != y: + break + n += 1 + return n + + +def place_by_path(module_tree: Dict[str, Any], left_out: List[str], components: Dict[str, Node]) -> Dict[str, List[str]]: + """Each component goes to the module holding the component whose directory shares the longest path with its own. + + A tie goes to the larger module, then to the module named first. Deterministic and free: the net under the model's + placement, so no component is ever left without a module. + """ + module_dirs = { + name: [_component_dir(fqdn, components) for fqdn in info.get('components', [])] + for name, info in module_tree.items() + } + placed: Dict[str, List[str]] = defaultdict(list) + for fqdn in left_out: + own = _component_dir(fqdn, components) + best = max( + module_dirs, + key=lambda name: ( + max((_shared_prefix(own, d) for d in module_dirs[name]), default=0), + len(module_dirs[name]), + -list(module_dirs).index(name), + ), + ) + placed[best].append(fqdn) + return dict(placed) + + +def _ask_placement( + module_tree: Dict[str, Any], + left_out: List[str], + components: Dict[str, Node], + config: Config, +) -> Dict[str, List[str]]: + """Ask the clustering model which existing module each left-out component belongs in.""" + import json + + modules = [] + for name, info in module_tree.items(): + members = info.get('components', []) + sample = ', '.join(fqdn.split('::')[-1] for fqdn in members[:12]) + modules.append(f"- {name} (path: {info.get('path') or '-'}; {len(members)} components, e.g. {sample})") + placed: Dict[str, List[str]] = defaultdict(list) + for start in range(0, len(left_out), PLACEMENT_BATCH_SIZE): + batch = left_out[start:start + PLACEMENT_BATCH_SIZE] + lines = [] + for idx, fqdn in enumerate(batch): + node = components.get(fqdn) + lines.append(f"{idx}: {fqdn.split('::')[-1]} ({getattr(node, 'relative_path', None) or fqdn.split('::')[0]})") + prompt = ( + "The components of a repository were grouped into these modules:\n" + + "\n".join(modules) + + "\n\nThese components were not placed in any module yet. Each one is an integer ID, its name and its file:\n" + + "\n".join(lines) + + "\n\nPlace EVERY component above in the one existing module it belongs to. Use only the module names listed." + " Answer with a JSON object from module name to the list of bare integer IDs placed in it, wrapped in" + " and , with nothing else." + ) + try: + response = call_llm(prompt, config, model=config.cluster_model) + body = response.split('')[1].split('')[0] + answer = json.loads(body) + except Exception as e: # a failed batch is placed by path below + logger.warning(f" ⚠️ Placement call failed for {len(batch)} components: {type(e).__name__}: {e}") + continue + if not isinstance(answer, dict): + continue + batch_ids = dict(enumerate(batch)) + taken = set() + for name, ids in answer.items(): + if name not in module_tree: + logger.warning(f" \u274c Placement named module '{name}', which clustering did not create; its IDs are placed by path") + continue + if not isinstance(ids, list): + logger.warning(f" \u274c Placement for module '{name}' is not a list of IDs; placed by path") + continue + resolved, _, _ = normalize_component_id_list(ids, batch_ids, context=f"placement into '{name}'") + for fqdn in resolved: + if fqdn not in taken: + taken.add(fqdn) + placed[name].append(fqdn) + return dict(placed) + + +def place_left_out_components( + module_tree: Dict[str, Any], + leaf_nodes: List[str], + components: Dict[str, Node], + config: Config, +) -> int: + """Put every leaf node the clustering left out into a module: the model's choice first, then by path. + + The documentation covers only the components a module holds, so a component no module holds is never documented + (one repository's clustering placed 376 of its 1,077 components). Returns how many were placed. + """ + held = {fqdn for info in module_tree.values() for fqdn in info.get('components', [])} + left_out = [fqdn for fqdn in dict.fromkeys(leaf_nodes) if fqdn in components and fqdn not in held] + if not left_out or not module_tree: + return 0 + logger.info(f"🧩 {len(left_out)} of {len(left_out) + len(held)} components left out by clustering; placing them") + by_model = _ask_placement(module_tree, left_out, components, config) + for name, fqdns in by_model.items(): + module_tree[name].setdefault('components', []).extend(fqdns) + model_placed = {fqdn for fqdns in by_model.values() for fqdn in fqdns} + rest = [fqdn for fqdn in left_out if fqdn not in model_placed] + by_path = place_by_path(module_tree, rest, components) if rest else {} + for name, fqdns in by_path.items(): + module_tree[name].setdefault('components', []).extend(fqdns) + logger.info(f" └─ placed {len(model_placed)} by the model and {len(rest)} by file path") + return len(left_out) + + def cluster_modules( leaf_nodes: List[str], components: Dict[str, Node], @@ -449,6 +578,8 @@ def cluster_modules( logger.debug(f"Skipping clustering for {current_module_name} because the module tree is too small: {len(module_tree)} modules") return {} + place_left_out_components(module_tree, leaf_nodes, components, config) + if current_module_tree == {}: current_module_tree = module_tree else: diff --git a/codewiki/src/be/dependency_analyzer/.ast_parser.md b/codewiki/src/be/dependency_analyzer/.ast_parser.md index 1caf398a..1cf964d7 100644 --- a/codewiki/src/be/dependency_analyzer/.ast_parser.md +++ b/codewiki/src/be/dependency_analyzer/.ast_parser.md @@ -1,16 +1,14 @@ ## ast_parser.py -This module implements Stage 2 of the dependency analysis pipeline: parsing one or more repositories into Abstract Syntax Tree (AST)-derived structural and call-graph representations, then building a namespaced, fully-qualified dependency graph of code components (functions, classes, etc.) suitable for downstream clustering, summarization, and documentation generation. +This module implements Stage 2 of the dependency analysis pipeline: parsing the repository into Abstract Syntax Tree (AST)-derived structural and call-graph representations, then building a namespaced, fully-qualified dependency graph of code components (functions, classes, etc.) suitable for downstream clustering, summarization, and documentation generation. ## Key Components -- **`DependencyParser`**: Main class orchestrating repository parsing. Supports both single-path and multi-path (multiple repository) modes. - - `parse_repository(filtered_folders=None)`: Entry point that dispatches to single- or multi-repository parsing based on the number of paths provided. - - `_parse_single_repository(repo_path, filtered_folders=None)`: Analyzes structure and call graph for one repository, builds components without namespacing. - - `_parse_multiple_repositories(filtered_folders=None)`: Analyzes each repository, namespaces its components (using directory name as prefix), merges all components, and resolves cross-namespace dependency edges. - - `_get_namespace_from_path(repo_path)`: Derives a namespace string from a repository's directory name. - - `_build_namespaced_components(call_graph_result, namespace, namespace_mapping, repo_index)`: Constructs `Node` objects keyed by fully-qualified domain name (FQDN) in `namespace.original_id` format, tracking `short_id`, `namespace`, and `is_from_deps` metadata. +- **`DependencyParser`**: Main class orchestrating repository parsing. Only the repository itself is parsed; code it imports from other repositories is looked up by the writing agents through the hub's code-graph tools. + - `parse_repository(filtered_folders=None)`: Analyzes structure and call graph for the repository and builds its components. + - `_get_namespace_from_path(repo_path)`: Derives a namespace string from the repository's directory name. + - `_build_components_from_analysis(call_graph_result)`: Constructs `Node` objects keyed by fully-qualified domain name (FQDN) in `namespace.original_id` format, tracking `short_id` and `namespace` metadata. - **`Node`** (imported from `codewiki.src.be.dependency_analyzer.models.core`): Data model representing a single code component (function, class, etc.) with metadata such as source location, docstring, parameters, and FQDN identifiers. @@ -21,7 +19,6 @@ This module implements Stage 2 of the dependency analysis pipeline: parsing one ```python from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser -# Single repository mode parser = DependencyParser( repo_path="/path/to/repo", include_patterns=["*.py", "*.cs"], @@ -29,12 +26,6 @@ parser = DependencyParser( ) components = parser.parse_repository() -# Multi-repository mode (e.g., main app + dependency) -parser = DependencyParser( - repo_path=["/path/to/main-app", "/path/to/shared-lib"] -) -components = parser.parse_repository() - # Inspect resulting components for fqdn, node in components.items(): print(fqdn, node.component_type, node.namespace) diff --git a/codewiki/src/be/dependency_analyzer/.dependency_graphs_builder.md b/codewiki/src/be/dependency_analyzer/.dependency_graphs_builder.md index 8f792aaa..a9edaafb 100644 --- a/codewiki/src/be/dependency_analyzer/.dependency_graphs_builder.md +++ b/codewiki/src/be/dependency_analyzer/.dependency_graphs_builder.md @@ -6,10 +6,10 @@ This module provides the `DependencyGraphBuilder` class, which orchestrates the ## Key Components - **`DependencyGraphBuilder`** — Main class that drives dependency analysis. - - `__init__(config: Config)` — Initializes the builder with a `Config` object containing repo paths, include/exclude patterns, and output directories. + - `__init__(config: Config)` — Initializes the builder with a `Config` object containing the repository path, include/exclude patterns, and output directories. - `build_dependency_graph() -> tuple[Dict[str, Any], List[str]]` — Core method that: 1. Ensures the output directory exists and computes sanitized file paths for saving results. - 2. Supports single-path or multi-path repository analysis via `Config.all_source_paths`. + 2. Analyzes the repository at `Config.repo_path` only. 3. Uses `DependencyParser` to parse source files into components (respecting include/exclude patterns). 4. Logs a breakdown of parsed component types. 5. Saves the dependency graph to disk via `parser.save_dependency_graph`. diff --git a/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py b/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py index cdc2d0da..cf9d028b 100644 --- a/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py +++ b/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py @@ -245,6 +245,18 @@ def _analyze_structure( logger.debug("└─ Initializing RepoAnalyzer...") repo_analyzer = RepoAnalyzer(include_patterns, exclude_patterns) result = repo_analyzer.analyze_repository_structure(repo_dir) + skipped = repo_analyzer.third_party_files + if skipped: + by_reason: Dict[str, int] = {} + for reason in skipped.values(): + by_reason[reason] = by_reason.get(reason, 0) + 1 + logger.info( + f"🧾 Left out {len(skipped)} file(s) as someone else's code (" + + ", ".join(f"{n} {reason}" for reason, n in sorted(by_reason.items())) + + "): " + + ", ".join(sorted(skipped)[:20]) + + (f" and {len(skipped) - 20} more" if len(skipped) > 20 else "") + ) logger.debug(f" ├─ Files found: {result.get('summary', {}).get('total_files', 0)}") logger.debug(f" └─ Languages detected: {', '.join(result.get('summary', {}).get('languages', []))}") diff --git a/codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py b/codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py index e3d42a32..37a6aa1d 100644 --- a/codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py +++ b/codewiki/src/be/dependency_analyzer/analysis/repo_analyzer.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import Dict, List, Optional, Union from codewiki.src.be.dependency_analyzer.utils.patterns import DEFAULT_IGNORE_PATTERNS, DEFAULT_INCLUDE_PATTERNS +from codewiki.src.be.dependency_analyzer.utils.third_party import third_party_reason class RepoAnalyzer: @@ -31,6 +32,8 @@ def __init__( ) # Track namespaces when analyzing multiple paths self._namespace_roots: Dict[str, str] = {} # namespace -> repo_path + # Files left out as someone else's code (copied libraries, minified builds): path -> why. + self.third_party_files: Dict[str, str] = {} def analyze_repository_structure(self, repo_dir: Union[str, List[str]]) -> Dict: """ @@ -138,7 +141,7 @@ def _get_namespace_from_path(self, repo_path: str) -> str: repo_path: Path to repository Returns: - Namespace string (e.g., 'openframe-frontend', 'ui-kit') + Namespace string: the repository directory's name """ return os.path.basename(os.path.abspath(repo_path).rstrip(os.sep)) @@ -165,6 +168,10 @@ def build_tree(path: Path, base_path: Path) -> Optional[Dict]: if path.is_file(): if not self._should_include_file(relative_path_str, path.name): return None + reason = third_party_reason(path) + if reason: + self.third_party_files[relative_path_str] = reason + return None size = path.stat().st_size return { diff --git a/codewiki/src/be/dependency_analyzer/ast_parser.py b/codewiki/src/be/dependency_analyzer/ast_parser.py index cc32defb..513196d4 100644 --- a/codewiki/src/be/dependency_analyzer/ast_parser.py +++ b/codewiki/src/be/dependency_analyzer/ast_parser.py @@ -1,13 +1,14 @@ -"""AST parsing and dependency graph construction for multi-repository codebases. +"""AST parsing and dependency graph construction for a repository. This module implements the core dependency analysis pipeline stage that: -- Parses one or more repositories (single-path or multi-path modes) into - structural and call-graph representations using the AnalysisService. +- Parses the repository into structural and call-graph representations + using the AnalysisService. Only the repository itself is parsed: code it + imports from other repositories is looked up on demand by the writing + agents through the hub's code-graph tools (see agent_tools/code_graph.py). - Builds Node-based components keyed by fully-qualified domain names (FQDNs) - in the canonical `module.path::ComponentName` format. -- Namespaces components originating from multiple repositories to avoid ID - collisions and tracks module membership for each component. -- Resolves intra- and cross-namespace dependency edges between components. + in the canonical `module.path::ComponentName` format, namespaced by the + repository's directory name, and tracks module membership for each one. +- Resolves dependency edges between components. - Persists the resulting dependency graph to disk for downstream consumers (e.g., clustering, LLM-based summarization, and documentation generation). """ @@ -16,7 +17,7 @@ import logging import argparse from dataclasses import dataclass, field -from typing import Dict, List, Set, Tuple, Optional, Any, Union +from typing import Dict, List, Set, Tuple, Optional, Any from pathlib import Path import re @@ -28,24 +29,18 @@ class DependencyParser: - """Parser for extracting code components from multi-language repositories.""" + """Parser for extracting code components from a multi-language repository.""" - def __init__(self, repo_path: Union[str, List[str]], include_patterns: List[str] = None, exclude_patterns: List[str] = None): + def __init__(self, repo_path: str, include_patterns: List[str] = None, exclude_patterns: List[str] = None): """ Initialize the dependency parser. Args: - repo_path: Path to the repository or list of paths to multiple repositories + repo_path: Path to the repository include_patterns: File patterns to include (e.g., ["*.cs", "*.py"]) exclude_patterns: File/directory patterns to exclude (e.g., ["*Tests*"]) """ - # Support both single path (str) and multiple paths (List[str]) - if isinstance(repo_path, str): - self.repo_paths = [os.path.abspath(repo_path)] - self.repo_path = self.repo_paths[0] # Maintain backward compatibility - else: - self.repo_paths = [os.path.abspath(p) for p in repo_path] - self.repo_path = self.repo_paths[0] # Default to first path for backward compat + self.repo_path = os.path.abspath(repo_path) self.components: Dict[str, Node] = {} self.modules: Set[str] = set() @@ -56,8 +51,7 @@ def __init__(self, repo_path: Union[str, List[str]], include_patterns: List[str] def parse_repository(self, filtered_folders: List[str] = None) -> Dict[str, Node]: """ - Parse repository or repositories and extract components. - If multiple paths were provided, components are namespaced by source directory. + Parse the repository and extract components. Args: filtered_folders: Optional list of folders to filter @@ -65,20 +59,8 @@ def parse_repository(self, filtered_folders: List[str] = None) -> Dict[str, Node Returns: Dictionary mapping component IDs to Node objects """ + repo_path = self.repo_path logger.info("🔍 Stage 2: AST Parsing & Dependency Analysis") - logger.info(f"├─ Repository paths: {len(self.repo_paths)}") - - if len(self.repo_paths) == 1: - logger.info(f"│ └─ Single-path mode: {self.repo_paths[0]}") - # Single path - maintain backward compatibility - return self._parse_single_repository(self.repo_paths[0], filtered_folders) - else: - logger.info(f"│ └─ Multi-path mode: {len(self.repo_paths)} sources") - # Multiple paths - use multi-path parsing - return self._parse_multiple_repositories(filtered_folders) - - def _parse_single_repository(self, repo_path: str, filtered_folders: List[str] = None) -> Dict[str, Node]: - """Parse a single repository (backward compatible).""" logger.info(f"📂 Parsing repository: {repo_path}") # Log custom patterns if set @@ -111,89 +93,6 @@ def _parse_single_repository(self, repo_path: str, filtered_folders: List[str] = logger.info(f" └─ Total modules: {len(self.modules)}") return self.components - def _parse_multiple_repositories(self, filtered_folders: List[str] = None) -> Dict[str, Node]: - """ - Parse multiple repositories and merge components with namespace prefixes. - - Each repository gets a namespace prefix based on its directory name. - Component IDs are prefixed to avoid collisions: {namespace}::{original_id} - - Returns: - Dictionary of all components from all repositories with namespaced IDs - """ - logger.info(f"🔍 Multi-path mode detected: analyzing {len(self.repo_paths)} source directories") - logger.info(f" ├─ Primary: {self.repo_paths[0]}") - for i, path in enumerate(self.repo_paths[1:], 1): - logger.info(f" └─ Additional #{i}: {path}") - - all_components: Dict[str, Node] = {} - namespace_mapping: Dict[str, str] = {} # Maps original IDs to namespaced IDs - - for idx, repo_path in enumerate(self.repo_paths): - # Create namespace from directory name - namespace = self._get_namespace_from_path(repo_path) - logger.info(f"\n📂 Analyzing path {idx+1}/{len(self.repo_paths)}: {repo_path}") - logger.info(f" └─ Namespace: '{namespace}'") - - # Log custom patterns if set - if self.include_patterns: - logger.info(f"Using custom include patterns: {self.include_patterns}") - if self.exclude_patterns: - logger.info(f"Using custom exclude patterns: {self.exclude_patterns}") - - # Analyze structure - structure_result = self.analysis_service._analyze_structure( - repo_path, - include_patterns=self.include_patterns, - exclude_patterns=self.exclude_patterns - ) - - # Analyze call graph - call_graph_result = self.analysis_service._analyze_call_graph( - structure_result["file_tree"], - repo_path - ) - - # Build components with namespace (idx=0 is main repo, idx>0 is deps) - repo_components = self._build_namespaced_components( - call_graph_result, - namespace, - namespace_mapping, - repo_index=idx - ) - - # Merge into all_components - all_components.update(repo_components) - - logger.info(f"📊 Namespace '{namespace}': found {len(repo_components)} components") - - # Log component type breakdown for this namespace - type_counts = {} - for comp in repo_components.values(): - comp_type = comp.component_type - type_counts[comp_type] = type_counts.get(comp_type, 0) + 1 - - if type_counts: - logger.info(f" └─ Component types:") - for comp_type, count in sorted(type_counts.items(), key=lambda x: x[1], reverse=True): - logger.info(f" • {comp_type}: {count}") - - # Update cross-repository dependencies - logger.info(f"\n🔗 Resolving cross-namespace dependencies...") - cross_deps_count = self._resolve_cross_namespace_dependencies(all_components, namespace_mapping) - if cross_deps_count > 0: - logger.info(f" └─ ✓ Resolved {cross_deps_count} cross-namespace dependencies") - else: - logger.info(f" └─ No cross-namespace dependencies found") - - self.components = all_components - logger.info(f"\n📊 Multi-path analysis complete:") - logger.info(f" ├─ Total components: {len(all_components)}") - logger.info(f" ├─ Total modules: {len(self.modules)}") - logger.info(f" └─ Namespaces: {len(self.repo_paths)}") - - return self.components - def _get_namespace_from_path(self, repo_path: str) -> str: """ Generate a namespace prefix from a repository path. @@ -202,151 +101,11 @@ def _get_namespace_from_path(self, repo_path: str) -> str: repo_path: Absolute path to repository Returns: - Namespace string (e.g., 'openframe-frontend', 'ui-kit') + Namespace string: the repository directory's name """ # Use the last directory name as namespace return os.path.basename(repo_path.rstrip(os.sep)) - def _build_namespaced_components( - self, - call_graph_result: Dict, - namespace: str, - namespace_mapping: Dict[str, str], - repo_index: int = 0 - ) -> Dict[str, Node]: - """ - Build components with namespace prefixes. - - Args: - call_graph_result: Result from call graph analysis - namespace: Namespace prefix for this repository - namespace_mapping: Global mapping of original IDs to namespaced IDs - repo_index: Index of this repository (0 = main, >0 = deps) - - Returns: - Dictionary of namespaced components - """ - functions = call_graph_result.get("functions", []) - relationships = call_graph_result.get("relationships", []) - - components: Dict[str, Node] = {} - - # First pass: Create components with namespaced IDs - for func_dict in functions: - original_id = func_dict.get("id", "") - if not original_id: - continue - - # Create FQDN (namespaced component ID) using '::' to separate - # the namespace/module path from the component identifier - fqdn = f"{namespace}.{original_id}" - - # Store mapping for dependency resolution - namespace_mapping[original_id] = fqdn - - # Create node with FQDN as id and metadata fields - node = Node( - id=fqdn, # FQDN as primary identifier - name=func_dict.get("name", ""), - component_type=func_dict.get("component_type", func_dict.get("node_type", "function")), - file_path=func_dict.get("file_path", ""), - relative_path=func_dict.get("relative_path", ""), - source_code=func_dict.get("source_code", func_dict.get("code_snippet", "")), - start_line=func_dict.get("start_line", 0), - end_line=func_dict.get("end_line", 0), - has_docstring=func_dict.get("has_docstring", bool(func_dict.get("docstring", ""))), - docstring=func_dict.get("docstring", "") or "", - parameters=func_dict.get("parameters", []), - node_type=func_dict.get("node_type", "function"), - base_classes=func_dict.get("base_classes"), - class_name=func_dict.get("class_name"), - display_name=func_dict.get("display_name", ""), - component_id=fqdn, - # FQDN metadata fields - short_id=original_id, - namespace=namespace, - is_from_deps=(repo_index > 0) - ) - - # Dictionary key is FQDN - components[fqdn] = node - - # Track module (with namespace) - # original_id is '::' from the analyzers, so the - # module path is everything before '::'. (The dot-split fallback is - # for ids that predate the '::' separator.) - module_path = ( - original_id.split("::")[0] - if "::" in original_id - else ".".join(original_id.split(".")[:-1]) - ) - if module_path: - self.modules.add(f"{namespace}.{module_path}") - - # Second pass: Add dependencies within this namespace - for rel_dict in relationships: - caller_id = rel_dict.get("caller", "") - callee_id = rel_dict.get("callee", "") - - # Map to namespaced IDs - namespaced_caller = namespace_mapping.get(caller_id) - namespaced_callee = namespace_mapping.get(callee_id) - - if namespaced_caller and namespaced_caller in components: - if namespaced_callee: - components[namespaced_caller].depends_on.add(namespaced_callee) - - return components - - def _resolve_cross_namespace_dependencies( - self, - all_components: Dict[str, Node], - namespace_mapping: Dict[str, str] - ) -> int: - """ - Resolve dependencies that cross namespace boundaries. - - This handles cases where one component in namespace A depends on - a component in namespace B. - - Args: - all_components: All components from all repositories - namespace_mapping: Mapping of original IDs to namespaced IDs - - Returns: - Number of cross-namespace dependencies resolved - """ - cross_deps_resolved = 0 - - # For each component, check if any dependencies need cross-namespace resolution - for component_id, component in sorted(all_components.items()): # ✅ SORT for determinism - resolved_deps = set() - - for dep_id in component.depends_on: - # If dependency exists in all_components, it's already resolved - if dep_id in all_components: - resolved_deps.add(dep_id) - else: - # Try to find it by name match across namespaces - dep_name = dep_id.split(".")[-1] - for other_id, other_component in sorted(all_components.items()): # ✅ SORT for determinism - if other_component.name == dep_name and other_id != component_id: - # Extract namespaces to check if it's cross-namespace - source_namespace = component_id.split(".")[0] - target_namespace = other_id.split(".")[0] - if source_namespace != target_namespace: - logger.debug(f" ├─ Cross-namespace dependency: {component_id} → {other_id}") - cross_deps_resolved += 1 - resolved_deps.add(other_id) - break - else: - # Keep original if not resolved - resolved_deps.add(dep_id) - - component.depends_on = resolved_deps - - return cross_deps_resolved - def _build_components_from_analysis(self, call_graph_result: Dict): functions = call_graph_result.get("functions", []) relationships = call_graph_result.get("relationships", []) @@ -383,8 +142,7 @@ def _build_components_from_analysis(self, call_graph_result: Dict): component_id=fqdn, # FQDN metadata fields short_id=original_id, - namespace=namespace, - is_from_deps=False # Single-path mode = main repo + namespace=namespace ) # Dictionary key is FQDN diff --git a/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py b/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py index 96d7abe7..87d8847b 100644 --- a/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py +++ b/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py @@ -49,20 +49,9 @@ def build_dependency_graph(self) -> tuple[Dict[str, Any], List[str]]: include_patterns = self.config.include_patterns if self.config.include_patterns else None exclude_patterns = self.config.exclude_patterns if self.config.exclude_patterns else None - # Build list of paths to analyze - repo_paths = self.config.all_source_paths - - # Log multi-path mode if enabled - if self.config.is_multi_path_mode(): - logger.info(f"🔍 Multi-path mode enabled: analyzing {len(repo_paths)} source paths") - logger.info(f" ├─ Primary: {repo_paths[0]}") - for i, path in enumerate(repo_paths[1:], 1): - logger.info(f" └─ Additional #{i}: {path}") - else: - logger.info(f"📁 Single-path mode: analyzing {repo_paths[0]}") - + logger.info(f"📁 Analyzing repository: {self.config.repo_path}") parser = DependencyParser( - repo_paths if len(repo_paths) > 1 else repo_paths[0], + self.config.repo_path, include_patterns=include_patterns, exclude_patterns=exclude_patterns ) diff --git a/codewiki/src/be/dependency_analyzer/models/.core.md b/codewiki/src/be/dependency_analyzer/models/.core.md index cb41a6f3..4da5a06e 100644 --- a/codewiki/src/be/dependency_analyzer/models/.core.md +++ b/codewiki/src/be/dependency_analyzer/models/.core.md @@ -5,7 +5,7 @@ Defines the core Pydantic data models used throughout the dependency analysis pi ## Key Components -- **`Node`**: Represents a single code entity (function, class, module, etc.) discovered during analysis. Uses a fully-qualified domain name (FQDN) format (`{namespace}.{original_id}`) for its `id` to disambiguate entities across the main repository and its dependencies. Tracks source location (`file_path`, `start_line`, `end_line`), documentation (`has_docstring`, `docstring`), structural metadata (`component_type`, `class_name`, `base_classes`, `parameters`), and outgoing dependencies (`depends_on`). Includes FQDN-related fields (`short_id`, `namespace`, `is_from_deps`) for display and provenance, all defaulted for backward compatibility. +- **`Node`**: Represents a single code entity (function, class, module, etc.) discovered during analysis. Uses a fully-qualified domain name (FQDN) format (`{namespace}.{original_id}`) for its `id`, namespaced by the repository's directory name. Tracks source location (`file_path`, `start_line`, `end_line`), documentation (`has_docstring`, `docstring`), structural metadata (`component_type`, `class_name`, `base_classes`, `parameters`), and outgoing dependencies (`depends_on`). Includes FQDN-related fields (`short_id`, `namespace`) for display and provenance, all defaulted for backward compatibility. - `get_display_name()`: Returns `display_name` if set, otherwise falls back to `name`. - **`CallRelationship`**: Represents a directed call-graph edge between two nodes, identified by `caller` and `callee` IDs. Optionally records the source `call_line` and whether the relationship was successfully `is_resolved` to a known node. @@ -26,7 +26,6 @@ node = Node( relative_path="utils.py", short_id="utils.parse_config", namespace="main", - is_from_deps=False, depends_on={"main.utils.load_file"}, ) diff --git a/codewiki/src/be/dependency_analyzer/models/core.py b/codewiki/src/be/dependency_analyzer/models/core.py index 3902c374..eea675ee 100644 --- a/codewiki/src/be/dependency_analyzer/models/core.py +++ b/codewiki/src/be/dependency_analyzer/models/core.py @@ -46,8 +46,7 @@ class Node(BaseModel): # FQDN metadata fields (all have defaults for backward compatibility) short_id: str = "" # Original ID without namespace (for display) - namespace: str = "" # Namespace (e.g., "main", "deps", "ui-kit") - is_from_deps: bool = False # True if from dependencies, False if from main repo + namespace: str = "" # Namespace: the repository's directory name def get_display_name(self) -> str: return self.display_name or self.name diff --git a/codewiki/src/be/dependency_analyzer/utils/patterns.py b/codewiki/src/be/dependency_analyzer/utils/patterns.py index 9fb003f7..defec38f 100644 --- a/codewiki/src/be/dependency_analyzer/utils/patterns.py +++ b/codewiki/src/be/dependency_analyzer/utils/patterns.py @@ -119,9 +119,16 @@ # Documentation ".docusaurus", # Other common patterns - ## Minified files + ## Minified files (any other minified file is found by its form: utils/third_party.py) "*.min.js", "*.min.css", + ## Folders that hold other projects' code (utils/third_party.py's THIRD_PARTY_DIRECTORIES) + "vendor", + "vendors", + "third_party", + "third-party", + "thirdparty", + "bower_components", ## Source maps "*.map", ## Terraform diff --git a/codewiki/src/be/dependency_analyzer/utils/third_party.py b/codewiki/src/be/dependency_analyzer/utils/third_party.py new file mode 100644 index 00000000..2e37b02e --- /dev/null +++ b/codewiki/src/be/dependency_analyzer/utils/third_party.py @@ -0,0 +1,60 @@ +""" +Which files are someone else's code: copied libraries and build output, never the repository's own source. + +A repository that ships its front end's libraries beside its own scripts (jQuery, Bootstrap, Chart.js, Font +Awesome, a minified copy of each) had them clustered and documented as its own modules: most of one run's 143 +modules, and its only failures (a 1.2M-token library no model can read). The rules are about a file's FORM and +LOCATION, never a list of library names: + +- minified: a `.js` / `.css` file whose average line is longer than 110 characters, GitHub Linguist's rule for + minified files (a person's code averages well under that; one repository's own files averaged 21 to 78), unless + it also holds at least 200 ordinary lines (non-empty, at most 110 characters): a hand-written file with one long + embedded data line has thousands of them and is code, while minified libraries have a few dozen at most + (OpenLayers 33, each Bootstrap theme 11); +- a library banner: the file opens with a `/*!` comment, the form minifiers and library builds keep for the + library's license (`/*! jQuery v3.7.1`, `/*! Bootstrap v5.3.3`); a project's own files open with `/**` or `//`; +- a vendored folder (`vendor`, `third_party`, `bower_components`, …): matched by the walk's own path patterns. + +A file the rules cannot read is kept: leaving out code the repository wrote is worse than documenting a library. +""" +from __future__ import annotations + +from pathlib import Path +from typing import Optional + +MINIFIED_EXTENSIONS = {'.js', '.mjs', '.cjs', '.css'} +# GitHub Linguist's threshold for a minified file (average characters per line). +MINIFIED_AVERAGE_LINE_LENGTH = 110 +# A file this many ordinary lines (non-empty, at most the threshold above) is hand-written whatever its average. +HAND_WRITTEN_MIN_LINES = 200 +# How much of a file the ordinary lines are counted in (a long embedded line can fill the first sample alone). +COUNT_BYTES = 16 * 1024 * 1024 +# What is read of a file to judge it: enough for any banner and a representative average. +SAMPLE_BYTES = 256 * 1024 +# Folders that hold other projects' code by convention (Linguist's vendored directories). +THIRD_PARTY_DIRECTORIES = ('vendor', 'vendors', 'third_party', 'third-party', 'thirdparty', 'bower_components') + + +def third_party_reason(path: Path) -> Optional[str]: + """Why a file is someone else's code (`minified`, `library banner`), or None for the repository's own.""" + suffix = path.suffix.lower() + if suffix not in MINIFIED_EXTENSIONS: + return None + try: + with open(path, 'rb') as handle: + sample = handle.read(SAMPLE_BYTES) + except OSError: + return None + text = sample.decode('utf-8', errors='replace').lstrip('') + if text.lstrip().startswith('/*!'): + return 'library banner' + lines = text.split('\n') + if len(text) / max(1, len(lines)) <= MINIFIED_AVERAGE_LINE_LENGTH: + return None + try: + with open(path, 'rb') as handle: + whole = handle.read(COUNT_BYTES).decode('utf-8', errors='replace') + except OSError: + return None + ordinary = sum(1 for line in whole.split('\n') if line.strip() and len(line) <= MINIFIED_AVERAGE_LINE_LENGTH) + return None if ordinary >= HAND_WRITTEN_MIN_LINES else 'minified' diff --git a/codewiki/src/be/documentation_generator.py b/codewiki/src/be/documentation_generator.py index ae3c7634..08875cb0 100644 --- a/codewiki/src/be/documentation_generator.py +++ b/codewiki/src/be/documentation_generator.py @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) # Local imports +from codewiki.src.be.agent_tools.code_graph import load_code_graph_tools from codewiki.src.be.dependency_analyzer import DependencyGraphBuilder from codewiki.src.be.llm_services import call_llm from codewiki.src.be.prompt_template import ( @@ -176,6 +177,9 @@ async def generate_module_documentation(self, components: Dict[str, Any], leaf_n """Generate documentation for all modules using dynamic programming approach.""" logger.info("📝 Stage 3.1: Module Documentation Generation") + # The code-graph tool list, read once off the event loop before any agent is built. + await load_code_graph_tools() + # Prepare output directory working_dir = os.path.abspath(self.config.docs_dir) file_manager.ensure_directory(working_dir) diff --git a/codewiki/src/be/flamingo_guidelines.py b/codewiki/src/be/flamingo_guidelines.py index 97761d68..304e4fac 100644 --- a/codewiki/src/be/flamingo_guidelines.py +++ b/codewiki/src/be/flamingo_guidelines.py @@ -379,3 +379,54 @@ def get_custom_instructions_section() -> str: "\n\n---\n" ) + + +ECOSYSTEM_FACTS_ENV_VAR = "ECOSYSTEM_FACTS_PATH" + + +def load_ecosystem_facts() -> str: + """ + The repository's ecosystem facts, generated by the hub from the code graph (`ecosystem.md`: the + artifacts it publishes and consumes with their pinned versions, who consumes it, its delivery chain). + + The Code Documentation workflow downloads them before this stage ("Fetch ecosystem facts") and names + the file in ECOSYSTEM_FACTS_PATH. The same facts reach every other documentation stage and the code + reviewer inline, so CodeWiki's writers start from them instead of each asking the graph for them again. + Optional: a repository with no graph yet has none. + """ + facts_path = os.environ.get(ECOSYSTEM_FACTS_ENV_VAR) + if not facts_path: + logger.info(f"[CodeWiki] {ECOSYSTEM_FACTS_ENV_VAR} not set - continuing without ecosystem facts") + return "" + try: + path = Path(facts_path) + if not path.exists(): + logger.info(f"[CodeWiki] No ecosystem facts at {facts_path} (no code graph for this repository yet)") + return "" + content = path.read_text(encoding="utf-8").strip() + logger.info(f"[CodeWiki] Loaded ecosystem facts from the code graph ({len(content)} chars)") + return content + except Exception as e: + logger.warning(f"[CodeWiki] Failed to load ecosystem facts: {e}") + return "" + + +ECOSYSTEM_FACTS = load_ecosystem_facts() + + +def get_ecosystem_facts_section() -> str: + """ + The ecosystem facts as a prompt section, or empty when there are none. Verbatim, braces included: the + prompts are filled by exact-placeholder replacement, never `.format()`, so escaping would hand the model + doubled braces in `${VAR}`-style values. + """ + if not ECOSYSTEM_FACTS: + return "" + return ( + "\n" + "Generated from the code graph: the ground truth for which artifacts this repository publishes, " + "which it depends on and at which versions, and which repositories consume it. Never contradict " + "these facts or invent a relationship they do not list.\n\n" + + ECOSYSTEM_FACTS + + "\n\n\n" + ) diff --git a/codewiki/src/be/llm_services.py b/codewiki/src/be/llm_services.py index 058aba98..911da85f 100644 --- a/codewiki/src/be/llm_services.py +++ b/codewiki/src/be/llm_services.py @@ -15,12 +15,21 @@ from pydantic_ai.models.openai import OpenAIModel from pydantic_ai.models.openai import OpenAIModelSettings -logger = logging.getLogger(__name__) +import re +from urllib.parse import urlparse + +from pydantic_ai.models import Model +from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings +from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings +from pydantic_ai.providers.anthropic import AnthropicProvider from pydantic_ai.providers.openai import OpenAIProvider + +logger = logging.getLogger(__name__) from pydantic_ai.models.fallback import FallbackModel from openai import OpenAI, OpenAIError from codewiki.src.config import Config +from codewiki.src.be.usage_tracker import record_completion # Global request counter for logging progress @@ -85,122 +94,83 @@ def get_model_max_token_field(stage: str = 'generation') -> str: return os.environ.get(env_var, 'max_tokens') -def create_main_model(config: Config) -> OpenAIModel: - """ - Create the main LLM model from configuration. +# Every SDK call retries a dropped connection, a rate limit and a 5xx on its own (backoff by the SDK), before +# the agent run sees an error at all. A run that still fails is retried whole by the orchestrator. +AGENT_MODEL_MAX_RETRIES = 6 - NOTE: Pydantic AI currently hardcodes the 'max_tokens' parameter name in OpenAIModelSettings. - For reasoning models (o3, o3-mini) that require 'max_completion_tokens', the direct API call - in call_llm() uses the correct parameter name. If Pydantic AI models fail with "Unrecognized - request argument supplied: max_tokens", the system will fall back to the direct API call. - """ - # Use per-provider max_tokens - max_tokens = getattr(config, 'main_max_tokens', None) or get_max_output_tokens() - # Check if model supports custom temperature (use per-provider field) - temperature = getattr(config, 'main_temperature', 0.0) - temperature_supported = getattr(config, 'main_temperature_supported', True) - # Build settings dict - only include temperature if model supports it - settings_dict = {'max_tokens': max_tokens} - if temperature_supported: - settings_dict['temperature'] = temperature +def _agent_api(base_url: str) -> str: + """Which wire API an agent model is called through, from its provider's host. - # Build provider with per-provider base_url and optional api_version header - base_url = getattr(config, 'main_base_url', None) + - Anthropic (`api.anthropic.com`): the native Messages API, so the agent's repeated prefix (instructions, tool + definitions, the conversation so far) is read from the prompt cache at a tenth of the input price. Through the + OpenAI-compatible endpoint nothing was cached: 6.8M input tokens and 0 cache reads on one repository. + - OpenAI (`api.openai.com`): the Responses API. Reasoning models refuse function tools on chat completions + ("Function tools with reasoning_effort are not supported ... use /v1/responses"), so every agent request + failed there and fell through to the fallback model. + - Any other host: OpenAI-compatible chat completions, as before. + """ + host = (urlparse(base_url).hostname or '').lower() + if host == 'anthropic.com' or host.endswith('.anthropic.com'): + return 'anthropic' + if host == 'api.openai.com': + return 'responses' + return 'chat' + + +def _create_agent_model(stage: str, model_name: str, config: Config) -> Model: + """One agent model (`main` or `fallback`), on the API its provider's host calls for.""" + max_tokens = getattr(config, f'{stage}_max_tokens', None) or get_max_output_tokens() + temperature = getattr(config, f'{stage}_temperature', 0.0) + temperature_supported = getattr(config, f'{stage}_temperature_supported', True) + base_url = getattr(config, f'{stage}_base_url', None) + api_key = getattr(config, f'{stage}_api_key', None) if not base_url: raise ValueError( - "main_base_url is required in configuration for main/generation model.\n" - f"Model: {config.main_model}\n" - "Please set via CLI: --main-base-url \n" - "Or in config file: main_base_url = ''" + f"{stage}_base_url is required in configuration for the {stage} model.\n" + f"Model: {model_name}\n" + f"Please set via CLI: --{stage}-base-url " ) - - # Prepare default headers for API version (Anthropic models) - default_headers = {} - api_version = getattr(config, 'main_api_version', None) - if api_version: - default_headers['anthropic-version'] = api_version - - # Get per-provider API key - api_key = getattr(config, 'main_api_key', None) if not api_key: raise ValueError( - "main_api_key is required in configuration for main/generation model.\n" - f"Model: {config.main_model}\n" - "Please set via CLI: --main-api-key \n" - "Different AI providers require different API keys." + f"{stage}_api_key is required in configuration for the {stage} model.\n" + f"Model: {model_name}\n" + f"Please set via CLI: --{stage}-api-key " ) - return OpenAIModel( - model_name=config.main_model, - provider=OpenAIProvider( - base_url=base_url, - api_key=api_key, - # NOTE: pydantic-ai's OpenAIProvider takes only base_url, api_key, - # openai_client and http_client - there is no default_headers - # parameter (verified against pydantic-ai 2.40.0), so passing one - # raises TypeError. To send anthropic-version here, build an - # AsyncOpenAI client with default_headers and pass it as - # openai_client=. - ), - settings=OpenAIModelSettings(**settings_dict) - ) - - -def create_fallback_model(config: Config) -> OpenAIModel: - """Create the fallback LLM model from configuration.""" - # Use per-provider max_tokens - max_tokens = getattr(config, 'fallback_max_tokens', None) or get_max_output_tokens() - # Check if model supports custom temperature (use per-provider field) - temperature = getattr(config, 'fallback_temperature', 0.0) - temperature_supported = getattr(config, 'fallback_temperature_supported', True) - - # Build settings dict - only include temperature if model supports it - settings_dict = {'max_tokens': max_tokens} + settings: dict = {'max_tokens': max_tokens, 'max_retries': AGENT_MODEL_MAX_RETRIES} if temperature_supported: - settings_dict['temperature'] = temperature - - # Build provider with per-provider base_url and optional api_version header - base_url = getattr(config, 'fallback_base_url', None) - if not base_url: - raise ValueError( - "fallback_base_url is required in configuration for fallback model.\n" - f"Model: {config.fallback_model}\n" - "Please set via CLI: --fallback-base-url \n" - "Or in config file: fallback_base_url = ''" + settings['temperature'] = temperature + + api = _agent_api(base_url) + logger.info(f"🔌 {stage} model {model_name}: {api} API at {base_url}") + if api == 'anthropic': + # The SDK appends /v1/messages itself, so the configured `.../v1` is the host. + anthropic_base = re.sub(r'/v1/?$', '', base_url.rstrip('/')) + return AnthropicModel( + model_name, + provider=AnthropicProvider(api_key=api_key, base_url=anthropic_base), + settings=AnthropicModelSettings( + **settings, + anthropic_cache_instructions=True, + anthropic_cache_tool_definitions=True, + anthropic_cache=True, + ), ) + provider = OpenAIProvider(base_url=base_url, api_key=api_key) + if api == 'responses': + return OpenAIResponsesModel(model_name, provider=provider, settings=OpenAIResponsesModelSettings(**settings)) + return OpenAIModel(model_name=model_name, provider=provider, settings=OpenAIModelSettings(**settings)) - # Prepare default headers for API version (Anthropic models) - default_headers = {} - api_version = getattr(config, 'fallback_api_version', None) - if api_version: - default_headers['anthropic-version'] = api_version - # Get per-provider API key - api_key = getattr(config, 'fallback_api_key', None) - if not api_key: - raise ValueError( - "fallback_api_key is required in configuration for fallback model.\n" - f"Model: {config.fallback_model}\n" - "Please set via CLI: --fallback-api-key \n" - "Different AI providers require different API keys." - ) +def create_main_model(config: Config) -> Model: + """The main agent model (module documentation).""" + return _create_agent_model('main', config.main_model, config) - return OpenAIModel( - model_name=config.fallback_model, - provider=OpenAIProvider( - base_url=base_url, - api_key=api_key, - # NOTE: pydantic-ai's OpenAIProvider takes only base_url, api_key, - # openai_client and http_client - there is no default_headers - # parameter (verified against pydantic-ai 2.40.0), so passing one - # raises TypeError. To send anthropic-version here, build an - # AsyncOpenAI client with default_headers and pass it as - # openai_client=. - ), - settings=OpenAIModelSettings(**settings_dict) - ) +def create_fallback_model(config: Config) -> Model: + """The model an agent request falls back to when the main model fails.""" + return _create_agent_model('fallback', config.fallback_model, config) def create_cluster_model(config: Config) -> OpenAIModel: @@ -446,6 +416,7 @@ def call_llm( logger.info(f" └─ 🚀 Sending request to LLM API...") response = client.chat.completions.create(**kwargs) + record_completion(kwargs.get("model") or model or "unknown", response) response_content = response.choices[0].message.content response_tokens = len(response_content) // 4 # Approximate token count diff --git a/codewiki/src/be/prompt_template.py b/codewiki/src/be/prompt_template.py index dda93534..3da0d1dd 100644 --- a/codewiki/src/be/prompt_template.py +++ b/codewiki/src/be/prompt_template.py @@ -2,13 +2,33 @@ from codewiki.src.be.flamingo_guidelines import ( get_guidelines_section, get_custom_instructions_section, - get_validation_rules_section + get_validation_rules_section, + get_ecosystem_facts_section, ) # Get sections for prompt injection (empty string if not available) _CUSTOM_INSTRUCTIONS_SECTION = get_custom_instructions_section() _GUIDELINES_SECTION = get_guidelines_section() _VALIDATION_RULES_SECTION = get_validation_rules_section() +# The served code-graph facts about this repository's dependencies (`ecosystem.md`), the ones every other +# documentation stage and the code reviewer get inline. +_ECOSYSTEM_FACTS_SECTION = get_ecosystem_facts_section() + + +# Code from other repositories is NOT parsed or copied: the writing agents look +# it up through the hub's code-graph tools (agent_tools/code_graph.py). Plain +# text with no braces, so it is safe inside the f-string prompts below. +_CODE_GRAPH_TOOLS_SECTION = """ +Code this repository imports from another repository is NOT on disk and is not among the components you can read. +When code-graph tools are available, look such code up instead of guessing: +- `resolve_code_imports`: what an import refers to and which repository defines it +- `get_code_symbol`: a definition, its signature and its callers +- `find_code_symbols`: the definitions under a package, module or crate (`com.example.data`, `@scope/pkg`), whichever repository holds them +Which artifacts this repository publishes and consumes, at which versions, and who consumes it, are in ECOSYSTEM_FACTS when present: read them there rather than asking for them again. +Document how THIS repository uses that code, never the other repository's internals. +Tool results arrive wrapped in a `<<""" def format_module_display_name(module_name: str) -> str: @@ -360,7 +380,10 @@ def format_module_display_name(module_name: str) -> str: - `str_replace_editor`: File system operations for creating and editing documentation files - `read_code_components`: Explore additional code dependencies not included in the provided components - `generate_sub_module_documentation`: Generate detailed documentation for individual sub-modules via sub-agents +- Code-graph tools (when available): look up code this repository imports from other repositories +{_CODE_GRAPH_TOOLS_SECTION} +{_ECOSYSTEM_FACTS_SECTION} {{custom_instructions}} """.strip() @@ -552,7 +575,10 @@ def format_module_display_name(module_name: str) -> str: - `str_replace_editor`: File system operations for creating and editing documentation files - `read_code_components`: Explore additional code dependencies not included in the provided components +- Code-graph tools (when available): look up code this repository imports from other repositories +{_CODE_GRAPH_TOOLS_SECTION} +{_ECOSYSTEM_FACTS_SECTION} {{custom_instructions}} """.strip() @@ -580,7 +606,7 @@ def format_module_display_name(module_name: str) -> str: """.strip() REPO_OVERVIEW_PROMPT = f""" -{_CUSTOM_INSTRUCTIONS_SECTION}{_GUIDELINES_SECTION}{_VALIDATION_RULES_SECTION}You are an AI documentation assistant. Your task is to generate a brief overview of the {{repo_name}} repository. +{_CUSTOM_INSTRUCTIONS_SECTION}{_GUIDELINES_SECTION}{_VALIDATION_RULES_SECTION}{_ECOSYSTEM_FACTS_SECTION}You are an AI documentation assistant. Your task is to generate a brief overview of the {{repo_name}} repository. The overview should be a brief documentation of the repository, including: - The purpose of the repository @@ -614,7 +640,7 @@ def format_module_display_name(module_name: str) -> str: """.strip() MODULE_OVERVIEW_PROMPT = f""" -{_CUSTOM_INSTRUCTIONS_SECTION}{_GUIDELINES_SECTION}{_VALIDATION_RULES_SECTION}You are an AI documentation assistant. Your task is to generate a brief overview of the {{display_name}} module. +{_CUSTOM_INSTRUCTIONS_SECTION}{_GUIDELINES_SECTION}{_VALIDATION_RULES_SECTION}{_ECOSYSTEM_FACTS_SECTION}You are an AI documentation assistant. Your task is to generate a brief overview of the {{display_name}} module. **IMPORTANT - Module Naming:** - **File name**: `{{module_name}}.md` (keep underscores for filename) @@ -653,7 +679,7 @@ def format_module_display_name(module_name: str) -> str: """.strip() CLUSTER_REPO_PROMPT = """ -Here is list of all potential core components of the repository (It's normal that some components are not essential to the repository): +Here is the list of every component of the repository: Each component is identified by an INTEGER ID followed by a human-readable description. @@ -694,7 +720,7 @@ def format_module_display_name(module_name: str) -> str: **Why this matters:** If you return quoted IDs like "0", the system will reject your entire response. -Please group the components into modules such that each group contains closely related components that together form a cohesive module. DO NOT include components that are not essential to the repository. +Please group the components into modules such that each group contains closely related components that together form a cohesive module. Place EVERY component listed above in exactly one module: the documentation covers only the components you place, so a component you leave out is never documented. **MODULE NAMING CONVENTION (REQUIRED):** - Use **kebab-case**: lowercase words separated by hyphens @@ -767,7 +793,7 @@ def format_module_display_name(module_name: str) -> str: {module_tree} -Here is list of all potential core components of the module {module_name} (It's normal that some components are not essential to the module): +Here is the list of every component of the module {module_name}: Each component is identified by an INTEGER ID followed by a human-readable description. @@ -808,7 +834,7 @@ def format_module_display_name(module_name: str) -> str: **Why this matters:** If you return quoted IDs like "0", the system will reject your entire response. -Please group the components into smaller sub-modules such that each group contains closely related components. DO NOT include components that are not essential to the module. +Please group the components into smaller sub-modules such that each group contains closely related components. Place EVERY component listed above in exactly one sub-module: a component you leave out is never documented. **MODULE NAMING CONVENTION (REQUIRED):** - Use **kebab-case**: lowercase words separated by hyphens diff --git a/codewiki/src/be/usage_tracker.py b/codewiki/src/be/usage_tracker.py new file mode 100644 index 00000000..31d545b9 --- /dev/null +++ b/codewiki/src/be/usage_tracker.py @@ -0,0 +1,147 @@ +""" +What a CodeWiki run spent, per model, from the model's own answers. + +Every agent run (the module writers) goes through a fallback model, so the model that answered is read +off each response (`ModelResponse.model_name`), never assumed from the configuration. The clustering +calls go through the OpenAI client directly and are recorded from that response's usage. + +Totals are logged as they grow, and, when `CODEWIKI_USAGE_FILE` is set, merged into that file in the +Code Documentation workflow's usage format (`usage.by_model[model] = {input, output, cacheRead}`, plus a +`funnel.stage2` entry), which the workflow's final report sends to the hub. Input tokens exclude cache +reads, which are counted on their own, as the hub prices them. +""" + +from __future__ import annotations + +import json +import logging +import os +import threading +from typing import Any, Dict + +logger = logging.getLogger(__name__) + +_lock = threading.Lock() +_by_model: Dict[str, Dict[str, int]] = {} +_runs = {"agent_runs": 0, "failed_attempts": 0, "requests": 0, "tool_calls": 0} + + +def _add(model: str, input_tokens: int, output_tokens: int, cache_read: int) -> None: + entry = _by_model.setdefault(model or "unknown", {"input": 0, "output": 0, "cacheRead": 0}) + entry["input"] += max(0, int(input_tokens or 0)) + entry["output"] += max(0, int(output_tokens or 0)) + entry["cacheRead"] += max(0, int(cache_read or 0)) + + +def record_agent_run(label: str, result: Any) -> None: + """One finished agent run: each of its model responses, attributed to the model that answered it.""" + record_messages(label, result.all_messages()) + + +async def run_and_record(agent: Any, label: str, *args: Any, **kwargs: Any) -> Any: + """Run an agent and record what it spent, a run that fails included. + + A failed run's model responses (answered before the error) were billed too, so they are recorded before the + error is raised: a run that is retried then counts every attempt. + """ + from pydantic_ai import capture_run_messages + + with capture_run_messages() as messages: + try: + result = await agent.run(*args, **kwargs) + except BaseException: + record_messages(f"{label} (failed attempt)", messages, finished=False) + raise + record_agent_run(label, result) + return result + + +def record_messages(label: str, messages: Any, finished: bool = True) -> None: + """Each model response of one agent run, attributed to the model that answered it.""" + try: + from pydantic_ai.messages import ModelResponse, ToolCallPart + except ImportError: # pragma: no cover - pydantic_ai is a dependency + return + run_tokens = {"input": 0, "output": 0, "cacheRead": 0, "requests": 0, "tool_calls": 0} + with _lock: + for message in messages: + if not isinstance(message, ModelResponse): + continue + usage = message.usage + cache_read = int(getattr(usage, "cache_read_tokens", 0) or 0) + input_tokens = int(getattr(usage, "input_tokens", 0) or 0) - cache_read + output_tokens = int(getattr(usage, "output_tokens", 0) or 0) + _add(message.model_name or "unknown", input_tokens, output_tokens, cache_read) + run_tokens["input"] += max(0, input_tokens) + run_tokens["output"] += output_tokens + run_tokens["cacheRead"] += cache_read + run_tokens["requests"] += 1 + run_tokens["tool_calls"] += sum(1 for part in message.parts if isinstance(part, ToolCallPart)) + _runs["agent_runs" if finished else "failed_attempts"] += 1 + _runs["requests"] += run_tokens["requests"] + _runs["tool_calls"] += run_tokens["tool_calls"] + logger.info( + f"💰 Usage {label}: {run_tokens['requests']} requests, {run_tokens['tool_calls']} tool calls, " + f"{run_tokens['input']} input + {run_tokens['cacheRead']} cache-read + {run_tokens['output']} output tokens; " + f"run total {json.dumps(_by_model, sort_keys=True)}" + ) + _write() + + +def record_completion(model: str, response: Any) -> None: + """One direct chat completion (clustering), from the OpenAI response's usage.""" + usage = getattr(response, "usage", None) + if usage is None: + return + details = getattr(usage, "prompt_tokens_details", None) + cache_read = int(getattr(details, "cached_tokens", 0) or 0) if details is not None else 0 + with _lock: + _add(model, int(getattr(usage, "prompt_tokens", 0) or 0) - cache_read, int(getattr(usage, "completion_tokens", 0) or 0), cache_read) + _runs["requests"] += 1 + logger.info(f"💰 Usage {model}: {getattr(usage, 'prompt_tokens', 0)} prompt + {getattr(usage, 'completion_tokens', 0)} completion tokens") + _write() + + +def totals() -> Dict[str, Any]: + with _lock: + return {"by_model": json.loads(json.dumps(_by_model)), **_runs} + + +# What this process has already added to the file, per model: each write re-reads the file and replaces THIS +# share only, so an update another writer made in between is kept, never overwritten by a stale copy. +_written: Dict[str, Dict[str, int]] = {} + + +def _write() -> None: + """Merge this run's totals into `CODEWIKI_USAGE_FILE` (the workflow's usage artifact), re-read on every write.""" + global _written + path = os.environ.get("CODEWIKI_USAGE_FILE") + if not path: + return + with _lock: + try: + with open(path, "r", encoding="utf-8") as handle: + current = json.load(handle) + except (OSError, ValueError): + current = {} + # Usage is a report, never a reason to stop documenting: any failure to merge or write it (a read-only + # path, a full disk, a file that is not the expected object) is logged and the run carries on. + try: + if not isinstance(current, dict): + current = {} + merged = {model: dict(values) for model, values in ((current.get("usage") or {}).get("by_model") or {}).items()} + for model, values in _by_model.items(): + entry = merged.setdefault(model, {"input": 0, "output": 0, "cacheRead": 0}) + before = _written.get(model, {}) + for key in ("input", "output", "cacheRead"): + entry[key] = max(0, entry.get(key, 0) - before.get(key, 0)) + values[key] + funnel = dict(current.get("funnel") or {}) + funnel["stage2"] = {"engine": "codewiki", **_runs} + tmp = f"{path}.{os.getpid()}.tmp" + with open(tmp, "w", encoding="utf-8") as handle: + json.dump({**current, "usage": {**(current.get("usage") or {}), "by_model": merged}, "funnel": funnel}, handle) + os.replace(tmp, path) + except Exception as error: # noqa: BLE001 - see above + logger.warning(f"⚠️ Usage not written to {path}: {type(error).__name__}: {error}") + return + _written = {model: dict(values) for model, values in _by_model.items()} diff --git a/codewiki/src/config.py b/codewiki/src/config.py index bcd89551..6b3312f2 100644 --- a/codewiki/src/config.py +++ b/codewiki/src/config.py @@ -10,17 +10,20 @@ arguments (`from_args`), for a web-app job (`from_web_job`), from explicit CLI parameters (`from_cli`), and from a `ConfigManager` (`from_config_manager`), along with helpers for -multi-path source validation and prompt-addition generation used by the +repository-path validation and prompt-addition generation used by the downstream documentation generation stages. """ from dataclasses import dataclass, field, fields, asdict from typing import Optional, List, Dict, Any import argparse +import logging import os import sys from dotenv import load_dotenv from codewiki.src.be.flamingo_guidelines import escape_format_braces + +logger = logging.getLogger(__name__) load_dotenv() # Constants @@ -113,10 +116,6 @@ class Config: # Optional separate directory for Mermaid diagrams (from fork) # When set, diagrams are extracted from markdown and saved as separate files diagrams_dir: str = None - # Multi-path support: additional source directories to analyze alongside repo_path - # When None, operates in single-path mode (backward compatible) - # When set, all paths are analyzed and merged into unified documentation - additional_source_paths: Optional[List[str]] = None def to_dict(self, include_secrets: bool = False) -> Dict[str, Any]: """ @@ -190,90 +189,23 @@ def custom_instructions(self) -> Optional[str]: return instructions_dict.get('custom_instructions') return None - @property - def all_source_paths(self) -> List[str]: - """ - Get all source paths to analyze (primary + additional). - - Returns: - List of absolute paths. Always includes repo_path as first element. - If additional_source_paths is None, returns single-element list. - """ - import logging - logger = logging.getLogger(__name__) - - paths = [os.path.abspath(self.repo_path)] - if self.additional_source_paths: - paths.extend([os.path.abspath(p) for p in self.additional_source_paths]) - logger.debug(f"🔍 all_source_paths property accessed:") - logger.debug(f" ├─ Primary: {paths[0]}") - for i, path in enumerate(paths[1:], 1): - logger.debug(f" └─ Additional #{i}: {path}") - else: - logger.debug(f"🔍 all_source_paths property accessed (single-path mode): {paths[0]}") - return paths - - def validate_source_paths(self) -> None: + def validate_repo_path(self) -> None: """ - Validate that all source paths exist and are accessible directories. + Validate that the repository path exists and is a directory. Raises: - ValueError: If any path does not exist or is not a directory - OSError: If any path cannot be accessed + ValueError: If the path does not exist or is not a directory """ - import logging - logger = logging.getLogger(__name__) + logger.info("🔍 Validating repository path...") - logger.info("🔍 Validating source paths...") - - # Validate primary repo_path - logger.debug(f" ├─ Checking primary path: {self.repo_path}") if not os.path.exists(self.repo_path): - raise ValueError(f"Primary repository path does not exist: {self.repo_path}") + raise ValueError(f"Repository path does not exist: {self.repo_path}") if not os.path.isdir(self.repo_path): - raise ValueError(f"Primary repository path is not a directory: {self.repo_path}") - logger.info(f" ├─ ✓ Primary path valid: {self.repo_path}") - - # Validate additional paths if configured - if self.additional_source_paths: - logger.info(f" ├─ Validating {len(self.additional_source_paths)} additional path(s)...") - for i, path in enumerate(self.additional_source_paths, start=1): - logger.debug(f" │ └─ Checking additional path #{i}: {path}") - if not os.path.exists(path): - raise ValueError(f"Additional source path #{i} does not exist: {path}") - if not os.path.isdir(path): - raise ValueError(f"Additional source path #{i} is not a directory: {path}") - - # Check for read access - if not os.access(path, os.R_OK): - raise OSError(f"Additional source path #{i} is not readable: {path}") - logger.info(f" │ ├─ ✓ Additional path #{i} valid: {path}") - logger.info(f" └─ ✓ All {len(self.additional_source_paths)} additional path(s) validated") - else: - logger.debug(f" └─ No additional paths to validate (single-path mode)") - - def is_multi_path_mode(self) -> bool: - """ - Check if configuration is in multi-path mode. - - Returns: - True if additional_source_paths is set and non-empty, False otherwise - """ - import logging - logger = logging.getLogger(__name__) - - result = bool(self.additional_source_paths) - if result: - logger.debug(f"🔍 Multi-path mode: ENABLED ({len(self.additional_source_paths)} additional paths)") - else: - logger.debug("🔍 Multi-path mode: DISABLED (single-path)") - return result + raise ValueError(f"Repository path is not a directory: {self.repo_path}") + logger.info(f" └─ ✓ Repository path valid: {self.repo_path}") def get_prompt_addition(self) -> str: """Generate prompt additions based on agent instructions.""" - import logging - logger = logging.getLogger(__name__) - if not self.agent_instructions: logger.debug("📋 get_prompt_addition: No agent_instructions configured") return "" @@ -416,8 +348,7 @@ def from_cli( main_max_token_field: str = "max_tokens", fallback_max_token_field: str = "max_tokens", agent_instructions: Optional[Dict[str, Any]] = None, - diagrams_dir: str = None, - additional_source_paths: Optional[List[str]] = None + diagrams_dir: str = None ) -> 'Config': """ Create configuration for CLI context. @@ -454,7 +385,6 @@ def from_cli( fallback_max_token_field: Parameter name for fallback model max tokens agent_instructions: Custom agent instructions dict diagrams_dir: Optional separate directory for Mermaid diagrams - additional_source_paths: Additional directories to analyze (multi-path mode) Returns: Config instance @@ -660,12 +590,11 @@ def from_cli( main_max_token_field=main_max_token_field, fallback_max_token_field=fallback_max_token_field, agent_instructions=agent_instructions, - diagrams_dir=diagrams_dir, - additional_source_paths=additional_source_paths + diagrams_dir=diagrams_dir ) - # Validate all source paths exist and are accessible - config.validate_source_paths() + # Validate the repository path exists and is a directory + config.validate_repo_path() return config @@ -718,13 +647,6 @@ def from_config_manager( if not config_obj.fallback_model: raise ValueError("fallback_model is not configured") - # Extract additional_source_paths from agent_instructions if present - additional_paths = None - if config_obj.agent_instructions: - agent_dict = config_obj.agent_instructions.to_dict() if hasattr(config_obj.agent_instructions, 'to_dict') else config_obj.agent_instructions - if isinstance(agent_dict, dict): - additional_paths = agent_dict.get('additional_source_paths') - # Use from_cli with all configuration from ConfigManager return cls.from_cli( repo_path=repo_path, @@ -757,6 +679,5 @@ def from_config_manager( main_max_token_field=config_obj.main_max_token_field, fallback_max_token_field=config_obj.fallback_max_token_field, agent_instructions=config_obj.agent_instructions.to_dict() if config_obj.agent_instructions else None, - diagrams_dir=None, - additional_source_paths=additional_paths + diagrams_dir=None ) diff --git a/docs/README.md b/docs/README.md index 849abb09..4665d75b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,11 +64,6 @@ Technical reference documentation generated directly from source code analysis: **Config Core** (`codewiki/src/config.py`) — Shared runtime configuration - [Config Core Overview](./reference/architecture/config-core/config-core.md) -**Test Multi Path Core** (`test-multi-path`) — Multi-source-path analysis validation -- [Test Multi Path Core Overview](./reference/architecture/test-multi-path-core/test-multi-path-core.md) -- [Test Fixtures](./reference/architecture/test-multi-path-core/test_fixtures.md) -- [Test Runners](./reference/architecture/test-multi-path-core/test_runners.md) - **Test Clustering Core** (`test_clustering`) — Module clustering validation - [Test Clustering Core Overview](./reference/architecture/test-clustering-core/test-clustering-core.md) - [Live Clustering Tests](./reference/architecture/test-clustering-core/live_clustering_tests.md) diff --git a/docs/diagrams/architecture/README-3.mmd b/docs/diagrams/architecture/README-3.mmd index 63c9b7e3..debc1ef8 100644 --- a/docs/diagrams/architecture/README-3.mmd +++ b/docs/diagrams/architecture/README-3.mmd @@ -7,8 +7,5 @@ flowchart LR Backend --> Agents["Agent Orchestration"] Backend --> Docs["Documentation Generation"] - MultiPathTests["Test Multi Path Core"] --> Config - MultiPathTests --> Analysis - ClusteringTests["Test Clustering Core"] --> Config ClusteringTests --> Backend diff --git a/docs/diagrams/architecture/test-multi-path-core-2.mmd b/docs/diagrams/architecture/test-multi-path-core-2.mmd deleted file mode 100644 index 6e475616..00000000 --- a/docs/diagrams/architecture/test-multi-path-core-2.mmd +++ /dev/null @@ -1,5 +0,0 @@ -flowchart LR - TMC["Test Multi Path Core"] -->|"configures"| ConfigCore["Config Core"] - TMC -->|"invokes"| BackendCore["Backend Core
DependencyGraphBuilder"] - BackendCore -->|"produces"| Graph["Component
Dependency Graph"] - TMC -->|"asserts on"| Graph diff --git a/docs/diagrams/architecture/test-multi-path-core.mmd b/docs/diagrams/architecture/test-multi-path-core.mmd deleted file mode 100644 index 36bc601c..00000000 --- a/docs/diagrams/architecture/test-multi-path-core.mmd +++ /dev/null @@ -1,29 +0,0 @@ -flowchart TD - subgraph fixtures_area["Test Fixtures"] - MainSvc["MainService
main/service.py"] - APICtrl["APIController
main/controller.py"] - PluginIface["PluginInterface
external/plugin.py"] - DataPlug["DataPlugin
external/plugin.py"] - end - - subgraph runners_area["Test Runners"] - IntTest["IntegrationTestRunner
integration_test.py"] - IntResults["TestResults
(integration_test)"] - MultiPathColors["Colors
test_multi_path.py"] - MultiPathResults["TestResults
(test_multi_path)"] - end - - IntTest -->|"builds"| DGB["DependencyGraphBuilder"] - MultiPathResults -.->|"used by"| MultiPathFuncs["test_* functions"] - MultiPathFuncs -->|"builds"| DGB - - DGB -->|"analyzes"| MainSvc - DGB -->|"analyzes"| APICtrl - DGB -->|"analyzes"| PluginIface - DGB -->|"analyzes"| DataPlug - - IntTest -->|"records"| IntResults - APICtrl -->|"depends on"| MainSvc - DataPlug -->|"implements"| PluginIface - - DGB -->|"reads"| Cfg["Config"] diff --git a/docs/diagrams/architecture/test_fixtures-2.mmd b/docs/diagrams/architecture/test_fixtures-2.mmd deleted file mode 100644 index a79c26b9..00000000 --- a/docs/diagrams/architecture/test_fixtures-2.mmd +++ /dev/null @@ -1,18 +0,0 @@ -flowchart LR - subgraph MainPkg["main package"] - Controller["APIController"] - Service["MainService"] - end - subgraph DepsPkg["deps package (external helper)"] - Validate["validate_input()"] - Process["process_data()"] - end - subgraph PluginPkg["external package"] - Base["PluginInterface"] - Plugin["DataPlugin"] - end - - Controller -->|"creates & delegates"| Service - Service -->|"calls"| Validate - Service -->|"calls"| Process - Plugin -->|"extends"| Base diff --git a/docs/diagrams/architecture/test_fixtures-3.mmd b/docs/diagrams/architecture/test_fixtures-3.mmd deleted file mode 100644 index fd771587..00000000 --- a/docs/diagrams/architecture/test_fixtures-3.mmd +++ /dev/null @@ -1,17 +0,0 @@ -sequenceDiagram - participant Client - participant Controller as "APIController" - participant Service as "MainService" - participant Helper as "deps.helper" - - Client->>Controller: handle_request("/process", data) - Controller->>Controller: request_count += 1 - Controller->>Service: process_request(data) - alt service not active - Service-->>Controller: raise RuntimeError - else service active - Service->>Helper: process_data(data) - Helper-->>Service: processed - Service-->>Controller: status success response - end - Controller-->>Client: response dict diff --git a/docs/diagrams/architecture/test_fixtures-4.mmd b/docs/diagrams/architecture/test_fixtures-4.mmd deleted file mode 100644 index 038b2e01..00000000 --- a/docs/diagrams/architecture/test_fixtures-4.mmd +++ /dev/null @@ -1,6 +0,0 @@ -flowchart TD - Create["Create DataPlugin(name)"] --> Init["initialize()"] - Init -->|"sets initialized = True"| Ready["Plugin Ready"] - Ready --> Exec["execute(context)"] - Exec -->|"initialized is True"| Result["Return processed result"] - Exec -->|"initialized is False"| Error["Raise RuntimeError"] diff --git a/docs/diagrams/architecture/test_fixtures.mmd b/docs/diagrams/architecture/test_fixtures.mmd deleted file mode 100644 index 8ae058b1..00000000 --- a/docs/diagrams/architecture/test_fixtures.mmd +++ /dev/null @@ -1,26 +0,0 @@ -classDiagram - class PluginInterface { - +initialize() bool - +execute(context: dict) dict - } - class DataPlugin { - +name: str - +initialized: bool - +initialize() bool - +execute(context: dict) dict - } - class MainService { - +name: str - +active: bool - +start(config: dict) bool - +process_request(data: dict) dict - +stop() - } - class APIController { - +service: MainService - +request_count: int - +handle_request(endpoint: str, data: dict) dict - } - - PluginInterface <|-- DataPlugin - APIController --> MainService : owns diff --git a/docs/diagrams/architecture/test_runners-2.mmd b/docs/diagrams/architecture/test_runners-2.mmd deleted file mode 100644 index abf82a28..00000000 --- a/docs/diagrams/architecture/test_runners-2.mmd +++ /dev/null @@ -1,42 +0,0 @@ -sequenceDiagram - participant Main as main() - participant ITR as IntegrationTestRunner - participant FS as Temp Filesystem - participant CFG as Config - participant DGB as DependencyGraphBuilder - participant TR as TestResults - - Main->>ITR: run() - ITR->>FS: setup_test_environment() - FS-->>ITR: main/, deps/, vendor/ created - ITR->>TR: add_test("Test directories created", True) - - ITR->>CFG: create_config() - CFG-->>ITR: Config(repo_path, additional_source_paths) - ITR->>TR: add_test("Config created with 3 source paths", True) - - ITR->>FS: validate_paths() - ITR->>TR: add_test("All paths validated successfully", ...) - - ITR->>DGB: DependencyGraphBuilder(config) - ITR->>DGB: build_dependency_graph() - DGB-->>ITR: (components, leaf_nodes) - ITR->>TR: add_test("Multi-path mode detected", ...) - - ITR->>ITR: verify_namespaces() - ITR->>TR: add_test("All expected namespaces present", ...) - ITR->>TR: add_test("Component counts match expectations", ...) - - ITR->>ITR: verify_cross_path_dependencies() - ITR->>TR: add_test("Cross-path dependencies detected", ...) - - ITR->>ITR: verify_no_warnings() - ITR->>TR: add_test("No warnings generated", ...) - - ITR->>ITR: verify_file_counts() - ITR->>TR: add_test("Component counts match expectations", ...) - - ITR->>ITR: print_detailed_output() - ITR->>TR: print_summary() - ITR->>FS: cleanup() [finally] - ITR-->>Main: exit code (0, 1, or 2) diff --git a/docs/diagrams/architecture/test_runners-3.mmd b/docs/diagrams/architecture/test_runners-3.mmd deleted file mode 100644 index f8e1fcab..00000000 --- a/docs/diagrams/architecture/test_runners-3.mmd +++ /dev/null @@ -1,24 +0,0 @@ -flowchart TD - Start["run_all_tests()"] --> T1["test_single_path()"] - Start --> T2["test_multiple_paths()"] - Start --> T3["test_component_namespacing()"] - Start --> T4["test_cross_path_dependencies()"] - Start --> T5["test_invalid_path_handling()"] - Start --> T6["test_empty_additional_paths()"] - Start --> T7["test_relative_vs_absolute_paths()"] - - T1 -->|"uses"| Cfg["create_test_config()"] - T2 -->|"uses"| Cfg - T3 -->|"uses"| Cfg - T4 -->|"uses"| Cfg - T5 -->|"uses"| Cfg - T6 -->|"uses"| Cfg - T7 -->|"uses"| Cfg - - Cfg -->|"builds"| ConfigObj["Config"] - ConfigObj -->|"passed to"| DGB["DependencyGraphBuilder"] - DGB -->|"build_dependency_graph()"| Components["components, leaf_nodes"] - - T1 & T2 & T3 & T4 & T5 & T6 & T7 -->|"boolean result"| Results["TestResults.add_test(name, passed)"] - Results --> Summary["TestResults.print_summary()"] - Summary --> Exit["process exit code"] diff --git a/docs/diagrams/architecture/test_runners.mmd b/docs/diagrams/architecture/test_runners.mmd deleted file mode 100644 index 765bf1d1..00000000 --- a/docs/diagrams/architecture/test_runners.mmd +++ /dev/null @@ -1,30 +0,0 @@ -flowchart TD - subgraph TestRunners["Test Runners"] - ITR["IntegrationTestRunner"] - TR1["TestResults (integration_test.py)"] - COL["Colors"] - TR2["TestResults (test_multi_path.py)"] - FUNCS["Function-based tests
(test_single_path, test_multiple_paths, ...)"] - end - - subgraph Fixtures["Test Fixtures (sibling module)"] - PLUGIN["DataPlugin / PluginInterface"] - CTRL["APIController"] - SVC["MainService"] - end - - subgraph Pipeline["Dependency Analysis Pipeline (external)"] - CFG["Config"] - DGB["DependencyGraphBuilder"] - end - - ITR -->|"creates temp files & drives"| CFG - ITR --> TR1 - ITR -->|"instantiates"| DGB - DGB -->|"produces Node graph"| ITR - - FUNCS -->|"reads fixture files from"| Fixtures - FUNCS -->|"builds"| CFG - FUNCS -->|"instantiates"| DGB - FUNCS -->|"records outcomes in"| TR2 - FUNCS -->|"prints via"| COL diff --git a/docs/reference/architecture/README.md b/docs/reference/architecture/README.md index ff950685..829fdc4a 100644 --- a/docs/reference/architecture/README.md +++ b/docs/reference/architecture/README.md @@ -70,7 +70,6 @@ CodeWiki/ │ ├── be/ Backend analysis, orchestration, and documentation engine │ ├── fe/ FastAPI web application and background job processing │ └── config.py Shared runtime configuration -├── test-multi-path/ Multi-source-path dependency analysis fixtures and tests └── test_clustering/ Module clustering diagnostic and validation tests ``` @@ -82,7 +81,6 @@ CodeWiki/ | Backend Core | `codewiki/src/be` | Implements repository analysis, dependency graph construction, module clustering, agent orchestration, and Markdown generation. | [Backend Core](backend-core.md) | | Frontend Core | `codewiki/src/fe` | Provides the web-facing application for repository submission, asynchronous processing, status tracking, caching, and documentation delivery. | [Frontend Core](frontend-core.md) | | Config Core | `codewiki/src/config.py` | Defines the shared `Config` runtime object for paths, LLM providers, generation limits, source roots, and agent instructions. | [Config Core](config-core.md) | -| Test Multi Path Core | `test-multi-path` | Validates analysis across a primary repository and additional source directories. | [Test Multi Path Core](test-multi-path-core.md) | | Test Clustering Core | `test_clustering` | Validates live and offline module-clustering behavior, ID normalization, and response validation. | [Test Clustering Core](test-clustering-core.md) | ## Module Relationships @@ -97,9 +95,6 @@ flowchart LR Backend --> Agents["Agent Orchestration"] Backend --> Docs["Documentation Generation"] - MultiPathTests["Test Multi Path Core"] --> Config - MultiPathTests --> Analysis - ClusteringTests["Test Clustering Core"] --> Config ClusteringTests --> Backend ``` diff --git a/docs/reference/architecture/test-multi-path-core/test-multi-path-core.md b/docs/reference/architecture/test-multi-path-core/test-multi-path-core.md deleted file mode 100644 index e6eb3f8e..00000000 --- a/docs/reference/architecture/test-multi-path-core/test-multi-path-core.md +++ /dev/null @@ -1,85 +0,0 @@ -# Test Multi Path Core - -## Purpose - -The Test Multi Path Core module is a self-contained test fixture and validation suite for CodeWiki's **multi-path dependency analysis** capability. This capability allows the [Backend Core](backend-core.md) dependency analyzer to ingest source code from a primary repository root *plus* one or more `additional_source_paths` (configured via [Config Core](config-core.md)), and to build a single, correctly namespaced dependency graph across all of them. - -This module exists purely to exercise and verify that behavior end-to-end. It provides: - -1. A small, realistic **sample application** spread across multiple simulated source roots (`main/`, `deps/`/`vendor/`, `external/`) with intentional cross-path imports. -2. **Test runner scripts** that configure the dependency analyzer against these fixtures, execute the full analysis pipeline, and assert on the resulting component graph — checking namespace correctness, component counts, cross-path dependency resolution, and error handling for invalid paths. - -Because it drives the real `DependencyGraphBuilder` and `Config` components from [Backend Core](backend-core.md) and [Config Core](config-core.md), this module doubles as executable documentation of how multi-path analysis is expected to behave. - -## Architecture Overview - -The module is organized into two complementary areas: static **fixture code** that stands in for a multi-directory codebase, and **test runner scripts** that drive analysis against those fixtures and validate the outcome. - -```mermaid -flowchart TD - subgraph fixtures_area["Test Fixtures"] - MainSvc["MainService
main/service.py"] - APICtrl["APIController
main/controller.py"] - PluginIface["PluginInterface
external/plugin.py"] - DataPlug["DataPlugin
external/plugin.py"] - end - - subgraph runners_area["Test Runners"] - IntTest["IntegrationTestRunner
integration_test.py"] - IntResults["TestResults
(integration_test)"] - MultiPathColors["Colors
test_multi_path.py"] - MultiPathResults["TestResults
(test_multi_path)"] - end - - IntTest -->|"builds"| DGB["DependencyGraphBuilder"] - MultiPathResults -.->|"used by"| MultiPathFuncs["test_* functions"] - MultiPathFuncs -->|"builds"| DGB - - DGB -->|"analyzes"| MainSvc - DGB -->|"analyzes"| APICtrl - DGB -->|"analyzes"| PluginIface - DGB -->|"analyzes"| DataPlug - - IntTest -->|"records"| IntResults - APICtrl -->|"depends on"| MainSvc - DataPlug -->|"implements"| PluginIface - - DGB -->|"reads"| Cfg["Config"] -``` - -Key relationships: - -- **Fixture components** (`MainService`, `APIController`, `PluginInterface`, `DataPlugin`) are ordinary application code with no awareness of testing — they exist only to be *discovered and analyzed* by the dependency analyzer running against multiple source roots. -- **Runner components** (`IntegrationTestRunner`, both `TestResults` classes, `Colors`) orchestrate the analysis pipeline (`Config` → `DependencyGraphBuilder`) against the fixtures and assert on the output. -- Both runner scripts independently import `Config` from [Config Core](config-core.md) and `DependencyGraphBuilder` from [Backend Core](backend-core.md), meaning this module is a **consumer** of those modules rather than a dependency of them. - -## Sub-Modules - -### Test Fixtures - -Sample application code intentionally spread across separate directories (`main/`, `external/`, and referenced `deps/`/`vendor/` paths) to simulate a realistic multi-root repository. Includes a small service/controller pair with a cross-file dependency, and a third-party-style plugin interface with a concrete implementation. This code is not exercised directly by end users — it is the input data that the test runners feed into the dependency analyzer. - -See [Test Fixtures](test_fixtures.md) for details on `MainService`, `APIController`, `PluginInterface`, and `DataPlugin`. - -### Test Runners - -Two independent, executable test scripts that each configure `Config` and `DependencyGraphBuilder` against the fixtures and validate results: `integration_test.py` runs a full generated-fixture end-to-end scenario (creating its own temporary files for `main/`, `deps/`, and `vendor/`), while `test_multi_path.py` runs a suite of scenario-based checks (single path, multiple paths, namespacing, cross-path dependencies, invalid paths, empty paths, relative vs. absolute paths) against the on-disk fixtures in this module. - -See [Test Runners](test_runners.md) for details on `IntegrationTestRunner`, `TestResults`, and `Colors`. - -## How This Module Fits Into the System - -```mermaid -flowchart LR - TMC["Test Multi Path Core"] -->|"configures"| ConfigCore["Config Core"] - TMC -->|"invokes"| BackendCore["Backend Core
DependencyGraphBuilder"] - BackendCore -->|"produces"| Graph["Component
Dependency Graph"] - TMC -->|"asserts on"| Graph -``` - -Test Multi Path Core does not provide functionality consumed by other modules in the system — it is a standalone validation harness. It depends on: - -- **[Config Core](config-core.md)** — for constructing `Config` instances with `additional_source_paths` set, which is the mechanism that enables multi-path mode. -- **[Backend Core](backend-core.md)** — specifically the dependency-analysis pipeline (`DependencyGraphBuilder`), which is the system under test. - -Its outputs are console reports and pass/fail exit codes rather than artifacts consumed elsewhere in the platform. diff --git a/docs/reference/architecture/test-multi-path-core/test_fixtures.md b/docs/reference/architecture/test-multi-path-core/test_fixtures.md deleted file mode 100644 index e650c8fe..00000000 --- a/docs/reference/architecture/test-multi-path-core/test_fixtures.md +++ /dev/null @@ -1,135 +0,0 @@ -# Test Fixtures - -The Test Fixtures module provides a small, self-contained set of sample application components used as **fixture code** for exercising the CodeWiki dependency-analysis and documentation-generation pipeline. It simulates a realistic multi-path Python project consisting of an API controller, a business service, and a third-party style plugin system. These fixtures are not part of CodeWiki's production runtime — they exist so that other CodeWiki subsystems (such as the [Backend Core](backend-core.md) dependency analyzers) have representative, cross-module code to parse, resolve, and document during testing. - -## Purpose and Scope - -This module is a child of the [Test Multi Path Core](test-multi-path-core.md) module. It contains the "application" half of the test fixture set: - -- A **plugin interface and implementation** (`external/plugin.py`) that mimics a third-party package with an abstract base class and a concrete subclass. -- A **main service** (`main/service.py`) that represents core business logic and depends on an external helper package (`deps.helper`). -- An **API controller** (`main/controller.py`) that sits in front of the service and routes simple HTTP-style endpoints to service methods. - -The sibling module, [Test Runners](test_runners.md), contains the test harnesses (`IntegrationTestRunner`, `TestResults`, `Colors`) that exercise these fixtures end-to-end. Together, the two child modules make up the full [Test Multi Path Core](test-multi-path-core.md) fixture project. - -## Core Components - -| Component | File | Responsibility | -|---|---|---| -| `PluginInterface` | `test-multi-path/external/plugin.py` | Abstract base class defining the plugin contract (`initialize`, `execute`). | -| `DataPlugin` | `test-multi-path/external/plugin.py` | Concrete plugin implementation that processes a `data` payload from a context dictionary. | -| `MainService` | `test-multi-path/main/service.py` | Core service exposing lifecycle (`start`/`stop`) and request-processing behavior; delegates data transformation to an external `deps.helper` module. | -| `APIController` | `test-multi-path/main/controller.py` | Thin routing layer that owns a `MainService` instance and dispatches requests based on endpoint string. | - -## Architecture - -The fixture code models a typical layered application: a controller delegates to a service, while a separate plugin subsystem demonstrates cross-package inheritance that the dependency analyzer must resolve correctly (base class defined in `external/plugin.py`, subclass in the same file, but conceptually representing a "third-party" dependency). - -```mermaid -classDiagram - class PluginInterface { - +initialize() bool - +execute(context: dict) dict - } - class DataPlugin { - +name: str - +initialized: bool - +initialize() bool - +execute(context: dict) dict - } - class MainService { - +name: str - +active: bool - +start(config: dict) bool - +process_request(data: dict) dict - +stop() - } - class APIController { - +service: MainService - +request_count: int - +handle_request(endpoint: str, data: dict) dict - } - - PluginInterface <|-- DataPlugin - APIController --> MainService : owns -``` - -## Component Relationships and Dependencies - -- `APIController` **instantiates and owns** a `MainService` object in its constructor, coupling the routing layer directly to a specific service instance. -- `MainService` depends on functions (`validate_input`, `process_data`) imported from an external `deps.helper` module. This module is outside the documented core components of this fixture set but represents the kind of cross-package call that the [Backend Core](backend-core.md) dependency-analysis subsystem is designed to trace. -- `DataPlugin` **inherits from** `PluginInterface`, overriding both `initialize()` and `execute()`. This inheritance relationship is a deliberate fixture pattern for validating that call-graph and class-hierarchy analyzers correctly link subclass methods back to their abstract base declarations. - -```mermaid -flowchart LR - subgraph MainPkg["main package"] - Controller["APIController"] - Service["MainService"] - end - subgraph DepsPkg["deps package (external helper)"] - Validate["validate_input()"] - Process["process_data()"] - end - subgraph PluginPkg["external package"] - Base["PluginInterface"] - Plugin["DataPlugin"] - end - - Controller -->|"creates & delegates"| Service - Service -->|"calls"| Validate - Service -->|"calls"| Process - Plugin -->|"extends"| Base -``` - -## Request Processing Flow - -The controller-to-service interaction demonstrates a simple, single-hop request pipeline used as a fixture for tracing call graphs across file boundaries. - -```mermaid -sequenceDiagram - participant Client - participant Controller as "APIController" - participant Service as "MainService" - participant Helper as "deps.helper" - - Client->>Controller: handle_request("/process", data) - Controller->>Controller: request_count += 1 - Controller->>Service: process_request(data) - alt service not active - Service-->>Controller: raise RuntimeError - else service active - Service->>Helper: process_data(data) - Helper-->>Service: processed - Service-->>Controller: status success response - end - Controller-->>Client: response dict -``` - -Key behavioral notes captured by this fixture: - -- `APIController.handle_request` supports two endpoints — `/process` (delegates to `MainService.process_request`) and `/health` (returns a status payload including the running `request_count`) — and returns an error payload for anything else. -- `MainService.process_request` raises a `RuntimeError` if the service has not been started via `start()`, exercising error-path analysis. -- `MainService.start` validates its `config` argument via `validate_input` before flipping the `active` flag to `True`. - -## Plugin Lifecycle Flow - -`DataPlugin` models a two-phase lifecycle — initialization followed by execution — common in plugin-based architectures. This is used as a fixture to validate that analyzers correctly track state (`initialized`) set in one method and checked in another. - -```mermaid -flowchart TD - Create["Create DataPlugin(name)"] --> Init["initialize()"] - Init -->|"sets initialized = True"| Ready["Plugin Ready"] - Ready --> Exec["execute(context)"] - Exec -->|"initialized is True"| Result["Return processed result"] - Exec -->|"initialized is False"| Error["Raise RuntimeError"] -``` - -## Role in the Test Multi-Path Fixture Set - -This module exists purely to provide realistic, multi-file, multi-package Python source for CodeWiki's analysis tooling to consume. It does not implement production functionality. Consumers of this fixture set typically: - -1. Point the [Backend Core](backend-core.md) dependency-analysis pipeline at the `test-multi-path` directory tree. -2. Verify that the resulting dependency graph correctly links `APIController` to `MainService` to `deps.helper`, and `DataPlugin` to `PluginInterface`. -3. Use the [Test Runners](test_runners.md) module's `IntegrationTestRunner` to validate the end-to-end analysis and documentation output against these fixtures. - -For information on how the overall fixture project is organized and how it relates to the top-level test harness, see [Test Multi Path Core](test-multi-path-core.md). diff --git a/docs/reference/architecture/test-multi-path-core/test_runners.md b/docs/reference/architecture/test-multi-path-core/test_runners.md deleted file mode 100644 index 76d18180..00000000 --- a/docs/reference/architecture/test-multi-path-core/test_runners.md +++ /dev/null @@ -1,235 +0,0 @@ -# Test Runners - -## Introduction - -The Test Runners module contains the executable test drivers used to validate CodeWiki's **multi-path source analysis** feature — the capability that allows the dependency analyzer to ingest code from a main repository plus one or more additional source directories (e.g. `deps/`, `vendor/`, `external/`) while keeping component identities correctly namespaced and dependency edges correctly resolved. - -This module provides two independent, script-style test drivers: - -- **`IntegrationTestRunner`** (in `integration_test.py`) — a self-contained, end-to-end integration test that builds its own temporary fixture files on disk, runs the full analysis pipeline, and asserts on the resulting component graph. -- **A function-based test suite** (in `test_multi_path.py`) built around the `Colors` and `TestResults` helper classes — a collection of discrete test functions that exercise multi-path analysis against pre-existing fixture directories (see the sibling [Test Fixtures](../test_fixtures/test_fixtures.md) module). - -Both drivers ultimately invoke the same production pipeline components — `Config` and `DependencyGraphBuilder` — that live outside this module, and both produce a human-readable pass/fail summary plus a process exit code suitable for CI. - -## Purpose and Scope - -The Test Runners module is a **verification harness**, not a production component. Its responsibilities are: - -1. Construct realistic multi-path repository layouts (either programmatically, in the case of `IntegrationTestRunner`, or by reading pre-built fixtures, in the case of `test_multi_path.py`). -2. Drive the dependency-analysis pipeline (`Config` → `DependencyGraphBuilder`) against those layouts. -3. Assert that: - - Components from every source path are discovered. - - Component IDs are namespaced by their source path (no ID collisions). - - Cross-path dependencies are captured where possible. - - No unexpected files/paths are missing or invalid. -4. Aggregate individual assertion results into a single summary report and translate the outcome into a process exit code (`0` = success, non-zero = failure). - -## Architecture Overview - -```mermaid -flowchart TD - subgraph TestRunners["Test Runners"] - ITR["IntegrationTestRunner"] - TR1["TestResults (integration_test.py)"] - COL["Colors"] - TR2["TestResults (test_multi_path.py)"] - FUNCS["Function-based tests
(test_single_path, test_multiple_paths, ...)"] - end - - subgraph Fixtures["Test Fixtures (sibling module)"] - PLUGIN["DataPlugin / PluginInterface"] - CTRL["APIController"] - SVC["MainService"] - end - - subgraph Pipeline["Dependency Analysis Pipeline (external)"] - CFG["Config"] - DGB["DependencyGraphBuilder"] - end - - ITR -->|"creates temp files & drives"| CFG - ITR --> TR1 - ITR -->|"instantiates"| DGB - DGB -->|"produces Node graph"| ITR - - FUNCS -->|"reads fixture files from"| Fixtures - FUNCS -->|"builds"| CFG - FUNCS -->|"instantiates"| DGB - FUNCS -->|"records outcomes in"| TR2 - FUNCS -->|"prints via"| COL -``` - -The two drivers are architecturally parallel but do not share code: each defines its own `TestResults` class with a slightly different API, reflecting their independent evolution as standalone scripts. Both, however, converge on the same two external dependencies: - -- **`Config`** — see the [Config Core](config-core.md) module for configuration fields such as `repo_path`, `additional_source_paths`, and `is_multi_path_mode()`. -- **`DependencyGraphBuilder`** — see the [Backend Core](backend-core.md) module for how the multi-path component graph is actually built. - -## Core Components - -### IntegrationTestRunner - -`IntegrationTestRunner` is a stateful orchestrator class that owns the entire lifecycle of a single, self-contained end-to-end test run. Unlike the function-based suite, it does not rely on pre-existing fixture files — it programmatically creates a temporary directory tree with three source roots (`main/`, `deps/`, `vendor/`) and hand-written Python files exhibiting realistic intra- and cross-path imports. - -**Key responsibilities (mapped to instance methods):** - -| Method | Responsibility | -|---|---| -| `setup_test_environment()` | Creates a temp directory with `main/`, `deps/`, `vendor/` subfolders and populates each with sample `.py` files (`_create_main_files`, `_create_deps_files`, `_create_vendor_files`). | -| `create_config()` | Builds a `Config` instance with `repo_path` set to `main/` and `additional_source_paths` set to `[deps/, vendor/]`. | -| `validate_paths()` | Confirms the root and all additional paths exist on disk. | -| `execute_dependency_parser()` | Instantiates `DependencyGraphBuilder(config)`, checks `config.is_multi_path_mode()`, and calls `build_dependency_graph()` to obtain `(components, leaf_nodes)`. | -| `verify_namespaces()` / `_verify_namespace_counts()` | Groups discovered component IDs by top-level namespace (`main`, `deps`, `vendor`) and checks expected counts (7, 5, 2 respectively). | -| `verify_cross_path_dependencies()` | Inspects each component's `dependencies` attribute for edges that cross namespace boundaries. | -| `verify_no_warnings()` | Checks whether the builder exposes a `warnings` attribute and, if so, that it is empty. | -| `verify_file_counts()` | Confirms the total component count across all namespaces matches the expected total (14). | -| `print_detailed_output()` | Prints all discovered component IDs, cross-namespace dependencies, and per-namespace counts for manual inspection. | -| `cleanup()` | Removes the temporary test directory (`shutil.rmtree`), always executed in the `finally` block of `run()`. | -| `run()` | Orchestrates the full sequence above in order and returns an integer exit code (`0` success, `1` assertion failure, `2` unhandled exception). | - -**Instance state** tracked across the run: `test_dir`, `main_path`, `deps_path`, `vendor_path`, `config`, `builder`, `components`, `leaf_nodes`, and a `results: TestResults` accumulator. - -### TestResults (integration_test.py) - -A lightweight accumulator used exclusively by `IntegrationTestRunner`. Each recorded entry is a dictionary with `name`, `passed`, and an optional `details` string (used both for failure diagnostics and non-fatal warnings). - -- `add_test(name, passed, details="")` — appends a result entry. -- `print_summary()` — prints total/passed/failed counts, lists failed assertions with their details, lists any "warning" entries (passed assertions that still carry a `details` message), and prints a final `✅ INTEGRATION TEST PASSED` / `❌ INTEGRATION TEST FAILED` banner. -- `all_passed()` — returns `True` only if every recorded entry passed; used by `IntegrationTestRunner.run()` to compute the process exit code. - -### Colors - -A minimal ANSI escape-code namespace used by the function-based test suite in `test_multi_path.py` for colorized console output. It defines class-level string constants: - -- `GREEN`, `RED`, `YELLOW`, `BLUE`, `BOLD`, `END` - -These are consumed by the module-level helper functions `print_header`, `print_success`, `print_error`, and `print_warning` to produce readable, color-coded terminal output while the test suite runs. - -### TestResults (test_multi_path.py) - -A separate, simpler accumulator (same class name as above, but a distinct implementation local to `test_multi_path.py`). It stores results as a `Dict[str, bool]` keyed by test name rather than a list of dictionaries with details. - -- `add_test(name, passed)` — records a boolean outcome for a named test. -- `print_summary()` — prints a formatted header, iterates all results printing `print_success`/`print_error` per test (using `Colors`), prints a `"passed/total"` tally, and returns `0` if all tests passed or `1` otherwise — directly usable as a process exit code. - -> **Note:** The two `TestResults` classes are intentionally independent implementations with different constructors and method signatures (`add_test(name, passed, details="")` returning nothing and printing a rich report, versus `add_test(name, passed)` with `print_summary()` returning an `int`). They are not meant to be interchanged; each is scoped to its own script. - -## Data Flow: IntegrationTestRunner Execution - -```mermaid -sequenceDiagram - participant Main as main() - participant ITR as IntegrationTestRunner - participant FS as Temp Filesystem - participant CFG as Config - participant DGB as DependencyGraphBuilder - participant TR as TestResults - - Main->>ITR: run() - ITR->>FS: setup_test_environment() - FS-->>ITR: main/, deps/, vendor/ created - ITR->>TR: add_test("Test directories created", True) - - ITR->>CFG: create_config() - CFG-->>ITR: Config(repo_path, additional_source_paths) - ITR->>TR: add_test("Config created with 3 source paths", True) - - ITR->>FS: validate_paths() - ITR->>TR: add_test("All paths validated successfully", ...) - - ITR->>DGB: DependencyGraphBuilder(config) - ITR->>DGB: build_dependency_graph() - DGB-->>ITR: (components, leaf_nodes) - ITR->>TR: add_test("Multi-path mode detected", ...) - - ITR->>ITR: verify_namespaces() - ITR->>TR: add_test("All expected namespaces present", ...) - ITR->>TR: add_test("Component counts match expectations", ...) - - ITR->>ITR: verify_cross_path_dependencies() - ITR->>TR: add_test("Cross-path dependencies detected", ...) - - ITR->>ITR: verify_no_warnings() - ITR->>TR: add_test("No warnings generated", ...) - - ITR->>ITR: verify_file_counts() - ITR->>TR: add_test("Component counts match expectations", ...) - - ITR->>ITR: print_detailed_output() - ITR->>TR: print_summary() - ITR->>FS: cleanup() [finally] - ITR-->>Main: exit code (0, 1, or 2) -``` - -## Data Flow: Function-Based Multi-Path Test Suite - -The `test_multi_path.py` driver takes a different shape: rather than one stateful orchestrator, it defines a series of independent test functions, each returning a boolean, which are collected and executed by `run_all_tests()`. - -```mermaid -flowchart TD - Start["run_all_tests()"] --> T1["test_single_path()"] - Start --> T2["test_multiple_paths()"] - Start --> T3["test_component_namespacing()"] - Start --> T4["test_cross_path_dependencies()"] - Start --> T5["test_invalid_path_handling()"] - Start --> T6["test_empty_additional_paths()"] - Start --> T7["test_relative_vs_absolute_paths()"] - - T1 -->|"uses"| Cfg["create_test_config()"] - T2 -->|"uses"| Cfg - T3 -->|"uses"| Cfg - T4 -->|"uses"| Cfg - T5 -->|"uses"| Cfg - T6 -->|"uses"| Cfg - T7 -->|"uses"| Cfg - - Cfg -->|"builds"| ConfigObj["Config"] - ConfigObj -->|"passed to"| DGB["DependencyGraphBuilder"] - DGB -->|"build_dependency_graph()"| Components["components, leaf_nodes"] - - T1 & T2 & T3 & T4 & T5 & T6 & T7 -->|"boolean result"| Results["TestResults.add_test(name, passed)"] - Results --> Summary["TestResults.print_summary()"] - Summary --> Exit["process exit code"] -``` - -Each test function follows the same shape: - -1. Resolve fixture paths (`main/`, `deps/`, `external/`) relative to the script's own directory — these fixtures are the ones documented in the sibling [Test Fixtures](test_fixtures.md) module (`APIController`, `MainService`, `DataPlugin`, `PluginInterface`). -2. Build a `Config` via the local `create_test_config()` helper, optionally supplying `additional_source_paths`. -3. Instantiate `DependencyGraphBuilder(config)` and call `build_dependency_graph()`. -4. Convert the returned `Node` objects to plain dictionaries via `model_dump()` for easy field inspection (`file_path`, `depends_on`). -5. Assert path-specific expectations (e.g., that `service.py` and `controller.py` are present under `main/`, that component IDs are unique, that cross-path dependency edges exist, that invalid paths raise `ValueError`/`OSError` from `config.validate_source_paths()`). -6. Return `True`/`False`, printed via the `Colors`-driven `print_success`/`print_error` helpers. - -Notable individual tests: - -- **`test_single_path`** — backward-compatibility check that a single `repo_path` with no `additional_source_paths` still analyzes correctly. -- **`test_multiple_paths`** — verifies that components from `main/`, `deps/`, and `external/` are all discovered in one pass. -- **`test_component_namespacing`** — asserts no duplicate component IDs are produced across paths and warns if an ID doesn't visibly reflect its source path. -- **`test_cross_path_dependencies`** — inspects each component's `depends_on` set to detect edges crossing between `main` and `deps`. -- **`test_invalid_path_handling`** — expects `config.validate_source_paths()` to raise on a nonexistent additional path. -- **`test_empty_additional_paths`** — confirms an empty `additional_source_paths` list behaves like the single-path case. -- **`test_relative_vs_absolute_paths`** — exercises path resolution behavior for relative vs. absolute additional paths. - -## Relationship to Other Modules - -- **[Test Fixtures](test_fixtures.md)** — the sibling module under [Test Multi Path Core](test-multi-path-core.md) that defines the on-disk sample components (`MainService`, `APIController`, `DataPlugin`, `PluginInterface`) consumed by `test_multi_path.py`'s function-based suite. `IntegrationTestRunner` does **not** depend on these fixtures; it generates its own files at runtime instead. -- **[Config Core](config-core.md)** — supplies the `repo_path` / `additional_source_paths` / `is_multi_path_mode()` / `validate_source_paths()` surface that both test drivers configure and exercise. -- **[Backend Core](backend-core.md)** — provides `DependencyGraphBuilder`, the production component whose multi-path behavior is the actual subject under test in this module. - -## Usage - -Both drivers are designed to be run as standalone scripts and return a process-appropriate exit code: - -```bash -# Run the self-contained end-to-end integration test -python integration_test.py - -# Run the fixture-based multi-path test suite -python test_multi_path.py -``` - -`integration_test.py` exits `0` on full success, `1` if any assertion failed, and `2` if the run crashed with an unhandled exception (stack trace printed via `traceback.print_exc()`). `test_multi_path.py`'s `run_all_tests()` returns `0` only if every individual test function returned `True`, otherwise `1`. - -## Summary - -The Test Runners module is the executable validation layer for CodeWiki's multi-path analysis capability. `IntegrationTestRunner` provides a fully self-contained, disk-based end-to-end scenario with rich diagnostic output, while the `Colors`/`TestResults` pairing in `test_multi_path.py` powers a broader, fixture-driven suite covering backward compatibility, namespacing, cross-path dependencies, and error handling. Together they validate the same underlying `Config` → `DependencyGraphBuilder` pipeline from two complementary angles, without depending on each other's internals. diff --git a/examples/.multi_path_example.md b/examples/.multi_path_example.md deleted file mode 100644 index f6f554c8..00000000 --- a/examples/.multi_path_example.md +++ /dev/null @@ -1,42 +0,0 @@ - -Demonstrates multi-path repository analysis using `DependencyParser` and `RepoAnalyzer`, showing how to analyze multiple source directories simultaneously with namespace-aware component tracking. - -## Key Components - -| Function | Description | -|---|---| -| `example_single_path()` | Backward-compatible single-path parsing with include/exclude patterns | -| `example_multiple_paths()` | Multi-repo parsing with automatic namespace grouping and cross-namespace dependency detection | -| `example_repo_analyzer_multi_path()` | High-level `RepoAnalyzer` usage across multiple repos, returning summary and file tree | -| `example_component_details()` | Accessing per-namespace component metadata (type, file path, line range, dependencies) | -| `example_saving_output()` | Persisting the full multi-path dependency graph to a JSON file | - -## Usage Example - -```python -from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser - -# Multi-path analysis with namespacing -parser = DependencyParser( - ["/path/to/frontend", "/path/to/backend", "/path/to/shared"], - include_patterns=["*.py", "*.ts", "*.tsx"], - exclude_patterns=["*test*", "*spec*", "node_modules/*"] -) - -components = parser.parse_repository() - -# Components are prefixed by namespace (derived from directory name) -for comp_id, component in components.items(): - namespace = comp_id.split('.')[0] # e.g., "frontend", "backend" - print(f"{namespace}: {comp_id} -> {component.name}") - -# Save the full graph to disk -parser.save_dependency_graph("/tmp/multi_path_analysis.json") -``` - -## Notes - -- All example functions use **placeholder paths** — replace `/path/to/...` with real directories before running. -- Component IDs are automatically **namespaced** by directory name (e.g., `frontend.src.components.Button`) to avoid collisions across repos. -- Cross-namespace dependencies are preserved and visible in the parsed output. -- Source: [`examples/multi_path_example.py`](https://github.com/flamingo-stack/CodeWiki/blob/main/examples/multi_path_example.py) \ No newline at end of file diff --git a/examples/multi_path_example.py b/examples/multi_path_example.py deleted file mode 100644 index e0a39533..00000000 --- a/examples/multi_path_example.py +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: Multi-Path Repository Analysis - -This script demonstrates how to use DependencyParser and RepoAnalyzer -with multiple source directories. -""" - -import sys -import os - -# Add CodeWiki to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser -from codewiki.src.be.dependency_analyzer.analysis.repo_analyzer import RepoAnalyzer - - -def example_single_path(): - """Example: Single path (backward compatible).""" - print("=" * 60) - print("EXAMPLE 1: Single Path (Backward Compatible)") - print("=" * 60) - - # Works exactly as before - parser = DependencyParser( - "/path/to/my-project", - include_patterns=["*.py", "*.ts"], - exclude_patterns=["*test*"] - ) - - components = parser.parse_repository() - - print(f"Found {len(components)} components") - print("\nSample component IDs:") - for comp_id in list(components.keys())[:5]: - print(f" - {comp_id}") - - -def example_multiple_paths(): - """Example: Multiple paths with namespacing.""" - print("\n" + "=" * 60) - print("EXAMPLE 2: Multiple Paths with Namespacing") - print("=" * 60) - - # Define multiple repositories to analyze - repositories = [ - "/path/to/openframe-frontend", - "/path/to/ui-kit", - "/path/to/shared-libraries" - ] - - # Create parser with multiple paths - parser = DependencyParser( - repositories, - include_patterns=["*.py", "*.ts", "*.tsx"], - exclude_patterns=["*test*", "*spec*", "node_modules/*"] - ) - - # Parse all repositories - components = parser.parse_repository() - - print(f"\nTotal components: {len(components)}") - print(f"Total modules: {len(parser.modules)}") - - # Group by namespace - namespaces = {} - for comp_id, component in components.items(): - namespace = comp_id.split('.')[0] - if namespace not in namespaces: - namespaces[namespace] = [] - namespaces[namespace].append(comp_id) - - print("\nComponents by namespace:") - for namespace, comp_list in namespaces.items(): - print(f" {namespace}: {len(comp_list)} components") - - # Show sample namespaced component IDs - print("\nSample namespaced component IDs:") - for comp_id in list(components.keys())[:10]: - print(f" - {comp_id}") - - # Show cross-namespace dependencies - print("\nCross-namespace dependencies:") - for comp_id, component in list(components.items())[:50]: - comp_namespace = comp_id.split('.')[0] - cross_deps = [ - dep for dep in component.depends_on - if dep.split('.')[0] != comp_namespace - ] - if cross_deps: - print(f" {comp_id} depends on:") - for dep in cross_deps[:3]: # Show first 3 - print(f" → {dep}") - if len(cross_deps) > 3: - print(f" ... and {len(cross_deps) - 3} more") - break # Just show first example - - -def example_repo_analyzer_multi_path(): - """Example: RepoAnalyzer with multiple paths.""" - print("\n" + "=" * 60) - print("EXAMPLE 3: RepoAnalyzer with Multiple Paths") - print("=" * 60) - - repositories = [ - "/path/to/frontend", - "/path/to/backend", - "/path/to/shared" - ] - - analyzer = RepoAnalyzer( - include_patterns=["*.py", "*.ts"], - exclude_patterns=["*test*", "build/*", "dist/*"] - ) - - result = analyzer.analyze_repository_structure(repositories) - - # Display summary - summary = result["summary"] - print(f"\nSummary:") - print(f" Total files: {summary['total_files']}") - print(f" Total size: {summary['total_size_kb']:.2f} KB") - print(f" Repositories: {summary['repositories']}") - print(f" Namespaces: {', '.join(summary['namespaces'])}") - - # Show file tree structure - print("\nFile tree structure (top level):") - file_tree = result["file_tree"] - for child in file_tree.get("children", [])[:3]: - namespace = child.get("_namespace", "unknown") - num_children = len(child.get("children", [])) - print(f" 📁 {namespace}/ ({num_children} items)") - - -def example_component_details(): - """Example: Accessing component details in multi-path mode.""" - print("\n" + "=" * 60) - print("EXAMPLE 4: Component Details in Multi-Path Mode") - print("=" * 60) - - parser = DependencyParser([ - "/path/to/project-a", - "/path/to/project-b" - ]) - - components = parser.parse_repository() - - # Find components from each namespace - for namespace in ["project-a", "project-b"]: - namespace_components = { - comp_id: comp for comp_id, comp in components.items() - if comp_id.startswith(f"{namespace}.") - } - - if namespace_components: - print(f"\n{namespace} components:") - sample_id = list(namespace_components.keys())[0] - sample_comp = namespace_components[sample_id] - - print(f" Sample: {sample_id}") - print(f" Name: {sample_comp.name}") - print(f" Type: {sample_comp.component_type}") - print(f" File: {sample_comp.relative_path}") - print(f" Lines: {sample_comp.start_line}-{sample_comp.end_line}") - print(f" Dependencies: {len(sample_comp.depends_on)}") - - -def example_saving_output(): - """Example: Saving multi-path analysis to JSON.""" - print("\n" + "=" * 60) - print("EXAMPLE 5: Saving Multi-Path Analysis") - print("=" * 60) - - parser = DependencyParser([ - "/path/to/repo1", - "/path/to/repo2" - ]) - - components = parser.parse_repository() - - # Save to JSON file - output_path = "/tmp/multi_path_analysis.json" - parser.save_dependency_graph(output_path) - - print(f"\nSaved {len(components)} components to:") - print(f" {output_path}") - print("\nJSON structure includes:") - print(" - Namespaced component IDs") - print(" - Cross-namespace dependencies") - print(" - Original file paths preserved") - print(" - Complete component metadata") - - -def main(): - """Run all examples.""" - print("\n") - print("╔" + "=" * 58 + "╗") - print("║" + " " * 10 + "Multi-Path Repository Analysis Examples" + " " * 8 + "║") - print("╚" + "=" * 58 + "╝") - - try: - # Note: These examples use placeholder paths - # Replace with actual repository paths to run - - print("\nNOTE: Examples use placeholder paths.") - print("Replace with actual paths to execute.\n") - - # Uncomment to run examples: - # example_single_path() - # example_multiple_paths() - # example_repo_analyzer_multi_path() - # example_component_details() - # example_saving_output() - - print("\n✅ Examples ready to run (update paths first)") - - except Exception as e: - print(f"\n❌ Error: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 651408c4..2e6e8d78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,9 @@ dependencies = [ "litellm>=1.77.0", "pydantic>=2.11.7", "pydantic-settings>=2.10.1", - "pydantic-ai>=2.0,<3.0", + "pydantic-ai>=2.16,<3.0", "requests>=2.32.4", + "httpx>=0.27", "python-dotenv>=1.1.1", "rich>=14.1.0", "networkx>=3.5", diff --git a/requirements.txt b/requirements.txt index 993722f3..1145e9ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -105,8 +105,8 @@ pyasn1_modules==0.4.2 pycares==4.11.0 pycparser==2.23 pydantic==2.11.7 -pydantic-ai>=2.0,<3.0 -pydantic-ai-slim>=2.0,<3.0 +pydantic-ai>=2.16,<3.0 +pydantic-ai-slim>=2.16,<3.0 pydantic-evals>=1.37.0 pydantic-graph>=1.37.0 pydantic-settings==2.10.1 diff --git a/test-multi-path/.debug_parser.md b/test-multi-path/.debug_parser.md deleted file mode 100644 index 5c04d630..00000000 --- a/test-multi-path/.debug_parser.md +++ /dev/null @@ -1,55 +0,0 @@ - -Diagnostic script that verifies the `DependencyParser` can locate and parse Python source files across single and multi-path repository configurations. - -## Key Components - -| Element | Description | -|---|---| -| `parser_single` | `DependencyParser` instance scanning only the `main/` subdirectory | -| `parser_multi` | `DependencyParser` instance scanning both `main/` and `deps/` subdirectories | -| `components_single` / `components_multi` | Parsed component dictionaries printed for quick inspection | - -## Usage Example - -```bash -# Run from the repository root -python debug_parser.py -``` - -Expected output: - -```text -Main path: /path/to/tests/main -Deps path: /path/to/tests/deps -Main exists: True -Deps exists: True - -Files in main: - - example.py - -Files in deps: - - helper.py - -====================================================================== -Testing single path parser... -====================================================================== - -Single path found 3 components - - example.MyClass: class - -====================================================================== -Testing multi-path parser... -====================================================================== - -Multi-path found 7 components - - helper.util_fn: function -``` - -## Notes - -- Prepends `CODEWIKI_ROOT` to `sys.path` so `codewiki` is importable without installation. -- Globs only `*.py` files when listing directory contents — non-Python files are ignored. -- Prints at most **5** components for the single-path run and **10** for the multi-path run to keep output readable. -- Useful when adding new test fixtures under `main/` or `deps/` to confirm the parser picks them up before running the full test suite. - -**Source:** [`debug_parser.py`](https://github.com/flamingo-stack/CodeWiki/blob/main/debug_parser.py) \ No newline at end of file diff --git a/test-multi-path/.integration_test.md b/test-multi-path/.integration_test.md deleted file mode 100644 index 481e40df..00000000 --- a/test-multi-path/.integration_test.md +++ /dev/null @@ -1,39 +0,0 @@ - -Concise documentation for `integration_test.py`: - ---- - -An end-to-end integration test script that validates CodeWiki's multi-path dependency analysis pipeline by creating a temporary multi-directory project (`main/`, `deps/`, `vendor/`) with cross-path Python imports, then running it through the full `Config` → `DependencyGraphBuilder` pipeline and asserting on the resulting component graph and namespacing. - -## Key Components - -- **`TestResults`** — Accumulates named pass/fail test outcomes with optional detail messages; provides `add_test()`, `print_summary()` (formatted report with counts and warnings), and `all_passed()` for overall status. -- **`IntegrationTestRunner`** — Orchestrates the full test workflow via sequential steps: - - `setup_test_environment()` — Creates a temp directory with `main/`, `deps/`, `vendor/` subdirectories. - - `_create_main_files()`, `_create_deps_files()`, `_create_vendor_files()` — Populate each directory with sample Python modules exhibiting realistic cross-path imports (e.g., `main/service.py` imports from `deps.helper` and `vendor.logger`). - - `create_config()` — Builds a `Config` object using `main/` as the repo root and `deps/`, `vendor/` as `additional_source_paths`. - - `validate_paths()` — Confirms all configured source paths exist on disk. - - `execute_dependency_parser()` — Instantiates `DependencyGraphBuilder`, verifies multi-path mode detection, and runs `build_dependency_graph()` to produce `components` and `leaf_nodes`. - - `verify_namespaces()` — Groups discovered components by top-level namespace (derived from the dotted component ID prefix, e.g., `main`, `deps`, `vendor`) and prints a breakdown for manual/assertion-based inspection. - -## Usage Example - -```python -# Run as a standalone script -python integration_test.py - -# Or drive it programmatically -from integration_test import IntegrationTestRunner - -runner = IntegrationTestRunner() -runner.setup_test_environment() -runner.create_config() -runner.validate_paths() -runner.execute_dependency_parser() -runner.verify_namespaces() - -runner.results.print_summary() -assert runner.results.all_passed() -``` - -**Note:** This script is intended to be executed directly from the `tests/` (or similar) directory adjacent to the `codewiki` package, since it inserts `CODEWIKI_ROOT` (`Path(__file__).parent.parent`) into `sys.path` to import `codewiki.src.config.Config` and `codewiki.src.be.dependency_analyzer.DependencyGraphBuilder`. Temporary directories are created via `tempfile.mkdtemp` and should be cleaned up (via `shutil`) after test completion. \ No newline at end of file diff --git a/test-multi-path/.test_multi_path.md b/test-multi-path/.test_multi_path.md deleted file mode 100644 index c05f4c71..00000000 --- a/test-multi-path/.test_multi_path.md +++ /dev/null @@ -1,36 +0,0 @@ - -## Overview - -`test_multi_path.py` is a manual/integration test script for CodeWiki's multi-path analysis feature. It verifies that the `DependencyGraphBuilder` can correctly ingest, namespace, and cross-reference components discovered across multiple source directories (a primary repo path plus additional external paths) without ID collisions or missed dependencies. - -## Key Components - -- **`Colors`** — ANSI color code constants used to format terminal output (success, error, warning, headers). -- **`TestResults`** — Accumulator class that records pass/fail status per test via `add_test()` and prints a final tally with `print_summary()`, returning a process-friendly exit code (`0` = all passed, `1` = failures). -- **Print helpers** — `print_header()`, `print_success()`, `print_error()`, `print_warning()`, `print_info()` for consistent colored console output. -- **`create_test_config(repo_path, output_dir, additional_paths=None)`** — Builds a `Config` object pointing to test fixture directories (`main/`, `deps/`, `external/`), wiring in placeholder LLM model/API settings for the `DependencyGraphBuilder`. -- **Test functions** — Each exercises a specific scenario against `DependencyGraphBuilder.build_dependency_graph()`: - - `test_single_path()` — Confirms backward compatibility when only one repo path is supplied. - - `test_multiple_paths()` — Confirms components from `main/`, `deps/`, and `external/` are all discovered. - - `test_component_namespacing()` — Confirms component IDs are unique across paths and encode path context to avoid collisions. - - `test_cross_path_dependencies()` — Confirms dependency edges correctly link components across different source paths (e.g., `main/service.py` importing `deps/helper.py`). - -## Usage Example - -```bash -# Run the test suite directly from the tests directory -python3 test_multi_path.py -``` - -```python -# Programmatic invocation of an individual test -from test_multi_path import test_single_path, test_multiple_paths, TestResults - -results = TestResults() -results.add_test("single_path", test_single_path()) -results.add_test("multiple_paths", test_multiple_paths()) - -exit_code = results.print_summary() -``` - -Each test expects fixture directories (`main/`, `deps/`, `external/`) to exist alongside the script, containing sample Python files (e.g., `service.py`, `controller.py`, `helper.py`, `utils.py`, `plugin.py`) used to validate discovery and dependency resolution. \ No newline at end of file diff --git a/test-multi-path/INTEGRATION_TEST_RESULTS.md b/test-multi-path/INTEGRATION_TEST_RESULTS.md deleted file mode 100644 index 2681a264..00000000 --- a/test-multi-path/INTEGRATION_TEST_RESULTS.md +++ /dev/null @@ -1,185 +0,0 @@ -# Multi-Path Integration Test Results - -## Test Overview -Complete end-to-end integration test for CodeWiki's multi-path analysis feature. - -**Test File**: `/Users/michaelassraf/Documents/GitHub/CodeWiki/test-multi-path/integration_test.py` - -**Execution Date**: February 2, 2026 - ---- - -## Test Results: ✅ PASSED (9/9 assertions) - -### Test Stages - -#### 1. ✅ Test Environment Setup -- Created temporary test directory structure -- Generated 10 Python files across 3 namespaces: - - `main/` - 5 files (service, api, models, controller, utils) - - `deps/` - 3 files (helper, validator, cache) - - `vendor/` - 2 files (logger, metrics) -- Files include realistic cross-path imports - -#### 2. ✅ Configuration Creation -- Created `Config` with: - - Primary path: `main/` - - Additional paths: `deps/`, `vendor/` -- Verified `is_multi_path_mode()` returns `True` - -#### 3. ✅ Path Validation -- Validated all 3 source paths exist -- Confirmed paths are accessible - -#### 4. ✅ Dependency Graph Building -- Executed `DependencyGraphBuilder.build_dependency_graph()` -- Successfully parsed **14 components** (classes + functions) -- Identified **8 leaf nodes** - -#### 5. ✅ Namespace Verification -- Verified all 3 expected namespaces present: `main`, `deps`, `vendor` -- Component namespace format: `namespace.module.Component` -- Example: `main.service.MainService`, `deps.helper.format_data` - -#### 6. ✅ Component Count Verification -- **main**: 7 components (5 classes + 2 functions) ✓ -- **deps**: 5 components (1 class + 4 functions) ✓ -- **vendor**: 2 components (2 classes) ✓ -- **Total**: 14 components ✓ - -#### 7. ✅ Cross-Path Dependencies -- **Status**: No cross-path dependencies detected (expected behavior) -- **Reason**: AST parser currently parses import statements but does not resolve them to specific components across namespaces -- **Future Enhancement**: Cross-namespace dependency resolution can be added later - -#### 8. ✅ Warning Validation -- No parser warnings generated -- Clean execution - -#### 9. ✅ Final Component Count -- Expected: 14 components -- Actual: 14 components -- Match: ✓ - ---- - -## Key Findings - -### ✅ Multi-Path Mode Works Correctly -1. **Configuration**: `additional_source_paths` parameter accepted -2. **Detection**: `is_multi_path_mode()` correctly identifies multi-path configs -3. **Path Aggregation**: `all_source_paths` includes all configured paths -4. **Parsing**: `DependencyGraphBuilder` analyzes all paths -5. **Namespacing**: Components correctly namespaced by source directory - -### ✅ Namespace Format -- **Pattern**: `..` -- **Examples**: - ``` - main.service.MainService - main.api.API - deps.helper.format_data - deps.validator.validate_input - vendor.logger.Logger - vendor.metrics.MetricsCollector - ``` - -### 📝 Current Limitations (Not Bugs) -1. **Cross-Namespace Dependencies**: Not resolved by AST parser - - Import statements are parsed - - But dependencies not linked across namespaces - - Future enhancement opportunity - -2. **Component-Level Namespacing**: Uses class/function names, not file names - - This is by design - - Provides more granular component identification - ---- - -## Component Breakdown - -### Main Namespace (7 components) -| Component | Type | File | -|-----------|------|------| -| `main.service.MainService` | Class | service.py | -| `main.api.API` | Class | api.py | -| `main.models.User` | Class | models.py | -| `main.models.Product` | Class | models.py | -| `main.controller.Controller` | Class | controller.py | -| `main.utils.sanitize` | Function | utils.py | -| `main.utils.format_error` | Function | utils.py | - -### Deps Namespace (5 components) -| Component | Type | File | -|-----------|------|------| -| `deps.cache.Cache` | Class | cache.py | -| `deps.helper.format_data` | Function | helper.py | -| `deps.helper.merge_dicts` | Function | helper.py | -| `deps.validator.validate_input` | Function | validator.py | -| `deps.validator.validate_email` | Function | validator.py | - -### Vendor Namespace (2 components) -| Component | Type | File | -|-----------|------|------| -| `vendor.logger.Logger` | Class | logger.py | -| `vendor.metrics.MetricsCollector` | Class | metrics.py | - ---- - -## Test Code Quality - -### Features -1. **Comprehensive Coverage**: Tests entire pipeline end-to-end -2. **Realistic Scenario**: Creates actual Python files with imports -3. **Detailed Assertions**: 9 distinct validation points -4. **Clear Output**: Step-by-step progress with visual indicators -5. **Self-Cleaning**: Automatic cleanup of test artifacts -6. **Error Handling**: Graceful failure with detailed error messages - -### Output Quality -- ✓ Clear section headers with separators -- ✓ Progress indicators (✓, ✗, ⚠) -- ✓ Detailed component listings -- ✓ Namespace breakdown -- ✓ Comprehensive summary - ---- - -## Running the Test - -```bash -cd /Users/michaelassraf/Documents/GitHub/CodeWiki -source .venv/bin/activate -python3 test-multi-path/integration_test.py -``` - -**Expected Exit Code**: `0` (success) - ---- - -## Conclusion - -The multi-path analysis feature is **fully functional** and **production-ready**: - -1. ✅ Configuration system supports multiple source paths -2. ✅ Path aggregation works correctly -3. ✅ All paths are analyzed -4. ✅ Components are correctly namespaced -5. ✅ No errors or warnings generated -6. ✅ Component counts match expectations - -**Recommendation**: Feature ready for production use with optional future enhancement of cross-namespace dependency resolution. - ---- - -## Next Steps - -### Optional Enhancements -1. **Cross-Namespace Dependency Resolution**: Extend AST parser to resolve imports across namespaces -2. **Visualization**: Add dependency graph visualization for multi-path projects -3. **CLI Flag**: Add `--multi-path` flag for explicit multi-path mode indication - -### Documentation Updates -- Update README with multi-path examples -- Add multi-path configuration guide -- Document namespace format conventions diff --git a/test-multi-path/README.md b/test-multi-path/README.md deleted file mode 100644 index a74a6ba5..00000000 --- a/test-multi-path/README.md +++ /dev/null @@ -1,267 +0,0 @@ -# Multi-Path Integration Tests - -End-to-end integration tests for CodeWiki's multi-path analysis feature. - -## Quick Start - -```bash -cd /Users/michaelassraf/Documents/GitHub/CodeWiki -source .venv/bin/activate -python3 test-multi-path/integration_test.py -``` - -**Expected Output**: `✅ INTEGRATION TEST PASSED` with all 9 assertions passing. - ---- - -## Test Files - -| File | Purpose | -|------|---------| -| `integration_test.py` | Complete end-to-end integration test (executable) | -| `test_multi_path.py` | Unit tests for Config (legacy) | -| `INTEGRATION_TEST_RESULTS.md` | Detailed test results and findings | -| `README.md` | This file | - ---- - -## What the Integration Test Does - -### 1. Environment Setup -- Creates temporary test directory -- Generates 10 Python files across 3 namespaces -- Creates realistic cross-path imports - -### 2. Configuration -- Creates `Config` with `additional_source_paths` -- Verifies multi-path mode detection - -### 3. Execution -- Runs `DependencyGraphBuilder` -- Parses all source paths -- Generates dependency graph - -### 4. Validation -- ✓ All paths analyzed -- ✓ Correct namespace prefixes -- ✓ Expected component counts -- ✓ No errors or warnings - -### 5. Output -- Detailed progress tracking -- Component listings -- Namespace breakdown -- Validation summary - ---- - -## Test Structure - -``` -test-multi-path/ -├── integration_test.py # Main integration test (598 lines) -├── test_multi_path.py # Legacy unit tests -├── INTEGRATION_TEST_RESULTS.md # Test results documentation -└── README.md # This file -``` - ---- - -## Test Stages - -The integration test runs **8 distinct stages**: - -1. **Setup Test Environment** - Create temp directories and Python files -2. **Creating Configuration** - Initialize Config with additional paths -3. **Validating Paths** - Verify all paths exist -4. **Executing Dependency Graph Builder** - Run analysis pipeline -5. **Verifying Component Namespaces** - Check namespace prefixes -6. **Verifying Cross-Path Dependencies** - Check import resolution -7. **Checking for Warnings** - Validate clean execution -8. **Verifying Component Counts** - Final count validation - ---- - -## Expected Test Output - -``` -================================================================================ -CODEWIKI MULTI-PATH INTEGRATION TEST -================================================================================ - -Testing complete end-to-end pipeline: - 1. Test environment setup - 2. Multi-path configuration - 3. Dependency parsing - 4. Namespace verification - 5. Cross-path dependency resolution - 6. Validation and reporting - -================================================================================ -STEP 1: Setting Up Test Environment -================================================================================ -📁 Test directory: /var/folders/.../codewiki_integration_xxx - ✓ Created main/ → ... - ✓ Created deps/ → ... - ✓ Created vendor/ → ... - ✓ Created 5 files in main/ - ✓ Created 3 files in deps/ - ✓ Created 2 files in vendor/ - -[... stages 2-8 ...] - -================================================================================ -VALIDATION SUMMARY -================================================================================ - -Total Assertions: 9 - ✓ Passed: 9 - -================================================================================ -✅ INTEGRATION TEST PASSED -================================================================================ -``` - ---- - -## Test Results - -### ✅ All Assertions Passing -1. Test directories created -2. Config created with 3 source paths -3. All paths validated successfully -4. Multi-path mode detected -5. All expected namespaces present -6. Component counts match expectations -7. Multi-path mode working (dependencies optional) -8. No warnings generated -9. Component counts match expectations - ---- - -## Key Findings - -### Namespace Format -Components are identified as: `..` - -**Examples:** -``` -main.service.MainService -main.api.API -deps.helper.format_data -deps.validator.validate_input -vendor.logger.Logger -vendor.metrics.MetricsCollector -``` - -### Component Counts -- **main**: 7 components (5 classes + 2 functions) -- **deps**: 5 components (1 class + 4 functions) -- **vendor**: 2 components (2 classes) -- **Total**: 14 components - -### Current Limitations -- Cross-namespace dependency resolution not implemented in AST parser -- Import statements parsed but not resolved across namespaces -- Future enhancement opportunity (not a bug) - ---- - -## Troubleshooting - -### Test Fails with Import Error -```bash -# Ensure virtual environment is activated -source .venv/bin/activate - -# Verify dependencies installed -pip install -r requirements.txt -``` - -### Test Fails with Permission Error -```bash -# Make test executable -chmod +x test-multi-path/integration_test.py -``` - -### Test Hangs -```bash -# Check for stuck processes -ps aux | grep python - -# Kill if necessary -pkill -f "python.*integration_test" -``` - ---- - -## Running Individual Tests - -### Run Integration Test -```bash -python3 test-multi-path/integration_test.py -``` - -### Run Legacy Unit Tests -```bash -python3 test-multi-path/test_multi_path.py -``` - ---- - -## Exit Codes - -| Code | Meaning | -|------|---------| -| `0` | All tests passed | -| `1` | Some assertions failed | -| `2` | Test crashed with exception | - ---- - -## Test Coverage - -The integration test covers: -- ✓ Configuration initialization -- ✓ Path validation -- ✓ Multi-path detection -- ✓ Dependency graph building -- ✓ Component parsing -- ✓ Namespace assignment -- ✓ Component counting -- ✓ Error handling -- ✓ Cleanup - -**Coverage**: 100% of multi-path pipeline - ---- - -## Development Notes - -### Adding New Assertions -1. Add assertion in appropriate stage method -2. Call `self._assert("Description", condition)` -3. Update expected counts in validation summary - -### Adding New Test Files -1. Create files in `_create_*_files()` methods -2. Update expected component counts -3. Add expected namespace entries - -### Debugging Failed Tests -1. Check `/tmp/integration_test.log` for detailed output -2. Review component IDs in "DETAILED OUTPUT SUMMARY" -3. Verify namespace breakdown matches expectations - ---- - -## References - -- **Feature Documentation**: See `INTEGRATION_TEST_RESULTS.md` -- **Implementation**: `/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/config.py` -- **Parser**: `/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/` - ---- - -*Last Updated: February 2, 2026* -*Test Status: ✅ PASSING (9/9 assertions)* diff --git a/test-multi-path/TEST_RESULTS.md b/test-multi-path/TEST_RESULTS.md deleted file mode 100644 index d79eb0fe..00000000 --- a/test-multi-path/TEST_RESULTS.md +++ /dev/null @@ -1,326 +0,0 @@ -# Multi-Path Feature Test Results - -## Summary - -**Status:** ✅ **ALL TESTS PASSED** (7/7) - -The CodeWiki multi-path feature is working correctly and ready for production use. - -## Test Execution - -```bash -cd /Users/michaelassraf/Documents/GitHub/CodeWiki/test-multi-path -bash run_tests.sh -``` - -## Results Breakdown - -### ✅ Test 1: Single Path Analysis (Backward Compatibility) -**Status:** PASSED - -**What it tests:** -- Verifies that single-path analysis still works correctly -- Ensures backward compatibility with existing workflows - -**Results:** -- Found 2 components from `main/` directory -- Correctly identified `controller.py` (controller.APIController) -- Correctly identified `service.py` (service.MainService) - -**Conclusion:** Single-path mode works identically to original behavior. - ---- - -### ✅ Test 2: Multiple Paths With Unique Components -**Status:** PASSED - -**What it tests:** -- Validates multi-path analysis functionality -- Ensures all source directories are analyzed - -**Results:** -- Successfully analyzed 3 directories: `main/`, `deps/`, `external/` -- Found 9 total components across all paths: - - `main/`: 2 components (service.py, controller.py) - - `deps/`: 5 components (3 in helper.py, 2 in utils.py) - - `external/`: 2 components (plugin.py) - -**Conclusion:** Multi-path analysis correctly discovers components from all source directories. - ---- - -### ✅ Test 3: Component ID Namespacing -**Status:** PASSED - -**What it tests:** -- Ensures no component ID collisions between paths -- Verifies proper namespacing of component IDs - -**Results:** -- All 7 component IDs are unique -- Component IDs include path prefixes: - - `main.controller.APIController` - - `main.service.MainService` - - `deps.helper.validate_input` - - `deps.helper.process_data` - - `deps.helper.format_output` - - `deps.utils.sanitize_string` - - `deps.utils.calculate_hash` - -**Conclusion:** Namespacing prevents ID collisions and provides clear source attribution. - ---- - -### ✅ Test 4: Cross-Path Dependencies -**Status:** PASSED (with informational warnings) - -**What it tests:** -- Verifies dependency tracking across path boundaries -- Checks that imports between different source directories are detected - -**Results:** -- Found 7 components and 2 dependency edges -- Note: Warning about missing cross-path dependencies is informational - - The test data has imports but they may not create strong dependencies - - The dependency detection is working correctly - -**Conclusion:** Dependency tracking works across multiple source paths. - ---- - -### ✅ Test 5: Invalid Path Handling -**Status:** PASSED - -**What it tests:** -- Tests error handling for non-existent paths -- Verifies validation logic - -**Results:** -- Correctly raised `ValueError` for invalid path: - ``` - "Additional source path #1 does not exist: /nonexistent/path/that/does/not/exist" - ``` - -**Conclusion:** Validation correctly rejects invalid paths with clear error messages. - ---- - -### ✅ Test 6: Empty Additional Paths -**Status:** PASSED - -**What it tests:** -- Tests behavior with empty additional paths list -- Ensures no errors with edge case input - -**Results:** -- Successfully analyzed main path only -- Found 2 components from `main/` directory -- Behaved identically to single-path mode - -**Conclusion:** Empty additional paths list works correctly (graceful degradation to single-path). - ---- - -### ✅ Test 7: Relative vs Absolute Paths -**Status:** PASSED - -**What it tests:** -- Validates path resolution logic -- Ensures absolute paths work correctly - -**Results:** -- Successfully analyzed with absolute paths -- Found 7 components across multiple directories -- Path normalization handled correctly - -**Conclusion:** Absolute paths work correctly. Relative path support would require additional resolution logic. - ---- - -## Feature Validation - -### Core Capabilities ✅ - -1. **Multi-Path Analysis** - - ✅ Analyzes multiple source directories in single run - - ✅ Discovers all components from all paths - - ✅ Works with 2+ additional source paths - -2. **Component Namespacing** - - ✅ Prevents ID collisions with path prefixes - - ✅ Clear attribution of components to source paths - - ✅ Namespace format: `{source_dir}.{module}.{component}` - -3. **Backward Compatibility** - - ✅ Single-path mode unchanged - - ✅ Empty additional paths gracefully handled - - ✅ Existing workflows unaffected - -4. **Error Handling** - - ✅ Invalid paths detected and rejected - - ✅ Clear error messages for debugging - - ✅ Validation runs before analysis - -### Test Coverage - -- ✅ **Functionality:** All core features tested -- ✅ **Edge Cases:** Invalid inputs, empty lists, single path -- ✅ **Integration:** Full end-to-end analysis pipeline -- ✅ **Compatibility:** Backward compatibility verified - ---- - -## Test Data - -### Structure -``` -test-multi-path/ -├── main/ # Main codebase (2 files) -│ ├── service.py # Service class with imports from deps -│ └── controller.py # Controller using service -├── deps/ # Dependency package (2 files) -│ ├── helper.py # Helper utilities (3 functions) -│ └── utils.py # Additional utilities (2 functions) -├── external/ # External plugin (1 file) -│ └── plugin.py # Plugin interface (2 classes) -├── test_multi_path.py # Test runner -├── debug_parser.py # Debug script -└── run_tests.sh # Test execution script -``` - -### Cross-Path Dependencies -- `main/service.py` imports from `deps/helper.py` - ```python - from deps.helper import process_data, validate_input - ``` -- Creates realistic multi-path scenario - ---- - -## Configuration Example - -```python -from codewiki.src.config import Config - -config = Config( - repo_path="/path/to/main/codebase", - additional_source_paths=[ - "/path/to/dependencies", - "/path/to/plugins", - "/path/to/shared/library" - ], - # ... other required config fields -) - -# Validate paths before use -config.validate_source_paths() - -# Check if multi-path mode is enabled -if config.is_multi_path_mode(): - print(f"Analyzing {len(config.all_source_paths)} source paths") -``` - ---- - -## Performance Characteristics - -Based on test execution: - -- **Single Path Analysis:** < 1 second for 2 components -- **Multi-Path Analysis:** < 2 seconds for 9 components across 3 paths -- **Memory Usage:** Minimal overhead for multiple paths -- **Scalability:** Linear growth with number of paths - ---- - -## Known Limitations - -1. **Relative Paths:** Currently only absolute paths are fully supported - - Workaround: Convert relative to absolute before passing to Config - - Enhancement: Add automatic relative path resolution - -2. **Pattern Application:** File patterns (include/exclude) not yet tested across paths - - Test 5 placeholder exists for future implementation - - Current patterns apply to all paths equally - -3. **Circular Dependencies:** Not specifically tested - - Existing dependency graph logic should handle them - - No multi-path specific issues expected - ---- - -## Recommendations - -### For Production Use - -1. ✅ **Ready to use** - All core functionality working -2. ✅ **Stable** - Backward compatible, no breaking changes -3. ✅ **Validated** - Comprehensive test coverage - -### For Future Enhancements - -1. **Relative Path Resolution** - - Add automatic resolution relative to main repo_path - - Or relative to current working directory - -2. **Pattern Filtering Tests** - - Verify include/exclude patterns work across all paths - - Test pattern precedence and inheritance - -3. **Performance Tests** - - Benchmark with larger codebases (100+ files per path) - - Test with 10+ source paths - - Profile memory usage with large dependency graphs - -4. **Documentation** - - Add multi-path examples to user docs - - Document namespacing behavior - - Provide migration guide from single-path - ---- - -## Conclusion - -The multi-path feature is **production-ready** with: - -- ✅ All 7 tests passing -- ✅ Core functionality working correctly -- ✅ Backward compatibility maintained -- ✅ Proper error handling -- ✅ Clear component namespacing -- ✅ Cross-path dependency tracking - -The feature successfully enables CodeWiki to analyze codebases distributed across multiple directories, which is essential for: - -- Monorepos with shared libraries -- Projects with external dependencies -- Multi-package Python projects -- Documentation of interconnected codebases - -**Recommendation:** Proceed with production deployment. - ---- - -## Files Modified - -1. **`/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/config.py`** - - Added: `all_source_paths` property - - Added: `validate_source_paths()` method - - Added: `is_multi_path_mode()` method - - Added: `additional_source_paths` field - -2. **`/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py`** - - Updated: Use `config.all_source_paths` property - - Updated: Log multi-path mode when enabled - -3. **`/Users/michaelassraf/Documents/GitHub/CodeWiki/codewiki/src/be/dependency_analyzer/ast_parser.py`** - - Already supported multi-path in constructor - - Already implemented `_parse_multiple_repositories()` - - Already implemented component namespacing - ---- - -**Test Date:** February 2, 2026 -**Test Environment:** Python 3.14, CodeWiki venv -**Test Duration:** ~2 seconds total -**Test Status:** ✅ ALL PASSED diff --git a/test-multi-path/debug_parser.py b/test-multi-path/debug_parser.py deleted file mode 100644 index 60a62f1d..00000000 --- a/test-multi-path/debug_parser.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -"""Debug script to check if parser finds files.""" - -import sys -import os -from pathlib import Path - -# Add CodeWiki to path -CODEWIKI_ROOT = Path(__file__).parent.parent -sys.path.insert(0, str(CODEWIKI_ROOT)) - -from codewiki.src.be.dependency_analyzer.ast_parser import DependencyParser - -# Test paths -main_path = Path(__file__).parent / "main" -deps_path = Path(__file__).parent / "deps" - -print(f"Main path: {main_path}") -print(f"Deps path: {deps_path}") -print(f"Main exists: {main_path.exists()}") -print(f"Deps exists: {deps_path.exists()}") - -print("\nFiles in main:") -for f in main_path.glob("*.py"): - print(f" - {f.name}") - -print("\nFiles in deps:") -for f in deps_path.glob("*.py"): - print(f" - {f.name}") - -print("\n" + "="*70) -print("Testing single path parser...") -print("="*70) - -parser_single = DependencyParser(str(main_path)) -components_single = parser_single.parse_repository() -print(f"\nSingle path found {len(components_single)} components") -for comp_id, comp in list(components_single.items())[:5]: - print(f" - {comp_id}: {comp.component_type}") - -print("\n" + "="*70) -print("Testing multi-path parser...") -print("="*70) - -parser_multi = DependencyParser([str(main_path), str(deps_path)]) -components_multi = parser_multi.parse_repository() -print(f"\nMulti-path found {len(components_multi)} components") -for comp_id, comp in list(components_multi.items())[:10]: - print(f" - {comp_id}: {comp.component_type}") diff --git a/test-multi-path/deps/.helper.md b/test-multi-path/deps/.helper.md deleted file mode 100644 index f1adb3d4..00000000 --- a/test-multi-path/deps/.helper.md +++ /dev/null @@ -1,38 +0,0 @@ - -Helper utilities for validating, processing, and formatting dictionary-based data structures. Intended as a shared dependency module used alongside the main codebase. - -## Key Components - -| Function | Description | -|---|---| -| `validate_input(data)` | Checks that input is a `dict` containing required keys `"type"` and `"value"` | -| `process_data(data)` | Validates and transforms input, returning a new dict with uppercased type, stripped value, and a fixed timestamp | -| `format_output(data)` | Renders a processed data dict as a human-readable string | - -## Usage Example - -```python -from helper import validate_input, process_data, format_output - -raw = {"type": "alert", "value": " disk usage high "} - -# Validate before processing -if validate_input(raw): - result = process_data(raw) - print(result) - # { - # "original": {"type": "alert", "value": " disk usage high "}, - # "processed_type": "ALERT", - # "processed_value": "disk usage high", - # "timestamp": "2024-01-01T00:00:00Z" - # } - - print(format_output(result)) - # [ALERT] disk usage high -``` - -## Notes - -- `process_data` raises `ValueError` if validation fails — always validate first or handle the exception. -- `format_output` uses `.get()` with safe defaults (`"UNKNOWN"` / empty string), so it won't raise on partial data. -- The `timestamp` field is currently hardcoded; replace with `datetime.utcnow().isoformat()` for production use. \ No newline at end of file diff --git a/test-multi-path/deps/.utils.md b/test-multi-path/deps/.utils.md deleted file mode 100644 index ee97200c..00000000 --- a/test-multi-path/deps/.utils.md +++ /dev/null @@ -1,27 +0,0 @@ - -Additional utility functions for string sanitization and simple hash calculation. - -## Key Components - -- **`sanitize_string(text: str) -> str`** — Strips non-alphanumeric characters from a string, preserving spaces. -- **`calculate_hash(data: str) -> int`** — Returns a simple 8-digit bounded hash of a string using Python's built-in `hash()`. - -## Usage Example - -```python -from utils import sanitize_string, calculate_hash - -# Sanitize a string -clean = sanitize_string("Hello, World! #2024") -# clean → "Hello World 2024" - -# Calculate a hash -hash_val = calculate_hash("my-data-key") -# hash_val → integer in range [0, 99_999_999] -``` - -> **Note:** `calculate_hash` uses Python's built-in `hash()`, which is non-deterministic across processes by default (due to hash randomization). Do not use this for persistent or cross-process key lookups. - -## Source - -[`utils.py`](https://github.com/flamingo-stack/CodeWiki/blob/main/utils.py) \ No newline at end of file diff --git a/test-multi-path/deps/helper.py b/test-multi-path/deps/helper.py deleted file mode 100644 index 6bcb3884..00000000 --- a/test-multi-path/deps/helper.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Helper utilities for data processing. - -This module is in a separate dependency package that should be analyzed -alongside the main codebase. -""" - -from typing import Any, Dict - - -def validate_input(data: Dict[str, Any]) -> bool: - """Validate input data structure. - - Args: - data: Input data to validate - - Returns: - True if valid, False otherwise - """ - if not isinstance(data, dict): - return False - - required_keys = ["type", "value"] - return all(key in data for key in required_keys) - - -def process_data(data: Dict[str, Any]) -> Dict[str, Any]: - """Process input data and return transformed result. - - Args: - data: Input data to process - - Returns: - Processed data - """ - if not validate_input(data): - raise ValueError("Invalid input data") - - # Transform data - result = { - "original": data, - "processed_type": data["type"].upper(), - "processed_value": str(data["value"]).strip(), - "timestamp": "2024-01-01T00:00:00Z" - } - - return result - - -def format_output(data: Dict[str, Any]) -> str: - """Format processed data for output. - - Args: - data: Data to format - - Returns: - Formatted string representation - """ - return f"[{data.get('processed_type', 'UNKNOWN')}] {data.get('processed_value', '')}" diff --git a/test-multi-path/deps/utils.py b/test-multi-path/deps/utils.py deleted file mode 100644 index 0ce15091..00000000 --- a/test-multi-path/deps/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Additional utility functions. -""" - - -def sanitize_string(text: str) -> str: - """Remove special characters from string. - - Args: - text: Input text - - Returns: - Sanitized text - """ - return "".join(c for c in text if c.isalnum() or c.isspace()) - - -def calculate_hash(data: str) -> int: - """Calculate simple hash of string data. - - Args: - data: Input data - - Returns: - Hash value - """ - return hash(data) % (10 ** 8) diff --git a/test-multi-path/external/.plugin.md b/test-multi-path/external/.plugin.md deleted file mode 100644 index c552583e..00000000 --- a/test-multi-path/external/.plugin.md +++ /dev/null @@ -1,42 +0,0 @@ - -Defines the plugin interface and a concrete data-processing implementation for the CodeWiki external plugin system. - -## Key Components - -| Name | Type | Description | -|------|------|-------------| -| `PluginInterface` | Abstract Base Class | Base interface defining the plugin contract with `initialize()` and `execute()` methods | -| `DataPlugin` | Class | Concrete plugin implementation for data processing; inherits from `PluginInterface` | -| `initialize()` | Method | Sets up the plugin and returns `True` on success; must be called before `execute()` | -| `execute(context)` | Method | Runs plugin logic against a context dict and returns a result dict | - -## Usage Example - -```python -from plugin import DataPlugin - -# Instantiate and initialize the plugin -plugin = DataPlugin(name="my_data_plugin") -plugin.initialize() - -# Execute with a context payload -result = plugin.execute(context={"data": {"key": "value"}}) -print(result) -# Output: {'plugin': 'my_data_plugin', 'result': "Processed: {'key': 'value'}"} -``` - -> **Note:** Calling `execute()` before `initialize()` raises a `RuntimeError`. Always call `initialize()` first. - -## Extending the Interface - -```python -from plugin import PluginInterface - -class CustomPlugin(PluginInterface): - def initialize(self) -> bool: - # Custom setup logic - return True - - def execute(self, context: dict) -> dict: - return {"plugin": "custom", "result": context} -``` \ No newline at end of file diff --git a/test-multi-path/external/plugin.py b/test-multi-path/external/plugin.py deleted file mode 100644 index 6c961718..00000000 --- a/test-multi-path/external/plugin.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -External plugin module. - -This is from a third package that should also be analyzed. -""" - - -class PluginInterface: - """Base interface for plugins.""" - - def initialize(self) -> bool: - """Initialize the plugin. - - Returns: - True if successful - """ - raise NotImplementedError("Subclasses must implement initialize()") - - def execute(self, context: dict) -> dict: - """Execute plugin logic. - - Args: - context: Execution context - - Returns: - Result data - """ - raise NotImplementedError("Subclasses must implement execute()") - - -class DataPlugin(PluginInterface): - """Plugin for data processing.""" - - def __init__(self, name: str): - """Initialize data plugin. - - Args: - name: Plugin name - """ - self.name = name - self.initialized = False - - def initialize(self) -> bool: - """Initialize the plugin.""" - self.initialized = True - return True - - def execute(self, context: dict) -> dict: - """Execute data processing. - - Args: - context: Execution context with 'data' key - - Returns: - Processed result - """ - if not self.initialized: - raise RuntimeError("Plugin not initialized") - - data = context.get("data", {}) - return { - "plugin": self.name, - "result": f"Processed: {data}" - } diff --git a/test-multi-path/integration_test.py b/test-multi-path/integration_test.py deleted file mode 100755 index aec0184e..00000000 --- a/test-multi-path/integration_test.py +++ /dev/null @@ -1,617 +0,0 @@ -#!/usr/bin/env python3 -""" -End-to-End Integration Test for Multi-Path Analysis Pipeline - -Tests the complete workflow: -1. Test environment setup with multiple source paths -2. Sample Python files with cross-path imports -3. Full pipeline execution (Config → DependencyParser) -4. Comprehensive assertions on all outputs -5. Detailed validation reporting -""" - -import os -import sys -import shutil -import tempfile -from pathlib import Path -from typing import Dict, List, Set - -# Add CodeWiki to path -CODEWIKI_ROOT = Path(__file__).parent.parent -sys.path.insert(0, str(CODEWIKI_ROOT)) - -from codewiki.src.config import Config -from codewiki.src.be.dependency_analyzer import DependencyGraphBuilder - - -class TestResults: - """Standardized accumulator for integration test results""" - - def __init__(self): - self.tests = [] - - def add_test(self, name: str, passed: bool, details: str = "") -> None: - """Record a single test result""" - self.tests.append({ - "name": name, - "passed": passed, - "details": details - }) - - def print_summary(self) -> None: - """Print final validation summary""" - print("\n" + "="*80) - print("VALIDATION SUMMARY") - print("="*80) - - total = len(self.tests) - passed_tests = [t for t in self.tests if t["passed"]] - failed_tests = [t for t in self.tests if not t["passed"]] - warning_tests = [t for t in self.tests if t["passed"] and t["details"]] - - passed = len(passed_tests) - failed = len(failed_tests) - - print(f"\nTotal Assertions: {total}") - print(f" ✓ Passed: {passed}") - if failed > 0: - print(f" ✗ Failed: {failed}") - - if failed > 0: - print("\nFailed Assertions:") - for t in failed_tests: - msg = f" ✗ {t['name']}" - if t["details"]: - msg += f" - {t['details']}" - print(msg) - - if warning_tests: - print("\nWarnings:") - for t in warning_tests: - print(f" ⚠ {t['details']}") - - print("\n" + "="*80) - if failed == 0: - print("✅ INTEGRATION TEST PASSED") - else: - print("❌ INTEGRATION TEST FAILED") - print("="*80) - - def all_passed(self) -> bool: - return all(t["passed"] for t in self.tests) - - -class IntegrationTestRunner: - """Manages end-to-end integration test execution""" - - def __init__(self): - self.test_dir = None - self.main_path = None - self.deps_path = None - self.vendor_path = None - self.config = None - self.builder = None - self.components = None - self.leaf_nodes = None - self.results = TestResults() - - def setup_test_environment(self) -> None: - """Create test directory structure with sample files""" - print("\n" + "="*80) - print("STEP 1: Setting Up Test Environment") - print("="*80) - - # Create temporary test directory - self.test_dir = Path(tempfile.mkdtemp(prefix="codewiki_integration_")) - print(f"📁 Test directory: {self.test_dir}") - - # Create three source paths - self.main_path = self.test_dir / "main" - self.deps_path = self.test_dir / "deps" - self.vendor_path = self.test_dir / "vendor" - - self.main_path.mkdir() - self.deps_path.mkdir() - self.vendor_path.mkdir() - - print(f" ✓ Created main/ → {self.main_path}") - print(f" ✓ Created deps/ → {self.deps_path}") - print(f" ✓ Created vendor/ → {self.vendor_path}") - - # Create sample files in main/ - self._create_main_files() - - # Create sample files in deps/ - self._create_deps_files() - - # Create sample files in vendor/ - self._create_vendor_files() - - self.results.add_test("Test directories created", True) - - def _create_main_files(self) -> None: - """Create sample Python files in main/ directory""" - - # main/service.py - imports from deps and vendor - (self.main_path / "service.py").write_text(""" -# Main service that uses dependencies from other paths -from deps.helper import format_data -from vendor.logger import Logger - -class MainService: - def __init__(self): - self.logger = Logger('MainService') - - def process(self, data): - formatted = format_data(data) - self.logger.info(f"Processed: {formatted}") - return formatted -""") - - # main/api.py - imports from main/ and deps/ - (self.main_path / "api.py").write_text(""" -# API layer that uses service and validation -from service import MainService -from deps.validator import validate_input - -class API: - def __init__(self): - self.service = MainService() - - def handle_request(self, data): - if validate_input(data): - return self.service.process(data) - return None -""") - - # main/models.py - standalone model definitions - (self.main_path / "models.py").write_text(""" -# Data models -class User: - def __init__(self, name, email): - self.name = name - self.email = email - -class Product: - def __init__(self, id, name, price): - self.id = id - self.name = name - self.price = price -""") - - # main/controller.py - uses api and models - (self.main_path / "controller.py").write_text(""" -# Controller layer -from api import API -from models import User, Product - -class Controller: - def __init__(self): - self.api = API() - - def create_user(self, name, email): - user = User(name, email) - return self.api.handle_request(user) -""") - - # main/utils.py - utility functions - (self.main_path / "utils.py").write_text(""" -# Utility functions -def sanitize(text): - return text.strip().lower() - -def format_error(message): - return f"ERROR: {message}" -""") - - print(f" ✓ Created 5 files in main/: service.py, api.py, models.py, controller.py, utils.py") - - def _create_deps_files(self) -> None: - """Create sample Python files in deps/ directory""" - - # deps/helper.py - utility functions used by main - (self.deps_path / "helper.py").write_text(""" -# Helper utilities -def format_data(data): - if isinstance(data, dict): - return {k: str(v) for k, v in data.items()} - return str(data) - -def merge_dicts(*dicts): - result = {} - for d in dicts: - result.update(d) - return result -""") - - # deps/validator.py - validation logic - (self.deps_path / "validator.py").write_text(""" -# Input validation -from vendor.logger import Logger - -logger = Logger('Validator') - -def validate_input(data): - if data is None: - logger.error("Data is None") - return False - return True - -def validate_email(email): - return '@' in email and '.' in email -""") - - # deps/cache.py - caching layer - (self.deps_path / "cache.py").write_text(""" -# Simple cache implementation -class Cache: - def __init__(self): - self._data = {} - - def get(self, key): - return self._data.get(key) - - def set(self, key, value): - self._data[key] = value -""") - - print(f" ✓ Created 3 files in deps/: helper.py, validator.py, cache.py") - - def _create_vendor_files(self) -> None: - """Create sample Python files in vendor/ directory""" - - # vendor/logger.py - logging utility - (self.vendor_path / "logger.py").write_text(""" -# Logging utility -import sys - -class Logger: - def __init__(self, name): - self.name = name - - def info(self, message): - print(f"[INFO] {self.name}: {message}") - - def error(self, message): - print(f"[ERROR] {self.name}: {message}", file=sys.stderr) -""") - - # vendor/metrics.py - metrics collection - (self.vendor_path / "metrics.py").write_text(""" -# Metrics collection -from logger import Logger - -class MetricsCollector: - def __init__(self): - self.logger = Logger('Metrics') - self.counters = {} - - def increment(self, name): - self.counters[name] = self.counters.get(name, 0) + 1 - self.logger.info(f"Counter {name}: {self.counters[name]}") -""") - - print(f" ✓ Created 2 files in vendor/: logger.py, metrics.py") - - def create_config(self) -> None: - """Create Config with additional_source_paths""" - print("\n" + "="*80) - print("STEP 2: Creating Configuration") - print("="*80) - - output_dir = str(self.test_dir / "output") - - # Create config with main path as root, others as additional - self.config = Config( - repo_path=str(self.main_path), - output_dir=output_dir, - dependency_graph_dir=output_dir, - docs_dir=output_dir, - max_depth=5, - main_model="gpt-4", - cluster_model="gpt-4", - fallback_model="claude-3", - cluster_api_key="test", - main_api_key="test", - fallback_api_key="test", - additional_source_paths=[ - str(self.deps_path), - str(self.vendor_path) - ] - ) - - print(f"✓ Config created with root: {self.config.repo_path}") - print(f"✓ Additional paths: {len(self.config.additional_source_paths)}") - for i, path in enumerate(self.config.additional_source_paths, 1): - print(f" {i}. {path}") - - self.results.add_test("Config created with 3 source paths", True) - - def validate_paths(self) -> None: - """Validate all configured paths exist""" - print("\n" + "="*80) - print("STEP 3: Validating Paths") - print("="*80) - - # Validate root path - root_exists = Path(self.config.repo_path).exists() - print(f"{'✓' if root_exists else '✗'} Root path exists: {self.config.repo_path}") - - # Validate additional paths - all_valid = root_exists - for path in self.config.additional_source_paths: - exists = Path(path).exists() - print(f"{'✓' if exists else '✗'} Additional path exists: {path}") - all_valid = all_valid and exists - - self.results.add_test("All paths validated successfully", all_valid) - - def execute_dependency_parser(self) -> None: - """Run DependencyGraphBuilder with all configured paths""" - print("\n" + "="*80) - print("STEP 4: Executing Dependency Graph Builder") - print("="*80) - - self.builder = DependencyGraphBuilder(self.config) - - # Check if multi-path mode was detected - is_multi = self.config.is_multi_path_mode() - print(f"{'✓' if is_multi else '✗'} Multi-path mode detected: {is_multi}") - self.results.add_test("Multi-path mode detected", is_multi) - - # Build dependency graph - print("\nBuilding dependency graph...") - self.components, self.leaf_nodes = self.builder.build_dependency_graph() - - print(f"✓ Dependency graph building complete") - print(f" Total components found: {len(self.components)}") - print(f" Total leaf nodes found: {len(self.leaf_nodes)}") - - def verify_namespaces(self) -> None: - """Verify components have correct namespace prefixes""" - print("\n" + "="*80) - print("STEP 5: Verifying Component Namespaces") - print("="*80) - - components = self.components - - # Group components by top-level namespace (first part before .) - namespaces: Dict[str, List[str]] = {} - for comp_id in components.keys(): - # Extract top-level namespace (e.g., "main" from "main.service.MainService") - top_level = comp_id.split(".")[0] - namespaces.setdefault(top_level, []).append(comp_id) - - print(f"\nFound {len(namespaces)} top-level namespaces:") - for namespace, comp_ids in sorted(namespaces.items()): - print(f"\n Namespace '{namespace}': {len(comp_ids)} components") - for comp_id in sorted(comp_ids): - print(f" - {comp_id}") - - # Expected top-level namespaces - expected = {"main", "deps", "vendor"} - found = set(namespaces.keys()) - - missing = expected - found - unexpected = found - expected - - if missing: - print(f"\n✗ Missing namespaces: {missing}") - self.results.add_test("All expected namespaces present", False) - else: - print(f"\n✓ All expected namespaces present: {expected}") - self.results.add_test("All expected namespaces present", True) - - if unexpected: - print(f"⚠ Unexpected namespaces: {unexpected}") - self.results.add_test("No unexpected namespaces", True, f"Unexpected namespaces: {unexpected}") - - # Verify component counts - self._verify_namespace_counts(namespaces) - - def _verify_namespace_counts(self, namespaces: Dict[str, List[str]]) -> None: - """Verify expected component counts per namespace""" - # Expected counts at CLASS/FUNCTION level, not file level - # main: MainService, API, Product, User, Controller, format_error, sanitize = 7 - # deps: Cache, format_data, merge_dicts, validate_input, validate_email = 5 - # vendor: Logger, MetricsCollector = 2 - expected_counts = { - "main": 7, # Classes + functions - "deps": 5, # Classes + functions - "vendor": 2 # Classes only - } - - print("\nVerifying component counts (classes + functions):") - all_match = True - for namespace, expected in expected_counts.items(): - actual = len(namespaces.get(namespace, [])) - match = actual == expected - print(f" {'✓' if match else '✗'} Namespace '{namespace}': expected {expected}, found {actual}") - if not match: - print(f" Found components: {sorted(namespaces.get(namespace, []))}") - all_match = all_match and match - - self.results.add_test("Component counts match expectations", all_match) - - def verify_cross_path_dependencies(self) -> None: - """Verify cross-path dependencies are correctly resolved""" - print("\n" + "="*80) - print("STEP 6: Verifying Cross-Path Dependencies") - print("="*80) - - components = self.components - - # Find cross-namespace dependencies (using dot notation) - cross_deps: List[tuple] = [] - for comp_id, component in components.items(): - # Extract top-level namespace - comp_namespace = comp_id.split(".")[0] - - # Check if component has dependencies attribute - if hasattr(component, 'dependencies'): - for dep_id in component.dependencies: - # Extract top-level namespace from dependency - dep_namespace = dep_id.split(".")[0] - if dep_namespace != comp_namespace and dep_namespace in ["main", "deps", "vendor"]: - cross_deps.append((comp_id, dep_id)) - - print(f"\nFound {len(cross_deps)} cross-namespace dependencies:") - for source, target in sorted(cross_deps): - print(f" {source}") - print(f" → {target}") - - # NOTE: Cross-path dependency resolution is not currently implemented in AST parser - # This is expected behavior - dependencies are detected at import statement level - # but not resolved to specific components across namespaces - if len(cross_deps) > 0: - print(f"\n✓ Cross-path dependencies detected: {len(cross_deps)} found") - self.results.add_test("Cross-path dependencies detected", True) - else: - print(f"\n✓ No cross-path dependencies detected (expected - not implemented in AST parser yet)") - print(f" Note: Import statements are parsed but not resolved across namespaces") - self.results.add_test("Multi-path mode working (dependencies optional)", True) - - def verify_no_warnings(self) -> None: - """Verify no 'not found' warnings were generated""" - print("\n" + "="*80) - print("STEP 7: Checking for Warnings") - print("="*80) - - # Check if parser has warnings attribute - has_warnings = hasattr(self.builder, 'warnings') - - if not has_warnings: - print("✓ No warning tracking mechanism (expected behavior)") - self.results.add_test("No warnings generated", True) - return - - warnings = getattr(self.builder, 'warnings', []) - - if warnings: - print(f"⚠ Found {len(warnings)} warnings:") - for warning in warnings: - print(f" - {warning}") - self.results.add_test("No warnings generated", False) - else: - print("✓ No warnings generated") - self.results.add_test("No warnings generated", True) - - def verify_file_counts(self) -> None: - """Verify expected number of components were analyzed""" - print("\n" + "="*80) - print("STEP 8: Verifying Component Counts") - print("="*80) - - components = self.components - - # Count components per namespace - comp_counts: Dict[str, int] = {} - for comp_id in components.keys(): - namespace = comp_id.split(".")[0] - comp_counts[namespace] = comp_counts.get(namespace, 0) + 1 - - print("\nComponent counts by namespace:") - total = 0 - for namespace, count in sorted(comp_counts.items()): - print(f" {namespace}: {count} components") - total += count - - expected_total = 14 # 7 main + 5 deps + 2 vendor - print(f"\nTotal components analyzed: {total}") - print(f"Expected: {expected_total}") - - match = total == expected_total - self.results.add_test("Component counts match expectations", match) - - def print_detailed_output(self) -> None: - """Print comprehensive analysis output""" - print("\n" + "="*80) - print("DETAILED OUTPUT SUMMARY") - print("="*80) - - print("\n1. ALL COMPONENT IDs FOUND:") - print("-" * 40) - for comp_id in sorted(self.components.keys()): - print(f" {comp_id}") - - print("\n2. CROSS-NAMESPACE DEPENDENCIES:") - print("-" * 40) - cross_deps = [] - for comp_id, component in self.components.items(): - comp_namespace = comp_id.split(".")[0] - if hasattr(component, 'dependencies'): - for dep_id in component.dependencies: - dep_namespace = dep_id.split(".")[0] - if dep_namespace != comp_namespace and dep_namespace in ["main", "deps", "vendor"]: - cross_deps.append((comp_id, dep_id)) - - if cross_deps: - for source, target in sorted(cross_deps): - print(f" {source} → {target}") - else: - print(" (none detected)") - - print("\n3. NAMESPACE → COMPONENT COUNT:") - print("-" * 40) - namespaces: Dict[str, int] = {} - for comp_id in self.components.keys(): - namespace = comp_id.split(".")[0] - namespaces[namespace] = namespaces.get(namespace, 0) + 1 - - for namespace, count in sorted(namespaces.items()): - print(f" {namespace}: {count} components") - - def cleanup(self) -> None: - """Clean up test directory""" - if self.test_dir and self.test_dir.exists(): - shutil.rmtree(self.test_dir) - print(f"\n🧹 Cleaned up test directory: {self.test_dir}") - - def run(self) -> int: - """Execute complete integration test""" - try: - self.setup_test_environment() - self.create_config() - self.validate_paths() - self.execute_dependency_parser() - self.verify_namespaces() - self.verify_cross_path_dependencies() - self.verify_no_warnings() - self.verify_file_counts() - self.print_detailed_output() - self.results.print_summary() - - return 0 if self.results.all_passed() else 1 - - except Exception as e: - print(f"\n❌ INTEGRATION TEST CRASHED: {e}") - import traceback - traceback.print_exc() - return 2 - - finally: - self.cleanup() - - -def main(): - """Main entry point""" - print("\n" + "="*80) - print("CODEWIKI MULTI-PATH INTEGRATION TEST") - print("="*80) - print("\nTesting complete end-to-end pipeline:") - print(" 1. Test environment setup") - print(" 2. Multi-path configuration") - print(" 3. Dependency parsing") - print(" 4. Namespace verification") - print(" 5. Cross-path dependency resolution") - print(" 6. Validation and reporting") - - runner = IntegrationTestRunner() - exit_code = runner.run() - - sys.exit(exit_code) - - -if __name__ == "__main__": - main() diff --git a/test-multi-path/main/.controller.md b/test-multi-path/main/.controller.md deleted file mode 100644 index d788ceaf..00000000 --- a/test-multi-path/main/.controller.md +++ /dev/null @@ -1,37 +0,0 @@ - -Handles HTTP-style API request routing by delegating business logic to `MainService` and tracking request counts. - -## Key Components - -- **`APIController`** — Main controller class that initializes a `MainService` instance and routes incoming requests to the appropriate handler. - - `__init__()` — Sets up the `MainService` dependency and initializes a request counter. - - `handle_request(endpoint, data)` — Routes requests based on endpoint path, returning processed results or health status. - -## Supported Endpoints - -| Endpoint | Behavior | -|---|---| -| `/process` | Delegates to `MainService.process_request()` | -| `/health` | Returns service health status and total request count | -| _(other)_ | Returns `{"error": "Unknown endpoint"}` | - -## Usage Example - -```python -from controller import APIController - -controller = APIController() - -# Process a request -response = controller.handle_request("/process", {"key": "value"}) - -# Check service health -health = controller.handle_request("/health", {}) -# {"status": "healthy", "requests": 1} -``` - -## Notes - -- Request count increments on **every** call to `handle_request`, including health checks. -- Unknown endpoints return an error dict rather than raising an exception — callers should check for the `"error"` key in the response. -- `MainService` is instantiated once at controller creation with the name `"api-service"`; it is not configurable via the constructor. \ No newline at end of file diff --git a/test-multi-path/main/.service.md b/test-multi-path/main/.service.md deleted file mode 100644 index abd10285..00000000 --- a/test-multi-path/main/.service.md +++ /dev/null @@ -1,51 +0,0 @@ - -Primary service component that manages application lifecycle and request processing, leveraging helper utilities from the `deps` package for validation and data transformation. - -## Key Components - -### `MainService` -The core service class responsible for managing service state and processing requests. - -| Method | Signature | Description | -|--------|-----------|-------------| -| `__init__` | `(name: str)` | Initializes the service with a name; sets `active = False` | -| `start` | `(config: dict) -> bool` | Validates config and activates the service | -| `process_request` | `(data: dict) -> dict` | Processes incoming data; raises `RuntimeError` if service is inactive | -| `stop` | `() -> None` | Deactivates the service | - -### External Dependencies -- `deps.helper.validate_input` — Validates configuration before startup -- `deps.helper.process_data` — Transforms raw request data - -## Usage Example - -```python -from service import MainService - -# Initialize and start the service -svc = MainService(name="my-service") - -config = {"timeout": 30, "retries": 3} -started = svc.start(config) - -if started: - # Process a request - response = svc.process_request({"key": "value"}) - print(response) - # { - # "status": "success", - # "service": "my-service", - # "result": - # } - - # Gracefully stop - svc.stop() -else: - print("Failed to start: invalid config") -``` - -## Notes - -- `process_request` raises `RuntimeError` if called before `start()` or after `stop()` -- `start()` returns `False` (without activating) when `validate_input` rejects the config -- Source: [`service.py`](https://github.com/flamingo-stack/CodeWiki/blob/main/service.py) \ No newline at end of file diff --git a/test-multi-path/main/controller.py b/test-multi-path/main/controller.py deleted file mode 100644 index 4ed6e950..00000000 --- a/test-multi-path/main/controller.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Controller component for handling API requests. -""" - -from service import MainService - - -class APIController: - """API Controller for routing requests.""" - - def __init__(self): - """Initialize the controller.""" - self.service = MainService("api-service") - self.request_count = 0 - - def handle_request(self, endpoint: str, data: dict) -> dict: - """Handle API request. - - Args: - endpoint: API endpoint path - data: Request data - - Returns: - Response data - """ - self.request_count += 1 - - if endpoint == "/process": - return self.service.process_request(data) - elif endpoint == "/health": - return {"status": "healthy", "requests": self.request_count} - else: - return {"error": "Unknown endpoint"} diff --git a/test-multi-path/main/service.py b/test-multi-path/main/service.py deleted file mode 100644 index 37343c02..00000000 --- a/test-multi-path/main/service.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Main service component. - -This is the primary service that uses helper utilities from the deps package. -""" - -from deps.helper import process_data, validate_input - - -class MainService: - """Main service class for the application.""" - - def __init__(self, name: str): - """Initialize the service. - - Args: - name: Service name - """ - self.name = name - self.active = False - - def start(self, config: dict) -> bool: - """Start the service with configuration. - - Args: - config: Configuration dictionary - - Returns: - True if started successfully - """ - if not validate_input(config): - return False - - self.active = True - return True - - def process_request(self, data: dict) -> dict: - """Process incoming request data. - - Args: - data: Request data to process - - Returns: - Processed response data - """ - if not self.active: - raise RuntimeError("Service not started") - - # Use helper from deps package - processed = process_data(data) - - return { - "status": "success", - "service": self.name, - "result": processed - } - - def stop(self): - """Stop the service.""" - self.active = False diff --git a/test-multi-path/run_tests.sh b/test-multi-path/run_tests.sh deleted file mode 100755 index b401f407..00000000 --- a/test-multi-path/run_tests.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -# Run multi-path tests with CodeWiki virtual environment - -set -e - -# Colors -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}==================================================================${NC}" -echo -e "${BLUE}CodeWiki Multi-Path Feature Test Suite${NC}" -echo -e "${BLUE}==================================================================${NC}" - -# Find CodeWiki root -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CODEWIKI_ROOT="$(dirname "$SCRIPT_DIR")" - -echo -e "\n${YELLOW}Setup:${NC}" -echo -e " Test directory: $SCRIPT_DIR" -echo -e " CodeWiki root: $CODEWIKI_ROOT" - -# Check virtual environment -if [ ! -d "$CODEWIKI_ROOT/venv" ]; then - echo -e "${RED}Error: Virtual environment not found at $CODEWIKI_ROOT/venv${NC}" - exit 1 -fi - -echo -e " Virtual env: $CODEWIKI_ROOT/venv" - -# Activate virtual environment and run tests -echo -e "\n${YELLOW}Running tests...${NC}\n" - -cd "$SCRIPT_DIR" -source "$CODEWIKI_ROOT/venv/bin/activate" -python test_multi_path.py - -exit_code=$? - -if [ $exit_code -eq 0 ]; then - echo -e "\n${GREEN}✓ Test suite completed successfully${NC}" -else - echo -e "\n${RED}✗ Test suite failed with exit code $exit_code${NC}" -fi - -exit $exit_code diff --git a/test-multi-path/test_multi_path.py b/test-multi-path/test_multi_path.py deleted file mode 100755 index fb8cb9fd..00000000 --- a/test-multi-path/test_multi_path.py +++ /dev/null @@ -1,546 +0,0 @@ -#!/usr/bin/env python3 -""" -Comprehensive test suite for CodeWiki multi-path feature. - -This script tests the ability to analyze code from multiple source directories -simultaneously while maintaining proper component namespacing and dependency tracking. - -Test Coverage: -1. Single path (backward compatibility) -2. Multiple paths with unique components -3. Component ID namespacing (no collisions) -4. Cross-path dependencies -5. Pattern application across paths -6. Invalid path handling -7. Empty additional paths -8. Relative vs absolute paths -""" - -import os -import sys -import json -import tempfile -import shutil -from pathlib import Path -from typing import Dict, List, Any - -# Add CodeWiki to path -CODEWIKI_ROOT = Path(__file__).parent.parent -sys.path.insert(0, str(CODEWIKI_ROOT)) - -from codewiki.src.config import Config -from codewiki.src.be.dependency_analyzer.dependency_graphs_builder import DependencyGraphBuilder - - -# ANSI color codes for output -class Colors: - GREEN = '\033[92m' - RED = '\033[91m' - YELLOW = '\033[93m' - BLUE = '\033[94m' - BOLD = '\033[1m' - END = '\033[0m' - - -class TestResults: - """Accumulates test results across the suite and prints a summary.""" - - def __init__(self): - self.results = {} - - def add_test(self, name: str, passed: bool): - """Record the result of a single test. - - Args: - name: Test name - passed: Whether the test passed - """ - self.results[name] = passed - - def print_summary(self) -> int: - """Print formatted summary of all recorded test results. - - Returns: - Exit code: 0 if all tests passed, 1 otherwise - """ - print_header("Test Results Summary") - - passed = sum(1 for r in self.results.values() if r) - total = len(self.results) - - for test_name, result in self.results.items(): - if result: - print_success(f"{test_name}") - else: - print_error(f"{test_name}") - - print(f"\n{Colors.BOLD}Total: {passed}/{total} tests passed{Colors.END}") - - if passed == total: - print(f"{Colors.GREEN}{Colors.BOLD}✓ All tests passed!{Colors.END}\n") - return 0 - else: - print(f"{Colors.RED}{Colors.BOLD}✗ Some tests failed{Colors.END}\n") - return 1 - - -def print_header(text: str): - """Print formatted test header.""" - print(f"\n{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}") - print(f"{Colors.BOLD}{Colors.BLUE}{text}{Colors.END}") - print(f"{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}\n") - - -def print_success(text: str): - """Print success message.""" - print(f"{Colors.GREEN}✓ {text}{Colors.END}") - - -def print_error(text: str): - """Print error message.""" - print(f"{Colors.RED}✗ {text}{Colors.END}") - - -def print_warning(text: str): - """Print warning message.""" - print(f"{Colors.YELLOW}⚠ {text}{Colors.END}") - - -def print_info(text: str): - """Print info message.""" - print(f" {text}") - - -def create_test_config( - repo_path: str, - output_dir: str, - additional_paths: List[str] = None -) -> Config: - """Create test configuration. - - Args: - repo_path: Main repository path - output_dir: Output directory - additional_paths: Additional source paths to analyze - - Returns: - Config instance - """ - return Config( - repo_path=repo_path, - output_dir=output_dir, - dependency_graph_dir=os.path.join(output_dir, "graphs"), - docs_dir=os.path.join(output_dir, "docs"), - max_depth=2, - main_model="gpt-4", - cluster_model="gpt-4", - fallback_model="gpt-3.5-turbo", - cluster_api_key="test-key", - main_api_key="test-key", - fallback_api_key="test-key", - cluster_base_url="http://localhost:4000", - main_base_url="http://localhost:4000", - fallback_base_url="http://localhost:4000", - additional_source_paths=additional_paths - ) - - -def test_single_path(): - """Test 1: Single path analysis (backward compatibility).""" - print_header("Test 1: Single Path Analysis (Backward Compatibility)") - - test_dir = Path(__file__).parent / "main" - output_dir = tempfile.mkdtemp(prefix="codewiki_test_single_") - - try: - config = create_test_config(str(test_dir), output_dir) - - print_info("Analyzing single path: main/") - builder = DependencyGraphBuilder(config) - components_dict, leaf_nodes = builder.build_dependency_graph() - - # components_dict is a Dict[str, Node], convert to dict for testing - components = {comp_id: comp.model_dump() for comp_id, comp in components_dict.items()} - if not components: - print_error("No components found") - return False - - print_success(f"Found {len(components)} components") - - # Check for expected files - expected_files = ["service.py", "controller.py"] - found_files = [] - for comp_id, comp_data in components.items(): - file_path = comp_data.get("file_path", "") - filename = os.path.basename(file_path) - if filename in expected_files: - found_files.append(filename) - print_info(f" - Found component: {filename} (ID: {comp_id})") - - if len(found_files) != len(expected_files): - print_error(f"Expected {len(expected_files)} files, found {len(found_files)}") - return False - - print_success("All expected components found in single path") - return True - - except Exception as e: - print_error(f"Test failed: {str(e)}") - import traceback - traceback.print_exc() - return False - finally: - shutil.rmtree(output_dir, ignore_errors=True) - - -def test_multiple_paths(): - """Test 2: Multiple paths with unique components.""" - print_header("Test 2: Multiple Paths With Unique Components") - - main_path = Path(__file__).parent / "main" - deps_path = Path(__file__).parent / "deps" - external_path = Path(__file__).parent / "external" - output_dir = tempfile.mkdtemp(prefix="codewiki_test_multi_") - - try: - config = create_test_config( - str(main_path), - output_dir, - additional_paths=[str(deps_path), str(external_path)] - ) - - print_info(f"Main path: {main_path}") - print_info(f"Additional paths: {deps_path}, {external_path}") - - builder = DependencyGraphBuilder(config) - components_dict, leaf_nodes = builder.build_dependency_graph() - - # components_dict is a Dict[str, Node], convert to dict for testing - components = {comp_id: comp.model_dump() for comp_id, comp in components_dict.items()} - if not components: - print_error("No components found") - return False - - print_success(f"Found {len(components)} total components") - - # Verify components from all paths - expected_components = { - "main": ["service.py", "controller.py"], - "deps": ["helper.py", "utils.py"], - "external": ["plugin.py"] - } - - found_by_path = {"main": [], "deps": [], "external": []} - - for comp_id, comp_data in components.items(): - file_path = comp_data.get("file_path", "") - filename = os.path.basename(file_path) - - if "main" in file_path: - found_by_path["main"].append(filename) - elif "deps" in file_path: - found_by_path["deps"].append(filename) - elif "external" in file_path: - found_by_path["external"].append(filename) - - all_found = True - for path_name, expected_files in expected_components.items(): - found_files = found_by_path[path_name] - print_info(f"\nPath: {path_name}/") - for filename in expected_files: - if filename in found_files: - print_success(f" ✓ {filename}") - else: - print_error(f" ✗ {filename} not found") - all_found = False - - if not all_found: - print_error("Some components missing from multi-path analysis") - return False - - print_success("\nAll components from all paths found") - return True - - except Exception as e: - print_error(f"Test failed: {str(e)}") - import traceback - traceback.print_exc() - return False - finally: - shutil.rmtree(output_dir, ignore_errors=True) - - -def test_component_namespacing(): - """Test 3: Component ID namespacing prevents collisions.""" - print_header("Test 3: Component ID Namespacing (No Collisions)") - - main_path = Path(__file__).parent / "main" - deps_path = Path(__file__).parent / "deps" - output_dir = tempfile.mkdtemp(prefix="codewiki_test_namespace_") - - try: - config = create_test_config( - str(main_path), - output_dir, - additional_paths=[str(deps_path)] - ) - - builder = DependencyGraphBuilder(config) - components_dict, leaf_nodes = builder.build_dependency_graph() - - # components_dict is a Dict[str, Node], convert to dict for testing - components = {comp_id: comp.model_dump() for comp_id, comp in components_dict.items()} - - # Check for ID uniqueness - component_ids = list(components.keys()) - if len(component_ids) != len(set(component_ids)): - print_error("Duplicate component IDs detected!") - return False - - print_success(f"All {len(component_ids)} component IDs are unique") - - # Verify namespacing pattern - for comp_id, comp_data in components.items(): - file_path = comp_data.get("file_path", "") - print_info(f"Component ID: {comp_id}") - print_info(f" File: {file_path}") - - # Component ID should include path information to prevent collisions - if "main" in file_path and "main" not in comp_id: - print_warning(f" Component ID may not include path namespace") - elif "deps" in file_path and "deps" not in comp_id: - print_warning(f" Component ID may not include path namespace") - - print_success("Component namespacing validated") - return True - - except Exception as e: - print_error(f"Test failed: {str(e)}") - import traceback - traceback.print_exc() - return False - finally: - shutil.rmtree(output_dir, ignore_errors=True) - - -def test_cross_path_dependencies(): - """Test 4: Dependencies between components in different paths.""" - print_header("Test 4: Cross-Path Dependencies") - - main_path = Path(__file__).parent / "main" - deps_path = Path(__file__).parent / "deps" - output_dir = tempfile.mkdtemp(prefix="codewiki_test_deps_") - - try: - config = create_test_config( - str(main_path), - output_dir, - additional_paths=[str(deps_path)] - ) - - print_info("Note: service.py imports from deps/helper.py") - builder = DependencyGraphBuilder(config) - components_dict, leaf_nodes = builder.build_dependency_graph() - - # components_dict is a Dict[str, Node], convert to dict for testing - components = {comp_id: comp.model_dump() for comp_id, comp in components_dict.items()} - - # Build edges from component dependencies - edges = [] - for comp_id, comp_data in components.items(): - depends_on = comp_data.get("depends_on", []) - if isinstance(depends_on, set): - depends_on = list(depends_on) - for dep in depends_on: - edges.append({"source": comp_id, "target": dep}) - - print_success(f"Found {len(components)} components") - print_success(f"Found {len(edges)} dependency edges") - - # Look for cross-path dependencies - cross_path_deps = [] - for edge in edges: - source_id = edge.get("source") - target_id = edge.get("target") - - if source_id and target_id: - source_comp = components.get(source_id, {}) - target_comp = components.get(target_id, {}) - - source_path = source_comp.get("file_path", "") - target_path = target_comp.get("file_path", "") - - # Check if dependency crosses path boundaries - source_in_main = "main" in source_path - source_in_deps = "deps" in source_path - target_in_main = "main" in target_path - target_in_deps = "deps" in target_path - - if (source_in_main and target_in_deps) or (source_in_deps and target_in_main): - cross_path_deps.append((source_path, target_path)) - print_info(f"Cross-path dependency: {os.path.basename(source_path)} → {os.path.basename(target_path)}") - - if cross_path_deps: - print_success(f"Found {len(cross_path_deps)} cross-path dependencies") - else: - print_warning("No cross-path dependencies detected (expected at least 1)") - print_warning("service.py should depend on helper.py") - - return True - - except Exception as e: - print_error(f"Test failed: {str(e)}") - import traceback - traceback.print_exc() - return False - finally: - shutil.rmtree(output_dir, ignore_errors=True) - - -def test_invalid_path_handling(): - """Test 6: Invalid path handling.""" - print_header("Test 6: Invalid Path Handling") - - main_path = Path(__file__).parent / "main" - invalid_path = "/nonexistent/path/that/does/not/exist" - output_dir = tempfile.mkdtemp(prefix="codewiki_test_invalid_") - - try: - config = create_test_config( - str(main_path), - output_dir, - additional_paths=[invalid_path] - ) - - print_info(f"Testing with invalid path: {invalid_path}") - - try: - # This should raise an error during validation - config.validate_source_paths() - print_error("Expected validation error for invalid path, but none was raised") - return False - - except (ValueError, OSError) as e: - print_success(f"Correctly raised error for invalid path: {str(e)}") - return True - - except Exception as e: - print_error(f"Unexpected error: {str(e)}") - return False - finally: - shutil.rmtree(output_dir, ignore_errors=True) - - -def test_empty_additional_paths(): - """Test 7: Empty additional paths list.""" - print_header("Test 7: Empty Additional Paths") - - main_path = Path(__file__).parent / "main" - output_dir = tempfile.mkdtemp(prefix="codewiki_test_empty_") - - try: - # Test with empty list - config = create_test_config( - str(main_path), - output_dir, - additional_paths=[] - ) - - print_info("Testing with empty additional_paths list") - builder = DependencyGraphBuilder(config) - components_dict, leaf_nodes = builder.build_dependency_graph() - - # components_dict is a Dict[str, Node], convert to dict for testing - components = {comp_id: comp.model_dump() for comp_id, comp in components_dict.items()} - if components: - print_success(f"Found {len(components)} components from main path") - return True - else: - print_error("No components found") - return False - - except Exception as e: - print_error(f"Test failed: {str(e)}") - import traceback - traceback.print_exc() - return False - finally: - shutil.rmtree(output_dir, ignore_errors=True) - - -def test_relative_vs_absolute_paths(): - """Test 8: Relative vs absolute path handling.""" - print_header("Test 8: Relative vs Absolute Paths") - - main_path = Path(__file__).parent / "main" - deps_path_relative = "deps" # Relative to main - deps_path_absolute = str(Path(__file__).parent / "deps") # Absolute - output_dir = tempfile.mkdtemp(prefix="codewiki_test_paths_") - - try: - print_info("Testing with absolute path") - config_abs = create_test_config( - str(main_path), - output_dir, - additional_paths=[deps_path_absolute] - ) - - builder = DependencyGraphBuilder(config_abs) - components_dict, leaf_nodes = builder.build_dependency_graph() - - # components_dict is a Dict[str, Node], convert to dict for testing - components_abs = {comp_id: comp.model_dump() for comp_id, comp in components_dict.items()} - - print_success(f"Absolute path: Found {len(components_abs)} components") - - # Note: Relative paths may not work as expected without proper resolution - print_info("\nNote: Relative paths require proper resolution logic") - print_info("Expected behavior: System should resolve relative to main repo_path") - - if len(components_abs) > 2: # Should have main + deps components - print_success("Multi-path analysis working with absolute paths") - return True - else: - print_warning("May only have analyzed main path") - return True # Not a failure, just informational - - except Exception as e: - print_error(f"Test failed: {str(e)}") - import traceback - traceback.print_exc() - return False - finally: - shutil.rmtree(output_dir, ignore_errors=True) - - -def run_all_tests(): - """Run all multi-path tests.""" - print_header("CodeWiki Multi-Path Feature Test Suite") - - tests = [ - ("Single Path (Backward Compatibility)", test_single_path), - ("Multiple Paths With Unique Components", test_multiple_paths), - ("Component ID Namespacing", test_component_namespacing), - ("Cross-Path Dependencies", test_cross_path_dependencies), - ("Invalid Path Handling", test_invalid_path_handling), - ("Empty Additional Paths", test_empty_additional_paths), - ("Relative vs Absolute Paths", test_relative_vs_absolute_paths), - ] - - test_results = TestResults() - - for test_name, test_func in tests: - try: - result = test_func() - test_results.add_test(test_name, result) - except Exception as e: - print_error(f"Test '{test_name}' crashed: {str(e)}") - import traceback - traceback.print_exc() - test_results.add_test(test_name, False) - - return test_results.print_summary() - - -if __name__ == "__main__": - sys.exit(run_all_tests()) diff --git a/test_short_id_normalization.py b/test_short_id_normalization.py index cdd71f0a..3d93dbdc 100644 --- a/test_short_id_normalization.py +++ b/test_short_id_normalization.py @@ -58,8 +58,7 @@ def test_short_id_normalization(): namespace="deps/lib-auth", component_type="class", file_path="/path/to/handlers.py", - relative_path="auth/handlers.py", - is_from_deps=True + relative_path="auth/handlers.py" ), } diff --git a/verify_fqdn_implementation.py b/verify_fqdn_implementation.py index 262c1062..9ea304f2 100755 --- a/verify_fqdn_implementation.py +++ b/verify_fqdn_implementation.py @@ -12,7 +12,7 @@ def check_node_model(): with open('codewiki/src/be/dependency_analyzer/models/core.py', 'r') as f: content = f.read() - required_fields = ['short_id:', 'namespace:', 'is_from_deps:'] + required_fields = ['short_id:', 'namespace:'] checks = {field: field in content for field in required_fields} print("✅ Node Model FQDN Fields Check:") @@ -28,14 +28,12 @@ def check_ast_parser(): content = f.read() checks = { - 'FQDN comment in multi-path': '# FQDN metadata fields' in content, + 'FQDN metadata comment': '# FQDN metadata fields' in content, 'fqdn = f"{namespace}.{original_id}"': 'fqdn = f"{namespace}.{original_id}"' in content, 'components[fqdn] = node': 'components[fqdn] = node' in content, 'id=fqdn': 'id=fqdn' in content, 'short_id=original_id': 'short_id=original_id' in content, 'namespace=namespace': 'namespace=namespace' in content, - 'is_from_deps=(repo_index > 0)': 'is_from_deps=(repo_index > 0)' in content, - 'is_from_deps=False': 'is_from_deps=False' in content, } print("\n✅ AST Parser FQDN Implementation Check:")