head_music 9.0.1 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -3
  3. data/CHANGELOG.md +18 -0
  4. data/CLAUDE.md +35 -15
  5. data/Gemfile +7 -1
  6. data/Gemfile.lock +91 -3
  7. data/README.md +18 -0
  8. data/Rakefile +7 -2
  9. data/head_music.gemspec +1 -1
  10. data/lib/head_music/analysis/dyad.rb +229 -0
  11. data/lib/head_music/analysis/melodic_interval.rb +1 -1
  12. data/lib/head_music/analysis/pitch_class_set.rb +111 -14
  13. data/lib/head_music/analysis/{pitch_set.rb → pitch_collection.rb} +11 -5
  14. data/lib/head_music/analysis/sonority.rb +50 -12
  15. data/lib/head_music/content/staff.rb +1 -1
  16. data/lib/head_music/content/voice.rb +1 -1
  17. data/lib/head_music/instruments/alternate_tuning.rb +102 -0
  18. data/lib/head_music/instruments/alternate_tunings.yml +78 -0
  19. data/lib/head_music/instruments/instrument.rb +251 -82
  20. data/lib/head_music/instruments/instrument_configuration.rb +66 -0
  21. data/lib/head_music/instruments/instrument_configuration_option.rb +38 -0
  22. data/lib/head_music/instruments/instrument_configurations.yml +288 -0
  23. data/lib/head_music/instruments/instrument_families.yml +77 -0
  24. data/lib/head_music/instruments/instrument_family.rb +3 -4
  25. data/lib/head_music/instruments/instruments.yml +795 -965
  26. data/lib/head_music/instruments/playing_technique.rb +75 -0
  27. data/lib/head_music/instruments/playing_techniques.yml +826 -0
  28. data/lib/head_music/instruments/score_order.rb +2 -5
  29. data/lib/head_music/instruments/staff.rb +61 -1
  30. data/lib/head_music/instruments/staff_scheme.rb +6 -4
  31. data/lib/head_music/instruments/stringing.rb +115 -0
  32. data/lib/head_music/instruments/stringing_course.rb +58 -0
  33. data/lib/head_music/instruments/stringings.yml +168 -0
  34. data/lib/head_music/instruments/variant.rb +0 -1
  35. data/lib/head_music/locales/de.yml +23 -0
  36. data/lib/head_music/locales/en.yml +100 -0
  37. data/lib/head_music/locales/es.yml +23 -0
  38. data/lib/head_music/locales/fr.yml +23 -0
  39. data/lib/head_music/locales/it.yml +23 -0
  40. data/lib/head_music/locales/ru.yml +23 -0
  41. data/lib/head_music/{rudiment → notation}/musical_symbol.rb +3 -3
  42. data/lib/head_music/notation/staff_mapping.rb +70 -0
  43. data/lib/head_music/notation/staff_position.rb +62 -0
  44. data/lib/head_music/notation.rb +7 -0
  45. data/lib/head_music/rudiment/alteration.rb +17 -47
  46. data/lib/head_music/rudiment/alterations.yml +32 -0
  47. data/lib/head_music/rudiment/chromatic_interval.rb +1 -1
  48. data/lib/head_music/rudiment/clef.rb +1 -1
  49. data/lib/head_music/rudiment/consonance.rb +14 -13
  50. data/lib/head_music/rudiment/key_signature.rb +0 -26
  51. data/lib/head_music/rudiment/rhythmic_unit/parser.rb +2 -2
  52. data/lib/head_music/rudiment/rhythmic_value/parser.rb +1 -1
  53. data/lib/head_music/rudiment/rhythmic_value.rb +1 -1
  54. data/lib/head_music/rudiment/spelling.rb +3 -0
  55. data/lib/head_music/rudiment/tempo.rb +1 -1
  56. data/lib/head_music/rudiment/tuning/just_intonation.rb +0 -39
  57. data/lib/head_music/rudiment/tuning/meantone.rb +0 -39
  58. data/lib/head_music/rudiment/tuning/pythagorean.rb +0 -39
  59. data/lib/head_music/rudiment/tuning.rb +20 -0
  60. data/lib/head_music/style/guidelines/consonant_climax.rb +2 -2
  61. data/lib/head_music/style/modern_tradition.rb +8 -11
  62. data/lib/head_music/style/tradition.rb +1 -1
  63. data/lib/head_music/time/clock_position.rb +84 -0
  64. data/lib/head_music/time/conductor.rb +264 -0
  65. data/lib/head_music/time/meter_event.rb +37 -0
  66. data/lib/head_music/time/meter_map.rb +173 -0
  67. data/lib/head_music/time/musical_position.rb +188 -0
  68. data/lib/head_music/time/smpte_timecode.rb +164 -0
  69. data/lib/head_music/time/tempo_event.rb +40 -0
  70. data/lib/head_music/time/tempo_map.rb +187 -0
  71. data/lib/head_music/time.rb +32 -0
  72. data/lib/head_music/utilities/case.rb +27 -0
  73. data/lib/head_music/utilities/hash_key.rb +1 -1
  74. data/lib/head_music/version.rb +1 -1
  75. data/lib/head_music.rb +41 -13
  76. data/user_stories/active/string-pitches.md +41 -0
  77. data/user_stories/backlog/notation-style.md +183 -0
  78. data/user_stories/{todo → backlog}/organizing-content.md +9 -1
  79. data/user_stories/done/consonance-dissonance-classification.md +117 -0
  80. data/user_stories/{todo → done}/dyad-analysis.md +4 -6
  81. data/user_stories/done/expand-playing-techniques.md +38 -0
  82. data/user_stories/{active → done}/handle-time.rb +5 -19
  83. data/user_stories/done/instrument-architecture.md +238 -0
  84. data/user_stories/done/move-musical-symbol-to-notation.md +161 -0
  85. data/user_stories/done/move-staff-mapping-to-notation.md +158 -0
  86. data/user_stories/done/move-staff-position-to-notation.md +141 -0
  87. data/user_stories/done/notation-module-foundation.md +102 -0
  88. data/user_stories/done/percussion_set.md +260 -0
  89. data/user_stories/{todo → done}/pitch-class-set-analysis.md +0 -40
  90. data/user_stories/done/sonority-identification.md +37 -0
  91. data/user_stories/epics/notation-module.md +135 -0
  92. data/user_stories/{todo → visioning}/agentic-daw.md +0 -1
  93. metadata +55 -20
  94. data/check_instrument_consistency.rb +0 -0
  95. data/lib/head_music/instruments/instrument_type.rb +0 -188
  96. data/test_translations.rb +0 -15
  97. data/user_stories/todo/consonance-dissonance-classification.md +0 -57
  98. data/user_stories/todo/material-and-scores.md +0 -10
  99. data/user_stories/todo/percussion_set.md +0 -1
  100. data/user_stories/todo/pitch-set-classification.md +0 -72
  101. data/user_stories/todo/sonority-identification.md +0 -67
  102. /data/user_stories/{active → done}/handle-time.md +0 -0
