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/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 across many elements collapses into one systemic finding rather than
286
- one per element.
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 [declined deliberately](../performance-audit-plan.md)static crawling finds 7–11 internal links on modern commerce homepages. |
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 | Level AAA | — | — |
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
- │ │ └── psiAggregator.ts # Cross-run arithmetic + redundancy rules
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.4.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",