Skip to content
learn

Appendix C: every divergence, and whose bug it is

Every lesson of this course prints a table with three columns: what the course computes from the textbook definition, what Guitar Alchemist answers for the same question, and ok or DIFF. Lessons 1 to 7 produce 40 DIFF rows. A reader is entitled to ask the obvious question about each one: is that a bug in GA, or is the course wrong?

This appendix answers it, row by row. The short version:

rows
GA’s bug, the course matches the theory 39
Neither: a defensible convention (with a real flaw underneath) 1
The course’s error 0

Forty rows, but not forty problems: they collapse into 19 distinct defects, because one wrong line can spoil eight rows. The grouping below is the useful view; the per-lesson tables after it are the detail.

# Defect in GA Rows Lessons Upstream Status
1 Pitch.Flat.DFlat/FFlat/GFlat(Octave) build the wrong note: each returns the note of the line below it 3 1 Fixed (factories corrected; EFlat/BFlat added)
2 PitchParser’s regex has no anchors and ignores case, so Eb2 matches as b2 = B2 1 1 Fixed (anchored regexes with static precompiled patterns)
3 Note.Flat.TryParse upper-cases before replacing ♭, so B becomes B♭ and D♭ is rejected 1 1 Fixed (natural B parses as natural B; Unicode ♭ handled)
4 PitchClass.TryParse tries the hex-like aliases A=10, B=11 before note names 1 1 Fixed (TryParseSetNotation separated from TryParse)
5 SimpleIntervalSize.TryParse and CompoundIntervalSize.TryParse throw instead of returning false 1 1 Fixed (returns false on invalid input)
6 ModalFamily groups sets by interval-class vector but calls the members Modes 2 2 Known design (groups by ICV)
7 ChordFormula.DetermineQuality never returns Major7, Minor7, HalfDiminished or Diminished7 2 3 Fixed (ChordFormula quality and suffix classification)
8 DetermineExtension reads nine semitones as a major sixth, never a diminished seventh 1 3 Fixed (diminished 7th distinguished from 6th/13th)
9 Note.Chromatic.ToAccidented is ToSharp().ToAccidented(): every black key gets a sharp name 8 3 Fixed (spelling-aware interval transposition in Chord)
10 Chord rotates its note list for an inversion, then re-analyses from the bass as if it were the root 1 3 Fixed (original Root and Formula preserved)
11 Voicing.HasBarre counts fret 0, so open strings make a barre 3 3 Fixed (fret > 0 filter in Voicing.HasBarre())
12 IntervalClassVectorId packs six counts as base-12 digits, and a count of 12 overflows its digit 1 4 Pinned limitation (preserves catalog ordering)
13 KeyTools.GetParallelKey has the same body as GetRelativeKey: it keeps the signature instead of the tonic 5 5 Fixed (preserves tonic with opposite mode)
14 KeyTools.GetNeighboringKeys looks a key up by its accidentals instead of its name, and throws 1 5 Fixed (circle of fifths position by signature count)
15 Key.GetInterval passes its arguments the wrong way round and returns the inversion 2 5 Fixed (root-to-note order restored)
16 Key.Major.TryParse throws on an unparseable root although it has a return false path 1 5 Fixed (returns false on unparseable root)
17 HarmonicFunction.FromDegree sees a degree number only, and its enum has no Subtonic 1 6 Fixed (Subtonic added with semitones-below-tonic check)
18 Cadences.yaml numbers one minor-key row from the parallel major (♭iii for G in E minor) 1 7 Fixed (corrected to iii in Cadences.yaml)
19 PitchClassSet.ClosestDiatonicKey breaks the major/minor tie on a normal form, which cannot encode a mode 4 7 Known heuristic (inherent PC-set tonic ambiguity)