@@ -0,0 +1,117 @@
1
+ # Consonance and Dissonance Classification
2
+
3
+ As a music theorist or counterpoint student
4
+
5
+ I want to classify intervals by their consonance and dissonance levels
6
+
7
+ So that I can apply proper voice leading rules
8
+
9
+ ## Scenario: Classify open consonances
10
+
11
+ Given I have a perfect fifth or perfect octave
12
+
13
+ When I check the consonance classification
14
+
15
+ Then it should be identified as "open consonance"
16
+
17
+ ## Scenario: Classify soft consonances
18
+
19
+ Given I have a third or sixth interval (major or minor)
20
+
21
+ When I check the consonance classification
22
+
23
+ Then it should be identified as "soft consonance"
24
+
25
+ ## Scenario: Classify mild dissonances
26
+
27
+ Given I have a major second or minor seventh
28
+
29
+ When I check the consonance classification
30
+
31
+ Then it should be identified as "mild dissonance"
32
+
33
+ ## Scenario: Classify sharp dissonances
34
+
35
+ Given I have a minor second or major seventh
36
+
37
+ When I check the consonance classification
38
+
39
+ Then it should be identified as "sharp dissonance"
40
+
41
+ ## Scenario: Handle perfect fourth context
42
+
43
+ Given I have a perfect fourth interval
44
+
45
+ When I check the consonance classification
46
+
47
+ Then it should indicate context-dependent classification
48
+
49
+ And note it can be either consonant or dissonant
50
+
51
+ ## Scenario: Classify tritone
52
+
53
+ Given I have a tritone interval
54
+
55
+ When I check the consonance classification
56
+
57
+ Then it should be identified as "neutral" or "restless"
58
+
59
+ ---
60
+
61
+ ## Implementation Notes
62
+
63
+ This user story was **mostly already implemented** with existing functionality. The following enhancements were made:
64
+
65
+ ### Changes Implemented
66
+
67
+ 1. **Added `neutral` consonance level** to `HeadMusic::Rudiment::Consonance`
68
+ - New constant: `NEUTRAL = :neutral`
69
+ - Added `neutral?` predicate method
70
+ - Added to `HeadMusic::Analysis::IntervalConsonance`
71
+
72
+ 2. **Changed P4 classification in ModernTradition** from `perfect_consonance` to `contextual`
73
+ - Perfect fourth is now correctly classified as context-dependent
74
+ - Consonant in upper voices, dissonant against bass
75
+ - Medieval tradition still classifies as `perfect_consonance`
76
+ - Renaissance tradition still classifies as `dissonance`
77
+
78
+ 3. **Changed tritone classification** from `dissonance` to `neutral`
79
+ - Both augmented fourth and diminished fifth now classified as `neutral`
80
+ - Reflects the ambiguous, restless quality of the tritone
81
+
82
+ ### Terminology Mapping
83
+
84
+ The user story terminology maps to existing library classifications:
85
+
86
+ | User Story Term | Library Term | Status |
87
+ |----------------|--------------|--------|
88
+ | "Open consonance" | `perfect_consonance` | ✅ Already exists |
89
+ | "Soft consonance" | `imperfect_consonance` | ✅ Already exists |
90
+ | "Mild dissonance" | `mild_dissonance` | ✅ Already exists (exact match!) |
91
+ | "Sharp dissonance" | `harsh_dissonance` | ✅ Already exists |
92
+ | P4 context-dependent | `contextual` | ✅ Now implemented |
93
+ | Tritone "neutral/restless" | `neutral` | ✅ Now implemented |
94
+
95
+ ### API Usage
96
+
97
+ ```ruby
98
+ interval = HeadMusic::Analysis::DiatonicInterval.get("P4")
99
+ interval.consonance # => #<Consonance @name=:contextual>
100
+ interval.contextual? # => true
101
+ interval.consonant? # => false (contextual is neither consonant nor dissonant)
102
+
103
+ tritone = HeadMusic::Analysis::DiatonicInterval.get("A4")
104
+ tritone.consonance # => #<Consonance @name=:neutral>
105
+ tritone.neutral? # => true
106
+ tritone.dissonant? # => false (neutral is not strictly dissonant)
107
+ ```
108
+
109
+ ### Test Coverage
110
+
111
+ - Updated all existing tests for new classifications
112
+ - Added comprehensive tests for `neutral` and `contextual` intervals
113
+ - 3736 tests passing (8 cantus firmus examples affected by P4 change)
114
+
115
+ ### Known Side Effects
116
+
117
+ The P4 classification change affects some cantus firmus style guide tests, as these historical examples may contain perfect fourths that were previously considered consonant. These can be addressed separately if needed by updating the style guidelines to account for contextual intervals.
@@ -6,6 +6,8 @@ I want to analyze two-note combinations (dyads)
6
6
 
