package httpapi import ( "encoding/json" "log/slog" "net/http" "sort" ) // capabilityTimelineItem summarizes one verb's track record — when the // agent first succeeded at it, and how reliable it's been since. Derived // directly from executions (which has real, growing data) rather than the // patterns/skills tables, which are correctly modeled but have zero writers // anywhere in the codebase today — building against them now would ship a // permanently empty page. See plans/2026-07-10-general-gated-execution.md // step 8 evaluation. type capabilityTimelineItem struct { Verb string `json:"verb"` FirstSuccess *string `json:"first_success"` Successes int `json:"successes"` Total int `json:"total"` } // serveLearningTimeline backs the Learning page's capability timeline: one // row per distinct verb (parsed via splitAction, same helper the activity // feed uses), ordered by when it first succeeded — an honest "the system // learned to do X" signal without depending on the unpopulated patterns // table. func (s *Server) serveLearningTimeline(w http.ResponseWriter, req *http.Request) { ctx := req.Context() rows, err := s.pool.Query(ctx, ` SELECT action, status, created_at::text FROM executions ORDER BY created_at`) if err != nil { writeProblem(w, req, http.StatusInternalServerError, "query failed", err.Error()) return } defer rows.Close() type agg struct { firstSuccess *string successes int total int } byVerb := map[string]*agg{} for rows.Next() { var action, status, createdAt string if err := rows.Scan(&action, &status, &createdAt); err != nil { slog.Error("httpapi: learning/timeline row scan failed", "error", err) continue } verb, _ := splitAction(action) a, ok := byVerb[verb] if !ok { a = &agg{} byVerb[verb] = a } a.total++ if status == "completed" { a.successes++ if a.firstSuccess == nil { ca := createdAt a.firstSuccess = &ca } } } items := make([]capabilityTimelineItem, 0, len(byVerb)) for verb, a := range byVerb { items = append(items, capabilityTimelineItem{ Verb: verb, FirstSuccess: a.firstSuccess, Successes: a.successes, Total: a.total, }) } // Verbs with at least one success sort by when that first happened; // verbs that have never succeeded sort last (nothing to celebrate yet). sort.Slice(items, func(i, j int) bool { fi, fj := items[i].FirstSuccess, items[j].FirstSuccess if fi == nil { return false } if fj == nil { return true } return *fi < *fj }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"items": items}) } type trendBucket struct { Day string `json:"day"` Successes int `json:"successes"` Failures int `json:"failures"` } // serveLearningTrend backs the Learning page's 30-day success/fail trend // chart — a daily bucket of execution outcomes, straight off the executions // table. func (s *Server) serveLearningTrend(w http.ResponseWriter, req *http.Request) { ctx := req.Context() rows, err := s.pool.Query(ctx, ` SELECT date_trunc('day', created_at)::date::text AS day, COUNT(*) FILTER (WHERE status = 'completed') AS successes, COUNT(*) FILTER (WHERE status = 'failed') AS failures FROM executions WHERE created_at > now() - interval '30 days' GROUP BY day ORDER BY day`) if err != nil { writeProblem(w, req, http.StatusInternalServerError, "query failed", err.Error()) return } defer rows.Close() items := []trendBucket{} for rows.Next() { var b trendBucket if err := rows.Scan(&b.Day, &b.Successes, &b.Failures); err != nil { slog.Error("httpapi: learning/trend row scan failed", "error", err) continue } items = append(items, b) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"items": items}) }