laxy-verify 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,15 +2,44 @@
2
2
 
3
3
  `laxy-verify` is a deployment blocker gate for frontend apps.
4
4
 
5
- Its job is simple:
5
+ **Free**: Run verification manually on any project.
6
+ **Pro**: GitHub Actions auto-runs it on every PR + Slack/Discord alerts when things break.
6
7
 
7
- - catch the broken build before merge
8
- - catch the broken user flow before QA or client review
9
- - catch the obvious mobile or viewport break before release
8
+ ## Quick start
9
+
10
+ Run it on a frontend app:
11
+
12
+ ```bash
13
+ cd your-project
14
+ npx laxy-verify .
15
+ ```
16
+
17
+ Generate config plus GitHub Actions workflow:
18
+
19
+ ```bash
20
+ npx laxy-verify --init
21
+ ```
22
+
23
+ That creates:
10
24
 
11
- It runs your production build, Lighthouse, and user-visible verification flows in one command, then leaves one decision and one evidence trail for local use or CI.
25
+ - `.laxy.yml`
26
+ - `.github/workflows/laxy-verify.yml`
27
+
28
+ Log in to unlock Pro features:
29
+
30
+ ```bash
31
+ npx laxy-verify login
32
+ npx laxy-verify whoami
33
+ ```
34
+
35
+ For CI, set `LAXY_TOKEN` instead of interactive login:
36
+
37
+ ```yaml
38
+ env:
39
+ LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
40
+ ```
12
41
 
13
- ## Why use this instead of stitching tools together?
42
+ ## Why laxy-verify?
14
43
 
15
44
  Most teams already have some mix of:
16
45
 
@@ -26,7 +55,7 @@ The gap is not "can these tools exist together?" The gap is "who turns that pile
26
55
  - one command instead of a custom build plus audit plus smoke-check script stack
27
56
  - one result file instead of scattered logs
28
57
  - one blocker-first decision instead of "build passed, but do we actually trust this release?"
29
- - a faster setup path with `--init` for config plus GitHub Actions
58
+ - **Pro**: GitHub Actions auto-run + Slack/Discord alerts so you never miss a failure
30
59
 
31
60
  This is most useful if you ship frontend apps and want a practical gate before:
32
61
 
@@ -201,7 +230,7 @@ Options:
201
230
  --config <path>
202
231
  --fail-on unverified|bronze|silver|gold
203
232
  --skip-lighthouse
204
- --plan-override free|pro|pro_plus
233
+ --plan-override free|pro|team
205
234
  --badge
206
235
  --init
207
236
  --multi-viewport
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Broken-links audit.
3
+ * Uses the crawl result to find all internal links, then validates each
4
+ * with an HTTP HEAD/GET request. Links that return 4xx/5xx or timeout are
5
+ * reported as blockers.
6
+ */
7
+ import { type CrawlResult } from "../crawler.js";
8
+ export interface BrokenLink {
9
+ url: string;
10
+ path: string;
11
+ status: number;
12
+ statusText: string;
13
+ severity: "critical" | "high";
14
+ }
15
+ export interface BrokenLinksResult {
16
+ brokenLinks: BrokenLink[];
17
+ checkedCount: number;
18
+ hasBrokenLinks: boolean;
19
+ summary: string;
20
+ }
21
+ export declare function auditBrokenLinks(crawlResult: CrawlResult, baseUrl: string, abortSignal?: AbortSignal): Promise<BrokenLinksResult>;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditBrokenLinks = auditBrokenLinks;
4
+ const TIMEOUT_MS = 5000;
5
+ const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
6
+ function isSuccessStatus(n) {
7
+ return VALID_OK_STATUS.includes(n);
8
+ }
9
+ async function auditBrokenLinks(crawlResult, baseUrl, abortSignal) {
10
+ const origin = new URL(baseUrl).origin;
11
+ const allUrls = [];
12
+ for (const page of crawlResult.pages) {
13
+ for (const href of page.internalLinks) {
14
+ try {
15
+ const url = new URL(href, baseUrl).href;
16
+ if (!allUrls.includes(url))
17
+ allUrls.push(url);
18
+ }
19
+ catch {
20
+ // skip malformed URLs
21
+ }
22
+ }
23
+ }
24
+ const uniqueUrls = allUrls;
25
+ const brokenLinks = [];
26
+ await Promise.all(uniqueUrls.map(async (url) => {
27
+ if (abortSignal?.aborted)
28
+ return;
29
+ try {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
32
+ let status = 0;
33
+ let statusText = "";
34
+ try {
35
+ const res = await fetch(url, {
36
+ method: "HEAD",
37
+ redirect: "follow",
38
+ signal: controller.signal,
39
+ });
40
+ status = res.status;
41
+ statusText = res.statusText;
42
+ }
43
+ catch {
44
+ // Fall back to GET if HEAD is not allowed
45
+ const controller2 = new AbortController();
46
+ const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
47
+ try {
48
+ const res = await fetch(url, {
49
+ method: "GET",
50
+ redirect: "follow",
51
+ signal: controller2.signal,
52
+ });
53
+ status = res.status;
54
+ statusText = res.statusText;
55
+ }
56
+ catch {
57
+ status = 0;
58
+ statusText = "timeout or network error";
59
+ }
60
+ clearTimeout(timer2);
61
+ }
62
+ clearTimeout(timer);
63
+ if (!isSuccessStatus(status)) {
64
+ const severity = status >= 500 ? "critical" : "high";
65
+ let path = url;
66
+ try {
67
+ path = new URL(url).pathname;
68
+ }
69
+ catch { }
70
+ brokenLinks.push({ url, path, status, statusText, severity });
71
+ }
72
+ }
73
+ catch {
74
+ // skip
75
+ }
76
+ }));
77
+ const summary = brokenLinks.length === 0
78
+ ? `All ${uniqueUrls.length} links OK`
79
+ : `${brokenLinks.length} broken link(s) found`;
80
+ return {
81
+ brokenLinks,
82
+ checkedCount: uniqueUrls.length,
83
+ hasBrokenLinks: brokenLinks.length > 0,
84
+ summary,
85
+ };
86
+ }
package/dist/cli.js CHANGED
@@ -57,6 +57,7 @@ const playwright_runner_js_1 = require("./playwright-runner.js");
57
57
  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
