@semanticintent/semantic-chirp-intelligence-mcp 3.1.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.
- package/.chirp-data/opponent.json +24 -0
- package/.chirp-data/roster.json +34 -0
- package/.chirp-data/standings.json +17 -0
- package/.nhl-schedule-cache/20262027.json +1 -0
- package/.nhl-schedule-cache/players-20262027-20252026.json +1 -0
- package/README.md +129 -79
- package/build/analyses/BreakoutAnalysis.js +25 -36
- package/build/analyses/DraftPickAnalysis.js +431 -0
- package/build/analyses/GamesInHandAnalysis.js +52 -121
- package/build/analyses/IceAnalysis.js +66 -57
- package/build/analyses/LineupAnalysis.js +26 -46
- package/build/analyses/ScheduleValueAnalysis.js +279 -0
- package/build/analyses/StreamingAnalysis.js +40 -76
- package/build/analyses/WeekendStreamAnalysis.js +130 -66
- package/build/config/tool-metadata.js +54 -0
- package/build/domain/nhl-teams.js +67 -0
- package/build/index.js +561 -653
- package/build/services/LeagueDataService.js +130 -0
- package/build/services/NhlScheduleService.js +357 -0
- package/build/services/NhlStatsService.js +297 -0
- package/build/services/RosterStore.js +233 -0
- package/package.json +12 -10
- package/scripts/preflight.mjs +67 -0
- package/scripts/smoke.mjs +92 -0
- package/.env.example +0 -8
- package/authenticate.js +0 -207
- package/build/experimental/semantic-breakout-tool.js +0 -188
- package/build/experimental/semantic-intent-parser.js +0 -222
- package/build/experimental/semantic-tool-integration.js +0 -146
- package/build/experimental/test-parser.js +0 -61
- package/build/services/YahooApiClient.js +0 -309
|
@@ -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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
47
|
-
//
|
|
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:
|
|
89
|
-
|
|
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
|
-
//
|
|
102
|
-
const gamesThisWeek = this.
|
|
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:
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
97
|
-
roster,
|
|
98
|
-
trendingAdds:
|
|
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
|
-
//
|
|
112
|
-
|
|
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:
|
|
167
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
186
|
-
has_back_to_back:
|
|
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
|
-
|
|
311
|
+
const dataAny = data;
|
|
312
312
|
const isTrending = data.trendingPlayers?.some((t) => t.player_id === player.player_id);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
const
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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: '
|
|
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
|
|
343
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
@@ -117,6 +117,60 @@ export const TOOL_METADATA = {
|
|
|
117
117
|
hockey_context: "head_to_head_dominance",
|
|
118
118
|
chirp_potential: "opponent_destruction"
|
|
119
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
|
+
},
|
|
120
174
|
analyze_trade: {
|
|
121
175
|
chirp_style: "trade_verdict",
|
|
122
176
|
discovery_tags: ["trade", "analysis", "value", "categories", "decision"],
|
|
@@ -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
|
+
}
|