mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-05 04:27:09 +00:00
feat(campaign): sweep seeds for one experiment cell
campaign.json lists the seeds a sweep intended to run and is written before the first run, so a host that dropped runs shows up as missing seeds rather than as a smaller sample. Seed 0 is rejected: sanderling test reads it as "derive a seed from the clock", which is why conformance/gates.sh controls nothing today. Each run contributes one runs.jsonl line carrying steps to first violation by origin step, the step that armed the failed obligation, so the survival analysis never reopens a trace. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
1 parent
9f9a01f773
commit
028534cadc
11 files changed
+1605
No files matched your search
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseSeeds expands a seed specification such as "1-10,20,30-32" into the
|
||||
// explicit seed list a campaign intends to run.
|
||||
func parseSeeds(specification string) ([]int64, error) {
|
||||
trimmed := strings.TrimSpace(specification)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("empty seed spec")
|
||||
}
|
||||
var seeds []int64
|
||||
seen := map[int64]bool{}
|
||||
for _, part := range strings.Split(trimmed, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
return nil, fmt.Errorf("empty seed in %q", specification)
|
||||
}
|
||||
expanded, err := expandSeedPart(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, seed := range expanded {
|
||||
if seen[seed] {
|
||||
return nil, fmt.Errorf("duplicate seed %d in %q", seed, specification)
|
||||
}
|
||||
seen[seed] = true
|
||||
seeds = append(seeds, seed)
|
||||
}
|
||||
}
|
||||
return seeds, nil
|
||||
}
|
||||
|
||||
func expandSeedPart(part string) ([]int64, error) {
|
||||
start, end, isRange := strings.Cut(part, "-")
|
||||
if !isRange {
|
||||
seed, err := parseSeed(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []int64{seed}, nil
|
||||
}
|
||||
first, err := parseSeed(strings.TrimSpace(start))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seed range %q: %w", part, err)
|
||||
}
|
||||
last, err := parseSeed(strings.TrimSpace(end))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seed range %q: %w", part, err)
|
||||
}
|
||||
if first > last {
|
||||
return nil, fmt.Errorf("seed range %q: start %d is above end %d", part, first, last)
|
||||
}
|
||||
seeds := make([]int64, 0, last-first+1)
|
||||
for seed := first; seed <= last; seed++ {
|
||||
seeds = append(seeds, seed)
|
||||
}
|
||||
return seeds, nil
|
||||
}
|
||||
|
||||
func parseSeed(text string) (int64, error) {
|
||||
seed, err := strconv.ParseInt(text, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid seed %q: want a positive integer", text)
|
||||
}
|
||||
if seed == 0 {
|
||||
// `sanderling test` reads --seed 0 as "derive a seed from the clock",
|
||||
// so a campaign listing seed 0 records a run nobody can reproduce.
|
||||
return 0, fmt.Errorf("seed 0 is not reproducible: sanderling test derives a random seed when --seed is 0, so list explicit non-zero seeds")
|
||||
}
|
||||
if seed < 0 {
|
||||
return 0, fmt.Errorf("invalid seed %d: want a positive integer", seed)
|
||||
}
|
||||
return seed, nil
|
||||
}
|
||||
Reference in new issue
Block a user