nfunc-mcp 0.4.0 → 0.5.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 +38 -4
- package/dist/mappers/a11yDedupe.js +38 -7
- package/dist/mappers/a11yDedupe.js.map +1 -1
- package/dist/mappers/defectFormatter.d.ts +9 -1
- package/dist/mappers/defectFormatter.js +53 -13
- package/dist/mappers/defectFormatter.js.map +1 -1
- package/dist/mappers/priorityMapper.d.ts +42 -0
- package/dist/mappers/priorityMapper.js +58 -0
- package/dist/mappers/priorityMapper.js.map +1 -1
- package/dist/mappers/runComparator.d.ts +85 -0
- package/dist/mappers/runComparator.js +165 -0
- package/dist/mappers/runComparator.js.map +1 -0
- package/dist/mappers/wcagLevels.d.ts +73 -0
- package/dist/mappers/wcagLevels.js +320 -0
- package/dist/mappers/wcagLevels.js.map +1 -0
- package/dist/tools/accessibility.d.ts +1 -0
- package/dist/tools/accessibility.js +488 -63
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/lighthouse.js +370 -102
- package/dist/tools/lighthouse.js.map +1 -1
- package/dist/utils/batchState.d.ts +75 -0
- package/dist/utils/batchState.js +128 -0
- package/dist/utils/batchState.js.map +1 -0
- package/dist/utils/outputParsers.js +26 -30
- package/dist/utils/outputParsers.js.map +1 -1
- package/dist/utils/psiParser.d.ts +11 -0
- package/dist/utils/psiParser.js +2 -2
- package/dist/utils/psiParser.js.map +1 -1
- package/dist/utils/urlInput.d.ts +30 -0
- package/dist/utils/urlInput.js +130 -0
- package/dist/utils/urlInput.js.map +1 -0
- package/docs/manual.md +218 -7
- package/package.json +1 -1
package/docs/manual.md
CHANGED
|
@@ -10,6 +10,8 @@ tools are and how to ask for them; this covers how to run and interpret them.
|
|
|
10
10
|
2. [Install and register](#install-and-register)
|
|
11
11
|
3. [`run_qa_gate` reference](#run_qa_gate-reference)
|
|
12
12
|
4. [Individual tool reference](#individual-tool-reference)
|
|
13
|
+
- [Auditing several URLs at once](#auditing-several-urls-at-once)
|
|
14
|
+
- [Comparing two runs (before vs after)](#comparing-two-runs-before-vs-after)
|
|
13
15
|
5. [PSI performance audit](#psi-performance-audit)
|
|
14
16
|
6. [Priority system](#priority-system)
|
|
15
17
|
7. [Project layout](#project-layout)
|
|
@@ -282,8 +284,71 @@ pa11y at WCAG 2 AA by default, violations only. Returns `url`, `standard`,
|
|
|
282
284
|
`runners`, `violation_count`, `raw_violation_count`, `findings`.
|
|
283
285
|
|
|
284
286
|
`raw_violation_count` versus `violation_count` shows the dedup at work: a rule
|
|
285
|
-
failing
|
|
286
|
-
one
|
|
287
|
+
failing on more than two elements collapses into one systemic finding carrying
|
|
288
|
+
`distinct_elements` and the full `sample_selectors` list, rather than one line
|
|
289
|
+
per element. Three gallery images with no alt text are one template to fix, not
|
|
290
|
+
three authoring mistakes.
|
|
291
|
+
|
|
292
|
+
This matters across pages as much as within one. Before the threshold was
|
|
293
|
+
lowered, a homepage with 11 duplicate ids collapsed to a single finding while a
|
|
294
|
+
category page with 10 listed every one — the same component, but one page
|
|
295
|
+
appeared four times worse. Collapsing consistently is what makes per-page counts
|
|
296
|
+
comparable at all.
|
|
297
|
+
|
|
298
|
+
#### WCAG conformance and priority
|
|
299
|
+
|
|
300
|
+
Findings are priced by what they cost a **conformance claim**, not by how bad
|
|
301
|
+
the defect feels. `target_level` names the level the project has committed to —
|
|
302
|
+
default `AA`, the legal and industry bar for essentially all commercial work.
|
|
303
|
+
|
|
304
|
+
| Finding | Priority | Why |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| Level **A** criterion fails | **P1** | The floor. While any Level A criterion fails, no higher level is achievable — AA conformance is impossible regardless of how the AA-specific criteria score |
|
|
307
|
+
| Level **AA** criterion fails | **P2** | Blocks an AA commitment |
|
|
308
|
+
| Criterion **above** the target | **P3** | An enhancement, not a gap. `target-size` is 2.5.5, Level **AAA** in WCAG 2.1 — it should not fail an AA audit |
|
|
309
|
+
| Not a success criterion | **P3** | A best-practice rule. Worth fixing; does not affect a conformance claim |
|
|
310
|
+
|
|
311
|
+
Two demotions apply after that: axe's `needsFurtherReview` (a maybe should not
|
|
312
|
+
gate a release as hard as a certainty) and an axe impact of `minor` — the "very
|
|
313
|
+
minor AA issue" tier.
|
|
314
|
+
|
|
315
|
+
Every finding carries `wcag_criterion`, `wcag_name`, `wcag_level` and
|
|
316
|
+
`blocks_target` in its evidence, and its description states what the failure
|
|
317
|
+
means for the claim.
|
|
318
|
+
|
|
319
|
+
Each run also returns a `conformance` block, and a batch adds a cross-page
|
|
320
|
+
rollup:
|
|
321
|
+
|
|
322
|
+
```jsonc
|
|
323
|
+
"conformance": {
|
|
324
|
+
"target_level": "AA",
|
|
325
|
+
"conformant": false,
|
|
326
|
+
"failing_criteria": { "A": 6, "AA": 1, "AAA": 0 },
|
|
327
|
+
"beyond_target": 0,
|
|
328
|
+
"failed_criteria": [
|
|
329
|
+
{ "criterion": "4.1.1", "name": "Parsing", "level": "A", "findings": 5, "blocks_target": true }
|
|
330
|
+
],
|
|
331
|
+
"summary": "Not Level AA conformant. 6 Level A criteria fail... Level A is the floor..."
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**The unit is the criterion, not the finding.** Twelve findings against one
|
|
336
|
+
criterion is one thing to fix and one line in a conformance statement. The
|
|
337
|
+
finding count answers "how much work"; the criterion count answers "are we
|
|
338
|
+
conformant". A report giving only the first is how a page ends up described as
|
|
339
|
+
having 22 accessibility issues when it fails five criteria.
|
|
340
|
+
|
|
341
|
+
Criterion levels are transcribed from
|
|
342
|
+
[WCAG 2.1](https://www.w3.org/TR/WCAG21/). htmlcs encodes the criterion in its
|
|
343
|
+
rule code; axe does not expose WCAG tags through pa11y, so its rule ids go
|
|
344
|
+
through a lookup table in `src/mappers/wcagLevels.ts`. Rules axe classifies as
|
|
345
|
+
best-practice map to no criterion deliberately — reporting one as a conformance
|
|
346
|
+
failure would overstate the legal position.
|
|
347
|
+
|
|
348
|
+
**Automated testing reaches roughly a third of WCAG criteria.** A `conformant:
|
|
349
|
+
true` result means nothing automated failed, not that the page conforms. Focus
|
|
350
|
+
order, keyboard traps, meaningful sequence, error suggestion and content on
|
|
351
|
+
hover all need a human.
|
|
287
352
|
|
|
288
353
|
#### Choosing an accessibility engine
|
|
289
354
|
|
|
@@ -316,6 +381,132 @@ ESLint config when it finds one, otherwise a QA-focused baseline. Returns
|
|
|
316
381
|
`path`, `tools_run`, `eslint_config_used`, `issue_count`, `findings`,
|
|
317
382
|
`warnings`.
|
|
318
383
|
|
|
384
|
+
|
|
385
|
+
### Auditing several URLs at once
|
|
386
|
+
|
|
387
|
+
`run_lighthouse` and `run_accessibility_check` both accept three input shapes in
|
|
388
|
+
the same `url` field, and detect which they were given:
|
|
389
|
+
|
|
390
|
+
| You pass | Detected as |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `https://site.com/page` | a single URL — one report, returned immediately |
|
|
393
|
+
| `https://a.com, https://b.com` (or newline-separated) | a list — batch mode |
|
|
394
|
+
| `./top-pages.csv` | a CSV — the URL column is found by name or by content |
|
|
395
|
+
|
|
396
|
+
A bare domain gets `https://` assumed, duplicates are dropped, and unparseable
|
|
397
|
+
entries are reported rather than silently skipped. An explicit `urls` array
|
|
398
|
+
works too.
|
|
399
|
+
|
|
400
|
+
**One URL behaves exactly as before** — same response shape, no batch fields.
|
|
401
|
+
Several URLs switch to batch mode:
|
|
402
|
+
|
|
403
|
+
- Each call is bounded by `max_seconds_per_call` (default **100 s**, chosen to
|
|
404
|
+
stay under the 120 s at which Claude Code backgrounds a tool call) and returns
|
|
405
|
+
a `cursor`. Keep calling until `complete` is true.
|
|
406
|
+
- Every report is written to `output_dir` as it lands — the **raw** Lighthouse
|
|
407
|
+
LHR, so the individual audits survive — and merged into a running
|
|
408
|
+
`_index.json`.
|
|
409
|
+
- **Re-run to fill gaps.** Call again with the same input and *no cursor*;
|
|
410
|
+
completed URL/variant pairs are skipped automatically. `skip_completed: false`
|
|
411
|
+
forces fresh measurements.
|
|
412
|
+
- The final call adds an `aggregate` block. Quote its numbers rather than
|
|
413
|
+
recomputing them.
|
|
414
|
+
|
|
415
|
+
Lighthouse runs **sequentially** in batch mode, unlike the single-URL path.
|
|
416
|
+
Two Chrome instances on one machine contend for CPU, and a performance audit
|
|
417
|
+
whose numbers came from a half-busy machine is not worth having. pa11y still
|
|
418
|
+
runs its engines concurrently — it is not measuring time.
|
|
419
|
+
|
|
420
|
+
#### What the aggregates tell you
|
|
421
|
+
|
|
422
|
+
`run_lighthouse` returns per-strategy means, a **per-template rollup** (the same
|
|
423
|
+
classifier the PSI plan tool uses, so twelve product URLs report as "PDP average
|
|
424
|
+
61" rather than as twelve rows), CWV verdict tallies, and outlier detection
|
|
425
|
+
against the median.
|
|
426
|
+
|
|
427
|
+
`run_accessibility_check` returns something a single-page run cannot: **which
|
|
428
|
+
rules fail across most pages**. A rule failing on 80%+ of pages is marked
|
|
429
|
+
`shared_layout: true` — it lives in the header, footer or base template, so one
|
|
430
|
+
fix clears every page. A rule failing on one page is that page's own bug. On a
|
|
431
|
+
four-page sample, `color-contrast` hit 4/4 while `image-alt` and `link-name` hit
|
|
432
|
+
1/4: two completely different pieces of work, and volume alone cannot separate
|
|
433
|
+
them.
|
|
434
|
+
|
|
435
|
+
#### Mobile accessibility
|
|
436
|
+
|
|
437
|
+
`form_factor` on `run_accessibility_check` defaults to `desktop` and accepts
|
|
438
|
+
`mobile` or `both`. Mobile emulates 412×823 at 2× DPR with touch, matching
|
|
439
|
+
`run_lighthouse`'s mobile profile so the two describe the same rendered page.
|
|
440
|
+
|
|
441
|
+
The viewport genuinely applies — a screenshot from the mobile run measures
|
|
442
|
+
824×23418 against 1280×2418 for the default. **But temper expectations:** on a
|
|
443
|
+
test site, neither htmlcs nor axe reported a single different violation between
|
|
444
|
+
the two viewports, because the rules both engines run here are structural —
|
|
445
|
+
missing labels, duplicate ids, absent alt text — and structure does not change
|
|
446
|
+
with width. It earns its keep on sites whose mobile DOM genuinely differs (a
|
|
447
|
+
hamburger nav, different components rendered), which is common on real commerce
|
|
448
|
+
sites.
|
|
449
|
+
|
|
450
|
+
For viewport-*dependent* accessibility defects today, `run_lighthouse` with
|
|
451
|
+
`form_factor: "both"` is the stronger tool: it reported `target-size` tagged
|
|
452
|
+
`form_factor_specific: true`, a touch-target failure that exists only on mobile
|
|
453
|
+
and that pa11y did not surface at all.
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
### Comparing two runs (before vs after)
|
|
457
|
+
|
|
458
|
+
A snapshot answers "what is wrong with this page". A developer about to open a
|
|
459
|
+
PR is asking something else: *did my fix work, and did I break anything?* A
|
|
460
|
+
violation count cannot separate those.
|
|
461
|
+
|
|
462
|
+
Both tools take `baseline_dir`. Point it at an earlier `output_dir`:
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
# 1. capture a baseline before touching anything
|
|
466
|
+
Run an accessibility check on http://localhost:3000, save to ./a11y-base
|
|
467
|
+
|
|
468
|
+
# 2. make the fix, then re-scan against it
|
|
469
|
+
Scan http://localhost:3000 again and compare to ./a11y-base
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The response gains a `comparison` block:
|
|
473
|
+
|
|
474
|
+
```jsonc
|
|
475
|
+
"comparison": {
|
|
476
|
+
"verdict": "mixed", // clean | improved | mixed | regression | unchanged
|
|
477
|
+
"summary": "5 defect(s) fixed, but 1 newly introduced (worst: P2). Total went 6 to 2; the drop is real but incomplete — check newly_introduced before treating this as a clean fix.",
|
|
478
|
+
"fixed": [{ "id": "image-alt", "was": "P1", ... }],
|
|
479
|
+
"still_failing": [{ "id": "color-contrast", "priority": "P3", ... }],
|
|
480
|
+
"newly_introduced": [{ "id": "aria-valid-attr-value", "priority": "P2", ... }],
|
|
481
|
+
"score_changes": [{ "category": "accessibility", "before": 94, "after": 64, "delta": -30 }]
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`newly_introduced` is the half that earns this feature. Measured on a real
|
|
486
|
+
page: adding `alt` text, an `aria-label` and a `<label>` fixed **five P1s** and
|
|
487
|
+
introduced one **P2**, because the `aria-labelledby` also added pointed at an id
|
|
488
|
+
that did not exist. A Lighthouse run against a page given a render-blocking
|
|
489
|
+
script reported `regression` with accessibility **94 → 64** and named all four
|
|
490
|
+
injected defects.
|
|
491
|
+
|
|
492
|
+
Notes on how it behaves:
|
|
493
|
+
|
|
494
|
+
- **This works with a single URL**, not only batches. A single-URL run touches
|
|
495
|
+
disk only when `output_dir` is given, so a one-off check stays a one-off while
|
|
496
|
+
the same call can seed a baseline.
|
|
497
|
+
- **`localhost` is fully supported** — pa11y and Lighthouse run Chrome on your
|
|
498
|
+
machine. This is the pre-PR check PSI cannot do.
|
|
499
|
+
- **`baseline_dir` may equal `output_dir`.** The baseline is read before
|
|
500
|
+
anything is written, so "compare against the last run in here" works.
|
|
501
|
+
- **`skip_completed` flips to `false` when comparing.** Re-measuring is the
|
|
502
|
+
whole point; skipping completed work would compare a run against itself.
|
|
503
|
+
- **Comparison is per `(url, variant, defect id)`.** Only runs present on both
|
|
504
|
+
sides are compared — a page that was not re-tested is reported under
|
|
505
|
+
`not_in_current` rather than counted as fixed, since a defect can only be
|
|
506
|
+
called fixed if the page was measured again.
|
|
507
|
+
- A defect that changed priority between runs counts as **still failing**, not
|
|
508
|
+
as fixed-and-reintroduced. It is the same defect on the same element.
|
|
509
|
+
|
|
319
510
|
---
|
|
320
511
|
|
|
321
512
|
## PSI performance audit
|
|
@@ -381,7 +572,16 @@ because it is real users, not because it is current.
|
|
|
381
572
|
| `sitemap` (default) | robots.txt, then `/sitemap.xml` and `/sitemap_index.xml`, then `<link rel="sitemap">`, then seven common CMS locations. Tiers only advance when earlier ones find nothing. |
|
|
382
573
|
| `list` | Explicit `urls` array. |
|
|
383
574
|
| `csv` | `csv_path` — column detected by name or by content. An analytics top-pages export is the best input for a performance audit, being traffic-weighted. |
|
|
384
|
-
| `crawl` | Not implemented, and
|
|
575
|
+
| `crawl` | Not implemented, and declined deliberately — see below. |
|
|
576
|
+
|
|
577
|
+
**Why there is no crawler.** Static link extraction was measured against real
|
|
578
|
+
homepages and found 11 internal paths on nodejs.org (whose sitemap has 1,723),
|
|
579
|
+
7 on gap.com, and nothing at all on a site behind a bot wall. The pages most
|
|
580
|
+
likely to lack a sitemap are the same ones that are client-rendered or
|
|
581
|
+
bot-protected, so a crawler fails precisely where it would be needed. An
|
|
582
|
+
analytics top-pages export is a better input anyway, being weighted by real
|
|
583
|
+
traffic. If one is ever built it should drive a headless browser rather than
|
|
584
|
+
parse static HTML.
|
|
385
585
|
|
|
386
586
|
Discovered URLs are clustered into **templates** by path shape, and the plan
|
|
387
587
|
tool proposes representative samples per template. Passing a URL with a path
|
|
@@ -434,9 +634,9 @@ output into a written report.
|
|
|
434
634
|
|
|
435
635
|
| Priority | Meaning | Lighthouse | WCAG | ESLint / Semgrep | CrUX field |
|
|
436
636
|
|---|---|---|---|---|---|
|
|
437
|
-
| P1 | Blocker — fix before shipping | Score < 50 | Level A | Semgrep security, ESLint error | Core vital rated poor |
|
|
438
|
-
| P2 | Warning — track before merging | 50–79 | Level AA | ESLint warning | Needs improvement, or any diagnostic |
|
|
439
|
-
| P3 | Advisory — log as tech debt | 80–89 |
|
|
637
|
+
| P1 | Blocker — fix before shipping | Score < 50 | **Level A failure** — puts the target out of reach | Semgrep security, ESLint error | Core vital rated poor |
|
|
638
|
+
| P2 | Warning — track before merging | 50–79 | **Level AA failure** | ESLint warning | Needs improvement, or any diagnostic |
|
|
639
|
+
| P3 | Advisory — log as tech debt | 80–89 | **Above the target**, or a best-practice rule | — | — |
|
|
440
640
|
| *(suppressed)* | Passing — never reported | ≥ 90 | — | — | Good |
|
|
441
641
|
|
|
442
642
|
Lighthouse findings are actually ranked by `weight × (1 − score)` — the category
|
|
@@ -476,7 +676,9 @@ Adjustments:
|
|
|
476
676
|
│ │ ├── compositeScore.ts # Per-tool sub-scores
|
|
477
677
|
│ │ ├── webVitalsMapper.ts # CrUX thresholds → priorities → prose
|
|
478
678
|
│ │ ├── labFieldComparator.ts # Lab vs field verdicts
|
|
479
|
-
│ │
|
|
679
|
+
│ │ ├── psiAggregator.ts # Cross-run arithmetic + redundancy rules
|
|
680
|
+
│ │ ├── runComparator.ts # Before/after diff: fixed, still failing, new
|
|
681
|
+
│ │ └── wcagLevels.ts # WCAG 2.1 criteria, levels, conformance rollup
|
|
480
682
|
│ └── utils/ # Cross-tool helpers
|
|
481
683
|
│ ├── shellRunner.ts # Subprocess choke point
|
|
482
684
|
│ ├── httpClient.ts # HTTP choke point (retry, deadline, redaction)
|
|
@@ -488,6 +690,8 @@ Adjustments:
|
|
|
488
690
|
│ ├── urlClassifier.ts # URL list → page templates
|
|
489
691
|
│ ├── csvReader.ts # URL extraction from CSV
|
|
490
692
|
│ ├── publicUrl.ts # Reachability + session-gate checks
|
|
693
|
+
│ ├── urlInput.ts # One URL / list / CSV → URL array
|
|
694
|
+
│ ├── batchState.ts # Cursor, budget, index merge, gap-fill
|
|
491
695
|
│ ├── eslintConfigDetector.ts
|
|
492
696
|
│ └── toolResponse.ts
|
|
493
697
|
├── docs/
|
|
@@ -552,6 +756,13 @@ served origin data.
|
|
|
552
756
|
Its CLI is not on PATH. Install it (see
|
|
553
757
|
[prerequisites](#prerequisites)) or ignore it — the rest of the gate still runs.
|
|
554
758
|
|
|
759
|
+
### A comparison reports nothing was compared
|
|
760
|
+
|
|
761
|
+
`baseline_dir` had no index, or it covers different URLs than this run. Check
|
|
762
|
+
`not_in_baseline` and `not_in_current` in the comparison block — only runs
|
|
763
|
+
present on both sides can be compared. Create a baseline by running once with
|
|
764
|
+
`output_dir` set.
|
|
765
|
+
|
|
555
766
|
### Lighthouse scores look far worse than expected
|
|
556
767
|
|
|
557
768
|
Check `form_factor`. The mobile profile applies a 4× CPU slowdown and simulated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nfunc-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for non-functional QA — Lighthouse, pa11y, ESLint and Semgrep in parallel with cross-tool correlation, plus PageSpeed Insights audits that compare lab scores against real-user Core Web Vitals from CrUX",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|