@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.
- package/README.md +592 -34
- package/dist/{chunk-QFTSX2BX.js → chunk-B3MBKB2U.js} +2016 -172
- package/dist/chunk-B3MBKB2U.js.map +1 -0
- package/dist/{chunk-VSCPOIWS.js → chunk-HG5AWUH7.js} +536 -61
- package/dist/chunk-HG5AWUH7.js.map +1 -0
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/yolo-daemon.js +9 -2
- package/dist/cli/yolo-daemon.js.map +1 -1
- package/dist/index.js +62 -6
- package/dist/index.js.map +1 -1
- package/dist/workers/agent-worker.js +1 -1
- package/package.json +2 -3
- package/QUICK_START.md +0 -228
- package/dist/chunk-QFTSX2BX.js.map +0 -1
- package/dist/chunk-VSCPOIWS.js.map +0 -1
|
@@ -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
|
|
893
|
-
version = "
|
|
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
|
-
|
|
903
|
-
|
|
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
|
-
|
|
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
|
|
917
|
-
|
|
918
|
-
this.
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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 (/
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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 (/<
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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 (/
|
|
956
|
-
|
|
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
|
-
"
|
|
961
|
-
"
|
|
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.
|
|
965
|
-
|
|
966
|
-
|
|
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 (/
|
|
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
|
-
|
|
975
|
-
"
|
|
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.
|
|
979
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
997
|
-
"
|
|
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
|
-
|
|
1380
|
+
this.getWCAGRef("1.3.1"),
|
|
1002
1381
|
true
|
|
1003
1382
|
));
|
|
1004
1383
|
}
|
|
1005
|
-
if (/
|
|
1384
|
+
if (/<div[^>]*class=["'][^"']*footer/i.test(line) && !/<footer/i.test(content)) {
|
|
1006
1385
|
issues.push(this.createIssue(
|
|
1007
1386
|
this.generateIssueId(),
|
|
1008
|
-
"
|
|
1009
|
-
"
|
|
1010
|
-
"
|
|
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.
|
|
1014
|
-
|
|
1392
|
+
0.7,
|
|
1393
|
+
this.getWCAGRef("1.3.1"),
|
|
1015
1394
|
true
|
|
1016
1395
|
));
|
|
1017
1396
|
}
|
|
1018
|
-
if (/<
|
|
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
|
-
"
|
|
1023
|
-
|
|
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.
|
|
1027
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1911
|
+
"impact": "User impact description",
|
|
1912
|
+
"code_snippet": "Problematic code",
|
|
1913
|
+
"fix": "Accessible implementation with example"
|
|
1070
1914
|
}],
|
|
1071
|
-
"
|
|
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
|
|
6547
|
-
for (
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
this.
|
|
6555
|
-
|
|
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
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
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
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
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
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
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
|
-
|
|
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:
|
|
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-
|
|
8861
|
+
//# sourceMappingURL=chunk-B3MBKB2U.js.map
|