package classify import "sort" const ( StatusAssigned = "assigned" StatusNeedsReview = "needs_review" // DefaultMinConfidence is the score below which even a single matching // kind is sent to manual review rather than auto-assigned. DefaultMinConfidence = 0.6 ) // RuleKind is one active workout kind's rule, as loaded from the store. type RuleKind struct { WorkoutKindID int64 Name string Rule Node } // ScoredKind is one kind that matched an activity's metrics, with its // confidence score. type ScoredKind struct { WorkoutKindID int64 `json:"workout_kind_id"` Name string `json:"name"` Score float64 `json:"score"` } // Result is the outcome of classifying one activity. type Result struct { Status string WorkoutKindID *int64 Confidence *float64 Candidates []ScoredKind // every kind that matched, sorted by score descending } // Classify evaluates every active kind's rule against ctx and decides // whether the activity is cleanly assignable, or needs manual review // because zero kinds matched, multiple kinds matched, or the single match's // confidence fell below minConfidence. func Classify(ctx MetricContext, kinds []RuleKind, minConfidence float64) Result { candidates := []ScoredKind{} for _, k := range kinds { matched, score := k.Rule.Evaluate(ctx) if matched { candidates = append(candidates, ScoredKind{WorkoutKindID: k.WorkoutKindID, Name: k.Name, Score: score}) } } sort.Slice(candidates, func(i, j int) bool { return candidates[i].Score > candidates[j].Score }) if len(candidates) != 1 { return Result{Status: StatusNeedsReview, Candidates: candidates} } only := candidates[0] if only.Score < minConfidence { return Result{Status: StatusNeedsReview, Candidates: candidates} } id := only.WorkoutKindID score := only.Score return Result{Status: StatusAssigned, WorkoutKindID: &id, Confidence: &score, Candidates: candidates} }