@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.
@@ -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
- apiClient;
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
- // 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
- ]);
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: rosterData,
39
- gamesInHand: gamesInHandData,
40
- streaming: streamingData,
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
- // 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
- });
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
- * Temporary stub for games in hand (to be migrated to its own analysis)
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
- async fetchGamesInHand() {
293
- // TODO: Replace with proper GamesInHandAnalysis when migrated
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
- games_in_hand_difference: 0,
296
- your_remaining: 0,
297
- opponent_remaining: 0
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
- * Temporary stub for streaming recommendations (to be migrated)
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
- async fetchStreamingRecommendations(lookAheadDays) {
304
- // TODO: Replace with proper StreamingAnalysis when migrated
316
+ streamingContext() {
305
317
  return {
306
318
  streaming_targets: [],
307
- optimal_timing: {
308
- best_days: [],
309
- avoid_days: []
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
- apiClient;
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
- 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
- }
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: Fetch today's NHL schedule from public API
228
- * Returns array of games for efficient lookup
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
- 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
- }
223
+ await NHL_SCHEDULE.load();
224
+ return [];
245
225
  }
246
226
  /**
247
- * Helper: Check if player has a game today
248
- * Uses pre-fetched NHL schedule for efficiency
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, todayGames) {
251
- if (!player.team || !todayGames.length)
235
+ hasGameToday(player, _todayGames) {
236
+ if (!player.team || !NHL_SCHEDULE.isAvailable())
252
237
  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
- });
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
+ }