+ const broken_links_js_1 = require("./audit/broken-links.js");
60
61
  const index_js_1 = require("./verification-core/index.js");
61
62
  const package_json_1 = __importDefault(require("../package.json"));
62
63
  function shouldFailVerificationResult(report, failOn) {
@@ -64,7 +65,7 @@ function shouldFailVerificationResult(report, failOn) {
64
65
  return false;
65
66
  if (report.verdict === "build-failed" || report.verdict === "hold")
66
67
  return true;
67
- if (report.tier === "pro_plus" && report.verdict === "investigate")
68
+ if (report.tier === "team" && report.verdict === "investigate")
68
69
  return true;
69
70
  return (0, grade_js_1.isWorseOrEqual)(report.grade, failOn);
70
71
  }
@@ -210,18 +211,14 @@ function consoleOutput(result) {
210
211
  }
211
212
  if (result.verification) {
212
213
  const view = result.verification.view;
213
- const isPro = view.tier === "pro" || view.tier === "pro_plus";
214
- const isProPlus = view.tier === "pro_plus";
215
- // verbose_failure: Pro에서 서버가 활성화하면 blockers 전체 설명 표시
216
- // failure_analysis: Pro+에서 서버가 활성화하면 warnings 전체 설명 + evidence 전체 표시
217
- const verboseFailure = isPro && (result._verbose_failure ?? isPro);
218
- const failureAnalysis = isProPlus && (result._failure_analysis ?? isProPlus);
214
+ const verboseFailure = result._verbose_failure ?? true;
215
+ const failureAnalysis = result._failure_analysis ?? true;
219
216
  console.log(` Verification depth: ${view.tier}`);
220
217
  console.log(` Decision question: ${view.question}`);
221
218
  console.log(` Decision: ${view.verdict} (${view.confidence})`);
222
219
  console.log(` Why it stopped here: ${view.summary}`);
223
- // Pro/Pro+: 체크 통과 목록 요약
224
- if (isPro && view.passes.length > 0) {
220
+ // Passed/Failed checks summary
221
+ if (view.passes.length > 0) {
225
222
  const passedChecks = view.passes.filter((p) => p.passed).map((p) => p.label);
226
223
  const failedChecks = view.passes.filter((p) => !p.passed).map((p) => p.label);
227
224
  if (passedChecks.length > 0)
@@ -238,8 +235,8 @@ function consoleOutput(result) {
238
235
  console.log(` Fix: ${blocker.action}`);
239
236
  }
240
237
  }
241
- // Warnings: Pro/Pro+에서만 표시, Review 액션은 failure_analysis(Pro+)에서 표시
242
- if (isPro && view.warnings.length > 0) {
238
+ // Warnings: always show all
239
+ if (view.warnings.length > 0) {
243
240
  console.log(" Risks to review:");
244
241
  for (const warning of view.warnings) {
245
242
  console.log(` - ${warning.title}`);
@@ -297,8 +294,8 @@ async function run() {
297
294
  --config <path> Path to .laxy.yml
298
295
  --fail-on unverified | bronze | silver | gold
299
296
  --skip-lighthouse Skip Lighthouse but still run build and E2E
300
- --plan-override free | pro | pro_plus (downgrade testing only)
301
- --multi-viewport Pro+: Lighthouse on desktop/tablet/mobile
297
+ --plan-override free | pro | team (downgrade testing only)
298
+ --multi-viewport team: Lighthouse on desktop/tablet/mobile
302
299
  --crawl Crawl the app to discover routes before E2E
303
300
  --badge Print shields.io badge markdown
304
301
  --help Show this help
@@ -423,12 +420,11 @@ async function run() {
423
420
  }
424
421
  const features = entitlements ?? {
425
422
  plan: "free",
426
- gold_grade: false,
427
- lighthouse_runs_3: false,
428
- verbose_failure: false,
429
- multi_viewport: false,
430
- failure_analysis: false,
431
- fast_lane: false,
423
+ // All verification features run on every plan
424
+ // Automation features — not available on Free
425
+ github_actions: false,
426
+ queue_priority: false,
427
+ parallel_execution: false,
432
428
  };
433
429
  let effectiveFeatures = features;
434
430
  if (args.planOverride) {
@@ -442,7 +438,8 @@ async function run() {
442
438
  return;
443
439
  }
444
440
  }
445
- if (effectiveFeatures.lighthouse_runs_3 && config.lighthouse_runs < 3) {
441
+ // Always run 3 Lighthouse passes for median score
442
+ if (config.lighthouse_runs < 3) {
446
443
  config = { ...config, lighthouse_runs: 3 };
447
444
  }
448
445
  let multiViewportScores = null;
@@ -455,6 +452,7 @@ async function run() {
455
452
  let visualDiffResult = null;
456
453
  let securityAuditResult = null;
457
454
  let mobileLighthouseScores = null;
455
+ let brokenLinksResult = null;
458
456
  const failureEvidence = [];
459
457
  if (buildResult.success) {
460
458
  let servePid;
@@ -482,15 +480,11 @@ async function run() {
482
480
  console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
483
481
  }
484
482
  }
485
- if (!args.skipLighthouse && args.multiViewport && !effectiveFeatures.multi_viewport) {
486
- console.log("\n Note: --multi-viewport requires Pro+. Run laxy-verify login with a paid account to unlock it.");
487
- }
488
- else if (!args.skipLighthouse && effectiveFeatures.multi_viewport) {
483
+ if (!args.skipLighthouse && args.multiViewport) {
489
484
  try {
490
485
  multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(port);
491
486
  (0, multi_viewport_js_1.printMultiViewportResults)(multiViewportScores, adjustedThresholds);
492
487
  allViewportsOk = (0, multi_viewport_js_1.allViewportsPass)(multiViewportScores, adjustedThresholds);
493
- // Surface screenshot diff issues in failure evidence
494
488
  if (multiViewportScores.screenshotDiffs) {
495
489
  for (const diff of multiViewportScores.screenshotDiffs) {
496
490
  if (!diff.baselineCreated && diff.diffPercent > 10) {
@@ -503,10 +497,9 @@ async function run() {
503
497
  console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
504
498
  }
505
499
  }
506
- // Pro: single mobile Lighthouse check (if not using full multi-viewport)
500
+ // Mobile Lighthouse check (runs when multi-viewport is not enabled)
507
501
  if (!args.skipLighthouse &&
508
- verificationTier === "pro" &&
509
- !effectiveFeatures.multi_viewport) {
502
+ !args.multiViewport) {
510
503
  try {
511
504
  mobileLighthouseScores = await (0, multi_viewport_js_1.runMobileLighthouse)(port);
512
505
  if (mobileLighthouseScores) {
@@ -521,18 +514,16 @@ async function run() {
521
514
  console.error(`Mobile Lighthouse error: ${mobileLhErr instanceof Error ? mobileLhErr.message : String(mobileLhErr)}`);
522
515
  }
523
516
  }
524
- // Pro/Pro+: security audit
525
- if (verificationTier !== "free") {
526
- try {
527
- securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
528
- if (securityAuditResult.critical > 0 || securityAuditResult.high > 0) {
529
- failureEvidence.push(`Security: ${securityAuditResult.summary}`);
530
- }
531
- }
532
- catch (secErr) {
533
- console.error(`Security audit error: ${secErr instanceof Error ? secErr.message : String(secErr)}`);
517
+ // Security audit (npm audit)
518
+ try {
519
+ securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
520
+ if (securityAuditResult.critical > 0 || securityAuditResult.high > 0) {
521
+ failureEvidence.push(`Security: ${securityAuditResult.summary}`);
534
522
  }
535
523
  }
524
+ catch (secErr) {
525
+ console.error(`Security audit error: ${secErr instanceof Error ? secErr.message : String(secErr)}`);
526
+ }
536
527
  const crawlEnabled = args.crawl || config.crawl;
537
528
  const crawlOpts = crawlEnabled
538
529
  ? { enabled: true, maxDepth: config.max_crawl_depth, maxPages: config.max_crawl_pages }
@@ -549,9 +540,21 @@ async function run() {
549
540
  };
550
541
  e2eCoverageGaps = e2eRuns.coverageGaps;
551
542
  e2eConsoleErrors = e2eRuns.consoleErrors;
552
- // Pro+ stability: run E2E a second time if first run passed all
553
- if (verificationTier === "pro_plus" && e2eRuns.passed === e2eRuns.results.length && e2eRuns.results.length > 0) {
554
- console.log(" [Pro+] Running stability pass (run 2/2)...");
543
+ // Broken links audit runs after E2E so we have the crawl result
544
+ if (e2eRuns.crawlResult && e2eRuns.crawlResult.totalLinks > 0) {
545
+ try {
546
+ brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl);
547
+ if (brokenLinksResult.hasBrokenLinks) {
548
+ failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
549
+ }
550
+ }
551
+ catch (blErr) {
552
+ console.error(`Broken links audit error: ${blErr instanceof Error ? blErr.message : String(blErr)}`);
553
+ }
554
+ }
555
+ // E2E stability: run a second time if first run passed all
556
+ if (e2eRuns.passed === e2eRuns.results.length && e2eRuns.results.length > 0) {
557
+ console.log(" Running stability pass (run 2/2)...");
555
558
  const e2eRuns2 = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
556
559
  if (e2eRuns2.passed < e2eRuns2.results.length) {
557
560
  e2eStabilityPassed = false;
@@ -563,7 +566,7 @@ async function run() {
563
566
  failureEvidence.push(`E2E stability: second run failed (${failedNames})`);
564
567
  }
565
568
  else {
566
- console.log(" [Pro+] Stability pass: OK (2/2 runs passed)");
569
+ console.log(" Stability pass: OK (2/2 runs passed)");
567
570
  }
568
571
  }
569
572
  if (e2eRuns.coverageGaps.length > 0) {
@@ -599,13 +602,11 @@ async function run() {
599
602
  console.log(" Note: Cross-browser testing requires playwright. Run: npm install -D playwright && npx playwright install");
600
603
  }
601
604
  }
602
- if (verificationTier === "pro_plus") {
603
- try {
604
- visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
605
- }
606
- catch (visualErr) {
607
- console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
608
- }
605
+ try {
606
+ visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
607
+ }
608
+ catch (visualErr) {
609
+ console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
609
610
  }
610
611
  }
611
612
  catch (serveErr) {
@@ -657,6 +658,13 @@ async function run() {
657
658
  summary: securityAuditResult.summary,
658
659
  }
659
660
  : undefined,
661
+ brokenLinksAudit: brokenLinksResult
662
+ ? {
663
+ checkedCount: brokenLinksResult.checkedCount,
664
+ brokenCount: brokenLinksResult.brokenLinks.length,
665
+ summary: brokenLinksResult.summary,
666
+ }
667
+ : undefined,
660
668
  failureEvidence,
661
669
  }, {
662
670
  tier: verificationTier,
@@ -685,8 +693,8 @@ async function run() {
685
693
  exitCode,
686
694
  config_fail_on: config.fail_on,
687
695
  _plan: effectiveFeatures.plan,
688
- _verbose_failure: effectiveFeatures.verbose_failure,
689
- _failure_analysis: effectiveFeatures.failure_analysis,
696
+ _verbose_failure: true,
697
+ _failure_analysis: true,
690
698
  verification: {
691
699
  tier: verificationTier,
692
700
  report: verificationReport,
package/dist/crawler.js CHANGED
@@ -220,7 +220,7 @@ async function crawlApp(baseUrl, options) {
220
220
  */
221
221
  function buildScenariosFromCrawl(crawlResult, tier) {
222
222
  const scenarios = [];
223
- const limit = tier === "pro_plus" ? 6 : tier === "pro" ? 4 : 2;
223
+ const limit = tier === "team" ? 6 : tier === "pro" ? 4 : 2;
224
224
  // Scenario 1: Root page render
225
225
  const rootPage = crawlResult.pages.find((p) => p.path === "/");
226
226
  if (rootPage) {
@@ -310,8 +310,8 @@ function buildScenariosFromCrawl(crawlResult, tier) {
310
310
  ],
311
311
  });
312
312
  }
313
- // Scenario: Button interactions (Pro+ only)
314
- if (tier === "pro_plus") {
313
+ // Scenario: Button interactions (Team only)
314
+ if (tier === "team") {
315
315
  for (const page of crawlResult.pages) {
316
316
  if (scenarios.length >= limit)
317
317
  break;
package/dist/e2e.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { VerificationTier } from "./verification-core/types.js";
2
2
  import type { UserScenario } from "./config.js";
3
- import { type CrawlOptions } from "./crawler.js";
3
+ import { type CrawlOptions, type CrawlResult } from "./crawler.js";
4
4
  export interface E2EStep {
5
5
  type: "click" | "fill" | "check_visible" | "check_text" | "wait" | "scroll" | "clear_fill" | "check_validation" | "check_healthy_page" | "goto";
6
6
  selector?: string;
@@ -44,5 +44,6 @@ export declare function runVerifyE2E(url: string, tier: VerificationTier, userSc
44
44
  failed: number;
45
45
  coverageGaps: string[];
46
46
  consoleErrors: string[];
47
+ crawlResult?: CrawlResult;
47
48
  }>;
48
49
  export {};
package/dist/e2e.js CHANGED
@@ -40,7 +40,7 @@ function getScenarioLimit(tier) {
40
40
  switch (tier) {
41
41
  case "pro":
42
42
  return 4;
43
- case "pro_plus":
43
+ case "team":
44
44
  return 5;
45
45
  default:
46
46
  return 2;
@@ -95,8 +95,8 @@ function getVerificationCoverageGaps(scenarios, tier) {
95
95
  if (tier !== "free" && !hasPrimaryAction) {
96
96
  gaps.push("No primary action scenario was detected, so the verify run could not validate a real user action.");
97
97
  }
98
- if (tier === "pro_plus" && scenarios.length < 4) {
99
- gaps.push("Too few meaningful scenarios were detected for a release-confidence pass, so this run stayed shallower than Pro+ expects.");
98
+ if (tier === "team" && scenarios.length < 4) {
99
+ gaps.push("Too few meaningful scenarios were detected for a release-confidence pass, so this run stayed shallower than Team expects.");
100
100
  }
101
101
  return gaps;
102
102
  }
@@ -168,7 +168,7 @@ function buildVerifyScenarios(snapshot, tier) {
168
168
  ],
169
169
  });
170
170
  }
171
- if (tier === "pro_plus" && clickTarget && fillTarget && clickTarget !== fillTarget) {
171
+ if (tier === "team" && clickTarget && fillTarget && clickTarget !== fillTarget) {
172
172
  scenarios.push({
173
173
  name: "Repeated interaction stability",
174
174
  steps: [
@@ -511,6 +511,7 @@ async function executeScenario(url, scenario) {
511
511
  async function runVerifyE2E(url, tier, userScenarios, crawlOptions) {
512
512
  let scenarios;
513
513
  let coverageGaps;
514
+ let crawlResultUsed;
514
515
  if (userScenarios && userScenarios.length > 0) {
515
516
  console.log(` Using ${userScenarios.length} user-defined scenario(s) from .laxy.yml`);
516
517
  scenarios = convertUserScenarios(userScenarios);
@@ -518,11 +519,10 @@ async function runVerifyE2E(url, tier, userScenarios, crawlOptions) {
518
519
  }
519
520
  else if (crawlOptions?.enabled) {
520
521
  console.log(" Crawling app to discover routes and interactions...");
521
- const crawlResult = await (0, crawler_js_1.crawlApp)(url, crawlOptions);
522
- console.log(` Crawled ${crawlResult.crawledCount} page(s), found ${crawlResult.totalLinks} internal link(s)`);
523
- scenarios = (0, crawler_js_1.buildScenariosFromCrawl)(crawlResult, tier);
522
+ crawlResultUsed = await (0, crawler_js_1.crawlApp)(url, crawlOptions);
523
+ console.log(` Crawled ${crawlResultUsed.crawledCount} page(s), found ${crawlResultUsed.totalLinks} internal link(s)`);
524
+ scenarios = (0, crawler_js_1.buildScenariosFromCrawl)(crawlResultUsed, tier);
524
525
  if (scenarios.length === 0) {
525
- // Fallback to DOM snapshot if crawl found nothing useful
526
526
  const snapshot = await captureDomSnapshot(url);
527
527
  scenarios = buildVerifyScenarios(snapshot, tier);
528
528
  }
@@ -540,5 +540,5 @@ async function runVerifyE2E(url, tier, userScenarios, crawlOptions) {
540
540
  const passed = results.filter((result) => result.passed).length;
541
541
  const failed = results.length - passed;
542
542
  const consoleErrors = Array.from(new Set(results.flatMap((r) => r.consoleErrors ?? []))).slice(0, 10);
543
- return { scenarios, results, passed, failed, coverageGaps, consoleErrors };
543
+ return { scenarios, results, passed, failed, coverageGaps, consoleErrors, crawlResult: crawlResultUsed };
544
544
  }
@@ -1,13 +1,10 @@
1
1
  export interface EntitlementFeatures {
2
2
  plan: string;
3
- gold_grade: boolean;
4
- lighthouse_runs_3: boolean;
5
- verbose_failure: boolean;
6
- multi_viewport: boolean;
7
- failure_analysis: boolean;
8
- fast_lane: boolean;
3
+ github_actions: boolean;
4
+ queue_priority: boolean;
5
+ parallel_execution: boolean;
9
6
  }
10
- export type TestablePlan = "free" | "pro" | "pro_plus";
7
+ export type TestablePlan = "free" | "pro" | "team";
11
8
  export declare function normalizePlan(plan?: string | null): TestablePlan;
12
9
  export declare function getEntitlements(): Promise<EntitlementFeatures>;
13
10
  export declare function applyPlanOverride(features: EntitlementFeatures, overridePlan?: TestablePlan): EntitlementFeatures;
@@ -14,27 +14,23 @@ exports.printPlanBanner = printPlanBanner;
14
14
  const auth_js_1 = require("./auth.js");
15
15
  const FREE_FEATURES = {
16
16
  plan: "free",
17
- gold_grade: true,
18
- lighthouse_runs_3: true,
19
- verbose_failure: true,
20
- multi_viewport: true,
21
- failure_analysis: true,
22
- fast_lane: true,
17
+ // Automation features — not available on Free
18
+ github_actions: false,
19
+ queue_priority: false,
20
+ parallel_execution: false,
23
21
  };
24
22
  let cache = null;
25
23
  const CACHE_TTL_MS = 5 * 60 * 1000;
26
24
  const PLAN_RANK = {
27
25
  free: 0,
28
26
  pro: 1,
29
- pro_plus: 2,
30
27
  team: 2,
31
- enterprise: 2,
32
28
  };
33
29
  function normalizePlan(plan) {
34
30
  if (plan === "pro")
35
31
  return "pro";
36
- if (plan === "pro_plus" || plan === "team" || plan === "enterprise")
37
- return "pro_plus";
32
+ if (plan === "team")
33
+ return "team";
38
34
  return "free";
39
35
  }
40
36
  function getPlanRank(plan) {
@@ -68,15 +64,22 @@ async function getEntitlements() {
68
64
  function applyPlanOverride(features, overridePlan) {
69
65
  if (!overridePlan)
70
66
  return features;
71
- return { ...features, plan: overridePlan };
67
+ const isTeam = overridePlan === "team";
68
+ return {
69
+ ...features,
70
+ plan: overridePlan,
71
+ // All verification features run on every plan
72
+ // Automation features — Team only
73
+ github_actions: isTeam,
74
+ queue_priority: isTeam,
75
+ parallel_execution: isTeam,
76
+ };
72
77
  }
73
78
  function printPlanBanner(features) {
74
79
  const planLabels = {
75
80
  free: "Free",
76
81
  pro: "Pro",
77
- pro_plus: "Pro+",
78
82
  team: "Team",
79
- enterprise: "Enterprise",
80
83
  };
81
84
  const label = planLabels[features.plan] ?? features.plan;
82
85
  if (features.plan !== "free") {
@@ -41,12 +41,8 @@ function titleCasePlan(plan) {
41
41
  switch (plan) {
42
42
  case "pro":
43
43
  return "Pro";
44
- case "pro_plus":
45
- return "Pro+";
46
44
  case "team":
47
45
  return "Team";
48
- case "enterprise":
49
- return "Enterprise";
50
46
  default:
51
47
  return "Free";
52
48
  }
@@ -64,7 +60,7 @@ function formatTimestamp(iso) {
64
60
  return date.toISOString().replace("T", " ").replace(".000Z", " UTC");
65
61
  }
66
62
  function sentenceForVerdict(view) {
67
- const isReleaseTier = view.tier === "pro_plus";
63
+ const isReleaseTier = view.tier === "team";
68
64
  switch (view.verdict) {
69
65
  case "client-ready":
70
66
  return "Yes. This run collected enough evidence to support a client-ready call.";
@@ -102,7 +98,7 @@ function defaultNextActions(result) {
102
98
  case "release-ready":
103
99
  return ["Ship this version, or archive this report as release evidence."];
104
100
  case "investigate":
105
- return view.tier === "pro_plus"
101
+ return view.tier === "team"
106
102
  ? ["Collect the missing verification evidence, then rerun the command before release."]
107
103
  : ["Collect the missing verification evidence, then rerun the command before sending this to a client."];
108
104
  case "build-failed":
@@ -185,7 +181,7 @@ function getReportFlavor(view) {
185
181
  switch (view.tier) {
186
182
  case "pro":
187
183
  return "delivery";
188
- case "pro_plus":
184
+ case "team":
189
185
  return "release";
190
186
  default:
191
187
  return "generic";
@@ -202,6 +202,16 @@ function getImprovementRecommendations(input, thresholds = exports.DEFAULT_LH_TH
202
202
  });
203
203
  }
204
204
  }
205
+ // Broken links audit
206
+ if (input.brokenLinksAudit && input.brokenLinksAudit.brokenCount > 0) {
207
+ findings.push({
208
+ category: "security",
209
+ severity: "high",
210
+ title: `Broken links detected (${input.brokenLinksAudit.brokenCount}/${input.brokenLinksAudit.checkedCount})`,
211
+ description: `${input.brokenLinksAudit.brokenCount} link(s) returned non-OK status codes. Users will hit 404/500 errors.`,
212
+ action: "Fix or remove the broken links and rerun the verification.",
213
+ });
214
+ }
205
215
  // Mobile Lighthouse — Pro tier mobile check
206
216
  if (input.mobileLighthouseScores) {
207
217
  const mobileScores = input.mobileLighthouseScores;
@@ -317,67 +327,23 @@ function buildVerificationReport(input, options) {
317
327
  }
318
328
  else if (blockers.length > 0) {
319
329
  verdict = "hold";
320
- confidence = tier === "free" ? "low" : "medium";
321
- summary = "Blocking verification issues were found. Hold release until the blockers are fixed.";
322
- }
323
- else if (tier === "pro_plus" &&
324
- coreChecksPassed &&
325
- evidence.e2eStabilityPassed &&
326
- proPlusEvidenceComplete &&
327
- !hasWarnings) {
328
- verdict = "release-ready";
329
- confidence = "high";
330
- summary = "Core verification checks passed. This run supports a release-ready call.";
331
- }
332
- else if (tier === "pro_plus" &&
333
- coreChecksPassed &&
334
- evidence.e2eStabilityPassed &&
335
- proPlusEvidenceComplete &&
336
- hasWarnings) {
337
- verdict = "investigate";
338
- confidence = "medium";
339
- summary = "Core checks passed, but non-blocking warnings remain. Review warnings before calling this run release-ready.";
340
- }
341
- else if (tier === "pro_plus" &&
342
- coreChecksPassed &&
343
- (!evidence.hasMultiViewportData || !evidence.hasComparableVisualDiffData)) {
344
- verdict = "investigate";
345
330
  confidence = "medium";
346
- const missingEvidence = [];
347
- if (!evidence.hasMultiViewportData)
348
- missingEvidence.push("multi-viewport evidence");
349
- if (!evidence.hasComparableVisualDiffData)
350
- missingEvidence.push("visual diff evidence");
351
- summary = `Core checks passed, but release-ready confidence still needs ${missingEvidence.join(" and ")}.`;
331
+ summary = "Blocking verification issues were found. Hold release until the blockers are fixed.";
352
332
  }
353
- else if (tier === "pro" && coreChecksPassed && !hasWarnings) {
333
+ else if (coreChecksPassed && !hasWarnings) {
354
334
  verdict = "client-ready";
355
- confidence = "medium";
356
- summary = "No blocking issues found. Build, E2E, and Lighthouse checks passed. This run supports a client-ready call.";
335
+ confidence = "high";
336
+ summary = "No blocking issues found. Build, E2E, and Lighthouse checks passed.";
357
337
  }
358
- else if (tier === "pro" && coreChecksPassed && hasWarnings) {
338
+ else if (coreChecksPassed && hasWarnings) {
359
339
  verdict = "investigate";
360
340
  confidence = "medium";
361
341
  summary = "Core checks passed, but warning-level risks remain. Review warnings before calling this run client-ready.";
362
342
  }
363
- else if (tier === "free") {
364
- if (evidence.hasConsoleErrors) {
365
- verdict = "quick-pass";
366
- confidence = "low";
367
- summary = "Build passed, but runtime console errors were detected. The app may crash or misbehave for users.";
368
- }
369
- else {
370
- verdict = "quick-pass";
371
- confidence = evidence.hasLighthouseData && evidence.lighthousePassed ? "medium" : "low";
372
- summary = "No immediate hard blockers were found in the quick verification pass.";
373
- }
374
- }
375
343
  else {
376
- verdict = "investigate";
377
- confidence = "medium";
378
- summary = evidence.lighthouseSkipped
379
- ? "The build is standing. Lighthouse was skipped, so review the remaining verification evidence before release."
380
- : "The build is standing, but deeper verification evidence should be reviewed before release.";
344
+ verdict = "quick-pass";
345
+ confidence = evidence.hasLighthouseData && evidence.lighthousePassed ? "medium" : "low";
346
+ summary = "No immediate hard blockers were found in the quick verification pass.";
381
347
  }
382
348
  const passes = [
383
349
  { key: "build", label: "Production build", passed: evidence.buildPassed },
@@ -21,13 +21,13 @@ const TIER_POLICIES = {
21
21
  maxBlockers: 5,
22
22
  maxWarnings: 5,
23
23
  },
24
- pro_plus: {
25
- tier: "pro_plus",
24
+ team: {
25
+ tier: "team",
26
26
  showDetailedLighthouse: true,
27
27
  showDetailedE2E: true,
28
28
  showReportExport: true,
29
- maxBlockers: 8,
30
- maxWarnings: 8,
29
+ maxBlockers: 10,
30
+ maxWarnings: 10,
31
31
  },
32
32
  };
33
33
  function getTierPolicy(tier = "free") {
@@ -36,16 +36,16 @@ function getTierPolicy(tier = "free") {
36
36
  function planToVerificationTier(plan) {
37
37
  if (plan === "pro")
38
38
  return "pro";
39
- if (plan === "pro_plus" || plan === "team" || plan === "enterprise")
40
- return "pro_plus";
39
+ if (plan === "team")
40
+ return "team";
41
41
  return "free";
42
42
  }
43
43
  function getVerificationTierQuestion(tier) {
44
44
  switch (tier) {
45
45
  case "pro":
46
46
  return "Ready to show a client?";
47
- case "pro_plus":
48
- return "Ready for production?";
47
+ case "team":
48
+ return "Ready for team deployment?";
49
49
  default:
50
50
  return "Any critical issues right now?";
51
51
  }
@@ -1,5 +1,5 @@
1
1
  export type VerificationGrade = "gold" | "silver" | "bronze" | "unverified";
2
- export type VerificationTier = "free" | "pro" | "pro_plus";
2
+ export type VerificationTier = "free" | "pro" | "team";
3
3
  export type ReleaseVerdict = "quick-pass" | "client-ready" | "investigate" | "hold" | "release-ready" | "build-failed";
4
4
  export interface LighthouseThresholds {
5
5
  performance: number;
@@ -38,9 +38,14 @@ export interface VerificationInput {
38
38
  high: number;
39
39
  summary: string;
40
40
  };
41
+ brokenLinksAudit?: {
42
+ checkedCount: number;
43
+ brokenCount: number;
44
+ summary: string;
45
+ };
41
46
  }
42
47
  export interface VerificationCheck {
43
- key: "build" | "e2e" | "lighthouse" | "viewport" | "visual" | "security" | "mobile-lh" | "console-errors";
48
+ key: "build" | "e2e" | "lighthouse" | "viewport" | "visual" | "security" | "mobile-lh" | "console-errors" | "broken-links";
44
49
  label: string;
45
50
  passed: boolean;
46
51
  }
package/package.json CHANGED
@@ -1,67 +1,67 @@
1
- {
2
- "name": "laxy-verify",
3
- "version": "1.1.23",
4
- "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
- "license": "MIT",
6
- "type": "commonjs",
7
- "homepage": "https://github.com/SUNgm24/Laxy/tree/main/laxy-verify#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/SUNgm24/Laxy.git",
11
- "directory": "laxy-verify"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/SUNgm24/Laxy/issues"
15
- },
16
- "keywords": [
17
- "frontend",
18
- "verification",
19
- "quality-gate",
20
- "release-readiness",
21
- "lighthouse",
22
- "e2e",
23
- "qa",
24
- "cli",
25
- "nextjs",
26
- "vite"
27
- ],
28
- "engines": {
29
- "node": ">=20.18.0 <25"
30
- },
31
- "bin": {
32
- "laxy-verify": "dist/cli.js"
33
- },
34
- "files": [
35
- "dist/"
36
- ],
37
- "scripts": {
38
- "build": "tsc",
39
- "start": "node dist/cli.js",
40
- "test": "vitest run",
41
- "test:coverage": "vitest run --coverage"
42
- },
43
- "dependencies": {
44
- "@lhci/cli": "^0.14.0",
45
- "chrome-launcher": "^0.13.4",
46
- "js-yaml": "^4.1.0",
47
- "lighthouse": "^12.1.0",
48
- "pixelmatch": "^7.1.0",
49
- "pngjs": "^7.0.0",
50
- "puppeteer": "^24.40.0",
51
- "tree-kill": "^1.2.2"
52
- },
53
- "devDependencies": {
54
- "@types/js-yaml": "^4.0.9",
55
- "@types/node": "^20.0.0",
56
- "typescript": "^5.4.0",
57
- "vitest": "^2.0.0"
58
- },
59
- "peerDependencies": {
60
- "playwright": "^1.40.0"
61
- },
62
- "peerDependenciesMeta": {
63
- "playwright": {
64
- "optional": true
65
- }
66
- }
1
+ {
2
+ "name": "laxy-verify",
3
+ "version": "1.1.25",
4
+ "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "homepage": "https://github.com/SUNgm24/Laxy/tree/main/laxy-verify#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/SUNgm24/Laxy.git",
11
+ "directory": "laxy-verify"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/SUNgm24/Laxy/issues"
15
+ },
16
+ "keywords": [
17
+ "frontend",
18
+ "verification",
19
+ "quality-gate",
20
+ "release-readiness",
21
+ "lighthouse",
22
+ "e2e",
23
+ "qa",
24
+ "cli",
25
+ "nextjs",
26
+ "vite"
27
+ ],
28
+ "engines": {
29
+ "node": "\u003e=20.18.0 \u003c25"
30
+ },
31
+ "bin": {
32
+ "laxy-verify": "dist/cli.js"
33
+ },
34
+ "files": [
35
+ "dist/"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "start": "node dist/cli.js",
40
+ "test": "vitest run",
41
+ "test:coverage": "vitest run --coverage"
42
+ },
43
+ "dependencies": {
44
+ "@lhci/cli": "^0.14.0",
45
+ "chrome-launcher": "^0.13.4",
46
+ "js-yaml": "^4.1.0",
47
+ "lighthouse": "^12.1.0",
48
+ "pixelmatch": "^7.1.0",
49
+ "pngjs": "^7.0.0",
50
+ "puppeteer": "^24.40.0",
51
+ "tree-kill": "^1.2.2"
52
+ },
53
+ "devDependencies": {
54
+ "@types/js-yaml": "^4.0.9",
55
+ "@types/node": "^20.0.0",
56
+ "typescript": "^5.4.0",
57
+ "vitest": "^2.0.0"
58
+ },
59
+ "peerDependencies": {
60
+ "playwright": "^1.40.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "playwright": {
64
+ "optional": true
65
+ }
66
+ }
67
67
  }