devrites 5.12.0 → 5.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -3
  3. package/docs/architecture.md +2 -2
  4. package/docs/command-map.md +3 -1
  5. package/docs/engine/commands.md +21 -0
  6. package/docs/flow.md +2 -1
  7. package/docs/skills.md +13 -4
  8. package/engine/help.go +4 -1
  9. package/engine/help_test.go +2 -0
  10. package/engine/internal/install/binary.go +4 -0
  11. package/engine/internal/install/install_test.go +22 -0
  12. package/engine/internal/overhaul/admit/admit_test.go +170 -0
  13. package/engine/internal/overhaul/admit/run.go +416 -0
  14. package/engine/internal/overhaul/bench/run.go +248 -0
  15. package/engine/internal/overhaul/bench/run_test.go +139 -0
  16. package/engine/internal/overhaul/overhaul.go +64 -0
  17. package/engine/internal/overhaul/ovio/ovio.go +158 -0
  18. package/engine/internal/overhaul/records/check.go +834 -0
  19. package/engine/internal/overhaul/records/records_test.go +577 -0
  20. package/engine/internal/overhaul/records/run.go +392 -0
  21. package/engine/internal/overhaul/render/run.go +776 -0
  22. package/engine/internal/overhaul/render/run_test.go +293 -0
  23. package/engine/internal/overhaul/score/run.go +600 -0
  24. package/engine/internal/overhaul/score/run_test.go +405 -0
  25. package/engine/internal/overhaul/snapshot/run.go +563 -0
  26. package/engine/internal/overhaul/snapshot/run_test.go +360 -0
  27. package/engine/main.go +4 -0
  28. package/engine/root_routing_test.go +2 -1
  29. package/pack/.claude/skills/overhaul/SKILL.md +201 -0
  30. package/pack/.claude/skills/overhaul/references/audit-domains.md +263 -0
  31. package/pack/.claude/skills/overhaul/references/lifecycle.md +308 -0
  32. package/pack/.claude/skills/overhaul/references/orchestration.md +264 -0
  33. package/pack/.claude/skills/overhaul/references/records.md +137 -0
  34. package/pack/.claude/skills/overhaul/references/repair-and-measurement.md +207 -0
  35. package/pack/.claude/skills/overhaul/references/review-and-report.md +137 -0
  36. package/pack/.claude/skills/overhaul/references/scope-and-safety.md +191 -0
  37. package/pack/.claude/skills/overhaul/references/scoring.md +222 -0
  38. package/pack/.claude/skills/overhaul/references/stack-profiles.md +264 -0
  39. package/pack/.claude/skills/overhaul/roles/backend-engineer.md +61 -0
  40. package/pack/.claude/skills/overhaul/roles/boundary.md +56 -0
  41. package/pack/.claude/skills/overhaul/roles/craft.md +77 -0
  42. package/pack/.claude/skills/overhaul/roles/frontend-engineer.md +63 -0
  43. package/pack/.claude/skills/overhaul/roles/implementer.md +59 -0
  44. package/pack/.claude/skills/overhaul/roles/recon.md +60 -0
  45. package/pack/.claude/skills/overhaul/roles/specialist.md +56 -0
  46. package/pack/.claude/skills/overhaul/roles/verifier.md +60 -0
  47. package/pack/generated/claude/skills/overhaul/SKILL.md +201 -0
  48. package/pack/generated/claude/skills/overhaul/references/audit-domains.md +263 -0
  49. package/pack/generated/claude/skills/overhaul/references/lifecycle.md +308 -0
  50. package/pack/generated/claude/skills/overhaul/references/orchestration.md +264 -0
  51. package/pack/generated/claude/skills/overhaul/references/records.md +137 -0
  52. package/pack/generated/claude/skills/overhaul/references/repair-and-measurement.md +207 -0
  53. package/pack/generated/claude/skills/overhaul/references/review-and-report.md +137 -0
  54. package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +191 -0
  55. package/pack/generated/claude/skills/overhaul/references/scoring.md +222 -0
  56. package/pack/generated/claude/skills/overhaul/references/stack-profiles.md +264 -0
  57. package/pack/generated/claude/skills/overhaul/roles/backend-engineer.md +61 -0
  58. package/pack/generated/claude/skills/overhaul/roles/boundary.md +56 -0
  59. package/pack/generated/claude/skills/overhaul/roles/craft.md +77 -0
  60. package/pack/generated/claude/skills/overhaul/roles/frontend-engineer.md +63 -0
  61. package/pack/generated/claude/skills/overhaul/roles/implementer.md +59 -0
  62. package/pack/generated/claude/skills/overhaul/roles/recon.md +60 -0
  63. package/pack/generated/claude/skills/overhaul/roles/specialist.md +56 -0
  64. package/pack/generated/claude/skills/overhaul/roles/verifier.md +60 -0
  65. package/pack/generated/codex/skills/overhaul/SKILL.md +201 -0
  66. package/pack/generated/codex/skills/overhaul/agents/openai.yaml +2 -0
  67. package/pack/generated/codex/skills/overhaul/references/audit-domains.md +263 -0
  68. package/pack/generated/codex/skills/overhaul/references/lifecycle.md +308 -0
  69. package/pack/generated/codex/skills/overhaul/references/orchestration.md +264 -0
  70. package/pack/generated/codex/skills/overhaul/references/records.md +137 -0
  71. package/pack/generated/codex/skills/overhaul/references/repair-and-measurement.md +207 -0
  72. package/pack/generated/codex/skills/overhaul/references/review-and-report.md +137 -0
  73. package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +191 -0
  74. package/pack/generated/codex/skills/overhaul/references/scoring.md +222 -0
  75. package/pack/generated/codex/skills/overhaul/references/stack-profiles.md +264 -0
  76. package/pack/generated/codex/skills/overhaul/roles/backend-engineer.md +61 -0
  77. package/pack/generated/codex/skills/overhaul/roles/boundary.md +56 -0
  78. package/pack/generated/codex/skills/overhaul/roles/craft.md +77 -0
  79. package/pack/generated/codex/skills/overhaul/roles/frontend-engineer.md +63 -0
  80. package/pack/generated/codex/skills/overhaul/roles/implementer.md +59 -0
  81. package/pack/generated/codex/skills/overhaul/roles/recon.md +60 -0
  82. package/pack/generated/codex/skills/overhaul/roles/specialist.md +56 -0
  83. package/pack/generated/codex/skills/overhaul/roles/verifier.md +60 -0
  84. package/pack/generated/devin/skills/overhaul/SKILL.md +201 -0
  85. package/pack/generated/devin/skills/overhaul/references/audit-domains.md +263 -0
  86. package/pack/generated/devin/skills/overhaul/references/lifecycle.md +308 -0
  87. package/pack/generated/devin/skills/overhaul/references/orchestration.md +264 -0
  88. package/pack/generated/devin/skills/overhaul/references/records.md +137 -0
  89. package/pack/generated/devin/skills/overhaul/references/repair-and-measurement.md +207 -0
  90. package/pack/generated/devin/skills/overhaul/references/review-and-report.md +137 -0
  91. package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +191 -0
  92. package/pack/generated/devin/skills/overhaul/references/scoring.md +222 -0
  93. package/pack/generated/devin/skills/overhaul/references/stack-profiles.md +264 -0
  94. package/pack/generated/devin/skills/overhaul/roles/backend-engineer.md +61 -0
  95. package/pack/generated/devin/skills/overhaul/roles/boundary.md +56 -0
  96. package/pack/generated/devin/skills/overhaul/roles/craft.md +77 -0
  97. package/pack/generated/devin/skills/overhaul/roles/frontend-engineer.md +63 -0
  98. package/pack/generated/devin/skills/overhaul/roles/implementer.md +59 -0
  99. package/pack/generated/devin/skills/overhaul/roles/recon.md +60 -0
  100. package/pack/generated/devin/skills/overhaul/roles/specialist.md +56 -0
  101. package/pack/generated/devin/skills/overhaul/roles/verifier.md +60 -0
  102. package/pack/generated/omp/skills/overhaul/SKILL.md +201 -0
  103. package/pack/generated/omp/skills/overhaul/references/audit-domains.md +263 -0
  104. package/pack/generated/omp/skills/overhaul/references/lifecycle.md +308 -0
  105. package/pack/generated/omp/skills/overhaul/references/orchestration.md +264 -0
  106. package/pack/generated/omp/skills/overhaul/references/records.md +137 -0
  107. package/pack/generated/omp/skills/overhaul/references/repair-and-measurement.md +207 -0
  108. package/pack/generated/omp/skills/overhaul/references/review-and-report.md +137 -0
  109. package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +191 -0
  110. package/pack/generated/omp/skills/overhaul/references/scoring.md +222 -0
  111. package/pack/generated/omp/skills/overhaul/references/stack-profiles.md +264 -0
  112. package/pack/generated/omp/skills/overhaul/roles/backend-engineer.md +61 -0
  113. package/pack/generated/omp/skills/overhaul/roles/boundary.md +56 -0
  114. package/pack/generated/omp/skills/overhaul/roles/craft.md +77 -0
  115. package/pack/generated/omp/skills/overhaul/roles/frontend-engineer.md +63 -0
  116. package/pack/generated/omp/skills/overhaul/roles/implementer.md +59 -0
  117. package/pack/generated/omp/skills/overhaul/roles/recon.md +60 -0
  118. package/pack/generated/omp/skills/overhaul/roles/specialist.md +56 -0
  119. package/pack/generated/omp/skills/overhaul/roles/verifier.md +60 -0
  120. package/pack/generated/pi/skills/overhaul/SKILL.md +201 -0
  121. package/pack/generated/pi/skills/overhaul/references/audit-domains.md +263 -0
  122. package/pack/generated/pi/skills/overhaul/references/lifecycle.md +308 -0
  123. package/pack/generated/pi/skills/overhaul/references/orchestration.md +264 -0
  124. package/pack/generated/pi/skills/overhaul/references/records.md +137 -0
  125. package/pack/generated/pi/skills/overhaul/references/repair-and-measurement.md +207 -0
  126. package/pack/generated/pi/skills/overhaul/references/review-and-report.md +137 -0
  127. package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +191 -0
  128. package/pack/generated/pi/skills/overhaul/references/scoring.md +222 -0
  129. package/pack/generated/pi/skills/overhaul/references/stack-profiles.md +264 -0
  130. package/pack/generated/pi/skills/overhaul/roles/backend-engineer.md +61 -0
  131. package/pack/generated/pi/skills/overhaul/roles/boundary.md +56 -0
  132. package/pack/generated/pi/skills/overhaul/roles/craft.md +77 -0
  133. package/pack/generated/pi/skills/overhaul/roles/frontend-engineer.md +63 -0
  134. package/pack/generated/pi/skills/overhaul/roles/implementer.md +59 -0
  135. package/pack/generated/pi/skills/overhaul/roles/recon.md +60 -0
  136. package/pack/generated/pi/skills/overhaul/roles/specialist.md +56 -0
  137. package/pack/generated/pi/skills/overhaul/roles/verifier.md +60 -0
  138. package/package.json +1 -1
