nfunc-mcp 0.2.0 → 0.4.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 +85 -335
- package/dist/index.js +27 -4
- package/dist/index.js.map +1 -1
- package/dist/mappers/a11yDedupe.d.ts +25 -0
- package/dist/mappers/a11yDedupe.js +95 -0
- package/dist/mappers/a11yDedupe.js.map +1 -0
- package/dist/mappers/compositeScore.d.ts +23 -0
- package/dist/mappers/compositeScore.js +107 -0
- package/dist/mappers/compositeScore.js.map +1 -0
- package/dist/mappers/correlator.js +94 -33
- package/dist/mappers/correlator.js.map +1 -1
- package/dist/mappers/defectFormatter.js +129 -15
- package/dist/mappers/defectFormatter.js.map +1 -1
- package/dist/mappers/labFieldComparator.d.ts +62 -0
- package/dist/mappers/labFieldComparator.js +134 -0
- package/dist/mappers/labFieldComparator.js.map +1 -0
- package/dist/mappers/priorityMapper.d.ts +38 -0
- package/dist/mappers/priorityMapper.js +126 -0
- package/dist/mappers/priorityMapper.js.map +1 -1
- package/dist/mappers/psiAggregator.d.ts +130 -0
- package/dist/mappers/psiAggregator.js +293 -0
- package/dist/mappers/psiAggregator.js.map +1 -0
- package/dist/mappers/webVitalsMapper.d.ts +52 -0
- package/dist/mappers/webVitalsMapper.js +131 -0
- package/dist/mappers/webVitalsMapper.js.map +1 -0
- package/dist/tools/accessibility.js +72 -31
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/lighthouse.d.ts +14 -0
- package/dist/tools/lighthouse.js +134 -34
- package/dist/tools/lighthouse.js.map +1 -1
- package/dist/tools/performanceAudit.d.ts +2 -0
- package/dist/tools/performanceAudit.js +446 -0
- package/dist/tools/performanceAudit.js.map +1 -0
- package/dist/tools/performanceAuditPlan.d.ts +2 -0
- package/dist/tools/performanceAuditPlan.js +438 -0
- package/dist/tools/performanceAuditPlan.js.map +1 -0
- package/dist/tools/qaGate.js +109 -34
- package/dist/tools/qaGate.js.map +1 -1
- package/dist/utils/csvReader.d.ts +20 -0
- package/dist/utils/csvReader.js +172 -0
- package/dist/utils/csvReader.js.map +1 -0
- package/dist/utils/httpClient.d.ts +84 -0
- package/dist/utils/httpClient.js +171 -0
- package/dist/utils/httpClient.js.map +1 -0
- package/dist/utils/outputParsers.d.ts +25 -0
- package/dist/utils/outputParsers.js +42 -0
- package/dist/utils/outputParsers.js.map +1 -1
- package/dist/utils/psiAuth.d.ts +26 -0
- package/dist/utils/psiAuth.js +36 -0
- package/dist/utils/psiAuth.js.map +1 -0
- package/dist/utils/psiParser.d.ts +124 -0
- package/dist/utils/psiParser.js +200 -0
- package/dist/utils/psiParser.js.map +1 -0
- package/dist/utils/publicUrl.d.ts +17 -0
- package/dist/utils/publicUrl.js +115 -0
- package/dist/utils/publicUrl.js.map +1 -0
- package/dist/utils/sitemapReader.d.ts +27 -0
- package/dist/utils/sitemapReader.js +272 -0
- package/dist/utils/sitemapReader.js.map +1 -0
- package/dist/utils/urlClassifier.d.ts +45 -0
- package/dist/utils/urlClassifier.js +267 -0
- package/dist/utils/urlClassifier.js.map +1 -0
- package/docs/manual.md +558 -0
- package/docs/psi-report-spec.md +174 -0
- package/package.json +14 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lab versus field disagreement — the reason this tool exists.
|
|
3
|
+
*
|
|
4
|
+
* A Lighthouse run is a simulation on one machine under one throttling
|
|
5
|
+
* profile. CrUX is what actually happened to real people. When the two agree,
|
|
6
|
+
* confidence is high. When they disagree, the direction of the disagreement is
|
|
7
|
+
* itself the finding, and it is not available from any other tool in this MCP.
|
|
8
|
+
*
|
|
9
|
+
* The Five Below homepage is the canonical case: lab CLS 0 against field CLS
|
|
10
|
+
* 0.55, and lab LCP 11.6 s against field LCP 2.8 s — both metrics inverted, in
|
|
11
|
+
* opposite directions, on one page. A report built on the lab numbers alone
|
|
12
|
+
* led with an LCP emergency real users were not experiencing and missed a
|
|
13
|
+
* layout-shift failure that 70% of them were.
|
|
14
|
+
*/
|
|
15
|
+
import type { Priority } from "../types.js";
|
|
16
|
+
import type { CruxMetric, LabMetrics, ParsedCrux, WebVital } from "../utils/psiParser.js";
|
|
17
|
+
export type LabFieldVerdict = "confirmed" | "worse_in_field" | "worse_in_lab" | "both_pass";
|
|
18
|
+
export interface MetricComparison {
|
|
19
|
+
metric: WebVital;
|
|
20
|
+
label: string;
|
|
21
|
+
lab: number | null;
|
|
22
|
+
lab_display: string | null;
|
|
23
|
+
field_p75: number;
|
|
24
|
+
field_display: string;
|
|
25
|
+
field_source: CruxMetric["source"];
|
|
26
|
+
verdict: LabFieldVerdict;
|
|
27
|
+
note: string;
|
|
28
|
+
}
|
|
29
|
+
/** Lighthouse audit ids, for matching a lab finding back to the vital it measures. */
|
|
30
|
+
export declare const AUDIT_TO_VITAL: Record<string, WebVital>;
|
|
31
|
+
/**
|
|
32
|
+
* Compare every field metric that has a lab counterpart.
|
|
33
|
+
*
|
|
34
|
+
* `both_pass` rows are returned rather than filtered, because the comparison
|
|
35
|
+
* table in the report is a statement about coverage — a reader needs to see
|
|
36
|
+
* that LCP was checked and agreed, not infer it from an absence.
|
|
37
|
+
*/
|
|
38
|
+
export declare function compareLabField(lab: LabMetrics, field: ParsedCrux | null): MetricComparison[];
|
|
39
|
+
/**
|
|
40
|
+
* Promote, but never past a diagnostic's ceiling.
|
|
41
|
+
*
|
|
42
|
+
* `webVitalsMapper` caps FCP and TTFB at P2 because they explain a Core Web
|
|
43
|
+
* Vital rather than being one. Field confirmation makes a finding more
|
|
44
|
+
* certain, not more important, so an unguarded promotion quietly defeated that
|
|
45
|
+
* cap and put FCP at P1 above the LCP it was describing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function promote(priority: Priority, vital?: WebVital): Priority;
|
|
48
|
+
export declare function demote(priority: Priority): Priority;
|
|
49
|
+
/**
|
|
50
|
+
* How a verdict should move a finding's priority.
|
|
51
|
+
*
|
|
52
|
+
* Field observation outranks simulation, so the two adjustments are not
|
|
53
|
+
* symmetric in what they mean: a promotion says "real users confirm this", a
|
|
54
|
+
* demotion says "only the simulation saw this". A demoted finding is tagged
|
|
55
|
+
* rather than dropped — the lab number is still true, it just is not evidence
|
|
56
|
+
* of user harm, and silently deleting it would hide a genuine regression
|
|
57
|
+
* signal from anyone comparing runs over time.
|
|
58
|
+
*/
|
|
59
|
+
export declare function adjustmentFor(verdict: LabFieldVerdict): {
|
|
60
|
+
direction: "promote" | "demote" | "none";
|
|
61
|
+
tag?: string;
|
|
62
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lab versus field disagreement — the reason this tool exists.
|
|
3
|
+
*
|
|
4
|
+
* A Lighthouse run is a simulation on one machine under one throttling
|
|
5
|
+
* profile. CrUX is what actually happened to real people. When the two agree,
|
|
6
|
+
* confidence is high. When they disagree, the direction of the disagreement is
|
|
7
|
+
* itself the finding, and it is not available from any other tool in this MCP.
|
|
8
|
+
*
|
|
9
|
+
* The Five Below homepage is the canonical case: lab CLS 0 against field CLS
|
|
10
|
+
* 0.55, and lab LCP 11.6 s against field LCP 2.8 s — both metrics inverted, in
|
|
11
|
+
* opposite directions, on one page. A report built on the lab numbers alone
|
|
12
|
+
* led with an LCP emergency real users were not experiencing and missed a
|
|
13
|
+
* layout-shift failure that 70% of them were.
|
|
14
|
+
*/
|
|
15
|
+
import { classifyVital, formatVitalValue, isCoreVital, vitalLabel } from "./webVitalsMapper.js";
|
|
16
|
+
/**
|
|
17
|
+
* Lab audits that measure the same thing as a field metric.
|
|
18
|
+
*
|
|
19
|
+
* INP is deliberately absent, and its absence is load-bearing. Lighthouse
|
|
20
|
+
* cannot produce an INP value at all — INP requires a real interaction, and a
|
|
21
|
+
* lab run never interacts with the page. Total Blocking Time is a *proxy* for
|
|
22
|
+
* responsiveness, not the same measurement, so comparing them would manufacture
|
|
23
|
+
* agreement or disagreement out of two different quantities. INP field findings
|
|
24
|
+
* therefore pass through the comparator untouched, which is why the systemic
|
|
25
|
+
* collapse rule in the aggregator has to exist.
|
|
26
|
+
*/
|
|
27
|
+
const LAB_EQUIVALENT = {
|
|
28
|
+
lcp: "lcpMs",
|
|
29
|
+
cls: "cls",
|
|
30
|
+
fcp: "fcpMs",
|
|
31
|
+
ttfb: "ttfbMs",
|
|
32
|
+
};
|
|
33
|
+
/** Lighthouse audit ids, for matching a lab finding back to the vital it measures. */
|
|
34
|
+
export const AUDIT_TO_VITAL = {
|
|
35
|
+
"largest-contentful-paint": "lcp",
|
|
36
|
+
"cumulative-layout-shift": "cls",
|
|
37
|
+
"first-contentful-paint": "fcp",
|
|
38
|
+
"server-response-time": "ttfb",
|
|
39
|
+
};
|
|
40
|
+
function noteFor(verdict, vital, labDisplay, fieldDisplay) {
|
|
41
|
+
const name = vitalLabel(vital);
|
|
42
|
+
switch (verdict) {
|
|
43
|
+
case "confirmed":
|
|
44
|
+
return `${name} fails in the lab (${labDisplay}) and for real users (${fieldDisplay}). Confirmed by two independent measurements — treat as real and fix.`;
|
|
45
|
+
case "worse_in_field":
|
|
46
|
+
return `The lab run passed ${name} at ${labDisplay}, but real users are at ${fieldDisplay}. The test environment is not reproducing what people actually experience — a real-world network, device class, geography or a third-party script that only loads in production. This is the highest-value finding type here, because no local tool can surface it.`;
|
|
47
|
+
case "worse_in_lab":
|
|
48
|
+
return `${name} fails in the lab (${labDisplay}) but real users are at ${fieldDisplay}, which passes. The lab profile is harsher than this page's actual audience. Treat the lab number as a stress signal rather than a user-experienced defect, and prioritise accordingly.`;
|
|
49
|
+
case "both_pass":
|
|
50
|
+
return `${name} is within target in both the lab (${labDisplay}) and the field (${fieldDisplay}).`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compare every field metric that has a lab counterpart.
|
|
55
|
+
*
|
|
56
|
+
* `both_pass` rows are returned rather than filtered, because the comparison
|
|
57
|
+
* table in the report is a statement about coverage — a reader needs to see
|
|
58
|
+
* that LCP was checked and agreed, not infer it from an absence.
|
|
59
|
+
*/
|
|
60
|
+
export function compareLabField(lab, field) {
|
|
61
|
+
if (!field)
|
|
62
|
+
return [];
|
|
63
|
+
const comparisons = [];
|
|
64
|
+
for (const [vital, labKey] of Object.entries(LAB_EQUIVALENT)) {
|
|
65
|
+
const fieldMetric = field.metrics[vital];
|
|
66
|
+
if (!fieldMetric)
|
|
67
|
+
continue;
|
|
68
|
+
const labValue = lab[labKey];
|
|
69
|
+
const fieldFails = classifyVital(vital, fieldMetric.p75) !== "good";
|
|
70
|
+
// No lab reading is not a pass; it is an unknown, and the field number
|
|
71
|
+
// stands on its own.
|
|
72
|
+
const labFails = labValue === null ? fieldFails : classifyVital(vital, labValue) !== "good";
|
|
73
|
+
const verdict = labFails && fieldFails ? "confirmed"
|
|
74
|
+
: !labFails && fieldFails ? "worse_in_field"
|
|
75
|
+
: labFails && !fieldFails ? "worse_in_lab"
|
|
76
|
+
: "both_pass";
|
|
77
|
+
const labDisplay = labValue === null ? null : formatVitalValue(vital, labValue);
|
|
78
|
+
const fieldDisplay = formatVitalValue(vital, fieldMetric.p75);
|
|
79
|
+
comparisons.push({
|
|
80
|
+
metric: vital,
|
|
81
|
+
label: vitalLabel(vital),
|
|
82
|
+
lab: labValue,
|
|
83
|
+
lab_display: labDisplay,
|
|
84
|
+
field_p75: fieldMetric.p75,
|
|
85
|
+
field_display: fieldDisplay,
|
|
86
|
+
field_source: fieldMetric.source,
|
|
87
|
+
verdict,
|
|
88
|
+
note: noteFor(verdict, vital, labDisplay, fieldDisplay),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return comparisons;
|
|
92
|
+
}
|
|
93
|
+
const UP = { P1: "P1", P2: "P1", P3: "P2" };
|
|
94
|
+
const DOWN = { P1: "P2", P2: "P3", P3: "P3" };
|
|
95
|
+
/**
|
|
96
|
+
* Promote, but never past a diagnostic's ceiling.
|
|
97
|
+
*
|
|
98
|
+
* `webVitalsMapper` caps FCP and TTFB at P2 because they explain a Core Web
|
|
99
|
+
* Vital rather than being one. Field confirmation makes a finding more
|
|
100
|
+
* certain, not more important, so an unguarded promotion quietly defeated that
|
|
101
|
+
* cap and put FCP at P1 above the LCP it was describing.
|
|
102
|
+
*/
|
|
103
|
+
export function promote(priority, vital) {
|
|
104
|
+
const promoted = UP[priority];
|
|
105
|
+
if (vital && !isCoreVital(vital) && promoted === "P1")
|
|
106
|
+
return "P2";
|
|
107
|
+
return promoted;
|
|
108
|
+
}
|
|
109
|
+
export function demote(priority) {
|
|
110
|
+
return DOWN[priority];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* How a verdict should move a finding's priority.
|
|
114
|
+
*
|
|
115
|
+
* Field observation outranks simulation, so the two adjustments are not
|
|
116
|
+
* symmetric in what they mean: a promotion says "real users confirm this", a
|
|
117
|
+
* demotion says "only the simulation saw this". A demoted finding is tagged
|
|
118
|
+
* rather than dropped — the lab number is still true, it just is not evidence
|
|
119
|
+
* of user harm, and silently deleting it would hide a genuine regression
|
|
120
|
+
* signal from anyone comparing runs over time.
|
|
121
|
+
*/
|
|
122
|
+
export function adjustmentFor(verdict) {
|
|
123
|
+
switch (verdict) {
|
|
124
|
+
case "confirmed":
|
|
125
|
+
return { direction: "promote", tag: "field_confirmed" };
|
|
126
|
+
case "worse_in_field":
|
|
127
|
+
return { direction: "promote", tag: "field_only" };
|
|
128
|
+
case "worse_in_lab":
|
|
129
|
+
return { direction: "demote", tag: "lab_only" };
|
|
130
|
+
default:
|
|
131
|
+
return { direction: "none" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=labFieldComparator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labFieldComparator.js","sourceRoot":"","sources":["../../src/mappers/labFieldComparator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
import type { Finding, Priority } from "../types.js";
|
|
2
2
|
export type { Priority };
|
|
3
3
|
export declare function lighthouseScoreToPriority(score: number): Priority | null;
|
|
4
|
+
/**
|
|
5
|
+
* Priority from an audit's actual impact on its Lighthouse category score.
|
|
6
|
+
*
|
|
7
|
+
* Scoring on `score` alone made every failing binary audit a P1: binary audits
|
|
8
|
+
* score exactly 0 when they fail, so a missing llms.txt ranked level with a
|
|
9
|
+
* 30-second Time to Interactive. On a real commerce page that produced 24
|
|
10
|
+
* Lighthouse P1s, 18 of them binary — and 19 of the 35 failing audits carried
|
|
11
|
+
* weight 0, meaning Lighthouse itself counts them toward no category score at
|
|
12
|
+
* all.
|
|
13
|
+
*
|
|
14
|
+
* `weight * (1 - score)` is the number of category points the audit actually
|
|
15
|
+
* costs — the same quantity Lighthouse uses to compute the category score. It
|
|
16
|
+
* ranks a weight-30 metric failing outright (30) above a weight-25 metric
|
|
17
|
+
* scoring 78 (5.5) above a weight-1 SEO check (1), which is the ordering a
|
|
18
|
+
* human triager would pick.
|
|
19
|
+
*
|
|
20
|
+
* Weight-0 diagnostics stay reportable but never block: they are supporting
|
|
21
|
+
* detail for the weighted metrics, and counting them again is double-counting
|
|
22
|
+
* (unused-javascript, bootup-time and mainthread-work-breakdown all describe
|
|
23
|
+
* the same overloaded main thread that total-blocking-time already charges for).
|
|
24
|
+
*
|
|
25
|
+
* `weight` undefined means the audit belongs to no category, which is not the
|
|
26
|
+
* same as weight 0 — fall back to the score-only mapping there.
|
|
27
|
+
*/
|
|
28
|
+
export declare function lighthouseImpactToPriority(score: number, weight: number | undefined): Priority | null;
|
|
4
29
|
export declare function wcagLevelToPriority(level: string): Priority | null;
|
|
30
|
+
/**
|
|
31
|
+
* Priority for an axe finding, from axe's own impact rating.
|
|
32
|
+
*
|
|
33
|
+
* Same principle as using Lighthouse's audit weight: the tool has already
|
|
34
|
+
* graded severity, so grade with it rather than inventing a parallel scheme.
|
|
35
|
+
* axe's ladder is critical > serious > moderate > minor.
|
|
36
|
+
*
|
|
37
|
+
* `needsFurtherReview` marks a rule that could not decide on its own and wants
|
|
38
|
+
* a human to confirm. Those are demoted one tier — a maybe should not gate a
|
|
39
|
+
* release with the same force as a definite.
|
|
40
|
+
*/
|
|
41
|
+
export declare function axeImpactToPriority(impact: string | undefined, needsReview: boolean | undefined): Priority | null;
|
|
42
|
+
export declare function a11yTechniqueToPriority(technique: string, wcagLevel: string): Priority | null;
|
|
5
43
|
export declare function staticAnalysisToPriority(source: "eslint" | "semgrep", severity: number | string, category?: string): Priority | null;
|
|
6
44
|
export declare function sortFindingsByPriority<T extends Finding>(findings: T[]): T[];
|
|
@@ -8,6 +8,42 @@ export function lighthouseScoreToPriority(score) {
|
|
|
8
8
|
return "P3";
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Priority from an audit's actual impact on its Lighthouse category score.
|
|
13
|
+
*
|
|
14
|
+
* Scoring on `score` alone made every failing binary audit a P1: binary audits
|
|
15
|
+
* score exactly 0 when they fail, so a missing llms.txt ranked level with a
|
|
16
|
+
* 30-second Time to Interactive. On a real commerce page that produced 24
|
|
17
|
+
* Lighthouse P1s, 18 of them binary — and 19 of the 35 failing audits carried
|
|
18
|
+
* weight 0, meaning Lighthouse itself counts them toward no category score at
|
|
19
|
+
* all.
|
|
20
|
+
*
|
|
21
|
+
* `weight * (1 - score)` is the number of category points the audit actually
|
|
22
|
+
* costs — the same quantity Lighthouse uses to compute the category score. It
|
|
23
|
+
* ranks a weight-30 metric failing outright (30) above a weight-25 metric
|
|
24
|
+
* scoring 78 (5.5) above a weight-1 SEO check (1), which is the ordering a
|
|
25
|
+
* human triager would pick.
|
|
26
|
+
*
|
|
27
|
+
* Weight-0 diagnostics stay reportable but never block: they are supporting
|
|
28
|
+
* detail for the weighted metrics, and counting them again is double-counting
|
|
29
|
+
* (unused-javascript, bootup-time and mainthread-work-breakdown all describe
|
|
30
|
+
* the same overloaded main thread that total-blocking-time already charges for).
|
|
31
|
+
*
|
|
32
|
+
* `weight` undefined means the audit belongs to no category, which is not the
|
|
33
|
+
* same as weight 0 — fall back to the score-only mapping there.
|
|
34
|
+
*/
|
|
35
|
+
export function lighthouseImpactToPriority(score, weight) {
|
|
36
|
+
if (score >= 90)
|
|
37
|
+
return null;
|
|
38
|
+
if (weight === undefined)
|
|
39
|
+
return lighthouseScoreToPriority(score);
|
|
40
|
+
const impact = weight * (1 - score / 100);
|
|
41
|
+
if (impact >= 10)
|
|
42
|
+
return "P1";
|
|
43
|
+
if (impact >= 3)
|
|
44
|
+
return "P2";
|
|
45
|
+
return "P3";
|
|
46
|
+
}
|
|
11
47
|
// Pass "notice" (or "warning" / "unknown") to suppress the finding.
|
|
12
48
|
export function wcagLevelToPriority(level) {
|
|
13
49
|
if (level === "A")
|
|
@@ -18,6 +54,96 @@ export function wcagLevelToPriority(level) {
|
|
|
18
54
|
return "P3";
|
|
19
55
|
return null;
|
|
20
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Priority for a pa11y violation, keyed on the WCAG technique rather than the
|
|
59
|
+
* conformance level.
|
|
60
|
+
*
|
|
61
|
+
* Level is not impact. Almost everything a WCAG2AA scan detects is Level A, so
|
|
62
|
+
* mapping A→P1 made all 17 pa11y findings on a real page P1 and left the tier
|
|
63
|
+
* doing no discrimination. Technique says what actually breaks: an unlabelled
|
|
64
|
+
* input stops a screen-reader user completing a form, while a duplicate id
|
|
65
|
+
* degrades an experience that still works.
|
|
66
|
+
*
|
|
67
|
+
* Keys are matched by the progressive-narrowing rule used for descriptions —
|
|
68
|
+
* "H91.InputText.Name" falls back to "H91" — and anything unlisted falls back
|
|
69
|
+
* to the conformance-level mapping.
|
|
70
|
+
*/
|
|
71
|
+
const TECHNIQUE_PRIORITY = {
|
|
72
|
+
// P1 — blocks a user from completing a task.
|
|
73
|
+
H91: "P1", // no accessible name on an interactive element
|
|
74
|
+
F68: "P1", // form control with no label
|
|
75
|
+
H44: "P1", // label not associated with its control
|
|
76
|
+
ARIA6: "P1", // empty/invalid aria-label
|
|
77
|
+
ARIA9: "P1", // aria-labelledby pointing at missing ids
|
|
78
|
+
H32: "P1", // form with no submit mechanism
|
|
79
|
+
H37: "P1", // img with no alt
|
|
80
|
+
F65: "P1", // img with neither alt nor title
|
|
81
|
+
H30: "P1", // link whose only content is an unlabelled image
|
|
82
|
+
H36: "P1", // image submit button with no alt
|
|
83
|
+
H53: "P1", // object with no fallback content
|
|
84
|
+
G202: "P1", // keyboard focus trapped or invisible
|
|
85
|
+
F54: "P1", // mouse-only interaction
|
|
86
|
+
F55: "P1", // focus stolen on hover
|
|
87
|
+
G18: "P1", // body text below 4.5:1 contrast
|
|
88
|
+
G145: "P1", // large text below 3:1 contrast
|
|
89
|
+
// P2 — degrades the experience without blocking it.
|
|
90
|
+
F77: "P2", // duplicate id
|
|
91
|
+
H93: "P2", // duplicate id (label/for variant)
|
|
92
|
+
G141: "P2", // heading levels skipped
|
|
93
|
+
H42: "P2", // text styled as a heading but not marked up
|
|
94
|
+
H25: "P2", // missing page title
|
|
95
|
+
F89: "P2", // empty page title
|
|
96
|
+
H57: "P2", // missing lang on <html>
|
|
97
|
+
H58: "P2", // unmarked language change
|
|
98
|
+
H64: "P2", // iframe with no title
|
|
99
|
+
H67: "P2", // decorative img carrying meaning
|
|
100
|
+
G94: "P2", // inaccurate alt text
|
|
101
|
+
"G1,G123,G124": "P2", // link to a named anchor that does not exist
|
|
102
|
+
H85: "P2", // ungrouped select options
|
|
103
|
+
G174: "P2", // no higher-contrast alternative
|
|
104
|
+
F40: "P2", // meta refresh with delay
|
|
105
|
+
F41: "P2", // auto-refresh with no user control
|
|
106
|
+
// P3 — advisory.
|
|
107
|
+
F92: "P3", // role="presentation" hiding real semantics
|
|
108
|
+
ARIA4: "P3", // role inappropriate for the content
|
|
109
|
+
H49: "P3", // emphasis conveyed only visually
|
|
110
|
+
F2: "P3", // meaning conveyed by formatting alone
|
|
111
|
+
G14: "P3", // meaning conveyed by colour alone
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Priority for an axe finding, from axe's own impact rating.
|
|
115
|
+
*
|
|
116
|
+
* Same principle as using Lighthouse's audit weight: the tool has already
|
|
117
|
+
* graded severity, so grade with it rather than inventing a parallel scheme.
|
|
118
|
+
* axe's ladder is critical > serious > moderate > minor.
|
|
119
|
+
*
|
|
120
|
+
* `needsFurtherReview` marks a rule that could not decide on its own and wants
|
|
121
|
+
* a human to confirm. Those are demoted one tier — a maybe should not gate a
|
|
122
|
+
* release with the same force as a definite.
|
|
123
|
+
*/
|
|
124
|
+
export function axeImpactToPriority(impact, needsReview) {
|
|
125
|
+
const base = impact === "critical" ? "P1"
|
|
126
|
+
: impact === "serious" ? "P2"
|
|
127
|
+
: impact === "moderate" ? "P3"
|
|
128
|
+
: impact === "minor" ? "P3"
|
|
129
|
+
: "P3"; // unrated: report, never block
|
|
130
|
+
if (!needsReview)
|
|
131
|
+
return base;
|
|
132
|
+
return base === "P1" ? "P2" : "P3";
|
|
133
|
+
}
|
|
134
|
+
export function a11yTechniqueToPriority(technique, wcagLevel) {
|
|
135
|
+
let key = technique;
|
|
136
|
+
while (key) {
|
|
137
|
+
const p = TECHNIQUE_PRIORITY[key];
|
|
138
|
+
if (p)
|
|
139
|
+
return p;
|
|
140
|
+
const lastDot = key.lastIndexOf(".");
|
|
141
|
+
if (lastDot < 0)
|
|
142
|
+
break;
|
|
143
|
+
key = key.slice(0, lastDot);
|
|
144
|
+
}
|
|
145
|
+
return wcagLevelToPriority(wcagLevel);
|
|
146
|
+
}
|
|
21
147
|
export function staticAnalysisToPriority(source, severity, category) {
|
|
22
148
|
// Security findings always win regardless of severity level.
|
|
23
149
|
if (source === "semgrep" && category === "security")
|
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"priorityMapper.js","sourceRoot":"","sources":["../../src/mappers/priorityMapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-run arithmetic.
|
|
3
|
+
*
|
|
4
|
+
* This module owns every number that describes the audit as a whole, because
|
|
5
|
+
* that is precisely where reading JSON by hand goes wrong. The manual Five
|
|
6
|
+
* Below report got "TBT fails on 22/22 pages" right and the direction of the
|
|
7
|
+
* CrUX CLS disagreement backwards, in the same document — the first is a count
|
|
8
|
+
* and the second is a comparison, and a human doing 26 files will eventually
|
|
9
|
+
* miss one. Everything here is computed so the report can quote rather than
|
|
10
|
+
* derive.
|
|
11
|
+
*
|
|
12
|
+
* It also owns the two redundancy rules, because both need the page set in
|
|
13
|
+
* view. A single-URL audit should still report every failing metric; the
|
|
14
|
+
* repetition only exists across a set.
|
|
15
|
+
*/
|
|
16
|
+
import type { Finding } from "../types.js";
|
|
17
|
+
import type { LabMetrics, ParsedCrux, WebVital } from "../utils/psiParser.js";
|
|
18
|
+
import type { MetricComparison } from "./labFieldComparator.js";
|
|
19
|
+
export interface RunResult {
|
|
20
|
+
template: string;
|
|
21
|
+
label: string;
|
|
22
|
+
url: string;
|
|
23
|
+
strategy: string;
|
|
24
|
+
runs: number;
|
|
25
|
+
scores: Record<string, number>;
|
|
26
|
+
lab: LabMetrics;
|
|
27
|
+
field: ParsedCrux | null;
|
|
28
|
+
comparisons: MetricComparison[];
|
|
29
|
+
findings: Finding[];
|
|
30
|
+
report_file: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Rule 2 — component suppression.
|
|
34
|
+
*
|
|
35
|
+
* FCP is a component of LCP: if the largest element paints late, the first one
|
|
36
|
+
* usually did too. Across the Five Below batch the two co-occurred on 17 of 25
|
|
37
|
+
* runs and FCP never once reached P1, so an FCP finding alongside an LCP
|
|
38
|
+
* finding is two tickets describing one defect. The FCP measurement is kept as
|
|
39
|
+
* evidence on the LCP finding rather than discarded.
|
|
40
|
+
*
|
|
41
|
+
* TTFB is deliberately *not* treated this way. It fired on one run out of 25,
|
|
42
|
+
* which is the signature of a metric carrying independent information.
|
|
43
|
+
*/
|
|
44
|
+
export declare function suppressComponentFindings(findings: Finding[]): Finding[];
|
|
45
|
+
export interface SystemicResult {
|
|
46
|
+
findings: Finding[];
|
|
47
|
+
/** Vitals whose per-page findings this replaces. */
|
|
48
|
+
collapsedVitals: WebVital[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Rule 1 — systemic collapse.
|
|
52
|
+
*
|
|
53
|
+
* A vital rated below "good" on 80%+ of runs is describing the site, not any
|
|
54
|
+
* one page. INP failed on 25 of 25 Five Below runs with a 2.1x spread and a
|
|
55
|
+
* 316 ms median: twenty-five findings that each say "fix this page" when the
|
|
56
|
+
* true statement is "this site's interaction handling is uniformly mediocre".
|
|
57
|
+
*
|
|
58
|
+
* The threshold is deliberately well above a simple majority — a vital failing
|
|
59
|
+
* on half the pages is discriminating between them, and that is information
|
|
60
|
+
* worth keeping per page.
|
|
61
|
+
*
|
|
62
|
+
* Priority is the worst observed, not an average: a vital that is poor
|
|
63
|
+
* everywhere is not less urgent for being ubiquitous.
|
|
64
|
+
*/
|
|
65
|
+
export declare function collapseSystemicFindings(runs: RunResult[]): SystemicResult;
|
|
66
|
+
export interface PsiAggregate {
|
|
67
|
+
run_count: number;
|
|
68
|
+
pages: number;
|
|
69
|
+
captured_at: string;
|
|
70
|
+
by_strategy: Record<string, {
|
|
71
|
+
performance: {
|
|
72
|
+
mean: number;
|
|
73
|
+
min: number;
|
|
74
|
+
max: number;
|
|
75
|
+
};
|
|
76
|
+
}>;
|
|
77
|
+
by_template: Array<{
|
|
78
|
+
template: string;
|
|
79
|
+
label: string;
|
|
80
|
+
sampled: number;
|
|
81
|
+
performance_mean: Record<string, number>;
|
|
82
|
+
worst_url: string;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Lab metrics failing across the audited runs. Renamed from
|
|
86
|
+
* `universal_failures`, which was a lie on any site where a metric failed on
|
|
87
|
+
* some pages but not others — it reported a 25% failure rate under a name the
|
|
88
|
+
* report spec told the agent to treat as a headline finding. `universal` now
|
|
89
|
+
* says explicitly whether the "fails on essentially every page" claim holds.
|
|
90
|
+
*/
|
|
91
|
+
lab_metric_failures: Array<{
|
|
92
|
+
metric: string;
|
|
93
|
+
label: string;
|
|
94
|
+
failing: number;
|
|
95
|
+
of: number;
|
|
96
|
+
pct: number;
|
|
97
|
+
universal: boolean;
|
|
98
|
+
range: string;
|
|
99
|
+
threshold: string;
|
|
100
|
+
}>;
|
|
101
|
+
cwv_verdicts: {
|
|
102
|
+
pass: number;
|
|
103
|
+
needs_improvement: number;
|
|
104
|
+
fail: number;
|
|
105
|
+
};
|
|
106
|
+
lab_vs_field_summary: {
|
|
107
|
+
worse_in_field: Array<{
|
|
108
|
+
metric: string;
|
|
109
|
+
runs: number;
|
|
110
|
+
}>;
|
|
111
|
+
worse_in_lab: Array<{
|
|
112
|
+
metric: string;
|
|
113
|
+
runs: number;
|
|
114
|
+
}>;
|
|
115
|
+
confirmed: Array<{
|
|
116
|
+
metric: string;
|
|
117
|
+
runs: number;
|
|
118
|
+
}>;
|
|
119
|
+
no_field_data: number;
|
|
120
|
+
};
|
|
121
|
+
outliers: Array<{
|
|
122
|
+
url: string;
|
|
123
|
+
strategy: string;
|
|
124
|
+
metric: string;
|
|
125
|
+
value: string;
|
|
126
|
+
vs_median_multiple: number;
|
|
127
|
+
confidence: string;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
export declare function aggregate(runs: RunResult[]): PsiAggregate;
|