What's wrong
In the chart format, each whole-token / extends the current chord by one beat (design spec docs/superpowers/specs/2026-07-02-music-type-safe-factories-design.md §Progression). Progression.TryReadChordEvents (Semantics.Music/Progression.Parse.cs:117-125) increments currentBeats for every /. But when the chord cell carried an explicit @n/d duration, FlushChord (Progression.Parse.cs:169) uses explicitDuration ?? Duration.Create(currentBeats, beatUnit), so every slash after it is counted and then thrown away. The input is accepted and its timing is changed without any error.
Reproduction
Progression.Parse("4/4 C@1/2 / / | G").ToString() -> "4/4 C / G"
The input puts G on the downbeat of bar 2. The parsed progression puts it on beat 3 of bar 1, and TotalDuration is 3/4 rather than the 5/4 the text reads as. The same path is reached through Section.Parse and Arrangement.Parse, which parse their chart lines with Progression.TryParse.
Related to this: the spec says Parse "treats | as validation", but the loop skips | tokens unconditionally (Progression.Parse.cs:111-114). A bar line in the wrong place is therefore never caught, and this timing shift goes unnoticed as well.
Suggested fix / acceptance criteria
- Reject a
/ token that follows a Chord@n/d cell, so TryParse returns false and Parse throws FormatException. The alternative is to define and document additive semantics (explicit duration plus N beats). Rejecting is the smaller change and keeps ToString()→Parse total, because ToString never emits a slash after an @ cell.
- Optionally, as the spec describes, validate that each
| lands on a bar boundary for whole-beat content.
- Tests:
"4/4 C@1/2 / | G" is rejected (or parses to the documented duration), and the existing ProgressionRoundTripTests still pass.
What's wrong
In the chart format, each whole-token
/extends the current chord by one beat (design specdocs/superpowers/specs/2026-07-02-music-type-safe-factories-design.md§Progression).Progression.TryReadChordEvents(Semantics.Music/Progression.Parse.cs:117-125) incrementscurrentBeatsfor every/. But when the chord cell carried an explicit@n/dduration,FlushChord(Progression.Parse.cs:169) usesexplicitDuration ?? Duration.Create(currentBeats, beatUnit), so every slash after it is counted and then thrown away. The input is accepted and its timing is changed without any error.Reproduction
The input puts G on the downbeat of bar 2. The parsed progression puts it on beat 3 of bar 1, and
TotalDurationis 3/4 rather than the 5/4 the text reads as. The same path is reached throughSection.ParseandArrangement.Parse, which parse their chart lines withProgression.TryParse.Related to this: the spec says
Parse"treats|as validation", but the loop skips|tokens unconditionally (Progression.Parse.cs:111-114). A bar line in the wrong place is therefore never caught, and this timing shift goes unnoticed as well.Suggested fix / acceptance criteria
/token that follows aChord@n/dcell, soTryParsereturns false andParsethrowsFormatException. The alternative is to define and document additive semantics (explicit duration plus N beats). Rejecting is the smaller change and keepsToString()→Parsetotal, becauseToStringnever emits a slash after an@cell.|lands on a bar boundary for whole-beat content."4/4 C@1/2 / | G"is rejected (or parses to the documented duration), and the existingProgressionRoundTripTestsstill pass.