@arclabs561/ai-visual-test 0.5.1
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/.secretsignore.example +20 -0
- package/CHANGELOG.md +360 -0
- package/CONTRIBUTING.md +63 -0
- package/DEPLOYMENT.md +80 -0
- package/LICENSE +22 -0
- package/README.md +142 -0
- package/SECURITY.md +108 -0
- package/api/health.js +34 -0
- package/api/validate.js +252 -0
- package/index.d.ts +1221 -0
- package/package.json +112 -0
- package/public/index.html +149 -0
- package/src/batch-optimizer.mjs +451 -0
- package/src/bias-detector.mjs +370 -0
- package/src/bias-mitigation.mjs +233 -0
- package/src/cache.mjs +433 -0
- package/src/config.mjs +268 -0
- package/src/constants.mjs +80 -0
- package/src/context-compressor.mjs +350 -0
- package/src/convenience.mjs +617 -0
- package/src/cost-tracker.mjs +257 -0
- package/src/cross-modal-consistency.mjs +170 -0
- package/src/data-extractor.mjs +232 -0
- package/src/dynamic-few-shot.mjs +140 -0
- package/src/dynamic-prompts.mjs +361 -0
- package/src/ensemble/index.mjs +53 -0
- package/src/ensemble-judge.mjs +366 -0
- package/src/error-handler.mjs +67 -0
- package/src/errors.mjs +167 -0
- package/src/experience-propagation.mjs +128 -0
- package/src/experience-tracer.mjs +487 -0
- package/src/explanation-manager.mjs +299 -0
- package/src/feedback-aggregator.mjs +248 -0
- package/src/game-goal-prompts.mjs +478 -0
- package/src/game-player.mjs +548 -0
- package/src/hallucination-detector.mjs +155 -0
- package/src/helpers/playwright.mjs +80 -0
- package/src/human-validation-manager.mjs +516 -0
- package/src/index.mjs +364 -0
- package/src/judge.mjs +929 -0
- package/src/latency-aware-batch-optimizer.mjs +192 -0
- package/src/load-env.mjs +159 -0
- package/src/logger.mjs +55 -0
- package/src/metrics.mjs +187 -0
- package/src/model-tier-selector.mjs +221 -0
- package/src/multi-modal/index.mjs +36 -0
- package/src/multi-modal-fusion.mjs +190 -0
- package/src/multi-modal.mjs +524 -0
- package/src/natural-language-specs.mjs +1071 -0
- package/src/pair-comparison.mjs +277 -0
- package/src/persona/index.mjs +42 -0
- package/src/persona-enhanced.mjs +200 -0
- package/src/persona-experience.mjs +572 -0
- package/src/position-counterbalance.mjs +140 -0
- package/src/prompt-composer.mjs +375 -0
- package/src/render-change-detector.mjs +583 -0
- package/src/research-enhanced-validation.mjs +436 -0
- package/src/retry.mjs +152 -0
- package/src/rubrics.mjs +231 -0
- package/src/score-tracker.mjs +277 -0
- package/src/smart-validator.mjs +447 -0
- package/src/spec-config.mjs +106 -0
- package/src/spec-templates.mjs +347 -0
- package/src/specs/index.mjs +38 -0
- package/src/temporal/index.mjs +102 -0
- package/src/temporal-adaptive.mjs +163 -0
- package/src/temporal-batch-optimizer.mjs +222 -0
- package/src/temporal-constants.mjs +69 -0
- package/src/temporal-context.mjs +49 -0
- package/src/temporal-decision-manager.mjs +271 -0
- package/src/temporal-decision.mjs +669 -0
- package/src/temporal-errors.mjs +58 -0
- package/src/temporal-note-pruner.mjs +173 -0
- package/src/temporal-preprocessor.mjs +543 -0
- package/src/temporal-prompt-formatter.mjs +219 -0
- package/src/temporal-validation.mjs +159 -0
- package/src/temporal.mjs +415 -0
- package/src/type-guards.mjs +311 -0
- package/src/uncertainty-reducer.mjs +470 -0
- package/src/utils/index.mjs +175 -0
- package/src/validation-framework.mjs +321 -0
- package/src/validation-result-normalizer.mjs +64 -0
- package/src/validation.mjs +243 -0
- package/src/validators/accessibility-programmatic.mjs +345 -0
- package/src/validators/accessibility-validator.mjs +223 -0
- package/src/validators/batch-validator.mjs +143 -0
- package/src/validators/hybrid-validator.mjs +268 -0
- package/src/validators/index.mjs +34 -0
- package/src/validators/prompt-builder.mjs +218 -0
- package/src/validators/rubric.mjs +85 -0
- package/src/validators/state-programmatic.mjs +260 -0
- package/src/validators/state-validator.mjs +291 -0
- package/vercel.json +27 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensemble Judging
|
|
3
|
+
*
|
|
4
|
+
* Implements multiple LLM judges with consensus voting.
|
|
5
|
+
* Research shows ensemble judging improves accuracy and reduces bias.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - Multiple judges (different providers or prompts)
|
|
9
|
+
* - Weighted voting
|
|
10
|
+
* - Consensus calculation
|
|
11
|
+
* - Disagreement analysis
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { VLLMJudge } from './judge.mjs';
|
|
15
|
+
import { detectBias, detectPositionBias } from './bias-detector.mjs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensemble Judge Class
|
|
19
|
+
*
|
|
20
|
+
* Manages multiple judges and aggregates their results.
|
|
21
|
+
*
|
|
22
|
+
* @class EnsembleJudge
|
|
23
|
+
*/
|
|
24
|
+
export class EnsembleJudge {
|
|
25
|
+
/**
|
|
26
|
+
* @param {import('./index.mjs').EnsembleJudgeOptions} [options={}] - Ensemble configuration
|
|
27
|
+
*/
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
const {
|
|
30
|
+
judges = [],
|
|
31
|
+
votingMethod = 'weighted_average', // 'weighted_average', 'majority', 'consensus', 'optimal'
|
|
32
|
+
weights = null, // Array of weights for each judge
|
|
33
|
+
judgeAccuracies = null, // Array of accuracy scores (0-1) for optimal weighting
|
|
34
|
+
minAgreement = 0.7, // Minimum agreement for consensus
|
|
35
|
+
enableBiasDetection = true
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
this.judges = judges.length > 0 ? judges : [new VLLMJudge()];
|
|
39
|
+
this.votingMethod = votingMethod;
|
|
40
|
+
this.judgeAccuracies = judgeAccuracies; // For optimal weighting (arXiv:2510.01499)
|
|
41
|
+
this.weights = weights || this.judges.map(() => 1.0);
|
|
42
|
+
this.minAgreement = minAgreement;
|
|
43
|
+
this.enableBiasDetection = enableBiasDetection;
|
|
44
|
+
|
|
45
|
+
// Calculate weights based on method
|
|
46
|
+
if (votingMethod === 'optimal' && this.judgeAccuracies) {
|
|
47
|
+
this.weights = this.calculateOptimalWeights(this.judgeAccuracies);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Normalize weights
|
|
51
|
+
const weightSum = this.weights.reduce((a, b) => a + b, 0);
|
|
52
|
+
this.normalizedWeights = this.weights.map(w => w / weightSum);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate optimal weights using inverse generalized sigmoid function
|
|
57
|
+
* Research: arXiv:2510.01499 - ω_i = σ_K^{-1}(x_i) where σ_K(x) = e^x/(K-1+e^x)
|
|
58
|
+
*
|
|
59
|
+
* CORRECTED: Uses generalized sigmoid σ_K(x) for N models, not standard logistic σ(x)
|
|
60
|
+
* For K=2 models, this reduces to standard logistic. For K>2, the formula differs.
|
|
61
|
+
*
|
|
62
|
+
* @param {number[]} accuracies - Array of accuracy scores (0-1) for each judge
|
|
63
|
+
* @returns {number[]} Optimal weights
|
|
64
|
+
*/
|
|
65
|
+
calculateOptimalWeights(accuracies) {
|
|
66
|
+
const K = accuracies.length; // Number of models
|
|
67
|
+
|
|
68
|
+
// Edge case: single judge gets weight 1.0
|
|
69
|
+
if (K === 1) {
|
|
70
|
+
return [1.0];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle edge cases: p=0 → -∞, p=1 → +∞, so clamp to [0.001, 0.999]
|
|
74
|
+
const clamped = accuracies.map(a => Math.max(0.001, Math.min(0.999, a)));
|
|
75
|
+
|
|
76
|
+
// CORRECT formula: σ_K^{-1}(x) = ln(x(K-1) / (1-x))
|
|
77
|
+
// This is the inverse of σ_K(x) = e^x/(K-1+e^x)
|
|
78
|
+
const inverseSigmoid = clamped.map(p => {
|
|
79
|
+
if (p <= 0 || p >= 1) return 0; // Safety check
|
|
80
|
+
const numerator = p * (K - 1);
|
|
81
|
+
const denominator = 1 - p;
|
|
82
|
+
if (denominator <= 0 || numerator <= 0) return 0; // Safety check (handles K=1 case)
|
|
83
|
+
const ratio = numerator / denominator;
|
|
84
|
+
if (ratio <= 0) return 0; // Safety check for ln(0) or ln(negative)
|
|
85
|
+
return Math.log(ratio);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Normalize to positive weights (shift by min to make all positive, preserve ratios)
|
|
89
|
+
const min = Math.min(...inverseSigmoid);
|
|
90
|
+
const shifted = inverseSigmoid.map(w => {
|
|
91
|
+
const shiftedValue = w - min + 1;
|
|
92
|
+
// Ensure positive weight (clamp to minimum 0.001 to avoid zero weights)
|
|
93
|
+
return Math.max(0.001, shiftedValue);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return shifted;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Evaluate screenshot with ensemble of judges
|
|
101
|
+
*
|
|
102
|
+
* @param {string} imagePath - Path to screenshot file
|
|
103
|
+
* @param {string} prompt - Evaluation prompt
|
|
104
|
+
* @param {import('./index.mjs').ValidationContext} [context={}] - Validation context
|
|
105
|
+
* @returns {Promise<import('./index.mjs').EnsembleResult>} Ensemble evaluation result
|
|
106
|
+
*/
|
|
107
|
+
async evaluate(imagePath, prompt, context = {}) {
|
|
108
|
+
// Run all judges in parallel
|
|
109
|
+
const judgments = await Promise.all(
|
|
110
|
+
this.judges.map((judge, index) =>
|
|
111
|
+
judge.judgeScreenshot(imagePath, prompt, {
|
|
112
|
+
...context,
|
|
113
|
+
judgeIndex: index,
|
|
114
|
+
judgeCount: this.judges.length
|
|
115
|
+
}).catch(error => ({
|
|
116
|
+
error: error.message,
|
|
117
|
+
judgeIndex: index,
|
|
118
|
+
score: null
|
|
119
|
+
}))
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Extract scores and results
|
|
124
|
+
const results = judgments.map((judgment, index) => ({
|
|
125
|
+
judgeIndex: index,
|
|
126
|
+
score: judgment.score,
|
|
127
|
+
assessment: judgment.assessment,
|
|
128
|
+
issues: judgment.issues || [],
|
|
129
|
+
reasoning: judgment.reasoning,
|
|
130
|
+
provider: judgment.provider,
|
|
131
|
+
error: judgment.error,
|
|
132
|
+
raw: judgment
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
// Aggregate results
|
|
136
|
+
const aggregated = this.aggregateResults(results);
|
|
137
|
+
|
|
138
|
+
// Detect biases if enabled
|
|
139
|
+
if (this.enableBiasDetection) {
|
|
140
|
+
aggregated.biasDetection = {
|
|
141
|
+
individual: results.map(r => detectBias(r.reasoning || '')),
|
|
142
|
+
position: detectPositionBias(results)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Calculate agreement
|
|
147
|
+
aggregated.agreement = this.calculateAgreement(results);
|
|
148
|
+
aggregated.disagreement = this.analyzeDisagreement(results);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...aggregated,
|
|
152
|
+
individualJudgments: results,
|
|
153
|
+
judgeCount: this.judges.length,
|
|
154
|
+
votingMethod: this.votingMethod
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Aggregate results based on voting method
|
|
160
|
+
*/
|
|
161
|
+
aggregateResults(results) {
|
|
162
|
+
const validResults = results.filter(r => r.score !== null && !r.error);
|
|
163
|
+
|
|
164
|
+
if (validResults.length === 0) {
|
|
165
|
+
return {
|
|
166
|
+
score: null,
|
|
167
|
+
assessment: 'error',
|
|
168
|
+
issues: ['All judges failed'],
|
|
169
|
+
reasoning: 'All judges encountered errors',
|
|
170
|
+
confidence: 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
switch (this.votingMethod) {
|
|
175
|
+
case 'weighted_average':
|
|
176
|
+
case 'optimal':
|
|
177
|
+
return this.weightedAverage(validResults);
|
|
178
|
+
case 'majority':
|
|
179
|
+
return this.majorityVote(validResults);
|
|
180
|
+
case 'consensus':
|
|
181
|
+
return this.consensusVote(validResults);
|
|
182
|
+
default:
|
|
183
|
+
return this.weightedAverage(validResults);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Weighted average voting
|
|
189
|
+
*/
|
|
190
|
+
weightedAverage(results) {
|
|
191
|
+
const scores = results.map((r, i) => ({
|
|
192
|
+
score: r.score,
|
|
193
|
+
weight: this.normalizedWeights[r.judgeIndex] || 1.0 / results.length
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
const weightedSum = scores.reduce((sum, s) => sum + (s.score * s.weight), 0);
|
|
197
|
+
const totalWeight = scores.reduce((sum, s) => sum + s.weight, 0);
|
|
198
|
+
const avgScore = totalWeight > 0 ? weightedSum / totalWeight : null;
|
|
199
|
+
|
|
200
|
+
// Aggregate issues (union)
|
|
201
|
+
const allIssues = new Set();
|
|
202
|
+
results.forEach(r => {
|
|
203
|
+
if (r.issues) r.issues.forEach(issue => allIssues.add(issue));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Aggregate reasoning
|
|
207
|
+
const reasoning = results
|
|
208
|
+
.map((r, i) => `Judge ${i + 1} (${r.provider}): ${r.reasoning || 'No reasoning'}`)
|
|
209
|
+
.join('\n\n');
|
|
210
|
+
|
|
211
|
+
// Determine assessment
|
|
212
|
+
const assessment = avgScore >= 7 ? 'pass' : avgScore >= 5 ? 'needs-improvement' : 'fail';
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
score: Math.round(avgScore * 10) / 10, // Round to 1 decimal
|
|
216
|
+
assessment,
|
|
217
|
+
issues: Array.from(allIssues),
|
|
218
|
+
reasoning: `Ensemble judgment (weighted average):\n${reasoning}`,
|
|
219
|
+
confidence: this.calculateConfidence(results, avgScore)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Majority vote
|
|
225
|
+
*/
|
|
226
|
+
majorityVote(results) {
|
|
227
|
+
const assessments = results.map(r => r.assessment || (r.score >= 7 ? 'pass' : r.score >= 5 ? 'needs-improvement' : 'fail'));
|
|
228
|
+
const assessmentCounts = {};
|
|
229
|
+
assessments.forEach(a => {
|
|
230
|
+
assessmentCounts[a] = (assessmentCounts[a] || 0) + 1;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const majorityAssessment = Object.entries(assessmentCounts)
|
|
234
|
+
.sort((a, b) => b[1] - a[1])[0][0];
|
|
235
|
+
|
|
236
|
+
// Average score of majority
|
|
237
|
+
const majorityResults = results.filter((r, i) => assessments[i] === majorityAssessment);
|
|
238
|
+
const avgScore = majorityResults.reduce((sum, r) => sum + r.score, 0) / majorityResults.length;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
score: Math.round(avgScore * 10) / 10,
|
|
242
|
+
assessment: majorityAssessment,
|
|
243
|
+
issues: Array.from(new Set(majorityResults.flatMap(r => r.issues || []))),
|
|
244
|
+
reasoning: `Majority vote: ${majorityAssessment} (${assessmentCounts[majorityAssessment]}/${results.length} judges)`,
|
|
245
|
+
confidence: assessmentCounts[majorityAssessment] / results.length
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Consensus vote (requires high agreement)
|
|
251
|
+
*/
|
|
252
|
+
consensusVote(results) {
|
|
253
|
+
const agreement = this.calculateAgreement(results);
|
|
254
|
+
|
|
255
|
+
if (agreement.score < this.minAgreement) {
|
|
256
|
+
// No consensus - return weighted average with low confidence
|
|
257
|
+
const avg = this.weightedAverage(results);
|
|
258
|
+
return {
|
|
259
|
+
...avg,
|
|
260
|
+
assessment: 'no-consensus',
|
|
261
|
+
confidence: agreement.score,
|
|
262
|
+
reasoning: `No consensus reached (agreement: ${(agreement.score * 100).toFixed(0)}%). ${avg.reasoning}`
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Consensus reached - return weighted average
|
|
267
|
+
return this.weightedAverage(results);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Calculate agreement between judges
|
|
272
|
+
*/
|
|
273
|
+
calculateAgreement(results) {
|
|
274
|
+
if (results.length < 2) {
|
|
275
|
+
return { score: 1.0, type: 'single_judge' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const scores = results.map(r => r.score).filter(s => s !== null);
|
|
279
|
+
if (scores.length < 2) {
|
|
280
|
+
return { score: 0, type: 'insufficient_scores' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Calculate variance
|
|
284
|
+
const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
285
|
+
const variance = scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / scores.length;
|
|
286
|
+
const stdDev = Math.sqrt(variance);
|
|
287
|
+
|
|
288
|
+
// Agreement is inverse of normalized standard deviation
|
|
289
|
+
// Max std dev for 0-10 scale is ~5, so normalize
|
|
290
|
+
const normalizedStdDev = stdDev / 5;
|
|
291
|
+
const agreement = Math.max(0, 1 - normalizedStdDev);
|
|
292
|
+
|
|
293
|
+
// Check assessment agreement
|
|
294
|
+
const assessments = results.map(r => r.assessment || (r.score >= 7 ? 'pass' : 'fail'));
|
|
295
|
+
const uniqueAssessments = new Set(assessments);
|
|
296
|
+
const assessmentAgreement = uniqueAssessments.size === 1 ? 1.0 : 0.5;
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
score: (agreement + assessmentAgreement) / 2,
|
|
300
|
+
scoreAgreement: agreement,
|
|
301
|
+
assessmentAgreement,
|
|
302
|
+
mean,
|
|
303
|
+
stdDev,
|
|
304
|
+
scores
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Analyze disagreement between judges
|
|
310
|
+
*/
|
|
311
|
+
analyzeDisagreement(results) {
|
|
312
|
+
if (results.length < 2) {
|
|
313
|
+
return { hasDisagreement: false };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const scores = results.map(r => r.score).filter(s => s !== null);
|
|
317
|
+
const assessments = results.map(r => r.assessment || (r.score >= 7 ? 'pass' : 'fail'));
|
|
318
|
+
|
|
319
|
+
const scoreRange = Math.max(...scores) - Math.min(...scores);
|
|
320
|
+
const uniqueAssessments = new Set(assessments);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
hasDisagreement: scoreRange > 2 || uniqueAssessments.size > 1,
|
|
324
|
+
scoreRange,
|
|
325
|
+
assessmentDisagreement: uniqueAssessments.size > 1,
|
|
326
|
+
uniqueAssessments: Array.from(uniqueAssessments),
|
|
327
|
+
maxScore: Math.max(...scores),
|
|
328
|
+
minScore: Math.min(...scores)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Calculate confidence in aggregated result
|
|
334
|
+
*/
|
|
335
|
+
calculateConfidence(results, avgScore) {
|
|
336
|
+
const agreement = this.calculateAgreement(results);
|
|
337
|
+
const disagreement = this.analyzeDisagreement(results);
|
|
338
|
+
|
|
339
|
+
// Confidence based on agreement and number of judges
|
|
340
|
+
const agreementConfidence = agreement.score;
|
|
341
|
+
const judgeCountConfidence = Math.min(1.0, results.length / 3); // More judges = more confidence
|
|
342
|
+
const disagreementPenalty = disagreement.hasDisagreement ? 0.2 : 0;
|
|
343
|
+
|
|
344
|
+
return Math.max(0, Math.min(1.0, (agreementConfidence * 0.7 + judgeCountConfidence * 0.3) - disagreementPenalty));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Create an ensemble judge with multiple providers
|
|
350
|
+
*
|
|
351
|
+
* @param {string[]} [providers=['gemini', 'openai']] - Array of provider names
|
|
352
|
+
* @param {import('./index.mjs').EnsembleJudgeOptions} [options={}] - Ensemble configuration
|
|
353
|
+
* @returns {EnsembleJudge} Configured ensemble judge
|
|
354
|
+
*/
|
|
355
|
+
export function createEnsembleJudge(providers = ['gemini', 'openai'], options = {}) {
|
|
356
|
+
const judges = providers.map(provider => {
|
|
357
|
+
const judge = new VLLMJudge({ provider });
|
|
358
|
+
return judge;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return new EnsembleJudge({
|
|
362
|
+
...options,
|
|
363
|
+
judges
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Error Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles unhandled promise rejections and uncaught exceptions.
|
|
5
|
+
* Prevents silent failures and improves debugging.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { error } from './logger.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize global error handlers
|
|
12
|
+
*
|
|
13
|
+
* **Opt-in**: This function is exported but not automatically called.
|
|
14
|
+
* Users must explicitly call `initErrorHandlers()` if they want global
|
|
15
|
+
* error handling for unhandled rejections and uncaught exceptions.
|
|
16
|
+
*
|
|
17
|
+
* Should be called early in application startup.
|
|
18
|
+
* Only call once per process.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```javascript
|
|
22
|
+
* import { initErrorHandlers } from 'ai-visual-test';
|
|
23
|
+
* initErrorHandlers(); // Opt-in to global error handling
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function initErrorHandlers() {
|
|
27
|
+
// Handle unhandled promise rejections
|
|
28
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
29
|
+
error('[Unhandled Rejection]', {
|
|
30
|
+
reason: reason instanceof Error ? {
|
|
31
|
+
message: reason.message,
|
|
32
|
+
stack: reason.stack,
|
|
33
|
+
name: reason.name
|
|
34
|
+
} : reason,
|
|
35
|
+
promise: promise?.toString?.() || 'Unknown promise'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// In production, you might want to:
|
|
39
|
+
// - Log to monitoring service (Sentry, DataDog, etc.)
|
|
40
|
+
// - Send alerts
|
|
41
|
+
// - Gracefully shutdown
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Handle uncaught exceptions
|
|
45
|
+
process.on('uncaughtException', (err) => {
|
|
46
|
+
error('[Uncaught Exception]', {
|
|
47
|
+
message: err.message,
|
|
48
|
+
stack: err.stack,
|
|
49
|
+
name: err.name
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// NOTE: Libraries should not call process.exit()
|
|
53
|
+
// Let the application decide how to handle uncaught exceptions.
|
|
54
|
+
// Users can add their own process.exit(1) if needed, or use a process manager
|
|
55
|
+
// that handles restarts automatically.
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Handle warnings
|
|
59
|
+
process.on('warning', (warning) => {
|
|
60
|
+
error('[Process Warning]', {
|
|
61
|
+
name: warning.name,
|
|
62
|
+
message: warning.message,
|
|
63
|
+
stack: warning.stack
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
package/src/errors.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Error Classes for ai-visual-test
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized error handling across the package.
|
|
5
|
+
* Based on Playwright's error handling patterns and industry best practices.
|
|
6
|
+
*
|
|
7
|
+
* All errors extend AIBrowserTestError for consistent error handling and serialization.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base error class for all ai-visual-test errors
|
|
12
|
+
*
|
|
13
|
+
* @class AIBrowserTestError
|
|
14
|
+
* @extends {Error}
|
|
15
|
+
*/
|
|
16
|
+
export class AIBrowserTestError extends Error {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} message - Error message
|
|
19
|
+
* @param {string} code - Error code
|
|
20
|
+
* @param {Record<string, unknown>} [details={}] - Additional error details
|
|
21
|
+
*/
|
|
22
|
+
constructor(message, code, details = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = this.constructor.name;
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.details = details;
|
|
27
|
+
|
|
28
|
+
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
29
|
+
if (Error.captureStackTrace) {
|
|
30
|
+
Error.captureStackTrace(this, this.constructor);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert error to JSON for serialization
|
|
36
|
+
*
|
|
37
|
+
* @returns {import('./index.mjs').AIBrowserTestError['toJSON']} JSON representation
|
|
38
|
+
*/
|
|
39
|
+
toJSON() {
|
|
40
|
+
return {
|
|
41
|
+
name: this.name,
|
|
42
|
+
code: this.code,
|
|
43
|
+
message: this.message,
|
|
44
|
+
details: this.details,
|
|
45
|
+
stack: this.stack
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validation error - thrown when validation fails
|
|
52
|
+
*
|
|
53
|
+
* @class ValidationError
|
|
54
|
+
* @extends {AIBrowserTestError}
|
|
55
|
+
*/
|
|
56
|
+
export class ValidationError extends AIBrowserTestError {
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} message - Error message
|
|
59
|
+
* @param {Record<string, unknown>} [details={}] - Additional error details
|
|
60
|
+
*/
|
|
61
|
+
constructor(message, details = {}) {
|
|
62
|
+
super(message, 'VALIDATION_ERROR', details);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cache error - thrown when cache operations fail
|
|
68
|
+
*/
|
|
69
|
+
export class CacheError extends AIBrowserTestError {
|
|
70
|
+
constructor(message, details = {}) {
|
|
71
|
+
super(message, 'CACHE_ERROR', details);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Config error - thrown when configuration is invalid
|
|
77
|
+
*/
|
|
78
|
+
export class ConfigError extends AIBrowserTestError {
|
|
79
|
+
constructor(message, details = {}) {
|
|
80
|
+
super(message, 'CONFIG_ERROR', details);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Provider error - thrown when VLLM provider operations fail
|
|
86
|
+
*/
|
|
87
|
+
export class ProviderError extends AIBrowserTestError {
|
|
88
|
+
constructor(message, provider, details = {}) {
|
|
89
|
+
super(message, 'PROVIDER_ERROR', { provider, ...details });
|
|
90
|
+
this.provider = provider;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Timeout error - thrown when operations timeout
|
|
96
|
+
*/
|
|
97
|
+
export class TimeoutError extends AIBrowserTestError {
|
|
98
|
+
constructor(message, timeout, details = {}) {
|
|
99
|
+
super(message, 'TIMEOUT_ERROR', { timeout, ...details });
|
|
100
|
+
this.timeout = timeout;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* File error - thrown when file operations fail
|
|
106
|
+
*/
|
|
107
|
+
export class FileError extends AIBrowserTestError {
|
|
108
|
+
constructor(message, filePath, details = {}) {
|
|
109
|
+
super(message, 'FILE_ERROR', { filePath, ...details });
|
|
110
|
+
this.filePath = filePath;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* State mismatch error - thrown when state validation fails
|
|
116
|
+
*
|
|
117
|
+
* @class StateMismatchError
|
|
118
|
+
* @extends {ValidationError}
|
|
119
|
+
*/
|
|
120
|
+
export class StateMismatchError extends ValidationError {
|
|
121
|
+
/**
|
|
122
|
+
* @param {string[]} discrepancies - List of discrepancies found
|
|
123
|
+
* @param {unknown} extracted - Extracted state
|
|
124
|
+
* @param {unknown} expected - Expected state
|
|
125
|
+
* @param {string} [message] - Custom error message
|
|
126
|
+
*/
|
|
127
|
+
constructor(discrepancies, extracted, expected, message) {
|
|
128
|
+
const defaultMessage = `State mismatch: ${discrepancies.length} discrepancy(ies) found`;
|
|
129
|
+
super(
|
|
130
|
+
message || defaultMessage,
|
|
131
|
+
{
|
|
132
|
+
discrepancies,
|
|
133
|
+
extracted,
|
|
134
|
+
expected,
|
|
135
|
+
discrepancyCount: discrepancies.length
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
this.discrepancies = discrepancies;
|
|
139
|
+
this.extracted = extracted;
|
|
140
|
+
this.expected = expected;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if error is an instance of AIBrowserTestError
|
|
146
|
+
*
|
|
147
|
+
* @param {unknown} error - Error to check
|
|
148
|
+
* @returns {error is AIBrowserTestError} True if error is an AIBrowserTestError
|
|
149
|
+
*/
|
|
150
|
+
export function isAIBrowserTestError(error) {
|
|
151
|
+
return error instanceof AIBrowserTestError;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if error is a specific error type
|
|
156
|
+
*
|
|
157
|
+
* @template {new (...args: any[]) => AIBrowserTestError} T
|
|
158
|
+
* @param {unknown} error - Error to check
|
|
159
|
+
* @param {T} errorClass - Error class constructor
|
|
160
|
+
* @returns {error is InstanceType<T>} True if error is instance of errorClass
|
|
161
|
+
*/
|
|
162
|
+
export function isErrorType(error, errorClass) {
|
|
163
|
+
return error instanceof errorClass;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|