Skip to content

Commit 50dcb18

Browse files
authored
Merge pull request #137 from mgifford/copilot/create-html-css-js-quality-page
Add HTML/CSS/JS Code Quality page surfacing Lighthouse best-practices audits
2 parents 327554c + 605c659 commit 50dcb18

9 files changed

Lines changed: 848 additions & 2 deletions

File tree

kitty-specs/002-daily-dap-quality-benchmarking/contracts/daily-report.schema.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,41 @@
111111
},
112112
"additionalProperties": false
113113
}
114+
},
115+
"code_quality_summary": {
116+
"description": "Aggregated Lighthouse best-practices / code quality audit results across all successfully scanned URLs.",
117+
"oneOf": [
118+
{ "type": "null" },
119+
{
120+
"type": "object",
121+
"required": [
122+
"total_scanned",
123+
"urls_with_deprecated_apis",
124+
"urls_with_console_errors",
125+
"urls_with_document_write",
126+
"urls_with_vulnerable_libraries"
127+
],
128+
"properties": {
129+
"total_scanned": { "type": "integer", "minimum": 0 },
130+
"urls_with_deprecated_apis": { "type": "integer", "minimum": 0 },
131+
"urls_with_console_errors": { "type": "integer", "minimum": 0 },
132+
"urls_with_document_write": { "type": "integer", "minimum": 0 },
133+
"urls_with_vulnerable_libraries": { "type": "integer", "minimum": 0 },
134+
"js_library_counts": { "type": "object" },
135+
"vulnerable_library_counts": { "type": "object" },
136+
"audit_urls": {
137+
"type": "object",
138+
"properties": {
139+
"deprecated_apis": { "type": "array", "items": { "type": "string" } },
140+
"console_errors": { "type": "array", "items": { "type": "string" } },
141+
"document_write": { "type": "array", "items": { "type": "string" } },
142+
"vulnerable_libraries": { "type": "array", "items": { "type": "string" } }
143+
}
144+
}
145+
},
146+
"additionalProperties": false
147+
}
148+
]
114149
}
115150
},
116151
"additionalProperties": true

src/publish/archive-writer.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'node:fs/promises';
22
import path from 'node:path';
3-
import { renderDailyReportPage, renderDashboardPage, render404Page } from './render-pages.js';
3+
import { renderDailyReportPage, renderDashboardPage, render404Page, renderCodeQualityPage } from './render-pages.js';
44
import { buildPressRelease } from '../cli/generate-press-release.js';
55

