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.
Files changed (45) hide show
  1. package/README.md +395 -0
  2. package/dist/config/qa-mcp-baseline.eslint.config.js +18 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +36 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/mappers/correlator.d.ts +28 -0
  7. package/dist/mappers/correlator.js +132 -0
  8. package/dist/mappers/correlator.js.map +1 -0
  9. package/dist/mappers/defectFormatter.d.ts +15 -0
  10. package/dist/mappers/defectFormatter.js +209 -0
  11. package/dist/mappers/defectFormatter.js.map +1 -0
  12. package/dist/mappers/priorityMapper.d.ts +6 -0
  13. package/dist/mappers/priorityMapper.js +37 -0
  14. package/dist/mappers/priorityMapper.js.map +1 -0
  15. package/dist/tools/accessibility.d.ts +2 -0
  16. package/dist/tools/accessibility.js +63 -0
  17. package/dist/tools/accessibility.js.map +1 -0
  18. package/dist/tools/lighthouse.d.ts +2 -0
  19. package/dist/tools/lighthouse.js +61 -0
  20. package/dist/tools/lighthouse.js.map +1 -0
  21. package/dist/tools/qaGate.d.ts +2 -0
  22. package/dist/tools/qaGate.js +383 -0
  23. package/dist/tools/qaGate.js.map +1 -0
  24. package/dist/tools/staticAnalysis.d.ts +2 -0
  25. package/dist/tools/staticAnalysis.js +172 -0
  26. package/dist/tools/staticAnalysis.js.map +1 -0
  27. package/dist/types.d.ts +7 -0
  28. package/dist/types.js +2 -0
  29. package/dist/types.js.map +1 -0
  30. package/dist/utils/eslintConfigDetector.d.ts +5 -0
  31. package/dist/utils/eslintConfigDetector.js +21 -0
  32. package/dist/utils/eslintConfigDetector.js.map +1 -0
  33. package/dist/utils/outputParsers.d.ts +56 -0
  34. package/dist/utils/outputParsers.js +184 -0
  35. package/dist/utils/outputParsers.js.map +1 -0
  36. package/dist/utils/reportGenerator.d.ts +21 -0
  37. package/dist/utils/reportGenerator.js +291 -0
  38. package/dist/utils/reportGenerator.js.map +1 -0
  39. package/dist/utils/shellRunner.d.ts +12 -0
  40. package/dist/utils/shellRunner.js +31 -0
  41. package/dist/utils/shellRunner.js.map +1 -0
  42. package/dist/utils/toolResponse.d.ts +12 -0
  43. package/dist/utils/toolResponse.js +26 -0
  44. package/dist/utils/toolResponse.js.map +1 -0
  45. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,395 @@
