grammar-inference-engine/docs/adr/0003-method-level-n-gram-clustering.md
tobjend ca7ccb36ff
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
docs: add 8 architecture decision records
ADR 0001: nvim-treesitter highlights.scm as capture source
ADR 0002: language-agnostic method extraction via child_by_field_name
ADR 0003: method-level n-gram clustering before inference
ADR 0004: frequency filter with min_coverage threshold
ADR 0005: import extraction per cluster
ADR 0006: argument pattern extraction via AST node classification
ADR 0007: JSON output for LLM prompt injection
ADR 0008: BEX ensemble for grammar inference
2026-07-03 22:01:35 +02:00

2.3 KiB

3. Method-level n-gram clustering before inference

Date: 2026-07-03

Status: Accepted

Context

The BEX ensemble (CRX, iDRegEx, kORE) infers grammars from sets of symbol sequences. When we run inference on all methods in a codebase, the sequences are too diverse — each file has different conventions, and the ensemble produces only a flat vocabulary bag like (any+assertEquals+assertTrue+every+listOf+verify)+.

This doesn't capture the ordering of calls or the distinct methodological styles present in the codebase.

Decision

Group methods by shared n-gram (default: 3-gram) call patterns before running inference.

Pipeline: preprocess_by_methodfrequency_filtercluster_methods → per-cluster infer_ensemble

The clustering algorithm:

  1. Extract call tokens from each method sequence (filter to function, reference.call, reference.class captures).
  2. Build an n-gram index: for each method, for each sliding window of size N, record the n-gram.
  3. Sort n-grams by frequency (most shared first).
  4. Assign each method to the largest matching cluster, then remove assigned methods.
  5. Remaining unclustered methods go to (other).

This produces 10-30 clusters for a typical test suite, each with 3-100+ methods sharing a call-order pattern.

Consequences

Positive:

  • iDRegEx and kOREInference now produce ordered grammars (e.g. every+.assertEquals.verify+.any?) because small, focused clusters have enough signal.
  • Each cluster reveals a distinct methodological style in the codebase (mockist TDD vs data-driven testing vs pure assertion).
  • The (other) cluster still captures the full vocabulary bag for diverse methods.

Negative:

  • Clustering adds a hyperparameter (ngram_size, default 3). Wrong value can produce too many tiny clusters or one giant cluster.
  • min_cluster_size (default 3) filters out tiny but potentially interesting patterns.
  • Methods in (other) never get ordered grammar inference — just vocabulary.

Alternatives Considered

  • Infer on all methods (no clustering): Produces flat vocabulary only. CRX works at 100% coverage, but iDRegEx and kORE fail on diverse inputs.
  • Infer per file: Too fine-grained — most files have 1-5 methods, not enough for inference.
  • Infer per directory: Better, but directories mix unrelated conventions (setup/teardown vs actual test logic).