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.
Files changed (136) hide show
  1. package/CHANGELOG.md +6 -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/overhaul/admit/admit_test.go +170 -0
  11. package/engine/internal/overhaul/admit/run.go +416 -0
  12. package/engine/internal/overhaul/bench/run.go +248 -0
  13. package/engine/internal/overhaul/bench/run_test.go +139 -0
  14. package/engine/internal/overhaul/overhaul.go +64 -0
  15. package/engine/internal/overhaul/ovio/ovio.go +158 -0
  16. package/engine/internal/overhaul/records/check.go +834 -0
  17. package/engine/internal/overhaul/records/records_test.go +577 -0
  18. package/engine/internal/overhaul/records/run.go +392 -0
  19. package/engine/internal/overhaul/render/run.go +776 -0
  20. package/engine/internal/overhaul/render/run_test.go +293 -0
  21. package/engine/internal/overhaul/score/run.go +600 -0
  22. package/engine/internal/overhaul/score/run_test.go +405 -0
  23. package/engine/internal/overhaul/snapshot/run.go +563 -0
  24. package/engine/internal/overhaul/snapshot/run_test.go +360 -0
  25. package/engine/main.go +4 -0
  26. package/engine/root_routing_test.go +2 -1
  27. package/pack/.claude/skills/overhaul/SKILL.md +201 -0
  28. package/pack/.claude/skills/overhaul/references/audit-domains.md +263 -0
  29. package/pack/.claude/skills/overhaul/references/lifecycle.md +308 -0
  30. package/pack/.claude/skills/overhaul/references/orchestration.md +264 -0
  31. package/pack/.claude/skills/overhaul/references/records.md +137 -0
  32. package/pack/.claude/skills/overhaul/references/repair-and-measurement.md +207 -0
  33. package/pack/.claude/skills/overhaul/references/review-and-report.md +137 -0
  34. package/pack/.claude/skills/overhaul/references/scope-and-safety.md +191 -0
  35. package/pack/.claude/skills/overhaul/references/scoring.md +222 -0
  36. package/pack/.claude/skills/overhaul/references/stack-profiles.md +264 -0
  37. package/pack/.claude/skills/overhaul/roles/backend-engineer.md +61 -0
  38. package/pack/.claude/skills/overhaul/roles/boundary.md +56 -0
  39. package/pack/.claude/skills/overhaul/roles/craft.md +77 -0
  40. package/pack/.claude/skills/overhaul/roles/frontend-engineer.md +63 -0
  41. package/pack/.claude/skills/overhaul/roles/implementer.md +59 -0
  42. package/pack/.claude/skills/overhaul/roles/recon.md +60 -0
  43. package/pack/.claude/skills/overhaul/roles/specialist.md +56 -0
  44. package/pack/.claude/skills/overhaul/roles/verifier.md +60 -0
  45. package/pack/generated/claude/skills/overhaul/SKILL.md +201 -0
  46. package/pack/generated/claude/skills/overhaul/references/audit-domains.md +263 -0
  47. package/pack/generated/claude/skills/overhaul/references/lifecycle.md +308 -0
  48. package/pack/generated/claude/skills/overhaul/references/orchestration.md +264 -0
  49. package/pack/generated/claude/skills/overhaul/references/records.md +137 -0
  50. package/pack/generated/claude/skills/overhaul/references/repair-and-measurement.md +207 -0
  51. package/pack/generated/claude/skills/overhaul/references/review-and-report.md +137 -0
  52. package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +191 -0
  53. package/pack/generated/claude/skills/overhaul/references/scoring.md +222 -0
  54. package/pack/generated/claude/skills/overhaul/references/stack-profiles.md +264 -0
  55. package/pack/generated/claude/skills/overhaul/roles/backend-engineer.md +61 -0
  56. package/pack/generated/claude/skills/overhaul/roles/boundary.md +56 -0
  57. package/pack/generated/claude/skills/overhaul/roles/craft.md +77 -0
  58. package/pack/generated/claude/skills/overhaul/roles/frontend-engineer.md +63 -0
  59. package/pack/generated/claude/skills/overhaul/roles/implementer.md +59 -0
  60. package/pack/generated/claude/skills/overhaul/roles/recon.md +60 -0
  61. package/pack/generated/claude/skills/overhaul/roles/specialist.md +56 -0
  62. package/pack/generated/claude/skills/overhaul/roles/verifier.md +60 -0
  63. package/pack/generated/codex/skills/overhaul/SKILL.md +201 -0
  64. package/pack/generated/codex/skills/overhaul/agents/openai.yaml +2 -0
  65. package/pack/generated/codex/skills/overhaul/references/audit-domains.md +263 -0
  66. package/pack/generated/codex/skills/overhaul/references/lifecycle.md +308 -0
  67. package/pack/generated/codex/skills/overhaul/references/orchestration.md +264 -0
  68. package/pack/generated/codex/skills/overhaul/references/records.md +137 -0
  69. package/pack/generated/codex/skills/overhaul/references/repair-and-measurement.md +207 -0
  70. package/pack/generated/codex/skills/overhaul/references/review-and-report.md +137 -0
  71. package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +191 -0
  72. package/pack/generated/codex/skills/overhaul/references/scoring.md +222 -0
  73. package/pack/generated/codex/skills/overhaul/references/stack-profiles.md +264 -0
  74. package/pack/generated/codex/skills/overhaul/roles/backend-engineer.md +61 -0
  75. package/pack/generated/codex/skills/overhaul/roles/boundary.md +56 -0
  76. package/pack/generated/codex/skills/overhaul/roles/craft.md +77 -0
  77. package/pack/generated/codex/skills/overhaul/roles/frontend-engineer.md +63 -0
  78. package/pack/generated/codex/skills/overhaul/roles/implementer.md +59 -0
  79. package/pack/generated/codex/skills/overhaul/roles/recon.md +60 -0
  80. package/pack/generated/codex/skills/overhaul/roles/specialist.md +56 -0
  81. package/pack/generated/codex/skills/overhaul/roles/verifier.md +60 -0
  82. package/pack/generated/devin/skills/overhaul/SKILL.md +201 -0
  83. package/pack/generated/devin/skills/overhaul/references/audit-domains.md +263 -0
  84. package/pack/generated/devin/skills/overhaul/references/lifecycle.md +308 -0
  85. package/pack/generated/devin/skills/overhaul/references/orchestration.md +264 -0
  86. package/pack/generated/devin/skills/overhaul/references/records.md +137 -0
  87. package/pack/generated/devin/skills/overhaul/references/repair-and-measurement.md +207 -0
  88. package/pack/generated/devin/skills/overhaul/references/review-and-report.md +137 -0
  89. package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +191 -0
  90. package/pack/generated/devin/skills/overhaul/references/scoring.md +222 -0
  91. package/pack/generated/devin/skills/overhaul/references/stack-profiles.md +264 -0
  92. package/pack/generated/devin/skills/overhaul/roles/backend-engineer.md +61 -0
  93. package/pack/generated/devin/skills/overhaul/roles/boundary.md +56 -0
  94. package/pack/generated/devin/skills/overhaul/roles/craft.md +77 -0
  95. package/pack/generated/devin/skills/overhaul/roles/frontend-engineer.md +63 -0
  96. package/pack/generated/devin/skills/overhaul/roles/implementer.md +59 -0
  97. package/pack/generated/devin/skills/overhaul/roles/recon.md +60 -0
  98. package/pack/generated/devin/skills/overhaul/roles/specialist.md +56 -0
  99. package/pack/generated/devin/skills/overhaul/roles/verifier.md +60 -0
  100. package/pack/generated/omp/skills/overhaul/SKILL.md +201 -0
  101. package/pack/generated/omp/skills/overhaul/references/audit-domains.md +263 -0
  102. package/pack/generated/omp/skills/overhaul/references/lifecycle.md +308 -0
  103. package/pack/generated/omp/skills/overhaul/references/orchestration.md +264 -0
  104. package/pack/generated/omp/skills/overhaul/references/records.md +137 -0
  105. package/pack/generated/omp/skills/overhaul/references/repair-and-measurement.md +207 -0
  106. package/pack/generated/omp/skills/overhaul/references/review-and-report.md +137 -0
  107. package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +191 -0
  108. package/pack/generated/omp/skills/overhaul/references/scoring.md +222 -0
  109. package/pack/generated/omp/skills/overhaul/references/stack-profiles.md +264 -0
  110. package/pack/generated/omp/skills/overhaul/roles/backend-engineer.md +61 -0
  111. package/pack/generated/omp/skills/overhaul/roles/boundary.md +56 -0
  112. package/pack/generated/omp/skills/overhaul/roles/craft.md +77 -0
  113. package/pack/generated/omp/skills/overhaul/roles/frontend-engineer.md +63 -0
  114. package/pack/generated/omp/skills/overhaul/roles/implementer.md +59 -0
  115. package/pack/generated/omp/skills/overhaul/roles/recon.md +60 -0
  116. package/pack/generated/omp/skills/overhaul/roles/specialist.md +56 -0
  117. package/pack/generated/omp/skills/overhaul/roles/verifier.md +60 -0
  118. package/pack/generated/pi/skills/overhaul/SKILL.md +201 -0
  119. package/pack/generated/pi/skills/overhaul/references/audit-domains.md +263 -0
  120. package/pack/generated/pi/skills/overhaul/references/lifecycle.md +308 -0
  121. package/pack/generated/pi/skills/overhaul/references/orchestration.md +264 -0
  122. package/pack/generated/pi/skills/overhaul/references/records.md +137 -0
  123. package/pack/generated/pi/skills/overhaul/references/repair-and-measurement.md +207 -0
  124. package/pack/generated/pi/skills/overhaul/references/review-and-report.md +137 -0
  125. package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +191 -0
  126. package/pack/generated/pi/skills/overhaul/references/scoring.md +222 -0
  127. package/pack/generated/pi/skills/overhaul/references/stack-profiles.md +264 -0
  128. package/pack/generated/pi/skills/overhaul/roles/backend-engineer.md +61 -0
  129. package/pack/generated/pi/skills/overhaul/roles/boundary.md +56 -0
  130. package/pack/generated/pi/skills/overhaul/roles/craft.md +77 -0
  131. package/pack/generated/pi/skills/overhaul/roles/frontend-engineer.md +63 -0
  132. package/pack/generated/pi/skills/overhaul/roles/implementer.md +59 -0
  133. package/pack/generated/pi/skills/overhaul/roles/recon.md +60 -0
  134. package/pack/generated/pi/skills/overhaul/roles/specialist.md +56 -0
  135. package/pack/generated/pi/skills/overhaul/roles/verifier.md +60 -0
  136. package/package.json +1 -1