7
7
  So that I can understand harmonic implications in two-part music
8
8
 
9
+ Every dyad can be interpreted as implying one or more chords (triad or seventh)
10
+
9
11
  Constructor should accept to pitches (or pitch classes) and an optional key
10
12
 
11
13
  ## Scenario: Identify interval in dyad
@@ -28,13 +30,11 @@ And each should contain the given pitches
28
30
 
29
31
  ## Scenario: List possible triads from third
30
32
 
31
- Given I have a dyad forming a minor third
33
+ Given I have a dyad forming a major or minor third or sixth
32
34
 
33
35
  When I request possible triads
34
36
 
35
- Then I should receive minor and diminished triad options
36
-
37
- And for a major third I should receive major and augmented options
37
+ Then I should receive major, minor, diminished, and/or augmented triads containing those pitch classes
38
38
 
39
39
  ## Scenario: Find possible seventh chords
40
40
 
@@ -44,8 +44,6 @@ When I request possible seventh chords
44
44
 
45
45
  Then I should receive all seventh chords containing those pitches
46
46
 
47
- And they should include appropriate inversions
48
-
49
47
  ## Scenario: Handle enharmonic possibilities
50
48
 
51
49
  Given I have a dyad with enharmonic possibilities
@@ -0,0 +1,38 @@
1
+ ## User Story: Load Playing Techniques from YAML Data File
2
+
3
+ **As a** client of the HeadMusic gem
4
+
5
+ **I want** access to a comprehensive catalog of playing techniques across all instrument families
6
+
7
+ **So that** I can notate and work with playing techniques for strings, winds, harp, keyboard, and percussion—not just drum kit techniques
8
+
9
+ ### Background
10
+
11
+ The current `PlayingTechnique` class uses a hardcoded `TECHNIQUES` constant containing only 14 percussion-focused techniques (stick, pedal, mallet, hand, brush, rim_shot, cross_stick, open, closed, damped, let_ring, choked, bow, bell).
12
+
13
+ A new `playing_techniques.yml` data file has been created with 50+ techniques organized by scope (common, strings, winds, percussion, harp, keyboard), including:
14
+
15
+ - **Common techniques**: legato, marcato, vibrato, con sordino, naturale, ordinario
16
+ - **String/wind techniques**: harmonics, bowing techniques
17
+ - **Percussion techniques**: rim shots, cross sticks, dead strokes, motor on/off
18
+ - **Harp techniques**: près de la table
19
+ - **Rich metadata**: origin language, meaning, notation variants
20
+
21
+ The YAML-driven approach aligns with how other HeadMusic components work (instruments, clefs, scales, etc.) and enables future extensibility.
22
+
23
+ ### Acceptance Criteria
24
+
25
+ - [ ] `PlayingTechnique.all` returns techniques loaded from `playing_techniques.yml`
26
+ - [ ] The `TECHNIQUES` constant is removed
27
+ - [ ] Each technique retains access to its metadata (scopes, origin, meaning, notations)
28
+ - [ ] `PlayingTechnique.get(identifier)` continues to work for any technique in the YAML
29
+ - [ ] New accessor methods for metadata: `#scopes`, `#origin`, `#meaning`, `#notations`
30
+ - [ ] Techniques can be filtered by scope (e.g., `PlayingTechnique.for_scope(:strings)`)
31
+ - [ ] Existing specs pass; new specs cover the expanded technique catalog
32
+ - [ ] Maintains 90%+ test coverage
33
+
34
+ ### Technical Notes
35
+
36
+ - Follow the existing YAML-loading pattern used by `Instrument`, `Clef`, and other data-driven classes
37
+ - The `HeadMusic::Named` mixin is already included, enabling future i18n support
38
+ - Consider caching loaded techniques for performance (consistent with other HeadMusic patterns)
@@ -21,8 +21,8 @@ class HeadMusic::Time::Conductor
21
21
 
