package strategy import ( "strings" ) const ( // DefaultHospiceWinRateThreshold retires strains below 15% win rate. DefaultHospiceWinRateThreshold = 0.15 // DefaultHospiceMinAttempts requires enough spread outcomes before auto-retire. DefaultHospiceMinAttempts = 5 ) // StrainRetiredBy identifies who sent a strain to hospice. type StrainRetiredBy string const ( StrainRetiredByOperator StrainRetiredBy = "operator" StrainRetiredByAI StrainRetiredBy = "ai" StrainRetiredByCourt StrainRetiredBy = "court" ) // StrainSpreadStats aggregates epidemiology wins/losses for one spread strain. type StrainSpreadStats struct { StrainID string Wins int Losses int } // NormalizeStrainID lowercases #RRGGBB strain identifiers. func NormalizeStrainID(strain string) string { s := strings.TrimSpace(strings.ToLower(strain)) if s == "" { return "" } if !strings.HasPrefix(s, "#") && len(s) == 6 { s = "#" + s } return s } // StrainFromSpreadLane maps a join/spread lane to its stable strain color id. func StrainFromSpreadLane(lane string) string { if s := NormalizeStrainID(SpreadStrainFromJoinLane(lane)); s != "" { return s } return NormalizeStrainID(lane) } // LaneInHospice reports whether a spread/join lane's strain is retired. func LaneInHospice(lane string, hospice map[string]bool) bool { if len(hospice) == 0 { return false } return hospice[StrainFromSpreadLane(lane)] } // StrainInHospice reports whether a strain id is in the hospice set. func StrainInHospice(strain string, hospice map[string]bool) bool { if len(hospice) == 0 { return false } return hospice[NormalizeStrainID(strain)] } // StrainWinRate returns wins / (wins + losses); 1.0 when no attempts recorded. func StrainWinRate(wins, losses int) float64 { total := wins + losses if total == 0 { return 1.0 } return float64(wins) / float64(total) } // ShouldAutoRetireStrain is true when AI hospice policy applies to low performers. func ShouldAutoRetireStrain(wins, losses int, threshold float64, minAttempts int) bool { total := wins + losses if total < minAttempts || minAttempts <= 0 { return false } if threshold <= 0 { threshold = DefaultHospiceWinRateThreshold } return StrainWinRate(wins, losses) < threshold } // PersonaPrimaryLane returns the first spread lane for a persona preset. func PersonaPrimaryLane(persona string) string { order := PersonaSpreadTierOrder(persona) if len(order) == 0 { return "" } return strings.TrimSpace(order[0]) }