fix(chapters): sync race-story titles after conflict trim

resolveConflicts already clips a still-open safety car so finish can
keep the last laps, but Title and EndTime stayed on the pre-trim
range. Rebuild range-bearing titles from the final laps and point
EndTime at the trimmed lap-index end.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 15:38:29 -04:00
parent 21b29bec6a
commit c736f8d353
2 changed files with 69 additions and 5 deletions

View File

@@ -72,7 +72,7 @@ func Detect(rc []RaceControl, positions []PositionSample, laps []Lap, totalLaps
chapters = append(chapters, detectDecisiveSwings(positions, lapIndex, totalLaps)...)
chapters = append(chapters, detectFinish(rc, lapIndex, totalLaps))
return resolveConflicts(chapters)
return resolveConflicts(chapters, lapIndex)
}
func normalizeTotalLaps(totalLaps int, laps []Lap, rc []RaceControl) int {
@@ -321,7 +321,7 @@ func detectPitPhases(laps []Lap, idx lapIndex) []Chapter {
}
ch := Chapter{
Kind: KindPitPhase,
Title: fmt.Sprintf("Pit phase (L%d-L%d)", start, end),
Title: pitPhaseTitle(start, end),
StartLap: start,
EndLap: end,
StartTime: idx.lapStart(start),
@@ -339,7 +339,7 @@ func detectPitPhases(laps []Lap, idx lapIndex) []Chapter {
drivers[driver] = true
}
last.DriverNumbers = sortedDriverNumbers(drivers)
last.Title = fmt.Sprintf("Pit phase (L%d-L%d)", last.StartLap, last.EndLap)
last.Title = pitPhaseTitle(last.StartLap, last.EndLap)
continue
}
windows = append(windows, ch)
@@ -479,7 +479,7 @@ func detectFinish(rc []RaceControl, idx lapIndex, totalLaps int) Chapter {
}
return Chapter{
Kind: KindFinish,
Title: fmt.Sprintf("Finish (L%d-L%d)", startLap, finishLap),
Title: finishTitle(startLap, finishLap),
StartLap: startLap,
EndLap: finishLap,
StartTime: idx.lapStart(startLap),
@@ -487,7 +487,31 @@ func detectFinish(rc []RaceControl, idx lapIndex, totalLaps int) Chapter {
}
}
func resolveConflicts(chapters []Chapter) []Chapter {
func pitPhaseTitle(startLap, endLap int) string {
return fmt.Sprintf("Pit phase (L%d-L%d)", startLap, endLap)
}
func finishTitle(startLap, endLap int) string {
return fmt.Sprintf("Finish (L%d-L%d)", startLap, endLap)
}
// titleForResolved rebuilds range-bearing titles from the final StartLap/EndLap
// after conflict resolution. Flag and pit-phase formatters are reused so stale
// pre-trim strings are never parsed.
func titleForResolved(ch Chapter) string {
switch ch.Kind {
case KindSafetyCar, KindVirtualSafetyCar, KindRedFlag:
return flagTitle(ch.Kind, ch.StartLap, ch.EndLap)
case KindPitPhase:
return pitPhaseTitle(ch.StartLap, ch.EndLap)
case KindFinish:
return finishTitle(ch.StartLap, ch.EndLap)
default:
return ch.Title
}
}
func resolveConflicts(chapters []Chapter, idx lapIndex) []Chapter {
normalized := make([]Chapter, 0, len(chapters))
for _, ch := range chapters {
if ch.StartLap <= 0 {
@@ -528,6 +552,7 @@ func resolveConflicts(chapters []Chapter) []Chapter {
if priority(ch.Kind) > priority(last.Kind) {
if last.StartLap < ch.StartLap {
last.EndLap = ch.StartLap - 1
last.EndTime = idx.lapEnd(last.EndLap)
out = append(out, ch)
} else {
*last = ch
@@ -541,6 +566,9 @@ func resolveConflicts(chapters []Chapter) []Chapter {
}
}
}
for i := range out {
out[i].Title = titleForResolved(out[i])
}
return out
}

View File

@@ -125,6 +125,42 @@ func TestDetectDecisiveSwingPersistsToFinish(t *testing.T) {
}
}
func TestDetectSafetyCarTrimmedForFinishRewritesTitle(t *testing.T) {
rcs := []RaceControl{
rc(3, models.CategorySafetyCar, "", "SAFETY CAR DEPLOYED"),
rc(72, models.CategoryFlag, models.FlagChequered, "CHEQUERED FLAG"),
}
got := Detect(rcs, nil, testLaps(72, nil), 72)
sc := findKind(got, KindSafetyCar)
if sc == nil {
t.Fatalf("chapters = %+v, want safety car", got)
}
if sc.StartLap != 3 || sc.EndLap != 70 || sc.Title != "Safety Car (L3-L70)" {
t.Fatalf("safety car = %+v, want StartLap=3 EndLap=70 Title=%q", *sc, "Safety Car (L3-L70)")
}
if wantEnd := lapTime(71); sc.EndTime != wantEnd {
t.Fatalf("safety car EndTime = %q, want lap-index end of L70 (%q)", sc.EndTime, wantEnd)
}
fin := findKind(got, KindFinish)
if fin == nil || fin.StartLap != 71 || fin.EndLap != 72 {
t.Fatalf("finish = %+v, want L71-L72", got)
}
for i := 1; i < len(got); i++ {
if got[i].StartLap <= got[i-1].EndLap {
t.Fatalf("chapters overlap at %d: %+v then %+v", i, got[i-1], got[i])
}
}
headed := ApplyHeadlines(got, nil, rcs, 0)
if scH := findKind(headed, KindSafetyCar); scH == nil || scH.Headline != "Safety Car (L3-L70)" {
t.Fatalf("headline = %+v, want fallback to resolved title Safety Car (L3-L70)", headed)
}
}
func TestDetectFlagPeriodsWinOverConflictingChapters(t *testing.T) {
pitOuts := map[int][]int{
12: {1, 2},