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,600 @@
|
|
|
1
|
+
// Package score implements the /overhaul score tool (calculation
|
|
2
|
+
// overhaul-score/1): a deterministic readiness index over a frozen control
|
|
3
|
+
// catalog, its results and its gate records. All arithmetic is exact
|
|
4
|
+
// (math/big.Rat) and thresholds compare at full precision. Exit 2 means
|
|
5
|
+
// invalid input: nothing may be treated as scored.
|
|
6
|
+
package score
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"fmt"
|
|
11
|
+
"io"
|
|
12
|
+
"maps"
|
|
13
|
+
"math/big"
|
|
14
|
+
"os"
|
|
15
|
+
"slices"
|
|
16
|
+
"sort"
|
|
17
|
+
|
|
18
|
+
"github.com/devrites/devrites/internal/overhaul/ovio"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const calc = "overhaul-score/1"
|
|
22
|
+
|
|
23
|
+
const usage = `usage: overhaul score --rubric <r.json> --results <s.json> --gates <g.json> [--out <scorecard.json>]
|
|
24
|
+
overhaul score compare --rubric <r.json> --baseline <a.json> --candidate <b.json>`
|
|
25
|
+
|
|
26
|
+
var (
|
|
27
|
+
statuses = set("PASS", "FAIL", "UNKNOWN", "BLOCKED", "NOT_RUN", "DEFERRED", "ACCEPTED_RISK")
|
|
28
|
+
gateStatuses = set("PASS", "FAIL", "UNKNOWN", "NOT_APPLICABLE", "WAIVED_OPERATIONAL")
|
|
29
|
+
auditGates = []string{"G-MANDATORY", "G-NO-CRITICAL-HIGH", "G-NO-OPEN-SERIOUS-LEAD", "G-COVERAGE",
|
|
30
|
+
"G-CATALOG-ADEQUACY", "G-LANE-RECEIPTS", "G-CONCURRENCY", "G-FRESHNESS", "G-REPORT-CONSISTENCY"}
|
|
31
|
+
repairGates = []string{"G-APPROVAL", "G-TASKS-COMPLETE", "G-ORACLES", "G-NO-REGRESSION", "G-PERF-TARGETS",
|
|
32
|
+
"G-PRESERVATION", "G-FINGERPRINTS", "G-OWNERSHIP", "G-FINAL-CHALLENGE"}
|
|
33
|
+
mayBeNA = set("G-PERF-TARGETS", "G-LANE-RECEIPTS", "G-CONCURRENCY")
|
|
34
|
+
// domainOrder is the fixed domain set in its documented order, which also
|
|
35
|
+
// orders the per-domain threshold rows.
|
|
36
|
+
domainOrder = []string{"correctness", "security", "reliability", "performance", "tests", "architecture", "ux", "operations"}
|
|
37
|
+
defaultPolicy = [][2]string{{"global_min", "9.7"}, {"lane_min", "9.7"}, {"domain_min", "9.0"}, {"lane_domain_min", "9.0"}}
|
|
38
|
+
transitions = map[[2]string]string{
|
|
39
|
+
{"FAIL", "PASS"}: "fixed-or-newly-proven",
|
|
40
|
+
{"UNKNOWN", "PASS"}: "evidence-only",
|
|
41
|
+
{"PASS", "FAIL"}: "regression-or-new-failure",
|
|
42
|
+
{"UNKNOWN", "FAIL"}: "newly-demonstrated-failure",
|
|
43
|
+
{"PASS", "UNKNOWN"}: "evidence-invalidated",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
func set(xs ...string) map[string]bool {
|
|
48
|
+
m := map[string]bool{}
|
|
49
|
+
for _, x := range xs {
|
|
50
|
+
m[x] = true
|
|
51
|
+
}
|
|
52
|
+
return m
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Run executes the tool with its arguments (tool name excluded).
|
|
56
|
+
func Run(args []string, stdout, stderr io.Writer) int {
|
|
57
|
+
out, err := run(args)
|
|
58
|
+
if err != nil {
|
|
59
|
+
fmt.Fprintf(stderr, "ERROR: %v\n", err)
|
|
60
|
+
return 2
|
|
61
|
+
}
|
|
62
|
+
fmt.Fprint(stdout, out)
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func run(args []string) (string, error) {
|
|
67
|
+
if len(args) > 0 && args[0] == "compare" {
|
|
68
|
+
o, err := flags(args[1:], []string{"--rubric", "--baseline", "--candidate"}, nil)
|
|
69
|
+
if err != nil {
|
|
70
|
+
return "", err
|
|
71
|
+
}
|
|
72
|
+
docs, _, err := load(o["--rubric"], o["--baseline"], o["--candidate"])
|
|
73
|
+
if err != nil {
|
|
74
|
+
return "", err
|
|
75
|
+
}
|
|
76
|
+
res, err := compare(docs[0], docs[1], docs[2])
|
|
77
|
+
if err != nil {
|
|
78
|
+
return "", err
|
|
79
|
+
}
|
|
80
|
+
b, err := ovio.MarshalIndent(res)
|
|
81
|
+
return string(b), err
|
|
82
|
+
}
|
|
83
|
+
o, err := flags(args, []string{"--rubric", "--results", "--gates"}, []string{"--out"})
|
|
84
|
+
if err != nil {
|
|
85
|
+
return "", err
|
|
86
|
+
}
|
|
87
|
+
docs, digs, err := load(o["--rubric"], o["--results"], o["--gates"])
|
|
88
|
+
if err != nil {
|
|
89
|
+
return "", err
|
|
90
|
+
}
|
|
91
|
+
if d := docs[1]["rubric_digest"]; d != nil && d != any(digs[0]) {
|
|
92
|
+
return "", fmt.Errorf("results were produced for a different rubric digest")
|
|
93
|
+
}
|
|
94
|
+
out, err := score(docs[0], docs[1], docs[2])
|
|
95
|
+
if err != nil {
|
|
96
|
+
return "", err
|
|
97
|
+
}
|
|
98
|
+
out["inputs"] = map[string]any{"rubric_sha256": digs[0], "results_sha256": digs[1], "gates_sha256": digs[2]}
|
|
99
|
+
b, err := ovio.MarshalIndent(out)
|
|
100
|
+
if err != nil {
|
|
101
|
+
return "", err
|
|
102
|
+
}
|
|
103
|
+
path, ok := o["--out"]
|
|
104
|
+
if !ok {
|
|
105
|
+
return string(b), nil
|
|
106
|
+
}
|
|
107
|
+
if err := os.WriteFile(path, b, 0o644); err != nil {
|
|
108
|
+
return "", err
|
|
109
|
+
}
|
|
110
|
+
return fmt.Sprintf("%s -> %s\n", out["readiness_verdict"], path), nil
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// flags parses "--name value" pairs; every required flag must be present and
|
|
114
|
+
// no other flag is accepted.
|
|
115
|
+
func flags(args, required, optional []string) (map[string]string, error) {
|
|
116
|
+
if len(args)%2 != 0 {
|
|
117
|
+
return nil, fmt.Errorf("every flag needs a value\n%s", usage)
|
|
118
|
+
}
|
|
119
|
+
o := map[string]string{}
|
|
120
|
+
for i := 0; i < len(args); i += 2 {
|
|
121
|
+
if !slices.Contains(required, args[i]) && !slices.Contains(optional, args[i]) {
|
|
122
|
+
return nil, fmt.Errorf("unknown argument %q\n%s", args[i], usage)
|
|
123
|
+
}
|
|
124
|
+
o[args[i]] = args[i+1]
|
|
125
|
+
}
|
|
126
|
+
for _, f := range required {
|
|
127
|
+
if _, ok := o[f]; !ok {
|
|
128
|
+
return nil, fmt.Errorf("missing %s\n%s", f, usage)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return o, nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func load(paths ...string) ([]map[string]any, []string, error) {
|
|
135
|
+
var docs []map[string]any
|
|
136
|
+
var digs []string
|
|
137
|
+
for _, p := range paths {
|
|
138
|
+
m, err := ovio.LoadObject(p)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return nil, nil, err
|
|
141
|
+
}
|
|
142
|
+
d, err := ovio.SHA256File(p)
|
|
143
|
+
if err != nil {
|
|
144
|
+
return nil, nil, err
|
|
145
|
+
}
|
|
146
|
+
docs, digs = append(docs, m), append(digs, d)
|
|
147
|
+
}
|
|
148
|
+
return docs, digs, nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// rat parses a JSON number (or numeric string) exactly.
|
|
152
|
+
func rat(v any) (*big.Rat, bool) {
|
|
153
|
+
s, ok := v.(string)
|
|
154
|
+
if n, isNum := v.(json.Number); isNum {
|
|
155
|
+
s, ok = n.String(), true
|
|
156
|
+
}
|
|
157
|
+
if !ok {
|
|
158
|
+
return nil, false
|
|
159
|
+
}
|
|
160
|
+
return new(big.Rat).SetString(s)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func num(v any, what string) (*big.Rat, error) {
|
|
164
|
+
r, ok := rat(v)
|
|
165
|
+
if !ok {
|
|
166
|
+
return nil, fmt.Errorf("%s is not a finite number: %v", what, v)
|
|
167
|
+
}
|
|
168
|
+
if r.Sign() <= 0 {
|
|
169
|
+
return nil, fmt.Errorf("%s must be > 0: %v", what, v)
|
|
170
|
+
}
|
|
171
|
+
return r, nil
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func needReason(v any, what string) error {
|
|
175
|
+
m := ovio.Obj(v)
|
|
176
|
+
if m == nil || !ovio.Truthy(m["reason"]) || !ovio.Truthy(m["evidence"]) {
|
|
177
|
+
return fmt.Errorf("%s needs reason and evidence", what)
|
|
178
|
+
}
|
|
179
|
+
return nil
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type control struct {
|
|
183
|
+
id, domain string
|
|
184
|
+
lanes []string
|
|
185
|
+
weight *big.Rat
|
|
186
|
+
na, hard, indep bool
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type rubricSpec struct {
|
|
190
|
+
policy map[string]*big.Rat
|
|
191
|
+
dw map[string]*big.Rat
|
|
192
|
+
excluded map[string]bool
|
|
193
|
+
lanes []string
|
|
194
|
+
laneExcl map[[2]string]bool
|
|
195
|
+
controls []control
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func checkRubric(r map[string]any) (*rubricSpec, error) {
|
|
199
|
+
if r["schema"] != "overhaul.rubric/1" {
|
|
200
|
+
return nil, fmt.Errorf("rubric schema must be overhaul.rubric/1")
|
|
201
|
+
}
|
|
202
|
+
errPolicy := fmt.Errorf("policy may only raise the default thresholds (global_min 9.7, lane_min 9.7, domain_min 9.0, lane_domain_min 9.0) and has no other keys")
|
|
203
|
+
pol := ovio.Obj(r["policy"])
|
|
204
|
+
if pol == nil && ovio.Truthy(r["policy"]) {
|
|
205
|
+
return nil, errPolicy
|
|
206
|
+
}
|
|
207
|
+
sp := &rubricSpec{policy: map[string]*big.Rat{}, dw: map[string]*big.Rat{}, excluded: map[string]bool{},
|
|
208
|
+
laneExcl: map[[2]string]bool{}}
|
|
209
|
+
for _, kd := range defaultPolicy {
|
|
210
|
+
def, _ := new(big.Rat).SetString(kd[1])
|
|
211
|
+
val := def
|
|
212
|
+
if v, ok := pol[kd[0]]; ok {
|
|
213
|
+
if val, ok = rat(v); !ok {
|
|
214
|
+
return nil, fmt.Errorf("policy %s is not a finite number: %v", kd[0], v)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if val.Cmp(def) < 0 {
|
|
218
|
+
return nil, errPolicy
|
|
219
|
+
}
|
|
220
|
+
sp.policy[kd[0]] = val
|
|
221
|
+
}
|
|
222
|
+
for k := range pol {
|
|
223
|
+
if sp.policy[k] == nil {
|
|
224
|
+
return nil, errPolicy
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
doms := ovio.Obj(r["domains"])
|
|
228
|
+
for _, d := range slices.Sorted(maps.Keys(doms)) {
|
|
229
|
+
w, err := num(doms[d], "domain weight "+d)
|
|
230
|
+
if err != nil {
|
|
231
|
+
return nil, err
|
|
232
|
+
}
|
|
233
|
+
sp.dw[d] = w
|
|
234
|
+
}
|
|
235
|
+
for _, e := range ovio.List(r["domain_exclusions"]) {
|
|
236
|
+
d, ok := ovio.Obj(e)["domain"].(string)
|
|
237
|
+
if err := needReason(e, "domain exclusion "+d); err != nil {
|
|
238
|
+
return nil, err
|
|
239
|
+
}
|
|
240
|
+
if !ok {
|
|
241
|
+
return nil, fmt.Errorf("domain exclusion needs a domain")
|
|
242
|
+
}
|
|
243
|
+
sp.excluded[d] = true
|
|
244
|
+
}
|
|
245
|
+
known := set(domainOrder...)
|
|
246
|
+
for d := range sp.dw {
|
|
247
|
+
if !known[d] {
|
|
248
|
+
return nil, fmt.Errorf("every domain in %v needs a weight or a recorded exclusion; no others", domainOrder)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for _, d := range domainOrder {
|
|
252
|
+
if sp.dw[d] == nil && !sp.excluded[d] {
|
|
253
|
+
return nil, fmt.Errorf("every domain in %v needs a weight or a recorded exclusion; no others", domainOrder)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
laneSet := map[string]bool{}
|
|
257
|
+
for _, l := range ovio.List(r["lanes"]) {
|
|
258
|
+
s, ok := l.(string)
|
|
259
|
+
if !ok || laneSet[s] {
|
|
260
|
+
return nil, fmt.Errorf("duplicate or non-string lane name: %v", l)
|
|
261
|
+
}
|
|
262
|
+
laneSet[s] = true
|
|
263
|
+
sp.lanes = append(sp.lanes, s)
|
|
264
|
+
}
|
|
265
|
+
for _, e := range ovio.List(r["lane_domain_exclusions"]) {
|
|
266
|
+
l, okL := ovio.Obj(e)["lane"].(string)
|
|
267
|
+
d, okD := ovio.Obj(e)["domain"].(string)
|
|
268
|
+
if err := needReason(e, fmt.Sprintf("lane-domain exclusion %s/%s", l, d)); err != nil {
|
|
269
|
+
return nil, err
|
|
270
|
+
}
|
|
271
|
+
if !okL || !okD {
|
|
272
|
+
return nil, fmt.Errorf("lane-domain exclusion needs a lane and a domain")
|
|
273
|
+
}
|
|
274
|
+
sp.laneExcl[[2]string{l, d}] = true
|
|
275
|
+
}
|
|
276
|
+
seen := map[string]bool{}
|
|
277
|
+
for _, cv := range ovio.List(r["controls"]) {
|
|
278
|
+
c := ovio.Obj(cv)
|
|
279
|
+
id, _ := c["id"].(string)
|
|
280
|
+
if id == "" || seen[id] {
|
|
281
|
+
return nil, fmt.Errorf("missing or duplicate control id: %q", id)
|
|
282
|
+
}
|
|
283
|
+
seen[id] = true
|
|
284
|
+
dom, _ := c["domain"].(string)
|
|
285
|
+
if sp.dw[dom] == nil {
|
|
286
|
+
return nil, fmt.Errorf("%s: unknown domain %v", id, c["domain"])
|
|
287
|
+
}
|
|
288
|
+
var lanes []string
|
|
289
|
+
var bad []any
|
|
290
|
+
for _, l := range ovio.List(c["lanes"]) {
|
|
291
|
+
if s, ok := l.(string); ok && laneSet[s] {
|
|
292
|
+
lanes = append(lanes, s)
|
|
293
|
+
} else {
|
|
294
|
+
bad = append(bad, l)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if len(bad) > 0 {
|
|
298
|
+
return nil, fmt.Errorf("%s: unknown lanes %v", id, bad)
|
|
299
|
+
}
|
|
300
|
+
na := c["na"]
|
|
301
|
+
if na != nil {
|
|
302
|
+
if err := needReason(na, id+" N/A"); err != nil {
|
|
303
|
+
return nil, err
|
|
304
|
+
}
|
|
305
|
+
if ovio.Obj(na)["approved"] != true {
|
|
306
|
+
return nil, fmt.Errorf("%s: N/A needs an approved rationale", id)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
w, err := num(c["weight"], id+" weight")
|
|
310
|
+
if err != nil {
|
|
311
|
+
return nil, err
|
|
312
|
+
}
|
|
313
|
+
sp.controls = append(sp.controls, control{id: id, domain: dom, lanes: lanes, weight: w, na: na != nil,
|
|
314
|
+
hard: ovio.Truthy(c["hard_gate"]), indep: ovio.Truthy(c["independent_verification"])})
|
|
315
|
+
}
|
|
316
|
+
return sp, nil
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// credit returns the control's effective status. Binary credit: only PASS
|
|
320
|
+
// earns; PASS without evidence or required verification counts as UNKNOWN.
|
|
321
|
+
func credit(c control, res map[string]any, notes *[]any) (string, error) {
|
|
322
|
+
st := "UNKNOWN"
|
|
323
|
+
if v, ok := res["status"]; ok {
|
|
324
|
+
s, isStr := v.(string)
|
|
325
|
+
if !isStr || !statuses[s] {
|
|
326
|
+
return "", fmt.Errorf("%s: unknown status %v", c.id, v)
|
|
327
|
+
}
|
|
328
|
+
st = s
|
|
329
|
+
}
|
|
330
|
+
if st == "PASS" && !ovio.Truthy(res["evidence"]) {
|
|
331
|
+
*notes = append(*notes, map[string]any{"control": c.id, "from": "PASS", "to": "UNKNOWN", "reason": "no evidence"})
|
|
332
|
+
st = "UNKNOWN"
|
|
333
|
+
}
|
|
334
|
+
if st == "PASS" && c.indep && !ovio.Truthy(res["verified_by"]) {
|
|
335
|
+
*notes = append(*notes, map[string]any{"control": c.id, "from": "PASS", "to": "UNKNOWN", "reason": "no independent verification"})
|
|
336
|
+
st = "UNKNOWN"
|
|
337
|
+
}
|
|
338
|
+
return st, nil
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// agg is 10 * SUM(w*p) / SUM(w) over the kept controls; nil on a zero denominator.
|
|
342
|
+
func agg(live []control, pass map[string]bool, keep func(control) bool) *big.Rat {
|
|
343
|
+
den, sum := new(big.Rat), new(big.Rat)
|
|
344
|
+
for _, c := range live {
|
|
345
|
+
if keep(c) {
|
|
346
|
+
den.Add(den, c.weight)
|
|
347
|
+
if pass[c.id] {
|
|
348
|
+
sum.Add(sum, c.weight)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if den.Sign() == 0 {
|
|
353
|
+
return nil
|
|
354
|
+
}
|
|
355
|
+
sum.Mul(sum, big.NewRat(10, 1))
|
|
356
|
+
return sum.Quo(sum, den)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// weighted is SUM(W_d * v_d) / SUM(W_d); nil when ds is empty or any v_d is nil.
|
|
360
|
+
func weighted(ds []string, w, v map[string]*big.Rat) (q, den *big.Rat) {
|
|
361
|
+
den, sum := new(big.Rat), new(big.Rat)
|
|
362
|
+
ok := len(ds) > 0
|
|
363
|
+
for _, d := range ds {
|
|
364
|
+
den.Add(den, w[d])
|
|
365
|
+
if v[d] == nil {
|
|
366
|
+
ok = false
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
sum.Add(sum, new(big.Rat).Mul(w[d], v[d]))
|
|
370
|
+
}
|
|
371
|
+
if !ok {
|
|
372
|
+
return nil, den
|
|
373
|
+
}
|
|
374
|
+
return sum.Quo(sum, den), den
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func show(x *big.Rat) any {
|
|
378
|
+
if x == nil {
|
|
379
|
+
return nil
|
|
380
|
+
}
|
|
381
|
+
return map[string]any{"exact": x.Num().String() + "/" + x.Denom().String(), "decimal": x.FloatString(12)}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
func score(rubric, results, gates map[string]any) (map[string]any, error) {
|
|
385
|
+
sp, err := checkRubric(rubric)
|
|
386
|
+
if err != nil {
|
|
387
|
+
return nil, err
|
|
388
|
+
}
|
|
389
|
+
if results["schema"] != "overhaul.results/1" {
|
|
390
|
+
return nil, fmt.Errorf("results schema must be overhaul.results/1")
|
|
391
|
+
}
|
|
392
|
+
rres := ovio.Obj(results["results"])
|
|
393
|
+
if rres == nil && ovio.Truthy(results["results"]) {
|
|
394
|
+
return nil, fmt.Errorf("results.results must be an object")
|
|
395
|
+
}
|
|
396
|
+
ids := map[string]bool{}
|
|
397
|
+
for _, c := range sp.controls {
|
|
398
|
+
ids[c.id] = true
|
|
399
|
+
}
|
|
400
|
+
var unknown []string
|
|
401
|
+
for k := range rres {
|
|
402
|
+
if !ids[k] {
|
|
403
|
+
unknown = append(unknown, k)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if len(unknown) > 0 {
|
|
407
|
+
sort.Strings(unknown)
|
|
408
|
+
return nil, fmt.Errorf("results for controls not in rubric: %v", unknown)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
notes := []any{}
|
|
412
|
+
status := map[string]string{}
|
|
413
|
+
pass := map[string]bool{}
|
|
414
|
+
var live []control
|
|
415
|
+
passes := 0
|
|
416
|
+
for _, c := range sp.controls {
|
|
417
|
+
if c.na {
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
rv, present := rres[c.id]
|
|
421
|
+
if present && ovio.Obj(rv) == nil {
|
|
422
|
+
return nil, fmt.Errorf("%s: result must be an object", c.id)
|
|
423
|
+
}
|
|
424
|
+
st, err := credit(c, ovio.Obj(rv), ¬es)
|
|
425
|
+
if err != nil {
|
|
426
|
+
return nil, err
|
|
427
|
+
}
|
|
428
|
+
status[c.id], pass[c.id] = st, st == "PASS"
|
|
429
|
+
if st == "PASS" {
|
|
430
|
+
passes++
|
|
431
|
+
}
|
|
432
|
+
live = append(live, c)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
var domains []string
|
|
436
|
+
for _, d := range domainOrder {
|
|
437
|
+
if sp.dw[d] != nil && !sp.excluded[d] {
|
|
438
|
+
domains = append(domains, d)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
dom, domShow := map[string]*big.Rat{}, map[string]any{}
|
|
442
|
+
for _, d := range domains {
|
|
443
|
+
dom[d] = agg(live, pass, func(c control) bool { return c.domain == d })
|
|
444
|
+
domShow[d] = show(dom[d])
|
|
445
|
+
}
|
|
446
|
+
q, _ := weighted(domains, sp.dw, dom)
|
|
447
|
+
|
|
448
|
+
type check struct {
|
|
449
|
+
scope string
|
|
450
|
+
v, least *big.Rat
|
|
451
|
+
}
|
|
452
|
+
checks := []check{{"global", q, sp.policy["global_min"]}}
|
|
453
|
+
for _, d := range domains {
|
|
454
|
+
checks = append(checks, check{"domain " + d, dom[d], sp.policy["domain_min"]})
|
|
455
|
+
}
|
|
456
|
+
laneOut := map[string]any{}
|
|
457
|
+
appearances := 0
|
|
458
|
+
for _, c := range live {
|
|
459
|
+
appearances += len(c.lanes)
|
|
460
|
+
}
|
|
461
|
+
for _, l := range sp.lanes {
|
|
462
|
+
var lds []string
|
|
463
|
+
for _, d := range domains {
|
|
464
|
+
if !sp.laneExcl[[2]string{l, d}] {
|
|
465
|
+
lds = append(lds, d)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
ld, ldShow := map[string]*big.Rat{}, map[string]any{}
|
|
469
|
+
for _, d := range lds {
|
|
470
|
+
ld[d] = agg(live, pass, func(c control) bool { return c.domain == d && slices.Contains(c.lanes, l) })
|
|
471
|
+
ldShow[d] = show(ld[d])
|
|
472
|
+
}
|
|
473
|
+
ql, den := weighted(lds, sp.dw, ld)
|
|
474
|
+
laneOut[l] = map[string]any{"Q": show(ql), "domains": ldShow, "weight_denominator": den.RatString()}
|
|
475
|
+
checks = append(checks, check{"lane " + l, ql, sp.policy["lane_min"]})
|
|
476
|
+
for _, d := range lds {
|
|
477
|
+
checks = append(checks, check{"lane " + l + " / " + d, ld[d], sp.policy["lane_domain_min"]})
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
rows := []any{}
|
|
482
|
+
assessable, allPass := true, true
|
|
483
|
+
for _, ck := range checks {
|
|
484
|
+
res := "PASS"
|
|
485
|
+
switch {
|
|
486
|
+
case ck.v == nil:
|
|
487
|
+
res, assessable = "NOT_ASSESSABLE", false
|
|
488
|
+
case ck.v.Cmp(ck.least) < 0:
|
|
489
|
+
res, allPass = "FAIL", false
|
|
490
|
+
}
|
|
491
|
+
rows = append(rows, map[string]any{"scope": ck.scope, "value": show(ck.v), "min": ck.least.RatString(), "result": res})
|
|
492
|
+
}
|
|
493
|
+
mandatory := []string{}
|
|
494
|
+
for _, c := range live {
|
|
495
|
+
if c.hard && !pass[c.id] {
|
|
496
|
+
mandatory = append(mandatory, c.id)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
mode := gates["mode"]
|
|
501
|
+
if mode != "audit" && mode != "remediation" {
|
|
502
|
+
return nil, fmt.Errorf("gates.mode must be audit or remediation")
|
|
503
|
+
}
|
|
504
|
+
gateOut := map[string]string{"G-THRESHOLDS": "FAIL", "G-MANDATORY": "PASS"}
|
|
505
|
+
if !assessable {
|
|
506
|
+
gateOut["G-THRESHOLDS"] = "UNKNOWN"
|
|
507
|
+
} else if allPass {
|
|
508
|
+
gateOut["G-THRESHOLDS"] = "PASS"
|
|
509
|
+
}
|
|
510
|
+
if len(mandatory) > 0 {
|
|
511
|
+
gateOut["G-MANDATORY"] = "FAIL"
|
|
512
|
+
}
|
|
513
|
+
names := auditGates
|
|
514
|
+
if mode == "remediation" {
|
|
515
|
+
names = append(slices.Clone(auditGates), repairGates...)
|
|
516
|
+
}
|
|
517
|
+
given := ovio.Obj(gates["gates"])
|
|
518
|
+
for _, g := range names {
|
|
519
|
+
if gateOut[g] != "" {
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
e := ovio.Obj(given[g])
|
|
523
|
+
if e == nil && ovio.Truthy(given[g]) {
|
|
524
|
+
return nil, fmt.Errorf("%s: gate record must be an object", g)
|
|
525
|
+
}
|
|
526
|
+
st := "UNKNOWN"
|
|
527
|
+
if v, ok := e["status"]; ok {
|
|
528
|
+
s, isStr := v.(string)
|
|
529
|
+
if !isStr || !gateStatuses[s] {
|
|
530
|
+
return nil, fmt.Errorf("%s: unknown gate status %v", g, v)
|
|
531
|
+
}
|
|
532
|
+
st = s
|
|
533
|
+
}
|
|
534
|
+
if st == "PASS" && !ovio.Truthy(e["evidence"]) {
|
|
535
|
+
st = "UNKNOWN"
|
|
536
|
+
notes = append(notes, map[string]any{"gate": g, "from": "PASS", "to": "UNKNOWN", "reason": "no evidence"})
|
|
537
|
+
}
|
|
538
|
+
if st == "NOT_APPLICABLE" && (!mayBeNA[g] || !ovio.Truthy(e["reason"])) {
|
|
539
|
+
return nil, fmt.Errorf("%s cannot be NOT_APPLICABLE without an allowed reason", g)
|
|
540
|
+
}
|
|
541
|
+
if st == "WAIVED_OPERATIONAL" && (g != "G-CONCURRENCY" || !ovio.Truthy(e["approval"])) {
|
|
542
|
+
return nil, fmt.Errorf("only G-CONCURRENCY may be WAIVED_OPERATIONAL, with an approval reference")
|
|
543
|
+
}
|
|
544
|
+
gateOut[g] = st
|
|
545
|
+
}
|
|
546
|
+
open := []string{}
|
|
547
|
+
for _, g := range slices.Sorted(maps.Keys(gateOut)) {
|
|
548
|
+
if s := gateOut[g]; s != "PASS" && s != "NOT_APPLICABLE" {
|
|
549
|
+
open = append(open, g)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
verdict := "NOT_READY"
|
|
553
|
+
switch {
|
|
554
|
+
case !assessable:
|
|
555
|
+
verdict = "NOT_ASSESSABLE"
|
|
556
|
+
case len(open) == 0:
|
|
557
|
+
verdict = "MEETS_TARGETS"
|
|
558
|
+
case len(open) == 1 && open[0] == "G-CONCURRENCY" && gateOut["G-CONCURRENCY"] == "WAIVED_OPERATIONAL":
|
|
559
|
+
verdict = "MEETS_TARGETS_WITH_APPROVED_DEGRADATION"
|
|
560
|
+
}
|
|
561
|
+
excluded := slices.AppendSeq([]string{}, maps.Keys(sp.excluded))
|
|
562
|
+
sort.Strings(excluded)
|
|
563
|
+
|
|
564
|
+
return map[string]any{
|
|
565
|
+
"schema": "overhaul.scorecard/1", "calculation": calc, "subject": results["subject"],
|
|
566
|
+
"fingerprint": results["fingerprint"], "rubric_rev": rubric["rev"],
|
|
567
|
+
"global": map[string]any{"Q": show(q), "domains": domShow, "excluded_domains": excluded},
|
|
568
|
+
"lanes": laneOut, "thresholds": rows, "gates": gateOut,
|
|
569
|
+
"mandatory_failures": mandatory, "downgrades": notes, "status_by_control": status,
|
|
570
|
+
"counts": map[string]any{"controls": len(sp.controls), "not_applicable": len(sp.controls) - len(live),
|
|
571
|
+
"pass": passes, "lane_appearances": appearances},
|
|
572
|
+
"readiness_verdict": verdict, "open_gates": open,
|
|
573
|
+
}, nil
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// compare scores baseline and candidate on one rubric and labels each
|
|
577
|
+
// per-control status transition.
|
|
578
|
+
func compare(rubric, a, b map[string]any) (map[string]any, error) {
|
|
579
|
+
sa, err := score(rubric, a, map[string]any{"mode": "audit"})
|
|
580
|
+
if err != nil {
|
|
581
|
+
return nil, fmt.Errorf("baseline: %w", err)
|
|
582
|
+
}
|
|
583
|
+
sb, err := score(rubric, b, map[string]any{"mode": "audit"})
|
|
584
|
+
if err != nil {
|
|
585
|
+
return nil, fmt.Errorf("candidate: %w", err)
|
|
586
|
+
}
|
|
587
|
+
before, after := sa["status_by_control"].(map[string]string), sb["status_by_control"].(map[string]string)
|
|
588
|
+
rows := []any{}
|
|
589
|
+
for _, id := range slices.Sorted(maps.Keys(before)) {
|
|
590
|
+
if before[id] != after[id] {
|
|
591
|
+
kind := transitions[[2]string{before[id], after[id]}]
|
|
592
|
+
if kind == "" {
|
|
593
|
+
kind = "status-change"
|
|
594
|
+
}
|
|
595
|
+
rows = append(rows, map[string]any{"control": id, "from": before[id], "to": after[id], "kind": kind})
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return map[string]any{"baseline_Q": sa["global"].(map[string]any)["Q"],
|
|
599
|
+
"candidate_Q": sb["global"].(map[string]any)["Q"], "transitions": rows}, nil
|
|
600
|
+
}
|