package patterns import ( "database/sql" "fmt" "sort" "strings" ) type Stats struct { TotalApplications int GhostRate float64 AvgScore float64 ScoreDistribution []Bucket TopSources []SourceStat TopArchetypes []ArchetypeStat StatusBreakdown []StatusStat HighScoreGhosts []Job } type Bucket struct { Range string Count int } type SourceStat struct { Source string Count int AvgScore float64 } type ArchetypeStat struct { Archetype string Count int AvgScore float64 } type StatusStat struct { Status string Count int } type Job struct { Company string Title string Score float64 AppliedAt string } // Analyze computes all statistics via direct SQL GROUP BY queries. func Analyze(db *sql.DB) (Stats, error) { stats := Stats{} // Total applications var total int err := db.QueryRow(`SELECT COUNT(*) FROM applications`).Scan(&total) if err != nil { return stats, fmt.Errorf("total count: %w", err) } stats.TotalApplications = total // Average score var avgScore sql.NullFloat64 err = db.QueryRow(`SELECT AVG(score) FROM applications WHERE score IS NOT NULL`).Scan(&avgScore) if err != nil { return stats, fmt.Errorf("avg score: %w", err) } if avgScore.Valid { stats.AvgScore = avgScore.Float64 } // Ghost rate: COUNT(Applied) / COUNT(Applied|Responded|Interview|Offer|Rejected) if err := computeGhostRate(db, &stats); err != nil { return stats, fmt.Errorf("ghost rate: %w", err) } // Score distribution if err := computeScoreDistribution(db, &stats); err != nil { return stats, fmt.Errorf("score distribution: %w", err) } // Top sources if err := computeTopSources(db, &stats); err != nil { return stats, fmt.Errorf("top sources: %w", err) } // Top archetypes if err := computeTopArchetypes(db, &stats); err != nil { return stats, fmt.Errorf("top archetypes: %w", err) } // Status breakdown if err := computeStatusBreakdown(db, &stats); err != nil { return stats, fmt.Errorf("status breakdown: %w", err) } // High-score ghosts if err := computeHighScoreGhosts(db, &stats); err != nil { return stats, fmt.Errorf("high score ghosts: %w", err) } return stats, nil } func computeGhostRate(db *sql.DB, stats *Stats) error { var applied, progressed int err := db.QueryRow(` SELECT COUNT(CASE WHEN status = 'Applied' THEN 1 END), COUNT(CASE WHEN status IN ('Applied','Responded','Interview','Offer','Rejected') THEN 1 END) FROM applications `).Scan(&applied, &progressed) if err != nil { return err } if progressed > 0 { stats.GhostRate = float64(applied) / float64(progressed) } return nil } func computeScoreDistribution(db *sql.DB, stats *Stats) error { rows, err := db.Query(` SELECT CASE WHEN score >= 4.0 THEN '4.0-5.0' WHEN score >= 3.0 THEN '3.0-4.0' WHEN score >= 2.0 THEN '2.0-3.0' ELSE '0.0-2.0' END AS range, COUNT(*) AS count FROM applications WHERE score IS NOT NULL GROUP BY range ORDER BY range DESC `) if err != nil { return err } defer rows.Close() for rows.Next() { var r string var count int if err := rows.Scan(&r, &count); err != nil { return err } stats.ScoreDistribution = append(stats.ScoreDistribution, Bucket{ Range: r, Count: count, }) } return rows.Err() } func computeTopSources(db *sql.DB, stats *Stats) error { rows, err := db.Query(` SELECT j.source, COUNT(*) AS count, AVG(a.score) AS avg_score FROM applications a JOIN jobs j ON a.job_id = j.id WHERE j.source IS NOT NULL GROUP BY j.source ORDER BY count DESC LIMIT 10 `) if err != nil { return err } defer rows.Close() for rows.Next() { var source string var count int var avgScore sql.NullFloat64 if err := rows.Scan(&source, &count, &avgScore); err != nil { return err } stat := SourceStat{ Source: source, Count: count, } if avgScore.Valid { stat.AvgScore = avgScore.Float64 } stats.TopSources = append(stats.TopSources, stat) } return rows.Err() } func computeTopArchetypes(db *sql.DB, stats *Stats) error { rows, err := db.Query(` SELECT archetype, COUNT(*) AS count, AVG(score) AS avg_score FROM applications WHERE archetype IS NOT NULL GROUP BY archetype ORDER BY count DESC LIMIT 10 `) if err != nil { return err } defer rows.Close() for rows.Next() { var archetype string var count int var avgScore sql.NullFloat64 if err := rows.Scan(&archetype, &count, &avgScore); err != nil { return err } stat := ArchetypeStat{ Archetype: archetype, Count: count, } if avgScore.Valid { stat.AvgScore = avgScore.Float64 } stats.TopArchetypes = append(stats.TopArchetypes, stat) } return rows.Err() } func computeStatusBreakdown(db *sql.DB, stats *Stats) error { rows, err := db.Query(` SELECT status, COUNT(*) AS count FROM applications GROUP BY status ORDER BY count DESC `) if err != nil { return err } defer rows.Close() statusOrder := map[string]int{ "Applied": 1, "Responded": 2, "Interview": 3, "Offer": 4, "Rejected": 5, "Discarded": 6, "SKIP": 7, "Evaluated": 8, } var tmp []StatusStat for rows.Next() { var status string var count int if err := rows.Scan(&status, &count); err != nil { return err } tmp = append(tmp, StatusStat{Status: status, Count: count}) } sort.Slice(tmp, func(i, j int) bool { iord, iok := statusOrder[tmp[i].Status] jord, jok := statusOrder[tmp[j].Status] if !iok || !jok { return tmp[i].Count > tmp[j].Count } return iord < jord }) stats.StatusBreakdown = tmp return rows.Err() } func computeHighScoreGhosts(db *sql.DB, stats *Stats) error { rows, err := db.Query(` SELECT j.company, j.title, a.score, a.applied_at FROM applications a JOIN jobs j ON a.job_id = j.id WHERE a.score >= 4.0 AND a.status = 'Applied' ORDER BY a.score DESC, a.applied_at DESC LIMIT 20 `) if err != nil { return err } defer rows.Close() for rows.Next() { var company, title string var score float64 var appliedAt sql.NullTime if err := rows.Scan(&company, &title, &score, &appliedAt); err != nil { return err } job := Job{ Company: company, Title: title, Score: score, } if appliedAt.Valid { job.AppliedAt = appliedAt.Time.Format("2006-01-02") } stats.HighScoreGhosts = append(stats.HighScoreGhosts, job) } return rows.Err() } // Format returns a plain-text summary for CLI output. func Format(s Stats) string { var b strings.Builder fmt.Fprintf(&b, "Applications: %d\n", s.TotalApplications) ghostPct := s.GhostRate * 100 fmt.Fprintf(&b, "Ghost Rate: %.0f%% (applied, never heard back)\n", ghostPct) fmt.Fprintf(&b, "Average Score: %.1f/5\n", s.AvgScore) if len(s.ScoreDistribution) > 0 { b.WriteString("\nScore Distribution:\n") for _, bucket := range s.ScoreDistribution { pct := float64(bucket.Count) * 100 / float64(s.TotalApplications) if s.TotalApplications == 0 { pct = 0 } fmt.Fprintf(&b, " %s: %d (%.0f%%)\n", bucket.Range, bucket.Count, pct) } } if len(s.TopSources) > 0 { b.WriteString("\nTop Sources:\n") for _, src := range s.TopSources { fmt.Fprintf(&b, " %s: %d jobs, avg %.1f\n", src.Source, src.Count, src.AvgScore) } } if len(s.TopArchetypes) > 0 { b.WriteString("\nTop Archetypes:\n") for _, arch := range s.TopArchetypes { fmt.Fprintf(&b, " %s: %d, avg %.1f\n", arch.Archetype, arch.Count, arch.AvgScore) } } if len(s.HighScoreGhosts) > 0 { b.WriteString("\nHigh-Score Ghosts (score ≥ 4.0, no response):\n") for _, job := range s.HighScoreGhosts { dateStr := "" if job.AppliedAt != "" { dateStr = " (" + job.AppliedAt + ")" } fmt.Fprintf(&b, " %s — %s (%.1f)%s\n", job.Company, job.Title, job.Score, dateStr) } } return b.String() }