@pseolint/core 0.3.1 → 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 +49 -1
- package/dist/ai/triage.d.ts.map +1 -1
- package/dist/ai/triage.js +8 -1
- package/dist/ai/triage.js.map +1 -1
- package/dist/auditor.d.ts.map +1 -1
- package/dist/auditor.js +495 -130
- package/dist/auditor.js.map +1 -1
- package/dist/backpressure.d.ts +68 -0
- package/dist/backpressure.d.ts.map +1 -0
- package/dist/backpressure.js +81 -0
- package/dist/backpressure.js.map +1 -0
- package/dist/cache.d.ts +73 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +258 -19
- package/dist/cache.js.map +1 -1
- package/dist/enrich-findings.d.ts.map +1 -1
- package/dist/enrich-findings.js +1 -14
- package/dist/enrich-findings.js.map +1 -1
- package/dist/fetch-observer.d.ts +97 -0
- package/dist/fetch-observer.d.ts.map +1 -0
- package/dist/fetch-observer.js +124 -0
- package/dist/fetch-observer.js.map +1 -0
- package/dist/formatters/console.d.ts +7 -9
- package/dist/formatters/console.d.ts.map +1 -1
- package/dist/formatters/console.js +218 -254
- package/dist/formatters/console.js.map +1 -1
- package/dist/formatters/html.d.ts +5 -1
- package/dist/formatters/html.d.ts.map +1 -1
- package/dist/formatters/html.js +352 -570
- package/dist/formatters/html.js.map +1 -1
- package/dist/formatters/index.d.ts +4 -1
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +1 -1
- package/dist/formatters/index.js.map +1 -1
- package/dist/formatters/json.d.ts +11 -1
- package/dist/formatters/json.d.ts.map +1 -1
- package/dist/formatters/json.js +5 -1
- package/dist/formatters/json.js.map +1 -1
- package/dist/formatters/markdown.d.ts +7 -1
- package/dist/formatters/markdown.d.ts.map +1 -1
- package/dist/formatters/markdown.js +77 -70
- package/dist/formatters/markdown.js.map +1 -1
- package/dist/index.d.ts +13 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/rule-references.d.ts.map +1 -1
- package/dist/rule-references.js +0 -6
- package/dist/rule-references.js.map +1 -1
- package/dist/rules/content/unique-value.d.ts.map +1 -1
- package/dist/rules/content/unique-value.js +1 -0
- package/dist/rules/content/unique-value.js.map +1 -1
- package/dist/rules/scope.d.ts.map +1 -1
- package/dist/rules/scope.js +6 -14
- package/dist/rules/scope.js.map +1 -1
- package/dist/rules/tech/robots-sitemap-presence.d.ts +9 -1
- package/dist/rules/tech/robots-sitemap-presence.d.ts.map +1 -1
- package/dist/rules/tech/robots-sitemap-presence.js +14 -5
- package/dist/rules/tech/robots-sitemap-presence.js.map +1 -1
- package/dist/safe-mode-preset.d.ts +27 -0
- package/dist/safe-mode-preset.d.ts.map +1 -0
- package/dist/safe-mode-preset.js +54 -0
- package/dist/safe-mode-preset.js.map +1 -0
- package/dist/site-classifier.d.ts +83 -0
- package/dist/site-classifier.d.ts.map +1 -0
- package/dist/site-classifier.js +205 -0
- package/dist/site-classifier.js.map +1 -0
- package/dist/ssrf-guard.d.ts +96 -0
- package/dist/ssrf-guard.d.ts.map +1 -0
- package/dist/ssrf-guard.js +268 -0
- package/dist/ssrf-guard.js.map +1 -0
- package/dist/types.d.ts +171 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/formatters/html.js
CHANGED
|
@@ -1,60 +1,28 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
const VERDICT_LABEL = {
|
|
2
|
+
ready: "Ready",
|
|
3
|
+
caution: "Caution",
|
|
4
|
+
concerning: "Concerning",
|
|
3
5
|
critical: "Critical",
|
|
4
|
-
error: "Error",
|
|
5
|
-
warning: "Warning",
|
|
6
|
-
info: "Info",
|
|
7
6
|
};
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const VERDICT_TONE = {
|
|
8
|
+
ready: "success",
|
|
9
|
+
caution: "warning",
|
|
10
|
+
concerning: "destructive",
|
|
11
|
+
critical: "critical",
|
|
12
|
+
};
|
|
13
|
+
const GRADE_TONE = {
|
|
14
|
+
A: "success",
|
|
15
|
+
B: "success",
|
|
16
|
+
C: "warning",
|
|
17
|
+
D: "destructive",
|
|
18
|
+
F: "critical",
|
|
19
|
+
};
|
|
20
|
+
const CATEGORY_LABEL = {
|
|
21
|
+
integrity: "Integrity",
|
|
22
|
+
discoverability: "Discoverability",
|
|
23
|
+
citation: "Citation",
|
|
24
|
+
data: "Data",
|
|
13
25
|
};
|
|
14
|
-
function severityTone(severity) {
|
|
15
|
-
switch (severity) {
|
|
16
|
-
case "critical":
|
|
17
|
-
case "error":
|
|
18
|
-
return "destructive";
|
|
19
|
-
case "warning":
|
|
20
|
-
return "warning";
|
|
21
|
-
case "info":
|
|
22
|
-
return "muted";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function scoreTone(score) {
|
|
26
|
-
if (score <= 40)
|
|
27
|
-
return "success";
|
|
28
|
-
if (score <= 69)
|
|
29
|
-
return "warning";
|
|
30
|
-
return "destructive";
|
|
31
|
-
}
|
|
32
|
-
function scoreVerdict(score) {
|
|
33
|
-
if (score <= 20)
|
|
34
|
-
return "Clean run";
|
|
35
|
-
if (score <= 40)
|
|
36
|
-
return "Low risk";
|
|
37
|
-
if (score <= 69)
|
|
38
|
-
return "Watch list";
|
|
39
|
-
if (score <= 84)
|
|
40
|
-
return "Elevated risk";
|
|
41
|
-
return "Doorway garden";
|
|
42
|
-
}
|
|
43
|
-
function categoryTone(pct) {
|
|
44
|
-
if (pct <= 40)
|
|
45
|
-
return "success";
|
|
46
|
-
if (pct <= 69)
|
|
47
|
-
return "warning";
|
|
48
|
-
return "destructive";
|
|
49
|
-
}
|
|
50
|
-
function effortLabel(effort) {
|
|
51
|
-
switch (effort) {
|
|
52
|
-
case "quick": return "quick fix";
|
|
53
|
-
case "moderate": return "moderate";
|
|
54
|
-
case "structural": return "structural";
|
|
55
|
-
default: return effort;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
26
|
function escapeHtml(text) {
|
|
59
27
|
return text
|
|
60
28
|
.replace(/&/g, "&")
|
|
@@ -64,542 +32,356 @@ function escapeHtml(text) {
|
|
|
64
32
|
}
|
|
65
33
|
function shortenUrl(url) {
|
|
66
34
|
try {
|
|
67
|
-
return new URL(url).pathname;
|
|
35
|
+
return new URL(url).pathname || "/";
|
|
68
36
|
}
|
|
69
37
|
catch {
|
|
70
38
|
return url;
|
|
71
39
|
}
|
|
72
40
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
data: "Data binding",
|
|
80
|
-
schema: "Structured data",
|
|
81
|
-
cannibal: "Cannibalisation",
|
|
82
|
-
aeo: "AEO readiness",
|
|
83
|
-
};
|
|
84
|
-
return map[name] ?? name.charAt(0).toUpperCase() + name.slice(1);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* AEO-specific score band label. Low score = AI-Ready (site is citable by
|
|
88
|
-
* LLMs in AI Overviews / answer engines); high score = Ghost (not discoverable).
|
|
89
|
-
*/
|
|
90
|
-
function aeoScoreLabel(score) {
|
|
91
|
-
if (score <= 20)
|
|
92
|
-
return "AI-Ready";
|
|
93
|
-
if (score <= 40)
|
|
94
|
-
return "Partial";
|
|
95
|
-
if (score <= 60)
|
|
96
|
-
return "Vulnerable";
|
|
97
|
-
if (score <= 80)
|
|
98
|
-
return "Invisible";
|
|
99
|
-
return "Ghost";
|
|
100
|
-
}
|
|
101
|
-
/** Group findings by ruleId. Preserves encounter order per bucket. */
|
|
102
|
-
function groupByRule(findings) {
|
|
103
|
-
const m = new Map();
|
|
104
|
-
for (const f of findings) {
|
|
105
|
-
const bucket = m.get(f.ruleId);
|
|
106
|
-
if (bucket)
|
|
107
|
-
bucket.push(f);
|
|
108
|
-
else
|
|
109
|
-
m.set(f.ruleId, [f]);
|
|
110
|
-
}
|
|
111
|
-
return m;
|
|
41
|
+
function severityTone(s) {
|
|
42
|
+
if (s === "critical" || s === "error")
|
|
43
|
+
return "destructive";
|
|
44
|
+
if (s === "warning")
|
|
45
|
+
return "warning";
|
|
46
|
+
return "muted";
|
|
112
47
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
impact: SEVERITY_WEIGHT[sev] * items.length,
|
|
124
|
-
});
|
|
48
|
+
function effortLabel(effort) {
|
|
49
|
+
switch (effort) {
|
|
50
|
+
case "quick":
|
|
51
|
+
return "quick fix";
|
|
52
|
+
case "moderate":
|
|
53
|
+
return "moderate";
|
|
54
|
+
case "structural":
|
|
55
|
+
return "structural";
|
|
56
|
+
default:
|
|
57
|
+
return effort;
|
|
125
58
|
}
|
|
126
|
-
impacts.sort((a, b) => {
|
|
127
|
-
if (b.impact !== a.impact)
|
|
128
|
-
return b.impact - a.impact;
|
|
129
|
-
const sa = SEVERITY_ORDER.indexOf(a.severity);
|
|
130
|
-
const sb = SEVERITY_ORDER.indexOf(b.severity);
|
|
131
|
-
if (sa !== sb)
|
|
132
|
-
return sa - sb;
|
|
133
|
-
if (b.pageCount !== a.pageCount)
|
|
134
|
-
return b.pageCount - a.pageCount;
|
|
135
|
-
return a.ruleId.localeCompare(b.ruleId);
|
|
136
|
-
});
|
|
137
|
-
return impacts;
|
|
138
59
|
}
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
<span class="cause-order">${c.fixOrder}</span>
|
|
148
|
-
<h3>${escapeHtml(c.label)}</h3>
|
|
149
|
-
<span class="sev sev-${severityTone(c.severity)}">${escapeHtml(c.severity)}</span>
|
|
150
|
-
</div>
|
|
151
|
-
<p class="cause-meta">${c.findingsCount} findings · ${c.affectedRuleIds.map(escapeHtml).join(", ")}</p>
|
|
152
|
-
<p class="cause-body">${escapeHtml(c.rationale)}</p>
|
|
153
|
-
</li>`).join("");
|
|
154
|
-
return `
|
|
155
|
-
<section class="ai-triage">
|
|
156
|
-
<header class="section-head">
|
|
157
|
-
<span class="eyebrow">AI Triage</span>
|
|
158
|
-
<span class="meta-mono">${escapeHtml(triage.modelUsed)} · ${cacheLabel} · ${tokens}${cost}</span>
|
|
159
|
-
</header>
|
|
160
|
-
${triage.narrative ? `<p class="narrative">${escapeHtml(triage.narrative)}</p>` : ""}
|
|
161
|
-
<ol class="causes">${causes}</ol>
|
|
162
|
-
</section>`;
|
|
60
|
+
function renderCategoryTile(label, cell) {
|
|
61
|
+
const tone = GRADE_TONE[cell.grade];
|
|
62
|
+
const issuesLabel = cell.issues === 0 ? "no issues" : `${cell.issues} ${cell.issues === 1 ? "issue" : "issues"}`;
|
|
63
|
+
return `<div class="cat-tile cat-${tone}">
|
|
64
|
+
<div class="cat-grade">${escapeHtml(cell.grade)}</div>
|
|
65
|
+
<div class="cat-label">${escapeHtml(label)}</div>
|
|
66
|
+
<div class="cat-issues mono">${escapeHtml(issuesLabel)}</div>
|
|
67
|
+
</div>`;
|
|
163
68
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<span class="sev sev-${severityTone(imp.severity)}">${escapeHtml(imp.severity)}</span>
|
|
180
|
-
${effortPill}
|
|
181
|
-
<span class="top-fix-pages mono">${pagesLabel}</span>
|
|
182
|
-
</div>
|
|
183
|
-
<p class="top-fix-msg">${escapeHtml(r.message)}</p>
|
|
184
|
-
</div>
|
|
185
|
-
</li>`;
|
|
186
|
-
}).join("");
|
|
187
|
-
return `
|
|
188
|
-
<section class="top-fixes">
|
|
189
|
-
<header class="section-head">
|
|
190
|
-
<span class="eyebrow">Top fixes by impact</span>
|
|
191
|
-
<span class="meta-mono">severity × pages affected</span>
|
|
192
|
-
</header>
|
|
193
|
-
<ol class="top-fix-list">${rows}</ol>
|
|
194
|
-
</section>`;
|
|
69
|
+
function renderCategoryTiles(categories) {
|
|
70
|
+
const order = [
|
|
71
|
+
"integrity",
|
|
72
|
+
"discoverability",
|
|
73
|
+
"citation",
|
|
74
|
+
"data",
|
|
75
|
+
];
|
|
76
|
+
return order
|
|
77
|
+
.map((key) => {
|
|
78
|
+
const cell = categories[key];
|
|
79
|
+
if (!cell)
|
|
80
|
+
return "";
|
|
81
|
+
return renderCategoryTile(CATEGORY_LABEL[key], cell);
|
|
82
|
+
})
|
|
83
|
+
.join("");
|
|
195
84
|
}
|
|
196
|
-
function
|
|
197
|
-
const
|
|
198
|
-
const effortPill =
|
|
199
|
-
? ` <span class="effort-pill effort-${escapeHtml(
|
|
85
|
+
function renderFindingRow(f) {
|
|
86
|
+
const tone = severityTone(f.severity);
|
|
87
|
+
const effortPill = f.effort
|
|
88
|
+
? ` <span class="effort-pill effort-${escapeHtml(f.effort)}">${escapeHtml(effortLabel(f.effort))}</span>`
|
|
200
89
|
: "";
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
if (urls.length > 0) {
|
|
206
|
-
const preview = urls.slice(0, 3);
|
|
207
|
-
const rest = urls.slice(3);
|
|
208
|
-
const previewHtml = `<ul class="page-preview">${preview.map((u) => `<li class="mono">${escapeHtml(shortenUrl(u))}</li>`).join("")}</ul>`;
|
|
209
|
-
const restHtml = rest.length > 0
|
|
210
|
-
? `<details class="page-extra">
|
|
211
|
-
<summary>${rest.length} more ${rest.length === 1 ? "page" : "pages"}</summary>
|
|
212
|
-
<ul class="page-list">${rest.map((u) => `<li class="mono">${escapeHtml(shortenUrl(u))}</li>`).join("")}</ul>
|
|
213
|
-
</details>`
|
|
214
|
-
: "";
|
|
215
|
-
pageList = previewHtml + restHtml;
|
|
216
|
-
}
|
|
217
|
-
// Cluster context (if any finding has it, show the first one — site-wide rules only fire once).
|
|
90
|
+
const url = f.pageUrl ? `<span class="finding-url mono">${escapeHtml(shortenUrl(f.pageUrl))}</span>` : "";
|
|
91
|
+
const fix = f.fix ? `<div class="fix"><span class="fix-label">Fix</span>${escapeHtml(f.fix)}</div>` : "";
|
|
92
|
+
const docsHref = f.docsUrl ?? `https://pseolint.dev/rules/${f.ruleId.split("/").pop() ?? f.ruleId}`;
|
|
93
|
+
// Cluster context (collapsed details) — only on findings that carry one.
|
|
218
94
|
let cluster = "";
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const ctx = withCluster.context;
|
|
95
|
+
if (f.context?.type === "cluster") {
|
|
96
|
+
const ctx = f.context;
|
|
222
97
|
const [minSim, maxSim] = ctx.similarityRange;
|
|
223
|
-
const
|
|
224
|
-
.map(p => `<li><span class="mono">${escapeHtml(shortenUrl(p.left))}</span>
|
|
98
|
+
const worstPairs = ctx.worstPairs
|
|
99
|
+
.map((p) => `<li><span class="mono">${escapeHtml(shortenUrl(p.left))}</span> ↔ <span class="mono">${escapeHtml(shortenUrl(p.right))}</span> <span class="sim mono">${(p.similarity * 100).toFixed(1)}%</span></li>`)
|
|
225
100
|
.join("");
|
|
226
|
-
const
|
|
227
|
-
.map(m => `<li class="mono">${escapeHtml(shortenUrl(m))}</li>`)
|
|
101
|
+
const members = ctx.members
|
|
102
|
+
.map((m) => `<li class="mono">${escapeHtml(shortenUrl(m))}</li>`)
|
|
228
103
|
.join("");
|
|
229
|
-
cluster =
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<ul class="member-list">${membersHtml}</ul>
|
|
237
|
-
</div>
|
|
238
|
-
</details>`;
|
|
104
|
+
cluster = `<details class="cluster">
|
|
105
|
+
<summary>${ctx.clusterSize} pages in cluster · ${(minSim * 100).toFixed(0)}–${(maxSim * 100).toFixed(0)}% similar</summary>
|
|
106
|
+
<p class="cluster-label">Worst pairs</p>
|
|
107
|
+
<ul class="pair-list">${worstPairs}</ul>
|
|
108
|
+
<p class="cluster-label">All members</p>
|
|
109
|
+
<ul class="member-list">${members}</ul>
|
|
110
|
+
</details>`;
|
|
239
111
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
${
|
|
249
|
-
${cluster}
|
|
250
|
-
${fix}
|
|
251
|
-
${ref}
|
|
112
|
+
return `<li class="finding finding-${tone}">
|
|
113
|
+
<div class="finding-head">
|
|
114
|
+
<code class="rule-id">${escapeHtml(f.ruleId)}</code>${effortPill}
|
|
115
|
+
${url}
|
|
116
|
+
</div>
|
|
117
|
+
<p class="finding-msg">${escapeHtml(f.message)}</p>
|
|
118
|
+
${fix}
|
|
119
|
+
${cluster}
|
|
120
|
+
<a class="docs-link" href="${escapeHtml(docsHref)}" target="_blank" rel="noopener">${escapeHtml(docsHref.replace(/^https?:\/\//, ""))} ↗</a>
|
|
252
121
|
</li>`;
|
|
253
122
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
</
|
|
279
|
-
|
|
123
|
+
function renderBucketSection(label, items, tone) {
|
|
124
|
+
if (items.length === 0) {
|
|
125
|
+
return `<section class="bucket bucket-empty">
|
|
126
|
+
<h2 class="bucket-heading bucket-${tone}"><span class="bucket-dot"></span>${escapeHtml(label)} <span class="bucket-count">none</span></h2>
|
|
127
|
+
</section>`;
|
|
128
|
+
}
|
|
129
|
+
const rows = items.map(renderFindingRow).join("");
|
|
130
|
+
return `<section class="bucket">
|
|
131
|
+
<h2 class="bucket-heading bucket-${tone}"><span class="bucket-dot"></span>${escapeHtml(label)} <span class="bucket-count">${items.length}</span></h2>
|
|
132
|
+
<ul class="finding-list">${rows}</ul>
|
|
133
|
+
</section>`;
|
|
134
|
+
}
|
|
135
|
+
function renderOriginReadinessCard(report) {
|
|
136
|
+
if (!report)
|
|
137
|
+
return "";
|
|
138
|
+
const verdictTone = report.verdict === "ready"
|
|
139
|
+
? "success"
|
|
140
|
+
: report.verdict === "concerning"
|
|
141
|
+
? "warning"
|
|
142
|
+
: "destructive";
|
|
143
|
+
const fw = report.detectedFramework ? ` · framework: ${escapeHtml(report.detectedFramework)}` : "";
|
|
144
|
+
return `<section class="card readiness">
|
|
145
|
+
<header class="section-head">
|
|
146
|
+
<span class="eyebrow">Origin readiness</span>
|
|
147
|
+
<span class="meta-mono">${report.liveFetchCount} live fetch${report.liveFetchCount === 1 ? "" : "es"}${fw}</span>
|
|
148
|
+
</header>
|
|
149
|
+
<div class="readiness-grid">
|
|
150
|
+
<div><span class="stat-label">Verdict</span><span class="stat-val tone-${verdictTone}">${escapeHtml(report.verdict)}</span></div>
|
|
151
|
+
<div><span class="stat-label">Median</span><span class="stat-val mono">${report.medianMs} ms</span></div>
|
|
152
|
+
<div><span class="stat-label">p95</span><span class="stat-val mono">${report.p95Ms} ms</span></div>
|
|
153
|
+
<div><span class="stat-label">5xx ratio</span><span class="stat-val mono">${(report.serverErrorRatio * 100).toFixed(1)}%</span></div>
|
|
154
|
+
<div><span class="stat-label">Cache assist</span><span class="stat-val mono">${(report.cacheAssistRatio * 100).toFixed(0)}%</span></div>
|
|
155
|
+
</div>
|
|
156
|
+
</section>`;
|
|
157
|
+
}
|
|
158
|
+
function renderTriageHtml(triage) {
|
|
159
|
+
const sorted = triage.rootCauses.slice().sort((a, b) => a.fixOrder - b.fixOrder);
|
|
160
|
+
const cost = triage.estimatedCostUsd !== undefined ? ` · est $${triage.estimatedCostUsd.toFixed(2)}` : "";
|
|
161
|
+
const cacheLabel = triage.cacheHit ? "cached" : "fresh";
|
|
162
|
+
const tokens = `${triage.tokenUsage.input.toLocaleString()} in / ${triage.tokenUsage.output.toLocaleString()} out`;
|
|
163
|
+
const causes = sorted
|
|
164
|
+
.map((c) => `
|
|
165
|
+
<li class="cause">
|
|
166
|
+
<div class="cause-head">
|
|
167
|
+
<span class="cause-order">${c.fixOrder}</span>
|
|
168
|
+
<h3>${escapeHtml(c.label)}</h3>
|
|
169
|
+
<span class="sev sev-${severityTone(c.severity)}">${escapeHtml(c.severity)}</span>
|
|
170
|
+
</div>
|
|
171
|
+
<p class="cause-meta">${c.findingsCount} findings · ${c.affectedRuleIds.map(escapeHtml).join(", ")}</p>
|
|
172
|
+
<p class="cause-body">${escapeHtml(c.rationale)}</p>
|
|
173
|
+
</li>`)
|
|
280
174
|
.join("");
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (b[1].length !== a[1].length)
|
|
290
|
-
return b[1].length - a[1].length;
|
|
291
|
-
return a[0].localeCompare(b[0]);
|
|
292
|
-
})
|
|
293
|
-
.map(([ruleId]) => ruleId);
|
|
294
|
-
const itemsHtml = orderedRuleIds.map((rid) => renderRuleCard(rid, byRule.get(rid), sev)).join("");
|
|
295
|
-
const ruleCount = byRule.size;
|
|
296
|
-
return `
|
|
297
|
-
<div class="sev-group">
|
|
298
|
-
<h3 class="sev-heading sev-${severityTone(sev)}">
|
|
299
|
-
<span class="sev-dot"></span>${SEVERITY_LABEL[sev]}
|
|
300
|
-
<span class="sev-count">${ruleCount} ${ruleCount === 1 ? "rule" : "rules"} · ${items.length} ${items.length === 1 ? "finding" : "findings"}</span>
|
|
301
|
-
</h3>
|
|
302
|
-
<ul class="finding-list">${itemsHtml}</ul>
|
|
303
|
-
</div>`;
|
|
304
|
-
}).join("");
|
|
305
|
-
const tone = scoreTone(summary.score);
|
|
306
|
-
const verdict = scoreVerdict(summary.score);
|
|
307
|
-
const groupScoresHtml = summary.groupScores && summary.groupPageCounts ? `
|
|
308
|
-
<section class="card">
|
|
309
|
-
<header class="section-head">
|
|
310
|
-
<span class="eyebrow">Group scores</span>
|
|
311
|
-
</header>
|
|
312
|
-
<table class="data-table">
|
|
313
|
-
<thead><tr><th>Group</th><th>Score</th><th>Pages</th></tr></thead>
|
|
314
|
-
<tbody>${Object.entries(summary.groupScores).map(([name, value]) => {
|
|
315
|
-
const count = summary.groupPageCounts[name] ?? 0;
|
|
316
|
-
const gTone = categoryTone(value);
|
|
317
|
-
return `<tr>
|
|
318
|
-
<td>${escapeHtml(name)}</td>
|
|
319
|
-
<td class="mono tabular tone-${gTone}">${value}</td>
|
|
320
|
-
<td class="mono tabular muted">${count}</td>
|
|
321
|
-
</tr>`;
|
|
322
|
-
}).join("")}</tbody>
|
|
323
|
-
</table>
|
|
324
|
-
</section>` : "";
|
|
325
|
-
return `<!DOCTYPE html>
|
|
326
|
-
<html lang="en">
|
|
327
|
-
<head>
|
|
328
|
-
<meta charset="utf-8">
|
|
329
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
330
|
-
<title>pseolint · audit report</title>
|
|
331
|
-
<style>
|
|
332
|
-
:root{
|
|
333
|
-
--bg:#16181c; --fg:#eef2f6; --muted:#9aa4ae; --muted-2:#6a7380;
|
|
334
|
-
--card:#1a1d22; --card-2:#1f2329; --border:#262a30; --border-strong:#3a3f47;
|
|
335
|
-
--success:#39d19f; --warning:#fbb838; --destructive:#e94b4b; --primary:#39d19f;
|
|
336
|
-
--r:18px; --r-lg:28px;
|
|
337
|
-
}
|
|
338
|
-
*{margin:0;padding:0;box-sizing:border-box}
|
|
339
|
-
html{color-scheme:dark}
|
|
340
|
-
body{
|
|
341
|
-
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
342
|
-
background:var(--bg); color:var(--fg);
|
|
343
|
-
font-feature-settings:"rlig" 1,"calt" 1;
|
|
344
|
-
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
|
345
|
-
line-height:1.5;
|
|
346
|
-
}
|
|
347
|
-
main{max-width:960px;margin:0 auto;padding:56px 24px 80px}
|
|
348
|
-
.mono{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace}
|
|
349
|
-
.tabular{font-variant-numeric:tabular-nums}
|
|
350
|
-
.display{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-style:italic;font-weight:400;letter-spacing:-0.01em}
|
|
351
|
-
.muted{color:var(--muted)}
|
|
352
|
-
.meta-mono{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;color:var(--muted);font-size:12px}
|
|
353
|
-
.eyebrow{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
354
|
-
|
|
355
|
-
.status-row{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:11px;letter-spacing:0.14em;text-transform:uppercase}
|
|
356
|
-
.status-dot{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--success)}
|
|
357
|
-
.status-dot.tone-warning{background:var(--warning)} .status-dot.tone-destructive{background:var(--destructive)}
|
|
358
|
-
|
|
359
|
-
h1.title{margin-top:12px;font-size:clamp(32px,5vw,56px);line-height:1;letter-spacing:-0.01em}
|
|
360
|
-
.title-row{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;margin-top:12px}
|
|
361
|
-
.src-link{color:var(--muted);font-size:12px;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;text-decoration:none}
|
|
362
|
-
.src-link:hover{color:var(--fg)}
|
|
363
|
-
.lead{margin-top:12px;color:var(--muted);font-size:14px;max-width:640px}
|
|
364
|
-
|
|
365
|
-
.card{margin-top:28px;padding:28px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
366
|
-
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
367
|
-
.hero{display:grid;grid-template-columns:auto 1fr;gap:36px;align-items:center}
|
|
368
|
-
@media (max-width:640px){.hero{grid-template-columns:1fr;gap:24px}}
|
|
369
|
-
.score-block{display:flex;flex-direction:column;align-items:flex-start;min-width:180px}
|
|
370
|
-
.score{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-weight:400;
|
|
371
|
-
font-size:128px;line-height:0.9;font-variant-numeric:tabular-nums;letter-spacing:-0.02em}
|
|
372
|
-
.tone-success{color:var(--success)} .tone-warning{color:var(--warning)} .tone-destructive{color:var(--destructive)}
|
|
373
|
-
.score-label{margin-top:6px;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted)}
|
|
374
|
-
.verdict{margin-top:14px;display:inline-flex;align-items:center;gap:8px;
|
|
375
|
-
padding:5px 10px;border:1px solid var(--border-strong);border-radius:999px;
|
|
376
|
-
background:var(--card-2);font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
377
|
-
font-size:11px;color:var(--muted)}
|
|
378
|
-
.verdict-dot{width:5px;height:5px;border-radius:999px;background:var(--success)}
|
|
379
|
-
|
|
380
|
-
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px 28px;margin-top:8px}
|
|
381
|
-
@media (max-width:640px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
382
|
-
.stat-label{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted)}
|
|
383
|
-
.stat-val{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
384
|
-
font-size:20px;font-variant-numeric:tabular-nums;margin-top:2px}
|
|
385
|
-
|
|
386
|
-
.template-banner{margin-top:20px;padding:14px 18px;border:1px solid color-mix(in oklab,var(--warning) 40%,transparent);
|
|
387
|
-
border-radius:var(--r);background:color-mix(in oklab,var(--warning) 8%,transparent);color:var(--fg);font-size:13px}
|
|
388
|
-
.template-banner strong{color:var(--warning)}
|
|
389
|
-
|
|
390
|
-
.section-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:18px}
|
|
391
|
-
.data-table{width:100%;border-collapse:collapse}
|
|
392
|
-
.data-table th,.data-table td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border);font-size:14px}
|
|
393
|
-
.data-table th{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
394
|
-
.data-table tr:last-child td{border-bottom:0}
|
|
395
|
-
.cat-name{width:40%}
|
|
396
|
-
.cat-bar{width:auto}
|
|
397
|
-
.cat-val{width:56px;text-align:right}
|
|
398
|
-
.bar-bg{background:var(--border);border-radius:999px;height:8px;width:100%;overflow:hidden}
|
|
399
|
-
.bar-fill{height:100%;border-radius:999px;transition:width .4s ease}
|
|
400
|
-
.bar-success{background:var(--success)} .bar-warning{background:var(--warning)} .bar-destructive{background:var(--destructive)}
|
|
401
|
-
|
|
402
|
-
.aeo-callout{margin-top:20px;padding:16px 20px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
403
|
-
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
404
|
-
.aeo-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:10px}
|
|
405
|
-
.aeo-body{display:grid;grid-template-columns:auto 1fr auto;gap:14px;align-items:center}
|
|
406
|
-
@media (max-width:640px){.aeo-body{grid-template-columns:1fr;gap:8px}}
|
|
407
|
-
.aeo-label{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-style:italic;font-size:28px;line-height:1;letter-spacing:-0.01em}
|
|
408
|
-
.aeo-bar{display:block;height:8px;background:var(--border);border-radius:999px;overflow:hidden}
|
|
409
|
-
.aeo-bar .bar-fill{height:100%;display:block;border-radius:999px}
|
|
410
|
-
.aeo-meta{font-size:11px;color:var(--muted)}
|
|
411
|
-
|
|
412
|
-
.top-fixes{margin-top:28px;padding:24px 28px 28px;background:color-mix(in oklab,var(--primary) 6%,var(--card) 94%);
|
|
413
|
-
border:1px solid color-mix(in oklab,var(--primary) 25%,var(--border));border-radius:var(--r-lg)}
|
|
414
|
-
.top-fix-list{list-style:none;display:flex;flex-direction:column;gap:10px}
|
|
415
|
-
.top-fix{display:grid;grid-template-columns:32px 1fr;gap:12px;align-items:start;padding:12px 14px;
|
|
416
|
-
background:var(--card);border:1px solid var(--border);border-radius:14px}
|
|
417
|
-
.top-fix-rank{display:inline-grid;place-items:center;width:24px;height:24px;border-radius:999px;
|
|
418
|
-
background:color-mix(in oklab,var(--primary) 22%,transparent);color:var(--primary);
|
|
419
|
-
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12px;font-weight:700;margin-top:1px}
|
|
420
|
-
.top-fix-body{min-width:0}
|
|
421
|
-
.top-fix-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
|
|
422
|
-
.top-fix-pages{color:var(--muted);font-size:11px;margin-left:auto}
|
|
423
|
-
.top-fix-msg{color:var(--fg);font-size:14px}
|
|
424
|
-
|
|
425
|
-
.findings-head{display:flex;align-items:baseline;justify-content:space-between;margin:56px 0 18px}
|
|
426
|
-
.findings-head h2{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
427
|
-
.findings-head .meta{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12px;color:var(--muted)}
|
|
428
|
-
|
|
429
|
-
.sev-group{margin-top:28px}
|
|
430
|
-
.sev-group:first-child{margin-top:0}
|
|
431
|
-
.sev-heading{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;margin-bottom:10px}
|
|
432
|
-
.sev-dot{display:inline-block;width:8px;height:8px;border-radius:999px;background:currentColor}
|
|
433
|
-
.sev-count{margin-left:auto;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
434
|
-
font-size:12px;color:var(--muted);font-weight:400}
|
|
435
|
-
.sev-destructive{color:var(--destructive)} .sev-warning{color:var(--warning)} .sev-muted{color:var(--muted)}
|
|
436
|
-
.sev-heading.sev-muted{color:var(--muted-2)}
|
|
437
|
-
|
|
438
|
-
.finding-list{list-style:none;column-count:2;column-gap:10px;column-fill:balance}
|
|
439
|
-
@media (max-width:720px){.finding-list{column-count:1}}
|
|
440
|
-
.finding{position:relative;padding:14px 16px 14px 18px;background:var(--card);
|
|
441
|
-
border:1px solid var(--border);border-radius:var(--r);overflow:hidden;
|
|
442
|
-
break-inside:avoid;-webkit-column-break-inside:avoid;page-break-inside:avoid;
|
|
443
|
-
margin-bottom:10px;display:inline-block;width:100%}
|
|
444
|
-
.finding::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--border-strong)}
|
|
445
|
-
.finding-destructive::before{background:var(--destructive)}
|
|
446
|
-
.finding-warning::before{background:var(--warning)}
|
|
447
|
-
.finding-muted::before{background:var(--muted-2)}
|
|
448
|
-
.finding-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
|
|
449
|
-
.rule-id{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12px;
|
|
450
|
-
color:var(--fg);background:var(--card-2);padding:2px 8px;border-radius:6px;border:1px solid var(--border)}
|
|
451
|
-
.finding-pages{color:var(--muted);font-size:11px;margin-left:auto}
|
|
452
|
-
.finding-msg{color:var(--fg);font-size:14px}
|
|
453
|
-
.page-preview{list-style:none;display:flex;flex-direction:column;gap:2px;margin-top:8px;font-size:12px;color:var(--muted)}
|
|
454
|
-
.page-extra{margin-top:6px}
|
|
455
|
-
.page-extra>summary{cursor:pointer;color:var(--muted);font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
456
|
-
font-size:12px;list-style:none;padding:4px 0}
|
|
457
|
-
.page-extra>summary::-webkit-details-marker{display:none}
|
|
458
|
-
.page-extra>summary::before{content:"▸ ";color:var(--muted-2)}
|
|
459
|
-
.page-extra[open]>summary::before{content:"▾ "}
|
|
460
|
-
.page-list{list-style:none;display:flex;flex-direction:column;gap:2px;font-size:12px;color:var(--muted);
|
|
461
|
-
max-height:240px;overflow-y:auto;padding:6px 0 0}
|
|
462
|
-
|
|
463
|
-
.effort-pill{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;
|
|
464
|
-
font-size:10px;font-weight:600;letter-spacing:0.04em;text-transform:lowercase;
|
|
465
|
-
border:1px solid transparent}
|
|
466
|
-
.effort-quick{background:color-mix(in oklab,var(--success) 14%,transparent);
|
|
467
|
-
color:var(--success);border-color:color-mix(in oklab,var(--success) 35%,transparent)}
|
|
468
|
-
.effort-moderate{background:color-mix(in oklab,var(--warning) 14%,transparent);
|
|
469
|
-
color:var(--warning);border-color:color-mix(in oklab,var(--warning) 35%,transparent)}
|
|
470
|
-
.effort-structural{background:color-mix(in oklab,var(--destructive) 14%,transparent);
|
|
471
|
-
color:var(--destructive);border-color:color-mix(in oklab,var(--destructive) 35%,transparent)}
|
|
472
|
-
|
|
473
|
-
.sev{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:10px;
|
|
474
|
-
font-weight:600;text-transform:lowercase;letter-spacing:0.04em;border:1px solid transparent}
|
|
475
|
-
.sev-destructive.sev,.sev.sev-destructive{background:color-mix(in oklab,var(--destructive) 14%,transparent);color:var(--destructive);border-color:color-mix(in oklab,var(--destructive) 35%,transparent)}
|
|
476
|
-
.sev-warning.sev,.sev.sev-warning{background:color-mix(in oklab,var(--warning) 14%,transparent);color:var(--warning);border-color:color-mix(in oklab,var(--warning) 35%,transparent)}
|
|
477
|
-
.sev-muted.sev,.sev.sev-muted{background:var(--card-2);color:var(--muted);border-color:var(--border-strong)}
|
|
478
|
-
|
|
479
|
-
.fix{margin-top:8px;padding:10px 12px;background:var(--card-2);border-radius:10px;
|
|
480
|
-
color:var(--muted);font-size:13px;line-height:1.55}
|
|
481
|
-
.fix-label{display:inline-block;margin-right:8px;padding:1px 6px;border-radius:4px;
|
|
482
|
-
background:color-mix(in oklab,var(--primary) 18%,transparent);color:var(--primary);
|
|
483
|
-
font-size:10px;letter-spacing:0.08em;text-transform:uppercase;font-weight:700;vertical-align:1px}
|
|
484
|
-
.ref{display:inline-block;margin-top:8px;color:var(--primary);font-size:12px;text-decoration:none}
|
|
485
|
-
.ref:hover{text-decoration:underline}
|
|
486
|
-
|
|
487
|
-
.finding details{margin-top:10px;border:1px solid var(--border);border-radius:12px;background:var(--card-2)}
|
|
488
|
-
.finding details.page-extra{border:0;background:transparent;margin-top:6px}
|
|
489
|
-
.finding details>summary{cursor:pointer;padding:10px 14px;color:var(--muted);font-size:12px;
|
|
490
|
-
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;list-style:none}
|
|
491
|
-
.finding details.page-extra>summary{padding:4px 0}
|
|
492
|
-
.finding details>summary::-webkit-details-marker{display:none}
|
|
493
|
-
.finding details>summary::before{content:"▸ ";color:var(--muted-2)}
|
|
494
|
-
.finding details[open]>summary::before{content:"▾ "}
|
|
495
|
-
.cluster-body{padding:0 14px 14px;border-top:1px solid var(--border)}
|
|
496
|
-
.cluster-label{margin:12px 0 6px;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
497
|
-
.pair-list,.member-list{list-style:none;display:flex;flex-direction:column;gap:4px;font-size:12px}
|
|
498
|
-
.member-list{max-height:220px;overflow-y:auto;padding-right:4px}
|
|
499
|
-
.arrow{color:var(--muted-2)}
|
|
500
|
-
.sim{color:var(--warning);font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace}
|
|
501
|
-
|
|
502
|
-
.ai-triage{margin-top:28px;padding:28px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
503
|
-
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
504
|
-
.ai-triage .narrative{color:var(--fg);font-size:15px;line-height:1.6;margin-bottom:18px}
|
|
505
|
-
.causes{list-style:none;display:flex;flex-direction:column;gap:12px}
|
|
506
|
-
.cause{padding:14px 16px;background:var(--card);border:1px solid var(--border);border-radius:14px}
|
|
507
|
-
.cause-head{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
508
|
-
.cause-order{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:999px;
|
|
509
|
-
background:color-mix(in oklab,var(--primary) 20%,transparent);color:var(--primary);
|
|
510
|
-
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:11px;font-weight:700}
|
|
511
|
-
.cause h3{font-size:15px;font-weight:600}
|
|
512
|
-
.cause-meta{color:var(--muted);font-size:12px;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;margin-bottom:6px}
|
|
513
|
-
.cause-body{color:var(--fg);font-size:14px;line-height:1.55}
|
|
514
|
-
|
|
515
|
-
.footer-note{margin-top:56px;padding:20px 22px;border:1px solid var(--border);border-radius:var(--r-lg);
|
|
516
|
-
background:color-mix(in oklab,var(--card) 60%,transparent);color:var(--muted);font-size:12px;line-height:1.6}
|
|
517
|
-
.footer-note strong{color:var(--fg);font-weight:600}
|
|
518
|
-
|
|
519
|
-
::selection{background:color-mix(in oklab,var(--primary) 75%,transparent);color:#0b1a14}
|
|
520
|
-
::-webkit-scrollbar{width:8px;height:8px}
|
|
521
|
-
::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:999px}
|
|
522
|
-
</style>
|
|
523
|
-
</head>
|
|
524
|
-
<body>
|
|
525
|
-
<main>
|
|
526
|
-
<div class="status-row">
|
|
527
|
-
<span class="status-dot tone-${tone}"></span>
|
|
528
|
-
pseolint · audit complete
|
|
529
|
-
</div>
|
|
530
|
-
|
|
531
|
-
<div class="title-row">
|
|
532
|
-
<h1 class="title display">Audit report</h1>
|
|
533
|
-
<span class="src-link">${summary.pageCount} pages · ${summary.findings.length} findings</span>
|
|
534
|
-
</div>
|
|
535
|
-
<p class="lead">Scored against 40+ rules covering Google's public SpamBrain guidance, programmatic-SEO research, and Answer Engine Optimization (AEO) — how citable your pages are to LLMs in AI Overviews. Risk score — lower is safer.</p>
|
|
536
|
-
|
|
537
|
-
<section class="card hero">
|
|
538
|
-
<div class="score-block">
|
|
539
|
-
<span class="score tone-${tone}">${summary.score}</span>
|
|
540
|
-
<span class="score-label">Risk score · /100</span>
|
|
541
|
-
<span class="verdict"><span class="verdict-dot tone-${tone}" style="background:var(--${tone})"></span>${escapeHtml(verdict)}</span>
|
|
542
|
-
</div>
|
|
543
|
-
<dl class="stats">
|
|
544
|
-
<div><dt class="stat-label">Pages</dt><dd class="stat-val">${summary.pageCount}</dd></div>
|
|
545
|
-
<div><dt class="stat-label">Errors</dt><dd class="stat-val tone-destructive">${totalErrors}</dd></div>
|
|
546
|
-
<div><dt class="stat-label">Warnings</dt><dd class="stat-val tone-warning">${counts.warning}</dd></div>
|
|
547
|
-
<div><dt class="stat-label">Info</dt><dd class="stat-val muted">${counts.info}</dd></div>
|
|
548
|
-
</dl>
|
|
549
|
-
</section>
|
|
550
|
-
|
|
551
|
-
${summary.templateDetected ? `<div class="template-banner"><strong>Template-generated content detected.</strong> Fix suggestions are tailored for template authors — one change can fix hundreds of pages.</div>` : ""}
|
|
552
|
-
|
|
553
|
-
${(() => {
|
|
554
|
-
const aeoScore = summary.categoryScores?.aeo;
|
|
555
|
-
if (aeoScore === undefined)
|
|
556
|
-
return "";
|
|
557
|
-
const aeoTone = categoryTone(aeoScore);
|
|
558
|
-
const label = aeoScoreLabel(aeoScore);
|
|
559
|
-
const invert = 100 - aeoScore;
|
|
560
|
-
return `
|
|
561
|
-
<section class="aeo-callout">
|
|
562
|
-
<div class="aeo-head">
|
|
563
|
-
<span class="eyebrow">AEO readiness</span>
|
|
564
|
-
<span class="meta-mono">answer-engine citability</span>
|
|
565
|
-
</div>
|
|
566
|
-
<div class="aeo-body">
|
|
567
|
-
<span class="aeo-label tone-${aeoTone}">${escapeHtml(label)}</span>
|
|
568
|
-
<span class="aeo-bar"><span class="bar-fill bar-${aeoTone}" style="width:${invert}%"></span></span>
|
|
569
|
-
<span class="aeo-meta mono">${invert}/100 ready · ${aeoScore}/100 risk</span>
|
|
570
|
-
</div>
|
|
175
|
+
return `
|
|
176
|
+
<section class="ai-triage">
|
|
177
|
+
<header class="section-head">
|
|
178
|
+
<span class="eyebrow">AI Triage</span>
|
|
179
|
+
<span class="meta-mono">${escapeHtml(triage.modelUsed)} · ${cacheLabel} · ${tokens}${cost}</span>
|
|
180
|
+
</header>
|
|
181
|
+
${triage.narrative ? `<p class="narrative">${escapeHtml(triage.narrative)}</p>` : ""}
|
|
182
|
+
<ol class="causes">${causes}</ol>
|
|
571
183
|
</section>`;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
184
|
+
}
|
|
185
|
+
export function formatHtml(summary, _options) {
|
|
186
|
+
const verdict = summary.verdict;
|
|
187
|
+
const verdictTone = VERDICT_TONE[verdict];
|
|
188
|
+
const issues = summary.issues;
|
|
189
|
+
const crawl = summary.diagnostics?.crawlStats;
|
|
190
|
+
const readiness = summary.diagnostics?.originReadiness;
|
|
191
|
+
const crawlMeta = crawl
|
|
192
|
+
? `${crawl.fetched} fetched · ${crawl.discovered} discovered · ${crawl.skipped} skipped`
|
|
193
|
+
: `${summary.pageCount} pages`;
|
|
194
|
+
return `<!DOCTYPE html>
|
|
195
|
+
<html lang="en">
|
|
196
|
+
<head>
|
|
197
|
+
<meta charset="utf-8">
|
|
198
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
199
|
+
<title>pseolint · audit report</title>
|
|
200
|
+
<style>
|
|
201
|
+
:root{
|
|
202
|
+
--bg:#16181c; --fg:#eef2f6; --muted:#9aa4ae; --muted-2:#6a7380;
|
|
203
|
+
--card:#1a1d22; --card-2:#1f2329; --border:#262a30; --border-strong:#3a3f47;
|
|
204
|
+
--success:#39d19f; --warning:#fbb838; --destructive:#e94b4b; --critical:#c0185a; --primary:#39d19f;
|
|
205
|
+
--r:18px; --r-lg:28px;
|
|
206
|
+
}
|
|
207
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
208
|
+
html{color-scheme:dark}
|
|
209
|
+
body{
|
|
210
|
+
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
211
|
+
background:var(--bg); color:var(--fg);
|
|
212
|
+
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
|
213
|
+
line-height:1.5;
|
|
214
|
+
}
|
|
215
|
+
main{max-width:960px;margin:0 auto;padding:56px 24px 80px}
|
|
216
|
+
.mono{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace}
|
|
217
|
+
.tabular{font-variant-numeric:tabular-nums}
|
|
218
|
+
.display{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-style:italic;font-weight:400;letter-spacing:-0.01em}
|
|
219
|
+
.muted{color:var(--muted)}
|
|
220
|
+
.meta-mono{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;color:var(--muted);font-size:12px}
|
|
221
|
+
.eyebrow{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
222
|
+
|
|
223
|
+
.status-row{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:11px;letter-spacing:0.14em;text-transform:uppercase}
|
|
224
|
+
.status-dot{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--success)}
|
|
225
|
+
.status-dot.tone-warning{background:var(--warning)} .status-dot.tone-destructive{background:var(--destructive)} .status-dot.tone-critical{background:var(--critical)}
|
|
226
|
+
|
|
227
|
+
h1.title{margin-top:12px;font-size:clamp(32px,5vw,56px);line-height:1;letter-spacing:-0.01em}
|
|
228
|
+
.title-row{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;margin-top:12px}
|
|
229
|
+
.src-link{color:var(--muted);font-size:12px;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace}
|
|
230
|
+
.lead{margin-top:12px;color:var(--muted);font-size:14px;max-width:640px}
|
|
231
|
+
|
|
232
|
+
.card{margin-top:28px;padding:28px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
233
|
+
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
234
|
+
|
|
235
|
+
/* Verdict badge */
|
|
236
|
+
.verdict-badge{display:inline-flex;align-items:center;gap:10px;padding:10px 18px;border-radius:999px;
|
|
237
|
+
border:1px solid var(--border-strong);background:var(--card-2);
|
|
238
|
+
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:14px;font-weight:600;
|
|
239
|
+
text-transform:uppercase;letter-spacing:0.08em}
|
|
240
|
+
.verdict-badge.tone-success{color:var(--success);border-color:color-mix(in oklab,var(--success) 45%,var(--border))}
|
|
241
|
+
.verdict-badge.tone-warning{color:var(--warning);border-color:color-mix(in oklab,var(--warning) 45%,var(--border))}
|
|
242
|
+
.verdict-badge.tone-destructive{color:var(--destructive);border-color:color-mix(in oklab,var(--destructive) 45%,var(--border))}
|
|
243
|
+
.verdict-badge.tone-critical{color:var(--critical);border-color:color-mix(in oklab,var(--critical) 45%,var(--border));background:color-mix(in oklab,var(--critical) 8%,var(--card-2))}
|
|
244
|
+
.verdict-dot{width:8px;height:8px;border-radius:999px;background:currentColor}
|
|
245
|
+
.verdict-headline{display:block;margin-top:14px;font-size:18px;color:var(--fg)}
|
|
246
|
+
|
|
247
|
+
/* Category tiles */
|
|
248
|
+
.cat-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:24px}
|
|
249
|
+
@media (max-width:640px){.cat-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
250
|
+
.cat-tile{padding:18px 16px;border-radius:var(--r);background:var(--card);border:1px solid var(--border);
|
|
251
|
+
display:flex;flex-direction:column;align-items:flex-start;gap:6px}
|
|
252
|
+
.cat-grade{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-style:italic;font-size:48px;line-height:1;font-variant-numeric:tabular-nums}
|
|
253
|
+
.cat-tile.cat-success .cat-grade{color:var(--success)}
|
|
254
|
+
.cat-tile.cat-warning .cat-grade{color:var(--warning)}
|
|
255
|
+
.cat-tile.cat-destructive .cat-grade{color:var(--destructive)}
|
|
256
|
+
.cat-tile.cat-critical .cat-grade{color:var(--critical)}
|
|
257
|
+
.cat-tile.cat-success{border-color:color-mix(in oklab,var(--success) 30%,var(--border))}
|
|
258
|
+
.cat-tile.cat-warning{border-color:color-mix(in oklab,var(--warning) 30%,var(--border))}
|
|
259
|
+
.cat-tile.cat-destructive{border-color:color-mix(in oklab,var(--destructive) 30%,var(--border))}
|
|
260
|
+
.cat-tile.cat-critical{border-color:color-mix(in oklab,var(--critical) 30%,var(--border))}
|
|
261
|
+
.cat-label{font-size:13px;font-weight:600;color:var(--fg)}
|
|
262
|
+
.cat-issues{font-size:11px;color:var(--muted)}
|
|
263
|
+
|
|
264
|
+
.template-banner{margin-top:20px;padding:14px 18px;border:1px solid color-mix(in oklab,var(--warning) 40%,transparent);
|
|
265
|
+
border-radius:var(--r);background:color-mix(in oklab,var(--warning) 8%,transparent);font-size:13px}
|
|
266
|
+
.template-banner strong{color:var(--warning)}
|
|
267
|
+
|
|
268
|
+
/* Origin readiness card */
|
|
269
|
+
.readiness-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin-top:8px}
|
|
270
|
+
@media (max-width:720px){.readiness-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
271
|
+
.readiness-grid>div{display:flex;flex-direction:column;gap:2px}
|
|
272
|
+
.stat-label{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted)}
|
|
273
|
+
.stat-val{font-size:18px;font-variant-numeric:tabular-nums}
|
|
274
|
+
.tone-success{color:var(--success)} .tone-warning{color:var(--warning)} .tone-destructive{color:var(--destructive)} .tone-critical{color:var(--critical)}
|
|
275
|
+
|
|
276
|
+
.section-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:18px}
|
|
277
|
+
|
|
278
|
+
/* Buckets */
|
|
279
|
+
.bucket{margin-top:36px}
|
|
280
|
+
.bucket-empty{opacity:0.55}
|
|
281
|
+
.bucket-heading{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;margin-bottom:14px;
|
|
282
|
+
text-transform:uppercase;letter-spacing:0.08em}
|
|
283
|
+
.bucket-dot{display:inline-block;width:8px;height:8px;border-radius:999px;background:currentColor}
|
|
284
|
+
.bucket-count{margin-left:auto;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
285
|
+
font-size:12px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0}
|
|
286
|
+
.bucket-destructive{color:var(--destructive)}
|
|
287
|
+
.bucket-warning{color:var(--warning)}
|
|
288
|
+
.bucket-muted{color:var(--muted-2)}
|
|
289
|
+
|
|
290
|
+
.finding-list{list-style:none;display:flex;flex-direction:column;gap:10px}
|
|
291
|
+
.finding{position:relative;padding:14px 16px 14px 18px;background:var(--card);
|
|
292
|
+
border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
|
|
293
|
+
.finding::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--border-strong)}
|
|
294
|
+
.finding-destructive::before{background:var(--destructive)}
|
|
295
|
+
.finding-warning::before{background:var(--warning)}
|
|
296
|
+
.finding-muted::before{background:var(--muted-2)}
|
|
297
|
+
.finding-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
|
|
298
|
+
.rule-id{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12px;
|
|
299
|
+
color:var(--fg);background:var(--card-2);padding:2px 8px;border-radius:6px;border:1px solid var(--border)}
|
|
300
|
+
.finding-url{color:var(--muted);font-size:12px;margin-left:auto}
|
|
301
|
+
.finding-msg{color:var(--fg);font-size:14px}
|
|
302
|
+
|
|
303
|
+
.effort-pill{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;
|
|
304
|
+
font-size:10px;font-weight:600;letter-spacing:0.04em;text-transform:lowercase;border:1px solid transparent}
|
|
305
|
+
.effort-quick{background:color-mix(in oklab,var(--success) 14%,transparent);color:var(--success);border-color:color-mix(in oklab,var(--success) 35%,transparent)}
|
|
306
|
+
.effort-moderate{background:color-mix(in oklab,var(--warning) 14%,transparent);color:var(--warning);border-color:color-mix(in oklab,var(--warning) 35%,transparent)}
|
|
307
|
+
.effort-structural{background:color-mix(in oklab,var(--destructive) 14%,transparent);color:var(--destructive);border-color:color-mix(in oklab,var(--destructive) 35%,transparent)}
|
|
308
|
+
|
|
309
|
+
.sev{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:10px;font-weight:600;text-transform:lowercase;letter-spacing:0.04em;border:1px solid transparent}
|
|
310
|
+
.sev.sev-destructive{background:color-mix(in oklab,var(--destructive) 14%,transparent);color:var(--destructive);border-color:color-mix(in oklab,var(--destructive) 35%,transparent)}
|
|
311
|
+
.sev.sev-warning{background:color-mix(in oklab,var(--warning) 14%,transparent);color:var(--warning);border-color:color-mix(in oklab,var(--warning) 35%,transparent)}
|
|
312
|
+
.sev.sev-muted{background:var(--card-2);color:var(--muted);border-color:var(--border-strong)}
|
|
313
|
+
|
|
314
|
+
.fix{margin-top:8px;padding:10px 12px;background:var(--card-2);border-radius:10px;color:var(--muted);font-size:13px;line-height:1.55}
|
|
315
|
+
.fix-label{display:inline-block;margin-right:8px;padding:1px 6px;border-radius:4px;
|
|
316
|
+
background:color-mix(in oklab,var(--primary) 18%,transparent);color:var(--primary);
|
|
317
|
+
font-size:10px;letter-spacing:0.08em;text-transform:uppercase;font-weight:700;vertical-align:1px}
|
|
318
|
+
.docs-link{display:inline-block;margin-top:8px;color:var(--primary);font-size:12px;text-decoration:none;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace}
|
|
319
|
+
.docs-link:hover{text-decoration:underline}
|
|
320
|
+
|
|
321
|
+
details.cluster{margin-top:10px;border:1px solid var(--border);border-radius:12px;background:var(--card-2);padding:0 14px 14px}
|
|
322
|
+
details.cluster>summary{cursor:pointer;padding:10px 0;color:var(--muted);font-size:12px;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;list-style:none}
|
|
323
|
+
details.cluster>summary::-webkit-details-marker{display:none}
|
|
324
|
+
details.cluster>summary::before{content:"▸ ";color:var(--muted-2)}
|
|
325
|
+
details.cluster[open]>summary::before{content:"▾ "}
|
|
326
|
+
.cluster-label{margin:12px 0 6px;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
327
|
+
.pair-list,.member-list{list-style:none;display:flex;flex-direction:column;gap:4px;font-size:12px}
|
|
328
|
+
.member-list{max-height:220px;overflow-y:auto;padding-right:4px}
|
|
329
|
+
.sim{color:var(--warning)}
|
|
330
|
+
|
|
331
|
+
.ai-triage{margin-top:28px;padding:28px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
332
|
+
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
333
|
+
.ai-triage .narrative{color:var(--fg);font-size:15px;line-height:1.6;margin-bottom:18px}
|
|
334
|
+
.causes{list-style:none;display:flex;flex-direction:column;gap:12px}
|
|
335
|
+
.cause{padding:14px 16px;background:var(--card);border:1px solid var(--border);border-radius:14px}
|
|
336
|
+
.cause-head{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
337
|
+
.cause-order{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:999px;
|
|
338
|
+
background:color-mix(in oklab,var(--primary) 20%,transparent);color:var(--primary);
|
|
339
|
+
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:11px;font-weight:700}
|
|
340
|
+
.cause h3{font-size:15px;font-weight:600}
|
|
341
|
+
.cause-meta{color:var(--muted);font-size:12px;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;margin-bottom:6px}
|
|
342
|
+
.cause-body{color:var(--fg);font-size:14px;line-height:1.55}
|
|
343
|
+
|
|
344
|
+
.footer-note{margin-top:56px;padding:20px 22px;border:1px solid var(--border);border-radius:var(--r-lg);
|
|
345
|
+
background:color-mix(in oklab,var(--card) 60%,transparent);color:var(--muted);font-size:12px;line-height:1.6}
|
|
346
|
+
.footer-note strong{color:var(--fg);font-weight:600}
|
|
347
|
+
</style>
|
|
348
|
+
</head>
|
|
349
|
+
<body>
|
|
350
|
+
<main>
|
|
351
|
+
<div class="status-row">
|
|
352
|
+
<span class="status-dot tone-${verdictTone}"></span>
|
|
353
|
+
pseolint · audit complete
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div class="title-row">
|
|
357
|
+
<h1 class="title display">Audit report</h1>
|
|
358
|
+
<span class="src-link">${escapeHtml(crawlMeta)}</span>
|
|
359
|
+
</div>
|
|
360
|
+
<p class="lead">Schema ${escapeHtml(summary.schemaVersion)}. Verdict, category grades, and bucketed issues — see docs links per finding for what each rule checks.</p>
|
|
361
|
+
|
|
362
|
+
<section class="card">
|
|
363
|
+
<span class="verdict-badge tone-${verdictTone}"><span class="verdict-dot"></span>${escapeHtml(VERDICT_LABEL[verdict])}</span>
|
|
364
|
+
<span class="verdict-headline">${escapeHtml(summary.headline)}</span>
|
|
365
|
+
<div class="cat-grid">${renderCategoryTiles(summary.categories)}</div>
|
|
366
|
+
</section>
|
|
367
|
+
|
|
368
|
+
${summary.templateDetected ? `<div class="template-banner"><strong>Template-generated content detected.</strong> Fix suggestions are tailored for template authors — one change can fix hundreds of pages.</div>` : ""}
|
|
369
|
+
|
|
370
|
+
${renderOriginReadinessCard(readiness)}
|
|
371
|
+
|
|
372
|
+
${renderBucketSection("Blockers", issues.blockers, "destructive")}
|
|
373
|
+
${renderBucketSection("Should fix", issues.shouldFix, "warning")}
|
|
374
|
+
${renderBucketSection("Informational", issues.informational, "muted")}
|
|
375
|
+
|
|
376
|
+
${summary.triage ? renderTriageHtml(summary.triage) : ""}
|
|
377
|
+
|
|
378
|
+
<section class="footer-note">
|
|
379
|
+
<strong>About this report.</strong> Verdict and grades are structured heuristics, not a verdict from Google. Severities escalate: info → warning → error → critical. Effort tags
|
|
380
|
+
(<span class="effort-pill effort-quick">quick fix</span> <span class="effort-pill effort-moderate">moderate</span> <span class="effort-pill effort-structural">structural</span>)
|
|
381
|
+
estimate the change cost per finding. Findings are bucketed: <em>blockers</em> must be fixed before shipping, <em>should fix</em> before scaling, <em>informational</em> for trend-watching.
|
|
382
|
+
</section>
|
|
383
|
+
</main>
|
|
384
|
+
</body>
|
|
603
385
|
</html>`;
|
|
604
386
|
}
|
|
605
387
|
//# sourceMappingURL=html.js.map
|