From c736f8d353918aa45b5f700b87c913a9667073fa Mon Sep 17 00:00:00 2001 From: AmanTahiliani Date: Sun, 30 Aug 2026 15:38:29 -0400 Subject: [PATCH] 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 --- internal/chapters/chapters.go | 38 ++++++++++++++++++++++++++---- internal/chapters/chapters_test.go | 36 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/internal/chapters/chapters.go b/internal/chapters/chapters.go index b7e50de..315c8a2 100644 --- a/internal/chapters/chapters.go +++ b/internal/chapters/chapters.go @@ -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 } diff --git a/internal/chapters/chapters_test.go b/internal/chapters/chapters_test.go index fb58637..acd6b20 100644 --- a/internal/chapters/chapters_test.go +++ b/internal/chapters/chapters_test.go @@ -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},