Files
Christophe Vila f689f74ae0 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>
2026-07-17 18:33:06 +02:00

64 lines
1.9 KiB
Go

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}
}