ai-code-battle/cmd/acb-evolver/internal/live/cycle.go
jedarden ea04f4debb style: apply gofmt alignment fixes across codebase
Tab/space alignment consistency from running gofmt on all packages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:40:33 -04:00

299 lines
7.5 KiB
Go

// Package live provides real-time cycle state tracking for the evolution observatory.
package live
import (
"fmt"
"sync"
"time"
)
// CycleState tracks the current evolution cycle status in real-time.
// This is updated throughout the cycle and exported to live.json.
type CycleState struct {
mu sync.RWMutex
Generation int
StartedAt time.Time
Phase string // generating, validating, evaluating, promoting, idle
CandidateID string
CandidateIsland string
CandidateLang string
ParentIDs []string
Validation *CycleValidation
Evaluation *CycleEvaluation
PromotionReason string // Set when promoted/rejected
CommunityHint string // Community hint that influenced this candidate
}
// CycleValidation tracks validation stage progress.
type CycleValidation struct {
SyntaxPassed bool
SyntaxTimeMs int
SchemaPassed bool
SchemaTimeMs int
SmokePassed bool
SmokeTimeMs int
LastErrorStage string
LastError string
}
// CycleEvaluation tracks arena evaluation progress.
type CycleEvaluation struct {
MatchesTotal int
MatchesPlayed int
Results []CycleMatchResult
}
// CycleMatchResult is a single evaluation match result.
type CycleMatchResult struct {
Opponent string
Won bool
Score string // e.g. "5-1"
}
// NewCycleState creates a new cycle state tracker.
func NewCycleState() *CycleState {
return &CycleState{
Phase: "idle",
}
}
// SetPhase updates the current phase and timestamp.
func (c *CycleState) SetPhase(phase string) {
c.mu.Lock()
defer c.mu.Unlock()
c.Phase = phase
if phase == "generating" && c.StartedAt.IsZero() {
c.StartedAt = time.Now().UTC()
}
}
// SetGeneration sets the current generation number.
func (c *CycleState) SetGeneration(gen int) {
c.mu.Lock()
defer c.mu.Unlock()
c.Generation = gen
}
// SetCandidate sets the current candidate being evaluated.
func (c *CycleState) SetCandidate(id, island, lang string, parents []string) {
c.mu.Lock()
defer c.mu.Unlock()
c.CandidateID = id
c.CandidateIsland = island
c.CandidateLang = lang
c.ParentIDs = parents
}
// SetValidationSyntax records the result of syntax validation.
func (c *CycleState) SetValidationSyntax(passed bool, timeMs int) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Validation == nil {
c.Validation = &CycleValidation{}
}
c.Validation.SyntaxPassed = passed
c.Validation.SyntaxTimeMs = timeMs
if !passed {
c.Validation.LastErrorStage = "syntax"
}
}
// SetValidationSchema records the result of schema validation.
func (c *CycleState) SetValidationSchema(passed bool, timeMs int) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Validation == nil {
c.Validation = &CycleValidation{}
}
c.Validation.SchemaPassed = passed
c.Validation.SchemaTimeMs = timeMs
if !passed {
c.Validation.LastErrorStage = "schema"
}
}
// SetValidationSmoke records the result of smoke test validation.
func (c *CycleState) SetValidationSmoke(passed bool, timeMs int) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Validation == nil {
c.Validation = &CycleValidation{}
}
c.Validation.SmokePassed = passed
c.Validation.SmokeTimeMs = timeMs
if !passed {
c.Validation.LastErrorStage = "smoke"
}
}
// SetValidationError records a validation error.
func (c *CycleState) SetValidationError(stage, errMsg string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Validation == nil {
c.Validation = &CycleValidation{}
}
c.Validation.LastErrorStage = stage
c.Validation.LastError = errMsg
}
// StartEvaluation initializes the evaluation phase.
func (c *CycleState) StartEvaluation(totalMatches int) {
c.mu.Lock()
defer c.mu.Unlock()
c.Evaluation = &CycleEvaluation{
MatchesTotal: totalMatches,
MatchesPlayed: 0,
Results: make([]CycleMatchResult, 0, totalMatches),
}
}
// AddEvaluationResult adds a match result to the evaluation.
func (c *CycleState) AddEvaluationResult(opponent string, won bool, score string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Evaluation == nil {
return
}
c.Evaluation.Results = append(c.Evaluation.Results, CycleMatchResult{
Opponent: opponent,
Won: won,
Score: score,
})
c.Evaluation.MatchesPlayed = len(c.Evaluation.Results)
}
// SetPromotionResult sets the final promotion decision.
func (c *CycleState) SetPromotionResult(reason string) {
c.mu.Lock()
defer c.mu.Unlock()
c.PromotionReason = reason
}
// SetCommunityHint sets the community hint that influenced this candidate.
func (c *CycleState) SetCommunityHint(hint string) {
c.mu.Lock()
defer c.mu.Unlock()
c.CommunityHint = hint
}
// SetPhaseInfo sets all candidate info at once (simplified interface).
func (c *CycleState) SetPhaseInfo(phase, candidateID, island, lang string, parents []string) {
c.mu.Lock()
defer c.mu.Unlock()
c.Phase = phase
c.CandidateID = candidateID
c.CandidateIsland = island
c.CandidateLang = lang
c.ParentIDs = parents
if phase == "generating" && c.StartedAt.IsZero() {
c.StartedAt = time.Now().UTC()
}
}
// SetArenaResult records the final arena result.
func (c *CycleState) SetArenaResult(wins, losses, draws, errors int, winRate float64) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Evaluation == nil {
c.Evaluation = &CycleEvaluation{
MatchesTotal: wins + losses + draws + errors,
Results: make([]CycleMatchResult, 0),
}
}
c.Evaluation.MatchesPlayed = c.Evaluation.MatchesTotal
// Add a summary result
c.Evaluation.Results = append(c.Evaluation.Results, CycleMatchResult{
Opponent: "arena",
Won: wins > losses,
Score: fmt.Sprintf("%d-%d-%d", wins, losses, draws),
})
}
// Reset clears the cycle state (use between cycles).
func (c *CycleState) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.Generation = 0
c.StartedAt = time.Time{}
c.Phase = "idle"
c.CandidateID = ""
c.CandidateIsland = ""
c.CandidateLang = ""
c.ParentIDs = nil
c.Validation = nil
c.Evaluation = nil
c.PromotionReason = ""
c.CommunityHint = ""
}
// ToCycleInfo converts the cycle state to an exportable CycleInfo.
func (c *CycleState) ToCycleInfo() *CycleInfo {
c.mu.RLock()
defer c.mu.RUnlock()
if c.Phase == "idle" || c.Generation == 0 {
return nil
}
info := &CycleInfo{
Generation: c.Generation,
StartedAt: c.StartedAt.UTC().Format(time.RFC3339),
Phase: c.Phase,
}
if c.CandidateID != "" {
info.Candidate = &Candidate{
ID: c.CandidateID,
Island: c.CandidateIsland,
Language: c.CandidateLang,
}
// Add parents
if len(c.ParentIDs) > 0 {
info.Candidate.Parents = make([]ParentInfo, len(c.ParentIDs))
for i, pid := range c.ParentIDs {
info.Candidate.Parents[i] = ParentInfo{ID: pid}
}
}
// Add validation status
if c.Validation != nil {
info.Candidate.Validation = &ValidationStatus{
Syntax: &StageResult{
Passed: c.Validation.SyntaxPassed,
TimeMs: c.Validation.SyntaxTimeMs,
Error: c.Validation.LastError,
},
Schema: &StageResult{
Passed: c.Validation.SchemaPassed,
TimeMs: c.Validation.SchemaTimeMs,
},
Smoke: &StageResult{
Passed: c.Validation.SmokePassed,
TimeMs: c.Validation.SmokeTimeMs,
},
}
}
// Add evaluation status
if c.Evaluation != nil {
info.Candidate.Evaluation = &EvaluationStatus{
MatchesTotal: c.Evaluation.MatchesTotal,
MatchesPlayed: c.Evaluation.MatchesPlayed,
}
if len(c.Evaluation.Results) > 0 {
info.Candidate.Evaluation.Results = make([]MatchResult, len(c.Evaluation.Results))
for i, r := range c.Evaluation.Results {
info.Candidate.Evaluation.Results[i] = MatchResult{
Opponent: r.Opponent,
Won: r.Won,
Score: r.Score,
}
}
}
}
}
return info
}