Split evidence-stats accounting into stats.go
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
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.
|
// Controller holds runtime state and the active commitment behind a mutex.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -1031,66 +1016,3 @@ func (c *Controller) applyVerdictLocked(v ai.Verdict) {
|
|||||||
c.driftStatus = driftDrifting
|
c.driftStatus = driftDrifting
|
||||||
c.driftReason = v.Reason
|
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},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user