182 lines
5.6 KiB
Markdown
182 lines
5.6 KiB
Markdown
# 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)
|