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.
Files changed (65) hide show
  1. package/README.md +85 -335
  2. package/dist/index.js +27 -4
  3. package/dist/index.js.map +1 -1
  4. package/dist/mappers/a11yDedupe.d.ts +25 -0
  5. package/dist/mappers/a11yDedupe.js +95 -0
  6. package/dist/mappers/a11yDedupe.js.map +1 -0
  7. package/dist/mappers/compositeScore.d.ts +23 -0
  8. package/dist/mappers/compositeScore.js +107 -0
  9. package/dist/mappers/compositeScore.js.map +1 -0
  10. package/dist/mappers/correlator.js +94 -33
  11. package/dist/mappers/correlator.js.map +1 -1
  12. package/dist/mappers/defectFormatter.js +129 -15
  13. package/dist/mappers/defectFormatter.js.map +1 -1
  14. package/dist/mappers/labFieldComparator.d.ts +62 -0
  15. package/dist/mappers/labFieldComparator.js +134 -0
  16. package/dist/mappers/labFieldComparator.js.map +1 -0
  17. package/dist/mappers/priorityMapper.d.ts +38 -0
  18. package/dist/mappers/priorityMapper.js +126 -0
  19. package/dist/mappers/priorityMapper.js.map +1 -1
  20. package/dist/mappers/psiAggregator.d.ts +130 -0
  21. package/dist/mappers/psiAggregator.js +293 -0
  22. package/dist/mappers/psiAggregator.js.map +1 -0
  23. package/dist/mappers/webVitalsMapper.d.ts +52 -0
  24. package/dist/mappers/webVitalsMapper.js +131 -0
  25. package/dist/mappers/webVitalsMapper.js.map +1 -0
  26. package/dist/tools/accessibility.js +72 -31
  27. package/dist/tools/accessibility.js.map +1 -1
  28. package/dist/tools/lighthouse.d.ts +14 -0
  29. package/dist/tools/lighthouse.js +134 -34
  30. package/dist/tools/lighthouse.js.map +1 -1
  31. package/dist/tools/performanceAudit.d.ts +2 -0
  32. package/dist/tools/performanceAudit.js +446 -0
  33. package/dist/tools/performanceAudit.js.map +1 -0
  34. package/dist/tools/performanceAuditPlan.d.ts +2 -0
  35. package/dist/tools/performanceAuditPlan.js +438 -0
  36. package/dist/tools/performanceAuditPlan.js.map +1 -0
  37. package/dist/tools/qaGate.js +109 -34
  38. package/dist/tools/qaGate.js.map +1 -1
  39. package/dist/utils/csvReader.d.ts +20 -0
  40. package/dist/utils/csvReader.js +172 -0
  41. package/dist/utils/csvReader.js.map +1 -0
  42. package/dist/utils/httpClient.d.ts +84 -0
  43. package/dist/utils/httpClient.js +171 -0
  44. package/dist/utils/httpClient.js.map +1 -0
  45. package/dist/utils/outputParsers.d.ts +25 -0
  46. package/dist/utils/outputParsers.js +42 -0
  47. package/dist/utils/outputParsers.js.map +1 -1
  48. package/dist/utils/psiAuth.d.ts +26 -0
  49. package/dist/utils/psiAuth.js +36 -0
  50. package/dist/utils/psiAuth.js.map +1 -0
  51. package/dist/utils/psiParser.d.ts +124 -0
  52. package/dist/utils/psiParser.js +200 -0
  53. package/dist/utils/psiParser.js.map +1 -0
  54. package/dist/utils/publicUrl.d.ts +17 -0
  55. package/dist/utils/publicUrl.js +115 -0
  56. package/dist/utils/publicUrl.js.map +1 -0
  57. package/dist/utils/sitemapReader.d.ts +27 -0
  58. package/dist/utils/sitemapReader.js +272 -0
  59. package/dist/utils/sitemapReader.js.map +1 -0
  60. package/dist/utils/urlClassifier.d.ts +45 -0
  61. package/dist/utils/urlClassifier.js +267 -0
  62. package/dist/utils/urlClassifier.js.map +1 -0
  63. package/docs/manual.md +558 -0
  64. package/docs/psi-report-spec.md +174 -0
  65. package/package.json +14 -4
