@@ -10,6 +10,10 @@ type Rubric struct {
1010 // Schema is the JSON Schema URL.
1111 Schema string `json:"$schema,omitempty"`
1212
13+ // SchemaVersion is the evaluation schema version (e.g., "v2").
14+ // Used for backwards compatibility.
15+ SchemaVersion string `json:"schemaVersion,omitempty"`
16+
1317 // Metadata contains report identification and audit info.
1418 Metadata ReportMetadata `json:"metadata"`
1519
@@ -28,6 +32,22 @@ type Rubric struct {
2832 // Reference contains gold/expected data for comparison.
2933 Reference * ReferenceData `json:"reference,omitempty"`
3034
35+ // IntScore is the overall 1-5 integer score.
36+ // Preferred for LLM judges as they are unreliable at finer granularity.
37+ IntScore IntegerScore `json:"intScore,omitempty"`
38+
39+ // Confidence is the overall confidence in the evaluation (0.0-1.0).
40+ // Low confidence evaluations may be routed to human review.
41+ Confidence float64 `json:"confidence,omitempty"`
42+
43+ // Pass is an explicit pass/fail gate, orthogonal to score.
44+ // A spec can have a high score but still fail due to blocking issues.
45+ Pass bool `json:"pass"`
46+
47+ // Blocking contains reason codes that caused failure.
48+ // Empty if Pass is true.
49+ Blocking []ReasonCode `json:"blocking,omitempty"`
50+
3151 // Categories contains results for each evaluation dimension.
3252 Categories []CategoryResult `json:"categories"`
3353
@@ -48,6 +68,11 @@ type Rubric struct {
4868
4969 // Summary is the overall assessment.
5070 Summary string `json:"summary"`
71+
72+ // Extensions contains domain-specific metadata.
73+ // Use this to store custom data without modifying the core schema.
74+ // Example: {"coverage": {...}, "metrics": {...}}
75+ Extensions map [string ]any `json:"extensions,omitempty"`
5176}
5277
5378// ReportMetadata contains report identification.
@@ -104,9 +129,13 @@ type ActionItem struct {
104129 Effort string `json:"effort,omitempty"`
105130}
106131
132+ // SchemaVersionV2 is the current schema version.
133+ const SchemaVersionV2 = "v2"
134+
107135// NewRubric creates a new rubric-based evaluation report.
108136func NewRubric (reviewType , document string ) * Rubric {
109137 return & Rubric {
138+ SchemaVersion : SchemaVersionV2 ,
110139 Metadata : ReportMetadata {
111140 Document : document ,
112141 GeneratedAt : time .Now ().UTC (),
@@ -119,6 +148,141 @@ func NewRubric(reviewType, document string) *Rubric {
119148 }
120149}
121150
151+ // SetIntScore sets the overall integer score.
152+ func (r * Rubric ) SetIntScore (score IntegerScore ) * Rubric {
153+ r .IntScore = score
154+ return r
155+ }
156+
157+ // SetConfidence sets the overall confidence value.
158+ func (r * Rubric ) SetConfidence (confidence float64 ) * Rubric {
159+ if confidence < 0 {
160+ confidence = 0
161+ }
162+ if confidence > 1 {
163+ confidence = 1
164+ }
165+ r .Confidence = confidence
166+ return r
167+ }
168+
169+ // SetPass sets the pass/fail status.
170+ func (r * Rubric ) SetPass (pass bool ) * Rubric {
171+ r .Pass = pass
172+ return r
173+ }
174+
175+ // AddBlocking adds a blocking reason code.
176+ func (r * Rubric ) AddBlocking (code ReasonCode ) * Rubric {
177+ r .Blocking = append (r .Blocking , code )
178+ r .Pass = false
179+ return r
180+ }
181+
182+ // SetBlocking sets the blocking reason codes.
183+ func (r * Rubric ) SetBlocking (codes []ReasonCode ) * Rubric {
184+ r .Blocking = codes
185+ if len (codes ) > 0 {
186+ r .Pass = false
187+ }
188+ return r
189+ }
190+
191+ // HasLowConfidence returns true if confidence is below the threshold (default 0.7).
192+ func (r * Rubric ) HasLowConfidence (threshold ... float64 ) bool {
193+ t := 0.7
194+ if len (threshold ) > 0 {
195+ t = threshold [0 ]
196+ }
197+ return r .Confidence > 0 && r .Confidence < t
198+ }
199+
200+ // NeedsHumanReview returns true if this evaluation should be reviewed by a human.
201+ func (r * Rubric ) NeedsHumanReview (confidenceThreshold ... float64 ) bool {
202+ // Low confidence overall
203+ if r .HasLowConfidence (confidenceThreshold ... ) {
204+ return true
205+ }
206+ // Any category with low confidence
207+ for _ , cat := range r .Categories {
208+ if cat .HasLowConfidence (confidenceThreshold ... ) {
209+ return true
210+ }
211+ }
212+ // Decision status is human_review
213+ if r .Decision .Status == DecisionHumanReview {
214+ return true
215+ }
216+ return false
217+ }
218+
219+ // ComputeOverallIntScore calculates the overall integer score from category scores.
220+ // Uses weighted average of category IntScores.
221+ func (r * Rubric ) ComputeOverallIntScore (rubricSet * RubricSet ) IntegerScore {
222+ if len (r .Categories ) == 0 {
223+ return ScoreAcceptable // Default to middle
224+ }
225+
226+ var totalWeight float64
227+ var weightedSum float64
228+
229+ for _ , cat := range r .Categories {
230+ if cat .IntScore == 0 {
231+ continue // Skip categories without integer scores
232+ }
233+ weight := 1.0
234+ if rubricSet != nil {
235+ if catDef := rubricSet .GetCategory (cat .Category ); catDef != nil && catDef .Weight > 0 {
236+ weight = catDef .Weight
237+ }
238+ }
239+ totalWeight += weight
240+ weightedSum += float64 (cat .IntScore ) * weight
241+ }
242+
243+ if totalWeight == 0 {
244+ return ScoreAcceptable
245+ }
246+
247+ avg := weightedSum / totalWeight
248+ return ParseIntegerScore (int (avg + 0.5 )) // Round to nearest
249+ }
250+
251+ // ComputeOverallConfidence calculates the overall confidence from category confidences.
252+ // Uses minimum confidence across all categories (weakest link).
253+ func (r * Rubric ) ComputeOverallConfidence () float64 {
254+ if len (r .Categories ) == 0 {
255+ return 0
256+ }
257+
258+ minConfidence := 1.0
259+ hasConfidence := false
260+
261+ for _ , cat := range r .Categories {
262+ if cat .Confidence > 0 {
263+ hasConfidence = true
264+ if cat .Confidence < minConfidence {
265+ minConfidence = cat .Confidence
266+ }
267+ }
268+ }
269+
270+ if ! hasConfidence {
271+ return 0
272+ }
273+ return minConfidence
274+ }
275+
276+ // CollectBlockingCodes gathers all blocking reason codes from findings.
277+ func (r * Rubric ) CollectBlockingCodes () []ReasonCode {
278+ return GetBlockingCodes (r .Findings )
279+ }
280+
281+ // IsV2 returns true if this is a v2 schema evaluation.
282+ func (r * Rubric ) IsV2 () bool {
283+ return r .SchemaVersion == SchemaVersionV2
284+ }
285+
122286// AddCategoryResult adds a category result.
123287func (r * Rubric ) AddCategoryResult (cr CategoryResult ) {
124288 r .Categories = append (r .Categories , cr )
@@ -135,6 +299,30 @@ func (r *Rubric) AddFinding(f Finding) {
135299func (r * Rubric ) Evaluate (rubricSet * RubricSet ) Decision {
136300 r .Decision = EvaluateResults (r .Categories , r .Findings , r .PassCriteria , rubricSet )
137301 r .OverallDecision = string (r .Decision .Status )
302+
303+ // Compute v2 fields
304+ r .Pass = r .Decision .Passed
305+ r .Blocking = r .CollectBlockingCodes ()
306+
307+ // Compute overall integer score if not set
308+ if r .IntScore == 0 {
309+ r .IntScore = r .ComputeOverallIntScore (rubricSet )
310+ }
311+
312+ // Compute overall confidence if not set
313+ if r .Confidence == 0 {
314+ r .Confidence = r .ComputeOverallConfidence ()
315+ }
316+
317+ // Check MinIntScore criteria (must be checked after IntScore is computed)
318+ if r .PassCriteria .MinIntScore > 0 && r .IntScore < r .PassCriteria .MinIntScore {
319+ r .Decision .Status = DecisionFail
320+ r .Decision .Passed = false
321+ r .Decision .Rationale = fmt .Sprintf ("Score %d is below minimum required %d" , r .IntScore , r .PassCriteria .MinIntScore )
322+ r .OverallDecision = string (r .Decision .Status )
323+ r .Pass = false
324+ }
325+
138326 return r .Decision
139327}
140328
@@ -270,3 +458,29 @@ func (r *Rubric) GetCategoryResult(categoryID string) *CategoryResult {
270458 }
271459 return nil
272460}
461+
462+ // SetExtension sets a single extension value.
463+ func (r * Rubric ) SetExtension (key string , value any ) {
464+ if r .Extensions == nil {
465+ r .Extensions = make (map [string ]any )
466+ }
467+ r .Extensions [key ] = value
468+ }
469+
470+ // GetExtension retrieves an extension value by key.
471+ // Returns nil if not found.
472+ func (r * Rubric ) GetExtension (key string ) any {
473+ if r .Extensions == nil {
474+ return nil
475+ }
476+ return r .Extensions [key ]
477+ }
478+
479+ // HasExtension checks if an extension exists.
480+ func (r * Rubric ) HasExtension (key string ) bool {
481+ if r .Extensions == nil {
482+ return false
483+ }
484+ _ , ok := r .Extensions [key ]
485+ return ok
486+ }
0 commit comments