# Phase 3: Static Analysis Core — User Acceptance Testing

## Summary

Built the static analysis pipeline infrastructure and Python reference adapter. The system can now:
- Walk a git repository to discover source files
- Parse Python files for imports, definitions, and entry points
- Construct an import graph with centrality metrics
- Detect module boundaries via label propagation clustering
- Score files for LLM priority selection
- Detect test files by naming convention
- Parse coverage.xml reports when available

## Automated Verification

All checks pass:
```bash
ruff check src/unfamiliar/analysis/
mypy src/unfamiliar/analysis/
```

## Manual Testing Steps

### 1. Verify Import Chain

```bash
python -c "from unfamiliar.analysis import analyze_repository, AnalysisResult, AnalysisError"
python -c "from unfamiliar.analysis.adapters import create_default_registry"
python -c "from unfamiliar.analysis.models import ParsedFile, ImportGraph, DetectedModule"
```

Expected: All imports succeed without errors.

### 2. Run Analysis on This Repository

```bash
python -c "
from pathlib import Path
from unfamiliar.analysis import analyze_repository

result = analyze_repository(Path('.').resolve())
print(f'Files: {len(result.files)}')
print(f'Parsed: {len(result.parsed_files)}')
print(f'Languages: {result.languages}')
print(f'Entry points: {len(result.entry_points)}')
print(f'Modules: {len(result.modules)}')
"
```

Expected:
- Files count > 0
- Parsed files count <= files count (only Python files parsed in MVP)
- Languages includes "python"
- At least 1 entry point detected (cli.py)

### 3. Test File Discovery

```bash
python -c "
from pathlib import Path
from unfamiliar.analysis.discovery import discover_files
from unfamiliar.analysis.adapters import create_default_registry

registry = create_default_registry()
files = discover_files(Path('.').resolve(), registry)
print(f'Discovered {len(files)} files')
for f in files[:5]:
    print(f'  {f.path}: {f.language}, {f.size_bytes} bytes')
"
```

Expected:
- Discovers files via git ls-files
- Files have SHA-256 hashes
- Python files classified as "python"

### 4. Test Python Adapter Parsing

```bash
python -c "
from pathlib import Path
from unfamiliar.analysis.adapters import PythonAdapter

adapter = PythonAdapter()
content = Path('src/unfamiliar/cli.py').read_bytes()
parsed = adapter.parse_file(Path('src/unfamiliar/cli.py'), content)

print(f'Imports: {len(parsed.imports)}')
print(f'Definitions: {len(parsed.definitions)}')
print(f'Entry points: {len(parsed.entry_points)}')
print(f'Complexity: {parsed.complexity}')
"
```

Expected:
- Imports detected (typer, pathlib, etc.)
- Definitions detected (CLI functions)
- Entry point detected (Typer app)
- Complexity > 0

### 5. Test Import Resolution

```bash
python -c "
from unfamiliar.analysis.adapters import PythonAdapter

adapter = PythonAdapter()
repo_files = {
    'src/unfamiliar/cli.py',
    'src/unfamiliar/config.py',
    'src/unfamiliar/constants.py',
}

# Test absolute import
result = adapter.resolve_import(
    'src/unfamiliar/cli.py',
    'unfamiliar.config',
    repo_files
)
print(f'Absolute import resolution: {result}')

# Test relative import
result = adapter.resolve_import(
    'src/unfamiliar/cli.py',
    '.config',
    repo_files
)
print(f'Relative import resolution: {result}')

# Test external import (should return None)
result = adapter.resolve_import(
    'src/unfamiliar/cli.py',
    'typer',
    repo_files
)
print(f'External import resolution: {result}')
"
```

Expected:
- Absolute import resolves to `src/unfamiliar/config.py`
- Relative import resolves to `src/unfamiliar/config.py`
- External import returns None

ACTUAL (before fix):
- All three resolved to None ❌

ROOT CAUSE: Two bugs in `python_adapter.py`:
1. `_resolve_relative_import`: `parts[:-level]` navigated up one too many levels.
   Level=1 (`.`) should stay in the current package directory, not go to parent.
   Fixed: `go_up = level - 1`, navigate up `go_up` times.
