fix: make weekend context transitions truthful

This commit is contained in:
2026-07-12 18:15:14 -04:00
parent adfa934f9d
commit 0b9c3fff0a
2 changed files with 99 additions and 26 deletions

View File

@@ -136,21 +136,6 @@ func (s *Service) ResolveWeekendContext(evidence LiveEvidence) (WeekendContext,
champMeetings := championshipMeetings(meetings, byMeeting) champMeetings := championshipMeetings(meetings, byMeeting)
out.TotalChampionshipRounds = len(champMeetings) out.TotalChampionshipRounds = len(champMeetings)
var previous, next, defaultAnalysis *contextCandidate
for i := range candidates {
c := &candidates[i]
completionEligible := c.start.IsZero() || !c.start.After(now) || c.archived
if c.complete && completionEligible && (previous == nil || candidateTime(*c).After(candidateTime(*previous))) {
previous = c
}
if c.complete && hasMeaningfulAnalysis(c.counts) && (c.start.IsZero() || !c.start.After(now)) && (defaultAnalysis == nil || candidateTime(*c).After(candidateTime(*defaultAnalysis))) {
defaultAnalysis = c
}
if !c.start.IsZero() && !c.start.Before(now) && (next == nil || c.start.Before(next.start)) {
next = c
}
}
var active *contextCandidate var active *contextCandidate
if evidence.Active { if evidence.Active {
for i := range candidates { for i := range candidates {
@@ -162,14 +147,21 @@ func (s *Service) ResolveWeekendContext(evidence LiveEvidence) (WeekendContext,
if active == nil { if active == nil {
active = syntheticLiveCandidate(evidence, now) active = syntheticLiveCandidate(evidence, now)
} }
if next != nil && active.session.SessionKey != 0 && next.session.SessionKey == active.session.SessionKey { }
next = nil
for i := range candidates { var previous, next, defaultAnalysis *contextCandidate
c := &candidates[i] for i := range candidates {
if c.session.SessionKey != active.session.SessionKey && c.start.After(now) && (next == nil || c.start.Before(next.start)) { c := &candidates[i]
next = c isActive := active != nil && active.session.SessionKey != 0 && c.session.SessionKey == active.session.SessionKey
} completionEligible := c.start.IsZero() || !c.start.After(now) || c.archived
} if !isActive && c.complete && completionEligible && (previous == nil || candidateTime(*c).After(candidateTime(*previous))) {
previous = c
}
if !isActive && c.complete && hasMeaningfulAnalysis(c.counts) && (c.start.IsZero() || !c.start.After(now)) && (defaultAnalysis == nil || candidateTime(*c).After(candidateTime(*defaultAnalysis))) {
defaultAnalysis = c
}
if !isActive && !c.start.IsZero() && !c.start.Before(now) && (next == nil || c.start.Before(next.start)) {
next = c
} }
} }
@@ -190,7 +182,7 @@ func (s *Service) ResolveWeekendContext(evidence LiveEvidence) (WeekendContext,
out.TemporalState = TemporalSessionLive out.TemporalState = TemporalSessionLive
} else { } else {
out.FocusMeeting = chooseFocusMeeting(meetings, previous, next) out.FocusMeeting = chooseFocusMeeting(meetings, previous, next)
out.TemporalState = classifyTemporalState(now, previous, next, candidates, champMeetings) out.TemporalState = classifyTemporalState(now, previous, next, candidates, champMeetings, championshipScheduleUnknown(champMeetings, byMeeting))
} }
if out.FocusMeeting != nil { if out.FocusMeeting != nil {
out.ChampionshipRound = championshipRound(champMeetings, int(out.FocusMeeting.MeetingKey)) out.ChampionshipRound = championshipRound(champMeetings, int(out.FocusMeeting.MeetingKey))
@@ -347,13 +339,18 @@ func chooseFocusMeeting(meetings []store.Meeting, previous, next *contextCandida
return nil return nil
} }
func classifyTemporalState(now time.Time, previous, next *contextCandidate, candidates []contextCandidate, championship []store.Meeting) TemporalState { func classifyTemporalState(now time.Time, previous, next *contextCandidate, candidates []contextCandidate, championship []store.Meeting, scheduleUnknown bool) TemporalState {
var latestStarted *contextCandidate var latestStarted *contextCandidate
for i := range candidates { for i := range candidates {
if !candidates[i].start.IsZero() && !candidates[i].start.After(now) && (latestStarted == nil || candidates[i].start.After(latestStarted.start)) { if !candidates[i].start.IsZero() && !candidates[i].start.After(now) && (latestStarted == nil || candidates[i].start.After(latestStarted.start)) {
latestStarted = &candidates[i] latestStarted = &candidates[i]
} }
} }
// Once a new meeting enters its preparation window, an ingest gap from an
// older meeting must not keep the product stuck in settling.
if next != nil && next.start.Sub(now) <= preSessionWindow && (latestStarted == nil || latestStarted.meeting.MeetingKey != next.meeting.MeetingKey) {
return TemporalPreSession
}
if latestStarted != nil && !latestStarted.complete && !latestStarted.end.IsZero() { if latestStarted != nil && !latestStarted.complete && !latestStarted.end.IsZero() {
if now.Before(latestStarted.end) { if now.Before(latestStarted.end) {
return TemporalPreSession return TemporalPreSession
@@ -369,12 +366,25 @@ func classifyTemporalState(now time.Time, previous, next *contextCandidate, cand
if next != nil && next.start.Sub(now) <= preSessionWindow { if next != nil && next.start.Sub(now) <= preSessionWindow {
return TemporalPreSession return TemporalPreSession
} }
if next == nil && len(championship) > 0 { if next == nil && len(championship) > 0 && !scheduleUnknown {
return TemporalSeasonComplete return TemporalSeasonComplete
} }
return TemporalBetweenWeekends return TemporalBetweenWeekends
} }
func championshipScheduleUnknown(meetings []store.Meeting, sessions map[int][]store.Session) bool {
for _, meeting := range meetings {
for _, session := range sessions[meeting.MeetingKey] {
if !session.IsCancelled && isChampionshipRace(session) {
if _, ok := parseContextTime(session.DateStart); !ok {
return true
}
}
}
}
return false
}
func meetingFinalSession(previous contextCandidate, candidates []contextCandidate) bool { func meetingFinalSession(previous contextCandidate, candidates []contextCandidate) bool {
latest := previous.start latest := previous.start
for _, c := range candidates { for _, c := range candidates {

View File

@@ -256,3 +256,66 @@ func TestResolveWeekendContextPartialFutureSchedule(t *testing.T) {
t.Fatalf("partial display range = %+v", got.FocusMeeting) t.Fatalf("partial display range = %+v", got.FocusMeeting)
} }
} }
func TestResolveWeekendContextActiveSessionIsNotCompletedOrDefault(t *testing.T) {
now, _ := time.Parse(time.RFC3339, "2026-07-05T15:00:00Z")
svc := contextService(t, now)
addContextMeeting(t, svc, 1, "British Grand Prix", "2026-07-03T09:00:00Z", "2026-07-05T16:00:00Z", false)
addContextSession(t, svc, 10, 1, "Qualifying", "2026-07-04T14:00:00Z", "2026-07-04T15:00:00Z", false)
completeContextSession(t, svc, 10, 1)
addContextSession(t, svc, 11, 1, "Race", "2026-07-05T14:00:00Z", "2026-07-05T16:00:00Z", false)
completeContextSession(t, svc, 11, 1)
got, err := svc.ResolveWeekendContext(LiveEvidence{Active: true, MeetingName: "British Grand Prix", CircuitName: "British Grand Prix", SessionName: "Race", SessionType: "Race", ObservedAt: now})
if err != nil {
t.Fatal(err)
}
if got.ActiveSession == nil || got.ActiveSession.Session.SessionKey != 11 {
t.Fatalf("active = %+v", got.ActiveSession)
}
if got.PreviousCompletedSession == nil || got.PreviousCompletedSession.Session.SessionKey != 10 {
t.Fatalf("previous = %+v, want earlier completed session", got.PreviousCompletedSession)
}
if got.DefaultAnalysisSession == nil || got.DefaultAnalysisSession.Session.SessionKey != 10 {
t.Fatalf("default = %+v, want earlier completed session", got.DefaultAnalysisSession)
}
}
func TestResolveWeekendContextOldIncompleteSessionDoesNotSuppressNextWeekend(t *testing.T) {
now, _ := time.Parse(time.RFC3339, "2026-07-16T12:00:00Z")
svc := contextService(t, now)
addContextMeeting(t, svc, 1, "British Grand Prix", "2026-07-03T09:00:00Z", "2026-07-05T16:00:00Z", false)
addContextSession(t, svc, 11, 1, "Race", "2026-07-05T14:00:00Z", "2026-07-05T16:00:00Z", false)
addContextMeeting(t, svc, 2, "Belgian Grand Prix", "2026-07-17T09:00:00Z", "2026-07-19T16:00:00Z", false)
addContextSession(t, svc, 21, 2, "Practice 1", "2026-07-17T09:00:00Z", "2026-07-17T10:00:00Z", false)
addContextSession(t, svc, 22, 2, "Race", "2026-07-19T14:00:00Z", "2026-07-19T16:00:00Z", false)
got, err := svc.ResolveWeekendContext(LiveEvidence{})
if err != nil {
t.Fatal(err)
}
if got.TemporalState != TemporalPreSession {
t.Fatalf("state = %s, want %s; context=%+v", got.TemporalState, TemporalPreSession, got)
}
if got.FocusMeeting == nil || got.FocusMeeting.MeetingKey != 2 {
t.Fatalf("focus = %+v, want Belgian weekend", got.FocusMeeting)
}
}
func TestResolveWeekendContextMissingScheduleDoesNotClaimSeasonComplete(t *testing.T) {
now, _ := time.Parse(time.RFC3339, "2026-07-01T12:00:00Z")
svc := contextService(t, now)
addContextMeeting(t, svc, 1, "British Grand Prix", "", "", false)
addContextSession(t, svc, 11, 1, "Race", "", "", false)
got, err := svc.ResolveWeekendContext(LiveEvidence{})
if err != nil {
t.Fatal(err)
}
if got.TemporalState != TemporalBetweenWeekends {
t.Fatalf("state = %s, want limited %s context", got.TemporalState, TemporalBetweenWeekends)
}
if got.TotalChampionshipRounds != 1 {
t.Fatalf("total rounds = %d, want scheduled round retained", got.TotalChampionshipRounds)
}
}