laxy-verify 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +181 -47
  2. package/dist/a11y-deep.d.ts +20 -0
  3. package/dist/a11y-deep.js +161 -0
  4. package/dist/ai-analysis.d.ts +28 -0
  5. package/dist/ai-analysis.js +32 -0
  6. package/dist/audit/broken-links.d.ts +5 -1
  7. package/dist/audit/broken-links.js +23 -12
  8. package/dist/bundle-size.d.ts +14 -0
  9. package/dist/bundle-size.js +209 -0
  10. package/dist/cli.js +391 -13
  11. package/dist/compare-env.d.ts +23 -0
  12. package/dist/compare-env.js +55 -0
  13. package/dist/config.d.ts +37 -0
  14. package/dist/config.js +106 -1
  15. package/dist/entitlement.d.ts +2 -0
  16. package/dist/entitlement.js +5 -1
  17. package/dist/init-analysis.d.ts +6 -0
  18. package/dist/init-analysis.js +302 -0
  19. package/dist/init.js +66 -0
  20. package/dist/lighthouse.d.ts +31 -1
  21. package/dist/lighthouse.js +76 -3
  22. package/dist/outdated-check.d.ts +17 -0
  23. package/dist/outdated-check.js +123 -0
  24. package/dist/report-markdown.d.ts +14 -0
  25. package/dist/report-markdown.js +21 -0
  26. package/dist/route-discovery.d.ts +7 -0
  27. package/dist/route-discovery.js +108 -0
  28. package/dist/secret-scan.d.ts +15 -0
  29. package/dist/secret-scan.js +218 -0
  30. package/dist/security-audit.d.ts +9 -1
  31. package/dist/security-audit.js +87 -24
  32. package/dist/seo-deep.d.ts +24 -0
  33. package/dist/seo-deep.js +147 -0
  34. package/dist/typecheck.d.ts +8 -0
  35. package/dist/typecheck.js +99 -0
  36. package/dist/verification-core/report.js +117 -0
  37. package/dist/verification-core/types.d.ts +58 -2
  38. package/dist/visual-diff.d.ts +8 -1
  39. package/dist/visual-diff.js +53 -8
  40. package/dist/vitals-budget.d.ts +23 -0
  41. package/dist/vitals-budget.js +168 -0
  42. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -58,8 +58,18 @@ const report_markdown_js_1 = require("./report-markdown.js");
58
58
  const visual_diff_js_1 = require("./visual-diff.js");
59
59
  const security_audit_js_1 = require("./security-audit.js");
60
60
  const broken_links_js_1 = require("./audit/broken-links.js");
61
+ const typecheck_js_1 = require("./typecheck.js");
62
+ const secret_scan_js_1 = require("./secret-scan.js");
63
+ const bundle_size_js_1 = require("./bundle-size.js");
64
+ const outdated_check_js_1 = require("./outdated-check.js");
65
+ const a11y_deep_js_1 = require("./a11y-deep.js");
66
+ const seo_deep_js_1 = require("./seo-deep.js");
67
+ const vitals_budget_js_1 = require("./vitals-budget.js");
61
68
  const index_js_1 = require("./verification-core/index.js");
62
69
  const trend_js_1 = require("./trend.js");
70
+ const ai_analysis_js_1 = require("./ai-analysis.js");
71
+ const compare_env_js_1 = require("./compare-env.js");
72
+ const route_discovery_js_1 = require("./route-discovery.js");
63
73
  const package_json_1 = __importDefault(require("../package.json"));
64
74
  let activeDevServerPid;
65
75
  let activeDevServerCleanup = null;
@@ -161,12 +171,38 @@ function parseArgs() {
161
171
  port: flags.port !== undefined ? Number(flags.port) : undefined,
162
172
  share: flags.share !== undefined,
163
173
  help: flags.help !== undefined || flags.h !== undefined,
174
+ compareUrl: flags.compare,
175
+ typecheck: flags.typecheck !== undefined,
176
+ secretScan: flags["secret-scan"] !== undefined,
177
+ bundleSize: flags["bundle-size"] !== undefined,
178
+ outdatedCheck: flags["outdated-check"] !== undefined,
179
+ a11yDeep: flags["a11y-deep"] !== undefined,
180
+ seoDeep: flags["seo-deep"] !== undefined,
181
+ vitalsBudget: flags["vitals-budget"] !== undefined,
164
182
  };
165
183
  }
166
184
  function writeResultFile(projectDir, result) {
167
185
  const filePath = path.join(projectDir, ".laxy-result.json");
168
186
  fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
169
187
  }
