@triedotdev/mcp 1.0.37 → 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=")) {
917
- issues.push(this.createIssue(
918
- this.generateIssueId(),
919
- "serious",
920
- "Image missing alt attribute",
921
- 'Add descriptive alt text for screen readers: alt="description"',
922
- file,
923
- lineNumber,
924
- 0.95,
925
- "WCAG 2.1 - 1.1.1 Non-text Content",
926
- true
927
- ));
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
+ }
928
1056
  }
929
- if (/onClick.*(?!onKeyDown|onKeyPress|onKeyUp)/i.test(line) && !/button|Button|<a /i.test(line)) {
930
- issues.push(this.createIssue(
931
- this.generateIssueId(),
932
- "moderate",
933
- "Click handler without keyboard equivalent",
934
- "Add onKeyDown handler for keyboard accessibility",
935
- file,
936
- lineNumber,
937
- 0.8,
938
- "WCAG 2.1 - 2.1.1 Keyboard",
939
- true
940
- ));
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
+ }
941
1072
  }
942
- if (/<input[^>]*(?!aria-label|id.*label)/i.test(line) && !/<label/i.test(lines.slice(Math.max(0, i - 2), i + 2).join(""))) {
943
- issues.push(this.createIssue(
944
- this.generateIssueId(),
945
- "moderate",
946
- "Form input may be missing accessible label",
947
- "Associate input with <label> element or use aria-label",
948
- file,
949
- lineNumber,
950
- 0.7,
951
- "WCAG 2.1 - 1.3.1 Info and Relationships",
952
- true
953
- ));
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
+ }
954
1243
  }
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)) {
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)) {
957
1247
  issues.push(this.createIssue(
958
1248
  this.generateIssueId(),
959
1249
  "low",
960
- "Potential color contrast issue",
961
- "Ensure color contrast ratio meets WCAG AA (4.5:1 for normal text)",
1250
+ "Disabled element without explanation",
1251
+ "Add tooltip or aria-describedby explaining why the action is unavailable.",
962
1252
  file,
963
1253
  lineNumber,
964
- 0.6,
965
- "WCAG 2.1 - 1.4.3 Contrast",
966
- false
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
967
1302
  ));
968
1303
  }
969
1304
  }
970
- if (/outline:\s*none|outline:\s*0/i.test(line)) {
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];
971
1308
  issues.push(this.createIssue(
972
1309
  this.generateIssueId(),
973
1310
  "serious",
974
- "Focus outline removed - breaks keyboard navigation",
975
- "Provide alternative focus indicator instead of removing outline",
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.",
976
1313
  file,
977
1314
  lineNumber,
978
- 0.9,
979
- "WCAG 2.1 - 2.4.7 Focus Visible",
1315
+ 0.88,
1316
+ this.getWCAGRef("2.4.3"),
980
1317
  true
981
1318
  ));
982
1319
  }
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
+ }
983
1338
  }
984
1339
  return issues;
985
1340
  }
