@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
|
@@ -10,34 +10,29 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
|
|
12
12
|
import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
|
|
13
|
+
import { LEAGUE_DATA, NO_ROSTER_MESSAGE, NO_OPPONENT_MESSAGE } from '../services/LeagueDataService.js';
|
|
14
|
+
import { NHL_STATS } from '../services/NhlStatsService.js';
|
|
15
|
+
import { NHL_SCHEDULE, NhlScheduleService } from '../services/NhlScheduleService.js';
|
|
13
16
|
/**
|
|
14
17
|
* ICE Analysis - The ultimate roster optimization engine
|
|
15
18
|
*/
|
|
16
19
|
export class IceAnalysis extends AnalysisTemplate {
|
|
17
|
-
|
|
18
|
-
leagueId;
|
|
19
|
-
teamId;
|
|
20
|
-
constructor(apiClient, leagueId, teamId) {
|
|
20
|
+
constructor() {
|
|
21
21
|
super("get_roster_transaction_recommendations", "ice_roster");
|
|
22
|
-
this.apiClient = apiClient;
|
|
23
|
-
this.leagueId = leagueId;
|
|
24
|
-
this.teamId = teamId;
|
|
25
22
|
}
|
|
26
23
|
/**
|
|
27
24
|
* Hook 1: Fetch raw data from Yahoo API
|
|
28
25
|
*/
|
|
29
26
|
async fetchData(args) {
|
|
30
27
|
const lookAheadDays = args.look_ahead_days || 7;
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.fetchStreamingRecommendations(lookAheadDays)
|
|
36
|
-
]);
|
|
28
|
+
// v4: the roster comes from what the user pasted, and everything else from
|
|
29
|
+
// the NHL public API. No account, no OAuth, no platform binding.
|
|
30
|
+
await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
|
|
31
|
+
const roster = LEAGUE_DATA.getRoster();
|
|
37
32
|
return {
|
|
38
|
-
roster
|
|
39
|
-
gamesInHand:
|
|
40
|
-
streaming:
|
|
33
|
+
roster,
|
|
34
|
+
gamesInHand: this.calculateGamesInHand(lookAheadDays),
|
|
35
|
+
streaming: this.streamingContext(),
|
|
41
36
|
lookAheadDays
|
|
42
37
|
};
|
|
43
38
|
}
|
|
@@ -45,31 +40,15 @@ export class IceAnalysis extends AnalysisTemplate {
|
|
|
45
40
|
* Hook 2: Prepare and transform data for analysis
|
|
46
41
|
*/
|
|
47
42
|
async prepareData(rawData, args) {
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
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
|
-
});
|
|
43
|
+
// 🏛️ Rule 3: an absent roster is reported, never treated as an empty one.
|
|
44
|
+
// "You have no players" and "you have not told me your players" are
|
|
45
|
+
// different statements and only one of them is true.
|
|
46
|
+
if (!rawData.roster) {
|
|
47
|
+
throw new Error(NO_ROSTER_MESSAGE);
|
|
48
|
+
}
|
|
49
|
+
const teamKey = rawData.roster.team_key;
|
|
50
|
+
const teamName = rawData.roster.team_name;
|
|
51
|
+
const players = rawData.roster.players;
|
|
73
52
|
// Return FantasyData with roster structure
|
|
74
53
|
// Store extra context (gamesInHand, streaming) for use in analyzeData
|
|
75
54
|
// Exclude 'roster' from spread to prevent overwriting our parsed roster
|
|
@@ -287,30 +266,60 @@ export class IceAnalysis extends AnalysisTemplate {
|
|
|
287
266
|
return recommendations;
|
|
288
267
|
}
|
|
289
268
|
/**
|
|
290
|
-
*
|
|
269
|
+
* Real schedule advantage over the look-ahead window.
|
|
270
|
+
*
|
|
271
|
+
* This was a stub returning zero, so ICE's "games in hand" line was never
|
|
272
|
+
* a measurement. It now counts each rostered player's actual club games
|
|
273
|
+
* from the NHL schedule. Without a stored opponent there is no differential
|
|
274
|
+
* to report, so it reports your own volume and says why.
|
|
291
275
|
*/
|
|
292
|
-
|
|
293
|
-
|
|
276
|
+
calculateGamesInHand(lookAheadDays) {
|
|
277
|
+
if (!NHL_SCHEDULE.isAvailable()) {
|
|
278
|
+
return {
|
|
279
|
+
available: false,
|
|
280
|
+
note: NHL_SCHEDULE.getUnavailableReason(),
|
|
281
|
+
games_in_hand_difference: 0
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const start = NhlScheduleService.today();
|
|
285
|
+
const end = NhlScheduleService.addDays(start, Math.max(0, lookAheadDays - 1));
|
|
286
|
+
const countFor = (roster) => (roster?.players ?? [])
|
|
287
|
+
.filter(p => p.selected_position !== 'IR')
|
|
288
|
+
.reduce((total, p) => total + NHL_SCHEDULE.countGamesInRange(p.team, start, end), 0);
|
|
289
|
+
const mine = countFor(LEAGUE_DATA.getRoster());
|
|
290
|
+
const opponent = LEAGUE_DATA.getOpponentRoster();
|
|
291
|
+
if (!opponent) {
|
|
292
|
+
return {
|
|
293
|
+
available: true,
|
|
294
|
+
your_remaining: mine,
|
|
295
|
+
opponent_remaining: null,
|
|
296
|
+
games_in_hand_difference: 0,
|
|
297
|
+
note: NO_OPPONENT_MESSAGE
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const theirs = countFor(opponent);
|
|
294
301
|
return {
|
|
295
|
-
|
|
296
|
-
your_remaining:
|
|
297
|
-
opponent_remaining:
|
|
302
|
+
available: true,
|
|
303
|
+
your_remaining: mine,
|
|
304
|
+
opponent_remaining: theirs,
|
|
305
|
+
games_in_hand_difference: mine - theirs,
|
|
306
|
+
window: { start, end }
|
|
298
307
|
};
|
|
299
308
|
}
|
|
300
309
|
/**
|
|
301
|
-
*
|
|
310
|
+
* Streaming context.
|
|
311
|
+
*
|
|
312
|
+
* Waiver-wire targets require knowing which players are unowned in your
|
|
313
|
+
* league, which no public data source can tell us. Rather than fabricate
|
|
314
|
+
* targets, this returns none and names the reason.
|
|
302
315
|
*/
|
|
303
|
-
|
|
304
|
-
// TODO: Replace with proper StreamingAnalysis when migrated
|
|
316
|
+
streamingContext() {
|
|
305
317
|
return {
|
|
306
318
|
streaming_targets: [],
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
market_intelligence: {
|
|
312
|
-
top_trending_team: "unknown"
|
|
313
|
-
}
|
|
319
|
+
unavailable_reason: 'Waiver targets need to know who is unowned in your league, which is ' +
|
|
320
|
+
'league-private. Roster, schedule and lineup analysis are unaffected.',
|
|
321
|
+
optimal_timing: { best_days: [], avoid_days: [] },
|
|
322
|
+
market_intelligence: { top_trending_team: 'unknown' }
|
|
314
323
|
};
|
|
315
324
|
}
|
|
316
325
|
}
|
|
@@ -7,33 +7,23 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
|
|
9
9
|
import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
|
|
10
|
+
import { NHL_SCHEDULE, NhlScheduleService } from '../services/NhlScheduleService.js';
|
|
11
|
+
import { LEAGUE_DATA, NO_ROSTER_MESSAGE } from '../services/LeagueDataService.js';
|
|
12
|
+
import { NHL_STATS } from '../services/NhlStatsService.js';
|
|
10
13
|
export class LineupAnalysis extends AnalysisTemplate {
|
|
11
|
-
|
|
12
|
-
leagueId;
|
|
13
|
-
teamId;
|
|
14
|
-
constructor(apiClient, leagueId, teamId) {
|
|
14
|
+
constructor() {
|
|
15
15
|
super("optimize_lineup", "lineup_optimization");
|
|
16
|
-
this.apiClient = apiClient;
|
|
17
|
-
this.leagueId = leagueId;
|
|
18
|
-
this.teamId = teamId;
|
|
19
16
|
}
|
|
20
17
|
/**
|
|
21
18
|
* Hook 1: Fetch raw data from Yahoo API
|
|
22
19
|
*/
|
|
23
20
|
async fetchData(args) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
roster: rosterData,
|
|
31
|
-
scoreboard: scoreboardData
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
throw new Error(`Failed to fetch lineup data: ${error}`);
|
|
36
|
-
}
|
|
21
|
+
// v4: roster from the paste-backed store, schedule from the NHL public API.
|
|
22
|
+
await Promise.all([NHL_STATS.load(), NHL_SCHEDULE.load()]);
|
|
23
|
+
const roster = LEAGUE_DATA.getRoster();
|
|
24
|
+
if (!roster)
|
|
25
|
+
throw new Error(NO_ROSTER_MESSAGE);
|
|
26
|
+
return { roster };
|
|
37
27
|
}
|
|
38
28
|
/**
|
|
39
29
|
* Hook 2: Prepare data into FantasyData structure
|
|
@@ -224,38 +214,28 @@ export class LineupAnalysis extends AnalysisTemplate {
|
|
|
224
214
|
};
|
|
225
215
|
}
|
|
226
216
|
/**
|
|
227
|
-
* Helper:
|
|
228
|
-
*
|
|
217
|
+
* Helper: Ensure the season schedule is loaded.
|
|
218
|
+
*
|
|
219
|
+
* Replaces a per-call fetch of a single day with the shared season schedule,
|
|
220
|
+
* which is cached to disk and reused by every other analysis.
|
|
229
221
|
*/
|
|
230
222
|
async fetchTodaySchedule() {
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
}
|
|
223
|
+
await NHL_SCHEDULE.load();
|
|
224
|
+
return [];
|
|
245
225
|
}
|
|
246
226
|
/**
|
|
247
|
-
* Helper: Check if player
|
|
248
|
-
*
|
|
227
|
+
* Helper: Check if a player's club plays today.
|
|
228
|
+
*
|
|
229
|
+
* Yahoo and the NHL spell five clubs differently (`LA`/`LAK`, `NJ`/`NJD`,
|
|
230
|
+
* `SJ`/`SJS`, `TB`/`TBL`, `StL`/`STL`). The previous implementation compared
|
|
231
|
+
* Yahoo's abbreviation to the NHL tricode directly, so those five clubs
|
|
232
|
+
* always reported "no game today" — silently, and only for them.
|
|
233
|
+
* `toNhlTricode` inside the schedule service resolves both spellings.
|
|
249
234
|
*/
|
|
250
|
-
hasGameToday(player,
|
|
251
|
-
if (!player.team || !
|
|
235
|
+
hasGameToday(player, _todayGames) {
|
|
236
|
+
if (!player.team || !NHL_SCHEDULE.isAvailable())
|
|
252
237
|
return false;
|
|
253
|
-
|
|
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
|
-
});
|
|
238
|
+
return NHL_SCHEDULE.hasGameOn(player.team, NhlScheduleService.today());
|
|
259
239
|
}
|
|
260
240
|
/**
|
|
261
241
|
* Helper: Calculate overall lineup health score (0-100)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🗓️ Schedule Value Analysis — the draft tiebreaker
|
|
3
|
+
*
|
|
4
|
+
* Rates all 32 NHL clubs on what their schedule is actually worth to a fantasy
|
|
5
|
+
* roster: total games, how many weeks give you a 4-game slate, how many
|
|
6
|
+
* strand you on two, and — the part no generic site can do — how many games
|
|
7
|
+
* they play during *your league's* playoff weeks.
|
|
8
|
+
*
|
|
9
|
+
* A public schedule grid has to guess when your playoffs are. This reads
|
|
10
|
+
* `playoff_start_week` out of your Yahoo league settings, so the window it
|
|
11
|
+
* scores is the one you actually play.
|
|
12
|
+
*
|
|
13
|
+
* Semantic Identity: Schedule Value Rater
|
|
14
|
+
* Intent: I tell you which clubs' schedules are worth a draft pick
|
|
15
|
+
* Chirp Style: strategic_advantage
|
|
16
|
+
*/
|
|
17
|
+
import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
|
|
18
|
+
import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
|
|
19
|
+
import { NHL_SCHEDULE, NhlScheduleService } from '../services/NhlScheduleService.js';
|
|
20
|
+
import { NHL_TRICODES, toNhlTricode } from '../domain/nhl-teams.js';
|
|
21
|
+
/** Yahoo fantasy weeks run Monday to Sunday. */
|
|
22
|
+
const DAYS_PER_WEEK = 7;
|
|
23
|
+
export class ScheduleValueAnalysis extends AnalysisTemplate {
|
|
24
|
+
constructor() {
|
|
25
|
+
super('schedule_value', 'schedule_advantage');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hook 1: the NHL schedule, plus the league settings that say when this
|
|
29
|
+
* league's playoffs actually are.
|
|
30
|
+
*/
|
|
31
|
+
async fetchData(args) {
|
|
32
|
+
await NHL_SCHEDULE.load();
|
|
33
|
+
return { settings: null };
|
|
34
|
+
}
|
|
35
|
+
async prepareData(rawData, args) {
|
|
36
|
+
const window = this.resolvePlayoffWindow(rawData.settings, args);
|
|
37
|
+
return {
|
|
38
|
+
playoffWindow: window,
|
|
39
|
+
leagueSettings: rawData.settings
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Hook 3: score every requested club.
|
|
44
|
+
*/
|
|
45
|
+
async analyzeData(data, args) {
|
|
46
|
+
if (!NHL_SCHEDULE.isAvailable()) {
|
|
47
|
+
return {
|
|
48
|
+
schedule_available: false,
|
|
49
|
+
schedule_note: NHL_SCHEDULE.getUnavailableReason(),
|
|
50
|
+
teams: []
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const dataAny = data;
|
|
54
|
+
const window = dataAny.playoffWindow;
|
|
55
|
+
const requested = args.teams?.length
|
|
56
|
+
? args.teams
|
|
57
|
+
.map(t => toNhlTricode(t))
|
|
58
|
+
.filter((t) => t !== null)
|
|
59
|
+
: [...NHL_TRICODES];
|
|
60
|
+
const rated = requested
|
|
61
|
+
.map(team => this.rateTeam(team, window))
|
|
62
|
+
.filter((t) => t !== null)
|
|
63
|
+
.sort((a, b) => b.value_score - a.value_score);
|
|
64
|
+
return {
|
|
65
|
+
schedule_available: true,
|
|
66
|
+
season: NHL_SCHEDULE.getSeason(),
|
|
67
|
+
playoff_window: window,
|
|
68
|
+
teams: rated
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async generateChirp(analysisResults, semanticContract, data) {
|
|
72
|
+
const contract = this.mergeContractWithDefaults(semanticContract);
|
|
73
|
+
if (!analysisResults.schedule_available) {
|
|
74
|
+
return {
|
|
75
|
+
...analysisResults,
|
|
76
|
+
chirp_intelligence: {
|
|
77
|
+
chirp: `No schedule, no verdict. ${analysisResults.schedule_note}`,
|
|
78
|
+
intent_summary: 'Schedule unavailable - nothing rated'
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const best = analysisResults.teams[0];
|
|
83
|
+
const worst = analysisResults.teams[analysisResults.teams.length - 1];
|
|
84
|
+
const windowResolved = analysisResults.playoff_window?.resolved === true;
|
|
85
|
+
let chirp;
|
|
86
|
+
if (!best || !worst) {
|
|
87
|
+
chirp = 'Nothing to rate.';
|
|
88
|
+
}
|
|
89
|
+
else if (windowResolved) {
|
|
90
|
+
chirp =
|
|
91
|
+
`${best.team} is the schedule you want (${best.playoff_games} games in your playoff window, ` +
|
|
92
|
+
`${best.weeks_with_4_plus} four-game weeks). ${worst.team} is the one you draft around ` +
|
|
93
|
+
`(${worst.playoff_games} playoff-window games, ${worst.weeks_with_2_or_fewer} weeks stuck on two or fewer). ` +
|
|
94
|
+
`Same player, different sweater, different season.`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
chirp =
|
|
98
|
+
`On regular season alone, ${best.team} gives you the most playable weeks ` +
|
|
99
|
+
`(${best.weeks_with_4_plus} four-game weeks) and ${worst.team} the fewest ` +
|
|
100
|
+
`(${worst.weeks_with_2_or_fewer} weeks stuck on two or fewer). ` +
|
|
101
|
+
`I could not read your playoff weeks, and that is the half that decides titles — ` +
|
|
102
|
+
`pass playoff_start_week and playoff_end_week and ask again.`;
|
|
103
|
+
}
|
|
104
|
+
const enhanced = ChirpIntelligence.enhance(this.toolName, analysisResults, contract);
|
|
105
|
+
return {
|
|
106
|
+
...enhanced,
|
|
107
|
+
chirp_intelligence: {
|
|
108
|
+
...enhanced.chirp_intelligence,
|
|
109
|
+
// The schedule verdict is the point of this tool, so it leads the chirp.
|
|
110
|
+
analysis_chirp: chirp
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async formatResponse(chirpEnhanced, data) {
|
|
115
|
+
const teams = chirpEnhanced.teams ?? [];
|
|
116
|
+
const topN = teams.slice(0, 8);
|
|
117
|
+
const recommendations = topN.map((team, index) => ({
|
|
118
|
+
priority: index < 3 ? 'HIGH' : 'MEDIUM',
|
|
119
|
+
action: 'target',
|
|
120
|
+
reasoning: `${team.team}: ${team.playoff_games} games in your playoff window, ` +
|
|
121
|
+
`${team.weeks_with_4_plus} four-game weeks, ${team.total_games} total. ${team.verdict}`
|
|
122
|
+
}));
|
|
123
|
+
const analysisInsights = {
|
|
124
|
+
schedule_source: chirpEnhanced.schedule_available
|
|
125
|
+
? `NHL public API (season ${chirpEnhanced.season})`
|
|
126
|
+
: `UNAVAILABLE - ${chirpEnhanced.schedule_note}`,
|
|
127
|
+
playoff_window: chirpEnhanced.playoff_window,
|
|
128
|
+
best_schedules: topN.map(t => ({
|
|
129
|
+
team: t.team,
|
|
130
|
+
value_score: t.value_score,
|
|
131
|
+
playoff_games: t.playoff_games,
|
|
132
|
+
four_game_weeks: t.weeks_with_4_plus
|
|
133
|
+
})),
|
|
134
|
+
worst_schedules: teams.slice(-5).map(t => ({
|
|
135
|
+
team: t.team,
|
|
136
|
+
value_score: t.value_score,
|
|
137
|
+
playoff_games: t.playoff_games,
|
|
138
|
+
light_weeks: t.weeks_with_2_or_fewer
|
|
139
|
+
})),
|
|
140
|
+
all_teams: teams
|
|
141
|
+
};
|
|
142
|
+
const metadata = {
|
|
143
|
+
analysis_type: this.analysisType,
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
team_context: { team_name: 'Schedule Value' },
|
|
146
|
+
semantic_contract_applied: true
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
analysis_insights: analysisInsights,
|
|
150
|
+
recommendations,
|
|
151
|
+
chirp_intelligence: chirpEnhanced.chirp_intelligence,
|
|
152
|
+
metadata
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// ==========================================
|
|
156
|
+
// 🎯 Scoring
|
|
157
|
+
// ==========================================
|
|
158
|
+
rateTeam(team, window) {
|
|
159
|
+
const profile = NHL_SCHEDULE.getTeamProfile(team);
|
|
160
|
+
if (!profile)
|
|
161
|
+
return null;
|
|
162
|
+
const playoffGames = window?.start && window?.end
|
|
163
|
+
? NHL_SCHEDULE.countGamesInRange(team, window.start, window.end)
|
|
164
|
+
: 0;
|
|
165
|
+
const playoffWeeks = window?.weeks ?? [];
|
|
166
|
+
return {
|
|
167
|
+
team,
|
|
168
|
+
total_games: profile.total_games,
|
|
169
|
+
weeks_with_4_plus: profile.weeks_with_4_plus,
|
|
170
|
+
weeks_with_2_or_fewer: profile.weeks_with_2_or_fewer,
|
|
171
|
+
back_to_backs: profile.back_to_backs,
|
|
172
|
+
playoff_games: playoffGames,
|
|
173
|
+
playoff_weeks: playoffWeeks,
|
|
174
|
+
value_score: this.scoreTeam(profile, playoffGames, playoffWeeks.length),
|
|
175
|
+
verdict: this.verdictFor(profile, playoffGames, playoffWeeks.length)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Composite 0-100 value.
|
|
180
|
+
*
|
|
181
|
+
* Weighted toward the playoff window, because a schedule edge in March is
|
|
182
|
+
* worth more than the same edge in November — you only need to win the weeks
|
|
183
|
+
* that eliminate people.
|
|
184
|
+
*/
|
|
185
|
+
scoreTeam(profile, playoffGames, playoffWeekCount) {
|
|
186
|
+
// Playoff-window games per week, normalized against a 4-game week ceiling.
|
|
187
|
+
// With no resolved window this component is neutral, not zero — an
|
|
188
|
+
// unresolved window is missing information, not a bad schedule.
|
|
189
|
+
const playoffRate = playoffWeekCount > 0
|
|
190
|
+
? Math.min(1, (playoffGames / playoffWeekCount) / 4)
|
|
191
|
+
: 0.5;
|
|
192
|
+
// Heavy weeks are streaming and stacking opportunities.
|
|
193
|
+
const heavyWeekRate = Math.min(1, profile.weeks_with_4_plus / 12);
|
|
194
|
+
// Light weeks are dead roster spots.
|
|
195
|
+
const lightWeekPenalty = Math.min(1, profile.weeks_with_2_or_fewer / 8);
|
|
196
|
+
const score = (0.50 * playoffRate) +
|
|
197
|
+
(0.35 * heavyWeekRate) -
|
|
198
|
+
(0.15 * lightWeekPenalty);
|
|
199
|
+
return Math.round(Math.max(0, Math.min(1, score + 0.15)) * 100);
|
|
200
|
+
}
|
|
201
|
+
verdictFor(profile, playoffGames, playoffWeekCount) {
|
|
202
|
+
if (playoffWeekCount === 0) {
|
|
203
|
+
// No playoff window resolved - judge only what is actually known.
|
|
204
|
+
if (profile.weeks_with_4_plus >= 9) {
|
|
205
|
+
return 'Streaming-friendly regular season. Playoff window not resolved.';
|
|
206
|
+
}
|
|
207
|
+
if (profile.weeks_with_2_or_fewer >= 6) {
|
|
208
|
+
return 'Lots of light weeks. Playoff window not resolved.';
|
|
209
|
+
}
|
|
210
|
+
return 'Ordinary regular-season schedule. Playoff window not resolved.';
|
|
211
|
+
}
|
|
212
|
+
const perWeek = playoffGames / playoffWeekCount;
|
|
213
|
+
if (perWeek >= 3.6 && profile.weeks_with_4_plus >= 8) {
|
|
214
|
+
return 'Draft tiebreaker in your favour - heavy all year and heavy when it counts.';
|
|
215
|
+
}
|
|
216
|
+
if (perWeek >= 3.6) {
|
|
217
|
+
return 'Playoff-window asset. Take the tiebreaker here.';
|
|
218
|
+
}
|
|
219
|
+
if (profile.weeks_with_4_plus >= 9) {
|
|
220
|
+
return 'Streaming-friendly regular season, ordinary playoff window.';
|
|
221
|
+
}
|
|
222
|
+
if (perWeek <= 2.8) {
|
|
223
|
+
return 'Light exactly when you need games. Break the tie the other way.';
|
|
224
|
+
}
|
|
225
|
+
return 'Neutral schedule - decide on talent, not games.';
|
|
226
|
+
}
|
|
227
|
+
// ==========================================
|
|
228
|
+
// 🎯 League playoff window
|
|
229
|
+
// ==========================================
|
|
230
|
+
/**
|
|
231
|
+
* Resolve the league's fantasy playoff weeks to real calendar dates.
|
|
232
|
+
*
|
|
233
|
+
* Yahoo gives `start_date` for week 1 and `playoff_start_week` as an index.
|
|
234
|
+
* Fantasy weeks run Monday to Sunday, so week N starts at week 1's Monday
|
|
235
|
+
* plus (N-1) weeks. Explicit args win over league settings, and both win
|
|
236
|
+
* over the fallback.
|
|
237
|
+
*/
|
|
238
|
+
resolvePlayoffWindow(settings, args) {
|
|
239
|
+
const leagueMeta = settings?.fantasy_content?.league?.[0] ?? {};
|
|
240
|
+
const leagueSettings = settings?.fantasy_content?.league?.[1]?.settings?.[0] ?? {};
|
|
241
|
+
// Yahoo's league start_date is the ideal week-1 anchor. When it is
|
|
242
|
+
// unavailable, the NHL season's own first game is the correct fallback:
|
|
243
|
+
// fantasy week 1 begins with the season. Without one of the two, an
|
|
244
|
+
// explicit playoff_start_week would be an index into nothing.
|
|
245
|
+
const startDate = leagueMeta.start_date ?? NHL_SCHEDULE.getSeasonStartDate() ?? undefined;
|
|
246
|
+
const anchorSource = leagueMeta.start_date ? 'Yahoo league start_date' : 'NHL season opener';
|
|
247
|
+
const endWeek = Number(args.playoff_end_week ?? leagueMeta.end_week ?? leagueSettings.end_week ?? 0);
|
|
248
|
+
const playoffStartWeek = Number(args.playoff_start_week ?? leagueSettings.playoff_start_week ?? 0);
|
|
249
|
+
if (!startDate || !playoffStartWeek || !endWeek || playoffStartWeek > endWeek) {
|
|
250
|
+
return {
|
|
251
|
+
resolved: false,
|
|
252
|
+
note: 'No playoff window resolved. Yahoo league settings did not supply ' +
|
|
253
|
+
'playoff_start_week / end_week. Pass playoff_start_week and playoff_end_week ' +
|
|
254
|
+
'explicitly to score your real playoff window; clubs are rated on their ' +
|
|
255
|
+
'regular-season schedule only until then.',
|
|
256
|
+
start: null,
|
|
257
|
+
end: null,
|
|
258
|
+
weeks: []
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const week1Monday = NhlScheduleService.weekStart(startDate);
|
|
262
|
+
const start = NhlScheduleService.addDays(week1Monday, (playoffStartWeek - 1) * DAYS_PER_WEEK);
|
|
263
|
+
const end = NhlScheduleService.addDays(week1Monday, endWeek * DAYS_PER_WEEK - 1);
|
|
264
|
+
const weeks = [];
|
|
265
|
+
for (let week = playoffStartWeek; week <= endWeek; week++) {
|
|
266
|
+
weeks.push(NhlScheduleService.addDays(week1Monday, (week - 1) * DAYS_PER_WEEK));
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
resolved: true,
|
|
270
|
+
source: args.playoff_start_week ? 'explicit argument' : 'Yahoo league settings',
|
|
271
|
+
week_1_anchor: `${week1Monday} (${anchorSource})`,
|
|
272
|
+
playoff_start_week: playoffStartWeek,
|
|
273
|
+
end_week: endWeek,
|
|
274
|
+
start,
|
|
275
|
+
end,
|
|
276
|
+
weeks
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|