grammar-inference-engine/docs/adr/0009-adaptive-clustering-with-multi-assignment.md
tobjend e23922a1b7
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: adaptive multi-assignment clustering; add ADRs 1-10
- 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)
2026-07-03 22:58:09 +02:00

2.8 KiB

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.