nfunc-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +395 -0
- package/dist/config/qa-mcp-baseline.eslint.config.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/mappers/correlator.d.ts +28 -0
- package/dist/mappers/correlator.js +132 -0
- package/dist/mappers/correlator.js.map +1 -0
- package/dist/mappers/defectFormatter.d.ts +15 -0
- package/dist/mappers/defectFormatter.js +209 -0
- package/dist/mappers/defectFormatter.js.map +1 -0
- package/dist/mappers/priorityMapper.d.ts +6 -0
- package/dist/mappers/priorityMapper.js +37 -0
- package/dist/mappers/priorityMapper.js.map +1 -0
- package/dist/tools/accessibility.d.ts +2 -0
- package/dist/tools/accessibility.js +63 -0
- package/dist/tools/accessibility.js.map +1 -0
- package/dist/tools/lighthouse.d.ts +2 -0
- package/dist/tools/lighthouse.js +61 -0
- package/dist/tools/lighthouse.js.map +1 -0
- package/dist/tools/qaGate.d.ts +2 -0
- package/dist/tools/qaGate.js +383 -0
- package/dist/tools/qaGate.js.map +1 -0
- package/dist/tools/staticAnalysis.d.ts +2 -0
- package/dist/tools/staticAnalysis.js +172 -0
- package/dist/tools/staticAnalysis.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/eslintConfigDetector.d.ts +5 -0
- package/dist/utils/eslintConfigDetector.js +21 -0
- package/dist/utils/eslintConfigDetector.js.map +1 -0
- package/dist/utils/outputParsers.d.ts +56 -0
- package/dist/utils/outputParsers.js +184 -0
- package/dist/utils/outputParsers.js.map +1 -0
- package/dist/utils/reportGenerator.d.ts +21 -0
- package/dist/utils/reportGenerator.js +291 -0
- package/dist/utils/reportGenerator.js.map +1 -0
- package/dist/utils/shellRunner.d.ts +12 -0
- package/dist/utils/shellRunner.js +31 -0
- package/dist/utils/shellRunner.js.map +1 -0
- package/dist/utils/toolResponse.d.ts +12 -0
- package/dist/utils/toolResponse.js +26 -0
- package/dist/utils/toolResponse.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { lighthouseScoreToPriority, wcagLevelToPriority, staticAnalysisToPriority, } from "./priorityMapper.js";
|
|
2
|
+
const QA_DESCRIPTIONS = {
|
|
3
|
+
"first-contentful-paint": (a) => `Users see the first piece of content ${a.displayValue} after navigation, which is slower than the recommended threshold for a smooth perceived load.`,
|
|
4
|
+
"largest-contentful-paint": (a) => `Users see the main page content ${a.displayValue} after navigation, above the recommended threshold for a responsive feel.`,
|
|
5
|
+
"speed-index": (a) => `The page takes ${a.displayValue} to visually populate, which feels sluggish to users on first load.`,
|
|
6
|
+
"total-blocking-time": (a) => `The page is unresponsive to user input for ${a.displayValue} during load, causing noticeable input lag.`,
|
|
7
|
+
"cumulative-layout-shift": (a) => `The page layout shifts unexpectedly during load (CLS ${a.displayValue}), causing users to misclick or lose their reading place.`,
|
|
8
|
+
interactive: (a) => `The page takes ${a.displayValue} before it can reliably respond to user input.`,
|
|
9
|
+
"server-response-time": (a) => `The server takes ${a.displayValue} to respond to the initial request, delaying every downstream load step.`,
|
|
10
|
+
"render-blocking-resources": (a) => `Render-blocking scripts or styles are delaying the first paint (${a.displayValue}); users stare at a blank page longer than necessary.`,
|
|
11
|
+
"unused-javascript": (a) => `The page ships JavaScript that is never executed (${a.displayValue}), inflating load time on slower connections.`,
|
|
12
|
+
"unused-css-rules": (a) => `The page ships CSS rules that go unused (${a.displayValue}), wasting bytes on every visit.`,
|
|
13
|
+
"uses-responsive-images": (a) => `Images larger than their displayed size are being served (${a.displayValue}), wasting bandwidth on mobile users.`,
|
|
14
|
+
"uses-optimized-images": (a) => `Images are not optimally compressed (${a.displayValue}); users on slower networks wait longer than necessary.`,
|
|
15
|
+
"color-contrast": () => `Text on the page does not have sufficient contrast against its background, making it hard to read for users with low vision.`,
|
|
16
|
+
"image-alt": () => `One or more images are missing alt text; screen-reader users cannot understand what the image conveys.`,
|
|
17
|
+
label: () => `Form fields are missing accessible labels; assistive-tech users cannot tell what each field is for.`,
|
|
18
|
+
"link-name": () => `One or more links have no discernible text; screen-reader users hear "link" with no destination.`,
|
|
19
|
+
"document-title": () => `The page is missing a <title>; users see a meaningless tab label and screen readers announce no page name.`,
|
|
20
|
+
"html-has-lang": () => `The <html> element has no lang attribute; screen readers may mispronounce content.`,
|
|
21
|
+
"meta-description": () => `The page is missing a meta description; search results show no preview snippet to users.`,
|
|
22
|
+
"is-on-https": () => `The page is served over HTTP rather than HTTPS; browsers warn users that the connection is not secure.`,
|
|
23
|
+
viewport: () => `The page has no viewport meta tag; on mobile, content renders at desktop width and users must pinch-zoom to read.`,
|
|
24
|
+
};
|
|
25
|
+
function fallbackDescription(audit) {
|
|
26
|
+
const detail = audit.displayValue ? ` (current: ${audit.displayValue})` : "";
|
|
27
|
+
return `The "${audit.title}" check did not meet the recommended quality threshold${detail}.`;
|
|
28
|
+
}
|
|
29
|
+
export function formatLighthouseFinding(audit) {
|
|
30
|
+
const priority = lighthouseScoreToPriority(audit.score);
|
|
31
|
+
if (!priority)
|
|
32
|
+
return null;
|
|
33
|
+
const describe = QA_DESCRIPTIONS[audit.id];
|
|
34
|
+
const description = describe ? describe(audit) : fallbackDescription(audit);
|
|
35
|
+
return {
|
|
36
|
+
priority,
|
|
37
|
+
title: audit.title,
|
|
38
|
+
description,
|
|
39
|
+
evidence: {
|
|
40
|
+
audit_id: audit.id,
|
|
41
|
+
value: audit.displayValue ?? "",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Keyed by the technique-plus-variant suffix of the pa11y rule code
|
|
46
|
+
// (e.g. "H91.InputText.Name" from
|
|
47
|
+
// "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.InputText.Name"). Lookup is
|
|
48
|
+
// progressive: the formatter tries the full suffix first, then strips trailing
|
|
49
|
+
// segments until a template matches. So a row keyed "H91" will catch any
|
|
50
|
+
// H91.* variant that isn't explicitly handled. Phrasing is deliberately
|
|
51
|
+
// user-impact-first so descriptions read like defect tickets.
|
|
52
|
+
const A11Y_DESCRIPTIONS = {
|
|
53
|
+
// Images
|
|
54
|
+
H37: () => `Image is missing an alt attribute; screen-reader users cannot tell what the image conveys or whether it is decorative.`,
|
|
55
|
+
H67: () => `An image marked as decorative carries meaningful content; assistive-tech users miss information sighted users rely on.`,
|
|
56
|
+
F65: () => `An image is missing both alt and title; screen-reader users hear no description of the content.`,
|
|
57
|
+
G94: () => `Alt text does not accurately describe the image; screen-reader users get a misleading description of what is shown.`,
|
|
58
|
+
// Links
|
|
59
|
+
H30: () => `A link contains only a non-text element with no accessible name; screen-reader users hear "link" with no destination.`,
|
|
60
|
+
"H91.A.NoContent": () => `A link has no visible text or accessible name; screen-reader users hear "link" with no indication of where it leads.`,
|
|
61
|
+
"H91.A.Name": () => `A link is missing an accessible name; screen-reader users cannot tell where it goes.`,
|
|
62
|
+
"H91.A.EmptyNoId": () => `An anchor has no href, text, or id; it is unreachable and unannounced to keyboard and screen-reader users.`,
|
|
63
|
+
// Buttons
|
|
64
|
+
"H91.Button.Name": () => `A button has no accessible name; screen-reader users hear "button" with no description of its action.`,
|
|
65
|
+
"H91.Button.Value": () => `An input button has no value or accessible name; screen-reader users cannot tell what action it triggers.`,
|
|
66
|
+
// Form fields (H91 input family)
|
|
67
|
+
"H91.InputText.Name": () => `A text input has no accessible label; screen-reader users cannot tell what data to enter in the field.`,
|
|
68
|
+
"H91.InputPassword.Name": () => `A password field has no accessible label; screen-reader users cannot tell that it expects a password.`,
|
|
69
|
+
"H91.InputEmail.Name": () => `An email input has no accessible label; screen-reader users cannot tell what data to enter.`,
|
|
70
|
+
"H91.InputSearch.Name": () => `A search input has no accessible label; screen-reader users cannot tell it is a search field.`,
|
|
71
|
+
"H91.InputTel.Name": () => `A telephone input has no accessible label; screen-reader users cannot tell that a phone number is expected.`,
|
|
72
|
+
"H91.InputNumber.Name": () => `A number input has no accessible label; screen-reader users cannot tell what numeric value is expected.`,
|
|
73
|
+
"H91.InputUrl.Name": () => `A URL input has no accessible label; screen-reader users cannot tell that a web address is expected.`,
|
|
74
|
+
"H91.InputCheckbox.Name": () => `A checkbox has no accessible label; screen-reader users cannot tell what option they are toggling.`,
|
|
75
|
+
"H91.InputRadio.Name": () => `A radio button has no accessible label; screen-reader users cannot tell which option it represents.`,
|
|
76
|
+
"H91.InputFile.Name": () => `A file-upload control has no accessible label; screen-reader users cannot tell what kind of file is expected.`,
|
|
77
|
+
"H91.Select.Name": () => `A dropdown has no accessible label; screen-reader users cannot tell what is being selected.`,
|
|
78
|
+
"H91.Textarea.Name": () => `A multi-line text area has no accessible label; screen-reader users cannot tell what content to enter.`,
|
|
79
|
+
// H91 catch-all for any input/element variant not explicitly listed above.
|
|
80
|
+
H91: () => `An interactive element has no accessible name available to assistive tech; screen-reader users cannot tell what it is or what it does.`,
|
|
81
|
+
// Form/label structure
|
|
82
|
+
H32: () => `A form has no submit mechanism; keyboard and assistive-tech users have no reliable way to complete and send the form.`,
|
|
83
|
+
H44: () => `A form control is not programmatically associated with a label; screen-reader users cannot tell what data the field expects.`,
|
|
84
|
+
H85: () => `Grouped options in a dropdown are not wrapped in optgroup; screen-reader users miss the grouping that sighted users see.`,
|
|
85
|
+
F68: () => `A form input has no accessible label; screen-reader users cannot identify what data to enter.`,
|
|
86
|
+
ARIA6: () => `An element uses aria-label, but it is empty or invalid; assistive tech receives no usable name for the element.`,
|
|
87
|
+
ARIA9: () => `An element references aria-labelledby ids that do not exist; assistive tech cannot resolve a name and announces nothing.`,
|
|
88
|
+
// Headings / structure / emphasis
|
|
89
|
+
H42: () => `Text is styled to look like a heading but is not marked up as one; screen-reader users cannot use heading navigation to scan the page.`,
|
|
90
|
+
H49: () => `Emphasis is conveyed only by visual styling; screen-reader users do not receive the same emphasis cue.`,
|
|
91
|
+
G141: () => `Heading levels skip a step (e.g. h2 jumps to h4); screen-reader users navigating by headings get a broken outline of the page.`,
|
|
92
|
+
// Language / page-level
|
|
93
|
+
H57: () => `The <html> element has no lang attribute; screen readers may use the wrong pronunciation profile for the page.`,
|
|
94
|
+
H58: () => `A passage in another language is not marked with lang; screen readers pronounce it using the page's default language.`,
|
|
95
|
+
H25: () => `The page has no <title>; users see a meaningless tab label and screen readers announce no page name.`,
|
|
96
|
+
F89: () => `The page <title> is empty; users see a meaningless tab label and screen readers announce no page name.`,
|
|
97
|
+
// Frames
|
|
98
|
+
H64: () => `An <iframe> has no title; screen-reader users hear "frame" with no description of its contents.`,
|
|
99
|
+
// Identifiers / aria references
|
|
100
|
+
H93: () => `Two elements share the same id; assistive tech may target the wrong element when users follow label/for or aria references.`,
|
|
101
|
+
F77: () => `Duplicate id detected; label/for and aria-* references resolve to the wrong element for assistive-tech users.`,
|
|
102
|
+
// Presentation role misuse
|
|
103
|
+
"F92,ARIA4": () => `An element marked role="presentation" still contains child elements with semantic meaning; the role hides that meaning from screen-reader users.`,
|
|
104
|
+
F92: () => `An element marked role="presentation" still contains child elements with semantic meaning; the role hides that meaning from screen-reader users.`,
|
|
105
|
+
ARIA4: () => `An element uses an ARIA role inappropriate for its content; assistive tech announces the element with the wrong semantics.`,
|
|
106
|
+
// Color / contrast
|
|
107
|
+
G18: () => `Text does not meet the minimum 4.5:1 contrast ratio against its background; low-vision users may be unable to read it.`,
|
|
108
|
+
G145: () => `Large text fails the minimum 3:1 contrast ratio; low-vision users may not be able to distinguish it from the background.`,
|
|
109
|
+
"G18.Fail": () => `Text does not meet the minimum 4.5:1 contrast ratio against its background; low-vision users may be unable to read it.`,
|
|
110
|
+
"G145.Fail": () => `Large text fails the minimum 3:1 contrast ratio; low-vision users may not be able to distinguish it from the background.`,
|
|
111
|
+
G174: () => `A mechanism to switch to a higher-contrast version is missing; users who need stronger contrast cannot read the page.`,
|
|
112
|
+
// Information conveyed by sensory cues alone
|
|
113
|
+
F2: () => `Information is conveyed by text formatting (bold, italic, color) alone; users who cannot perceive that formatting miss the meaning.`,
|
|
114
|
+
G14: () => `Information is conveyed by color alone; users who cannot distinguish colors miss the meaning that color carries.`,
|
|
115
|
+
// Timing / refresh
|
|
116
|
+
F40: () => `The page uses a meta refresh with a delay; users on assistive tech may be moved away before they can finish reading.`,
|
|
117
|
+
F41: () => `The page auto-refreshes without a user-controllable mechanism; assistive-tech users lose their place without warning.`,
|
|
118
|
+
// Keyboard / focus
|
|
119
|
+
G202: () => `Keyboard focus is trapped or not visible on an element; keyboard-only users cannot tell where they are or escape the component.`,
|
|
120
|
+
F54: () => `Interactivity depends on a mouse-only event; keyboard-only users cannot trigger the same action.`,
|
|
121
|
+
F55: () => `An interactive element steals focus on hover; keyboard users are pulled away from where they intended to be.`,
|
|
122
|
+
};
|
|
123
|
+
function lookupA11yTemplate(technique) {
|
|
124
|
+
// Progressive narrowing: try the full key, then strip trailing segments
|
|
125
|
+
// until we find a match or run out. "H91.InputText.Name" → "H91.InputText" → "H91".
|
|
126
|
+
let key = technique;
|
|
127
|
+
while (key) {
|
|
128
|
+
const tpl = A11Y_DESCRIPTIONS[key];
|
|
129
|
+
if (tpl)
|
|
130
|
+
return tpl;
|
|
131
|
+
const lastDot = key.lastIndexOf(".");
|
|
132
|
+
if (lastDot < 0)
|
|
133
|
+
break;
|
|
134
|
+
key = key.slice(0, lastDot);
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
function fallbackA11yDescription(violation) {
|
|
139
|
+
// Last-resort phrasing for rules we haven't keyed. Frame it as a user-affecting
|
|
140
|
+
// gap rather than restating the raw pa11y message — the rule_code stays in
|
|
141
|
+
// evidence for traceability.
|
|
142
|
+
const criterion = violation.criterion
|
|
143
|
+
? `WCAG ${violation.criterion}`
|
|
144
|
+
: "an accessibility rule";
|
|
145
|
+
return `This element fails ${criterion}; assistive-tech users (screen readers, keyboard-only, or low-vision) are likely affected.`;
|
|
146
|
+
}
|
|
147
|
+
// QA-native descriptions keyed by ESLint rule ID. Focus on the bug class the
|
|
148
|
+
// rule guards against, not a restatement of the rule name.
|
|
149
|
+
const ESLINT_RULE_DESCRIPTIONS = {
|
|
150
|
+
"no-unused-vars": "Dead code or unused imports can hide incomplete error handlers; if a variable was meant to carry an error or state its absence means that path is never tested or exercised under failure conditions.",
|
|
151
|
+
"no-undef": "A reference to an undeclared variable will throw a ReferenceError at runtime, but only when that specific code path is exercised — tests that skip this branch will give a false-green result.",
|
|
152
|
+
eqeqeq: "Loose equality (==) silently coerces types: '0' == false, null == undefined, and '' == 0 all evaluate to true, creating logic bugs that pass unit tests operating on correctly typed data.",
|
|
153
|
+
"no-unreachable": "Code after a return, throw, or break is never executed; if it contains error handling, cleanup, or assertions those code paths are permanently dead and cannot be covered by any test.",
|
|
154
|
+
"no-console": "Leftover console statements can leak sensitive data to production logs and are a signal that the code was debugged hastily rather than properly instrumented — a common precursor to data-exposure regressions.",
|
|
155
|
+
"no-empty": "An empty catch block silently swallows exceptions; errors that should fail the flow are suppressed, making failures invisible to monitoring, alerting, and QA gate checks.",
|
|
156
|
+
"no-constant-condition": "A constant condition (while(true) or if(true)) produces unreachable branches or potential infinite loops that can hang test runners, block event loops, and cause silent production outages.",
|
|
157
|
+
};
|
|
158
|
+
function buildStaticAnalysisDescription(issue) {
|
|
159
|
+
const known = ESLINT_RULE_DESCRIPTIONS[issue.ruleId];
|
|
160
|
+
if (known)
|
|
161
|
+
return known;
|
|
162
|
+
if (issue.source === "semgrep" && issue.category === "security") {
|
|
163
|
+
return (`Security vulnerability detected by Semgrep (${issue.ruleId}): ${issue.message} ` +
|
|
164
|
+
`This class of issue can lead to data exposure, injection attacks, or authentication bypass — ` +
|
|
165
|
+
`bugs that are rarely caught by functional tests because they require adversarial inputs.`);
|
|
166
|
+
}
|
|
167
|
+
const label = issue.source === "eslint" ? "ESLint" : "Semgrep";
|
|
168
|
+
return (`${label} rule ${issue.ruleId} flagged a potential issue: ${issue.message} ` +
|
|
169
|
+
`This type of finding can indicate a bug class that manifests only under specific conditions ` +
|
|
170
|
+
`not covered by the current test suite.`);
|
|
171
|
+
}
|
|
172
|
+
export function formatStaticAnalysisFinding(issue) {
|
|
173
|
+
const priority = staticAnalysisToPriority(issue.source, issue.severity, issue.category);
|
|
174
|
+
if (!priority)
|
|
175
|
+
return null;
|
|
176
|
+
const firstLine = issue.message.split("\n")[0]?.trim() ?? issue.ruleId;
|
|
177
|
+
return {
|
|
178
|
+
priority,
|
|
179
|
+
title: `[${issue.ruleId}] ${firstLine}`,
|
|
180
|
+
description: buildStaticAnalysisDescription(issue),
|
|
181
|
+
evidence: {
|
|
182
|
+
file: issue.file,
|
|
183
|
+
line: issue.line,
|
|
184
|
+
rule_id: issue.ruleId,
|
|
185
|
+
source: issue.source,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function formatA11yFinding(violation) {
|
|
190
|
+
if (violation.type === "notice")
|
|
191
|
+
return null;
|
|
192
|
+
const priority = wcagLevelToPriority(violation.wcagLevel);
|
|
193
|
+
if (!priority)
|
|
194
|
+
return null;
|
|
195
|
+
const describe = lookupA11yTemplate(violation.technique);
|
|
196
|
+
const description = describe
|
|
197
|
+
? describe(violation)
|
|
198
|
+
: fallbackA11yDescription(violation);
|
|
199
|
+
return {
|
|
200
|
+
priority,
|
|
201
|
+
title: violation.message.split(/\.\s|\.$/)[0] || violation.code,
|
|
202
|
+
description,
|
|
203
|
+
evidence: {
|
|
204
|
+
rule_code: violation.code,
|
|
205
|
+
selector: violation.selector,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=defectFormatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defectFormatter.js","sourceRoot":"","sources":["../../src/mappers/defectFormatter.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAK7B,MAAM,eAAe,GAA8B;IACjD,wBAAwB,EAAE,CAAC,CAAC,EAAE,EAAE,CAC9B,wCAAwC,CAAC,CAAC,YAAY,gGAAgG;IACxJ,0BAA0B,EAAE,CAAC,CAAC,EAAE,EAAE,CAChC,mCAAmC,CAAC,CAAC,YAAY,2EAA2E;IAC9H,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,CACnB,kBAAkB,CAAC,CAAC,YAAY,qEAAqE;IACvG,qBAAqB,EAAE,CAAC,CAAC,EAAE,EAAE,CAC3B,8CAA8C,CAAC,CAAC,YAAY,6CAA6C;IAC3G,yBAAyB,EAAE,CAAC,CAAC,EAAE,EAAE,CAC/B,wDAAwD,CAAC,CAAC,YAAY,2DAA2D;IACnI,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CACjB,kBAAkB,CAAC,CAAC,YAAY,gDAAgD;IAClF,sBAAsB,EAAE,CAAC,CAAC,EAAE,EAAE,CAC5B,oBAAoB,CAAC,CAAC,YAAY,0EAA0E;IAC9G,2BAA2B,EAAE,CAAC,CAAC,EAAE,EAAE,CACjC,mEAAmE,CAAC,CAAC,YAAY,uDAAuD;IAC1I,mBAAmB,EAAE,CAAC,CAAC,EAAE,EAAE,CACzB,qDAAqD,CAAC,CAAC,YAAY,+CAA+C;IACpH,kBAAkB,EAAE,CAAC,CAAC,EAAE,EAAE,CACxB,4CAA4C,CAAC,CAAC,YAAY,kCAAkC;IAC9F,wBAAwB,EAAE,CAAC,CAAC,EAAE,EAAE,CAC9B,6DAA6D,CAAC,CAAC,YAAY,uCAAuC;IACpH,uBAAuB,EAAE,CAAC,CAAC,EAAE,EAAE,CAC7B,wCAAwC,CAAC,CAAC,YAAY,yDAAyD;IACjH,gBAAgB,EAAE,GAAG,EAAE,CACrB,8HAA8H;IAChI,WAAW,EAAE,GAAG,EAAE,CAChB,wGAAwG;IAC1G,KAAK,EAAE,GAAG,EAAE,CACV,qGAAqG;IACvG,WAAW,EAAE,GAAG,EAAE,CAChB,kGAAkG;IACpG,gBAAgB,EAAE,GAAG,EAAE,CACrB,4GAA4G;IAC9G,eAAe,EAAE,GAAG,EAAE,CACpB,oFAAoF;IACtF,kBAAkB,EAAE,GAAG,EAAE,CACvB,0FAA0F;IAC5F,aAAa,EAAE,GAAG,EAAE,CAClB,wGAAwG;IAC1G,QAAQ,EAAE,GAAG,EAAE,CACb,mHAAmH;CACtH,CAAC;AAEF,SAAS,mBAAmB,CAAC,KAAyB;IACpD,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,OAAO,QAAQ,KAAK,CAAC,KAAK,yDAAyD,MAAM,GAAG,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,KAAyB;IAEzB,MAAM,QAAQ,GAAG,yBAAyB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACxD,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3C,MAAM,WAAW,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC5E,OAAO;QACL,QAAQ;QACR,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,WAAW;QACX,QAAQ,EAAE;YACR,QAAQ,EAAE,KAAK,CAAC,EAAE;YAClB,KAAK,EAAE,KAAK,CAAC,YAAY,IAAI,EAAE;SAChC;KACF,CAAC;AACJ,CAAC;AAID,oEAAoE;AACpE,kCAAkC;AAClC,yEAAyE;AACzE,+EAA+E;AAC/E,yEAAyE;AACzE,wEAAwE;AACxE,8DAA8D;AAC9D,MAAM,iBAAiB,GAAkC;IACvD,SAAS;IACT,GAAG,EAAE,GAAG,EAAE,CACR,wHAAwH;IAC1H,GAAG,EAAE,GAAG,EAAE,CACR,wHAAwH;IAC1H,GAAG,EAAE,GAAG,EAAE,CACR,iGAAiG;IACnG,GAAG,EAAE,GAAG,EAAE,CACR,qHAAqH;IAEvH,QAAQ;IACR,GAAG,EAAE,GAAG,EAAE,CACR,uHAAuH;IACzH,iBAAiB,EAAE,GAAG,EAAE,CACtB,sHAAsH;IACxH,YAAY,EAAE,GAAG,EAAE,CACjB,sFAAsF;IACxF,iBAAiB,EAAE,GAAG,EAAE,CACtB,4GAA4G;IAE9G,UAAU;IACV,iBAAiB,EAAE,GAAG,EAAE,CACtB,uGAAuG;IACzG,kBAAkB,EAAE,GAAG,EAAE,CACvB,2GAA2G;IAE7G,iCAAiC;IACjC,oBAAoB,EAAE,GAAG,EAAE,CACzB,wGAAwG;IAC1G,wBAAwB,EAAE,GAAG,EAAE,CAC7B,uGAAuG;IACzG,qBAAqB,EAAE,GAAG,EAAE,CAC1B,6FAA6F;IAC/F,sBAAsB,EAAE,GAAG,EAAE,CAC3B,+FAA+F;IACjG,mBAAmB,EAAE,GAAG,EAAE,CACxB,6GAA6G;IAC/G,sBAAsB,EAAE,GAAG,EAAE,CAC3B,yGAAyG;IAC3G,mBAAmB,EAAE,GAAG,EAAE,CACxB,sGAAsG;IACxG,wBAAwB,EAAE,GAAG,EAAE,CAC7B,oGAAoG;IACtG,qBAAqB,EAAE,GAAG,EAAE,CAC1B,qGAAqG;IACvG,oBAAoB,EAAE,GAAG,EAAE,CACzB,+GAA+G;IACjH,iBAAiB,EAAE,GAAG,EAAE,CACtB,6FAA6F;IAC/F,mBAAmB,EAAE,GAAG,EAAE,CACxB,wGAAwG;IAE1G,2EAA2E;IAC3E,GAAG,EAAE,GAAG,EAAE,CACR,wIAAwI;IAE1I,uBAAuB;IACvB,GAAG,EAAE,GAAG,EAAE,CACR,uHAAuH;IACzH,GAAG,EAAE,GAAG,EAAE,CACR,8HAA8H;IAChI,GAAG,EAAE,GAAG,EAAE,CACR,0HAA0H;IAC5H,GAAG,EAAE,GAAG,EAAE,CACR,+FAA+F;IACjG,KAAK,EAAE,GAAG,EAAE,CACV,iHAAiH;IACnH,KAAK,EAAE,GAAG,EAAE,CACV,0HAA0H;IAE5H,kCAAkC;IAClC,GAAG,EAAE,GAAG,EAAE,CACR,wIAAwI;IAC1I,GAAG,EAAE,GAAG,EAAE,CACR,wGAAwG;IAC1G,IAAI,EAAE,GAAG,EAAE,CACT,gIAAgI;IAElI,wBAAwB;IACxB,GAAG,EAAE,GAAG,EAAE,CACR,gHAAgH;IAClH,GAAG,EAAE,GAAG,EAAE,CACR,uHAAuH;IACzH,GAAG,EAAE,GAAG,EAAE,CACR,sGAAsG;IACxG,GAAG,EAAE,GAAG,EAAE,CACR,wGAAwG;IAE1G,SAAS;IACT,GAAG,EAAE,GAAG,EAAE,CACR,iGAAiG;IAEnG,gCAAgC;IAChC,GAAG,EAAE,GAAG,EAAE,CACR,6HAA6H;IAC/H,GAAG,EAAE,GAAG,EAAE,CACR,+GAA+G;IAEjH,2BAA2B;IAC3B,WAAW,EAAE,GAAG,EAAE,CAChB,kJAAkJ;IACpJ,GAAG,EAAE,GAAG,EAAE,CACR,kJAAkJ;IACpJ,KAAK,EAAE,GAAG,EAAE,CACV,4HAA4H;IAE9H,mBAAmB;IACnB,GAAG,EAAE,GAAG,EAAE,CACR,wHAAwH;IAC1H,IAAI,EAAE,GAAG,EAAE,CACT,0HAA0H;IAC5H,UAAU,EAAE,GAAG,EAAE,CACf,wHAAwH;IAC1H,WAAW,EAAE,GAAG,EAAE,CAChB,0HAA0H;IAC5H,IAAI,EAAE,GAAG,EAAE,CACT,uHAAuH;IAEzH,6CAA6C;IAC7C,EAAE,EAAE,GAAG,EAAE,CACP,qIAAqI;IACvI,GAAG,EAAE,GAAG,EAAE,CACR,kHAAkH;IAEpH,mBAAmB;IACnB,GAAG,EAAE,GAAG,EAAE,CACR,sHAAsH;IACxH,GAAG,EAAE,GAAG,EAAE,CACR,uHAAuH;IAEzH,mBAAmB;IACnB,IAAI,EAAE,GAAG,EAAE,CACT,iIAAiI;IACnI,GAAG,EAAE,GAAG,EAAE,CACR,kGAAkG;IACpG,GAAG,EAAE,GAAG,EAAE,CACR,8GAA8G;CACjH,CAAC;AAEF,SAAS,kBAAkB,CAAC,SAAiB;IAC3C,wEAAwE;IACxE,oFAAoF;IACpF,IAAI,GAAG,GAAG,SAAS,CAAC;IACpB,OAAO,GAAG,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;QACpB,MAAM,OAAO,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,OAAO,GAAG,CAAC;YAAE,MAAM;QACvB,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,uBAAuB,CAAC,SAAyB;IACxD,gFAAgF;IAChF,2EAA2E;IAC3E,6BAA6B;IAC7B,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS;QACnC,CAAC,CAAC,QAAQ,SAAS,CAAC,SAAS,EAAE;QAC/B,CAAC,CAAC,uBAAuB,CAAC;IAC5B,OAAO,sBAAsB,SAAS,4FAA4F,CAAC;AACrI,CAAC;AAeD,6EAA6E;AAC7E,2DAA2D;AAC3D,MAAM,wBAAwB,GAA2B;IACvD,gBAAgB,EACd,uMAAuM;IACzM,UAAU,EACR,gMAAgM;IAClM,MAAM,EACJ,4LAA4L;IAC9L,gBAAgB,EACd,wLAAwL;IAC1L,YAAY,EACV,iNAAiN;IACnN,UAAU,EACR,4KAA4K;IAC9K,uBAAuB,EACrB,8LAA8L;CACjM,CAAC;AAEF,SAAS,8BAA8B,CAAC,KAA0B;IAChE,MAAM,KAAK,GAAG,wBAAwB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IAExB,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QAChE,OAAO,CACL,+CAA+C,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,OAAO,GAAG;YACjF,+FAA+F;YAC/F,0FAA0F,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/D,OAAO,CACL,GAAG,KAAK,SAAS,KAAK,CAAC,MAAM,+BAA+B,KAAK,CAAC,OAAO,GAAG;QAC5E,8FAA8F;QAC9F,wCAAwC,CACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,KAA0B;IAE1B,MAAM,QAAQ,GAAG,wBAAwB,CACvC,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,QAAQ,EACd,KAAK,CAAC,QAAQ,CACf,CAAC;IACF,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAE3B,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,KAAK,CAAC,MAAM,CAAC;IACvE,OAAO;QACL,QAAQ;QACR,KAAK,EAAE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE;QACvC,WAAW,EAAE,8BAA8B,CAAC,KAAK,CAAC;QAClD,QAAQ,EAAE;YACR,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,MAAM;YACrB,MAAM,EAAE,KAAK,CAAC,MAAM;SACrB;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,SAAyB;IACzD,IAAI,SAAS,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,QAAQ,GAAG,mBAAmB,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1D,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,QAAQ;QAC1B,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC;QACrB,CAAC,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC;IACvC,OAAO;QACL,QAAQ;QACR,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,IAAI;QAC/D,WAAW;QACX,QAAQ,EAAE;YACR,SAAS,EAAE,SAAS,CAAC,IAAI;YACzB,QAAQ,EAAE,SAAS,CAAC,QAAQ;SAC7B;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Finding, Priority } from "../types.js";
|
|
2
|
+
export type { Priority };
|
|
3
|
+
export declare function lighthouseScoreToPriority(score: number): Priority | null;
|
|
4
|
+
export declare function wcagLevelToPriority(level: string): Priority | null;
|
|
5
|
+
export declare function staticAnalysisToPriority(source: "eslint" | "semgrep", severity: number | string, category?: string): Priority | null;
|
|
6
|
+
export declare function sortFindingsByPriority<T extends Finding>(findings: T[]): T[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const PRIORITY_ORDER = { P1: 0, P2: 1, P3: 2 };
|
|
2
|
+
export function lighthouseScoreToPriority(score) {
|
|
3
|
+
if (score < 50)
|
|
4
|
+
return "P1";
|
|
5
|
+
if (score < 80)
|
|
6
|
+
return "P2";
|
|
7
|
+
if (score < 90)
|
|
8
|
+
return "P3";
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
// Pass "notice" (or "warning" / "unknown") to suppress the finding.
|
|
12
|
+
export function wcagLevelToPriority(level) {
|
|
13
|
+
if (level === "A")
|
|
14
|
+
return "P1";
|
|
15
|
+
if (level === "AA")
|
|
16
|
+
return "P2";
|
|
17
|
+
if (level === "AAA")
|
|
18
|
+
return "P3";
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function staticAnalysisToPriority(source, severity, category) {
|
|
22
|
+
// Security findings always win regardless of severity level.
|
|
23
|
+
if (source === "semgrep" && category === "security")
|
|
24
|
+
return "P1";
|
|
25
|
+
if (source === "eslint" && severity === 2)
|
|
26
|
+
return "P2";
|
|
27
|
+
if (source === "semgrep" && (severity === "warning" || severity === "error"))
|
|
28
|
+
return "P2";
|
|
29
|
+
if (source === "eslint" && severity === 1)
|
|
30
|
+
return "P3";
|
|
31
|
+
// "info" or anything unrecognised → suppress
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function sortFindingsByPriority(findings) {
|
|
35
|
+
return findings.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=priorityMapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"priorityMapper.js","sourceRoot":"","sources":["../../src/mappers/priorityMapper.ts"],"names":[],"mappings":"AAIA,MAAM,cAAc,GAA6B,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;AAEzE,MAAM,UAAU,yBAAyB,CAAC,KAAa;IACrD,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IAC5B,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IAC5B,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,IAAI,KAAK,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC/B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,MAA4B,EAC5B,QAAyB,EACzB,QAAiB;IAEjB,6DAA6D;IAC7D,IAAI,MAAM,KAAK,SAAS,IAAI,QAAQ,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACjE,IAAI,MAAM,KAAK,QAAQ,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1F,IAAI,MAAM,KAAK,QAAQ,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,6CAA6C;IAC7C,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAoB,QAAa;IACrE,OAAO,QAAQ,CAAC,IAAI,CAClB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAClE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runShell } from "../utils/shellRunner.js";
|
|
3
|
+
import { parsePa11yJSON } from "../utils/outputParsers.js";
|
|
4
|
+
import { shellErrorResponse, parseErrorResponse, } from "../utils/toolResponse.js";
|
|
5
|
+
import { formatA11yFinding } from "../mappers/defectFormatter.js";
|
|
6
|
+
import { sortFindingsByPriority } from "../mappers/priorityMapper.js";
|
|
7
|
+
const inputShape = {
|
|
8
|
+
url: z.string().url(),
|
|
9
|
+
standard: z.enum(["WCAG2A", "WCAG2AA", "WCAG2AAA"]).optional(),
|
|
10
|
+
ignore: z.array(z.string()).optional(),
|
|
11
|
+
};
|
|
12
|
+
export function registerAccessibilityTool(server) {
|
|
13
|
+
server.registerTool("run_accessibility_check", {
|
|
14
|
+
description: "Runs pa11y against a URL and returns a QA-style report with WCAG " +
|
|
15
|
+
"violations mapped to priorities (Level A → P1, AA → P2, AAA → P3; " +
|
|
16
|
+
"notices are filtered out). Requires the pa11y CLI to be installed " +
|
|
17
|
+
"globally on PATH (`npm install -g pa11y`).",
|
|
18
|
+
inputSchema: inputShape,
|
|
19
|
+
}, async ({ url, standard, ignore }) => {
|
|
20
|
+
const resolvedStandard = standard ?? "WCAG2AA";
|
|
21
|
+
const args = [
|
|
22
|
+
url,
|
|
23
|
+
"--reporter",
|
|
24
|
+
"json",
|
|
25
|
+
"--standard",
|
|
26
|
+
resolvedStandard,
|
|
27
|
+
];
|
|
28
|
+
if (ignore && ignore.length > 0) {
|
|
29
|
+
args.push("--ignore", ignore.join(";"));
|
|
30
|
+
}
|
|
31
|
+
const result = await runShell("pa11y", args, { timeoutMs: 120_000 });
|
|
32
|
+
// pa11y exits 2 when issues are found — that's a successful run with data.
|
|
33
|
+
// Exit 1 means pa11y itself failed (browser launch, bad URL, etc.).
|
|
34
|
+
const ranSuccessfully = result.exitCode === 0 || result.exitCode === 2;
|
|
35
|
+
if (!ranSuccessfully || !result.stdout) {
|
|
36
|
+
return shellErrorResponse("pa11y did not produce a usable report", result);
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = parsePa11yJSON(result.stdout);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return parseErrorResponse("Failed to parse pa11y JSON", err, result);
|
|
44
|
+
}
|
|
45
|
+
const findings = [];
|
|
46
|
+
for (const violation of parsed.violations) {
|
|
47
|
+
const finding = formatA11yFinding(violation);
|
|
48
|
+
if (finding)
|
|
49
|
+
findings.push(finding);
|
|
50
|
+
}
|
|
51
|
+
sortFindingsByPriority(findings);
|
|
52
|
+
const report = {
|
|
53
|
+
url,
|
|
54
|
+
standard: resolvedStandard,
|
|
55
|
+
violation_count: findings.length,
|
|
56
|
+
findings,
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=accessibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../../src/tools/accessibility.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runShell } from "../utils/shellRunner.js";
|
|
3
|
+
import { parseLighthouseJSON } from "../utils/outputParsers.js";
|
|
4
|
+
import { shellErrorResponse, parseErrorResponse, } from "../utils/toolResponse.js";
|
|
5
|
+
import { formatLighthouseFinding } from "../mappers/defectFormatter.js";
|
|
6
|
+
import { sortFindingsByPriority } from "../mappers/priorityMapper.js";
|
|
7
|
+
const inputShape = {
|
|
8
|
+
url: z.string().url(),
|
|
9
|
+
categories: z.array(z.string()).optional(),
|
|
10
|
+
thresholds: z.record(z.string(), z.number()).optional(),
|
|
11
|
+
};
|
|
12
|
+
export function registerLighthouseTool(server) {
|
|
13
|
+
server.registerTool("run_lighthouse", {
|
|
14
|
+
description: "Runs Google Lighthouse against a URL and returns a QA-style report " +
|
|
15
|
+
"with category scores, TTFB, and prioritised findings (P1/P2/P3). " +
|
|
16
|
+
"Requires the Lighthouse CLI to be installed globally on PATH " +
|
|
17
|
+
"(`npm install -g lighthouse`) and a Chrome/Chromium binary available.",
|
|
18
|
+
inputSchema: inputShape,
|
|
19
|
+
}, async ({ url, categories, thresholds }) => {
|
|
20
|
+
const args = [
|
|
21
|
+
url,
|
|
22
|
+
"--output=json",
|
|
23
|
+
"--quiet",
|
|
24
|
+
"--chrome-flags=--headless",
|
|
25
|
+
];
|
|
26
|
+
if (categories && categories.length > 0) {
|
|
27
|
+
args.push(`--only-categories=${categories.join(",")}`);
|
|
28
|
+
}
|
|
29
|
+
const result = await runShell("lighthouse", args, { timeoutMs: 120_000 });
|
|
30
|
+
if (!result.stdout) {
|
|
31
|
+
return shellErrorResponse("Lighthouse produced no JSON output", result);
|
|
32
|
+
}
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = parseLighthouseJSON(result.stdout);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return parseErrorResponse("Failed to parse Lighthouse JSON", err, result);
|
|
39
|
+
}
|
|
40
|
+
const findings = [];
|
|
41
|
+
for (const audit of parsed.failedAudits) {
|
|
42
|
+
const threshold = thresholds?.[audit.id];
|
|
43
|
+
if (typeof threshold === "number" && audit.score >= threshold)
|
|
44
|
+
continue;
|
|
45
|
+
const finding = formatLighthouseFinding(audit);
|
|
46
|
+
if (finding)
|
|
47
|
+
findings.push(finding);
|
|
48
|
+
}
|
|
49
|
+
sortFindingsByPriority(findings);
|
|
50
|
+
const report = {
|
|
51
|
+
url,
|
|
52
|
+
scores: parsed.categoryScores,
|
|
53
|
+
ttfb_ms: parsed.ttfbMs,
|
|
54
|
+
findings,
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(report, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=lighthouse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lighthouse.js","sourceRoot":"","sources":["../../src/tools/lighthouse.ts"],"names":[],"mappings":"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"}
|