22
22
  def initialize(attributes = {})
23
23
  attributes = attributes.symbolize_keys
24
- @starting_position = attributes.get(:starting_position, HeadMusic::Time::Position.new)
25
- @starting_smpte_timecode = attributes.get(:starting_smpte_timecode, HeadMusic::Time::SmpteTimecode.new)
24
+ @starting_position = attributes.fetch(:starting_position, HeadMusic::Time::MusicalPosition.new)
25
+ @starting_smpte_timecode = attributes.fetch(:starting_smpte_timecode, HeadMusic::Time::SmpteTimecode.new)
26
26
  end
27
27
  end
28
28
 
@@ -31,6 +31,7 @@ class HeadMusic::Time::MeterEvent
31
31
 
32
32
  def initialize(position, meter)
33
33
  @position = position
34
+ @meter = meter
34
35
  end
35
36
  end
36
37
 
@@ -44,21 +45,6 @@ class HeadMusic::Time::TempoEvent
44
45
  end
45
46
  end
46
47
 
47
- # Abstract superclass
48
- class HeadMusic::Time::Position
49
- include Comparable
50
-
51
- def initialize(value, meter: nil, tempo: nil)
52
- @value = value
53
- @meter = meter || HeadMusic::Rudiment::Meter.default
54
- @tempo = tempo || HeadMusic::Rudiment::Tempo.default
55
- end
56
-
57
- def <=>(other)
58
- to_i <=> other.to_i
59
- end
60
- end
61
-
62
48
  # A value object representing ellapsed nanoseconds of clock time
63
49
  class HeadMusic::Time::ClockPosition
64
50
  include Comparable
@@ -86,7 +72,7 @@ class HeadMusic::Time::ClockPosition
86
72
  end
87
73
 
88
74
  def +(other)
89
- HeadMusic::Time::Value.new(nanoseconds + other.to_i)
75
+ HeadMusic::Time::ClockPosition.new(nanoseconds + other.to_i)
90
76
  end
91
77
 
92
78
  def <=>(other)
@@ -156,7 +142,7 @@ class HeadMusic::Time::MusicalPosition
156
142
  bar_delta, @beat = beat.divmod(meter.counts_per_bar)
157
143
  @bar += bar_delta
158
144
  end
159
- HeadMusic::Time::Position.new(@bar, @beat, @tick, @subtick)
145
+ self
160
146
  end
161
147
  end
162
148
 
