You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Key.RomanNumeralOf produces duplicate/incorrect labels for modes with more than 7 degrees #249
RomanNumeralOf indexes a fixed 7-entry table with RomanNumerals[(degree.Degree - 1) % RomanNumerals.Length]. degree.Degree comes from Scale.DegreeOf, which returns i + 1 where i ranges over Mode.Intervals.Count — and Mode supports 8-note modes (OctatonicHalfWhole/OctatonicWholeHalf) and the 12-note Chromatic mode. For these modes the % 7 wraps distinct scale degrees onto the same roman numeral, silently.
Why it matters (concrete failure scenario)
Keychromatic=Key.Create(PitchClass.Create(0),Mode.Chromatic);chromatic.RomanNumeralOf(Chord.Parse("C#"));// "II" (degree 2)chromatic.RomanNumeralOf(Chord.Parse("Ab"));// "II" (degree 9) — same label, different chord
A chord rooted at scale degree 2 (C♯) and one rooted at degree 9 (A♭) both resolve to "II". The tonic (degree 1) and the 8th chromatic degree similarly collide onto "I". Mode.OctatonicHalfWhole (8 degrees) has the same collision between degree 8 and the tonic. This is also inconsistent with the inverse operation: ChordFromRomanNumeral explicitly bounds-checks the degree and throws FormatException rather than wrapping — so the two "both directions" APIs disagree on how to handle non-heptatonic keys.
Suggested fix / acceptance criteria
Either (a) throw a descriptive exception from RomanNumeralOf when degree.Degree > Mode.DegreeCount for modes without a conventional 7-degree numeral system (mirroring ChordFromRomanNumeral's guard), or (b) size/select the numeral table from the mode's actual degree count so degrees beyond 7 get distinct labels instead of wrapping. Add a test asserting RomanNumeralOf never returns the same string for two distinct, non-enharmonic scale degrees within one mode, at least for Chromatic and both octatonic modes.
Category: Bug Priority: Medium Area:Semantics.Music — Key.RomanNumeralOf
Medium: a genuine correctness bug producing silently wrong labels, but confined to the non-heptatonic modes (Chromatic, OctatonicHalfWhole, OctatonicWholeHalf). Diatonic keys — the overwhelming majority of use — are unaffected, which is what keeps it off High. The sharpest detail is the internal inconsistency the issue points out: ChordFromRomanNumeral bounds-checks the degree and throws, while RomanNumeralOf wraps with % 7, so the two halves of a round trip disagree about whether a 9th degree is meaningful.
Suggested assignment: none specific.
Related: open PR #250 ("Read C6/9 as a six-nine chord, not a slash bass") is in the same Semantics.Music chord/key area, but a distinct defect — not a duplicate and not blocking. Worth a glance for whoever picks this up only because the two touch neighbouring parsing/labelling code.
Open PR covering this: none.
Suggested next step: pick (a) or (b) deliberately rather than by whichever is easier to write, because they encode different claims. (a) throwing says roman-numeral analysis is undefined outside conventional degree systems, which is defensible and matches what ChordFromRomanNumeral already asserts. (b) extending the table says these modes have a numeral spelling — which requires deciding what the 8th and 12th degrees are actually called, and there is no single convention to appeal to. (a) is the smaller claim and restores the symmetry between the two directions; (b) needs a musicological answer before it needs code. The proposed test — no two distinct non-enharmonic degrees within one mode share a string — is the right invariant either way.
Activity
matt-edmondson commentedon Sep 16, 2026
Triage
Category: Bug
Priority: Medium
Area:
Semantics.Music—Key.RomanNumeralOfMedium: a genuine correctness bug producing silently wrong labels, but confined to the non-heptatonic modes (
Chromatic,OctatonicHalfWhole,OctatonicWholeHalf). Diatonic keys — the overwhelming majority of use — are unaffected, which is what keeps it off High. The sharpest detail is the internal inconsistency the issue points out:ChordFromRomanNumeralbounds-checks the degree and throws, whileRomanNumeralOfwraps with% 7, so the two halves of a round trip disagree about whether a 9th degree is meaningful.Suggested assignment: none specific.
Related: open PR #250 ("Read
C6/9as a six-nine chord, not a slash bass") is in the sameSemantics.Musicchord/key area, but a distinct defect — not a duplicate and not blocking. Worth a glance for whoever picks this up only because the two touch neighbouring parsing/labelling code.Open PR covering this: none.
Suggested next step: pick (a) or (b) deliberately rather than by whichever is easier to write, because they encode different claims. (a) throwing says roman-numeral analysis is undefined outside conventional degree systems, which is defensible and matches what
ChordFromRomanNumeralalready asserts. (b) extending the table says these modes have a numeral spelling — which requires deciding what the 8th and 12th degrees are actually called, and there is no single convention to appeal to. (a) is the smaller claim and restores the symmetry between the two directions; (b) needs a musicological answer before it needs code. The proposed test — no two distinct non-enharmonic degrees within one mode share a string — is the right invariant either way.Generated by Claude Code
Refuse to label scale degrees the roman numerals cannot spell [patch]