feat(core): typo-tolerant suggestions for missing skills#1
Conversation
closestMatches previously used a bidirectional substring check, so any edit-distance-1 typo (e.g. "code-revewer") returned no suggestions even when an obvious candidate existed. Rank suggestions in two tiers: exact and substring matches first (preserving existing positional ordering), then Levenshtein-close candidates within a length-relative threshold. Threshold scales as ceil(len/3) with a floor of 1 so short queries don't pull in unrelated candidates while longer ones tolerate multiple edits.
fa6f2c8 to
712397f
Compare
|
Hey, nice change — substring-only was a sharp edge, the tiered approach reads well. A few thoughts:
Tests are great — the rune symmetry case is the kind of thing people skip. Once 1 and 2 are in I'm good to merge; 3/4/5 are at your discretion. |
- Use builtin min instead of a hand-rolled minInt3 helper. Go 1.21+ has variadic min/max for ordered types and the module already targets a newer toolchain. - Preallocate the ranked slice to len(candidates) so the append loop doesn't repeatedly grow the backing array. - Skip empty candidate strings. The bidirectional substring check (Contains(query, c)) would otherwise treat every empty candidate as a match against every query. - Document the (len+2)/3 typo threshold as a staircase heuristic so the next reader doesn't have to derive the reasoning.
|
Thanks for the careful review. Pushed 9363403 addressing 1–4:
CI should be green; let me know if you want anything else tweaked. |
|
Thanks, all looks good. Merging once CI's green — will go out in the next release. |
Summary
closestMatchespowers the "did you mean: …" hint shown after/skillswhen a requested name isn't found. The previous implementation used a bidirectional substring check, so an edit-distance-1 typo returned no suggestion at all — e.g.code-revewer(missing one letter) produced an empty list, defeating the purpose of the prompt.This PR replaces the matcher with a tiered ranking:
ceil(len/3)(floor 1). Ranked by ascending edit distance.The threshold scales with query length so short queries don't pull in wildly unrelated candidates (
zzzagainstalpha/betastill returns nothing), while longer queries can absorb a couple of edits.Why a length-relative threshold
len ≤ 3→ threshold 1 (catches single edits, rejects unrelated short strings)len = 4–6→ threshold 2len = 12→ threshold 4This matches typical autocorrect heuristics — short tokens demand high precision, longer tokens have more slack.
Implementation notes
cafévscafe→ distance 1).sort.SliceStablewith explicit tie-breakers, so output is deterministic.Tests
Existing
TestClosestMatchestable preserved unchanged; new cases added for:code-revewer→code-reviewer)abagainstxy/cdreturns nil)nguardsAdded
TestLevenshteintable covering identity, insertion, deletion, substitution, the canonicalkitten/sittingcase, empty inputs, and a rune-level (café/cafe) check with symmetry assertions.Test plan
make test— all packages greengo vet ./...— cleanmake build— clean