@@ -0,0 +1,238 @@
1
+ # Instrument Inheritance Architecture
2
+
3
+ AS a developer
4
+
5
+ I WANT instruments to resolve attributes through composable parent-based inheritance with configurable options
6
+
7
+ SO THAT I can model the full complexity of instruments with a simple, familiar pattern
8
+
9
+ ## Background
10
+
11
+ The current instrument architecture uses nested objects (GenericInstrument → Variant → StaffScheme → Staff) which conflates several independent concerns:
12
+
13
+ 1. **Species defaults** - What a trumpet typically is
14
+ 2. **Pitched identity** - This specific clarinet is in A
15
+ 3. **Physical configuration** - Piccolo trumpet with A leadpipe installed
16
+ 4. **Notation conventions** - British brass band uses treble clef for euphonium
17
+
18
+ This story addresses concerns 1-3 through parent-based inheritance. Notation (concern 4) is orthogonal and will be handled separately.
19
+
20
+ ## The Inheritance Pattern
21
+
22
+ ### Self-Referential Instrument
23
+
24
+ ```
25
+ ┌─────────────────────────────────────────────────────────┐
26
+ │ Instrument │
27
+ │ belongs_to :parent (optional) │
28
+ │ has_many :instrument_configurations │
29
+ │ │
30
+ │ Attributes resolve via parent chain: │
31
+ │ pitch_key || parent&.pitch_key │
32
+ └─────────────────────────────────────────────────────────┘
33
+ ```
34
+
35
+ ### Inheritance Examples
36
+
37
+ ```
38
+ trumpet (no parent)
39
+ ├── piccolo_trumpet (parent: trumpet, different range)
40
+ ├── bass_trumpet (parent: trumpet, different range)
41
+ └── pocket_trumpet (parent: trumpet, same attributes)
42
+
43
+ clarinet (no parent, pitch_key: b_flat)
44
+ ├── clarinet_in_a (parent: clarinet, pitch_key: a)
45
+ ├── clarinet_in_c (parent: clarinet, pitch_key: c)
46
+ ├── clarinet_in_e_flat (parent: clarinet, pitch_key: e_flat)
47
+ └── bass_clarinet (parent: clarinet, different range)
48
+ ```
49
+
50
+ ### Resolution Example
51
+
52
+ ```ruby
53
+ clarinet = Instrument.get("clarinet")
54
+ clarinet.pitch_key # => "b_flat"
55
+ clarinet.family_key # => "clarinet"
56
+
57
+ clarinet_in_a = Instrument.get("clarinet_in_a")
58
+ clarinet_in_a.pitch_key # => "a" (own attribute)
59
+ clarinet_in_a.family_key # => "clarinet" (from parent)
60
+ clarinet_in_a.parent # => clarinet
61
+ ```
62
+
63
+ ### Key Characteristics
64
+
65
+ 1. **Simple**: One class, one relationship, familiar pattern
66
+ 2. **Natural resolution**: `attribute || parent&.attribute`
67
+ 3. **Arbitrary depth**: Can model instrument families at any level
68
+ 4. **Data-driven**: YAML or database records, not code
69
+
70
+ ## Core Attributes
71
+
72
+ | Attribute | Type | Description |
73
+ |-----------|------|-------------|
74
+ | `name_key` | String | Primary identifier ("trumpet", "clarinet_in_a") |
75
+ | `alias_name_keys` | Array | Alternative names |
76
+ | `pitch_key` | String | The pitch designation ("b_flat", "a", "f") |
77
+ | `family_key` | String | Instrument family ("trumpet", "clarinet") |
78
+ | `orchestra_section_key` | String | Section ("brass", "woodwind", "strings") |
79
+ | `classification_keys` | Array | Hornbostel-Sachs style classifications |
80
+ | `default_clef_key` | String | Primary clef for this instrument |
81
+ | `range_low` | String | Lowest playable pitch |
82
+ | `range_high` | String | Highest playable pitch |
83
+
84
+ ## Configuration System
85
+
86
+ Instruments can have configurable options that modify their attributes when selected.
87
+
88
+ ### Structure
89
+
90
+ ```ruby
91
+ class Instrument
92
+ belongs_to :parent, class_name: "Instrument", optional: true
93
+ has_many :instrument_configurations # applies to self and descendants
94
+ end
95
+
96
+ class InstrumentConfiguration
97
+ belongs_to :instrument
98
+ has_many :instrument_configuration_options
99
+
100
+ # name_key: "leadpipe", "mute", "crook", "extension", "attachment"
101
+ end
102
+
103
+ class InstrumentConfigurationOption
104
+ belongs_to :instrument_configuration
105
+
106
+ # name_key: "a_leadpipe", "straight_mute", "f_crook"
107
+ # transposition_semitones: Integer (e.g., -1 for A leadpipe on Bb instrument)
108
+ # lowest_pitch_semitones: Integer (e.g., -4 for C extension on double bass)
109
+ end
110
+ ```
111
+
112
+ ### Configuration Examples
113
+
114
+ | Instrument | Configuration | Options |
115
+ |------------|---------------|---------|
116
+ | Piccolo trumpet | `leadpipe` | `b_flat` (default), `a` (transposition: -1) |
117
+ | Bass trombone | `f_attachment` | `disengaged`, `engaged` (lowest_pitch: -6) |
118
+ | Double bass | `c_extension` | `without`, `with` (lowest_pitch: -4) |
119
+ | Natural horn | `crook` | `e_flat`, `f`, `g`, etc. (various transpositions) |
120
+ | Trumpet | `mute` | `open`, `straight`, `cup`, `harmon` |
121
+ | Guitar | `capo` | `fret_0` through `fret_12` (transposition: 0-12) |
122
+
123
+ ### Configuration Resolution
124
+
125
+ - `instrument.instrument_configurations` walks parent chain, collects all
126
+ - No configuration selected = base instrument attributes unchanged
127
+ - Configuration selection lives in composition context (Part/Voice), not on Instrument
128
+ - A Part specifies target characteristics; configuration selection is derived/validated
129
+
130
+ ```ruby
131
+ # The instrument defines what CAN be configured
132
+ piccolo_trumpet = Instrument.get("piccolo_trumpet")
133
+ piccolo_trumpet.instrument_configurations # => [leadpipe config from parent chain]
134
+
135
+ # A Part specifies what characteristics are needed
136
+ part.instrument # => piccolo_trumpet
137
+ part.target_pitch_key # => "a"
138
+ part.effective_transposition # resolved via configuration options
139
+ ```
140
+
141
+ ## User Stories
142
+
143
+ ### STORY 1: Implement parent-based attribute resolution
144
+
145
+ AS a developer
146
+ WHEN I access an attribute on an Instrument
147
+ I WANT it resolved through the parent chain
148
+ SO THAT child instruments inherit from parents
149
+
150
+ **Acceptance criteria:**
151
+ - `instrument.pitch_key` returns own value or walks parent chain
152
+ - All core attributes support parent chain resolution
153
+ - Returns nil if no instrument in chain provides the attribute
154
+
155
+ ### STORY 2: Implement configuration inheritance
156
+
157
+ AS a developer
158
+ WHEN I query an instrument's available configurations
159
+ I WANT to get configurations from the entire parent chain
160
+ SO THAT child instruments inherit configurable options
161
+
162
+ **Acceptance criteria:**
163
+ - `instrument.instrument_configurations` collects from self and all ancestors
164
+ - Configurations defined on parent apply to all descendants
165
+ - Child can define additional configurations
166
+
167
+ ### STORY 3: Model configuration options with effects
168
+
169
+ AS a developer
170
+ WHEN I define configuration options
171
+ I WANT to specify their effects on instrument attributes
172
+ SO THAT transposition and range changes are calculable
173
+
174
+ **Acceptance criteria:**
175
+ - `InstrumentConfigurationOption` has `transposition_semitones` attribute
176
+ - `InstrumentConfigurationOption` has `lowest_pitch_semitones` attribute
177
+ - Effects are integers representing semitone adjustments
178
+
179
+ ### STORY 4: Migrate existing instrument data
180
+
181
+ AS a developer
182
+ WHEN I use the existing `Instrument.get()` API
183
+ I WANT it to work with the new inheritance architecture
184
+ SO THAT existing code continues to function
185
+
186
+ **Acceptance criteria:**
187
+ - `Instrument.get("trumpet")` returns instrument with no parent
188
+ - `Instrument.get("clarinet_in_a")` returns instrument with parent: clarinet
189
+ - All existing tests pass
190
+
191
+ ### STORY 5: Remove Variant class
192
+
193
+ AS a developer
194
+ WHEN pitched variants are modeled as child instruments
195
+ I WANT to remove the separate Variant class
196
+ SO THAT the model is simplified
197
+
198
+ **Acceptance criteria:**
199
+ - `Variant` class removed
200
+ - All pitched variants are now `Instrument` records with parents
201
+ - YAML structure updated to reflect inheritance
202
+
203
+ ## Migration Strategy
204
+
205
+ 1. **Phase 1**: Add `parent` relationship to Instrument class
206
+ 2. **Phase 2**: Implement parent chain attribute resolution
207
+ 3. **Phase 3**: Migrate YAML data to parent-based structure
208
+ 4. **Phase 4**: Create InstrumentConfiguration and InstrumentConfigurationOption classes
209
+ 5. **Phase 5**: Remove Variant class and update GenericInstrument
210
+ 6. **Phase 6**: Update all specs
211
+
212
+ ## Acceptance Criteria
213
+
214
+ - [x] `Instrument` class has `belongs_to :parent` relationship
215
+ - [x] Attributes resolve through parent chain
216
+ - [x] `Instrument#instrument_configurations` collects from ancestor chain
217
+ - [x] `InstrumentConfiguration` class exists with `name_key`
218
+ - [x] `InstrumentConfigurationOption` class exists with effect attributes
219
+ - [x] `Variant` class removed
220
+ - [x] YAML structure uses parent-based inheritance
221
+ - [x] `Instrument.get()` API preserved for backward compatibility
222
+ - [x] All existing tests pass
223
+ - [x] New tests cover inheritance and configuration
224
+ - [x] Maintains 90%+ test coverage
225
+
226
+ ## Deferred
227
+
228
+ - **Tunings**: String instruments need individual string modeling before alternate tunings can be represented
229
+ - **Notation**: Clef and transposition display conventions are orthogonal; see 002-notation-style.md
230
+
231
+ ## Open Questions
232
+
233
+ 1. **Multiple staves**: Piano needs two staves (grand staff). Is this an attribute on the instrument, or a notation concern?
234
+
235
+ 2. **Configuration selection in composition**: How does a Part specify which configuration options are selected? Options:
236
+ - Part stores selected options directly
237
+ - Part stores target attributes, system validates/suggests configurations
238
+ - Both (target attributes preferred, explicit selection as override)
@@ -0,0 +1,161 @@
1
+ # Move MusicalSymbol to Notation Module
2
+
3
+ AS a developer
4
+
5
+ I WANT MusicalSymbol in the HeadMusic::Notation module
6
+
7
+ SO THAT symbol representation logic (ASCII, Unicode, HTML) lives with other visual notation concerns rather than abstract music theory
8
+
9
+ ## Background
10
+
11
+ `MusicalSymbol` is a container class that holds multiple representations of a musical symbol:
12
+ - ASCII representation (plain text, e.g., "#" for sharp)
13
+ - Unicode representation (musical symbols, e.g., "♯" for sharp)
14
+ - HTML entity representation (e.g., "&sharp;" for sharp)
15
+
16
+ It's currently located in `HeadMusic::Rudiment`, but it's purely about visual presentation, not music theory. Symbols are how we visually represent musical concepts, making this a notation concern.
17
+
18
+ The class is used by:
19
+ - `Clef` - for clef symbols (treble, bass, alto, etc.)
20
+ - `Alteration` - for accidental symbols (sharp, flat, natural, etc.)
21
+
22
+ Moving it to Notation clarifies that this is about rendering and display, separate from the theoretical concepts themselves.
23
+
24
+ ## Scenario: Getting symbol representations
25
+
26
+ Given a sharp accidental
27
+
28
+ When I access its musical symbol
29
+
30
+ Then I can get the ASCII representation "#"
31
+
32
+ And I can get the Unicode representation "♯"
33
+
34
+ And I can get the HTML entity "&sharp;"
35
+
36
+ ## Scenario: Using symbols for text output
37
+
38
+ Given I need to display a clef in plain text
39
+
40
+ When I use the clef's MusicalSymbol
41
+
42
+ Then I get the appropriate ASCII character representation
43
+
44
+ ## Scenario: Using symbols for web display
45
+
46
+ Given I need to display an accidental on a web page
47
+
48
+ When I use the alteration's MusicalSymbol
49
+
50
+ Then I can choose between Unicode (for modern browsers) or HTML entity (for compatibility)
51
+
52
+ ## Technical Notes
53
+
54
+ ### Current State
55
+
56
+ **Location:** `lib/head_music/rudiment/musical_symbol.rb`
57
+ **Class:** `HeadMusic::Rudiment::MusicalSymbol`
58
+ **Tests:** `spec/head_music/rudiment/musical_symbol_spec.rb`
59
+ **Used by:**
60
+ - `lib/head_music/rudiment/clef.rb`
61
+ - `lib/head_music/rudiment/alteration.rb`
62
+
63
+ ### Proposed Changes
64
+
65
+ 1. **Move file:**
66
+ - From: `lib/head_music/rudiment/musical_symbol.rb`
67
+ - To: `lib/head_music/notation/musical_symbol.rb`
68
+
69
+ 2. **Update class definition:**
70
+ ```ruby
71
+ # lib/head_music/notation/musical_symbol.rb
72
+ module HeadMusic::Notation; end
73
+
74
+ class HeadMusic::Notation::MusicalSymbol
75
+ attr_reader :ascii, :unicode, :html_entity
76
+
77
+ def initialize(ascii: nil, unicode: nil, html_entity: nil)
78
+ @ascii = ascii
79
+ @unicode = unicode
80
+ @html_entity = html_entity
81
+ end
82
+
83
+ def to_s
84
+ unicode || ascii
85
+ end
86
+ end
87
+ ```
88
+
89
+ 3. **Move spec file:**
90
+ - From: `spec/head_music/rudiment/musical_symbol_spec.rb`
91
+ - To: `spec/head_music/notation/musical_symbol_spec.rb`
92
+
93
+ 4. **Update spec:**
94
+ ```ruby
95
+ describe HeadMusic::Notation::MusicalSymbol do
96
+ # All tests remain unchanged except the describe statement
97
+ ```
98
+
99
+ 5. **Update references in Clef:**
100
+ ```ruby
101
+ # lib/head_music/rudiment/clef.rb
102
+ # Update MusicalSymbol references to use HeadMusic::Notation::MusicalSymbol
103
+ ```
104
+
105
+ 6. **Update references in Alteration:**
106
+ ```ruby
107
+ # lib/head_music/rudiment/alteration.rb
108
+ # Update MusicalSymbol references to use HeadMusic::Notation::MusicalSymbol
109
+ ```
110
+
111
+ 7. **Update loading:**
112
+ ```ruby
113
+ # lib/head_music/notation.rb
114
+ module HeadMusic::Notation; end
115
+
116
+ require "head_music/notation/staff_position"
117
+ require "head_music/notation/musical_symbol"
118
+ ```
119
+
120
+ ### Files to Update
121
+
122
+ - Move: `lib/head_music/rudiment/musical_symbol.rb` → `lib/head_music/notation/musical_symbol.rb`
123
+ - Move: `spec/head_music/rudiment/musical_symbol_spec.rb` → `spec/head_music/notation/musical_symbol_spec.rb`
124
+ - Update: `lib/head_music/notation.rb` (add require)
125
+ - Update: `lib/head_music/rudiment/clef.rb` (update MusicalSymbol references)
126
+ - Update: `lib/head_music/rudiment/alteration.rb` (update MusicalSymbol references)
127
+ - Remove: `lib/head_music/rudiment.rb` require for musical_symbol
128
+
129
+ ## Acceptance Criteria
130
+
131
+ - [ ] `HeadMusic::Notation::MusicalSymbol` class exists
132
+ - [ ] Original file `lib/head_music/rudiment/musical_symbol.rb` removed
133
+ - [ ] Spec file at `spec/head_music/notation/musical_symbol_spec.rb`
134
+ - [ ] All existing MusicalSymbol tests pass
135
+ - [ ] `Clef` references to MusicalSymbol updated and working
136
+ - [ ] `Alteration` references to MusicalSymbol updated and working
137
+ - [ ] `lib/head_music/notation.rb` requires musical_symbol
138
+ - [ ] `lib/head_music/rudiment.rb` no longer requires musical_symbol
139
+ - [ ] All Clef tests pass
140
+ - [ ] All Alteration tests pass
141
+ - [ ] All existing tests across entire codebase still pass
142
+ - [ ] Maintains 90%+ test coverage
143
+ - [ ] No deprecation warnings or breaking changes for internal usage
144
+
145
+ ## Implementation Steps
146
+
147
+ 1. Create `lib/head_music/notation/musical_symbol.rb` with updated module path
148
+ 2. Copy class implementation unchanged
149
+ 3. Create `spec/head_music/notation/musical_symbol_spec.rb`
150
+ 4. Update describe statement in spec
151
+ 5. Update `lib/head_music/notation.rb` to require musical_symbol
152
+ 6. Update references in `lib/head_music/rudiment/clef.rb`
153
+ 7. Update references in `lib/head_music/rudiment/alteration.rb`
154
+ 8. Remove require from `lib/head_music/rudiment.rb`
155
+ 9. Run tests: `bundle exec rspec spec/head_music/notation/musical_symbol_spec.rb`
156
+ 10. Run tests: `bundle exec rspec spec/head_music/rudiment/clef_spec.rb`
157
+ 11. Run tests: `bundle exec rspec spec/head_music/rudiment/alteration_spec.rb`
158
+ 12. Run full test suite: `bundle exec rspec`
159
+ 13. Run linter: `bundle exec rubocop -a`
160
+ 14. Delete original files after verifying everything works
161
+ 15. Verify 90%+ coverage maintained