fix(song): sanitize caged + guideTones on emit, not just decode (#544 follow-up) (#547)

Post-merge Codex review of #544 found chord_template_to_wire emitted ct.caged and
ct.guide_tones raw — so a directly-constructed ChordTemplate(caged="X") or
guide_tones=[99] would write a schema-invalid value to the feedpak wire, even
though the decoder guards on input. The spec constrains caged to C/A/G/E/D and
guideTones to 0..11.

Run the same _sanitize_caged / _sanitize_guide_tones guards on emit: caged is
written only when a valid enum value, guideTones only as the in-range ints (empty
result -> key omitted). +1 test (invalid caged dropped, mixed guideTones filtered to
the valid in-range subset, wholly-invalid list omitted).

Codex-reviewed: clean. 154 song tests pass.

Part of got-feedback/feedback#334.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos 2026-06-21 12:04:08 +02:00 committed by GitHub
parent e518910baa
commit 73e3fe2226
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 23 additions and 4 deletions

View File

@ -328,10 +328,15 @@ def chord_template_to_wire(ct: ChordTemplate) -> dict:
if ct.voicing:
out["voicing"] = ct.voicing
# CAGED shape + guide tones (§6.6) — default-omitted, mirroring voicing.
if ct.caged:
out["caged"] = ct.caged
if ct.guide_tones:
out["guideTones"] = list(ct.guide_tones)
# Sanitize on EMIT too (not just on decode): a directly-constructed template
# must not be able to write a non-enum `caged` or an out-of-range `guideTone`
# to the wire (the spec constrains caged to C/A/G/E/D and guideTones to 0..11).
_caged = _sanitize_caged(ct.caged)
if _caged:
out["caged"] = _caged
_guide_tones = _sanitize_guide_tones(ct.guide_tones)
if _guide_tones:
out["guideTones"] = _guide_tones
return out

View File

@ -514,6 +514,20 @@ def test_template_caged_tolerates_malformed(bad):
assert arr.chord_templates[0].caged == ""
def test_template_caged_guide_tones_sanitized_on_emit():
"""A directly-constructed template can't write an invalid caged / out-of-range
guideTone to the wire the emitter sanitizes, not just the decoder."""
ct = ChordTemplate(name="Am", fingers=[-1] * 6, frets=[-1] * 6,
caged="X", guide_tones=[3, 99, -1, "x", True])
wire = chord_template_to_wire(ct)
assert "caged" not in wire # non-enum dropped, not emitted
assert wire["guideTones"] == [3] # only the valid in-range int survives
# A wholly-invalid guideTones list omits the key entirely.
ct2 = ChordTemplate(name="Am", fingers=[-1] * 6, frets=[-1] * 6,
guide_tones=[42, "nope"])
assert "guideTones" not in chord_template_to_wire(ct2)
def test_template_guide_tones_round_trip():
"""A non-empty guideTones list survives the template wire + round-trip."""
ct = ChordTemplate(name="G7", display_name="G7", fingers=[3, 2, 0, 0, 0, 1],