devrites 5.12.0 → 5.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -3
  3. package/docs/architecture.md +2 -2
  4. package/docs/command-map.md +3 -1
  5. package/docs/engine/commands.md +21 -0
  6. package/docs/flow.md +2 -1
  7. package/docs/skills.md +13 -4
  8. package/engine/help.go +4 -1
  9. package/engine/help_test.go +2 -0
  10. package/engine/internal/install/binary.go +4 -0
  11. package/engine/internal/install/install_test.go +22 -0
  12. package/engine/internal/overhaul/admit/admit_test.go +170 -0
  13. package/engine/internal/overhaul/admit/run.go +416 -0
  14. package/engine/internal/overhaul/bench/run.go +248 -0
  15. package/engine/internal/overhaul/bench/run_test.go +139 -0
  16. package/engine/internal/overhaul/overhaul.go +64 -0
  17. package/engine/internal/overhaul/ovio/ovio.go +158 -0
  18. package/engine/internal/overhaul/records/check.go +834 -0
  19. package/engine/internal/overhaul/records/records_test.go +577 -0
  20. package/engine/internal/overhaul/records/run.go +392 -0
  21. package/engine/internal/overhaul/render/run.go +776 -0
  22. package/engine/internal/overhaul/render/run_test.go +293 -0
  23. package/engine/internal/overhaul/score/run.go +600 -0
  24. package/engine/internal/overhaul/score/run_test.go +405 -0
  25. package/engine/internal/overhaul/snapshot/run.go +563 -0
  26. package/engine/internal/overhaul/snapshot/run_test.go +360 -0
  27. package/engine/main.go +4 -0
  28. package/engine/root_routing_test.go +2 -1
  29. package/pack/.claude/skills/overhaul/SKILL.md +201 -0
  30. package/pack/.claude/skills/overhaul/references/audit-domains.md +263 -0
  31. package/pack/.claude/skills/overhaul/references/lifecycle.md +308 -0
  32. package/pack/.claude/skills/overhaul/references/orchestration.md +264 -0
  33. package/pack/.claude/skills/overhaul/references/records.md +137 -0
  34. package/pack/.claude/skills/overhaul/references/repair-and-measurement.md +207 -0
  35. package/pack/.claude/skills/overhaul/references/review-and-report.md +137 -0
  36. package/pack/.claude/skills/overhaul/references/scope-and-safety.md +191 -0
  37. package/pack/.claude/skills/overhaul/references/scoring.md +222 -0
  38. package/pack/.claude/skills/overhaul/references/stack-profiles.md +264 -0
  39. package/pack/.claude/skills/overhaul/roles/backend-engineer.md +61 -0
  40. package/pack/.claude/skills/overhaul/roles/boundary.md +56 -0
  41. package/pack/.claude/skills/overhaul/roles/craft.md +77 -0
  42. package/pack/.claude/skills/overhaul/roles/frontend-engineer.md +63 -0
  43. package/pack/.claude/skills/overhaul/roles/implementer.md +59 -0
  44. package/pack/.claude/skills/overhaul/roles/recon.md +60 -0
  45. package/pack/.claude/skills/overhaul/roles/specialist.md +56 -0
  46. package/pack/.claude/skills/overhaul/roles/verifier.md +60 -0
  47. package/pack/generated/claude/skills/overhaul/SKILL.md +201 -0
  48. package/pack/generated/claude/skills/overhaul/references/audit-domains.md +263 -0
  49. package/pack/generated/claude/skills/overhaul/references/lifecycle.md +308 -0
  50. package/pack/generated/claude/skills/overhaul/references/orchestration.md +264 -0
  51. package/pack/generated/claude/skills/overhaul/references/records.md +137 -0
  52. package/pack/generated/claude/skills/overhaul/references/repair-and-measurement.md +207 -0
  53. package/pack/generated/claude/skills/overhaul/references/review-and-report.md +137 -0
  54. package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +191 -0
  55. package/pack/generated/claude/skills/overhaul/references/scoring.md +222 -0
  56. package/pack/generated/claude/skills/overhaul/references/stack-profiles.md +264 -0
  57. package/pack/generated/claude/skills/overhaul/roles/backend-engineer.md +61 -0
  58. package/pack/generated/claude/skills/overhaul/roles/boundary.md +56 -0
  59. package/pack/generated/claude/skills/overhaul/roles/craft.md +77 -0
  60. package/pack/generated/claude/skills/overhaul/roles/frontend-engineer.md +63 -0
  61. package/pack/generated/claude/skills/overhaul/roles/implementer.md +59 -0
  62. package/pack/generated/claude/skills/overhaul/roles/recon.md +60 -0
  63. package/pack/generated/claude/skills/overhaul/roles/specialist.md +56 -0
  64. package/pack/generated/claude/skills/overhaul/roles/verifier.md +60 -0
  65. package/pack/generated/codex/skills/overhaul/SKILL.md +201 -0
  66. package/pack/generated/codex/skills/overhaul/agents/openai.yaml +2 -0
  67. package/pack/generated/codex/skills/overhaul/references/audit-domains.md +263 -0
  68. package/pack/generated/codex/skills/overhaul/references/lifecycle.md +308 -0
  69. package/pack/generated/codex/skills/overhaul/references/orchestration.md +264 -0
  70. package/pack/generated/codex/skills/overhaul/references/records.md +137 -0
  71. package/pack/generated/codex/skills/overhaul/references/repair-and-measurement.md +207 -0
  72. package/pack/generated/codex/skills/overhaul/references/review-and-report.md +137 -0
  73. package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +191 -0
  74. package/pack/generated/codex/skills/overhaul/references/scoring.md +222 -0
  75. package/pack/generated/codex/skills/overhaul/references/stack-profiles.md +264 -0
  76. package/pack/generated/codex/skills/overhaul/roles/backend-engineer.md +61 -0
  77. package/pack/generated/codex/skills/overhaul/roles/boundary.md +56 -0
  78. package/pack/generated/codex/skills/overhaul/roles/craft.md +77 -0
  79. package/pack/generated/codex/skills/overhaul/roles/frontend-engineer.md +63 -0
  80. package/pack/generated/codex/skills/overhaul/roles/implementer.md +59 -0
  81. package/pack/generated/codex/skills/overhaul/roles/recon.md +60 -0
  82. package/pack/generated/codex/skills/overhaul/roles/specialist.md +56 -0
  83. package/pack/generated/codex/skills/overhaul/roles/verifier.md +60 -0
  84. package/pack/generated/devin/skills/overhaul/SKILL.md +201 -0
  85. package/pack/generated/devin/skills/overhaul/references/audit-domains.md +263 -0
  86. package/pack/generated/devin/skills/overhaul/references/lifecycle.md +308 -0
  87. package/pack/generated/devin/skills/overhaul/references/orchestration.md +264 -0
  88. package/pack/generated/devin/skills/overhaul/references/records.md +137 -0
  89. package/pack/generated/devin/skills/overhaul/references/repair-and-measurement.md +207 -0
  90. package/pack/generated/devin/skills/overhaul/references/review-and-report.md +137 -0
  91. package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +191 -0
  92. package/pack/generated/devin/skills/overhaul/references/scoring.md +222 -0
  93. package/pack/generated/devin/skills/overhaul/references/stack-profiles.md +264 -0
  94. package/pack/generated/devin/skills/overhaul/roles/backend-engineer.md +61 -0
  95. package/pack/generated/devin/skills/overhaul/roles/boundary.md +56 -0
  96. package/pack/generated/devin/skills/overhaul/roles/craft.md +77 -0
  97. package/pack/generated/devin/skills/overhaul/roles/frontend-engineer.md +63 -0
  98. package/pack/generated/devin/skills/overhaul/roles/implementer.md +59 -0
  99. package/pack/generated/devin/skills/overhaul/roles/recon.md +60 -0
  100. package/pack/generated/devin/skills/overhaul/roles/specialist.md +56 -0
  101. package/pack/generated/devin/skills/overhaul/roles/verifier.md +60 -0
  102. package/pack/generated/omp/skills/overhaul/SKILL.md +201 -0
  103. package/pack/generated/omp/skills/overhaul/references/audit-domains.md +263 -0
  104. package/pack/generated/omp/skills/overhaul/references/lifecycle.md +308 -0
  105. package/pack/generated/omp/skills/overhaul/references/orchestration.md +264 -0
  106. package/pack/generated/omp/skills/overhaul/references/records.md +137 -0
  107. package/pack/generated/omp/skills/overhaul/references/repair-and-measurement.md +207 -0
  108. package/pack/generated/omp/skills/overhaul/references/review-and-report.md +137 -0
  109. package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +191 -0
  110. package/pack/generated/omp/skills/overhaul/references/scoring.md +222 -0
  111. package/pack/generated/omp/skills/overhaul/references/stack-profiles.md +264 -0
  112. package/pack/generated/omp/skills/overhaul/roles/backend-engineer.md +61 -0
  113. package/pack/generated/omp/skills/overhaul/roles/boundary.md +56 -0
  114. package/pack/generated/omp/skills/overhaul/roles/craft.md +77 -0
  115. package/pack/generated/omp/skills/overhaul/roles/frontend-engineer.md +63 -0
  116. package/pack/generated/omp/skills/overhaul/roles/implementer.md +59 -0
  117. package/pack/generated/omp/skills/overhaul/roles/recon.md +60 -0
  118. package/pack/generated/omp/skills/overhaul/roles/specialist.md +56 -0
  119. package/pack/generated/omp/skills/overhaul/roles/verifier.md +60 -0
  120. package/pack/generated/pi/skills/overhaul/SKILL.md +201 -0
  121. package/pack/generated/pi/skills/overhaul/references/audit-domains.md +263 -0
  122. package/pack/generated/pi/skills/overhaul/references/lifecycle.md +308 -0
  123. package/pack/generated/pi/skills/overhaul/references/orchestration.md +264 -0
  124. package/pack/generated/pi/skills/overhaul/references/records.md +137 -0
  125. package/pack/generated/pi/skills/overhaul/references/repair-and-measurement.md +207 -0
  126. package/pack/generated/pi/skills/overhaul/references/review-and-report.md +137 -0
  127. package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +191 -0
  128. package/pack/generated/pi/skills/overhaul/references/scoring.md +222 -0
  129. package/pack/generated/pi/skills/overhaul/references/stack-profiles.md +264 -0
  130. package/pack/generated/pi/skills/overhaul/roles/backend-engineer.md +61 -0
  131. package/pack/generated/pi/skills/overhaul/roles/boundary.md +56 -0
  132. package/pack/generated/pi/skills/overhaul/roles/craft.md +77 -0
  133. package/pack/generated/pi/skills/overhaul/roles/frontend-engineer.md +63 -0
  134. package/pack/generated/pi/skills/overhaul/roles/implementer.md +59 -0
  135. package/pack/generated/pi/skills/overhaul/roles/recon.md +60 -0
  136. package/pack/generated/pi/skills/overhaul/roles/specialist.md +56 -0
  137. package/pack/generated/pi/skills/overhaul/roles/verifier.md +60 -0
  138. package/package.json +1 -1
@@ -0,0 +1,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
+ }