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
@@ -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
+ }
package/dist/config.d.ts CHANGED
@@ -18,6 +18,13 @@ export interface UserScenario {
18
18
  name: string;
19
19
  steps: UserScenarioStep[];
20
20
  }
21
+ export interface VisualDiffConfig {
22
+ pixelmatchThreshold: number;
23
+ warnThreshold: number;
24
+ rollbackThreshold: number;
25
+ ignoreSelectors: string[];
26
+ disableAnimations: boolean;
27
+ }
21
28
  export interface LaxyConfig {
22
29
  framework: string;
23
30
  build_command: string;
@@ -34,18 +41,61 @@ export interface LaxyConfig {
34
41
  max_crawl_depth: number;
35
42
  max_crawl_pages: number;
36
43
  browsers: string[];
44
+ /** Explicit list of routes to audit with Lighthouse (e.g. ["/", "/about", "/pricing"]). */
45
+ lighthouse_routes?: string[];
46
+ /** Extra routes to audit even if the crawler cannot discover them (e.g. SPA router.push routes). */
47
+ extra_routes?: string[];
48
+ /** Maximum number of crawl-discovered routes to run Lighthouse on. Default: 5. */
49
+ max_lighthouse_routes: number;
50
+ visual_diff: VisualDiffConfig;
51
+ /** Run TypeScript type check (tsc --noEmit). Default: false. */
52
+ typecheck: boolean;
53
+ /** Run secret/credential leak scan. Default: false. */
54
+ secret_scan: boolean;
55
+ /** Paths to ignore during secret scan. */
56
+ secret_scan_ignore_paths: string[];
57
+ /** Run bundle size analysis. Default: false. */
58
+ bundle_size: boolean;
59
+ /** Run outdated dependency check. Default: false. */
60
+ outdated_check: boolean;
61
+ /** Run deep accessibility audit (axe-core). Default: false. */
62
+ a11y_deep: boolean;
63
+ /** Run deep SEO audit. Default: false. */
64
+ seo_deep: boolean;
65
+ /** Run Core Web Vitals budget check. Default: false. */
66
+ vitals_budget: boolean;
37
67
  }
38
68
  export declare class ConfigParseError extends Error {
39
69
  constructor(msg: string);
40
70
  }
71
+ export interface TeamThresholdsConfig {
72
+ performance: number;
73
+ accessibility: number;
74
+ seo: number;
75
+ best_practices: number;
76
+ fail_on: FailOn;
77
+ }
78
+ /**
79
+ * 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
80
+ * 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
81
+ */
82
+ export declare function fetchTeamThresholds(): Promise<TeamThresholdsConfig | null>;
41
83
  export interface LoadConfigOptions {
42
84
  dir: string;
43
85
  configPath?: string;
44
86
  cliFlags?: {
45
87
  failOn?: FailOn;
46
88
  skipLighthouse?: boolean;
89
+ typecheck?: boolean;
90
+ secretScan?: boolean;
91
+ bundleSize?: boolean;
92
+ outdatedCheck?: boolean;
93
+ a11yDeep?: boolean;
94
+ seoDeep?: boolean;
95
+ vitalsBudget?: boolean;
47
96
  };
48
97
  ciMode: boolean;
98
+ teamThresholds?: TeamThresholdsConfig | null;
49
99
  }
50
100
  export declare function loadConfig(options: LoadConfigOptions): LaxyConfig & {
51
101
  ciMode: boolean;
package/dist/config.js CHANGED
@@ -34,10 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ConfigParseError = void 0;
37
+ exports.fetchTeamThresholds = fetchTeamThresholds;
37
38
  exports.loadConfig = loadConfig;
38
39
  const fs = __importStar(require("node:fs"));
39
40
  const path = __importStar(require("node:path"));
40
41
  const yaml = __importStar(require("js-yaml"));
42
+ const auth_js_1 = require("./auth.js");
41
43
  const DEFAULT_CONFIG = {
42
44
  framework: "auto",
43
45
  build_command: "",
@@ -46,7 +48,7 @@ const DEFAULT_CONFIG = {
46
48
  port: 3000,
47
49
  build_timeout: 300,
48
50
  dev_timeout: 60,
49
- lighthouse_runs: 1,
51
+ lighthouse_runs: 3,
50
52
  thresholds: {
51
53
  performance: 70,
52
54
  accessibility: 85,
@@ -58,6 +60,22 @@ const DEFAULT_CONFIG = {
58
60
  max_crawl_depth: 3,
59
61
  max_crawl_pages: 10,
60
62
  browsers: ["chromium"],
63
+ max_lighthouse_routes: 5,
64
+ visual_diff: {
65
+ pixelmatchThreshold: 0.1,
66
+ warnThreshold: 30,
67
+ rollbackThreshold: 60,
68
+ ignoreSelectors: [],
69
+ disableAnimations: true,
70
+ },
71
+ typecheck: false,
72
+ secret_scan: false,
73
+ secret_scan_ignore_paths: [],
74
+ bundle_size: false,
75
+ outdated_check: false,
76
+ a11y_deep: false,
77
+ seo_deep: false,
78
+ vitals_budget: false,
61
79
  };
62
80
  const VALID_FAIL_ON = ["unverified", "bronze", "silver", "gold"];
63
81
  class ConfigParseError extends Error {
@@ -103,6 +121,47 @@ function parseYaml(filePath) {
103
121
  if (browsers.length > 0)
104
122
  result.browsers = browsers;
105
123
  }
124
+ if (Array.isArray(raw.lighthouse_routes)) {
125
+ const routes = raw.lighthouse_routes
126
+ .filter((r) => typeof r === "string" && r.startsWith("/"))
127
+ .slice(0, 20);
128
+ if (routes.length > 0)
129
+ result.lighthouse_routes = routes;
130
+ }
131
+ if (Array.isArray(raw.extra_routes)) {
132
+ const routes = raw.extra_routes
133
+ .filter((r) => typeof r === "string" && r.startsWith("/"))
134
+ .slice(0, 20);
135
+ if (routes.length > 0)
136
+ result.extra_routes = routes;
137
+ }
138
+ if (typeof raw.max_lighthouse_routes === "number") {
139
+ result.max_lighthouse_routes = Math.max(1, Math.min(20, raw.max_lighthouse_routes));
140
+ }
141
+ if (typeof raw.visual_diff === "object" &&
142
+ raw.visual_diff !== null &&
143
+ !Array.isArray(raw.visual_diff)) {
144
+ const vd = raw.visual_diff;
145
+ const visualDiff = {};
146
+ if (typeof vd.pixelmatch_threshold === "number") {
147
+ visualDiff.pixelmatchThreshold = Math.max(0, Math.min(1, vd.pixelmatch_threshold));
148
+ }
149
+ if (typeof vd.warn_threshold === "number") {
150
+ visualDiff.warnThreshold = Math.max(0, Math.min(100, vd.warn_threshold));
151
+ }
152
+ if (typeof vd.rollback_threshold === "number") {
153
+ visualDiff.rollbackThreshold = Math.max(0, Math.min(100, vd.rollback_threshold));
154
+ }
155
+ if (Array.isArray(vd.ignore_selectors)) {
156
+ visualDiff.ignoreSelectors = vd.ignore_selectors
157
+ .filter((selector) => typeof selector === "string" && selector.trim().length > 0)
158
+ .slice(0, 30);
159
+ }
160
+ if (typeof vd.disable_animations === "boolean") {
161
+ visualDiff.disableAnimations = vd.disable_animations;
162
+ }
163
+ result.visual_diff = visualDiff;
164
+ }
106
165
  if (typeof raw.fail_on === "string") {
107
166
  const f = raw.fail_on;
108
167
  if (!VALID_FAIL_ON.includes(f)) {
@@ -162,14 +221,66 @@ function parseYaml(filePath) {
162
221
  result.scenarios = scenarios;
163
222
  }
164
223
  }
224
+ if (typeof raw.typecheck === "boolean")
225
+ result.typecheck = raw.typecheck;
226
+ if (typeof raw.secret_scan === "boolean")
227
+ result.secret_scan = raw.secret_scan;
228
+ if (Array.isArray(raw.secret_scan_ignore_paths)) {
229
+ result.secret_scan_ignore_paths = raw.secret_scan_ignore_paths
230
+ .filter((p) => typeof p === "string");
231
+ }
232
+ if (typeof raw.bundle_size === "boolean")
233
+ result.bundle_size = raw.bundle_size;
234
+ if (typeof raw.outdated_check === "boolean")
235
+ result.outdated_check = raw.outdated_check;
236
+ if (typeof raw.a11y_deep === "boolean")
237
+ result.a11y_deep = raw.a11y_deep;
238
+ if (typeof raw.seo_deep === "boolean")
239
+ result.seo_deep = raw.seo_deep;
240
+ if (typeof raw.vitals_budget === "boolean")
241
+ result.vitals_budget = raw.vitals_budget;
165
242
  return result;
166
243
  }
244
+ /**
245
+ * 로그인된 CLI 토큰으로 팀 공통 임계값을 서버에서 가져온다.
246
+ * 토큰 없음 / 팀 없음 / 네트워크 오류 시 null 반환 (graceful degradation).
247
+ */
248
+ async function fetchTeamThresholds() {
249
+ const token = (0, auth_js_1.loadToken)();
250
+ if (!token)
251
+ return null;
252
+ try {
253
+ const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/team-thresholds`, {
254
+ headers: { Authorization: `Bearer ${token}` },
255
+ signal: AbortSignal.timeout(5000),
256
+ });
257
+ if (!res.ok)
258
+ return null;
259
+ const data = (await res.json());
260
+ return data.thresholds ?? null;
261
+ }
262
+ catch {
263
+ return null;
264
+ }
265
+ }
167
266
  function loadConfig(options) {
168
267
  const configPath = options.configPath ?? path.join(options.dir, ".laxy.yml");
169
268
  let base = {};
170
- if (fs.existsSync(configPath)) {
269
+ const hasLocalConfig = fs.existsSync(configPath);
270
+ if (hasLocalConfig) {
171
271
  base = parseYaml(configPath);
172
272
  }
273
+ // 팀 임계값: 로컬 .laxy.yml에 thresholds가 없을 때만 적용
274
+ const team = options.teamThresholds ?? null;
275
+ const teamThresholdFallback = (!hasLocalConfig || !base.thresholds) && team
276
+ ? {
277
+ performance: team.performance,
278
+ accessibility: team.accessibility,
279
+ seo: team.seo,
280
+ bestPractices: team.best_practices,
281
+ }
282
+ : {};
283
+ const teamFailOnFallback = (!hasLocalConfig || !base.fail_on) && team ? team.fail_on : undefined;
173
284
  const config = {
174
285
  ...DEFAULT_CONFIG,
175
286
  framework: base.framework ?? DEFAULT_CONFIG.framework,
@@ -180,14 +291,33 @@ function loadConfig(options) {
180
291
  build_timeout: base.build_timeout ?? DEFAULT_CONFIG.build_timeout,
181
292
  dev_timeout: base.dev_timeout ?? DEFAULT_CONFIG.dev_timeout,
182
293
  lighthouse_runs: base.lighthouse_runs ?? DEFAULT_CONFIG.lighthouse_runs,
183
- fail_on: base.fail_on ?? DEFAULT_CONFIG.fail_on,
294
+ fail_on: base.fail_on ?? teamFailOnFallback ?? DEFAULT_CONFIG.fail_on,
184
295
  scenarios: base.scenarios,
185
296
  crawl: base.crawl ?? DEFAULT_CONFIG.crawl,
186
297
  max_crawl_depth: base.max_crawl_depth ?? DEFAULT_CONFIG.max_crawl_depth,
187
298
  max_crawl_pages: base.max_crawl_pages ?? DEFAULT_CONFIG.max_crawl_pages,
188
299
  browsers: base.browsers ?? DEFAULT_CONFIG.browsers,
300
+ lighthouse_routes: base.lighthouse_routes,
301
+ extra_routes: base.extra_routes,
302
+ max_lighthouse_routes: base.max_lighthouse_routes ?? DEFAULT_CONFIG.max_lighthouse_routes,
303
+ visual_diff: {
304
+ ...DEFAULT_CONFIG.visual_diff,
305
+ ...(base.visual_diff ?? {}),
306
+ },
307
+ typecheck: base.typecheck ?? DEFAULT_CONFIG.typecheck,
308
+ secret_scan: base.secret_scan ?? DEFAULT_CONFIG.secret_scan,
309
+ secret_scan_ignore_paths: base.secret_scan_ignore_paths ?? DEFAULT_CONFIG.secret_scan_ignore_paths,
310
+ bundle_size: base.bundle_size ?? DEFAULT_CONFIG.bundle_size,
311
+ outdated_check: base.outdated_check ?? DEFAULT_CONFIG.outdated_check,
312
+ a11y_deep: base.a11y_deep ?? DEFAULT_CONFIG.a11y_deep,
313
+ seo_deep: base.seo_deep ?? DEFAULT_CONFIG.seo_deep,
314
+ vitals_budget: base.vitals_budget ?? DEFAULT_CONFIG.vitals_budget,
315
+ };
316
+ config.thresholds = {
317
+ ...DEFAULT_CONFIG.thresholds,
318
+ ...teamThresholdFallback,
319
+ ...(base.thresholds ?? {}),
189
320
  };
190
- config.thresholds = { ...DEFAULT_CONFIG.thresholds, ...(base.thresholds ?? {}) };
191
321
  // CLI flag overrides
192
322
  if (options.cliFlags?.failOn !== undefined) {
193
323
  if (!VALID_FAIL_ON.includes(options.cliFlags.failOn)) {
@@ -211,5 +341,20 @@ function loadConfig(options) {
211
341
  if (options.cliFlags?.skipLighthouse) {
212
342
  // Effectively disables Lighthouse grading
213
343
  }
344
+ // CLI flag overrides for new checks
345
+ if (options.cliFlags?.typecheck !== undefined)
346
+ config.typecheck = options.cliFlags.typecheck;
347
+ if (options.cliFlags?.secretScan !== undefined)
348
+ config.secret_scan = options.cliFlags.secretScan;
349
+ if (options.cliFlags?.bundleSize !== undefined)
350
+ config.bundle_size = options.cliFlags.bundleSize;
351
+ if (options.cliFlags?.outdatedCheck !== undefined)
352
+ config.outdated_check = options.cliFlags.outdatedCheck;
353
+ if (options.cliFlags?.a11yDeep !== undefined)
354
+ config.a11y_deep = options.cliFlags.a11yDeep;
355
+ if (options.cliFlags?.seoDeep !== undefined)
356
+ config.seo_deep = options.cliFlags.seoDeep;
357
+ if (options.cliFlags?.vitalsBudget !== undefined)
358
+ config.vitals_budget = options.cliFlags.vitalsBudget;
214
359
  return { ...config, ciMode };
215
360
  }
@@ -3,6 +3,8 @@ export interface EntitlementFeatures {
3
3
  github_actions: boolean;
4
4
  queue_priority: boolean;
5
5
  parallel_execution: boolean;
6
+ ai_failure_analysis: boolean;
7
+ compare_env: boolean;
6
8
  }
7
9
  export type TestablePlan = "free" | "pro" | "team";
8
10
  export declare function normalizePlan(plan?: string | null): TestablePlan;
@@ -18,6 +18,8 @@ const FREE_FEATURES = {
18
18
  github_actions: false,
19
19
  queue_priority: false,
20
20
  parallel_execution: false,
21
+ ai_failure_analysis: false,
22
+ compare_env: false,
21
23
  };
22
24
  let cache = null;
23
25
  const CACHE_TTL_MS = 5 * 60 * 1000;
@@ -70,8 +72,10 @@ function applyPlanOverride(features, overridePlan) {
70
72
  ...features,
71
73
  plan: overridePlan,
72
74
  // All verification features run on every plan
73
- // github_actions — Pro and Team
75
+ // github_actions, ai_failure_analysis, compare_env — Pro and Team
74
76
  github_actions: isPro,
77
+ ai_failure_analysis: isPro,
78
+ compare_env: isPro,
75
79
  // queue_priority, parallel_execution — Team only
76
80
  queue_priority: isTeam,
77
81
  parallel_execution: isTeam,
@@ -0,0 +1,6 @@
1
+ import type { UserScenario } from "./config.js";
2
+ export interface InitAnalysisResult {
3
+ scenarios: UserScenario[];
4
+ extraRoutes: string[];
5
+ }
6
+ export declare function analyzeProjectForInit(dir: string): InitAnalysisResult;