extractExperimentData reads state.json from the experiment artifact directory under logsPath and returns a populated ExperimentData or nil when no experiment artifact is present. When the state file contains a non-empty "runs" array (written by pick_experiment.cjs v2+), the assignments of the most
(logsPath string)
| 71 | // variant is inferred by the max-count heuristic: the variant with the highest cumulative |
| 72 | // count is assumed to have been selected last (ties broken by sorted variant order). |
| 73 | func extractExperimentData(logsPath string) *ExperimentData { |
| 74 | if logsPath == "" { |
| 75 | return nil |
| 76 | } |
| 77 | |
| 78 | experimentDataLog.Printf("Extracting experiment data from: %s", logsPath) |
| 79 | |
| 80 | statePath := findExperimentStatePath(logsPath) |
| 81 | if statePath == "" { |
| 82 | experimentDataLog.Print("No experiment state file found") |
| 83 | return nil |
| 84 | } |
| 85 | |
| 86 | experimentDataLog.Printf("Reading experiment state from: %s", statePath) |
| 87 | raw, err := os.ReadFile(statePath) |
| 88 | if err != nil { |
| 89 | return nil |
| 90 | } |
| 91 | |
| 92 | var state experimentStateJSON |
| 93 | if err := json.Unmarshal(raw, &state); err != nil || len(state.Counts) == 0 { |
| 94 | return nil |
| 95 | } |
| 96 | |
| 97 | experimentDataLog.Printf("Found %d experiment(s) in state file", len(state.Counts)) |
| 98 | |
| 99 | // When per-run records are available, use the most recent run's assignments directly |
| 100 | // instead of inferring them from cumulative counts. |
| 101 | if len(state.Runs) > 0 { |
| 102 | lastRun := state.Runs[len(state.Runs)-1] |
| 103 | if len(lastRun.Assignments) > 0 { |
| 104 | experimentDataLog.Printf("Using run record from run_id=%s (timestamp=%s)", lastRun.RunID, lastRun.Timestamp) |
| 105 | return &ExperimentData{ |
| 106 | Assignments: lastRun.Assignments, |
| 107 | CumulativeCounts: state.Counts, |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // Derive this-run assignments: the variant selected on the most-recent run is |
| 113 | // the one with the maximum count (ties resolved by sorted order). |
| 114 | assignments := make(map[string]string, len(state.Counts)) |
| 115 | names := sliceutil.SortedKeys(state.Counts) |
| 116 | |
| 117 | for _, name := range names { |
| 118 | variantCounts := state.Counts[name] |
| 119 | selected := deriveLastSelectedVariant(variantCounts) |
| 120 | assignments[name] = selected |
| 121 | experimentDataLog.Printf("Experiment %q: selected variant=%q", name, selected) |
| 122 | } |
| 123 | |
| 124 | return &ExperimentData{ |
| 125 | Assignments: assignments, |
| 126 | CumulativeCounts: state.Counts, |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | // formatExperimentLabel returns a compact, human-readable label summarising the |