Files
apex-public/internal/patterns/patterns.go
T
2026-07-06 11:05:50 -04:00

352 lines
7.6 KiB
Go

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