baseguard 1.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/.eslintrc.json +25 -0
- package/.prettierrc +8 -0
- package/README.md +94 -0
- package/bin/base.js +494 -0
- package/dist/ai/fix-manager.d.ts +67 -0
- package/dist/ai/fix-manager.d.ts.map +1 -0
- package/dist/ai/fix-manager.js +326 -0
- package/dist/ai/fix-manager.js.map +1 -0
- package/dist/ai/gemini-analyzer.d.ts +116 -0
- package/dist/ai/gemini-analyzer.d.ts.map +1 -0
- package/dist/ai/gemini-analyzer.js +572 -0
- package/dist/ai/gemini-analyzer.js.map +1 -0
- package/dist/ai/index.d.ts +4 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +5 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/jules-implementer.d.ts +115 -0
- package/dist/ai/jules-implementer.d.ts.map +1 -0
- package/dist/ai/jules-implementer.js +387 -0
- package/dist/ai/jules-implementer.js.map +1 -0
- package/dist/commands/automation.d.ts +5 -0
- package/dist/commands/automation.d.ts.map +1 -0
- package/dist/commands/automation.js +305 -0
- package/dist/commands/automation.js.map +1 -0
- package/dist/commands/check.d.ts +9 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +113 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +324 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/fix.d.ts +9 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +207 -0
- package/dist/commands/fix.js.map +1 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +125 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/core/api-key-manager.d.ts +83 -0
- package/dist/core/api-key-manager.d.ts.map +1 -0
- package/dist/core/api-key-manager.js +244 -0
- package/dist/core/api-key-manager.js.map +1 -0
- package/dist/core/baseguard.d.ts +46 -0
- package/dist/core/baseguard.d.ts.map +1 -0
- package/dist/core/baseguard.js +132 -0
- package/dist/core/baseguard.js.map +1 -0
- package/dist/core/baseline-checker.d.ts +63 -0
- package/dist/core/baseline-checker.d.ts.map +1 -0
- package/dist/core/baseline-checker.js +502 -0
- package/dist/core/baseline-checker.js.map +1 -0
- package/dist/core/cache-manager.d.ts +88 -0
- package/dist/core/cache-manager.d.ts.map +1 -0
- package/dist/core/cache-manager.js +213 -0
- package/dist/core/cache-manager.js.map +1 -0
- package/dist/core/configuration.d.ts +140 -0
- package/dist/core/configuration.d.ts.map +1 -0
- package/dist/core/configuration.js +474 -0
- package/dist/core/configuration.js.map +1 -0
- package/dist/core/directory-filter.d.ts +90 -0
- package/dist/core/directory-filter.d.ts.map +1 -0
- package/dist/core/directory-filter.js +319 -0
- package/dist/core/directory-filter.js.map +1 -0
- package/dist/core/error-handler.d.ts +110 -0
- package/dist/core/error-handler.d.ts.map +1 -0
- package/dist/core/error-handler.js +392 -0
- package/dist/core/error-handler.js.map +1 -0
- package/dist/core/file-processor.d.ts +80 -0
- package/dist/core/file-processor.d.ts.map +1 -0
- package/dist/core/file-processor.js +259 -0
- package/dist/core/file-processor.js.map +1 -0
- package/dist/core/gitignore-manager.d.ts +44 -0
- package/dist/core/gitignore-manager.d.ts.map +1 -0
- package/dist/core/gitignore-manager.js +147 -0
- package/dist/core/gitignore-manager.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +13 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lazy-loader.d.ts +68 -0
- package/dist/core/lazy-loader.d.ts.map +1 -0
- package/dist/core/lazy-loader.js +260 -0
- package/dist/core/lazy-loader.js.map +1 -0
- package/dist/core/memory-manager.d.ts +1 -0
- package/dist/core/memory-manager.d.ts.map +1 -0
- package/dist/core/memory-manager.js +2 -0
- package/dist/core/memory-manager.js.map +1 -0
- package/dist/core/startup-optimizer.d.ts +45 -0
- package/dist/core/startup-optimizer.d.ts.map +1 -0
- package/dist/core/startup-optimizer.js +140 -0
- package/dist/core/startup-optimizer.js.map +1 -0
- package/dist/git/automation-engine.d.ts +58 -0
- package/dist/git/automation-engine.d.ts.map +1 -0
- package/dist/git/automation-engine.js +318 -0
- package/dist/git/automation-engine.js.map +1 -0
- package/dist/git/github-manager.d.ts +71 -0
- package/dist/git/github-manager.d.ts.map +1 -0
- package/dist/git/github-manager.js +226 -0
- package/dist/git/github-manager.js.map +1 -0
- package/dist/git/hook-manager.d.ts +43 -0
- package/dist/git/hook-manager.d.ts.map +1 -0
- package/dist/git/hook-manager.js +191 -0
- package/dist/git/hook-manager.js.map +1 -0
- package/dist/git/index.d.ts +4 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +5 -0
- package/dist/git/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/feature-validator.d.ts +60 -0
- package/dist/parsers/feature-validator.d.ts.map +1 -0
- package/dist/parsers/feature-validator.js +483 -0
- package/dist/parsers/feature-validator.js.map +1 -0
- package/dist/parsers/index.d.ts +8 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +9 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/parser-manager.d.ts +103 -0
- package/dist/parsers/parser-manager.d.ts.map +1 -0
- package/dist/parsers/parser-manager.js +321 -0
- package/dist/parsers/parser-manager.js.map +1 -0
- package/dist/parsers/parser.d.ts +23 -0
- package/dist/parsers/parser.d.ts.map +1 -0
- package/dist/parsers/parser.js +6 -0
- package/dist/parsers/parser.js.map +1 -0
- package/dist/parsers/react-parser.d.ts +22 -0
- package/dist/parsers/react-parser.d.ts.map +1 -0
- package/dist/parsers/react-parser.js +307 -0
- package/dist/parsers/react-parser.js.map +1 -0
- package/dist/parsers/svelte-parser.d.ts +33 -0
- package/dist/parsers/svelte-parser.d.ts.map +1 -0
- package/dist/parsers/svelte-parser.js +408 -0
- package/dist/parsers/svelte-parser.js.map +1 -0
- package/dist/parsers/vanilla-parser.d.ts +31 -0
- package/dist/parsers/vanilla-parser.d.ts.map +1 -0
- package/dist/parsers/vanilla-parser.js +590 -0
- package/dist/parsers/vanilla-parser.js.map +1 -0
- package/dist/parsers/vue-parser.d.ts +9 -0
- package/dist/parsers/vue-parser.d.ts.map +1 -0
- package/dist/parsers/vue-parser.js +16 -0
- package/dist/parsers/vue-parser.js.map +1 -0
- package/dist/terminal-header.d.ts +12 -0
- package/dist/terminal-header.js +45 -0
- package/dist/types/index.d.ts +83 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/components.d.ts +133 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +482 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/help.d.ts +11 -0
- package/dist/ui/help.d.ts.map +1 -0
- package/dist/ui/help.js +161 -0
- package/dist/ui/help.js.map +1 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +5 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/prompts.d.ts +63 -0
- package/dist/ui/prompts.d.ts.map +1 -0
- package/dist/ui/prompts.js +611 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/ui/terminal-header.d.ts +13 -0
- package/dist/ui/terminal-header.d.ts.map +1 -0
- package/dist/ui/terminal-header.js +46 -0
- package/dist/ui/terminal-header.js.map +1 -0
- package/package.json +80 -0
- package/src/ai/__tests__/gemini-analyzer.test.ts +181 -0
- package/src/ai/fix-manager.ts +362 -0
- package/src/ai/gemini-analyzer.ts +671 -0
- package/src/ai/index.ts +4 -0
- package/src/ai/jules-implementer.ts +459 -0
- package/src/commands/automation.ts +344 -0
- package/src/commands/check.ts +299 -0
- package/src/commands/config.ts +365 -0
- package/src/commands/fix.ts +234 -0
- package/src/commands/index.ts +6 -0
- package/src/commands/init.ts +142 -0
- package/src/commands/status.ts +0 -0
- package/src/core/api-key-manager.ts +298 -0
- package/src/core/baseguard.ts +742 -0
- package/src/core/baseline-checker.ts +563 -0
- package/src/core/cache-manager.ts +270 -0
- package/src/core/configuration-recovery.ts +676 -0
- package/src/core/configuration.ts +559 -0
- package/src/core/debug-logger.ts +590 -0
- package/src/core/directory-filter.ts +421 -0
- package/src/core/error-handler.ts +517 -0
- package/src/core/file-processor.ts +331 -0
- package/src/core/gitignore-manager.ts +169 -0
- package/src/core/graceful-degradation-manager.ts +596 -0
- package/src/core/index.ts +13 -0
- package/src/core/lazy-loader.ts +307 -0
- package/src/core/logger.ts +0 -0
- package/src/core/memory-manager.ts +294 -0
- package/src/core/startup-optimizer.ts +173 -0
- package/src/core/system-error-handler.ts +746 -0
- package/src/git/automation-engine.ts +361 -0
- package/src/git/github-manager.ts +260 -0
- package/src/git/hook-manager.ts +210 -0
- package/src/git/index.ts +4 -0
- package/src/index.ts +8 -0
- package/src/parsers/feature-validator.ts +559 -0
- package/src/parsers/index.ts +8 -0
- package/src/parsers/parser-manager.ts +419 -0
- package/src/parsers/parser.ts +26 -0
- package/src/parsers/react-parser-optimized.ts +161 -0
- package/src/parsers/react-parser.ts +359 -0
- package/src/parsers/svelte-parser.ts +506 -0
- package/src/parsers/vanilla-parser.ts +682 -0
- package/src/parsers/vue-parser.ts +472 -0
- package/src/types/index.ts +92 -0
- package/src/ui/components.ts +567 -0
- package/src/ui/help.ts +193 -0
- package/src/ui/index.ts +4 -0
- package/src/ui/prompts.ts +688 -0
- package/src/ui/terminal-header.ts +59 -0
- package/test-config-commands.js +56 -0
- package/test-header-simple.js +33 -0
- package/test-terminal-header.js +12 -0
- package/test-ui.js +29 -0
- package/tests/e2e/baseguard.e2e.test.ts +516 -0
- package/tests/e2e/cross-platform.e2e.test.ts +420 -0
- package/tests/e2e/git-integration.e2e.test.ts +487 -0
- package/tests/fixtures/react-project/package.json +14 -0
- package/tests/fixtures/react-project/src/App.css +76 -0
- package/tests/fixtures/react-project/src/App.tsx +77 -0
- package/tests/fixtures/svelte-project/package.json +11 -0
- package/tests/fixtures/svelte-project/src/App.svelte +369 -0
- package/tests/fixtures/vanilla-project/index.html +76 -0
- package/tests/fixtures/vanilla-project/script.js +331 -0
- package/tests/fixtures/vanilla-project/styles.css +359 -0
- package/tests/fixtures/vue-project/package.json +12 -0
- package/tests/fixtures/vue-project/src/App.vue +216 -0
- package/tsconfig.json +36 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import type { Violation, Analysis } from '../types/index.js';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { ErrorHandler, APIError, ErrorType } from '../core/error-handler.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cache entry for analysis results
|
|
7
|
+
*/
|
|
8
|
+
interface CacheEntry {
|
|
9
|
+
analysis: Analysis;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
ttl: number; // Time to live in milliseconds
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gemini AI analyzer for compatibility violations
|
|
16
|
+
*/
|
|
17
|
+
export class GeminiAnalyzer {
|
|
18
|
+
private apiKey: string;
|
|
19
|
+
private baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent';
|
|
20
|
+
private cache = new Map<string, CacheEntry>();
|
|
21
|
+
private readonly cacheTtl = 24 * 60 * 60 * 1000; // 24 hours
|
|
22
|
+
|
|
23
|
+
constructor(apiKey: string) {
|
|
24
|
+
this.apiKey = apiKey;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Analyze a violation using Gemini AI with web search
|
|
29
|
+
*/
|
|
30
|
+
async analyzeViolation(violation: Violation): Promise<Analysis> {
|
|
31
|
+
// Check cache first
|
|
32
|
+
const cacheKey = this.generateCacheKey(violation);
|
|
33
|
+
const cachedResult = this.getCachedAnalysis(cacheKey);
|
|
34
|
+
|
|
35
|
+
if (cachedResult) {
|
|
36
|
+
console.log(`Using cached analysis for ${violation.feature}`);
|
|
37
|
+
return cachedResult;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const context = ErrorHandler.createContext('gemini_analysis', {
|
|
41
|
+
feature: violation.feature,
|
|
42
|
+
browser: violation.browser,
|
|
43
|
+
file: violation.file
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const prompt = this.buildAnalysisPrompt(violation);
|
|
48
|
+
|
|
49
|
+
const response = await ErrorHandler.withRetry(
|
|
50
|
+
() => this.makeApiCall(prompt),
|
|
51
|
+
{
|
|
52
|
+
maxRetries: 2,
|
|
53
|
+
retryableErrors: [ErrorType.NETWORK, ErrorType.TIMEOUT, ErrorType.RATE_LIMIT, ErrorType.SERVER_ERROR]
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
|
|
59
|
+
// Validate response structure
|
|
60
|
+
ErrorHandler.validateAPIResponse(data, ['candidates']);
|
|
61
|
+
|
|
62
|
+
if (!data.candidates || data.candidates.length === 0) {
|
|
63
|
+
throw new APIError(
|
|
64
|
+
'No analysis candidates returned from Gemini API',
|
|
65
|
+
ErrorType.VALIDATION,
|
|
66
|
+
{
|
|
67
|
+
suggestions: [
|
|
68
|
+
'Try rephrasing the analysis request',
|
|
69
|
+
'Check if the feature is supported by Gemini',
|
|
70
|
+
'Try again with a different violation'
|
|
71
|
+
],
|
|
72
|
+
context
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const candidate = data.candidates[0];
|
|
78
|
+
const content = candidate.content?.parts?.[0]?.text;
|
|
79
|
+
const groundingMetadata = candidate.groundingMetadata;
|
|
80
|
+
|
|
81
|
+
if (!content) {
|
|
82
|
+
throw new APIError(
|
|
83
|
+
'Empty analysis content returned from Gemini API',
|
|
84
|
+
ErrorType.VALIDATION,
|
|
85
|
+
{
|
|
86
|
+
suggestions: [
|
|
87
|
+
'Try the analysis again',
|
|
88
|
+
'Check if the violation data is complete',
|
|
89
|
+
'Verify API key permissions'
|
|
90
|
+
],
|
|
91
|
+
context
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const analysis = this.parseAnalysisResponse(content, groundingMetadata, violation);
|
|
97
|
+
|
|
98
|
+
// Cache the result
|
|
99
|
+
this.cacheAnalysis(cacheKey, analysis);
|
|
100
|
+
|
|
101
|
+
return analysis;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const apiError = ErrorHandler.handleAPIError(error, context);
|
|
104
|
+
|
|
105
|
+
// Log error for debugging
|
|
106
|
+
console.error('Gemini analysis failed:', {
|
|
107
|
+
feature: violation.feature,
|
|
108
|
+
error: apiError.message,
|
|
109
|
+
type: apiError.type
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Return fallback analysis with error context
|
|
113
|
+
return this.createFallbackAnalysis(violation, apiError);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Make API call to Gemini (retry logic handled by ErrorHandler)
|
|
119
|
+
*/
|
|
120
|
+
private async makeApiCall(prompt: string): Promise<Response> {
|
|
121
|
+
const response = await fetch(this.baseUrl, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'x-goog-api-key': this.apiKey,
|
|
125
|
+
'Content-Type': 'application/json'
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
contents: [{
|
|
129
|
+
parts: [{ text: prompt }]
|
|
130
|
+
}],
|
|
131
|
+
tools: [{
|
|
132
|
+
googleSearchRetrieval: {
|
|
133
|
+
dynamicRetrievalConfig: {
|
|
134
|
+
mode: 'MODE_DYNAMIC',
|
|
135
|
+
dynamicThreshold: 0.7
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}],
|
|
139
|
+
generationConfig: {
|
|
140
|
+
temperature: 0.1,
|
|
141
|
+
topK: 40,
|
|
142
|
+
topP: 0.95,
|
|
143
|
+
maxOutputTokens: 2048
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Let ErrorHandler classify the error based on status code
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
151
|
+
(error as any).response = response;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build analysis prompt for Gemini
|
|
160
|
+
*/
|
|
161
|
+
private buildAnalysisPrompt(violation: Violation): string {
|
|
162
|
+
return `
|
|
163
|
+
Analyze this browser compatibility issue and provide actionable insights:
|
|
164
|
+
|
|
165
|
+
COMPATIBILITY ISSUE:
|
|
166
|
+
- Feature: ${violation.feature}
|
|
167
|
+
- File: ${violation.file} (line ${violation.line})
|
|
168
|
+
- Unsupported Browser: ${violation.browser} ${violation.required}
|
|
169
|
+
- Baseline Status: ${violation.baselineStatus}
|
|
170
|
+
- Code Context: ${violation.context}
|
|
171
|
+
|
|
172
|
+
Please research and provide:
|
|
173
|
+
|
|
174
|
+
1. MARKET IMPACT: What percentage of users are affected by this compatibility issue? Search for current browser market share data for ${violation.browser} ${violation.required}.
|
|
175
|
+
|
|
176
|
+
2. USER EXPERIENCE: Explain in plain English what will happen to users on the unsupported browser. Will the feature fail silently, break the layout, or cause JavaScript errors?
|
|
177
|
+
|
|
178
|
+
3. FIX STRATEGY: What's the best approach to fix this? Options include:
|
|
179
|
+
- Progressive enhancement with @supports
|
|
180
|
+
- Feature detection with JavaScript
|
|
181
|
+
- Polyfills or libraries
|
|
182
|
+
- Alternative implementations
|
|
183
|
+
|
|
184
|
+
4. BEST PRACTICES: Search for current best practices from MDN, web.dev, or CSS-Tricks for implementing ${violation.feature} with fallbacks.
|
|
185
|
+
|
|
186
|
+
5. CODE EXAMPLES: What specific techniques should be used for the fix?
|
|
187
|
+
|
|
188
|
+
Format your response with clear sections and cite your sources.
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse Gemini response into structured analysis
|
|
194
|
+
*/
|
|
195
|
+
private parseAnalysisResponse(content: string, groundingMetadata: any, violation: Violation): Analysis {
|
|
196
|
+
// Extract sources from grounding metadata
|
|
197
|
+
const sources = this.extractSources(groundingMetadata);
|
|
198
|
+
|
|
199
|
+
// Parse structured information from response
|
|
200
|
+
const userImpact = this.extractUserImpact(content);
|
|
201
|
+
const marketShare = this.extractMarketShare(content);
|
|
202
|
+
const fixStrategy = this.extractFixStrategy(content);
|
|
203
|
+
const bestPractices = this.extractBestPractices(content);
|
|
204
|
+
|
|
205
|
+
// Calculate confidence based on grounding quality
|
|
206
|
+
const confidence = this.calculateConfidence(groundingMetadata, content);
|
|
207
|
+
|
|
208
|
+
// Clean up the plain English explanation
|
|
209
|
+
const plainEnglish = this.cleanupPlainEnglish(content);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
violation,
|
|
213
|
+
userImpact,
|
|
214
|
+
marketShare,
|
|
215
|
+
fixStrategy,
|
|
216
|
+
bestPractices,
|
|
217
|
+
sources,
|
|
218
|
+
plainEnglish,
|
|
219
|
+
confidence
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Extract sources from grounding metadata
|
|
225
|
+
*/
|
|
226
|
+
private extractSources(groundingMetadata: any): string[] {
|
|
227
|
+
if (!groundingMetadata?.groundingChunks) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return groundingMetadata.groundingChunks
|
|
232
|
+
.map((chunk: any) => chunk.web?.uri)
|
|
233
|
+
.filter((uri: string) => uri && uri.startsWith('http'))
|
|
234
|
+
.slice(0, 5); // Limit to top 5 sources
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extract user impact from response text
|
|
239
|
+
*/
|
|
240
|
+
private extractUserImpact(content: string): string {
|
|
241
|
+
// Look for specific impact patterns with context
|
|
242
|
+
const patterns = [
|
|
243
|
+
/(\d+(?:\.\d+)?%[^.]*(?:users?|browsers?|market|traffic|visitors?))/i,
|
|
244
|
+
/(?:affects?|impacts?)[^.]*(\d+(?:\.\d+)?%[^.]*)/i,
|
|
245
|
+
/(?:approximately|around|about)\s+(\d+(?:\.\d+)?%[^.]*(?:users?|browsers?))/i,
|
|
246
|
+
/(users?\s+on\s+[^.]*(?:may|will|could)[^.]*)/i
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
for (const pattern of patterns) {
|
|
250
|
+
const match = content.match(pattern);
|
|
251
|
+
if (match && match[1]) {
|
|
252
|
+
return match[1].trim();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Look for general impact statements without percentages
|
|
257
|
+
const generalPatterns = [
|
|
258
|
+
/(?:affects?|impacts?)[^.]*users?[^.]*\./i,
|
|
259
|
+
/users?\s+(?:may|will|could)[^.]*experience[^.]*\./i,
|
|
260
|
+
/compatibility\s+issues?[^.]*users?[^.]*\./i
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
for (const pattern of generalPatterns) {
|
|
264
|
+
const match = content.match(pattern);
|
|
265
|
+
if (match && match[0]) {
|
|
266
|
+
return match[0].trim();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return `Users on ${this.getCurrentViolation()?.browser || 'older browsers'} may experience compatibility issues`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Extract market share percentage from response
|
|
275
|
+
*/
|
|
276
|
+
private extractMarketShare(content: string): number {
|
|
277
|
+
// Look for percentage patterns in context of market share or usage
|
|
278
|
+
const patterns = [
|
|
279
|
+
/market\s+share[^.]*?(\d+(?:\.\d+)?)%/i,
|
|
280
|
+
/(\d+(?:\.\d+)?)%[^.]*(?:market|users?|browsers?|traffic)/i,
|
|
281
|
+
/usage[^.]*?(\d+(?:\.\d+)?)%/i,
|
|
282
|
+
/(\d+(?:\.\d+)?)%[^.]*(?:affected|impacted)/i
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
for (const pattern of patterns) {
|
|
286
|
+
const match = content.match(pattern);
|
|
287
|
+
if (match && match[1]) {
|
|
288
|
+
const percentage = parseFloat(match[1]);
|
|
289
|
+
if (percentage >= 0 && percentage <= 100) {
|
|
290
|
+
return percentage / 100;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Fallback: look for any percentage and validate it's reasonable
|
|
296
|
+
const anyPercentMatch = content.match(/(\d+(?:\.\d+)?)%/);
|
|
297
|
+
if (anyPercentMatch && anyPercentMatch[1]) {
|
|
298
|
+
const percentage = parseFloat(anyPercentMatch[1]);
|
|
299
|
+
if (percentage >= 0.1 && percentage <= 50) { // Reasonable range for browser market share
|
|
300
|
+
return percentage / 100;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return 0.05; // Default 5% if not found
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get current violation being processed (for fallback messages)
|
|
309
|
+
*/
|
|
310
|
+
private getCurrentViolation(): Violation | null {
|
|
311
|
+
// This would be set during processing, but for now return null
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Extract fix strategy from response
|
|
317
|
+
*/
|
|
318
|
+
private extractFixStrategy(content: string): string {
|
|
319
|
+
// Look for strategy keywords
|
|
320
|
+
const strategies = [
|
|
321
|
+
'progressive enhancement',
|
|
322
|
+
'feature detection',
|
|
323
|
+
'polyfill',
|
|
324
|
+
'fallback',
|
|
325
|
+
'@supports',
|
|
326
|
+
'graceful degradation'
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
for (const strategy of strategies) {
|
|
330
|
+
if (content.toLowerCase().includes(strategy)) {
|
|
331
|
+
return strategy;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return 'progressive enhancement';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Extract best practices from response
|
|
340
|
+
*/
|
|
341
|
+
private extractBestPractices(content: string): string[] {
|
|
342
|
+
const practices: string[] = [];
|
|
343
|
+
const lowerContent = content.toLowerCase();
|
|
344
|
+
|
|
345
|
+
// Look for specific best practices mentioned in the response
|
|
346
|
+
const practicePatterns = [
|
|
347
|
+
{ pattern: /@supports|feature detection/i, practice: 'Use @supports for feature detection' },
|
|
348
|
+
{ pattern: /fallback|alternative/i, practice: 'Provide fallback implementations' },
|
|
349
|
+
{ pattern: /polyfill/i, practice: 'Consider using polyfills' },
|
|
350
|
+
{ pattern: /progressive enhancement/i, practice: 'Use progressive enhancement' },
|
|
351
|
+
{ pattern: /graceful degradation/i, practice: 'Implement graceful degradation' },
|
|
352
|
+
{ pattern: /test|testing/i, practice: 'Test across target browsers' },
|
|
353
|
+
{ pattern: /vendor prefix/i, practice: 'Use vendor prefixes when needed' },
|
|
354
|
+
{ pattern: /modernizr/i, practice: 'Consider using Modernizr for feature detection' }
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
for (const { pattern, practice } of practicePatterns) {
|
|
358
|
+
if (pattern.test(content) && !practices.includes(practice)) {
|
|
359
|
+
practices.push(practice);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Extract numbered or bulleted best practices from the response
|
|
364
|
+
const listMatches = content.match(/(?:^|\n)\s*[-*•]\s*([^.\n]+)/gm);
|
|
365
|
+
if (listMatches) {
|
|
366
|
+
for (const match of listMatches.slice(0, 3)) { // Limit to 3 additional practices
|
|
367
|
+
const practice = match.replace(/^[\s\n-*•]+/, '').trim();
|
|
368
|
+
if (practice.length > 10 && practice.length < 100 && !practices.some(p => p.includes(practice))) {
|
|
369
|
+
practices.push(practice);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return practices.length > 0 ? practices : ['Follow progressive enhancement principles'];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Clean up plain English explanation
|
|
379
|
+
*/
|
|
380
|
+
private cleanupPlainEnglish(content: string): string {
|
|
381
|
+
// Remove excessive whitespace and normalize line breaks
|
|
382
|
+
let cleaned = content.replace(/\n\s*\n/g, '\n\n').trim();
|
|
383
|
+
|
|
384
|
+
// Remove markdown formatting that might interfere with display
|
|
385
|
+
cleaned = cleaned.replace(/\*\*(.*?)\*\*/g, '$1'); // Remove bold
|
|
386
|
+
cleaned = cleaned.replace(/\*(.*?)\*/g, '$1'); // Remove italic
|
|
387
|
+
|
|
388
|
+
// Ensure it's not too long for display
|
|
389
|
+
if (cleaned.length > 1500) {
|
|
390
|
+
const sentences = cleaned.split(/[.!?]+/);
|
|
391
|
+
let truncated = '';
|
|
392
|
+
for (const sentence of sentences) {
|
|
393
|
+
if (truncated.length + sentence.length > 1400) break;
|
|
394
|
+
truncated += sentence + '.';
|
|
395
|
+
}
|
|
396
|
+
cleaned = truncated + '..';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return cleaned;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Calculate confidence score based on grounding quality
|
|
404
|
+
*/
|
|
405
|
+
private calculateConfidence(groundingMetadata: any, content: string): number {
|
|
406
|
+
let confidence = 0.5; // Base confidence
|
|
407
|
+
|
|
408
|
+
// Increase confidence if we have grounding sources
|
|
409
|
+
if (groundingMetadata?.groundingChunks?.length > 0) {
|
|
410
|
+
confidence += 0.2;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Increase confidence if response contains specific data
|
|
414
|
+
if (content.includes('%')) {
|
|
415
|
+
confidence += 0.1;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Increase confidence if response mentions authoritative sources
|
|
419
|
+
const authoritativeSources = ['mdn', 'web.dev', 'caniuse', 'w3c'];
|
|
420
|
+
if (authoritativeSources.some(source => content.toLowerCase().includes(source))) {
|
|
421
|
+
confidence += 0.2;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return Math.min(confidence, 1.0);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Create fallback analysis when API fails
|
|
429
|
+
*/
|
|
430
|
+
private createFallbackAnalysis(violation: Violation, error: APIError): Analysis {
|
|
431
|
+
let fallbackMessage = `Analysis unavailable: ${error.message}`;
|
|
432
|
+
let confidence = 0.3;
|
|
433
|
+
|
|
434
|
+
// Provide more specific fallback based on error type
|
|
435
|
+
if (error.type === ErrorType.RATE_LIMIT) {
|
|
436
|
+
fallbackMessage = `Rate limit reached. Using basic compatibility analysis for ${violation.feature}.`;
|
|
437
|
+
confidence = 0.4;
|
|
438
|
+
} else if (error.type === ErrorType.AUTHENTICATION) {
|
|
439
|
+
fallbackMessage = `API authentication failed. Using offline compatibility analysis for ${violation.feature}.`;
|
|
440
|
+
confidence = 0.2;
|
|
441
|
+
} else if (error.type === ErrorType.NETWORK) {
|
|
442
|
+
fallbackMessage = `Network unavailable. Using cached compatibility data for ${violation.feature}.`;
|
|
443
|
+
confidence = 0.5;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Get fallback suggestions based on error type
|
|
447
|
+
const fallbackSuggestions = ErrorHandler.getFallbackSuggestions(error.type);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
violation,
|
|
451
|
+
userImpact: `Users on ${violation.browser} ${violation.required} may experience compatibility issues with ${violation.feature}`,
|
|
452
|
+
marketShare: this.estimateMarketShare(violation.browser, violation.required),
|
|
453
|
+
fixStrategy: this.getDefaultFixStrategy(violation.feature),
|
|
454
|
+
bestPractices: [
|
|
455
|
+
'Use @supports for CSS feature detection',
|
|
456
|
+
'Implement fallback solutions',
|
|
457
|
+
'Test across target browsers',
|
|
458
|
+
...fallbackSuggestions.slice(0, 2)
|
|
459
|
+
],
|
|
460
|
+
sources: [],
|
|
461
|
+
plainEnglish: `${fallbackMessage} Consider using progressive enhancement techniques and testing across your target browsers.`,
|
|
462
|
+
confidence
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Estimate market share for fallback analysis
|
|
468
|
+
*/
|
|
469
|
+
private estimateMarketShare(browser: string, version: string): number {
|
|
470
|
+
// Conservative estimates based on typical browser usage patterns
|
|
471
|
+
const estimates: Record<string, number> = {
|
|
472
|
+
'chrome': 0.65,
|
|
473
|
+
'firefox': 0.08,
|
|
474
|
+
'safari': 0.18,
|
|
475
|
+
'edge': 0.05,
|
|
476
|
+
'opera': 0.02,
|
|
477
|
+
'samsung': 0.02
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const baseShare = estimates[browser.toLowerCase()] || 0.05;
|
|
481
|
+
|
|
482
|
+
// Reduce estimate for older versions
|
|
483
|
+
if (version !== 'baseline' && version !== 'baseline-newly') {
|
|
484
|
+
const versionNum = parseInt(version, 10);
|
|
485
|
+
if (!isNaN(versionNum)) {
|
|
486
|
+
// Rough estimate: older versions have lower usage
|
|
487
|
+
const currentYear = new Date().getFullYear();
|
|
488
|
+
const estimatedYear = 2008 + (versionNum / 10); // Very rough estimation
|
|
489
|
+
const yearsDiff = currentYear - estimatedYear;
|
|
490
|
+
|
|
491
|
+
if (yearsDiff > 2) {
|
|
492
|
+
return baseShare * 0.1; // Much lower for old versions
|
|
493
|
+
} else if (yearsDiff > 1) {
|
|
494
|
+
return baseShare * 0.3; // Lower for somewhat old versions
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return baseShare;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get default fix strategy based on feature type
|
|
504
|
+
*/
|
|
505
|
+
private getDefaultFixStrategy(feature: string): string {
|
|
506
|
+
const lowerFeature = feature.toLowerCase();
|
|
507
|
+
|
|
508
|
+
if (lowerFeature.includes('css') || lowerFeature.includes('grid') || lowerFeature.includes('flex')) {
|
|
509
|
+
return 'progressive enhancement with @supports';
|
|
510
|
+
} else if (lowerFeature.includes('api') || lowerFeature.includes('js')) {
|
|
511
|
+
return 'feature detection with polyfills';
|
|
512
|
+
} else if (lowerFeature.includes('element') || lowerFeature.includes('html')) {
|
|
513
|
+
return 'graceful degradation';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return 'progressive enhancement';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Generate cache key for a violation
|
|
521
|
+
*/
|
|
522
|
+
private generateCacheKey(violation: Violation): string {
|
|
523
|
+
// Create a hash based on feature, browser, and version
|
|
524
|
+
const keyData = `${violation.feature}:${violation.browser}:${violation.required}:${violation.baselineStatus}`;
|
|
525
|
+
return createHash('md5').update(keyData).digest('hex');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get cached analysis if available and not expired
|
|
530
|
+
*/
|
|
531
|
+
private getCachedAnalysis(cacheKey: string): Analysis | null {
|
|
532
|
+
const entry = this.cache.get(cacheKey);
|
|
533
|
+
|
|
534
|
+
if (!entry) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const now = Date.now();
|
|
539
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
540
|
+
// Cache expired, remove it
|
|
541
|
+
this.cache.delete(cacheKey);
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return entry.analysis;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Cache analysis result
|
|
550
|
+
*/
|
|
551
|
+
private cacheAnalysis(cacheKey: string, analysis: Analysis): void {
|
|
552
|
+
const entry: CacheEntry = {
|
|
553
|
+
analysis,
|
|
554
|
+
timestamp: Date.now(),
|
|
555
|
+
ttl: this.cacheTtl
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
this.cache.set(cacheKey, entry);
|
|
559
|
+
|
|
560
|
+
// Clean up old entries periodically
|
|
561
|
+
this.cleanupCache();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Clean up expired cache entries
|
|
566
|
+
*/
|
|
567
|
+
private cleanupCache(): void {
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
|
|
570
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
571
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
572
|
+
this.cache.delete(key);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Clear all cached analyses
|
|
579
|
+
*/
|
|
580
|
+
public clearCache(): void {
|
|
581
|
+
this.cache.clear();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get cache statistics
|
|
586
|
+
*/
|
|
587
|
+
public getCacheStats(): { size: number; hitRate?: number } {
|
|
588
|
+
return {
|
|
589
|
+
size: this.cache.size
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Analyze multiple violations with concurrency control
|
|
595
|
+
*/
|
|
596
|
+
async analyzeViolations(violations: Violation[], maxConcurrent = 3): Promise<Analysis[]> {
|
|
597
|
+
const results: Analysis[] = [];
|
|
598
|
+
|
|
599
|
+
// Process violations in batches to avoid overwhelming the API
|
|
600
|
+
for (let i = 0; i < violations.length; i += maxConcurrent) {
|
|
601
|
+
const batch = violations.slice(i, i + maxConcurrent);
|
|
602
|
+
const batchPromises = batch.map(violation => this.analyzeViolation(violation));
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
const batchResults = await Promise.all(batchPromises);
|
|
606
|
+
results.push(...batchResults);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
console.error(`Error processing batch ${i / maxConcurrent + 1}:`, error);
|
|
609
|
+
|
|
610
|
+
// Process individually as fallback
|
|
611
|
+
for (const violation of batch) {
|
|
612
|
+
try {
|
|
613
|
+
const analysis = await this.analyzeViolation(violation);
|
|
614
|
+
results.push(analysis);
|
|
615
|
+
} catch (individualError) {
|
|
616
|
+
console.error(`Error analyzing ${violation.feature}:`, individualError);
|
|
617
|
+
results.push(this.createFallbackAnalysis(violation, ErrorHandler.handleAPIError(individualError)));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Add delay between batches to respect rate limits
|
|
623
|
+
if (i + maxConcurrent < violations.length) {
|
|
624
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return results;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Validate API key format
|
|
633
|
+
*/
|
|
634
|
+
public static validateApiKey(apiKey: string): boolean {
|
|
635
|
+
// Gemini API keys typically start with 'AIza' and are 39 characters long
|
|
636
|
+
// But they can also have other formats, so let's be more flexible
|
|
637
|
+
return /^AIza[A-Za-z0-9_-]{35,}$/.test(apiKey) || apiKey.length >= 20;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Test API connectivity
|
|
642
|
+
*/
|
|
643
|
+
async testConnection(): Promise<{ success: boolean; error?: string; errorType?: ErrorType }> {
|
|
644
|
+
try {
|
|
645
|
+
const testPrompt = 'Test connection. Please respond with "OK".';
|
|
646
|
+
const response = await this.makeApiCall(testPrompt);
|
|
647
|
+
|
|
648
|
+
// If we get here, the API call succeeded
|
|
649
|
+
const data = await response.json();
|
|
650
|
+
|
|
651
|
+
// Validate we got a proper response
|
|
652
|
+
if (data.candidates && data.candidates.length > 0) {
|
|
653
|
+
return { success: true };
|
|
654
|
+
} else {
|
|
655
|
+
return {
|
|
656
|
+
success: false,
|
|
657
|
+
error: 'API responded but returned no content',
|
|
658
|
+
errorType: ErrorType.VALIDATION
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
} catch (error) {
|
|
662
|
+
const apiError = ErrorHandler.handleAPIError(error);
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
success: false,
|
|
666
|
+
error: apiError.message,
|
|
667
|
+
errorType: apiError.type
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
package/src/ai/index.ts
ADDED