agentic-qe 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/.claude/agents/qe-a11y-ally.md +751 -0
  2. package/.claude/agents/qx-partner.md +120 -4
  3. package/.claude/skills/testability-scoring/SKILL.md +107 -6
  4. package/CHANGELOG.md +86 -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 +1326 -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 +4 -2
@@ -0,0 +1,1326 @@
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 Audi e-mobility and electric vehicles';
131
+ }
132
+ else if (context.includes('design') || context.includes('interior')) {
133
+ suggestedLabel = 'Read more about Audi design and interior features';
134
+ }
135
+ else if (context.includes('performance') || context.includes('engine')) {
136
+ suggestedLabel = 'Read more about Audi performance and engineering';
137
+ }
138
+ else if (context.includes('technology') || context.includes('innovation')) {
139
+ suggestedLabel = 'Read more about Audi technology and innovation';
140
+ }
141
+ else if (context.includes('sustainability') || context.includes('environment')) {
142
+ suggestedLabel = 'Read more about Audi 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
+ Audi 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 if (ctx.includes('audi')) {
369
+ // Use page title or H1 as context
370
+ const mainContext = video.context.split(' - ')[0] || 'Audi';
371
+ captionFile += `00:00:00.000 --> 00:00:05.000
372
+ ${mainContext}
373
+
374
+ 00:00:05.000 --> 00:00:10.000
375
+ Innovative automotive excellence
376
+ from ${ctx.includes('audi') ? 'Audi' : 'a premium manufacturer'}
377
+
378
+ 00:00:10.000 --> 00:00:15.000
379
+ [Vehicle showcase with background music]
380
+
381
+ 00:00:15.000 --> 00:00:20.000
382
+ Vorsprung durch Technik
383
+ Progress through technology`;
384
+ }
385
+ else {
386
+ // Fallback with whatever context we found
387
+ const description = video.context || 'Vehicle presentation video';
388
+ captionFile += `00:00:00.000 --> 00:00:05.000
389
+ ${description}
390
+
391
+ 00:00:05.000 --> 00:00:10.000
392
+ [Narration describing key features]
393
+
394
+ 00:00:10.000 --> 00:00:15.000
395
+ [Background music continues]
396
+
397
+ 00:00:15.000 --> 00:00:20.000
398
+ [Closing statement about brand values]`;
399
+ }
400
+ } // End fallback
401
+ // Create howToFix with captions AND extended description for blind users
402
+ 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}`;
403
+ if (visionUsed && extendedDescription) {
404
+ 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>`;
405
+ }
406
+ violations.push({
407
+ id: `custom-video-no-captions-${idx}`,
408
+ wcagCriterion: '1.2.2',
409
+ wcagLevel: 'A',
410
+ severity: 'critical',
411
+ description: visionUsed
412
+ ? 'Video lacks synchronized captions (analyzed with AI Vision)'
413
+ : 'Video lacks synchronized captions',
414
+ impact: 'Deaf and hard-of-hearing users cannot access video content',
415
+ elements: [{
416
+ selector: video.selector,
417
+ html: video.html,
418
+ context: {
419
+ surroundingText: video.context,
420
+ semanticRole: 'video'
421
+ }
422
+ }],
423
+ howToFix,
424
+ helpUrl: '',
425
+ userImpact: {
426
+ affectedUserPercentage: 15,
427
+ disabilityTypes: ['deaf', 'hard-of-hearing'],
428
+ severity: 'blocks-usage'
429
+ }
430
+ });
431
+ } // End for loop
432
+ // Check for aria-hidden elements with focusable children
433
+ const ariaHiddenIssues = await page.evaluate(() => {
434
+ const elements = Array.from(document.querySelectorAll('[aria-hidden="true"]'));
435
+ const issues = [];
436
+ elements.forEach((el, index) => {
437
+ // Find focusable children
438
+ const focusableChildren = Array.from(el.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'));
439
+ if (focusableChildren.length > 0) {
440
+ // Get details about the focusable elements
441
+ const childrenInfo = focusableChildren.map(child => ({
442
+ tag: child.tagName.toLowerCase(),
443
+ text: child.textContent?.trim().slice(0, 100) || '',
444
+ type: child.getAttribute('type') || '',
445
+ html: child.outerHTML.slice(0, 200)
446
+ }));
447
+ issues.push({
448
+ selector: `[aria-hidden="true"]:nth-of-type(${index + 1})`,
449
+ html: el.outerHTML.slice(0, 300),
450
+ className: el.className || '',
451
+ focusableCount: focusableChildren.length,
452
+ children: childrenInfo
453
+ });
454
+ }
455
+ });
456
+ return issues;
457
+ });
458
+ ariaHiddenIssues.forEach((issue, idx) => {
459
+ // Generate SPECIFIC fix based on what the focusable children are
460
+ const childTypes = issue.children.map((c) => c.tag);
461
+ const hasButtons = childTypes.includes('button');
462
+ const hasInputs = childTypes.includes('input');
463
+ const hasLinks = childTypes.includes('a');
464
+ // Extract button/link text for context
465
+ const interactiveText = issue.children
466
+ .filter((c) => c.text)
467
+ .map((c) => `${c.tag.toUpperCase()}: "${c.text}"`)
468
+ .slice(0, 3)
469
+ .join(', ');
470
+ let specificFix = '';
471
+ let rationale = '';
472
+ // Cookie consent detection
473
+ if (interactiveText.toLowerCase().includes('einstellung') ||
474
+ interactiveText.toLowerCase().includes('cookie') ||
475
+ interactiveText.toLowerCase().includes('consent')) {
476
+ 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>`;
477
+ rationale = `Cookie consent elements (${interactiveText}) are hidden with aria-hidden="true" but remain keyboard-focusable. Add tabindex="-1" to prevent focus.`;
478
+ }
479
+ else if (hasButtons || hasLinks) {
480
+ const elements = issue.children.map((c) => ` <${c.tag} tabindex="-1">${c.text || '...'}</${c.tag}>`).join('\n');
481
+ 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>`;
482
+ rationale = `Found ${issue.focusableCount} focusable elements (${childTypes.join(', ')}) inside aria-hidden container. These must have tabindex="-1" to prevent keyboard focus.`;
483
+ }
484
+ else if (hasInputs) {
485
+ 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>`;
486
+ rationale = `Form inputs inside aria-hidden element remain focusable. Add tabindex="-1" to all inputs.`;
487
+ }
488
+ violations.push({
489
+ id: `custom-aria-hidden-focusable-${idx}`,
490
+ wcagCriterion: '4.1.2',
491
+ wcagLevel: 'A',
492
+ severity: 'serious',
493
+ description: `aria-hidden element contains ${issue.focusableCount} focusable ${issue.focusableCount === 1 ? 'element' : 'elements'}: ${interactiveText}`,
494
+ impact: 'Keyboard users can focus elements that are marked as hidden from screen readers, creating confusion',
495
+ elements: [{
496
+ selector: issue.selector,
497
+ html: issue.html,
498
+ context: {
499
+ surroundingText: `Contains: ${interactiveText}`,
500
+ semanticRole: 'container'
501
+ }
502
+ }],
503
+ howToFix: specificFix,
504
+ helpUrl: '',
505
+ userImpact: {
506
+ affectedUserPercentage: 10,
507
+ disabilityTypes: ['blind', 'screen-reader-users', 'keyboard-only-users'],
508
+ severity: 'impairs-usage'
509
+ }
510
+ });
511
+ });
512
+ }
513
+ catch (error) {
514
+ console.error('Custom heuristic checks failed:', error);
515
+ }
516
+ return violations;
517
+ }
518
+ /**
519
+ * Performs comprehensive WCAG 2.2 accessibility scan
520
+ */
521
+ async function scanComprehensive(params) {
522
+ const startTime = performance.now();
523
+ const scanId = SecureRandom_js_1.SecureRandom.generateId(12);
524
+ let browser = null;
525
+ let page = null;
526
+ try {
527
+ // Validate parameters
528
+ if (!params.url) {
529
+ throw new Error('URL is required');
530
+ }
531
+ if (!['A', 'AA', 'AAA'].includes(params.level)) {
532
+ throw new Error('Invalid WCAG level. Must be A, AA, or AAA');
533
+ }
534
+ // Launch browser with context (required by axe-core)
535
+ browser = await playwright_2.chromium.launch({ headless: true });
536
+ const context = await browser.newContext();
537
+ page = await context.newPage();
538
+ // Navigate to URL with increased timeout
539
+ await page.goto(params.url, {
540
+ waitUntil: 'domcontentloaded', // Changed from networkidle for better reliability
541
+ timeout: 60000
542
+ });
543
+ // Extract page metadata for context-aware remediation
544
+ const pageMetadata = await page.evaluate(() => ({
545
+ title: document.title || '',
546
+ language: document.documentElement.lang || document.documentElement.getAttribute('xml:lang') || 'en'
547
+ }));
548
+ // Build axe-core configuration
549
+ const wcagTags = getWCAGTags(params.level);
550
+ const axeBuilder = new playwright_1.default({ page })
551
+ .withTags(wcagTags);
552
+ // Run axe-core scan
553
+ const axeResults = await axeBuilder.analyze();
554
+ // Run CUSTOM heuristic scans for issues axe-core misses
555
+ const customViolations = await runCustomHeuristicChecks(page, params);
556
+ // Merge axe-core violations with custom violations
557
+ const allAxeViolations = [...axeResults.violations];
558
+ // Convert axe violations to our format with enhanced metadata
559
+ const violations = await Promise.all(axeResults.violations.map(async (v, idx) => {
560
+ const elements = await Promise.all(v.nodes.map(async (node) => {
561
+ const element = {
562
+ selector: node.target.join(' '),
563
+ html: node.html
564
+ };
565
+ // Add context if enabled
566
+ if (params.options?.includeContext) {
567
+ element.context = await analyzeElementContext(page, node.target.join(' '));
568
+ }
569
+ return element;
570
+ }));
571
+ const wcagCriterion = extractWCAGCriterion(v.tags);
572
+ // Get EN 301 549 mapping and legal risk
573
+ const en301549 = (0, en_301_549_mapping_js_1.getEN301549Requirement)(wcagCriterion);
574
+ const legalRisk = (0, en_301_549_mapping_js_1.getLegalRiskLevel)(wcagCriterion);
575
+ return {
576
+ id: `violation-${scanId}-${idx}`,
577
+ wcagCriterion,
578
+ wcagLevel: extractWCAGLevel(v.tags),
579
+ severity: mapSeverity(v.impact),
580
+ description: v.description,
581
+ impact: v.help,
582
+ elements,
583
+ howToFix: v.helpUrl,
584
+ helpUrl: v.helpUrl,
585
+ userImpact: calculateUserImpact(v.impact, v.id),
586
+ en301549,
587
+ legalRisk
588
+ };
589
+ }));
590
+ // Add custom heuristic violations
591
+ violations.push(...customViolations);
592
+ // Calculate summary
593
+ const summary = {
594
+ total: violations.length,
595
+ critical: violations.filter(v => v.severity === 'critical').length,
596
+ serious: violations.filter(v => v.severity === 'serious').length,
597
+ moderate: violations.filter(v => v.severity === 'moderate').length,
598
+ minor: violations.filter(v => v.severity === 'minor').length
599
+ };
600
+ // Calculate compliance score
601
+ const score = calculateComplianceScore(violations);
602
+ const status = determineComplianceStatus(score, violations);
603
+ const productionReady = isProductionReady(violations, score);
604
+ // Generate context-aware remediations if enabled
605
+ let remediations;
606
+ if (params.options?.includeContext && violations.length > 0) {
607
+ remediations = generateContextAwareRemediations(violations);
608
+ }
609
+ // Detect video elements for caption recommendations
610
+ const videoElements = await page.evaluate(() => {
611
+ return document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]').length;
612
+ });
613
+ // Assess EU Accessibility Act compliance
614
+ const euAccessibilityAct = (0, eu_accessibility_act_js_1.assessEAACompliance)('websites', // Category for web applications
615
+ violations.map(v => ({
616
+ wcagCriterion: v.wcagCriterion,
617
+ severity: v.severity
618
+ })), ['EU'] // Default to EU market
619
+ );
620
+ const scanTime = performance.now() - startTime;
621
+ const elementsAnalyzed = violations.reduce((sum, v) => sum + v.elements.length, 0);
622
+ const result = {
623
+ scanId: `a11y-${scanId}`,
624
+ url: params.url,
625
+ compliance: {
626
+ status,
627
+ score,
628
+ level: params.level,
629
+ productionReady
630
+ },
631
+ violations,
632
+ summary,
633
+ remediations,
634
+ performance: {
635
+ scanTime,
636
+ elementsAnalyzed
637
+ },
638
+ euAccessibilityAct,
639
+ videoElements: videoElements > 0 ? videoElements : undefined
640
+ };
641
+ // Generate Markdown report (default: true)
642
+ const shouldGenerateMarkdown = params.options?.generateMarkdownReport !== false;
643
+ const shouldOutputToConsole = params.options?.outputToConsole !== false;
644
+ if (shouldGenerateMarkdown) {
645
+ try {
646
+ const markdownReport = (0, markdown_report_generator_js_1.generateMarkdownReport)({
647
+ url: params.url,
648
+ scanId,
649
+ timestamp: new Date().toISOString(),
650
+ violations,
651
+ complianceScore: result.compliance.score,
652
+ complianceStatus: result.compliance.status,
653
+ level: params.level,
654
+ pageLanguage: pageMetadata.language,
655
+ pageTitle: pageMetadata.title,
656
+ includeCodeExamples: true
657
+ });
658
+ // Determine report path
659
+ const reportsDir = params.options?.reportPath
660
+ ? path.dirname(params.options.reportPath)
661
+ : path.join(process.cwd(), 'docs', 'reports');
662
+ const reportFileName = params.options?.reportPath
663
+ ? path.basename(params.options.reportPath).replace(/\.(html|md)$/, '.md')
664
+ : `a11y-report-${scanId}.md`;
665
+ // Ensure reports directory exists
666
+ if (!fs.existsSync(reportsDir)) {
667
+ fs.mkdirSync(reportsDir, { recursive: true });
668
+ }
669
+ const reportPath = path.join(reportsDir, reportFileName);
670
+ fs.writeFileSync(reportPath, markdownReport, 'utf-8');
671
+ result.htmlReportPath = reportPath; // Reuse this field for now
672
+ // Output to console if requested
673
+ if (shouldOutputToConsole) {
674
+ console.log('\n' + '='.repeat(80));
675
+ console.log(markdownReport);
676
+ console.log('='.repeat(80) + '\n');
677
+ console.log(`📄 Report saved to: ${reportPath}\n`);
678
+ }
679
+ }
680
+ catch (error) {
681
+ console.error('Failed to generate Markdown report:', error);
682
+ // Don't fail the entire scan if report generation fails
683
+ }
684
+ }
685
+ // Generate HTML report if explicitly requested (deprecated)
686
+ if (params.options?.generateHTMLReport) {
687
+ try {
688
+ const htmlReport = (0, html_report_generator_js_1.generateHTMLReport)(result, {
689
+ title: `Accessibility Scan Report - ${new URL(params.url).hostname}`,
690
+ includeCodeExamples: true,
691
+ theme: 'light'
692
+ });
693
+ const reportsDir = params.options.reportPath
694
+ ? path.dirname(params.options.reportPath)
695
+ : path.join(process.cwd(), 'docs', 'reports');
696
+ const reportFileName = params.options.reportPath
697
+ ? path.basename(params.options.reportPath)
698
+ : `a11y-report-${scanId}-${Date.now()}.html`;
699
+ if (!fs.existsSync(reportsDir)) {
700
+ fs.mkdirSync(reportsDir, { recursive: true });
701
+ }
702
+ const reportPath = path.join(reportsDir, reportFileName);
703
+ fs.writeFileSync(reportPath, htmlReport, 'utf-8');
704
+ // Don't override the Markdown report path
705
+ if (!shouldGenerateMarkdown) {
706
+ result.htmlReportPath = reportPath;
707
+ }
708
+ }
709
+ catch (error) {
710
+ console.error('Failed to generate HTML report:', error);
711
+ }
712
+ }
713
+ return {
714
+ success: true,
715
+ data: result,
716
+ metadata: {
717
+ requestId: scanId,
718
+ timestamp: new Date().toISOString(),
719
+ executionTime: scanTime,
720
+ agent: 'qe-a11y-ally',
721
+ version: '1.0.0'
722
+ }
723
+ };
724
+ }
725
+ catch (error) {
726
+ const executionTime = performance.now() - startTime;
727
+ const qeError = {
728
+ code: 'A11Y_SCAN_FAILED',
729
+ message: error instanceof Error ? error.message : 'Accessibility scan failed',
730
+ details: {
731
+ params,
732
+ error: error instanceof Error ? error.stack : String(error)
733
+ }
734
+ };
735
+ return {
736
+ success: false,
737
+ error: qeError,
738
+ metadata: {
739
+ requestId: scanId,
740
+ timestamp: new Date().toISOString(),
741
+ executionTime,
742
+ agent: 'qe-a11y-ally',
743
+ version: '1.0.0'
744
+ }
745
+ };
746
+ }
747
+ finally {
748
+ // Cleanup
749
+ if (page)
750
+ await page.close().catch(() => { });
751
+ if (browser)
752
+ await browser.close().catch(() => { });
753
+ }
754
+ }
755
+ /**
756
+ * Get WCAG tags for axe-core based on level
757
+ */
758
+ function getWCAGTags(level) {
759
+ const baseTags = ['wcag2a'];
760
+ if (level === 'AA' || level === 'AAA') {
761
+ baseTags.push('wcag2aa', 'wcag22aa');
762
+ }
763
+ if (level === 'AAA') {
764
+ baseTags.push('wcag2aaa');
765
+ }
766
+ return baseTags;
767
+ }
768
+ /**
769
+ * Extract WCAG criterion from axe tags
770
+ */
771
+ function extractWCAGCriterion(tags) {
772
+ const wcagTag = tags.find(t => t.match(/wcag\d+/));
773
+ if (!wcagTag)
774
+ return 'Unknown';
775
+ // Extract criterion number (e.g., "wcag111" -> "1.1.1")
776
+ const match = wcagTag.match(/wcag(\d)(\d)(\d)/);
777
+ if (match) {
778
+ return `${match[1]}.${match[2]}.${match[3]}`;
779
+ }
780
+ return wcagTag.toUpperCase();
781
+ }
782
+ /**
783
+ * Extract WCAG level from tags
784
+ */
785
+ function extractWCAGLevel(tags) {
786
+ if (tags.some(t => t.includes('wcag2aaa')))
787
+ return 'AAA';
788
+ if (tags.some(t => t.includes('wcag2aa')))
789
+ return 'AA';
790
+ if (tags.some(t => t.includes('wcag2a')))
791
+ return 'A';
792
+ return 'Unknown';
793
+ }
794
+ /**
795
+ * Map axe-core impact to severity
796
+ */
797
+ function mapSeverity(impact) {
798
+ switch (impact) {
799
+ case 'critical': return 'critical';
800
+ case 'serious': return 'serious';
801
+ case 'moderate': return 'moderate';
802
+ default: return 'minor';
803
+ }
804
+ }
805
+ /**
806
+ * Calculate compliance score based on violations
807
+ */
808
+ function calculateComplianceScore(violations) {
809
+ if (violations.length === 0)
810
+ return 100;
811
+ const weights = {
812
+ critical: 20,
813
+ serious: 10,
814
+ moderate: 5,
815
+ minor: 2
816
+ };
817
+ const totalDeductions = violations.reduce((sum, v) => {
818
+ return sum + (weights[v.severity] || 0);
819
+ }, 0);
820
+ return Math.max(0, 100 - totalDeductions);
821
+ }
822
+ /**
823
+ * Determine compliance status
824
+ */
825
+ function determineComplianceStatus(score, violations) {
826
+ const hasCritical = violations.some(v => v.severity === 'critical');
827
+ if (hasCritical)
828
+ return 'non-compliant';
829
+ if (score >= 90)
830
+ return 'compliant';
831
+ return 'partially-compliant';
832
+ }
833
+ /**
834
+ * Determine if application is production ready
835
+ */
836
+ function isProductionReady(violations, score) {
837
+ const hasCritical = violations.some(v => v.severity === 'critical');
838
+ const hasMultipleSerious = violations.filter(v => v.severity === 'serious').length >= 3;
839
+ return !hasCritical && !hasMultipleSerious && score >= 85;
840
+ }
841
+ /**
842
+ * Calculate user impact for a violation
843
+ */
844
+ function calculateUserImpact(impact, ruleId) {
845
+ // Map common violations to affected disability types
846
+ const disabilityMapping = {
847
+ 'color-contrast': ['low-vision', 'color-blindness'],
848
+ 'image-alt': ['blind', 'screen-reader-users'],
849
+ 'label': ['blind', 'screen-reader-users'],
850
+ 'aria': ['blind', 'screen-reader-users'],
851
+ 'keyboard': ['motor-impairment', 'keyboard-only-users'],
852
+ 'focus': ['motor-impairment', 'keyboard-only-users']
853
+ };
854
+ const disabilityTypes = [];
855
+ if (ruleId) {
856
+ for (const [key, types] of Object.entries(disabilityMapping)) {
857
+ if (ruleId.includes(key)) {
858
+ disabilityTypes.push(...types);
859
+ }
860
+ }
861
+ }
862
+ // Estimate affected user percentage
863
+ let affectedPercentage = 5; // Default 5%
864
+ if (impact === 'critical')
865
+ affectedPercentage = 15;
866
+ else if (impact === 'serious')
867
+ affectedPercentage = 10;
868
+ // Determine severity
869
+ let severity = 'minor-inconvenience';
870
+ if (impact === 'critical')
871
+ severity = 'blocks-usage';
872
+ else if (impact === 'serious')
873
+ severity = 'impairs-usage';
874
+ return {
875
+ affectedUserPercentage: affectedPercentage,
876
+ disabilityTypes: disabilityTypes.length > 0 ? [...new Set(disabilityTypes)] : ['general'],
877
+ severity
878
+ };
879
+ }
880
+ /**
881
+ * Analyze element context for context-aware remediation
882
+ * Enhanced with AccName computation and APG pattern suggestions
883
+ */
884
+ async function analyzeElementContext(page, selector) {
885
+ try {
886
+ const elementData = await page.evaluate((sel) => {
887
+ const element = document.querySelector(sel);
888
+ if (!element)
889
+ return undefined;
890
+ // Collect attributes
891
+ const attributes = {};
892
+ for (const attr of element.attributes) {
893
+ attributes[attr.name] = attr.value;
894
+ }
895
+ return {
896
+ tagName: element.tagName.toLowerCase(),
897
+ parentElement: element.parentElement?.tagName.toLowerCase(),
898
+ surroundingText: element.parentElement?.textContent?.slice(0, 100),
899
+ semanticRole: element.getAttribute('role') || element.tagName.toLowerCase(),
900
+ textContent: element.textContent?.slice(0, 200),
901
+ attributes
902
+ };
903
+ }, selector);
904
+ if (!elementData)
905
+ return undefined;
906
+ // Compute accessible name
907
+ const accName = (0, accname_computation_js_1.computeAccessibleName)({
908
+ tagName: elementData.tagName,
909
+ role: elementData.attributes['role'],
910
+ attributes: elementData.attributes,
911
+ textContent: elementData.textContent
912
+ }, { includeTrace: false });
913
+ // Suggest APG pattern
914
+ const suggestedPattern = (0, apg_patterns_js_1.suggestAPGPattern)({
915
+ role: elementData.attributes['role'],
916
+ tagName: elementData.tagName,
917
+ attributes: elementData.attributes,
918
+ context: elementData.surroundingText
919
+ });
920
+ return {
921
+ parentElement: elementData.parentElement,
922
+ surroundingText: elementData.surroundingText,
923
+ semanticRole: elementData.semanticRole,
924
+ accName,
925
+ suggestedPattern: suggestedPattern || undefined
926
+ };
927
+ }
928
+ catch (error) {
929
+ return undefined;
930
+ }
931
+ }
932
+ /**
933
+ * Generate context-aware remediations
934
+ */
935
+ function generateContextAwareRemediations(violations) {
936
+ return violations.map((violation, index) => {
937
+ // Calculate priority based on severity and user impact
938
+ const priorityScore = calculatePriorityScore(violation);
939
+ // Estimate remediation effort
940
+ const effort = estimateRemediationEffort(violation);
941
+ // Generate remediation recommendations
942
+ const recommendations = generateRecommendations(violation);
943
+ // Calculate ROI (priority / effort)
944
+ const roi = priorityScore / effort.hours;
945
+ return {
946
+ violationId: violation.id,
947
+ priority: priorityScore,
948
+ estimatedEffort: effort,
949
+ recommendations,
950
+ roi
951
+ };
952
+ }).sort((a, b) => b.roi - a.roi); // Sort by ROI descending
953
+ }
954
+ /**
955
+ * Calculate priority score (1-10)
956
+ */
957
+ function calculatePriorityScore(violation) {
958
+ const severityScores = {
959
+ critical: 10,
960
+ serious: 7,
961
+ moderate: 4,
962
+ minor: 2
963
+ };
964
+ const baseScore = severityScores[violation.severity];
965
+ const impactMultiplier = (violation.userImpact?.affectedUserPercentage || 5) / 10;
966
+ return Math.min(10, baseScore + impactMultiplier);
967
+ }
968
+ /**
969
+ * Estimate remediation effort
970
+ */
971
+ function estimateRemediationEffort(violation) {
972
+ // Simple heuristic based on violation type
973
+ const elementCount = violation.elements.length;
974
+ let baseHours = 0.5;
975
+ let complexity = 'simple';
976
+ if (violation.wcagCriterion.startsWith('1.1')) {
977
+ // Alt text violations - simple
978
+ baseHours = 0.25 * elementCount;
979
+ complexity = 'trivial';
980
+ }
981
+ else if (violation.wcagCriterion.startsWith('4.1')) {
982
+ // ARIA violations - moderate
983
+ baseHours = 0.5 * elementCount;
984
+ complexity = 'moderate';
985
+ }
986
+ else if (violation.wcagCriterion.startsWith('2.1')) {
987
+ // Keyboard navigation - can be complex
988
+ baseHours = 1 * elementCount;
989
+ complexity = 'complex';
990
+ }
991
+ return {
992
+ hours: Math.max(0.25, baseHours),
993
+ complexity
994
+ };
995
+ }
996
+ /**
997
+ * Generate remediation recommendations
998
+ * Enhanced with APG patterns and AccName intelligence
999
+ */
1000
+ function generateRecommendations(violation) {
1001
+ const recommendations = [];
1002
+ const element = violation.elements[0];
1003
+ const context = element?.context;
1004
+ // PRIORITY 1: Check if this is a CUSTOM violation with specific howToFix code
1005
+ // Custom violations from runCustomHeuristicChecks() have detailed, ready-to-use code
1006
+ if (violation.howToFix && violation.id.startsWith('custom-')) {
1007
+ // Extract code from howToFix field
1008
+ // Format can be:
1009
+ // 1. "Add descriptive aria-label: aria-label="...""
1010
+ // 2. "Add caption track:\n\n<track ...>\n\nGenerated caption file..."
1011
+ const howToFix = violation.howToFix;
1012
+ // For generic link violations with aria-label recommendations
1013
+ if (howToFix.includes('aria-label=')) {
1014
+ const ariaMatch = howToFix.match(/aria-label="([^"]+)"/);
1015
+ if (ariaMatch) {
1016
+ const ariaLabel = ariaMatch[1];
1017
+ recommendations.push({
1018
+ approach: 'aria-enhancement',
1019
+ priority: 1,
1020
+ code: `aria-label="${ariaLabel}"`,
1021
+ rationale: violation.description,
1022
+ wcagCriteria: [violation.wcagCriterion],
1023
+ confidence: 0.9
1024
+ });
1025
+ return recommendations; // Return immediately - don't generate generic recommendations
1026
+ }
1027
+ }
1028
+ // For video caption violations with WebVTT files
1029
+ if (howToFix.includes('<track') && howToFix.includes('WEBVTT')) {
1030
+ recommendations.push({
1031
+ approach: 'semantic-html',
1032
+ priority: 1,
1033
+ code: howToFix, // Use the entire howToFix as it contains both HTML and WebVTT
1034
+ rationale: violation.description,
1035
+ wcagCriteria: [violation.wcagCriterion],
1036
+ confidence: 0.95
1037
+ });
1038
+ return recommendations; // Return immediately - this is complete, copy-paste-ready code
1039
+ }
1040
+ // For aria-hidden focusability violations with tabindex fixes
1041
+ if (howToFix.includes('<!-- ISSUE:') && howToFix.includes('tabindex="-1"')) {
1042
+ recommendations.push({
1043
+ approach: 'semantic-html',
1044
+ priority: 1,
1045
+ code: howToFix, // Use the entire howToFix with issue + fix comments
1046
+ rationale: violation.description,
1047
+ wcagCriteria: [violation.wcagCriterion],
1048
+ confidence: 0.9
1049
+ });
1050
+ return recommendations; // Return immediately - this is complete, copy-paste-ready code
1051
+ }
1052
+ }
1053
+ // Use APG pattern if suggested
1054
+ if (context?.suggestedPattern) {
1055
+ const { pattern, confidence, reason } = context.suggestedPattern;
1056
+ recommendations.push({
1057
+ approach: 'semantic-html',
1058
+ priority: 1,
1059
+ code: (0, apg_patterns_js_1.generatePatternCodeExample)(pattern.name, {
1060
+ includeJavaScript: true,
1061
+ includeCSS: false
1062
+ }),
1063
+ rationale: `Use W3C APG ${pattern.name} pattern: ${reason}. Reference: ${pattern.apgUrl}`,
1064
+ wcagCriteria: pattern.wcagCriteria,
1065
+ confidence
1066
+ });
1067
+ }
1068
+ // Use AccName recommendation if available
1069
+ if (context?.accName && !context.accName.sufficient) {
1070
+ const accNameRec = (0, accname_computation_js_1.generateAccessibleNameRecommendation)(context.accName, {
1071
+ tagName: element.html.match(/<(\w+)/)?.[1] || 'div',
1072
+ attributes: {},
1073
+ context: context.surroundingText
1074
+ });
1075
+ if (accNameRec.priority === 'critical' || accNameRec.priority === 'high') {
1076
+ recommendations.push({
1077
+ approach: context.accName.source.type === 'aria-label' ? 'aria-enhancement' : 'semantic-html',
1078
+ priority: accNameRec.priority === 'critical' ? 1 : 2,
1079
+ code: accNameRec.codeExample,
1080
+ rationale: `${accNameRec.recommendation}. Current accessible name: "${context.accName.accessibleName}" (quality: ${context.accName.quality}/100)`,
1081
+ wcagCriteria: [violation.wcagCriterion, '4.1.2'],
1082
+ confidence: 0.9
1083
+ });
1084
+ }
1085
+ }
1086
+ // Generate recommendations based on violation type
1087
+ if (violation.description.toLowerCase().includes('aria-label')) {
1088
+ if (recommendations.length === 0) {
1089
+ recommendations.push({
1090
+ approach: 'aria-enhancement',
1091
+ priority: 1,
1092
+ code: generateARIARecommendation(violation),
1093
+ rationale: 'Add descriptive ARIA label for screen reader users',
1094
+ wcagCriteria: [violation.wcagCriterion],
1095
+ confidence: 0.85
1096
+ });
1097
+ }
1098
+ }
1099
+ if (violation.description.toLowerCase().includes('alt') || violation.description.toLowerCase().includes('image')) {
1100
+ const element = violation.elements[0];
1101
+ const html = element?.html || '';
1102
+ const selector = element?.selector || '';
1103
+ // Extract actual image URL and parent link context
1104
+ const srcMatch = html.match(/src="([^"]+)"/);
1105
+ // Check BOTH HTML and CSS selector for parent aria-label
1106
+ let parentMatch = html.match(/aria-label="([^"]+)"/);
1107
+ if (!parentMatch && selector) {
1108
+ // Extract from CSS selector like: .parent[aria-label="Text"] > .child
1109
+ parentMatch = selector.match(/aria-label=["']([^"']+)["']/);
1110
+ }
1111
+ let specificAlt = '';
1112
+ let rationale = 'Image requires descriptive alt text for screen reader users.';
1113
+ if (srcMatch) {
1114
+ const src = srcMatch[1];
1115
+ // Analyze parent link context
1116
+ if (parentMatch) {
1117
+ const parentLabel = parentMatch[1];
1118
+ // Generate SPECIFIC alt text based on parent link
1119
+ if (parentLabel.toLowerCase().includes('e-mobility') || parentLabel.toLowerCase().includes('electric')) {
1120
+ specificAlt = 'alt="Audi electric vehicle showcasing e-mobility technology"';
1121
+ rationale = `Parent link: "${parentLabel}". Image shows Audi's electric vehicle for this e-mobility section.`;
1122
+ }
1123
+ else {
1124
+ specificAlt = `alt="Visual for: ${parentLabel}"`;
1125
+ rationale = `Parent link says "${parentLabel}". Image provides visual context for this link.`;
1126
+ }
1127
+ }
1128
+ // Analyze image URL for brand/product context
1129
+ else if (src.includes('audi.com') || src.includes('dam.audi')) {
1130
+ if (src.includes('mobility') || src.includes('e-tron') || src.includes('electric')) {
1131
+ specificAlt = 'alt="Audi electric vehicle showcasing e-mobility technology"';
1132
+ rationale = 'Image URL suggests Audi e-mobility content. Alt text describes the electric vehicle shown.';
1133
+ }
1134
+ else {
1135
+ specificAlt = 'alt="Audi promotional image"';
1136
+ rationale = 'Image from Audi Digital Asset Management. Describe the specific Audi product or feature shown.';
1137
+ }
1138
+ }
1139
+ // Generic but more specific than placeholder
1140
+ else {
1141
+ const filename = src.split('/').pop()?.split('?')[0] || '';
1142
+ const cleanName = filename
1143
+ .replace(/\.(jpg|png|svg|webp|gif)$/i, '')
1144
+ .replace(/[-_]/g, ' ')
1145
+ .replace(/\d{2,}/g, '')
1146
+ .trim();
1147
+ if (cleanName && cleanName.length > 3) {
1148
+ specificAlt = `alt="${cleanName}"`;
1149
+ rationale = `Inferred from filename "${cleanName}". Verify this accurately describes the image content.`;
1150
+ }
1151
+ else {
1152
+ specificAlt = generateAltTextRecommendation(violation);
1153
+ rationale = 'Analyze the image content and provide specific description.';
1154
+ }
1155
+ }
1156
+ }
1157
+ else {
1158
+ specificAlt = generateAltTextRecommendation(violation);
1159
+ }
1160
+ recommendations.push({
1161
+ approach: 'semantic-html',
1162
+ priority: 1,
1163
+ code: `<!-- Current HTML -->\n${element?.html?.slice(0, 150) || '<img ...>'}${element?.html && element.html.length > 150 ? '...' : ''}\n\n<!-- RECOMMENDED FIX -->\n${specificAlt}`,
1164
+ rationale,
1165
+ wcagCriteria: [violation.wcagCriterion],
1166
+ confidence: parentMatch ? 0.85 : 0.75
1167
+ });
1168
+ }
1169
+ // Default generic recommendation
1170
+ if (recommendations.length === 0) {
1171
+ recommendations.push({
1172
+ approach: 'semantic-html',
1173
+ priority: 2,
1174
+ code: `<!-- Fix required for ${violation.wcagCriterion} -->\n<!-- See: ${violation.helpUrl} -->`,
1175
+ rationale: violation.impact,
1176
+ wcagCriteria: [violation.wcagCriterion],
1177
+ confidence: 0.7
1178
+ });
1179
+ }
1180
+ return recommendations.slice(0, 3); // Limit to top 3 recommendations
1181
+ }
1182
+ /**
1183
+ * Generate ARIA label recommendation with SPECIFIC context-aware suggestions
1184
+ */
1185
+ function generateARIARecommendation(violation) {
1186
+ const element = violation.elements[0];
1187
+ if (!element)
1188
+ return 'aria-label="[Describe the action this element performs]"';
1189
+ const html = element.html.toLowerCase();
1190
+ const context = element.context;
1191
+ let suggestedLabel = '';
1192
+ // Analyze actual element content and context
1193
+ if (html.includes('close') || html.includes('×') || html.includes('x')) {
1194
+ const parentContext = context?.surroundingText?.toLowerCase() || '';
1195
+ if (parentContext.includes('modal') || parentContext.includes('dialog')) {
1196
+ suggestedLabel = 'Close dialog';
1197
+ }
1198
+ else if (parentContext.includes('menu')) {
1199
+ suggestedLabel = 'Close menu';
1200
+ }
1201
+ else if (parentContext.includes('banner') || parentContext.includes('notification')) {
1202
+ suggestedLabel = 'Close notification';
1203
+ }
1204
+ else {
1205
+ suggestedLabel = 'Close';
1206
+ }
1207
+ }
1208
+ else if (html.includes('menu') || html.includes('☰') || html.includes('hamburger')) {
1209
+ suggestedLabel = 'Open navigation menu';
1210
+ }
1211
+ else if (html.includes('search')) {
1212
+ suggestedLabel = 'Search';
1213
+ }
1214
+ else if (html.includes('cart') || html.includes('shopping')) {
1215
+ suggestedLabel = 'View shopping cart';
1216
+ }
1217
+ else if (html.includes('user') || html.includes('account') || html.includes('profile')) {
1218
+ suggestedLabel = 'My account';
1219
+ }
1220
+ else if (html.includes('chevron') || html.includes('arrow')) {
1221
+ if (html.includes('right') || html.includes('next')) {
1222
+ suggestedLabel = 'Next';
1223
+ }
1224
+ else if (html.includes('left') || html.includes('prev')) {
1225
+ suggestedLabel = 'Previous';
1226
+ }
1227
+ else if (html.includes('down')) {
1228
+ suggestedLabel = 'Expand';
1229
+ }
1230
+ else {
1231
+ suggestedLabel = 'Navigate';
1232
+ }
1233
+ }
1234
+ else if (html.includes('play')) {
1235
+ suggestedLabel = 'Play video';
1236
+ }
1237
+ else if (html.includes('pause')) {
1238
+ suggestedLabel = 'Pause video';
1239
+ }
1240
+ else if (context?.surroundingText) {
1241
+ // Extract meaningful text from context
1242
+ const text = context.surroundingText
1243
+ .replace(/\s+/g, ' ')
1244
+ .trim()
1245
+ .slice(0, 50);
1246
+ if (text && text.length > 3) {
1247
+ suggestedLabel = text;
1248
+ }
1249
+ }
1250
+ if (!suggestedLabel) {
1251
+ suggestedLabel = '[Describe the specific action: e.g., "Submit contact form", "Download PDF", "Open filters"]';
1252
+ }
1253
+ return `aria-label="${suggestedLabel}"`;
1254
+ }
1255
+ /**
1256
+ * Generate SPECIFIC alt text recommendation based on actual image context
1257
+ */
1258
+ function generateAltTextRecommendation(violation) {
1259
+ const element = violation.elements[0];
1260
+ if (!element)
1261
+ return 'alt="[Describe what this specific image shows]"';
1262
+ const html = element.html.toLowerCase();
1263
+ let suggestedAlt = '';
1264
+ // Extract image src for context
1265
+ const srcMatch = html.match(/src="([^"]+)"/);
1266
+ const src = srcMatch ? srcMatch[1].toLowerCase() : '';
1267
+ // Analyze image filename and path for clues
1268
+ if (src) {
1269
+ if (src.includes('logo')) {
1270
+ // Extract brand name from URL if possible
1271
+ const urlParts = src.split('/');
1272
+ const domain = urlParts.find(part => part.includes('.com') || part.includes('.org'));
1273
+ if (domain) {
1274
+ const brandName = domain.split('.')[0];
1275
+ suggestedAlt = `${brandName.charAt(0).toUpperCase() + brandName.slice(1)} logo`;
1276
+ }
1277
+ else {
1278
+ suggestedAlt = 'Company logo';
1279
+ }
1280
+ }
1281
+ else if (src.includes('icon')) {
1282
+ suggestedAlt = '[Decorative icon - use alt="" if purely decorative, or describe its meaning]';
1283
+ }
1284
+ else if (src.includes('product') || src.includes('item')) {
1285
+ suggestedAlt = '[Product name and key features, e.g., "Audi e-tron GT electric vehicle in metallic silver"]';
1286
+ }
1287
+ else if (src.includes('hero') || src.includes('banner')) {
1288
+ suggestedAlt = '[Main subject of banner image, e.g., "Customer using mobile app to track delivery"]';
1289
+ }
1290
+ else if (src.includes('team') || src.includes('person') || src.includes('profile')) {
1291
+ suggestedAlt = '[Person\'s name and role, e.g., "Jane Smith, Chief Technology Officer"]';
1292
+ }
1293
+ else if (src.includes('chart') || src.includes('graph') || src.includes('diagram')) {
1294
+ suggestedAlt = '[Describe the data shown, e.g., "Bar chart showing 40% increase in sales from 2023 to 2024"]';
1295
+ }
1296
+ else {
1297
+ // Try to infer from URL structure
1298
+ const filename = src.split('/').pop()?.split('?')[0] || '';
1299
+ const cleanName = filename
1300
+ .replace(/\.(jpg|png|svg|webp|gif)$/i, '')
1301
+ .replace(/[-_]/g, ' ')
1302
+ .trim();
1303
+ if (cleanName && cleanName.length > 3) {
1304
+ suggestedAlt = cleanName;
1305
+ }
1306
+ else {
1307
+ suggestedAlt = '[Describe what this image shows and its purpose on the page]';
1308
+ }
1309
+ }
1310
+ }
1311
+ // Check if it's in a link to provide more context
1312
+ const context = element.context;
1313
+ if (context?.parentElement === 'a' || html.includes('<a ')) {
1314
+ if (suggestedAlt.startsWith('[')) {
1315
+ suggestedAlt = '[Image shows: ... ] - Link destination: [where this link goes]';
1316
+ }
1317
+ else {
1318
+ suggestedAlt = `${suggestedAlt} - [add link destination, e.g., "Learn more about ${suggestedAlt}"]`;
1319
+ }
1320
+ }
1321
+ if (!suggestedAlt) {
1322
+ suggestedAlt = '[Describe the specific content of this image]';
1323
+ }
1324
+ return `alt="${suggestedAlt}"`;
1325
+ }
1326
+ //# sourceMappingURL=scan-comprehensive.js.map