@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.
Files changed (93) hide show
  1. package/.secretsignore.example +20 -0
  2. package/CHANGELOG.md +360 -0
  3. package/CONTRIBUTING.md +63 -0
  4. package/DEPLOYMENT.md +80 -0
  5. package/LICENSE +22 -0
  6. package/README.md +142 -0
  7. package/SECURITY.md +108 -0
  8. package/api/health.js +34 -0
  9. package/api/validate.js +252 -0
  10. package/index.d.ts +1221 -0
  11. package/package.json +112 -0
  12. package/public/index.html +149 -0
  13. package/src/batch-optimizer.mjs +451 -0
  14. package/src/bias-detector.mjs +370 -0
  15. package/src/bias-mitigation.mjs +233 -0
  16. package/src/cache.mjs +433 -0
  17. package/src/config.mjs +268 -0
  18. package/src/constants.mjs +80 -0
  19. package/src/context-compressor.mjs +350 -0
  20. package/src/convenience.mjs +617 -0
  21. package/src/cost-tracker.mjs +257 -0
  22. package/src/cross-modal-consistency.mjs +170 -0
  23. package/src/data-extractor.mjs +232 -0
  24. package/src/dynamic-few-shot.mjs +140 -0
  25. package/src/dynamic-prompts.mjs +361 -0
  26. package/src/ensemble/index.mjs +53 -0
  27. package/src/ensemble-judge.mjs +366 -0
  28. package/src/error-handler.mjs +67 -0
  29. package/src/errors.mjs +167 -0
  30. package/src/experience-propagation.mjs +128 -0
  31. package/src/experience-tracer.mjs +487 -0
  32. package/src/explanation-manager.mjs +299 -0
  33. package/src/feedback-aggregator.mjs +248 -0
  34. package/src/game-goal-prompts.mjs +478 -0
  35. package/src/game-player.mjs +548 -0
  36. package/src/hallucination-detector.mjs +155 -0
  37. package/src/helpers/playwright.mjs +80 -0
  38. package/src/human-validation-manager.mjs +516 -0
  39. package/src/index.mjs +364 -0
  40. package/src/judge.mjs +929 -0
  41. package/src/latency-aware-batch-optimizer.mjs +192 -0
  42. package/src/load-env.mjs +159 -0
  43. package/src/logger.mjs +55 -0
  44. package/src/metrics.mjs +187 -0
  45. package/src/model-tier-selector.mjs +221 -0
  46. package/src/multi-modal/index.mjs +36 -0
  47. package/src/multi-modal-fusion.mjs +190 -0
  48. package/src/multi-modal.mjs +524 -0
  49. package/src/natural-language-specs.mjs +1071 -0
  50. package/src/pair-comparison.mjs +277 -0
  51. package/src/persona/index.mjs +42 -0
  52. package/src/persona-enhanced.mjs +200 -0
  53. package/src/persona-experience.mjs +572 -0
  54. package/src/position-counterbalance.mjs +140 -0
  55. package/src/prompt-composer.mjs +375 -0
  56. package/src/render-change-detector.mjs +583 -0
  57. package/src/research-enhanced-validation.mjs +436 -0
  58. package/src/retry.mjs +152 -0
  59. package/src/rubrics.mjs +231 -0
  60. package/src/score-tracker.mjs +277 -0
  61. package/src/smart-validator.mjs +447 -0
  62. package/src/spec-config.mjs +106 -0
  63. package/src/spec-templates.mjs +347 -0
  64. package/src/specs/index.mjs +38 -0
  65. package/src/temporal/index.mjs +102 -0
  66. package/src/temporal-adaptive.mjs +163 -0
  67. package/src/temporal-batch-optimizer.mjs +222 -0
  68. package/src/temporal-constants.mjs +69 -0
  69. package/src/temporal-context.mjs +49 -0
  70. package/src/temporal-decision-manager.mjs +271 -0
  71. package/src/temporal-decision.mjs +669 -0
  72. package/src/temporal-errors.mjs +58 -0
  73. package/src/temporal-note-pruner.mjs +173 -0
  74. package/src/temporal-preprocessor.mjs +543 -0
  75. package/src/temporal-prompt-formatter.mjs +219 -0
  76. package/src/temporal-validation.mjs +159 -0
  77. package/src/temporal.mjs +415 -0
  78. package/src/type-guards.mjs +311 -0
  79. package/src/uncertainty-reducer.mjs +470 -0
  80. package/src/utils/index.mjs +175 -0
  81. package/src/validation-framework.mjs +321 -0
  82. package/src/validation-result-normalizer.mjs +64 -0
  83. package/src/validation.mjs +243 -0
  84. package/src/validators/accessibility-programmatic.mjs +345 -0
  85. package/src/validators/accessibility-validator.mjs +223 -0
  86. package/src/validators/batch-validator.mjs +143 -0
  87. package/src/validators/hybrid-validator.mjs +268 -0
  88. package/src/validators/index.mjs +34 -0
  89. package/src/validators/prompt-builder.mjs +218 -0
  90. package/src/validators/rubric.mjs +85 -0
  91. package/src/validators/state-programmatic.mjs +260 -0
  92. package/src/validators/state-validator.mjs +291 -0
  93. package/vercel.json +27 -0
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Validation Framework
3
+ *
4
+ * Provides comprehensive validation of:
5
+ * 1. Temporal perception accuracy (human time scales)
6
+ * 2. VLLM judgment accuracy (against human ground truth)
7
+ * 3. Gameplay temporal experience correctness
8
+ * 4. Webpage evaluation correctness
9
+ *
10
+ * This framework validates that our systems produce correct results,
11
+ * not just that they work.
12
+ */
13
+
14
+ import { humanPerceptionTime } from './temporal-decision.mjs';
15
+ import { TIME_SCALES } from './temporal-constants.mjs';
16
+ import { aggregateTemporalNotes, calculateCoherenceExported as calculateCoherence } from './temporal.mjs';
17
+ import {
18
+ compareJudgments,
19
+ collectHumanJudgment,
20
+ loadHumanJudgment
21
+ } from '../evaluation/human-validation/human-validation.mjs';
22
+ import { getHumanValidationManager } from './human-validation-manager.mjs';
23
+ import { log, warn } from './logger.mjs';
24
+
25
+ /**
26
+ * Validate temporal perception against research values
27
+ *
28
+ * @param {Object} options - Validation options
29
+ * @returns {Object} Validation results
30
+ */
31
+ export function validateTemporalPerception(options = {}) {
32
+ const results = {
33
+ researchAlignment: {},
34
+ consistency: {},
35
+ recommendations: []
36
+ };
37
+
38
+ // Validate visual appeal time (50ms research base)
39
+ const visualAppealTime = humanPerceptionTime('visual-appeal', {
40
+ attentionLevel: 'focused',
41
+ actionComplexity: 'simple'
42
+ });
43
+
44
+ results.researchAlignment.visualAppeal = {
45
+ researchValue: 50, // Lindgaard research
46
+ actualValue: visualAppealTime,
47
+ aligned: visualAppealTime >= 50 && visualAppealTime <= 200,
48
+ note: visualAppealTime >= 100 ? 'Enforces 100ms minimum (implementation constraint)' : 'Matches research (50ms)'
49
+ };
50
+
51
+ // Validate instant threshold (100ms research)
52
+ results.researchAlignment.instantThreshold = {
53
+ researchValue: 100, // NN/g research
54
+ actualValue: TIME_SCALES.INSTANT,
55
+ aligned: TIME_SCALES.INSTANT === 100,
56
+ note: TIME_SCALES.INSTANT === 100 ? 'Matches research' : 'Does not match research'
57
+ };
58
+
59
+ // Validate reading time scales with content
60
+ const shortReading = humanPerceptionTime('reading', { contentLength: 100 });
61
+ const longReading = humanPerceptionTime('reading', { contentLength: 1000 });
62
+
63
+ results.researchAlignment.readingTime = {
64
+ scalesWithContent: longReading > shortReading,
65
+ shortContent: shortReading,
66
+ longContent: longReading,
67
+ note: longReading > shortReading ? 'Reading time scales with content' : 'Reading time does not scale correctly'
68
+ };
69
+
70
+ // Generate recommendations
71
+ if (!results.researchAlignment.visualAppeal.aligned) {
72
+ results.recommendations.push('Visual appeal time does not align with research (50ms)');
73
+ }
74
+ if (!results.researchAlignment.instantThreshold.aligned) {
75
+ results.recommendations.push('Instant threshold does not match research (100ms)');
76
+ }
77
+ if (!results.researchAlignment.readingTime.scalesWithContent) {
78
+ results.recommendations.push('Reading time does not scale correctly with content length');
79
+ }
80
+
81
+ return results;
82
+ }
83
+
84
+ /**
85
+ * Validate VLLM judgment accuracy against human ground truth
86
+ *
87
+ * @param {Array} humanJudgments - Human judgment ground truth
88
+ * @param {Array} vllmJudgments - VLLM judgments to validate
89
+ * @param {Object} options - Validation options
90
+ * @returns {Object} Validation results
91
+ */
92
+ export function validateVLLMAccuracy(humanJudgments, vllmJudgments, options = {}) {
93
+ const {
94
+ minCorrelation = 0.7,
95
+ maxMAE = 1.0,
96
+ minKappa = 0.6
97
+ } = options;
98
+
99
+ try {
100
+ const calibration = compareJudgments(humanJudgments, vllmJudgments);
101
+
102
+ const results = {
103
+ calibration,
104
+ isValid: false,
105
+ issues: [],
106
+ recommendations: []
107
+ };
108
+
109
+ // Check correlation
110
+ if (calibration.agreement.pearson < minCorrelation) {
111
+ results.issues.push(`Low correlation (${calibration.agreement.pearson.toFixed(3)} < ${minCorrelation})`);
112
+ results.isValid = false;
113
+ }
114
+
115
+ // Check MAE
116
+ if (calibration.agreement.mae > maxMAE) {
117
+ results.issues.push(`High MAE (${calibration.agreement.mae.toFixed(2)} > ${maxMAE})`);
118
+ results.isValid = false;
119
+ }
120
+
121
+ // Check Kappa
122
+ if (calibration.agreement.kappa < minKappa) {
123
+ results.issues.push(`Low Kappa (${calibration.agreement.kappa.toFixed(3)} < ${minKappa})`);
124
+ results.isValid = false;
125
+ }
126
+
127
+ // If all checks pass
128
+ if (results.issues.length === 0) {
129
+ results.isValid = true;
130
+ results.recommendations.push('VLLM judgments align well with human ground truth');
131
+ } else {
132
+ results.recommendations.push(...calibration.recommendations);
133
+ }
134
+
135
+ return results;
136
+ } catch (error) {
137
+ return {
138
+ error: error.message,
139
+ isValid: false,
140
+ issues: ['Failed to compare judgments'],
141
+ recommendations: ['Ensure human and VLLM judgments are properly matched']
142
+ };
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Validate gameplay temporal experience
148
+ *
149
+ * @param {Array} gameplayNotes - Temporal notes from gameplay
150
+ * @param {Object} options - Validation options
151
+ * @returns {Object} Validation results
152
+ */
153
+ export function validateGameplayTemporal(gameplayNotes, options = {}) {
154
+ const {
155
+ minCoherenceForSmooth = 0.7,
156
+ maxCoherenceForErratic = 0.5
157
+ } = options;
158
+
159
+ if (!gameplayNotes || gameplayNotes.length === 0) {
160
+ return {
161
+ isValid: false,
162
+ issues: ['No gameplay notes provided'],
163
+ recommendations: ['Provide gameplay notes for validation']
164
+ };
165
+ }
166
+
167
+ const aggregated = aggregateTemporalNotes(gameplayNotes, options);
168
+
169
+ const results = {
170
+ aggregated,
171
+ isValid: true,
172
+ issues: [],
173
+ recommendations: []
174
+ };
175
+
176
+ // Check coherence
177
+ if (aggregated.coherence < minCoherenceForSmooth && aggregated.coherence > maxCoherenceForErratic) {
178
+ results.issues.push(`Moderate coherence (${aggregated.coherence.toFixed(3)}) - neither smooth nor clearly erratic`);
179
+ results.recommendations.push('Review gameplay notes for consistency issues');
180
+ }
181
+
182
+ // Check for conflicts
183
+ if (aggregated.conflicts && aggregated.conflicts.length > 0) {
184
+ results.issues.push(`Detected ${aggregated.conflicts.length} conflicts in gameplay notes`);
185
+ results.recommendations.push('Review conflicting observations in gameplay notes');
186
+ }
187
+
188
+ // Check window count
189
+ if (aggregated.windows.length < 2) {
190
+ results.issues.push('Insufficient windows for temporal analysis (need at least 2)');
191
+ results.recommendations.push('Collect more gameplay notes or use smaller window size');
192
+ }
193
+
194
+ return results;
195
+ }
196
+
197
+ /**
198
+ * Validate webpage evaluation correctness
199
+ *
200
+ * Validates that VLLM judgments about webpages align with human expectations.
201
+ *
202
+ * @param {Array} evaluations - Array of evaluation results
203
+ * @param {Object} groundTruth - Ground truth data (if available)
204
+ * @param {Object} options - Validation options
205
+ * @returns {Object} Validation results
206
+ */
207
+ export function validateWebpageEvaluation(evaluations, groundTruth = null, options = {}) {
208
+ const results = {
209
+ evaluations,
210
+ isValid: true,
211
+ issues: [],
212
+ recommendations: []
213
+ };
214
+
215
+ // If ground truth available, compare
216
+ if (groundTruth) {
217
+ const humanJudgments = groundTruth.humanJudgments || [];
218
+ const vllmJudgments = evaluations.map(eval => ({
219
+ id: eval.id || `eval-${Date.now()}`,
220
+ vllmScore: eval.score,
221
+ vllmIssues: eval.issues || [],
222
+ vllmReasoning: eval.reasoning || '',
223
+ provider: eval.provider || 'unknown',
224
+ timestamp: eval.timestamp || new Date().toISOString()
225
+ }));
226
+
227
+ if (humanJudgments.length > 0 && vllmJudgments.length > 0) {
228
+ const accuracy = validateVLLMAccuracy(humanJudgments, vllmJudgments, options);
229
+ results.accuracy = accuracy;
230
+ results.isValid = accuracy.isValid;
231
+ results.issues.push(...accuracy.issues);
232
+ results.recommendations.push(...accuracy.recommendations);
233
+ }
234
+ }
235
+
236
+ // Validate evaluation structure
237
+ for (const eval of evaluations) {
238
+ if (eval.score === null || eval.score === undefined) {
239
+ results.issues.push(`Evaluation ${eval.id || 'unknown'} has null/undefined score`);
240
+ results.isValid = false;
241
+ }
242
+ if (eval.score !== null && (eval.score < 0 || eval.score > 10)) {
243
+ results.issues.push(`Evaluation ${eval.id || 'unknown'} has invalid score: ${eval.score}`);
244
+ results.isValid = false;
245
+ }
246
+ if (!Array.isArray(eval.issues)) {
247
+ results.issues.push(`Evaluation ${eval.id || 'unknown'} has non-array issues`);
248
+ results.isValid = false;
249
+ }
250
+ }
251
+
252
+ return results;
253
+ }
254
+
255
+ /**
256
+ * Comprehensive validation report
257
+ *
258
+ * Validates all aspects: temporal perception, VLLM accuracy, gameplay, webpage evaluation
259
+ *
260
+ * @param {Object} data - Validation data
261
+ * @param {Object} options - Validation options
262
+ * @returns {Object} Comprehensive validation report
263
+ */
264
+ export function validateComprehensive(data, options = {}) {
265
+ const report = {
266
+ temporalPerception: null,
267
+ vllmAccuracy: null,
268
+ gameplayTemporal: null,
269
+ webpageEvaluation: null,
270
+ overall: {
271
+ isValid: true,
272
+ issues: [],
273
+ recommendations: []
274
+ }
275
+ };
276
+
277
+ // Validate temporal perception
278
+ if (data.temporalPerception !== false) {
279
+ report.temporalPerception = validateTemporalPerception(options);
280
+ if (report.temporalPerception.recommendations.length > 0) {
281
+ report.overall.issues.push('Temporal perception validation issues');
282
+ report.overall.recommendations.push(...report.temporalPerception.recommendations);
283
+ }
284
+ }
285
+
286
+ // Validate VLLM accuracy
287
+ if (data.humanJudgments && data.vllmJudgments) {
288
+ report.vllmAccuracy = validateVLLMAccuracy(data.humanJudgments, data.vllmJudgments, options);
289
+ if (!report.vllmAccuracy.isValid) {
290
+ report.overall.isValid = false;
291
+ report.overall.issues.push('VLLM accuracy validation failed');
292
+ report.overall.recommendations.push(...report.vllmAccuracy.recommendations);
293
+ }
294
+ }
295
+
296
+ // Validate gameplay temporal
297
+ if (data.gameplayNotes) {
298
+ report.gameplayTemporal = validateGameplayTemporal(data.gameplayNotes, options);
299
+ if (report.gameplayTemporal.issues.length > 0) {
300
+ report.overall.issues.push('Gameplay temporal validation issues');
301
+ report.overall.recommendations.push(...report.gameplayTemporal.recommendations);
302
+ }
303
+ }
304
+
305
+ // Validate webpage evaluation
306
+ if (data.evaluations) {
307
+ report.webpageEvaluation = validateWebpageEvaluation(
308
+ data.evaluations,
309
+ data.groundTruth,
310
+ options
311
+ );
312
+ if (!report.webpageEvaluation.isValid) {
313
+ report.overall.isValid = false;
314
+ report.overall.issues.push('Webpage evaluation validation failed');
315
+ report.overall.recommendations.push(...report.webpageEvaluation.recommendations);
316
+ }
317
+ }
318
+
319
+ return report;
320
+ }
321
+
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Validation Result Normalizer
3
+ *
4
+ * Ensures validation results have consistent structure.
5
+ * Centralizes normalization logic to avoid duplication.
6
+ */
7
+
8
+ import { warn } from './logger.mjs';
9
+
10
+ /**
11
+ * Normalize validation result to ensure consistent structure
12
+ *
13
+ * @param {Object} result - Validation result from validateScreenshot
14
+ * @param {string} [source] - Source function name for logging
15
+ * @returns {Object} Normalized validation result
16
+ */
17
+ export function normalizeValidationResult(result, source = 'unknown') {
18
+ if (!result) {
19
+ warn(`[Normalizer] ${source}: result is null/undefined`);
20
+ return {
21
+ enabled: false,
22
+ score: null,
23
+ issues: [],
24
+ reasoning: 'Result was null or undefined',
25
+ assessment: null
26
+ };
27
+ }
28
+
29
+ // Create a copy to avoid mutating the original
30
+ const normalized = { ...result };
31
+
32
+ // Ensure enabled field is always present (default to true if not specified)
33
+ if (normalized.enabled === undefined) {
34
+ // If enabled is not specified, infer from presence of other fields
35
+ normalized.enabled = normalized.score !== null || normalized.judgment || normalized.provider !== undefined;
36
+ }
37
+
38
+ // Ensure score is always present (may be null)
39
+ if (normalized.score === null || normalized.score === undefined) {
40
+ if (normalized.score === undefined) {
41
+ warn(`[Normalizer] ${source}: score is undefined, defaulting to null`);
42
+ }
43
+ normalized.score = null;
44
+ }
45
+
46
+ // Ensure issues is always an array
47
+ if (!Array.isArray(normalized.issues)) {
48
+ warn(`[Normalizer] ${source}: issues is not an array, defaulting to empty array`);
49
+ normalized.issues = [];
50
+ }
51
+
52
+ // Ensure reasoning is always present
53
+ if (!normalized.reasoning) {
54
+ normalized.reasoning = normalized.judgment || normalized.message || 'No reasoning provided';
55
+ }
56
+
57
+ // Ensure assessment is present (may be null)
58
+ if (normalized.assessment === undefined) {
59
+ normalized.assessment = null;
60
+ }
61
+
62
+ return normalized;
63
+ }
64
+
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Input Validation Utilities
3
+ *
4
+ * Provides validation functions for common input types to prevent
5
+ * security vulnerabilities and improve error messages.
6
+ */
7
+
8
+ import { ValidationError } from './errors.mjs';
9
+ import { existsSync } from 'fs';
10
+ import { normalize, resolve } from 'path';
11
+
12
+ /**
13
+ * Validate image file path
14
+ *
15
+ * @param {string} imagePath - Path to image file
16
+ * @returns {true} Always returns true if valid
17
+ * @throws {ValidationError} If path is invalid, empty, or contains path traversal
18
+ */
19
+ export function validateImagePath(imagePath) {
20
+ if (typeof imagePath !== 'string') {
21
+ throw new ValidationError('Image path must be a string', null, {
22
+ received: typeof imagePath
23
+ });
24
+ }
25
+
26
+ if (imagePath.length === 0) {
27
+ throw new ValidationError('Image path cannot be empty');
28
+ }
29
+
30
+ // Check for path traversal attempts
31
+ const normalized = normalize(imagePath);
32
+ if (normalized.includes('..')) {
33
+ throw new ValidationError('Invalid image path: path traversal detected', imagePath);
34
+ }
35
+
36
+ // Validate file extension
37
+ const validExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
38
+ const hasValidExtension = validExtensions.some(ext =>
39
+ imagePath.toLowerCase().endsWith(ext)
40
+ );
41
+
42
+ if (!hasValidExtension) {
43
+ throw new ValidationError('Invalid image format. Supported: png, jpg, jpeg, gif, webp', imagePath);
44
+ }
45
+
46
+ // Check if file exists (optional - may not exist in all contexts)
47
+ // This is a soft check - don't throw if file doesn't exist yet
48
+ // The actual file operations will handle missing files
49
+
50
+ return true;
51
+ }
52
+
53
+ /**
54
+ * Validate prompt string
55
+ *
56
+ * @param {string} prompt - Prompt text to validate
57
+ * @param {number} [maxLength=10000] - Maximum allowed length
58
+ * @returns {true} Always returns true if valid
59
+ * @throws {ValidationError} If prompt is invalid, empty, or too long
60
+ */
61
+ export function validatePrompt(prompt, maxLength = 10000) {
62
+ if (typeof prompt !== 'string') {
63
+ throw new ValidationError('Prompt must be a string', null, {
64
+ received: typeof prompt
65
+ });
66
+ }
67
+
68
+ if (prompt.length === 0) {
69
+ throw new ValidationError('Prompt cannot be empty');
70
+ }
71
+
72
+ if (prompt.length > maxLength) {
73
+ throw new ValidationError(`Prompt too long (max ${maxLength} characters)`, null, {
74
+ length: prompt.length,
75
+ maxLength
76
+ });
77
+ }
78
+
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * Validate context object
84
+ *
85
+ * @param {unknown} context - Context object to validate (can be null/undefined)
86
+ * @param {number} [maxSize=50000] - Maximum serialized size in bytes
87
+ * @returns {true} Always returns true if valid
88
+ * @throws {ValidationError} If context is invalid type, too large, or non-serializable
89
+ */
90
+ export function validateContext(context, maxSize = 50000) {
91
+ if (context === null || context === undefined) {
92
+ return true; // Context is optional
93
+ }
94
+
95
+ if (typeof context !== 'object' || Array.isArray(context)) {
96
+ throw new ValidationError('Context must be an object', null, {
97
+ received: Array.isArray(context) ? 'array' : typeof context
98
+ });
99
+ }
100
+
101
+ // Check size by stringifying
102
+ try {
103
+ const contextString = JSON.stringify(context);
104
+ if (contextString.length > maxSize) {
105
+ throw new ValidationError(`Context too large (max ${maxSize} bytes)`, null, {
106
+ size: contextString.length,
107
+ maxSize
108
+ });
109
+ }
110
+ } catch (error) {
111
+ if (error instanceof ValidationError) {
112
+ throw error;
113
+ }
114
+ throw new ValidationError('Context contains non-serializable data', null, {
115
+ originalError: error.message
116
+ });
117
+ }
118
+
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Validate timeout value
124
+ *
125
+ * @param {number} timeout - Timeout value in milliseconds
126
+ * @param {number} [min=1000] - Minimum allowed timeout
127
+ * @param {number} [max=300000] - Maximum allowed timeout
128
+ * @returns {true} Always returns true if valid
129
+ * @throws {ValidationError} If timeout is invalid, too short, or too long
130
+ */
131
+ export function validateTimeout(timeout, min = 1000, max = 300000) {
132
+ if (typeof timeout !== 'number') {
133
+ throw new ValidationError('Timeout must be a number', null, {
134
+ received: typeof timeout
135
+ });
136
+ }
137
+
138
+ if (timeout < min) {
139
+ throw new ValidationError(`Timeout too short (min ${min}ms)`, null, {
140
+ timeout,
141
+ min
142
+ });
143
+ }
144
+
145
+ if (timeout > max) {
146
+ throw new ValidationError(`Timeout too long (max ${max}ms)`, null, {
147
+ timeout,
148
+ max
149
+ });
150
+ }
151
+
152
+ return true;
153
+ }
154
+
155
+ /**
156
+ * Validate schema object for data extraction
157
+ *
158
+ * @param {unknown} schema - Schema object to validate
159
+ * @returns {true} Always returns true if valid
160
+ * @throws {ValidationError} If schema is invalid or has invalid field types
161
+ */
162
+ export function validateSchema(schema) {
163
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
164
+ throw new ValidationError('Schema must be a non-empty object', null, {
165
+ received: Array.isArray(schema) ? 'array' : typeof schema
166
+ });
167
+ }
168
+
169
+ const validTypes = ['string', 'number', 'boolean', 'object', 'array'];
170
+
171
+ for (const [key, field] of Object.entries(schema)) {
172
+ if (typeof field !== 'object' || Array.isArray(field)) {
173
+ throw new ValidationError(`Schema field "${key}" must be an object`, null, {
174
+ field
175
+ });
176
+ }
177
+
178
+ if (!field.type || !validTypes.includes(field.type)) {
179
+ throw new ValidationError(`Schema field "${key}" has invalid type`, null, {
180
+ type: field.type,
181
+ validTypes
182
+ });
183
+ }
184
+ }
185
+
186
+ return true;
187
+ }
188
+
189
+ /**
190
+ * Validate file path (general purpose)
191
+ *
192
+ * @param {string} filePath - File path to validate
193
+ * @param {{
194
+ * mustExist?: boolean;
195
+ * allowedExtensions?: string[] | null;
196
+ * maxLength?: number;
197
+ * }} [options={}] - Validation options
198
+ * @returns {true} Always returns true if valid
199
+ * @throws {ValidationError} If path is invalid, empty, too long, has path traversal, wrong extension, or doesn't exist (if required)
200
+ */
201
+ export function validateFilePath(filePath, options = {}) {
202
+ const {
203
+ mustExist = false,
204
+ allowedExtensions = null,
205
+ maxLength = 4096
206
+ } = options;
207
+
208
+ if (typeof filePath !== 'string') {
209
+ throw new ValidationError('File path must be a string', null, {
210
+ received: typeof filePath
211
+ });
212
+ }
213
+
214
+ if (filePath.length === 0) {
215
+ throw new ValidationError('File path cannot be empty');
216
+ }
217
+
218
+ if (filePath.length > maxLength) {
219
+ throw new ValidationError(`File path too long (max ${maxLength} characters)`, filePath);
220
+ }
221
+
222
+ // Check for path traversal
223
+ const normalized = normalize(filePath);
224
+ if (normalized.includes('..')) {
225
+ throw new ValidationError('Invalid file path: path traversal detected', filePath);
226
+ }
227
+
228
+ // Check extension if specified
229
+ if (allowedExtensions) {
230
+ const ext = normalized.substring(normalized.lastIndexOf('.'));
231
+ if (!allowedExtensions.includes(ext.toLowerCase())) {
232
+ throw new ValidationError(`Invalid file extension. Allowed: ${allowedExtensions.join(', ')}`, filePath);
233
+ }
234
+ }
235
+
236
+ // Check existence if required
237
+ if (mustExist && !existsSync(filePath)) {
238
+ throw new ValidationError('File does not exist', filePath);
239
+ }
240
+
241
+ return true;
242
+ }
243
+