@semanticintent/semantic-chirp-intelligence-mcp 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,316 @@
1
+ /**
2
+ * 🏒❄️ ICE Analysis - Intent Chirp Engine
3
+ *
4
+ * Concrete implementation of AnalysisTemplate for roster transaction recommendations.
5
+ * This is the flagship "ICE" tool - championship-level optimization with savage analysis.
6
+ *
7
+ * Analysis Type: ice_roster
8
+ * Semantic Identity: ICE - Intent Chirp Engine
9
+ * Default Intensity: ice_cold
10
+ */
11
+ import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
12
+ import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
13
+ /**
14
+ * ICE Analysis - The ultimate roster optimization engine
15
+ */
16
+ export class IceAnalysis extends AnalysisTemplate {
17
+ apiClient;
18
+ leagueId;
19
+ teamId;
20
+ constructor(apiClient, leagueId, teamId) {
21
+ super("get_roster_transaction_recommendations", "ice_roster");
22
+ this.apiClient = apiClient;
23
+ this.leagueId = leagueId;
24
+ this.teamId = teamId;
25
+ }
26
+ /**
27
+ * Hook 1: Fetch raw data from Yahoo API
28
+ */
29
+ async fetchData(args) {
30
+ const lookAheadDays = args.look_ahead_days || 7;
31
+ // Fetch all data needed for ICE analysis in parallel
32
+ const [rosterData, gamesInHandData, streamingData] = await Promise.all([
33
+ this.apiClient.getTeamRoster(this.leagueId, this.teamId),
34
+ this.fetchGamesInHand(),
35
+ this.fetchStreamingRecommendations(lookAheadDays)
36
+ ]);
37
+ return {
38
+ roster: rosterData,
39
+ gamesInHand: gamesInHandData,
40
+ streaming: streamingData,
41
+ lookAheadDays
42
+ };
43
+ }
44
+ /**
45
+ * Hook 2: Prepare and transform data for analysis
46
+ */
47
+ async prepareData(rawData, args) {
48
+ // Parse roster from Yahoo API response
49
+ const teamArray = rawData.roster.fantasy_content.team[0];
50
+ const rosterData = rawData.roster.fantasy_content.team[1].roster["0"].players;
51
+ const teamKey = teamArray.find((item) => item.team_key)?.team_key;
52
+ const teamName = teamArray.find((item) => item.name)?.name;
53
+ // Parse players
54
+ const playerKeys = Object.keys(rosterData).filter(key => key !== 'count');
55
+ const players = playerKeys.map(key => {
56
+ const playerData = rosterData[key].player[0];
57
+ const positionData = rosterData[key].player[1];
58
+ const player_id = playerData.find((item) => item.player_id)?.player_id;
59
+ const name = playerData.find((item) => item.name)?.name?.full;
60
+ const position = positionData.eligible_positions?.position.join(',') || '';
61
+ const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
62
+ const selected_position = positionData.selected_position?.position || '';
63
+ const status = playerData.find((item) => item.status)?.status || '';
64
+ return {
65
+ player_id,
66
+ name,
67
+ position,
68
+ team,
69
+ selected_position,
70
+ status
71
+ };
72
+ });
73
+ // Return FantasyData with roster structure
74
+ // Store extra context (gamesInHand, streaming) for use in analyzeData
75
+ // Exclude 'roster' from spread to prevent overwriting our parsed roster
76
+ const { roster: _roster, ...extendedData } = rawData;
77
+ return {
78
+ roster: {
79
+ team_key: teamKey,
80
+ team_name: teamName,
81
+ players: players
82
+ },
83
+ // Include extended data (gamesInHand, streaming) without overwriting roster
84
+ ...extendedData
85
+ };
86
+ }
87
+ /**
88
+ * Hook 3: Execute core ICE analysis logic
89
+ */
90
+ async analyzeData(data, args) {
91
+ // Guard: Ensure roster exists
92
+ if (!data.roster) {
93
+ throw new Error("No roster data available for analysis");
94
+ }
95
+ // Analyze current roster strengths/weaknesses
96
+ const rosterAnalysis = this.analyzeRosterStrengths(data);
97
+ // Find all transaction opportunities
98
+ const recommendations = [];
99
+ // Access extended data properties (gamesInHand, streaming)
100
+ const extendedData = data;
101
+ // 1. CRITICAL: Injured players in active lineup
102
+ const injuredActive = (data.roster?.players || []).filter((p) => p.status && p.status !== "" && !p.selected_position.includes("IR"));
103
+ for (const player of injuredActive) {
104
+ recommendations.push({
105
+ priority: "CRITICAL",
106
+ action: "drop", // Aligned with RecommendationAction type
107
+ player: player,
108
+ reasoning: `${player.name} is ${player.status} but still in active lineup - move to IR`
109
+ });
110
+ }
111
+ // 2. HIGH: Position weakness fixes
112
+ const weakPositions = this.identifyWeakPositions(data, rosterAnalysis);
113
+ for (const position of weakPositions) {
114
+ const bestAvailable = extendedData.streaming?.streaming_targets
115
+ ?.filter((p) => args.target_positions ? args.target_positions.includes(position.position) : true)
116
+ .filter((p) => p.position.includes(position.position))
117
+ .slice(0, 3) || [];
118
+ if (bestAvailable.length > 0) {
119
+ const dropCandidate = this.findBestDropCandidate(data, position.position);
120
+ recommendations.push({
121
+ priority: "HIGH",
122
+ action: "pickup", // Aligned with RecommendationAction type
123
+ pickup: bestAvailable[0],
124
+ drop: dropCandidate,
125
+ reasoning: `Strengthen ${position.position} - ${position.weakness_reason}`
126
+ });
127
+ }
128
+ }
129
+ // 3. MEDIUM: Schedule optimization
130
+ const gamesDiff = extendedData.gamesInHand?.games_in_hand_difference || 0;
131
+ if (gamesDiff < 0) {
132
+ const volumePickups = extendedData.streaming?.streaming_targets
133
+ ?.filter((t) => t.team_trending_count >= 3)
134
+ .slice(0, 2) || [];
135
+ for (const pickup of volumePickups) {
136
+ recommendations.push({
137
+ priority: "MEDIUM",
138
+ action: "volume_play",
139
+ pickup: pickup,
140
+ reasoning: `Opponent has ${Math.abs(gamesDiff)} more games - need volume players`
141
+ });
142
+ }
143
+ }
144
+ // 4. Bench optimizations
145
+ const benchUpgrades = this.findBenchUpgrades(data);
146
+ recommendations.push(...benchUpgrades);
147
+ // Sort by priority
148
+ const sortedRecommendations = recommendations
149
+ .sort((a, b) => {
150
+ const priorityOrder = {
151
+ "CRITICAL": 0,
152
+ "HIGH": 1,
153
+ "MEDIUM": 2,
154
+ "LOW": 3
155
+ };
156
+ return (priorityOrder[a.priority] || 99) - (priorityOrder[b.priority] || 99);
157
+ })
158
+ .slice(0, 8); // Top 8 recommendations
159
+ return {
160
+ roster_analysis: rosterAnalysis,
161
+ immediate_issues: injuredActive.length,
162
+ games_disadvantage: gamesDiff,
163
+ weak_positions: weakPositions,
164
+ recommendations: sortedRecommendations,
165
+ optimal_timing: extendedData.streaming?.optimal_timing,
166
+ market_intelligence: extendedData.streaming?.market_intelligence
167
+ };
168
+ }
169
+ /**
170
+ * Hook 4: Generate chirp intelligence commentary
171
+ */
172
+ async generateChirp(analysisResults, semanticContract, data) {
173
+ // Use ChirpIntelligence service to enhance results
174
+ return ChirpIntelligence.enhance(this.toolName, analysisResults, semanticContract);
175
+ }
176
+ /**
177
+ * Hook 5: Format final response structure
178
+ */
179
+ async formatResponse(chirpEnhanced, data) {
180
+ // Format insights according to AnalysisInsights interface
181
+ const insights = {
182
+ immediate_issues: chirpEnhanced.immediate_issues || 0,
183
+ games_disadvantage: chirpEnhanced.games_disadvantage || 0,
184
+ weak_positions: chirpEnhanced.weak_positions || [],
185
+ optimal_timing: chirpEnhanced.optimal_timing,
186
+ market_intelligence: chirpEnhanced.market_intelligence
187
+ };
188
+ return {
189
+ analysis_insights: insights,
190
+ recommendations: chirpEnhanced.recommendations,
191
+ chirp_intelligence: chirpEnhanced.chirp_intelligence,
192
+ metadata: chirpEnhanced.metadata
193
+ };
194
+ }
195
+ // ==========================================
196
+ // Private Helper Methods
197
+ // ==========================================
198
+ analyzeRosterStrengths(data) {
199
+ const positions = {
200
+ C: [], LW: [], RW: [], D: [], G: [],
201
+ bench: [], ir: [], active: [],
202
+ position_counts: { C: 0, LW: 0, RW: 0, D: 0, G: 0, bench: 0 },
203
+ strength_score: 0,
204
+ weakest_position: ""
205
+ };
206
+ if (!data.roster || !data.roster.players)
207
+ return positions;
208
+ data.roster.players.forEach((player) => {
209
+ if (player.selected_position === "BN") {
210
+ positions.bench.push(player);
211
+ }
212
+ else if (player.selected_position.includes("IR")) {
213
+ positions.ir.push(player);
214
+ }
215
+ else {
216
+ positions.active.push(player);
217
+ if (player.position.includes("C"))
218
+ positions.C.push(player);
219
+ if (player.position.includes("LW"))
220
+ positions.LW.push(player);
221
+ if (player.position.includes("RW"))
222
+ positions.RW.push(player);
223
+ if (player.position.includes("D"))
224
+ positions.D.push(player);
225
+ if (player.position.includes("G"))
226
+ positions.G.push(player);
227
+ }
228
+ });
229
+ positions.position_counts = {
230
+ C: positions.C.length,
231
+ LW: positions.LW.length,
232
+ RW: positions.RW.length,
233
+ D: positions.D.length,
234
+ G: positions.G.length,
235
+ bench: positions.bench.length
236
+ };
237
+ // Calculate strength score (simple heuristic)
238
+ positions.strength_score = Object.values(positions.position_counts).reduce((a, b) => a + b, 0);
239
+ // Find weakest position
240
+ const positionCounts = positions.position_counts;
241
+ positions.weakest_position = Object.entries(positionCounts)
242
+ .filter(([pos]) => pos !== 'bench')
243
+ .sort(([, a], [, b]) => a - b)[0]?.[0] || "";
244
+ return positions;
245
+ }
246
+ identifyWeakPositions(data, analysis) {
247
+ const weakPositions = [];
248
+ const avgPerPosition = analysis.strength_score / 5;
249
+ for (const [position, count] of Object.entries(analysis.position_counts)) {
250
+ if (position !== 'bench' && count < avgPerPosition * 0.7) {
251
+ weakPositions.push({
252
+ position,
253
+ current_count: count,
254
+ weakness_reason: `Only ${count} players vs average of ${avgPerPosition.toFixed(1)}`
255
+ });
256
+ }
257
+ }
258
+ return weakPositions;
259
+ }
260
+ findBestDropCandidate(data, position) {
261
+ if (!data.roster || !data.roster.players)
262
+ return null;
263
+ const benchPlayers = data.roster.players.filter((p) => p.selected_position === "BN");
264
+ return benchPlayers.length > 0 ? benchPlayers[0] : null;
265
+ }
266
+ findBenchUpgrades(data) {
267
+ const recommendations = [];
268
+ if (!data.roster || !data.roster.players)
269
+ return recommendations;
270
+ const extendedData = data;
271
+ const benchPlayers = data.roster.players.filter((p) => p.selected_position === "BN");
272
+ // Simple heuristic: suggest top streaming targets for bench slots
273
+ const topTargets = extendedData.streaming?.streaming_targets?.slice(0, Math.min(2, benchPlayers.length)) || [];
274
+ for (let i = 0; i < topTargets.length; i++) {
275
+ const target = topTargets[i];
276
+ const benchPlayer = benchPlayers[i];
277
+ if (benchPlayer) {
278
+ recommendations.push({
279
+ priority: "LOW",
280
+ action: "bench_upgrade",
281
+ pickup: target,
282
+ drop: benchPlayer,
283
+ reasoning: `Upgrade bench: ${target.name} trending better than ${benchPlayer.name}`
284
+ });
285
+ }
286
+ }
287
+ return recommendations;
288
+ }
289
+ /**
290
+ * Temporary stub for games in hand (to be migrated to its own analysis)
291
+ */
292
+ async fetchGamesInHand() {
293
+ // TODO: Replace with proper GamesInHandAnalysis when migrated
294
+ return {
295
+ games_in_hand_difference: 0,
296
+ your_remaining: 0,
297
+ opponent_remaining: 0
298
+ };
299
+ }
300
+ /**
301
+ * Temporary stub for streaming recommendations (to be migrated)
302
+ */
303
+ async fetchStreamingRecommendations(lookAheadDays) {
304
+ // TODO: Replace with proper StreamingAnalysis when migrated
305
+ return {
306
+ streaming_targets: [],
307
+ optimal_timing: {
308
+ best_days: [],
309
+ avoid_days: []
310
+ },
311
+ market_intelligence: {
312
+ top_trending_team: "unknown"
313
+ }
314
+ };
315
+ }
316
+ }
@@ -0,0 +1,284 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * LineupAnalysis - Template Method Pattern Implementation
4
+ *
5
+ * Optimizes daily lineup by identifying benched players who should be active
6
+ * and active players who should be benched.
7
+ */
8
+ import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
9
+ import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
10
+ export class LineupAnalysis extends AnalysisTemplate {
11
+ apiClient;
12
+ leagueId;
13
+ teamId;
14
+ constructor(apiClient, leagueId, teamId) {
15
+ super("optimize_lineup", "lineup_optimization");
16
+ this.apiClient = apiClient;
17
+ this.leagueId = leagueId;
18
+ this.teamId = teamId;
19
+ }
20
+ /**
21
+ * Hook 1: Fetch raw data from Yahoo API
22
+ */
23
+ async fetchData(args) {
24
+ try {
25
+ // Fetch current roster with positions
26
+ const rosterData = await this.apiClient.getTeamRoster(this.leagueId, this.teamId);
27
+ // Fetch today's scoreboard to see who's playing
28
+ const scoreboardData = await this.apiClient.getLeagueScoreboard(this.leagueId);
29
+ return {
30
+ roster: rosterData,
31
+ scoreboard: scoreboardData
32
+ };
33
+ }
34
+ catch (error) {
35
+ throw new Error(`Failed to fetch lineup data: ${error}`);
36
+ }
37
+ }
38
+ /**
39
+ * Hook 2: Prepare data into FantasyData structure
40
+ */
41
+ async prepareData(rawData, args) {
42
+ const { roster, scoreboard } = rawData;
43
+ const teamArray = roster.fantasy_content?.team?.[0] || [];
44
+ const rosterData = roster.fantasy_content?.team?.[1]?.roster?.['0']?.players || {};
45
+ // Fetch today's NHL schedule once for all players
46
+ const todaySchedule = await this.fetchTodaySchedule();
47
+ // Use object key iteration like IceAnalysis for consistent player parsing
48
+ const playerKeys = Object.keys(rosterData).filter(key => key !== 'count');
49
+ // Parse player data first (synchronous)
50
+ const parsedPlayers = playerKeys.map(key => {
51
+ // Yahoo API returns player data as nested arrays
52
+ // player[0] is an array of objects, player[1] contains position info
53
+ const player = rosterData[key].player?.[0] || rosterData[key];
54
+ const positionInfo = rosterData[key].player?.[1] || {};
55
+ // Use .find() pattern like IceAnalysis since player is an array of property objects
56
+ const player_id = Array.isArray(player)
57
+ ? player.find((item) => item.player_id)?.player_id
58
+ : player.player_id;
59
+ const name = Array.isArray(player)
60
+ ? player.find((item) => item.name)?.name?.full
61
+ : player.name?.full;
62
+ const position = Array.isArray(player)
63
+ ? player.find((item) => item.display_position)?.display_position ||
64
+ player.find((item) => item.primary_positions)?.primary_positions?.[0]
65
+ : player.display_position || player.primary_positions?.[0];
66
+ const team = Array.isArray(player)
67
+ ? player.find((item) => item.editorial_team_abbr)?.editorial_team_abbr
68
+ : player.editorial_team_abbr;
69
+ const status = Array.isArray(player)
70
+ ? player.find((item) => item.status)?.status
71
+ : player.status;
72
+ const selectedPosition = positionInfo.selected_position?.[1]?.position ||
73
+ positionInfo.selected_position?.position ||
74
+ 'BN';
75
+ return {
76
+ player_id: player_id || '',
77
+ name: name || 'Unknown',
78
+ position: position || 'Unknown',
79
+ team: team || '',
80
+ selected_position: Array.isArray(selectedPosition) ? selectedPosition : [selectedPosition],
81
+ status: status || ''
82
+ };
83
+ });
84
+ // Add has_game_today flag using the schedule we fetched
85
+ const players = parsedPlayers.map((player) => ({
86
+ ...player,
87
+ has_game_today: this.hasGameToday(player, todaySchedule)
88
+ }));
89
+ // Extract team info using .find() pattern like IceAnalysis
90
+ const team_key = Array.isArray(teamArray)
91
+ ? teamArray.find((item) => item.team_key)?.team_key
92
+ : teamArray.team_key;
93
+ const team_name = Array.isArray(teamArray)
94
+ ? teamArray.find((item) => item.name)?.name
95
+ : teamArray.name;
96
+ return {
97
+ roster: {
98
+ team_key: team_key || '',
99
+ team_name: team_name || 'Your Team',
100
+ players
101
+ },
102
+ scoreboard: scoreboard
103
+ };
104
+ }
105
+ /**
106
+ * Hook 3: Analyze data to identify lineup issues
107
+ */
108
+ async analyzeData(data, args) {
109
+ const issues = [];
110
+ const players = data.roster.players;
111
+ // Issue 1: CRITICAL - Injured players in active lineup
112
+ const injuredActive = players.filter(p => p.status &&
113
+ p.status !== 'Healthy' &&
114
+ !p.selected_position.includes('IR') &&
115
+ !p.selected_position.includes('BN'));
116
+ for (const player of injuredActive) {
117
+ issues.push({
118
+ type: 'active_injured',
119
+ severity: 'CRITICAL',
120
+ player,
121
+ recommendation: `Move ${player.name} (${player.status}) to IR or bench immediately`
122
+ });
123
+ }
124
+ // Issue 2: HIGH - Healthy players on bench with games today
125
+ const benchedWithGames = players.filter(p => p.selected_position.includes('BN') &&
126
+ p.has_game_today &&
127
+ (!p.status || p.status === 'Healthy'));
128
+ for (const player of benchedWithGames) {
129
+ issues.push({
130
+ type: 'benched_active',
131
+ severity: 'HIGH',
132
+ player,
133
+ recommendation: `${player.name} is benched but has a game today - activate if possible`
134
+ });
135
+ }
136
+ // Issue 3: MEDIUM - Active players without games today
137
+ const activeNoGames = players.filter(p => !p.selected_position.includes('BN') &&
138
+ !p.selected_position.includes('IR') &&
139
+ !p.has_game_today);
140
+ const benchedHealthy = players.filter(p => p.selected_position.includes('BN') &&
141
+ p.has_game_today &&
142
+ (!p.status || p.status === 'Healthy'));
143
+ if (activeNoGames.length > 0 && benchedHealthy.length > 0) {
144
+ // Suggest swaps
145
+ const swapCount = Math.min(activeNoGames.length, benchedHealthy.length);
146
+ for (let i = 0; i < swapCount; i++) {
147
+ issues.push({
148
+ type: 'position_inefficiency',
149
+ severity: 'MEDIUM',
150
+ recommendation: `Swap ${activeNoGames[i].name} (no game) with ${benchedHealthy[i].name} (playing)`
151
+ });
152
+ }
153
+ }
154
+ // Issue 4: LOW - Empty active slots that could be filled
155
+ const activeSlots = players.filter(p => !p.selected_position.includes('BN') &&
156
+ !p.selected_position.includes('IR')).length;
157
+ const benchPlayers = players.filter(p => p.selected_position.includes('BN')).length;
158
+ // Estimate if there are likely empty slots (simplified)
159
+ // In production, would check actual league roster settings
160
+ const estimatedMaxActiveSlots = 12; // Typical fantasy hockey active roster size
161
+ if (activeSlots < estimatedMaxActiveSlots && benchPlayers > 0) {
162
+ issues.push({
163
+ type: 'empty_slot',
164
+ severity: 'LOW',
165
+ recommendation: `You may have empty active slots - review your lineup`
166
+ });
167
+ }
168
+ return issues;
169
+ }
170
+ /**
171
+ * Hook 4: Generate chirp-enhanced response
172
+ */
173
+ async generateChirp(analysisResults, semanticContract, data) {
174
+ return ChirpIntelligence.enhance(this.toolName, { lineup_issues: analysisResults }, semanticContract);
175
+ }
176
+ /**
177
+ * Hook 5: Format final response
178
+ */
179
+ async formatResponse(chirpEnhanced, data) {
180
+ const lineupIssues = chirpEnhanced.lineup_issues;
181
+ // Convert to standard Recommendation format
182
+ const recommendations = lineupIssues.map(issue => {
183
+ // Map issue type to action
184
+ let action = 'lineup_change';
185
+ if (issue.type === 'active_injured') {
186
+ action = 'move_to_ir';
187
+ }
188
+ else if (issue.type === 'benched_active') {
189
+ action = 'lineup_change';
190
+ }
191
+ return {
192
+ priority: issue.severity,
193
+ action,
194
+ player: issue.player,
195
+ reasoning: issue.recommendation
196
+ };
197
+ });
198
+ const criticalCount = lineupIssues.filter(i => i.severity === 'CRITICAL').length;
199
+ const highCount = lineupIssues.filter(i => i.severity === 'HIGH').length;
200
+ const analysisInsights = {
201
+ lineup_health: {
202
+ total_issues: lineupIssues.length,
203
+ critical_issues: criticalCount,
204
+ high_priority_issues: highCount,
205
+ lineup_score: this.calculateLineupScore(lineupIssues)
206
+ },
207
+ immediate_actions: lineupIssues
208
+ .filter(i => i.severity === 'CRITICAL' || i.severity === 'HIGH')
209
+ .map(i => i.recommendation)
210
+ };
211
+ const metadata = {
212
+ analysis_type: this.analysisType,
213
+ timestamp: new Date().toISOString(),
214
+ team_context: {
215
+ team_name: data.roster.team_name
216
+ },
217
+ semantic_contract_applied: true
218
+ };
219
+ return {
220
+ analysis_insights: analysisInsights,
221
+ recommendations,
222
+ chirp_intelligence: chirpEnhanced.chirp_intelligence,
223
+ metadata
224
+ };
225
+ }
226
+ /**
227
+ * Helper: Fetch today's NHL schedule from public API
228
+ * Returns array of games for efficient lookup
229
+ */
230
+ async fetchTodaySchedule() {
231
+ try {
232
+ // Get today's date in YYYY-MM-DD format
233
+ const today = new Date().toISOString().split('T')[0];
234
+ // Fetch today's NHL schedule from public NHL API
235
+ const response = await fetch(`https://api-web.nhle.com/v1/schedule/${today}`);
236
+ if (!response.ok)
237
+ return [];
238
+ const schedule = await response.json();
239
+ return schedule.gameWeek?.[0]?.games || [];
240
+ }
241
+ catch (error) {
242
+ console.error('[DEBUG] Error fetching NHL schedule:', error);
243
+ return [];
244
+ }
245
+ }
246
+ /**
247
+ * Helper: Check if player has a game today
248
+ * Uses pre-fetched NHL schedule for efficiency
249
+ */
250
+ hasGameToday(player, todayGames) {
251
+ if (!player.team || !todayGames.length)
252
+ return false;
253
+ const playerTeam = player.team.toUpperCase();
254
+ return todayGames.some((game) => {
255
+ const homeTeam = game.homeTeam?.abbrev?.toUpperCase();
256
+ const awayTeam = game.awayTeam?.abbrev?.toUpperCase();
257
+ return homeTeam === playerTeam || awayTeam === playerTeam;
258
+ });
259
+ }
260
+ /**
261
+ * Helper: Calculate overall lineup health score (0-100)
262
+ */
263
+ calculateLineupScore(issues) {
264
+ let score = 100;
265
+ // Deduct points for each issue based on severity
266
+ for (const issue of issues) {
267
+ switch (issue.severity) {
268
+ case 'CRITICAL':
269
+ score -= 20;
270
+ break;
271
+ case 'HIGH':
272
+ score -= 10;
273
+ break;
274
+ case 'MEDIUM':
275
+ score -= 5;
276
+ break;
277
+ case 'LOW':
278
+ score -= 2;
279
+ break;
280
+ }
281
+ }
282
+ return Math.max(0, score);
283
+ }
284
+ }