package registry import ( "encoding/json" "fmt" "math" "sort" "strings" ) // ThresholdSpec is one tunable number on a skill: its default and the range an // operator may move it within. The ranges are the product's safety rails — a // stall timeout of 0 minutes or a cash cap of ₹10 lakh is a typo, not a policy. type ThresholdSpec struct { Key string `json:"key"` Label string `json:"label"` Unit string `json:"unit,omitempty"` Default float64 `json:"default"` Min float64 `json:"min"` Max float64 `json:"max"` Step float64 `json:"step"` } // onStep reports whether v sits on the spec's step grid, measured from Min. // Tolerant of float noise: 0.7 must pass a 0.05 step starting at 0.5. func (s ThresholdSpec) onStep(v float64) bool { if s.Step <= 0 { return true } n := (v - s.Min) / s.Step return math.Abs(n-math.Round(n)) < 1e-6 } func (s ThresholdSpec) check(v float64) error { if math.IsNaN(v) || math.IsInf(v, 0) { return fmt.Errorf("%s must be a number", s.Key) } if v < s.Min || v > s.Max { return fmt.Errorf("%s must be between %s and %s", s.Key, fmtNum(s.Min), fmtNum(s.Max)) } if !s.onStep(v) { return fmt.Errorf("%s must move in steps of %s from %s", s.Key, fmtNum(s.Step), fmtNum(s.Min)) } return nil } func fmtNum(v float64) string { return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.4f", v), "0"), ".") } // EffectiveThresholds is what a skill actually runs with: the stored value for // every key the schema still defines and that is still in range, the default // for everything else. Keys the schema no longer has are dropped. A schema // change in code therefore never strands a skill on a value it can no longer // validate, and never needs a data migration. func EffectiveThresholds(schema []ThresholdSpec, stored map[string]float64) map[string]float64 { out := make(map[string]float64, len(schema)) for _, s := range schema { v, ok := stored[s.Key] if !ok || s.check(v) != nil { v = s.Default } out[s.Key] = v } return out } // ApplyThresholdPatch validates an operator's patch against the schema and // returns the full resulting set. Unknown keys and non-numbers are refused, not // ignored: a misspelt key silently doing nothing is exactly the failure an // operator cannot see. All keys are checked before any error is returned, so // the message lists every problem at once. func ApplyThresholdPatch(schema []ThresholdSpec, current map[string]float64, patch map[string]any) (map[string]float64, error) { byKey := make(map[string]ThresholdSpec, len(schema)) for _, s := range schema { byKey[s.Key] = s } next := EffectiveThresholds(schema, current) var problems []string keys := make([]string, 0, len(patch)) for k := range patch { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { spec, known := byKey[k] if !known { problems = append(problems, fmt.Sprintf("%s is not a threshold of this skill", k)) continue } v, isNum := patch[k].(float64) if !isNum { problems = append(problems, fmt.Sprintf("%s must be a number", k)) continue } if err := spec.check(v); err != nil { problems = append(problems, err.Error()) continue } next[k] = v } if len(problems) > 0 { return nil, &ValidationError{Msg: strings.Join(problems, "; ")} } return next, nil } // ParseThresholds reads a stored jsonb thresholds document. Empty, null or // malformed reads as "nothing stored", which EffectiveThresholds turns into // the defaults — a bad row degrades to defaults rather than failing a read. func ParseThresholds(raw string) map[string]float64 { out := map[string]float64{} if strings.TrimSpace(raw) == "" { return out } _ = json.Unmarshal([]byte(raw), &out) if out == nil { out = map[string]float64{} } return out } // ParseSchema reads a stored thresholds schema. Malformed reads as no schema. func ParseSchema(raw string) []ThresholdSpec { var out []ThresholdSpec if strings.TrimSpace(raw) == "" { return []ThresholdSpec{} } if err := json.Unmarshal([]byte(raw), &out); err != nil || out == nil { return []ThresholdSpec{} } return out } // DefaultThresholds is the value set a freshly seeded skill starts with. func DefaultThresholds(schema []ThresholdSpec) map[string]float64 { return EffectiveThresholds(schema, nil) } func mustJSON(v any) string { b, err := json.Marshal(v) if err != nil { // Only ever called on values built in this package; a failure here is // a programming error, and the seed tests exercise every one. panic(err) } return string(b) }