devrites 5.14.0 → 5.14.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 (212) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +6 -3
  3. package/docs/adr/0033-rite-fast-own-agents.md +45 -0
  4. package/docs/adr/README.md +2 -1
  5. package/docs/architecture.md +6 -4
  6. package/docs/command-map.md +2 -1
  7. package/docs/flow.md +3 -2
  8. package/docs/orchestration.md +3 -0
  9. package/docs/quick-reference.md +2 -1
  10. package/docs/skills.md +13 -8
  11. package/docs/usage.md +2 -2
  12. package/engine/internal/install/apply.go +5 -3
  13. package/engine/internal/install/binary.go +3 -1
  14. package/engine/internal/install/install.go +0 -25
  15. package/engine/internal/lib/claims.go +15 -0
  16. package/engine/internal/lib/claims_test.go +28 -0
  17. package/engine/internal/lib/context_next_test.go +53 -0
  18. package/engine/internal/lib/dispatch.go +28 -7
  19. package/engine/internal/lib/manifest_union.go +14 -0
  20. package/engine/internal/overhaul/admit/admit_test.go +9 -0
  21. package/engine/internal/overhaul/admit/run.go +9 -0
  22. package/engine/internal/overhaul/bench/run.go +327 -38
  23. package/engine/internal/overhaul/bench/run_test.go +131 -0
  24. package/engine/internal/overhaul/bench/stats.go +154 -0
  25. package/engine/internal/overhaul/ovio/ovio.go +4 -0
  26. package/engine/internal/overhaul/records/check.go +3 -0
  27. package/engine/internal/overhaul/records/consistency.go +217 -0
  28. package/engine/internal/overhaul/records/records_test.go +81 -0
  29. package/engine/internal/overhaul/records/run.go +1 -1
  30. package/engine/internal/overhaul/render/run.go +2 -0
  31. package/engine/internal/overhaul/render/run_test.go +5 -1
  32. package/engine/internal/overhaul/score/run.go +58 -9
  33. package/engine/internal/overhaul/score/run_test.go +84 -8
  34. package/engine/internal/parallel/ops.go +5 -0
  35. package/engine/internal/state/write.go +8 -2
  36. package/engine/internal/version/version_test.go +27 -0
  37. package/engine/main.go +2 -2
  38. package/pack/.claude/agents/fast-builder.md +90 -0
  39. package/pack/.claude/agents/fast-checker.md +60 -0
  40. package/pack/.claude/agents/fast-critic.md +70 -0
  41. package/pack/.claude/agents/fast-planner.md +95 -0
  42. package/pack/.claude/agents/overhaul-backend-engineer.md +5 -4
  43. package/pack/.claude/agents/overhaul-boundary.md +2 -2
  44. package/pack/.claude/agents/overhaul-craft.md +1 -1
  45. package/pack/.claude/agents/overhaul-frontend-engineer.md +5 -3
  46. package/pack/.claude/agents/overhaul-implementer.md +9 -5
  47. package/pack/.claude/agents/overhaul-specialist.md +6 -3
  48. package/pack/.claude/agents/overhaul-verifier.md +3 -3
  49. package/pack/.claude/settings.json +1 -1
  50. package/pack/.claude/skills/devrites-lib/reference/intent-map.md +2 -0
  51. package/pack/.claude/skills/overhaul/SKILL.md +2 -1
  52. package/pack/.claude/skills/overhaul/references/audit-domains.md +139 -12
  53. package/pack/.claude/skills/overhaul/references/lifecycle.md +2 -0
  54. package/pack/.claude/skills/overhaul/references/orchestration.md +13 -5
  55. package/pack/.claude/skills/overhaul/references/records.md +12 -3
  56. package/pack/.claude/skills/overhaul/references/repair-and-measurement.md +108 -5
  57. package/pack/.claude/skills/overhaul/references/scope-and-safety.md +17 -2
  58. package/pack/.claude/skills/overhaul/references/scoring.md +36 -7
  59. package/pack/.claude/skills/overhaul/references/stack-profiles.md +52 -0
  60. package/pack/.claude/skills/rite/SKILL.md +4 -1
  61. package/pack/.claude/skills/rite-doctor/SKILL.md +2 -0
  62. package/pack/.claude/skills/rite-fast/SKILL.md +183 -0
  63. package/pack/.claude/skills/rite-fast/reference/artifacts.md +107 -0
  64. package/pack/.claude/skills/rite-fast/reference/check-and-triage.md +51 -0
  65. package/pack/.claude/skills/rite-quick/SKILL.md +2 -0
  66. package/pack/generated/claude/agents/fast-builder.md +90 -0
  67. package/pack/generated/claude/agents/fast-checker.md +60 -0
  68. package/pack/generated/claude/agents/fast-critic.md +70 -0
  69. package/pack/generated/claude/agents/fast-planner.md +95 -0
  70. package/pack/generated/claude/agents/overhaul-backend-engineer.md +5 -4
  71. package/pack/generated/claude/agents/overhaul-boundary.md +2 -2
  72. package/pack/generated/claude/agents/overhaul-craft.md +1 -1
  73. package/pack/generated/claude/agents/overhaul-frontend-engineer.md +5 -3
  74. package/pack/generated/claude/agents/overhaul-implementer.md +9 -5
  75. package/pack/generated/claude/agents/overhaul-specialist.md +6 -3
  76. package/pack/generated/claude/agents/overhaul-verifier.md +3 -3
  77. package/pack/generated/claude/settings.json +1 -1
  78. package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +2 -0
  79. package/pack/generated/claude/skills/overhaul/SKILL.md +2 -1
  80. package/pack/generated/claude/skills/overhaul/references/audit-domains.md +139 -12
  81. package/pack/generated/claude/skills/overhaul/references/lifecycle.md +2 -0
  82. package/pack/generated/claude/skills/overhaul/references/orchestration.md +13 -5
  83. package/pack/generated/claude/skills/overhaul/references/records.md +12 -3
  84. package/pack/generated/claude/skills/overhaul/references/repair-and-measurement.md +108 -5
  85. package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +17 -2
  86. package/pack/generated/claude/skills/overhaul/references/scoring.md +36 -7
  87. package/pack/generated/claude/skills/overhaul/references/stack-profiles.md +52 -0
  88. package/pack/generated/claude/skills/rite/SKILL.md +4 -1
  89. package/pack/generated/claude/skills/rite-doctor/SKILL.md +2 -0
  90. package/pack/generated/claude/skills/rite-fast/SKILL.md +183 -0
  91. package/pack/generated/claude/skills/rite-fast/reference/artifacts.md +107 -0
  92. package/pack/generated/claude/skills/rite-fast/reference/check-and-triage.md +51 -0
  93. package/pack/generated/claude/skills/rite-quick/SKILL.md +2 -0
  94. package/pack/generated/codex/AGENTS.md +1 -0
  95. package/pack/generated/codex/agents/fast-builder.toml +90 -0
  96. package/pack/generated/codex/agents/fast-checker.toml +60 -0
  97. package/pack/generated/codex/agents/fast-critic.toml +70 -0
  98. package/pack/generated/codex/agents/fast-planner.toml +95 -0
  99. package/pack/generated/codex/agents/overhaul-backend-engineer.toml +5 -4
  100. package/pack/generated/codex/agents/overhaul-boundary.toml +2 -2
  101. package/pack/generated/codex/agents/overhaul-craft.toml +1 -1
  102. package/pack/generated/codex/agents/overhaul-frontend-engineer.toml +5 -3
  103. package/pack/generated/codex/agents/overhaul-implementer.toml +9 -5
  104. package/pack/generated/codex/agents/overhaul-specialist.toml +6 -3
  105. package/pack/generated/codex/agents/overhaul-verifier.toml +3 -3
  106. package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +2 -0
  107. package/pack/generated/codex/skills/overhaul/SKILL.md +2 -1
  108. package/pack/generated/codex/skills/overhaul/references/audit-domains.md +139 -12
  109. package/pack/generated/codex/skills/overhaul/references/lifecycle.md +2 -0
  110. package/pack/generated/codex/skills/overhaul/references/orchestration.md +13 -5
  111. package/pack/generated/codex/skills/overhaul/references/records.md +12 -3
  112. package/pack/generated/codex/skills/overhaul/references/repair-and-measurement.md +108 -5
  113. package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +17 -2
  114. package/pack/generated/codex/skills/overhaul/references/scoring.md +36 -7
  115. package/pack/generated/codex/skills/overhaul/references/stack-profiles.md +52 -0
  116. package/pack/generated/codex/skills/rite/SKILL.md +4 -1
  117. package/pack/generated/codex/skills/rite-doctor/SKILL.md +2 -0
  118. package/pack/generated/codex/skills/rite-fast/SKILL.md +183 -0
  119. package/pack/generated/codex/skills/rite-fast/agents/openai.yaml +2 -0
  120. package/pack/generated/codex/skills/rite-fast/reference/artifacts.md +107 -0
  121. package/pack/generated/codex/skills/rite-fast/reference/check-and-triage.md +51 -0
  122. package/pack/generated/codex/skills/rite-quick/SKILL.md +2 -0
  123. package/pack/generated/devin/AGENTS.md +1 -0
  124. package/pack/generated/devin/agents/fast-builder.md +96 -0
  125. package/pack/generated/devin/agents/fast-checker.md +63 -0
  126. package/pack/generated/devin/agents/fast-critic.md +73 -0
  127. package/pack/generated/devin/agents/fast-planner.md +98 -0
  128. package/pack/generated/devin/agents/overhaul-backend-engineer.md +5 -4
  129. package/pack/generated/devin/agents/overhaul-boundary.md +2 -2
  130. package/pack/generated/devin/agents/overhaul-craft.md +1 -1
  131. package/pack/generated/devin/agents/overhaul-frontend-engineer.md +5 -3
  132. package/pack/generated/devin/agents/overhaul-implementer.md +9 -5
  133. package/pack/generated/devin/agents/overhaul-specialist.md +6 -3
  134. package/pack/generated/devin/agents/overhaul-verifier.md +3 -3
  135. package/pack/generated/devin/skills/devrites-lib/reference/intent-map.md +2 -0
  136. package/pack/generated/devin/skills/overhaul/SKILL.md +2 -1
  137. package/pack/generated/devin/skills/overhaul/references/audit-domains.md +139 -12
  138. package/pack/generated/devin/skills/overhaul/references/lifecycle.md +2 -0
  139. package/pack/generated/devin/skills/overhaul/references/orchestration.md +13 -5
  140. package/pack/generated/devin/skills/overhaul/references/records.md +12 -3
  141. package/pack/generated/devin/skills/overhaul/references/repair-and-measurement.md +108 -5
  142. package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +17 -2
  143. package/pack/generated/devin/skills/overhaul/references/scoring.md +36 -7
  144. package/pack/generated/devin/skills/overhaul/references/stack-profiles.md +52 -0
  145. package/pack/generated/devin/skills/rite/SKILL.md +4 -1
  146. package/pack/generated/devin/skills/rite-doctor/SKILL.md +2 -0
  147. package/pack/generated/devin/skills/rite-fast/SKILL.md +183 -0
  148. package/pack/generated/devin/skills/rite-fast/reference/artifacts.md +107 -0
  149. package/pack/generated/devin/skills/rite-fast/reference/check-and-triage.md +51 -0
  150. package/pack/generated/devin/skills/rite-quick/SKILL.md +2 -0
  151. package/pack/generated/omp/agents/fast-builder.md +89 -0
  152. package/pack/generated/omp/agents/fast-checker.md +59 -0
  153. package/pack/generated/omp/agents/fast-critic.md +69 -0
  154. package/pack/generated/omp/agents/fast-planner.md +94 -0
  155. package/pack/generated/omp/agents/overhaul-backend-engineer.md +5 -4
  156. package/pack/generated/omp/agents/overhaul-boundary.md +2 -2
  157. package/pack/generated/omp/agents/overhaul-craft.md +1 -1
  158. package/pack/generated/omp/agents/overhaul-frontend-engineer.md +5 -3
  159. package/pack/generated/omp/agents/overhaul-implementer.md +9 -5
  160. package/pack/generated/omp/agents/overhaul-specialist.md +6 -3
  161. package/pack/generated/omp/agents/overhaul-verifier.md +3 -3
  162. package/pack/generated/omp/commands/rite-fast.md +5 -0
  163. package/pack/generated/omp/skills/devrites-lib/reference/intent-map.md +2 -0
  164. package/pack/generated/omp/skills/overhaul/SKILL.md +2 -1
  165. package/pack/generated/omp/skills/overhaul/references/audit-domains.md +139 -12
  166. package/pack/generated/omp/skills/overhaul/references/lifecycle.md +2 -0
  167. package/pack/generated/omp/skills/overhaul/references/orchestration.md +13 -5
  168. package/pack/generated/omp/skills/overhaul/references/records.md +12 -3
  169. package/pack/generated/omp/skills/overhaul/references/repair-and-measurement.md +108 -5
  170. package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +17 -2
  171. package/pack/generated/omp/skills/overhaul/references/scoring.md +36 -7
  172. package/pack/generated/omp/skills/overhaul/references/stack-profiles.md +52 -0
  173. package/pack/generated/omp/skills/rite/SKILL.md +4 -1
  174. package/pack/generated/omp/skills/rite-doctor/SKILL.md +2 -0
  175. package/pack/generated/omp/skills/rite-fast/SKILL.md +183 -0
  176. package/pack/generated/omp/skills/rite-fast/reference/artifacts.md +107 -0
  177. package/pack/generated/omp/skills/rite-fast/reference/check-and-triage.md +51 -0
  178. package/pack/generated/omp/skills/rite-quick/SKILL.md +2 -0
  179. package/pack/generated/pi/AGENTS.md +1 -0
  180. package/pack/generated/pi/agents/fast-builder.md +90 -0
  181. package/pack/generated/pi/agents/fast-checker.md +60 -0
  182. package/pack/generated/pi/agents/fast-critic.md +70 -0
  183. package/pack/generated/pi/agents/fast-planner.md +95 -0
  184. package/pack/generated/pi/agents/overhaul-backend-engineer.md +5 -4
  185. package/pack/generated/pi/agents/overhaul-boundary.md +2 -2
  186. package/pack/generated/pi/agents/overhaul-craft.md +1 -1
  187. package/pack/generated/pi/agents/overhaul-frontend-engineer.md +5 -3
  188. package/pack/generated/pi/agents/overhaul-implementer.md +9 -5
  189. package/pack/generated/pi/agents/overhaul-specialist.md +6 -3
  190. package/pack/generated/pi/agents/overhaul-verifier.md +3 -3
  191. package/pack/generated/pi/prompts/rite-fast.md +5 -0
  192. package/pack/generated/pi/skills/devrites-lib/reference/intent-map.md +2 -0
  193. package/pack/generated/pi/skills/overhaul/SKILL.md +2 -1
  194. package/pack/generated/pi/skills/overhaul/references/audit-domains.md +139 -12
  195. package/pack/generated/pi/skills/overhaul/references/lifecycle.md +2 -0
  196. package/pack/generated/pi/skills/overhaul/references/orchestration.md +13 -5
  197. package/pack/generated/pi/skills/overhaul/references/records.md +12 -3
  198. package/pack/generated/pi/skills/overhaul/references/repair-and-measurement.md +108 -5
  199. package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +17 -2
  200. package/pack/generated/pi/skills/overhaul/references/scoring.md +36 -7
  201. package/pack/generated/pi/skills/overhaul/references/stack-profiles.md +52 -0
  202. package/pack/generated/pi/skills/rite/SKILL.md +4 -1
  203. package/pack/generated/pi/skills/rite-doctor/SKILL.md +2 -0
  204. package/pack/generated/pi/skills/rite-fast/SKILL.md +183 -0
  205. package/pack/generated/pi/skills/rite-fast/reference/artifacts.md +107 -0
  206. package/pack/generated/pi/skills/rite-fast/reference/check-and-triage.md +51 -0
  207. package/pack/generated/pi/skills/rite-quick/SKILL.md +2 -0
  208. package/package.json +1 -1
  209. package/scripts/codex-generate.sh +2 -1
  210. package/scripts/devin-generate.sh +4 -3
  211. package/scripts/omp-generate.sh +3 -3
  212. package/scripts/pi-generate.sh +4 -3
