@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.
- package/.env.example +8 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/authenticate.js +207 -0
- package/build/analyses/BreakoutAnalysis.js +386 -0
- package/build/analyses/GamesInHandAnalysis.js +257 -0
- package/build/analyses/IceAnalysis.js +316 -0
- package/build/analyses/LineupAnalysis.js +284 -0
- package/build/analyses/StreamingAnalysis.js +246 -0
- package/build/analyses/WeekendStreamAnalysis.js +599 -0
- package/build/config/chirp-styles.js +36 -0
- package/build/config/personality-modes.js +36 -0
- package/build/config/tool-metadata.js +113 -0
- package/build/domain/governance.js +322 -0
- package/build/domain/types.js +14 -0
- package/build/experimental/semantic-breakout-tool.js +188 -0
- package/build/experimental/semantic-intent-parser.js +222 -0
- package/build/experimental/semantic-tool-integration.js +146 -0
- package/build/experimental/test-parser.js +61 -0
- package/build/index.js +1549 -0
- package/build/services/ChirpIntelligence.js +213 -0
- package/build/services/YahooApiClient.js +309 -0
- package/build/template/AnalysisTemplate.js +167 -0
- package/build/types.js +2 -0
- package/package.json +50 -0
|
@@ -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
|
+
}
|