agentic-qe 2.4.0 → 2.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 (174) hide show
  1. package/.claude/agents/qe-a11y-ally.md +855 -0
  2. package/.claude/agents/qx-partner.md +120 -4
  3. package/.claude/skills/testability-scoring/SKILL.md +107 -6
  4. package/CHANGELOG.md +135 -0
  5. package/README.md +7 -6
  6. package/dist/agents/AccessibilityAllyAgent.d.ts +168 -0
  7. package/dist/agents/AccessibilityAllyAgent.d.ts.map +1 -0
  8. package/dist/agents/AccessibilityAllyAgent.js +462 -0
  9. package/dist/agents/AccessibilityAllyAgent.js.map +1 -0
  10. package/dist/agents/SONAIntegration.d.ts +109 -0
  11. package/dist/agents/SONAIntegration.d.ts.map +1 -0
  12. package/dist/agents/SONAIntegration.js +167 -0
  13. package/dist/agents/SONAIntegration.js.map +1 -0
  14. package/dist/agents/index.d.ts +3 -0
  15. package/dist/agents/index.d.ts.map +1 -1
  16. package/dist/agents/index.js +93 -2
  17. package/dist/agents/index.js.map +1 -1
  18. package/dist/cli/init/agents.js +1 -1
  19. package/dist/cli/init/claude-config.js +2 -2
  20. package/dist/cli/init/database-init.js +1 -1
  21. package/dist/core/cache/BinaryCacheImpl.d.ts +161 -0
  22. package/dist/core/cache/BinaryCacheImpl.d.ts.map +1 -0
  23. package/dist/core/cache/BinaryCacheImpl.js +685 -0
  24. package/dist/core/cache/BinaryCacheImpl.js.map +1 -0
  25. package/dist/core/cache/BinaryMetadataCache.d.ts +244 -0
  26. package/dist/core/cache/BinaryMetadataCache.d.ts.map +1 -1
  27. package/dist/core/cache/BinaryMetadataCache.js +63 -1
  28. package/dist/core/cache/BinaryMetadataCache.js.map +1 -1
  29. package/dist/core/cache/index.d.ts +1 -0
  30. package/dist/core/cache/index.d.ts.map +1 -1
  31. package/dist/core/cache/index.js +10 -1
  32. package/dist/core/cache/index.js.map +1 -1
  33. package/dist/core/memory/AgentDBService.d.ts +30 -4
  34. package/dist/core/memory/AgentDBService.d.ts.map +1 -1
  35. package/dist/core/memory/AgentDBService.js +122 -12
  36. package/dist/core/memory/AgentDBService.js.map +1 -1
  37. package/dist/core/memory/CachedHNSWVectorMemory.d.ts +153 -0
  38. package/dist/core/memory/CachedHNSWVectorMemory.d.ts.map +1 -0
  39. package/dist/core/memory/CachedHNSWVectorMemory.js +329 -0
  40. package/dist/core/memory/CachedHNSWVectorMemory.js.map +1 -0
  41. package/dist/core/memory/HNSWVectorMemory.js +1 -1
  42. package/dist/core/memory/RuVectorPatternStore.d.ts.map +1 -1
  43. package/dist/core/memory/RuVectorPatternStore.js +8 -2
  44. package/dist/core/memory/RuVectorPatternStore.js.map +1 -1
  45. package/dist/core/memory/UnifiedMemoryCoordinator.d.ts +50 -0
  46. package/dist/core/memory/UnifiedMemoryCoordinator.d.ts.map +1 -1
  47. package/dist/core/memory/UnifiedMemoryCoordinator.js +206 -0
  48. package/dist/core/memory/UnifiedMemoryCoordinator.js.map +1 -1
  49. package/dist/core/memory/index.d.ts +2 -0
  50. package/dist/core/memory/index.d.ts.map +1 -1
  51. package/dist/core/memory/index.js +8 -1
  52. package/dist/core/memory/index.js.map +1 -1
  53. package/dist/core/optimization/RecursiveOptimizer.d.ts +233 -0
  54. package/dist/core/optimization/RecursiveOptimizer.d.ts.map +1 -0
  55. package/dist/core/optimization/RecursiveOptimizer.js +509 -0
  56. package/dist/core/optimization/RecursiveOptimizer.js.map +1 -0
  57. package/dist/core/strategies/SONALearningStrategy.d.ts +115 -0
  58. package/dist/core/strategies/SONALearningStrategy.d.ts.map +1 -0
  59. package/dist/core/strategies/SONALearningStrategy.js +656 -0
  60. package/dist/core/strategies/SONALearningStrategy.js.map +1 -0
  61. package/dist/core/strategies/TRMLearningStrategy.d.ts +162 -0
  62. package/dist/core/strategies/TRMLearningStrategy.d.ts.map +1 -0
  63. package/dist/core/strategies/TRMLearningStrategy.js +670 -0
  64. package/dist/core/strategies/TRMLearningStrategy.js.map +1 -0
  65. package/dist/core/strategies/index.d.ts +10 -1
  66. package/dist/core/strategies/index.d.ts.map +1 -1
  67. package/dist/core/strategies/index.js +4 -1
  68. package/dist/core/strategies/index.js.map +1 -1
  69. package/dist/learning/SONAFeedbackLoop.d.ts +168 -0
  70. package/dist/learning/SONAFeedbackLoop.d.ts.map +1 -0
  71. package/dist/learning/SONAFeedbackLoop.js +344 -0
  72. package/dist/learning/SONAFeedbackLoop.js.map +1 -0
  73. package/dist/learning/baselines/BaselineCollector.d.ts +1 -1
  74. package/dist/learning/baselines/BaselineCollector.js +1 -1
  75. package/dist/learning/baselines/StandardTaskSuite.d.ts +1 -1
  76. package/dist/learning/baselines/StandardTaskSuite.js +1 -1
  77. package/dist/learning/index.d.ts +2 -0
  78. package/dist/learning/index.d.ts.map +1 -1
  79. package/dist/learning/index.js +6 -1
  80. package/dist/learning/index.js.map +1 -1
  81. package/dist/mcp/server-instructions.d.ts +1 -1
  82. package/dist/mcp/server-instructions.js +1 -1
  83. package/dist/mcp/server.d.ts.map +1 -1
  84. package/dist/mcp/server.js +23 -16
  85. package/dist/mcp/server.js.map +1 -1
  86. package/dist/mcp/services/AgentRegistry.d.ts.map +1 -1
  87. package/dist/mcp/services/AgentRegistry.js +6 -1
  88. package/dist/mcp/services/AgentRegistry.js.map +1 -1
  89. package/dist/mcp/tools/qe/accessibility/accname-computation.d.ts +114 -0
  90. package/dist/mcp/tools/qe/accessibility/accname-computation.d.ts.map +1 -0
  91. package/dist/mcp/tools/qe/accessibility/accname-computation.js +566 -0
  92. package/dist/mcp/tools/qe/accessibility/accname-computation.js.map +1 -0
  93. package/dist/mcp/tools/qe/accessibility/apg-patterns.d.ts +103 -0
  94. package/dist/mcp/tools/qe/accessibility/apg-patterns.d.ts.map +1 -0
  95. package/dist/mcp/tools/qe/accessibility/apg-patterns.js +1028 -0
  96. package/dist/mcp/tools/qe/accessibility/apg-patterns.js.map +1 -0
  97. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.d.ts +48 -0
  98. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.d.ts.map +1 -0
  99. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.js +565 -0
  100. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.js.map +1 -0
  101. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.d.ts +117 -0
  102. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.d.ts.map +1 -0
  103. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.js +571 -0
  104. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.js.map +1 -0
  105. package/dist/mcp/tools/qe/accessibility/html-report-generator.d.ts +23 -0
  106. package/dist/mcp/tools/qe/accessibility/html-report-generator.d.ts.map +1 -0
  107. package/dist/mcp/tools/qe/accessibility/html-report-generator.js +1152 -0
  108. package/dist/mcp/tools/qe/accessibility/html-report-generator.js.map +1 -0
  109. package/dist/mcp/tools/qe/accessibility/index.d.ts +22 -0
  110. package/dist/mcp/tools/qe/accessibility/index.d.ts.map +1 -0
  111. package/dist/mcp/tools/qe/accessibility/index.js +38 -0
  112. package/dist/mcp/tools/qe/accessibility/index.js.map +1 -0
  113. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.d.ts +18 -0
  114. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.d.ts.map +1 -0
  115. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.js +549 -0
  116. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.js.map +1 -0
  117. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.d.ts +139 -0
  118. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.d.ts.map +1 -0
  119. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.js +1300 -0
  120. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.js.map +1 -0
  121. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.d.ts +138 -0
  122. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.d.ts.map +1 -0
  123. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.js +1298 -0
  124. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.js.map +1 -0
  125. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.d.ts +50 -0
  126. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.d.ts.map +1 -0
  127. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.js +469 -0
  128. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.js.map +1 -0
  129. package/dist/mcp/tools/qe/accessibility/webvtt-generator.d.ts +193 -0
  130. package/dist/mcp/tools/qe/accessibility/webvtt-generator.d.ts.map +1 -0
  131. package/dist/mcp/tools/qe/accessibility/webvtt-generator.js +511 -0
  132. package/dist/mcp/tools/qe/accessibility/webvtt-generator.js.map +1 -0
  133. package/dist/mcp/tools.d.ts +1 -0
  134. package/dist/mcp/tools.d.ts.map +1 -1
  135. package/dist/mcp/tools.js +61 -0
  136. package/dist/mcp/tools.js.map +1 -1
  137. package/dist/providers/HybridRouter.d.ts +34 -3
  138. package/dist/providers/HybridRouter.d.ts.map +1 -1
  139. package/dist/providers/HybridRouter.js +69 -4
  140. package/dist/providers/HybridRouter.js.map +1 -1
  141. package/dist/providers/LLMProviderFactory.d.ts +68 -1
  142. package/dist/providers/LLMProviderFactory.d.ts.map +1 -1
  143. package/dist/providers/LLMProviderFactory.js +173 -6
  144. package/dist/providers/LLMProviderFactory.js.map +1 -1
  145. package/dist/providers/OpenRouterProvider.d.ts +150 -0
  146. package/dist/providers/OpenRouterProvider.d.ts.map +1 -0
  147. package/dist/providers/OpenRouterProvider.js +545 -0
  148. package/dist/providers/OpenRouterProvider.js.map +1 -0
  149. package/dist/providers/RuvllmProvider.d.ts +130 -16
  150. package/dist/providers/RuvllmProvider.d.ts.map +1 -1
  151. package/dist/providers/RuvllmProvider.js +399 -83
  152. package/dist/providers/RuvllmProvider.js.map +1 -1
  153. package/dist/providers/index.d.ts +33 -4
  154. package/dist/providers/index.d.ts.map +1 -1
  155. package/dist/providers/index.js +72 -21
  156. package/dist/providers/index.js.map +1 -1
  157. package/dist/telemetry/instrumentation/agent.d.ts +1 -1
  158. package/dist/telemetry/instrumentation/agent.js +1 -1
  159. package/dist/telemetry/instrumentation/index.d.ts +1 -1
  160. package/dist/telemetry/instrumentation/index.js +1 -1
  161. package/dist/types/index.d.ts +2 -1
  162. package/dist/types/index.d.ts.map +1 -1
  163. package/dist/types/index.js +2 -0
  164. package/dist/types/index.js.map +1 -1
  165. package/dist/types/ruvllm.d.ts +97 -0
  166. package/dist/types/ruvllm.d.ts.map +1 -0
  167. package/dist/types/ruvllm.js +46 -0
  168. package/dist/types/ruvllm.js.map +1 -0
  169. package/dist/utils/ruvllm-loader.d.ts +94 -0
  170. package/dist/utils/ruvllm-loader.d.ts.map +1 -0
  171. package/dist/utils/ruvllm-loader.js +87 -0
  172. package/dist/utils/ruvllm-loader.js.map +1 -0
  173. package/docs/reference/agents.md +36 -1
  174. package/package.json +5 -2
