Skip to content

fix: rogue "s" next to roman numeral harmony labels - #596

Closed
FelipeDefensor wants to merge 2 commits into
0.6.5from
fix/harmony-roman-numeral-rogue-s
Closed

fix: rogue "s" next to roman numeral harmony labels#596
FelipeDefensor wants to merge 2 commits into
0.6.5from
fix/harmony-roman-numeral-rogue-s

Conversation

@FelipeDefensor

Copy link
Copy Markdown
Collaborator

Harmony labels in roman-numeral display mode carry a stray s inside the numeral: a first-inversion triad reads Is6 instead of I6, a third-inversion dominant seventh Vss42 instead of V42.

Reported against 0.6.4 while importing a harmony CSV; this branch is off v0.6.4.

Cause

_figs_to_str emits "s" for every figured-bass slot without an accidental:

prefix = "%" if len(figures) > 2 else ""
accs = "".join(_fmt_mod(mod, "s") for _, mod in figures)

That placeholder is only consumed by MusAnalysis inside the three-figure stack introduced by % — the encoding is positional, so each of the three accidental slots needs a filler (%sbs765). The one- and two-figure forms have no blank slot, so the character reaches the glyph run and is drawn as a literal letter.

Verified against the font: Is6 really renders I + s + superscript 6. Two-figure forms accept accidentals positionally without any filler (#b65 draws ♯6 over ♭5), so dropping the placeholder outside the stack is the whole fix.

Fix

Emit the placeholder only in the stacked form. The % path is unchanged — V%sss432 is identical before and after.

Repro bundle

The fixture commit sits before the fix, so "before" is one checkout back.

One-time setup (re-running can discard local edits — it aborts when the head branch is checked out in another worktree, and --force resets the branch):

gh pr checkout <this PR>

After (branch tip):

uv run --python 3.12 tilia "$PWD/repro/harmony-roman-s.tla"

Before:

git checkout HEAD~1
uv run --python 3.12 tilia "$PWD/repro/harmony-roman-s.tla"

Back to the tip:

git checkout -

Acceptance criteria

  • Do: look at the Harmony timeline — nine harmonies at 0–80 s, all in roman-numeral display mode.
  • Was: stray s inside the numerals — Is6, Iss64, Vss65, Vss43, Vss42, Vss65/V, iiss65.
  • Should be: I6, I64, V65, V43, V42, V65/V, ii65. The I at 0 s and the stacked V%sss432 at 80 s are unchanged controls.

Both states were checked by actually checking out the two commits and opening that file, not inferred from the diff.

Fixture lifecycle

repro/harmony-roman-s.tla and its generator tests/repro_harmony_roman_s.py are throwaway — delete both before merge. The generator is not auto-collected (no test_ prefix); run it with uv run --python 3.12 pytest tests/repro_harmony_roman_s.py -q. Harmony is outside the CLI's timelines add / components coverage, so it builds state through commands.execute(...) and saves via file.save, as CLAUDE.md prescribes.

The durable artifact is TestRomanNumeralDisplay::test_roman_label_has_no_blank_accidental_placeholder, which pins I6 / I64 / ii7 / ii65 / V65 / V43 / V42, plus a companion test pinning V%sss432 so the stacked form stays covered.

Out of scope, found while investigating

  • Applied chords carry figure accidentals: viio65/ii renders viio#b65/II. The numeral is spelled against the applied key but _chord_fb computes the figures against the home key.
  • A two-figure group whose lower figure alone is chromatic cannot be expressed — MusAnalysis fills accidentals from the top and has no blank slot outside %. Unchanged by this PR (before it, that case also printed the literal s).
  • mode._get_music21_object_from_text returns None when the text does not start with a note name, so a key row with an invalid symbol raises TypeError: cannot unpack non-iterable NoneType object and aborts the whole CSV import instead of reporting one error.

Fixture and its generator, per the repro-bundle guideline in #580.
Harmony is outside the CLI's `timelines add` / `components` coverage, so
the state is built through `commands.execute(...)` and saved with
`file.save`.

The fixture reproduces on v0.6.4: the same file shows Is6 / Iss64 /
Vss65 / Vss43 / Vss42 / Vss65/V / iiss65 without the fix, and I6 / I64 /
V65 / V43 / V42 / V65/V / ii65 with it. The root-position I and the
stacked V%sss432 are unchanged either way, as controls.

Both files are throwaway: delete before merge. The durable artifact is
TestRomanNumeralDisplay::test_roman_label_has_no_blank_accidental_placeholder.
_figs_to_str emitted "s" for every figured-bass slot without an
accidental. That placeholder is only consumed by MusAnalysis inside the
three-figure stack introduced by "%"; there is no blank slot in the one-
and two-figure forms, so the character was drawn literally: "Is6" for a
first-inversion triad, "Vss42" for a third-inversion dominant seventh.

Emit the placeholder only in the stacked form.
@FelipeDefensor

Copy link
Copy Markdown
Collaborator Author

Superseded by the combined 0.6.5 release branch, which carries this fix plus three changes that came out of review:

  • the % stack is now taken only for its exact shape (three figures, single-character accidental slots), so the literal s no longer survives for four-figure groups or double accidentals — over a 10500-label sweep, malformed stacks 1728 → 0 and stray s 988 → 0;
  • the regression tests gained accidental-bearing cases, which the original parametrize had none of;
  • the throwaway repro bundle was removed, as the commit message intended.

The remaining defect in this area — accidentals being emitted as a block before the numbers, so they can be drawn against the wrong figure — is pre-existing at v0.6.4 and tracked separately in #598.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant