Refuse to label scale degrees the roman numerals cannot spell - #251
Merged
Merged
Conversation
RomanNumeralOf indexed a fixed seven-entry table with `% 7`, so in a mode with more than seven degrees two distinct degrees were handed the same numeral. In Chromatic, degree 8 came back as "I" alongside the tonic and degree 9 as "II" alongside degree 2; both octatonic modes collided their eighth degree onto the tonic. Guard the degree instead of wrapping it, which is what ChordFromRomanNumeral already does in the other direction, so a round trip no longer disagrees with itself about whether a ninth degree means anything. Degrees one to seven are unchanged, including in Chromatic, so every diatonic key keeps its labels. Fixes #249 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LP6XbVRYgiHfN3o2a7MC4x
|
This was referenced Sep 17, 2026
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.



Fixes #249
The bug
Key.RomanNumeralOfindexed a fixed seven-entry table withRomanNumerals[(degree.Degree - 1) % RomanNumerals.Length].Scale.DegreeOfreturns a degree overMode.Intervals.Count, and three modes have more than seven of those —Chromatic(12),OctatonicHalfWhole(8) andOctatonicWholeHalf(8). For those, the% 7wrapped distinct degrees onto the same numeral, silently:The two halves of a round trip also disagreed:
ChordFromRomanNumeralbounds-checks the degree and throws rather than wrapping, so it already asserted that a ninth degree means nothing whileRomanNumeralOfhappily invented a label for one.The change
Option (a) from the issue, which the triage comment recommends: guard rather than extend the table. Extending it would require deciding what the eighth and twelfth degrees are called, and there is no convention to appeal to — that is a musicological answer, not a code one. Guarding makes the smaller claim, that roman-numeral analysis is undefined outside a conventional degree system, and restores the symmetry with
ChordFromRomanNumeral.RomanNumeralOfthrowsArgumentExceptionwhen the resolved degree is beyond the seventh, naming the degree and the mode.ArgumentExceptionrather thanChordFromRomanNumeral'sFormatExceptionbecause nothing is being parsed here — this is a validation failure, which is what CLAUDE.md's error-handling standard asks for.% 7is gone from the index, so the guard is what keeps it in range rather than there being two overlapping defences.<exception>docs onRomanNumeralOfand onProgression.RomanNumerals, its one caller.Degrees one through seven are untouched, including in
Chromatic, so every diatonic key — the overwhelming majority of use — keeps exactly the labels it had.Tests
RomanNumeral_DistinctDegreesNeverShareALabelis the invariant the issue asks for: acrossChromaticand both octatonic modes, walk every scale degree and assert no two distinct degrees produce the same string, with degrees past the seventh required to throw. It fails onmainat chromatic degree 8 (Expected exception of exact type ArgumentException but no exception was thrown) and passes with the change — verified by running it against the unfixed source first.RomanNumeral_ChromaticKeyStillLabelsTheFirstSevenDegreespins the behaviour that is deliberately not changing, so a future widening of the guard cannot quietly take the working chromatic labels with it.Full suite: 1252 tests, 1244 passed, 8 skipped (Windows-only), 0 failed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LP6XbVRYgiHfN3o2a7MC4x
Generated by Claude Code