devrites 5.12.0 → 5.13.0
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.
- package/CHANGELOG.md +6 -0
- package/README.md +5 -3
- package/docs/architecture.md +2 -2
- package/docs/command-map.md +3 -1
- package/docs/engine/commands.md +21 -0
- package/docs/flow.md +2 -1
- package/docs/skills.md +13 -4
- package/engine/help.go +4 -1
- package/engine/help_test.go +2 -0
- package/engine/internal/overhaul/admit/admit_test.go +170 -0
- package/engine/internal/overhaul/admit/run.go +416 -0
- package/engine/internal/overhaul/bench/run.go +248 -0
- package/engine/internal/overhaul/bench/run_test.go +139 -0
- package/engine/internal/overhaul/overhaul.go +64 -0
- package/engine/internal/overhaul/ovio/ovio.go +158 -0
- package/engine/internal/overhaul/records/check.go +834 -0
- package/engine/internal/overhaul/records/records_test.go +577 -0
- package/engine/internal/overhaul/records/run.go +392 -0
- package/engine/internal/overhaul/render/run.go +776 -0
- package/engine/internal/overhaul/render/run_test.go +293 -0
- package/engine/internal/overhaul/score/run.go +600 -0
- package/engine/internal/overhaul/score/run_test.go +405 -0
- package/engine/internal/overhaul/snapshot/run.go +563 -0
- package/engine/internal/overhaul/snapshot/run_test.go +360 -0
- package/engine/main.go +4 -0
- package/engine/root_routing_test.go +2 -1
- package/pack/.claude/skills/overhaul/SKILL.md +201 -0
- package/pack/.claude/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/.claude/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/.claude/skills/overhaul/references/orchestration.md +264 -0
- package/pack/.claude/skills/overhaul/references/records.md +137 -0
- package/pack/.claude/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/.claude/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/.claude/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/.claude/skills/overhaul/references/scoring.md +222 -0
- package/pack/.claude/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/.claude/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/.claude/skills/overhaul/roles/boundary.md +56 -0
- package/pack/.claude/skills/overhaul/roles/craft.md +77 -0
- package/pack/.claude/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/.claude/skills/overhaul/roles/implementer.md +59 -0
- package/pack/.claude/skills/overhaul/roles/recon.md +60 -0
- package/pack/.claude/skills/overhaul/roles/specialist.md +56 -0
- package/pack/.claude/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/claude/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/claude/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/claude/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/claude/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/claude/skills/overhaul/references/records.md +137 -0
- package/pack/generated/claude/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/claude/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/claude/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/claude/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/claude/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/claude/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/claude/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/claude/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/claude/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/claude/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/claude/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/claude/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/codex/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/codex/skills/overhaul/agents/openai.yaml +2 -0
- package/pack/generated/codex/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/codex/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/codex/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/codex/skills/overhaul/references/records.md +137 -0
- package/pack/generated/codex/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/codex/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/codex/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/codex/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/codex/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/codex/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/codex/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/codex/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/codex/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/codex/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/codex/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/codex/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/devin/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/devin/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/devin/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/devin/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/devin/skills/overhaul/references/records.md +137 -0
- package/pack/generated/devin/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/devin/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/devin/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/devin/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/devin/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/devin/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/devin/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/devin/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/devin/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/devin/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/devin/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/devin/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/omp/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/omp/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/omp/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/omp/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/omp/skills/overhaul/references/records.md +137 -0
- package/pack/generated/omp/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/omp/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/omp/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/omp/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/omp/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/omp/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/omp/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/omp/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/omp/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/omp/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/omp/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/omp/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/pi/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/pi/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/pi/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/pi/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/pi/skills/overhaul/references/records.md +137 -0
- package/pack/generated/pi/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/pi/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/pi/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/pi/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/pi/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/pi/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/pi/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/pi/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/pi/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/pi/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/pi/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/pi/skills/overhaul/roles/verifier.md +60 -0
- 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
|
+
}
|