package archive import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "time" ) type RunArchive struct { Version int `json:"version"` Run RunSummary `json:"run"` Rounds []RoundStats `json:"rounds"` } type RunSummary struct { SupplyObservation json.RawMessage `json:"supplyObservation,omitempty"` SyncType string `json:"syncType,omitempty"` RunID string `json:"runId"` Operation string `json:"operation"` Status string `json:"status"` SuccessCount int64 `json:"successCount"` FailedCount int64 `json:"failedCount"` CreatedAt string `json:"createdAt"` CompletedAt string `json:"completedAt"` } type RoundStats struct { RunID string `json:"runId"` Round int `json:"round"` EnqueuedCount int64 `json:"enqueuedCount"` ActiveCount int64 `json:"activeCount"` NextRoundCount int64 `json:"nextRoundCount"` SuccessCount int64 `json:"successCount"` FailedCount int64 `json:"failedCount"` UpstreamAttemptCount int64 `json:"upstreamAttemptCount"` BatchCount int64 `json:"batchCount"` StartedAt string `json:"startedAt"` CompletedAt string `json:"completedAt"` ErrorCounts map[string]int `json:"errorCounts"` } func Load(path string) (RunArchive, error) { body, err := os.ReadFile(path) if err != nil { return RunArchive{}, err } var a RunArchive if err := json.Unmarshal(body, &a); err != nil { return RunArchive{}, fmt.Errorf("parse %s: %w", path, err) } if err := a.Validate(); err != nil { return RunArchive{}, fmt.Errorf("validate %s: %w", path, err) } if err := ValidateFilename(path, a.Run.RunID); err != nil { return RunArchive{}, err } return a, nil } func ValidateFilename(path, runID string) error { want := "run-" + runID + ".json" if filepath.Base(path) != want { return fmt.Errorf("archive filename %q does not match RunID; expected %q", filepath.Base(path), want) } return nil } func (a *RunArchive) Validate() error { if a.Version != 1 { return fmt.Errorf("unsupported version %d", a.Version) } if a.Run.RunID == "" || a.Run.Operation == "" { return fmt.Errorf("runId and operation are required") } if a.Run.Status != "completed" && a.Run.Status != "completed_with_failures" { return fmt.Errorf("run is not terminal: %s", a.Run.Status) } if len(a.Rounds) == 0 { return fmt.Errorf("rounds must be non-empty") } copyRounds := append([]RoundStats(nil), a.Rounds...) sort.Slice(copyRounds, func(i, j int) bool { return copyRounds[i].Round < copyRounds[j].Round }) var success, failed int64 for i, r := range copyRounds { if r.Round != i+1 { return fmt.Errorf("round sequence is not contiguous at %d", r.Round) } if r.RunID != "" && r.RunID != a.Run.RunID { return fmt.Errorf("round %d runId does not match root runId", r.Round) } if r.EnqueuedCount < 0 || r.ActiveCount < 0 || r.NextRoundCount < 0 || r.SuccessCount < 0 || r.FailedCount < 0 || r.UpstreamAttemptCount < 0 || r.BatchCount < 0 { return fmt.Errorf("round %d contains negative count", r.Round) } if r.EnqueuedCount != r.SuccessCount+r.FailedCount+r.NextRoundCount { return fmt.Errorf("round %d count does not close", r.Round) } if _, err := parseTime(r.StartedAt); err != nil { return fmt.Errorf("round %d invalid startedAt: %w", r.Round, err) } if _, err := parseTime(r.CompletedAt); err != nil { return fmt.Errorf("round %d invalid completedAt: %w", r.Round, err) } success += r.SuccessCount failed += r.FailedCount } if a.Run.SuccessCount != success || a.Run.FailedCount != failed { return fmt.Errorf("run totals do not match rounds: run success=%d failed=%d, rounds success=%d failed=%d", a.Run.SuccessCount, a.Run.FailedCount, success, failed) } if _, err := parseTime(a.Run.CreatedAt); err != nil { return fmt.Errorf("invalid createdAt: %w", err) } if _, err := parseTime(a.Run.CompletedAt); err != nil { return fmt.Errorf("invalid completedAt: %w", err) } return nil } func (a RunArchive) TotalCount() int64 { return a.Run.SuccessCount + a.Run.FailedCount } func (a RunArchive) SuccessRate() float64 { if a.TotalCount() == 0 { return 0 } return float64(a.Run.SuccessCount) / float64(a.TotalCount()) * 100 } func (a RunArchive) Duration() time.Duration { start, err1 := parseTime(a.Run.CreatedAt) end, err2 := parseTime(a.Run.CompletedAt) if err1 != nil || err2 != nil { return 0 } return end.Sub(start) } func (r RoundStats) SuccessRate() float64 { if r.EnqueuedCount == 0 { return 0 } return float64(r.SuccessCount) / float64(r.EnqueuedCount) * 100 } func (r RoundStats) Duration() time.Duration { start, err1 := parseTime(r.StartedAt) end, err2 := parseTime(r.CompletedAt) if err1 != nil || err2 != nil { return 0 } return end.Sub(start) } func parseTime(value string) (time.Time, error) { for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05.999", "2006-01-02 15:04:05"} { if t, err := time.Parse(layout, value); err == nil { return t, nil } } return time.Time{}, fmt.Errorf("unsupported time %q", value) }