@triedotdev/mcp 1.0.38 → 1.0.39

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.
@@ -887,10 +887,72 @@ var ComprehensionAgent = class extends BaseAgent {
887
887
  };
888
888
 
889
889
  // src/agents/accessibility.ts
890
+ var WCAG_CRITERIA = {
891
+ // Perceivable
892
+ "1.1.1": { name: "Non-text Content", level: "A", description: "All non-text content has text alternatives" },
893
+ "1.3.1": { name: "Info and Relationships", level: "A", description: "Information and relationships are programmatically determinable" },
894
+ "1.3.2": { name: "Meaningful Sequence", level: "A", description: "Reading order is correct and meaningful" },
895
+ "1.3.5": { name: "Identify Input Purpose", level: "AA", description: "Input purpose can be programmatically determined" },
896
+ "1.4.1": { name: "Use of Color", level: "A", description: "Color is not the only means of conveying information" },
897
+ "1.4.3": { name: "Contrast (Minimum)", level: "AA", description: "Text has 4.5:1 contrast ratio (3:1 for large text)" },
898
+ "1.4.4": { name: "Resize Text", level: "AA", description: "Text can be resized up to 200% without loss of functionality" },
899
+ "1.4.10": { name: "Reflow", level: "AA", description: "Content can reflow without horizontal scrolling at 320px width" },
900
+ "1.4.11": { name: "Non-text Contrast", level: "AA", description: "UI components have 3:1 contrast ratio" },
901
+ "1.4.12": { name: "Text Spacing", level: "AA", description: "No loss of content when text spacing is adjusted" },
902
+ "1.4.13": { name: "Content on Hover or Focus", level: "AA", description: "Hover/focus content is dismissible, hoverable, persistent" },
903
+ // Operable
904
+ "2.1.1": { name: "Keyboard", level: "A", description: "All functionality is available from keyboard" },
905
+ "2.1.2": { name: "No Keyboard Trap", level: "A", description: "Keyboard focus can be moved away using standard keys" },
906
+ "2.4.1": { name: "Bypass Blocks", level: "A", description: "Skip links or landmarks allow bypassing repeated content" },
907
+ "2.4.3": { name: "Focus Order", level: "A", description: "Focus order preserves meaning and operability" },
908
+ "2.4.4": { name: "Link Purpose (In Context)", level: "A", description: "Link purpose is clear from text or context" },
909
+ "2.4.6": { name: "Headings and Labels", level: "AA", description: "Headings and labels describe topic or purpose" },
910
+ "2.4.7": { name: "Focus Visible", level: "AA", description: "Keyboard focus indicator is visible" },
911
+ "2.5.5": { name: "Target Size", level: "AAA", description: "Touch targets are at least 44\xD744 CSS pixels" },
912
+ "2.5.8": { name: "Target Size (Minimum)", level: "AA", description: "Touch targets are at least 24\xD724 CSS pixels" },
913
+ // Understandable
914
+ "3.2.1": { name: "On Focus", level: "A", description: "Focus does not trigger unexpected context changes" },
915
+ "3.2.2": { name: "On Input", level: "A", description: "Input does not trigger unexpected context changes" },
916
+ "3.3.1": { name: "Error Identification", level: "A", description: "Errors are identified and described in text" },
917
+ "3.3.2": { name: "Labels or Instructions", level: "A", description: "Labels or instructions are provided for user input" },
918
+ // Robust
919
+ "4.1.1": { name: "Parsing", level: "A", description: "HTML is well-formed with unique IDs" },
920
+ "4.1.2": { name: "Name, Role, Value", level: "A", description: "UI components have accessible name, role, and state" },
921
+ "4.1.3": { name: "Status Messages", level: "AA", description: "Status messages are announced to assistive technology" }
922
+ };
923
+ var REQUIRED_ARIA_ATTRIBUTES = {
924
+ "checkbox": ["aria-checked"],
925
+ "combobox": ["aria-expanded", "aria-controls"],
926
+ "grid": ["aria-rowcount", "aria-colcount"],
927
+ "gridcell": [],
928
+ "heading": ["aria-level"],
929
+ "listbox": [],
930
+ "menu": [],
931
+ "menubar": [],
932
+ "menuitem": [],
933
+ "menuitemcheckbox": ["aria-checked"],
934
+ "menuitemradio": ["aria-checked"],
935
+ "option": ["aria-selected"],
936
+ "progressbar": ["aria-valuenow", "aria-valuemin", "aria-valuemax"],
937
+ "radio": ["aria-checked"],
938
+ "scrollbar": ["aria-controls", "aria-valuenow", "aria-valuemin", "aria-valuemax", "aria-orientation"],
939
+ "searchbox": [],
940
+ "separator": [],
941
+ "slider": ["aria-valuenow", "aria-valuemin", "aria-valuemax"],
942
+ "spinbutton": ["aria-valuenow", "aria-valuemin", "aria-valuemax"],
943
+ "switch": ["aria-checked"],
944
+ "tab": ["aria-selected"],
945
+ "tablist": [],
946
+ "tabpanel": [],
947
+ "textbox": [],
948
+ "tree": [],
949
+ "treegrid": [],
950
+ "treeitem": []
951
+ };
890
952
  var AccessibilityAgent = class extends BaseAgent {
891
953
  name = "accessibility";
892
- description = "WCAG 2.1 accessibility compliance, keyboard nav, screen readers, color contrast";
893
- version = "1.0.0";
954
+ description = "WCAG 2.1 AA compliance: screen readers, keyboard nav, color contrast, touch targets, semantic HTML, ARIA patterns";
955
+ version = "2.0.0";
894
956
  shouldActivate(context) {
895
957
  return context.touchesUI;
896
958
  }
@@ -899,154 +961,934 @@ var AccessibilityAgent = class extends BaseAgent {
899
961
  for (const file of files) {
900
962
  try {
901
963
  const content = await this.readFile(file);
902
- issues.push(...this.analyzeAccessibility(content, file));
903
- issues.push(...this.analyzeUXPatterns(content, file));
964
+ if (this.isFrontendFile(file)) {
965
+ issues.push(...this.analyzeImages(content, file));
966
+ issues.push(...this.analyzeInteractiveElements(content, file));
967
+ issues.push(...this.analyzeFormAccessibility(content, file));
968
+ issues.push(...this.analyzeKeyboardNavigation(content, file));
969
+ issues.push(...this.analyzeSemanticStructure(content, file));
970
+ issues.push(...this.analyzeARIAUsage(content, file));
971
+ issues.push(...this.analyzeColorAccessibility(content, file));
972
+ issues.push(...this.analyzeTouchTargets(content, file));
973
+ issues.push(...this.analyzeMotionAccessibility(content, file));
974
+ issues.push(...this.analyzeLinks(content, file));
975
+ }
904
976
  } catch (error) {
905
977
  console.error(`Accessibility Agent: Error reading file ${file}:`, error);
906
978
  }
907
979
  }
980
+ const criticalCount = issues.filter((i) => i.severity === "critical").length;
981
+ const seriousCount = issues.filter((i) => i.severity === "serious").length;
982
+ if (criticalCount > 0 || seriousCount > 2) {
983
+ const score = this.calculateAccessibilityScore(issues);
984
+ issues.push(this.createIssue(
985
+ this.generateIssueId(),
986
+ criticalCount > 0 ? "critical" : "serious",
987
+ `Accessibility Score: ${score}/100 \u2014 ${criticalCount} critical, ${seriousCount} serious issues`,
988
+ `Review and fix accessibility issues starting with critical problems. Use axe-core or Lighthouse for additional validation.`,
989
+ files[0] || "project",
990
+ void 0,
991
+ 0.95,
992
+ void 0,
993
+ false
994
+ ));
995
+ }
908
996
  return issues;
909
997
  }
910
- analyzeAccessibility(content, file) {
998
+ isFrontendFile(file) {
999
+ return /\.(tsx|jsx|vue|svelte|astro|html|htm)$/.test(file);
1000
+ }
1001
+ /**
1002
+ * Calculate accessibility score based on issues
1003
+ */
1004
+ calculateAccessibilityScore(issues) {
1005
+ const criticalPenalty = issues.filter((i) => i.severity === "critical").length * 20;
1006
+ const seriousPenalty = issues.filter((i) => i.severity === "serious").length * 10;
1007
+ const moderatePenalty = issues.filter((i) => i.severity === "moderate").length * 5;
1008
+ const lowPenalty = issues.filter((i) => i.severity === "low").length * 2;
1009
+ return Math.max(0, 100 - criticalPenalty - seriousPenalty - moderatePenalty - lowPenalty);
1010
+ }
1011
+ /**
1012
+ * Get WCAG criterion string
1013
+ */
1014
+ getWCAGRef(criterionId) {
1015
+ const criterion = WCAG_CRITERIA[criterionId];
1016
+ return `WCAG 2.1 ${criterion.level} - ${criterionId} ${criterion.name}`;
1017
+ }
1018
+ // ============================================================================
1019
+ // IMAGE ACCESSIBILITY — WCAG 1.1.1
1020
+ // ============================================================================
1021
+ analyzeImages(content, file) {
911
1022
  const issues = [];
912
1023
  const lines = content.split("\n");
913
1024
  for (let i = 0; i < lines.length; i++) {
914
1025
  const line = lines[i];
915
1026
  const lineNumber = i + 1;
916
- if (/<img[^>]*(?!alt=)[^>]*>/i.test(line) && !line.includes("alt=")) {
1027
+ if (/<img\s/i.test(line)) {
1028
+ if (!line.includes("alt=")) {
1029
+ issues.push(this.createIssue(
1030
+ this.generateIssueId(),
1031
+ "critical",
1032
+ "Image missing alt attribute",
1033
+ 'Add alt="description" for informative images or alt="" for decorative images. Screen readers cannot describe this image.',
1034
+ file,
1035
+ lineNumber,
1036
+ 0.98,
1037
+ this.getWCAGRef("1.1.1"),
1038
+ true
1039
+ ));
1040
+ } else if (/alt=["']\s*["']/i.test(line)) {
1041
+ const context = lines.slice(Math.max(0, i - 3), i + 3).join("\n");
1042
+ if (/hero|logo|banner|product|avatar|icon/i.test(context) && !/decorative|spacer|divider/i.test(context)) {
1043
+ issues.push(this.createIssue(
1044
+ this.generateIssueId(),
1045
+ "moderate",
1046
+ "Empty alt on potentially informative image",
1047
+ "If this image conveys information, add descriptive alt text. Empty alt is only for purely decorative images.",
1048
+ file,
1049
+ lineNumber,
1050
+ 0.7,
1051
+ this.getWCAGRef("1.1.1"),
1052
+ true
1053
+ ));
1054
+ }
1055
+ }
1056
+ }
1057
+ if (/background-image:\s*url\(/i.test(line) || /backgroundImage.*url\(/i.test(line)) {
1058
+ const context = lines.slice(Math.max(0, i - 5), i + 5).join("\n");
1059
+ if (/hero|banner|feature|card/i.test(context) && !/aria-label|sr-only|visually-hidden/i.test(context)) {
1060
+ issues.push(this.createIssue(
1061
+ this.generateIssueId(),
1062
+ "moderate",
1063
+ "Background image may need text alternative",
1064
+ "If this background image conveys information, add a text alternative using aria-label or visually hidden text.",
1065
+ file,
1066
+ lineNumber,
1067
+ 0.65,
1068
+ this.getWCAGRef("1.1.1"),
1069
+ false
1070
+ ));
1071
+ }
1072
+ }
1073
+ if (/<svg\s/i.test(line)) {
1074
+ const svgContext = this.getMultiLineElement(lines, i, "svg");
1075
+ if (!/aria-label|aria-labelledby|<title>|role=["']img["']/i.test(svgContext) && !/aria-hidden=["']true["']/i.test(svgContext)) {
1076
+ issues.push(this.createIssue(
1077
+ this.generateIssueId(),
1078
+ "serious",
1079
+ "SVG missing accessible name",
1080
+ 'Add aria-label, aria-labelledby with <title>, or aria-hidden="true" if decorative.',
1081
+ file,
1082
+ lineNumber,
1083
+ 0.85,
1084
+ this.getWCAGRef("1.1.1"),
1085
+ true
1086
+ ));
1087
+ }
1088
+ }
1089
+ }
1090
+ return issues;
1091
+ }
1092
+ // ============================================================================
1093
+ // INTERACTIVE ELEMENTS — WCAG 4.1.2, 2.1.1
1094
+ // ============================================================================
1095
+ analyzeInteractiveElements(content, file) {
1096
+ const issues = [];
1097
+ const lines = content.split("\n");
1098
+ for (let i = 0; i < lines.length; i++) {
1099
+ const line = lines[i];
1100
+ const lineNumber = i + 1;
1101
+ if (/<button/i.test(line) || /Button\s/i.test(line)) {
1102
+ const buttonContext = this.getMultiLineElement(lines, i, "button") || this.getMultiLineElement(lines, i, "Button");
1103
+ const hasIconOnly = />\s*<(Icon|svg|i\s|img)[^>]*>\s*<\/(button|Button)/i.test(buttonContext) || />\s*<[A-Z][a-zA-Z]*Icon[^>]*\s*\/>\s*<\/(button|Button)/i.test(buttonContext) || /<(Icon|svg)[^>]*\/>\s*<\/(button|Button)/i.test(buttonContext);
1104
+ const hasAccessibleName = /aria-label|aria-labelledby|title=|sr-only|visually-hidden/i.test(buttonContext);
1105
+ if (hasIconOnly && !hasAccessibleName) {
1106
+ issues.push(this.createIssue(
1107
+ this.generateIssueId(),
1108
+ "critical",
1109
+ "Icon-only button missing accessible name",
1110
+ 'Add aria-label="Close" or include visually hidden text. Screen reader users cannot identify this button.',
1111
+ file,
1112
+ lineNumber,
1113
+ 0.95,
1114
+ this.getWCAGRef("4.1.2"),
1115
+ true
1116
+ ));
1117
+ }
1118
+ }
1119
+ if (/onClick|@click|v-on:click|\(click\)/i.test(line)) {
1120
+ if (/<(div|span|li|td|tr|p|section|article)\s/i.test(line) && !/role=["'](button|link|menuitem|tab|option)/i.test(line)) {
1121
+ const elementContext = lines.slice(Math.max(0, i - 2), i + 3).join("\n");
1122
+ const hasKeyboardSupport = /onKeyDown|onKeyPress|onKeyUp|@keydown|@keypress|@keyup|\(keydown\)|\(keypress\)/i.test(elementContext);
1123
+ const hasRole = /role=/i.test(elementContext);
1124
+ const hasTabIndex = /tabIndex|tabindex/i.test(elementContext);
1125
+ if (!hasKeyboardSupport) {
1126
+ issues.push(this.createIssue(
1127
+ this.generateIssueId(),
1128
+ "critical",
1129
+ "Non-semantic element with click handler lacks keyboard support",
1130
+ `Add role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && handleClick()}. Or use a <button> element instead.`,
1131
+ file,
1132
+ lineNumber,
1133
+ 0.92,
1134
+ this.getWCAGRef("2.1.1"),
1135
+ true
1136
+ ));
1137
+ } else if (!hasRole || !hasTabIndex) {
1138
+ issues.push(this.createIssue(
1139
+ this.generateIssueId(),
1140
+ "serious",
1141
+ "Clickable element missing role or tabIndex",
1142
+ 'Add role="button" and tabIndex={0} for proper keyboard accessibility.',
1143
+ file,
1144
+ lineNumber,
1145
+ 0.85,
1146
+ this.getWCAGRef("4.1.2"),
1147
+ true
1148
+ ));
1149
+ }
1150
+ }
1151
+ }
1152
+ if (/<a\s/i.test(line) || /<Link\s/i.test(line)) {
1153
+ const linkContext = this.getMultiLineElement(lines, i, "a") || this.getMultiLineElement(lines, i, "Link");
1154
+ if (!/href=|to=/i.test(linkContext)) {
1155
+ issues.push(this.createIssue(
1156
+ this.generateIssueId(),
1157
+ "serious",
1158
+ "Link element without href",
1159
+ "Add href attribute or use a <button> if this triggers an action. Links must have a destination.",
1160
+ file,
1161
+ lineNumber,
1162
+ 0.9,
1163
+ this.getWCAGRef("2.4.4"),
1164
+ true
1165
+ ));
1166
+ }
1167
+ }
1168
+ }
1169
+ return issues;
1170
+ }
1171
+ // ============================================================================
1172
+ // FORM ACCESSIBILITY — WCAG 1.3.1, 3.3.2, 1.3.5
1173
+ // ============================================================================
1174
+ analyzeFormAccessibility(content, file) {
1175
+ const issues = [];
1176
+ const lines = content.split("\n");
1177
+ for (let i = 0; i < lines.length; i++) {
1178
+ const line = lines[i];
1179
+ const lineNumber = i + 1;
1180
+ if (/<input\s/i.test(line) || /<Input\s/i.test(line) || /<textarea/i.test(line) || /<select/i.test(line)) {
1181
+ const inputContext = lines.slice(Math.max(0, i - 5), i + 5).join("\n");
1182
+ const hasLabel = /<label/i.test(inputContext) && /htmlFor|for=/i.test(inputContext);
1183
+ const hasAriaLabel = /aria-label=/i.test(line);
1184
+ const hasAriaLabelledBy = /aria-labelledby=/i.test(line);
1185
+ const hasPlaceholder = /placeholder=/i.test(line);
1186
+ const hasTitle = /title=/i.test(line);
1187
+ if (!hasLabel && !hasAriaLabel && !hasAriaLabelledBy) {
1188
+ if (hasPlaceholder && !hasTitle) {
1189
+ issues.push(this.createIssue(
1190
+ this.generateIssueId(),
1191
+ "serious",
1192
+ "Form input uses placeholder as only label",
1193
+ "Placeholder text disappears when typing. Add a visible <label> or aria-label for persistent identification.",
1194
+ file,
1195
+ lineNumber,
1196
+ 0.88,
1197
+ this.getWCAGRef("3.3.2"),
1198
+ true
1199
+ ));
1200
+ } else if (!hasTitle) {
1201
+ issues.push(this.createIssue(
1202
+ this.generateIssueId(),
1203
+ "serious",
1204
+ "Form input without accessible label",
1205
+ 'Add <label htmlFor="id"> associated with input, or use aria-label/aria-labelledby.',
1206
+ file,
1207
+ lineNumber,
1208
+ 0.85,
1209
+ this.getWCAGRef("1.3.1"),
1210
+ true
1211
+ ));
1212
+ }
1213
+ }
1214
+ if (/type=["'](email|tel|password|name|address|cc-|url)/i.test(line) && !/autoComplete|autocomplete/i.test(line)) {
1215
+ issues.push(this.createIssue(
1216
+ this.generateIssueId(),
1217
+ "moderate",
1218
+ "Input missing autocomplete attribute",
1219
+ 'Add autocomplete attribute (e.g., autocomplete="email") to help users fill forms.',
1220
+ file,
1221
+ lineNumber,
1222
+ 0.7,
1223
+ this.getWCAGRef("1.3.5"),
1224
+ true
1225
+ ));
1226
+ }
1227
+ }
1228
+ if (/required(?!=\{false\})/i.test(line) && !/aria-required|required.*\*/i.test(line)) {
1229
+ const labelContext = lines.slice(Math.max(0, i - 5), i + 5).join("\n");
1230
+ if (!/\*|required|Required/i.test(labelContext)) {
1231
+ issues.push(this.createIssue(
1232
+ this.generateIssueId(),
1233
+ "moderate",
1234
+ "Required field lacks visual indication",
1235
+ 'Add visual indicator (e.g., asterisk *) and aria-required="true" for clarity.',
1236
+ file,
1237
+ lineNumber,
1238
+ 0.75,
1239
+ this.getWCAGRef("3.3.2"),
1240
+ true
1241
+ ));
1242
+ }
1243
+ }
1244
+ if (/disabled(?!=\{false\})/i.test(line)) {
1245
+ const elementContext = lines.slice(Math.max(0, i - 2), i + 2).join("\n");
1246
+ if (!/title=|aria-describedby|tooltip|Tooltip/i.test(elementContext)) {
1247
+ issues.push(this.createIssue(
1248
+ this.generateIssueId(),
1249
+ "low",
1250
+ "Disabled element without explanation",
1251
+ "Add tooltip or aria-describedby explaining why the action is unavailable.",
1252
+ file,
1253
+ lineNumber,
1254
+ 0.65,
1255
+ void 0,
1256
+ true
1257
+ ));
1258
+ }
1259
+ }
1260
+ }
1261
+ return issues;
1262
+ }
1263
+ // ============================================================================
1264
+ // KEYBOARD NAVIGATION — WCAG 2.1.1, 2.1.2, 2.4.3, 2.4.7
1265
+ // ============================================================================
1266
+ analyzeKeyboardNavigation(content, file) {
1267
+ const issues = [];
1268
+ const lines = content.split("\n");
1269
+ for (let i = 0; i < lines.length; i++) {
1270
+ const line = lines[i];
1271
+ const lineNumber = i + 1;
1272
+ if (/outline:\s*none|outline:\s*0|outline-width:\s*0/i.test(line)) {
1273
+ const styleContext = lines.slice(Math.max(0, i - 3), i + 3).join("\n");
1274
+ const hasFocusReplacement = /focus-visible|ring-|box-shadow.*focus|border.*focus|outline.*focus-visible/i.test(styleContext);
1275
+ if (!hasFocusReplacement) {
1276
+ issues.push(this.createIssue(
1277
+ this.generateIssueId(),
1278
+ "serious",
1279
+ "Focus outline removed without replacement",
1280
+ "Add focus-visible:ring-2 or custom focus indicator. Keyboard users cannot see where they are.",
1281
+ file,
1282
+ lineNumber,
1283
+ 0.92,
1284
+ this.getWCAGRef("2.4.7"),
1285
+ true
1286
+ ));
1287
+ }
1288
+ }
1289
+ if (/focus:outline-none|outline-none/i.test(line)) {
1290
+ const styleContext = lines.slice(Math.max(0, i - 1), i + 2).join("\n");
1291
+ if (!/focus-visible:ring|focus:ring|focus-visible:border|focus:border/i.test(styleContext)) {
1292
+ issues.push(this.createIssue(
1293
+ this.generateIssueId(),
1294
+ "serious",
1295
+ "Tailwind outline-none without focus replacement",
1296
+ "Add focus-visible:ring-2 focus-visible:ring-offset-2 for visible focus indicator.",
1297
+ file,
1298
+ lineNumber,
1299
+ 0.9,
1300
+ this.getWCAGRef("2.4.7"),
1301
+ true
1302
+ ));
1303
+ }
1304
+ }
1305
+ if (/tabIndex=\{?["']?([1-9]\d*)["']?\}?|tabindex=["']([1-9]\d*)["']/i.test(line)) {
1306
+ const match = line.match(/tabIndex=\{?["']?([1-9]\d*)["']?\}?|tabindex=["']([1-9]\d*)["']/i);
1307
+ const value = match?.[1] || match?.[2];
917
1308
  issues.push(this.createIssue(
918
1309
  this.generateIssueId(),
919
1310
  "serious",
920
- "Image missing alt attribute",
921
- 'Add descriptive alt text for screen readers: alt="description"',
1311
+ `Positive tabIndex value (${value}) disrupts natural tab order`,
1312
+ "Use tabIndex={0} for focusable elements or tabIndex={-1} for programmatic focus. Positive values create confusing navigation.",
922
1313
  file,
923
1314
  lineNumber,
924
- 0.95,
925
- "WCAG 2.1 - 1.1.1 Non-text Content",
1315
+ 0.88,
1316
+ this.getWCAGRef("2.4.3"),
926
1317
  true
927
1318
  ));
928
1319
  }
929
- if (/onClick.*(?!onKeyDown|onKeyPress|onKeyUp)/i.test(line) && !/button|Button|<a /i.test(line)) {
1320
+ if (/modal|dialog|drawer|overlay/i.test(line.toLowerCase())) {
1321
+ const componentContext = lines.slice(i, Math.min(lines.length, i + 30)).join("\n");
1322
+ if (/onClose|onDismiss|closeModal/i.test(componentContext)) {
1323
+ if (!/onKeyDown.*Escape|useEscapeKey|handleEscape|key.*===.*Escape/i.test(componentContext)) {
1324
+ issues.push(this.createIssue(
1325
+ this.generateIssueId(),
1326
+ "serious",
1327
+ "Modal/dialog may trap keyboard focus without Escape key support",
1328
+ "Add Escape key handler to close modal and manage focus trap with focus-trap library.",
1329
+ file,
1330
+ lineNumber,
1331
+ 0.75,
1332
+ this.getWCAGRef("2.1.2"),
1333
+ false
1334
+ ));
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+ return issues;
1340
+ }
1341
+ // ============================================================================
1342
+ // SEMANTIC STRUCTURE — WCAG 1.3.1, 2.4.6
1343
+ // ============================================================================
1344
+ analyzeSemanticStructure(content, file) {
1345
+ const issues = [];
1346
+ const lines = content.split("\n");
1347
+ const headingLevels = [];
1348
+ for (let i = 0; i < lines.length; i++) {
1349
+ const line = lines[i];
1350
+ const lineNumber = i + 1;
1351
+ const headingMatch = line.match(/<h([1-6])|role=["']heading["'].*aria-level=["'](\d)["']/i);
1352
+ if (headingMatch) {
1353
+ const level = parseInt(headingMatch[1] || headingMatch[2] || "0");
1354
+ if (level > 0) {
1355
+ headingLevels.push({ level, line: lineNumber });
1356
+ }
1357
+ }
1358
+ if (/<div[^>]*class=["'][^"']*nav|navigation/i.test(line) && !/<nav/i.test(content)) {
930
1359
  issues.push(this.createIssue(
931
1360
  this.generateIssueId(),
932
1361
  "moderate",
933
- "Click handler without keyboard equivalent",
934
- "Add onKeyDown handler for keyboard accessibility",
1362
+ "Navigation using div instead of semantic <nav>",
1363
+ "Use <nav> element for navigation sections. Screen readers announce this as navigation.",
935
1364
  file,
936
1365
  lineNumber,
937
1366
  0.8,
938
- "WCAG 2.1 - 2.1.1 Keyboard",
1367
+ this.getWCAGRef("1.3.1"),
939
1368
  true
940
1369
  ));
941
1370
  }
942
- if (/<input[^>]*(?!aria-label|id.*label)/i.test(line) && !/<label/i.test(lines.slice(Math.max(0, i - 2), i + 2).join(""))) {
1371
+ if (/<div[^>]*class=["'][^"']*header/i.test(line) && !/<header/i.test(content)) {
943
1372
  issues.push(this.createIssue(
944
1373
  this.generateIssueId(),
945
- "moderate",
946
- "Form input may be missing accessible label",
947
- "Associate input with <label> element or use aria-label",
1374
+ "low",
1375
+ "Header content using div instead of semantic <header>",
1376
+ "Use <header> element for page/section headers for better screen reader navigation.",
1377
+ file,
1378
+ lineNumber,
1379
+ 0.7,
1380
+ this.getWCAGRef("1.3.1"),
1381
+ true
1382
+ ));
1383
+ }
1384
+ if (/<div[^>]*class=["'][^"']*footer/i.test(line) && !/<footer/i.test(content)) {
1385
+ issues.push(this.createIssue(
1386
+ this.generateIssueId(),
1387
+ "low",
1388
+ "Footer content using div instead of semantic <footer>",
1389
+ "Use <footer> element for page/section footers for better screen reader navigation.",
948
1390
  file,
949
1391
  lineNumber,
950
1392
  0.7,
951
- "WCAG 2.1 - 1.3.1 Info and Relationships",
1393
+ this.getWCAGRef("1.3.1"),
1394
+ true
1395
+ ));
1396
+ }
1397
+ if (/<div[^>]*class=["'][^"']*main/i.test(line) && !/<main/i.test(content)) {
1398
+ issues.push(this.createIssue(
1399
+ this.generateIssueId(),
1400
+ "moderate",
1401
+ "Main content using div instead of semantic <main>",
1402
+ 'Use <main> element for primary content. Allows "skip to main content" functionality.',
1403
+ file,
1404
+ lineNumber,
1405
+ 0.75,
1406
+ this.getWCAGRef("2.4.1"),
952
1407
  true
953
1408
  ));
954
1409
  }
955
- if (/color:\s*['"]?#[0-9a-fA-F]{3,6}/i.test(line) || /color:\s*['"]?rgb\(/i.test(line)) {
956
- if (/#fff|#FFF|white|#000|#333|gray/i.test(line)) {
1410
+ if (/<div[^>]*class=["'][^"']*(list|items|menu)[^"']*["']/i.test(line)) {
1411
+ const context = lines.slice(i, Math.min(lines.length, i + 10)).join("\n");
1412
+ if (!/<ul|<ol|<menu|role=["']list/i.test(context)) {
957
1413
  issues.push(this.createIssue(
958
1414
  this.generateIssueId(),
959
1415
  "low",
960
- "Potential color contrast issue",
961
- "Ensure color contrast ratio meets WCAG AA (4.5:1 for normal text)",
1416
+ "List-like content without list semantics",
1417
+ 'Use <ul>/<ol> for lists or add role="list" with role="listitem" children.',
962
1418
  file,
963
1419
  lineNumber,
964
- 0.6,
965
- "WCAG 2.1 - 1.4.3 Contrast",
1420
+ 0.65,
1421
+ this.getWCAGRef("1.3.1"),
966
1422
  false
967
1423
  ));
968
1424
  }
969
1425
  }
970
- if (/outline:\s*none|outline:\s*0/i.test(line)) {
1426
+ }
1427
+ for (let i = 1; i < headingLevels.length; i++) {
1428
+ const current = headingLevels[i];
1429
+ const previous = headingLevels[i - 1];
1430
+ if (current.level > previous.level + 1) {
1431
+ issues.push(this.createIssue(
1432
+ this.generateIssueId(),
1433
+ "moderate",
1434
+ `Skipped heading level: h${previous.level} to h${current.level}`,
1435
+ `Heading levels should not skip. Go from h${previous.level} to h${previous.level + 1}. Screen reader users navigate by headings.`,
1436
+ file,
1437
+ current.line,
1438
+ 0.85,
1439
+ this.getWCAGRef("2.4.6"),
1440
+ true
1441
+ ));
1442
+ }
1443
+ }
1444
+ if (headingLevels.length > 0 && headingLevels[0].level !== 1) {
1445
+ issues.push(this.createIssue(
1446
+ this.generateIssueId(),
1447
+ "moderate",
1448
+ `First heading is h${headingLevels[0].level} instead of h1`,
1449
+ "Each page should start with an h1 describing the page content.",
1450
+ file,
1451
+ headingLevels[0].line,
1452
+ 0.8,
1453
+ this.getWCAGRef("2.4.6"),
1454
+ true
1455
+ ));
1456
+ }
1457
+ return issues;
1458
+ }
1459
+ // ============================================================================
1460
+ // ARIA USAGE — WCAG 4.1.2
1461
+ // ============================================================================
1462
+ analyzeARIAUsage(content, file) {
1463
+ const issues = [];
1464
+ const lines = content.split("\n");
1465
+ for (let i = 0; i < lines.length; i++) {
1466
+ const line = lines[i];
1467
+ const lineNumber = i + 1;
1468
+ const roleMatch = line.match(/role=["']([a-z]+)["']/i);
1469
+ if (roleMatch) {
1470
+ const role = roleMatch[1].toLowerCase();
1471
+ const requiredAttrs = REQUIRED_ARIA_ATTRIBUTES[role];
1472
+ if (requiredAttrs && requiredAttrs.length > 0) {
1473
+ const elementContext = this.getMultiLineElement(lines, i, void 0) || line;
1474
+ const missingAttrs = requiredAttrs.filter((attr) => !new RegExp(attr, "i").test(elementContext));
1475
+ if (missingAttrs.length > 0) {
1476
+ issues.push(this.createIssue(
1477
+ this.generateIssueId(),
1478
+ "serious",
1479
+ `Role "${role}" missing required ARIA attributes: ${missingAttrs.join(", ")}`,
1480
+ `Add ${missingAttrs.map((a) => `${a}="value"`).join(" ")} for proper screen reader support.`,
1481
+ file,
1482
+ lineNumber,
1483
+ 0.88,
1484
+ this.getWCAGRef("4.1.2"),
1485
+ true
1486
+ ));
1487
+ }
1488
+ }
1489
+ }
1490
+ if (/aria-hidden=["']true["']/i.test(line)) {
1491
+ const elementContext = lines.slice(Math.max(0, i - 1), i + 5).join("\n");
1492
+ if (/<(button|a|input|select|textarea)|tabIndex=\{?["']?0|tabindex=["']0/i.test(elementContext)) {
1493
+ issues.push(this.createIssue(
1494
+ this.generateIssueId(),
1495
+ "serious",
1496
+ "aria-hidden on element containing focusable content",
1497
+ "Remove aria-hidden or add tabIndex={-1} to focusable children. Hidden content should not be focusable.",
1498
+ file,
1499
+ lineNumber,
1500
+ 0.85,
1501
+ this.getWCAGRef("4.1.2"),
1502
+ true
1503
+ ));
1504
+ }
1505
+ }
1506
+ if (/aria-expanded=["'](yes|no)["']/i.test(line)) {
971
1507
  issues.push(this.createIssue(
972
1508
  this.generateIssueId(),
973
1509
  "serious",
974
- "Focus outline removed - breaks keyboard navigation",
975
- "Provide alternative focus indicator instead of removing outline",
1510
+ 'Invalid aria-expanded value (use "true"/"false" not "yes"/"no")',
1511
+ 'Use aria-expanded="true" or aria-expanded="false".',
976
1512
  file,
977
1513
  lineNumber,
978
- 0.9,
979
- "WCAG 2.1 - 2.4.7 Focus Visible",
1514
+ 0.95,
1515
+ this.getWCAGRef("4.1.2"),
980
1516
  true
981
1517
  ));
982
1518
  }
1519
+ if (/aria-label=/i.test(line)) {
1520
+ if (/<(div|span|p|section)\s/i.test(line) && !/role=/i.test(line)) {
1521
+ issues.push(this.createIssue(
1522
+ this.generateIssueId(),
1523
+ "moderate",
1524
+ "aria-label on non-interactive element without role",
1525
+ "Add an appropriate role or use aria-labelledby for non-interactive regions.",
1526
+ file,
1527
+ lineNumber,
1528
+ 0.7,
1529
+ this.getWCAGRef("4.1.2"),
1530
+ false
1531
+ ));
1532
+ }
1533
+ }
1534
+ if (/aria-live=/i.test(line)) {
1535
+ if (!/aria-live=["'](polite|assertive|off)["']/i.test(line)) {
1536
+ issues.push(this.createIssue(
1537
+ this.generateIssueId(),
1538
+ "moderate",
1539
+ "Invalid aria-live value",
1540
+ 'Use aria-live="polite" for non-urgent updates or aria-live="assertive" for critical alerts.',
1541
+ file,
1542
+ lineNumber,
1543
+ 0.85,
1544
+ this.getWCAGRef("4.1.3"),
1545
+ true
1546
+ ));
1547
+ }
1548
+ }
1549
+ if (/toast|notification|alert|error.*message|success.*message/i.test(line)) {
1550
+ const context = lines.slice(Math.max(0, i - 3), i + 5).join("\n");
1551
+ if (!/aria-live|role=["']alert|role=["']status/i.test(context)) {
1552
+ issues.push(this.createIssue(
1553
+ this.generateIssueId(),
1554
+ "moderate",
1555
+ "Status message may not be announced to screen readers",
1556
+ 'Add role="status" aria-live="polite" for status messages or role="alert" for errors.',
1557
+ file,
1558
+ lineNumber,
1559
+ 0.75,
1560
+ this.getWCAGRef("4.1.3"),
1561
+ true
1562
+ ));
1563
+ }
1564
+ }
983
1565
  }
984
1566
  return issues;
985
1567
  }
986
- analyzeUXPatterns(content, file) {
1568
+ // ============================================================================
1569
+ // COLOR ACCESSIBILITY — WCAG 1.4.1, 1.4.3, 1.4.11
1570
+ // ============================================================================
1571
+ analyzeColorAccessibility(content, file) {
987
1572
  const issues = [];
988
1573
  const lines = content.split("\n");
989
1574
  for (let i = 0; i < lines.length; i++) {
990
1575
  const line = lines[i];
991
1576
  const lineNumber = i + 1;
992
- if (/disabled(?!=\{false\})/i.test(line) && !/title=|aria-describedby/i.test(line)) {
1577
+ if (/color:\s*(red|green|#f00|#0f0|#ff0000|#00ff00)/i.test(line)) {
1578
+ const context = lines.slice(Math.max(0, i - 5), i + 5).join("\n");
1579
+ if (/error|success|warning|valid|invalid/i.test(context)) {
1580
+ if (!/icon|Icon|✓|✗|×|⚠|aria-label|sr-only|visually-hidden/i.test(context)) {
1581
+ issues.push(this.createIssue(
1582
+ this.generateIssueId(),
1583
+ "serious",
1584
+ "Color may be the only indicator of status",
1585
+ "Add icon, text, or other visual indicator besides color. Colorblind users cannot distinguish red/green.",
1586
+ file,
1587
+ lineNumber,
1588
+ 0.8,
1589
+ this.getWCAGRef("1.4.1"),
1590
+ true
1591
+ ));
1592
+ }
1593
+ }
1594
+ }
1595
+ if (/color:\s*#([0-9a-f]{3,6})/i.test(line)) {
1596
+ const hexMatch = line.match(/color:\s*#([0-9a-f]{3,6})/i);
1597
+ if (hexMatch) {
1598
+ const hex = hexMatch[1].toLowerCase();
1599
+ const lightness = this.getRelativeLuminance(hex);
1600
+ if (lightness > 0.3 && lightness < 0.5) {
1601
+ issues.push(this.createIssue(
1602
+ this.generateIssueId(),
1603
+ "moderate",
1604
+ "Text color may have insufficient contrast",
1605
+ "Verify 4.5:1 contrast ratio at webaim.org/resources/contrastchecker. Consider darker (#374151) or lighter (#d1d5db) alternatives.",
1606
+ file,
1607
+ lineNumber,
1608
+ 0.65,
1609
+ this.getWCAGRef("1.4.3"),
1610
+ false
1611
+ ));
1612
+ }
1613
+ }
1614
+ }
1615
+ if (/focus.*border/i.test(line) && !/ring|outline|shadow/i.test(line)) {
993
1616
  issues.push(this.createIssue(
994
1617
  this.generateIssueId(),
995
1618
  "low",
996
- "Disabled element without explanation",
997
- "Add tooltip or text explaining why the action is unavailable",
1619
+ "Focus indicator uses only border - verify 3:1 contrast",
1620
+ "Ensure focus border has 3:1 contrast against adjacent colors. Consider adding focus ring.",
998
1621
  file,
999
1622
  lineNumber,
1000
- 0.7,
1623
+ 0.6,
1624
+ this.getWCAGRef("1.4.11"),
1625
+ false
1626
+ ));
1627
+ }
1628
+ }
1629
+ return issues;
1630
+ }
1631
+ // ============================================================================
1632
+ // TOUCH TARGETS — WCAG 2.5.5, 2.5.8
1633
+ // ============================================================================
1634
+ analyzeTouchTargets(content, file) {
1635
+ const issues = [];
1636
+ const lines = content.split("\n");
1637
+ for (let i = 0; i < lines.length; i++) {
1638
+ const line = lines[i];
1639
+ const lineNumber = i + 1;
1640
+ if (/<(button|a|input|select)|onClick|@click/i.test(line)) {
1641
+ const sizeMatch = line.match(/(?:width|height|size):\s*(\d+)(?:px)?/i) || line.match(/(?:w-|h-)(\d+)/);
1642
+ if (sizeMatch) {
1643
+ const size = parseInt(sizeMatch[1]);
1644
+ const pxSize = size <= 12 ? size * 4 : size;
1645
+ if (pxSize < 24) {
1646
+ issues.push(this.createIssue(
1647
+ this.generateIssueId(),
1648
+ "moderate",
1649
+ `Touch target size ${pxSize}px is below WCAG minimum (24px)`,
1650
+ "Increase touch target to at least 24\xD724px (WCAG AA) or 44\xD744px (WCAG AAA) for mobile usability.",
1651
+ file,
1652
+ lineNumber,
1653
+ 0.75,
1654
+ this.getWCAGRef("2.5.8"),
1655
+ true
1656
+ ));
1657
+ } else if (pxSize < 44) {
1658
+ issues.push(this.createIssue(
1659
+ this.generateIssueId(),
1660
+ "low",
1661
+ `Touch target size ${pxSize}px is below recommended 44px`,
1662
+ "Consider increasing to 44\xD744px for optimal mobile usability (WCAG AAA).",
1663
+ file,
1664
+ lineNumber,
1665
+ 0.6,
1666
+ this.getWCAGRef("2.5.5"),
1667
+ false
1668
+ ));
1669
+ }
1670
+ }
1671
+ }
1672
+ if (/(Icon|icon).*size=["']?(sm|xs|small|12|14|16)/i.test(line)) {
1673
+ const context = lines.slice(Math.max(0, i - 2), i + 2).join("\n");
1674
+ if (/<button|onClick/i.test(context) && !/p-|padding/i.test(context)) {
1675
+ issues.push(this.createIssue(
1676
+ this.generateIssueId(),
1677
+ "moderate",
1678
+ "Small icon button may have insufficient touch target",
1679
+ "Add padding (e.g., p-2 or p-3) to ensure 44\xD744px touch target around small icons.",
1680
+ file,
1681
+ lineNumber,
1682
+ 0.7,
1683
+ this.getWCAGRef("2.5.8"),
1684
+ true
1685
+ ));
1686
+ }
1687
+ }
1688
+ }
1689
+ return issues;
1690
+ }
1691
+ // ============================================================================
1692
+ // MOTION ACCESSIBILITY — WCAG 2.3.3
1693
+ // ============================================================================
1694
+ analyzeMotionAccessibility(content, file) {
1695
+ const issues = [];
1696
+ if (/animation:|@keyframes|animate-|transition:/i.test(content)) {
1697
+ if (!/prefers-reduced-motion/i.test(content)) {
1698
+ issues.push(this.createIssue(
1699
+ this.generateIssueId(),
1700
+ "moderate",
1701
+ "Animations without prefers-reduced-motion support",
1702
+ "Add @media (prefers-reduced-motion: reduce) { animation: none; transition: none; } for motion-sensitive users.",
1703
+ file,
1001
1704
  void 0,
1705
+ 0.8,
1706
+ "WCAG 2.1 AA - 2.3.3 Animation from Interactions",
1707
+ false
1708
+ ));
1709
+ }
1710
+ }
1711
+ if (/autoPlay|autoplay/i.test(content)) {
1712
+ if (!/muted|controls/i.test(content)) {
1713
+ issues.push(this.createIssue(
1714
+ this.generateIssueId(),
1715
+ "serious",
1716
+ "Autoplaying media without mute or controls",
1717
+ "Add muted attribute or provide controls. Autoplay audio can disorient screen reader users.",
1718
+ file,
1719
+ void 0,
1720
+ 0.85,
1721
+ "WCAG 2.1 A - 1.4.2 Audio Control",
1722
+ true
1723
+ ));
1724
+ }
1725
+ }
1726
+ return issues;
1727
+ }
1728
+ // ============================================================================
1729
+ // LINK ACCESSIBILITY — WCAG 2.4.4
1730
+ // ============================================================================
1731
+ analyzeLinks(content, file) {
1732
+ const issues = [];
1733
+ const lines = content.split("\n");
1734
+ for (let i = 0; i < lines.length; i++) {
1735
+ const line = lines[i];
1736
+ const lineNumber = i + 1;
1737
+ if (/<a[^>]*>\s*<\/a>/i.test(line)) {
1738
+ issues.push(this.createIssue(
1739
+ this.generateIssueId(),
1740
+ "critical",
1741
+ "Empty link with no text content",
1742
+ 'Add meaningful link text or aria-label. Screen readers announce this as "link" with no destination.',
1743
+ file,
1744
+ lineNumber,
1745
+ 0.95,
1746
+ this.getWCAGRef("2.4.4"),
1002
1747
  true
1003
1748
  ));
1004
1749
  }
1005
- if (/placeholder=/i.test(line) && !/<label|aria-label/i.test(lines.slice(Math.max(0, i - 2), i + 2).join(""))) {
1750
+ if (/href=["']#["']\s*>/i.test(line) && !/onClick|@click/i.test(line)) {
1006
1751
  issues.push(this.createIssue(
1007
1752
  this.generateIssueId(),
1008
1753
  "moderate",
1009
- "Input uses placeholder as only label",
1010
- "Add visible label - placeholder disappears when user types",
1754
+ 'Link with placeholder href="#"',
1755
+ "Add meaningful destination or use <button> for actions. Placeholder links confuse users.",
1011
1756
  file,
1012
1757
  lineNumber,
1013
- 0.75,
1014
- void 0,
1758
+ 0.8,
1759
+ this.getWCAGRef("2.4.4"),
1015
1760
  true
1016
1761
  ));
1017
1762
  }
1018
- if (/<a[^>]*>\s*<\/a>/i.test(line) || /href=['"]#['"]/i.test(line)) {
1763
+ if (/>(?:click here|here|read more|learn more|more)<\/a>/i.test(line)) {
1019
1764
  issues.push(this.createIssue(
1020
1765
  this.generateIssueId(),
1021
1766
  "moderate",
1022
- "Empty or placeholder link",
1023
- "Add meaningful href and link text",
1767
+ 'Generic link text ("click here", "read more")',
1768
+ "Use descriptive link text that makes sense out of context. Screen reader users navigate by link list.",
1024
1769
  file,
1025
1770
  lineNumber,
1026
- 0.85,
1027
- "WCAG 2.1 - 2.4.4 Link Purpose",
1771
+ 0.75,
1772
+ this.getWCAGRef("2.4.4"),
1028
1773
  true
1029
1774
  ));
1030
1775
  }
1776
+ if (/target=["']_blank["']/i.test(line)) {
1777
+ if (!/aria-label.*new window|aria-label.*new tab|external|External|↗|↪/i.test(line)) {
1778
+ issues.push(this.createIssue(
1779
+ this.generateIssueId(),
1780
+ "low",
1781
+ "Link opens in new window without warning",
1782
+ 'Add visual indicator (\u2197) and aria-label mentioning "opens in new window" for user awareness.',
1783
+ file,
1784
+ lineNumber,
1785
+ 0.65,
1786
+ this.getWCAGRef("3.2.5"),
1787
+ true
1788
+ ));
1789
+ }
1790
+ }
1791
+ if (/<a[^>]*href=["']([^"']+)["'][^>]*>.*<img/i.test(line)) {
1792
+ issues.push(this.createIssue(
1793
+ this.generateIssueId(),
1794
+ "low",
1795
+ "Image and text link may create duplicate links",
1796
+ 'Combine image and text in single link or add aria-hidden="true" to decorative image link.',
1797
+ file,
1798
+ lineNumber,
1799
+ 0.6,
1800
+ this.getWCAGRef("2.4.4"),
1801
+ false
1802
+ ));
1803
+ }
1031
1804
  }
1032
1805
  return issues;
1033
1806
  }
1807
+ // ============================================================================
1808
+ // UTILITY METHODS
1809
+ // ============================================================================
1810
+ /**
1811
+ * Get multi-line element content starting at line index
1812
+ */
1813
+ getMultiLineElement(lines, startIndex, tagName) {
1814
+ const startLine = lines[startIndex];
1815
+ if (!startLine) return null;
1816
+ const tag = tagName || startLine.match(/<([a-zA-Z][a-zA-Z0-9]*)/)?.[1];
1817
+ if (!tag) return startLine;
1818
+ if (new RegExp(`</${tag}|/>`, "i").test(startLine)) {
1819
+ return startLine;
1820
+ }
1821
+ let content = startLine;
1822
+ for (let j = startIndex + 1; j < Math.min(lines.length, startIndex + 20); j++) {
1823
+ content += "\n" + lines[j];
1824
+ if (new RegExp(`</${tag}>|/>`, "i").test(lines[j])) {
1825
+ break;
1826
+ }
1827
+ }
1828
+ return content;
1829
+ }
1830
+ /**
1831
+ * Calculate relative luminance for contrast checking
1832
+ */
1833
+ getRelativeLuminance(hex) {
1834
+ let normalized = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
1835
+ const r = parseInt(normalized.slice(0, 2), 16) / 255;
1836
+ const g = parseInt(normalized.slice(2, 4), 16) / 255;
1837
+ const b = parseInt(normalized.slice(4, 6), 16) / 255;
1838
+ const [R, G, B] = [r, g, b].map(
1839
+ (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
1840
+ );
1841
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
1842
+ }
1034
1843
  /**
1035
1844
  * AI Enhancement for accessibility review
1036
1845
  */
1037
1846
  getAIEnhancementSystemPrompt() {
1038
- return `You are a WCAG 2.1 accessibility expert. Review code for inclusive design.
1039
-
1040
- Analyze detected issues and code for:
1041
- 1. Screen reader compatibility (ARIA labels, roles, live regions)
1042
- 2. Keyboard navigation (focus management, tab order, focus trapping)
1043
- 3. Color contrast (4.5:1 for text, 3:1 for large text)
1044
- 4. Form accessibility (labels, error messages, required fields)
1045
- 5. Dynamic content (loading states, announcements, focus management)
1046
- 6. Reduced motion support (prefers-reduced-motion)
1047
- 7. Touch target sizes (44x44px minimum)
1048
-
1049
- Output STRICT JSON:
1847
+ return `You are a WCAG 2.1 AA accessibility expert and inclusive design advocate.
1848
+ Review code for comprehensive accessibility compliance.
1849
+
1850
+ ## Analysis Categories
1851
+
1852
+ ### Critical (Blocks Access)
1853
+ - Images without alt text
1854
+ - Icon-only buttons missing aria-label
1855
+ - Non-semantic click handlers without keyboard support
1856
+ - Empty links
1857
+ - Missing form labels
1858
+
1859
+ ### Serious (Significantly Impairs)
1860
+ - Focus outline removed without replacement
1861
+ - Positive tabIndex values disrupting tab order
1862
+ - Role without required ARIA attributes
1863
+ - Color-only status indicators
1864
+ - Links without href
1865
+
1866
+ ### Moderate (Creates Barriers)
1867
+ - Skipped heading levels
1868
+ - Missing autocomplete on inputs
1869
+ - Touch targets under 44px
1870
+ - Generic link text
1871
+ - Missing prefers-reduced-motion
1872
+
1873
+ ### Low (Best Practices)
1874
+ - Semantic element opportunities
1875
+ - Status messages without aria-live
1876
+ - External links without warning
1877
+
1878
+ ## WCAG Success Criteria Reference
1879
+ - 1.1.1: Non-text Content (images, icons)
1880
+ - 1.3.1: Info and Relationships (semantic HTML)
1881
+ - 1.4.1: Use of Color (color-only info)
1882
+ - 1.4.3: Contrast (Minimum)
1883
+ - 2.1.1: Keyboard accessibility
1884
+ - 2.4.3: Focus Order
1885
+ - 2.4.4: Link Purpose
1886
+ - 2.4.6: Headings and Labels
1887
+ - 2.4.7: Focus Visible
1888
+ - 2.5.5: Target Size
1889
+ - 4.1.2: Name, Role, Value
1890
+
1891
+ ## Output Format
1050
1892
  {
1051
1893
  "validated": [{
1052
1894
  "original_issue": "...",
@@ -1054,10 +1896,11 @@ Output STRICT JSON:
1054
1896
  "confidence": 0-100,
1055
1897
  "file": "path",
1056
1898
  "line": 123,
1057
- "severity": "serious",
1058
- "wcag_criterion": "WCAG 2.1 - X.X.X Name",
1899
+ "severity": "critical" | "serious" | "moderate" | "low",
1900
+ "wcag_criterion": "WCAG 2.1 AA - X.X.X Name",
1059
1901
  "impact": "How this affects users with disabilities",
1060
- "fix": "Accessible code fix"
1902
+ "code_snippet": "The problematic code",
1903
+ "fix": "Accessible code fix with example"
1061
1904
  }],
1062
1905
  "additional": [{
1063
1906
  "issue": "Accessibility issue found",
@@ -1065,10 +1908,18 @@ Output STRICT JSON:
1065
1908
  "line": 123,
1066
1909
  "severity": "moderate",
1067
1910
  "wcag_criterion": "WCAG criterion",
1068
- "impact": "User impact",
1069
- "fix": "Accessible implementation"
1911
+ "impact": "User impact description",
1912
+ "code_snippet": "Problematic code",
1913
+ "fix": "Accessible implementation with example"
1070
1914
  }],
1071
- "summary": "Overall accessibility assessment"
1915
+ "score": {
1916
+ "overall": 85,
1917
+ "critical_count": 0,
1918
+ "serious_count": 2,
1919
+ "moderate_count": 5,
1920
+ "low_count": 3
1921
+ },
1922
+ "summary": "Overall accessibility assessment with key recommendations"
1072
1923
  }`;
1073
1924
  }
1074
1925
  };
@@ -8007,4 +8858,4 @@ export {
8007
8858
  CustomAgent,
8008
8859
  getAgentRegistry
8009
8860
  };
8010
- //# sourceMappingURL=chunk-HRNBSXN2.js.map
8861
+ //# sourceMappingURL=chunk-B3MBKB2U.js.map