blob: a038f603bf1a8edb86263406a3cfe034e90b8523 [file] [log] [blame]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00001// Package experiment provides support for experimental features.
2package experiment
3
4import (
5 "fmt"
6 "io"
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -07007 "log/slog"
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -07008 "os"
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00009 "strings"
10 "sync"
11)
12
13// Experiment represents an experimental feature.
14// Experiments are global.
15type Experiment struct {
16 Name string // The name of the experiment used in -x flag
17 Description string // A short description of what the experiment does
18 Enabled bool // Whether the experiment is enabled
19}
20
21var (
22 mu sync.Mutex
23 experiments = []Experiment{
24 {
25 Name: "list",
26 Description: "List all available experiments and exit",
27 },
28 {
29 Name: "all",
30 Description: "Enable all experiments",
31 },
Josh Bleecher Snyder503b5e32025-05-05 13:30:55 -070032 {
Josh Bleecher Snydere2518e52025-04-29 11:13:40 -070033 Name: "llm_review",
34 Description: "Add an LLM step to the codereview tool",
35 },
Josh Bleecher Snyder0e5b8c62025-05-14 20:58:20 +000036
Josh Bleecher Snyder31785ae2025-05-06 01:50:58 +000037 {
38 Name: "kb",
39 Description: "Enable knowledge_base tool",
40 },
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000041 }
42 byName = map[string]*Experiment{}
43)
44
45func Enabled(name string) bool {
46 mu.Lock()
47 defer mu.Unlock()
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -070048 e, ok := byName[name]
49 if !ok {
50 slog.Error("unknown experiment", "name", name)
51 return false
52 }
53 return e.Enabled
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000054}
55
56func init() {
Josh Bleecher Snydere01ea0e2025-05-06 02:03:26 +000057 for i := range experiments {
58 e := &experiments[i]
59 byName[e.Name] = e
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000060 }
61}
62
63func (e Experiment) String() string {
64 return fmt.Sprintf("\t%-15s %s\n", e.Name, e.Description)
65}
66
67// Fprint writes a list of all available experiments to w.
68func Fprint(w io.Writer) {
69 mu.Lock()
70 defer mu.Unlock()
71
72 fmt.Fprintln(w, "Available experiments:")
73 for _, e := range experiments {
74 fmt.Fprintln(w, e)
75 }
76}
77
78// Flag is a custom flag type that allows for comma-separated
79// values and can be used multiple times.
80type Flag struct {
81 Value string
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070082 set bool // whether the flag has been set explicitly
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000083}
84
85// String returns the string representation of the flag value.
86func (f *Flag) String() string {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070087 return f.Get().(string)
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000088}
89
90// Set adds a value to the flag.
91func (f *Flag) Set(value string) error {
Josh Bleecher Snyder085f74c2025-05-06 01:58:25 +000092 if f.Value == "" {
93 f.Value = value
94 } else {
95 f.Value = f.Value + "," + value // quadratic, doesn't matter, tiny N
96 }
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070097 f.set = true
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000098 return nil
99}
100
101// Get returns the flag values.
102func (f *Flag) Get() any {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700103 if f.set {
104 return f.Value
105 }
106 return os.Getenv("SKETCH_EXPERIMENT")
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000107}
108
109// Process handles all flag values, enabling the appropriate experiments.
110func (f *Flag) Process() error {
111 mu.Lock()
112 defer mu.Unlock()
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700113 v := f.String()
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000114
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700115 for name := range strings.SplitSeq(v, ",") {
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000116 name = strings.TrimSpace(name)
117 if name == "" {
118 continue
119 }
120 e, ok := byName[name]
121 if !ok {
122 return fmt.Errorf("unknown experiment: %q", name)
123 }
124 e.Enabled = true
125 }
126 if byName["all"].Enabled {
Josh Bleecher Snyderd42577f2025-05-05 17:59:00 -0700127 for i := range experiments {
128 e := &experiments[i]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000129 if e.Name == "list" {
130 continue
131 }
132 e.Enabled = true
133 }
134 }
135 return nil
136}