@@ -0,0 +1,416 @@
1
+ // Package admit implements the /overhaul admit tool: read-only admission
2
+ // checks for worker output.
3
+ //
4
+ // receipt RUN RECEIPT [--observed FILE] receipt vs its dispatch packet; writers vs observed paths
5
+ // anchor ROOT PROPOSALS every quoted location matches its file lines (ROOT = snapshot tree or repo)
6
+ //
7
+ // Exit: 0 admissible, 1 reject (reasons listed), 2 usage/IO error, 3 duplicate
8
+ // or late receipt. Nothing here edits canonical records; the coordinator
9
+ // records the verdict in a staged generation.
10
+ package admit
11
+
12
+ import (
13
+ "encoding/json"
14
+ "fmt"
15
+ "io"
16
+ "math"
17
+ "os"
18
+ "path/filepath"
19
+ "slices"
20
+ "sort"
21
+ "strconv"
22
+ "strings"
23
+ "unicode/utf16"
24
+ "unicode/utf8"
25
+
26
+ "github.com/devrites/devrites/internal/overhaul/ovio"
27
+ "github.com/devrites/devrites/internal/overhaul/records"
28
+ )
29
+
30
+ const usage = `usage: overhaul admit <command> ...
31
+
32
+ receipt RUN RECEIPT [--observed FILE] receipt vs its dispatch packet; writers vs observed paths
33
+ anchor ROOT PROPOSALS every quoted location matches its file lines (ROOT = snapshot tree or repo)
34
+ Exit: 0 admissible, 1 reject (reasons listed), 2 usage/IO error, 3 duplicate or late receipt.
35
+ `
36
+
37
+ var (
38
+ writers = map[string]bool{"implementer": true}
39
+ outcomes = []string{"findings", "gap", "no-findings", "patch", "verdict"} // sorted
40
+ )
41
+
42
+ // Run executes the tool with its arguments (tool name excluded).
43
+ func Run(args []string, stdout, stderr io.Writer) int {
44
+ var code int
45
+ var err error
46
+ switch {
47
+ case len(args) >= 3 && args[0] == "receipt":
48
+ observed, hasObserved := "", false
49
+ for i, a := range args {
50
+ if a == "--observed" {
51
+ if i+1 >= len(args) {
52
+ fmt.Fprint(stderr, usage)
53
+ return 2
54
+ }
55
+ observed, hasObserved = args[i+1], true
56
+ break
57
+ }
58
+ }
59
+ code, err = receipt(args[1], args[2], observed, hasObserved, stdout)
60
+ case len(args) >= 3 && args[0] == "anchor":
61
+ code, err = anchor(args[1], args[2], stdout, stderr)
62
+ default:
63
+ fmt.Fprint(stderr, usage)
64
+ return 2
65
+ }
66
+ if err != nil {
67
+ fmt.Fprintf(stderr, "ERROR: %v\n", err)
68
+ return 2
69
+ }
70
+ return code
71
+ }
72
+
73
+ // join mirrors os.path.join: an absolute part replaces everything before it.
74
+ func join(base, rel string) string {
75
+ switch {
76
+ case strings.HasPrefix(rel, "/"):
77
+ return rel
78
+ case base == "" || strings.HasSuffix(base, "/"):
79
+ return base + rel
80
+ }
81
+ return base + "/" + rel
82
+ }
83
+
84
+ func realpath(p string) string {
85
+ abs, err := filepath.Abs(p)
86
+ if err != nil {
87
+ abs = p
88
+ }
89
+ return ovio.Resolve(abs)
90
+ }
91
+
92
+ // pyJSON renders s like Python's json.dumps (ensure_ascii=True).
93
+ func pyJSON(s string) string {
94
+ var b strings.Builder
95
+ b.WriteByte('"')
96
+ for _, r := range s {
97
+ switch r {
98
+ case '"':
99
+ b.WriteString(`\"`)
100
+ case '\\':
101
+ b.WriteString(`\\`)
102
+ case '\n':
103
+ b.WriteString(`\n`)
104
+ case '\r':
105
+ b.WriteString(`\r`)
106
+ case '\t':
107
+ b.WriteString(`\t`)
108
+ case '\b':
109
+ b.WriteString(`\b`)
110
+ case '\f':
111
+ b.WriteString(`\f`)
112
+ default:
113
+ switch {
114
+ case r > 0xffff:
115
+ r1, r2 := utf16.EncodeRune(r)
116
+ fmt.Fprintf(&b, `\u%04x\u%04x`, r1, r2)
117
+ case r < 0x20 || r > 0x7e:
118
+ fmt.Fprintf(&b, `\u%04x`, r)
119
+ default:
120
+ b.WriteRune(r)
121
+ }
122
+ }
123
+ }
124
+ b.WriteByte('"')
125
+ return b.String()
126
+ }
127
+
128
+ func pyValue(v any) string {
129
+ switch x := v.(type) {
130
+ case string:
131
+ return pyJSON(x)
132
+ case []string:
133
+ parts := make([]string, len(x))
134
+ for i, s := range x {
135
+ parts[i] = pyJSON(s)
136
+ }
137
+ return "[" + strings.Join(parts, ", ") + "]"
138
+ }
139
+ b, _ := json.Marshal(v)
140
+ return string(b)
141
+ }
142
+
143
+ // verdict prints {"verdict": v, <key>: val} exactly as Python's json.dumps does.
144
+ func verdict(w io.Writer, v, key string, val any) {
145
+ fmt.Fprintf(w, "{\"verdict\": %s, %s: %s}\n", pyJSON(v), pyJSON(key), pyValue(val))
146
+ }
147
+
148
+ func keyed(m any, k string) (any, error) {
149
+ o, ok := m.(map[string]any)
150
+ if !ok {
151
+ return nil, fmt.Errorf("'%s': parent is not an object", k)
152
+ }
153
+ v, ok := o[k]
154
+ if !ok {
155
+ return nil, fmt.Errorf("'%s'", k)
156
+ }
157
+ return v, nil
158
+ }
159
+
160
+ // strSet collects string members keyed by records.Repr (non-strings never match paths).
161
+ func strSet(vals []any) map[string]any {
162
+ s := map[string]any{}
163
+ for _, v := range vals {
164
+ s[records.Repr(v)] = v
165
+ }
166
+ return s
167
+ }
168
+
169
+ func sortedVals(s map[string]any) []any {
170
+ out := make([]any, 0, len(s))
171
+ for _, v := range s {
172
+ out = append(out, v)
173
+ }
174
+ sort.Slice(out, func(i, j int) bool { return records.PyStr(out[i]) < records.PyStr(out[j]) })
175
+ return out
176
+ }
177
+
178
+ func receipt(run, path, observed string, hasObserved bool, stdout io.Writer) (int, error) {
179
+ cur, err := os.ReadFile(join(run, "CURRENT"))
180
+ if err != nil {
181
+ return 0, err
182
+ }
183
+ gen := join(run, strings.TrimSpace(string(cur)))
184
+ rj, err := ovio.LoadObject(join(gen, "run.json"))
185
+ if err != nil {
186
+ return 0, err
187
+ }
188
+ disp, err := ovio.LoadObject(join(gen, "dispatch.json"))
189
+ if err != nil {
190
+ return 0, err
191
+ }
192
+ rc, err := ovio.LoadObject(path)
193
+ if err != nil {
194
+ return 0, err
195
+ }
196
+ eq := func(a, b any) bool { return records.Repr(a) == records.Repr(b) }
197
+ var why []string
198
+ var x map[string]any
199
+ for _, it := range ovio.List(disp["attempts"]) {
200
+ if a := ovio.Obj(it); eq(a["task_id"], rc["task_id"]) && eq(a["attempt_id"], rc["attempt_id"]) {
201
+ x = a
202
+ break
203
+ }
204
+ }
205
+ if ovio.Str(rc["schema"]) != "overhaul.receipt/1" || !eq(rc["run_id"], rj["run_id"]) {
206
+ why = append(why, "wrong schema or run")
207
+ }
208
+ if x == nil {
209
+ why = append(why, "no such dispatched attempt")
210
+ } else {
211
+ if s := ovio.Str(x["status"]); s == "admitted" || s == "rejected" || s == "cancelled" {
212
+ verdict(stdout, "DUPLICATE_OR_LATE", "attempt_status", x["status"])
213
+ return 3, nil
214
+ }
215
+ for _, k := range []string{"role", "lane", "snapshot"} {
216
+ if !eq(rc[k], x[k]) {
217
+ why = append(why, k+" differs from the dispatch packet")
218
+ }
219
+ }
220
+ for _, k := range []string{"started_at", "finished_at"} {
221
+ if !ovio.Truthy(rc[k]) {
222
+ why = append(why, "missing "+k)
223
+ }
224
+ }
225
+ if !slices.Contains(outcomes, ovio.Str(rc["outcome"])) {
226
+ why = append(why, "outcome must be one of "+records.Repr(outcomes))
227
+ }
228
+ if ovio.Str(rc["outcome"]) == "no-findings" && !ovio.Truthy(rc["inspected"]) {
229
+ why = append(why, "no-findings without inspected ranges is malformed")
230
+ }
231
+ if writers[ovio.Str(x["role"])] {
232
+ v, err := keyed(rj, "revisions")
233
+ if err == nil {
234
+ v, err = keyed(v, "plan")
235
+ }
236
+ if err == nil {
237
+ v, err = keyed(v, "file")
238
+ }
239
+ if err != nil {
240
+ return 0, err
241
+ }
242
+ plan, err := ovio.LoadObject(join(run, ovio.Str(v)))
243
+ if err != nil {
244
+ return 0, err
245
+ }
246
+ allowed := map[string]any{}
247
+ for _, t := range ovio.List(plan["tasks"]) {
248
+ if tm := ovio.Obj(t); eq(tm["id"], x["task_id"]) {
249
+ for k, p := range strSet(ovio.List(tm["allowed_paths"])) {
250
+ allowed[k] = p
251
+ }
252
+ }
253
+ }
254
+ claimed := strSet(ovio.List(rc["changed_paths"]))
255
+ outside := map[string]any{}
256
+ for k, p := range claimed {
257
+ if _, ok := allowed[k]; !ok {
258
+ outside[k] = p
259
+ }
260
+ }
261
+ if len(outside) > 0 {
262
+ why = append(why, "changed paths outside contract: "+records.Repr(sortedVals(outside)))
263
+ }
264
+ if !hasObserved {
265
+ why = append(why, "writer receipt needs observed changed paths (--observed)")
266
+ } else {
267
+ b, err := os.ReadFile(observed) // #nosec G304 -- observed-paths file is an explicit operator argument
268
+ if err != nil {
269
+ return 0, err
270
+ }
271
+ text := strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(string(b)) // universal newlines
272
+ seen := map[string]any{}
273
+ for _, ln := range strings.Split(text, "\n") {
274
+ if ln = strings.TrimSpace(ln); ln != "" {
275
+ seen[records.Repr(ln)] = ln
276
+ }
277
+ }
278
+ same := len(seen) == len(claimed)
279
+ for k := range seen {
280
+ if _, ok := claimed[k]; !ok {
281
+ same = false
282
+ }
283
+ }
284
+ if !same {
285
+ why = append(why, fmt.Sprintf("observed paths %s differ from claimed %s",
286
+ records.Repr(sortedVals(seen)), records.Repr(sortedVals(claimed))))
287
+ }
288
+ }
289
+ }
290
+ }
291
+ return result(stdout, why, "ADMISSIBLE"), nil
292
+ }
293
+
294
+ func result(stdout io.Writer, why []string, ok string) int {
295
+ if why == nil {
296
+ why = []string{}
297
+ }
298
+ if len(why) > 0 {
299
+ verdict(stdout, "REJECT", "reasons", why)
300
+ return 1
301
+ }
302
+ verdict(stdout, ok, "reasons", why)
303
+ return 0
304
+ }
305
+
306
+ // splitlines is Python's str.splitlines().
307
+ func splitlines(s string) []string {
308
+ var out []string
309
+ start := 0
310
+ for i := 0; i < len(s); {
311
+ r, size := utf8.DecodeRuneInString(s[i:])
312
+ i += size
313
+ switch r {
314
+ case '\n', '\r', '\v', '\f', 0x1c, 0x1d, 0x1e, 0x85, 0x2028, 0x2029:
315
+ out = append(out, s[start:i-size])
316
+ if r == '\r' && i < len(s) && s[i] == '\n' {
317
+ i++
318
+ }
319
+ start = i
320
+ }
321
+ }
322
+ if start < len(s) {
323
+ out = append(out, s[start:])
324
+ }
325
+ return out
326
+ }
327
+
328
+ // pyInt is Python's int() on a decoded JSON value.
329
+ func pyInt(v any) (int, error) {
330
+ switch x := v.(type) {
331
+ case json.Number:
332
+ if i, err := x.Int64(); err == nil {
333
+ return int(i), nil
334
+ }
335
+ f, err := x.Float64()
336
+ if err != nil || math.IsInf(f, 0) || math.IsNaN(f) {
337
+ return 0, fmt.Errorf("invalid line number %s", x)
338
+ }
339
+ return int(f), nil
340
+ case string:
341
+ i, err := strconv.Atoi(strings.TrimSpace(x))
342
+ if err != nil {
343
+ return 0, fmt.Errorf("invalid literal for int() with base 10: %s", records.Repr(x))
344
+ }
345
+ return i, nil
346
+ case bool:
347
+ if x {
348
+ return 1, nil
349
+ }
350
+ return 0, nil
351
+ }
352
+ return 0, fmt.Errorf("int() argument must be a string or a number, not %s", records.Repr(v))
353
+ }
354
+
355
+ func anchor(root, path string, stdout, stderr io.Writer) (int, error) {
356
+ doc, err := ovio.LoadJSON(path)
357
+ if err != nil {
358
+ return 0, err
359
+ }
360
+ items, ok := ovio.Obj(doc)["findings"].([]any)
361
+ if !ok || len(items) == 0 {
362
+ fmt.Fprintln(stderr, `ERROR: expected {"findings": [...]} with at least one finding`)
363
+ return 2, nil
364
+ }
365
+ why := []string{}
366
+ base := realpath(root)
367
+ for _, it := range items {
368
+ f := ovio.Obj(it)
369
+ id := records.PyStr(f["id"])
370
+ if !ovio.Truthy(f["locations"]) {
371
+ why = append(why, id+": no locations to anchor")
372
+ }
373
+ for _, l := range ovio.List(f["locations"]) {
374
+ loc := ovio.Obj(l)
375
+ rel, quote := ovio.Str(loc["path"]), ovio.Str(loc["quote"])
376
+ full := realpath(join(base, rel))
377
+ if fi, err := os.Stat(full); !strings.HasPrefix(full, base+string(os.PathSeparator)) || err != nil || !fi.Mode().IsRegular() {
378
+ why = append(why, fmt.Sprintf("%s: %s is not a file under the reviewed tree", id, rel))
379
+ continue
380
+ }
381
+ b, err := os.ReadFile(full) // #nosec G304 -- anchor path checked to stay inside the reviewed tree
382
+ if err != nil {
383
+ return 0, err
384
+ }
385
+ lines := splitlines(strings.ToValidUTF8(string(b), "�"))
386
+ start, ok := loc["start"]
387
+ if !ok {
388
+ start = json.Number("0")
389
+ }
390
+ end, ok := loc["end"]
391
+ if !ok {
392
+ end = start
393
+ }
394
+ s, err := pyInt(start)
395
+ if err != nil {
396
+ return 0, err
397
+ }
398
+ e, err := pyInt(end)
399
+ if err != nil {
400
+ return 0, err
401
+ }
402
+ if !(1 <= s && s <= e && e <= len(lines)) {
403
+ why = append(why, fmt.Sprintf("%s: %s lines %d-%d outside file (%d lines)", id, rel, s, e, len(lines)))
404
+ continue
405
+ }
406
+ q := strings.Join(strings.Fields(quote), " ")
407
+ w := strings.Join(strings.Fields(strings.Join(lines[s-1:e], " ")), " ")
408
+ if q == "" || !strings.Contains(w, q) {
409
+ why = append(why, fmt.Sprintf("%s: quote not found at %s:%d-%d", id, rel, s, e))
410
+ } else if utf8.RuneCountInString(strings.ReplaceAll(q, " ", "")) < 8 && q != w {
411
+ why = append(why, fmt.Sprintf("%s: quote at %s:%d is too short to anchor; quote the whole line", id, rel, s))
412
+ }
413
+ }
414
+ }
415
+ return result(stdout, why, "ANCHORED"), nil
416
+ }
@@ -0,0 +1,248 @@
1
+ // Package bench implements the /overhaul bench tool: ratio of medians with a
2
+ // seeded percentile-bootstrap 95% confidence interval.
3
+ //
4
+ // Input: {"direction": "lower_is_better"|"higher_is_better", "baseline": [...],
5
+ // "candidate": [...], "order": ["A","B",...], "target": 2.0, "materiality": 1.05,
6
+ // "resamples": 10000, "seed": 1, "resolution": 0}. The unit of resampling is one
7
+ // independent invocation per sample; never pass inner iterations. A k-fold claim
8
+ // is supported only when the CI lower bound >= k.
9
+ package bench
10
+
11
+ import (
12
+ "encoding/json"
13
+ "errors"
14
+ "fmt"
15
+ "io"
16
+ "math"
17
+ "math/rand/v2"
18
+ "reflect"
19
+ "sort"
20
+ "strconv"
21
+ "strings"
22
+
23
+ "github.com/devrites/devrites/internal/overhaul/ovio"
24
+ )
25
+
26
+ const usage = "usage: devrites-engine overhaul bench <result.json>\n"
27
+
28
+ // Run executes the tool with its arguments (tool name excluded).
29
+ func Run(args []string, stdout, stderr io.Writer) int {
30
+ if len(args) != 1 {
31
+ fmt.Fprint(stderr, usage)
32
+ return 2
33
+ }
34
+ d, err := ovio.LoadObject(args[0])
35
+ var res any
36
+ if err == nil {
37
+ res, err = compare(d)
38
+ }
39
+ var out []byte
40
+ if err == nil {
41
+ out, err = ovio.MarshalIndent(res)
42
+ }
43
+ if err != nil {
44
+ fmt.Fprintf(stderr, "ERROR: %v\n", err)
45
+ return 2
46
+ }
47
+ _, _ = stdout.Write(out)
48
+ return 0
49
+ }
50
+
51
+ type result struct {
52
+ Verdict string `json:"verdict"`
53
+ Ratio float64 `json:"ratio"`
54
+ CI95 [2]float64 `json:"ci95"`
55
+ BaselineMedian float64 `json:"baseline_median"`
56
+ CandidateMedian float64 `json:"candidate_median"`
57
+ AbsoluteDelta float64 `json:"absolute_delta"`
58
+ PercentChange float64 `json:"percent_change"`
59
+ N [2]int `json:"n"`
60
+ Target float64 `json:"target"`
61
+ Materiality float64 `json:"materiality"`
62
+ Notes []string `json:"notes"`
63
+ }
64
+
65
+ type noRatio struct {
66
+ Verdict string `json:"verdict"`
67
+ Reason string `json:"reason"`
68
+ BaselineMedian float64 `json:"baseline_median"`
69
+ CandidateMedian float64 `json:"candidate_median"`
70
+ Notes []string `json:"notes"`
71
+ }
72
+
73
+ const (
74
+ noteFew = "fewer than 10 invocations per arm: exploratory only"
75
+ noteBlocked = "arms not recorded as interleaved: contamination risk, verdict capped at inconclusive"
76
+ )
77
+
78
+ func compare(d map[string]any) (any, error) {
79
+ dir := d["direction"]
80
+ if dir != "lower_is_better" && dir != "higher_is_better" {
81
+ return nil, fmt.Errorf("direction must be lower_is_better or higher_is_better, got %#v", dir)
82
+ }
83
+ lower := dir == "lower_is_better"
84
+ a, err := samples(d, "baseline")
85
+ if err != nil {
86
+ return nil, err
87
+ }
88
+ b, err := samples(d, "candidate")
89
+ if err != nil {
90
+ return nil, err
91
+ }
92
+ if len(a) == 0 || len(b) == 0 {
93
+ return nil, errors.New("no median for empty data")
94
+ }
95
+ notes := []string{}
96
+ n := min(len(a), len(b))
97
+ if n < 10 {
98
+ notes = append(notes, noteFew)
99
+ }
100
+ order := ovio.List(d["order"])
101
+ switches := 0
102
+ for i := 1; i < len(order); i++ {
103
+ if !reflect.DeepEqual(order[i-1], order[i]) {
104
+ switches++
105
+ }
106
+ }
107
+ if len(order) != len(a)+len(b) || switches < n/2 {
108
+ notes = append(notes, noteBlocked)
109
+ }
110
+ ma, mb := median(a), median(b)
111
+ floor, err := number(d, "resolution", 0)
112
+ if err != nil {
113
+ return nil, err
114
+ }
115
+ if min(ma, mb) <= floor {
116
+ return noRatio{"NO_RATIO", "median at or below measurement resolution", ma, mb, notes}, nil
117
+ }
118
+ seed := int64(1)
119
+ if v, ok := d["seed"]; ok {
120
+ num, isNum := v.(json.Number)
121
+ s, err := num.Int64()
122
+ if !isNum || err != nil {
123
+ return nil, fmt.Errorf("seed must be an integer, got %v", v)
124
+ }
125
+ seed = s
126
+ }
127
+ resamples, err := number(d, "resamples", 10000)
128
+ if err != nil {
129
+ return nil, err
130
+ }
131
+ if int(resamples) < 1 {
132
+ return nil, fmt.Errorf("resamples must be at least 1, got %v", resamples)
133
+ }
134
+ rng := rand.New(rand.NewPCG(uint64(seed), 0)) // #nosec G404 -- bootstrap resampling needs a seeded, reproducible PRNG, not secrecy
135
+ boots := make([]float64, int(resamples))
136
+ ra, rb := make([]float64, len(a)), make([]float64, len(b))
137
+ for i := range boots {
138
+ for j := range ra {
139
+ ra[j] = a[rng.IntN(len(a))]
140
+ }
141
+ for j := range rb {
142
+ rb[j] = b[rng.IntN(len(b))]
143
+ }
144
+ boots[i] = ratio(median(ra), median(rb), lower)
145
+ }
146
+ sort.Float64s(boots)
147
+ hiIdx := int(0.975*float64(len(boots))) - 1
148
+ if hiIdx < 0 { // Python's boots[-1]
149
+ hiIdx += len(boots)
150
+ }
151
+ lo, hi := boots[int(0.025*float64(len(boots)))], boots[hiIdx]
152
+ target, err := number(d, "target", 1)
153
+ if err != nil {
154
+ return nil, err
155
+ }
156
+ mat, err := number(d, "materiality", 1)
157
+ if err != nil {
158
+ return nil, err
159
+ }
160
+ var verdict string
161
+ switch {
162
+ case hi < 1:
163
+ verdict = "REGRESSION"
164
+ case lo >= target && target > 1:
165
+ verdict = "SUPPORTS_TARGET"
166
+ case lo >= mat && lo > 1 && target > 1:
167
+ verdict = "IMPROVEMENT_BELOW_TARGET"
168
+ case lo >= mat && lo > 1:
169
+ verdict = "IMPROVEMENT"
170
+ default:
171
+ verdict = "INCONCLUSIVE"
172
+ }
173
+ if len(notes) > 0 && verdict != "REGRESSION" {
174
+ verdict = "INCONCLUSIVE"
175
+ }
176
+ delta := mb - ma
177
+ return result{
178
+ Verdict: verdict, Ratio: round(ratio(ma, mb, lower), 4), CI95: [2]float64{round(lo, 4), round(hi, 4)},
179
+ BaselineMedian: ma, CandidateMedian: mb, AbsoluteDelta: delta,
180
+ PercentChange: round(100*delta/ma, 2), N: [2]int{len(a), len(b)},
181
+ Target: target, Materiality: mat, Notes: notes,
182
+ }, nil
183
+ }
184
+
185
+ func ratio(base, cand float64, lowerBetter bool) float64 {
186
+ if lowerBetter {
187
+ return base / cand
188
+ }
189
+ return cand / base
190
+ }
191
+
192
+ func median(xs []float64) float64 {
193
+ s := append([]float64(nil), xs...)
194
+ sort.Float64s(s)
195
+ m := len(s) / 2
196
+ if len(s)%2 == 1 {
197
+ return s[m]
198
+ }
199
+ return (s[m-1] + s[m]) / 2
200
+ }
201
+
202
+ func round(x float64, places int) float64 {
203
+ p := math.Pow(10, float64(places))
204
+ return math.Round(x*p) / p
205
+ }
206
+
207
+ func samples(d map[string]any, key string) ([]float64, error) {
208
+ v, ok := d[key]
209
+ if !ok {
210
+ return nil, fmt.Errorf("missing %q", key)
211
+ }
212
+ list, ok := v.([]any)
213
+ if !ok {
214
+ return nil, fmt.Errorf("%s must be an array", key)
215
+ }
216
+ out := make([]float64, len(list))
217
+ for i, x := range list {
218
+ f, err := toFloat(x, key)
219
+ if err != nil {
220
+ return nil, err
221
+ }
222
+ out[i] = f
223
+ }
224
+ return out, nil
225
+ }
226
+
227
+ func number(d map[string]any, key string, def float64) (float64, error) {
228
+ v, ok := d[key]
229
+ if !ok {
230
+ return def, nil
231
+ }
232
+ return toFloat(v, key)
233
+ }
234
+
235
+ // toFloat accepts JSON numbers and numeric strings, like Python's float().
236
+ func toFloat(v any, key string) (float64, error) {
237
+ switch x := v.(type) {
238
+ case json.Number:
239
+ return x.Float64()
240
+ case string:
241
+ f, err := strconv.ParseFloat(strings.TrimSpace(x), 64)
242
+ if err != nil {
243
+ return 0, fmt.Errorf("%s: could not convert %q to float", key, x)
244
+ }
245
+ return f, nil
246
+ }
247
+ return 0, fmt.Errorf("%s: expected a number, got %v", key, v)
248
+ }