188
+ function uniqueRouteList(routes) {
189
+ const seen = new Set();
190
+ const result = [];
191
+ for (const route of routes) {
192
+ if (!route)
193
+ continue;
194
+ const normalized = route.startsWith("/") ? route : `/${route}`;
195
+ const trimmed = normalized.split("?")[0]?.split("#")[0]?.replace(/\/+/g, "/") ?? "/";
196
+ const finalRoute = trimmed.endsWith("/") && trimmed !== "/" ? trimmed.slice(0, -1) : trimmed;
197
+ if (finalRoute === "/" || finalRoute.startsWith("/api/") || finalRoute.startsWith("/_next/"))
198
+ continue;
199
+ if (seen.has(finalRoute))
200
+ continue;
201
+ seen.add(finalRoute);
202
+ result.push(finalRoute);
203
+ }
204
+ return result;
205
+ }
170
206
  function summarizeViewportIssues(scores, thresholds) {
171
207
  if (!scores)
172
208
  return { count: 0 };
@@ -208,12 +244,30 @@ function consoleOutput(result) {
208
244
  const lh = result.lighthouse;
209
245
  const t = result.thresholds;
210
246
  const check = (passed) => (passed ? " OK" : " FAIL");
211
- console.log(" Lighthouse:");
247
+ const routeLabel = result.lighthouseRoutes && result.lighthouseRoutes.length > 1
248
+ ? ` (weighted avg of ${result.lighthouseRoutes.length} routes)`
249
+ : "";
250
+ console.log(` Lighthouse${routeLabel}:`);
212
251
  console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
213
252
  console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
214
253
  console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
215
254
  console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
216
255
  console.log(` Runs: ${lh.runs}`);
256
+ if (result.lighthouseRoutes && result.lighthouseRoutes.length > 1) {
257
+ console.log(" Per-route:");
258
+ for (const r of result.lighthouseRoutes) {
259
+ if (r.scores) {
260
+ const pass = r.scores.performance >= t.performance &&
261
+ r.scores.accessibility >= t.accessibility &&
262
+ r.scores.seo >= t.seo &&
263
+ r.scores.bestPractices >= t.bestPractices;
264
+ console.log(` ${r.route.padEnd(20)} P=${r.scores.performance} A=${r.scores.accessibility} S=${r.scores.seo} BP=${r.scores.bestPractices}${pass ? "" : " FAIL"}`);
265
+ }
266
+ else {
267
+ console.log(` ${r.route.padEnd(20)} (no data)`);
268
+ }
269
+ }
270
+ }
217
271
  }
218
272
  else {
219
273
  console.log(" Lighthouse: skipped");
@@ -238,6 +292,27 @@ function consoleOutput(result) {
238
292
  const ml = result.mobileLighthouse;
239
293
  console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
240
294
  }
295
+ if (result.typecheck && !result.typecheck.skipped) {
296
+ console.log(` TypeScript: ${result.typecheck.passed ? "OK" : `${result.typecheck.errorCount} error(s)`}`);
297
+ }
298
+ if (result.secretScan && !result.secretScan.skipped) {
299
+ console.log(` Secret scan: ${result.secretScan.passed ? "OK" : `${result.secretScan.findings.length} finding(s)`} (${result.secretScan.filesScanned} files)`);
300
+ }
301
+ if (result.bundleSize && !result.bundleSize.skipped) {
302
+ console.log(` Bundle size: ${result.bundleSize.advisory}`);
303
+ }
304
+ if (result.outdatedCheck && !result.outdatedCheck.skipped) {
305
+ console.log(` Outdated: ${result.outdatedCheck.advisory}`);
306
+ }
307
+ if (result.a11yDeep && !result.a11yDeep.skipped) {
308
+ console.log(` A11y deep: ${result.a11yDeep.summary}`);
309
+ }
310
+ if (result.seoDeep && !result.seoDeep.skipped) {
311
+ console.log(` SEO deep: ${result.seoDeep.summary}`);
312
+ }
313
+ if (result.vitalsBudget && !result.vitalsBudget.skipped) {
314
+ console.log(` Vitals budget: ${result.vitalsBudget.summary}`);
315
+ }
241
316
  if (result.verification) {
242
317
  const view = result.verification.view;
243
318
  const verboseFailure = result._verbose_failure ?? true;
@@ -293,6 +368,9 @@ function consoleOutput(result) {
293
368
  if (result.github.status === "status_set")
294
369
  console.log(` Status check: ${result.github.grade}`);
295
370
  }
371
+ if (result.compareEnv) {
372
+ (0, compare_env_js_1.printEnvComparison)(result.compareEnv);
373
+ }
296
374
  console.log(" Result: .laxy-result.json");
297
375
  if (result.markdownReportPath) {
298
376
  console.log(` Report: ${path.basename(result.markdownReportPath)}`);
@@ -329,10 +407,18 @@ async function run() {
329
407
  --skip-lighthouse Skip Lighthouse but still run build and E2E
330
408
  --port <port> Use an already-running dev server on this port (skip build & server start)
331
409
  --share Create a public share link for this verification result (Pro)
410
+ --compare <url> Compare Lighthouse scores against a reference environment URL (Pro)
332
411
  --plan-override free | pro | team (testing metadata only)
333
412
  --multi-viewport Lighthouse on desktop/tablet/mobile
334
- --crawl Crawl the app to discover routes before E2E
413
+ --crawl Crawl the app to discover routes before E2E (also enables multi-route Lighthouse)
335
414
  --badge Print shields.io badge markdown
415
+ --typecheck Run TypeScript type check (tsc --noEmit)
416
+ --secret-scan Scan for hardcoded secrets and credentials
417
+ --bundle-size Analyze bundle size (Next.js/Vite)
418
+ --outdated-check Check for outdated dependencies
419
+ --a11y-deep Deep accessibility audit (axe-core)
420
+ --seo-deep Deep SEO audit (meta, OG, JSON-LD)
421
+ --vitals-budget Core Web Vitals budget check (LCP, CLS, INP)
336
422
  --help Show this help
337
423
 
338
424
  Exit codes:
@@ -347,6 +433,7 @@ async function run() {
347
433
  npx laxy-verify . --fail-on silver # Block Bronze or worse
348
434
  npx laxy-verify . --port 3001 # Use existing dev server on port 3001
349
435
  npx laxy-verify . --share # Save and share a public verification link
436
+ npx laxy-verify . --compare https://staging.example.com # Compare vs reference env (Pro)
350
437
 
351
438
  Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
352
439
  `);
@@ -437,6 +524,13 @@ async function run() {
437
524
  cliFlags: {
438
525
  failOn: args.failOn,
439
526
  skipLighthouse: args.skipLighthouse,
527
+ typecheck: args.typecheck,
528
+ secretScan: args.secretScan,
529
+ bundleSize: args.bundleSize,
530
+ outdatedCheck: args.outdatedCheck,
531
+ a11yDeep: args.a11yDeep,
532
+ seoDeep: args.seoDeep,
533
+ vitalsBudget: args.vitalsBudget,
440
534
  },
441
535
  teamThresholds,
442
536
  });
@@ -446,6 +540,21 @@ async function run() {
446
540
  exitGracefully(2);
447
541
  return;
448
542
  }
543
+ if (args.compareUrl) {
544
+ try {
545
+ const u = new URL(args.compareUrl);
546
+ if (!["http:", "https:"].includes(u.protocol)) {
547
+ console.error(`Config error: --compare URL must use http or https protocol: ${args.compareUrl}`);
548
+ exitGracefully(2);
549
+ return;
550
+ }
551
+ }
552
+ catch {
553
+ console.error(`Config error: --compare value is not a valid URL: ${args.compareUrl}`);
554
+ exitGracefully(2);
555
+ return;
556
+ }
557
+ }
449
558
  let detected;
450
559
  try {
451
560
  detected = (0, detect_js_1.detect)(args.projectDir);
@@ -517,11 +626,11 @@ async function run() {
517
626
  }
518
627
  const features = entitlements ?? {
519
628
  plan: "free",
520
- // All verification features run on every plan
521
- // Automation features ??not available on Free
522
629
  github_actions: false,
523
630
  queue_priority: false,
524
631
  parallel_execution: false,
632
+ ai_failure_analysis: false,
633
+ compare_env: false,
525
634
  };
526
635
  let effectiveFeatures = features;
527
636
  if (args.planOverride) {
@@ -539,17 +648,28 @@ async function run() {
539
648
  if (config.lighthouse_runs < 3) {
540
649
  config = { ...config, lighthouse_runs: 3 };
541
650
  }
651
+ let multiRouteLhResult = null;
542
652
  let multiViewportScores = null;
543
653
  let allViewportsOk = false;
544
654
  let e2eResult;
545
655
  let crossBrowserResults;
546
656
  let e2eCoverageGaps = [];
547
657
  let e2eConsoleErrors = [];
658
+ let lastCrawlRoutes = [];
659
+ let runtimeDiscoveredRoutes = [];
548
660
  let e2eStabilityPassed = true;
549
661
  let visualDiffResult = null;
550
662
  let securityAuditResult = null;
551
663
  let mobileLighthouseScores = null;
552
664
  let brokenLinksResult = null;
665
+ let compareEnvResult = null;
666
+ let typecheckResult = null;
667
+ let secretScanResult = null;
668
+ let bundleSizeResult = null;
669
+ let outdatedCheckResult = null;
670
+ let a11yDeepResult = null;
671
+ let seoDeepResult = null;
672
+ let vitalsBudgetResult = null;
553
673
  const failureEvidence = [];
554
674
  if (buildResult.success) {
555
675
  let servePid;
@@ -559,13 +679,37 @@ async function run() {
559
679
  servePid = serve.pid;
560
680
  activeDevServerPid = serve.pid;
561
681
  }
562
- const verifyUrl = `http://127.0.0.1:${port}/`;
682
+ const verifyUrl = `http://localhost:${port}/`;
563
683
  const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
684
+ try {
685
+ const runtimeRouteDiscovery = await (0, route_discovery_js_1.discoverRuntimeRoutes)(verifyUrl);
686
+ runtimeDiscoveredRoutes = runtimeRouteDiscovery.routes;
687
+ if (runtimeDiscoveredRoutes.length > 0) {
688
+ console.log(` Runtime route discovery: found ${runtimeDiscoveredRoutes.length} route(s) from app bundles.`);
689
+ }
690
+ }
691
+ catch (routeErr) {
692
+ console.error(`Runtime route discovery warning: ${routeErr instanceof Error ? routeErr.message : String(routeErr)}`);
693
+ }
564
694
  if (!args.skipLighthouse) {
565
695
  try {
566
- const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
567
- lighthouseErrorCount = lhResult.errors.length;
568
- scores = lhResult.scores ?? undefined;
696
+ const explicitRoutes = config.lighthouse_routes?.length
697
+ ? config.lighthouse_routes
698
+ : config.extra_routes?.length
699
+ ? config.extra_routes
700
+ : undefined;
701
+ if (explicitRoutes && explicitRoutes.length > 0) {
702
+ // Multi-route mode: run on all explicitly configured routes
703
+ multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, explicitRoutes);
704
+ lighthouseErrorCount = multiRouteLhResult.perRoute.reduce((n, r) => n + r.errors.length, 0);
705
+ scores = multiRouteLhResult.aggregated ?? undefined;
706
+ }
707
+ else {
708
+ // Single-route mode (default): root only
709
+ const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
710
+ lighthouseErrorCount = lhResult.errors.length;
711
+ scores = lhResult.scores ?? undefined;
712
+ }
569
713
  if (scores) {
570
714
  lighthouseResult = {
571
715
  performance: scores.performance,
@@ -599,8 +743,10 @@ async function run() {
599
743
  }
600
744
  // Security audit (npm audit)
601
745
  try {
602
- securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
603
- if (securityAuditResult.critical > 0 || securityAuditResult.high > 0) {
746
+ securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir, verifyUrl);
747
+ if (securityAuditResult.critical > 0 ||
748
+ securityAuditResult.high > 0 ||
749
+ securityAuditResult.missingHeaders.length > 0) {
604
750
  failureEvidence.push(`Security: ${securityAuditResult.summary}`);
605
751
  }
606
752
  }
@@ -630,11 +776,22 @@ async function run() {
630
776
  const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
631
777
  e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
632
778
  }
779
+ // Store crawl-discovered routes for post-E2E Lighthouse pass
780
+ lastCrawlRoutes = e2eRuns.crawlResult.pages
781
+ .map((p) => p.path)
782
+ .filter((p) => typeof p === "string" && p.startsWith("/"));
633
783
  }
634
784
  // Broken links audit — runs after E2E so we have the crawl result
635
- if (e2eRuns.crawlResult && e2eRuns.crawlResult.totalLinks > 0) {
785
+ const auditRoutes = uniqueRouteList([
786
+ ...(config.extra_routes ?? []),
787
+ ...runtimeDiscoveredRoutes,
788
+ ...lastCrawlRoutes,
789
+ ]);
790
+ if (e2eRuns.crawlResult || auditRoutes.length > 0) {
636
791
  try {
637
- brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl);
792
+ brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl, {
793
+ extraRoutes: auditRoutes,
794
+ });
638
795
  if (brokenLinksResult.hasBrokenLinks) {
639
796
  failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
640
797
  }
@@ -670,6 +827,34 @@ async function run() {
670
827
  catch (e2eErr) {
671
828
  console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
672
829
  }
830
+ // Crawl-based multi-route Lighthouse (only if no explicit lighthouse_routes configured)
831
+ if (!args.skipLighthouse &&
832
+ !config.lighthouse_routes?.length &&
833
+ uniqueRouteList([
834
+ ...(config.extra_routes ?? []),
835
+ ...runtimeDiscoveredRoutes,
836
+ ...lastCrawlRoutes,
837
+ ]).length > 1) {
838
+ try {
839
+ const crawlRoutes = uniqueRouteList([
840
+ ...(config.extra_routes ?? []),
841
+ ...runtimeDiscoveredRoutes,
842
+ ...lastCrawlRoutes,
843
+ ]).slice(0, config.max_lighthouse_routes);
844
+ console.log(`\n Running Lighthouse on ${crawlRoutes.length} discovered routes...`);
845
+ multiRouteLhResult = await (0, lighthouse_js_1.runLighthouseOnRoutes)(port, config.lighthouse_runs, crawlRoutes);
846
+ if (multiRouteLhResult.aggregated) {
847
+ scores = multiRouteLhResult.aggregated;
848
+ lighthouseResult = {
849
+ ...multiRouteLhResult.aggregated,
850
+ runs: config.lighthouse_runs,
851
+ };
852
+ }
853
+ }
854
+ catch (crawlLhErr) {
855
+ console.error(`Crawl-based Lighthouse error: ${crawlLhErr instanceof Error ? crawlLhErr.message : String(crawlLhErr)}`);
856
+ }
857
+ }
673
858
  // Cross-browser E2E via Playwright (if non-chromium browsers configured)
674
859
  const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
675
860
  if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
@@ -694,11 +879,118 @@ async function run() {
694
879
  }
695
880
  }
696
881
  try {
697
- visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
882
+ visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify", {
883
+ pixelmatchThreshold: config.visual_diff.pixelmatchThreshold,
884
+ warnThreshold: config.visual_diff.warnThreshold,
885
+ rollbackThreshold: config.visual_diff.rollbackThreshold,
886
+ ignoreSelectors: config.visual_diff.ignoreSelectors,
887
+ disableAnimations: config.visual_diff.disableAnimations,
888
+ });
698
889
  }
699
890
  catch (visualErr) {
700
891
  console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
701
892
  }
893
+ // Mobile Lighthouse (standalone — not part of multi-viewport)
894
+ if (!args.skipLighthouse && !args.multiViewport) {
895
+ try {
896
+ mobileLighthouseScores = await (0, multi_viewport_js_1.runMobileLighthouse)(port);
897
+ }
898
+ catch (mlErr) {
899
+ console.error(`Mobile Lighthouse error: ${mlErr instanceof Error ? mlErr.message : String(mlErr)}`);
900
+ }
901
+ }
902
+ // TypeScript type check
903
+ if (config.typecheck) {
904
+ try {
905
+ typecheckResult = await (0, typecheck_js_1.runTypecheck)(args.projectDir);
906
+ if (!typecheckResult.skipped && !typecheckResult.passed) {
907
+ failureEvidence.push(`TypeScript: ${typecheckResult.errorCount} type error(s)`);
908
+ }
909
+ }
910
+ catch (tcErr) {
911
+ console.error(`Typecheck error: ${tcErr instanceof Error ? tcErr.message : String(tcErr)}`);
912
+ }
913
+ }
914
+ // Secret scan
915
+ if (config.secret_scan) {
916
+ try {
917
+ secretScanResult = await (0, secret_scan_js_1.runSecretScan)(args.projectDir, {
918
+ ignorePaths: config.secret_scan_ignore_paths,
919
+ });
920
+ if (!secretScanResult.skipped && !secretScanResult.passed) {
921
+ failureEvidence.push(`Secret scan: ${secretScanResult.findings.length} finding(s)`);
922
+ }
923
+ }
924
+ catch (ssErr) {
925
+ console.error(`Secret scan error: ${ssErr instanceof Error ? ssErr.message : String(ssErr)}`);
926
+ }
927
+ }
928
+ // Bundle size analysis (advisory)
929
+ if (config.bundle_size) {
930
+ try {
931
+ bundleSizeResult = await (0, bundle_size_js_1.runBundleSize)(args.projectDir);
932
+ }
933
+ catch (bsErr) {
934
+ console.error(`Bundle size error: ${bsErr instanceof Error ? bsErr.message : String(bsErr)}`);
935
+ }
936
+ }
937
+ // Outdated dependency check (advisory)
938
+ if (config.outdated_check) {
939
+ try {
940
+ outdatedCheckResult = await (0, outdated_check_js_1.runOutdatedCheck)(args.projectDir);
941
+ }
942
+ catch (ocErr) {
943
+ console.error(`Outdated check error: ${ocErr instanceof Error ? ocErr.message : String(ocErr)}`);
944
+ }
945
+ }
946
+ // Deep accessibility audit (axe-core)
947
+ if (config.a11y_deep) {
948
+ try {
949
+ a11yDeepResult = await (0, a11y_deep_js_1.runA11yDeep)(verifyUrl);
950
+ if (!a11yDeepResult.skipped && !a11yDeepResult.passed) {
951
+ failureEvidence.push(`A11y deep: ${a11yDeepResult.summary}`);
952
+ }
953
+ }
954
+ catch (a11yErr) {
955
+ console.error(`A11y deep error: ${a11yErr instanceof Error ? a11yErr.message : String(a11yErr)}`);
956
+ }
957
+ }
958
+ // Deep SEO audit
959
+ if (config.seo_deep) {
960
+ try {
961
+ seoDeepResult = await (0, seo_deep_js_1.runSeoDeep)(verifyUrl);
962
+ if (!seoDeepResult.skipped && !seoDeepResult.passed) {
963
+ failureEvidence.push(`SEO deep: ${seoDeepResult.summary}`);
964
+ }
965
+ }
966
+ catch (seoErr) {
967
+ console.error(`SEO deep error: ${seoErr instanceof Error ? seoErr.message : String(seoErr)}`);
968
+ }
969
+ }
970
+ // Core Web Vitals budget check
971
+ if (config.vitals_budget) {
972
+ try {
973
+ vitalsBudgetResult = await (0, vitals_budget_js_1.runVitalsBudget)(verifyUrl);
974
+ if (!vitalsBudgetResult.skipped && !vitalsBudgetResult.passed) {
975
+ failureEvidence.push(`Vitals budget: ${vitalsBudgetResult.summary}`);
976
+ }
977
+ }
978
+ catch (vbErr) {
979
+ console.error(`Vitals budget error: ${vbErr instanceof Error ? vbErr.message : String(vbErr)}`);
980
+ }
981
+ }
982
+ // Pro: multi-environment comparison
983
+ if (effectiveFeatures.compare_env && args.compareUrl) {
984
+ try {
985
+ compareEnvResult = await (0, compare_env_js_1.runEnvComparison)(port, args.compareUrl, 1);
986
+ }
987
+ catch (cmpErr) {
988
+ console.error(`Env comparison error: ${cmpErr instanceof Error ? cmpErr.message : String(cmpErr)}`);
989
+ }
990
+ }
991
+ else if (args.compareUrl && !effectiveFeatures.compare_env) {
992
+ console.log(" [warn] --compare requires a logged-in Pro account. Skipping env comparison.");
993
+ }
702
994
  }
703
995
  catch (serveErr) {
704
996
  console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
@@ -711,6 +1003,21 @@ async function run() {
711
1003
  }
712
1004
  const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
713
1005
  const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
1006
+ // Multi-route Lighthouse: add per-route failures to evidence
1007
+ const allRoutesPass = multiRouteLhResult
1008
+ ? multiRouteLhResult.allRoutesPass(adjustedThresholds)
1009
+ : true;
1010
+ if (multiRouteLhResult && !allRoutesPass) {
1011
+ const failingRoutes = multiRouteLhResult.perRoute.filter((r) => r.scores !== null &&
1012
+ (r.scores.performance < adjustedThresholds.performance ||
1013
+ r.scores.accessibility < adjustedThresholds.accessibility ||
1014
+ r.scores.seo < adjustedThresholds.seo ||
1015
+ r.scores.bestPractices < adjustedThresholds.bestPractices));
1016
+ for (const fr of failingRoutes.slice(0, 3)) {
1017
+ const s = fr.scores;
1018
+ failureEvidence.push(`Lighthouse ${fr.route}: P=${s.performance} A=${s.accessibility} SEO=${s.seo} BP=${s.bestPractices}`);
1019
+ }
1020
+ }
714
1021
  failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
715
1022
  ? e2eResult.results
716
1023
  .filter((scenario) => !scenario.passed)
@@ -754,11 +1061,38 @@ async function run() {
754
1061
  summary: brokenLinksResult.summary,
755
1062
  }
756
1063
  : undefined,
1064
+ typecheck: typecheckResult
1065
+ ? { passed: typecheckResult.passed, errorCount: typecheckResult.errorCount, skipped: typecheckResult.skipped }
1066
+ : undefined,
1067
+ secretScan: secretScanResult
1068
+ ? { passed: secretScanResult.passed, findingsCount: secretScanResult.findings.length, filesScanned: secretScanResult.filesScanned, skipped: secretScanResult.skipped }
1069
+ : undefined,
1070
+ a11yDeep: a11yDeepResult
1071
+ ? { passed: a11yDeepResult.passed, criticalCount: a11yDeepResult.criticalCount, seriousCount: a11yDeepResult.seriousCount, summary: a11yDeepResult.summary, skipped: a11yDeepResult.skipped }
1072
+ : undefined,
1073
+ seoDeep: seoDeepResult
1074
+ ? { passed: seoDeepResult.passed, errorCount: seoDeepResult.errorCount, warningCount: seoDeepResult.warningCount, summary: seoDeepResult.summary, skipped: seoDeepResult.skipped }
1075
+ : undefined,
1076
+ vitalsBudget: vitalsBudgetResult
1077
+ ? { passed: vitalsBudgetResult.passed, lcp: vitalsBudgetResult.lcp, cls: vitalsBudgetResult.cls, inp: vitalsBudgetResult.inp, summary: vitalsBudgetResult.summary, skipped: vitalsBudgetResult.skipped }
1078
+ : undefined,
1079
+ bundleSize: bundleSizeResult
1080
+ ? { framework: bundleSizeResult.framework, firstLoadJsKb: bundleSizeResult.firstLoadJsKb, largestChunkKb: bundleSizeResult.largestChunkKb, advisory: bundleSizeResult.advisory, skipped: bundleSizeResult.skipped }
1081
+ : undefined,
1082
+ outdatedCheck: outdatedCheckResult
1083
+ ? { outdatedCount: outdatedCheckResult.outdatedCount, majorOutdated: outdatedCheckResult.majorOutdated, advisory: outdatedCheckResult.advisory, skipped: outdatedCheckResult.skipped }
1084
+ : undefined,
757
1085
  failureEvidence,
758
1086
  }, {
759
1087
  tier: verificationTier,
760
1088
  thresholds: adjustedThresholds,
761
1089
  });
1090
+ // Gold eligibility: every route must individually pass all thresholds.
1091
+ // If the weighted-average aggregate earned Gold but some routes individually failed,
1092
+ // downgrade to Silver to enforce the per-route requirement.
1093
+ if (multiRouteLhResult && !allRoutesPass && verificationReport.grade === "gold") {
1094
+ verificationReport.grade = "silver";
1095
+ }
762
1096
  const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
763
1097
  const unifiedGrade = verificationReport.grade;
764
1098
  const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
@@ -773,9 +1107,19 @@ async function run() {
773
1107
  e2e: e2eResult,
774
1108
  crossBrowser: crossBrowserResults,
775
1109
  lighthouse: lighthouseResult,
1110
+ lighthouseRoutes: multiRouteLhResult
1111
+ ? multiRouteLhResult.perRoute.map((r) => ({ route: r.route, scores: r.scores }))
1112
+ : undefined,
776
1113
  mobileLighthouse: mobileLighthouseScores,
777
1114
  security: securityAuditResult,
778
1115
  visualDiff: visualDiffResult,
1116
+ typecheck: typecheckResult,
1117
+ secretScan: secretScanResult,
1118
+ bundleSize: bundleSizeResult,
1119
+ outdatedCheck: outdatedCheckResult,
1120
+ a11yDeep: a11yDeepResult,
1121
+ seoDeep: seoDeepResult,
1122
+ vitalsBudget: vitalsBudgetResult,
779
1123
  thresholds: adjustedThresholds,
780
1124
  ciMode: config.ciMode,
781
1125
  framework: detected.framework,
@@ -789,6 +1133,7 @@ async function run() {
789
1133
  report: verificationReport,
790
1134
  view: verificationView,
791
1135
  },
1136
+ compareEnv: compareEnvResult ?? undefined,
792
1137
  };
793
1138
  const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
794
1139
  if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
@@ -849,6 +1194,7 @@ async function run() {
849
1194
  ? "client-ready"
850
1195
  : "hold",
851
1196
  share: args.share,
1197
+ blockers: verificationView.blockers.slice(0, 5).map((b) => b.title),
852
1198
  scores: {
853
1199
  performance: scores?.performance,
854
1200
  accessibility: scores?.accessibility,
@@ -890,6 +1236,38 @@ async function run() {
890
1236
  }
891
1237
  }
892
1238
  writeResultFile(args.projectDir, resultObj);
1239
+ // Pro: AI failure analysis — runs after result is finalized, before output
1240
+ if (effectiveFeatures.ai_failure_analysis &&
1241
+ verificationReport.verdict !== "client-ready" &&
1242
+ verificationReport.verdict !== "release-ready" &&
1243
+ args.format !== "json") {
1244
+ try {
1245
+ console.log("\n Requesting AI failure analysis...");
1246
+ const aiAnalysis = await (0, ai_analysis_js_1.requestAiFailureAnalysis)({
1247
+ grade: unifiedGrade,
1248
+ blockers: verificationView.blockers.map((b) => ({ title: b.title, action: b.action })),
1249
+ lighthouseScores: scores ?? null,
1250
+ thresholds: adjustedThresholds,
1251
+ buildErrors: buildResult.errors.slice(0, 3),
1252
+ e2eFailed: e2eResult ? e2eResult.failed : 0,
1253
+ e2eTotal: e2eResult ? e2eResult.total : 0,
1254
+ securitySummary: securityAuditResult?.summary,
1255
+ });
1256
+ if (aiAnalysis) {
1257
+ console.log("\n AI Analysis (Pro):");
1258
+ console.log(` Root cause: ${aiAnalysis.rootCause}`);
1259
+ if (aiAnalysis.topFixes.length > 0) {
1260
+ console.log(" Top fixes:");
1261
+ for (const fix of aiAnalysis.topFixes) {
1262
+ console.log(` - ${fix}`);
1263
+ }
1264
+ }
1265
+ }
1266
+ }
1267
+ catch {
1268
+ // AI analysis errors are non-critical
1269
+ }
1270
+ }
893
1271
  if (args.format === "json") {
894
1272
  console.log(JSON.stringify(resultObj, null, 2));
895
1273
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Multi-environment Lighthouse comparison (Pro feature).
3
+ *
4
+ * Compares Lighthouse scores between the local build and a reference
5
+ * environment URL (e.g. staging, production) to detect regressions
6
+ * introduced by the current PR.
7
+ */
8
+ import type { LighthouseScores } from "./grade.js";
9
+ export interface EnvComparisonResult {
10
+ localUrl: string;
11
+ compareUrl: string;
12
+ local: LighthouseScores | null;
13
+ compare: LighthouseScores | null;
14
+ /** Positive = local is better, negative = compare env is better */
15
+ delta: {
16
+ performance: number | null;
17
+ accessibility: number | null;
18
+ seo: number | null;
19
+ bestPractices: number | null;
20
+ } | null;
21
+ }
22
+ export declare function runEnvComparison(localPort: number, compareUrl: string, runs?: number): Promise<EnvComparisonResult>;
23
+ export declare function printEnvComparison(result: EnvComparisonResult): void;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runEnvComparison = runEnvComparison;
4
+ exports.printEnvComparison = printEnvComparison;
5
+ const lighthouse_js_1 = require("./lighthouse.js");
6
+ function scoreDelta(local, compare) {
7
+ if (!local || !compare)
8
+ return null;
9
+ return {
10
+ performance: local.performance - compare.performance,
11
+ accessibility: local.accessibility - compare.accessibility,
12
+ seo: local.seo - compare.seo,
13
+ bestPractices: local.bestPractices - compare.bestPractices,
14
+ };
15
+ }
16
+ async function runEnvComparison(localPort, compareUrl, runs = 1) {
17
+ const localUrl = `http://127.0.0.1:${localPort}/`;
18
+ console.log(`\n [compare] Running env comparison: local vs ${compareUrl}`);
19
+ const [localResult, compareResult] = await Promise.all([
20
+ (0, lighthouse_js_1.runLighthouseOnUrl)(localUrl, runs),
21
+ (0, lighthouse_js_1.runLighthouseOnUrl)(compareUrl, runs),
22
+ ]);
23
+ return {
24
+ localUrl,
25
+ compareUrl,
26
+ local: localResult.scores,
27
+ compare: compareResult.scores,
28
+ delta: scoreDelta(localResult.scores, compareResult.scores),
29
+ };
30
+ }
31
+ function printEnvComparison(result) {
32
+ console.log("\n Environment comparison:");
33
+ console.log(` Local: ${result.localUrl}`);
34
+ console.log(` Compare: ${result.compareUrl}`);
35
+ if (!result.local && !result.compare) {
36
+ console.log(" Both environments returned no Lighthouse data.");
37
+ return;
38
+ }
39
+ const fmt = (v) => (v === null ? "—" : String(v));
40
+ const fmtDelta = (d) => {
41
+ if (d === null)
42
+ return "";
43
+ if (d > 0)
44
+ return ` (+${d})`;
45
+ if (d < 0)
46
+ return ` (${d})`;
47
+ return " (=)";
48
+ };
49
+ const d = result.delta;
50
+ console.log(" Metric Local Compare Delta");
51
+ console.log(` Performance ${fmt(result.local?.performance ?? null).padEnd(6)} ${fmt(result.compare?.performance ?? null).padEnd(8)}${fmtDelta(d?.performance ?? null)}`);
52
+ console.log(` Accessibility ${fmt(result.local?.accessibility ?? null).padEnd(6)} ${fmt(result.compare?.accessibility ?? null).padEnd(8)}${fmtDelta(d?.accessibility ?? null)}`);
53
+ console.log(` SEO ${fmt(result.local?.seo ?? null).padEnd(6)} ${fmt(result.compare?.seo ?? null).padEnd(8)}${fmtDelta(d?.seo ?? null)}`);
54
+ console.log(` Best Practices ${fmt(result.local?.bestPractices ?? null).padEnd(6)} ${fmt(result.compare?.bestPractices ?? null).padEnd(8)}${fmtDelta(d?.bestPractices ?? null)}`);
55
+ }