feat(foundation): add fuzzymatch for did-you-mean connector suggestions - #2769
Merged
Conversation
devarismeroxa
force-pushed
the
feat/ws1-fuzzymatch
branch
from
August 5, 2026 17:38
3cd4ed7 to
999738e
Compare
devarismeroxa
force-pushed
the
feat/ws1-fuzzymatch
branch
2 times, most recently
from
August 5, 2026 21:50
4361ad9 to
5b62014
Compare
Two commands have been blocked on this utility not existing. The `repair` design doc (20260712-repair-command.md, §6) left "connector plugin not found" unrepairable: "the correct plugin name is not mechanically knowable... Deferred until a did-you-mean index exists". `conduit generate` (20260722-conduit-generate.md, §7) then made it a hard prerequisite rather than polish — its acceptance bar requires an unknown connector to produce a closest match and an install suggestion, never a fabricated plugin name. The generate use is the demanding one. When a model invents a connector, feeding "`postgre` does not exist; did you mean `postgres`?" back into the retry prompt converts a hallucination into a self-correction inside the retry budget instead of a terminal failure. Two named consumers, so this is shared infrastructure, not speculative generality. Two invariants, both enforced by test and by the fuzzer: 1. Output is deterministic — ordered by edit distance, then lexicographically. An error message whose wording depends on map iteration order cannot be asserted on, alerted on, or put in a golden file. 2. A suggestion is never fabricated. Every returned string is an element of candidates, and nothing is returned unless it clears the similarity floor. A confident wrong name is worse than silence when a model will act on it. Per the design doc: plain Levenshtein (real connector typos are substitutions, omissions, and insertions, not adjacent transpositions), case-insensitive (names arrive from natural-language prompts), and a similarity floor that is the looser of an absolute 2-edit bound and a relative 30%-of-length bound. Neither bound alone works: a flat 2 edits on a 26-character name is stricter than anyone typing by hand manages, and 30% of a 3-character name is zero. Levenshtein keeps two rows rather than the full matrix — it runs once per candidate over the whole catalog inside a retry loop, and the discarded rows can no longer be read. Tests, each mutation-verified: - absolute bound only -> FloorIsLooserOfTwoBounds fails - relative bound only -> 5 tests fail - lexicographic tie-break dropped -> DeterministicAcrossCandidateOrder fails - duplicate-candidate skip dropped -> DuplicatesDoNotConsumeSlots fails - floor widened 5x -> NeverFabricates fails (suggests `kafka` for `mysql`) - case folding dropped -> RealTypos fails The first pass of the long-name case did NOT kill the absolute-only mutation: the candidate I picked sat exactly at distance 2, the absolute bound, so it proved nothing. Replaced with a distance-4 case and the mutation now kills. The test asserts the distance explicitly so the next edit can't silently reintroduce a non-discriminating case. FuzzSuggest asserts both invariants plus the cap on arbitrary input: 9.7M executions, no failures. Risk tier: 3. New leaf package, no existing call sites, no data path. Roadmap: v0.20 WS1 (`conduit generate`), prerequisite per design doc §7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
devarismeroxa
force-pushed
the
feat/ws1-fuzzymatch
branch
from
August 5, 2026 22:55
5b62014 to
c033e38
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Two commands have been blocked on this utility not existing.
repairleft the "connector plugin not found" class unrepairable — "the correct plugin name is not mechanically knowable... Deferred until a did-you-mean index exists" (20260712-repair-command.md, §6).conduit generatethen made it a hard prerequisite rather than polish: its acceptance bar requires an unknown connector to produce a closest match and an install suggestion, never a fabricated plugin name (20260722-conduit-generate.md, §7).The
generateuse is the demanding one. When a model invents a connector, feeding`postgre` does not exist; did you mean `postgres`?back into the retry prompt converts a hallucination into a self-correction inside the retry budget, instead of a terminal failure.Two named consumers — shared infrastructure, not speculative generality.
Invariants
Both enforced by test and by the fuzzer:
candidates, and nothing is returned unless it clears the similarity floor. A confident wrong name is worse than silence when a model is going to act on it.Design choices (all from §7, not invented here)
postgre,kafak), not adjacent transpositions. Simpler to audit, no coverage lost —TestSuggest_TranspositionIsCoveredpins that a transposition still lands inside the floor at cost 2max(2 edits, 30% of length)Adversarial self-review
nilvs empty slice. The doc comment promisednilon no match; the code returned[]string{}viamake([]string, 0). Caught by the first test run. Callers branch on== nilandlen() == 0interchangeably only if the two never diverge.postgresconsume all three suggestion slots and crowd out the real alternative.TestLevenshtein_MultiByteRunespins that distance counts characters, not bytes.Tests — every mutation verified
FloorIsLooserOfTwoBoundsRealTypos,TranspositionIsCovered,FloorIsLooserOfTwoBounds,DeterministicAcrossCandidateOrder,RespectsMaxSuggestionsDeterministicAcrossCandidateOrderDuplicatesDoNotConsumeSlotsRealTypos,NeverFabricates,FloorIsLooserOfTwoBounds— suggestskafkaformysqlRealTyposFuzzSuggestasserts both invariants plus the cap on arbitrary input: 9.7M executions, no failures.Tests use the real built-in connector list (
file,generator,kafka,log,postgres,s3) rather than invented fixtures, so the floor is tuned against distances that occur in practice.NeverFabricateschecks the connectors people actually ask for and don't have —mysql,mongo,snowflake,redis,bigquery— and requires zero suggestions for each.What was run
go build ./...,go vet,golangci-lint run(0 issues),go test -race, and the 45s fuzz run above.Risk tier
3. New leaf package, no existing call sites, no data path, no serialized format. No new dependencies.
Roadmap
v0.20 WS1 (
conduit generate) — prerequisite per design doc §7. Once merged,repair's v2 scope should revisit the connector-not-found row it deferred for exactly this reason.🤖 Generated with Claude Code
https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD