package session import ( "antidrift/internal/evidence" "antidrift/internal/store" "time" ) // bucketKey identifies a time bucket; Title is the scrubbed title. type bucketKey struct{ Class, Title string } // EvidenceStats is the in-memory accounting for the current session only. type EvidenceStats struct { SessionID string StartedUnix int64 Buckets map[bucketKey]time.Duration SwitchCount int Current evidence.WindowSnapshot lastFocusAt time.Time lastKey bucketKey hasLast bool } // applyEvent advances stats by one observation: it credits the prior segment to // the prior bucket, counts a context switch on key change, and records the new // current window. Used by both live tracking and crash replay. Caller holds mu. func (c *Controller) applyEvent(now time.Time, snap evidence.WindowSnapshot) { if c.stats.hasLast { c.stats.Buckets[c.stats.lastKey] += now.Sub(c.stats.lastFocusAt) } newKey := keyFor(snap) if c.stats.hasLast && newKey != c.stats.lastKey { c.stats.SwitchCount++ } c.stats.lastKey = newKey c.stats.lastFocusAt = now c.stats.hasLast = true c.stats.Current = snap } // replayStats rebuilds in-memory stats from the raw session log after a crash. func (c *Controller) replayStats(sessionID string) { events, err := store.ReplaySession(c.sessionsDir, sessionID) if err != nil || len(events) == 0 { c.stats = &EvidenceStats{ SessionID: sessionID, StartedUnix: c.clock().Unix(), Buckets: map[bucketKey]time.Duration{}, } return } c.stats = &EvidenceStats{ SessionID: sessionID, StartedUnix: events[0].AtUnixMillis / 1000, Buckets: map[bucketKey]time.Duration{}, } for _, e := range events { c.applyEvent(time.UnixMilli(e.AtUnixMillis), snapFromEvent(e)) } } func keyFor(snap evidence.WindowSnapshot) bucketKey { if !snap.Health.Available { return bucketKey{Class: "", Title: unavailableTitle} } return bucketKey{Class: snap.Class, Title: evidence.ScrubTitle(snap.Title)} } func focusEvent(now time.Time, snap evidence.WindowSnapshot) store.FocusEvent { return store.FocusEvent{ AtUnixMillis: now.UnixMilli(), Class: snap.Class, Title: snap.Title, Available: snap.Health.Available, Reason: snap.Health.Reason, } } func snapFromEvent(e store.FocusEvent) evidence.WindowSnapshot { return evidence.WindowSnapshot{ Title: e.Title, Class: e.Class, Health: evidence.EvidenceHealth{Available: e.Available, Reason: e.Reason}, } }