feat: rank growth events with grounded evidence

This commit is contained in:
2026-08-22 18:44:59 +08:00
parent 7f9c16be4b
commit 1a8a61cfe7
20 changed files with 2555 additions and 132 deletions

View File

@@ -73,6 +73,57 @@ func AnalyzeEventEffect(history []DailyPoint, event Event, catalog []Event) Even
return effect
}
// MostRecentExactOccurrence returns only a completed occurrence with the same
// canonical occasion identity and variant. Similar titles, tags, and families
// are deliberately ignored so they cannot be represented as merchant history
// for the upcoming event.
func MostRecentExactOccurrence(upcoming Event, catalog []Event) (Event, bool) {
if strings.TrimSpace(upcoming.OccasionKey) == "" || strings.TrimSpace(upcoming.EventVariant) == "" {
return Event{}, false
}
var result Event
found := false
for _, candidate := range catalog {
if candidate.OccasionKey != upcoming.OccasionKey || candidate.EventVariant != upcoming.EventVariant || !dateBefore(candidate.EndDate, upcoming.StartDate) {
continue
}
if !found || dateAfter(candidate.EndDate, result.EndDate) || dayKey(candidate.EndDate) == dayKey(result.EndDate) && candidate.ID < result.ID {
result = candidate
found = true
}
}
return result, found
}
func RankEvent(event Event, input RankInput) EventRank {
rank := EventRank{Promotable: event.CommercialPosture != "NO_PROMOTION" && event.CommercialPosture != "CAUTION" && event.Tone != "MOURNING" && event.Tone != "MEMORIAL"}
rank.ScoreBPS = event.ImportanceBPS + event.ConfidenceBPS + event.RelevanceBPS
merchantTerms := normalizedSet(input.MerchantTerms)
for value := range normalizedSet(append(slices.Clone(event.Categories), event.Tags...)) {
if _, ok := merchantTerms[value]; ok {
rank.ScoreBPS += 2_000
rank.Factors = append(rank.Factors, value)
}
}
slices.Sort(rank.Factors)
if input.WithinPreparationWindow {
rank.ScoreBPS += 1_000
rank.Factors = append(rank.Factors, "PREPARATION_WINDOW")
}
if input.HistoricalEligible {
rank.ScoreBPS += 20_000 + abs64(input.HistoricalUpliftBPS)/10
rank.Factors = append(rank.Factors, "HISTORICAL_EVIDENCE")
}
return rank
}
func abs64(value int64) int64 {
if value < 0 {
return -value
}
return value
}
func MatchScoreBPS(historical, upcoming Event) (int64, string) {
if historical.Family != "" && strings.EqualFold(strings.TrimSpace(historical.Family), strings.TrimSpace(upcoming.Family)) {
return 10_000, "FAMILY"