blob: 60bc0b5eb5242f71d7f84d9768ea175ef52375e3 [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 Snyderb4782142025-05-05 19:16:54 +00008 "strings"
9 "sync"
10)
11
12// Experiment represents an experimental feature.
13// Experiments are global.
14type Experiment struct {
15 Name string // The name of the experiment used in -x flag
16 Description string // A short description of what the experiment does
17 Enabled bool // Whether the experiment is enabled
18}
19
20var (
21 mu sync.Mutex
22 experiments = []Experiment{
23 {
24 Name: "list",
25 Description: "List all available experiments and exit",
26 },
27 {
28 Name: "all",
29 Description: "Enable all experiments",
30 },
Josh Bleecher Snyder503b5e32025-05-05 13:30:55 -070031 {
Josh Bleecher Snydere2518e52025-04-29 11:13:40 -070032 Name: "llm_review",
33 Description: "Add an LLM step to the codereview tool",
34 },
Josh Bleecher Snydera997be62025-05-07 22:52:46 +000035 {
36 Name: "memory",
37 Description: "Enable memory subsystem (dear_llm.md)",
38 },
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000039 }
40 byName = map[string]*Experiment{}
41)
42
43func Enabled(name string) bool {
44 mu.Lock()
45 defer mu.Unlock()
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -070046 e, ok := byName[name]
47 if !ok {
48 slog.Error("unknown experiment", "name", name)
49 return false
50 }
51 return e.Enabled
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000052}
53
54func init() {
Josh Bleecher Snydere01ea0e2025-05-06 02:03:26 +000055 for i := range experiments {
56 e := &experiments[i]
57 byName[e.Name] = e
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000058 }
59}
60
61func (e Experiment) String() string {
62 return fmt.Sprintf("\t%-15s %s\n", e.Name, e.Description)
63}
64
65// Fprint writes a list of all available experiments to w.
66func Fprint(w io.Writer) {
67 mu.Lock()
68 defer mu.Unlock()
69
70 fmt.Fprintln(w, "Available experiments:")
71 for _, e := range experiments {
72 fmt.Fprintln(w, e)
73 }
74}
75
76// Flag is a custom flag type that allows for comma-separated
77// values and can be used multiple times.
78type Flag struct {
79 Value string
80}
81
82// String returns the string representation of the flag value.
83func (f *Flag) String() string {
84 return f.Value
85}
86
87// Set adds a value to the flag.
88func (f *Flag) Set(value string) error {
Josh Bleecher Snyder085f74c2025-05-06 01:58:25 +000089 if f.Value == "" {
90 f.Value = value
91 } else {
92 f.Value = f.Value + "," + value // quadratic, doesn't matter, tiny N
93 }
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000094 return nil
95}
96
97// Get returns the flag values.
98func (f *Flag) Get() any {
99 return f.Value
100}
101
102// Process handles all flag values, enabling the appropriate experiments.
103func (f *Flag) Process() error {
104 mu.Lock()
105 defer mu.Unlock()
106
107 for name := range strings.SplitSeq(f.Value, ",") {
108 name = strings.TrimSpace(name)
109 if name == "" {
110 continue
111 }
112 e, ok := byName[name]
113 if !ok {
114 return fmt.Errorf("unknown experiment: %q", name)
115 }
116 e.Enabled = true
117 }
118 if byName["all"].Enabled {
Josh Bleecher Snyderd42577f2025-05-05 17:59:00 -0700119 for i := range experiments {
120 e := &experiments[i]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000121 if e.Name == "list" {
122 continue
123 }
124 e.Enabled = true
125 }
126 }
127 return nil
128}