feature/treesitter-tag-queries #2
4 changed files with 66 additions and 14 deletions
20
README.md
20
README.md
|
|
@ -51,14 +51,19 @@ The primary interface is a **Model Context Protocol (MCP)** server. Connect any
|
||||||
| Tool | Parameters | What it does |
|
| Tool | Parameters | What it does |
|
||||||
|------|-----------|-------------|
|
|------|-----------|-------------|
|
||||||
| `infer_best_grammar` | `sequences`, `prefer`, `kmax`, `N`, `min_coverage` | Infer grammar from raw sequences. Runs CRX + iDRegEx, picks best by MDL. Set `prefer` to run only one algorithm. Set `min_coverage < 1.0` for optional core+outlier analysis. |
|
| `infer_best_grammar` | `sequences`, `prefer`, `kmax`, `N`, `min_coverage` | Infer grammar from raw sequences. Runs CRX + iDRegEx, picks best by MDL. Set `prefer` to run only one algorithm. Set `min_coverage < 1.0` for optional core+outlier analysis. |
|
||||||
| `analyze_directory` | `directory`, `slice`, `min_coverage`, `prefer`, `kmax`, `include`, `exclude`, `main_only`, `max_mdl`, `persist` | Scan a source code directory and infer behavioral conventions per package. Returns YAML grouped by module. Auto-persists to `{directory}/.dervish/grammars.yml`. |
|
| `analyze_directory` | `directory`, `slice`, `min_coverage`, `prefer`, `kmax`, `include`, `exclude`, `main_only`, `max_mdl`, `persist`, `min_structure`, `decompose`, `max_seq_length`, `cluster_method`, `crx_method` | Scan a source code directory and infer behavioral conventions per package. Returns YAML grouped by module. Auto-persists to `{directory}/.dervish/grammars.yml`. |
|
||||||
|
|
||||||
**`analyze_directory` parameters:**
|
**`analyze_directory` parameters:**
|
||||||
- **`directory`**: Path to source code directory to analyze.
|
- **`directory`**: Path to source code directory to analyze.
|
||||||
- **`slice`**: `'package'` (per-directory, default) or `'flat'` (one per language).
|
- **`slice`**: `'package'` (per-directory, default) or `'flat'` (one per language).
|
||||||
- **`main_only`**: Exclude test files when `True`. Default `False`.
|
- **`main_only`**: Exclude test files when `True`. Default `False`.
|
||||||
- **`max_mdl`**: Drop groups with MDL above this threshold. Default 200. Lower = tighter patterns only.
|
- **`max_mdl`**: Drop groups with MDL above this threshold. Default 200. Lower = tighter patterns only.
|
||||||
|
- **`min_structure`**: Minimum grammar structure score (0.0–1.0). Only returns grammars at or above this threshold. Default 0.5.
|
||||||
- **`persist`**: Write results to `{directory}/.dervish/grammars.yml`. Default `True`.
|
- **`persist`**: Write results to `{directory}/.dervish/grammars.yml`. Default `True`.
|
||||||
|
- **`decompose`**: Break long sequences into shorter fragments before inference. Helps on diverse codebases. Default `False`.
|
||||||
|
- **`max_seq_length`**: Max fragment length when decomposing. Default 5.
|
||||||
|
- **`cluster_method`**: `'first-symbol'` (fast) or `'distributional'` (context-similarity). Default `'first-symbol'`.
|
||||||
|
- **`crx_method`**: `'standard'` (fast) or `'refined'` (cluster-then-infer). Default `'standard'`.
|
||||||
- Other parameters same as `infer_best_grammar`.
|
- Other parameters same as `infer_best_grammar`.
|
||||||
|
|
||||||
**Parameters explained for `infer_best_grammar`:**
|
**Parameters explained for `infer_best_grammar`:**
|
||||||
|
|
@ -130,9 +135,12 @@ For analyzing source code directories (tree-sitter based):
|
||||||
```bash
|
```bash
|
||||||
python -m bex.tag_preprocessor.analyze /path/to/codebase --verbose
|
python -m bex.tag_preprocessor.analyze /path/to/codebase --verbose
|
||||||
python -m bex.tag_preprocessor.analyze /path/to/codebase --slice package --include '**/src/**'
|
python -m bex.tag_preprocessor.analyze /path/to/codebase --slice package --include '**/src/**'
|
||||||
|
python -m bex.tag_preprocessor.analyze /path/to/codebase --slice package --decompose --min-structure 0.5
|
||||||
```
|
```
|
||||||
|
|
||||||
Key flags: `--slice package` (per-directory grammars), `--verbose` (progress), `--include`/`--exclude` (glob filters), `--kore` (enable slow kORE in ensemble).
|
Key flags: `--slice package` (per-directory grammars), `--decompose` (break long sequences into fragments for better inference), `--min-structure 0.5` (return only high-structure grammars), `--cluster-method distributional` (context-similarity clustering), `--verbose` (progress), `--include`/`--exclude` (glob filters).
|
||||||
|
|
||||||
|
The pipeline outputs SORE grammars (converted to GBNF internally for llama.cpp constrained decoding).
|
||||||
|
|
||||||
## Why not just use a schema?
|
## Why not just use a schema?
|
||||||
|
|
||||||
|
|
@ -159,18 +167,20 @@ Dervish has been tested against public datasets from Ansible Galaxy, Helm, and G
|
||||||
|
|
||||||
The sweet spot: **multiple implementations of the same abstract task** with a shared but undocumented pattern. Not everything works — Dockerfiles, pre-commit configs, and schema-enforced formats are too rigid or too diverse to yield a convention.
|
The sweet spot: **multiple implementations of the same abstract task** with a shared but undocumented pattern. Not everything works — Dockerfiles, pre-commit configs, and schema-enforced formats are too rigid or too diverse to yield a convention.
|
||||||
|
|
||||||
> **kOREInference note:** Algorithm 4 (iDRegEx with MDL, arXiv 1004.2372) is included for paper-faithful correctness. On real tool-sequence data, its rwr₀ repair step returns ∅ because the k-OA is rarely SORE (interconnected symbols). The ensemble falls back to CRX or iDRegEx automatically.
|
> **kOREInference note:** Algorithm 4 (iDRegEx with MDL, arXiv 1004.2372) is available via `--kore` flag for paper-faithful correctness. On real tool-sequence data, its rwr₀ repair step returns ∅ because the k-OA is rarely SORE (interconnected symbols). The ensemble falls back to CRX or iDRegEx automatically.
|
||||||
|
|
||||||
## Algorithm Selection Guide
|
## Algorithm Selection Guide
|
||||||
|
|
||||||
| When | Use | Why |
|
| When | Use | Why |
|
||||||
|------|-----|-----|
|
|------|-----|-----|
|
||||||
| Clean, structured data with full vocabulary | **CRX** | Single-pass, deterministic. Accepts all sequences. |
|
| Clean, structured data with full vocabulary | **CRX** | Single-pass, deterministic. Accepts all sequences. |
|
||||||
| Few examples, or want minimal common core | **iDRegEx** or **kOREInference** | Probabilistic EM, finds only what's shared. |
|
| Few examples, or want minimal common core | **iDRegEx** | Probabilistic EM, finds only what's shared. |
|
||||||
| Don't know which is better | **Ensemble (default)** | Runs CRX + iDRegEx, picks best by MDL score. |
|
| Don't know which is better | **Ensemble (default)** | Runs CRX + iDRegEx, picks best by MDL score. |
|
||||||
| Want core pattern + outlier detection | **Ensemble + `min_coverage<1`** | Finds tight grammar for majority, flags outliers. |
|
| Want core pattern + outlier detection | **Ensemble + `min_coverage<1`** | Finds tight grammar for majority, flags outliers. |
|
||||||
| Data is clearly one type | `prefer='crx'` | Skips ensemble comparison, runs CRX alone. |
|
| Data is clearly one type | `prefer='crx'` | Skips ensemble comparison, runs CRX alone. |
|
||||||
|
|
||||||
|
> **kORE note:** kOREInference (Algorithm 4) is available via `--kore` flag but excluded from the default ensemble. On real-world data it returns ∅ in ~80% of cases because the k-OA is rarely SORE. CRX or iDRegEx handle these cases better.
|
||||||
|
|
||||||
## When each algorithm wins
|
## When each algorithm wins
|
||||||
|
|
||||||
| Data property | Winner | Why |
|
| Data property | Winner | Why |
|
||||||
|
|
@ -178,7 +188,7 @@ The sweet spot: **multiple implementations of the same abstract task** with a sh
|
||||||
| Diverse patterns, full vocabulary needed | CRX | Captures all symbols. iDRegEx returns ∅. |
|
| Diverse patterns, full vocabulary needed | CRX | Captures all symbols. iDRegEx returns ∅. |
|
||||||
| Clean sequences with clear core | iDRegEx | Extracts minimal common subsequence. CRX buries it in optional noise. |
|
| Clean sequences with clear core | iDRegEx | Extracts minimal common subsequence. CRX buries it in optional noise. |
|
||||||
| Interconnected (non-SORE) data | CRX | kOREInference (rwr₀) returns ∅ when k-OA is not SORE. CRX handles it. |
|
| Interconnected (non-SORE) data | CRX | kOREInference (rwr₀) returns ∅ when k-OA is not SORE. CRX handles it. |
|
||||||
| Single sequence | iDRegEx (+ RWR₀) | RWR₀ repair produces a grammatical regex from one example. |
|
| Single sequence | iDRegEx | iDRegEx handles noise better. |
|
||||||
| 2–3 sequences | iDRegEx | CRX overfits. iDRegEx handles noise better. |
|
| 2–3 sequences | iDRegEx | CRX overfits. iDRegEx handles noise better. |
|
||||||
| Many sequences, tight pattern | CRX | Learns precise concatenation with optional suffixes. |
|
| Many sequences, tight pattern | CRX | Learns precise concatenation with optional suffixes. |
|
||||||
| Want majority pattern + outlier list | CRX + `min_coverage` | Core analysis finds tight grammar for ~80%, flags the rest. |
|
| Want majority pattern + outlier list | CRX + `min_coverage` | Core analysis finds tight grammar for ~80%, flags the rest. |
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,7 @@ def infer_best_grammar(
|
||||||
than passing all examples. Pass the existing sequences, get back a
|
than passing all examples. Pass the existing sequences, get back a
|
||||||
pattern you can follow to generate new instances.
|
pattern you can follow to generate new instances.
|
||||||
|
|
||||||
Runs CRX + iDRegEx, picks best by scoring. kORE is excluded by
|
Runs CRX + iDRegEx, picks best by scoring.
|
||||||
default (slow, rarely wins on real data). Set prefer='koreinference'
|
|
||||||
to force it.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sequences: List of sequences, each a list of strings (symbols in
|
sequences: List of sequences, each a list of strings (symbols in
|
||||||
|
|
@ -106,8 +104,12 @@ def analyze_directory(
|
||||||
persist: bool = True,
|
persist: bool = True,
|
||||||
method: str = "langsize",
|
method: str = "langsize",
|
||||||
min_methods: int = 3,
|
min_methods: int = 3,
|
||||||
min_structure: float = 0.2,
|
min_structure: float = 0.5,
|
||||||
split_mixed: bool = True,
|
split_mixed: bool = True,
|
||||||
|
decompose: bool = False,
|
||||||
|
max_seq_length: int = 5,
|
||||||
|
cluster_method: str = "first-symbol",
|
||||||
|
crx_method: str = "standard",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Scan a source code directory and infer behavioral conventions
|
"""Scan a source code directory and infer behavioral conventions
|
||||||
(regular expression grammars) per package. Returns compact patterns
|
(regular expression grammars) per package. Returns compact patterns
|
||||||
|
|
@ -140,13 +142,25 @@ def analyze_directory(
|
||||||
method: Scoring method — 'langsize' (default) or 'mdl'.
|
method: Scoring method — 'langsize' (default) or 'mdl'.
|
||||||
min_methods: Minimum methods per group to attempt inference. Default 3.
|
min_methods: Minimum methods per group to attempt inference. Default 3.
|
||||||
min_structure: Minimum grammar structure score (0.0–1.0). Groups
|
min_structure: Minimum grammar structure score (0.0–1.0). Groups
|
||||||
below this produce flat bags. Default 0.2.
|
below this produce flat bags. Default 0.5 (only returns
|
||||||
|
high-structure grammars).
|
||||||
split_mixed: When True (default), recursively split groups with
|
split_mixed: When True (default), recursively split groups with
|
||||||
diverse first symbols into uniform sub-groups before inference.
|
diverse first symbols into uniform sub-groups before inference.
|
||||||
|
decompose: When True, decompose long sequences into shorter
|
||||||
|
fragments before inference. Helps on codebases with many
|
||||||
|
unique method patterns. Default False.
|
||||||
|
max_seq_length: Maximum fragment length when decompose=True. Default 5.
|
||||||
|
cluster_method: How to split mixed groups — 'first-symbol' (fast,
|
||||||
|
crude) or 'distributional' (context-similarity clustering).
|
||||||
|
Default 'first-symbol'.
|
||||||
|
crx_method: CRX variant — 'standard' (fast, Algorithm 7) or
|
||||||
|
'refined' (cluster-then-infer, tighter on flat bags).
|
||||||
|
Default 'standard'.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
YAML string with grammars grouped by top-level module, sorted
|
YAML string with grammars grouped by top-level module, sorted
|
||||||
by score (tightest/most useful first).
|
by score (tightest/most useful first). Only returns grammars
|
||||||
|
meeting the min_structure threshold.
|
||||||
"""
|
"""
|
||||||
results = _analyze_directory(
|
results = _analyze_directory(
|
||||||
directory,
|
directory,
|
||||||
|
|
@ -161,8 +175,12 @@ def analyze_directory(
|
||||||
min_methods=min_methods,
|
min_methods=min_methods,
|
||||||
split_mixed=split_mixed,
|
split_mixed=split_mixed,
|
||||||
min_structure=min_structure,
|
min_structure=min_structure,
|
||||||
|
decompose=decompose,
|
||||||
|
max_seq_length=max_seq_length,
|
||||||
|
cluster_method=cluster_method,
|
||||||
|
crx_method=crx_method,
|
||||||
)
|
)
|
||||||
yaml_content = _build_yaml_output(results, directory, max_mdl=max_mdl)
|
yaml_content = _build_yaml_output(results, directory, max_mdl=max_mdl, min_structure=min_structure)
|
||||||
if persist:
|
if persist:
|
||||||
_persist_grammars(yaml_content, directory)
|
_persist_grammars(yaml_content, directory)
|
||||||
return yaml_content
|
return yaml_content
|
||||||
|
|
|
||||||
|
|
@ -883,10 +883,11 @@ def _build_json_output(results):
|
||||||
return json.dumps(output, indent=2)
|
return json.dumps(output, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def _build_yaml_output(results, dir_path, max_mdl=200.0):
|
def _build_yaml_output(results, dir_path, max_mdl=200.0, min_structure=0.0):
|
||||||
"""Build YAML output grouped by top-level module, sorted by MDL.
|
"""Build YAML output grouped by top-level module, sorted by MDL.
|
||||||
|
|
||||||
Filters out (other), no-grammar groups, and groups above max_mdl.
|
Filters out (other), no-grammar groups, groups above max_mdl,
|
||||||
|
and groups below min_structure.
|
||||||
Returns YAML string.
|
Returns YAML string.
|
||||||
"""
|
"""
|
||||||
import yaml
|
import yaml
|
||||||
|
|
@ -908,6 +909,8 @@ def _build_yaml_output(results, dir_path, max_mdl=200.0):
|
||||||
best = result["best"]
|
best = result["best"]
|
||||||
if best["mdl_score"] > max_mdl:
|
if best["mdl_score"] > max_mdl:
|
||||||
continue
|
continue
|
||||||
|
if grammar_structure_score(best["grammar"]) < min_structure:
|
||||||
|
continue
|
||||||
|
|
||||||
# Extract top-level module from package path
|
# Extract top-level module from package path
|
||||||
parts = label.replace(os.sep, "/").split("/")
|
parts = label.replace(os.sep, "/").split("/")
|
||||||
|
|
@ -917,6 +920,8 @@ def _build_yaml_output(results, dir_path, max_mdl=200.0):
|
||||||
all_grammars = meta.get("all_grammars", [])
|
all_grammars = meta.get("all_grammars", [])
|
||||||
if all_grammars:
|
if all_grammars:
|
||||||
for leaf_label, leaf_grammar, leaf_score, leaf_count in all_grammars:
|
for leaf_label, leaf_grammar, leaf_score, leaf_count in all_grammars:
|
||||||
|
if grammar_structure_score(leaf_grammar) < min_structure:
|
||||||
|
continue
|
||||||
entry = {
|
entry = {
|
||||||
"package": leaf_label,
|
"package": leaf_label,
|
||||||
"methods": leaf_count,
|
"methods": leaf_count,
|
||||||
|
|
|
||||||
|
|
@ -770,3 +770,22 @@ The pipeline should: (1) run refined CRX, (2) if trivial, fall back to CRX.
|
||||||
|
|
||||||
**Recommendation:** Make refined CRX the default for `--split-mixed` mode.
|
**Recommendation:** Make refined CRX the default for `--split-mixed` mode.
|
||||||
Keep standard CRX as fallback. No need for iDRegEx or kORE in the pipeline.
|
Keep standard CRX as fallback. No need for iDRegEx or kORE in the pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Round 18: Decomposition Forest (Phase 2)
|
||||||
|
|
||||||
|
**Goal:** Break down long sequences into shorter fragments before inference (Crucio Phase 2).
|
||||||
|
|
||||||
|
**Method:** Implemented `bex/decompose.py` with prefix/suffix/window extraction.
|
||||||
|
|
||||||
|
**Results:**
|
||||||
|
- RAGSAK: 21 → 80 grammars (3.8× increase)
|
||||||
|
- FastAPI: 111 → 118 grammars (small increase)
|
||||||
|
|
||||||
|
**Key insight:** Decomposition creates diverse fragments, so skip diversity check when enabled.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `bex/decompose.py`: decompose_sequence(), decompose_all(), decompose_with_coverage()
|
||||||
|
- `bex/tag_preprocessor/analyze.py`: --decompose, --max-seq-length flags
|
||||||
|
- `tests/test_decompose.py`: 12 new tests
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue