laxy-verify 1.3.0 → 1.3.1
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 +479 -474
- package/dist/ai-analysis.d.ts +28 -0
- package/dist/ai-analysis.js +32 -0
- package/dist/audit/broken-links.d.ts +25 -25
- package/dist/audit/broken-links.js +97 -97
- package/dist/badge.d.ts +2 -2
- package/dist/badge.js +18 -18
- package/dist/cli.js +1245 -1246
- package/dist/compare-env.d.ts +23 -0
- package/dist/compare-env.js +55 -0
- package/dist/config.d.ts +102 -102
- package/dist/config.js +360 -360
- package/dist/entitlement.d.ts +15 -15
- package/dist/entitlement.js +98 -98
- package/dist/init-analysis.d.ts +6 -0
- package/dist/init-analysis.js +302 -0
- package/dist/init.js +132 -132
- package/dist/lighthouse.d.ts +37 -37
- package/dist/lighthouse.js +231 -231
- package/dist/report-markdown.d.ts +53 -53
- package/dist/report-markdown.js +407 -407
- package/dist/route-discovery.d.ts +7 -0
- package/dist/route-discovery.js +108 -0
- package/dist/security-audit.d.ts +17 -17
- package/dist/security-audit.js +127 -127
- package/dist/verification-core/report.js +526 -526
- package/dist/verification-core/types.d.ts +164 -164
- package/dist/visual-diff.d.ts +33 -33
- package/dist/visual-diff.js +213 -213
- package/package.json +1 -1
|
@@ -1,526 +1,526 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_LH_THRESHOLDS = void 0;
|
|
4
|
-
exports.getLighthousePass = getLighthousePass;
|
|
5
|
-
exports.getVerificationGrade = getVerificationGrade;
|
|
6
|
-
exports.buildVerificationEvidence = buildVerificationEvidence;
|
|
7
|
-
exports.getImprovementRecommendations = getImprovementRecommendations;
|
|
8
|
-
exports.buildVerificationReport = buildVerificationReport;
|
|
9
|
-
exports.buildTierVerificationView = buildTierVerificationView;
|
|
10
|
-
const tier_policy_js_1 = require("./tier-policy.js");
|
|
11
|
-
exports.DEFAULT_LH_THRESHOLDS = {
|
|
12
|
-
performance: 70,
|
|
13
|
-
accessibility: 85,
|
|
14
|
-
seo: 80,
|
|
15
|
-
bestPractices: 80,
|
|
16
|
-
};
|
|
17
|
-
function getLighthousePass(lighthouseScores, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
18
|
-
if (!lighthouseScores)
|
|
19
|
-
return false;
|
|
20
|
-
return (lighthouseScores.performance >= thresholds.performance &&
|
|
21
|
-
lighthouseScores.accessibility >= thresholds.accessibility &&
|
|
22
|
-
lighthouseScores.seo >= thresholds.seo &&
|
|
23
|
-
lighthouseScores.bestPractices >= thresholds.bestPractices);
|
|
24
|
-
}
|
|
25
|
-
function getVerificationGrade(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
26
|
-
const buildPassed = input.buildSuccess === true;
|
|
27
|
-
const e2ePassedAll = typeof input.e2ePassed === "number" &&
|
|
28
|
-
typeof input.e2eTotal === "number" &&
|
|
29
|
-
input.e2eTotal > 0 &&
|
|
30
|
-
input.e2ePassed === input.e2eTotal;
|
|
31
|
-
const lighthousePassed = getLighthousePass(input.lighthouseScores, thresholds);
|
|
32
|
-
// Gold disqualifiers: console errors or critical security vulnerabilities
|
|
33
|
-
// mean the app has known problems — Gold should require a clean run.
|
|
34
|
-
const hasConsoleErrors = typeof input.e2eConsoleErrorCount === "number" && input.e2eConsoleErrorCount > 0;
|
|
35
|
-
const hasCriticalSecurity = !!input.securityAudit && input.securityAudit.critical > 0;
|
|
36
|
-
const goldDisqualified = hasConsoleErrors || hasCriticalSecurity;
|
|
37
|
-
if (buildPassed && e2ePassedAll && lighthousePassed && !goldDisqualified)
|
|
38
|
-
return "gold";
|
|
39
|
-
if (buildPassed && e2ePassedAll)
|
|
40
|
-
return "silver";
|
|
41
|
-
if (buildPassed)
|
|
42
|
-
return "bronze";
|
|
43
|
-
return "unverified";
|
|
44
|
-
}
|
|
45
|
-
function buildVerificationEvidence(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
46
|
-
const buildPassed = input.buildSuccess === true;
|
|
47
|
-
const hasE2EData = typeof input.e2eTotal === "number" && input.e2eTotal > 0;
|
|
48
|
-
const hasLighthouseData = !!input.lighthouseScores;
|
|
49
|
-
const lighthouseSkipped = input.lighthouseSkipped === true;
|
|
50
|
-
const e2ePassedAll = hasE2EData &&
|
|
51
|
-
typeof input.e2ePassed === "number" &&
|
|
52
|
-
typeof input.e2eTotal === "number" &&
|
|
53
|
-
input.e2ePassed === input.e2eTotal;
|
|
54
|
-
const hasMultiViewportData = typeof input.viewportIssues === "number" || typeof input.multiViewportPassed === "boolean";
|
|
55
|
-
const multiViewportPassed = hasMultiViewportData
|
|
56
|
-
? input.multiViewportPassed === true ||
|
|
57
|
-
(input.multiViewportPassed !== false && (input.viewportIssues ?? 0) <= 0)
|
|
58
|
-
: false;
|
|
59
|
-
const hasVisualDiffData = typeof input.visualDiffVerdict === "string";
|
|
60
|
-
const hasComparableVisualDiffData = hasVisualDiffData && input.hasVisualBaseline === true;
|
|
61
|
-
const visualDiffPassed = hasComparableVisualDiffData &&
|
|
62
|
-
input.visualDiffVerdict !== "warn" &&
|
|
63
|
-
input.visualDiffVerdict !== "rollback";
|
|
64
|
-
const lighthousePassed = getLighthousePass(input.lighthouseScores, thresholds);
|
|
65
|
-
const e2eStabilityPassed = input.e2eStabilityPassed !== false;
|
|
66
|
-
const hasConsoleErrors = typeof input.e2eConsoleErrorCount === "number" && input.e2eConsoleErrorCount > 0;
|
|
67
|
-
const hasSecurityData = !!input.securityAudit;
|
|
68
|
-
const securityPassed = hasSecurityData
|
|
69
|
-
? input.securityAudit.critical === 0 && input.securityAudit.high === 0
|
|
70
|
-
: true;
|
|
71
|
-
const hasMobileLighthouseData = !!input.mobileLighthouseScores;
|
|
72
|
-
const mobileLighthousePassed = hasMobileLighthouseData
|
|
73
|
-
? getLighthousePass(input.mobileLighthouseScores, thresholds)
|
|
74
|
-
: false;
|
|
75
|
-
const hasTypecheckData = !!input.typecheck && !input.typecheck.skipped;
|
|
76
|
-
const typecheckPassed = hasTypecheckData ? input.typecheck.passed : true;
|
|
77
|
-
const hasSecretScanData = !!input.secretScan && !input.secretScan.skipped;
|
|
78
|
-
const secretScanPassed = hasSecretScanData ? input.secretScan.passed : true;
|
|
79
|
-
const hasA11yDeepData = !!input.a11yDeep && !input.a11yDeep.skipped;
|
|
80
|
-
const a11yDeepPassed = hasA11yDeepData ? input.a11yDeep.passed : true;
|
|
81
|
-
const hasSeoDeepData = !!input.seoDeep && !input.seoDeep.skipped;
|
|
82
|
-
const seoDeepPassed = hasSeoDeepData ? input.seoDeep.passed : true;
|
|
83
|
-
const hasVitalsBudgetData = !!input.vitalsBudget && !input.vitalsBudget.skipped;
|
|
84
|
-
const vitalsBudgetPassed = hasVitalsBudgetData ? input.vitalsBudget.passed : true;
|
|
85
|
-
return {
|
|
86
|
-
input,
|
|
87
|
-
thresholds,
|
|
88
|
-
buildPassed,
|
|
89
|
-
hasE2EData,
|
|
90
|
-
e2ePassedAll,
|
|
91
|
-
e2eStabilityPassed,
|
|
92
|
-
hasLighthouseData,
|
|
93
|
-
lighthouseSkipped,
|
|
94
|
-
hasMultiViewportData,
|
|
95
|
-
multiViewportPassed,
|
|
96
|
-
hasVisualDiffData,
|
|
97
|
-
hasComparableVisualDiffData,
|
|
98
|
-
visualDiffPassed,
|
|
99
|
-
lighthousePassed,
|
|
100
|
-
hasConsoleErrors,
|
|
101
|
-
hasSecurityData,
|
|
102
|
-
securityPassed,
|
|
103
|
-
hasMobileLighthouseData,
|
|
104
|
-
mobileLighthousePassed,
|
|
105
|
-
hasTypecheckData,
|
|
106
|
-
typecheckPassed,
|
|
107
|
-
hasSecretScanData,
|
|
108
|
-
secretScanPassed,
|
|
109
|
-
hasA11yDeepData,
|
|
110
|
-
a11yDeepPassed,
|
|
111
|
-
hasSeoDeepData,
|
|
112
|
-
seoDeepPassed,
|
|
113
|
-
hasVitalsBudgetData,
|
|
114
|
-
vitalsBudgetPassed,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function getImprovementRecommendations(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
118
|
-
const findings = [];
|
|
119
|
-
const errors = input.buildErrors ?? [];
|
|
120
|
-
if (input.buildSuccess === false) {
|
|
121
|
-
if (errors.some((error) => /TS\d+|type/i.test(error))) {
|
|
122
|
-
findings.push({
|
|
123
|
-
category: "build",
|
|
124
|
-
severity: "critical",
|
|
125
|
-
title: "TypeScript build errors",
|
|
126
|
-
description: "Type errors are blocking a clean production build.",
|
|
127
|
-
action: "Fix the TypeScript errors first, then rerun verification.",
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
if (errors.some((error) => /Module not found|Cannot find module|Failed to resolve/i.test(error))) {
|
|
131
|
-
findings.push({
|
|
132
|
-
category: "build",
|
|
133
|
-
severity: "critical",
|
|
134
|
-
title: "Missing or unresolved modules",
|
|
135
|
-
description: "The build cannot resolve one or more imports or packages.",
|
|
136
|
-
action: "Check import paths, package installation, and package.json consistency.",
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
if (errors.some((error) => /SyntaxError|Unexpected token/i.test(error))) {
|
|
140
|
-
findings.push({
|
|
141
|
-
category: "build",
|
|
142
|
-
severity: "critical",
|
|
143
|
-
title: "Syntax errors in source code",
|
|
144
|
-
description: "The code contains syntax issues that stop the build from completing.",
|
|
145
|
-
action: "Fix the syntax errors, then rerun the build verification.",
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
if (findings.every((finding) => finding.category !== "build")) {
|
|
149
|
-
findings.push({
|
|
150
|
-
category: "build",
|
|
151
|
-
severity: "critical",
|
|
152
|
-
title: "Build failed",
|
|
153
|
-
description: "Production build verification did not pass.",
|
|
154
|
-
action: "Inspect the build logs and resolve the blocking errors before release.",
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (typeof input.e2ePassed === "number" &&
|
|
159
|
-
typeof input.e2eTotal === "number" &&
|
|
160
|
-
input.e2eTotal > 0 &&
|
|
161
|
-
input.e2ePassed < input.e2eTotal) {
|
|
162
|
-
const failedCount = input.e2eTotal - input.e2ePassed;
|
|
163
|
-
findings.push({
|
|
164
|
-
category: "e2e",
|
|
165
|
-
severity: "high",
|
|
166
|
-
title: `E2E failures (${failedCount}/${input.e2eTotal})`,
|
|
167
|
-
description: "One or more verification scenarios failed.",
|
|
168
|
-
action: "Fix the broken user flow and rerun the verification scenarios.",
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if ((input.e2eCoverageGaps?.length ?? 0) > 0) {
|
|
172
|
-
findings.push({
|
|
173
|
-
category: "e2e",
|
|
174
|
-
severity: "high",
|
|
175
|
-
title: `Verification coverage gaps (${input.e2eCoverageGaps?.length ?? 0})`,
|
|
176
|
-
description: input.e2eCoverageGaps.join(" "),
|
|
177
|
-
action: "Add or expose a stable primary user flow, then rerun verification so the checks cover real interactions.",
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
if (input.e2eStabilityPassed === false) {
|
|
181
|
-
findings.push({
|
|
182
|
-
category: "e2e",
|
|
183
|
-
severity: "high",
|
|
184
|
-
title: "E2E stability check failed",
|
|
185
|
-
description: "E2E scenarios passed on the first run but failed on the second stability run.",
|
|
186
|
-
action: "Fix flaky tests or unstable application state, then rerun verification.",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
// Runtime console errors — answers "이거 당장 크게 터지나?"
|
|
190
|
-
if (typeof input.e2eConsoleErrorCount === "number" && input.e2eConsoleErrorCount > 0) {
|
|
191
|
-
findings.push({
|
|
192
|
-
category: "runtime",
|
|
193
|
-
severity: input.e2eConsoleErrorCount >= 3 ? "high" : "medium",
|
|
194
|
-
title: `Runtime console errors detected (${input.e2eConsoleErrorCount})`,
|
|
195
|
-
description: "The app emits console errors during E2E verification, indicating runtime issues.",
|
|
196
|
-
action: "Open browser DevTools, fix the console errors, and rerun verification.",
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
// Security audit — answers "클라이언트한테 보내도 되는 수준인가?"
|
|
200
|
-
if (input.securityAudit) {
|
|
201
|
-
const audit = input.securityAudit;
|
|
202
|
-
if (audit.critical > 0) {
|
|
203
|
-
findings.push({
|
|
204
|
-
category: "security",
|
|
205
|
-
severity: "critical",
|
|
206
|
-
title: `Critical security vulnerabilities (${audit.critical})`,
|
|
207
|
-
description: `npm audit found ${audit.critical} critical vulnerabilities: ${audit.summary}`,
|
|
208
|
-
action: "Run npm audit fix or update the vulnerable packages before deployment.",
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
else if (audit.high > 0) {
|
|
212
|
-
findings.push({
|
|
213
|
-
category: "security",
|
|
214
|
-
severity: "high",
|
|
215
|
-
title: `High security vulnerabilities (${audit.high})`,
|
|
216
|
-
description: `npm audit found ${audit.high} high-severity vulnerabilities: ${audit.summary}`,
|
|
217
|
-
action: "Run npm audit fix or update the vulnerable packages.",
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
else if (audit.totalVulnerabilities > 0) {
|
|
221
|
-
findings.push({
|
|
222
|
-
category: "security",
|
|
223
|
-
severity: "medium",
|
|
224
|
-
title: `Security vulnerabilities (${audit.totalVulnerabilities})`,
|
|
225
|
-
description: `npm audit found moderate/low vulnerabilities: ${audit.summary}`,
|
|
226
|
-
action: "Review npm audit output and update packages when convenient.",
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// Broken links audit
|
|
231
|
-
if (input.brokenLinksAudit && input.brokenLinksAudit.brokenCount > 0) {
|
|
232
|
-
findings.push({
|
|
233
|
-
category: "security",
|
|
234
|
-
severity: "high",
|
|
235
|
-
title: `Broken links detected (${input.brokenLinksAudit.brokenCount}/${input.brokenLinksAudit.checkedCount})`,
|
|
236
|
-
description: `${input.brokenLinksAudit.brokenCount} link(s) returned non-OK status codes. Users will hit 404/500 errors.`,
|
|
237
|
-
action: "Fix or remove the broken links and rerun the verification.",
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
// Mobile Lighthouse — Pro tier mobile check
|
|
241
|
-
if (input.mobileLighthouseScores) {
|
|
242
|
-
const mobileScores = input.mobileLighthouseScores;
|
|
243
|
-
if (mobileScores.performance < thresholds.performance) {
|
|
244
|
-
findings.push({
|
|
245
|
-
category: "performance",
|
|
246
|
-
severity: "high",
|
|
247
|
-
title: `Mobile performance below threshold (${mobileScores.performance} / ${thresholds.performance})`,
|
|
248
|
-
description: "Mobile users will experience slow load times or poor interactivity.",
|
|
249
|
-
action: "Optimize mobile performance: reduce JS bundles, defer non-critical assets, optimize images.",
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
if (mobileScores.accessibility < thresholds.accessibility) {
|
|
253
|
-
findings.push({
|
|
254
|
-
category: "accessibility",
|
|
255
|
-
severity: "high",
|
|
256
|
-
title: `Mobile accessibility below threshold (${mobileScores.accessibility} / ${thresholds.accessibility})`,
|
|
257
|
-
description: "Mobile accessibility issues detected that may block users.",
|
|
258
|
-
action: "Fix touch targets, contrast, and semantic structure for mobile layouts.",
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const hasMultiViewportData = typeof input.viewportIssues === "number" || typeof input.multiViewportPassed === "boolean";
|
|
263
|
-
const multiViewportPassed = input.multiViewportPassed === true ||
|
|
264
|
-
(input.multiViewportPassed !== false && (input.viewportIssues ?? 0) <= 0);
|
|
265
|
-
if (hasMultiViewportData && !multiViewportPassed) {
|
|
266
|
-
findings.push({
|
|
267
|
-
category: "viewport",
|
|
268
|
-
severity: "high",
|
|
269
|
-
title: `Multi-viewport issues detected (${input.viewportIssues ?? 0})`,
|
|
270
|
-
description: input.multiViewportSummary ||
|
|
271
|
-
"One or more responsive layout or viewport-specific verification issues were found.",
|
|
272
|
-
action: "Fix the responsive layout issues and rerun the multi-viewport verification pass.",
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
if (input.visualDiffVerdict === "rollback") {
|
|
276
|
-
findings.push({
|
|
277
|
-
category: "visual",
|
|
278
|
-
severity: "high",
|
|
279
|
-
title: `Visual regression detected (${input.visualDiffPercentage ?? 0}%)`,
|
|
280
|
-
description: "The visual diff is large enough to recommend a rollback or release hold.",
|
|
281
|
-
action: "Review the visual diff artifacts and fix the unintended UI regression before release.",
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
else if (input.visualDiffVerdict === "warn") {
|
|
285
|
-
findings.push({
|
|
286
|
-
category: "visual",
|
|
287
|
-
severity: "medium",
|
|
288
|
-
title: `Visual change needs review (${input.visualDiffPercentage ?? 0}%)`,
|
|
289
|
-
description: "The visual diff changed enough to require a manual review before release.",
|
|
290
|
-
action: "Check the visual diff and confirm the UI change is intentional.",
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
if ((input.lighthouseErrorCount ?? 0) > 0) {
|
|
294
|
-
findings.push({
|
|
295
|
-
category: "performance",
|
|
296
|
-
severity: "medium",
|
|
297
|
-
title: `Lighthouse instability detected (${input.lighthouseErrorCount})`,
|
|
298
|
-
description: "One or more Lighthouse runs errored even though a report was recovered.",
|
|
299
|
-
action: "Rerun Lighthouse and inspect the failing run logs before trusting this result in CI.",
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
// TypeScript type errors — blocker-capable
|
|
303
|
-
if (input.typecheck && !input.typecheck.skipped && !input.typecheck.passed) {
|
|
304
|
-
findings.push({
|
|
305
|
-
category: "typecheck",
|
|
306
|
-
severity: input.typecheck.errorCount >= 5 ? "high" : "medium",
|
|
307
|
-
title: `TypeScript type errors (${input.typecheck.errorCount})`,
|
|
308
|
-
description: "Type errors indicate potential runtime bugs that the build pipeline may not catch.",
|
|
309
|
-
action: "Fix the TypeScript errors and rerun verification.",
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
// Secret scan — blocker-capable
|
|
313
|
-
if (input.secretScan && !input.secretScan.skipped && !input.secretScan.passed) {
|
|
314
|
-
findings.push({
|
|
315
|
-
category: "secrets",
|
|
316
|
-
severity: "high",
|
|
317
|
-
title: `Hardcoded secrets detected (${input.secretScan.findingsCount})`,
|
|
318
|
-
description: `Found ${input.secretScan.findingsCount} potential secret(s) in ${input.secretScan.filesScanned} scanned files. Leaked secrets are a deployment risk.`,
|
|
319
|
-
action: "Move secrets to environment variables and rotate any compromised credentials.",
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
// Deep accessibility — blocker-capable
|
|
323
|
-
if (input.a11yDeep && !input.a11yDeep.skipped && !input.a11yDeep.passed) {
|
|
324
|
-
const critical = input.a11yDeep.criticalCount;
|
|
325
|
-
const serious = input.a11yDeep.seriousCount;
|
|
326
|
-
findings.push({
|
|
327
|
-
category: "accessibility",
|
|
328
|
-
severity: critical > 0 ? "high" : "medium",
|
|
329
|
-
title: `WCAG violations found (${critical} critical, ${serious} serious)`,
|
|
330
|
-
description: input.a11yDeep.summary,
|
|
331
|
-
action: "Fix the critical and serious WCAG violations before release.",
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
// Deep SEO — advisory
|
|
335
|
-
if (input.seoDeep && !input.seoDeep.skipped && !input.seoDeep.passed) {
|
|
336
|
-
findings.push({
|
|
337
|
-
category: "seo",
|
|
338
|
-
severity: "medium",
|
|
339
|
-
title: `SEO issues detected (${input.seoDeep.errorCount} errors, ${input.seoDeep.warningCount} warnings)`,
|
|
340
|
-
description: input.seoDeep.summary,
|
|
341
|
-
action: "Fix missing or incorrect SEO meta tags.",
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
// Vitals budget — advisory
|
|
345
|
-
if (input.vitalsBudget && !input.vitalsBudget.skipped && !input.vitalsBudget.passed) {
|
|
346
|
-
findings.push({
|
|
347
|
-
category: "vitals",
|
|
348
|
-
severity: "medium",
|
|
349
|
-
title: `Core Web Vitals budget exceeded`,
|
|
350
|
-
description: input.vitalsBudget.summary,
|
|
351
|
-
action: "Optimize performance to meet the Core Web Vitals budget thresholds.",
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
// Outdated dependencies — advisory
|
|
355
|
-
if (input.outdatedCheck && !input.outdatedCheck.skipped && input.outdatedCheck.majorOutdated > 0) {
|
|
356
|
-
findings.push({
|
|
357
|
-
category: "bestPractices",
|
|
358
|
-
severity: "medium",
|
|
359
|
-
title: `${input.outdatedCheck.majorOutdated} major version(s) behind latest`,
|
|
360
|
-
description: input.outdatedCheck.advisory,
|
|
361
|
-
action: "Update outdated dependencies, especially those with major version gaps.",
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
// Bundle size — advisory (only if thresholds exceeded)
|
|
365
|
-
if (input.bundleSize && !input.bundleSize.skipped) {
|
|
366
|
-
const firstLoad = input.bundleSize.firstLoadJsKb;
|
|
367
|
-
const largest = input.bundleSize.largestChunkKb;
|
|
368
|
-
if ((firstLoad !== null && firstLoad > 200) || (largest !== null && largest > 300)) {
|
|
369
|
-
findings.push({
|
|
370
|
-
category: "performance",
|
|
371
|
-
severity: "medium",
|
|
372
|
-
title: `Bundle size advisory threshold exceeded`,
|
|
373
|
-
description: input.bundleSize.advisory,
|
|
374
|
-
action: "Reduce bundle size by code-splitting, tree-shaking, or lazy-loading large dependencies.",
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
const lighthouseScores = input.lighthouseScores;
|
|
379
|
-
if (!lighthouseScores) {
|
|
380
|
-
return findings;
|
|
381
|
-
}
|
|
382
|
-
const lighthouseFinding = (category, actual, required, title, description, action) => ({
|
|
383
|
-
category,
|
|
384
|
-
severity: required - actual >= 20 ? "high" : "medium",
|
|
385
|
-
title: `${title} (${actual} / ${required})`,
|
|
386
|
-
description,
|
|
387
|
-
action,
|
|
388
|
-
});
|
|
389
|
-
if (lighthouseScores.performance < thresholds.performance) {
|
|
390
|
-
findings.push(lighthouseFinding("performance", lighthouseScores.performance, thresholds.performance, "Performance below threshold", "Runtime performance is below the minimum verification threshold.", "Reduce heavy assets, expensive scripts, and blocking work on initial load."));
|
|
391
|
-
}
|
|
392
|
-
if (lighthouseScores.accessibility < thresholds.accessibility) {
|
|
393
|
-
findings.push(lighthouseFinding("accessibility", lighthouseScores.accessibility, thresholds.accessibility, "Accessibility below threshold", "Accessibility checks are below the minimum verification threshold.", "Fix labels, semantics, contrast, and keyboard accessibility issues."));
|
|
394
|
-
}
|
|
395
|
-
if (lighthouseScores.seo < thresholds.seo) {
|
|
396
|
-
findings.push(lighthouseFinding("seo", lighthouseScores.seo, thresholds.seo, "SEO below threshold", "SEO checks are below the minimum verification threshold.", "Fix title, description, crawl settings, and indexable metadata."));
|
|
397
|
-
}
|
|
398
|
-
if (lighthouseScores.bestPractices < thresholds.bestPractices) {
|
|
399
|
-
findings.push(lighthouseFinding("bestPractices", lighthouseScores.bestPractices, thresholds.bestPractices, "Best practices below threshold", "Best practices checks are below the minimum verification threshold.", "Fix browser warnings, unsafe patterns, and platform-level issues."));
|
|
400
|
-
}
|
|
401
|
-
return findings.sort((a, b) => {
|
|
402
|
-
const priority = { critical: 0, high: 1, medium: 2 };
|
|
403
|
-
return priority[a.severity] - priority[b.severity];
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
function buildVerificationReport(input, options) {
|
|
407
|
-
const thresholds = options?.thresholds ?? exports.DEFAULT_LH_THRESHOLDS;
|
|
408
|
-
const tier = options?.tier ?? "free";
|
|
409
|
-
const evidence = buildVerificationEvidence(input, thresholds);
|
|
410
|
-
const findings = getImprovementRecommendations(input, thresholds);
|
|
411
|
-
const blockers = findings.filter((finding) => finding.severity === "critical" || finding.severity === "high");
|
|
412
|
-
const warnings = findings.filter((finding) => finding.severity === "medium");
|
|
413
|
-
const hasWarnings = warnings.length > 0;
|
|
414
|
-
const coreChecksPassed = evidence.buildPassed && evidence.e2ePassedAll && evidence.lighthousePassed;
|
|
415
|
-
const teamEvidenceComplete = evidence.hasMultiViewportData &&
|
|
416
|
-
evidence.multiViewportPassed &&
|
|
417
|
-
evidence.hasComparableVisualDiffData &&
|
|
418
|
-
evidence.visualDiffPassed;
|
|
419
|
-
const grade = getVerificationGrade(input, thresholds);
|
|
420
|
-
const failureEvidence = (input.failureEvidence ?? []).filter(Boolean).slice(0, 5);
|
|
421
|
-
let verdict;
|
|
422
|
-
let confidence;
|
|
423
|
-
let summary;
|
|
424
|
-
if (!evidence.buildPassed) {
|
|
425
|
-
verdict = "build-failed";
|
|
426
|
-
confidence = "low";
|
|
427
|
-
summary = "Build failed. Fix the blocking build errors before relying on this verification result.";
|
|
428
|
-
}
|
|
429
|
-
else if (blockers.length > 0) {
|
|
430
|
-
verdict = "hold";
|
|
431
|
-
confidence = "medium";
|
|
432
|
-
summary = "Blocking verification issues were found. Hold release until the blockers are fixed.";
|
|
433
|
-
}
|
|
434
|
-
else if (tier === "team" && coreChecksPassed && !hasWarnings && teamEvidenceComplete) {
|
|
435
|
-
verdict = "release-ready";
|
|
436
|
-
confidence = "high";
|
|
437
|
-
summary = "All core checks and release-evidence checks passed. This run is strong enough to call release-ready.";
|
|
438
|
-
}
|
|
439
|
-
else if (coreChecksPassed && !hasWarnings) {
|
|
440
|
-
verdict = "client-ready";
|
|
441
|
-
confidence = "high";
|
|
442
|
-
summary = "No blocking issues found. Build, E2E, and Lighthouse checks passed.";
|
|
443
|
-
}
|
|
444
|
-
else if (coreChecksPassed && hasWarnings) {
|
|
445
|
-
verdict = "investigate";
|
|
446
|
-
confidence = "medium";
|
|
447
|
-
summary = "Core checks passed, but warning-level risks remain. Review warnings before calling this run client-ready.";
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
verdict = "quick-pass";
|
|
451
|
-
confidence = evidence.hasLighthouseData && evidence.lighthousePassed ? "medium" : "low";
|
|
452
|
-
summary = "No immediate hard blockers were found in the quick verification pass.";
|
|
453
|
-
}
|
|
454
|
-
const passes = [
|
|
455
|
-
{ key: "build", label: "Production build", passed: evidence.buildPassed },
|
|
456
|
-
...(evidence.hasE2EData
|
|
457
|
-
? [{ key: "e2e", label: `E2E ${input.e2ePassed ?? 0}/${input.e2eTotal ?? 0}`, passed: evidence.e2ePassedAll }]
|
|
458
|
-
: []),
|
|
459
|
-
...(evidence.hasConsoleErrors
|
|
460
|
-
? [{ key: "console-errors", label: `Console errors (${input.e2eConsoleErrorCount ?? 0})`, passed: false }]
|
|
461
|
-
: [{ key: "console-errors", label: "No console errors", passed: true }]),
|
|
462
|
-
...(evidence.hasSecurityData
|
|
463
|
-
? [{ key: "security", label: `Security (${input.securityAudit?.summary ?? "unknown"})`, passed: evidence.securityPassed }]
|
|
464
|
-
: []),
|
|
465
|
-
...(evidence.hasMobileLighthouseData
|
|
466
|
-
? [{ key: "mobile-lh", label: "Mobile Lighthouse", passed: evidence.mobileLighthousePassed }]
|
|
467
|
-
: []),
|
|
468
|
-
...(evidence.hasMultiViewportData
|
|
469
|
-
? [{
|
|
470
|
-
key: "viewport",
|
|
471
|
-
label: `Viewport ${input.viewportIssues ?? 0} issues`,
|
|
472
|
-
passed: evidence.multiViewportPassed,
|
|
473
|
-
}]
|
|
474
|
-
: []),
|
|
475
|
-
...(evidence.hasVisualDiffData
|
|
476
|
-
? [{
|
|
477
|
-
key: "visual",
|
|
478
|
-
label: input.hasVisualBaseline
|
|
479
|
-
? `Visual diff ${input.visualDiffPercentage ?? 0}%`
|
|
480
|
-
: "Visual baseline seeded",
|
|
481
|
-
passed: evidence.visualDiffPassed,
|
|
482
|
-
}]
|
|
483
|
-
: []),
|
|
484
|
-
...(evidence.hasLighthouseData
|
|
485
|
-
? [{ key: "lighthouse", label: "Lighthouse thresholds", passed: evidence.lighthousePassed }]
|
|
486
|
-
: []),
|
|
487
|
-
...(evidence.hasTypecheckData
|
|
488
|
-
? [{ key: "typecheck", label: `TypeScript (${input.typecheck?.errorCount ?? 0} errors)`, passed: evidence.typecheckPassed }]
|
|
489
|
-
: []),
|
|
490
|
-
...(evidence.hasSecretScanData
|
|
491
|
-
? [{ key: "secret-scan", label: `Secret scan (${input.secretScan?.findingsCount ?? 0} findings)`, passed: evidence.secretScanPassed }]
|
|
492
|
-
: []),
|
|
493
|
-
...(evidence.hasA11yDeepData
|
|
494
|
-
? [{ key: "a11y-deep", label: `WCAG deep (${input.a11yDeep?.criticalCount ?? 0} critical)`, passed: evidence.a11yDeepPassed }]
|
|
495
|
-
: []),
|
|
496
|
-
...(evidence.hasSeoDeepData
|
|
497
|
-
? [{ key: "seo-deep", label: `SEO deep (${input.seoDeep?.errorCount ?? 0} errors)`, passed: evidence.seoDeepPassed }]
|
|
498
|
-
: []),
|
|
499
|
-
...(evidence.hasVitalsBudgetData
|
|
500
|
-
? [{ key: "vitals-budget", label: "Core Web Vitals budget", passed: evidence.vitalsBudgetPassed }]
|
|
501
|
-
: []),
|
|
502
|
-
...(input.bundleSize && !input.bundleSize.skipped
|
|
503
|
-
? [{ key: "bundle-size", label: `Bundle size (${input.bundleSize.framework})`, passed: true }]
|
|
504
|
-
: []),
|
|
505
|
-
...(input.outdatedCheck && !input.outdatedCheck.skipped
|
|
506
|
-
? [{ key: "outdated-check", label: `Outdated deps (${input.outdatedCheck.outdatedCount} outdated)`, passed: input.outdatedCheck.majorOutdated === 0 }]
|
|
507
|
-
: []),
|
|
508
|
-
];
|
|
509
|
-
const nextActions = [...blockers, ...warnings].slice(0, 4).map((finding) => finding.action);
|
|
510
|
-
return {
|
|
511
|
-
tier,
|
|
512
|
-
verdict,
|
|
513
|
-
confidence,
|
|
514
|
-
summary,
|
|
515
|
-
grade,
|
|
516
|
-
blockers,
|
|
517
|
-
warnings,
|
|
518
|
-
passes,
|
|
519
|
-
nextActions,
|
|
520
|
-
failureEvidence,
|
|
521
|
-
evidence,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
function buildTierVerificationView(input, options) {
|
|
525
|
-
return (0, tier_policy_js_1.getTierVerificationView)(buildVerificationReport(input, options));
|
|
526
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_LH_THRESHOLDS = void 0;
|
|
4
|
+
exports.getLighthousePass = getLighthousePass;
|
|
5
|
+
exports.getVerificationGrade = getVerificationGrade;
|
|
6
|
+
exports.buildVerificationEvidence = buildVerificationEvidence;
|
|
7
|
+
exports.getImprovementRecommendations = getImprovementRecommendations;
|
|
8
|
+
exports.buildVerificationReport = buildVerificationReport;
|
|
9
|
+
exports.buildTierVerificationView = buildTierVerificationView;
|
|
10
|
+
const tier_policy_js_1 = require("./tier-policy.js");
|
|
11
|
+
exports.DEFAULT_LH_THRESHOLDS = {
|
|
12
|
+
performance: 70,
|
|
13
|
+
accessibility: 85,
|
|
14
|
+
seo: 80,
|
|
15
|
+
bestPractices: 80,
|
|
16
|
+
};
|
|
17
|
+
function getLighthousePass(lighthouseScores, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
18
|
+
if (!lighthouseScores)
|
|
19
|
+
return false;
|
|
20
|
+
return (lighthouseScores.performance >= thresholds.performance &&
|
|
21
|
+
lighthouseScores.accessibility >= thresholds.accessibility &&
|
|
22
|
+
lighthouseScores.seo >= thresholds.seo &&
|
|
23
|
+
lighthouseScores.bestPractices >= thresholds.bestPractices);
|
|
24
|
+
}
|
|
25
|
+
function getVerificationGrade(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
26
|
+
const buildPassed = input.buildSuccess === true;
|
|
27
|
+
const e2ePassedAll = typeof input.e2ePassed === "number" &&
|
|
28
|
+
typeof input.e2eTotal === "number" &&
|
|
29
|
+
input.e2eTotal > 0 &&
|
|
30
|
+
input.e2ePassed === input.e2eTotal;
|
|
31
|
+
const lighthousePassed = getLighthousePass(input.lighthouseScores, thresholds);
|
|
32
|
+
// Gold disqualifiers: console errors or critical security vulnerabilities
|
|
33
|
+
// mean the app has known problems — Gold should require a clean run.
|
|
34
|
+
const hasConsoleErrors = typeof input.e2eConsoleErrorCount === "number" && input.e2eConsoleErrorCount > 0;
|
|
35
|
+
const hasCriticalSecurity = !!input.securityAudit && input.securityAudit.critical > 0;
|
|
36
|
+
const goldDisqualified = hasConsoleErrors || hasCriticalSecurity;
|
|
37
|
+
if (buildPassed && e2ePassedAll && lighthousePassed && !goldDisqualified)
|
|
38
|
+
return "gold";
|
|
39
|
+
if (buildPassed && e2ePassedAll)
|
|
40
|
+
return "silver";
|
|
41
|
+
if (buildPassed)
|
|
42
|
+
return "bronze";
|
|
43
|
+
return "unverified";
|
|
44
|
+
}
|
|
45
|
+
function buildVerificationEvidence(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
46
|
+
const buildPassed = input.buildSuccess === true;
|
|
47
|
+
const hasE2EData = typeof input.e2eTotal === "number" && input.e2eTotal > 0;
|
|
48
|
+
const hasLighthouseData = !!input.lighthouseScores;
|
|
49
|
+
const lighthouseSkipped = input.lighthouseSkipped === true;
|
|
50
|
+
const e2ePassedAll = hasE2EData &&
|
|
51
|
+
typeof input.e2ePassed === "number" &&
|
|
52
|
+
typeof input.e2eTotal === "number" &&
|
|
53
|
+
input.e2ePassed === input.e2eTotal;
|
|
54
|
+
const hasMultiViewportData = typeof input.viewportIssues === "number" || typeof input.multiViewportPassed === "boolean";
|
|
55
|
+
const multiViewportPassed = hasMultiViewportData
|
|
56
|
+
? input.multiViewportPassed === true ||
|
|
57
|
+
(input.multiViewportPassed !== false && (input.viewportIssues ?? 0) <= 0)
|
|
58
|
+
: false;
|
|
59
|
+
const hasVisualDiffData = typeof input.visualDiffVerdict === "string";
|
|
60
|
+
const hasComparableVisualDiffData = hasVisualDiffData && input.hasVisualBaseline === true;
|
|
61
|
+
const visualDiffPassed = hasComparableVisualDiffData &&
|
|
62
|
+
input.visualDiffVerdict !== "warn" &&
|
|
63
|
+
input.visualDiffVerdict !== "rollback";
|
|
64
|
+
const lighthousePassed = getLighthousePass(input.lighthouseScores, thresholds);
|
|
65
|
+
const e2eStabilityPassed = input.e2eStabilityPassed !== false;
|
|
66
|
+
const hasConsoleErrors = typeof input.e2eConsoleErrorCount === "number" && input.e2eConsoleErrorCount > 0;
|
|
67
|
+
const hasSecurityData = !!input.securityAudit;
|
|
68
|
+
const securityPassed = hasSecurityData
|
|
69
|
+
? input.securityAudit.critical === 0 && input.securityAudit.high === 0
|
|
70
|
+
: true;
|
|
71
|
+
const hasMobileLighthouseData = !!input.mobileLighthouseScores;
|
|
72
|
+
const mobileLighthousePassed = hasMobileLighthouseData
|
|
73
|
+
? getLighthousePass(input.mobileLighthouseScores, thresholds)
|
|
74
|
+
: false;
|
|
75
|
+
const hasTypecheckData = !!input.typecheck && !input.typecheck.skipped;
|
|
76
|
+
const typecheckPassed = hasTypecheckData ? input.typecheck.passed : true;
|
|
77
|
+
const hasSecretScanData = !!input.secretScan && !input.secretScan.skipped;
|
|
78
|
+
const secretScanPassed = hasSecretScanData ? input.secretScan.passed : true;
|
|
79
|
+
const hasA11yDeepData = !!input.a11yDeep && !input.a11yDeep.skipped;
|
|
80
|
+
const a11yDeepPassed = hasA11yDeepData ? input.a11yDeep.passed : true;
|
|
81
|
+
const hasSeoDeepData = !!input.seoDeep && !input.seoDeep.skipped;
|
|
82
|
+
const seoDeepPassed = hasSeoDeepData ? input.seoDeep.passed : true;
|
|
83
|
+
const hasVitalsBudgetData = !!input.vitalsBudget && !input.vitalsBudget.skipped;
|
|
84
|
+
const vitalsBudgetPassed = hasVitalsBudgetData ? input.vitalsBudget.passed : true;
|
|
85
|
+
return {
|
|
86
|
+
input,
|
|
87
|
+
thresholds,
|
|
88
|
+
buildPassed,
|
|
89
|
+
hasE2EData,
|
|
90
|
+
e2ePassedAll,
|
|
91
|
+
e2eStabilityPassed,
|
|
92
|
+
hasLighthouseData,
|
|
93
|
+
lighthouseSkipped,
|
|
94
|
+
hasMultiViewportData,
|
|
95
|
+
multiViewportPassed,
|
|
96
|
+
hasVisualDiffData,
|
|
97
|
+
hasComparableVisualDiffData,
|
|
98
|
+
visualDiffPassed,
|
|
99
|
+
lighthousePassed,
|
|
100
|
+
hasConsoleErrors,
|
|
101
|
+
hasSecurityData,
|
|
102
|
+
securityPassed,
|
|
103
|
+
hasMobileLighthouseData,
|
|
104
|
+
mobileLighthousePassed,
|
|
105
|
+
hasTypecheckData,
|
|
106
|
+
typecheckPassed,
|
|
107
|
+
hasSecretScanData,
|
|
108
|
+
secretScanPassed,
|
|
109
|
+
hasA11yDeepData,
|
|
110
|
+
a11yDeepPassed,
|
|
111
|
+
hasSeoDeepData,
|
|
112
|
+
seoDeepPassed,
|
|
113
|
+
hasVitalsBudgetData,
|
|
114
|
+
vitalsBudgetPassed,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function getImprovementRecommendations(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
|
|
118
|
+
const findings = [];
|
|
119
|
+
const errors = input.buildErrors ?? [];
|
|
120
|
+
if (input.buildSuccess === false) {
|
|
121
|
+
if (errors.some((error) => /TS\d+|type/i.test(error))) {
|
|
122
|
+
findings.push({
|
|
123
|
+
category: "build",
|
|
124
|
+
severity: "critical",
|
|
125
|
+
title: "TypeScript build errors",
|
|
126
|
+
description: "Type errors are blocking a clean production build.",
|
|
127
|
+
action: "Fix the TypeScript errors first, then rerun verification.",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (errors.some((error) => /Module not found|Cannot find module|Failed to resolve/i.test(error))) {
|
|
131
|
+
findings.push({
|
|
132
|
+
category: "build",
|
|
133
|
+
severity: "critical",
|
|
134
|
+
title: "Missing or unresolved modules",
|
|
135
|
+
description: "The build cannot resolve one or more imports or packages.",
|
|
136
|
+
action: "Check import paths, package installation, and package.json consistency.",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (errors.some((error) => /SyntaxError|Unexpected token/i.test(error))) {
|
|
140
|
+
findings.push({
|
|
141
|
+
category: "build",
|
|
142
|
+
severity: "critical",
|
|
143
|
+
title: "Syntax errors in source code",
|
|
144
|
+
description: "The code contains syntax issues that stop the build from completing.",
|
|
145
|
+
action: "Fix the syntax errors, then rerun the build verification.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (findings.every((finding) => finding.category !== "build")) {
|
|
149
|
+
findings.push({
|
|
150
|
+
category: "build",
|
|
151
|
+
severity: "critical",
|
|
152
|
+
title: "Build failed",
|
|
153
|
+
description: "Production build verification did not pass.",
|
|
154
|
+
action: "Inspect the build logs and resolve the blocking errors before release.",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (typeof input.e2ePassed === "number" &&
|
|
159
|
+
typeof input.e2eTotal === "number" &&
|
|
160
|
+
input.e2eTotal > 0 &&
|
|
161
|
+
input.e2ePassed < input.e2eTotal) {
|
|
162
|
+
const failedCount = input.e2eTotal - input.e2ePassed;
|
|
163
|
+
findings.push({
|
|
164
|
+
category: "e2e",
|
|
165
|
+
severity: "high",
|
|
166
|
+
title: `E2E failures (${failedCount}/${input.e2eTotal})`,
|
|
167
|
+
description: "One or more verification scenarios failed.",
|
|
168
|
+
action: "Fix the broken user flow and rerun the verification scenarios.",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if ((input.e2eCoverageGaps?.length ?? 0) > 0) {
|
|
172
|
+
findings.push({
|
|
173
|
+
category: "e2e",
|
|
174
|
+
severity: "high",
|
|
175
|
+
title: `Verification coverage gaps (${input.e2eCoverageGaps?.length ?? 0})`,
|
|
176
|
+
description: input.e2eCoverageGaps.join(" "),
|
|
177
|
+
action: "Add or expose a stable primary user flow, then rerun verification so the checks cover real interactions.",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (input.e2eStabilityPassed === false) {
|
|
181
|
+
findings.push({
|
|
182
|
+
category: "e2e",
|
|
183
|
+
severity: "high",
|
|
184
|
+
title: "E2E stability check failed",
|
|
185
|
+
description: "E2E scenarios passed on the first run but failed on the second stability run.",
|
|
186
|
+
action: "Fix flaky tests or unstable application state, then rerun verification.",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Runtime console errors — answers "이거 당장 크게 터지나?"
|
|
190
|
+
if (typeof input.e2eConsoleErrorCount === "number" && input.e2eConsoleErrorCount > 0) {
|
|
191
|
+
findings.push({
|
|
192
|
+
category: "runtime",
|
|
193
|
+
severity: input.e2eConsoleErrorCount >= 3 ? "high" : "medium",
|
|
194
|
+
title: `Runtime console errors detected (${input.e2eConsoleErrorCount})`,
|
|
195
|
+
description: "The app emits console errors during E2E verification, indicating runtime issues.",
|
|
196
|
+
action: "Open browser DevTools, fix the console errors, and rerun verification.",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Security audit — answers "클라이언트한테 보내도 되는 수준인가?"
|
|
200
|
+
if (input.securityAudit) {
|
|
201
|
+
const audit = input.securityAudit;
|
|
202
|
+
if (audit.critical > 0) {
|
|
203
|
+
findings.push({
|
|
204
|
+
category: "security",
|
|
205
|
+
severity: "critical",
|
|
206
|
+
title: `Critical security vulnerabilities (${audit.critical})`,
|
|
207
|
+
description: `npm audit found ${audit.critical} critical vulnerabilities: ${audit.summary}`,
|
|
208
|
+
action: "Run npm audit fix or update the vulnerable packages before deployment.",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else if (audit.high > 0) {
|
|
212
|
+
findings.push({
|
|
213
|
+
category: "security",
|
|
214
|
+
severity: "high",
|
|
215
|
+
title: `High security vulnerabilities (${audit.high})`,
|
|
216
|
+
description: `npm audit found ${audit.high} high-severity vulnerabilities: ${audit.summary}`,
|
|
217
|
+
action: "Run npm audit fix or update the vulnerable packages.",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else if (audit.totalVulnerabilities > 0) {
|
|
221
|
+
findings.push({
|
|
222
|
+
category: "security",
|
|
223
|
+
severity: "medium",
|
|
224
|
+
title: `Security vulnerabilities (${audit.totalVulnerabilities})`,
|
|
225
|
+
description: `npm audit found moderate/low vulnerabilities: ${audit.summary}`,
|
|
226
|
+
action: "Review npm audit output and update packages when convenient.",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Broken links audit
|
|
231
|
+
if (input.brokenLinksAudit && input.brokenLinksAudit.brokenCount > 0) {
|
|
232
|
+
findings.push({
|
|
233
|
+
category: "security",
|
|
234
|
+
severity: "high",
|
|
235
|
+
title: `Broken links detected (${input.brokenLinksAudit.brokenCount}/${input.brokenLinksAudit.checkedCount})`,
|
|
236
|
+
description: `${input.brokenLinksAudit.brokenCount} link(s) returned non-OK status codes. Users will hit 404/500 errors.`,
|
|
237
|
+
action: "Fix or remove the broken links and rerun the verification.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// Mobile Lighthouse — Pro tier mobile check
|
|
241
|
+
if (input.mobileLighthouseScores) {
|
|
242
|
+
const mobileScores = input.mobileLighthouseScores;
|
|
243
|
+
if (mobileScores.performance < thresholds.performance) {
|
|
244
|
+
findings.push({
|
|
245
|
+
category: "performance",
|
|
246
|
+
severity: "high",
|
|
247
|
+
title: `Mobile performance below threshold (${mobileScores.performance} / ${thresholds.performance})`,
|
|
248
|
+
description: "Mobile users will experience slow load times or poor interactivity.",
|
|
249
|
+
action: "Optimize mobile performance: reduce JS bundles, defer non-critical assets, optimize images.",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (mobileScores.accessibility < thresholds.accessibility) {
|
|
253
|
+
findings.push({
|
|
254
|
+
category: "accessibility",
|
|
255
|
+
severity: "high",
|
|
256
|
+
title: `Mobile accessibility below threshold (${mobileScores.accessibility} / ${thresholds.accessibility})`,
|
|
257
|
+
description: "Mobile accessibility issues detected that may block users.",
|
|
258
|
+
action: "Fix touch targets, contrast, and semantic structure for mobile layouts.",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const hasMultiViewportData = typeof input.viewportIssues === "number" || typeof input.multiViewportPassed === "boolean";
|
|
263
|
+
const multiViewportPassed = input.multiViewportPassed === true ||
|
|
264
|
+
(input.multiViewportPassed !== false && (input.viewportIssues ?? 0) <= 0);
|
|
265
|
+
if (hasMultiViewportData && !multiViewportPassed) {
|
|
266
|
+
findings.push({
|
|
267
|
+
category: "viewport",
|
|
268
|
+
severity: "high",
|
|
269
|
+
title: `Multi-viewport issues detected (${input.viewportIssues ?? 0})`,
|
|
270
|
+
description: input.multiViewportSummary ||
|
|
271
|
+
"One or more responsive layout or viewport-specific verification issues were found.",
|
|
272
|
+
action: "Fix the responsive layout issues and rerun the multi-viewport verification pass.",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (input.visualDiffVerdict === "rollback") {
|
|
276
|
+
findings.push({
|
|
277
|
+
category: "visual",
|
|
278
|
+
severity: "high",
|
|
279
|
+
title: `Visual regression detected (${input.visualDiffPercentage ?? 0}%)`,
|
|
280
|
+
description: "The visual diff is large enough to recommend a rollback or release hold.",
|
|
281
|
+
action: "Review the visual diff artifacts and fix the unintended UI regression before release.",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else if (input.visualDiffVerdict === "warn") {
|
|
285
|
+
findings.push({
|
|
286
|
+
category: "visual",
|
|
287
|
+
severity: "medium",
|
|
288
|
+
title: `Visual change needs review (${input.visualDiffPercentage ?? 0}%)`,
|
|
289
|
+
description: "The visual diff changed enough to require a manual review before release.",
|
|
290
|
+
action: "Check the visual diff and confirm the UI change is intentional.",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if ((input.lighthouseErrorCount ?? 0) > 0) {
|
|
294
|
+
findings.push({
|
|
295
|
+
category: "performance",
|
|
296
|
+
severity: "medium",
|
|
297
|
+
title: `Lighthouse instability detected (${input.lighthouseErrorCount})`,
|
|
298
|
+
description: "One or more Lighthouse runs errored even though a report was recovered.",
|
|
299
|
+
action: "Rerun Lighthouse and inspect the failing run logs before trusting this result in CI.",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
// TypeScript type errors — blocker-capable
|
|
303
|
+
if (input.typecheck && !input.typecheck.skipped && !input.typecheck.passed) {
|
|
304
|
+
findings.push({
|
|
305
|
+
category: "typecheck",
|
|
306
|
+
severity: input.typecheck.errorCount >= 5 ? "high" : "medium",
|
|
307
|
+
title: `TypeScript type errors (${input.typecheck.errorCount})`,
|
|
308
|
+
description: "Type errors indicate potential runtime bugs that the build pipeline may not catch.",
|
|
309
|
+
action: "Fix the TypeScript errors and rerun verification.",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// Secret scan — blocker-capable
|
|
313
|
+
if (input.secretScan && !input.secretScan.skipped && !input.secretScan.passed) {
|
|
314
|
+
findings.push({
|
|
315
|
+
category: "secrets",
|
|
316
|
+
severity: "high",
|
|
317
|
+
title: `Hardcoded secrets detected (${input.secretScan.findingsCount})`,
|
|
318
|
+
description: `Found ${input.secretScan.findingsCount} potential secret(s) in ${input.secretScan.filesScanned} scanned files. Leaked secrets are a deployment risk.`,
|
|
319
|
+
action: "Move secrets to environment variables and rotate any compromised credentials.",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// Deep accessibility — blocker-capable
|
|
323
|
+
if (input.a11yDeep && !input.a11yDeep.skipped && !input.a11yDeep.passed) {
|
|
324
|
+
const critical = input.a11yDeep.criticalCount;
|
|
325
|
+
const serious = input.a11yDeep.seriousCount;
|
|
326
|
+
findings.push({
|
|
327
|
+
category: "accessibility",
|
|
328
|
+
severity: critical > 0 ? "high" : "medium",
|
|
329
|
+
title: `WCAG violations found (${critical} critical, ${serious} serious)`,
|
|
330
|
+
description: input.a11yDeep.summary,
|
|
331
|
+
action: "Fix the critical and serious WCAG violations before release.",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// Deep SEO — advisory
|
|
335
|
+
if (input.seoDeep && !input.seoDeep.skipped && !input.seoDeep.passed) {
|
|
336
|
+
findings.push({
|
|
337
|
+
category: "seo",
|
|
338
|
+
severity: "medium",
|
|
339
|
+
title: `SEO issues detected (${input.seoDeep.errorCount} errors, ${input.seoDeep.warningCount} warnings)`,
|
|
340
|
+
description: input.seoDeep.summary,
|
|
341
|
+
action: "Fix missing or incorrect SEO meta tags.",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Vitals budget — advisory
|
|
345
|
+
if (input.vitalsBudget && !input.vitalsBudget.skipped && !input.vitalsBudget.passed) {
|
|
346
|
+
findings.push({
|
|
347
|
+
category: "vitals",
|
|
348
|
+
severity: "medium",
|
|
349
|
+
title: `Core Web Vitals budget exceeded`,
|
|
350
|
+
description: input.vitalsBudget.summary,
|
|
351
|
+
action: "Optimize performance to meet the Core Web Vitals budget thresholds.",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// Outdated dependencies — advisory
|
|
355
|
+
if (input.outdatedCheck && !input.outdatedCheck.skipped && input.outdatedCheck.majorOutdated > 0) {
|
|
356
|
+
findings.push({
|
|
357
|
+
category: "bestPractices",
|
|
358
|
+
severity: "medium",
|
|
359
|
+
title: `${input.outdatedCheck.majorOutdated} major version(s) behind latest`,
|
|
360
|
+
description: input.outdatedCheck.advisory,
|
|
361
|
+
action: "Update outdated dependencies, especially those with major version gaps.",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// Bundle size — advisory (only if thresholds exceeded)
|
|
365
|
+
if (input.bundleSize && !input.bundleSize.skipped) {
|
|
366
|
+
const firstLoad = input.bundleSize.firstLoadJsKb;
|
|
367
|
+
const largest = input.bundleSize.largestChunkKb;
|
|
368
|
+
if ((firstLoad !== null && firstLoad > 200) || (largest !== null && largest > 300)) {
|
|
369
|
+
findings.push({
|
|
370
|
+
category: "performance",
|
|
371
|
+
severity: "medium",
|
|
372
|
+
title: `Bundle size advisory threshold exceeded`,
|
|
373
|
+
description: input.bundleSize.advisory,
|
|
374
|
+
action: "Reduce bundle size by code-splitting, tree-shaking, or lazy-loading large dependencies.",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const lighthouseScores = input.lighthouseScores;
|
|
379
|
+
if (!lighthouseScores) {
|
|
380
|
+
return findings;
|
|
381
|
+
}
|
|
382
|
+
const lighthouseFinding = (category, actual, required, title, description, action) => ({
|
|
383
|
+
category,
|
|
384
|
+
severity: required - actual >= 20 ? "high" : "medium",
|
|
385
|
+
title: `${title} (${actual} / ${required})`,
|
|
386
|
+
description,
|
|
387
|
+
action,
|
|
388
|
+
});
|
|
389
|
+
if (lighthouseScores.performance < thresholds.performance) {
|
|
390
|
+
findings.push(lighthouseFinding("performance", lighthouseScores.performance, thresholds.performance, "Performance below threshold", "Runtime performance is below the minimum verification threshold.", "Reduce heavy assets, expensive scripts, and blocking work on initial load."));
|
|
391
|
+
}
|
|
392
|
+
if (lighthouseScores.accessibility < thresholds.accessibility) {
|
|
393
|
+
findings.push(lighthouseFinding("accessibility", lighthouseScores.accessibility, thresholds.accessibility, "Accessibility below threshold", "Accessibility checks are below the minimum verification threshold.", "Fix labels, semantics, contrast, and keyboard accessibility issues."));
|
|
394
|
+
}
|
|
395
|
+
if (lighthouseScores.seo < thresholds.seo) {
|
|
396
|
+
findings.push(lighthouseFinding("seo", lighthouseScores.seo, thresholds.seo, "SEO below threshold", "SEO checks are below the minimum verification threshold.", "Fix title, description, crawl settings, and indexable metadata."));
|
|
397
|
+
}
|
|
398
|
+
if (lighthouseScores.bestPractices < thresholds.bestPractices) {
|
|
399
|
+
findings.push(lighthouseFinding("bestPractices", lighthouseScores.bestPractices, thresholds.bestPractices, "Best practices below threshold", "Best practices checks are below the minimum verification threshold.", "Fix browser warnings, unsafe patterns, and platform-level issues."));
|
|
400
|
+
}
|
|
401
|
+
return findings.sort((a, b) => {
|
|
402
|
+
const priority = { critical: 0, high: 1, medium: 2 };
|
|
403
|
+
return priority[a.severity] - priority[b.severity];
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function buildVerificationReport(input, options) {
|
|
407
|
+
const thresholds = options?.thresholds ?? exports.DEFAULT_LH_THRESHOLDS;
|
|
408
|
+
const tier = options?.tier ?? "free";
|
|
409
|
+
const evidence = buildVerificationEvidence(input, thresholds);
|
|
410
|
+
const findings = getImprovementRecommendations(input, thresholds);
|
|
411
|
+
const blockers = findings.filter((finding) => finding.severity === "critical" || finding.severity === "high");
|
|
412
|
+
const warnings = findings.filter((finding) => finding.severity === "medium");
|
|
413
|
+
const hasWarnings = warnings.length > 0;
|
|
414
|
+
const coreChecksPassed = evidence.buildPassed && evidence.e2ePassedAll && evidence.lighthousePassed;
|
|
415
|
+
const teamEvidenceComplete = evidence.hasMultiViewportData &&
|
|
416
|
+
evidence.multiViewportPassed &&
|
|
417
|
+
evidence.hasComparableVisualDiffData &&
|
|
418
|
+
evidence.visualDiffPassed;
|
|
419
|
+
const grade = getVerificationGrade(input, thresholds);
|
|
420
|
+
const failureEvidence = (input.failureEvidence ?? []).filter(Boolean).slice(0, 5);
|
|
421
|
+
let verdict;
|
|
422
|
+
let confidence;
|
|
423
|
+
let summary;
|
|
424
|
+
if (!evidence.buildPassed) {
|
|
425
|
+
verdict = "build-failed";
|
|
426
|
+
confidence = "low";
|
|
427
|
+
summary = "Build failed. Fix the blocking build errors before relying on this verification result.";
|
|
428
|
+
}
|
|
429
|
+
else if (blockers.length > 0) {
|
|
430
|
+
verdict = "hold";
|
|
431
|
+
confidence = "medium";
|
|
432
|
+
summary = "Blocking verification issues were found. Hold release until the blockers are fixed.";
|
|
433
|
+
}
|
|
434
|
+
else if (tier === "team" && coreChecksPassed && !hasWarnings && teamEvidenceComplete) {
|
|
435
|
+
verdict = "release-ready";
|
|
436
|
+
confidence = "high";
|
|
437
|
+
summary = "All core checks and release-evidence checks passed. This run is strong enough to call release-ready.";
|
|
438
|
+
}
|
|
439
|
+
else if (coreChecksPassed && !hasWarnings) {
|
|
440
|
+
verdict = "client-ready";
|
|
441
|
+
confidence = "high";
|
|
442
|
+
summary = "No blocking issues found. Build, E2E, and Lighthouse checks passed.";
|
|
443
|
+
}
|
|
444
|
+
else if (coreChecksPassed && hasWarnings) {
|
|
445
|
+
verdict = "investigate";
|
|
446
|
+
confidence = "medium";
|
|
447
|
+
summary = "Core checks passed, but warning-level risks remain. Review warnings before calling this run client-ready.";
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
verdict = "quick-pass";
|
|
451
|
+
confidence = evidence.hasLighthouseData && evidence.lighthousePassed ? "medium" : "low";
|
|
452
|
+
summary = "No immediate hard blockers were found in the quick verification pass.";
|
|
453
|
+
}
|
|
454
|
+
const passes = [
|
|
455
|
+
{ key: "build", label: "Production build", passed: evidence.buildPassed },
|
|
456
|
+
...(evidence.hasE2EData
|
|
457
|
+
? [{ key: "e2e", label: `E2E ${input.e2ePassed ?? 0}/${input.e2eTotal ?? 0}`, passed: evidence.e2ePassedAll }]
|
|
458
|
+
: []),
|
|
459
|
+
...(evidence.hasConsoleErrors
|
|
460
|
+
? [{ key: "console-errors", label: `Console errors (${input.e2eConsoleErrorCount ?? 0})`, passed: false }]
|
|
461
|
+
: [{ key: "console-errors", label: "No console errors", passed: true }]),
|
|
462
|
+
...(evidence.hasSecurityData
|
|
463
|
+
? [{ key: "security", label: `Security (${input.securityAudit?.summary ?? "unknown"})`, passed: evidence.securityPassed }]
|
|
464
|
+
: []),
|
|
465
|
+
...(evidence.hasMobileLighthouseData
|
|
466
|
+
? [{ key: "mobile-lh", label: "Mobile Lighthouse", passed: evidence.mobileLighthousePassed }]
|
|
467
|
+
: []),
|
|
468
|
+
...(evidence.hasMultiViewportData
|
|
469
|
+
? [{
|
|
470
|
+
key: "viewport",
|
|
471
|
+
label: `Viewport ${input.viewportIssues ?? 0} issues`,
|
|
472
|
+
passed: evidence.multiViewportPassed,
|
|
473
|
+
}]
|
|
474
|
+
: []),
|
|
475
|
+
...(evidence.hasVisualDiffData
|
|
476
|
+
? [{
|
|
477
|
+
key: "visual",
|
|
478
|
+
label: input.hasVisualBaseline
|
|
479
|
+
? `Visual diff ${input.visualDiffPercentage ?? 0}%`
|
|
480
|
+
: "Visual baseline seeded",
|
|
481
|
+
passed: evidence.visualDiffPassed,
|
|
482
|
+
}]
|
|
483
|
+
: []),
|
|
484
|
+
...(evidence.hasLighthouseData
|
|
485
|
+
? [{ key: "lighthouse", label: "Lighthouse thresholds", passed: evidence.lighthousePassed }]
|
|
486
|
+
: []),
|
|
487
|
+
...(evidence.hasTypecheckData
|
|
488
|
+
? [{ key: "typecheck", label: `TypeScript (${input.typecheck?.errorCount ?? 0} errors)`, passed: evidence.typecheckPassed }]
|
|
489
|
+
: []),
|
|
490
|
+
...(evidence.hasSecretScanData
|
|
491
|
+
? [{ key: "secret-scan", label: `Secret scan (${input.secretScan?.findingsCount ?? 0} findings)`, passed: evidence.secretScanPassed }]
|
|
492
|
+
: []),
|
|
493
|
+
...(evidence.hasA11yDeepData
|
|
494
|
+
? [{ key: "a11y-deep", label: `WCAG deep (${input.a11yDeep?.criticalCount ?? 0} critical)`, passed: evidence.a11yDeepPassed }]
|
|
495
|
+
: []),
|
|
496
|
+
...(evidence.hasSeoDeepData
|
|
497
|
+
? [{ key: "seo-deep", label: `SEO deep (${input.seoDeep?.errorCount ?? 0} errors)`, passed: evidence.seoDeepPassed }]
|
|
498
|
+
: []),
|
|
499
|
+
...(evidence.hasVitalsBudgetData
|
|
500
|
+
? [{ key: "vitals-budget", label: "Core Web Vitals budget", passed: evidence.vitalsBudgetPassed }]
|
|
501
|
+
: []),
|
|
502
|
+
...(input.bundleSize && !input.bundleSize.skipped
|
|
503
|
+
? [{ key: "bundle-size", label: `Bundle size (${input.bundleSize.framework})`, passed: true }]
|
|
504
|
+
: []),
|
|
505
|
+
...(input.outdatedCheck && !input.outdatedCheck.skipped
|
|
506
|
+
? [{ key: "outdated-check", label: `Outdated deps (${input.outdatedCheck.outdatedCount} outdated)`, passed: input.outdatedCheck.majorOutdated === 0 }]
|
|
507
|
+
: []),
|
|
508
|
+
];
|
|
509
|
+
const nextActions = [...blockers, ...warnings].slice(0, 4).map((finding) => finding.action);
|
|
510
|
+
return {
|
|
511
|
+
tier,
|
|
512
|
+
verdict,
|
|
513
|
+
confidence,
|
|
514
|
+
summary,
|
|
515
|
+
grade,
|
|
516
|
+
blockers,
|
|
517
|
+
warnings,
|
|
518
|
+
passes,
|
|
519
|
+
nextActions,
|
|
520
|
+
failureEvidence,
|
|
521
|
+
evidence,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function buildTierVerificationView(input, options) {
|
|
525
|
+
return (0, tier_policy_js_1.getTierVerificationView)(buildVerificationReport(input, options));
|
|
526
|
+
}
|