@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,447 @@
1
+ /**
2
+ * Smart Validator Selector
3
+ *
4
+ * Automatically selects the best validator type based on available context.
5
+ * Guides users to the right tool for the job.
6
+ *
7
+ * Design Philosophy:
8
+ * - If you have page access and can measure it programmatically → use programmatic
9
+ * - If you only have screenshots and need semantic evaluation → use VLLM
10
+ * - If you need both accuracy and semantic understanding → use hybrid
11
+ *
12
+ * This prevents the common mistake of using VLLM for measurable things.
13
+ */
14
+
15
+ import { ValidationError } from './errors.mjs';
16
+ import {
17
+ checkElementContrast,
18
+ checkAllTextContrast,
19
+ checkKeyboardNavigation
20
+ } from './validators/accessibility-programmatic.mjs';
21
+ import {
22
+ validateStateProgrammatic as validateStateProg,
23
+ validateElementPosition as validatePosProg
24
+ } from './validators/state-programmatic.mjs';
25
+ import {
26
+ validateAccessibilityHybrid,
27
+ validateStateHybrid
28
+ } from './validators/hybrid-validator.mjs';
29
+ import {
30
+ AccessibilityValidator,
31
+ StateValidator
32
+ } from './validators/index.mjs';
33
+ import { validateScreenshot } from './judge.mjs';
34
+ import { log, warn } from './logger.mjs';
35
+
36
+ /**
37
+ * Smart accessibility validation
38
+ *
39
+ * Automatically chooses the best validator based on available context:
40
+ * - Has page access → uses programmatic (fast, deterministic)
41
+ * - Only has screenshot → uses VLLM (semantic evaluation)
42
+ * - Has both and needs semantic context → uses hybrid (best of both)
43
+ *
44
+ * @param {Object} options - Validation options
45
+ * @param {any} [options.page] - Playwright page object (if available)
46
+ * @param {string} [options.screenshotPath] - Path to screenshot (if available)
47
+ * @param {string} [options.selector] - CSS selector for element (if checking specific element)
48
+ * @param {number} [options.minContrast] - Minimum contrast ratio (default: 4.5)
49
+ * @param {boolean} [options.useHybrid] - Force hybrid validation (default: auto-detect)
50
+ * @param {boolean} [options.needSemantic] - Need semantic evaluation (default: false)
51
+ * @returns {Promise<Object>} Validation result
52
+ */
53
+ export async function validateAccessibilitySmart(options = {}) {
54
+ const {
55
+ page = null,
56
+ screenshotPath = null,
57
+ selector = null,
58
+ minContrast = 4.5,
59
+ useHybrid = null, // null = auto-detect
60
+ needSemantic = false
61
+ } = options;
62
+
63
+ // Validate inputs
64
+ if (!page && !screenshotPath) {
65
+ throw new ValidationError(
66
+ 'validateAccessibilitySmart: Either page or screenshotPath is required',
67
+ { options }
68
+ );
69
+ }
70
+
71
+ // Auto-detect: use hybrid if both available and semantic needed
72
+ const shouldUseHybrid = useHybrid !== null
73
+ ? useHybrid
74
+ : (page && screenshotPath && needSemantic);
75
+
76
+ // Decision tree:
77
+ // 1. Has page access → use programmatic (fast, deterministic)
78
+ // 2. Has both + need semantic → use hybrid (best of both)
79
+ // 3. Only screenshot → use VLLM (semantic evaluation)
80
+
81
+ if (page && !shouldUseHybrid) {
82
+ // Use programmatic validator (fast, deterministic)
83
+ log('[SmartValidator] Using programmatic accessibility validator (fast, deterministic)');
84
+
85
+ if (selector) {
86
+ // Check specific element
87
+ return await checkElementContrast(page, selector, minContrast);
88
+ } else {
89
+ // Check all text elements
90
+ const contrast = await checkAllTextContrast(page, minContrast);
91
+ const keyboard = await checkKeyboardNavigation(page);
92
+
93
+ return {
94
+ contrast,
95
+ keyboard,
96
+ method: 'programmatic',
97
+ speed: 'fast',
98
+ cost: 'free'
99
+ };
100
+ }
101
+ } else if (page && screenshotPath && shouldUseHybrid) {
102
+ // Use hybrid validator (programmatic + VLLM)
103
+ log('[SmartValidator] Using hybrid accessibility validator (programmatic + VLLM)');
104
+
105
+ return await validateAccessibilityHybrid(
106
+ page,
107
+ screenshotPath,
108
+ minContrast,
109
+ { testType: 'accessibility-smart' }
110
+ );
111
+ } else if (screenshotPath) {
112
+ // Use VLLM validator (semantic evaluation)
113
+ log('[SmartValidator] Using VLLM accessibility validator (semantic evaluation)');
114
+ warn('[SmartValidator] Consider using programmatic validator if you have page access (faster, more reliable)');
115
+
116
+ const validator = new AccessibilityValidator({
117
+ minContrast,
118
+ standards: ['WCAG-AA']
119
+ });
120
+
121
+ return await validator.validateAccessibility(screenshotPath, {
122
+ testType: 'accessibility-smart'
123
+ });
124
+ } else {
125
+ throw new ValidationError(
126
+ 'validateAccessibilitySmart: Invalid combination of options',
127
+ { page: !!page, screenshotPath: !!screenshotPath }
128
+ );
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Smart state validation
134
+ *
135
+ * Automatically chooses the best validator based on available context:
136
+ * - Has page access + direct state → uses programmatic (fast, deterministic)
137
+ * - Has page access + screenshot + need semantic → uses hybrid (best of both)
138
+ * - Only screenshot → uses VLLM (extracts state from screenshot)
139
+ *
140
+ * @param {Object} options - Validation options
141
+ * @param {any} [options.page] - Playwright page object (if available)
142
+ * @param {string} [options.screenshotPath] - Path to screenshot (if available)
143
+ * @param {Object} options.expectedState - Expected state object
144
+ * @param {Object} [options.selectors] - Map of state keys to CSS selectors
145
+ * @param {number} [options.tolerance] - Pixel tolerance (default: 5)
146
+ * @param {boolean} [options.useHybrid] - Force hybrid validation (default: auto-detect)
147
+ * @param {boolean} [options.needSemantic] - Need semantic evaluation (default: false)
148
+ * @returns {Promise<Object>} Validation result
149
+ */
150
+ export async function validateStateSmart(options = {}) {
151
+ const {
152
+ page = null,
153
+ screenshotPath = null,
154
+ expectedState,
155
+ selectors = {},
156
+ tolerance = 5,
157
+ useHybrid = null, // null = auto-detect
158
+ needSemantic = false
159
+ } = options;
160
+
161
+ // Validate inputs
162
+ if (!expectedState || typeof expectedState !== 'object') {
163
+ throw new ValidationError(
164
+ 'validateStateSmart: expectedState is required and must be an object',
165
+ { received: typeof expectedState }
166
+ );
167
+ }
168
+
169
+ if (!page && !screenshotPath) {
170
+ throw new ValidationError(
171
+ 'validateStateSmart: Either page or screenshotPath is required',
172
+ { options }
173
+ );
174
+ }
175
+
176
+ // Check if we have direct state access (window.gameState, etc.)
177
+ let hasDirectState = false;
178
+ if (page) {
179
+ try {
180
+ const gameState = await page.evaluate(() => window.gameState || null);
181
+ hasDirectState = gameState !== null;
182
+ } catch (e) {
183
+ // Ignore - no direct state access
184
+ }
185
+ }
186
+
187
+ // Auto-detect: use hybrid if both available and semantic needed
188
+ const shouldUseHybrid = useHybrid !== null
189
+ ? useHybrid
190
+ : (page && screenshotPath && needSemantic);
191
+
192
+ // Decision tree:
193
+ // 1. Has page access + direct state → use programmatic (fast, deterministic)
194
+ // 2. Has page access + selectors → use programmatic (fast, deterministic)
195
+ // 3. Has both + need semantic → use hybrid (best of both)
196
+ // 4. Only screenshot → use VLLM (extracts state from screenshot)
197
+
198
+ if (page && (hasDirectState || Object.keys(selectors).length > 0) && !shouldUseHybrid) {
199
+ // Use programmatic validator (fast, deterministic)
200
+ log('[SmartValidator] Using programmatic state validator (fast, deterministic)');
201
+
202
+ return await validateStateProg(
203
+ page,
204
+ expectedState,
205
+ { selectors, tolerance }
206
+ );
207
+ } else if (page && screenshotPath && shouldUseHybrid) {
208
+ // Use hybrid validator (programmatic + VLLM)
209
+ log('[SmartValidator] Using hybrid state validator (programmatic + VLLM)');
210
+
211
+ return await validateStateHybrid(
212
+ page,
213
+ screenshotPath,
214
+ expectedState,
215
+ { selectors, tolerance, testType: 'state-smart' }
216
+ );
217
+ } else if (screenshotPath) {
218
+ // Use VLLM validator (extracts state from screenshot)
219
+ log('[SmartValidator] Using VLLM state validator (extracts state from screenshot)');
220
+ warn('[SmartValidator] Consider using programmatic validator if you have page access (faster, more reliable)');
221
+
222
+ const validator = new StateValidator({ tolerance });
223
+ return await validator.validateState(
224
+ screenshotPath,
225
+ expectedState,
226
+ { testType: 'state-smart' }
227
+ );
228
+ } else {
229
+ throw new ValidationError(
230
+ 'validateStateSmart: Invalid combination of options',
231
+ { page: !!page, screenshotPath: !!screenshotPath, hasDirectState, selectors: Object.keys(selectors).length }
232
+ );
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Smart element validation
238
+ *
239
+ * Validates element visibility, position, contrast, etc. using the best available method.
240
+ *
241
+ * @param {Object} options - Validation options
242
+ * @param {any} options.page - Playwright page object
243
+ * @param {string} options.selector - CSS selector for element
244
+ * @param {Object} [options.checks] - What to check: { visible, position, contrast }
245
+ * @param {Object} [options.expectedPosition] - Expected position {x, y, width, height}
246
+ * @param {number} [options.minContrast] - Minimum contrast ratio
247
+ * @param {number} [options.tolerance] - Pixel tolerance for position (default: 5)
248
+ * @returns {Promise<Object>} Validation result
249
+ */
250
+ export async function validateElementSmart(options = {}) {
251
+ const {
252
+ page,
253
+ selector,
254
+ checks = { visible: true, position: false, contrast: false },
255
+ expectedPosition = null,
256
+ minContrast = 4.5,
257
+ tolerance = 5
258
+ } = options;
259
+
260
+ if (!page || typeof page.evaluate !== 'function') {
261
+ throw new ValidationError(
262
+ 'validateElementSmart: page is required and must be a Playwright Page object',
263
+ { received: typeof page }
264
+ );
265
+ }
266
+
267
+ if (!selector || typeof selector !== 'string') {
268
+ throw new ValidationError(
269
+ 'validateElementSmart: selector is required and must be a string',
270
+ { received: typeof selector }
271
+ );
272
+ }
273
+
274
+ const results = {};
275
+
276
+ // Always use programmatic (we have page access)
277
+ if (checks.visible) {
278
+ const visible = await page.locator(selector).isVisible();
279
+ results.visible = visible;
280
+ if (!visible) {
281
+ results.errors = results.errors || [];
282
+ results.errors.push(`Element ${selector} is not visible`);
283
+ }
284
+ }
285
+
286
+ if (checks.position && expectedPosition) {
287
+ const position = await validatePosProg(
288
+ page,
289
+ selector,
290
+ expectedPosition,
291
+ tolerance
292
+ );
293
+ results.position = position;
294
+ if (!position.matches) {
295
+ results.errors = results.errors || [];
296
+ results.errors.push(`Element ${selector} position mismatch: ${position.diff}`);
297
+ }
298
+ }
299
+
300
+ if (checks.contrast) {
301
+ const contrast = await checkElementContrast(page, selector, minContrast);
302
+ results.contrast = contrast;
303
+ if (!contrast.passes) {
304
+ results.errors = results.errors || [];
305
+ results.errors.push(`Element ${selector} contrast ${contrast.ratio}:1 < ${minContrast}:1`);
306
+ }
307
+ }
308
+
309
+ results.passes = !results.errors || results.errors.length === 0;
310
+ results.method = 'programmatic';
311
+ results.speed = 'fast';
312
+ results.cost = 'free';
313
+
314
+ return results;
315
+ }
316
+
317
+ /**
318
+ * Smart validation with automatic tool selection
319
+ *
320
+ * This is the main entry point that automatically selects the best validator
321
+ * based on what you're trying to validate and what context you have.
322
+ *
323
+ * @param {Object} options - Validation options
324
+ * @param {string} options.type - Type of validation: 'accessibility', 'state', 'element', 'visual'
325
+ * @param {any} [options.page] - Playwright page object (if available)
326
+ * @param {string} [options.screenshotPath] - Path to screenshot (if available)
327
+ * @param {Object} [options.expectedState] - Expected state (for state validation)
328
+ * @param {string} [options.selector] - CSS selector (for element validation)
329
+ * @param {Object} [options.checks] - What to check (for element validation)
330
+ * @param {string} [options.prompt] - Evaluation prompt (for visual validation)
331
+ * @param {Object} [options.context] - Additional context
332
+ * @returns {Promise<Object>} Validation result
333
+ */
334
+ export async function validateSmart(options = {}) {
335
+ const { type, ...rest } = options;
336
+
337
+ if (!type) {
338
+ throw new ValidationError(
339
+ 'validateSmart: type is required (accessibility, state, element, or visual)',
340
+ { received: type }
341
+ );
342
+ }
343
+
344
+ switch (type) {
345
+ case 'accessibility':
346
+ return await validateAccessibilitySmart(rest);
347
+
348
+ case 'state':
349
+ return await validateStateSmart(rest);
350
+
351
+ case 'element':
352
+ return await validateElementSmart(rest);
353
+
354
+ case 'visual':
355
+ // For visual validation, always use VLLM (semantic evaluation)
356
+ if (!rest.screenshotPath) {
357
+ throw new ValidationError(
358
+ 'validateSmart: screenshotPath is required for visual validation',
359
+ { type }
360
+ );
361
+ }
362
+ if (!rest.prompt) {
363
+ throw new ValidationError(
364
+ 'validateSmart: prompt is required for visual validation',
365
+ { type }
366
+ );
367
+ }
368
+ log('[SmartValidator] Using VLLM for visual validation (semantic evaluation)');
369
+ return await validateScreenshot(
370
+ rest.screenshotPath,
371
+ rest.prompt,
372
+ { testType: 'visual-smart', ...rest.context }
373
+ );
374
+
375
+ default:
376
+ throw new ValidationError(
377
+ `validateSmart: Unknown type "${type}" (must be: accessibility, state, element, or visual)`,
378
+ { received: type }
379
+ );
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Helper to detect if validation can be done programmatically
385
+ *
386
+ * @param {Object} options - Validation options
387
+ * @param {any} [options.page] - Playwright page object
388
+ * @param {string} [options.screenshotPath] - Path to screenshot
389
+ * @param {string} [options.type] - Type of validation
390
+ * @returns {Object} Detection result with recommendations
391
+ */
392
+ export function detectValidationMethod(options = {}) {
393
+ const { page, screenshotPath, type } = options;
394
+
395
+ const hasPage = page && typeof page.evaluate === 'function';
396
+ const hasScreenshot = !!screenshotPath;
397
+
398
+ const recommendations = [];
399
+
400
+ if (type === 'accessibility' || type === 'state') {
401
+ if (hasPage) {
402
+ recommendations.push({
403
+ method: 'programmatic',
404
+ reason: 'Has page access - use programmatic validator (fast, deterministic, free)',
405
+ speed: 'fast',
406
+ cost: 'free',
407
+ reliability: 'high'
408
+ });
409
+ }
410
+
411
+ if (hasPage && hasScreenshot) {
412
+ recommendations.push({
413
+ method: 'hybrid',
414
+ reason: 'Has both page and screenshot - use hybrid validator (programmatic ground truth + VLLM semantic)',
415
+ speed: 'medium',
416
+ cost: 'api',
417
+ reliability: 'high'
418
+ });
419
+ }
420
+
421
+ if (hasScreenshot && !hasPage) {
422
+ recommendations.push({
423
+ method: 'vllm',
424
+ reason: 'Only has screenshot - use VLLM validator (semantic evaluation)',
425
+ speed: 'slow',
426
+ cost: 'api',
427
+ reliability: 'medium'
428
+ });
429
+ }
430
+ } else if (type === 'visual') {
431
+ recommendations.push({
432
+ method: 'vllm',
433
+ reason: 'Visual validation requires semantic evaluation - use VLLM',
434
+ speed: 'slow',
435
+ cost: 'api',
436
+ reliability: 'medium'
437
+ });
438
+ }
439
+
440
+ return {
441
+ hasPage,
442
+ hasScreenshot,
443
+ recommendations,
444
+ recommended: recommendations[0] || null
445
+ };
446
+ }
447
+
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Configuration for Natural Language Specs
3
+ *
4
+ * Makes spec parsing, execution, and error analysis configurable per project/test.
5
+ * Follows the library's config pattern (like createConfig in config.mjs).
6
+ */
7
+
8
+ import { getConfig } from './config.mjs';
9
+ import { log } from './logger.mjs';
10
+
11
+ /**
12
+ * Default spec configuration
13
+ */
14
+ const DEFAULT_SPEC_CONFIG = {
15
+ // Context extraction
16
+ useLLM: true,
17
+ fallback: 'regex',
18
+ provider: null, // Auto-detect from config
19
+
20
+ // Spec validation
21
+ validateBeforeExecute: true,
22
+ strictValidation: false, // If true, throw on validation errors
23
+
24
+ // Error analysis
25
+ enableErrorAnalysis: true,
26
+ errorAnalysisOptions: {
27
+ saveReport: true,
28
+ outputPath: null
29
+ },
30
+
31
+ // Template system
32
+ templateDir: null, // Custom template directory
33
+ autoLoadTemplates: true,
34
+
35
+ // Execution
36
+ timeout: 30000,
37
+ retryOnFailure: false,
38
+ maxRetries: 3
39
+ };
40
+
41
+ /**
42
+ * Create spec configuration
43
+ *
44
+ * Merges with global config and environment variables.
45
+ *
46
+ * @param {Object} [options={}] - Configuration options
47
+ * @returns {Object} Spec configuration
48
+ */
49
+ export function createSpecConfig(options = {}) {
50
+ const globalConfig = getConfig();
51
+
52
+ // Merge with defaults
53
+ const config = {
54
+ ...DEFAULT_SPEC_CONFIG,
55
+ ...options
56
+ };
57
+
58
+ // Auto-detect provider from global config if not specified
59
+ if (!config.provider && globalConfig.provider) {
60
+ config.provider = globalConfig.provider;
61
+ }
62
+
63
+ // Respect environment variables
64
+ if (process.env.SPEC_USE_LLM !== undefined) {
65
+ config.useLLM = process.env.SPEC_USE_LLM === 'true';
66
+ }
67
+ if (process.env.SPEC_VALIDATE_BEFORE_EXECUTE !== undefined) {
68
+ config.validateBeforeExecute = process.env.SPEC_VALIDATE_BEFORE_EXECUTE === 'true';
69
+ }
70
+ if (process.env.SPEC_STRICT_VALIDATION !== undefined) {
71
+ config.strictValidation = process.env.SPEC_STRICT_VALIDATION === 'true';
72
+ }
73
+ if (process.env.SPEC_TEMPLATE_DIR) {
74
+ config.templateDir = process.env.SPEC_TEMPLATE_DIR;
75
+ }
76
+
77
+ return config;
78
+ }
79
+
80
+ /**
81
+ * Get current spec configuration (singleton)
82
+ */
83
+ let specConfigInstance = null;
84
+
85
+ export function getSpecConfig() {
86
+ if (!specConfigInstance) {
87
+ specConfigInstance = createSpecConfig();
88
+ }
89
+ return specConfigInstance;
90
+ }
91
+
92
+ /**
93
+ * Set spec configuration (useful for testing)
94
+ */
95
+ export function setSpecConfig(config) {
96
+ specConfigInstance = config;
97
+ log('[SpecConfig] Configuration updated');
98
+ }
99
+
100
+ /**
101
+ * Reset spec configuration to defaults
102
+ */
103
+ export function resetSpecConfig() {
104
+ specConfigInstance = null;
105
+ }
106
+