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,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("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;", "'", "&#x27;").Replace
52
+ mdEsc = strings.NewReplacer(`\`, `\\`, "|", `\|`, "\n", " ", "<", "&lt;").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
+ }