Add Strain Hospice for graceful low-win strain retirement.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Archive failed epidemiology strains to museum hospice with SQLite persistence, operator/AI/court triggers, breeding and graft guards, topology museum nodes, and Seer plus oath ledger accountability.
This commit is contained in:
94
server/internal/strategy/hospice.go
Normal file
94
server/internal/strategy/hospice.go
Normal file
@@ -0,0 +1,94 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user