# Plan: `analyze_directory` MCP Tool

## Goal

Add an `analyze_directory` MCP tool that scans a source code directory, infers behavioral conventions per package, returns YAML grouped by module, and auto-persists to `{directory}/.dervish/grammars.yml`.

## Design Decisions (locked)

| Decision | Choice |
|----------|--------|
| Signature | New separate tool (not extending `infer_best_grammar`) |
| Persistence | Auto-save to `{directory}/.dervish/grammars.yml` (on by default, `persist=False` to opt out) |
| Output format | YAML grouped by top-level module, sorted by MDL |
| Heuristics | MDL threshold (default 200), drop `(other)`, drop no-grammar, optional `main_only` |
| YAML library | pyyaml (`yaml.dump()`) |

## Signature

```python
@mcp.tool()
def analyze_directory(
    directory: str,              # Path to source code directory
    slice: str = "package",      # "flat" or "package"
    min_coverage: float = 0.8,   # Outlier removal threshold
    prefer: str = "",            # Force CRX or iDRegEx
    kmax: int = 2,               # k-ORE context depth
    include: str = "",           # Glob filter (include)
    exclude: str = "",           # Glob filter (exclude)
    main_only: bool = False,     # Exclude test code paths
    max_mdl: float = 200,        # Drop groups above this MDL
    persist: bool = True,        # Auto-save to .dervish/
) -> str:
```

## Output format (YAML)

```yaml
# RAGSAK — 35 patterns (1594 methods, 462 files)

agents:
  - package: agents/rag/embabel
    methods: 52
    grammar: "info+"
    algorithm: CRX
    mdl: 28.0

modules:
  - package: modules/common/.../ids
    methods: 18
    grammar: "(of|requireSafeId)"
    algorithm: iDRegEx
    mdl: 34.0
  - package: modules/ingestion/.../batch
    methods: 16
    grammar: "info?.StepBuilder?.listener+?.build"
    algorithm: CRX
    mdl: 13.2

infrastructure:
  - package: infrastructure/.../service/cleanup
    methods: 4
    grammar: "info.(deleteByJobId+deleteByKnowledgeBaseId)"
    algorithm: CRX
    mdl: 7.0
```

## Persistence

- Write to `{directory}/.dervish/grammars.yml`
- Create `.dervish/` dir if missing
- Overwrite on each run (idempotent)
- File is committed to git alongside the code
- LLM reads it later without recomputing

## Heuristics

1. **MDL threshold** — drop groups with MDL > `max_mdl` (default 200)
2. **Drop `(other)`** — ungrouped methods, always excluded
3. **Drop no-grammar** — groups where both algorithms failed, always excluded
4. **`main_only`** — excludes test paths when enabled

Test path patterns for `main_only`:
```
**/test/**, **/tests/**, **/*Test*/**
**/*_test.*, **/*_spec.*, **/test_*.py
**/*Test.kt, **/*Test.java, **/*Test.js
**/*Spec.*, **/*_test.go
```

## Files to modify

| File | Change |
|------|--------|
| `bex/tag_preprocessor/analyze.py` | Add `_is_main_source()`, `_build_yaml_output()`, `_persist_grammars()`, wire `main_only` into `analyze_directory()` |
| `bex/mcp_server.py` | Add `analyze_directory` MCP tool |
| `README.md` | Document new tool in MCP tools table |
| `AGENTS.md` | Document new tool in MCP tools table |

## Implementation steps

### Step 1: `_is_main_source()` in `analyze.py`

```python
TEST_PATH_PATTERNS = [
    "**/test/**", "**/tests/**", "**/*Test*/**",
    "**/*_test.*", "**/*_spec.*", "**/test_*.py",
    "**/*Test.kt", "**/*Test.java", "**/*Test.js",
    "**/*Spec.*", "**/*_test.go",
]

def _is_main_source(filepath):
    """Return True if file path looks like main source (not test)."""
    for pattern in TEST_PATH_PATTERNS:
        if _match_glob(filepath, pattern):
            return False
    return True
```

### Step 2: `_build_yaml_output()` in `analyze.py`

Convert results dict to YAML grouped by top-level module:

```python
def _build_yaml_output(results, dir_path):
    """Build YAML output grouped by top-level module.
    
    Returns YAML string with grammar per package, sorted by MDL within each module.
    Skips (other) and no-grammar groups.
    """
```

Logic:
- Extract top-level module from package path (first directory component)
- Sort groups by MDL within each module
- Skip `(other)` and no-grammar groups
- Return `yaml.dump()` output

### Step 3: `_persist_grammars()` in `analyze.py`

```python
def _persist_grammars(yaml_content, dir_path):
    """Write grammars to {dir_path}/.dervish/grammars.yml"""
    dervish_dir = os.path.join(dir_path, ".dervish")
    os.makedirs(dervish_dir, exist_ok=True)
    with open(os.path.join(dervish_dir, "grammars.yml"), "w") as f:
        f.write(yaml_content)
```

### Step 4: Wire `analyze_directory()` in `analyze.py`

- Add `main_only` parameter
- When `main_only=True`, filter files through `_is_main_source()` before processing
- Apply MDL threshold filtering after inference
- Return filtered results

### Step 5: Add MCP tool in `mcp_server.py`

```python
@mcp.tool()
def analyze_directory(...) -> str:
```

Flow:
1. Call `analyze_directory()` from `tag_preprocessor.analyze`
2. Filter by `max_mdl`, drop `(other)`, drop no-grammar
3. Build YAML via `_build_yaml_output()`
4. If `persist=True`, call `_persist_grammars()`
5. Return YAML string

### Step 6: Update docs

- README.md: Add `analyze_directory` to MCP tools table
- AGENTS.md: Add `analyze_directory` to MCP tools table

## Verification

1. Run: `python -m bex.mcp_server` (starts MCP server)
2. Call `analyze_directory(directory="/home/tobi/Desktop/kesai/RAGSAK")`
3. Verify YAML output is grouped by module, sorted by MDL
4. Verify `.dervish/grammars.yml` created in RAGSAK
5. Run existing tests: `python -m pytest tests/`
6. Run: `python -m bex.tag_preprocessor.analyze /home/tobi/Desktop/kesai/RAGSAK --slice package --verbose` (verify no regression)
