Skip to content

fix: write single-note spanner starts before their stops - #431

Merged
webern merged 3 commits into
mainfrom
claude/429-rate-limit-fix-t9kkq4
Aug 28, 2026
Merged

fix: write single-note spanner starts before their stops#431
webern merged 3 commits into
mainfrom
claude/429-rate-limit-fix-t9kkq4

Conversation

@webern

@webern webern commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Human Summary

When a spanner starts and stops on the same note, the start needs to be emitted before the stop, but mx had not considered single note spanners.

This looks like a solid fix for, at least the known and cited cases from #429. I will not close the issue so we can track future cases of out of order spanner start and stop problems.

The test file submitted in the issue has been added to the corpus and passes round trip now.

Summary

NotationsWriter wrote every spanner stop before every start. That is right for a note that chains two spanners (one ends where the next begins, #139), but backwards for a spanner contained in one note: a nested tuplet covering exactly one note, or a glissando or slide drawn from a note toward a rest, came out stop-then-start, so a reader saw a stop for a span that was never open followed by a start that never closes.

Tuplets are decided locally in NotationsWriter: matching numberLevels on one note always mean a single-note tuplet (two different tuplets cannot share a note, because a note carries only one time-modification), so each start is followed by its same-note stop and the remaining stops come last. The nested case now comes out as Finale writes it: start 2, stop 2, stop 1.

Glissando, slide, and wavy line cannot be decided locally: a chained note can carry the same number as a single-note span, and only the stream of earlier events tells them apart. The SpannerResolver's existing serialized walk now also records same-note pairs (a stop that closes nothing open pairs with a same-bucket start on its own note) and exposes them through sameNoteSpanPartner; the writer holds those stops back until right after their starts. Chained spanners keep the stop-before-start order of #139, covered by the existing GlissandoApiTest chain test.

The reporter's fixtures from the issue are added to the corpus as data/rpatters1/gliss_to_rest-ref.xml and tuplet_singletons-ref.xml (Finale's exports, both with the start-then-stop shape). The gliss fixture now survives the strict api roundtrip and is pinned in roundtrip-baseline.txt. The tuplet fixture is not pinned: its ordering is fixed, but the writer restates tuplet-actual/tuplet-normal on a start whose source omits them, which is a separate fidelity gap.

Testing

  • New *_SingleNoteSpanner tests fail before the fix (7 of 8 cases, all start/stop inversions), pass after (36 assertions in 8 test cases)
  • Existing chain test (stop before start on a chained note, same explicit number) still passes
  • Full api suite passes (6411 assertions in 593 test cases) and all three examples run
  • api-roundtrip regression gate passes, 409 of 409 pinned including the newly pinned gliss_to_rest-ref.xml
  • core roundtrip passes with the corpus additions (840 files, pinned count updated)

References

webern added 3 commits August 26, 2026 23:33
tuplet_singletons-ref.xml and gliss_to_rest-ref.xml, attached by
rpatters1 to issue #429: Finale's exports of a bar of two single-note
tuplets and of a glissando and a slide drawn to a rest. Both place a
spanner's start and stop on one note in start-then-stop order -- the
shape NotationsWriter currently inverts. Pinned corert count 838 -> 840;
audit artifacts regenerated.
NotationsWriter wrote every spanner stop before every start, which is
right for a note that chains two spanners but backwards for a spanner
contained in one note: a nested tuplet covering exactly one note, or a
glissando or slide drawn to a rest, came out stop-then-start, closing a
span that was never open (#429).

Tuplets are decided locally: matching numberLevels on one note always
mean a single-note tuplet, so each start is followed by its same-note
stop and the remaining stops come last. Glissando, slide, and wavy line
cannot be decided locally -- a chained note can carry the same number as
a single-note span -- so the SpannerResolver's serialized walk now
records the pairs (a stop that closes nothing open pairs with a
same-bucket start on its own note) and the writer holds those stops
back until right after their starts. Chained spanners keep the
stop-before-start order of #139.
The #429 fixture passes the strict read -> write -> read comparison now
that single-note spanners are written start-first, so it defends the
ordering. tuplet_singletons-ref.xml is not pinned: its ordering is fixed
too, but the writer restates tuplet-actual/tuplet-normal on a start
whose source omits them, which is a separate fidelity gap.
@webern webern added bug software defect non-breaking fixes or implementation that do not require breaking changes impl Affects the mx::impl layer ai Issues opened by, or through, a coding agent. labels Aug 27, 2026 — with Claude
@webern
webern merged commit 03df12e into main Aug 28, 2026
8 checks passed
@webern
webern deleted the claude/429-rate-limit-fix-t9kkq4 branch August 28, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Issues opened by, or through, a coding agent. bug software defect impl Affects the mx::impl layer non-breaking fixes or implementation that do not require breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant