laxy-verify 1.2.1 → 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 +193 -64
  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 +432 -16
  11. package/dist/compare-env.d.ts +23 -0
  12. package/dist/compare-env.js +55 -0
  13. package/dist/config.d.ts +50 -0
  14. package/dist/config.js +149 -4
  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;
@@ -159,13 +169,40 @@ function parseArgs() {
159
169
  crawl: flags.crawl !== undefined,
160
170
  planOverride: flags["plan-override"],
161
171
  port: flags.port !== undefined ? Number(flags.port) : undefined,
172
+ share: flags.share !== undefined,
162
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,
163
182
  };
164
183
  }
165
184
  function writeResultFile(projectDir, result) {
166
185
  const filePath = path.join(projectDir, ".laxy-result.json");
167
186
  fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
168
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
+ }
169
206
  function summarizeViewportIssues(scores, thresholds) {
170
207
  if (!scores)
171
208
  return { count: 0 };
@@ -207,12 +244,30 @@ function consoleOutput(result) {
207
244
  const lh = result.lighthouse;
208
245
  const t = result.thresholds;
209
246
  const check = (passed) => (passed ? " OK" : " FAIL");
210
- 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}:`);
211
251
  console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
212
252
  console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
213
253
  console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
214
254
  console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
215
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
+ }
216
271
  }
217
272
  else {
218
273
  console.log(" Lighthouse: skipped");
@@ -237,6 +292,27 @@ function consoleOutput(result) {
237
292
  const ml = result.mobileLighthouse;
238
293
  console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
239
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
+ }
240
316
  if (result.verification) {
241
317
  const view = result.verification.view;
242
318
  const verboseFailure = result._verbose_failure ?? true;
@@ -292,10 +368,16 @@ function consoleOutput(result) {
292
368
  if (result.github.status === "status_set")
293
369
  console.log(` Status check: ${result.github.grade}`);
294
370
  }
371
+ if (result.compareEnv) {
372
+ (0, compare_env_js_1.printEnvComparison)(result.compareEnv);
373
+ }
295
374
  console.log(" Result: .laxy-result.json");
296
375
  if (result.markdownReportPath) {
297
376
  console.log(` Report: ${path.basename(result.markdownReportPath)}`);
298
377
  }
378
+ if (result.share?.url) {
379
+ console.log(` Share: ${result.share.url}`);
380
+ }
299
381
  console.log(` Exit code: ${result.exitCode}`);
300
382
  }
301
383
  async function run() {
@@ -324,10 +406,19 @@ async function run() {
324
406
  --fail-on unverified | bronze | silver | gold
325
407
  --skip-lighthouse Skip Lighthouse but still run build and E2E
326
408
  --port <port> Use an already-running dev server on this port (skip build & server start)
409
+ --share Create a public share link for this verification result (Pro)
410
+ --compare <url> Compare Lighthouse scores against a reference environment URL (Pro)
327
411
  --plan-override free | pro | team (testing metadata only)
328
412
  --multi-viewport Lighthouse on desktop/tablet/mobile
329
- --crawl Crawl the app to discover routes before E2E
413
+ --crawl Crawl the app to discover routes before E2E (also enables multi-route Lighthouse)
330
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)
331
422
  --help Show this help
332
423
 
333
424
  Exit codes:
@@ -341,6 +432,8 @@ async function run() {
341
432
  npx laxy-verify . --ci # CI mode
342
433
  npx laxy-verify . --fail-on silver # Block Bronze or worse
343
434
  npx laxy-verify . --port 3001 # Use existing dev server on port 3001
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)
344
437
 
345
438
  Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
346
439
  `);
@@ -417,6 +510,11 @@ async function run() {
417
510
  exitGracefully(0);
418
511
  return;
419
512
  }
513
+ // 팀 공통 임계값을 서버에서 로드 (토큰 없음/네트워크 오류 시 null)
514
+ const teamThresholds = await (0, config_js_1.fetchTeamThresholds)();
515
+ if (teamThresholds && args.format !== "json") {
516
+ console.log(" Team thresholds loaded from laxy.dev");
517
+ }
420
518
  let config;