@@ -0,0 +1,171 @@
1
+ /**
2
+ * HTTP counterpart to shellRunner.
3
+ *
4
+ * Every tool before this one reached the outside world by spawning a process,
5
+ * so `runShell` was the single choke point for timeout, error and duration
6
+ * handling. PSI is an HTTP API, and bending shellRunner around `curl` would
7
+ * trade a typed response for string parsing. This mirrors shellRunner's
8
+ * contract instead: never throws for a reachable-but-unhappy endpoint, always
9
+ * reports duration, and leaves the decision about what counts as failure to
10
+ * the caller.
11
+ *
12
+ * Node 18+ ships global fetch, so this adds no dependency.
13
+ */
14
+ /**
15
+ * Sites behind a bot wall reject the default Node fetch agent outright, which
16
+ * turns a healthy site into a 403 and a sitemap into a "not found". Naming
17
+ * ourselves honestly and looking like a browser gets through most of them;
18
+ * a caller can override it via `headers`.
19
+ */
20
+ const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
21
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 nfunc-mcp/qa-audit";
22
+ /** Query parameters whose values are stripped before a URL is quoted anywhere. */
23
+ const SENSITIVE_PARAMS = new Set(["key", "api_key", "apikey", "token", "access_token"]);
24
+ /**
25
+ * A URL safe to put in an error message.
26
+ *
27
+ * The PSI request URL carries the API key in the query string, so any error
28
+ * that interpolates the URL leaks the key into the tool response — and from
29
+ * there into the conversation transcript. Redacting at the point of formatting
30
+ * rather than asking every call site to remember is the only version of this
31
+ * that stays correct.
32
+ */
33
+ export function sanitizeUrl(url) {
34
+ try {
35
+ const u = new URL(url);
36
+ for (const param of u.searchParams.keys()) {
37
+ if (SENSITIVE_PARAMS.has(param.toLowerCase())) {
38
+ u.searchParams.set(param, "REDACTED");
39
+ }
40
+ }
41
+ return u.toString();
42
+ }
43
+ catch {
44
+ return "[unparseable url]";
45
+ }
46
+ }
47
+ function redactAll(message, secrets) {
48
+ let out = message;
49
+ for (const secret of secrets) {
50
+ // A short "secret" would redact half the message; a real key is far longer.
51
+ if (secret && secret.length >= 8)
52
+ out = out.split(secret).join("REDACTED");
53
+ }
54
+ return out;
55
+ }
56
+ /** Retry only what a retry can plausibly fix. A 400 will be a 400 next time too. */
57
+ function isRetryable(status) {
58
+ return status === 408 || status === 429 || status >= 500;
59
+ }
60
+ /**
61
+ * `Retry-After` is either a delay in seconds or an HTTP date. Google sends
62
+ * seconds on 429s, but the date form is legal and cheap to support.
63
+ */
64
+ function retryAfterMs(header) {
65
+ if (!header)
66
+ return null;
67
+ const seconds = Number(header);
68
+ if (Number.isFinite(seconds) && seconds >= 0)
69
+ return seconds * 1000;
70
+ const date = Date.parse(header);
71
+ if (!Number.isNaN(date))
72
+ return Math.max(0, date - Date.now());
73
+ return null;
74
+ }
75
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
76
+ export async function httpGet(url, options = {}) {
77
+ const { timeoutMs = 60_000, retries = 2, retryDelayMs = 2_000, headers, raw = false, totalBudgetMs, retryOnTimeout = false, redact = [], } = options;
78
+ const start = Date.now();
79
+ const safeUrl = sanitizeUrl(url);
80
+ let lastError = "";
81
+ let lastStatus = 0;
82
+ const deadline = totalBudgetMs === undefined ? null : start + totalBudgetMs;
83
+ const remainingMs = () => (deadline === null ? Infinity : deadline - Date.now());
84
+ for (let attempt = 1; attempt <= retries + 1; attempt++) {
85
+ const budgetLeft = remainingMs();
86
+ if (budgetLeft <= 0) {
87
+ lastError = lastError || `Request to ${safeUrl} ran out of time budget`;
88
+ break;
89
+ }
90
+ try {
91
+ const response = await fetch(url, {
92
+ headers: { "user-agent": DEFAULT_USER_AGENT, ...headers },
93
+ signal: AbortSignal.timeout(Math.min(timeoutMs, budgetLeft)),
94
+ });
95
+ lastStatus = response.status;
96
+ if (response.ok) {
97
+ const bytes = new Uint8Array(await response.arrayBuffer());
98
+ return {
99
+ ok: true,
100
+ status: response.status,
101
+ body: raw ? "" : new TextDecoder().decode(bytes),
102
+ ...(raw ? { bytes } : {}),
103
+ durationMs: Date.now() - start,
104
+ attempts: attempt,
105
+ };
106
+ }
107
+ // Read the body before deciding — a non-2xx from PSI carries a JSON
108
+ // error object that says far more than the status code does.
109
+ const body = await response.text().catch(() => "");
110
+ lastError = `HTTP ${response.status} from ${safeUrl}${body ? `: ${body.slice(0, 300)}` : ""}`;
111
+ if (!isRetryable(response.status) || attempt > retries) {
112
+ return {
113
+ ok: false,
114
+ status: response.status,
115
+ body,
116
+ durationMs: Date.now() - start,
117
+ attempts: attempt,
118
+ error: redactAll(lastError, redact),
119
+ };
120
+ }
121
+ const serverAsked = retryAfterMs(response.headers.get("retry-after"));
122
+ const backoff = Math.min(serverAsked ?? retryDelayMs * 2 ** (attempt - 1), Math.max(0, remainingMs()));
123
+ await sleep(backoff);
124
+ }
125
+ catch (err) {
126
+ // Timeouts and network failures land here. AbortSignal.timeout raises a
127
+ // TimeoutError, which reads as an unexplained abort unless named.
128
+ const e = err;
129
+ const timedOut = e.name === "TimeoutError" || e.name === "AbortError";
130
+ lastStatus = 0;
131
+ lastError = timedOut
132
+ ? `Request to ${safeUrl} timed out after ${timeoutMs} ms`
133
+ : `Request to ${safeUrl} failed: ${e.message ?? "unknown network error"}`;
134
+ if (timedOut && !retryOnTimeout)
135
+ break;
136
+ if (attempt > retries)
137
+ break;
138
+ await sleep(Math.min(retryDelayMs * 2 ** (attempt - 1), Math.max(0, remainingMs())));
139
+ }
140
+ }
141
+ return {
142
+ ok: false,
143
+ status: lastStatus,
144
+ body: "",
145
+ durationMs: Date.now() - start,
146
+ attempts: retries + 1,
147
+ error: redactAll(lastError, redact),
148
+ };
149
+ }
150
+ /**
151
+ * `httpGet` plus JSON parsing, with malformed JSON demoted to `ok: false`
152
+ * rather than a thrown SyntaxError. A 200 carrying truncated JSON is a failed
153
+ * request as far as every caller is concerned.
154
+ */
155
+ export async function httpGetJson(url, options = {}) {
156
+ const result = await httpGet(url, options);
157
+ if (!result.ok)
158
+ return { ...result, data: null };
159
+ try {
160
+ return { ...result, data: JSON.parse(result.body) };
161
+ }
162
+ catch (err) {
163
+ return {
164
+ ...result,
165
+ ok: false,
166
+ data: null,
167
+ error: redactAll(`Response from ${sanitizeUrl(url)} was not valid JSON: ${err.message}`, options.redact ?? []),
168
+ };
169
+ }
170
+ }
171
+ //# sourceMappingURL=httpClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpClient.js","sourceRoot":"","sources":["../../src/utils/httpClient.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,17 @@ export interface LighthouseAuditRef {
6
6
  displayValue: string;
