laxy-verify 1.2.2 → 1.3.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.
@@ -1,409 +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
- return {
76
- input,
77
- thresholds,
78
- buildPassed,
79
- hasE2EData,
80
- e2ePassedAll,
81
- e2eStabilityPassed,
82
- hasLighthouseData,
83
- lighthouseSkipped,
84
- hasMultiViewportData,
85
- multiViewportPassed,
86
- hasVisualDiffData,
87
- hasComparableVisualDiffData,
88
- visualDiffPassed,
89
- lighthousePassed,
90
- hasConsoleErrors,
91
- hasSecurityData,
92
- securityPassed,
93
- hasMobileLighthouseData,
94
- mobileLighthousePassed,
95
- };
96
- }
97
- function getImprovementRecommendations(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
98
- const findings = [];
99
- const errors = input.buildErrors ?? [];
100
- if (input.buildSuccess === false) {
101
- if (errors.some((error) => /TS\d+|type/i.test(error))) {
102
- findings.push({
103
- category: "build",
104
- severity: "critical",
105
- title: "TypeScript build errors",
106
- description: "Type errors are blocking a clean production build.",
107
- action: "Fix the TypeScript errors first, then rerun verification.",
108
- });
109
- }
110
- if (errors.some((error) => /Module not found|Cannot find module|Failed to resolve/i.test(error))) {
111
- findings.push({
112
- category: "build",
113
- severity: "critical",
114
- title: "Missing or unresolved modules",
115
- description: "The build cannot resolve one or more imports or packages.",
116
- action: "Check import paths, package installation, and package.json consistency.",
117
- });
118
- }
119
- if (errors.some((error) => /SyntaxError|Unexpected token/i.test(error))) {
120
- findings.push({
121
- category: "build",
122
- severity: "critical",
123
- title: "Syntax errors in source code",
124
- description: "The code contains syntax issues that stop the build from completing.",
125
- action: "Fix the syntax errors, then rerun the build verification.",
126
- });
127
- }
128
- if (findings.every((finding) => finding.category !== "build")) {
129
- findings.push({
130
- category: "build",
131
- severity: "critical",
132
- title: "Build failed",
133
- description: "Production build verification did not pass.",
134
- action: "Inspect the build logs and resolve the blocking errors before release.",
135
- });
136
- }
137
- }
138
- if (typeof input.e2ePassed === "number" &&
139
- typeof input.e2eTotal === "number" &&
140
- input.e2eTotal > 0 &&
141
- input.e2ePassed < input.e2eTotal) {
142
- const failedCount = input.e2eTotal - input.e2ePassed;
143
- findings.push({
144
- category: "e2e",
145
- severity: "high",
146
- title: `E2E failures (${failedCount}/${input.e2eTotal})`,
147
- description: "One or more verification scenarios failed.",
148
- action: "Fix the broken user flow and rerun the verification scenarios.",
149
- });
150
- }
151
- if ((input.e2eCoverageGaps?.length ?? 0) > 0) {
152
- findings.push({
153
- category: "e2e",
154
- severity: "high",
155
- title: `Verification coverage gaps (${input.e2eCoverageGaps?.length ?? 0})`,
156
- description: input.e2eCoverageGaps.join(" "),
157
- action: "Add or expose a stable primary user flow, then rerun verification so the checks cover real interactions.",
158
- });
159
- }
160
- if (input.e2eStabilityPassed === false) {
161
- findings.push({
162
- category: "e2e",
163
- severity: "high",
164
- title: "E2E stability check failed",
165
- description: "E2E scenarios passed on the first run but failed on the second stability run.",
166
- action: "Fix flaky tests or unstable application state, then rerun verification.",
167
- });
168
- }
169
- // Runtime console errors — answers "이거 당장 크게 터지나?"
170
- if (typeof input.e2eConsoleErrorCount === "number" && input.e2eConsoleErrorCount > 0) {
171
- findings.push({
172
- category: "runtime",
173
- severity: input.e2eConsoleErrorCount >= 3 ? "high" : "medium",
174
- title: `Runtime console errors detected (${input.e2eConsoleErrorCount})`,
175
- description: "The app emits console errors during E2E verification, indicating runtime issues.",
176
- action: "Open browser DevTools, fix the console errors, and rerun verification.",
177
- });
178
- }
179
- // Security audit — answers "클라이언트한테 보내도 되는 수준인가?"
180
- if (input.securityAudit) {
181
- const audit = input.securityAudit;
182
- if (audit.critical > 0) {
183
- findings.push({
184
- category: "security",
185
- severity: "critical",
186
- title: `Critical security vulnerabilities (${audit.critical})`,
187
- description: `npm audit found ${audit.critical} critical vulnerabilities: ${audit.summary}`,
188
- action: "Run npm audit fix or update the vulnerable packages before deployment.",
189
- });
190
- }
191
- else if (audit.high > 0) {
192
- findings.push({
193
- category: "security",
194
- severity: "high",
195
- title: `High security vulnerabilities (${audit.high})`,
196
- description: `npm audit found ${audit.high} high-severity vulnerabilities: ${audit.summary}`,
197
- action: "Run npm audit fix or update the vulnerable packages.",
198
- });
199
- }
200
- else if (audit.totalVulnerabilities > 0) {
201
- findings.push({
202
- category: "security",
203
- severity: "medium",
204
- title: `Security vulnerabilities (${audit.totalVulnerabilities})`,
205
- description: `npm audit found moderate/low vulnerabilities: ${audit.summary}`,
206
- action: "Review npm audit output and update packages when convenient.",
207
- });
208
- }
209
- }
210
- // Broken links audit
211
- if (input.brokenLinksAudit && input.brokenLinksAudit.brokenCount > 0) {
212
- findings.push({
213
- category: "security",
214
- severity: "high",
215
- title: `Broken links detected (${input.brokenLinksAudit.brokenCount}/${input.brokenLinksAudit.checkedCount})`,
216
- description: `${input.brokenLinksAudit.brokenCount} link(s) returned non-OK status codes. Users will hit 404/500 errors.`,
217
- action: "Fix or remove the broken links and rerun the verification.",
218
- });
219
- }
220
- // Mobile Lighthouse Pro tier mobile check
221
- if (input.mobileLighthouseScores) {
222
- const mobileScores = input.mobileLighthouseScores;
223
- if (mobileScores.performance < thresholds.performance) {
224
- findings.push({
225
- category: "performance",
226
- severity: "high",
227
- title: `Mobile performance below threshold (${mobileScores.performance} / ${thresholds.performance})`,
228
- description: "Mobile users will experience slow load times or poor interactivity.",
229
- action: "Optimize mobile performance: reduce JS bundles, defer non-critical assets, optimize images.",
230
- });
231
- }
232
- if (mobileScores.accessibility < thresholds.accessibility) {
233
- findings.push({
234
- category: "accessibility",
235
- severity: "high",
236
- title: `Mobile accessibility below threshold (${mobileScores.accessibility} / ${thresholds.accessibility})`,
237
- description: "Mobile accessibility issues detected that may block users.",
238
- action: "Fix touch targets, contrast, and semantic structure for mobile layouts.",
239
- });
240
- }
241
- }
242
- const hasMultiViewportData = typeof input.viewportIssues === "number" || typeof input.multiViewportPassed === "boolean";
243
- const multiViewportPassed = input.multiViewportPassed === true ||
244
- (input.multiViewportPassed !== false && (input.viewportIssues ?? 0) <= 0);
245
- if (hasMultiViewportData && !multiViewportPassed) {
246
- findings.push({
247
- category: "viewport",
248
- severity: "high",
249
- title: `Multi-viewport issues detected (${input.viewportIssues ?? 0})`,
250
- description: input.multiViewportSummary ||
251
- "One or more responsive layout or viewport-specific verification issues were found.",
252
- action: "Fix the responsive layout issues and rerun the multi-viewport verification pass.",
253
- });
254
- }
255
- if (input.visualDiffVerdict === "rollback") {
256
- findings.push({
257
- category: "visual",
258
- severity: "high",
259
- title: `Visual regression detected (${input.visualDiffPercentage ?? 0}%)`,
260
- description: "The visual diff is large enough to recommend a rollback or release hold.",
261
- action: "Review the visual diff artifacts and fix the unintended UI regression before release.",
262
- });
263
- }
264
- else if (input.visualDiffVerdict === "warn") {
265
- findings.push({
266
- category: "visual",
267
- severity: "medium",
268
- title: `Visual change needs review (${input.visualDiffPercentage ?? 0}%)`,
269
- description: "The visual diff changed enough to require a manual review before release.",
270
- action: "Check the visual diff and confirm the UI change is intentional.",
271
- });
272
- }
273
- if ((input.lighthouseErrorCount ?? 0) > 0) {
274
- findings.push({
275
- category: "performance",
276
- severity: "medium",
277
- title: `Lighthouse instability detected (${input.lighthouseErrorCount})`,
278
- description: "One or more Lighthouse runs errored even though a report was recovered.",
279
- action: "Rerun Lighthouse and inspect the failing run logs before trusting this result in CI.",
280
- });
281
- }
282
- const lighthouseScores = input.lighthouseScores;
283
- if (!lighthouseScores) {
284
- return findings;
285
- }
286
- const lighthouseFinding = (category, actual, required, title, description, action) => ({
287
- category,
288
- severity: required - actual >= 20 ? "high" : "medium",
289
- title: `${title} (${actual} / ${required})`,
290
- description,
291
- action,
292
- });
293
- if (lighthouseScores.performance < thresholds.performance) {
294
- 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."));
295
- }
296
- if (lighthouseScores.accessibility < thresholds.accessibility) {
297
- 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."));
298
- }
299
- if (lighthouseScores.seo < thresholds.seo) {
300
- 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."));
301
- }
302
- if (lighthouseScores.bestPractices < thresholds.bestPractices) {
303
- 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."));
304
- }
305
- return findings.sort((a, b) => {
306
- const priority = { critical: 0, high: 1, medium: 2 };
307
- return priority[a.severity] - priority[b.severity];
308
- });
309
- }
310
- function buildVerificationReport(input, options) {
311
- const thresholds = options?.thresholds ?? exports.DEFAULT_LH_THRESHOLDS;
312
- const tier = options?.tier ?? "free";
313
- const evidence = buildVerificationEvidence(input, thresholds);
314
- const findings = getImprovementRecommendations(input, thresholds);
315
- const blockers = findings.filter((finding) => finding.severity === "critical" || finding.severity === "high");
316
- const warnings = findings.filter((finding) => finding.severity === "medium");
317
- const hasWarnings = warnings.length > 0;
318
- const coreChecksPassed = evidence.buildPassed && evidence.e2ePassedAll && evidence.lighthousePassed;
319
- const teamEvidenceComplete = evidence.hasMultiViewportData &&
320
- evidence.multiViewportPassed &&
321
- evidence.hasComparableVisualDiffData &&
322
- evidence.visualDiffPassed;
323
- const grade = getVerificationGrade(input, thresholds);
324
- const failureEvidence = (input.failureEvidence ?? []).filter(Boolean).slice(0, 5);
325
- let verdict;
326
- let confidence;
327
- let summary;
328
- if (!evidence.buildPassed) {
329
- verdict = "build-failed";
330
- confidence = "low";
331
- summary = "Build failed. Fix the blocking build errors before relying on this verification result.";
332
- }
333
- else if (blockers.length > 0) {
334
- verdict = "hold";
335
- confidence = "medium";
336
- summary = "Blocking verification issues were found. Hold release until the blockers are fixed.";
337
- }
338
- else if (tier === "team" && coreChecksPassed && !hasWarnings && teamEvidenceComplete) {
339
- verdict = "release-ready";
340
- confidence = "high";
341
- summary = "All core checks and release-evidence checks passed. This run is strong enough to call release-ready.";
342
- }
343
- else if (coreChecksPassed && !hasWarnings) {
344
- verdict = "client-ready";
345
- confidence = "high";
346
- summary = "No blocking issues found. Build, E2E, and Lighthouse checks passed.";
347
- }
348
- else if (coreChecksPassed && hasWarnings) {
349
- verdict = "investigate";
350
- confidence = "medium";
351
- summary = "Core checks passed, but warning-level risks remain. Review warnings before calling this run client-ready.";
352
- }
353
- else {
354
- verdict = "quick-pass";
355
- confidence = evidence.hasLighthouseData && evidence.lighthousePassed ? "medium" : "low";
356
- summary = "No immediate hard blockers were found in the quick verification pass.";
357
- }
358
- const passes = [
359
- { key: "build", label: "Production build", passed: evidence.buildPassed },
360
- ...(evidence.hasE2EData
361
- ? [{ key: "e2e", label: `E2E ${input.e2ePassed ?? 0}/${input.e2eTotal ?? 0}`, passed: evidence.e2ePassedAll }]
362
- : []),
363
- ...(evidence.hasConsoleErrors
364
- ? [{ key: "console-errors", label: `Console errors (${input.e2eConsoleErrorCount ?? 0})`, passed: false }]
365
- : [{ key: "console-errors", label: "No console errors", passed: true }]),
366
- ...(evidence.hasSecurityData
367
- ? [{ key: "security", label: `Security (${input.securityAudit?.summary ?? "unknown"})`, passed: evidence.securityPassed }]
368
- : []),
369
- ...(evidence.hasMobileLighthouseData
370
- ? [{ key: "mobile-lh", label: "Mobile Lighthouse", passed: evidence.mobileLighthousePassed }]
371
- : []),
372
- ...(evidence.hasMultiViewportData
373
- ? [{
374
- key: "viewport",
375
- label: `Viewport ${input.viewportIssues ?? 0} issues`,
376
- passed: evidence.multiViewportPassed,
377
- }]
378
- : []),
379
- ...(evidence.hasVisualDiffData
380
- ? [{
381
- key: "visual",
382
- label: input.hasVisualBaseline
383
- ? `Visual diff ${input.visualDiffPercentage ?? 0}%`
384
- : "Visual baseline seeded",
385
- passed: evidence.visualDiffPassed,
386
- }]
387
- : []),
388
- ...(evidence.hasLighthouseData
389
- ? [{ key: "lighthouse", label: "Lighthouse thresholds", passed: evidence.lighthousePassed }]
390
- : []),
391
- ];
392
- const nextActions = [...blockers, ...warnings].slice(0, 4).map((finding) => finding.action);
393
- return {
394
- tier,
395
- verdict,
396
- confidence,
397
- summary,
398
- grade,
399
- blockers,
400
- warnings,
401
- passes,
402
- nextActions,
403
- failureEvidence,
404
- evidence,
405
- };
406
- }
407
- function buildTierVerificationView(input, options) {
408
- return (0, tier_policy_js_1.getTierVerificationView)(buildVerificationReport(input, options));
409
- }
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
+ }