Lesson 22: Similar chords
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Lesson 21 measured the embedding search. For chords that share a shape in any key, GA points elsewhere: “Transposition-agnostic “same-shape” similarity uses the ICV path (IcvNeighborsSkill / Grothendieck), not the embedding” (CLAUDE.md line 43). IcvNeighborsSkill is that path in the chatbot (IcvNeighborsSkill.cs lines 9-49), registered with the other skills (GaPlugin.cs line 101). It reads one chord, builds its pitch-class set, and lists the sets whose interval-class vectors are within an L1 distance of 2 of the chord’s, from GrothendieckService.FindNearby. Lesson 14 met FindNearby through the substitution skill, and the music theory course through the MCP tool ga_icv_neighbors; this lesson asks the chatbot’s skill.
All GA links point to commit a826864, the course’s pin, unless they name another. On GA’s main at f4f5b4a (2026-10-01), IcvNeighborsSkill and IntervalClassVectorSkill only mark their refusal Declined, DefaultRoutingHintProvider and GrothendieckDelta are unchanged, and FindNearby compares the source with each set by value; the pitch-class sets themselves changed, so GaMain asks again. Its output prints the same tables as the pin’s, under titles that say “on main”. The output comes from:
dotnet run --project code/ga-ai/GaAi -c Release -- l22dotnet run --project code/ga-ai/GaMain -c Release -- l22How the skill reads a question
Section titled “How the skill reads a question”The skill has no keyword test: its CanHandle always says no, and the router compares a question with each skill’s description and example prompts, adds the hint boosts, and sends it to the best score if that score reaches a minimum confidence (SemanticIntentRouter.cs lines 129-257). Those prompts were reworded on 2026-06-16, to keep them away from the neighboring skills’, and the comment above them says which words went. The patterns that read the question still need them:
// Routing anchors emphasise the user GOAL — "find OTHER chords SIMILAR to // one chord" — using resemble/similar/most-like/interval-profile. Two // curation passes (routing-ambiguity diagnostic, 2026-06-16): // 1. dropped the bare "ICV" framing (owned by IntervalClassVectorSkill, // the single-chord ICV intent): -0.05 -> +0.036 silhouette. // 2. dropped "close/nearby/adjacent" (collided with GrothendieckDeltaSkill's // "how close are X and Y") and "chords to C major" (collided with // ChordInfoSkill's "what is a C major chord"). "other … resemble/similar" // keeps the find-similar goal while shedding both neighbours' vocabulary. public IReadOnlyList<string> ExamplePrompts => [ "which chords are most similar to Dm7", "what other chords resemble Cmaj7", "find chords with a similar sound to G7", "chords related to F major by interval content", "what chords share Gmaj7's interval profile", "list chords most like E minor", "chords with similar interval content to Bm7b5", "what voicings are most similar to Am", "which chords are closest in interval content to Fmaj7", "chords that resemble Cmaj7 harmonically", ];
public bool CanHandle(string message) => false; // semantic-routing only
private const int DefaultMaxDistance = 2; private const int MaxNeighborsToShow = 8;
// Single-chord pattern — anchored on "neighbors/near/close/adjacent" + a // chord token. Word boundary protects against routing on prose that // happens to mention a single chord-letter. private static readonly Regex NeighborsPattern = new(@"\b(?:icv\s+neighbors?|neighbors?|nearby|close\s+to|near|adjacent|harmonic(?:ally)?\s+(?:close|near|adjacent))\s+(?:to\s+|of\s+)?(?<chord>[A-Ga-g][b#♭♯]?(?:maj|min|m|dim|aug|sus|add|dom)?\d*(?:[b#♭♯]\d+)*°?)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Reverse anchor: "<chord> ... (icv-)neighbors" / "<chord> ... close" private static readonly Regex NeighborsPatternReverse = new(@"\b(?<chord>[A-Ga-g][b#♭♯]?(?:maj|min|m|dim|aug|sus|add|dom)?\d*(?:[b#♭♯]\d+)*°?)\b[^.?!]*?\b(?:icv\s+neighbors?|neighbors?|harmonic(?:ally)?\s+(?:close|near|adjacent))\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);ExecuteAsync tries the first pattern, then the second, takes the chord they capture, and builds its set (lines 102-118). When neither pattern matches, it answers “Ask about ICV-neighbor pitch-class sets near a chord”, with a confidence of 0.1 (lines 254-260). On the embedding path, DefaultRoutingHintProvider also adds 0.06 to an intent whose rule matches the question; the rule for this skill wants “icv neighbors”, “harmonically close” or a few variants (DefaultRoutingHintProvider.cs lines 136-139).
GA’s phrasings
Section titled “GA’s phrasings”The program asks the skill its ten example prompts, the four phrasings of its doc comment (lines 17-23), the two its refusal suggests, and the parked draft’s three, and asks DefaultRoutingHintProvider which intents each one would boost:
== IcvNeighborsSkill's example prompts and GA's other phrasings for it, at the pinprompt from the skill's answer routing hints, +0.06 eachwhich chords are most similar to Dm7 anchor declined nonewhat other chords resemble Cmaj7 anchor declined nonefind chords with a similar sound to G7 anchor declined nonechords related to F major by interval content anchor declined nonewhat chords share Gmaj7's interval profile anchor declined nonelist chords most like E minor anchor declined nonechords with similar interval content to Bm7b5 anchor declined nonewhat voicings are most similar to Am anchor declined nonewhich chords are closest in interval content to Fmaj7 anchor declined nonechords that resemble Cmaj7 harmonically anchor declined noneWhat chords are harmonically close to Cmaj7 doc comment neighbors of Cmaj7 skill.icvneighborsNearby pitch-class sets to C major doc comment declined noneFind ICV neighbors of Dm7 doc comment neighbors of Dm7 skill.icvneighbors, skill.intervalclassvectorClosest chord to G7 in ICV space doc comment declined skill.intervalclassvectorICV neighbors of Cmaj7 refusal neighbors of Cmaj7 skill.icvneighbors, skill.intervalclassvectorwhat chords are harmonically close to Dm7 refusal neighbors of Dm7 skill.icvneighborsHarmonically similar chords to Cmaj7 parked draft declined noneICV neighbours of [0,3,6,9] parked draft declined skill.intervalclassvectorWhat's close to a half-diminished chord? parked draft neighbors of a skill.intervalanchors 10: answered 0, declined 10, hinted toward skill.icvneighbors 0CanHandle accepts 0 of them- The skill declines all ten of its example prompts. “most similar”, “resemble”, “closest in interval content”: none is a word the patterns read. A question worded like these is the likeliest to reach the skill, since the router compares questions with them, and the skill gives it its refusal. On
main, the refusal is markedDeclined, “so a caller may route it to another handler” (GuitarAlchemistAgentBase.cslines 347-352 onmain). - No hint rule boosts the skill for them, and two of the doc comment’s phrasings, “Nearby pitch-class sets to C major” and “Closest chord to G7 in ICV space”, are declined too.
- The refusal’s own suggestion, “ICV neighbors of Cmaj7”, is boosted toward two skills, this one and
skill.intervalclassvector, whose rule matches “icv” anywhere (lines 124-126).
The sister skill
Section titled “The sister skill”IntervalClassVectorSkill computes the vector of one chord, and its example prompts were curated in the same pass: it kept the ICV vocabulary as “the discriminator vs the neighbors / delta / path skills, which were de-ICV’d” (IntervalClassVectorSkill.cs lines 43-60). The program asks it its own:
== IntervalClassVectorSkill's example prompts, at the pinprompt the skill's answerwhat is the interval-class vector of Cmaj7 ICV of Cmaj7interval class vector of Dm7 ICV of Dm7compute the ICV of the major scale ICV of the major scaleinterval-class vector of {0,2,4,5,7,9,11} ICV of {0,2,4,5,7,9,11}what's the interval vector for G7 ICV of G7how many tritones does Cmaj7 contain declinedcompute the interval-class vector of Fmaj7 ICV of Fmaj7ICV of the dorian mode ICV of the dorianinterval class vector for {0,1,4,8} ICV of {0,1,4,8}what's the interval content of Am declinedanchors 10: answered 8It answers eight. Its two refusals, “how many tritones does Cmaj7 contain” and “what’s the interval content of Am”, are the two prompts with neither a set of numbers nor “interval vector”, “interval-class vector” or “ICV”, which its chord and scale patterns need (lines 64-77).
The chords it reads
Section titled “The chords it reads”The chord is a letter, an accidental, one of eight words, digits, alterations and an optional °, then a word boundary. TryBuildPcSet builds its set from a table of 16 qualities (lines 196-232):
private static int[] QualityIntervals(string quality) { var q = quality.ToLowerInvariant().Trim(); if (q == string.Empty || q == "maj" || q == "major") return [0, 4, 7]; if (q is "m" or "min" or "minor" or "-") return [0, 3, 7]; if (q is "dim" or "°" or "o") return [0, 3, 6]; if (q is "aug" or "+") return [0, 4, 8]; if (q is "7") return [0, 4, 7, 10]; if (q is "m7" or "min7" or "-7") return [0, 3, 7, 10]; if (q is "maj7" or "major7" or "M7") return [0, 4, 7, 11]; if (q is "m7b5" or "min7b5" or "ø" or "ø7") return [0, 3, 6, 10]; if (q is "dim7" or "°7" or "o7") return [0, 3, 6, 9]; if (q is "sus2") return [0, 2, 7]; if (q is "sus4" or "sus") return [0, 5, 7]; if (q is "6") return [0, 4, 7, 9]; if (q is "m6") return [0, 3, 7, 9]; if (q is "9") return [0, 4, 7, 10, 2]; if (q is "maj9") return [0, 4, 7, 11, 2]; if (q is "m9") return [0, 3, 7, 10, 2]; return [0, 4, 7]; }The program asks “ICV neighbors of” 27 chords, and compares the set the skill builds with the chord’s notes:
== The chords the skill reads, after "ICV neighbors of", at the pinchord its notes read as notes built rightC C E G C C E G yesCm C D# G Cm C D# G yesCmaj7 C E G B Cmaj7 C E G B yesCM7 C E G B CM7 C D# G A# noCmin7 C D# G A# Cmin7 C D# G A# yesCdom7 C E G A# Cdom7 C E G noCm7b5 C D# F# A# Cm7b5 C D# F# A# yesCø7 C D# F# A# declined - noCdim C D# F# Cdim C D# F# yesC° C D# F# C C E G noCdim7 C D# F# A Cdim7 C D# F# A yesC°7 C D# F# A C° C D# F# noCaug C E G# Caug C E G# yesC+ C E G# C C E G noCadd9 C D E G Cadd9 C E G noC7sus4 C F G A# declined - noC7b9 C C# E G A# C7b9 C E G noC7#9 C D# E G A# C7#9 C E G noCm11 C D D# F G A# Cm11 C E G noC13 C D E G A A# C13 C E G noCmMaj7 C D# G B declined - noC5 C G C5 C E G noF#m7b5 C E F# A F#m7b5 C E F# A yesB♭7 D F G# A# B♭7 D F G# A# yesC minor C D# G C C E G noA minor C E A A C# E A noa Cmaj7 C E G B a C# E A nochords 27: built right 10- “CM7”, a major seventh, is built as C minor seventh. The suffix is lowercased before the table is read, so the
"M7"of line 221 can never match, and “m7” does. - An unknown suffix gives a major triad, without a warning. Cdom7, Cadd9, C7b9, C7#9, Cm11, C13 and C5 are all answered as C E G; “C minor” and “A minor” are read as C and A, since a space ends the chord.
- ° and + end the chord. They aren’t letters, so the word boundary falls before them: C° and C+ become C, and C°7 the triad C°, since a boundary also falls between ° and 7. ø is a letter, so Cø7 has no boundary after its C and is declined, like C7sus4 and CmMaj7, whose suffixes the pattern doesn’t have.
- “a” is a chord. The pattern ignores case, so the article in “close to a half-diminished chord”, the parked draft’s phrasing, is read as A major, and so is the one in “ICV neighbors of a Cmaj7”.
The neighbors
Section titled “The neighbors”The skill asks FindNearby for every set within 3, recomputes each distance itself, and keeps the first eight at distance 1 or 2 (lines 120-140):
var sourceIcv = source.IntervalClassVector; var rawNeighbors = grothendieck.FindNearby(source, DefaultMaxDistance + 1).ToList();
var neighbors = rawNeighbors .Select(n => (n.Set, Delta: ComputeTrueDelta(sourceIcv, n.Set.IntervalClassVector))) .Where(t => !ReferenceEquals(t.Set, source)) // skip the source itself .Where(t => t.Delta.l1 > 0) // skip exact ICV-identical (same set class) .Where(t => t.Delta.l1 <= DefaultMaxDistance) .OrderBy(t => t.Delta.l1) .Take(MaxNeighborsToShow) .ToList();FindNearby scans PitchClassSet.Items in the order of their ids, which are their bitmasks, and sorts what it keeps by a cost, the distance times 0.6, keeping that order between equal costs (GrothendieckService.cs lines 38-90). The program counts, for each of the skill’s 16 qualities on C, the sets at distance 1 or 2, and compares the eight rows with the first eight of them by id:
== The neighbors the skill lists for a chord, at the pinchord its vector sets 1-2 off set classes of notes rows of notes L1, cost set classes with a C the first by idC <0 0 1 1 1 0> 108 6 2, 3 8 2, 3 2, 1.20 5 6 yesCm <0 0 1 1 1 0> 108 6 2, 3 8 2, 3 2, 1.20 5 6 yesCdim <0 0 2 0 0 1> 18 2 2 8 2 2, 1.20 2 2 yesCaug <0 0 0 3 0 0> 12 1 2 8 2 2, 1.20 1 2 yesC7 <0 1 2 1 1 1> 132 6 4 8 4 2, 1.20 5 8 yesCm7 <0 1 2 1 2 0> 72 3 4 8 4 2, 1.20 3 6 yesCmaj7 <1 0 1 2 2 0> 72 4 4 8 4 2, 1.20 4 6 yesCm7b5 <0 1 2 1 1 1> 132 6 4 8 4 2, 1.20 5 8 yesCdim7 <0 0 4 0 0 2> 0 0 0 - - 0 0 -Csus2 <0 1 0 0 2 0> 48 3 2, 3 8 2, 3 2, 1.20 3 4 yesCsus4 <0 1 0 0 2 0> 48 3 2, 3 8 2, 3 2, 1.20 3 4 yesC6 <0 1 2 1 2 0> 72 3 4 8 4 2, 1.20 3 6 yesCm6 <0 1 2 1 1 1> 132 6 4 8 4 2, 1.20 5 8 yesC9 <0 3 2 2 2 1> 24 1 5 8 5 2, 1.20 1 4 yesCmaj9 <1 2 2 2 3 0> 72 3 5 8 5 2, 1.20 3 5 yesCm9 <1 2 2 2 3 0> 72 3 5 8 5 2, 1.20 3 5 yesthe rows for C: {0,3} C D# 2-3 <0 0 1 0 0 0> {0,4} C E 2-4 <0 0 0 1 0 0> {1,4} C# E 2-3 <0 0 1 0 0 0> {0,1,4} C C# E 3-3 <1 0 1 1 0 0> {0,3,4} C D# E 3-3 <1 0 1 1 0 0> {0,5} C F 2-5 <0 0 0 0 1 0> {1,5} C# F 2-4 <0 0 0 1 0 0> {0,1,5} C C# F 3-4 <1 0 0 1 1 0>Cdim7: No pitch-class sets within L1 = 2. Try a wider radius.chords 192: qualities whose 12 roots get the same rows 16 of 16; different answers 10, different vectors 10- Every row is at distance 2, with a cost of 1.20. Between two sets of the same size the distance is even, and a set of another size is at least 3 away from a chord of three notes or more, except a two-note set from a triad. “Sorted by harmonic cost” sorts nothing, and the eight rows are the first eight by bitmask, which favors the lowest pitch classes: six of C’s eight rows hold a C, and none goes above F.
- The rows depend on the chord’s vector only. The 192 chords of 12 roots and 16 qualities get 10 different answers, one per vector: C and F#m get the same eight rows.
- A triad’s neighbors are mostly intervals. Five of C’s eight rows are two-note sets, and the three others each hold a semitone, which C major doesn’t. Cdim’s and Caug’s rows are two-note sets only.
- Cdim7 gets no neighbor, and the advice to “Try a wider radius”, though the radius is a constant,
DefaultMaxDistance(line 76), that the user can’t change.
The music theory course found that ga_icv_neighbors, the MCP tool on the same service, printed twelve lines of C’s own set class for C at the pin; on main it lists each set class once, at its real distance (ChordAtonalTool.cs lines 337-348 on main). The skill computed real distances from the start (lines 122-129), and still lists several transpositions of one set class: C’s eight rows hold five set classes.
The parked skill draft
Section titled “The parked skill draft”skills-dev/_pending-tools/icv-neighbors/DRAFT.md is a chatbot skill written to call ga_icv_neighbors, parked like those of lessons 19 to 21 (skills-dev/_pending-tools/README.md line 52):
- it says the tool is “not yet implemented in Common/GA.Business.ML/Agents/Mcp/” and points to
Common/GA.Business.ML/Agents/Mcp/AtonalMcpTools.cs(lines 18 and 72), a file GA doesn’t have; the tool is inGaMcpServer/Tools/ChordAtonalTool.cs(lines 220-254); - it expects
chord,topKandmetric, with Euclidean, Manhattan or cosine distances, and an answer ofNeighbours(lines 31-41); the tool takessymbolandmaxDistanceand returns lines of text; - its example gives Cmaj7’s vector and three neighbors with a distance each (lines 53-58):
== The parked draft's example for Cmaj7, at the pinchord notes vector Forte L1 the draftCmaj7 C E G B <1 0 1 2 2 0> 4-20 0 [1, 0, 1, 2, 2, 0], 4-20Am7 A C E G <0 1 2 1 2 0> 4-26 4 [1,0,1,2,2,0], distance 0.0, same set classCm9 without its root D# G A# D <1 0 1 2 2 0> 4-20 0 distance 0.6Fmaj7 F A C E <1 0 1 2 2 0> 4-20 0 distance 0.0, same set classAm7 isn’t of Cmaj7’s set class: it is A C E G, C6’s notes, 4-26, at distance 4. Cm9 without its root is E♭ major seventh, of Cmaj7’s class, at distance 0, not 0.6. Of the draft’s three phrasings, the skill declines two and reads A major in the third.
Where the course stops
Section titled “Where the course stops”- The router isn’t run: it needs the embeddings. The lesson asks the skill the questions the router is likeliest to send it, its own example prompts.
- The chords’ notes are the course’s, a textbook’s for each symbol.
ga_icv_neighborsisn’t run here; the music theory course runs it.- The program calls the skills’ methods directly, not through the chatbot.
Reported upstream
Section titled “Reported upstream”- Reported after this lesson was written, in GA issue #798: the example prompts the patterns don’t read, the chords the table builds wrong, the neighbors listed by bitmask, and the parked skill draft.
Exercises
Section titled “Exercises”- “which chords are most similar to Dm7” is one of the skill’s example prompts. Why does the skill decline it?
- C’s rows include
{0,4}, C and E. What is the distance between C’s vector,<0 0 1 1 1 0>, and this set’s,<0 0 0 1 0 0>? Why can’t a neighbor of Cmaj7 have three notes? - Why do C and F#m get the same eight rows?
- “ICV neighbors of CM7” answers for C minor seventh. Which line turns M7 into m7?
Solutions
- Its patterns need “neighbors”, “nearby”, “close to”, “near” or “adjacent” right before the chord, “neighbors” after it, or “harmonically close”, “near” or “adjacent” on either side (lines 82-89); the prompt has none of them, so
ExecuteAsyncgives the refusal. - 1 + 1 = 2: C’s vector counts a minor third, E–G, and a fifth, C–G, that C–E lacks. A vector counts the intervals between every pair of notes: 6 for four notes, 3 for three, so a three-note set is at least 3 away from Cmaj7, more than the skill’s 2.
- Both are triads of the same set class, with the vector
<0 0 1 1 1 0>.FindNearbycompares vectors, not notes, and returns the sets by bitmask, so the eight first at distance 2 are the same. - Line 214,
quality.ToLowerInvariant(): “M7” becomes “m7”, which line 220 reads as a minor seventh.
Key takeaways
Section titled “Key takeaways”- Routing anchors and the parser that follows them are one contract: rewording one without the other sends questions to a refusal.
- A skill’s example prompts make the first test of the skill.
- A table that falls back to a major triad answers questions it can’t read.
- “Sorted by cost” means little when every cost ties: the order is then the order of enumeration.
- Neighbors in vector space depend on the vector, not on the chord: every chord of a set class gets the same list.
Sources
Section titled “Sources”- GuitarAlchemist/ga at
a826864:Common/GA.Business.ML/Agents/Skills/IcvNeighborsSkill.cs,Common/GA.Business.ML/Agents/Skills/IntervalClassVectorSkill.cs,Common/GA.Business.ML/Agents/Intents/DefaultRoutingHintProvider.cs,Common/GA.Business.ML/Agents/Intents/SemanticIntentRouter.cs,Common/GA.Domain.Services/Atonal/Grothendieck/GrothendieckService.cs,Common/GA.Business.Core.Orchestration/Plugins/GaPlugin.cs,GaMcpServer/Tools/ChordAtonalTool.cs,skills-dev/_pending-tools/icv-neighbors/DRAFT.md,CLAUDE.md. - GuitarAlchemist/ga at
f4f5b4a: the same skills, with their refusals markedDeclined, andChordAtonalTool.cs. - The course’s programs:
code/ga-ai/GaAi/Lesson22.cs,code/ga-ai/GaMain/Program.cs,code/ga-ai/Shared/IcvNeighborsProbe.cs.