2. `_resolve_absolute_import`: Only tried exact matches like `unfamiliar/config.py`,
   missing files in `src/` layout at `src/unfamiliar/config.py`.
   Fixed: Added suffix match `repo_file.endswith("/" + candidate)`.

ACTUAL (after fix — 2026-03-02):
- Absolute import resolution: src/unfamiliar/config.py ✓
- Relative import resolution:  src/unfamiliar/config.py ✓
- External import resolution:  None ✓

### 6. Test Import Graph Construction

```bash
python -c "
from pathlib import Path
from unfamiliar.analysis import analyze_repository
from unfamiliar.analysis.graph import get_hub_files, get_connected_components

result = analyze_repository(Path('.').resolve())
hubs = get_hub_files(result.import_graph, threshold=2)
print(f'Hub files (in-degree >= 2): {hubs}')

components = get_connected_components(result.import_graph)
print(f'Connected components: {len(components)}')
"
```

Expected:
- Hub files identified (files imported by multiple others)
- At least 1 connected component

### 7. Test Module Clustering

```bash
python -c "
from pathlib import Path
from unfamiliar.analysis import analyze_repository

result = analyze_repository(Path('.').resolve())
for module in result.modules[:5]:
    print(f'Module: {module.primary_path}')
    print(f'  Files: {module.file_count}')
    print(f'  Confidence: {module.confidence:.2f}')
    print(f'  Categories: {[c.value for c in module.category_hints]}')
"
```

Expected:
- Modules detected with confidence scores
- Category hints based on file patterns

### 8. Test File Scoring

```bash
python -c "
from pathlib import Path
from unfamiliar.analysis import analyze_repository

result = analyze_repository(Path('.').resolve())
print('Top 10 scored files:')
for sf in result.scored_files[:10]:
    print(f'  {sf.path}: {sf.score} ({sf.reasons})')
"
```

Expected:
- Files scored by entry point, centrality, complexity, test status
- Entry point file (cli.py) has highest score
- Scores sorted descending

### 9. Test Detection on Empty/Edge Cases

```bash
python -c "
from pathlib import Path
from unfamiliar.analysis.adapters import PythonAdapter

adapter = PythonAdapter()

# Test empty file
parsed = adapter.parse_file(Path('empty.py'), b'')
print(f'Empty file - imports: {len(parsed.imports)}, defs: {len(parsed.definitions)}')

# Test syntax error
parsed = adapter.parse_file(Path('broken.py'), b'def broken(')
print(f'Syntax error - imports: {len(parsed.imports)}, defs: {len(parsed.definitions)}')

# Test file with main guard
code = b'''
if __name__ == \"__main__\":
    print(\"hello\")
'''
parsed = adapter.parse_file(Path('main.py'), code)
print(f'Main guard - entry points: {len(parsed.entry_points)}')
"
```

Expected:
- Empty file: graceful handling, empty lists
- Syntax error: graceful handling, empty lists
- Main guard: entry point detected

## Expected Behaviors

1. **File Discovery**: Only tracked git files are discovered (respects .gitignore)
2. **Binary Files**: Skipped (fail UTF-8 decode)
3. **Parse Errors**: Return empty ParsedFile, log warning
4. **External Imports**: Return None (stdlib, third-party)
5. **Relative Imports**: Resolved relative to importing file
6. **Coverage Reports**: coverage.xml parsed when present

## Edge Cases to Check

- [x] Repository with no Python files → languages only has "unknown"
- [x] Repository with only test files → all files marked as test files
- [x] File with circular imports → handled without infinite loop
- [x] Very deep relative imports (....module) → handled or returns None
- [x] Empty repository → empty AnalysisResult

## Phase Completion Checklist

- [x] All ruff checks pass on analysis package
- [x] All mypy checks pass on analysis package
- [x] Import chain works without circular imports
- [x] analyze_repository runs end-to-end
- [x] Python adapter parses files correctly
- [x] Import resolution handles relative and absolute imports (fixed 2026-03-02)
- [x] Module clustering produces valid output
- [x] File scoring ranks by importance signals
- [x] Test detection identifies test files
