@semanticintent/semantic-chirp-intelligence-mcp 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * 🏒 Chirp Intelligence Service
3
+ *
4
+ * Generates contextual hockey chirp commentary based on:
5
+ * - Semantic chirp contracts (intensity, personality, intent)
6
+ * - Tool metadata and semantic identity
7
+ * - Analysis data and context
8
+ * - Governance enforcement (immutability, validation)
9
+ */
10
+ import { GOVERNANCE_MONITOR, validateSemanticChirpContract, auditSemanticContract } from '../domain/governance.js';
11
+ import { CHIRP_STYLES } from '../config/chirp-styles.js';
12
+ import { PERSONALITY_MODES } from '../config/personality-modes.js';
13
+ import { TOOL_METADATA } from '../config/tool-metadata.js';
14
+ export class ChirpIntelligence {
15
+ /**
16
+ * Generate chirp-enhanced analysis results
17
+ *
18
+ * @param toolName - Name of the tool generating the analysis
19
+ * @param originalData - Raw analysis data to enhance
20
+ * @param semanticContract - Chirp parameters with semantic intent
21
+ * @returns Enhanced data with chirp intelligence layer
22
+ */
23
+ static enhance(toolName, originalData, semanticContract) {
24
+ // 🏛️ Governance: Validate semantic contract
25
+ validateSemanticChirpContract(semanticContract, toolName);
26
+ // 🏛️ Governance: Freeze contract to prevent violations
27
+ const frozenContract = Object.freeze({ ...semanticContract });
28
+ // 🔍 Audit immutability enforcement
29
+ auditSemanticContract(frozenContract, toolName, "enforcement");
30
+ // 🛡️ Protected contract with Proxy for runtime enforcement
31
+ const protectedContract = this.createProtectedContract(frozenContract, toolName);
32
+ // If chirp disabled, return original data
33
+ if (protectedContract.enable_chirp === false) {
34
+ return originalData;
35
+ }
36
+ const metadata = TOOL_METADATA[toolName];
37
+ if (!metadata) {
38
+ return originalData;
39
+ }
40
+ const chirpStyle = CHIRP_STYLES[protectedContract.chirp_intensity || 'standard'];
41
+ const personality = PERSONALITY_MODES[protectedContract.personality_mode || 'analytical'];
42
+ return {
43
+ // Original data preserved
44
+ ...originalData,
45
+ // NEW: Chirp Intelligence Layer
46
+ chirp_intelligence: {
47
+ // 🎯 Semantic Anchoring (Rule 1): Use observable semantic property
48
+ tool_identity: metadata.is_ice_engine
49
+ ? metadata.tool_semantic_identity
50
+ : `${toolName} with chirp intelligence`,
51
+ style: chirpStyle.tone,
52
+ personality: personality.voice,
53
+ intensity: protectedContract.chirp_intensity || 'standard',
54
+ semantic_context: metadata.hockey_context,
55
+ // Dynamic chirp based on data
56
+ analysis_chirp: this.generateContextualChirp(toolName, originalData, chirpStyle, personality),
57
+ // Intent-driven one-liner
58
+ intent_summary: this.generateIntentSummary(originalData, personality),
59
+ // Hockey wisdom
60
+ ice_cold_truth: this.generateICETruth(originalData, chirpStyle)
61
+ },
62
+ // Discovery metadata
63
+ metadata: {
64
+ tool_tags: metadata.discovery_tags,
65
+ intent_category: metadata.intent_category,
66
+ chirp_energy: chirpStyle.energy,
67
+ hockey_wisdom_level: "ICE_tier",
68
+ semantic_depth: "enhanced"
69
+ }
70
+ };
71
+ }
72
+ /**
73
+ * Create protected contract with Proxy for immutability enforcement
74
+ */
75
+ static createProtectedContract(frozen, toolName) {
76
+ return new Proxy(frozen, {
77
+ set() {
78
+ GOVERNANCE_MONITOR.trackViolation({
79
+ rule: "Rule 4 - Immutability Protection",
80
+ severity: "error",
81
+ tool_name: toolName,
82
+ violation_type: "attempted_mutation",
83
+ details: "Attempted to set property on immutable ChirpParameters"
84
+ });
85
+ throw new Error('🚨 Semantic contract violation: ChirpParameters are immutable after creation');
86
+ },
87
+ deleteProperty() {
88
+ GOVERNANCE_MONITOR.trackViolation({
89
+ rule: "Rule 4 - Immutability Protection",
90
+ severity: "error",
91
+ tool_name: toolName,
92
+ violation_type: "attempted_property_deletion",
93
+ details: "Attempted to delete property from immutable ChirpParameters"
94
+ });
95
+ throw new Error('🚨 Semantic contract violation: Cannot delete ChirpParameters properties');
96
+ }
97
+ });
98
+ }
99
+ /**
100
+ * Generate contextual chirp based on tool's chirp potential
101
+ */
102
+ static generateContextualChirp(toolName, data, chirpStyle, personality) {
103
+ const metadata = TOOL_METADATA[toolName];
104
+ if (!metadata)
105
+ return "";
106
+ switch (metadata.chirp_potential) {
107
+ case "roster_weaknesses":
108
+ return this.generateRosterChirp(data, chirpStyle, personality);
109
+ case "schedule_domination":
110
+ return this.generateScheduleChirp(data, chirpStyle, personality);
111
+ case "brutal_optimization":
112
+ return this.generateOptimizationChirp(data, chirpStyle, personality);
113
+ case "weekly_performance":
114
+ return this.generateWeeklyPerformanceChirp(data, chirpStyle, personality);
115
+ case "pickup_strategy":
116
+ return this.generatePickupStrategyChirp(data, chirpStyle, personality);
117
+ default:
118
+ return this.generateGenericChirp(data, chirpStyle, personality);
119
+ }
120
+ }
121
+ static generateRosterChirp(data, chirpStyle, personality) {
122
+ const injured = data.roster?.filter((p) => p.status && p.status !== "").length || 0;
123
+ if (injured > 0 && chirpStyle.tone === "brutal_truth") {
124
+ return `${chirpStyle.prefix} you've got ${injured} injured players mucking up your lineup. That's not championship material! ${chirpStyle.suffix}`;
125
+ }
126
+ if (injured > 0 && chirpStyle.tone === "encouraging") {
127
+ return `${chirpStyle.prefix} moving those ${injured} injured players to IR to optimize your roster. ${chirpStyle.suffix}`;
128
+ }
129
+ if (injured > 0 && chirpStyle.tone === "championship_enforcer") {
130
+ return `${chirpStyle.prefix} ${injured} injured players dragging down your roster. Champions handle their IR like pros. ${chirpStyle.suffix}`;
131
+ }
132
+ return `${personality.phrases[0]} your team composition looks solid.`;
133
+ }
134
+ static generateScheduleChirp(data, chirpStyle, personality) {
135
+ const advantage = data.advantage;
136
+ const diff = Math.abs(data.games_in_hand_difference || 0);
137
+ if (advantage === "opponent" && chirpStyle.tone === "brutal_truth") {
138
+ return `${chirpStyle.prefix} your opponent has ${diff} more games than you and you're just sitting there? Time to drop the mittens and get aggressive! ${chirpStyle.suffix}`;
139
+ }
140
+ if (advantage === "you" && chirpStyle.tone === "championship_enforcer") {
141
+ return `${chirpStyle.prefix} You've got ${diff} more games. This is where champions separate from the pretenders. ${chirpStyle.suffix}`;
142
+ }
143
+ if (advantage === "you" && chirpStyle.tone === "direct_honest") {
144
+ return `${chirpStyle.prefix} capitalize on your ${diff}-game advantage ${chirpStyle.suffix}`;
145
+ }
146
+ return `${personality.phrases[0]} the schedule advantage situation.`;
147
+ }
148
+ static generateOptimizationChirp(data, chirpStyle, personality) {
149
+ const criticalIssues = data.immediate_issues || 0;
150
+ const recommendations = data.recommendations?.length || 0;
151
+ if (criticalIssues > 0 && chirpStyle.tone === "brutal_truth") {
152
+ return `${chirpStyle.prefix} you've got ${criticalIssues} critical lineup issues and ${recommendations} ways to fix them. Stop window shopping and start dominating! ${chirpStyle.suffix}`;
153
+ }
154
+ if (criticalIssues === 0 && chirpStyle.tone === "championship_enforcer") {
155
+ return `${chirpStyle.prefix} Your lineup is solid but ICE found ${recommendations} ways to push you over the top. ${chirpStyle.suffix}`;
156
+ }
157
+ if (recommendations > 5 && chirpStyle.tone === "direct_honest") {
158
+ return `${chirpStyle.prefix} execute these ${recommendations} optimizations ${chirpStyle.suffix}`;
159
+ }
160
+ return `${personality.phrases[0]} ${recommendations} optimization opportunities to consider.`;
161
+ }
162
+ static generateWeeklyPerformanceChirp(data, chirpStyle, personality) {
163
+ const yourGames = data.games_in_hand?.your_remaining || 0;
164
+ const oppGames = data.games_in_hand?.opponent_remaining || 0;
165
+ if (yourGames > oppGames && chirpStyle.tone === "championship_enforcer") {
166
+ return `${chirpStyle.prefix} You've got more games left - time to bury them. ${chirpStyle.suffix}`;
167
+ }
168
+ if (yourGames < oppGames && chirpStyle.tone === "brutal_truth") {
169
+ return `${chirpStyle.prefix} they've got more games - every stat matters now! ${chirpStyle.suffix}`;
170
+ }
171
+ return `${personality.phrases[0]} your weekly matchup positioning.`;
172
+ }
173
+ static generatePickupStrategyChirp(data, chirpStyle, personality) {
174
+ const targets = data.streaming_targets?.length || 0;
175
+ const hotTeam = data.market_intelligence?.top_trending_team || "unknown";
176
+ if (targets > 10 && chirpStyle.tone === "championship_enforcer") {
177
+ return `${chirpStyle.prefix} ${targets} targets identified. Focus on ${hotTeam} players for maximum impact. ${chirpStyle.suffix}`;
178
+ }
179
+ if (targets > 10 && chirpStyle.tone === "brutal_truth") {
180
+ return `${chirpStyle.prefix} ${targets} players better than what you've got - are you here to compete or participate? ${chirpStyle.suffix}`;
181
+ }
182
+ return `${personality.phrases[0]} ${targets} streaming opportunities on the wire.`;
183
+ }
184
+ static generateGenericChirp(data, chirpStyle, personality) {
185
+ return `${personality.phrases[0]} the data patterns. ${chirpStyle.prefix} taking action based on these insights. ${chirpStyle.suffix}`;
186
+ }
187
+ static generateIntentSummary(data, personality) {
188
+ switch (personality.focus) {
189
+ case "championship_mindset":
190
+ return "Championship strategy: Execute these moves for league domination";
191
+ case "data_driven":
192
+ return "Statistical analysis: Data-driven recommendations for optimal performance";
193
+ case "entertainment_value":
194
+ return "Bottom line: Time to separate the contenders from the pretenders";
195
+ case "winning_strategy":
196
+ return "Elite strategy: Next-level moves for next-level results";
197
+ default:
198
+ return "Action required: Strategic improvements identified";
199
+ }
200
+ }
201
+ static generateICETruth(data, chirpStyle) {
202
+ if (chirpStyle.tone === "championship_enforcer") {
203
+ return "❄️ ICE Cold Truth: Champions make moves, pretenders make excuses.";
204
+ }
205
+ if (chirpStyle.tone === "brutal_truth") {
206
+ return "🔥 Savage Reality: Your competition isn't waiting - neither should you.";
207
+ }
208
+ if (chirpStyle.tone === "direct_honest") {
209
+ return "💪 Real Talk: Smart players act on good intel.";
210
+ }
211
+ return "🧠 Smart Play: Optimal decisions lead to optimal results.";
212
+ }
213
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * 🏒 Yahoo Fantasy API Client Service
3
+ *
4
+ * Handles all Yahoo Fantasy Sports API interactions including:
5
+ * - OAuth token management (load, save, refresh)
6
+ * - Authenticated API requests
7
+ * - Token expiration handling
8
+ * - Error handling and retries
9
+ */
10
+ import fs from 'fs';
11
+ import https from 'https';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ export class YahooApiClient {
17
+ cachedToken = null;
18
+ tokenFile;
19
+ apiBase;
20
+ clientId;
21
+ clientSecret;
22
+ constructor(clientId, clientSecret, apiBase = "https://fantasysports.yahooapis.com/fantasy/v2") {
23
+ this.clientId = clientId;
24
+ this.clientSecret = clientSecret;
25
+ this.apiBase = apiBase;
26
+ this.tokenFile = path.join(__dirname, "..", "..", ".yahoo-oauth.json");
27
+ }
28
+ /**
29
+ * Load OAuth token from file system
30
+ */
31
+ loadToken() {
32
+ try {
33
+ console.error(`[DEBUG] Looking for token at: ${this.tokenFile}`);
34
+ if (fs.existsSync(this.tokenFile)) {
35
+ const tokenData = fs.readFileSync(this.tokenFile, "utf8");
36
+ const token = JSON.parse(tokenData);
37
+ console.error(`[DEBUG] Token loaded successfully`);
38
+ this.cachedToken = token;
39
+ return token;
40
+ }
41
+ else {
42
+ console.error(`[DEBUG] Token file not found`);
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error("[ERROR] Error loading token:", error);
47
+ }
48
+ return null;
49
+ }
50
+ /**
51
+ * Save OAuth token to file system
52
+ */
53
+ saveToken(token) {
54
+ token.expires_at = Date.now() + (token.expires_in * 1000);
55
+ fs.writeFileSync(this.tokenFile, JSON.stringify(token, null, 2));
56
+ this.cachedToken = token;
57
+ console.error("[DEBUG] Token saved successfully");
58
+ }
59
+ /**
60
+ * Refresh expired OAuth token
61
+ */
62
+ async refreshAccessToken() {
63
+ const token = this.cachedToken || this.loadToken();
64
+ if (!token) {
65
+ throw new Error("No refresh token available. Please re-authenticate.");
66
+ }
67
+ console.error("[DEBUG] Refreshing access token...");
68
+ const tokenData = new URLSearchParams({
69
+ client_id: this.clientId,
70
+ client_secret: this.clientSecret,
71
+ redirect_uri: "oob",
72
+ refresh_token: token.refresh_token,
73
+ grant_type: "refresh_token",
74
+ });
75
+ const options = {
76
+ hostname: "api.login.yahoo.com",
77
+ port: 443,
78
+ path: "/oauth2/get_token",
79
+ method: "POST",
80
+ headers: {
81
+ "Content-Type": "application/x-www-form-urlencoded",
82
+ "Content-Length": tokenData.toString().length,
83
+ },
84
+ };
85
+ return new Promise((resolve, reject) => {
86
+ const req = https.request(options, (res) => {
87
+ let data = "";
88
+ res.on("data", (chunk) => {
89
+ data += chunk;
90
+ });
91
+ res.on("end", () => {
92
+ try {
93
+ const newToken = JSON.parse(data);
94
+ this.saveToken(newToken);
95
+ console.error("[DEBUG] Token refreshed successfully");
96
+ resolve(newToken.access_token);
97
+ }
98
+ catch (error) {
99
+ reject(new Error(`Failed to parse token response: ${error}`));
100
+ }
101
+ });
102
+ });
103
+ req.on("error", (error) => {
104
+ reject(error);
105
+ });
106
+ req.write(tokenData.toString());
107
+ req.end();
108
+ });
109
+ }
110
+ /**
111
+ * Get valid access token, refreshing if necessary
112
+ */
113
+ async getValidAccessToken() {
114
+ const token = this.cachedToken || this.loadToken();
115
+ if (!token) {
116
+ throw new Error("No authentication token found! Run: node authenticate.js");
117
+ }
118
+ // Check if token is expired (with 5 minute buffer)
119
+ if (token.expires_at && token.expires_at < Date.now() + 300000) {
120
+ console.error("[DEBUG] Token expired or expiring soon, refreshing...");
121
+ return await this.refreshAccessToken();
122
+ }
123
+ return token.access_token;
124
+ }
125
+ /**
126
+ * Make authenticated request to Yahoo Fantasy API
127
+ *
128
+ * @param endpoint - API endpoint path (e.g., "/team/nhl.l.12345.t.1/roster")
129
+ * @param format - Response format (default: "json")
130
+ * @returns Parsed API response
131
+ */
132
+ async request(endpoint, format = "json") {
133
+ const accessToken = await this.getValidAccessToken();
134
+ const url = `${this.apiBase}${endpoint}${endpoint.includes('?') ? '&' : '?'}format=${format}`;
135
+ console.error(`[DEBUG] API Request: ${endpoint}`);
136
+ return new Promise((resolve, reject) => {
137
+ const urlObj = new URL(url);
138
+ const options = {
139
+ hostname: urlObj.hostname,
140
+ port: 443,
141
+ path: urlObj.pathname + urlObj.search,
142
+ method: "GET",
143
+ headers: {
144
+ Authorization: `Bearer ${accessToken}`,
145
+ Accept: "application/json",
146
+ },
147
+ };
148
+ const req = https.request(options, (res) => {
149
+ let data = "";
150
+ res.on("data", (chunk) => {
151
+ data += chunk;
152
+ });
153
+ res.on("end", () => {
154
+ if (res.statusCode === 401) {
155
+ // Token expired, try to refresh and retry
156
+ this.refreshAccessToken()
157
+ .then(() => this.request(endpoint, format))
158
+ .then(resolve)
159
+ .catch(reject);
160
+ return;
161
+ }
162
+ if (res.statusCode !== 200) {
163
+ reject(new Error(`API returned status ${res.statusCode}: ${data}`));
164
+ return;
165
+ }
166
+ try {
167
+ const parsed = JSON.parse(data);
168
+ resolve(parsed);
169
+ }
170
+ catch (error) {
171
+ reject(new Error(`Failed to parse API response: ${error}`));
172
+ }
173
+ });
174
+ });
175
+ req.on("error", (error) => {
176
+ reject(error);
177
+ });
178
+ req.end();
179
+ });
180
+ }
181
+ /**
182
+ * Helper: Strip nhl.l. prefix from league ID if present
183
+ */
184
+ stripLeaguePrefix(leagueId) {
185
+ return leagueId.replace(/^nhl\.l\./, '');
186
+ }
187
+ /**
188
+ * Helper: Strip team number from full team ID (nhl.l.12345.t.6 -> 6)
189
+ */
190
+ extractTeamNumber(teamId) {
191
+ const match = teamId.match(/\.t\.(\d+)$/);
192
+ return match ? match[1] : teamId.replace(/^.*\.t\./, '');
193
+ }
194
+ /**
195
+ * Convenience method: Get team roster
196
+ */
197
+ async getTeamRoster(leagueId, teamId) {
198
+ const cleanLeagueId = this.stripLeaguePrefix(leagueId);
199
+ const cleanTeamId = this.extractTeamNumber(teamId);
200
+ return this.request(`/team/nhl.l.${cleanLeagueId}.t.${cleanTeamId}/roster`);
201
+ }
202
+ /**
203
+ * Convenience method: Get league standings
204
+ */
205
+ async getLeagueStandings(leagueId) {
206
+ const cleanLeagueId = this.stripLeaguePrefix(leagueId);
207
+ return this.request(`/league/nhl.l.${cleanLeagueId}/standings`);
208
+ }
209
+ /**
210
+ * Convenience method: Get team matchup
211
+ */
212
+ async getTeamMatchup(leagueId, teamId, week) {
213
+ const cleanLeagueId = this.stripLeaguePrefix(leagueId);
214
+ const cleanTeamId = this.extractTeamNumber(teamId);
215
+ const weekParam = week ? `;week=${week}` : '';
216
+ return this.request(`/team/nhl.l.${cleanLeagueId}.t.${cleanTeamId}/matchups${weekParam}`);
217
+ }
218
+ /**
219
+ * Convenience method: Get league scoreboard
220
+ */
221
+ async getLeagueScoreboard(leagueId, week) {
222
+ const cleanLeagueId = this.stripLeaguePrefix(leagueId);
223
+ const weekParam = week ? `;week=${week}` : '';
224
+ return this.request(`/league/nhl.l.${cleanLeagueId}/scoreboard${weekParam}`);
225
+ }
226
+ /**
227
+ * Convenience method: Get team stats
228
+ */
229
+ async getTeamStats(leagueId, teamId) {
230
+ const cleanLeagueId = this.stripLeaguePrefix(leagueId);
231
+ const cleanTeamId = this.extractTeamNumber(teamId);
232
+ return this.request(`/team/nhl.l.${cleanLeagueId}.t.${cleanTeamId}/stats`);
233
+ }
234
+ /**
235
+ * Convenience method: Get league settings
236
+ */
237
+ async getLeagueSettings(leagueId) {
238
+ return this.request(`/league/nhl.l.${leagueId}/settings`);
239
+ }
240
+ /**
241
+ * Convenience method: Get players (for search, trending, etc.)
242
+ */
243
+ async getPlayers(leagueId, queryParams = '') {
244
+ return this.request(`/league/nhl.l.${leagueId}/players${queryParams}`);
245
+ }
246
+ /**
247
+ * Convenience method: Search players by position
248
+ */
249
+ async searchPlayers(position, count = 25, leagueId) {
250
+ const league = leagueId || process.env.YAHOO_LEAGUE_ID;
251
+ const cleanLeagueId = this.stripLeaguePrefix(league);
252
+ let queryParams = `;status=A;count=${count}`;
253
+ if (position) {
254
+ queryParams += `;position=${position}`;
255
+ }
256
+ const data = await this.request(`/league/nhl.l.${cleanLeagueId}/players${queryParams}`);
257
+ // Parse player data
258
+ const playersData = data.fantasy_content.league[1].players;
259
+ const players = Object.keys(playersData)
260
+ .filter(key => key !== 'count')
261
+ .map(key => {
262
+ const playerData = playersData[key].player[0];
263
+ const playerId = playerData.find((item) => item.player_id)?.player_id;
264
+ const name = playerData.find((item) => item.name)?.name?.full;
265
+ const displayPosition = playerData.find((item) => item.display_position)?.display_position;
266
+ const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
267
+ const percentOwned = playerData.find((item) => item.percent_owned)?.percent_owned?.value || "0";
268
+ return {
269
+ player_id: playerId,
270
+ name: name,
271
+ position: displayPosition,
272
+ team: team,
273
+ percent_owned: parseFloat(percentOwned),
274
+ selected_position: []
275
+ };
276
+ });
277
+ return { players };
278
+ }
279
+ /**
280
+ * Convenience method: Get trending players
281
+ */
282
+ async getTrendingPlayers(trendType = 'add', count = 25, leagueId) {
283
+ const league = leagueId || process.env.YAHOO_LEAGUE_ID;
284
+ const cleanLeagueId = this.stripLeaguePrefix(league);
285
+ const sortParam = trendType === 'add' ? 'AR' : 'OR';
286
+ const data = await this.request(`/league/nhl.l.${cleanLeagueId}/players;status=A;sort=${sortParam};count=${count}`);
287
+ const playersData = data.fantasy_content.league[1].players;
288
+ const players = Object.keys(playersData)
289
+ .filter(key => key !== 'count')
290
+ .map(key => {
291
+ const playerData = playersData[key].player[0];
292
+ const playerId = playerData.find((item) => item.player_id)?.player_id;
293
+ const name = playerData.find((item) => item.name)?.name?.full;
294
+ const displayPosition = playerData.find((item) => item.display_position)?.display_position;
295
+ const team = playerData.find((item) => item.editorial_team_abbr)?.editorial_team_abbr;
296
+ const percentOwned = playerData.find((item) => item.percent_owned)?.percent_owned?.value || "0";
297
+ return {
298
+ player_id: playerId,
299
+ name: name,
300
+ position: displayPosition,
301
+ team: team,
302
+ percent_owned: parseFloat(percentOwned),
303
+ trending: trendType,
304
+ selected_position: []
305
+ };
306
+ });
307
+ return { players, trend_type: trendType };
308
+ }
309
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * 🏛️ Analysis Template Base Class
3
+ *
4
+ * Implements the Template Method Pattern with Semantic Anchoring Governance.
5
+ *
6
+ * This abstract class defines the skeleton algorithm for all fantasy hockey analyses,
7
+ * allowing concrete implementations to customize specific steps while preserving
8
+ * the overall structure and governance enforcement.
9
+ *
10
+ * 🎯 Design Pattern: Template Method
11
+ * - Fixed algorithm structure (executeAnalysis)
12
+ * - Customizable hook methods (abstract methods)
13
+ * - Semantic governance integrated at architecture level
14
+ *
15
+ * 🏛️ Governance Integration:
16
+ * - Rule 1 (Semantic Over Structural): Analysis driven by semantic contracts
17
+ * - Rule 2 (Intent Preservation): Chirp parameters validated and preserved
18
+ * - Rule 3 (Observable Anchoring): Metadata provides semantic identity
19
+ * - Rule 4 (Immutability): Results frozen before return
20
+ */
21
+ import { GOVERNANCE_MONITOR, validateSemanticChirpContract } from '../domain/governance.js';
22
+ import { TOOL_METADATA } from '../config/tool-metadata.js';
23
+ /**
24
+ * Abstract base class for all fantasy hockey analyses
25
+ */
26
+ export class AnalysisTemplate {
27
+ toolName;
28
+ analysisType;
29
+ constructor(toolName, analysisType) {
30
+ this.toolName = toolName;
31
+ this.analysisType = analysisType;
32
+ }
33
+ /**
34
+ * 🎯 TEMPLATE METHOD: Main algorithm with fixed structure
35
+ *
36
+ * This method defines the invariant steps that all analyses must follow:
37
+ * 1. Validate semantic contract (governance)
38
+ * 2. Fetch and prepare data
39
+ * 3. Execute domain-specific analysis
40
+ * 4. Generate chirp intelligence
41
+ * 5. Format and freeze results
42
+ *
43
+ * Concrete classes CANNOT override this method - they implement the hooks.
44
+ */
45
+ async executeAnalysis(args, semanticContract) {
46
+ const startTime = Date.now();
47
+ // 🏛️ Governance Step 1: Validate semantic contract
48
+ this.validateContract(semanticContract);
49
+ // Track analysis start
50
+ GOVERNANCE_MONITOR.trackAnalysisStart(this.analysisType);
51
+ try {
52
+ // Step 2: Fetch data (hook method)
53
+ const rawData = await this.fetchData(args);
54
+ // Step 3: Prepare data for analysis (hook method)
55
+ const preparedData = await this.prepareData(rawData, args);
56
+ // Step 4: Execute core analysis logic (hook method)
57
+ const analysisResults = await this.analyzeData(preparedData, args);
58
+ // Step 5: Generate chirp intelligence (hook method)
59
+ const chirpEnhanced = await this.generateChirp(analysisResults, semanticContract, preparedData);
60
+ // Step 6: Format response (hook method)
61
+ const response = await this.formatResponse(chirpEnhanced, preparedData);
62
+ // 🏛️ Governance Step 7: Freeze response (immutability)
63
+ const frozenResponse = this.freezeResponse(response);
64
+ // Track analysis completion
65
+ const duration = Date.now() - startTime;
66
+ GOVERNANCE_MONITOR.trackAnalysisComplete(this.analysisType, duration);
67
+ return frozenResponse;
68
+ }
69
+ catch (error) {
70
+ // Track failed analysis
71
+ const duration = Date.now() - startTime;
72
+ GOVERNANCE_MONITOR.trackAnalysisComplete(this.analysisType, duration);
73
+ throw error;
74
+ }
75
+ }
76
+ // ==========================================
77
+ // 🛡️ GOVERNANCE METHODS (Concrete - Shared)
78
+ // ==========================================
79
+ /**
80
+ * Validate semantic contract and track governance
81
+ */
82
+ validateContract(contract) {
83
+ // 🏛️ Rule 2: Intent Preservation
84
+ // Note: validateSemanticChirpContract internally calls auditSemanticContract
85
+ validateSemanticChirpContract(contract, this.toolName);
86
+ }
87
+ /**
88
+ * Freeze response to enforce immutability
89
+ *
90
+ * 🏛️ Rule 4: Immutability Protection
91
+ */
92
+ freezeResponse(response) {
93
+ // Deep freeze the response object
94
+ Object.freeze(response);
95
+ if (response.analysis_insights) {
96
+ Object.freeze(response.analysis_insights);
97
+ }
98
+ if (response.recommendations) {
99
+ response.recommendations.forEach((rec) => Object.freeze(rec));
100
+ Object.freeze(response.recommendations);
101
+ }
102
+ if (response.chirp_intelligence) {
103
+ Object.freeze(response.chirp_intelligence);
104
+ }
105
+ if (response.metadata) {
106
+ Object.freeze(response.metadata);
107
+ }
108
+ GOVERNANCE_MONITOR.immutability_enforced++;
109
+ return response;
110
+ }
111
+ /**
112
+ * Get tool metadata for semantic decisions
113
+ *
114
+ * 🏛️ Rule 3: Observable Anchoring
115
+ */
116
+ getToolMetadata() {
117
+ return TOOL_METADATA[this.toolName] || {};
118
+ }
119
+ /**
120
+ * Check if this is an ICE (Intent Chirp Engine) tool
121
+ *
122
+ * 🏛️ Rule 1: Semantic Over Structural
123
+ * Uses observable property instead of string comparison
124
+ */
125
+ isIceTool() {
126
+ const metadata = this.getToolMetadata();
127
+ return metadata.is_ice_engine === true;
128
+ }
129
+ /**
130
+ * Get semantic defaults for this tool
131
+ */
132
+ getSemanticDefaults() {
133
+ const metadata = this.getToolMetadata();
134
+ // ICE tools default to ice_cold intensity
135
+ if (this.isIceTool()) {
136
+ return {
137
+ chirp_intensity: "ice_cold",
138
+ personality_mode: "championship_coach",
139
+ enable_chirp: true,
140
+ semantic_intent: "system_default"
141
+ };
142
+ }
143
+ // Standard tools use standard intensity
144
+ return {
145
+ chirp_intensity: "standard",
146
+ personality_mode: "analytical",
147
+ enable_chirp: true,
148
+ semantic_intent: "system_default"
149
+ };
150
+ }
151
+ /**
152
+ * Merge user contract with semantic defaults
153
+ *
154
+ * 🏛️ Rule 2: Intent Preservation
155
+ * User-provided values take precedence over defaults
156
+ */
157
+ mergeContractWithDefaults(userContract) {
158
+ const defaults = this.getSemanticDefaults();
159
+ return {
160
+ ...defaults,
161
+ ...userContract,
162
+ // If user explicitly set values, preserve semantic intent
163
+ semantic_intent: userContract.semantic_intent ||
164
+ (Object.keys(userContract).length > 0 ? "user_requested" : "system_default")
165
+ };
166
+ }
167
+ }