diff --git a/internal/session/session.go b/internal/session/session.go index 0719f7f..9ba98e5 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -96,21 +96,6 @@ var ErrNotPlanning = errors.New("session: coaching is only available while plann var ErrNotActive = errors.New("session: only available while a commitment is active") -// 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 -} - // Controller holds runtime state and the active commitment behind a mutex. type Controller struct { mu sync.Mutex @@ -1031,66 +1016,3 @@ func (c *Controller) applyVerdictLocked(v ai.Verdict) { c.driftStatus = driftDrifting c.driftReason = v.Reason } - -// 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}, - } -} diff --git a/internal/session/stats.go b/internal/session/stats.go new file mode 100644 index 0000000..60db52d --- /dev/null +++ b/internal/session/stats.go @@ -0,0 +1,85 @@ +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}, + } +}