@semanticintent/semantic-chirp-intelligence-mcp 3.0.0 → 4.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.
@@ -6,87 +6,37 @@
6
6
  */
7
7
  import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
8
8
  import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
9
+ import { NHL_SCHEDULE, NhlScheduleService } from '../services/NhlScheduleService.js';
10
+ import { LEAGUE_DATA, LeagueDataService } from '../services/LeagueDataService.js';
11
+ import { NHL_STATS } from '../services/NhlStatsService.js';
9
12
  export class StreamingAnalysis extends AnalysisTemplate {
10
- apiClient;
11
- leagueId;
12
- teamId;
13
- constructor(apiClient, leagueId, teamId) {
13
+ constructor() {
14
14
  super("get_streaming_recommendations", "streaming_strategy");
15
- this.apiClient = apiClient;
16
- this.leagueId = leagueId;
17
- this.teamId = teamId;
18
15
  }
19
16
  /**
20
17
  * Hook 1: Fetch raw data from Yahoo API
21
18
  */
22
19
  async fetchData(args) {
23
- try {
24
- // Fetch trending players (hot pickups)
25
- // Note: apiClient.request() already includes /fantasy/v2 base
26
- const cleanLeagueId = this.leagueId.replace(/^nhl\.l\./, '');
27
- const trendingData = await this.apiClient.request(`/league/nhl.l.${cleanLeagueId}/players;status=A;sort=AR`);
28
- // Fetch your current roster to avoid recommending owned players
29
- const rosterData = await this.apiClient.getTeamRoster(this.leagueId, this.teamId);
30
- // Fetch league scoreboard for schedule data
31
- const scoreboardData = await this.apiClient.getLeagueScoreboard(this.leagueId);
32
- return {
33
- trending: trendingData,
34
- roster: rosterData,
35
- scoreboard: scoreboardData
36
- };
37
- }
38
- catch (error) {
39
- throw new Error(`Failed to fetch streaming data: ${error}`);
40
- }
20
+ // v4: no waiver wire exists without league-private ownership data, so the
21
+ // pool is "NHL players not on the rosters you gave me", ranked by
22
+ // production. The caveat travels with the results.
23
+ await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
24
+ return {
25
+ pool: LEAGUE_DATA.getPlayerPool({ limit: 120 }),
26
+ roster: LEAGUE_DATA.getRoster(),
27
+ pool_caveat: LeagueDataService.POOL_CAVEAT
28
+ };
41
29
  }
42
30
  /**
43
31
  * Hook 2: Prepare data into FantasyData structure
44
32
  */
45
33
  async prepareData(rawData, args) {
46
- const { trending, roster, scoreboard } = rawData;
47
- // Parse current roster to filter out owned players
48
- const ownedPlayerIds = new Set();
49
- const rosterPlayers = roster.fantasy_content?.team?.[1]?.roster?.['0']?.players?.['0']?.player || [];
50
- for (const playerData of rosterPlayers) {
51
- const player = playerData.player?.[0] || playerData;
52
- ownedPlayerIds.add(player.player_id);
53
- }
54
- // Parse available trending players
55
- const availablePlayers = [];
56
- const trendingPlayers = trending.fantasy_content?.league?.[1]?.players?.['0']?.player || [];
57
- for (const playerData of trendingPlayers) {
58
- const player = playerData.player?.[0] || playerData;
59
- const playerId = player.player_id;
60
- // Skip if already owned
61
- if (ownedPlayerIds.has(playerId)) {
62
- continue;
63
- }
64
- // Apply position filter if specified
65
- const position = player.display_position || player.primary_positions?.[0] || 'Unknown';
66
- if (args.position_filter && args.position_filter.length > 0) {
67
- if (!args.position_filter.includes(position)) {
68
- continue;
69
- }
70
- }
71
- availablePlayers.push({
72
- player_id: playerId,
73
- name: player.name?.full || 'Unknown',
74
- position: position,
75
- team: player.editorial_team_abbr || '',
76
- selected_position: ['FA'], // Free agent
77
- status: player.status,
78
- percent_owned: parseFloat(player.percent_owned?.value || '0'),
79
- stats: this.parsePlayerStats(player)
80
- });
81
- // Limit to top available players
82
- if (availablePlayers.length >= (args.max_recommendations || 10) * 2) {
83
- break;
84
- }
85
- }
34
+ // The pool is already resolved NHL players; there is no platform payload
35
+ // left to traverse.
86
36
  return {
87
- availablePlayers,
88
- roster: this.parseRoster(roster),
89
- scoreboard: scoreboard
37
+ availablePlayers: rawData.pool ?? [],
38
+ roster: rawData.roster ?? undefined,
39
+ poolCaveat: rawData.pool_caveat
90
40
  };
91
41
  }
92
42
  /**
@@ -98,8 +48,8 @@ export class StreamingAnalysis extends AnalysisTemplate {
98
48
  const streamingRecommendations = [];
99
49
  // Analyze each available player
100
50
  for (const player of data.availablePlayers || []) {
101
- // Calculate games this week (simplified - would use actual schedule in production)
102
- const gamesThisWeek = this.estimateGamesThisWeek(player, lookAheadDays);
51
+ // Real games in the look-ahead window for this player's actual club
52
+ const gamesThisWeek = this.countGamesThisWeek(player, lookAheadDays);
103
53
  // Determine pickup priority based on games and ownership
104
54
  let pickupPriority;
105
55
  let reasoning;
@@ -156,6 +106,10 @@ export class StreamingAnalysis extends AnalysisTemplate {
156
106
  reasoning: analysis.reasoning
157
107
  }));
158
108
  const analysisInsights = {
109
+ pool_caveat: data.poolCaveat,
110
+ schedule_source: this.scheduleAvailable()
111
+ ? `NHL public API (season ${NHL_SCHEDULE.getSeason()})`
112
+ : `UNAVAILABLE - ${NHL_SCHEDULE.getUnavailableReason()}; game counts shown as 0`,
159
113
  streaming_summary: {
160
114
  total_recommendations: streamingData.length,
161
115
  high_priority_count: streamingData.filter(s => s.pickup_priority === 'HIGH' || s.pickup_priority === 'CRITICAL').length,
@@ -216,13 +170,23 @@ export class StreamingAnalysis extends AnalysisTemplate {
216
170
  };
217
171
  }
218
172
  /**
219
- * Helper: Estimate games this week for a player
173
+ * Helper: Count this player's real club games in the look-ahead window.
174
+ *
175
+ * Every player used to return the same number here, which made the
176
+ * "4 games this week, low ownership" branch unreachable in practice.
177
+ * Returns 0 when the schedule is unavailable so no player is promoted on
178
+ * imaginary volume; `scheduleAvailable()` reports why.
220
179
  */
221
- estimateGamesThisWeek(player, lookAheadDays) {
222
- // Simplified calculation - in production would use actual NHL schedule
223
- // Average NHL team plays 3-4 games per week
224
- const gamesPerWeek = 3.5;
225
- return Math.round((lookAheadDays / 7) * gamesPerWeek);
180
+ countGamesThisWeek(player, lookAheadDays) {
181
+ if (!NHL_SCHEDULE.isAvailable())
182
+ return 0;
183
+ const start = NhlScheduleService.today();
184
+ const end = NhlScheduleService.addDays(start, Math.max(0, lookAheadDays - 1));
185
+ return NHL_SCHEDULE.countGamesInRange(player.team, start, end);
186
+ }
187
+ /** Whether recommendations in this run were backed by the real schedule. */
188
+ scheduleAvailable() {
189
+ return NHL_SCHEDULE.isAvailable();
226
190
  }
227
191
  /**
228
192
  * Helper: Describe recent performance
@@ -20,15 +20,12 @@
20
20
  */
21
21
  import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
22
22
  import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
23
+ import { NHL_SCHEDULE, NhlScheduleService } from '../services/NhlScheduleService.js';
24
+ import { LEAGUE_DATA, LeagueDataService } from '../services/LeagueDataService.js';
25
+ import { NHL_STATS } from '../services/NhlStatsService.js';
23
26
  export class WeekendStreamAnalysis extends AnalysisTemplate {
24
- yahooClient;
25
- leagueId;
26
- teamId;
27
- constructor(yahooClient, leagueId, teamId) {
27
+ constructor() {
28
28
  super('analyze_weekend_streams', 'streaming_recommendations');
29
- this.yahooClient = yahooClient;
30
- this.leagueId = leagueId;
31
- this.teamId = teamId;
32
29
  }
33
30
  /**
34
31
  * Semantic Identity for this analysis
@@ -75,28 +72,27 @@ CHIRP STYLE: desperate_or_legit
75
72
  * - Trending players
76
73
  */
77
74
  async fetchData(args) {
78
- const ownershipMax = args.ownership_max || 50;
79
75
  const positions = args.position_filter || ['C', 'LW', 'RW', 'D', 'G'];
80
- // Fetch free agents across requested positions
81
- const freeAgentPromises = positions.map(pos => this.yahooClient.searchPlayers(pos, 50, this.leagueId));
82
- const [freeAgentResults, roster, trendingAdds] = await Promise.all([
83
- Promise.all(freeAgentPromises),
84
- this.yahooClient.getTeamRoster(this.leagueId, this.teamId),
85
- this.yahooClient.getTrendingPlayers('add', 25, this.leagueId)
86
- ]);
87
- // Combine and deduplicate free agents
88
- const allFreeAgents = [];
89
- for (const result of freeAgentResults) {
90
- if (result.players) {
91
- allFreeAgents.push(...result.players);
76
+ await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load(), NHL_SCHEDULE.loadStandings()]);
77
+ // One pool per requested position, deduplicated — replaces the Yahoo
78
+ // free-agent search. Ownership is league-private, so these are candidates
79
+ // to check rather than confirmed adds.
80
+ const seen = new Set();
81
+ const freeAgents = [];
82
+ for (const pos of positions) {
83
+ for (const p of LEAGUE_DATA.getPlayerPool({ position: pos, limit: 40 })) {
84
+ if (seen.has(p.player_id))
85
+ continue;
86
+ seen.add(p.player_id);
87
+ freeAgents.push(p);
92
88
  }
93
89
  }
94
- const uniqueFreeAgents = Array.from(new Map(allFreeAgents.map(p => [p.player_id, p])).values()).filter(p => (p.percent_owned || 0) <= ownershipMax);
95
90
  return {
96
- freeAgents: uniqueFreeAgents,
97
- roster,
98
- trendingAdds: trendingAdds.players || [],
99
- dateRange: args.date_range
91
+ freeAgents,
92
+ roster: LEAGUE_DATA.getRoster(),
93
+ trendingAdds: [],
94
+ dateRange: args.date_range,
95
+ pool_caveat: LeagueDataService.POOL_CAVEAT
100
96
  };
101
97
  }
102
98
  /**
@@ -108,16 +104,16 @@ CHIRP STYLE: desperate_or_legit
108
104
  async prepareData(rawData, args) {
109
105
  // Identify roster gaps
110
106
  const rosterGaps = this.identifyRosterGaps(rawData.roster, args);
111
- // Get weekend schedules for each free agent
112
- // (In real implementation, would fetch NHL schedule API)
113
- const weekendSchedules = this.mockWeekendSchedules(rawData.freeAgents, rawData.dateRange);
107
+ // Real weekend schedules from the NHL public API
108
+ const weekendSchedules = this.buildWeekendSchedules(rawData.freeAgents, rawData.dateRange);
114
109
  // Return with custom properties attached
115
110
  const result = {
116
111
  availablePlayers: rawData.freeAgents,
117
112
  roster: rawData.roster,
118
113
  trendingPlayers: rawData.trendingAdds,
119
114
  rosterGaps: rosterGaps,
120
- weekendSchedules: weekendSchedules
115
+ weekendSchedules: weekendSchedules,
116
+ poolCaveat: rawData.pool_caveat
121
117
  };
122
118
  return result;
123
119
  }
@@ -163,27 +159,25 @@ CHIRP STYLE: desperate_or_legit
163
159
  };
164
160
  }
165
161
  /**
166
- * Layer 3: Mock weekend schedules
167
- * (In production, integrate with NHL Schedule API)
162
+ * Layer 3: Real weekend schedules from the NHL public API.
163
+ *
164
+ * Each candidate gets the games their actual club actually plays in the
165
+ * requested window — including whether those games are back-to-back, which
166
+ * is a real signal for goalies rather than a coin flip.
168
167
  */
169
- mockWeekendSchedules(players, dateRange) {
168
+ buildWeekendSchedules(players, dateRange) {
170
169
  const schedules = new Map();
170
+ const start = dateRange?.start ?? NhlScheduleService.today();
171
+ const end = dateRange?.end ?? NhlScheduleService.addDays(start, 2);
171
172
  for (const player of players) {
172
- // Mock 1-3 weekend games
173
- const gameCount = Math.floor(Math.random() * 3) + 1;
174
- const games = [];
175
- for (let i = 0; i < gameCount; i++) {
176
- games.push({
177
- date: dateRange.start,
178
- opponent: this.getRandomOpponent(player.team),
179
- home: Math.random() > 0.5
180
- });
181
- }
173
+ const games = NHL_SCHEDULE.isAvailable()
174
+ ? NHL_SCHEDULE.getGamesInRange(player.team, start, end)
175
+ : [];
182
176
  schedules.set(player.player_id, {
183
177
  player_id: player.player_id,
184
- games,
185
- game_count: gameCount,
186
- has_back_to_back: gameCount >= 2 && Math.random() > 0.7
178
+ games: games.map(g => ({ date: g.date, opponent: g.opponent, home: g.home })),
179
+ game_count: games.length,
180
+ has_back_to_back: NHL_SCHEDULE.isAvailable() && NHL_SCHEDULE.countBackToBacks(player.team, start, end) > 0
187
181
  });
188
182
  }
189
183
  return schedules;
@@ -305,31 +299,86 @@ CHIRP STYLE: desperate_or_legit
305
299
  return 'monitor';
306
300
  }
307
301
  /**
308
- * Layer 3: Evaluate player metrics
302
+ * Layer 3: Evaluate player metrics from Yahoo's real stats.
303
+ *
304
+ * Season stats give the baseline, last-month stats give the current form,
305
+ * and the projection weights recent form 60/40 over the season line. Where a
306
+ * player has no stats at all (a true unknown), `has_stats` is false and the
307
+ * caller can see the score rests on schedule and role signals only —
308
+ * previously this method returned `Math.random()` and nothing said so.
309
309
  */
310
310
  async evaluateMetrics(player, data) {
311
- // Mock metrics - in production, fetch real stats
311
+ const dataAny = data;
312
312
  const isTrending = data.trendingPlayers?.some((t) => t.player_id === player.player_id);
313
- const basePerformance = Math.random() * 0.8 + 0.2; // 0.2 - 1.0 PPG
314
- const recent_ppg = basePerformance;
315
- const projected_fpg = isTrending ? basePerformance * 1.15 : basePerformance;
316
- // TOI estimate (higher for better players)
317
- const ownership = player.percent_owned || 0;
318
- const opportunity_toi = 10 + (ownership / 100) * 10 + Math.random() * 5; // 10-25 min
313
+ // v4: NHL season statistics ride on the player. There is no public source
314
+ // of recent-form splits, so form weighting is not applied — the season
315
+ // line is the projection basis, and `games_sampled` says so.
316
+ const season = player.stats ?? null;
317
+ const seasonPoints = (season?.goals ?? 0) + (season?.assists ?? 0);
318
+ const recentPoints = seasonPoints;
319
+ const seasonGames = season?.games_played ?? 0;
320
+ const recentGames = 0;
321
+ const hasStats = seasonGames > 0 || recentGames > 0;
322
+ const seasonPpg = seasonGames > 0 ? seasonPoints / seasonGames : 0;
323
+ const recentPpg = recentGames > 0 ? recentPoints / recentGames : seasonPpg;
324
+ // Weight current form over the season baseline, but only when there is
325
+ // enough recent sample to mean anything.
326
+ const formWeight = recentGames >= 3 ? 0.6 : 0;
327
+ const projectedBase = (formWeight * recentPpg) + ((1 - formWeight) * seasonPpg);
328
+ const projected_fpg = isTrending ? projectedBase * 1.15 : projectedBase;
319
329
  return {
320
- recent_ppg,
321
- projected_fpg,
322
- opportunity_toi,
323
- pp_role: opportunity_toi > 18 ? 'PP1' : opportunity_toi > 15 ? 'PP2' : 'None',
324
- line_position: opportunity_toi > 16 ? 'Top-6' : 'Bottom-6'
330
+ has_stats: hasStats,
331
+ recent_ppg: Number(recentPpg.toFixed(3)),
332
+ season_ppg: Number(seasonPpg.toFixed(3)),
333
+ games_sampled: { season: seasonGames, recent: recentGames },
334
+ projected_fpg: Number(projected_fpg.toFixed(3)),
335
+ opportunity_toi: this.estimateOpportunity(player, season, null),
336
+ pp_role: this.derivePowerPlayRole(season, null, seasonGames, recentGames),
337
+ line_position: projectedBase >= 0.5 ? 'Top-6' : 'Bottom-6'
325
338
  };
326
339
  }
340
+ /**
341
+ * Fantasy points proxy from a labelled stat line.
342
+ * Uses Yahoo's own points category when the league reports it, otherwise
343
+ * reconstructs it from goals and assists.
344
+ */
345
+ /**
346
+ * Opportunity signal on the 0-25 scale the upside formula expects.
347
+ *
348
+ * Yahoo does not expose time on ice in standard league stat lines, so this
349
+ * derives opportunity from observable production volume (shots per game) and
350
+ * power-play involvement rather than inventing a minutes figure.
351
+ */
352
+ estimateOpportunity(player, season, _recent) {
353
+ const games = season?.games_played ?? 0;
354
+ if (games === 0)
355
+ return 0;
356
+ const shotsPerGame = (season?.shots ?? 0) / games;
357
+ const ppPointsPerGame = (season?.power_play_goals ?? 0) / games;
358
+ // Shot volume scaled to ~0-15, power-play involvement worth up to ~10.
359
+ const shotComponent = Math.min(15, shotsPerGame * 5);
360
+ const ppComponent = Math.min(10, ppPointsPerGame * 40);
361
+ return Number((shotComponent + ppComponent).toFixed(1));
362
+ }
363
+ /** Power-play role inferred from actual power-play production. */
364
+ derivePowerPlayRole(season, _recent, seasonGames, recentGames) {
365
+ const games = seasonGames || recentGames;
366
+ if (games === 0)
367
+ return 'Unknown';
368
+ const ppp = season?.power_play_goals ?? 0;
369
+ const perGame = ppp / games;
370
+ if (perGame >= 0.35)
371
+ return 'PP1';
372
+ if (perGame >= 0.12)
373
+ return 'PP2';
374
+ return 'None';
375
+ }
327
376
  /**
328
377
  * Layer 1: Analyze schedule ease
329
378
  */
330
379
  analyzeScheduleEase(schedule, player) {
331
380
  if (schedule.game_count === 0) {
332
- return { ease_score: 0, matchup_quality: 'average' };
381
+ return { ease_score: 0, matchup_quality: 'unknown' };
333
382
  }
334
383
  // Base score on game count
335
384
  let easeScore = schedule.game_count * 30;
@@ -339,8 +388,16 @@ CHIRP STYLE: desperate_or_legit
339
388
  // Penalty for back-to-back
340
389
  if (schedule.has_back_to_back)
341
390
  easeScore -= 10;
342
- // Opponent strength (mock - in production use team rankings)
343
- const opponentStrength = Math.random() * 100;
391
+ // Opponent strength from real NHL standings (goals allowed per game, ranked).
392
+ // 0 = softest defence to face, 100 = stingiest.
393
+ const opponentStrength = this.averageOpponentDifficulty(schedule);
394
+ if (opponentStrength === null) {
395
+ // No standings data - rate on schedule volume alone rather than guessing.
396
+ return {
397
+ ease_score: Math.max(0, Math.min(100, easeScore)),
398
+ matchup_quality: 'unknown'
399
+ };
400
+ }
344
401
  if (opponentStrength < 30) {
345
402
  easeScore += 20; // Weak opponent
346
403
  }
@@ -479,12 +536,19 @@ CHIRP STYLE: desperate_or_legit
479
536
  };
480
537
  }
481
538
  /**
482
- * Generate random opponent (mock)
539
+ * Mean defensive difficulty of the opponents on this weekend slate.
540
+ * Returns null when standings are unavailable, so the caller can decline to
541
+ * rate the matchup rather than invent a rating.
483
542
  */
484
- getRandomOpponent(excludeTeam) {
485
- const teams = ['BOS', 'CAR', 'COL', 'DAL', 'EDM', 'FLA', 'NYR', 'TOR', 'VGK', 'WPG'];
486
- const filtered = teams.filter(t => t !== excludeTeam);
487
- return filtered[Math.floor(Math.random() * filtered.length)];
543
+ averageOpponentDifficulty(schedule) {
544
+ if (!NHL_SCHEDULE.hasStandings() || schedule.games.length === 0)
545
+ return null;
546
+ const ratings = schedule.games
547
+ .map(game => NHL_SCHEDULE.getTeamStrength(game.opponent)?.difficulty)
548
+ .filter((d) => typeof d === 'number');
549
+ if (ratings.length === 0)
550
+ return null;
551
+ return ratings.reduce((sum, d) => sum + d, 0) / ratings.length;
488
552
  }
489
553
  /**
490
554
  * Generate chirp intelligence
@@ -109,5 +109,73 @@ export const TOOL_METADATA = {
109
109
  intent_category: "market_intelligence",
110
110
  hockey_context: "waiver_trends",
111
111
  chirp_potential: "trend_opportunities"
112
+ },
113
+ chirp_opponent: {
114
+ chirp_style: "savage_trash_talk",
115
+ discovery_tags: ["opponent", "chirp", "trash_talk", "weaknesses", "rivalry"],
116
+ intent_category: "psychological_warfare",
117
+ hockey_context: "head_to_head_dominance",
118
+ chirp_potential: "opponent_destruction"
119
+ },
120
+ set_roster: {
121
+ chirp_style: "analytical_assessment",
122
+ discovery_tags: ["roster", "paste", "setup", "players", "import"],
123
+ intent_category: "league_state_capture",
124
+ hockey_context: "roster_definition",
125
+ chirp_potential: "roster_reality"
126
+ },
127
+ set_opponent_roster: {
128
+ chirp_style: "matchup_assessment",
129
+ discovery_tags: ["opponent", "paste", "matchup", "scouting", "import"],
130
+ intent_category: "league_state_capture",
131
+ hockey_context: "head_to_head_battle",
132
+ chirp_potential: "opponent_weaknesses"
133
+ },
134
+ set_standings: {
135
+ chirp_style: "competitive_reality",
136
+ discovery_tags: ["standings", "paste", "league", "rankings", "import"],
137
+ intent_category: "league_state_capture",
138
+ hockey_context: "competitive_landscape",
139
+ chirp_potential: "standings_truth"
140
+ },
141
+ show_stored_data: {
142
+ chirp_style: "analytical_assessment",
143
+ discovery_tags: ["state", "stored", "roster", "debug", "inspect"],
144
+ intent_category: "league_state_capture",
145
+ hockey_context: "roster_analysis",
146
+ chirp_potential: "state_visibility"
147
+ },
148
+ schedule_value: {
149
+ chirp_style: "strategic_advantage",
150
+ discovery_tags: ["draft", "schedule", "playoff_weeks", "games_per_week", "tiebreaker"],
151
+ intent_category: "draft_intelligence",
152
+ hockey_context: "schedule_warfare",
153
+ chirp_potential: "schedule_domination",
154
+ // 🆕 Template Pattern Metadata
155
+ uses_template_pattern: true,
156
+ analysis_class: "ScheduleValueAnalysis",
157
+ template_version: "1.0.0",
158
+ analysis_type: "schedule_advantage"
159
+ },
160
+ chirp_draft_pick: {
161
+ chirp_style: "ice_cold_truth",
162
+ discovery_tags: ["draft", "pick", "ADP", "value", "roster_build"],
163
+ intent_category: "draft_intelligence",
164
+ hockey_context: "draft_day_decisions",
165
+ chirp_potential: "draft_reality",
166
+ is_ice_engine: true,
167
+ tool_semantic_identity: "ICE - Intent Chirp Engine (Draft)",
168
+ // 🆕 Template Pattern Metadata
169
+ uses_template_pattern: true,
170
+ analysis_class: "DraftPickAnalysis",
171
+ template_version: "1.0.0",
172
+ analysis_type: "draft_pick"
173
+ },
174
+ analyze_trade: {
175
+ chirp_style: "trade_verdict",
176
+ discovery_tags: ["trade", "analysis", "value", "categories", "decision"],
177
+ intent_category: "trade_intelligence",
178
+ hockey_context: "roster_maneuvering",
179
+ chirp_potential: "trade_reality"
112
180
  }
113
181
  };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 🏒 NHL Team Identity — Yahoo ↔ NHL abbreviation mapping
3
+ *
4
+ * 🏛️ Rule 3 (Observable Anchoring): a team's identity is anchored to its NHL
5
+ * tricode, which is the only abbreviation the NHL public API answers to.
6
+ * Yahoo publishes a *different* set of abbreviations for the same 32 clubs
7
+ * (`LA` vs `LAK`, `NJ` vs `NJD`, `SJ` vs `SJS`, `TB` vs `TBL`, `StL` vs `STL`).
8
+ *
9
+ * Comparing Yahoo's abbreviation directly against an NHL tricode silently fails
10
+ * for those five clubs — the player simply looks like they never play. Every
11
+ * crossing of that boundary must go through `toNhlTricode`.
12
+ */
13
+ /** The 32 NHL tricodes as the public API spells them. */
14
+ export const NHL_TRICODES = [
15
+ 'ANA', 'BOS', 'BUF', 'CAR', 'CBJ', 'CGY', 'CHI', 'COL',
16
+ 'DAL', 'DET', 'EDM', 'FLA', 'LAK', 'MIN', 'MTL', 'NJD',
17
+ 'NSH', 'NYI', 'NYR', 'OTT', 'PHI', 'PIT', 'SEA', 'SJS',
18
+ 'STL', 'TBL', 'TOR', 'UTA', 'VAN', 'VGK', 'WPG', 'WSH'
19
+ ];
20
+ const TRICODE_SET = new Set(NHL_TRICODES);
21
+ /**
22
+ * Yahoo `editorial_team_abbr` values that differ from the NHL tricode,
23
+ * plus a few historical/alternate spellings seen in the wild.
24
+ * Keys are compared upper-cased.
25
+ */
26
+ const ALIAS_TO_TRICODE = {
27
+ // Yahoo's short forms — the five that silently break naive comparison
28
+ LA: 'LAK',
29
+ NJ: 'NJD',
30
+ SJ: 'SJS',
31
+ TB: 'TBL',
32
+ STL: 'STL', // StL upper-cases cleanly, listed for intent
33
+ // Alternate spellings / other providers
34
+ LV: 'VGK',
35
+ VEG: 'VGK',
36
+ WAS: 'WSH',
37
+ WSH: 'WSH',
38
+ CLS: 'CBJ',
39
+ CLB: 'CBJ',
40
+ MON: 'MTL',
41
+ NAS: 'NSH',
42
+ CAL: 'CGY',
43
+ TBL: 'TBL',
44
+ ARI: 'UTA', // Arizona relocated to Utah
45
+ PHX: 'UTA',
46
+ UTA: 'UTA',
47
+ UTAH: 'UTA'
48
+ };
49
+ /**
50
+ * Normalize any provider's team abbreviation to an NHL tricode.
51
+ * Returns `null` for unknown or missing input rather than guessing — callers
52
+ * must decide what an unmappable team means for their analysis.
53
+ */
54
+ export function toNhlTricode(abbr) {
55
+ if (!abbr)
56
+ return null;
57
+ const upper = String(abbr).trim().toUpperCase();
58
+ if (!upper)
59
+ return null;
60
+ if (TRICODE_SET.has(upper))
61
+ return upper;
62
+ return ALIAS_TO_TRICODE[upper] ?? null;
63
+ }
64
+ /** True when the abbreviation resolves to a real NHL club. */
65
+ export function isKnownTeam(abbr) {
66
+ return toNhlTricode(abbr) !== null;
67
+ }