BuildStats derives a Stats from flow.db + transcripts + on-disk dirs.
(o BuildOpts)
| 51 | |
| 52 | // BuildStats derives a Stats from flow.db + transcripts + on-disk dirs. |
| 53 | func BuildStats(o BuildOpts) (Stats, error) { |
| 54 | s := Stats{ |
| 55 | Window: windowLabel(o.Since), |
| 56 | Project: o.Project, |
| 57 | LookupsByKind: map[LookupKind]int{}, |
| 58 | DollarPerHour: o.Constants.DollarPerHour, |
| 59 | } |
| 60 | |
| 61 | tasks, err := flowdb.ListTasks(o.DB, flowdb.TaskFilter{IncludeArchived: true, Project: o.Project}) |
| 62 | if err != nil { |
| 63 | return s, fmt.Errorf("list tasks: %w", err) |
| 64 | } |
| 65 | |
| 66 | // avgKBFileBytes is computed once: average byte size of kb/*.md files. |
| 67 | avgKBFileBytes := computeAvgKBFileBytes(filepath.Join(o.Root, "kb")) |
| 68 | |
| 69 | weekly := map[time.Time]*WeeklyPoint{} |
| 70 | seen := map[string]bool{} |
| 71 | var contextBytes int64 |
| 72 | for _, t := range tasks { |
| 73 | if !t.SessionID.Valid || t.SessionID.String == "" { |
| 74 | continue |
| 75 | } |
| 76 | path := filepath.Join(o.ClaudeProjects, claude.EncodeCwd(t.WorkDir), t.SessionID.String+".jsonl") |
| 77 | if _, statErr := os.Stat(path); statErr != nil { |
| 78 | continue |
| 79 | } |
| 80 | roll, scanErr := o.Cache.ScanFile(path, t.Slug) |
| 81 | if scanErr != nil { |
| 82 | continue |
| 83 | } |
| 84 | seen[path] = true |
| 85 | |
| 86 | // Compute per-task file sizes once (0 if files are missing). |
| 87 | briefBytes := fileSize(filepath.Join(o.Root, "tasks", t.Slug, "brief.md")) |
| 88 | updatesBytes := dirSize(filepath.Join(o.Root, "tasks", t.Slug, "updates"), ".md") |
| 89 | |
| 90 | for _, l := range roll.Lookups { |
| 91 | if !o.Since.IsZero() && (l.TS.IsZero() || l.TS.Before(o.Since)) { |
| 92 | continue |
| 93 | } |
| 94 | s.LookupsByKind[l.Kind]++ |
| 95 | s.LookupsTotal++ |
| 96 | wk := weekStart(l.TS) |
| 97 | wp := weekly[wk] |
| 98 | if wp == nil { |
| 99 | wp = &WeeklyPoint{WeekStart: wk} |
| 100 | weekly[wk] = wp |
| 101 | } |
| 102 | wp.Lookups++ |
| 103 | |
| 104 | // Accumulate context bytes saved per lookup kind. |
| 105 | switch l.Kind { |
| 106 | case LookupResume: |
| 107 | contextBytes += briefBytes + updatesBytes |
| 108 | case LookupReference: |
| 109 | contextBytes += briefBytes |
| 110 | case LookupCrossTask: |