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,776 @@
|
|
|
1
|
+
// Package render implements the /overhaul render tool: offline review and
|
|
2
|
+
// report views (HTML + Markdown) projected from one generation's canonical
|
|
3
|
+
// JSON. Every repository-derived string is escaped; pages carry no scripts, no
|
|
4
|
+
// remote assets and a hash-pinned stylesheet under a restrictive CSP.
|
|
5
|
+
package render
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"bytes"
|
|
9
|
+
"crypto/sha256"
|
|
10
|
+
"encoding/base64"
|
|
11
|
+
"encoding/json"
|
|
12
|
+
"errors"
|
|
13
|
+
"fmt"
|
|
14
|
+
"io"
|
|
15
|
+
"io/fs"
|
|
16
|
+
"math/big"
|
|
17
|
+
"os"
|
|
18
|
+
"path/filepath"
|
|
19
|
+
"regexp"
|
|
20
|
+
"sort"
|
|
21
|
+
"strings"
|
|
22
|
+
"unicode/utf16"
|
|
23
|
+
|
|
24
|
+
"github.com/devrites/devrites/internal/overhaul/ovio"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const usage = "usage: devrites-engine overhaul render <run> <gen-dir> [review|report]\n"
|
|
28
|
+
|
|
29
|
+
// css is byte-identical to the Python recipe so the CSP hash matches.
|
|
30
|
+
const css = `:root{color-scheme:light dark;--fg:#1b1f24;--bg:#fff;--muted:#57606a;--line:#d0d7de;--bad:#a40e26;--ok:#1a7f37}
|
|
31
|
+
@media (prefers-color-scheme:dark){:root{--fg:#e6edf3;--bg:#0d1117;--muted:#9da7b3;--line:#3d444d;--bad:#ff7b72;--ok:#56d364}}
|
|
32
|
+
body{margin:0 auto;max-width:72rem;padding:1rem;font:16px/1.5 system-ui,sans-serif;color:var(--fg);background:var(--bg)}
|
|
33
|
+
*,*::before,*::after{box-sizing:border-box}main *{min-width:0}img,svg{max-width:100%}
|
|
34
|
+
a{color:inherit}a:focus-visible,textarea:focus-visible,.scroll:focus-visible{outline:3px solid #0969da;outline-offset:2px}
|
|
35
|
+
.skip{position:absolute;left:-9999px}.skip:focus{left:1rem;top:1rem;background:var(--bg);padding:.5rem}
|
|
36
|
+
nav ul{display:flex;flex-wrap:wrap;gap:.25rem 1rem;padding:0;list-style:none}
|
|
37
|
+
.scroll{overflow-x:auto}table{border-collapse:collapse;width:100%;margin:.5rem 0 1rem}
|
|
38
|
+
th,td{border:1px solid var(--line);padding:.3rem .5rem;text-align:left;vertical-align:top;overflow-wrap:anywhere}
|
|
39
|
+
caption{text-align:left;font-weight:600}code,pre,textarea{font:13px/1.4 ui-monospace,monospace}code{overflow-wrap:anywhere}
|
|
40
|
+
.muted{color:var(--muted)}.FAIL,.NOT_READY,.UNKNOWN,.NOT_ASSESSABLE,.BLOCKED{color:var(--bad);font-weight:600}
|
|
41
|
+
.PASS,.MEETS_TARGETS{color:var(--ok);font-weight:600}textarea{width:100%;min-height:5rem}
|
|
42
|
+
@media print{nav,.skip{display:none}}`
|
|
43
|
+
|
|
44
|
+
var (
|
|
45
|
+
rowID = regexp.MustCompile(`^[A-Z][A-Z0-9]*-[A-Za-z0-9._-]+$`)
|
|
46
|
+
unsafeID = regexp.MustCompile(`[^A-Za-z0-9_-]`)
|
|
47
|
+
flags = set("PASS", "FAIL", "UNKNOWN", "NOT_READY", "MEETS_TARGETS", "NOT_ASSESSABLE", "BLOCKED")
|
|
48
|
+
leads = set("candidate", "needs-validation")
|
|
49
|
+
reviewed = set("semantically-reviewed", "cross-boundary-reviewed", "verified")
|
|
50
|
+
// esc matches Python html.escape(quote=True) byte for byte.
|
|
51
|
+
esc = strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """, "'", "'").Replace
|
|
52
|
+
mdEsc = strings.NewReplacer(`\`, `\\`, "|", `\|`, "\n", " ", "<", "<").Replace
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
func set(xs ...string) map[string]bool {
|
|
56
|
+
m := map[string]bool{}
|
|
57
|
+
for _, x := range xs {
|
|
58
|
+
m[x] = true
|
|
59
|
+
}
|
|
60
|
+
return m
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type row []any
|
|
64
|
+
|
|
65
|
+
type section struct {
|
|
66
|
+
anchor, heading string
|
|
67
|
+
cols []string
|
|
68
|
+
body []row
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Run executes `render <run> <gen-dir> [review|report]` and prints the HTML path.
|
|
72
|
+
func Run(args []string, stdout, stderr io.Writer) int {
|
|
73
|
+
if len(args) < 2 || len(args) > 3 {
|
|
74
|
+
fmt.Fprint(stderr, usage)
|
|
75
|
+
return 2
|
|
76
|
+
}
|
|
77
|
+
kind := "review"
|
|
78
|
+
if len(args) == 3 {
|
|
79
|
+
kind = args[2]
|
|
80
|
+
}
|
|
81
|
+
if kind != "review" && kind != "report" {
|
|
82
|
+
fmt.Fprint(stderr, usage)
|
|
83
|
+
return 2
|
|
84
|
+
}
|
|
85
|
+
home, _ := os.UserHomeDir()
|
|
86
|
+
path, err := render(args[0], args[1], kind, home)
|
|
87
|
+
if err != nil {
|
|
88
|
+
fmt.Fprintf(stderr, "overhaul render: %v\n", err)
|
|
89
|
+
return 2
|
|
90
|
+
}
|
|
91
|
+
fmt.Fprintln(stdout, path)
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type view struct{ home string }
|
|
96
|
+
|
|
97
|
+
func (v view) redact(s string) string {
|
|
98
|
+
if v.home == "" || v.home == "/" {
|
|
99
|
+
return s
|
|
100
|
+
}
|
|
101
|
+
return strings.ReplaceAll(s, v.home, "~")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func empty(x any) bool {
|
|
105
|
+
switch t := x.(type) {
|
|
106
|
+
case nil:
|
|
107
|
+
return true
|
|
108
|
+
case string:
|
|
109
|
+
return t == ""
|
|
110
|
+
case []any:
|
|
111
|
+
return len(t) == 0
|
|
112
|
+
case map[string]any:
|
|
113
|
+
return len(t) == 0
|
|
114
|
+
}
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// e renders one HTML cell value.
|
|
119
|
+
func (v view) e(x any) string {
|
|
120
|
+
if empty(x) {
|
|
121
|
+
return `<span class="muted">not recorded</span>`
|
|
122
|
+
}
|
|
123
|
+
switch t := x.(type) {
|
|
124
|
+
case []any:
|
|
125
|
+
parts := make([]string, len(t))
|
|
126
|
+
for i, y := range t {
|
|
127
|
+
parts[i] = v.e(y)
|
|
128
|
+
}
|
|
129
|
+
return strings.Join(parts, ", ")
|
|
130
|
+
case map[string]any:
|
|
131
|
+
return esc(v.redact(pyJSON(t)))
|
|
132
|
+
}
|
|
133
|
+
s := esc(v.redact(pyStr(x)))
|
|
134
|
+
if flags[s] {
|
|
135
|
+
return `<span class="` + s + `">` + s + `</span>`
|
|
136
|
+
}
|
|
137
|
+
return s
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// m renders one Markdown table cell value.
|
|
141
|
+
func (v view) m(x any) string {
|
|
142
|
+
if empty(x) {
|
|
143
|
+
return "not recorded"
|
|
144
|
+
}
|
|
145
|
+
if t, ok := x.([]any); ok {
|
|
146
|
+
parts := make([]string, len(t))
|
|
147
|
+
for i, y := range t {
|
|
148
|
+
parts[i] = v.m(y)
|
|
149
|
+
}
|
|
150
|
+
return strings.Join(parts, ", ")
|
|
151
|
+
}
|
|
152
|
+
return mdEsc(v.redact(pyStr(x)))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// pyStr mirrors Python str() of a decoded JSON value. Numbers keep their JSON
|
|
156
|
+
// source text; lists and objects use json.dumps(sort_keys=True) form.
|
|
157
|
+
func pyStr(x any) string {
|
|
158
|
+
switch t := x.(type) {
|
|
159
|
+
case nil:
|
|
160
|
+
return "None"
|
|
161
|
+
case bool:
|
|
162
|
+
if t {
|
|
163
|
+
return "True"
|
|
164
|
+
}
|
|
165
|
+
return "False"
|
|
166
|
+
case string:
|
|
167
|
+
return t
|
|
168
|
+
case json.Number:
|
|
169
|
+
return t.String()
|
|
170
|
+
}
|
|
171
|
+
return pyJSON(x)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// pyJSON mirrors Python json.dumps(v, sort_keys=True) (ASCII-only output).
|
|
175
|
+
func pyJSON(x any) string {
|
|
176
|
+
var b strings.Builder
|
|
177
|
+
var walk func(any)
|
|
178
|
+
walk = func(x any) {
|
|
179
|
+
switch t := x.(type) {
|
|
180
|
+
case nil:
|
|
181
|
+
b.WriteString("null")
|
|
182
|
+
case bool:
|
|
183
|
+
fmt.Fprint(&b, t)
|
|
184
|
+
case json.Number:
|
|
185
|
+
b.WriteString(t.String())
|
|
186
|
+
case string:
|
|
187
|
+
b.WriteByte('"')
|
|
188
|
+
for _, r := range t {
|
|
189
|
+
switch {
|
|
190
|
+
case r == '"' || r == '\\':
|
|
191
|
+
b.WriteByte('\\')
|
|
192
|
+
b.WriteRune(r)
|
|
193
|
+
case r == '\n':
|
|
194
|
+
b.WriteString(`\n`)
|
|
195
|
+
case r == '\r':
|
|
196
|
+
b.WriteString(`\r`)
|
|
197
|
+
case r == '\t':
|
|
198
|
+
b.WriteString(`\t`)
|
|
199
|
+
case r == '\b':
|
|
200
|
+
b.WriteString(`\b`)
|
|
201
|
+
case r == '\f':
|
|
202
|
+
b.WriteString(`\f`)
|
|
203
|
+
case r >= 0x20 && r <= 0x7e:
|
|
204
|
+
b.WriteRune(r)
|
|
205
|
+
case r > 0xffff:
|
|
206
|
+
r1, r2 := utf16.EncodeRune(r)
|
|
207
|
+
fmt.Fprintf(&b, `\u%04x\u%04x`, r1, r2)
|
|
208
|
+
default:
|
|
209
|
+
fmt.Fprintf(&b, `\u%04x`, r)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
b.WriteByte('"')
|
|
213
|
+
case []any:
|
|
214
|
+
b.WriteByte('[')
|
|
215
|
+
for i, y := range t {
|
|
216
|
+
if i > 0 {
|
|
217
|
+
b.WriteString(", ")
|
|
218
|
+
}
|
|
219
|
+
walk(y)
|
|
220
|
+
}
|
|
221
|
+
b.WriteByte(']')
|
|
222
|
+
case map[string]any:
|
|
223
|
+
b.WriteByte('{')
|
|
224
|
+
for i, k := range sortedKeys(t) {
|
|
225
|
+
if i > 0 {
|
|
226
|
+
b.WriteString(", ")
|
|
227
|
+
}
|
|
228
|
+
walk(k)
|
|
229
|
+
b.WriteString(": ")
|
|
230
|
+
walk(t[k])
|
|
231
|
+
}
|
|
232
|
+
b.WriteByte('}')
|
|
233
|
+
default:
|
|
234
|
+
fmt.Fprint(&b, t)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
walk(x)
|
|
238
|
+
return b.String()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
func sortedKeys(m map[string]any) []string {
|
|
242
|
+
keys := make([]string, 0, len(m))
|
|
243
|
+
for k := range m {
|
|
244
|
+
keys = append(keys, k)
|
|
245
|
+
}
|
|
246
|
+
sort.Strings(keys)
|
|
247
|
+
return keys
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// load reads a JSON object; a missing file is an empty object.
|
|
251
|
+
func load(path string) (map[string]any, error) {
|
|
252
|
+
m, err := ovio.LoadObject(path)
|
|
253
|
+
if errors.Is(err, fs.ErrNotExist) {
|
|
254
|
+
return map[string]any{}, nil
|
|
255
|
+
}
|
|
256
|
+
return m, err
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// objectKeys returns the keys of the top-level object field name in document
|
|
260
|
+
// order: Python iterates rubric domains in JSON order, Go maps do not.
|
|
261
|
+
func objectKeys(path, name string) []string {
|
|
262
|
+
b, err := os.ReadFile(path) // #nosec G304 -- record file inside the operator-selected generation
|
|
263
|
+
if err != nil {
|
|
264
|
+
return nil
|
|
265
|
+
}
|
|
266
|
+
dec := json.NewDecoder(bytes.NewReader(b))
|
|
267
|
+
if _, err := dec.Token(); err != nil {
|
|
268
|
+
return nil
|
|
269
|
+
}
|
|
270
|
+
var skip json.RawMessage
|
|
271
|
+
for dec.More() {
|
|
272
|
+
k, err := dec.Token()
|
|
273
|
+
if err != nil {
|
|
274
|
+
return nil
|
|
275
|
+
}
|
|
276
|
+
if k != name {
|
|
277
|
+
if dec.Decode(&skip) != nil {
|
|
278
|
+
return nil
|
|
279
|
+
}
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
if d, _ := dec.Token(); d != json.Delim('{') {
|
|
283
|
+
return nil
|
|
284
|
+
}
|
|
285
|
+
var keys []string
|
|
286
|
+
seen := map[string]bool{}
|
|
287
|
+
for dec.More() {
|
|
288
|
+
k, err := dec.Token()
|
|
289
|
+
if err != nil || dec.Decode(&skip) != nil {
|
|
290
|
+
return keys
|
|
291
|
+
}
|
|
292
|
+
if s := k.(string); !seen[s] {
|
|
293
|
+
seen[s] = true
|
|
294
|
+
keys = append(keys, s)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return keys
|
|
298
|
+
}
|
|
299
|
+
return nil
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func objs(v any) []map[string]any {
|
|
303
|
+
l := ovio.List(v)
|
|
304
|
+
out := make([]map[string]any, len(l))
|
|
305
|
+
for i, x := range l {
|
|
306
|
+
out[i] = ovio.Obj(x)
|
|
307
|
+
}
|
|
308
|
+
return out
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func strs(xs []string) []any {
|
|
312
|
+
out := make([]any, len(xs))
|
|
313
|
+
for i, x := range xs {
|
|
314
|
+
out[i] = x
|
|
315
|
+
}
|
|
316
|
+
return out
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// sf formats with every argument converted through pyStr, like Python "%s".
|
|
320
|
+
func sf(format string, xs ...any) string {
|
|
321
|
+
for i, x := range xs {
|
|
322
|
+
xs[i] = pyStr(x)
|
|
323
|
+
}
|
|
324
|
+
return fmt.Sprintf(format, xs...)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
func has(list any, s string) bool {
|
|
328
|
+
for _, x := range ovio.List(list) {
|
|
329
|
+
if y, ok := x.(string); ok && y == s {
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func rowsOf(items []map[string]any, fields ...string) []row {
|
|
337
|
+
out := make([]row, 0, len(items))
|
|
338
|
+
for _, x := range items {
|
|
339
|
+
r := make(row, len(fields))
|
|
340
|
+
for i, f := range fields {
|
|
341
|
+
r[i] = ovio.Get(x, f)
|
|
342
|
+
}
|
|
343
|
+
out = append(out, r)
|
|
344
|
+
}
|
|
345
|
+
return out
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
func filter(xs []map[string]any, keep func(map[string]any) bool) []map[string]any {
|
|
349
|
+
var out []map[string]any
|
|
350
|
+
for _, x := range xs {
|
|
351
|
+
if keep(x) {
|
|
352
|
+
out = append(out, x)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return out
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
func pick(card map[string]any, scope string) any {
|
|
359
|
+
for _, r := range objs(card["thresholds"]) {
|
|
360
|
+
if s, ok := r["scope"].(string); ok && s == scope {
|
|
361
|
+
return ovio.Get(r, "value.exact")
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return nil
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
func scorecardRows(rubric map[string]any, domains []string, base, sc map[string]any) ([]row, error) {
|
|
368
|
+
live := filter(objs(rubric["controls"]), func(c map[string]any) bool { return c != nil && c["na"] == nil })
|
|
369
|
+
dw, lanes, st := ovio.Obj(rubric["domains"]), ovio.Strings(rubric["lanes"]), ovio.Obj(sc["status_by_control"])
|
|
370
|
+
type scope struct{ name, lane, dom string }
|
|
371
|
+
scopes := []scope{{"global", "", ""}}
|
|
372
|
+
for _, d := range domains {
|
|
373
|
+
scopes = append(scopes, scope{"domain " + d, "", d})
|
|
374
|
+
}
|
|
375
|
+
for _, l := range lanes {
|
|
376
|
+
scopes = append(scopes, scope{"lane " + l, l, ""})
|
|
377
|
+
}
|
|
378
|
+
for _, l := range lanes {
|
|
379
|
+
for _, d := range domains {
|
|
380
|
+
scopes = append(scopes, scope{"lane " + l + " / " + d, l, d})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
var out []row
|
|
384
|
+
for _, s := range scopes {
|
|
385
|
+
cs := filter(live, func(c map[string]any) bool {
|
|
386
|
+
return (s.dom == "" || c["domain"] == any(s.dom)) && (s.lane == "" || has(c["lanes"], s.lane))
|
|
387
|
+
})
|
|
388
|
+
b, c := pick(base, s.name), pick(sc, s.name)
|
|
389
|
+
if b == nil && c == nil {
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
sum := new(big.Rat)
|
|
393
|
+
gaps, blockers := []any{}, []any{}
|
|
394
|
+
for _, ctl := range cs {
|
|
395
|
+
w, ok := ctl["weight"]
|
|
396
|
+
if !ok {
|
|
397
|
+
w = json.Number("0")
|
|
398
|
+
}
|
|
399
|
+
r, ok := new(big.Rat).SetString(pyStr(w))
|
|
400
|
+
if !ok {
|
|
401
|
+
return nil, fmt.Errorf("control %s: invalid weight %s", pyStr(ctl["id"]), pyStr(w))
|
|
402
|
+
}
|
|
403
|
+
sum.Add(sum, r)
|
|
404
|
+
status := ovio.Str(st[ovio.Str(ctl["id"])])
|
|
405
|
+
if status != "PASS" && status != "FAIL" {
|
|
406
|
+
gaps = append(gaps, ctl["id"])
|
|
407
|
+
}
|
|
408
|
+
if ovio.Truthy(ctl["hard_gate"]) && status != "PASS" {
|
|
409
|
+
blockers = append(blockers, ctl["id"])
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
var weight any
|
|
413
|
+
if s.dom != "" {
|
|
414
|
+
weight = dw[s.dom]
|
|
415
|
+
}
|
|
416
|
+
out = append(out, row{s.name, weight, sum.RatString(), b, c, gaps, blockers})
|
|
417
|
+
}
|
|
418
|
+
return out, nil
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
type built struct {
|
|
422
|
+
rj map[string]any
|
|
423
|
+
secs []section
|
|
424
|
+
approve, stamp string
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
func build(run, gen string) (*built, error) {
|
|
428
|
+
R := map[string]map[string]any{}
|
|
429
|
+
for _, n := range []string{"run", "coverage", "stack-profiles", "dispatch", "contracts", "findings", "evidence",
|
|
430
|
+
"approval", "scorecard", "scorecard-baseline", "results-baseline", "results-candidate", "cycles"} {
|
|
431
|
+
m, err := load(filepath.Join(gen, n+".json"))
|
|
432
|
+
if err != nil {
|
|
433
|
+
return nil, err
|
|
434
|
+
}
|
|
435
|
+
R[n] = m
|
|
436
|
+
}
|
|
437
|
+
rj := R["run"]
|
|
438
|
+
pr := ovio.Obj(ovio.Get(rj, "revisions.plan"))
|
|
439
|
+
plan, digest := map[string]any{}, "none"
|
|
440
|
+
if len(pr) > 0 {
|
|
441
|
+
p := filepath.Join(run, ovio.Str(pr["file"]))
|
|
442
|
+
var err error
|
|
443
|
+
if plan, err = load(p); err != nil {
|
|
444
|
+
return nil, err
|
|
445
|
+
}
|
|
446
|
+
if digest, err = ovio.SHA256File(p); err != nil {
|
|
447
|
+
return nil, err
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
rfile := ovio.Get(plan, "rubric.file")
|
|
451
|
+
if !ovio.Truthy(rfile) {
|
|
452
|
+
rfile = ovio.Get(rj, "revisions.rubric.file")
|
|
453
|
+
}
|
|
454
|
+
rubric, domains := map[string]any{}, []string(nil)
|
|
455
|
+
if ovio.Truthy(rfile) {
|
|
456
|
+
p := filepath.Join(run, pyStr(rfile))
|
|
457
|
+
var err error
|
|
458
|
+
if rubric, err = load(p); err != nil {
|
|
459
|
+
return nil, err
|
|
460
|
+
}
|
|
461
|
+
domains = objectKeys(p, "domains")
|
|
462
|
+
}
|
|
463
|
+
finds, att := objs(R["findings"]["findings"]), objs(R["dispatch"]["attempts"])
|
|
464
|
+
sc, base := R["scorecard"], R["scorecard-baseline"]
|
|
465
|
+
cov := objs(R["coverage"]["files"])
|
|
466
|
+
tasks := objs(plan["tasks"])
|
|
467
|
+
|
|
468
|
+
laneSet := set("frontend", "backend", "boundary")
|
|
469
|
+
for _, x := range att {
|
|
470
|
+
if l := ovio.Str(x["lane"]); l != "" {
|
|
471
|
+
laneSet[l] = true
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
lanes := make([]string, 0, len(laneSet))
|
|
475
|
+
for l := range laneSet {
|
|
476
|
+
lanes = append(lanes, l)
|
|
477
|
+
}
|
|
478
|
+
sort.Strings(lanes)
|
|
479
|
+
admitted := func(x map[string]any) bool { return ovio.Str(x["status"]) == "admitted" }
|
|
480
|
+
changed := map[string]any{}
|
|
481
|
+
for _, x := range att {
|
|
482
|
+
if ovio.Str(x["role"]) == "implementer" && admitted(x) {
|
|
483
|
+
changed[pyStr(x["task_id"])] = x["changed_paths"]
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
byF := map[string][]map[string]any{}
|
|
487
|
+
for _, t := range tasks {
|
|
488
|
+
for _, fid := range ovio.List(t["findings"]) {
|
|
489
|
+
byF[pyStr(fid)] = append(byF[pyStr(fid)], t)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
var laneRows []row
|
|
494
|
+
for _, lane := range lanes {
|
|
495
|
+
xs := filter(att, func(x map[string]any) bool { return ovio.Str(x["lane"]) == lane && admitted(x) })
|
|
496
|
+
var windows, open []string
|
|
497
|
+
profSet := map[string]bool{}
|
|
498
|
+
for _, x := range xs {
|
|
499
|
+
windows = append(windows, sf("%s %s to %s", x["role"], x["started_at"], x["finished_at"]))
|
|
500
|
+
for _, p := range ovio.List(x["profiles"]) {
|
|
501
|
+
profSet[pyStr(p)] = true
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
var profs []string
|
|
505
|
+
for p := range profSet {
|
|
506
|
+
profs = append(profs, p)
|
|
507
|
+
}
|
|
508
|
+
sort.Strings(profs)
|
|
509
|
+
done, total := 0, 0
|
|
510
|
+
for _, c := range cov {
|
|
511
|
+
if !ovio.Truthy(c["eligible"]) {
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
for _, r := range objs(c["ranges"]) {
|
|
515
|
+
ls := r["lanes"]
|
|
516
|
+
if !ovio.Truthy(ls) {
|
|
517
|
+
ls = c["lanes"]
|
|
518
|
+
}
|
|
519
|
+
if !has(ls, lane) {
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
total++
|
|
523
|
+
if reviewed[ovio.Str(r["state"])] {
|
|
524
|
+
done++
|
|
525
|
+
} else {
|
|
526
|
+
open = append(open, sf("%s:%s-%s %s", c["path"], r["start"], r["end"], r["state"]))
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
laneScore := ovio.Get(ovio.Obj(ovio.Obj(sc["lanes"])[lane]), "Q.exact")
|
|
531
|
+
laneRows = append(laneRows, row{lane, strs(windows), strs(profs), fmt.Sprintf("%d / %d", done, total), strs(open), laneScore})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
var result []row
|
|
535
|
+
for _, k := range []string{"run_id", "mode", "assessment_only", "phase", "readiness_verdict", "execution_outcome",
|
|
536
|
+
"identities.comparison", "identities.baseline", "identities.candidate", "capabilities", "degradation", "budget", "stop"} {
|
|
537
|
+
result = append(result, row{k, ovio.Get(rj, k)})
|
|
538
|
+
}
|
|
539
|
+
S := []section{
|
|
540
|
+
{"result", "Scope, identities and result", []string{"field", "value"}, result},
|
|
541
|
+
{"lane-coverage", "Lane, stack and boundary coverage", []string{"lane", "admitted reviewers and windows", "profiles",
|
|
542
|
+
"reviewed / eligible ranges", "open ranges", "lane score"}, laneRows},
|
|
543
|
+
}
|
|
544
|
+
for _, lane := range lanes {
|
|
545
|
+
var body []row
|
|
546
|
+
for _, x := range att {
|
|
547
|
+
if ovio.Str(x["lane"]) == lane {
|
|
548
|
+
body = append(body, row{sf("%s#%s", x["task_id"], x["attempt_id"]), x["role"], x["status"], x["profiles"],
|
|
549
|
+
x["started_at"], x["finished_at"], x["receipt"]})
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
S = append(S, section{"lane-" + unsafeID.ReplaceAllString(lane, "-"), "Lane: " + lane,
|
|
553
|
+
[]string{"attempt", "role", "status", "profiles", "started", "finished", "receipt"}, body})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
var covRows []row
|
|
557
|
+
for _, c := range cov {
|
|
558
|
+
var rs []string
|
|
559
|
+
for _, r := range objs(c["ranges"]) {
|
|
560
|
+
rs = append(rs, sf("%s-%s %s", r["start"], r["end"], r["state"]))
|
|
561
|
+
}
|
|
562
|
+
covRows = append(covRows, row{c["path"], c["eligible"], c["lanes"], strs(rs), ovio.Get(c, "exclusion.reason")})
|
|
563
|
+
}
|
|
564
|
+
notLead := func(f map[string]any) bool { s := ovio.Str(f["status"]); return !leads[s] && s != "rejected" }
|
|
565
|
+
opp := func(f map[string]any) bool { return ovio.Str(f["kind"]) == "opportunity" }
|
|
566
|
+
var repairs, rejected, planRows, controls, changes []row
|
|
567
|
+
for _, f := range finds {
|
|
568
|
+
for _, t := range byF[pyStr(f["id"])] {
|
|
569
|
+
repairs = append(repairs, row{sf("%s / %s", f["id"], t["id"]), []any{f["severity"], f["confidence"]},
|
|
570
|
+
f["failing_scenario"], changed[pyStr(t["id"])], t["oracle"], []any{f["status"], f["verified_by"]}, t["risk"]})
|
|
571
|
+
}
|
|
572
|
+
if ovio.Str(f["status"]) == "rejected" {
|
|
573
|
+
var reason any
|
|
574
|
+
if h := ovio.List(f["history"]); len(h) > 0 {
|
|
575
|
+
reason = ovio.Get(h[len(h)-1], "reason")
|
|
576
|
+
}
|
|
577
|
+
rejected = append(rejected, row{f["id"], f["title"], reason})
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
bench := filter(objs(R["evidence"]["items"]), func(x map[string]any) bool { return ovio.Str(x["kind"]) == "benchmark" })
|
|
581
|
+
var perfFields []string
|
|
582
|
+
for _, k := range []string{"view", "scenario", "metric", "baseline_median", "candidate_median", "absolute_delta", "ratio",
|
|
583
|
+
"ci95", "n", "target", "guardrails", "verdict"} {
|
|
584
|
+
perfFields = append(perfFields, "measurement."+k)
|
|
585
|
+
}
|
|
586
|
+
for _, t := range tasks {
|
|
587
|
+
planRows = append(planRows, row{t["id"], t["lane"], t["findings"], t["deps"], t["allowed_paths"], t["oracle"],
|
|
588
|
+
sf("%s / %s", t["benefit"], t["cost"]), t["rollback"], t["risk"], sf("%s / %s", t["writer_role"], t["verifier_role"])})
|
|
589
|
+
}
|
|
590
|
+
for _, c := range objs(rubric["controls"]) {
|
|
591
|
+
id := ovio.Str(c["id"])
|
|
592
|
+
controls = append(controls, row{c["id"], c["domain"], c["lanes"], c["weight"], c["hard_gate"],
|
|
593
|
+
ovio.Obj(base["status_by_control"])[id], ovio.Obj(sc["status_by_control"])[id]})
|
|
594
|
+
}
|
|
595
|
+
var scoreRows []row
|
|
596
|
+
for _, r := range objs(sc["thresholds"]) {
|
|
597
|
+
scoreRows = append(scoreRows, row{r["scope"], ovio.Get(r, "value.exact"), ovio.Get(r, "value.decimal"), r["min"], r["result"]})
|
|
598
|
+
}
|
|
599
|
+
gates := ovio.Obj(sc["gates"])
|
|
600
|
+
for _, g := range sortedKeys(gates) {
|
|
601
|
+
scoreRows = append(scoreRows, row{g, nil, nil, nil, gates[g]})
|
|
602
|
+
}
|
|
603
|
+
card, err := scorecardRows(rubric, domains, base, sc)
|
|
604
|
+
if err != nil {
|
|
605
|
+
return nil, err
|
|
606
|
+
}
|
|
607
|
+
for _, x := range filter(att, admitted) {
|
|
608
|
+
changes = append(changes, row{sf("%s#%s", x["task_id"], x["attempt_id"]), x["task_id"], x["changed_paths"]})
|
|
609
|
+
}
|
|
610
|
+
orNone := func(v any) string {
|
|
611
|
+
if ovio.Truthy(v) {
|
|
612
|
+
return pyStr(v)
|
|
613
|
+
}
|
|
614
|
+
return "none"
|
|
615
|
+
}
|
|
616
|
+
verdict := "not computed"
|
|
617
|
+
if v, ok := sc["readiness_verdict"]; ok {
|
|
618
|
+
verdict = pyStr(v)
|
|
619
|
+
}
|
|
620
|
+
S = append(S,
|
|
621
|
+
section{"coverage", "Coverage ledger", []string{"path", "eligible", "lanes", "ranges (state)", "exclusion"}, covRows},
|
|
622
|
+
section{"profiles", "Stack profiles", []string{"id", "revision", "status", "file", "sha256"},
|
|
623
|
+
rowsOf(objs(R["stack-profiles"]["profiles"]), "id", "rev", "status", "file", "sha256")},
|
|
624
|
+
section{"contracts", "Contracts and boundaries", []string{"id", "producer", "consumers", "version", "evidence", "open questions"},
|
|
625
|
+
rowsOf(objs(R["contracts"]["contracts"]), "id", "producer", "consumers", "version", "evidence", "questions")},
|
|
626
|
+
section{"findings", "Confirmed findings", []string{"id", "title", "kind", "severity", "status", "lanes", "locations", "failing case", "impact", "evidence"},
|
|
627
|
+
rowsOf(filter(finds, func(f map[string]any) bool { return notLead(f) && !opp(f) }), "id", "title", "kind",
|
|
628
|
+
"severity", "status", "lanes", "locations", "failing_scenario", "impact", "evidence")},
|
|
629
|
+
section{"opportunities", "Opportunities (not defects)", []string{"id", "title", "status", "locations", "benefit"},
|
|
630
|
+
rowsOf(filter(finds, func(f map[string]any) bool { return opp(f) && notLead(f) }), "id", "title", "status", "locations", "impact")},
|
|
631
|
+
section{"repairs", "Findings and repairs", []string{"finding / task", "severity and confidence", "previous failure", "change made",
|
|
632
|
+
"regression proof", "independent result", "remaining risk"}, repairs},
|
|
633
|
+
section{"leads", "Unvalidated leads (no severity)", []string{"id", "title", "status", "potential impact", "missing fact"},
|
|
634
|
+
rowsOf(filter(finds, func(f map[string]any) bool { return leads[ovio.Str(f["status"])] }), "id", "title", "status", "potential_impact", "blockers")},
|
|
635
|
+
section{"rejected", "Rejected candidates", []string{"id", "title", "reason"}, rejected},
|
|
636
|
+
section{"performance", "Performance before/after", []string{"view", "scenario", "metric", "before", "after", "delta", "ratio",
|
|
637
|
+
"CI 95%", "n", "target", "guardrails", "verdict"}, rowsOf(bench, perfFields...)},
|
|
638
|
+
section{"plan", "Repair plan revision " + orNone(plan["rev"]), []string{"task", "lane", "findings", "deps", "allowed paths",
|
|
639
|
+
"oracle", "benefit / cost", "rollback", "risk", "writer / verifier"}, planRows},
|
|
640
|
+
section{"controls", "Rubric controls (revision " + orNone(rubric["rev"]) + ")", []string{"control", "domain", "lanes", "weight",
|
|
641
|
+
"hard gate", "baseline", "candidate"}, controls},
|
|
642
|
+
section{"score", "Scorecard (verdict " + verdict + ")", []string{"scope", "exact", "decimal", "minimum", "result"}, scoreRows},
|
|
643
|
+
section{"scorecard", "Transparent scorecard (baseline vs candidate)", []string{"scope", "domain weight",
|
|
644
|
+
"applicable control weight", "baseline", "candidate", "evidence gaps", "hard blockers"}, card},
|
|
645
|
+
section{"approvals", "Approvals and decisions", []string{"id", "kind", "status", "plan revision", "tasks", "channel", "quote"},
|
|
646
|
+
rowsOf(objs(R["approval"]["events"]), "id", "kind", "status", "plan_rev", "tasks", "channel", "quote")},
|
|
647
|
+
section{"cycles", "Iteration history", []string{"cycle", "deficits", "hypothesis", "agents", "changes", "outcome", "scores", "next / stop"},
|
|
648
|
+
rowsOf(objs(R["cycles"]["cycles"]), "id", "deficits", "hypothesis", "agents", "changes", "outcome", "scores", "next")},
|
|
649
|
+
section{"dependencies", "Dependency decisions", []string{"dependency", "before", "after", "decision", "evidence date", "reason", "proof", "risk"},
|
|
650
|
+
rowsOf(objs(R["stack-profiles"]["dependencies"]), "name", "before", "after", "decision", "evidence_date", "reason", "proof", "risk")},
|
|
651
|
+
section{"changes", "Changed paths", []string{"attempt", "task", "paths"}, changes},
|
|
652
|
+
section{"questions", "Open questions and remaining work", []string{"id", "question", "blocks", "status"},
|
|
653
|
+
rowsOf(objs(rj["questions"]), "id", "text", "blocks", "status")},
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
approve := ""
|
|
657
|
+
if len(pr) > 0 && !ovio.Truthy(rj["assessment_only"]) {
|
|
658
|
+
ids := make([]string, len(tasks))
|
|
659
|
+
for i, t := range tasks {
|
|
660
|
+
if id, ok := t["id"]; ok {
|
|
661
|
+
ids[i] = pyStr(id)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
approve = sf("Approve overhaul %s plan revision %s, digest %s, tasks %s. Keep all listed constraints and exclusions.",
|
|
665
|
+
rj["run_id"], pr["rev"], digest, strings.Join(ids, " "))
|
|
666
|
+
}
|
|
667
|
+
planStamp := "none"
|
|
668
|
+
if len(pr) > 0 {
|
|
669
|
+
planStamp = sf("r%s:%s", pr["rev"], digest)
|
|
670
|
+
}
|
|
671
|
+
stamp := sf("overhaul-stamp run=%s generation=%s plan=%s", rj["run_id"],
|
|
672
|
+
strings.ReplaceAll(filepath.Base(strings.TrimRight(gen, "/")), ".tmp", ""), planStamp)
|
|
673
|
+
return &built{rj, S, approve, stamp}, nil
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
func mdTable(v view, cols []string, body []row) []string {
|
|
677
|
+
out := []string{"| " + strings.Join(cols, " | ") + " |", "|" + strings.Repeat("---|", len(cols))}
|
|
678
|
+
for _, r := range body {
|
|
679
|
+
cells := make([]string, len(r))
|
|
680
|
+
for i, x := range r {
|
|
681
|
+
cells[i] = v.m(x)
|
|
682
|
+
}
|
|
683
|
+
out = append(out, "| "+strings.Join(cells, " | ")+" |")
|
|
684
|
+
}
|
|
685
|
+
return out
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
func render(run, gen, kind, home string) (string, error) {
|
|
689
|
+
b, err := build(run, gen)
|
|
690
|
+
if err != nil {
|
|
691
|
+
return "", err
|
|
692
|
+
}
|
|
693
|
+
v := view{home}
|
|
694
|
+
vd := filepath.Join(gen, "views")
|
|
695
|
+
if err := os.MkdirAll(vd, 0o755); err != nil {
|
|
696
|
+
return "", err
|
|
697
|
+
}
|
|
698
|
+
runID := pyStr(b.rj["run_id"])
|
|
699
|
+
if kind == "review" {
|
|
700
|
+
for _, s := range b.secs {
|
|
701
|
+
if s.anchor != "plan" || len(s.body) == 0 {
|
|
702
|
+
continue
|
|
703
|
+
}
|
|
704
|
+
lines := append([]string{"<!-- " + b.stamp + " -->", "# Overhaul plan " + runID, ""}, mdTable(v, s.cols, s.body)...)
|
|
705
|
+
if b.approve != "" {
|
|
706
|
+
lines = append(lines, "", "```text", b.approve, "```")
|
|
707
|
+
}
|
|
708
|
+
if err := os.WriteFile(filepath.Join(vd, "plan.md"), []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
|
|
709
|
+
return "", err
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
sum := sha256.Sum256([]byte(css))
|
|
714
|
+
csp := "default-src 'none'; style-src 'sha256-" + base64.StdEncoding.EncodeToString(sum[:]) +
|
|
715
|
+
"'; img-src data:; base-uri 'none'; form-action 'none'"
|
|
716
|
+
title := "Overhaul " + kind + " " + runID
|
|
717
|
+
var nav strings.Builder
|
|
718
|
+
for _, s := range b.secs {
|
|
719
|
+
fmt.Fprintf(&nav, `<li><a href="#%s">%s</a></li>`, esc(s.anchor), esc(s.heading))
|
|
720
|
+
}
|
|
721
|
+
out := []string{`<!doctype html><html lang="en"><head><meta charset="utf-8">`,
|
|
722
|
+
`<meta http-equiv="Content-Security-Policy" content="` + csp + `">`,
|
|
723
|
+
`<meta name="viewport" content="width=device-width,initial-scale=1">`,
|
|
724
|
+
"<title>" + esc(title) + "</title><style>" + css + "</style></head><body>",
|
|
725
|
+
// Escaped (unlike the Python recipe) so a hostile run id cannot close the comment.
|
|
726
|
+
"<!-- " + esc(b.stamp) + " -->", `<a class="skip" href="#main">Skip to content</a>`,
|
|
727
|
+
`<header><h1>` + esc(title) + `</h1><p class="muted"><code>` + esc(b.stamp) + `</code></p></header>`,
|
|
728
|
+
`<nav aria-label="Sections"><ul>` + nav.String() + "</ul></nav>",
|
|
729
|
+
`<main id="main">`}
|
|
730
|
+
md := []string{"<!-- " + b.stamp + " -->", "# " + title, "", "`" + b.stamp + "`", ""}
|
|
731
|
+
summary := sf("Outcome %s · verdict %s · phase %s", b.rj["execution_outcome"], b.rj["readiness_verdict"], b.rj["phase"])
|
|
732
|
+
out = append(out, `<p role="status"><strong>`+esc(summary)+`</strong></p>`)
|
|
733
|
+
md = append(md, "**"+v.m(summary)+"**", "")
|
|
734
|
+
if kind == "review" && b.approve != "" {
|
|
735
|
+
out = append(out, `<section id="decide"><h2>Decision</h2><p>Nothing on this page approves anything. Copy a message into the `+
|
|
736
|
+
`conversation with the agent; only your message there counts. Edit the task list to approve a subset, or `+
|
|
737
|
+
`reply with rejections, deferrals or questions per task ID.</p><label for="ap">Approval message</label>`+
|
|
738
|
+
`<textarea id="ap" readonly>`+esc(b.approve)+`</textarea></section>`)
|
|
739
|
+
md = append(md, "## Decision", "", "Only a message you send in the conversation counts. Approval text:", "", "```text", b.approve, "```", "")
|
|
740
|
+
}
|
|
741
|
+
for _, s := range b.secs {
|
|
742
|
+
out = append(out, `<section id="`+esc(s.anchor)+`"><h2>`+esc(s.heading)+`</h2>`)
|
|
743
|
+
md = append(md, "## "+v.m(s.heading), "")
|
|
744
|
+
if len(s.body) == 0 {
|
|
745
|
+
out = append(out, `<p class="muted">No records.</p></section>`)
|
|
746
|
+
md = append(md, "No records.", "")
|
|
747
|
+
continue
|
|
748
|
+
}
|
|
749
|
+
var th strings.Builder
|
|
750
|
+
for _, c := range s.cols {
|
|
751
|
+
th.WriteString(`<th scope="col">` + esc(c) + `</th>`)
|
|
752
|
+
}
|
|
753
|
+
out = append(out, `<div class="scroll" role="region" tabindex="0" aria-label="`+esc(s.heading)+`"><table><caption>`+
|
|
754
|
+
esc(s.heading)+`</caption><thead><tr>`+th.String()+`</tr></thead><tbody>`)
|
|
755
|
+
for _, r := range s.body {
|
|
756
|
+
id := ""
|
|
757
|
+
if first := pyStr(r[0]); rowID.MatchString(first) {
|
|
758
|
+
id = ` id="` + esc(first) + `"`
|
|
759
|
+
}
|
|
760
|
+
var td strings.Builder
|
|
761
|
+
for _, x := range r {
|
|
762
|
+
td.WriteString("<td>" + v.e(x) + "</td>")
|
|
763
|
+
}
|
|
764
|
+
out = append(out, "<tr"+id+">"+td.String()+"</tr>")
|
|
765
|
+
}
|
|
766
|
+
out = append(out, "</tbody></table></div></section>")
|
|
767
|
+
md = append(append(md, mdTable(v, s.cols, s.body)...), "")
|
|
768
|
+
}
|
|
769
|
+
out = append(out, "</main></body></html>")
|
|
770
|
+
for ext, text := range map[string][]string{"html": out, "md": md} {
|
|
771
|
+
if err := os.WriteFile(filepath.Join(vd, kind+"."+ext), []byte(strings.Join(text, "\n")+"\n"), 0o644); err != nil {
|
|
772
|
+
return "", err
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return filepath.Join(vd, kind+".html"), nil
|
|
776
|
+
}
|