@@ -0,0 +1,154 @@
1
+ package bench
2
+
3
+ import (
4
+ "math"
5
+ "sort"
6
+ )
7
+
8
+ // uNull returns P(U = u) for u = 0..n*m under the Mann-Whitney null with arm
9
+ // sizes n and m, from the generating function prod_i (1-q^(m+i))/(1-q^i).
10
+ // Cancellation leaves the upper half inaccurate; only the lower tail is read.
11
+ func uNull(n, m int) []float64 {
12
+ c := make([]float64, n*m+1)
13
+ c[0] = 1
14
+ for i := 1; i <= n; i++ {
15
+ for u := len(c) - 1; u >= m+i; u-- {
16
+ c[u] -= c[u-m-i]
17
+ }
18
+ for u := i; u < len(c); u++ {
19
+ c[u] += c[u-i]
20
+ }
21
+ }
22
+ total := 0.0
23
+ for _, v := range c {
24
+ total += v
25
+ }
26
+ for i := range c {
27
+ c[i] /= total
28
+ }
29
+ return c
30
+ }
31
+
32
+ // exactLimit bounds the arm sizes for the exact null distribution, well inside
33
+ // float64 range and cheap to compute; beyond it the (slightly conservative)
34
+ // normal approximation takes over.
35
+ const exactLimit = 400
36
+
37
+ // maxPairs caps n*m for the pairwise differences, which are held in memory.
38
+ const maxPairs = 4_000_000
39
+
40
+ // tailCount returns the largest k with P(U <= k-1) <= alpha/2: the pairwise
41
+ // differences D_(1..k-1) and D_(nm-k+2..nm) fall outside a (1-alpha) interval,
42
+ // whose endpoints are D_(k) and D_(nm+1-k). Zero means no finite interval.
43
+ func tailCount(n, m int, alpha float64) int {
44
+ if n+m <= exactLimit {
45
+ p, cum, k := uNull(n, m), 0.0, 0
46
+ for u := range p {
47
+ if cum += p[u]; cum > alpha/2 {
48
+ break
49
+ }
50
+ k = u + 1
51
+ }
52
+ return k
53
+ }
54
+ mean := float64(n*m) / 2
55
+ sd := math.Sqrt(float64(n*m*(n+m+1)) / 12)
56
+ return max(0, int(math.Floor(mean-normalQuantile(1-alpha/2)*sd-0.5))+1)
57
+ }
58
+
59
+ // hlRatio is the Hodges-Lehmann ratio estimate exp(median(ln x_i - ln y_j))
60
+ // with its distribution-free (1-alpha) bounds. ok is false when a sample is
61
+ // not positive or the arms are too small for a finite interval.
62
+ func hlRatio(x, y []float64, alpha float64) (est, lo, hi float64, ok bool) {
63
+ if len(x)*len(y) > maxPairs {
64
+ return 0, 0, 0, false
65
+ }
66
+ z := make([]float64, 0, len(x)*len(y))
67
+ for _, a := range x {
68
+ for _, b := range y {
69
+ if a <= 0 || b <= 0 {
70
+ return 0, 0, 0, false
71
+ }
72
+ z = append(z, math.Log(a)-math.Log(b))
73
+ }
74
+ }
75
+ k := tailCount(len(x), len(y), alpha)
76
+ if k == 0 {
77
+ return 0, 0, 0, false
78
+ }
79
+ sort.Float64s(z)
80
+ return math.Exp(median(z)), math.Exp(z[k-1]), math.Exp(z[len(z)-k]), true
81
+ }
82
+
83
+ // quantileBounds returns one-sided (1-alpha) distribution-free bounds for the
84
+ // p-quantile of xs from its order statistics; a missing bound is ±Inf. With
85
+ // F(j) = P(Bin(n, p) <= j), the upper bound is x_(k) for the smallest k with
86
+ // F(k-1) >= 1-alpha and the lower bound x_(j) for the largest j with
87
+ // F(j-1) <= alpha.
88
+ func quantileBounds(xs []float64, p, alpha float64) (lcb, ucb float64) {
89
+ s := append([]float64(nil), xs...)
90
+ sort.Float64s(s)
91
+ n := len(s)
92
+ lcb, ucb = math.Inf(-1), math.Inf(1)
93
+ lg := func(k int) float64 { v, _ := math.Lgamma(float64(k) + 1); return v }
94
+ cdf := 0.0
95
+ for i := 0; i < n; i++ { // cdf = F(i) after this step
96
+ cdf += math.Exp(lg(n) - lg(i) - lg(n-i) + float64(i)*math.Log(p) + float64(n-i)*math.Log1p(-p))
97
+ if cdf <= alpha {
98
+ lcb = s[i] // j = i+1
99
+ }
100
+ if cdf >= 1-alpha {
101
+ ucb = s[i] // k = i+1
102
+ break
103
+ }
104
+ }
105
+ return lcb, ucb
106
+ }
107
+
108
+ // quantile7 is the linearly interpolated sample quantile (Hyndman-Fan type 7).
109
+ func quantile7(xs []float64, p float64) float64 {
110
+ s := append([]float64(nil), xs...)
111
+ sort.Float64s(s)
112
+ h := p * float64(len(s)-1)
113
+ i := int(math.Floor(h))
114
+ if i+1 >= len(s) {
115
+ return s[len(s)-1]
116
+ }
117
+ return s[i] + (h-float64(i))*(s[i+1]-s[i])
118
+ }
119
+
120
+ // wilson returns the Wilson score interval for e events in n trials.
121
+ func wilson(p float64, n, z float64) (lo, hi float64) {
122
+ d := 1 + z*z/n
123
+ c := (p + z*z/(2*n)) / d
124
+ h := z * math.Sqrt(p*(1-p)/n+z*z/(4*n*n)) / d
125
+ return max(0, c-h), min(1, c+h)
126
+ }
127
+
128
+ // newcombe returns the hybrid score interval (Newcombe 1998, method 10) for
129
+ // p1 - p2 from proportions over n1 and n2 trials.
130
+ func newcombe(p1, n1, p2, n2, z float64) (lo, hi float64) {
131
+ l1, u1 := wilson(p1, n1, z)
132
+ l2, u2 := wilson(p2, n2, z)
133
+ d := p1 - p2
134
+ return d - math.Sqrt((p1-l1)*(p1-l1)+(u2-p2)*(u2-p2)), d + math.Sqrt((u1-p1)*(u1-p1)+(p2-l2)*(p2-l2))
135
+ }
136
+
137
+ // normalQuantile is the standard normal inverse CDF (Acklam's rational
138
+ // approximation, relative error below 1.2e-9).
139
+ func normalQuantile(p float64) float64 {
140
+ a := []float64{-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02, 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00}
141
+ b := []float64{-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02, 6.680131188771972e+01, -1.328068155288572e+01}
142
+ c := []float64{-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00, -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00}
143
+ d := []float64{7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00}
144
+ switch {
145
+ case p < 0.02425:
146
+ q := math.Sqrt(-2 * math.Log(p))
147
+ return (((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q + c[5]) / ((((d[0]*q+d[1])*q+d[2])*q+d[3])*q + 1)
148
+ case p > 1-0.02425:
149
+ return -normalQuantile(1 - p)
150
+ }
151
+ q := p - 0.5
152
+ r := q * q
153
+ return (((((a[0]*r+a[1])*r+a[2])*r+a[3])*r+a[4])*r + a[5]) * q / (((((b[0]*r+b[1])*r+b[2])*r+b[3])*r+b[4])*r + 1)
154
+ }
@@ -156,3 +156,7 @@ func Truthy(v any) bool {
156
156
  }
157
157
  return true
158
158
  }
159
+
160
+ // Domains is the fixed set of score domains in their documented order. Findings,
161
+ // receipts, the applicability matrix and the rubric all use these keys.
162
+ var Domains = []string{"correctness", "security", "reliability", "performance", "tests", "architecture", "ux", "operations"}
@@ -580,6 +580,9 @@ func Check(run, genDir, prevDir string) ([]string, error) {
580
580
  add("run.json: readiness_verdict differs from scorecard.json")
581
581
  }
582
582
  }