1
+ # qa-mcp
2
+
3
+ A local MCP server that gives Claude (or any MCP client) a full non-functional QA toolkit. Run Lighthouse, WCAG accessibility checks, and static code analysis — individually or all at once — and get back structured, prioritised findings you can act on immediately.
4
+
5
+ ---
6
+
7
+ ## Table of contents
8
+
9
+ 1. [What it does](#what-it-does)
10
+ 2. [Prerequisites](#prerequisites)
11
+ 3. [Install & build](#install--build)
12
+ 4. [Register with Claude Code](#register-with-claude-code)
13
+ 5. [Tools](#tools)
14
+ 6. [run_qa_gate — the main tool](#run_qa_gate--the-main-tool)
15
+ - [Inputs](#inputs)
16
+ - [Release readiness tiers](#release-readiness-tiers)
17
+ - [Composite score](#composite-score)
18
+ - [Scorecard](#scorecard)
19
+ - [Cross-tool corroboration](#cross-tool-corroboration)
20
+ - [HTML report](#html-report)
21
+ - [Output shape](#output-shape)
22
+ 7. [Individual tools](#individual-tools)
23
+ 8. [Priority system](#priority-system)
24
+ 9. [Project layout](#project-layout)
25
+ 10. [How to prompt](#how-to-prompt)
26
+
27
+ ---
28
+
29
+ ## What it does
30
+
31
+ `qa-mcp` wires four QA tools into Claude's tool-use loop:
32
+
33
+ | Capability | Tools | What it checks |
34
+ |---|---|---|
35
+ | Performance | Lighthouse | LCP, TTI, TBT, CLS, bundle size, caching |
36
+ | Accessibility | Lighthouse + pa11y | WCAG 2 AA violations, ARIA, contrast, labels |
37
+ | SEO | Lighthouse | Crawlability, robots.txt, meta, link text |
38
+ | Best practices | Lighthouse | HTTPS, deprecated APIs, third-party cookies |
39
+ | Code quality | ESLint | Dead code, undeclared vars, swallowed errors |
40
+ | Security patterns | Semgrep | OWASP JS/TS patterns |
41
+
42
+ The `run_qa_gate` orchestrator runs all of them in parallel, cross-correlates findings across tools, and produces a single structured report with a release readiness verdict, a composite health score, a per-tool scorecard, and a browser-openable HTML report.
43
+
44
+ ---
45
+
46
+ ## Prerequisites
47
+
48
+ The following CLI tools must be available on your PATH:
49
+
50
+ ```bash
51
+ npm install -g lighthouse # Lighthouse
52
+ npm install -g pa11y # pa11y
53
+ npm install -g eslint # ESLint
54
+ brew install semgrep # Semgrep (or pip install semgrep)
55
+ ```
56
+
57
+ Verify each is reachable:
58
+
59
+ ```bash
60
+ lighthouse --version
61
+ pa11y --version
62
+ eslint --version
63
+ semgrep --version
64
+ ```
65
+
66
+ If a tool is missing, the gate still runs — that tool's scorecard entry will show `UNAVAILABLE` and its findings are omitted from the report.
67
+
68
+ ---
69
+
70
+ ## Install & build
71
+
72
+ ```bash
73
+ git clone <repo-url>
74
+ cd qa-mcp
75
+ npm install
76
+ npm run build
77
+ ```
78
+
79
+ ### Scripts
80
+
81
+ | Script | Purpose |
82
+ |---|---|
83
+ | `npm run build` | Compile TypeScript → `dist/` |
84
+ | `npm start` | Run the compiled server |
85
+ | `npm run dev` | Run from source with hot reload (`tsx watch`) |
86
+
87
+ ---
88
+
89
+ ## Register with Claude Code
90
+
91
+ ### Option A — CLI (recommended)
92
+
93
+ ```bash
94
+ claude mcp add qa-mcp -- node /absolute/path/to/qa-mcp/dist/index.js
95
+ ```
96
+
97
+ ### Option B — Manual config
98
+
99
+ Add to `~/.claude.json` under `mcpServers`:
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "qa-mcp": {
105
+ "command": "node",
106
+ "args": ["/absolute/path/to/qa-mcp/dist/index.js"]
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### Verify the connection
113
+
114
+ 1. Run `/mcp` in Claude Code — `qa-mcp` should show as `connected`.
115
+ 2. Ask Claude: *"Call the qa-mcp ping tool."*
116
+ 3. Expected response:
117
+ ```json
118
+ { "status": "ok", "timestamp": "2026-05-20T12:00:00.000Z" }
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Tools
124
+
125
+ | Tool | Description | Inputs |
126
+ |---|---|---|
127
+ | `ping` | Health check — confirms the server is up | — |
128
+ | `run_lighthouse` | Full Lighthouse audit for a URL | `url` |
129
+ | `run_accessibility_check` | pa11y WCAG audit for a URL | `url`, `standard` (optional), `ignore` (optional) |
130
+ | `run_static_analysis` | ESLint + Semgrep scan for a local codebase | `path` |
131
+ | `run_qa_gate` | All tools in parallel + correlation + HTML report | `url` and/or `path` |
132
+
133
+ ---
134
+
135
+ ## run_qa_gate — the main tool
136
+
137
+ This is the tool to reach for in nearly every QA workflow. It replaces running tools individually and adds cross-tool intelligence on top.
138
+
139
+ ### Inputs
140
+
141
+ Both inputs are **optional** — provide whichever you have. At least one is required.
142
+
143
+ | Input | Type | When to provide |
144
+ |---|---|---|
145
+ | `url` | string (URL) | You have a running page — production, staging, preview URL, or localhost. Enables Lighthouse and pa11y. |
146
+ | `path` | string (path) | You have a local codebase. Enables ESLint and Semgrep. |
147
+ | `context` | string | Optional. Free-text description of the project (e.g. `"React e-commerce checkout"`). Helps Claude interpret results. |
148
+
149
+ **URL only** — browser-based checks, static analysis skipped:
150
+ ```
151
+ QA snapshot — https://staging.myapp.com
152
+ ```
153
+
154
+ **Path only** — static analysis only, browser checks skipped:
155
+ ```
156
+ QA snapshot — /path/to/my-feature-branch
157
+ ```
158
+
159
+ **Both** — full suite:
160
+ ```
161
+ QA snapshot — https://staging.myapp.com, code at /path/to/repo
162
+ ```
163
+
164
+ ### Release readiness tiers
165
+
166
+ The `release_readiness` field replaces a binary pass/fail with four actionable tiers:
167
+
168
+ | Value | Meaning | Condition |
169
+ |---|---|---|
170
+ | `BLOCKED` | Cannot ship — P1 issues exist | Any P1 finding |
171
+ | `CONDITIONAL` | Shippable with caveats — track P2s before merging | P2 findings, no P1s |
172
+ | `ADVISORY` | Safe to ship — P3s are tech debt to log | Only P3 findings |
173
+ | `CLEAR` | No issues detected | Zero findings |
174
+
175
+ ### Composite score
176
+
177
+ A single `composite_score` (0–100) gives a continuous health measure across all tools.
178
+
179
+ **Formula:** Start at 100, deduct by finding severity:
180
+
181
+ | Priority | Deduction |
182
+ |---|---|
183
+ | P1 | −15 per finding |
184
+ | P2 | −7 per finding |
185
+ | P3 | −3 per finding |
186
+
187
+ Score is floored at 0. Tracks improvement over time — a score trending upward sprint-over-sprint is a healthy signal.
188
+
189
+ ### Scorecard
190
+
191
+ A compact `scorecard` array gives a one-line status per tool:
192
+
193
+ ```json
194
+ [
195
+ { "tool": "Lighthouse", "gate": "WARN", "score": 75,
196
+ "breakdown": { "performance": 52, "accessibility": 98, "seo": 100, "best-practices": 58 } },
197
+ { "tool": "pa11y", "gate": "PASS", "issues": 0 },
198
+ { "tool": "ESLint / Semgrep", "gate": "SKIPPED" }
199
+ ]
200
+ ```
201
+
202
+ Gate values:
203
+
204
+ | Gate | Meaning |
205
+ |---|---|
206
+ | `PASS` | No issues at this tool's threshold |
207
+ | `WARN` | Issues exist but below the FAIL threshold |
208
+ | `FAIL` | Issues at P1 level (or Lighthouse avg < 50) |
209
+ | `SKIPPED` | Input not provided (URL or path not supplied) |
210
+ | `UNAVAILABLE` | Tool was invoked but is not installed or errored |
211
+
212
+ ### Cross-tool corroboration
213
+
214
+ When Lighthouse and pa11y independently flag the same accessibility gap, those findings are:
215
+
216
+ 1. **Merged** into a single entry in `corroborated_findings`
217
+ 2. **Priority-promoted** one tier (P3→P2, P2→P1)
218
+ 3. **Annotated** with `confidence: "high"` and `confirmed_by: ["lighthouse", "pa11y"]`
219
+
220
+ These are the highest-confidence findings in any report — two independent tools agreeing is stronger evidence than either alone. They appear in their own dedicated section above all other findings, and bubble to the top of `top_issues`.
221
+
222
+ **Corroboration mapping (Rule 1):**
223
+
224
+ | Lighthouse audit | pa11y technique |
225
+ |---|---|
226
+ | `color-contrast` | `.G18`, `.G145`, `.G174` |
227
+ | `image-alt` | `.H37`, `.H67`, `.F65` |
228
+ | `label` | `.H44`, `.F68`, `.H91.Input` |
229
+ | `link-name` | `.H30`, `.H91.A.` |
230
+ | `html-has-lang` | `.H57` |
231
+ | `button-name` | `.H91.Button` |
232
+ | *(and more)* | |
233
+
234
+ **Performance ↔ code linkage (Rule 2):** If a Lighthouse performance finding's display value contains a filename that also appears in a static analysis finding, the static finding is added as `related_findings` on the Lighthouse entry. Findings are not merged — they remain linked by reference.
235
+
236
+ ### HTML report
237
+
238
+ Every `run_qa_gate` call automatically writes a self-contained HTML file to `/tmp` and returns its path as `report_file`:
239
+
240
+ ```json
241
+ {
242
+ "report_file": "file:///tmp/qa-report-myapp-com-1234567890.html"
243
+ }
244
+ ```
245
+
246
+ Open the path in any browser to get:
247
+
248
+ - Release readiness banner (colour-coded)
249
+ - Composite score gauge (SVG arc, 0–100)
250
+ - Per-tool scorecard table
251
+ - Cross-confirmed findings section (highlighted)
252
+ - Collapsible finding cards grouped by P1 / P2 / P3
253
+ - Evidence and selector for each finding
254
+
255
+ No server required — the file is fully self-contained with inline CSS.
256
+
257
+ ### Output shape
258
+
259
+ ```jsonc
260
+ {
261
+ "release_readiness": "BLOCKED", // BLOCKED | CONDITIONAL | ADVISORY | CLEAR
262
+ "composite_score": 22, // 0–100
263
+ "report_file": "file:///tmp/qa-report-xxx.html",
264
+ "scorecard": [ ... ], // per-tool gate + score/issues
265
+ "eslint_config_used": "project", // present only when path was supplied
266
+ "summary": "110 findings (101 P1, 9 P2) across 2 tools. ...",
267
+ "corroborated_findings": [ ... ], // cross-confirmed, confidence: "high"
268
+ "top_issues": [ ... ], // top 3 findings (corroborated first)
269
+ "all_findings": [ ... ], // all findings sorted by priority
270
+ "correlations_found": 1,
271
+ "errors": [ ... ] // present only if a tool errored
272
+ }
273
+ ```
274
+
275
+ Each finding:
276
+
277
+ ```jsonc
278
+ {
279
+ "priority": "P1", // P1 | P2 | P3
280
+ "title": "Largest Contentful Paint",
281
+ "description": "Users see main content 34s after navigation...",
282
+ "evidence": { "audit_id": "largest-contentful-paint", "value": "34.3 s" },
283
+ "source_tool": "lighthouse",
284
+ // corroborated findings also have:
285
+ "confirmed_by": ["lighthouse", "pa11y"],
286
+ "confidence": "high"
287
+ }
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Individual tools
293
+
294
+ ### run_lighthouse
295
+
296
+ Runs a full Lighthouse audit against a URL.
297
+
298
+ ```
299
+ Run Lighthouse on https://myapp.com
300
+ ```
301
+
302
+ Returns: `url`, `scores` (per category), `ttfb_ms`, `findings` (priority-ordered).
303
+
304
+ ### run_accessibility_check
305
+
306
+ Runs pa11y against a URL at WCAG 2 AA by default. Returns only violations (errors) — use the CLI directly with `--include-notices --include-warnings` for the full checklist.
307
+
308
+ ```
309
+ Run an accessibility check on https://myapp.com
310
+ Run accessibility check at AAA standard on https://myapp.com
311
+ ```
312
+
313
+ Returns: `url`, `standard`, `violation_count`, `findings`.
314
+
315
+ ### run_static_analysis
316
+
317
+ Runs ESLint and Semgrep in parallel against a local directory. Automatically uses the project's own ESLint config if one is found; otherwise falls back to a QA-focused baseline config.
318
+
319
+ ```
320
+ Run static analysis on /path/to/repo
321
+ ```
322
+
323
+ Returns: `path`, `tools_run`, `eslint_config_used`, `issue_count`, `findings`, `warnings`.
324
+
325
+ ---
326
+
327
+ ## Priority system
328
+
329
+ | Priority | Meaning | Lighthouse threshold | WCAG level | ESLint / Semgrep |
330
+ |---|---|---|---|---|
331
+ | P1 | Blocker — fix before shipping | Score < 50 | Level A | Semgrep security, ESLint error |
332
+ | P2 | Warning — track before merging | Score 50–79 | Level AA | ESLint warning |
333
+ | P3 | Advisory — log as tech debt | Score 80–89 | Level AAA | — |
334
+ | *(suppressed)* | Passing — not reported | Score ≥ 90 | — | — |
335
+
336
+ Corroborated findings are promoted one tier above where either tool would place them individually.
337
+
338
+ ---
339
+
340
+ ## Project layout
341
+
342
+ ```
343
+ qa-mcp/
344
+ ├── src/
345
+ │ ├── index.ts # MCP server bootstrap + tool registration
346
+ │ ├── types.ts # Shared types (Finding, Priority)
347
+ │ ├── config/
348
+ │ │ └── qa-mcp-baseline.eslint.config.js # Fallback ESLint config
349
+ │ ├── tools/
350
+ │ │ ├── qaGate.ts # Orchestrator — runs all tools, builds report
351
+ │ │ ├── lighthouse.ts # run_lighthouse tool
352
+ │ │ ├── accessibility.ts # run_accessibility_check tool
353
+ │ │ └── staticAnalysis.ts # run_static_analysis tool
354
+ │ ├── mappers/
355
+ │ │ ├── correlator.ts # Cross-tool correlation engine (Rule 1 + 2)
356
+ │ │ ├── defectFormatter.ts # Raw tool output → Finding objects
357
+ │ │ └── priorityMapper.ts # Score/severity → P1/P2/P3
358
+ │ └── utils/
359
+ │ ├── reportGenerator.ts # HTML report builder
360
+ │ ├── shellRunner.ts # CLI execution with timeout + error handling
361
+ │ ├── outputParsers.ts # JSON parsers for each tool's output
362
+ │ ├── eslintConfigDetector.ts # Detects project ESLint config
363
+ │ └── toolResponse.ts # MCP error response helpers
364
+ ├── dist/ # Compiled output (gitignored)
365
+ ├── package.json
366
+ ├── tsconfig.json
367
+ └── README.md
368
+ ```
369
+
370
+ ---
371
+
372
+ ## How to prompt
373
+
374
+ The shortest working prompts:
375
+
376
+ ```
377
+ # Full suite
378
+ QA snapshot — https://myapp.com, code at /path/to/repo
379
+
380
+ # URL only (Lighthouse + pa11y)
381
+ QA snapshot — https://myapp.com
382
+
383
+ # Local branch only (ESLint + Semgrep)
384
+ QA snapshot — /path/to/my-feature-branch
385
+ ```
386
+
387
+ Alternative trigger phrases (all invoke `run_qa_gate`):
388
+
389
+ ```
390
+ Health check on https://myapp.com
391
+ Is https://myapp.com ready to ship? Code at /path/to/repo
392
+ Any red flags? /path/to/repo
393
+ ```
394
+
395
+ After the run, Claude will surface the `report_file` path. Open it in your browser for the full visual dashboard.
@@ -0,0 +1,18 @@
1
+ // QA MCP baseline ESLint config — purely bug-risk rules, no style/formatting.
2
+ // Targets JS/JSX files only; TypeScript files require @typescript-eslint/parser.
3
+ // Used as fallback when the target repo has no ESLint config of its own.
4
+ export default [
5
+ {
6
+ files: ["**/*.js", "**/*.mjs", "**/*.cjs", "**/*.jsx"],
7
+ ignores: ["**/node_modules/**", "**/dist/**"],
8
+ rules: {
9
+ "no-unused-vars": "error",
10
+ "no-undef": "error",
11
+ eqeqeq: "error",
12
+ "no-unreachable": "error",
13
+ "no-console": "warn",
14
+ "no-empty": "error",
15
+ "no-constant-condition": "error",
16
+ },
17
+ },
18
+ ];
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerLighthouseTool } from "./tools/lighthouse.js";
5
+ import { registerAccessibilityTool } from "./tools/accessibility.js";
6
+ import { registerStaticAnalysisTool } from "./tools/staticAnalysis.js";
7
+ import { registerQaGateTool } from "./tools/qaGate.js";
8
+ const server = new McpServer({
9
+ name: "qa-mcp",
10
+ version: "0.1.0",
11
+ });
12
+ server.registerTool("ping", {
13
+ description: "Health-check tool. Returns ok status and current ISO timestamp.",
14
+ inputSchema: {},
15
+ }, async () => {
16
+ const payload = {
17
+ status: "ok",
18
+ timestamp: new Date().toISOString(),
19
+ };
20
+ return {
21
+ content: [{ type: "text", text: JSON.stringify(payload) }],
22
+ };
23
+ });
24
+ registerLighthouseTool(server);
25
+ registerAccessibilityTool(server);
26
+ registerStaticAnalysisTool(server);
27
+ registerQaGateTool(server);
28
+ async function main() {
29
+ const transport = new StdioServerTransport();
30
+ await server.connect(transport);
31
+ }
32
+ main().catch((err) => {
33
+ console.error("qa-mcp fatal error:", err);
34
+ process.exit(1);
35
+ });
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,MAAM,CAAC,YAAY,CACjB,MAAM,EACN;IACE,WAAW,EAAE,iEAAiE;IAC9E,WAAW,EAAE,EAAE;CAChB,EACD,KAAK,IAAI,EAAE;IACT,MAAM,OAAO,GAAG;QACd,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;KAC3D,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC/B,yBAAyB,CAAC,MAAM,CAAC,CAAC;AAClC,0BAA0B,CAAC,MAAM,CAAC,CAAC;AACnC,kBAAkB,CAAC,MAAM,CAAC,CAAC;AAE3B,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Finding } from "../types.js";
2
+ export interface CorrelatedFinding extends Finding {
3
+ id: string;
4
+ source_tool: string;
5
+ confirmed_by?: string[];
6
+ related_findings?: string[];
7
+ }
8
+ export interface ToolReports {
9
+ lighthouse: {
10
+ scores: Record<string, number>;
11
+ ttfb_ms: number | null;
12
+ findings: Finding[];
13
+ } | null;
14
+ a11y: {
15
+ violation_count: number;
16
+ findings: Finding[];
17
+ } | null;
18
+ static: {
19
+ issue_count: number;
20
+ findings: Finding[];
21
+ } | null;
22
+ }
23
+ export interface CorrelatorResult {
24
+ correlated_findings: CorrelatedFinding[];
25
+ unique_findings: CorrelatedFinding[];
26
+ correlations_count: number;
27
+ }
28
+ export declare function correlate(reports: ToolReports): CorrelatorResult;
@@ -0,0 +1,132 @@
1
+ const PRIORITY_ORDER = { P1: 0, P2: 1, P3: 2 };
2
+ function promotePriority(p) {
3
+ if (p === "P3")
4
+ return "P2";
5
+ if (p === "P2")
6
+ return "P1";
7
+ return "P1";
8
+ }
9
+ // Maps Lighthouse accessibility audit IDs to substrings that appear inside the
10
+ // pa11y rule_code for the same class of issue. A leading "." prevents matching
11
+ // partial segment names (e.g. ".H44" won't accidentally match ".1H44").
12
+ const LH_TO_PA11Y_SUBSTRINGS = {
13
+ "color-contrast": [".G18", ".G145", ".G174"],
14
+ "image-alt": [".H37", ".H67", ".F65"],
15
+ label: [".H44", ".F68", ".H91.Input", ".H91.Select", ".H91.Textarea"],
16
+ "link-name": [".H30", ".H91.A."],
17
+ "document-title": [".H25", ".F89"],
18
+ "html-has-lang": [".H57"],
19
+ "frame-title": [".H64"],
20
+ "button-name": [".H91.Button"],
21
+ "select-name": [".H91.Select"],
22
+ "duplicate-id-aria": [".H93", ".F77"],
23
+ };
24
+ // Lighthouse performance audit IDs where Rule 2 (code linkage) is relevant.
25
+ const PERFORMANCE_AUDIT_IDS = new Set([
26
+ "render-blocking-resources",
27
+ "unused-javascript",
28
+ "unused-css-rules",
29
+ "bootup-time",
30
+ "mainthread-work-breakdown",
31
+ "total-blocking-time",
32
+ ]);
33
+ function makeId(tool, f) {
34
+ const e = f.evidence;
35
+ if (tool === "lighthouse")
36
+ return `lh:${String(e["audit_id"] ?? f.title)}`;
37
+ if (tool === "a11y") {
38
+ const code = String(e["rule_code"] ?? "").split(".").slice(-2).join(".");
39
+ const sel = String(e["selector"] ?? "").slice(0, 16);
40
+ return `a11y:${code}:${sel}`;
41
+ }
42
+ const basename = String(e["file"] ?? "").split("/").pop() ?? "";
43
+ return `static:${String(e["source"] ?? "")}:${basename}:${String(e["line"] ?? "")}`;
44
+ }
45
+ function tag(tool, findings) {
46
+ return findings.map((f) => ({ ...f, id: makeId(tool, f), source_tool: tool }));
47
+ }
48
+ // Rule 1 — Double-confirmed accessibility:
49
+ // A Lighthouse accessibility finding and a pa11y finding that cover the same
50
+ // WCAG technique are merged into one finding with priority promoted one tier
51
+ // and confirmed_by set to both tools.
52
+ function applyRule1(lhFindings, a11yFindings) {
53
+ const correlated = [];
54
+ const consumedLhIds = new Set();
55
+ const consumedA11yIds = new Set();
56
+ for (const lhF of lhFindings) {
57
+ const auditId = String(lhF.evidence["audit_id"] ?? "");
58
+ const substrings = LH_TO_PA11Y_SUBSTRINGS[auditId];
59
+ if (!substrings)
60
+ continue;
61
+ for (const a11yF of a11yFindings) {
62
+ if (consumedA11yIds.has(a11yF.id))
63
+ continue;
64
+ const ruleCode = String(a11yF.evidence["rule_code"] ?? "");
65
+ if (!substrings.some((s) => ruleCode.includes(s)))
66
+ continue;
67
+ // Use the better (lower index = higher priority) of the two raw priorities
68
+ // as the base, then promote it one tier.
69
+ const basePriority = PRIORITY_ORDER[lhF.priority] <= PRIORITY_ORDER[a11yF.priority]
70
+ ? lhF.priority
71
+ : a11yF.priority;
72
+ correlated.push({
73
+ id: `corr:lh+a11y:${auditId}`,
74
+ source_tool: "lighthouse+pa11y",
75
+ priority: promotePriority(basePriority),
76
+ title: lhF.title,
77
+ description: `[Lighthouse] ${lhF.description} ` +
78
+ `[pa11y] ${a11yF.description} ` +
79
+ `Two independent tools flagged the same accessibility gap — ` +
80
+ `this cross-tool confirmation increases confidence that the issue is real and affects real users.`,
81
+ evidence: {
82
+ ...lhF.evidence,
83
+ pa11y_rule_code: ruleCode,
84
+ pa11y_selector: a11yF.evidence["selector"] ?? "",
85
+ },
86
+ confirmed_by: ["lighthouse", "pa11y"],
87
+ });
88
+ consumedLhIds.add(lhF.id);
89
+ consumedA11yIds.add(a11yF.id);
90
+ break; // one correlation per Lighthouse finding
91
+ }
92
+ }
93
+ return { correlated, consumedLhIds, consumedA11yIds };
94
+ }
95
+ // Rule 2 — Performance ↔ code linkage:
96
+ // If a Lighthouse performance finding's description or display value contains a
97
+ // filename that also appears in a static analysis finding, the static finding is
98
+ // added as a related_finding on the Lighthouse entry.
99
+ // Note: full coverage requires the Lighthouse parser to extract resource URLs
100
+ // from audit detail items — the current parser only surfaces displayValue, so
101
+ // matches depend on whether filenames appear there.
102
+ function applyRule2(lhFindings, staticFindings) {
103
+ if (staticFindings.length === 0)
104
+ return lhFindings;
105
+ return lhFindings.map((lhF) => {
106
+ const auditId = String(lhF.evidence["audit_id"] ?? "");
107
+ if (!PERFORMANCE_AUDIT_IDS.has(auditId))
108
+ return lhF;
109
+ const haystack = `${lhF.description} ${String(lhF.evidence["value"] ?? "")}`.toLowerCase();
110
+ const related = staticFindings
111
+ .filter((sf) => {
112
+ const basename = String(sf.evidence["file"] ?? "").split("/").pop() ?? "";
113
+ return basename.length > 3 && haystack.includes(basename.toLowerCase());
114
+ })
115
+ .map((sf) => sf.id);
116
+ return related.length > 0 ? { ...lhF, related_findings: related } : lhF;
117
+ });
118
+ }
119
+ export function correlate(reports) {
120
+ const lhTagged = tag("lighthouse", reports.lighthouse?.findings ?? []);
121
+ const a11yTagged = tag("a11y", reports.a11y?.findings ?? []);
122
+ const staticTagged = tag("static", reports.static?.findings ?? []);
123
+ const { correlated, consumedLhIds, consumedA11yIds } = applyRule1(lhTagged, a11yTagged);
124
+ const remainingLh = applyRule2(lhTagged.filter((f) => !consumedLhIds.has(f.id)), staticTagged);
125
+ const remainingA11y = a11yTagged.filter((f) => !consumedA11yIds.has(f.id));
126
+ return {
127
+ correlated_findings: correlated,
128
+ unique_findings: [...remainingLh, ...remainingA11y, ...staticTagged],
129
+ correlations_count: correlated.length,
130
+ };
131
+ }
132
+ //# sourceMappingURL=correlator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlator.js","sourceRoot":"","sources":["../../src/mappers/correlator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { LighthouseAuditRef, Pa11yViolation } from "../utils/outputParsers.js";
2
+ import type { Finding } from "../types.js";
3
+ export declare function formatLighthouseFinding(audit: LighthouseAuditRef): Finding | null;
4
+ export interface StaticAnalysisIssue {
5
+ source: "eslint" | "semgrep";
6
+ file: string;
7
+ line: number;
8
+ column?: number;
9
+ ruleId: string;
10
+ message: string;
11
+ severity: number | string;
12
+ category?: string;
13
+ }
14
+ export declare function formatStaticAnalysisFinding(issue: StaticAnalysisIssue): Finding | null;
15
+ export declare function formatA11yFinding(violation: Pa11yViolation): Finding | null;