Three patterns account for most of them.

  • Copy-paste inside a block of near-identical members: defects 1 and 13. Three factory methods each returning the next line’s note, and a GetParallelKey whose body was never changed after being copied from GetRelativeKey.
  • A pitch class asked to remember a letter: defects 8, 9 and 12, and the core of 19. A pitch class, a set id and an interval count are all reductions: they deliberately throw away the spelling, the octave or the tonic. Every row in this group is that discarded information being asked for again downstream.
  • TryParse that throws: defects 5 and 16, in three types. The IParsable<TSelf>.TryParse contract says a failed parse returns false.

Lesson 1: notes, pitch classes and the fretboard

Section titled “Lesson 1: notes, pitch classes and the fretboard”
Row GA answers Verdict Why
Pitch.Flat.DFlat(4) → Db4 D4 GA, defect 1 The method builds Note.Flat.D, the natural, although Note.Flat.DFlat exists.
Pitch.Flat.FFlat(4) → Fb4 G4 GA, defect 1 It builds Note.Flat.G. F♭ is enharmonically E and can never be G.
Pitch.Flat.GFlat(4) → Gb4 A4 GA, defect 1 It builds Note.Flat.A, the next line’s note again.
Pitch.Sharp.TryParse("Eb2") → rejected B2 GA, defect 2 ([A-G])(#?)(10|11|[0-9]) is unanchored and case-insensitive, so the match slides past the E and reads b2.
Note.Flat.Parse("B") → B Bb GA, defect 3 ToUpperInvariant() runs before Replace("♭", "b"), so the trailing-B test both over- and under-fires.
PitchClass.Parse("A") → 9 10 Neither Reading A as the digit 10 is a documented set-theory convention, so 10 is not wrong. But the aliases are tried before note names, so the note A can never be parsed as 9 — that part is a defect.
IntervalSize.TryParse("x") → False throws ArgumentException GA, defect 5 SimpleIntervalSize and CompoundIntervalSize both throw where the contract requires false.

An extra finding not in any row: the Flat(Octave) factory block has no EFlat(Octave) and no BFlat(Octave) at all, although the per-octave properties EFlat0, BFlat0 and their siblings exist. Only that overload family is unreliable.

Row GA answers Verdict Why
harmonic minor has 7 modes 14 GA, defect 6 A mode is a rotation, so a seven-note scale has seven. GA’s 14 are harmonic minor’s 7 rotations plus the 7 of its mirror image, harmonic major, which has the same interval-class vector.
the blues scale has 6 modes 24 GA, defect 6 6 rotations, 6 of the mirror, and 12 sets of a Z-related set class that shares the vector.

GA’s class comment describes what it really builds — sets “that share the same interval vector” — so the computation is right and the name is wrong. Ian Ring answers 7 and 6.

Lesson 3: chords, symbols, inversions and voicings

Section titled “Lesson 3: chords, symbols, inversions and voicings”
Row GA answers Verdict Why
Cmaj7 → maj7 7 GA, defect 7 Major plus seventh, and Major contributes no prefix, so it prints the dominant seventh’s symbol.
Cm7b5 → m7b5 dim7 GA, defect 7 The half-diminished chord prints with the fully diminished chord’s symbol.
Cdim7 → dim7 dim6 GA, defect 8 Nine semitones are read as a major sixth; only the spelling B𝄫 says otherwise, and a formula in semitones has none.
Cm → C Eb G C D# G GA, defect 9
Eb → Eb G Bb Eb G A# GA, defect 9 By its letters E♭–A♯ is a fourth, not the chord’s fifth.
Ab → Ab C Eb Ab C D# GA, defect 9
Bbm7 → Bb Db F Ab Bb C# F G# GA, defect 9 Two of four degrees land on the wrong letter.
Cdim → C Eb Gb C D# F# GA, defect 9 F♯ names an augmented fourth, not a diminished fifth.
Cdim7 → C Eb Gb Bbb C D# F# A GA, defect 9 The double flat is the textbook spelling; A is a sixth.
Gb7 → Gb Bb Db Fb Gb A# C# E GA, defect 9
exercise: F#dim7 → F# A C Eb F# A C D# GA, defect 9 The letters of a diminished seventh chord are F A C E.
C/E → inversion 1, Major inversion 1, Other GA, defect 10 After the rotation the bass sits in Notes[0], so the re-analysis skips the third and finds no third at all.
032010 is not a barre barre GA, defect 11
022100 is not a barre barre GA, defect 11
320003 is not a barre barre GA, defect 11 Three open strings share “fret 0”; no finger presses an open string.

Two of these have a correct implementation elsewhere in the same commit: CadenceChordParser and the F# DslCommand know the right symbols, and DetectBarreRequirement does test fret > 0 and does require adjacent strings.

Row GA answers Verdict Why
chromatic aggregate ICV → <12 12 12 12 12 6> <1 1 1 1 0 6> GA, defect 12 The counts are computed correctly, then packed as base-12 digits where 12 does not fit.

GA documents this one itself, as a “KNOWN LIMITATION” in the type’s own comment, with base 13 or a six-field record noted as the fix. It is the only affected set: an 11-note set peaks at 10 per interval class.

Row GA answers Verdict Why
parallel of C → C minor A minor GA, defect 13 The parallel key shares the tonic; GA keeps the signature, which is the relative key.
parallel of A♭ → A♭ minor F minor GA, defect 13
parallel of A minor → A major C major GA, defect 13
parallel of E minor → E major G major GA, defect 13
exercise: parallel of A♭ → A♭ minor, 7 flats F minor GA, defect 13 The exercise reruns the same expression.
neighbours of C → F, G throws InvalidOperationException GA, defect 14 The tool looks the key up again by "" (C major’s accidentals) instead of "Key of C".
Key.Major.C.GetInterval(E) → M3 m6 GA, defect 15
Key.Major.G.GetInterval(F#) → M7 m2 GA, defect 15 Both return the inversion of the documented interval.
Key.Major.TryParse("H") → False throws GA, defect 16 The method has a return false path a few lines below.

GA’s core has no relative-key or parallel-key member at all, so the MCP tool is the only code answering these two questions. Its comment inside the correct method already swaps the two words, so the confusion is in the vocabulary, not only in one body.

Row GA answers Verdict Why
VII of A natural minor → G major, Subtonic LeadingTone GA, defect 17 The seventh degree of natural minor is a whole step below the tonic, so it is the subtonic; a leading tone is a half step below.
Row GA answers Verdict Why
Chromatic Mediant (Metal), Em–Gm in E minor → i iii i biii GA, defect 18 G is the unaltered third degree of E minor. Read in E minor, ♭iii points at G♭, which the chord does not contain. GA’s own F# analyser uses the minor-scale numerals.
key of C F G C → C major A minor GA, defect 19
key of G D Em C → G major E minor GA, defect 19
key of Dm7 G7 Cmaj7 → C major A minor GA, defect 19
key of C Am F G7 → C major A minor GA, defect 19 Every seven-note diatonic collection has the normal form 0 1 3 5 6 8 T, which contains 3, so the mode flag is always raised and the answer is always the relative minor.

The last four are worth separating from the rest of this appendix, because the fix is not a line: a pitch-class set has no tonic. C F G C and Am F C G are the same set. No function of the set alone can name one of the two relative keys; naming one needs the order of the chords, which ClosestDiatonicKey never receives. Returning a key signature, or the pair — which GetCompatibleKeys already does — is the honest answer for that input.

It is not a bug report filed against GA, and nothing here has been published outside this repository. It is the record of what a reader should conclude when a lesson prints DIFF: in this course, so far, it has meant GA, every time but one.

It is also not a claim that the course is authoritative. The course’s own computations are plain implementations of textbook definitions, and its barre rule, its key-finding heuristic and its chord-symbol table are all approximations it declares as such where they appear. Where the course and GA agree, both could still be wrong; those rows print ok and this appendix says nothing about them.