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

@@ -3,10 +3,15 @@ package eventcatalog
import (
"errors"
"fmt"
"regexp"
"strings"
"time"
)
var canonicalOccasionKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$`)
var jalaliBreaks = [...]int{-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}
type EventType string
type Tone string
type CommercialPosture string
@@ -41,7 +46,8 @@ const (
type Event struct {
ID string `json:"id"`
OccasionKey string `json:"occasionKey,omitempty"`
Variant string `json:"variant,omitempty"`
Variant string `json:"eventVariant,omitempty"`
JalaliYear int `json:"jalaliYear,omitempty"`
TitleFA string `json:"titleFa"`
TitleEN string `json:"titleEn,omitempty"`
Aliases []string `json:"aliases,omitempty"`
@@ -86,6 +92,7 @@ func ResolveRecurrence(rule Recurrence, year int, location *time.Location) (time
func ValidateEvents(events []Event) error {
seen := make(map[string]struct{}, len(events))
canonical := make(map[string]struct{}, len(events))
canonicalYears := make(map[string]struct{}, len(events))
for _, event := range events {
if strings.TrimSpace(event.ID) == "" || strings.TrimSpace(event.TitleFA) == "" || strings.TrimSpace(event.SourceURI) == "" || strings.TrimSpace(event.SourceVersion) == "" || strings.TrimSpace(event.InclusionRationale) == "" || strings.TrimSpace(event.GeographicScope) == "" {
return fmt.Errorf("event catalog: event %q is incomplete", event.ID)
@@ -94,8 +101,11 @@ func ValidateEvents(events []Event) error {
return fmt.Errorf("event catalog: duplicate event %q", event.ID)
}
seen[event.ID] = struct{}{}
if event.OccasionKey == "" {
event.OccasionKey = event.ID
if !canonicalOccasionKeyPattern.MatchString(event.OccasionKey) {
return fmt.Errorf("event catalog: event %q has invalid canonical occasion key", event.ID)
}
if hasYearSegment(event.OccasionKey) {
return fmt.Errorf("event catalog: event %q has year-specific occasion key", event.ID)
}
if event.Variant == "" {
event.Variant = "PRIMARY"
@@ -137,10 +147,106 @@ func ValidateEvents(events []Event) error {
return fmt.Errorf("event catalog: duplicate canonical occurrence %q", event.ID)
}
canonical[canonicalKey] = struct{}{}
if event.JalaliYear != 0 {
canonicalYearKey := fmt.Sprintf("%s\x00%s\x00%d", event.OccasionKey, event.Variant, event.JalaliYear)
if _, exists := canonicalYears[canonicalYearKey]; exists {
return fmt.Errorf("event catalog: duplicate canonical occurrence in Jalali year %d for event %q", event.JalaliYear, event.ID)
}
canonicalYears[canonicalYearKey] = struct{}{}
}
}
return nil
}
// ValidateDeclaredCoverage verifies that each year advertised by an import has
// at least one dated event. JalaliYear is explicit review metadata: it avoids
// guessing calendar boundaries from Gregorian dates during catalog approval.
func ValidateDeclaredCoverage(events []Event, startJalaliYear, endJalaliYear int) error {
if startJalaliYear <= 0 || endJalaliYear < startJalaliYear {
return errors.New("event catalog: invalid declared Jalali coverage")
}
covered := make(map[int]struct{}, endJalaliYear-startJalaliYear+1)
for _, event := range events {
if event.JalaliYear < startJalaliYear || event.JalaliYear > endJalaliYear {
return fmt.Errorf("event catalog: event %q lacks valid Jalali year within declared coverage", event.ID)
}
start, err := time.Parse(time.DateOnly, event.StartDate)
if err != nil {
return fmt.Errorf("event catalog: event %q start date: %w", event.ID, err)
}
end, err := time.Parse(time.DateOnly, event.EndDate)
if err != nil {
return fmt.Errorf("event catalog: event %q end date: %w", event.ID, err)
}
startJalaliYear := gregorianToJalaliYear(start)
endJalaliYear := gregorianToJalaliYear(end)
belongsToOccurrenceYear := startJalaliYear == event.JalaliYear || endJalaliYear == event.JalaliYear
preparesImmediateNextYear := event.Variant == "CAMPAIGN_WINDOW" && endJalaliYear+1 == event.JalaliYear && gregorianToJalaliYear(end.AddDate(0, 0, 1)) == event.JalaliYear
if !belongsToOccurrenceYear && !preparesImmediateNextYear {
return fmt.Errorf("event catalog: event %q Jalali year is inconsistent with its Gregorian date", event.ID)
}
covered[event.JalaliYear] = struct{}{}
}
for year := startJalaliYear; year <= endJalaliYear; year++ {
if _, exists := covered[year]; !exists {
return fmt.Errorf("event catalog: declared coverage has no event in Jalali year %d", year)
}
}
return nil
}
func hasYearSegment(value string) bool {
digits := 0
for _, character := range value {
if character >= '0' && character <= '9' {
digits++
if digits >= 4 {
return true
}
} else {
digits = 0
}
}
return false
}
func gregorianToJalaliYear(date time.Time) int {
jy := date.Year() - 621
firstFarvardin := gregorianDayNumber(date.Year(), 3, jalaliMarchDay(jy))
if gregorianDayNumber(date.Year(), int(date.Month()), date.Day()) < firstFarvardin {
jy--
}
return jy
}
func jalaliMarchDay(jy int) int {
gy := jy + 621
leapJ := -14
previousBreak := jalaliBreaks[0]
jump := 0
for index := 1; index < len(jalaliBreaks); index++ {
currentBreak := jalaliBreaks[index]
jump = currentBreak - previousBreak
if jy < currentBreak {
break
}
leapJ += jump/33*8 + (jump%33)/4
previousBreak = currentBreak
}
n := jy - previousBreak
leapJ += n/33*8 + (n%33+3)/4
if jump%33 == 4 && jump-n == 4 {
leapJ++
}
leapG := gy/4 - ((gy/100+1)*3)/4 - 150
return 20 + leapJ - leapG
}
func gregorianDayNumber(gy, gm, gd int) int {
dayNumber := ((gy+(gm-8)/6+100100)*1461)/4 + (153*((gm+9)%12)+2)/5 + gd - 34840408
return dayNumber - (((gy+100100+(gm-8)/6)/100)*3)/4 + 752
}
func nthWeekday(year int, month time.Month, weekday time.Weekday, occurrence int, location *time.Location) time.Time {
first := time.Date(year, month, 1, 0, 0, 0, 0, location)
offset := (int(weekday) - int(first.Weekday()) + 7) % 7