986
- analyzeUXPatterns(content, file) {
1341
+ // ============================================================================
1342
+ // SEMANTIC STRUCTURE — WCAG 1.3.1, 2.4.6
1343
+ // ============================================================================
1344
+ analyzeSemanticStructure(content, file) {
987
1345
  const issues = [];
988
1346
  const lines = content.split("\n");
1347
+ const headingLevels = [];
989
1348
  for (let i = 0; i < lines.length; i++) {
990
1349
  const line = lines[i];
991
1350
  const lineNumber = i + 1;
992
- if (/disabled(?!=\{false\})/i.test(line) && !/title=|aria-describedby/i.test(line)) {
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)) {
1359
+ issues.push(this.createIssue(
1360
+ this.generateIssueId(),
1361
+ "moderate",
1362
+ "Navigation using div instead of semantic <nav>",
1363
+ "Use <nav> element for navigation sections. Screen readers announce this as navigation.",
1364
+ file,
1365
+ lineNumber,
1366
+ 0.8,
1367
+ this.getWCAGRef("1.3.1"),
1368
+ true
1369
+ ));
1370
+ }
1371
+ if (/<div[^>]*class=["'][^"']*header/i.test(line) && !/<header/i.test(content)) {
993
1372
  issues.push(this.createIssue(
994
1373
  this.generateIssueId(),
995
1374
  "low",
996
- "Disabled element without explanation",
997
- "Add tooltip or text explaining why the action is unavailable",
1375
+ "Header content using div instead of semantic <header>",
1376
+ "Use <header> element for page/section headers for better screen reader navigation.",
998
1377
  file,
999
1378
  lineNumber,
1000
1379
  0.7,
1001
- void 0,
1380
+ this.getWCAGRef("1.3.1"),
1002
1381
  true
1003
1382
  ));
1004
1383
  }
1005
- if (/placeholder=/i.test(line) && !/<label|aria-label/i.test(lines.slice(Math.max(0, i - 2), i + 2).join(""))) {
1384
+ if (/<div[^>]*class=["'][^"']*footer/i.test(line) && !/<footer/i.test(content)) {
1006
1385
  issues.push(this.createIssue(
1007
1386
  this.generateIssueId(),
1008
- "moderate",
1009
- "Input uses placeholder as only label",
1010
- "Add visible label - placeholder disappears when user types",
1387
+ "low",
1388
+ "Footer content using div instead of semantic <footer>",
1389
+ "Use <footer> element for page/section footers for better screen reader navigation.",
1011
1390
  file,
1012
1391
  lineNumber,
1013
- 0.75,
1014
- void 0,
1392
+ 0.7,
1393
+ this.getWCAGRef("1.3.1"),
1015
1394
  true
1016
1395
  ));
1017
1396
  }
1018
- if (/<a[^>]*>\s*<\/a>/i.test(line) || /href=['"]#['"]/i.test(line)) {
1397
+ if (/<div[^>]*class=["'][^"']*main/i.test(line) && !/<main/i.test(content)) {
1019
1398
  issues.push(this.createIssue(
1020
1399
  this.generateIssueId(),
1021
1400
  "moderate",
1022
- "Empty or placeholder link",
1023
- "Add meaningful href and link text",
1401
+ "Main content using div instead of semantic <main>",
1402
+ 'Use <main> element for primary content. Allows "skip to main content" functionality.',
1024
1403
  file,
1025
1404
  lineNumber,
1026
- 0.85,
1027
- "WCAG 2.1 - 2.4.4 Link Purpose",
1405
+ 0.75,
1406
+ this.getWCAGRef("2.4.1"),
1028
1407
  true
1029
1408
  ));
1030
1409
  }
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)) {
1413
+ issues.push(this.createIssue(
1414
+ this.generateIssueId(),
1415
+ "low",
1416
+ "List-like content without list semantics",
1417
+ 'Use <ul>/<ol> for lists or add role="list" with role="listitem" children.',
1418
+ file,
1419
+ lineNumber,
1420
+ 0.65,
1421
+ this.getWCAGRef("1.3.1"),
1422
+ false
1423
+ ));
1424
+ }
1425
+ }
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)) {
1507
+ issues.push(this.createIssue(
1508
+ this.generateIssueId(),
1509
+ "serious",
1510
+ 'Invalid aria-expanded value (use "true"/"false" not "yes"/"no")',
1511
+ 'Use aria-expanded="true" or aria-expanded="false".',
1512
+ file,
1513
+ lineNumber,
1514
+ 0.95,
1515
+ this.getWCAGRef("4.1.2"),
1516
+ true
1517
+ ));
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
+ }
1031
1565
  }
1032
1566
  return issues;
1033
1567
  }
1568
+ // ============================================================================
1569
+ // COLOR ACCESSIBILITY — WCAG 1.4.1, 1.4.3, 1.4.11
1570
+ // ============================================================================
1571
+ analyzeColorAccessibility(content, file) {
1572
+ const issues = [];
1573
+ const lines = content.split("\n");
1574
+ for (let i = 0; i < lines.length; i++) {
1575
+ const line = lines[i];
1576
+ const lineNumber = i + 1;
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)) {
1616
+ issues.push(this.createIssue(
1617
+ this.generateIssueId(),
1618
+ "low",
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.",
1621
+ file,
1622
+ lineNumber,
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,
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"),
1747
+ true
1748
+ ));
1749
+ }
1750
+ if (/href=["']#["']\s*>/i.test(line) && !/onClick|@click/i.test(line)) {
1751
+ issues.push(this.createIssue(
1752
+ this.generateIssueId(),
1753
+ "moderate",
1754
+ 'Link with placeholder href="#"',
1755
+ "Add meaningful destination or use <button> for actions. Placeholder links confuse users.",
1756
+ file,
1757
+ lineNumber,
1758
+ 0.8,
1759
+ this.getWCAGRef("2.4.4"),
1760
+ true
1761
+ ));
1762
+ }
1763
+ if (/>(?:click here|here|read more|learn more|more)<\/a>/i.test(line)) {
1764
+ issues.push(this.createIssue(
1765
+ this.generateIssueId(),
1766
+ "moderate",
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.",
1769
+ file,
1770
+ lineNumber,
1771
+ 0.75,
1772
+ this.getWCAGRef("2.4.4"),
1773
+ true
1774
+ ));
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
+ }
1804
+ }
1805
+ return issues;
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.
1847
+ return `You are a WCAG 2.1 AA accessibility expert and inclusive design advocate.
1848
+ Review code for comprehensive accessibility compliance.
1039
1849
 
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)
1850
+ ## Analysis Categories
1048
1851
 
1049
- Output STRICT JSON:
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
  };
@@ -6541,107 +7392,1096 @@ var DataFlowAgent = class extends BaseAgent {
6541
7392
  }
6542
7393
  return issues;
6543
7394
  }
6544
- checkDataTransformations(content, file) {
7395
+ checkDataTransformations(content, file) {
7396
+ const issues = [];
7397
+ const lines = content.split("\n");
7398
+ for (let i = 0; i < lines.length; i++) {
7399
+ const line = lines[i] || "";
7400
+ const lineNumber = i + 1;
7401
+ if (/JSON\.parse\(/.test(line)) {
7402
+ const context = lines.slice(Math.max(0, i - 3), Math.min(i + 5, lines.length)).join("\n");
7403
+ if (!/try|catch|\.catch/.test(context)) {
7404
+ issues.push(this.createIssue(
7405
+ this.generateIssueId(),
7406
+ "moderate",
7407
+ "JSON.parse without error handling - will throw on invalid JSON",
7408
+ "Wrap in try/catch or use a safe parse utility",
7409
+ file,
7410
+ lineNumber,
7411
+ 0.85,
7412
+ void 0,
7413
+ false,
7414
+ { category: "parse-error", effort: "easy" }
7415
+ ));
7416
+ }
7417
+ }
7418
+ if (/parseInt\s*\([^,)]+\)/.test(line)) {
7419
+ if (!/,\s*10/.test(line)) {
7420
+ issues.push(this.createIssue(
7421
+ this.generateIssueId(),
7422
+ "low",
7423
+ "parseInt without radix parameter",
7424
+ "Add radix: parseInt(value, 10) to avoid octal issues",
7425
+ file,
7426
+ lineNumber,
7427
+ 0.7,
7428
+ void 0,
7429
+ true,
7430
+ { category: "parse", effort: "trivial" }
7431
+ ));
7432
+ }
7433
+ }
7434
+ if (/Number\(|parseFloat\(|\+\s*\w+/.test(line)) {
7435
+ const context = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
7436
+ if (!/isNaN|Number\.isFinite|isFinite|\|\|\s*0/.test(context)) {
7437
+ issues.push(this.createIssue(
7438
+ this.generateIssueId(),
7439
+ "low",
7440
+ "Number conversion without NaN check",
7441
+ "Check for NaN: isNaN(result) or provide fallback: value || 0",
7442
+ file,
7443
+ lineNumber,
7444
+ 0.55,
7445
+ void 0,
7446
+ false,
7447
+ { category: "nan", effort: "easy" }
7448
+ ));
7449
+ }
7450
+ }
7451
+ }
7452
+ return issues;
7453
+ }
7454
+ checkTypeCoercion(content, file) {
7455
+ const issues = [];
7456
+ const lines = content.split("\n");
7457
+ for (let i = 0; i < lines.length; i++) {
7458
+ const line = lines[i] || "";
7459
+ const lineNumber = i + 1;
7460
+ if (/[^!=]==[^=]/.test(line) && !/===/.test(line)) {
7461
+ if (/null|undefined|""|\d+|true|false/.test(line)) {
7462
+ issues.push(this.createIssue(
7463
+ this.generateIssueId(),
7464
+ "moderate",
7465
+ "Loose equality (==) may cause unexpected type coercion",
7466
+ "Use strict equality (===) for predictable comparisons",
7467
+ file,
7468
+ lineNumber,
7469
+ 0.8,
7470
+ void 0,
7471
+ true,
7472
+ { category: "equality", effort: "trivial" }
7473
+ ));
7474
+ }
7475
+ }
7476
+ if (/if\s*\(\s*\w+\s*\)/.test(line)) {
7477
+ const varName = line.match(/if\s*\(\s*(\w+)\s*\)/)?.[1];
7478
+ if (varName && /count|total|length|index|num|amount|price/i.test(varName)) {
7479
+ issues.push(this.createIssue(
7480
+ this.generateIssueId(),
7481
+ "low",
7482
+ "Truthy check on number variable - 0 is falsy but might be valid",
7483
+ "Consider explicit: if (count !== undefined) or if (count > 0)",
7484
+ file,
7485
+ lineNumber,
7486
+ 0.6,
7487
+ void 0,
7488
+ false,
7489
+ { category: "falsy", effort: "easy" }
7490
+ ));
7491
+ }
7492
+ }
7493
+ }
7494
+ return issues;
7495
+ }
7496
+ };
7497
+
7498
+ // src/agents/moneybags.ts
7499
+ var MONEYBAGS_ASCII = `
7500
+ \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
7501
+ \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
7502
+ \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2554\u255D
7503
+ \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2588\u2588\u2554\u255D
7504
+ \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551
7505
+ \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D
7506
+
7507
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
7508
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
7509
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
7510
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551
7511
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
7512
+ \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
7513
+ `;
7514
+ var MONEYBAGS_QUOTES = [
7515
+ `"Every bug has a price tag. I'm here to show you the receipt."`,
7516
+ `"That 'temporary fix' will cost you $50k when it hits production."`,
7517
+ `"Show me the bug, I'll show you the money you're about to lose."`,
7518
+ '"A penny saved in development is $30 saved in production."',
7519
+ `"Your CFO doesn't care about code quality. They care about THIS."`,
7520
+ `"Floating-point for money? That's going to be an expensive lesson."`,
7521
+ '"The best time to fix a bug was yesterday. The second best time is before production."',
7522
+ '"I see dead... budget allocations for emergency fixes."',
7523
+ '"You can pay me now, or pay production support later. Your choice."',
7524
+ '"That empty catch block? $5,600 per minute when it fails silently."'
7525
+ ];
7526
+ var BASE_COST_BY_SEVERITY = {
7527
+ critical: 5e3,
7528
+ // Critical bugs found in dev - still expensive to fix properly
7529
+ serious: 2e3,
7530
+ // Serious issues need careful remediation
7531
+ moderate: 500,
7532
+ // Moderate issues are quicker fixes
7533
+ low: 100
7534
+ // Low severity - mostly cleanup
7535
+ };
7536
+ var PRODUCTION_MULTIPLIER = {
7537
+ critical: 30,
7538
+ // Critical bugs can cause outages, breaches
7539
+ serious: 20,
7540
+ // Serious bugs cause significant user impact
7541
+ moderate: 10,
7542
+ // Moderate bugs require hotfixes, user support
7543
+ low: 5
7544
+ // Low bugs accumulate tech debt
7545
+ };
7546
+ var CATEGORY_MULTIPLIERS = {
7547
+ // Security categories - based on breach cost data
7548
+ "security": { multiplier: 8, reason: "Security vulnerabilities can lead to data breaches (avg $4.45M)" },
7549
+ "authentication": { multiplier: 10, reason: "Auth bypass leads to complete system compromise" },
7550
+ "authorization": { multiplier: 8, reason: "Authorization flaws expose sensitive data" },
7551
+ "injection": { multiplier: 12, reason: "SQL/Command injection enables full system takeover" },
7552
+ "xss": { multiplier: 5, reason: "XSS enables session hijacking and data theft" },
7553
+ "secrets": { multiplier: 15, reason: "Exposed secrets require immediate rotation and audit" },
7554
+ "cryptography": { multiplier: 6, reason: "Weak crypto undermines entire security model" },
7555
+ // Data integrity categories
7556
+ "data-loss": { multiplier: 20, reason: "Data loss can be irrecoverable and legally actionable" },
7557
+ "data-corruption": { multiplier: 15, reason: "Data corruption requires manual recovery and validation" },
7558
+ "privacy": { multiplier: 10, reason: "Privacy violations carry regulatory fines (GDPR: 4% revenue)" },
7559
+ // Financial categories
7560
+ "payment": { multiplier: 25, reason: "Payment bugs can cause direct financial loss or fraud" },
7561
+ "billing": { multiplier: 20, reason: "Billing errors require refunds and erode trust" },
7562
+ "financial-calculation": { multiplier: 15, reason: "Incorrect calculations compound over time" },
7563
+ // Reliability categories
7564
+ "crash": { multiplier: 8, reason: "Crashes cause downtime (avg $5,600/minute)" },
7565
+ "memory-leak": { multiplier: 5, reason: "Memory leaks cause gradual degradation and outages" },
7566
+ "deadlock": { multiplier: 10, reason: "Deadlocks require restarts and cause data inconsistency" },
7567
+ "race-condition": { multiplier: 8, reason: "Race conditions cause intermittent, hard-to-debug failures" },
7568
+ // User experience
7569
+ "accessibility": { multiplier: 3, reason: "Accessibility issues can lead to lawsuits (ADA compliance)" },
7570
+ "performance": { multiplier: 2, reason: "Performance issues hurt conversion rates (~7% per 1s delay)" },
7571
+ "ux": { multiplier: 1.5, reason: "UX bugs increase support costs and churn" },
7572
+ // Code quality
7573
+ "bug": { multiplier: 2, reason: "General bugs require developer time and QA cycles" },
7574
+ "type-error": { multiplier: 1.5, reason: "Type errors caught early save debugging time" },
7575
+ "logic-error": { multiplier: 3, reason: "Logic errors produce incorrect business outcomes" },
7576
+ // Default
7577
+ "default": { multiplier: 2, reason: "General code issues" }
7578
+ };
7579
+ var CONTEXT_MULTIPLIERS = {
7580
+ touchesPayments: { multiplier: 5, description: "Payment processing code" },
7581
+ touchesAuth: { multiplier: 4, description: "Authentication/authorization code" },
7582
+ touchesUserData: { multiplier: 3, description: "User PII handling" },
7583
+ touchesHealthData: { multiplier: 6, description: "Health data (HIPAA liability)" },
7584
+ touchesDatabase: { multiplier: 2.5, description: "Database operations" },
7585
+ touchesAPI: { multiplier: 2, description: "External API integrations" },
7586
+ touchesCrypto: { multiplier: 3, description: "Cryptographic operations" },
7587
+ touchesFileSystem: { multiplier: 2, description: "File system operations" }
7588
+ };
7589
+ var EFFORT_HOURS = {
7590
+ trivial: 0.5,
7591
+ easy: 2,
7592
+ medium: 8,
7593
+ hard: 24
7594
+ };
7595
+ var DEVELOPER_HOURLY_RATE = 150;
7596
+ var DEFAULT_USER_COUNT = 250;
7597
+ var USER_COUNT_MULTIPLIERS = [
7598
+ { threshold: 0, multiplier: 0.1, label: "Pre-launch (0 users)" },
7599
+ { threshold: 50, multiplier: 0.3, label: "MVP (50 users)" },
7600
+ { threshold: 250, multiplier: 1, label: "Early stage (250 users)" },
7601
+ // Baseline (default)
7602
+ { threshold: 1e3, multiplier: 2, label: "Growing (1K users)" },
7603
+ { threshold: 5e3, multiplier: 4, label: "Traction (5K users)" },
7604
+ { threshold: 25e3, multiplier: 8, label: "Scale-up (25K users)" },
7605
+ { threshold: 1e5, multiplier: 15, label: "Growth (100K users)" },
7606
+ { threshold: 5e5, multiplier: 25, label: "Large (500K users)" },
7607
+ { threshold: 1e6, multiplier: 40, label: "Enterprise (1M+ users)" }
7608
+ ];
7609
+ var PER_USER_COSTS = {
7610
+ "data-loss": 5,
7611
+ // $5 per affected user (notification, support)
7612
+ "privacy": 3.5,
7613
+ // $3.50 per user (GDPR: can be much higher)
7614
+ "payment": 0.5,
7615
+ // $0.50 per transaction affected
7616
+ "billing": 0.25,
7617
+ // $0.25 per billing record affected
7618
+ "security": 2,
7619
+ // $2 per potentially affected user
7620
+ "accessibility": 0.1
7621
+ // $0.10 per user (support costs)
7622
+ };
7623
+ var MoneybagAgent = class extends BaseAgent {
7624
+ name = "moneybags";
7625
+ description = "Estimates the dollar cost of bugs based on severity, category, and user scale. Uses industry research (IBM, NIST, Ponemon) with configurable user count scaling.";
7626
+ version = "1.1.0";
7627
+ bannerShown = false;
7628
+ config = {
7629
+ userCount: DEFAULT_USER_COUNT,
7630
+ developerRate: DEVELOPER_HOURLY_RATE
7631
+ };
7632
+ /**
7633
+ * Configure the Moneybags agent
7634
+ * @param config User count and other scaling options
7635
+ */
7636
+ configure(config) {
7637
+ this.config = { ...this.config, ...config };
7638
+ }
7639
+ /**
7640
+ * Get the current user count
7641
+ */
7642
+ get userCount() {
7643
+ return this.config.userCount ?? DEFAULT_USER_COUNT;
7644
+ }
7645
+ /**
7646
+ * Get the user scale multiplier for current user count
7647
+ */
7648
+ getUserScaleMultiplier() {
7649
+ const count = this.userCount;
7650
+ let scale = USER_COUNT_MULTIPLIERS[0];
7651
+ for (const tier of USER_COUNT_MULTIPLIERS) {
7652
+ if (count >= tier.threshold) {
7653
+ scale = tier;
7654
+ }
7655
+ }
7656
+ return { multiplier: scale.multiplier, label: scale.label };
7657
+ }
7658
+ /**
7659
+ * Display the Moneybags entrance banner
7660
+ */
7661
+ displayMoneybagsBanner() {
7662
+ if (this.bannerShown) return;
7663
+ this.bannerShown = true;
7664
+ const quote = MONEYBAGS_QUOTES[Math.floor(Math.random() * MONEYBAGS_QUOTES.length)];
7665
+ console.error("\n" + "\u2550".repeat(60));
7666
+ console.error(MONEYBAGS_ASCII);
7667
+ console.error(" \u{1F4B0} Bug Cost Estimator v" + this.version + " \u{1F4B0}");
7668
+ console.error("");
7669
+ console.error(" " + quote);
7670
+ console.error("\u2550".repeat(60) + "\n");
7671
+ }
7672
+ // Run after other agents so we can analyze their findings
7673
+ get priority() {
7674
+ return {
7675
+ name: this.name,
7676
+ tier: 3,
7677
+ // Run after primary agents
7678
+ estimatedTimeMs: 50,
7679
+ dependencies: ["security", "bug-finding", "privacy", "performance"]
7680
+ };
7681
+ }
7682
+ shouldActivate(context) {
7683
+ return context.touchesPayments || context.touchesAuth || context.touchesUserData || context.touchesHealthData || context.touchesDatabase || context.isNewFeature;
7684
+ }
7685
+ getActivationConfidence(context) {
7686
+ let confidence = 0.3;
7687
+ if (context.touchesPayments) confidence += 0.3;
7688
+ if (context.touchesAuth) confidence += 0.2;
7689
+ if (context.touchesUserData) confidence += 0.15;
7690
+ if (context.touchesHealthData) confidence += 0.25;
7691
+ if (context.touchesDatabase) confidence += 0.1;
7692
+ return Math.min(confidence, 1);
7693
+ }
7694
+ /**
7695
+ * Check file relevance for cost analysis
7696
+ */
7697
+ checkFileRelevance(file, content) {
7698
+ if (/node_modules|\.d\.ts$|\.min\.|dist\/|build\/|\.lock$/.test(file)) {
7699
+ return { isRelevant: false, reason: "Skip file", priority: "low", indicators: [] };
7700
+ }
7701
+ const indicators = [];
7702
+ let priority = "low";
7703
+ if (/payment|stripe|paypal|billing|credit|debit|invoice/i.test(content)) {
7704
+ indicators.push("payment processing");
7705
+ priority = "high";
7706
+ }
7707
+ if (/auth|login|session|jwt|token|oauth|password/i.test(content)) {
7708
+ indicators.push("authentication");
7709
+ priority = "high";
7710
+ }
7711
+ if (/encrypt|decrypt|crypto|hash|secret|key/i.test(content)) {
7712
+ indicators.push("cryptography");
7713
+ priority = "high";
7714
+ }
7715
+ if (/pii|ssn|social.?security|health|hipaa|gdpr|dob|birth/i.test(content)) {
7716
+ indicators.push("sensitive data");
7717
+ priority = "high";
7718
+ }
7719
+ if (/database|sql|query|prisma|mongoose|sequelize/i.test(content)) {
7720
+ indicators.push("database operations");
7721
+ if (priority === "low") priority = "medium";
7722
+ }
7723
+ if (/api|fetch|axios|http|request/i.test(content)) {
7724
+ indicators.push("API calls");
7725
+ if (priority === "low") priority = "medium";
7726
+ }
7727
+ return {
7728
+ isRelevant: content.length > 50,
7729
+ reason: indicators.length > 0 ? `Cost-sensitive: ${indicators.join(", ")}` : "General code",
7730
+ priority,
7731
+ indicators
7732
+ };
7733
+ }
7734
+ /**
7735
+ * Estimate the cost of an issue
7736
+ * Costs are scaled based on user count (default: 1,000 users)
7737
+ */
7738
+ estimateCost(issue, context) {
7739
+ const baseCost = BASE_COST_BY_SEVERITY[issue.severity];
7740
+ const productionMultiplier = PRODUCTION_MULTIPLIER[issue.severity];
7741
+ const category = this.inferCategory(issue);
7742
+ const categoryData = CATEGORY_MULTIPLIERS[category] || CATEGORY_MULTIPLIERS["default"];
7743
+ const categoryMultiplier = categoryData.multiplier;
7744
+ const categoryReason = categoryData.reason;
7745
+ let contextMultiplier = 1;
7746
+ const contextFactors = [];
7747
+ for (const [key, data] of Object.entries(CONTEXT_MULTIPLIERS)) {
7748
+ if (context[key]) {
7749
+ contextMultiplier *= Math.sqrt(data.multiplier);
7750
+ contextFactors.push(data.description);
7751
+ }
7752
+ }
7753
+ contextMultiplier = Math.min(contextMultiplier, 10);
7754
+ const userScale = this.getUserScaleMultiplier();
7755
+ const userScaleMultiplier = userScale.multiplier;
7756
+ const userScaleLabel = userScale.label;
7757
+ const userCount = this.userCount;
7758
+ const perUserRate = PER_USER_COSTS[category] || 0;
7759
+ const affectedUserPercent = {
7760
+ critical: 0.75,
7761
+ // 75% of users potentially affected
7762
+ serious: 0.4,
7763
+ // 40% of users
7764
+ moderate: 0.15,
7765
+ // 15% of users
7766
+ low: 0.05
7767
+ // 5% of users
7768
+ };
7769
+ const affectedUsers = Math.round(userCount * affectedUserPercent[issue.severity]);
7770
+ const perUserCost = Math.round(perUserRate * affectedUsers);
7771
+ const effort = issue.effort || "medium";
7772
+ const fixHours = EFFORT_HOURS[effort] || 8;
7773
+ const developerRate = this.config.developerRate ?? DEVELOPER_HOURLY_RATE;
7774
+ const fixCost = fixHours * developerRate;
7775
+ const totalNowCost = Math.round(
7776
+ baseCost * categoryMultiplier * contextMultiplier * userScaleMultiplier + fixCost
7777
+ );
7778
+ const totalProductionCost = Math.round(
7779
+ baseCost * productionMultiplier * categoryMultiplier * contextMultiplier * userScaleMultiplier + perUserCost + // Add per-user costs for production impact
7780
+ fixCost * 3
7781
+ // Production fixes take 3x longer (debugging, deployment, rollback, post-mortem)
7782
+ );
7783
+ const savings = totalProductionCost - totalNowCost;
7784
+ const summary = this.generateCostSummary(
7785
+ issue,
7786
+ totalNowCost,
7787
+ totalProductionCost,
7788
+ savings,
7789
+ contextFactors,
7790
+ userCount
7791
+ );
7792
+ return {
7793
+ baseCost,
7794
+ productionCost: baseCost * productionMultiplier,
7795
+ categoryMultiplier,
7796
+ categoryReason,
7797
+ contextMultiplier: Math.round(contextMultiplier * 100) / 100,
7798
+ contextFactors,
7799
+ userScaleMultiplier,
7800
+ userScaleLabel,
7801
+ userCount,
7802
+ perUserCost,
7803
+ fixCost,
7804
+ totalNowCost,
7805
+ totalProductionCost,
7806
+ savings,
7807
+ summary
7808
+ };
7809
+ }
7810
+ /**
7811
+ * Infer the category of an issue based on its content
7812
+ */
7813
+ inferCategory(issue) {
7814
+ const text = `${issue.issue} ${issue.fix} ${issue.category || ""}`.toLowerCase();
7815
+ if (/sql.?inject|command.?inject|inject/i.test(text)) return "injection";
7816
+ if (/xss|cross.?site|script/i.test(text)) return "xss";
7817
+ if (/secret|api.?key|password|credential|token.*expos/i.test(text)) return "secrets";
7818
+ if (/auth.*bypass|missing.*auth|unauthorized/i.test(text)) return "authentication";
7819
+ if (/idor|access.?control|authorization/i.test(text)) return "authorization";
7820
+ if (/crypto|encrypt|hash|cipher/i.test(text)) return "cryptography";
7821
+ if (/secur|vuln/i.test(text)) return "security";
7822
+ if (/data.?loss|delete|truncat/i.test(text)) return "data-loss";
7823
+ if (/corrupt|invalid.?data|data.?integrit/i.test(text)) return "data-corruption";
7824
+ if (/privacy|pii|gdpr|personal.?data/i.test(text)) return "privacy";
7825
+ if (/payment|stripe|charge|refund|transaction/i.test(text)) return "payment";
7826
+ if (/billing|invoice|subscription/i.test(text)) return "billing";
7827
+ if (/calculat|amount|price|total|sum/i.test(text)) return "financial-calculation";
7828
+ if (/crash|exception|fatal|panic/i.test(text)) return "crash";
7829
+ if (/memory.?leak|heap|oom/i.test(text)) return "memory-leak";
7830
+ if (/deadlock|lock|mutex/i.test(text)) return "deadlock";
7831
+ if (/race|concurrent|thread.?safe/i.test(text)) return "race-condition";
7832
+ if (/accessib|a11y|aria|screen.?reader/i.test(text)) return "accessibility";
7833
+ if (/perform|slow|latency|timeout/i.test(text)) return "performance";
7834
+ if (/type.?error|typescript|type/i.test(text)) return "type-error";
7835
+ if (/logic|incorrect|wrong/i.test(text)) return "logic-error";
7836
+ if (/bug|issue|error/i.test(text)) return "bug";
7837
+ return "default";
7838
+ }
7839
+ /**
7840
+ * Generate a human-readable cost summary
7841
+ */
7842
+ generateCostSummary(_issue, nowCost, productionCost, savings, contextFactors, userCount) {
7843
+ const formatCurrency = (n) => n >= 1e3 ? `$${(n / 1e3).toFixed(1)}k` : `$${n}`;
7844
+ const formatUsers = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(0)}K` : `${n}`;
7845
+ let summary = `\u{1F4B0} Fix now: ${formatCurrency(nowCost)} | If production: ${formatCurrency(productionCost)} | Save: ${formatCurrency(savings)}`;
7846
+ summary += ` (${formatUsers(userCount)} users)`;
7847
+ if (contextFactors.length > 0) {
7848
+ summary += ` | Risk factors: ${contextFactors.join(", ")}`;
7849
+ }
7850
+ const scaledHighThreshold = 5e4 * (userCount / 1e3);
7851
+ const scaledMediumThreshold = 1e4 * (userCount / 1e3);
7852
+ if (productionCost > scaledHighThreshold) {
7853
+ summary = `\u{1F525} HIGH COST RISK - ${summary}`;
7854
+ } else if (productionCost > scaledMediumThreshold) {
7855
+ summary = `\u26A0\uFE0F SIGNIFICANT COST - ${summary}`;
7856
+ }
7857
+ return summary;
7858
+ }
7859
+ /**
7860
+ * Pattern-based analysis - Moneybags focuses on analyzing OTHER agents' findings
7861
+ * but also catches high-cost patterns directly
7862
+ */
7863
+ async analyzeFiles(files, _context) {
7864
+ this.displayMoneybagsBanner();
7865
+ const issues = [];
7866
+ const HIGH_COST_PATTERNS = [
7867
+ {
7868
+ pattern: /(?:price|amount|total|cost)\s*=\s*parseFloat\s*\([^)]+\)/i,
7869
+ severity: "serious",
7870
+ issue: "Floating-point arithmetic for money calculations",
7871
+ fix: "Use integer cents or a decimal library (e.g., Decimal.js, dinero.js) to avoid rounding errors",
7872
+ category: "financial-calculation"
7873
+ },
7874
+ {
7875
+ pattern: /Math\.floor.*(?:price|amount|total)/i,
7876
+ severity: "moderate",
7877
+ issue: "Flooring money values can cause rounding discrepancies",
7878
+ fix: "Use proper rounding (Math.round) or banker's rounding for financial calculations",
7879
+ category: "financial-calculation"
7880
+ },
7881
+ {
7882
+ pattern: /catch\s*\([^)]*\)\s*\{\s*\}/,
7883
+ severity: "moderate",
7884
+ issue: "Empty catch block swallowing errors",
7885
+ fix: "Log or handle errors appropriately to avoid silent failures",
7886
+ category: "bug"
7887
+ },
7888
+ {
7889
+ pattern: /(?:if|while)\s*\(\s*\w+\s*=[^=]/,
7890
+ severity: "serious",
7891
+ issue: "Assignment in condition (likely meant to compare)",
7892
+ fix: "Use === for comparison, not = for assignment",
7893
+ category: "logic-error"
7894
+ },
7895
+ {
7896
+ pattern: /DELETE\s+FROM\s+\w+\s*(?:WHERE\s+1|;)/i,
7897
+ severity: "critical",
7898
+ issue: "Dangerous DELETE statement - may delete all records",
7899
+ fix: "Add proper WHERE clause with specific conditions",
7900
+ category: "data-loss"
7901
+ },
7902
+ {
7903
+ pattern: /(?:DROP|TRUNCATE)\s+TABLE/i,
7904
+ severity: "critical",
7905
+ issue: "Destructive SQL operation in code",
7906
+ fix: "Move destructive operations to migration scripts with proper safeguards",
7907
+ category: "data-loss"
7908
+ }
7909
+ ];
7910
+ for (const file of files) {
7911
+ if (/node_modules|\.d\.ts$|\.min\.|dist\/|build\//.test(file)) continue;
7912
+ try {
7913
+ const content = await this.readFile(file);
7914
+ const lines = content.split("\n");
7915
+ for (let i = 0; i < lines.length; i++) {
7916
+ const line = lines[i] || "";
7917
+ for (const { pattern, severity, issue: issueText, fix, category } of HIGH_COST_PATTERNS) {
7918
+ if (pattern.test(line)) {
7919
+ this.progress?.found(severity, `${issueText} at line ${i + 1}`);
7920
+ const codeContext = {
7921
+ changeType: "general",
7922
+ isNewFeature: false,
7923
+ touchesUserData: /user|customer|client/i.test(content),
7924
+ touchesAuth: /auth|login|session/i.test(content),
7925
+ touchesPayments: /payment|stripe|charge/i.test(content),
7926
+ touchesDatabase: /sql|query|database/i.test(content),
7927
+ touchesAPI: /api|fetch|axios/i.test(content),
7928
+ touchesUI: false,
7929
+ touchesHealthData: /health|hipaa|medical/i.test(content),
7930
+ touchesSecurityConfig: false,
7931
+ linesChanged: 0,
7932
+ filePatterns: [],
7933
+ touchesCrypto: /crypto|encrypt|hash/i.test(content),
7934
+ touchesFileSystem: /fs\.|readFile|writeFile/i.test(content),
7935
+ touchesThirdPartyAPI: false,
7936
+ touchesLogging: false,
7937
+ touchesErrorHandling: false,
7938
+ hasTests: false,
7939
+ complexity: "medium",
7940
+ patterns: {
7941
+ hasAsyncCode: /async|await|promise/i.test(content),
7942
+ hasFormHandling: false,
7943
+ hasFileUploads: false,
7944
+ hasEmailHandling: false,
7945
+ hasRateLimiting: false,
7946
+ hasWebSockets: false,
7947
+ hasCaching: false,
7948
+ hasQueue: false
7949
+ }
7950
+ };
7951
+ const baseIssue = this.createIssue(
7952
+ this.generateIssueId(),
7953
+ severity,
7954
+ issueText,
7955
+ fix,
7956
+ file,
7957
+ i + 1,
7958
+ 0.9,
7959
+ void 0,
7960
+ true,
7961
+ { category }
7962
+ );
7963
+ const estimate = this.estimateCost(baseIssue, codeContext);
7964
+ const issueWithCost = {
7965
+ ...baseIssue,
7966
+ issue: `${issueText} | ${estimate.summary}`
7967
+ };
7968
+ issues.push(issueWithCost);
7969
+ }
7970
+ }
7971
+ }
7972
+ } catch {
7973
+ }
7974
+ }
7975
+ return issues;
7976
+ }
7977
+ /**
7978
+ * Override to add cost analysis to AI enhancement
7979
+ */
7980
+ getAIEnhancementSystemPrompt() {
7981
+ return `You are a senior engineer performing cost-impact analysis on code issues.
7982
+
7983
+ Your goal is to identify bugs that will cost the most money if they reach production.
7984
+
7985
+ FOCUS ON HIGH-COST ISSUES:
7986
+ 1. **Financial Bugs** (25x multiplier): Payment, billing, pricing calculation errors
7987
+ 2. **Data Loss** (20x multiplier): Accidental deletes, data corruption, no backups
7988
+ 3. **Security Breaches** (15x multiplier): Exposed secrets, auth bypass, injection
7989
+ 4. **Privacy Violations** (10x multiplier): PII exposure, GDPR violations
7990
+ 5. **System Crashes** (8x multiplier): Production downtime ($5,600/minute average)
7991
+
7992
+ COST ESTIMATION FACTORS:
7993
+ - Critical bugs: $5k now, $150k in production
7994
+ - Serious bugs: $2k now, $40k in production
7995
+ - Moderate bugs: $500 now, $5k in production
7996
+ - Low bugs: $100 now, $500 in production
7997
+
7998
+ For each issue, estimate:
7999
+ - Cost to fix NOW (during development)
8000
+ - Cost if it reaches PRODUCTION (support, hotfix, reputation, legal)
8001
+ - Savings by fixing immediately
8002
+
8003
+ Output STRICT JSON:
8004
+ {
8005
+ "validated": [{
8006
+ "original_issue": "...",
8007
+ "verdict": "TRUE_POSITIVE" | "FALSE_POSITIVE",
8008
+ "confidence": 0-100,
8009
+ "file": "path",
8010
+ "line": 123,
8011
+ "severity": "critical",
8012
+ "cost_now": 5000,
8013
+ "cost_production": 150000,
8014
+ "cost_category": "payment|security|data-loss|privacy|crash|performance",
8015
+ "fix": "Code fix"
8016
+ }],
8017
+ "additional": [{
8018
+ "issue": "High-cost bug found",
8019
+ "file": "path",
8020
+ "line": 123,
8021
+ "severity": "serious",
8022
+ "cost_now": 2000,
8023
+ "cost_production": 40000,
8024
+ "cost_category": "billing",
8025
+ "fix": "Code fix"
8026
+ }],
8027
+ "total_risk_if_shipped": 250000,
8028
+ "summary": "Cost impact assessment"
8029
+ }`;
8030
+ }
8031
+ /**
8032
+ * Aggregate cost report for all issues
8033
+ */
8034
+ generateCostReport(issues, context) {
8035
+ const estimates = issues.map((issue) => ({
8036
+ issue,
8037
+ estimate: this.estimateCost(issue, context)
8038
+ }));
8039
+ const breakdown = ["critical", "serious", "moderate", "low"].map((severity) => {
8040
+ const filtered = estimates.filter((e) => e.issue.severity === severity);
8041
+ return {
8042
+ severity,
8043
+ count: filtered.length,
8044
+ nowCost: filtered.reduce((sum, e) => sum + e.estimate.totalNowCost, 0),
8045
+ productionCost: filtered.reduce((sum, e) => sum + e.estimate.totalProductionCost, 0)
8046
+ };
8047
+ });
8048
+ const totalNowCost = breakdown.reduce((sum, b) => sum + b.nowCost, 0);
8049
+ const totalProductionCost = breakdown.reduce((sum, b) => sum + b.productionCost, 0);
8050
+ const totalSavings = totalProductionCost - totalNowCost;
8051
+ const userScale = this.getUserScaleMultiplier();
8052
+ const userCount = this.userCount;
8053
+ const scaleMultiplier = userScale.multiplier;
8054
+ let riskLevel = "low";
8055
+ if (totalProductionCost > 1e5 * scaleMultiplier) riskLevel = "critical";
8056
+ else if (totalProductionCost > 25e3 * scaleMultiplier) riskLevel = "high";
8057
+ else if (totalProductionCost > 5e3 * scaleMultiplier) riskLevel = "medium";
8058
+ const formatCurrency = (n) => n >= 1e6 ? `$${(n / 1e6).toFixed(2)}M` : n >= 1e3 ? `$${(n / 1e3).toFixed(1)}k` : `$${n}`;
8059
+ const formatUsers = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}K` : `${n}`;
8060
+ const summary = `
8061
+ \u{1F4B0} COST ANALYSIS REPORT
8062
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
8063
+ \u{1F465} User Scale: ${formatUsers(userCount)} users (${userScale.label})
8064
+ \u2514\u2500 Costs scaled ${userScale.multiplier}x from 1K baseline
8065
+
8066
+ \u{1F4CA} Total Issues: ${issues.length}
8067
+ \u251C\u2500 Critical: ${breakdown.find((b) => b.severity === "critical")?.count || 0}
8068
+ \u251C\u2500 Serious: ${breakdown.find((b) => b.severity === "serious")?.count || 0}
8069
+ \u251C\u2500 Moderate: ${breakdown.find((b) => b.severity === "moderate")?.count || 0}
8070
+ \u2514\u2500 Low: ${breakdown.find((b) => b.severity === "low")?.count || 0}
8071
+
8072
+ \u{1F4B5} COST IMPACT
8073
+ \u251C\u2500 Fix now: ${formatCurrency(totalNowCost)}
8074
+ \u251C\u2500 If production: ${formatCurrency(totalProductionCost)}
8075
+ \u2514\u2500 Savings by fixing now: ${formatCurrency(totalSavings)} \u26A1
8076
+
8077
+ \u{1F3AF} RISK LEVEL: ${riskLevel.toUpperCase()}
8078
+ ${riskLevel === "critical" ? " \u26A0\uFE0F URGENT: High-value bugs require immediate attention" : ""}
8079
+ ${riskLevel === "high" ? " \u26A0\uFE0F ATTENTION: Significant financial risk detected" : ""}
8080
+
8081
+ \u{1F4C8} Based on industry research:
8082
+ \u2022 IBM: Production bugs cost 30x more to fix
8083
+ \u2022 Ponemon: Average data breach costs $4.45M
8084
+ \u2022 Gartner: Downtime averages $5,600/minute
8085
+
8086
+ \u{1F4A1} Default: 250 users. Scale with: trie scan --users 10000
8087
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
8088
+ `.trim();
8089
+ return {
8090
+ totalNowCost,
8091
+ totalProductionCost,
8092
+ totalSavings,
8093
+ userCount,
8094
+ userScaleLabel: userScale.label,
8095
+ breakdown,
8096
+ riskLevel,
8097
+ summary
8098
+ };
8099
+ }
8100
+ };
8101
+
8102
+ // src/agents/production-ready.ts
8103
+ var PRODUCTION_READY_ASCII = `
8104
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557
8105
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
8106
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
8107
+ \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
8108
+ \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
8109
+ \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
8110
+
8111
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
8112
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
8113
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D
8114
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D
8115
+ \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551
8116
+ \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D
8117
+ `;
8118
+ var PRODUCTION_QUOTES = [
8119
+ '"Ship it? Not until I say so."',
8120
+ '"Works on my machine" is not a deployment strategy.',
8121
+ '"The last mile is where dreams become revenue\u2014or nightmares."',
8122
+ '"Production is where your code meets reality. Is yours ready?"',
8123
+ '"One missing health check = 3am pager duty."',
8124
+ `"You wouldn't deploy without tests. Why deploy without production checks?"`,
8125
+ '"Fortune favors the prepared. So does uptime."',
8126
+ `"Your users don't care about your sprint velocity. They care if it works."`
8127
+ ];
8128
+ var PRODUCTION_PATTERNS = {
8129
+ // Health & Reliability
8130
+ healthEndpoint: {
8131
+ pattern: /\/health|healthcheck|health-check|readiness|liveness/i,
8132
+ category: "reliability",
8133
+ requirement: "Health check endpoint",
8134
+ severity: "serious"
8135
+ },
8136
+ gracefulShutdown: {
8137
+ pattern: /SIGTERM|SIGINT|process\.on\s*\(\s*['"]SIG/i,
8138
+ category: "reliability",
8139
+ requirement: "Graceful shutdown handling",
8140
+ severity: "moderate"
8141
+ },
8142
+ // Scalability
8143
+ connectionPooling: {
8144
+ pattern: /connectionLimit|pool|poolSize|max_connections|maxConnections/i,
8145
+ category: "scalability",
8146
+ requirement: "Database connection pooling",
8147
+ severity: "serious"
8148
+ },
8149
+ inMemorySession: {
8150
+ pattern: /express-session.*(?!redis|memcached|mongo)|session\s*=\s*\{\}/i,
8151
+ category: "scalability",
8152
+ requirement: "External session store (not in-memory)",
8153
+ severity: "serious"
8154
+ },
8155
+ // Error Handling
8156
+ globalErrorHandler: {
8157
+ pattern: /app\.use\s*\(\s*\(?err|errorHandler|process\.on\s*\(\s*['"]uncaughtException/i,
8158
+ category: "reliability",
8159
+ requirement: "Global error handler",
8160
+ severity: "serious"
8161
+ },
8162
+ // Security Headers
8163
+ securityHeaders: {
8164
+ pattern: /helmet|contentSecurityPolicy|X-Frame-Options|X-Content-Type|Strict-Transport/i,
8165
+ category: "security",
8166
+ requirement: "Security headers (CSP, HSTS, etc.)",
8167
+ severity: "serious"
8168
+ },
8169
+ // Rate Limiting
8170
+ rateLimiting: {
8171
+ pattern: /rateLimit|rate-limit|throttle|express-rate-limit|slowDown/i,
8172
+ category: "security",
8173
+ requirement: "API rate limiting",
8174
+ severity: "moderate"
8175
+ },
8176
+ // Logging
8177
+ structuredLogging: {
8178
+ pattern: /pino|winston|bunyan|structured.*log|log.*json/i,
8179
+ category: "observability",
8180
+ requirement: "Structured logging",
8181
+ severity: "moderate"
8182
+ },
8183
+ // Monitoring
8184
+ monitoring: {
8185
+ pattern: /prometheus|datadog|newrelic|sentry|bugsnag|opentelemetry|@sentry/i,
8186
+ category: "observability",
8187
+ requirement: "Error/performance monitoring",
8188
+ severity: "moderate"
8189
+ }
8190
+ };
8191
+ var PRODUCTION_ANTIPATTERNS = [
8192
+ {
8193
+ pattern: /console\.(log|debug|info)\s*\(/g,
8194
+ severity: "low",
8195
+ issue: "Console logging in production code",
8196
+ fix: "Replace with structured logging (pino, winston) that can be disabled in production",
8197
+ category: "observability"
8198
+ },
8199
+ {
8200
+ pattern: /localhost:\d+|127\.0\.0\.1:\d+/g,
8201
+ severity: "serious",
8202
+ issue: "Hardcoded localhost URL will fail in production",
8203
+ fix: "Use environment variables for all URLs: process.env.API_URL",
8204
+ category: "configuration"
8205
+ },
8206
+ {
8207
+ pattern: /TODO.*prod|FIXME.*deploy|hack.*ship/gi,
8208
+ severity: "serious",
8209
+ issue: "TODO/FIXME comment flagged for production",
8210
+ fix: "Resolve the TODO before deploying to production",
8211
+ category: "code-quality"
8212
+ },
8213
+ {
8214
+ pattern: /process\.env\.\w+\s*\|\|\s*['"][^'"]+['"]/g,
8215
+ severity: "moderate",
8216
+ issue: "Hardcoded fallback for environment variable",
8217
+ fix: "Fail fast if required env vars are missing: throw if !process.env.VAR",
8218
+ category: "configuration"
8219
+ },
8220
+ {
8221
+ pattern: /throw\s+['"][^'"]+['"]/g,
8222
+ severity: "moderate",
8223
+ issue: "Throwing string instead of Error object",
8224
+ fix: 'Throw proper Error objects: throw new Error("message")',
8225
+ category: "reliability"
8226
+ },
8227
+ {
8228
+ pattern: /\.then\s*\([^)]*\)\s*(?!\.catch)/g,
8229
+ severity: "moderate",
8230
+ issue: "Promise without .catch() - unhandled rejection risk",
8231
+ fix: "Add .catch() handler or use try/catch with async/await",
8232
+ category: "reliability"
8233
+ },
8234
+ {
8235
+ pattern: /setTimeout\s*\([^,]+,\s*\d{5,}\)/g,
8236
+ severity: "moderate",
8237
+ issue: "Long setTimeout (>10s) - use proper job queue for background tasks",
8238
+ fix: "Use a job queue (Bull, Agenda, BullMQ) for background processing",
8239
+ category: "scalability"
8240
+ },
8241
+ {
8242
+ pattern: /fs\.(readFileSync|writeFileSync)/g,
8243
+ severity: "moderate",
8244
+ issue: "Synchronous file I/O blocks event loop",
8245
+ fix: "Use async fs methods: fs.promises.readFile, fs.promises.writeFile",
8246
+ category: "performance"
8247
+ }
8248
+ ];
8249
+ var PRODUCTION_CONFIG_FILES = [
8250
+ /package\.json$/,
8251
+ /\.env\.example$/,
8252
+ /docker-compose.*\.ya?ml$/,
8253
+ /Dockerfile$/,
8254
+ /\.github\/workflows/,
8255
+ /vercel\.json$/,
8256
+ /netlify\.toml$/
8257
+ ];
8258
+ var ProductionReadyAgent = class extends BaseAgent {
8259
+ name = "production-ready";
8260
+ description = "Production readiness checker: health endpoints, graceful shutdown, connection pooling, security headers, monitoring, and deployment gates";
8261
+ version = "1.0.0";
8262
+ bannerShown = false;
8263
+ foundRequirements = /* @__PURE__ */ new Set();
8264
+ get priority() {
8265
+ return {
8266
+ name: this.name,
8267
+ tier: 3,
8268
+ // Run after other agents for comprehensive check
8269
+ estimatedTimeMs: 100,
8270
+ dependencies: ["security", "bug-finding", "performance", "devops"]
8271
+ };
8272
+ }
8273
+ shouldActivate(context) {
8274
+ return context.linesChanged > 100 || context.touchesAPI || context.touchesDatabase || context.touchesAuth || context.touchesPayments || context.isNewFeature;
8275
+ }
8276
+ getActivationConfidence(context) {
8277
+ let confidence = 0.2;
8278
+ if (context.touchesAPI) confidence += 0.3;
8279
+ if (context.touchesDatabase) confidence += 0.2;
8280
+ if (context.touchesAuth) confidence += 0.2;
8281
+ if (context.touchesPayments) confidence += 0.3;
8282
+ if (context.linesChanged > 200) confidence += 0.2;
8283
+ return Math.min(confidence, 1);
8284
+ }
8285
+ displayBanner() {
8286
+ if (this.bannerShown) return;
8287
+ this.bannerShown = true;
8288
+ const quote = PRODUCTION_QUOTES[Math.floor(Math.random() * PRODUCTION_QUOTES.length)];
8289
+ console.error("\n" + "\u2550".repeat(60));
8290
+ console.error(PRODUCTION_READY_ASCII);
8291
+ console.error(" \u{1F680} Production Readiness Gate v" + this.version + " \u{1F680}");
8292
+ console.error("");
8293
+ console.error(" " + quote);
8294
+ console.error("\u2550".repeat(60) + "\n");
8295
+ }
8296
+ checkFileRelevance(file, content) {
8297
+ if (/node_modules|\.d\.ts$|\.min\.|dist\/|build\/|\.lock$/.test(file)) {
8298
+ return { isRelevant: false, reason: "Skip file", priority: "low", indicators: [] };
8299
+ }
8300
+ const indicators = [];
8301
+ let priority = "low";
8302
+ if (/index\.[jt]s$|main\.[jt]s$|app\.[jt]s$|server\.[jt]s$/i.test(file)) {
8303
+ indicators.push("application entry point");
8304
+ priority = "high";
8305
+ }
8306
+ if (PRODUCTION_CONFIG_FILES.some((p) => p.test(file))) {
8307
+ indicators.push("configuration file");
8308
+ priority = "high";
8309
+ }
8310
+ if (/express|fastify|koa|hapi|nest/i.test(content)) {
8311
+ indicators.push("web framework");
8312
+ if (priority === "low") priority = "medium";
8313
+ }
8314
+ if (/database|prisma|sequelize|mongoose|pg|mysql/i.test(content)) {
8315
+ indicators.push("database code");
8316
+ if (priority === "low") priority = "medium";
8317
+ }
8318
+ return {
8319
+ isRelevant: content.length > 50,
8320
+ reason: indicators.length > 0 ? `Production-critical: ${indicators.join(", ")}` : "General code",
8321
+ priority,
8322
+ indicators
8323
+ };
8324
+ }
8325
+ async analyzeFiles(files, _context) {
8326
+ this.displayBanner();
8327
+ this.foundRequirements.clear();
6545
8328
  const issues = [];
6546
- const lines = content.split("\n");
6547
- for (let i = 0; i < lines.length; i++) {
6548
- const line = lines[i] || "";
6549
- const lineNumber = i + 1;
6550
- if (/JSON\.parse\(/.test(line)) {
6551
- const context = lines.slice(Math.max(0, i - 3), Math.min(i + 5, lines.length)).join("\n");
6552
- if (!/try|catch|\.catch/.test(context)) {
6553
- issues.push(this.createIssue(
6554
- this.generateIssueId(),
6555
- "moderate",
6556
- "JSON.parse without error handling - will throw on invalid JSON",
6557
- "Wrap in try/catch or use a safe parse utility",
6558
- file,
6559
- lineNumber,
6560
- 0.85,
6561
- void 0,
6562
- false,
6563
- { category: "parse-error", effort: "easy" }
6564
- ));
6565
- }
6566
- }
6567
- if (/parseInt\s*\([^,)]+\)/.test(line)) {
6568
- if (!/,\s*10/.test(line)) {
6569
- issues.push(this.createIssue(
6570
- this.generateIssueId(),
6571
- "low",
6572
- "parseInt without radix parameter",
6573
- "Add radix: parseInt(value, 10) to avoid octal issues",
6574
- file,
6575
- lineNumber,
6576
- 0.7,
6577
- void 0,
6578
- true,
6579
- { category: "parse", effort: "trivial" }
6580
- ));
8329
+ const allContent = [];
8330
+ for (const file of files) {
8331
+ if (/node_modules|\.d\.ts$|\.min\.|dist\/|build\//.test(file)) continue;
8332
+ try {
8333
+ const content = await this.readFile(file);
8334
+ allContent.push(content);
8335
+ for (const [key, config] of Object.entries(PRODUCTION_PATTERNS)) {
8336
+ if (config.pattern.test(content)) {
8337
+ this.foundRequirements.add(key);
8338
+ }
6581
8339
  }
6582
- }
6583
- if (/Number\(|parseFloat\(|\+\s*\w+/.test(line)) {
6584
- const context = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
6585
- if (!/isNaN|Number\.isFinite|isFinite|\|\|\s*0/.test(context)) {
6586
- issues.push(this.createIssue(
6587
- this.generateIssueId(),
6588
- "low",
6589
- "Number conversion without NaN check",
6590
- "Check for NaN: isNaN(result) or provide fallback: value || 0",
6591
- file,
6592
- lineNumber,
6593
- 0.55,
6594
- void 0,
6595
- false,
6596
- { category: "nan", effort: "easy" }
6597
- ));
8340
+ const lines = content.split("\n");
8341
+ for (let i = 0; i < lines.length; i++) {
8342
+ const line = lines[i] || "";
8343
+ for (const antiPattern of PRODUCTION_ANTIPATTERNS) {
8344
+ antiPattern.pattern.lastIndex = 0;
8345
+ if (antiPattern.pattern.test(line)) {
8346
+ if (antiPattern.issue.includes("Console") && /\.(test|spec)\.[jt]sx?$/.test(file)) {
8347
+ continue;
8348
+ }
8349
+ this.progress?.found(antiPattern.severity, `${antiPattern.issue} at line ${i + 1}`);
8350
+ issues.push(this.createIssue(
8351
+ this.generateIssueId(),
8352
+ antiPattern.severity,
8353
+ `\u{1F680} [NOT PROD READY] ${antiPattern.issue}`,
8354
+ antiPattern.fix,
8355
+ file,
8356
+ i + 1,
8357
+ 0.9,
8358
+ void 0,
8359
+ true,
8360
+ { category: antiPattern.category }
8361
+ ));
8362
+ }
8363
+ }
6598
8364
  }
8365
+ } catch {
6599
8366
  }
6600
8367
  }
6601
- return issues;
6602
- }
6603
- checkTypeCoercion(content, file) {
6604
- const issues = [];
6605
- const lines = content.split("\n");
6606
- for (let i = 0; i < lines.length; i++) {
6607
- const line = lines[i] || "";
6608
- const lineNumber = i + 1;
6609
- if (/[^!=]==[^=]/.test(line) && !/===/.test(line)) {
6610
- if (/null|undefined|""|\d+|true|false/.test(line)) {
8368
+ const combinedContent = allContent.join("\n");
8369
+ for (const [key, config] of Object.entries(PRODUCTION_PATTERNS)) {
8370
+ if (!this.foundRequirements.has(key)) {
8371
+ if (!config.pattern.test(combinedContent)) {
8372
+ this.progress?.found(config.severity, `Missing: ${config.requirement}`);
6611
8373
  issues.push(this.createIssue(
6612
8374
  this.generateIssueId(),
6613
- "moderate",
6614
- "Loose equality (==) may cause unexpected type coercion",
6615
- "Use strict equality (===) for predictable comparisons",
6616
- file,
6617
- lineNumber,
6618
- 0.8,
8375
+ config.severity,
8376
+ `\u{1F680} [MISSING] ${config.requirement}`,
8377
+ this.getRequirementFix(key),
8378
+ "project",
6619
8379
  void 0,
6620
- true,
6621
- { category: "equality", effort: "trivial" }
6622
- ));
6623
- }
6624
- }
6625
- if (/if\s*\(\s*\w+\s*\)/.test(line)) {
6626
- const varName = line.match(/if\s*\(\s*(\w+)\s*\)/)?.[1];
6627
- if (varName && /count|total|length|index|num|amount|price/i.test(varName)) {
6628
- issues.push(this.createIssue(
6629
- this.generateIssueId(),
6630
- "low",
6631
- "Truthy check on number variable - 0 is falsy but might be valid",
6632
- "Consider explicit: if (count !== undefined) or if (count > 0)",
6633
- file,
6634
- lineNumber,
6635
- 0.6,
8380
+ 0.8,
6636
8381
  void 0,
6637
8382
  false,
6638
- { category: "falsy", effort: "easy" }
8383
+ { category: config.category }
6639
8384
  ));
6640
8385
  }
6641
8386
  }
6642
8387
  }
8388
+ this.logReadinessSummary(issues);
6643
8389
  return issues;
6644
8390
  }
8391
+ getRequirementFix(key) {
8392
+ const fixes = {
8393
+ healthEndpoint: 'Add health check endpoint: app.get("/health", (req, res) => res.json({ status: "ok" }))',
8394
+ gracefulShutdown: 'Handle SIGTERM: process.on("SIGTERM", async () => { await server.close(); process.exit(0); })',
8395
+ connectionPooling: "Configure connection pool: { max: 20, min: 5, acquire: 30000, idle: 10000 }",
8396
+ inMemorySession: "Use Redis for sessions: new RedisStore({ client: redisClient })",
8397
+ globalErrorHandler: 'Add error middleware: app.use((err, req, res, next) => { logger.error(err); res.status(500).json({ error: "Internal error" }); })',
8398
+ securityHeaders: "Add helmet middleware: app.use(helmet())",
8399
+ rateLimiting: "Add rate limiting: app.use(rateLimit({ windowMs: 15*60*1000, max: 100 }))",
8400
+ structuredLogging: 'Use structured logger: const logger = pino({ level: process.env.LOG_LEVEL || "info" })',
8401
+ monitoring: "Add error tracking: Sentry.init({ dsn: process.env.SENTRY_DSN })"
8402
+ };
8403
+ return fixes[key] || "See production readiness documentation";
8404
+ }
8405
+ logReadinessSummary(issues) {
8406
+ const totalRequirements = Object.keys(PRODUCTION_PATTERNS).length;
8407
+ const metRequirements = this.foundRequirements.size;
8408
+ const missingRequirements = totalRequirements - metRequirements;
8409
+ const criticalIssues = issues.filter((i) => i.severity === "critical").length;
8410
+ const seriousIssues = issues.filter((i) => i.severity === "serious").length;
8411
+ const readinessScore = Math.max(0, Math.round(
8412
+ metRequirements / totalRequirements * 50 + (50 - criticalIssues * 20 - seriousIssues * 10)
8413
+ ));
8414
+ const status = criticalIssues > 0 || seriousIssues > 2 ? "\u274C NOT READY TO SHIP" : seriousIssues > 0 ? "\u26A0\uFE0F SHIP WITH CAUTION" : "\u2705 READY TO SHIP";
8415
+ console.error("\n" + "\u2500".repeat(50));
8416
+ console.error("\u{1F4CA} PRODUCTION READINESS REPORT");
8417
+ console.error("\u2500".repeat(50));
8418
+ console.error(` Score: ${readinessScore}/100`);
8419
+ console.error(` Requirements: ${metRequirements}/${totalRequirements} met`);
8420
+ console.error(` Critical issues: ${criticalIssues}`);
8421
+ console.error(` Serious issues: ${seriousIssues}`);
8422
+ console.error("");
8423
+ console.error(` ${status}`);
8424
+ console.error("\u2500".repeat(50) + "\n");
8425
+ }
8426
+ getAIEnhancementSystemPrompt() {
8427
+ return `You are a production readiness engineer reviewing code for deployment.
8428
+
8429
+ Check for these CRITICAL production requirements:
8430
+
8431
+ 1. **Reliability**
8432
+ - Health check endpoints (/health, /ready, /live)
8433
+ - Graceful shutdown (SIGTERM handling)
8434
+ - Global error handlers
8435
+ - Circuit breakers for external services
8436
+
8437
+ 2. **Scalability**
8438
+ - Database connection pooling
8439
+ - External session storage (Redis, not in-memory)
8440
+ - Stateless application design
8441
+ - No blocking sync operations
8442
+
8443
+ 3. **Security**
8444
+ - Security headers (helmet, CSP, HSTS)
8445
+ - Rate limiting on API endpoints
8446
+ - No hardcoded secrets or URLs
8447
+ - Input validation on all endpoints
8448
+
8449
+ 4. **Observability**
8450
+ - Structured logging (not console.log)
8451
+ - Error tracking (Sentry, etc.)
8452
+ - Metrics collection
8453
+ - Request tracing
8454
+
8455
+ 5. **Configuration**
8456
+ - Environment variables for all config
8457
+ - Fail-fast on missing required vars
8458
+ - No localhost/hardcoded URLs
8459
+
8460
+ Output STRICT JSON:
8461
+ {
8462
+ "validated": [{
8463
+ "original_issue": "...",
8464
+ "verdict": "TRUE_POSITIVE" | "FALSE_POSITIVE",
8465
+ "confidence": 0-100,
8466
+ "file": "path",
8467
+ "line": 123,
8468
+ "severity": "critical" | "serious" | "moderate" | "low",
8469
+ "category": "reliability" | "scalability" | "security" | "observability" | "configuration",
8470
+ "fix": "Specific production-ready fix"
8471
+ }],
8472
+ "additional": [{
8473
+ "issue": "Production readiness gap",
8474
+ "file": "path",
8475
+ "line": 123,
8476
+ "severity": "serious",
8477
+ "category": "reliability",
8478
+ "fix": "How to make production-ready"
8479
+ }],
8480
+ "readiness_score": 0-100,
8481
+ "ship_verdict": "READY" | "CAUTION" | "NOT_READY",
8482
+ "summary": "Production readiness assessment"
8483
+ }`;
8484
+ }
6645
8485
  };
6646
8486
 
6647
8487
  // src/agents/custom-agent.ts
@@ -6887,7 +8727,11 @@ var AgentRegistryImpl = class {
6887
8727
  new PerformanceAgent(),
6888
8728
  new E2EAgent(),
6889
8729
  new VisualQAAgent(),
6890
- new DataFlowAgent()
8730
+ new DataFlowAgent(),
8731
+ // Cost analysis agent
8732
+ new MoneybagAgent(),
8733
+ // Production readiness gate
8734
+ new ProductionReadyAgent()
6891
8735
  ];
6892
8736
  console.error(`Loaded config for ${builtinAgents.length} built-in agents`);
6893
8737
  for (const agent of builtinAgents) {
@@ -7014,4 +8858,4 @@ export {
7014
8858
  CustomAgent,
7015
8859
  getAgentRegistry
7016
8860
  };
7017
- //# sourceMappingURL=chunk-QFTSX2BX.js.map
8861
+ //# sourceMappingURL=chunk-B3MBKB2U.js.map