583
+ if err := consistency(run, genDir, R, add); err != nil {
584
+ return nil, err
585
+ }
583
586
  ap := ovio.List(R["approval.json"]["events"])
584
587
  uniq(ap, idKey, "approval", add)
585
588
  stopAt := -1
@@ -0,0 +1,217 @@
1
+ package records
2
+
3
+ import (
4
+ "encoding/json"
5
+ "slices"
6
+
7
+ "github.com/devrites/devrites/internal/overhaul/ovio"
8
+ )
9
+
10
+ var (
11
+ jsonZero = any(json.Number("0"))
12
+ domains = set(ovio.Domains...)
13
+ cellStates = set("applicable", "not-applicable", "blocked")
14
+ rangeStates = set("inventoried", "tool-scanned", "semantically-reviewed", "cross-boundary-reviewed", "verified", "excluded", "blocked")
15
+ reviewedRange = set("semantically-reviewed", "cross-boundary-reviewed", "verified", "excluded")
16
+ openFinding = set("confirmed", "approved-for-fix", "in-progress", "fixed-unverified", "deferred", "accepted-risk")
17
+ serious = set("critical", "high")
18
+ )
19
+
20
+ // consistency checks that findings, the coverage ledger, the applicability
21
+ // matrix, the control results and the recorded gates agree with each other, so
22
+ // a gate or control cannot claim PASS against the run's own records.
23
+ func consistency(run, genDir string, R map[string]map[string]any, add func(string, ...any)) error {
24
+ controls, err := rubricControls(run, R["run.json"])
25
+ if err != nil {
26
+ return err
27
+ }
28
+ fs := ovio.List(R["findings.json"]["findings"])
29
+ for _, it := range fs {
30
+ f := ovio.Obj(it)
31
+ fid := PyStr(f["id"])
32
+ if !domains[str(f, "domain")] {
33
+ add("finding %s: domain must be one of %s", fid, Repr(ovio.Domains))
34
+ }
35
+ if controls == nil {
36
+ continue
37
+ }
38
+ cs := ovio.List(f["controls"])
39
+ if len(cs) == 0 && severityOK[str(f, "status")] {
40
+ add("finding %s: a confirmed finding names the rubric controls it fails", fid)
41
+ }
42
+ for _, c := range cs {
43
+ if !controls.has(c) {
44
+ add("finding %s: unknown control %s", fid, PyStr(c))
45
+ }
46
+ }
47
+ }
48
+
49
+ cov := R["coverage.json"]
50
+ cells := ovio.List(cov["applicability"])
51
+ seen := vset{}
52
+ for _, it := range cells {
53
+ c := ovio.Obj(it)
54
+ key := Repr([]any{c["domain"], c["component"], c["lane"]})
55
+ if seen.has(key) {
56
+ add("applicability %s: duplicate cell", key)
57
+ }
58
+ seen.add(key)
59
+ if !domains[str(c, "domain")] || str(c, "component") == "" || str(c, "lane") == "" {
60
+ add("applicability %s: needs a known domain, a component and a lane", key)
61
+ }
62
+ st := str(c, "state")
63
+ if !cellStates[st] {
64
+ add("applicability %s: state must be applicable, not-applicable or blocked", key)
65
+ }
66
+ if st != "applicable" && !ovio.Truthy(c["reason"]) {
67
+ add("applicability %s: %s needs a reason", key, st)
68
+ }
69
+ for _, r := range ovio.List(c["receipts"]) {
70
+ if p := ovio.Str(r); p == "" || !inside(run, p) || !isFile(join(run, p)) {
71
+ add("applicability %s: receipt %s missing or outside the run dir", key, PyStr(r))
72
+ }
73
+ }
74
+ }
75
+ for _, it := range ovio.List(cov["files"]) {
76
+ for _, r := range ovio.List(ovio.Obj(it)["ranges"]) {
77
+ if st := str(ovio.Obj(r), "state"); !rangeStates[st] {
78
+ add("coverage %s: range state %s is not a coverage state", PyStr(ovio.Obj(it)["path"]), Repr(st))
79
+ }
80
+ }
81
+ }
82
+
83
+ results := ""
84
+ for _, name := range []string{"results-candidate.json", "results-baseline.json"} {
85
+ if isFile(join(genDir, name)) {
86
+ results = name
87
+ break
88
+ }
89
+ }
90
+ if results != "" {
91
+ doc, err := ovio.LoadObject(join(genDir, results))
92
+ if err != nil {
93
+ return err
94
+ }
95
+ res := ovio.Obj(doc["results"])
96
+ for _, it := range fs {
97
+ f := ovio.Obj(it)
98
+ if !openFinding[str(f, "status")] {
99
+ continue
100
+ }
101
+ for _, c := range ovio.List(f["controls"]) {
102
+ if str(ovio.Obj(res[ovio.Str(c)]), "status") == "PASS" {
103
+ add("finding %s: control %s is PASS in %s while the finding is %s",
104
+ PyStr(f["id"]), PyStr(c), results, str(f, "status"))
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ scp := join(genDir, "scorecard.json")
111
+ if !isFile(scp) {
112
+ return nil
113
+ }
114
+ sc, err := ovio.LoadObject(scp)
115
+ if err != nil {
116
+ return err
117
+ }
118
+ gates := ovio.Obj(sc["gates"])
119
+ claims := func(g string) bool { return str(gates, g) == "PASS" }
120
+ for _, it := range fs {
121
+ f := ovio.Obj(it)
122
+ fid, status := PyStr(f["id"]), str(f, "status")
123
+ if claims("G-NO-CRITICAL-HIGH") && openFinding[status] && serious[str(f, "severity")] {
124
+ add("scorecard: G-NO-CRITICAL-HIGH is PASS but finding %s is %s %s", fid, status, str(f, "severity"))
125
+ }
126
+ if claims("G-NO-OPEN-SERIOUS-LEAD") && (status == "candidate" || status == "needs-validation") &&
127
+ serious[str(f, "potential_impact")] {
128
+ add("scorecard: G-NO-OPEN-SERIOUS-LEAD is PASS but lead %s is open with potential impact %s", fid, str(f, "potential_impact"))
129
+ }
130
+ }
131
+ if !claims("G-COVERAGE") {
132
+ return nil
133
+ }
134
+ admitted := map[string]bool{}
135
+ for _, it := range ovio.List(R["dispatch.json"]["attempts"]) {
136
+ if x := ovio.Obj(it); str(x, "status") == "admitted" {
137
+ admitted[str(x, "receipt")] = true
138
+ }
139
+ }
140
+ if len(cells) == 0 {
141
+ add("scorecard: G-COVERAGE is PASS without an applicability matrix in coverage.json")
142
+ }
143
+ for _, it := range cells {
144
+ c := ovio.Obj(it)
145
+ key := Repr([]any{c["domain"], c["component"], c["lane"]})
146
+ switch str(c, "state") {
147
+ case "blocked":
148
+ add("scorecard: G-COVERAGE is PASS but applicability %s is blocked", key)
149
+ case "applicable":
150
+ if !checkedBy(run, str(c, "domain"), ovio.List(c["receipts"]), admitted) {
151
+ add("scorecard: G-COVERAGE is PASS but no admitted, non-gap receipt of applicability %s lists %s in domains_checked", key, str(c, "domain"))
152
+ }
153
+ }
154
+ }
155
+ for _, it := range ovio.List(cov["files"]) {
156
+ f := ovio.Obj(it)
157
+ if !ovio.Truthy(f["eligible"]) {
158
+ continue
159
+ }
160
+ rs := ovio.List(f["ranges"])
161
+ done := len(rs) > 0 || eq(f["lines"], jsonZero)
162
+ for _, r := range rs {
163
+ done = done && reviewedRange[str(ovio.Obj(r), "state")]
164
+ }
165
+ if !done {
166
+ add("scorecard: G-COVERAGE is PASS but eligible file %s has unreviewed ranges", PyStr(f["path"]))
167
+ }
168
+ }
169
+ return nil
170
+ }
171
+
172
+ // checkedBy reports whether any admitted, non-gap receipt records domain in
173
+ // domains_checked.
174
+ func checkedBy(run, domain string, receipts []any, admitted map[string]bool) bool {
175
+ for _, r := range receipts {
176
+ p := ovio.Str(r)
177
+ if !admitted[p] || !inside(run, p) || !isFile(join(run, p)) {
178
+ continue
179
+ }
180
+ rc, err := ovio.LoadObject(join(run, p))
181
+ if err != nil || str(rc, "outcome") == "gap" {
182
+ continue
183
+ }
184
+ if slices.ContainsFunc(ovio.List(rc["domains_checked"]), func(d any) bool { return d == any(domain) }) {
185
+ return true
186
+ }
187
+ }
188
+ return false
189
+ }
190
+
191
+ // rubricControls returns the control IDs of the run's rubric (revisions.rubric,
192
+ // else the plan's pinned rubric), or nil when no rubric is frozen yet.
193
+ func rubricControls(run string, rj map[string]any) (vset, error) {
194
+ revs := ovio.Obj(rj["revisions"])
195
+ rf := str(ovio.Obj(revs["rubric"]), "file")
196
+ if rf == "" {
197
+ if pf := str(ovio.Obj(revs["plan"]), "file"); pf != "" && inside(run, pf) && isFile(join(run, pf)) {
198
+ plan, err := ovio.LoadObject(join(run, pf))
199
+ if err != nil {
200
+ return nil, err
201
+ }
202
+ rf = str(ovio.Obj(plan["rubric"]), "file")
203
+ }
204
+ }
205
+ if rf == "" || !inside(run, rf) || !isFile(join(run, rf)) {
206
+ return nil, nil
207
+ }
208
+ doc, err := ovio.LoadObject(join(run, rf))
209
+ if err != nil {
210
+ return nil, err
211
+ }
212
+ ids := vset{}
213
+ for _, c := range ovio.List(doc["controls"]) {
214
+ ids.add(ovio.Obj(c)["id"])
215
+ }
216
+ return ids, nil
217
+ }
@@ -80,6 +80,7 @@ func (f *fixture) records() map[string]J {
80
80
  "coverage.json": {"schema": "overhaul.coverage/1", "files": L{J{"path": "src/a.py", "eligible": true}}},
81
81
  "findings.json": {"schema": "overhaul.findings/1", "findings": L{J{"id": "F-1", "fingerprint": "fp-1",
82
82
  "kind": "defect", "scope_origin": "pre-existing", "status": "confirmed", "severity": "high",
83
+ "domain": "correctness", "controls": L{"C-1"},
83
84
  "evidence": L{"E-1"}, "locations": L{J{"path": "src/a.py", "start": 1, "end": 1},
84
85
  J{"path": "mobile-app:src/api.ts", "external": true}},
85
86
  "history": L{J{"status": "candidate"}, J{"status": "confirmed"}}}}},
@@ -186,6 +187,25 @@ func TestPublishThenValidate(t *testing.T) {
186
187
  }
187
188
  }
188
189
 
190
+ func cell(r map[string]J) J { return r["coverage.json"]["applicability"].(L)[0].(J) }
191
+
192
+ // scorecard records a scorecard with the given gates and points run.json at its rubric.
193
+ func scorecard(r map[string]J, gates J) {
194
+ r["run.json"]["revisions"].(J)["rubric"] = J{"rev": 1, "file": "revisions/rubric-r1.json"}
195
+ r["scorecard.json"] = J{"schema": "overhaul.scorecard/1", "gates": gates}
196
+ }
197
+
198
+ // reviewed records one applicability cell in state st, backed by a receipt that
199
+ // checked its domain, and a fully reviewed range for the fixture's file.
200
+ func reviewed(f *fixture, r map[string]J, st string) {
201
+ write(f.t, filepath.Join(f.run, "receipts/R-dom.json"), J{"outcome": "no-findings", "domains_checked": L{"correctness"}})
202
+ addAttempt(r, J{"task_id": "W-1", "attempt_id": "A1", "role": "reviewer", "status": "admitted", "receipt": "receipts/R-dom.json"})
203
+ r["coverage.json"]["applicability"] = L{J{"domain": "correctness", "component": "api", "lane": "backend",
204
+ "state": st, "reason": "no GUI", "receipts": L{"receipts/R-dom.json"}}}
205
+ r["coverage.json"]["files"] = L{J{"path": "src/a.py", "eligible": true,
206
+ "ranges": L{J{"start": 1, "end": 1, "state": "semantically-reviewed"}}}}
207
+ }
208
+
189
209
  func TestSingleGenerationRules(t *testing.T) {
190
210
  verifiedHistory := L{J{"status": "candidate"}, J{"status": "confirmed"}, J{"status": "approved-for-fix"},
191
211
  J{"status": "in-progress"}, J{"status": "fixed-unverified"}, J{"status": "verified"}}
@@ -370,6 +390,67 @@ func TestSingleGenerationRules(t *testing.T) {
370
390
  {"ineligible coverage without reason", func(f *fixture, r map[string]J) {
371
391
  r["coverage.json"]["files"] = L{J{"path": "src/a.py", "eligible": false}}
372
392
  }, "coverage src/a.py: ineligible without an exclusion reason"},
393
+ {"finding without domain", func(f *fixture, r map[string]J) { delete(finding(r), "domain") }, "finding F-1: domain must be one of"},
394
+ {"confirmed finding without controls", func(f *fixture, r map[string]J) { delete(finding(r), "controls") },
395
+ "finding F-1: a confirmed finding names the rubric controls it fails"},
396
+ {"finding with unknown control", func(f *fixture, r map[string]J) { finding(r)["controls"] = L{"C-9"} }, "finding F-1: unknown control C-9"},
397
+ {"open finding with passing control", func(f *fixture, r map[string]J) {
398
+ r["results-candidate.json"] = J{"schema": "overhaul.results/1", "results": J{"C-1": J{"status": "PASS"}}}
399
+ }, "finding F-1: control C-1 is PASS in results-candidate.json while the finding is confirmed"},
400
+ {"critical-high gate against open finding", func(f *fixture, r map[string]J) {
401
+ scorecard(r, J{"G-NO-CRITICAL-HIGH": "PASS"})
402
+ }, "G-NO-CRITICAL-HIGH is PASS but finding F-1 is confirmed high"},
403
+ {"serious-lead gate against open lead", func(f *fixture, r map[string]J) {
404
+ r["findings.json"]["findings"] = append(r["findings.json"]["findings"].(L), J{"id": "F-2", "fingerprint": "fp-2",
405
+ "kind": "defect", "scope_origin": "pre-existing", "status": "candidate", "domain": "security",
406
+ "potential_impact": "critical", "history": L{J{"status": "candidate"}}})
407
+ scorecard(r, J{"G-NO-OPEN-SERIOUS-LEAD": "PASS"})
408
+ }, "G-NO-OPEN-SERIOUS-LEAD is PASS but lead F-2 is open with potential impact critical"},
409
+ {"coverage gate without matrix", func(f *fixture, r map[string]J) { scorecard(r, J{"G-COVERAGE": "PASS"}) },
410
+ "G-COVERAGE is PASS without an applicability matrix"},
411
+ {"coverage gate with blocked cell", func(f *fixture, r map[string]J) {
412
+ reviewed(f, r, "blocked")
413
+ scorecard(r, J{"G-COVERAGE": "PASS"})
414
+ }, "applicability ['correctness', 'api', 'backend'] is blocked"},
415
+ {"coverage gate with unchecked domain", func(f *fixture, r map[string]J) {
416
+ reviewed(f, r, "applicable")
417
+ cell(r)["receipts"] = L{"receipts/R-1.json"}
418
+ scorecard(r, J{"G-COVERAGE": "PASS"})
419
+ }, "no admitted, non-gap receipt of applicability ['correctness', 'api', 'backend'] lists correctness in domains_checked"},
420
+ {"coverage gate with unreviewed range", func(f *fixture, r map[string]J) {
421
+ reviewed(f, r, "applicable")
422
+ r["coverage.json"]["files"] = L{J{"path": "src/a.py", "eligible": true, "ranges": L{J{"start": 1, "end": 1, "state": "tool-scanned"}}}}
423
+ scorecard(r, J{"G-COVERAGE": "PASS"})
424
+ }, "eligible file src/a.py has unreviewed ranges"},
425
+ {"coverage gate with rejected receipt", func(f *fixture, r map[string]J) {
426
+ reviewed(f, r, "applicable")
427
+ attempts(r)[len(attempts(r))-1].(J)["status"] = "rejected"
428
+ scorecard(r, J{"G-COVERAGE": "PASS"})
429
+ }, "no admitted, non-gap receipt of applicability"},
430
+ {"coverage gate with gap receipt", func(f *fixture, r map[string]J) {
431
+ reviewed(f, r, "applicable")
432
+ write(f.t, filepath.Join(f.run, "receipts/R-dom.json"), J{"outcome": "gap", "domains_checked": L{"correctness"}})
433
+ scorecard(r, J{"G-COVERAGE": "PASS"})
434
+ }, "no admitted, non-gap receipt of applicability"},
435
+ {"coverage gate counts empty files", func(f *fixture, r map[string]J) {
436
+ reviewed(f, r, "applicable")
437
+ r["coverage.json"]["files"] = L{J{"path": "src/a.py", "eligible": true, "lines": 0}}
438
+ scorecard(r, J{"G-COVERAGE": "PASS"})
439
+ }, ""},
440
+ {"coverage gate backed by records", func(f *fixture, r map[string]J) {
441
+ reviewed(f, r, "applicable")
442
+ scorecard(r, J{"G-COVERAGE": "PASS"})
443
+ }, ""},
444
+ {"unknown applicability state", func(f *fixture, r map[string]J) {
445
+ reviewed(f, r, "maybe")
446
+ }, "state must be applicable, not-applicable or blocked"},
447
+ {"not-applicable cell without reason", func(f *fixture, r map[string]J) {
448
+ reviewed(f, r, "not-applicable")
449
+ delete(cell(r), "reason")
450
+ }, "not-applicable needs a reason"},
451
+ {"unknown range state", func(f *fixture, r map[string]J) {
452
+ r["coverage.json"]["files"] = L{J{"path": "src/a.py", "eligible": true, "ranges": L{J{"start": 1, "end": 1, "state": "done"}}}}
453
+ }, "range state 'done' is not a coverage state"},
373
454
  }
374
455
  for _, c := range cases {
375
456
  t.Run(c.name, func(t *testing.T) {
@@ -339,7 +339,7 @@ func publish(run string, stdout, stderr io.Writer) (int, error) {
339
339
  }
340
340
  manifest := map[string]any{"schema": "overhaul.manifest/1", "generation": gen, "previous": previous,
341
341
  "files": files, "revisions": revs}
342
- if err := ovio.WriteJSON(join(tmp, "manifest.json"), manifest, 0o666); err != nil {
342
+ if err := ovio.WriteJSON(join(tmp, "manifest.json"), manifest, 0o644); err != nil {
343
343
  return 0, err
344
344
  }
345
345
  if err := os.Rename(tmp, join(run, gen)); err != nil {
@@ -731,6 +731,8 @@ func build(run, gen string) (*built, error) {
731
731
  }
732
732
  S = append(S,
733
733
  section{"coverage", "Coverage ledger", []string{"path", "eligible", "lanes", "ranges (state)", "exclusion"}, covRows},
734
+ section{"applicability", "Applicability matrix (a cell without a receipt is not checked)", []string{"domain", "component", "lane", "state", "reason", "receipts"},
735
+ rowsOf(objs(R["coverage"]["applicability"]), "domain", "component", "lane", "state", "reason", "receipts")},
734
736
  section{"profiles", "Stack profiles", []string{"id", "revision", "status", "file", "sha256"},
735
737
  rowsOf(objs(R["stack-profiles"]["profiles"]), "id", "rev", "status", "file", "sha256")},
736
738
  section{"contracts", "Contracts and boundaries", []string{"id", "producer", "consumers", "version", "evidence", "open questions"},
@@ -73,7 +73,8 @@ func fixture(t *testing.T, withPlan bool) (run, gen string) {
73
73
  obj{"path": fakeHome + "/proj/a.go", "eligible": true, "lanes": []any{"backend"},
74
74
  "ranges": []any{obj{"start": 1, "end": 9, "state": "verified"}}},
75
75
  obj{"path": "vendor/x.js", "eligible": false, "exclusion": obj{"reason": hostile}},
76
- }})
76
+ }, "applicability": []any{obj{"domain": "operations", "component": "api", "lane": "backend", "state": "blocked",
77
+ "reason": "no container runtime"}}})
77
78
  writeJSON(t, filepath.Join(gen, "findings.json"), obj{"findings": []any{
78
79
  obj{"id": "F-1", "title": hostile, "kind": "defect", "severity": "high", "status": "confirmed",
79
80
  "failing_scenario": "nil deref"},
@@ -259,6 +260,9 @@ func TestSectionContents(t *testing.T) {
259
260
  if !strings.Contains(sectionHTML(t, h, "approvals"), "QUOTE-approve r1") {
260
261
  t.Fatal("approval quote missing")
261
262
  }
263
+ if !strings.Contains(sectionHTML(t, h, "applicability"), "no container runtime") {
264
+ t.Fatal("blocked applicability cell missing from the report")
265
+ }
262
266
  }
263
267
 
264
268
  func TestPlanMarkdown(t *testing.T) {
@@ -31,11 +31,16 @@ var (
31
31
  repairGates = []string{"G-APPROVAL", "G-TASKS-COMPLETE", "G-ORACLES", "G-NO-REGRESSION", "G-PERF-TARGETS",
32
32
  "G-PRESERVATION", "G-FINGERPRINTS", "G-OWNERSHIP", "G-FINAL-CHALLENGE"}
33
33
  mayBeNA = set("G-PERF-TARGETS", "G-LANE-RECEIPTS", "G-CONCURRENCY")
34
- // domainOrder is the fixed domain set in its documented order, which also
35
- // orders the per-domain threshold rows.
36
- domainOrder = []string{"correctness", "security", "reliability", "performance", "tests", "architecture", "ux", "operations"}
37
- defaultPolicy = [][2]string{{"global_min", "9.7"}, {"lane_min", "9.7"}, {"domain_min", "9.0"}, {"lane_domain_min", "9.0"}}
38
- transitions = map[[2]string]string{
34
+ // domainOrder also orders the per-domain threshold rows.
35
+ domainOrder = ovio.Domains
36
+ // defaultDomainWeights is the documented default weight table; a rubric
37
+ // that deviates records the reason and the approval in weight_deviations.
38
+ defaultDomainWeights = map[string]int64{"correctness": 20, "security": 20, "reliability": 15, "performance": 15,
39
+ "tests": 10, "architecture": 10, "ux": 5, "operations": 5}
40
+ controlWeights = set("1", "3", "5")
41
+ evaluations = set("executed", "static", "source-proof", "human")
42
+ defaultPolicy = [][2]string{{"global_min", "9.7"}, {"lane_min", "9.7"}, {"domain_min", "9.0"}, {"lane_domain_min", "9.0"}}
43
+ transitions = map[[2]string]string{
39
44
  {"FAIL", "PASS"}: "fixed-or-newly-proven",
40
45
  {"UNKNOWN", "PASS"}: "evidence-only",
41
46
  {"PASS", "FAIL"}: "regression-or-new-failure",
@@ -69,10 +74,15 @@ func run(args []string) (string, error) {
69
74
  if err != nil {
70
75
  return "", err
71
76
  }
72
- docs, _, err := load(o["--rubric"], o["--baseline"], o["--candidate"])
77
+ docs, digs, err := load(o["--rubric"], o["--baseline"], o["--candidate"])
73
78
  if err != nil {
74
79
  return "", err
75
80
  }
81
+ for i, arm := range []string{"baseline", "candidate"} {
82
+ if err := sameRubric(docs[i+1], digs[0], arm); err != nil {
83
+ return "", err
84
+ }
85
+ }
76
86
  res, err := compare(docs[0], docs[1], docs[2])
77
87
  if err != nil {
78
88
  return "", err
@@ -88,8 +98,8 @@ func run(args []string) (string, error) {
88
98
  if err != nil {
89
99
  return "", err
90
100
  }
91
- if d := docs[1]["rubric_digest"]; d != nil && d != any(digs[0]) {
92
- return "", fmt.Errorf("results were produced for a different rubric digest")
101
+ if err := sameRubric(docs[1], digs[0], "results"); err != nil {
102
+ return "", err
93
103
  }
94
104
  out, err := score(docs[0], docs[1], docs[2])
95
105
  if err != nil {
@@ -110,6 +120,18 @@ func run(args []string) (string, error) {
110
120
  return fmt.Sprintf("%s -> %s\n", out["readiness_verdict"], path), nil
111
121
  }
112
122
 
123
+ // sameRubric requires results to name the digest of the rubric they were
124
+ // produced for, so two scorecards are only comparable on one rubric revision.
125
+ func sameRubric(results map[string]any, digest, what string) error {
126
+ switch d := results["rubric_digest"]; {
127
+ case d == nil:
128
+ return fmt.Errorf("%s: rubric_digest is required", what)
129
+ case d != any(digest):
130
+ return fmt.Errorf("%s were produced for a different rubric digest", what)
131
+ }
132
+ return nil
133
+ }
134
+
113
135
  // flags parses "--name value" pairs; every required flag must be present and
114
136
  // no other flag is accepted.
115
137
  func flags(args, required, optional []string) (map[string]string, error) {
@@ -181,6 +203,7 @@ func needReason(v any, what string) error {
181
203
 
182
204
  type control struct {
183
205
  id, domain string
206
+ evaluation string
184
207
  lanes []string
185
208
  weight *big.Rat
186
209
  na, hard, indep bool
@@ -224,12 +247,27 @@ func checkRubric(r map[string]any) (*rubricSpec, error) {
224
247
  return nil, errPolicy
225
248
  }
226
249
  }
250
+ deviations := map[string]bool{}
251
+ for _, e := range ovio.List(r["weight_deviations"]) {
252
+ m := ovio.Obj(e)
253
+ d, _ := m["domain"].(string)
254
+ if d == "" || !ovio.Truthy(m["reason"]) || !ovio.Truthy(m["approval"]) {
255
+ return nil, fmt.Errorf("weight deviation %q needs a domain, a reason and an approval reference", d)
256
+ }
257
+ if _, known := defaultDomainWeights[d]; !known || deviations[d] {
258
+ return nil, fmt.Errorf("weight deviation %q names an unknown or repeated domain", d)
259
+ }
260
+ deviations[d] = true
261
+ }
227
262
  doms := ovio.Obj(r["domains"])
228
263
  for _, d := range slices.Sorted(maps.Keys(doms)) {
229
264
  w, err := num(doms[d], "domain weight "+d)
230
265
  if err != nil {
231
266
  return nil, err
232
267
  }
268
+ if def, ok := defaultDomainWeights[d]; ok && w.Cmp(big.NewRat(def, 1)) != 0 && !deviations[d] {
269
+ return nil, fmt.Errorf("domain weight %s is %s, not the default %d, without a weight_deviations entry", d, w.RatString(), def)
270
+ }
233
271
  sp.dw[d] = w
234
272
  }
235
273
  for _, e := range ovio.List(r["domain_exclusions"]) {
@@ -310,8 +348,15 @@ func checkRubric(r map[string]any) (*rubricSpec, error) {
310
348
  if err != nil {
311
349
  return nil, err
312
350
  }
351
+ if !controlWeights[w.RatString()] {
352
+ return nil, fmt.Errorf("%s: weight must be 1, 3 or 5, got %s", id, w.RatString())
353
+ }
354
+ ev, _ := c["evaluation"].(string)
355
+ if !evaluations[ev] {
356
+ return nil, fmt.Errorf("%s: evaluation must be executed, static, source-proof or human, got %v", id, c["evaluation"])
357
+ }
313
358
  sp.controls = append(sp.controls, control{id: id, domain: dom, lanes: lanes, weight: w, na: na != nil,
314
- hard: ovio.Truthy(c["hard_gate"]), indep: ovio.Truthy(c["independent_verification"])})
359
+ hard: ovio.Truthy(c["hard_gate"]), indep: ovio.Truthy(c["independent_verification"]), evaluation: ev})
315
360
  }
316
361
  return sp, nil
317
362
  }
@@ -331,6 +376,10 @@ func credit(c control, res map[string]any, notes *[]any) (string, error) {
331
376
  *notes = append(*notes, map[string]any{"control": c.id, "from": "PASS", "to": "UNKNOWN", "reason": "no evidence"})
332
377
  st = "UNKNOWN"
333
378
  }
379
+ if st == "PASS" && c.domain == "performance" && c.evaluation == "static" {
380
+ *notes = append(*notes, map[string]any{"control": c.id, "from": "PASS", "to": "UNKNOWN", "reason": "static inspection establishes no performance metric"})
381
+ st = "UNKNOWN"
382
+ }
334
383
  if st == "PASS" && c.indep && !ovio.Truthy(res["verified_by"]) {
335
384
  *notes = append(*notes, map[string]any{"control": c.id, "from": "PASS", "to": "UNKNOWN", "reason": "no independent verification"})
336
385
  st = "UNKNOWN"