// Package jevdecide is an HTTP client for the local jev-service typed decide API. // Product wiring is fail-open: timeouts, auth errors, and circuit opens fall back // to existing deterministic rules. See .doc/ops/short-term/2026-09-24-jev-product-shadow.md. package jevdecide import "strings" // Mode controls product-side integration. type Mode string const ( ModeOff Mode = "off" // zero behavior change (default) ModeShadow Mode = "shadow" // rules route; jev recorded for comparison ModeAssist Mode = "assist" // jev may augment selected decisions (with rule floor) ) // Question is one typed decide question (choice / noul / numeric). type Question struct { Type string `json:"type"` Question string `json:"question"` Options []string `json:"options,omitempty"` Min *float64 `json:"min,omitempty"` Max *float64 `json:"max,omitempty"` } // Answer is one typed decide answer. type Answer struct { Type string `json:"type"` Choice string `json:"choice,omitempty"` Confidence float64 `json:"confidence"` Probabilities map[string]float64 `json:"probabilities,omitempty"` Yes bool `json:"yes,omitempty"` Noul float64 `json:"noul,omitempty"` Value float64 `json:"value,omitempty"` Error string `json:"error,omitempty"` } // Response is the /v1/decide envelope. type Response struct { Model string `json:"model"` Answers map[string]Answer `json:"answers"` } // Margin returns top1−top2 probability gap (0 if fewer than two options). func (a Answer) Margin() float64 { if len(a.Probabilities) < 2 { return a.Confidence } var top1, top2 float64 for _, p := range a.Probabilities { if p >= top1 { top2 = top1 top1 = p } else if p > top2 { top2 = p } } return top1 - top2 } // Reliable reports whether the answer is usable for assist routing. func (a Answer) Reliable(minConf, minMargin float64) bool { if a.Error != "" || strings.TrimSpace(a.Choice) == "" { return false } return a.Confidence >= minConf && a.Margin() >= minMargin } // ProfileBucket maps a complexity score to a closed enum for jev choice. func ProfileBucket(score int) string { switch { case score >= 60: return "high" case score >= 30: return "mid" default: return "low" } }