feat: SDD workflow — test sync, song generation + validation, ReaScript hybrid pipeline

- compose-test-sync: fix 3 failing tests (NOTE_TO_MIDI, DrumLoopAnalyzer mock, section name)
- generate-song: CLI wrapper + RPP validator (6 structural checks) + 4 e2e tests
- reascript-hybrid: ReaScriptGenerator + command protocol + CLI + 16 unit tests
- 110/110 tests passing
- Full SDD cycle (propose→spec→design→tasks→apply→verify) for all 3 changes
This commit is contained in:
renato97
2026-05-03 22:00:26 -03:00
parent 7729d5f12f
commit 48bc271afc
25 changed files with 2842 additions and 343 deletions

View File

@@ -48,8 +48,7 @@ def _mock_main(tmp_path, extra_args=None):
output = tmp_path / "track.rpp"
fake_analysis = _fake_analysis()
with patch("scripts.compose.SampleSelector") as mock_cls, \
patch("scripts.compose.DrumLoopAnalyzer") as mock_analyzer_cls:
with patch("scripts.compose.SampleSelector") as mock_cls:
mock_sel = MagicMock()
mock_sel._samples = [
{
@@ -94,10 +93,6 @@ def _mock_main(tmp_path, extra_args=None):
]
mock_cls.return_value = mock_sel
mock_analyzer = MagicMock()
mock_analyzer.analyze.return_value = fake_analysis
mock_analyzer_cls.return_value = mock_analyzer
from scripts.compose import main
original_argv = sys.argv
try:
@@ -174,7 +169,7 @@ class TestDrumloopFirstTracks:
track = build_clap_track(mock_selector, sections, offsets)
positions = [c.position for c in track.clips]
assert 1.0 in positions, "Clap on beat 2 (pos 1.0)"
assert 2.0 in positions, "Clap on beat 2 (backbeat)"
assert 3.5 in positions, "Clap on beat 3.5 (dembow)"
def test_bass_uses_kick_free_zones(self):
@@ -185,9 +180,9 @@ class TestDrumloopFirstTracks:
sections = [SectionDef(name="verse", bars=4, energy=1.0)]
offsets = [0.0]
track = build_bass_track(analysis, sections, offsets, "A", True)
track = build_bass_track(sections, offsets, "A", True)
assert len(track.clips) > 0, "Bass should have clips"
assert all(n.duration == 0.5 for n in track.clips[0].midi_notes), "Bass notes should be 0.5 beats"
assert all(n.duration == 1.5 for n in track.clips[0].midi_notes), "Bass notes should be 1.5 beats (808 pattern)"
def test_chords_change_on_downbeats(self):
from scripts.compose import build_chords_track
@@ -197,7 +192,7 @@ class TestDrumloopFirstTracks:
sections = [SectionDef(name="verse", bars=8, energy=1.0)]
offsets = [0.0]
track = build_chords_track(analysis, sections, offsets, "A", True)
track = build_chords_track(sections, offsets, "A", True)
starts = sorted(set(n.start for n in track.clips[0].midi_notes))
for s in starts:
assert s % 4.0 == 0.0, f"Chord change at beat {s} — should be on downbeat"
@@ -206,11 +201,10 @@ class TestDrumloopFirstTracks:
from scripts.compose import build_melody_track
from src.core.schema import SectionDef
analysis = _fake_analysis()
sections = [SectionDef(name="verse", bars=4, energy=1.0)]
sections = [SectionDef(name="chorus", bars=4, energy=1.0)]
offsets = [0.0]
track = build_melody_track(analysis, sections, offsets, "A", True, seed=42)
track = build_melody_track(sections, offsets, "A", True, seed=42)
assert len(track.clips) > 0, "Melody should have clips"
pitches = {n.pitch for n in track.clips[0].midi_notes}
assert len(pitches) > 1, "Melody should use multiple notes"