@@ -0,0 +1,293 @@
1
+ package render
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/sha256"
6
+ "encoding/base64"
7
+ "encoding/json"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+ "testing"
12
+ )
13
+
14
+ const (
15
+ hostile = "\"><img src=x onerror=alert(1)><script>alert(2)</script>|`rm -rf /`"
16
+ hostileEsc = "&quot;&gt;&lt;img src=x onerror=alert(1)&gt;&lt;script&gt;alert(2)&lt;/script&gt;|`rm -rf /`"
17
+ fakeHome = "/Users/tester"
18
+ badLane = `x"><a href="#p">y</a>`
19
+ )
20
+
21
+ func writeJSON(t *testing.T, path string, v any) {
22
+ t.Helper()
23
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ b, err := json.Marshal(v)
27
+ if err != nil {
28
+ t.Fatal(err)
29
+ }
30
+ if err := os.WriteFile(path, b, 0o644); err != nil {
31
+ t.Fatal(err)
32
+ }
33
+ }
34
+
35
+ type obj = map[string]any
36
+
37
+ // fixture builds a run dir and generation; withPlan=false makes an
38
+ // assessment-only audit that has a rubric revision but no plan.
39
+ func fixture(t *testing.T, withPlan bool) (run, gen string) {
40
+ t.Helper()
41
+ t.Setenv("HOME", fakeHome)
42
+ t.Setenv("USERPROFILE", fakeHome) // os.UserHomeDir on Windows
43
+ run = t.TempDir()
44
+ gen = filepath.Join(run, "generations", "g0003.tmp")
45
+ writeJSON(t, filepath.Join(run, "revisions", "rubric-r1.json"), obj{
46
+ "rev": 1,
47
+ "domains": obj{"correctness": 1, "security": 1},
48
+ "lanes": []any{"backend"},
49
+ "controls": []any{
50
+ obj{"id": "C-1", "domain": "correctness", "lanes": []any{"backend"}, "weight": 0.1, "hard_gate": true},
51
+ obj{"id": "C-2", "domain": "security", "lanes": []any{"backend"}, "weight": 0.2},
52
+ },
53
+ })
54
+ revisions := obj{"rubric": obj{"rev": 1, "file": "revisions/rubric-r1.json"}}
55
+ if withPlan {
56
+ writeJSON(t, filepath.Join(run, "revisions", "plan-r1.json"), obj{
57
+ "rev": 1,
58
+ "rubric": obj{"file": "revisions/rubric-r1.json"},
59
+ "tasks": []any{
60
+ obj{"id": "T-1", "lane": "backend", "findings": []any{"F-1"}, "oracle": "go test ./x", "risk": "low"},
61
+ obj{"id": "T-2", "lane": "frontend"},
62
+ },
63
+ })
64
+ revisions["plan"] = obj{"rev": 1, "file": "revisions/plan-r1.json"}
65
+ }
66
+ writeJSON(t, filepath.Join(gen, "run.json"), obj{
67
+ "run_id": "R1", "mode": "repair", "assessment_only": !withPlan, "phase": "review",
68
+ "readiness_verdict": "NOT_READY", "execution_outcome": "AWAITING_APPROVAL", "revisions": revisions,
69
+ })
70
+ writeJSON(t, filepath.Join(gen, "coverage.json"), obj{"files": []any{
71
+ obj{"path": fakeHome + "/proj/a.go", "eligible": true, "lanes": []any{"backend"},
72
+ "ranges": []any{obj{"start": 1, "end": 9, "state": "verified"}}},
73
+ obj{"path": "vendor/x.js", "eligible": false, "exclusion": obj{"reason": hostile}},
74
+ }})
75
+ writeJSON(t, filepath.Join(gen, "findings.json"), obj{"findings": []any{
76
+ obj{"id": "F-1", "title": hostile, "kind": "defect", "severity": "high", "status": "confirmed",
77
+ "failing_scenario": "nil deref"},
78
+ obj{"id": "F-2", "title": "OPP-TITLE", "kind": "opportunity", "status": "confirmed"},
79
+ }})
80
+ writeJSON(t, filepath.Join(gen, "evidence.json"), obj{"items": []any{}})
81
+ writeJSON(t, filepath.Join(gen, "dispatch.json"), obj{"attempts": []any{
82
+ obj{"task_id": "T-1", "attempt_id": 1, "lane": "backend", "role": "implementer", "status": "admitted",
83
+ "changed_paths": []any{"x/a.go"}},
84
+ obj{"task_id": "T-9", "attempt_id": 1, "lane": badLane, "role": "reviewer", "status": "admitted"},
85
+ }})
86
+ writeJSON(t, filepath.Join(gen, "approval.json"), obj{"events": []any{
87
+ obj{"id": "A-1", "kind": "plan", "status": "recorded", "quote": "QUOTE-approve r1"},
88
+ }})
89
+ writeJSON(t, filepath.Join(gen, "cycles.json"), obj{"cycles": []any{}})
90
+ writeJSON(t, filepath.Join(gen, "scorecard.json"), obj{
91
+ "readiness_verdict": "NOT_READY",
92
+ "status_by_control": obj{"C-1": "FAIL"},
93
+ "thresholds": []any{obj{"scope": "global", "value": obj{"exact": "1/2", "decimal": "0.5"}}},
94
+ })
95
+ writeJSON(t, filepath.Join(gen, "scorecard-baseline.json"), obj{
96
+ "thresholds": []any{obj{"scope": "global", "value": obj{"exact": "1/4"}}},
97
+ })
98
+ return run, gen
99
+ }
100
+
101
+ func renderKind(t *testing.T, run, gen, kind string) (string, string) {
102
+ t.Helper()
103
+ var out, errb bytes.Buffer
104
+ if code := Run([]string{run, gen, kind}, &out, &errb); code != 0 {
105
+ t.Fatalf("exit %d: %s", code, errb.String())
106
+ }
107
+ want := filepath.Join(gen, "views", kind+".html")
108
+ if strings.TrimSpace(out.String()) != want {
109
+ t.Fatalf("printed %q, want %q", out.String(), want)
110
+ }
111
+ h, err := os.ReadFile(want)
112
+ if err != nil {
113
+ t.Fatal(err)
114
+ }
115
+ m, err := os.ReadFile(filepath.Join(gen, "views", kind+".md"))
116
+ if err != nil {
117
+ t.Fatal(err)
118
+ }
119
+ return string(h), string(m)
120
+ }
121
+
122
+ // sectionHTML returns the HTML of one <section id="...">.
123
+ func sectionHTML(t *testing.T, page, id string) string {
124
+ t.Helper()
125
+ i := strings.Index(page, `<section id="`+id+`">`)
126
+ if i < 0 {
127
+ t.Fatalf("section %q missing", id)
128
+ }
129
+ s := page[i:]
130
+ return s[:strings.Index(s, "</section>")]
131
+ }
132
+
133
+ // tags returns every markup tag; escaped text never contains a raw '<'.
134
+ func tags(page string) []string {
135
+ var out []string
136
+ for _, part := range strings.Split(page, "<")[1:] {
137
+ out = append(out, part[:strings.IndexByte(part, '>')])
138
+ }
139
+ return out
140
+ }
141
+
142
+ func TestHostileTextIsInert(t *testing.T) {
143
+ run, gen := fixture(t, true)
144
+ h, _ := renderKind(t, run, gen, "review")
145
+ for _, tag := range tags(h) {
146
+ low := strings.ToLower(tag)
147
+ if strings.HasPrefix(low, "script") || strings.HasPrefix(low, "img") || strings.Contains(low, "onerror") {
148
+ t.Fatalf("live markup injected: <%s>", tag)
149
+ }
150
+ }
151
+ for _, id := range []string{"findings", "coverage"} {
152
+ if !strings.Contains(sectionHTML(t, h, id), hostileEsc) {
153
+ t.Errorf("escaped hostile text missing in %s", id)
154
+ }
155
+ }
156
+ }
157
+
158
+ func TestCSPPinsStylesheetAndNoRemoteURLs(t *testing.T) {
159
+ run, gen := fixture(t, true)
160
+ h, _ := renderKind(t, run, gen, "review")
161
+ style := h[strings.Index(h, "<style>")+len("<style>") : strings.Index(h, "</style>")]
162
+ sum := sha256.Sum256([]byte(style))
163
+ want := `content="default-src 'none'; style-src 'sha256-` + base64.StdEncoding.EncodeToString(sum[:]) +
164
+ `'; img-src data:; base-uri 'none'; form-action 'none'"`
165
+ if !strings.Contains(h, want) {
166
+ t.Fatal("CSP missing or hash does not match the emitted stylesheet")
167
+ }
168
+ if strings.Contains(h, "http://") || strings.Contains(h, "https://") || strings.Contains(h, "<script") {
169
+ t.Fatal("remote URL or script present")
170
+ }
171
+ if nt, nc := strings.Count(h, "<table>"), strings.Count(h, "<caption>"); nt == 0 || nt != nc {
172
+ t.Fatalf("tables %d captions %d", nt, nc)
173
+ }
174
+ if strings.Count(h, `<div class="scroll" role="region" tabindex="0"`) != strings.Count(h, "<table>") {
175
+ t.Fatal("table outside a focusable scroll region")
176
+ }
177
+ }
178
+
179
+ func TestMarkdownEscapesAndHomeRedacted(t *testing.T) {
180
+ run, gen := fixture(t, true)
181
+ h, md := renderKind(t, run, gen, "review")
182
+ if strings.Contains(md, "<script>") || !strings.Contains(md, "&lt;/script>\\|`rm -rf /`") {
183
+ t.Fatal("markdown cell not escaped")
184
+ }
185
+ for _, page := range []string{h, md} {
186
+ if strings.Contains(page, fakeHome) || !strings.Contains(page, "~/proj/a.go") {
187
+ t.Fatal("home path not redacted")
188
+ }
189
+ }
190
+ }
191
+
192
+ func TestRowIDsDecisionAndStamp(t *testing.T) {
193
+ run, gen := fixture(t, true)
194
+ h, md := renderKind(t, run, gen, "review")
195
+ if !strings.Contains(h, `<tr id="F-1">`) {
196
+ t.Fatal("row id F-1 missing")
197
+ }
198
+ if strings.Contains(h, `<tr id="nil deref"`) || strings.Contains(h, `<tr id="run_id"`) {
199
+ t.Fatal("row id on a non-ID first cell")
200
+ }
201
+ d, f := strings.Index(h, `<section id="decide">`), strings.Index(h, `<section id="findings">`)
202
+ if d < 0 || d > f {
203
+ t.Fatal("decision section missing or after findings")
204
+ }
205
+ if !strings.Contains(sectionHTML(t, h, "decide"), `<textarea id="ap" readonly>Approve overhaul R1 plan revision 1, digest `) {
206
+ t.Fatal("approval textarea missing")
207
+ }
208
+ if !strings.HasPrefix(md, "<!-- overhaul-stamp run=R1 generation=g0003 plan=r1:") ||
209
+ !strings.Contains(h, "<!-- overhaul-stamp run=R1 generation=g0003 plan=r1:") {
210
+ t.Fatal("stamp missing")
211
+ }
212
+ if !strings.Contains(h, `<p role="status"><strong>Outcome AWAITING_APPROVAL · verdict NOT_READY · phase review</strong></p>`) {
213
+ t.Fatal("status summary missing")
214
+ }
215
+ }
216
+
217
+ func TestHostileLaneCannotInject(t *testing.T) {
218
+ run, gen := fixture(t, true)
219
+ h, md := renderKind(t, run, gen, "review")
220
+ if strings.Contains(h, `<a href="#p">`) || strings.Contains(md, `<a href="#p">`) {
221
+ t.Fatal("lane name injected an anchor")
222
+ }
223
+ if !strings.Contains(h, `<section id="lane-x---a-href---p--y--a-">`) {
224
+ t.Fatal("lane anchor not sanitized")
225
+ }
226
+ }
227
+
228
+ func TestSectionContents(t *testing.T) {
229
+ run, gen := fixture(t, true)
230
+ h, md := renderKind(t, run, gen, "review")
231
+ for _, id := range []string{"findings", "leads", "rejected", "repairs"} {
232
+ if strings.Contains(sectionHTML(t, h, id), "OPP-TITLE") {
233
+ t.Fatalf("opportunity leaked into %s", id)
234
+ }
235
+ }
236
+ if !strings.Contains(sectionHTML(t, h, "opportunities"), "OPP-TITLE") {
237
+ t.Fatal("opportunity missing")
238
+ }
239
+ if !strings.Contains(md, "| F-1 / T-1 | high, not recorded | nil deref | x/a.go | go test ./x |") {
240
+ t.Fatal("repairs join missing")
241
+ }
242
+ card := sectionHTML(t, h, "scorecard")
243
+ if !strings.Contains(card, "<td>global</td><td><span class=\"muted\">not recorded</span></td><td>3/10</td><td>1/4</td><td>1/2</td><td>C-2</td><td>C-1</td>") {
244
+ t.Fatalf("transparent scorecard global row wrong: %s", card)
245
+ }
246
+ if !strings.Contains(sectionHTML(t, h, "approvals"), "QUOTE-approve r1") {
247
+ t.Fatal("approval quote missing")
248
+ }
249
+ }
250
+
251
+ func TestPlanMarkdown(t *testing.T) {
252
+ run, gen := fixture(t, true)
253
+ renderKind(t, run, gen, "review")
254
+ b, err := os.ReadFile(filepath.Join(gen, "views", "plan.md"))
255
+ if err != nil {
256
+ t.Fatal(err)
257
+ }
258
+ p := string(b)
259
+ if !strings.HasPrefix(p, "<!-- overhaul-stamp run=R1 generation=g0003 plan=r1:") ||
260
+ !strings.Contains(p, "| T-1 | backend | F-1 |") ||
261
+ !strings.Contains(p, "```text\nApprove overhaul R1 plan revision 1, digest ") {
262
+ t.Fatalf("plan.md incomplete:\n%s", p)
263
+ }
264
+ }
265
+
266
+ func TestAssessmentOnlyAudit(t *testing.T) {
267
+ run, gen := fixture(t, false)
268
+ for _, kind := range []string{"review", "report"} {
269
+ h, md := renderKind(t, run, gen, kind)
270
+ ctl := sectionHTML(t, h, "controls")
271
+ if !strings.Contains(ctl, `<tr id="C-1">`) || !strings.Contains(ctl, `<tr id="C-2">`) {
272
+ t.Fatal("rubric controls not listed from run.json revisions")
273
+ }
274
+ if strings.Contains(h+md, "Approve overhaul") || strings.Contains(h, `id="decide"`) {
275
+ t.Fatal("assessment-only run shows an approval message")
276
+ }
277
+ if !strings.Contains(h, "plan=none") {
278
+ t.Fatal("stamp should record plan=none")
279
+ }
280
+ }
281
+ if _, err := os.Stat(filepath.Join(gen, "views", "plan.md")); !os.IsNotExist(err) {
282
+ t.Fatal("plan.md written without a plan")
283
+ }
284
+ }
285
+
286
+ func TestUsage(t *testing.T) {
287
+ var out, errb bytes.Buffer
288
+ for _, args := range [][]string{{"run"}, {"run", "gen", "bogus"}} {
289
+ if Run(args, &out, &errb) != 2 {
290
+ t.Fatalf("%v: want exit 2", args)
291
+ }
292
+ }
293
+ }