7
7
  numericValue?: number;
8
8
  numericUnit?: string;
9
+ /**
10
+ * The audit's weight inside its Lighthouse category, taken from
11
+ * categories[].auditRefs[]. This is Lighthouse's own statement of how much
12
+ * the audit matters: weight 0 means it is a diagnostic that contributes
13
+ * nothing to the category score. Undefined when the audit belongs to no
14
+ * category (rare — treat as unknown, not as zero).
15
+ */
16
+ weight?: number;
17
+ /** Highest-weighted category the audit belongs to, for traceability. */
18
+ category?: string;
19
+ scoreDisplayMode?: string;
9
20
  }
10
21
  export interface ParsedLighthouse {
11
22
  categoryScores: Record<string, number>;
@@ -15,6 +26,8 @@ export interface ParsedLighthouse {
15
26
  }
16
27
  export declare function parseLighthouseJSON(rawJson: string): ParsedLighthouse;
17
28
  export type WcagLevel = "A" | "AA" | "AAA" | "unknown";
29
+ export type A11yRunner = "htmlcs" | "axe";
30
+ export type AxeImpact = "critical" | "serious" | "moderate" | "minor";
18
31
  export interface Pa11yViolation {
19
32
  code: string;
20
33
  technique: string;
@@ -24,6 +37,18 @@ export interface Pa11yViolation {
24
37
  type: "error" | "warning" | "notice";
25
38
  wcagLevel: WcagLevel;
26
39
  criterion: string | null;
40
+ /**
41
+ * Which engine produced this. The two emit completely different `code`
42
+ * shapes — htmlcs gives WCAG technique paths
43
+ * ("WCAG2AA.Principle1.Guideline1_1.1_1_1.H37"), axe gives bare rule ids
44
+ * ("image-alt") — so every consumer has to branch on this rather than
45
+ * pattern-matching the code.
46
+ */
47
+ runner: A11yRunner;
48
+ /** axe only: axe's own severity rating, its equivalent of Lighthouse weight. */
49
+ impact?: AxeImpact;
50
+ /** axe only: the rule could not decide alone and wants human confirmation. */
51
+ needsReview?: boolean;
27
52
  }
28
53
  export interface ParsedPa11y {
29
54
  violationCount: number;
@@ -1,10 +1,21 @@
1
1
  export function parseLighthouseJSON(rawJson) {
2
2
  const lhr = JSON.parse(rawJson);
3
3
  const categoryScores = {};
4
+ // An audit can appear in more than one category; keep the highest weight,
5
+ // since that is the strongest claim Lighthouse makes about its importance.
6
+ const auditWeights = {};
4
7
  for (const [key, cat] of Object.entries(lhr.categories ?? {})) {
5
8
  if (typeof cat?.score === "number") {
6
9
  categoryScores[key] = Math.round(cat.score * 100);
7
10
  }
11
+ for (const ref of cat?.auditRefs ?? []) {
12
+ if (!ref.id || typeof ref.weight !== "number")
13
+ continue;
14
+ const prev = auditWeights[ref.id];
15
+ if (!prev || ref.weight > prev.weight) {
16
+ auditWeights[ref.id] = { weight: ref.weight, category: key };
17
+ }
18
+ }
8
19
  }
9
20
  const scoreValues = Object.values(categoryScores);
10
21
  const totalScore = scoreValues.length > 0
@@ -21,6 +32,7 @@ export function parseLighthouseJSON(rawJson) {
21
32
  const scorePct = Math.round(audit.score * 100);
22
33
  if (scorePct >= 90)
23
34
  continue;
35
+ const weightRef = auditWeights[id];
24
36
  failedAudits.push({
25
37
  id,
26
38
  title: audit.title ?? id,
@@ -29,6 +41,9 @@ export function parseLighthouseJSON(rawJson) {
29
41
  displayValue: audit.displayValue ?? "",
30
42
  numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined,
31
43
  numericUnit: audit.numericUnit,
44
+ weight: weightRef?.weight,
45
+ category: weightRef?.category,
46
+ scoreDisplayMode: audit.scoreDisplayMode,
32
47
  });
33
48
  }
34
49
  return { categoryScores, totalScore, ttfbMs, failedAudits };
@@ -108,6 +123,9 @@ function deriveWcagLevel(code) {
108
123
  return { level: "A", criterion };
109
124
  return { level: "unknown", criterion };
110
125
  }
126
+ function isAxeImpact(v) {
127
+ return v === "critical" || v === "serious" || v === "moderate" || v === "minor";
128
+ }
111
129
  function extractTechnique(code) {
112
130
  // pa11y/HTMLCS code shape: WCAG2AA.PrincipleN.GuidelineN_N.N_N_N.<technique>[.<variant>...]
113
131
  // The technique starts at the segment immediately after the criterion (e.g. "1_1_1").
@@ -167,6 +185,29 @@ export function parsePa11yJSON(rawJson) {
167
185
  for (const issue of issues) {
168
186
  const code = issue.code ?? "";
169
187
  const type = (issue.type ?? "error");
188
+ const runner = issue.runner === "axe" ? "axe" : "htmlcs";
189
+ if (runner === "axe") {
190
+ // axe codes are bare rule ids with no WCAG path to parse, so the
191
+ // criterion-derivation below cannot classify them — it would return
192
+ // level "unknown", which the priority mapper drops. Severity comes from
193
+ // axe's own impact rating instead. (Skipping this branch silently
194
+ // discarded every axe finding.)
195
+ const impact = issue.runnerExtras?.impact;
196
+ violations.push({
197
+ code,
198
+ technique: code,
199
+ message: issue.message ?? "",
200
+ selector: issue.selector ?? "",
201
+ context: issue.context ?? "",
202
+ type,
203
+ wcagLevel: "unknown",
204
+ criterion: null,
205
+ runner,
206
+ impact: isAxeImpact(impact) ? impact : undefined,
207
+ needsReview: issue.runnerExtras?.needsFurtherReview === true,
208
+ });
209
+ continue;
210
+ }
170
211
  const { level, criterion } = deriveWcagLevel(code);
171
212
  violations.push({
172
213
  code,
@@ -177,6 +218,7 @@ export function parsePa11yJSON(rawJson) {
177
218
  type,
178
219
  wcagLevel: level,
179
220
  criterion,
221
+ runner,
180
222
  });
181
223
  }
182
224
  return { violationCount: violations.length, violations };
@@ -1 +1 @@
1
- {"version":3,"file":"outputParsers.js","sourceRoot":"","sources":["../../src/utils/outputParsers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outputParsers.js","sourceRoot":"","sources":["../../src/utils/outputParsers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * PSI API key resolution.
3
+ *
4
+ * Precedence: explicit tool input → PAGESPEED_API_KEY → keyless.
5
+ *
6
+ * The preferred setup is the `env` block in the MCP client config, because the
7
+ * client launches this server and we do not control its flags — `--env-file`
8
+ * is not available to us. An explicit input is supported for people who want
9
+ * to paste a key once, but it is the worst option: it lands in the
10
+ * conversation transcript and in any client-side logging, so it is never
11
+ * echoed back and every error that touches a request URL is redacted.
12
+ */
13
+ export type KeySource = "input" | "env" | "none";
14
+ export interface ResolvedKey {
15
+ key: string | null;
16
+ source: KeySource;
17
+ }
18
+ export declare const KEY_ENV_VAR = "PAGESPEED_API_KEY";
19
+ /**
20
+ * Keyless PSI is rate-limited hard enough that a batch will 429 partway
21
+ * through, leaving a half-finished audit and a confusing error. Refusing up
22
+ * front with instructions is a better failure than discovering it at run 12.
23
+ */
24
+ export declare const KEYLESS_RUN_CAP = 4;
25
+ export declare function resolveApiKey(explicit?: string): ResolvedKey;
26
+ export declare function keylessWarning(runs: number): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * PSI API key resolution.
3
+ *
4
+ * Precedence: explicit tool input → PAGESPEED_API_KEY → keyless.
5
+ *
6
+ * The preferred setup is the `env` block in the MCP client config, because the
7
+ * client launches this server and we do not control its flags — `--env-file`
8
+ * is not available to us. An explicit input is supported for people who want
9
+ * to paste a key once, but it is the worst option: it lands in the
10
+ * conversation transcript and in any client-side logging, so it is never
11
+ * echoed back and every error that touches a request URL is redacted.
12
+ */
13
+ export const KEY_ENV_VAR = "PAGESPEED_API_KEY";
14
+ /**
15
+ * Keyless PSI is rate-limited hard enough that a batch will 429 partway
16
+ * through, leaving a half-finished audit and a confusing error. Refusing up
17
+ * front with instructions is a better failure than discovering it at run 12.
18
+ */
19
+ export const KEYLESS_RUN_CAP = 4;
20
+ export function resolveApiKey(explicit) {
21
+ const trimmed = explicit?.trim();
22
+ if (trimmed)
23
+ return { key: trimmed, source: "input" };
24
+ const fromEnv = process.env[KEY_ENV_VAR]?.trim();
25
+ if (fromEnv)
26
+ return { key: fromEnv, source: "env" };
27
+ return { key: null, source: "none" };
28
+ }
29
+ export function keylessWarning(runs) {
30
+ return (`No ${KEY_ENV_VAR} set — running unauthenticated against a shared, ` +
31
+ `heavily rate-limited quota. This is fine for ${KEYLESS_RUN_CAP} runs or ` +
32
+ `fewer; ${runs} runs will almost certainly hit HTTP 429 partway through. ` +
33
+ `Get a key from the Google Cloud console (enable the PageSpeed Insights ` +
34
+ `API) and set ${KEY_ENV_VAR} in your MCP client config.`);
35
+ }
36
+ //# sourceMappingURL=psiAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"psiAuth.js","sourceRoot":"","sources":["../../src/utils/psiAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAE/C;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AAEjC,MAAM,UAAU,aAAa,CAAC,QAAiB;IAC7C,MAAM,OAAO,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjC,IAAI,OAAO;QAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAEtD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC;IACjD,IAAI,OAAO;QAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAEpD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,CACL,MAAM,WAAW,mDAAmD;QACpE,gDAAgD,eAAe,WAAW;QAC1E,UAAU,IAAI,4DAA4D;QAC1E,yEAAyE;QACzE,gBAAgB,WAAW,6BAA6B,CACzD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * PageSpeed Insights response parsing.
3
+ *
4
+ * One PSI call returns two unrelated datasets: `lighthouseResult` (a lab run
5
+ * on Google's hardware) and `loadingExperience` (CrUX field data from real
6
+ * Chrome users). They are measured differently, they disagree routinely, and
7
+ * conflating them produces confidently wrong reports — so they stay separate
8
+ * all the way through this module and only meet in the comparator.
9
+ *
10
+ * The lab half is the same LHR schema `parseLighthouseJSON` already handles
11
+ * and is delegated to it unchanged. Everything here is about the field half
12
+ * and about pulling numeric lab metrics that survive aggregation.
13
+ */
14
+ import { type ParsedLighthouse } from "./outputParsers.js";
15
+ export type CruxCategory = "FAST" | "AVERAGE" | "SLOW" | "NONE";
16
+ /** The five field metrics PSI reports. `fid` is deliberately absent — see below. */
17
+ export type WebVital = "lcp" | "inp" | "cls" | "fcp" | "ttfb";
18
+ export type FieldSource = "url" | "origin";
19
+ export interface CruxMetric {
20
+ /**
21
+ * 75th percentile across the collection window. Milliseconds for every
22
+ * metric except `cls`, which is unitless.
23
+ */
24
+ p75: number;
25
+ category: CruxCategory;
26
+ /** Share of real users in each bucket. Sums to ~1. */
27
+ distribution: {
28
+ good: number;
29
+ needsImprovement: number;
30
+ poor: number;
31
+ };
32
+ /**
33
+ * Whether this specific metric came from the URL or from origin-wide data.
34
+ * Per metric, not per response — see the note on `parseCrux`.
35
+ */
36
+ source: FieldSource;
37
+ }
38
+ export interface ParsedCrux {
39
+ /**
40
+ * Always 28. PSI does not return the collection window, but CrUX in PSI is
41
+ * defined as a trailing 28-day aggregate, so this is a documented constant
42
+ * rather than a parsed value. Stated explicitly because reports must not
43
+ * describe field data as real-time.
44
+ */
45
+ collectionPeriodDays: 28;
46
+ overallCategory: CruxCategory | null;
47
+ /** Which source the majority of metrics came from, for a one-line summary. */
48
+ primarySource: FieldSource;
49
+ metrics: Partial<Record<WebVital, CruxMetric>>;
50
+ }
51
+ export interface LabMetrics {
52
+ lcpMs: number | null;
53
+ fcpMs: number | null;
54
+ cls: number | null;
55
+ tbtMs: number | null;
56
+ speedIndexMs: number | null;
57
+ ttiMs: number | null;
58
+ ttfbMs: number | null;
59
+ }
60
+ export interface ParsedPsi {
61
+ requestedUrl: string;
62
+ finalUrl: string;
63
+ strategy: string;
64
+ fetchTime: string | null;
65
+ lighthouseVersion: string | null;
66
+ scores: Record<string, number>;
67
+ lab: LabMetrics;
68
+ /** Null when the URL has too little real-user traffic and origin fallback is off or empty. */
69
+ field: ParsedCrux | null;
70
+ lighthouse: ParsedLighthouse;
71
+ /** Lighthouse audits that failed, for the existing defect formatter. */
72
+ runWarnings: string[];
73
+ }
74
+ /** PSI returned a 200 whose Lighthouse run did not actually complete. */
75
+ export declare class PsiRuntimeError extends Error {
76
+ readonly code: string;
77
+ constructor(code: string, message: string);
78
+ }
79
+ interface RawCruxMetric {
80
+ percentile?: number;
81
+ category?: string;
82
+ distributions?: Array<{
83
+ min?: number;
84
+ max?: number;
85
+ proportion?: number;
86
+ }>;
87
+ }
88
+ interface RawLoadingExperience {
89
+ id?: string;
90
+ overall_category?: string;
91
+ metrics?: Record<string, RawCruxMetric>;
92
+ }
93
+ /**
94
+ * Merge URL-level and origin-level CrUX into one metric set.
95
+ *
96
+ * Field availability is **per metric, not per URL** — a page can have enough
97
+ * traffic for CrUX to report CLS but not LCP. A real response in the Five
98
+ * Below batch carried two of the five metrics at URL level while the origin
99
+ * carried all five, which is why the manual audit shows "No field data" for
100
+ * one metric and a category for another on the same row.
101
+ *
102
+ * So the fallback runs per metric, and each metric records where it came from.
103
+ * A single response-level `field_source` flag would have to either discard the
104
+ * URL-level metrics that do exist or silently label origin data as page data,
105
+ * and origin CrUX for a homepage says nothing about a checkout page.
106
+ */
107
+ export declare function parseCrux(urlLevel: RawLoadingExperience | undefined, originLevel: RawLoadingExperience | undefined, originFallback: boolean): ParsedCrux | null;
108
+ export interface ParsePsiOptions {
109
+ strategy: string;
110
+ /** Fall back to origin-wide CrUX per metric when URL-level data is absent. */
111
+ originFallback?: boolean;
112
+ }
113
+ /**
114
+ * Parse a full PSI v5 response.
115
+ *
116
+ * Throws `PsiRuntimeError` when Lighthouse itself failed inside a 200
117
+ * response. PSI does this routinely — a page that times out or refuses the
118
+ * fetch comes back as HTTP 200 with `lighthouseResult.runtimeError` set and
119
+ * every score null. Parsing it anyway records a real page as scoring zero
120
+ * across the board, which is worse than a failed run, because a zero looks
121
+ * like data. Callers should treat this as retryable.
122
+ */
123
+ export declare function parsePsiResponse(rawJson: string, options: ParsePsiOptions): ParsedPsi;
124
+ export {};