66
async function writeJson(filePath, payload) {
@@ -132,6 +132,7 @@ export async function writeCommittedSnapshot({
132132
const notFoundPagePath = path.join(docsRoot, '404.html');
133133
const dailyReportPath = path.join(dailyDir, 'report.json');
134134
const dailyPagePath = path.join(dailyDir, 'index.html');
135+
const codeQualityPagePath = path.join(dailyDir, 'code-quality.html');
135136
const axeFindingsPath = path.join(dailyDir, 'axe-findings.json');
136137
const axeFindingsCsvPath = path.join(dailyDir, 'axe-findings.csv');
137138
const lighthouseHistoryCsvPath = path.join(dailyDir, 'lighthouse-history.csv');
@@ -142,6 +143,7 @@ export async function writeCommittedSnapshot({
142143
await fs.writeFile(notFoundPagePath, render404Page(), 'utf8');
143144
await writeJson(dailyReportPath, report);
144145
await fs.writeFile(dailyPagePath, renderDailyReportPage(report), 'utf8');
146+
await fs.writeFile(codeQualityPagePath, renderCodeQualityPage(report), 'utf8');
145147
const axeFindingsReport = buildAxeFindingsReport(report);
146148
await writeJson(axeFindingsPath, axeFindingsReport);
147149
await fs.writeFile(axeFindingsCsvPath, buildAxeFindingsCsv(axeFindingsReport), 'utf8');
@@ -164,6 +166,7 @@ export async function writeCommittedSnapshot({
164166
reports_root: reportsRoot,
165167
report_json_path: dailyReportPath,
166168
report_page_path: dailyPagePath,
169+
code_quality_page_path: codeQualityPagePath,
167170
axe_findings_path: axeFindingsPath,
168171
axe_findings_csv_path: axeFindingsCsvPath,
169172
lighthouse_history_csv_path: lighthouseHistoryCsvPath,

src/publish/build-daily-report.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,93 @@ function coerceScore(value) {
1010
return 0;
1111
}
1212

13+
/**
14+
* Produce a compact per-URL code quality summary suitable for the top_urls array.
15+
* Avoids embedding full audit item payloads to keep the report JSON size manageable.
16+
*
17+
* @param {object|null} audits - raw code_quality_audits from a URL scan result
18+
* @returns {object|null}
19+
*/
20+
function summarizeCodeQualityAudits(audits) {
21+
if (!audits) return null;
22+
return {
23+
deprecated_apis_passing: audits.deprecated_apis?.passing ?? null,
24+
deprecated_apis_count: audits.deprecated_apis?.items?.length ?? 0,
25+
errors_in_console_passing: audits.errors_in_console?.passing ?? null,
26+
errors_in_console_count: audits.errors_in_console?.count ?? 0,
27+
no_document_write_passing: audits.no_document_write?.passing ?? null,
28+
vulnerable_libraries_passing: audits.vulnerable_libraries?.passing ?? null,
29+
vulnerable_libraries_count: audits.vulnerable_libraries?.items?.length ?? 0,
30+
vulnerable_library_names: (audits.vulnerable_libraries?.items ?? [])
31+
.map((item) => item.library)
32+
.filter(Boolean),
33+
js_libraries: (audits.js_libraries?.items ?? []).map((l) => l.name).filter(Boolean)
34+
};
35+
}
36+
37+
/**
38+
* Aggregate code quality audit results across all successfully scanned URLs.
39+
*
40+
* @param {Array} urlResults - normalized URL scan results
41+
* @returns {object}
42+
*/
43+
export function buildCodeQualitySummary(urlResults = []) {
44+
const successful = urlResults.filter(
45+
(r) => r?.scan_status === 'success' && r.code_quality_audits != null
46+
);
47+
48+
const auditUrls = {
49+
deprecated_apis: [],
50+
console_errors: [],
51+
document_write: [],
52+
vulnerable_libraries: []
53+
};
54+
55+
const jsLibraryCounts = {};
56+
const vulnerableLibraryCounts = {};
57+
58+
for (const result of successful) {
59+
const qa = result.code_quality_audits;
60+
61+
if (qa.deprecated_apis?.passing === false) {
62+
auditUrls.deprecated_apis.push(result.url);
63+
}
64+
if (qa.errors_in_console?.passing === false) {
65+
auditUrls.console_errors.push(result.url);
66+
}
67+
if (qa.no_document_write?.passing === false) {
68+
auditUrls.document_write.push(result.url);
69+
}
70+
if (qa.vulnerable_libraries?.passing === false) {
71+
auditUrls.vulnerable_libraries.push(result.url);
72+
for (const lib of qa.vulnerable_libraries.items ?? []) {
73+
if (lib.library) {
74+
if (!vulnerableLibraryCounts[lib.library]) {
75+
vulnerableLibraryCounts[lib.library] = { count: 0, severity: lib.severity };
76+
}
77+
vulnerableLibraryCounts[lib.library].count += 1;
78+
}
79+
}
80+
}
81+
for (const lib of qa.js_libraries?.items ?? []) {
82+
if (lib.name) {
83+
jsLibraryCounts[lib.name] = (jsLibraryCounts[lib.name] ?? 0) + 1;
84+
}
85+
}
86+
}
87+
88+
return {
89+
total_scanned: successful.length,
90+
urls_with_deprecated_apis: auditUrls.deprecated_apis.length,
91+
urls_with_console_errors: auditUrls.console_errors.length,
92+
urls_with_document_write: auditUrls.document_write.length,
93+
urls_with_vulnerable_libraries: auditUrls.vulnerable_libraries.length,
94+
js_library_counts: jsLibraryCounts,
95+
vulnerable_library_counts: vulnerableLibraryCounts,
96+
audit_urls: auditUrls
97+
};
98+
}
99+
13100
function normalizeTopUrls(urlResults = [], dotgovLookup = null) {
14101
return urlResults
15102
.map((result) => {
@@ -29,6 +116,7 @@ function normalizeTopUrls(urlResults = [], dotgovLookup = null) {
29116
core_web_vitals_status: result.core_web_vitals_status ?? 'unknown',
30117
lcp_value_ms: typeof result.lcp_value_ms === 'number' ? result.lcp_value_ms : null,
31118
detected_technologies: result.detected_technologies ?? null,
119+
code_quality_summary: summarizeCodeQualityAudits(result.code_quality_audits),
32120
lighthouse_scores:
33121
result.scan_status === 'success'
34122
? {
@@ -86,6 +174,8 @@ export function buildDailyReport({
86174
);
87175
techSummary.required_links_summary = buildRequiredLinksSummary(requiredLinks ?? {});
88176

177+
const codeQualitySummary = buildCodeQualitySummary(urlResults);
178+
89179
const sourceDataDate = urlResults.reduce((latest, result) => {
90180
const candidate = result?.source_date;
91181
if (!candidate) {
@@ -121,6 +211,7 @@ export function buildDailyReport({
121211
source_data_date: sourceDataDate,
122212
top_urls: topUrls,
123213
tech_summary: techSummary,
214+
code_quality_summary: codeQualitySummary,
124215
trend_window_days: historyWindow?.window_days ?? 30,
125216
history_series: historySeries,
126217
generated_at: runMetadata.generated_at,

0 commit comments

Comments
 (0)