Skip to content

Commit c455da4

Browse files
grokifyclaude
andcommitted
feat(rubric): add Extensions field and MinIntScore to Rubric
Add v2 schema fields to Rubric: - SchemaVersion: tracks schema version for compatibility - IntScore: overall 1-5 integer score - Confidence: overall confidence level (0.0-1.0) - Pass: explicit pass/fail gate orthogonal to score - Blocking: reason codes that caused failure - Extensions: domain-specific metadata without schema changes Add MinIntScore to PassCriteria: - Minimum overall IntegerScore (1-5) required to pass - Checked after IntScore is computed in Evaluate() Add helper methods: - SetIntScore(), SetConfidence(), SetPass() - AddBlocking(), SetBlocking() - HasLowConfidence(), NeedsHumanReview() - ComputeOverallIntScore(), ComputeOverallConfidence() - CollectBlockingCodes(), IsV2() - SetExtension(), GetExtension(), HasExtension() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1dbe78a commit c455da4

2 files changed

Lines changed: 219 additions & 0 deletions

File tree

rubric/criteria.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ type PassCriteria struct {
1010
// MaxFindings limits findings by severity.
1111
// Use -1 for unlimited.
1212
MaxFindings *FindingLimits `json:"maxFindingsSeverity,omitempty"`
13+
14+
// MinIntScore is the minimum overall IntegerScore (1-5) required to pass.
15+
// If set, the overall score must be >= this value.
16+
// Use 0 to disable this check.
17+
MinIntScore IntegerScore `json:"minIntScore,omitempty"`
1318
}
1419

1520
// DefaultPassCriteria returns standard pass criteria.

rubric/report.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
108136
func 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.
123287
func (r *Rubric) AddCategoryResult(cr CategoryResult) {
124288
r.Categories = append(r.Categories, cr)
@@ -135,6 +299,30 @@ func (r *Rubric) AddFinding(f Finding) {
135299
func (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

Comments
 (0)