-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTrendAnalyzer.lua
More file actions
116 lines (97 loc) · 3.98 KB
/
TrendAnalyzer.lua
File metadata and controls
116 lines (97 loc) · 3.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
local _, ns = ...
-- Math bound at file scope (Utils\Math.lua loads earlier in the .toc). The
-- framework RegisterModule's modules but never calls OnInitialize, so the
-- old OnInitialize-based binding never ran and left Math nil.
local Math = ns.Math
local TrendAnalyzer = {}
local CORE_METRICS = { "pressureScore", "burstScore", "survivabilityScore", "rotationConsistencyScore" }
local MIN_SAMPLES_FOR_TREND = 10
local R_SQUARED_THRESHOLD = 0.3
function TrendAnalyzer:AnalyzeTrends(context)
local trends = {}
local ok, mbs = pcall(function()
return ns.Addon:GetModule("MetricBaselineService", true)
end)
if not ok or not mbs then return trends end
for _, metricName in ipairs(CORE_METRICS) do
local baseline = mbs:GetBaseline(context, metricName)
if baseline and baseline.values and #baseline.values >= MIN_SAMPLES_FOR_TREND then
local xValues = {}
local yValues = {}
for i, v in ipairs(baseline.values) do
xValues[i] = i
yValues[i] = v
end
local reg = Math.LinearRegression(xValues, yValues)
if reg.rSquared >= R_SQUARED_THRESHOLD then
local firstVal = baseline.values[1]
local lastVal = baseline.values[#baseline.values]
local pctChange = 0
if firstVal and firstVal ~= 0 then
pctChange = ((lastVal - firstVal) / math.abs(firstVal)) * 100
end
local direction = reg.slope > 0 and "improving" or "declining"
local label = metricName:gsub("(%u)", " %1"):gsub("^%s", ""):gsub("Score$", " Score")
trends[#trends + 1] = {
metricName = metricName,
direction = direction,
slope = reg.slope,
rSquared = reg.rSquared,
pctChange = pctChange,
sampleCount = #baseline.values,
message = string.format("%s %s %+.0f%% over last %d sessions",
label, direction, pctChange, #baseline.values),
}
end
end
end
return trends
end
function TrendAnalyzer:DetectTilt(context, recentSessions)
if not recentSessions or #recentSessions < 3 then return nil end
local ok, mbs = pcall(function()
return ns.Addon:GetModule("MetricBaselineService", true)
end)
if not ok or not mbs then return nil end
local baseline = mbs:GetBaseline(context, "pressureScore")
if not baseline or not baseline.mean or baseline.mean <= 0 then return nil end
-- Check consecutive losses
local consecutiveLosses = 0
for i = #recentSessions, 1, -1 do
local s = recentSessions[i]
if s and s.result == ns.Constants.SESSION_RESULT.LOST then
consecutiveLosses = consecutiveLosses + 1
else
break
end
end
if consecutiveLosses < 3 then return nil end
-- Compute EMA of recent pressure scores
local ema = nil
for _, s in ipairs(recentSessions) do
local pressure = s.metrics and s.metrics.pressureScore
if pressure then
ema = Math.ExponentialMovingAverage(pressure, ema, 0.3)
end
end
if not ema then return nil end
local threshold = baseline.mean * 0.85
if ema < threshold then
return {
reasonCode = "TILT_WARNING",
severity = "medium",
confidence = 0.7,
controllability = "outcome_based",
effort = 1,
message = string.format("Performance declining — %d consecutive losses with pressure EMA %.0f vs baseline %.0f. Consider taking a break.",
consecutiveLosses, ema, baseline.mean),
evidence = {
consecutiveLosses = consecutiveLosses,
ema = ema,
baselineMean = baseline.mean,
},
}
end
return nil
end
ns.Addon:RegisterModule("TrendAnalyzer", TrendAnalyzer)