# 12. Remove n-gram clustering from pipeline

**Date:** 2026-07-04

**Status:** Accepted

## Context

The pipeline grouped method sequences by shared n-gram call patterns before
inference. The idea: methods in the same category (test, config, helper) would
cluster together, and each cluster would get a cleaner, more specific grammar.

## What Was Removed

- `cluster_methods()` — built n-gram→method index from call tokens, assigned
  methods to shared-pattern clusters, dumped remainder to `(other)`.
- `cluster_methods_adaptive()` — retried clustering at ngram=2 then ngram=1
  when `(other)` exceeded 60% of methods.
- `_extract_call_tokens()` import from `code.py` (still in `code.py` but no
  longer called by the pipeline).
- `--min-cluster-size` and `--ngram-size` CLI flags.
- `cluster` parameter in `analyze_directory()`.
- Per-cluster loop in `analyze_clusters()` — metadata extraction (files,
  imports, arg patterns) now runs once across all methods.

## Why Removed

1. **No benefit for diverse codebases.** On Kotlin SpringRAG (1581 methods),
   21 named clusters formed (3-13 methods each) but 1254 landed in `(other)`.
   The named clusters were too small to produce better grammars than running
   inference once on the filtered set.

2. **Added complexity for zero signal gain.** The n-gram labels (e.g.
   `locator → click → waitForTimeout`) restated what CRX already outputs as
   `(locator+click+waitForTimeout)+`.per-cluster infer_ensemble call was
   redundant with the single-pass result.

3. **Slower.** 22 extra BEX calls (one per named cluster) for grammars
   that would appear in the single-pass result anyway.

## Pipeline After Removal

```
preprocess_by_method → frequency_filter(0.2) → infer_ensemble(0.8)
```

Single pass. Metadata extracted once.

## How to Reintroduce

The removed code is preserved in the archive branch:

```
git archive/unreverted-25898c2
```

Files:
- `bex/tag_preprocessor/analyze.py` contains `cluster_methods()`,
  `cluster_methods_adaptive()`, and the per-cluster loop body.

To restore:
1. Cherry-pick or copy the two function definitions.
2. Re-add `_extract_call_tokens` to the import from `code.py`.
3. Re-add the `--min-cluster-size` and `--ngram-size` CLI flags.
4. Change `analyze_clusters()` back to: cluster → per-cluster filter → per-cluster infer.
5. Restore the cluster parameter in `analyze_directory()`.

## Performance Impact

| Metric | Before (with clustering) | After (single pass) |
|--------|--------------------------|---------------------|
| Pipeline time (460 .kt files) | ~42s | ~3s |
| Inference time per cluster | ~22s on (other) | ~0.1s total |
| Named clusters | 21 tiny + (other) | 1 group |
| Grammar quality | same `assertEquals+` | same `assertEquals+` |