421
519
  try {
422
520
  config = (0, config_js_1.loadConfig)({
@@ -426,7 +524,15 @@ async function run() {
426
524
  cliFlags: {
427
525
  failOn: args.failOn,
428
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,
429
534
  },
535
+ teamThresholds,
430
536
  });
431
537
  }
432
538
  catch (err) {
@@ -434,6 +540,21 @@ async function run() {
434
540
  exitGracefully(2);
435
541
  return;
436
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
+ }
437
558
  let detected;
438
559
  try {
439
560
  detected = (0, detect_js_1.detect)(args.projectDir);
@@ -465,7 +586,9 @@ async function run() {
465
586
  exitGracefully(2);
466
587
  return;
467
588
  }
468
- console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
589
+ if (args.format !== "json") {
590
+ console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
591
+ }
469
592
  }
470
593
  let buildResult;
471
594
  if (useExistingServer) {
@@ -503,11 +626,11 @@ async function run() {
503
626
  }
504
627
  const features = entitlements ?? {
505
628
  plan: "free",
506
- // All verification features run on every plan
507
- // Automation features ??not available on Free
508
629
  github_actions: false,
509
630
  queue_priority: false,
510
631
  parallel_execution: false,
632
+ ai_failure_analysis: false,
633
+ compare_env: false,
511
634
  };
512
635
  let effectiveFeatures = features;
513
636
  if (args.planOverride) {
@@ -525,17 +648,28 @@ async function run() {
525
648
  if (config.lighthouse_runs < 3) {
526
649
  config = { ...config, lighthouse_runs: 3 };
527
650
  }
651
+ let multiRouteLhResult = null;
528
652
  let multiViewportScores = null;
529
653
  let allViewportsOk = false;
530
654
  let e2eResult;
531
655
  let crossBrowserResults;
532
656
  let e2eCoverageGaps = [];
533
657
  let e2eConsoleErrors = [];
658
+ let lastCrawlRoutes = [];
659
+ let runtimeDiscoveredRoutes = [];
534
660
  let e2eStabilityPassed = true;
535
661
  let visualDiffResult = null;
536
662
  let securityAuditResult = null;
537
663
  let mobileLighthouseScores = null;
538
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;
539
673
  const failureEvidence = [];
540
674
  if (buildResult.success) {
541
675
  let servePid;
@@ -545,13 +679,37 @@ async function run() {
545
679
  servePid = serve.pid;
546
680
  activeDevServerPid = serve.pid;
547
681
  }
548
- const verifyUrl = `http://127.0.0.1:${port}/`;
682
+ const verifyUrl = `http://localhost:${port}/`;
549
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
+ }
550
694
  if (!args.skipLighthouse) {
551
695
  try {
552
- const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
553
- lighthouseErrorCount = lhResult.errors.length;
554
- 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
+ }
555
713
  if (scores) {
556
714
  lighthouseResult = {
557
715
  performance: scores.performance,
@@ -585,8 +743,10 @@ async function run() {
585
743
  }
586
744
  // Security audit (npm audit)
587
745
  try {
588
- securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
589
- 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) {
590
750
  failureEvidence.push(`Security: ${securityAuditResult.summary}`);
591
751
  }
592
752
  }
@@ -616,11 +776,22 @@ async function run() {
616
776
  const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
617
777
  e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
618
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("/"));
619
783
  }
620
784
  // Broken links audit — runs after E2E so we have the crawl result
621
- 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) {
622
791
  try {
623
- 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
+ });
624
795
  if (brokenLinksResult.hasBrokenLinks) {
625
796
  failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
626
797
  }
@@ -656,6 +827,34 @@ async function run() {
656
827
  catch (e2eErr) {
657
828
  console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
658
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
+ }
659
858
  // Cross-browser E2E via Playwright (if non-chromium browsers configured)
660
859
  const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
661
860
  if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
@@ -680,11 +879,118 @@ async function run() {
680
879
  }
681
880
  }
682
881
  try {
683
- 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
+ });
684
889
  }
685
890
  catch (visualErr) {
686
891
  console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
687
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
+ }
688
994
  }
689
995
  catch (serveErr) {
690
996
  console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
@@ -697,6 +1003,21 @@ async function run() {
697
1003
  }
698
1004
  const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
699
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
+ }
700
1021
  failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
701
1022
  ? e2eResult.results
702
1023
  .filter((scenario) => !scenario.passed)
@@ -740,11 +1061,38 @@ async function run() {
740
1061
  summary: brokenLinksResult.summary,
741
1062
  }
742
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,
743
1085
  failureEvidence,
744
1086
  }, {
745
1087
  tier: verificationTier,
746
1088
  thresholds: adjustedThresholds,
747
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
+ }
748
1096
  const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
749
1097
  const unifiedGrade = verificationReport.grade;
750
1098
  const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
@@ -759,9 +1107,19 @@ async function run() {
759
1107
  e2e: e2eResult,
760
1108
  crossBrowser: crossBrowserResults,
761
1109
  lighthouse: lighthouseResult,
1110
+ lighthouseRoutes: multiRouteLhResult
1111
+ ? multiRouteLhResult.perRoute.map((r) => ({ route: r.route, scores: r.scores }))
1112
+ : undefined,
762
1113
  mobileLighthouse: mobileLighthouseScores,
763
1114
  security: securityAuditResult,
764
1115
  visualDiff: visualDiffResult,
1116
+ typecheck: typecheckResult,
1117
+ secretScan: secretScanResult,
1118
+ bundleSize: bundleSizeResult,
1119
+ outdatedCheck: outdatedCheckResult,
1120
+ a11yDeep: a11yDeepResult,
1121
+ seoDeep: seoDeepResult,
1122
+ vitalsBudget: vitalsBudgetResult,
765
1123
  thresholds: adjustedThresholds,
766
1124
  ciMode: config.ciMode,
767
1125
  framework: detected.framework,
@@ -775,6 +1133,7 @@ async function run() {
775
1133
  report: verificationReport,
776
1134
  view: verificationView,
777
1135
  },
1136
+ compareEnv: compareEnvResult ?? undefined,
778
1137
  };
779
1138
  const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
780
1139
  if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
@@ -814,6 +1173,9 @@ async function run() {
814
1173
  // Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
815
1174
  const token = (0, auth_js_1.loadToken)();
816
1175
  const isPro = effectiveFeatures.plan === "pro" || effectiveFeatures.plan === "team";
1176
+ if (args.share && (!token || !isPro)) {
1177
+ console.warn(" [warn] --share requires a logged-in Pro or Team account.");
1178
+ }
817
1179
  if (token && isPro) {
818
1180
  try {
819
1181
  const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
@@ -828,7 +1190,11 @@ async function run() {
828
1190
  repo_id: repoId,
829
1191
  project_name: path.basename(path.resolve(args.projectDir)),
830
1192
  grade: unifiedGrade,
831
- verdict: verificationReport.verdict,
1193
+ verdict: verificationReport.verdict === "client-ready" || verificationReport.verdict === "release-ready"
1194
+ ? "client-ready"
1195
+ : "hold",
1196
+ share: args.share,
1197
+ blockers: verificationView.blockers.slice(0, 5).map((b) => b.title),
832
1198
  scores: {
833
1199
  performance: scores?.performance,
834
1200
  accessibility: scores?.accessibility,
@@ -840,18 +1206,68 @@ async function run() {
840
1206
  console_error_count: e2eConsoleErrors.length,
841
1207
  broken_link_count: brokenLinksResult?.brokenLinks.length,
842
1208
  },
843
- full_result: { framework: detected.framework },
1209
+ full_result: {
1210
+ framework: detected.framework,
1211
+ build: { success: buildResult.success },
1212
+ e2e: e2eResult ? { passed: e2eResult.passed, total: e2eResult.total } : null,
1213
+ lighthouse: lighthouseResult,
1214
+ verification: {
1215
+ report: verificationReport,
1216
+ },
1217
+ },
844
1218
  }),
845
1219
  });
846
1220
  if (!saveRes.ok) {
847
1221
  const errBody = await saveRes.json().catch(() => ({}));
848
1222
  console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
849
1223
  }
1224
+ else {
1225
+ const saveData = await saveRes.json().catch(() => ({}));
1226
+ if (saveData.share_id && saveData.share_url) {
1227
+ resultObj.share = {
1228
+ id: saveData.share_id,
1229
+ url: saveData.share_url,
1230
+ };
1231
+ }
1232
+ }
850
1233
  }
851
1234
  catch {
852
1235
  // 네트워크 오류는 검증 결과에 영향 없음
853
1236
  }
854
1237
  }
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
+ }
855
1271
  if (args.format === "json") {
856
1272
  console.log(JSON.stringify(resultObj, null, 2));
857
1273
  }