@@ -0,0 +1,1298 @@
1
+ "use strict";
2
+ /**
3
+ * Comprehensive WCAG 2.2 Accessibility Scan
4
+ *
5
+ * Provides comprehensive accessibility scanning with context-aware remediation
6
+ * recommendations. Uses axe-core for WCAG validation and custom heuristics
7
+ * for intelligent ARIA suggestions.
8
+ *
9
+ * Enhanced with:
10
+ * - EN 301 549 EU compliance mapping
11
+ * - ARIA Authoring Practices Guide (APG) patterns
12
+ * - AccName (Accessible Name) computation
13
+ * - WebVTT caption generation for videos
14
+ * - EU Accessibility Act legal framework
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ var __importDefault = (this && this.__importDefault) || function (mod) {
50
+ return (mod && mod.__esModule) ? mod : { "default": mod };
51
+ };
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.scanComprehensive = scanComprehensive;
54
+ const SecureRandom_js_1 = require("../../../../utils/SecureRandom.js");
55
+ const playwright_1 = __importDefault(require("@axe-core/playwright"));
56
+ const playwright_2 = require("playwright");
57
+ const html_report_generator_js_1 = require("./html-report-generator.js");
58
+ const markdown_report_generator_js_1 = require("./markdown-report-generator.js");
59
+ const video_vision_analyzer_js_1 = require("./video-vision-analyzer.js");
60
+ const en_301_549_mapping_js_1 = require("./en-301-549-mapping.js");
61
+ const apg_patterns_js_1 = require("./apg-patterns.js");
62
+ const accname_computation_js_1 = require("./accname-computation.js");
63
+ const eu_accessibility_act_js_1 = require("./eu-accessibility-act.js");
64
+ const fs = __importStar(require("fs"));
65
+ const path = __importStar(require("path"));
66
+ /**
67
+ * Run custom heuristic checks for issues axe-core doesn't detect
68
+ */
69
+ async function runCustomHeuristicChecks(page, params) {
70
+ const violations = [];
71
+ try {
72
+ // Check for generic link text
73
+ const genericLinks = await page.evaluate(() => {
74
+ const links = Array.from(document.querySelectorAll('a'));
75
+ const genericPatterns = /^(read more|click here|learn more|more|here|link|view more|see more)$/i;
76
+ return links
77
+ .map((link, index) => {
78
+ const text = link.textContent?.trim() || '';
79
+ if (!text.match(genericPatterns))
80
+ return null;
81
+ // Get context from parent elements
82
+ let context = '';
83
+ let current = link.parentElement;
84
+ let depth = 0;
85
+ while (current && depth < 3) {
86
+ // Look for headings
87
+ const heading = current.querySelector('h1, h2, h3, h4, h5, h6');
88
+ if (heading) {
89
+ context = heading.textContent?.trim() || '';
90
+ break;
91
+ }
92
+ // Look for strong text
93
+ const strong = current.querySelector('strong, b');
94
+ if (strong && !strong.contains(link)) {
95
+ context = strong.textContent?.trim() || '';
96
+ break;
97
+ }
98
+ // Get aria-label from parent
99
+ const ariaLabel = current.getAttribute('aria-label');
100
+ if (ariaLabel) {
101
+ context = ariaLabel;
102
+ break;
103
+ }
104
+ current = current.parentElement;
105
+ depth++;
106
+ }
107
+ // If still no context, get surrounding text
108
+ if (!context && link.parentElement) {
109
+ const parentText = link.parentElement.textContent?.replace(text, '').trim() || '';
110
+ context = parentText.slice(0, 100);
111
+ }
112
+ return {
113
+ text,
114
+ href: link.getAttribute('href') || '',
115
+ selector: `a:nth-of-type(${index + 1})`,
116
+ html: link.outerHTML.slice(0, 200),
117
+ context,
118
+ hasAriaLabel: !!link.getAttribute('aria-label')
119
+ };
120
+ })
121
+ .filter(Boolean);
122
+ });
123
+ // Create violations for links without aria-label
124
+ genericLinks.forEach((link, idx) => {
125
+ if (!link.hasAriaLabel) {
126
+ // Generate SPECIFIC aria-label based on context
127
+ let suggestedLabel = '';
128
+ const context = link.context.toLowerCase();
129
+ if (context.includes('e-mobility') || context.includes('electric')) {
130
+ suggestedLabel = 'Read more about e-mobility and electric vehicles';
131
+ }
132
+ else if (context.includes('design') || context.includes('interior')) {
133
+ suggestedLabel = 'Read more about design and interior features';
134
+ }
135
+ else if (context.includes('performance') || context.includes('engine')) {
136
+ suggestedLabel = 'Read more about performance and engineering';
137
+ }
138
+ else if (context.includes('technology') || context.includes('innovation')) {
139
+ suggestedLabel = 'Read more about technology and innovation';
140
+ }
141
+ else if (context.includes('sustainability') || context.includes('environment')) {
142
+ suggestedLabel = 'Read more about sustainability initiatives';
143
+ }
144
+ else if (link.context && link.context.length > 5) {
145
+ // Use the actual context
146
+ suggestedLabel = `Read more about ${link.context.slice(0, 50)}`;
147
+ }
148
+ else {
149
+ suggestedLabel = 'Read more about this topic';
150
+ }
151
+ violations.push({
152
+ id: `custom-generic-link-${idx}`,
153
+ wcagCriterion: '2.4.4',
154
+ wcagLevel: 'A',
155
+ severity: 'serious',
156
+ description: `Generic link text "${link.text}" without descriptive aria-label`,
157
+ impact: 'Link purpose unclear from link text alone',
158
+ elements: [{
159
+ selector: link.selector,
160
+ html: link.html,
161
+ context: {
162
+ surroundingText: link.context,
163
+ semanticRole: 'link'
164
+ }
165
+ }],
166
+ howToFix: `Add descriptive aria-label: aria-label="${suggestedLabel}"`,
167
+ helpUrl: '',
168
+ userImpact: {
169
+ affectedUserPercentage: 10,
170
+ disabilityTypes: ['blind', 'screen-reader-users'],
171
+ severity: 'impairs-usage'
172
+ }
173
+ });
174
+ }
175
+ });
176
+ // Check for videos without captions - WITH ENHANCED CONTEXT EXTRACTION
177
+ const videosWithoutCaptions = await page.evaluate(() => {
178
+ const videos = Array.from(document.querySelectorAll('video'));
179
+ return videos.map((video, index) => {
180
+ const hasTrack = video.querySelector('track[kind="captions"], track[kind="subtitles"]');
181
+ if (hasTrack)
182
+ return null;
183
+ // ENHANCED context extraction for intelligent captions
184
+ let context = '';
185
+ let sources = [];
186
+ const nearbyHeadings = [];
187
+ const nearbyText = [];
188
+ // 1. Video element attributes
189
+ const videoTitle = video.getAttribute('title') || video.getAttribute('aria-label') || '';
190
+ if (videoTitle) {
191
+ sources.push(videoTitle);
192
+ }
193
+ // 2. Try immediate parent heading
194
+ const parent = video.closest('section, article, div[class*="hero"], div[class*="banner"], div[class*="content"]');
195
+ if (parent) {
196
+ // Get ALL headings in parent
197
+ const headings = parent.querySelectorAll('h1, h2, h3, h4, h5, h6');
198
+ headings.forEach(h => {
199
+ if (h.textContent?.trim()) {
200
+ nearbyHeadings.push(h.textContent.trim());
201
+ if (sources.length === 0) {
202
+ sources.push(h.textContent.trim());
203
+ }
204
+ }
205
+ });
206
+ // Get nearby paragraphs
207
+ const paragraphs = parent.querySelectorAll('p');
208
+ paragraphs.forEach(p => {
209
+ if (p.textContent?.trim()) {
210
+ nearbyText.push(p.textContent.trim());
211
+ }
212
+ });
213
+ }
214
+ // 3. Page title
215
+ const pageTitle = document.title;
216
+ if (sources.length === 0 && pageTitle) {
217
+ sources.push(`Page: ${pageTitle}`);
218
+ }
219
+ // 4. Try ANY h1 on the page
220
+ if (sources.length === 0) {
221
+ const h1 = document.querySelector('h1');
222
+ if (h1?.textContent?.trim()) {
223
+ sources.push(h1.textContent.trim());
224
+ nearbyHeadings.push(h1.textContent.trim());
225
+ }
226
+ }
227
+ // 5. Meta description
228
+ if (sources.length === 0) {
229
+ const metaDesc = document.querySelector('meta[name="description"]');
230
+ if (metaDesc) {
231
+ const desc = metaDesc.getAttribute('content');
232
+ if (desc) {
233
+ sources.push(desc.slice(0, 100));
234
+ }
235
+ }
236
+ }
237
+ // 6. Video poster URL for clues
238
+ const poster = video.getAttribute('poster');
239
+ // 7. Page URL
240
+ const pageUrl = window.location.href;
241
+ context = sources.join(' - ');
242
+ return {
243
+ selector: `video:nth-of-type(${index + 1})`,
244
+ html: video.outerHTML.slice(0, 200),
245
+ src: video.getAttribute('src') || video.querySelector('source')?.getAttribute('src') || '',
246
+ poster: poster || '',
247
+ context,
248
+ // Enhanced context for intelligent captions
249
+ videoTitle,
250
+ pageTitle,
251
+ nearbyHeadings: nearbyHeadings.slice(0, 5), // Top 5 headings
252
+ nearbyText: nearbyText.slice(0, 3).map(t => t.slice(0, 150)), // Top 3 paragraphs, truncated
253
+ pageUrl,
254
+ duration: video.duration || 0
255
+ };
256
+ }).filter((v) => v !== null);
257
+ });
258
+ // Auto-detect Ollama availability for FREE vision analysis
259
+ // Map 'free' to 'ollama' since they use the same backend
260
+ const rawProvider = params.options?.visionProvider || 'free';
261
+ const provider = rawProvider === 'free' ? 'ollama' : rawProvider;
262
+ let useVisionAPI = params.options?.enableVisionAPI;
263
+ // Auto-enable vision if Ollama is available (FREE!)
264
+ if (useVisionAPI === undefined) {
265
+ try {
266
+ const ollamaUrl = params.options?.ollamaBaseUrl || 'http://localhost:11434';
267
+ const checkOllama = await fetch(`${ollamaUrl}/api/tags`, {
268
+ method: 'GET',
269
+ signal: AbortSignal.timeout(2000) // 2 second timeout
270
+ });
271
+ useVisionAPI = checkOllama.ok;
272
+ if (useVisionAPI) {
273
+ console.log('✅ Ollama detected - enabling FREE video analysis');
274
+ }
275
+ }
276
+ catch (error) {
277
+ useVisionAPI = false;
278
+ console.log('ℹ️ Ollama not detected - video captions will use context-based fallback');
279
+ }
280
+ }
281
+ for (const [idx, video] of videosWithoutCaptions.entries()) {
282
+ let captionFile = 'WEBVTT\n\n';
283
+ let extendedDescription = '';
284
+ let visionUsed = false;
285
+ // Try Vision API if enabled
286
+ if (useVisionAPI) {
287
+ try {
288
+ const providerName = rawProvider === 'free' ? 'Ollama (FREE)' : provider;
289
+ console.log(`🎬 Analyzing video ${idx + 1}/${videosWithoutCaptions.length} with ${providerName}...`);
290
+ const frames = await (0, video_vision_analyzer_js_1.extractVideoFrames)(page, video.selector, {
291
+ maxFrames: params.options?.visionMaxFrames || 10,
292
+ intervalSeconds: params.options?.visionFrameInterval || 3
293
+ });
294
+ if (frames.length > 0) {
295
+ const analysis = await (0, video_vision_analyzer_js_1.analyzeVideoWithVision)(frames, {
296
+ provider,
297
+ anthropicApiKey: params.options?.anthropicApiKey || process.env.ANTHROPIC_API_KEY,
298
+ ollamaBaseUrl: params.options?.ollamaBaseUrl,
299
+ ollamaModel: params.options?.ollamaModel,
300
+ // Pass enhanced context for intelligent fallback captions
301
+ videoContext: {
302
+ pageTitle: video.pageTitle,
303
+ videoTitle: video.videoTitle,
304
+ videoSrc: video.src,
305
+ posterSrc: video.poster,
306
+ nearbyHeadings: video.nearbyHeadings,
307
+ nearbyText: video.nearbyText,
308
+ pageUrl: video.pageUrl,
309
+ duration: video.duration
310
+ }
311
+ });
312
+ captionFile = analysis.webVTT;
313
+ extendedDescription = analysis.extendedDescription;
314
+ visionUsed = true;
315
+ console.log(`✅ Vision analysis complete: ${analysis.sceneDescriptions.length} scenes described`);
316
+ }
317
+ }
318
+ catch (error) {
319
+ console.warn(`⚠️ Vision API failed for video ${idx + 1}, falling back to context-based captions:`, error);
320
+ }
321
+ }
322
+ // Fall back to context-based captions if Vision not used
323
+ if (!visionUsed) {
324
+ const ctx = video.context.toLowerCase();
325
+ // Use the extracted context to generate SPECIFIC captions
326
+ if (ctx.includes('e-mobility') || ctx.includes('electric') || ctx.includes('e-tron')) {
327
+ captionFile += `00:00:00.000 --> 00:00:05.000
328
+ Electric vehicle demonstration
329
+
330
+ 00:00:05.000 --> 00:00:10.000
331
+ Experience the future of e-mobility
332
+ with zero-emission technology
333
+
334
+ 00:00:10.000 --> 00:00:15.000
335
+ [Electric motor sound - quiet acceleration]
336
+
337
+ 00:00:15.000 --> 00:00:20.000
338
+ Sustainable performance for the modern driver`;
339
+ }
340
+ else if (ctx.includes('design') || ctx.includes('interior') || ctx.includes('exterior')) {
341
+ captionFile += `00:00:00.000 --> 00:00:05.000
342
+ ${video.context}
343
+
344
+ 00:00:05.000 --> 00:00:10.000
345
+ Showcasing innovative design philosophy
346
+ and premium craftsmanship
347
+
348
+ 00:00:10.000 --> 00:00:15.000
349
+ [Ambient background music]
350
+
351
+ 00:00:15.000 --> 00:00:20.000
352
+ Where form meets function`;
353
+ }
354
+ else if (ctx.includes('safety') || ctx.includes('technology')) {
355
+ captionFile += `00:00:00.000 --> 00:00:05.000
356
+ ${video.context}
357
+
358
+ 00:00:05.000 --> 00:00:10.000
359
+ Advanced driver assistance systems
360
+ protecting what matters most
361
+
362
+ 00:00:10.000 --> 00:00:15.000
363
+ [Demonstration of safety features]
364
+
365
+ 00:00:15.000 --> 00:00:20.000
366
+ Technology you can trust`;
367
+ }
368
+ else {
369
+ // Fallback with whatever context we found
370
+ const description = video.context || 'Vehicle presentation video';
371
+ captionFile += `00:00:00.000 --> 00:00:05.000
372
+ ${description}
373
+
374
+ 00:00:05.000 --> 00:00:10.000
375
+ [Narration describing key features]
376
+
377
+ 00:00:10.000 --> 00:00:15.000
378
+ [Background music continues]
379
+
380
+ 00:00:15.000 --> 00:00:20.000
381
+ [Closing statement about brand values]`;
382
+ }
383
+ } // End fallback
384
+ // Create howToFix with captions AND extended description for blind users
385
+ let howToFix = `Add caption track:\n\n<track kind="captions" src="captions.vtt" srclang="en" label="English">\n\nGenerated caption file (save as captions.vtt):\n\n${captionFile}`;
386
+ if (visionUsed && extendedDescription) {
387
+ howToFix += `\n\n--- VIDEO DESCRIPTION FOR BLIND USERS ---\nAdd aria-describedby attribute with detailed scene description:\n\n<video aria-describedby="video-desc-${idx}">\n ...\n</video>\n\n<div id="video-desc-${idx}" style="position: absolute; left: -10000px;">\n${extendedDescription}\n</div>`;
388
+ }
389
+ violations.push({
390
+ id: `custom-video-no-captions-${idx}`,
391
+ wcagCriterion: '1.2.2',
392
+ wcagLevel: 'A',
393
+ severity: 'critical',
394
+ description: visionUsed
395
+ ? 'Video lacks synchronized captions (analyzed with AI Vision)'
396
+ : 'Video lacks synchronized captions',
397
+ impact: 'Deaf and hard-of-hearing users cannot access video content',
398
+ elements: [{
399
+ selector: video.selector,
400
+ html: video.html,
401
+ context: {
402
+ surroundingText: video.context,
403
+ semanticRole: 'video'
404
+ }
405
+ }],
406
+ howToFix,
407
+ helpUrl: '',
408
+ userImpact: {
409
+ affectedUserPercentage: 15,
410
+ disabilityTypes: ['deaf', 'hard-of-hearing'],
411
+ severity: 'blocks-usage'
412
+ }
413
+ });
414
+ } // End for loop
415
+ // Check for aria-hidden elements with focusable children
416
+ const ariaHiddenIssues = await page.evaluate(() => {
417
+ const elements = Array.from(document.querySelectorAll('[aria-hidden="true"]'));
418
+ const issues = [];
419
+ elements.forEach((el, index) => {
420
+ // Find focusable children
421
+ const focusableChildren = Array.from(el.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'));
422
+ if (focusableChildren.length > 0) {
423
+ // Get details about the focusable elements
424
+ const childrenInfo = focusableChildren.map(child => ({
425
+ tag: child.tagName.toLowerCase(),
426
+ text: child.textContent?.trim().slice(0, 100) || '',
427
+ type: child.getAttribute('type') || '',
428
+ html: child.outerHTML.slice(0, 200)
429
+ }));
430
+ issues.push({
431
+ selector: `[aria-hidden="true"]:nth-of-type(${index + 1})`,
432
+ html: el.outerHTML.slice(0, 300),
433
+ className: el.className || '',
434
+ focusableCount: focusableChildren.length,
435
+ children: childrenInfo
436
+ });
437
+ }
438
+ });
439
+ return issues;
440
+ });
441
+ ariaHiddenIssues.forEach((issue, idx) => {
442
+ // Generate SPECIFIC fix based on what the focusable children are
443
+ const childTypes = issue.children.map((c) => c.tag);
444
+ const hasButtons = childTypes.includes('button');
445
+ const hasInputs = childTypes.includes('input');
446
+ const hasLinks = childTypes.includes('a');
447
+ // Extract button/link text for context
448
+ const interactiveText = issue.children
449
+ .filter((c) => c.text)
450
+ .map((c) => `${c.tag.toUpperCase()}: "${c.text}"`)
451
+ .slice(0, 3)
452
+ .join(', ');
453
+ let specificFix = '';
454
+ let rationale = '';
455
+ // Cookie consent detection
456
+ if (interactiveText.toLowerCase().includes('einstellung') ||
457
+ interactiveText.toLowerCase().includes('cookie') ||
458
+ interactiveText.toLowerCase().includes('consent')) {
459
+ specificFix = `<!-- ISSUE: Cookie consent UI hidden but still focusable -->\n${issue.html.slice(0, 150)}...\n\n<!-- FIX: Add tabindex="-1" to all interactive elements -->\n<div aria-hidden="true">\n <button tabindex="-1">Einstellungen anpassen</button>\n <input tabindex="-1" type="checkbox">\n</div>`;
460
+ rationale = `Cookie consent elements (${interactiveText}) are hidden with aria-hidden="true" but remain keyboard-focusable. Add tabindex="-1" to prevent focus.`;
461
+ }
462
+ else if (hasButtons || hasLinks) {
463
+ const elements = issue.children.map((c) => ` <${c.tag} tabindex="-1">${c.text || '...'}</${c.tag}>`).join('\n');
464
+ specificFix = `<!-- ISSUE: Interactive elements in hidden container -->\n<!-- Elements found: ${interactiveText} -->\n\n<!-- FIX: Add tabindex="-1" to prevent keyboard focus -->\n<div aria-hidden="true">\n${elements}\n</div>`;
465
+ rationale = `Found ${issue.focusableCount} focusable elements (${childTypes.join(', ')}) inside aria-hidden container. These must have tabindex="-1" to prevent keyboard focus.`;
466
+ }
467
+ else if (hasInputs) {
468
+ specificFix = `<!-- ISSUE: Form inputs in hidden container -->\n\n<!-- FIX: Add tabindex="-1" to inputs -->\n<div aria-hidden="true">\n <input tabindex="-1" type="${issue.children[0].type || 'text'}">\n</div>`;
469
+ rationale = `Form inputs inside aria-hidden element remain focusable. Add tabindex="-1" to all inputs.`;
470
+ }
471
+ violations.push({
472
+ id: `custom-aria-hidden-focusable-${idx}`,
473
+ wcagCriterion: '4.1.2',
474
+ wcagLevel: 'A',
475
+ severity: 'serious',
476
+ description: `aria-hidden element contains ${issue.focusableCount} focusable ${issue.focusableCount === 1 ? 'element' : 'elements'}: ${interactiveText}`,
477
+ impact: 'Keyboard users can focus elements that are marked as hidden from screen readers, creating confusion',
478
+ elements: [{
479
+ selector: issue.selector,
480
+ html: issue.html,
481
+ context: {
482
+ surroundingText: `Contains: ${interactiveText}`,
483
+ semanticRole: 'container'
484
+ }
485
+ }],
486
+ howToFix: specificFix,
487
+ helpUrl: '',
488
+ userImpact: {
489
+ affectedUserPercentage: 10,
490
+ disabilityTypes: ['blind', 'screen-reader-users', 'keyboard-only-users'],
491
+ severity: 'impairs-usage'
492
+ }
493
+ });
494
+ });
495
+ }
496
+ catch (error) {
497
+ console.error('Custom heuristic checks failed:', error);
498
+ }
499
+ return violations;
500
+ }
501
+ /**
502
+ * Performs comprehensive WCAG 2.2 accessibility scan
503
+ */
504
+ async function scanComprehensive(params) {
505
+ const startTime = performance.now();
506
+ const scanId = SecureRandom_js_1.SecureRandom.generateId(12);
507
+ let browser = null;
508
+ let page = null;
509
+ try {
510
+ // Validate parameters
511
+ if (!params.url) {
512
+ throw new Error('URL is required');
513
+ }
514
+ if (!['A', 'AA', 'AAA'].includes(params.level)) {
515
+ throw new Error('Invalid WCAG level. Must be A, AA, or AAA');
516
+ }
517
+ // Launch browser with context (required by axe-core)
518
+ browser = await playwright_2.chromium.launch({ headless: true });
519
+ const context = await browser.newContext();
520
+ page = await context.newPage();
521
+ // Navigate to URL with increased timeout
522
+ await page.goto(params.url, {
523
+ waitUntil: 'domcontentloaded', // Changed from networkidle for better reliability
524
+ timeout: 60000
525
+ });
526
+ // Extract page metadata for context-aware remediation
527
+ const pageMetadata = await page.evaluate(() => ({
528
+ title: document.title || '',
529
+ language: document.documentElement.lang || document.documentElement.getAttribute('xml:lang') || 'en'
530
+ }));
531
+ // Build axe-core configuration
532
+ const wcagTags = getWCAGTags(params.level);
533
+ const axeBuilder = new playwright_1.default({ page })
534
+ .withTags(wcagTags);
535
+ // Run axe-core scan
536
+ const axeResults = await axeBuilder.analyze();
537
+ // Run CUSTOM heuristic scans for issues axe-core misses
538
+ const customViolations = await runCustomHeuristicChecks(page, params);
539
+ // Merge axe-core violations with custom violations
540
+ const allAxeViolations = [...axeResults.violations];
541
+ // Convert axe violations to our format with enhanced metadata
542
+ const violations = await Promise.all(axeResults.violations.map(async (v, idx) => {
543
+ const elements = await Promise.all(v.nodes.map(async (node) => {
544
+ const element = {
545
+ selector: node.target.join(' '),
546
+ html: node.html
547
+ };
548
+ // Add context if enabled
549
+ if (params.options?.includeContext) {
550
+ element.context = await analyzeElementContext(page, node.target.join(' '));
551
+ }
552
+ return element;
553
+ }));
554
+ const wcagCriterion = extractWCAGCriterion(v.tags);
555
+ // Get EN 301 549 mapping and legal risk
556
+ const en301549 = (0, en_301_549_mapping_js_1.getEN301549Requirement)(wcagCriterion);
557
+ const legalRisk = (0, en_301_549_mapping_js_1.getLegalRiskLevel)(wcagCriterion);
558
+ return {
559
+ id: `violation-${scanId}-${idx}`,
560
+ wcagCriterion,
561
+ wcagLevel: extractWCAGLevel(v.tags),
562
+ severity: mapSeverity(v.impact),
563
+ description: v.description,
564
+ impact: v.help,
565
+ elements,
566
+ howToFix: v.helpUrl,
567
+ helpUrl: v.helpUrl,
568
+ userImpact: calculateUserImpact(v.impact, v.id),
569
+ en301549,
570
+ legalRisk
571
+ };
572
+ }));
573
+ // Add custom heuristic violations
574
+ violations.push(...customViolations);
575
+ // Calculate summary
576
+ const summary = {
577
+ total: violations.length,
578
+ critical: violations.filter(v => v.severity === 'critical').length,
579
+ serious: violations.filter(v => v.severity === 'serious').length,
580
+ moderate: violations.filter(v => v.severity === 'moderate').length,
581
+ minor: violations.filter(v => v.severity === 'minor').length
582
+ };
583
+ // Calculate compliance score
584
+ const score = calculateComplianceScore(violations);
585
+ const status = determineComplianceStatus(score, violations);
586
+ const productionReady = isProductionReady(violations, score);
587
+ // Generate context-aware remediations if enabled
588
+ let remediations;
589
+ if (params.options?.includeContext && violations.length > 0) {
590
+ remediations = generateContextAwareRemediations(violations);
591
+ }
592
+ // Detect video elements for caption recommendations
593
+ const videoElements = await page.evaluate(() => {
594
+ return document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]').length;
595
+ });
596
+ // Assess EU Accessibility Act compliance
597
+ const euAccessibilityAct = (0, eu_accessibility_act_js_1.assessEAACompliance)('websites', // Category for web applications
598
+ violations.map(v => ({
599
+ wcagCriterion: v.wcagCriterion,
600
+ severity: v.severity
601
+ })), ['EU'] // Default to EU market
602
+ );
603
+ const scanTime = performance.now() - startTime;
604
+ const elementsAnalyzed = violations.reduce((sum, v) => sum + v.elements.length, 0);
605
+ const result = {
606
+ scanId: `a11y-${scanId}`,
607
+ url: params.url,
608
+ compliance: {
609
+ status,
610
+ score,
611
+ level: params.level,
612
+ productionReady
613
+ },
614
+ violations,
615
+ summary,
616
+ remediations,
617
+ performance: {
618
+ scanTime,
619
+ elementsAnalyzed
620
+ },
621
+ euAccessibilityAct,
622
+ videoElements: videoElements > 0 ? videoElements : undefined
623
+ };
624
+ // Generate Markdown report (default: true)
625
+ const shouldGenerateMarkdown = params.options?.generateMarkdownReport !== false;
626
+ const shouldOutputToConsole = params.options?.outputToConsole !== false;
627
+ if (shouldGenerateMarkdown) {
628
+ try {
629
+ const markdownReport = (0, markdown_report_generator_js_1.generateMarkdownReport)({
630
+ url: params.url,
631
+ scanId,
632
+ timestamp: new Date().toISOString(),
633
+ violations,
634
+ complianceScore: result.compliance.score,
635
+ complianceStatus: result.compliance.status,
636
+ level: params.level,
637
+ pageLanguage: pageMetadata.language,
638
+ pageTitle: pageMetadata.title,
639
+ includeCodeExamples: true
640
+ });
641
+ // Determine report path
642
+ const reportsDir = params.options?.reportPath
643
+ ? path.dirname(params.options.reportPath)
644
+ : path.join(process.cwd(), 'docs', 'reports');
645
+ const reportFileName = params.options?.reportPath
646
+ ? path.basename(params.options.reportPath).replace(/\.(html|md)$/, '.md')
647
+ : `a11y-report-${scanId}.md`;
648
+ // Ensure reports directory exists
649
+ if (!fs.existsSync(reportsDir)) {
650
+ fs.mkdirSync(reportsDir, { recursive: true });
651
+ }
652
+ const reportPath = path.join(reportsDir, reportFileName);
653
+ fs.writeFileSync(reportPath, markdownReport, 'utf-8');
654
+ result.htmlReportPath = reportPath; // Reuse this field for now
655
+ // Output to console if requested
656
+ if (shouldOutputToConsole) {
657
+ console.log('\n' + '='.repeat(80));
658
+ console.log(markdownReport);
659
+ console.log('='.repeat(80) + '\n');
660
+ console.log(`📄 Report saved to: ${reportPath}\n`);
661
+ }
662
+ }
663
+ catch (error) {
664
+ console.error('Failed to generate Markdown report:', error);
665
+ // Don't fail the entire scan if report generation fails
666
+ }
667
+ }
668
+ // Generate HTML report if explicitly requested (deprecated)
669
+ if (params.options?.generateHTMLReport) {
670
+ try {
671
+ const htmlReport = (0, html_report_generator_js_1.generateHTMLReport)(result, {
672
+ title: `Accessibility Scan Report - ${new URL(params.url).hostname}`,
673
+ includeCodeExamples: true,
674
+ theme: 'light'
675
+ });
676
+ const reportsDir = params.options.reportPath
677
+ ? path.dirname(params.options.reportPath)
678
+ : path.join(process.cwd(), 'docs', 'reports');
679
+ const reportFileName = params.options.reportPath
680
+ ? path.basename(params.options.reportPath)
681
+ : `a11y-report-${scanId}-${Date.now()}.html`;
682
+ if (!fs.existsSync(reportsDir)) {
683
+ fs.mkdirSync(reportsDir, { recursive: true });
684
+ }
685
+ const reportPath = path.join(reportsDir, reportFileName);
686
+ fs.writeFileSync(reportPath, htmlReport, 'utf-8');
687
+ // Don't override the Markdown report path
688
+ if (!shouldGenerateMarkdown) {
689
+ result.htmlReportPath = reportPath;
690
+ }
691
+ }
692
+ catch (error) {
693
+ console.error('Failed to generate HTML report:', error);
694
+ }
695
+ }
696
+ return {
697
+ success: true,
698
+ data: result,
699
+ metadata: {
700
+ requestId: scanId,
701
+ timestamp: new Date().toISOString(),
702
+ executionTime: scanTime,
703
+ agent: 'qe-a11y-ally',
704
+ version: '1.0.0'
705
+ }
706
+ };
707
+ }
708
+ catch (error) {
709
+ const executionTime = performance.now() - startTime;
710
+ const qeError = {
711
+ code: 'A11Y_SCAN_FAILED',
712
+ message: error instanceof Error ? error.message : 'Accessibility scan failed',
713
+ details: {
714
+ params,
715
+ error: error instanceof Error ? error.stack : String(error)
716
+ }
717
+ };
718
+ return {
719
+ success: false,
720
+ error: qeError,
721
+ metadata: {
722
+ requestId: scanId,
723
+ timestamp: new Date().toISOString(),
724
+ executionTime,
725
+ agent: 'qe-a11y-ally',
726
+ version: '1.0.0'
727
+ }
728
+ };
729
+ }
730
+ finally {
731
+ // Cleanup
732
+ if (page)
733
+ await page.close().catch(() => { });
734
+ if (browser)
735
+ await browser.close().catch(() => { });
736
+ }
737
+ }
738
+ /**
739
+ * Get WCAG tags for axe-core based on level
740
+ */
741
+ function getWCAGTags(level) {
742
+ const baseTags = ['wcag2a'];
743
+ if (level === 'AA' || level === 'AAA') {
744
+ baseTags.push('wcag2aa', 'wcag22aa');
745
+ }
746
+ if (level === 'AAA') {
747
+ baseTags.push('wcag2aaa');
748
+ }
749
+ return baseTags;
750
+ }
751
+ /**
752
+ * Extract WCAG criterion from axe tags
753
+ */
754
+ function extractWCAGCriterion(tags) {
755
+ const wcagTag = tags.find(t => t.match(/wcag\d+/));
756
+ if (!wcagTag)
757
+ return 'Unknown';
758
+ // Extract criterion number (e.g., "wcag111" -> "1.1.1")
759
+ const match = wcagTag.match(/wcag(\d)(\d)(\d)/);
760
+ if (match) {
761
+ return `${match[1]}.${match[2]}.${match[3]}`;
762
+ }
763
+ return wcagTag.toUpperCase();
764
+ }
765
+ /**
766
+ * Extract WCAG level from tags
767
+ */
768
+ function extractWCAGLevel(tags) {
769
+ if (tags.some(t => t.includes('wcag2aaa')))
770
+ return 'AAA';
771
+ if (tags.some(t => t.includes('wcag2aa')))
772
+ return 'AA';
773
+ if (tags.some(t => t.includes('wcag2a')))
774
+ return 'A';
775
+ return 'Unknown';
776
+ }
777
+ /**
778
+ * Map axe-core impact to severity
779
+ */
780
+ function mapSeverity(impact) {
781
+ switch (impact) {
782
+ case 'critical': return 'critical';
783
+ case 'serious': return 'serious';
784
+ case 'moderate': return 'moderate';
785
+ default: return 'minor';
786
+ }
787
+ }
788
+ /**
789
+ * Calculate compliance score based on violations
790
+ */
791
+ function calculateComplianceScore(violations) {
792
+ if (violations.length === 0)
793
+ return 100;
794
+ const weights = {
795
+ critical: 20,
796
+ serious: 10,
797
+ moderate: 5,
798
+ minor: 2
799
+ };
800
+ const totalDeductions = violations.reduce((sum, v) => {
801
+ return sum + (weights[v.severity] || 0);
802
+ }, 0);
803
+ return Math.max(0, 100 - totalDeductions);
804
+ }
805
+ /**
806
+ * Determine compliance status
807
+ */
808
+ function determineComplianceStatus(score, violations) {
809
+ const hasCritical = violations.some(v => v.severity === 'critical');
810
+ if (hasCritical)
811
+ return 'non-compliant';
812
+ if (score >= 90)
813
+ return 'compliant';
814
+ return 'partially-compliant';
815
+ }
816
+ /**
817
+ * Determine if application is production ready
818
+ */
819
+ function isProductionReady(violations, score) {
820
+ const hasCritical = violations.some(v => v.severity === 'critical');
821
+ const hasMultipleSerious = violations.filter(v => v.severity === 'serious').length >= 3;
822
+ return !hasCritical && !hasMultipleSerious && score >= 85;
823
+ }
824
+ /**
825
+ * Calculate user impact for a violation
826
+ */
827
+ function calculateUserImpact(impact, ruleId) {
828
+ // Map common violations to affected disability types
829
+ const disabilityMapping = {
830
+ 'color-contrast': ['low-vision', 'color-blindness'],
831
+ 'image-alt': ['blind', 'screen-reader-users'],
832
+ 'label': ['blind', 'screen-reader-users'],
833
+ 'aria': ['blind', 'screen-reader-users'],
834
+ 'keyboard': ['motor-impairment', 'keyboard-only-users'],
835
+ 'focus': ['motor-impairment', 'keyboard-only-users']
836
+ };
837
+ const disabilityTypes = [];
838
+ if (ruleId) {
839
+ for (const [key, types] of Object.entries(disabilityMapping)) {
840
+ if (ruleId.includes(key)) {
841
+ disabilityTypes.push(...types);
842
+ }
843
+ }
844
+ }
845
+ // Estimate affected user percentage
846
+ let affectedPercentage = 5; // Default 5%
847
+ if (impact === 'critical')
848
+ affectedPercentage = 15;
849
+ else if (impact === 'serious')
850
+ affectedPercentage = 10;
851
+ // Determine severity
852
+ let severity = 'minor-inconvenience';
853
+ if (impact === 'critical')
854
+ severity = 'blocks-usage';
855
+ else if (impact === 'serious')
856
+ severity = 'impairs-usage';
857
+ return {
858
+ affectedUserPercentage: affectedPercentage,
859
+ disabilityTypes: disabilityTypes.length > 0 ? [...new Set(disabilityTypes)] : ['general'],
860
+ severity
861
+ };
862
+ }
863
+ /**
864
+ * Analyze element context for context-aware remediation
865
+ * Enhanced with AccName computation and APG pattern suggestions
866
+ */
867
+ async function analyzeElementContext(page, selector) {
868
+ try {
869
+ const elementData = await page.evaluate((sel) => {
870
+ const element = document.querySelector(sel);
871
+ if (!element)
872
+ return undefined;
873
+ // Collect attributes
874
+ const attributes = {};
875
+ for (const attr of element.attributes) {
876
+ attributes[attr.name] = attr.value;
877
+ }
878
+ return {
879
+ tagName: element.tagName.toLowerCase(),
880
+ parentElement: element.parentElement?.tagName.toLowerCase(),
881
+ surroundingText: element.parentElement?.textContent?.slice(0, 100),
882
+ semanticRole: element.getAttribute('role') || element.tagName.toLowerCase(),
883
+ textContent: element.textContent?.slice(0, 200),
884
+ attributes
885
+ };
886
+ }, selector);
887
+ if (!elementData)
888
+ return undefined;
889
+ // Compute accessible name
890
+ const accName = (0, accname_computation_js_1.computeAccessibleName)({
891
+ tagName: elementData.tagName,
892
+ role: elementData.attributes['role'],
893
+ attributes: elementData.attributes,
894
+ textContent: elementData.textContent
895
+ }, { includeTrace: false });
896
+ // Suggest APG pattern
897
+ const suggestedPattern = (0, apg_patterns_js_1.suggestAPGPattern)({
898
+ role: elementData.attributes['role'],
899
+ tagName: elementData.tagName,
900
+ attributes: elementData.attributes,
901
+ context: elementData.surroundingText
902
+ });
903
+ return {
904
+ parentElement: elementData.parentElement,
905
+ surroundingText: elementData.surroundingText,
906
+ semanticRole: elementData.semanticRole,
907
+ accName,
908
+ suggestedPattern: suggestedPattern || undefined
909
+ };
910
+ }
911
+ catch (error) {
912
+ return undefined;
913
+ }
914
+ }
915
+ /**
916
+ * Generate context-aware remediations
917
+ */
918
+ function generateContextAwareRemediations(violations) {
919
+ return violations.map((violation, index) => {
920
+ // Calculate priority based on severity and user impact
921
+ const priorityScore = calculatePriorityScore(violation);
922
+ // Estimate remediation effort
923
+ const effort = estimateRemediationEffort(violation);
924
+ // Generate remediation recommendations
925
+ const recommendations = generateRecommendations(violation);
926
+ // Calculate ROI (priority / effort)
927
+ const roi = priorityScore / effort.hours;
928
+ return {
929
+ violationId: violation.id,
930
+ priority: priorityScore,
931
+ estimatedEffort: effort,
932
+ recommendations,
933
+ roi
934
+ };
935
+ }).sort((a, b) => b.roi - a.roi); // Sort by ROI descending
936
+ }
937
+ /**
938
+ * Calculate priority score (1-10)
939
+ */
940
+ function calculatePriorityScore(violation) {
941
+ const severityScores = {
942
+ critical: 10,
943
+ serious: 7,
944
+ moderate: 4,
945
+ minor: 2
946
+ };
947
+ const baseScore = severityScores[violation.severity];
948
+ const impactMultiplier = (violation.userImpact?.affectedUserPercentage || 5) / 10;
949
+ return Math.min(10, baseScore + impactMultiplier);
950
+ }
951
+ /**
952
+ * Estimate remediation effort
953
+ */
954
+ function estimateRemediationEffort(violation) {
955
+ // Simple heuristic based on violation type
956
+ const elementCount = violation.elements.length;
957
+ let baseHours = 0.5;
958
+ let complexity = 'simple';
959
+ if (violation.wcagCriterion.startsWith('1.1')) {
960
+ // Alt text violations - simple
961
+ baseHours = 0.25 * elementCount;
962
+ complexity = 'trivial';
963
+ }
964
+ else if (violation.wcagCriterion.startsWith('4.1')) {
965
+ // ARIA violations - moderate
966
+ baseHours = 0.5 * elementCount;
967
+ complexity = 'moderate';
968
+ }
969
+ else if (violation.wcagCriterion.startsWith('2.1')) {
970
+ // Keyboard navigation - can be complex
971
+ baseHours = 1 * elementCount;
972
+ complexity = 'complex';
973
+ }
974
+ return {
975
+ hours: Math.max(0.25, baseHours),
976
+ complexity
977
+ };
978
+ }
979
+ /**
980
+ * Generate remediation recommendations
981
+ * Enhanced with APG patterns and AccName intelligence
982
+ */
983
+ function generateRecommendations(violation) {
984
+ const recommendations = [];
985
+ const element = violation.elements[0];
986
+ const context = element?.context;
987
+ // PRIORITY 1: Check if this is a CUSTOM violation with specific howToFix code
988
+ // Custom violations from runCustomHeuristicChecks() have detailed, ready-to-use code
989
+ if (violation.howToFix && violation.id.startsWith('custom-')) {
990
+ // Extract code from howToFix field
991
+ // Format can be:
992
+ // 1. "Add descriptive aria-label: aria-label="...""
993
+ // 2. "Add caption track:\n\n<track ...>\n\nGenerated caption file..."
994
+ const howToFix = violation.howToFix;
995
+ // For generic link violations with aria-label recommendations
996
+ if (howToFix.includes('aria-label=')) {
997
+ const ariaMatch = howToFix.match(/aria-label="([^"]+)"/);
998
+ if (ariaMatch) {
999
+ const ariaLabel = ariaMatch[1];
1000
+ recommendations.push({
1001
+ approach: 'aria-enhancement',
1002
+ priority: 1,
1003
+ code: `aria-label="${ariaLabel}"`,
1004
+ rationale: violation.description,
1005
+ wcagCriteria: [violation.wcagCriterion],
1006
+ confidence: 0.9
1007
+ });
1008
+ return recommendations; // Return immediately - don't generate generic recommendations
1009
+ }
1010
+ }
1011
+ // For video caption violations with WebVTT files
1012
+ if (howToFix.includes('<track') && howToFix.includes('WEBVTT')) {
1013
+ recommendations.push({
1014
+ approach: 'semantic-html',
1015
+ priority: 1,
1016
+ code: howToFix, // Use the entire howToFix as it contains both HTML and WebVTT
1017
+ rationale: violation.description,
1018
+ wcagCriteria: [violation.wcagCriterion],
1019
+ confidence: 0.95
1020
+ });
1021
+ return recommendations; // Return immediately - this is complete, copy-paste-ready code
1022
+ }
1023
+ // For aria-hidden focusability violations with tabindex fixes
1024
+ if (howToFix.includes('<!-- ISSUE:') && howToFix.includes('tabindex="-1"')) {
1025
+ recommendations.push({
1026
+ approach: 'semantic-html',
1027
+ priority: 1,
1028
+ code: howToFix, // Use the entire howToFix with issue + fix comments
1029
+ rationale: violation.description,
1030
+ wcagCriteria: [violation.wcagCriterion],
1031
+ confidence: 0.9
1032
+ });
1033
+ return recommendations; // Return immediately - this is complete, copy-paste-ready code
1034
+ }
1035
+ }
1036
+ // Use APG pattern if suggested
1037
+ if (context?.suggestedPattern) {
1038
+ const { pattern, confidence, reason } = context.suggestedPattern;
1039
+ recommendations.push({
1040
+ approach: 'semantic-html',
1041
+ priority: 1,
1042
+ code: (0, apg_patterns_js_1.generatePatternCodeExample)(pattern.name, {
1043
+ includeJavaScript: true,
1044
+ includeCSS: false
1045
+ }),
1046
+ rationale: `Use W3C APG ${pattern.name} pattern: ${reason}. Reference: ${pattern.apgUrl}`,
1047
+ wcagCriteria: pattern.wcagCriteria,
1048
+ confidence
1049
+ });
1050
+ }
1051
+ // Use AccName recommendation if available
1052
+ if (context?.accName && !context.accName.sufficient) {
1053
+ const accNameRec = (0, accname_computation_js_1.generateAccessibleNameRecommendation)(context.accName, {
1054
+ tagName: element.html.match(/<(\w+)/)?.[1] || 'div',
1055
+ attributes: {},
1056
+ context: context.surroundingText
1057
+ });
1058
+ if (accNameRec.priority === 'critical' || accNameRec.priority === 'high') {
1059
+ recommendations.push({
1060
+ approach: context.accName.source.type === 'aria-label' ? 'aria-enhancement' : 'semantic-html',
1061
+ priority: accNameRec.priority === 'critical' ? 1 : 2,
1062
+ code: accNameRec.codeExample,
1063
+ rationale: `${accNameRec.recommendation}. Current accessible name: "${context.accName.accessibleName}" (quality: ${context.accName.quality}/100)`,
1064
+ wcagCriteria: [violation.wcagCriterion, '4.1.2'],
1065
+ confidence: 0.9
1066
+ });
1067
+ }
1068
+ }
1069
+ // Generate recommendations based on violation type
1070
+ if (violation.description.toLowerCase().includes('aria-label')) {
1071
+ if (recommendations.length === 0) {
1072
+ recommendations.push({
1073
+ approach: 'aria-enhancement',
1074
+ priority: 1,
1075
+ code: generateARIARecommendation(violation),
1076
+ rationale: 'Add descriptive ARIA label for screen reader users',
1077
+ wcagCriteria: [violation.wcagCriterion],
1078
+ confidence: 0.85
1079
+ });
1080
+ }
1081
+ }
1082
+ if (violation.description.toLowerCase().includes('alt') || violation.description.toLowerCase().includes('image')) {
1083
+ const element = violation.elements[0];
1084
+ const html = element?.html || '';
1085
+ const selector = element?.selector || '';
1086
+ // Extract actual image URL and parent link context
1087
+ const srcMatch = html.match(/src="([^"]+)"/);
1088
+ // Check BOTH HTML and CSS selector for parent aria-label
1089
+ let parentMatch = html.match(/aria-label="([^"]+)"/);
1090
+ if (!parentMatch && selector) {
1091
+ // Extract from CSS selector like: .parent[aria-label="Text"] > .child
1092
+ parentMatch = selector.match(/aria-label=["']([^"']+)["']/);
1093
+ }
1094
+ let specificAlt = '';
1095
+ let rationale = 'Image requires descriptive alt text for screen reader users.';
1096
+ if (srcMatch) {
1097
+ const src = srcMatch[1];
1098
+ // Analyze parent link context
1099
+ if (parentMatch) {
1100
+ const parentLabel = parentMatch[1];
1101
+ // Generate SPECIFIC alt text based on parent link
1102
+ if (parentLabel.toLowerCase().includes('e-mobility') || parentLabel.toLowerCase().includes('electric')) {
1103
+ specificAlt = 'alt="Electric vehicle showcasing e-mobility technology"';
1104
+ rationale = `Parent link: "${parentLabel}". Image shows electric vehicle for this e-mobility section.`;
1105
+ }
1106
+ else {
1107
+ specificAlt = `alt="Visual for: ${parentLabel}"`;
1108
+ rationale = `Parent link says "${parentLabel}". Image provides visual context for this link.`;
1109
+ }
1110
+ }
1111
+ // Generic but more specific than placeholder
1112
+ else {
1113
+ const filename = src.split('/').pop()?.split('?')[0] || '';
1114
+ const cleanName = filename
1115
+ .replace(/\.(jpg|png|svg|webp|gif)$/i, '')
1116
+ .replace(/[-_]/g, ' ')
1117
+ .replace(/\d{2,}/g, '')
1118
+ .trim();
1119
+ if (cleanName && cleanName.length > 3) {
1120
+ specificAlt = `alt="${cleanName}"`;
1121
+ rationale = `Inferred from filename "${cleanName}". Verify this accurately describes the image content.`;
1122
+ }
1123
+ else {
1124
+ specificAlt = generateAltTextRecommendation(violation);
1125
+ rationale = 'Analyze the image content and provide specific description.';
1126
+ }
1127
+ }
1128
+ }
1129
+ else {
1130
+ specificAlt = generateAltTextRecommendation(violation);
1131
+ }
1132
+ recommendations.push({
1133
+ approach: 'semantic-html',
1134
+ priority: 1,
1135
+ code: `<!-- Current HTML -->\n${element?.html?.slice(0, 150) || '<img ...>'}${element?.html && element.html.length > 150 ? '...' : ''}\n\n<!-- RECOMMENDED FIX -->\n${specificAlt}`,
1136
+ rationale,
1137
+ wcagCriteria: [violation.wcagCriterion],
1138
+ confidence: parentMatch ? 0.85 : 0.75
1139
+ });
1140
+ }
1141
+ // Default generic recommendation
1142
+ if (recommendations.length === 0) {
1143
+ recommendations.push({
1144
+ approach: 'semantic-html',
1145
+ priority: 2,
1146
+ code: `<!-- Fix required for ${violation.wcagCriterion} -->\n<!-- See: ${violation.helpUrl} -->`,
1147
+ rationale: violation.impact,
1148
+ wcagCriteria: [violation.wcagCriterion],
1149
+ confidence: 0.7
1150
+ });
1151
+ }
1152
+ return recommendations.slice(0, 3); // Limit to top 3 recommendations
1153
+ }
1154
+ /**
1155
+ * Generate ARIA label recommendation with SPECIFIC context-aware suggestions
1156
+ */
1157
+ function generateARIARecommendation(violation) {
1158
+ const element = violation.elements[0];
1159
+ if (!element)
1160
+ return 'aria-label="[Describe the action this element performs]"';
1161
+ const html = element.html.toLowerCase();
1162
+ const context = element.context;
1163
+ let suggestedLabel = '';
1164
+ // Analyze actual element content and context
1165
+ if (html.includes('close') || html.includes('×') || html.includes('x')) {
1166
+ const parentContext = context?.surroundingText?.toLowerCase() || '';
1167
+ if (parentContext.includes('modal') || parentContext.includes('dialog')) {
1168
+ suggestedLabel = 'Close dialog';
1169
+ }
1170
+ else if (parentContext.includes('menu')) {
1171
+ suggestedLabel = 'Close menu';
1172
+ }
1173
+ else if (parentContext.includes('banner') || parentContext.includes('notification')) {
1174
+ suggestedLabel = 'Close notification';
1175
+ }
1176
+ else {
1177
+ suggestedLabel = 'Close';
1178
+ }
1179
+ }
1180
+ else if (html.includes('menu') || html.includes('☰') || html.includes('hamburger')) {
1181
+ suggestedLabel = 'Open navigation menu';
1182
+ }
1183
+ else if (html.includes('search')) {
1184
+ suggestedLabel = 'Search';
1185
+ }
1186
+ else if (html.includes('cart') || html.includes('shopping')) {
1187
+ suggestedLabel = 'View shopping cart';
1188
+ }
1189
+ else if (html.includes('user') || html.includes('account') || html.includes('profile')) {
1190
+ suggestedLabel = 'My account';
1191
+ }
1192
+ else if (html.includes('chevron') || html.includes('arrow')) {
1193
+ if (html.includes('right') || html.includes('next')) {
1194
+ suggestedLabel = 'Next';
1195
+ }
1196
+ else if (html.includes('left') || html.includes('prev')) {
1197
+ suggestedLabel = 'Previous';
1198
+ }
1199
+ else if (html.includes('down')) {
1200
+ suggestedLabel = 'Expand';
1201
+ }
1202
+ else {
1203
+ suggestedLabel = 'Navigate';
1204
+ }
1205
+ }
1206
+ else if (html.includes('play')) {
1207
+ suggestedLabel = 'Play video';
1208
+ }
1209
+ else if (html.includes('pause')) {
1210
+ suggestedLabel = 'Pause video';
1211
+ }
1212
+ else if (context?.surroundingText) {
1213
+ // Extract meaningful text from context
1214
+ const text = context.surroundingText
1215
+ .replace(/\s+/g, ' ')
1216
+ .trim()
1217
+ .slice(0, 50);
1218
+ if (text && text.length > 3) {
1219
+ suggestedLabel = text;
1220
+ }
1221
+ }
1222
+ if (!suggestedLabel) {
1223
+ suggestedLabel = '[Describe the specific action: e.g., "Submit contact form", "Download PDF", "Open filters"]';
1224
+ }
1225
+ return `aria-label="${suggestedLabel}"`;
1226
+ }
1227
+ /**
1228
+ * Generate SPECIFIC alt text recommendation based on actual image context
1229
+ */
1230
+ function generateAltTextRecommendation(violation) {
1231
+ const element = violation.elements[0];
1232
+ if (!element)
1233
+ return 'alt="[Describe what this specific image shows]"';
1234
+ const html = element.html.toLowerCase();
1235
+ let suggestedAlt = '';
1236
+ // Extract image src for context
1237
+ const srcMatch = html.match(/src="([^"]+)"/);
1238
+ const src = srcMatch ? srcMatch[1].toLowerCase() : '';
1239
+ // Analyze image filename and path for clues
1240
+ if (src) {
1241
+ if (src.includes('logo')) {
1242
+ // Extract brand name from URL if possible
1243
+ const urlParts = src.split('/');
1244
+ const domain = urlParts.find(part => part.includes('.com') || part.includes('.org'));
1245
+ if (domain) {
1246
+ const brandName = domain.split('.')[0];
1247
+ suggestedAlt = `${brandName.charAt(0).toUpperCase() + brandName.slice(1)} logo`;
1248
+ }
1249
+ else {
1250
+ suggestedAlt = 'Company logo';
1251
+ }
1252
+ }
1253
+ else if (src.includes('icon')) {
1254
+ suggestedAlt = '[Decorative icon - use alt="" if purely decorative, or describe its meaning]';
1255
+ }
1256
+ else if (src.includes('product') || src.includes('item')) {
1257
+ suggestedAlt = '[Product name and key features, e.g., "Electric vehicle in metallic silver"]';
1258
+ }
1259
+ else if (src.includes('hero') || src.includes('banner')) {
1260
+ suggestedAlt = '[Main subject of banner image, e.g., "Customer using mobile app to track delivery"]';
1261
+ }
1262
+ else if (src.includes('team') || src.includes('person') || src.includes('profile')) {
1263
+ suggestedAlt = '[Person\'s name and role, e.g., "Jane Smith, Chief Technology Officer"]';
1264
+ }
1265
+ else if (src.includes('chart') || src.includes('graph') || src.includes('diagram')) {
1266
+ suggestedAlt = '[Describe the data shown, e.g., "Bar chart showing 40% increase in sales from 2023 to 2024"]';
1267
+ }
1268
+ else {
1269
+ // Try to infer from URL structure
1270
+ const filename = src.split('/').pop()?.split('?')[0] || '';
1271
+ const cleanName = filename
1272
+ .replace(/\.(jpg|png|svg|webp|gif)$/i, '')
1273
+ .replace(/[-_]/g, ' ')
1274
+ .trim();
1275
+ if (cleanName && cleanName.length > 3) {
1276
+ suggestedAlt = cleanName;
1277
+ }
1278
+ else {
1279
+ suggestedAlt = '[Describe what this image shows and its purpose on the page]';
1280
+ }
1281
+ }
1282
+ }
1283
+ // Check if it's in a link to provide more context
1284
+ const context = element.context;
1285
+ if (context?.parentElement === 'a' || html.includes('<a ')) {
1286
+ if (suggestedAlt.startsWith('[')) {
1287
+ suggestedAlt = '[Image shows: ... ] - Link destination: [where this link goes]';
1288
+ }
1289
+ else {
1290
+ suggestedAlt = `${suggestedAlt} - [add link destination, e.g., "Learn more about ${suggestedAlt}"]`;
1291
+ }
1292
+ }
1293
+ if (!suggestedAlt) {
1294
+ suggestedAlt = '[Describe the specific content of this image]';
1295
+ }
1296
+ return `alt="${suggestedAlt}"`;
1297
+ }
1298
+ //# sourceMappingURL=scan-comprehensive.js.map