Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Multi-assignment clustering (no greedy 'used' set) - Adaptive ngram fallback (shrink when (other) > 60%) - Add docs/adr/ with 10 architecture decision records - Fix ADR 1 (query modification description) - Fix ADR 3 (multi-assignment + adaptive shrink) - Fix ADR 5 (import sort order clarification) - Fix ADR 6 (remove Kotlin call_suffix references) - New ADR 9 (adaptive clustering rationale) - New ADR 10 (universal package mapping via relpath)
49 lines
2.8 KiB
Markdown
49 lines
2.8 KiB
Markdown
# 9. Adaptive clustering with multi-assignment
|
|
|
|
**Date:** 2026-07-03
|
|
|
|
**Status:** Accepted
|
|
|
|
## Context
|
|
|
|
The original clustering (ADR 3) assigned each method to exactly one cluster — the first matching n-gram sorted by frequency. This caused a "winner-takes-all" problem: the most common token (e.g., `assertEquals`) claimed 327 methods, leaving 1,254 methods in `(other)` even when they shared other patterns like `mockk` or `every`.
|
|
|
|
Additionally, the optimal ngram_size varies per codebase. A small JS test suite benefits from 3-grams (catches multi-step Playwright patterns), while a large Kotlin monorepo needs 1-grams (or even overlapping patterns) to escape the `(other)` blob.
|
|
|
|
## Decision
|
|
|
|
Two changes to `cluster_methods`:
|
|
|
|
### Multi-assignment
|
|
|
|
Remove the `used` set. A method belongs to every cluster whose n-gram appears in its call sequence. This reveals overlapping patterns — e.g., a method containing both `assertEquals` and `mockk` appears in both clusters, telling the LLM "this method is both assertion-heavy AND mock-heavy."
|
|
|
|
Add `max_clusters=20` to prevent output bloat from many single-token n-grams. The 20 most frequent n-grams form clusters; the rest go to `(other)`.
|
|
|
|
### Adaptive ngram fallback
|
|
|
|
New `cluster_methods_adaptive()` wrapper:
|
|
|
|
1. Run `cluster_methods` with `ngram_size=N`.
|
|
2. If `(other)` exceeds 60% of total methods, retry with `ngram_size=N-1`.
|
|
3. Repeat down to `ngram_size=1`.
|
|
|
|
This ensures the clustering adapts to codebase diversity without manual tuning. A diverse monorepo with 1,500+ methods that share few 3-grams automatically falls back to 2-gram or 1-gram clustering.
|
|
|
|
## Consequences
|
|
|
|
**Positive:**
|
|
- Overlapping patterns surface richer signals: "327 methods call `assertEquals`, 200 methods call `mockk` (some are both)."
|
|
- Adaptive fallback eliminates manual tuning for diverse codebases.
|
|
- `max_clusters=20` keeps output concise for LLM context windows.
|
|
|
|
**Negative:**
|
|
- `method_count` sums to more than total methods (one method counted in N clusters). Users must interpret counts as "methods matching this pattern," not "methods exclusive to this cluster."
|
|
- `(other)` may still be large at ngram=1 if most methods share no single call token with ≥3 peers (rare but possible).
|
|
- Adaptive fallback adds a re-clustering pass (negligible cost — clustering is cheap vs inference).
|
|
|
|
## Alternatives Considered
|
|
|
|
- **Greedy assignment (ADR 3 original)**: Creates clean mutually exclusive clusters, but loses signal from overlapping patterns. The `(other)` blob grows uncontrollably.
|
|
- **Hierarchical clustering**: More sophisticated grouping but adds complexity — no clear benefit for our use case (clusters are consumed by an LLM, not analyzed by a human).
|
|
- **Fixed ngram_size with manual flag**: Passes the tuning burden to the user. Adaptive removes friction.
|