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.
- package/CHANGELOG.md +12 -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/install/binary.go +4 -0
- package/engine/internal/install/install_test.go +22 -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,139 @@
|
|
|
1
|
+
package bench
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"math/rand/v2"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func gauss(rng *rand.Rand, mean, sd float64, n int) []float64 {
|
|
14
|
+
out := make([]float64, n)
|
|
15
|
+
for i := range out {
|
|
16
|
+
out[i] = mean + rng.NormFloat64()*sd
|
|
17
|
+
}
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func interleaved(n int) []string {
|
|
22
|
+
var o []string
|
|
23
|
+
for range n {
|
|
24
|
+
o = append(o, "A", "B")
|
|
25
|
+
}
|
|
26
|
+
return o
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// run writes d as the result file, runs the CLI and decodes its output.
|
|
30
|
+
func run(t *testing.T, d map[string]any) (int, map[string]any, string) {
|
|
31
|
+
t.Helper()
|
|
32
|
+
b, _ := json.Marshal(d)
|
|
33
|
+
path := filepath.Join(t.TempDir(), "result.json")
|
|
34
|
+
if err := os.WriteFile(path, b, 0o600); err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
var out, errb bytes.Buffer
|
|
38
|
+
code := Run([]string{path}, &out, &errb)
|
|
39
|
+
var res map[string]any
|
|
40
|
+
if code == 0 {
|
|
41
|
+
if err := json.Unmarshal(out.Bytes(), &res); err != nil {
|
|
42
|
+
t.Fatalf("bad output %q: %v", out.String(), err)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return code, res, errb.String()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func with(d map[string]any, kv ...any) map[string]any {
|
|
49
|
+
c := map[string]any{}
|
|
50
|
+
for k, v := range d {
|
|
51
|
+
c[k] = v
|
|
52
|
+
}
|
|
53
|
+
for i := 0; i < len(kv); i += 2 {
|
|
54
|
+
c[kv[i].(string)] = kv[i+1]
|
|
55
|
+
}
|
|
56
|
+
return c
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func verdict(t *testing.T, d map[string]any) (string, map[string]any) {
|
|
60
|
+
t.Helper()
|
|
61
|
+
code, res, stderr := run(t, d)
|
|
62
|
+
if code != 0 {
|
|
63
|
+
t.Fatalf("exit %d: %s", code, stderr)
|
|
64
|
+
}
|
|
65
|
+
return res["verdict"].(string), res
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func fixture() (fast map[string]any, rng *rand.Rand) {
|
|
69
|
+
rng = rand.New(rand.NewPCG(7, 0))
|
|
70
|
+
fast = map[string]any{
|
|
71
|
+
"direction": "lower_is_better",
|
|
72
|
+
"baseline": gauss(rng, 30, 1, 20), "candidate": gauss(rng, 10, 0.4, 20),
|
|
73
|
+
"order": interleaved(20), "target": 2.0, "materiality": 1.05,
|
|
74
|
+
}
|
|
75
|
+
return fast, rng
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func TestBench(t *testing.T) {
|
|
79
|
+
fast, rng := fixture()
|
|
80
|
+
|
|
81
|
+
t.Run("clear 3x speedup supports target 2", func(t *testing.T) {
|
|
82
|
+
if v, res := verdict(t, fast); v != "SUPPORTS_TARGET" {
|
|
83
|
+
t.Fatalf("got %v", res)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
t.Run("noisy data does not support target", func(t *testing.T) {
|
|
87
|
+
noisy := map[string]any{
|
|
88
|
+
"direction": "lower_is_better", "baseline": gauss(rng, 20, 9, 12), "candidate": gauss(rng, 10, 6, 12),
|
|
89
|
+
"order": interleaved(12), "target": 2.0, "materiality": 1.05,
|
|
90
|
+
}
|
|
91
|
+
v, res := verdict(t, noisy)
|
|
92
|
+
if lo := res["ci95"].([]any)[0].(float64); v == "SUPPORTS_TARGET" || lo >= 2 {
|
|
93
|
+
t.Fatalf("got %v", res)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
t.Run("three cherry-picked samples are inconclusive", func(t *testing.T) {
|
|
97
|
+
picked := with(fast, "baseline", fast["baseline"].([]float64)[:3],
|
|
98
|
+
"candidate", fast["candidate"].([]float64)[:3], "order", interleaved(3))
|
|
99
|
+
if v, res := verdict(t, picked); v != "INCONCLUSIVE" {
|
|
100
|
+
t.Fatalf("got %v", res)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
t.Run("blocked order is inconclusive", func(t *testing.T) {
|
|
104
|
+
order := append(strings.Split(strings.Repeat("A", 20), ""), strings.Split(strings.Repeat("B", 20), "")...)
|
|
105
|
+
if v, res := verdict(t, with(fast, "order", order)); v != "INCONCLUSIVE" {
|
|
106
|
+
t.Fatalf("got %v", res)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
t.Run("swapped arms regress", func(t *testing.T) {
|
|
110
|
+
if v, res := verdict(t, with(fast, "baseline", fast["candidate"], "candidate", fast["baseline"])); v != "REGRESSION" {
|
|
111
|
+
t.Fatalf("got %v", res)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
t.Run("zero baseline has no ratio", func(t *testing.T) {
|
|
115
|
+
if v, res := verdict(t, with(fast, "baseline", make([]float64, 20))); v != "NO_RATIO" {
|
|
116
|
+
t.Fatalf("got %v", res)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
t.Run("higher is better throughput", func(t *testing.T) {
|
|
120
|
+
thr := map[string]any{
|
|
121
|
+
"direction": "higher_is_better", "baseline": gauss(rng, 100, 2, 15), "candidate": gauss(rng, 320, 5, 15),
|
|
122
|
+
"order": interleaved(15), "target": 3.0,
|
|
123
|
+
}
|
|
124
|
+
if v, res := verdict(t, thr); v != "SUPPORTS_TARGET" {
|
|
125
|
+
t.Fatalf("got %v", res)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
t.Run("unknown or missing direction is an error", func(t *testing.T) {
|
|
129
|
+
swapped := with(fast, "baseline", fast["candidate"], "candidate", fast["baseline"])
|
|
130
|
+
missing := with(swapped)
|
|
131
|
+
delete(missing, "direction")
|
|
132
|
+
for _, d := range []map[string]any{with(swapped, "direction", "latency_ms"), missing} {
|
|
133
|
+
code, _, stderr := run(t, d)
|
|
134
|
+
if code != 2 || !strings.Contains(stderr, "direction must be lower_is_better or higher_is_better") {
|
|
135
|
+
t.Fatalf("exit %d stderr %q", code, stderr)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Package overhaul dispatches the deterministic tools behind the standalone
|
|
2
|
+
// /overhaul skill: run-record generations and validation, worker-output
|
|
3
|
+
// admission, baseline snapshots, readiness scoring, benchmark statistics and
|
|
4
|
+
// escaped offline views. It never calls a model or the network.
|
|
5
|
+
package overhaul
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
|
|
11
|
+
"github.com/devrites/devrites/internal/overhaul/admit"
|
|
12
|
+
"github.com/devrites/devrites/internal/overhaul/bench"
|
|
13
|
+
"github.com/devrites/devrites/internal/overhaul/records"
|
|
14
|
+
"github.com/devrites/devrites/internal/overhaul/render"
|
|
15
|
+
"github.com/devrites/devrites/internal/overhaul/score"
|
|
16
|
+
"github.com/devrites/devrites/internal/overhaul/snapshot"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Usage is the command-family help text.
|
|
20
|
+
const Usage = `usage: devrites-engine overhaul <tool> ...
|
|
21
|
+
|
|
22
|
+
overhaul records digest <file>
|
|
23
|
+
overhaul records stage <run>
|
|
24
|
+
overhaul records publish <run>
|
|
25
|
+
overhaul records validate <run>
|
|
26
|
+
overhaul admit receipt <run> <receipt.json> [--observed <paths.txt>]
|
|
27
|
+
overhaul admit anchor <tree> <proposals.json>
|
|
28
|
+
overhaul snapshot capture <repo> <out>
|
|
29
|
+
overhaul snapshot fingerprint <repo>
|
|
30
|
+
overhaul snapshot verify <repo> <out> [--agent-paths <file>]
|
|
31
|
+
overhaul snapshot state <repo> <out.json>
|
|
32
|
+
overhaul snapshot delta <before.json> <repo>
|
|
33
|
+
overhaul score --rubric <r.json> --results <s.json> --gates <g.json> [--out <scorecard.json>]
|
|
34
|
+
overhaul score compare --rubric <r.json> --baseline <a.json> --candidate <b.json>
|
|
35
|
+
overhaul bench <result.json>
|
|
36
|
+
overhaul render <run> <staged-generation> <review|report>
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
// Run executes one overhaul tool. Exit codes follow each tool: 0 ok, 1 rule
|
|
40
|
+
// violation or rejection, 2 usage or I/O error, 3 duplicate or late receipt.
|
|
41
|
+
func Run(args []string, stdout, stderr io.Writer) int {
|
|
42
|
+
if len(args) == 0 {
|
|
43
|
+
fmt.Fprint(stderr, Usage)
|
|
44
|
+
return 2
|
|
45
|
+
}
|
|
46
|
+
rest := args[1:]
|
|
47
|
+
switch args[0] {
|
|
48
|
+
case "records":
|
|
49
|
+
return records.Run(rest, stdout, stderr)
|
|
50
|
+
case "admit":
|
|
51
|
+
return admit.Run(rest, stdout, stderr)
|
|
52
|
+
case "snapshot":
|
|
53
|
+
return snapshot.Run(rest, stdout, stderr)
|
|
54
|
+
case "score":
|
|
55
|
+
return score.Run(rest, stdout, stderr)
|
|
56
|
+
case "bench":
|
|
57
|
+
return bench.Run(rest, stdout, stderr)
|
|
58
|
+
case "render":
|
|
59
|
+
return render.Run(rest, stdout, stderr)
|
|
60
|
+
default:
|
|
61
|
+
fmt.Fprintf(stderr, "devrites: unknown overhaul tool %q\n\n%s", args[0], Usage)
|
|
62
|
+
return 2
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Package ovio holds the small file, digest and JSON helpers shared by the
|
|
2
|
+
// /overhaul run-area tools. Run records are plain JSON documents read as
|
|
3
|
+
// generic maps so the tools enforce exactly the documented fields.
|
|
4
|
+
package ovio
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"bytes"
|
|
8
|
+
"crypto/sha256"
|
|
9
|
+
"encoding/hex"
|
|
10
|
+
"encoding/json"
|
|
11
|
+
"fmt"
|
|
12
|
+
"os"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"strings"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// SHA256Bytes returns the lowercase hex SHA-256 of b.
|
|
18
|
+
func SHA256Bytes(b []byte) string {
|
|
19
|
+
sum := sha256.Sum256(b)
|
|
20
|
+
return hex.EncodeToString(sum[:])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// SHA256File returns the lowercase hex SHA-256 of the file's exact bytes.
|
|
24
|
+
func SHA256File(path string) (string, error) {
|
|
25
|
+
b, err := os.ReadFile(path) // #nosec G304 -- callers pass operator-selected run-area paths
|
|
26
|
+
if err != nil {
|
|
27
|
+
return "", err
|
|
28
|
+
}
|
|
29
|
+
return SHA256Bytes(b), nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// LoadJSON decodes a UTF-8 JSON document. Numbers stay json.Number so digests,
|
|
33
|
+
// weights and ratios are never rounded through float64 by accident.
|
|
34
|
+
func LoadJSON(path string) (any, error) {
|
|
35
|
+
b, err := os.ReadFile(path) // #nosec G304 -- callers pass operator-selected run-area paths
|
|
36
|
+
if err != nil {
|
|
37
|
+
return nil, err
|
|
38
|
+
}
|
|
39
|
+
dec := json.NewDecoder(bytes.NewReader(b))
|
|
40
|
+
dec.UseNumber()
|
|
41
|
+
var v any
|
|
42
|
+
if err := dec.Decode(&v); err != nil {
|
|
43
|
+
return nil, fmt.Errorf("%s: invalid JSON (%v)", path, err)
|
|
44
|
+
}
|
|
45
|
+
return v, nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// LoadObject decodes a JSON document that must be an object.
|
|
49
|
+
func LoadObject(path string) (map[string]any, error) {
|
|
50
|
+
v, err := LoadJSON(path)
|
|
51
|
+
if err != nil {
|
|
52
|
+
return nil, err
|
|
53
|
+
}
|
|
54
|
+
m, ok := v.(map[string]any)
|
|
55
|
+
if !ok {
|
|
56
|
+
return nil, fmt.Errorf("%s: expected a JSON object", path)
|
|
57
|
+
}
|
|
58
|
+
return m, nil
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// MarshalIndent renders v as two-space indented JSON with sorted object keys
|
|
62
|
+
// and a trailing newline.
|
|
63
|
+
func MarshalIndent(v any) ([]byte, error) {
|
|
64
|
+
b, err := json.MarshalIndent(v, "", " ")
|
|
65
|
+
if err != nil {
|
|
66
|
+
return nil, err
|
|
67
|
+
}
|
|
68
|
+
return append(b, '\n'), nil
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// WriteJSON writes v as indented JSON.
|
|
72
|
+
func WriteJSON(path string, v any, perm os.FileMode) error {
|
|
73
|
+
b, err := MarshalIndent(v)
|
|
74
|
+
if err != nil {
|
|
75
|
+
return err
|
|
76
|
+
}
|
|
77
|
+
return os.WriteFile(path, b, perm)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// resolve follows symlinks for the longest existing prefix of path, like
|
|
81
|
+
// Python's os.path.realpath on a path that may not exist yet.
|
|
82
|
+
func resolve(path string) string {
|
|
83
|
+
path = filepath.Clean(path)
|
|
84
|
+
if r, err := filepath.EvalSymlinks(path); err == nil {
|
|
85
|
+
return r
|
|
86
|
+
}
|
|
87
|
+
parent := filepath.Dir(path)
|
|
88
|
+
if parent == path {
|
|
89
|
+
return path
|
|
90
|
+
}
|
|
91
|
+
return filepath.Join(resolve(parent), filepath.Base(path))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Resolve is the exported form of resolve for callers that compare paths.
|
|
95
|
+
func Resolve(path string) string { return resolve(path) }
|
|
96
|
+
|
|
97
|
+
// Get walks a dotted path through nested objects; missing keys give nil.
|
|
98
|
+
func Get(v any, dotted string) any {
|
|
99
|
+
for _, k := range strings.Split(dotted, ".") {
|
|
100
|
+
m, ok := v.(map[string]any)
|
|
101
|
+
if !ok {
|
|
102
|
+
return nil
|
|
103
|
+
}
|
|
104
|
+
v = m[k]
|
|
105
|
+
}
|
|
106
|
+
return v
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Str returns v as a string, or "" when it is not one.
|
|
110
|
+
func Str(v any) string {
|
|
111
|
+
s, _ := v.(string)
|
|
112
|
+
return s
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// List returns v as a slice, or nil when it is not one.
|
|
116
|
+
func List(v any) []any {
|
|
117
|
+
l, _ := v.([]any)
|
|
118
|
+
return l
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Obj returns v as an object, or nil when it is not one.
|
|
122
|
+
func Obj(v any) map[string]any {
|
|
123
|
+
m, _ := v.(map[string]any)
|
|
124
|
+
return m
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Strings returns the string members of a JSON array.
|
|
128
|
+
func Strings(v any) []string {
|
|
129
|
+
var out []string
|
|
130
|
+
for _, x := range List(v) {
|
|
131
|
+
if s, ok := x.(string); ok {
|
|
132
|
+
out = append(out, s)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Truthy mirrors JSON truthiness used by the run records: nil, false, "",
|
|
139
|
+
// 0, empty arrays and empty objects are false.
|
|
140
|
+
func Truthy(v any) bool {
|
|
141
|
+
switch x := v.(type) {
|
|
142
|
+
case nil:
|
|
143
|
+
return false
|
|
144
|
+
case bool:
|
|
145
|
+
return x
|
|
146
|
+
case string:
|
|
147
|
+
return x != ""
|
|
148
|
+
case json.Number:
|
|
149
|
+
return x.String() != "0" && x.String() != "0.0"
|
|
150
|
+
case float64:
|
|
151
|
+
return x != 0
|
|
152
|
+
case []any:
|
|
153
|
+
return len(x) > 0
|
|
154
|
+
case map[string]any:
|
|
155
|
+
return len(x) > 0
|
|
156
|
+
}
|
|
157
|
+
return true
|
|
158
|
+
}
|