返回 DeepSeek-Reasonix
ablation.go
根目录 / internal / ablation / ablation.go
1 // Package ablation switches individual Reasonix subsystems off so a benchmark
2 // can attribute a change in solve rate to one of them.
3 package ablation
4
5 import (
6 "fmt"
7 "sort"
8 "strings"
9 )
10
11 type Module string
12
13 const (
14 Evidence Module = "evidence"
15 Planner Module = "planner"
16 Subagent Module = "subagent"
17 Retrieval Module = "retrieval"
18 Compaction Module = "compaction"
19 )
20
21 // Modules returns every switchable module in the order arm names use.
22 func Modules() []Module {
23 return []Module{Evidence, Planner, Subagent, Retrieval, Compaction}
24 }
25
26 // Set is the group of modules disabled for a run. The zero value is the
27 // control arm: everything on.
28 type Set struct {
29 off map[Module]bool
30 }
31
32 // Parse reads a spec such as "evidence,planner". "" and "none" mean the control
33 // arm; "all" disables every module.
34 func Parse(spec string) (Set, error) {
35 spec = strings.TrimSpace(spec)
36 if spec == "" || strings.EqualFold(spec, "none") {
37 return Set{}, nil
38 }
39 if strings.EqualFold(spec, "all") {
40 return New(Modules()...), nil
41 }
42 known := map[Module]bool{}
43 for _, m := range Modules() {
44 known[m] = true
45 }
46 var mods []Module
47 for _, field := range strings.FieldsFunc(spec, func(r rune) bool { return r == ',' || r == ' ' }) {
48 m := Module(strings.ToLower(strings.TrimSpace(field)))
49 if !known[m] {
50 return Set{}, fmt.Errorf("unknown ablation module %q (want %s, or none/all)", field, joinModules(Modules(), ", "))
51 }
52 mods = append(mods, m)
53 }
54 return New(mods...), nil
55 }
56
57 // New returns a Set with the given modules disabled.
58 func New(mods ...Module) Set {
59 if len(mods) == 0 {
60 return Set{}
61 }
62 off := make(map[Module]bool, len(mods))
63 for _, m := range mods {
64 off[m] = true
65 }
66 return Set{off: off}
67 }
68
69 func (s Set) Off(m Module) bool { return s.off[m] }
70
71 func (s Set) Empty() bool { return len(s.off) == 0 }
72
73 // Arm is the published name of this configuration: "full" for the control arm,
74 // otherwise "no-evidence+no-planner". Stable across runs so results from
75 // different machines group by the same key.
76 func (s Set) Arm() string {
77 if s.Empty() {
78 return "full"
79 }
80 parts := make([]string, 0, len(s.off))
81 for _, m := range s.disabled() {
82 parts = append(parts, "no-"+string(m))
83 }
84 return strings.Join(parts, "+")
85 }
86
87 // String round-trips back through Parse.
88 func (s Set) String() string {
89 if s.Empty() {
90 return "none"
91 }
92 return joinModules(s.disabled(), ",")
93 }
94
95 func (s Set) disabled() []Module {
96 order := map[Module]int{}
97 for i, m := range Modules() {
98 order[m] = i
99 }
100 out := make([]Module, 0, len(s.off))
101 for m := range s.off {
102 out = append(out, m)
103 }
104 sort.Slice(out, func(i, j int) bool { return order[out[i]] < order[out[j]] })
105 return out
106 }
107
108 func joinModules(mods []Module, sep string) string {
109 parts := make([]string, len(mods))
110 for i, m := range mods {
111 parts[i] = string(m)
112 }
113 return strings.Join(parts, sep)
114 }
115
115 lines GO