Initial commit: smartrun MVP
Garmin run classification and progression tracker. Go backend (MCP client to mcp-garmin, SQLite store, deterministic rule engine, REST API) and React/TS frontend (Dashboard, Review Queue, Workout Kinds). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
63
backend/internal/classify/engine.go
Normal file
63
backend/internal/classify/engine.go
Normal file
@@ -0,0 +1,63 @@
|
||||
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}
|
||||
}
|
||||
Reference in New Issue
Block a user