laxy-verify 1.1.6 → 1.1.8

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
@@ -3,10 +3,12 @@
3
3
  Frontend quality gate for AI-generated code. Build + Lighthouse verification with grade output (Gold/Silver/Bronze/Unverified).
4
4
 
5
5
  ```bash
6
- npx laxy-verify --init # Auto-detect framework, generate config
7
- npx laxy-verify . # Run verification
8
- npx laxy-verify login # Log in for Pro/Pro+ features
9
- npx laxy-verify --badge # Show shields.io badge
6
+ npx laxy-verify --init --run # 설정 생성 + 즉시 검증 (원커맨드)
7
+ npx laxy-verify --init # Auto-detect framework, generate config
8
+ npx laxy-verify . # Run verification
9
+ npx laxy-verify login # Log in for Pro/Pro+ features
10
+ npx laxy-verify --badge # Show shields.io badge
11
+ npx laxy-verify --help # 도움말 보기
10
12
  ```
11
13
 
12
14
  ## Quick Start
@@ -82,6 +84,14 @@ env:
82
84
  LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
83
85
  ```
84
86
 
87
+ **LAXY_TOKEN을 GitHub Secrets에 등록하는 방법:**
88
+
89
+ 1. 로컬에서 로그인: `npx laxy-verify login`
90
+ 2. 토큰 복사: `cat ~/.laxy/credentials.json` → `"token"` 값을 복사
91
+ 3. GitHub 리포 → Settings → Secrets and variables → Actions → **New repository secret**
92
+ 4. Name: `LAXY_TOKEN`, Value: 복사한 토큰 붙여넣기
93
+ 5. `--init` 으로 생성된 workflow 파일은 이미 `LAXY_TOKEN`을 포함합니다
94
+
85
95
  ### Multi-viewport (Pro+)
86
96
 
87
97
  ```bash
package/dist/build.d.ts CHANGED
@@ -8,4 +8,4 @@ export declare class BuildError extends Error {
8
8
  timedOut: boolean;
9
9
  constructor(message: string, errors: string[], timedOut?: boolean);
10
10
  }
11
- export declare function runBuild(command: string, timeoutSec: number): Promise<BuildResult>;
11
+ export declare function runBuild(command: string, timeoutSec: number, cwd?: string): Promise<BuildResult>;
package/dist/build.js CHANGED
@@ -18,13 +18,17 @@ class BuildError extends Error {
18
18
  }
19
19
  }
20
20
  exports.BuildError = BuildError;
21
- function runBuild(command, timeoutSec) {
21
+ function runBuild(command, timeoutSec, cwd) {
22
22
  return new Promise((resolve, reject) => {
23
23
  const startTime = Date.now();
24
24
  const stderrLines = [];
25
25
  const errorLines = [];
26
- console.log(`\n Building: ${command}`);
27
- const proc = (0, node_child_process_1.spawn)(command, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
26
+ console.log(`\n Building: ${command}${cwd ? ` (cwd: ${cwd})` : ""}`);
27
+ const proc = (0, node_child_process_1.spawn)(command, {
28
+ shell: true,
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ cwd,
31
+ });
28
32
  let timedOut = false;
29
33
  const timer = setTimeout(() => {
30
34
  timedOut = true;
package/dist/cli.js CHANGED
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  return result;
34
34
  };
35
35
  })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
36
39
  Object.defineProperty(exports, "__esModule", { value: true });
37
40
  const fs = __importStar(require("node:fs"));
38
41
  const path = __importStar(require("node:path"));
@@ -49,6 +52,21 @@ const status_js_1 = require("./status.js");
49
52
  const auth_js_1 = require("./auth.js");
50
53
  const entitlement_js_1 = require("./entitlement.js");
51
54
  const multi_viewport_js_1 = require("./multi-viewport.js");
55
+ const index_js_1 = require("./verification-core/index.js");
56
+ const package_json_1 = __importDefault(require("../package.json"));
57
+ function exitGracefully(code) {
58
+ if (process.platform === "win32") {
59
+ setTimeout(() => process.exit(code), 100);
60
+ return;
61
+ }
62
+ process.exit(code);
63
+ }
64
+ async function ensurePortAvailableForVerification(port) {
65
+ const status = await (0, serve_js_1.probeServerStatus)(port);
66
+ if (status === null)
67
+ return;
68
+ throw new Error(`An existing local server is already responding on port ${port} (HTTP ${status}). Stop the running app before using laxy-verify, because the verification build can invalidate an active dev session.`);
69
+ }
52
70
  function parseArgs() {
53
71
  const raw = process.argv.slice(2);
54
72
  // Single-pass: collect flags and first positional
@@ -80,20 +98,20 @@ function parseArgs() {
80
98
  }
81
99
  }
82
100
  }
83
- // 서브커맨드 처리: login [email], logout, whoami
101
+ // ?쒕툕而ㅻ㎤??泥섎━: login [email], logout, whoami
84
102
  let subcommand;
85
103
  let subcommandArg;
86
104
  if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
87
105
  subcommand = projectDir;
88
106
  projectDir = ".";
89
- // login 뒤에 이메일이 올 수 있음
107
+ // login ?ㅼ뿉 ?대찓?쇱씠 ?????덉쓬
90
108
  if (subcommand === "login" && raw.length > 0 && !raw[0].startsWith("-")) {
91
109
  // already consumed via loop above if it was first; re-check flags
92
110
  }
93
- // 번째 non-flag 이메일로 취급
111
+ // 泥?踰덉㎏ non-flag瑜??대찓?쇰줈 痍④툒
94
112
  subcommandArg = flags["email"];
95
113
  if (!subcommandArg) {
96
- // login PSM@example.com 형태 positional already captured in projectDir before reset
114
+ // login PSM@example.com ?뺥깭 ??positional already captured in projectDir before reset
97
115
  }
98
116
  }
99
117
  return {
@@ -107,29 +125,59 @@ function parseArgs() {
107
125
  skipLighthouse: flags["skip-lighthouse"] !== undefined,
108
126
  badge: flags["badge"] !== undefined,
109
127
  init: flags["init"] !== undefined,
128
+ initRun: flags["init"] !== undefined && flags["run"] !== undefined,
110
129
  multiViewport: flags["multi-viewport"] !== undefined,
111
130
  failureAnalysis: flags["failure-analysis"] !== undefined,
131
+ help: flags["help"] !== undefined || flags["h"] !== undefined,
112
132
  };
113
133
  }
114
134
  function writeResultFile(projectDir, result) {
115
135
  const filePath = path.join(projectDir, ".laxy-result.json");
116
136
  fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
117
137
  }
138
+ function summarizeViewportIssues(scores, thresholds) {
139
+ if (!scores)
140
+ return { count: 0 };
141
+ const failed = [];
142
+ for (const [label, viewportScores] of Object.entries(scores)) {
143
+ if (!viewportScores) {
144
+ failed.push(`${label}: missing`);
145
+ continue;
146
+ }
147
+ const passes = viewportScores.performance >= thresholds.performance &&
148
+ viewportScores.accessibility >= thresholds.accessibility &&
149
+ viewportScores.seo >= thresholds.seo &&
150
+ viewportScores.bestPractices >= thresholds.bestPractices;
151
+ if (!passes) {
152
+ failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
153
+ }
154
+ }
155
+ return {
156
+ count: failed.length,
157
+ summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
158
+ };
159
+ }
118
160
  function consoleOutput(result) {
119
161
  const gradeLabel = result.grade;
120
- const checkEmoji = result.grade !== "Unverified" ? " " : "";
121
- console.log(`\n Laxy Verify ${gradeLabel}${checkEmoji}`);
162
+ const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
163
+ console.log(`\n Laxy Verify ${gradeLabel}${checkEmoji}`);
122
164
  console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
123
165
  if (result.build.errors.length > 0) {
124
- const last5 = result.build.errors.slice(-5);
125
166
  console.log(` Errors:`);
126
- for (const e of last5)
167
+ // 泥?踰덉㎏ 'error' ?ㅼ썙?쒓? ?ы븿??以?(?먯씤 ?뚯븙??
168
+ const firstError = result.build.errors.find(e => /error/i.test(e));
169
+ // 留덉?留?5以?(鍮뚮뱶 醫낅즺 而⑦뀓?ㅽ듃)
170
+ const last5 = result.build.errors.slice(-5);
171
+ const toShow = firstError && !last5.includes(firstError)
172
+ ? [firstError, " ...", ...last5]
173
+ : last5;
174
+ for (const e of toShow)
127
175
  console.error(` ${e}`);
128
176
  }
129
177
  if (result.lighthouse !== null) {
130
178
  const lh = result.lighthouse;
131
179
  const t = result.thresholds;
132
- const check = (passed) => passed ? " " : " ";
180
+ const check = (passed) => (passed ? " OK" : " FAIL");
133
181
  console.log(` Lighthouse:`);
134
182
  console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
135
183
  console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
@@ -140,6 +188,28 @@ function consoleOutput(result) {
140
188
  else {
141
189
  console.log(` Lighthouse: skipped`);
142
190
  }
191
+ if (result.verification) {
192
+ const view = result.verification.view;
193
+ console.log(` Verification tier: ${view.tier}`);
194
+ console.log(` Question: ${view.question}`);
195
+ console.log(` Verdict: ${view.verdict} (${view.confidence})`);
196
+ console.log(` Summary: ${view.summary}`);
197
+ if (view.blockers.length > 0) {
198
+ console.log(` Blockers:`);
199
+ for (const blocker of view.blockers)
200
+ console.log(` - ${blocker.title}`);
201
+ }
202
+ if (view.nextActions.length > 0) {
203
+ console.log(` Next actions:`);
204
+ for (const action of view.nextActions)
205
+ console.log(` - ${action}`);
206
+ }
207
+ if (view.failureEvidence.length > 0) {
208
+ console.log(` Evidence:`);
209
+ for (const item of view.failureEvidence)
210
+ console.log(` - ${item}`);
211
+ }
212
+ }
143
213
  if (result.github) {
144
214
  if (result.github.status === "comment_posted")
145
215
  console.log(` PR comment: posted`);
@@ -148,43 +218,100 @@ function consoleOutput(result) {
148
218
  }
149
219
  console.log(` Result: .laxy-result.json`);
150
220
  console.log(` Exit code: ${result.exitCode}`);
221
+ // Upsell CTA: Gold ?ъ꽦?????먭퀬 Free ?뚮옖????
222
+ if (result.grade === "silver" || result.grade === "bronze") {
223
+ if (!result._plan || result._plan === "free") {
224
+ console.log(`\n Gold ?깃툒???먰븯?좊떎硫?Pro/Pro+濡??낃렇?덉씠?쒗븯?몄슂:`);
225
+ console.log(` ??https://laxy-blue.vercel.app/pricing`);
226
+ }
227
+ }
151
228
  }
152
229
  async function run() {
153
230
  const args = parseArgs();
154
- // ── 서브커맨드 처리 ──────────────────────────────────────────────────────
231
+ // ?€?€ --help ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
232
+ if (args.help) {
233
+ console.log(`
234
+ laxy-verify v${package_json_1.default.version}
235
+ Frontend quality gate: build + Lighthouse verification
236
+
237
+ Usage:
238
+ npx laxy-verify [project-dir] [options]
239
+ npx laxy-verify <subcommand>
240
+
241
+ Subcommands:
242
+ login [email] Log in to unlock Pro/Pro+ features
243
+ logout Remove saved credentials
244
+ whoami Show current login status
245
+
246
+ Options:
247
+ --init Generate .laxy.yml + GitHub workflow file
248
+ --init --run Generate config and immediately run verification
249
+ --format console | json (default: console)
250
+ --ci CI mode: -10 Performance threshold, runs=3
251
+ --config <path> Path to .laxy.yml
252
+ --fail-on unverified | bronze | silver | gold
253
+ --skip-lighthouse Build-only (max Bronze grade)
254
+ --multi-viewport Pro+: Lighthouse on desktop/tablet/mobile
255
+ --badge Print shields.io badge markdown
256
+ --help Show this help
257
+
258
+ Exit codes:
259
+ 0 Grade meets or exceeds fail_on threshold
260
+ 1 Grade worse than fail_on, or build failed
261
+ 2 Configuration error
262
+
263
+ Examples:
264
+ npx laxy-verify --init --run # Setup + first verification
265
+ npx laxy-verify . # Run in current directory
266
+ npx laxy-verify . --ci # CI mode
267
+ npx laxy-verify . --fail-on silver # Require Silver or better
268
+
269
+ Docs: https://github.com/psungmin24/laxy-verify
270
+ `);
271
+ exitGracefully(0);
272
+ return;
273
+ }
274
+ // ?€?€ ?쒕툕而ㅻ㎤??泥섎━ ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
155
275
  if (args.subcommand === "login") {
156
276
  await (0, auth_js_1.login)(args.subcommandArg);
157
- process.exit(0);
277
+ // Windows: fetch()??TCP ?뚯폆??closing 以묒뿉 process.exit()??利됱떆 ?몄텧?섎㈃
278
+ // UV_HANDLE_CLOSING Assertion 諛쒖깮. setTimeout?쇰줈 UV 猷⑦봽???뺣━ ?쒓컙??以€??
279
+ exitGracefully(0);
158
280
  return;
159
281
  }
160
282
  if (args.subcommand === "logout") {
161
283
  (0, auth_js_1.clearToken)();
162
- process.exit(0);
284
+ exitGracefully(0);
163
285
  return;
164
286
  }
165
287
  if (args.subcommand === "whoami") {
166
288
  (0, auth_js_1.whoami)();
167
- process.exit(0);
289
+ exitGracefully(0);
168
290
  return;
169
291
  }
170
292
  // --init
171
293
  if (args.init) {
172
294
  (0, init_js_1.runInit)(args.projectDir);
173
- process.exit(0);
174
- return;
295
+ if (!args.initRun) {
296
+ console.log(`\n ?ㅼ쓬 ?④퀎: npx laxy-verify . (?먮뒗 --init --run ?쇰줈 諛붾줈 ?ㅽ뻾)`);
297
+ exitGracefully(0);
298
+ return;
299
+ }
300
+ console.log("\n 泥?踰덉㎏ 寃€利앹쓣 ?쒖옉?⑸땲??..\n");
301
+ // initRun: fall through to run verification below
175
302
  }
176
303
  // --badge
177
304
  if (args.badge) {
178
305
  const resultPath = path.join(args.projectDir, ".laxy-result.json");
179
306
  if (!fs.existsSync(resultPath)) {
180
307
  console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
181
- process.exit(2);
308
+ exitGracefully(2);
182
309
  return;
183
310
  }
184
311
  const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
185
312
  const badge = (0, badge_js_1.generateBadge)(content.grade);
186
313
  console.log(badge);
187
- process.exit(0);
314
+ exitGracefully(0);
188
315
  return;
189
316
  }
190
317
  // Load config
@@ -202,7 +329,7 @@ async function run() {
202
329
  }
203
330
  catch (err) {
204
331
  console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
205
- process.exit(2);
332
+ exitGracefully(2);
206
333
  return;
207
334
  }
208
335
  // Auto-detect framework + package manager
@@ -212,17 +339,25 @@ async function run() {
212
339
  }
213
340
  catch (err) {
214
341
  console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
215
- process.exit(2);
342
+ exitGracefully(2);
216
343
  return;
217
344
  }
218
345
  // Merge config overrides
219
346
  const buildCmd = config.build_command || detected.buildCmd;
220
347
  const devCmd = config.dev_command || detected.devCmd;
221
348
  const port = config.port;
349
+ try {
350
+ await ensurePortAvailableForVerification(port);
351
+ }
352
+ catch (err) {
353
+ console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
354
+ exitGracefully(2);
355
+ return;
356
+ }
222
357
  // Phase 1: Build
223
358
  let buildResult;
224
359
  try {
225
- buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout);
360
+ buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
226
361
  }
227
362
  catch (err) {
228
363
  buildResult = {
@@ -241,14 +376,14 @@ async function run() {
241
376
  seo: config.thresholds.seo,
242
377
  bestPractices: config.thresholds.bestPractices,
243
378
  };
244
- // ── 플랜 기능 조회 (토큰 없으면 Free 폴백) ──────────────────────────────
379
+ // ?€?€ ?뚮옖 湲곕뒫 議고쉶 (?좏겙 ?놁쑝硫?Free ?대갚) ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
245
380
  let entitlements = null;
246
381
  try {
247
382
  entitlements = await (0, entitlement_js_1.getEntitlements)();
248
383
  (0, entitlement_js_1.printPlanBanner)(entitlements);
249
384
  }
250
385
  catch {
251
- // 네트워크 오류 Free로 진행
386
+ // ?ㅽ듃?뚰겕 ?ㅻ쪟 ??Free濡?吏꾪뻾
252
387
  }
253
388
  const features = entitlements ?? {
254
389
  plan: "free",
@@ -259,7 +394,7 @@ async function run() {
259
394
  failure_analysis: false,
260
395
  fast_lane: false,
261
396
  };
262
- // Pro 이상이면 Lighthouse 3 실행 (더 정밀)
397
+ // Pro ?댁긽?대㈃ Lighthouse 3???ㅽ뻾 (???뺣?)
263
398
  if (features.lighthouse_runs_3 && config.lighthouse_runs < 3) {
264
399
  config = { ...config, lighthouse_runs: 3 };
265
400
  }
@@ -269,7 +404,7 @@ async function run() {
269
404
  if (buildResult.success && !args.skipLighthouse) {
270
405
  let servePid;
271
406
  try {
272
- const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout);
407
+ const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
273
408
  servePid = serve.pid;
274
409
  try {
275
410
  const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
@@ -287,9 +422,9 @@ async function run() {
287
422
  catch (lhErr) {
288
423
  console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
289
424
  }
290
- // ── Pro+: 멀티 뷰포트 (dev server 돌리는 중) ─────────────────────
425
+ // ?€?€ Pro+: 硫€??酉고룷??(dev server媛€ ?뚮━??以? ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
291
426
  if (args.multiViewport && !features.multi_viewport) {
292
- console.log("\n ⚠️ --multi-viewportPro+ 플랜 전용입니다. laxy-verify login 으로 로그인하세요.");
427
+ console.log("\n ?좑툘 --multi-viewport??Pro+ ?뚮옖 ?꾩슜?낅땲?? laxy-verify login ?쇰줈 濡쒓렇?명븯?몄슂.");
293
428
  }
294
429
  else if (features.multi_viewport) {
295
430
  try {
@@ -319,6 +454,27 @@ async function run() {
319
454
  failOn: config.fail_on,
320
455
  goldEligible: features.gold_grade && allViewportsOk,
321
456
  });
457
+ const verificationTier = (0, index_js_1.planToVerificationTier)(features.plan);
458
+ const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
459
+ const failureEvidence = [
460
+ ...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`),
461
+ ...(viewportSummary.count > 0 && viewportSummary.summary
462
+ ? [`Viewport: ${viewportSummary.summary}`]
463
+ : []),
464
+ ];
465
+ const verificationReport = (0, index_js_1.buildVerificationReport)({
466
+ buildSuccess: buildResult.success,
467
+ buildErrors: buildResult.errors,
468
+ viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
469
+ multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
470
+ multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
471
+ lighthouseScores: scores,
472
+ failureEvidence,
473
+ }, {
474
+ tier: verificationTier,
475
+ thresholds: adjustedThresholds,
476
+ });
477
+ const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
322
478
  // Build result object
323
479
  const resultObj = {
324
480
  grade: gradeResult.grade.charAt(0).toUpperCase() + gradeResult.grade.slice(1), // Capitalize
@@ -334,6 +490,12 @@ async function run() {
334
490
  framework: detected.framework,
335
491
  exitCode: gradeResult.exitCode,
336
492
  config_fail_on: config.fail_on,
493
+ _plan: features.plan,
494
+ verification: {
495
+ tier: verificationTier,
496
+ report: verificationReport,
497
+ view: verificationView,
498
+ },
337
499
  };
338
500
  // GitHub integration (only in Actions)
339
501
  const inGitHubActions = !!process.env.GITHUB_ACTIONS;
@@ -362,9 +524,9 @@ async function run() {
362
524
  if (inGitHubActions && process.env.GITHUB_OUTPUT) {
363
525
  fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
364
526
  }
365
- process.exit(gradeResult.exitCode);
527
+ exitGracefully(gradeResult.exitCode);
366
528
  }
367
529
  run().catch((err) => {
368
530
  console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
369
- process.exit(1);
531
+ exitGracefully(1);
370
532
  });
package/dist/serve.d.ts CHANGED
@@ -8,5 +8,6 @@ export interface ServeResult {
8
8
  pid: number;
9
9
  port: number;
10
10
  }
11
- export declare function startDevServer(command: string, port: number, timeoutSec: number): Promise<ServeResult>;
11
+ export declare function probeServerStatus(port: number): Promise<number | null>;
12
+ export declare function startDevServer(command: string, port: number, timeoutSec: number, cwd?: string): Promise<ServeResult>;
12
13
  export declare function stopDevServer(pid: number): void;
package/dist/serve.js CHANGED
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.DevServerTimeoutError = exports.PortConflictError = void 0;
40
+ exports.probeServerStatus = probeServerStatus;
40
41
  exports.startDevServer = startDevServer;
41
42
  exports.stopDevServer = stopDevServer;
42
43
  const node_child_process_1 = require("node:child_process");
@@ -67,13 +68,17 @@ function httpGet(url) {
67
68
  });
68
69
  });
69
70
  }
70
- async function startDevServer(command, port, timeoutSec) {
71
+ function probeServerStatus(port) {
72
+ return httpGet(`http://localhost:${port}/`);
73
+ }
74
+ async function startDevServer(command, port, timeoutSec, cwd) {
71
75
  return new Promise((resolve, reject) => {
72
- console.log(`Starting dev server: ${command}`);
76
+ console.log(`Starting dev server: ${command}${cwd ? ` (cwd: ${cwd})` : ""}`);
73
77
  const proc = (0, node_child_process_1.spawn)(command, {
74
78
  shell: true,
75
79
  stdio: ["ignore", "pipe", "pipe"],
76
80
  env: { ...process.env, PORT: String(port) },
81
+ cwd,
77
82
  });
78
83
  // Pipe output to console
79
84
  proc.stdout?.on("data", (chunk) => {
@@ -103,7 +108,7 @@ async function startDevServer(command, port, timeoutSec) {
103
108
  reject(new DevServerTimeoutError(port, timeoutSec));
104
109
  return;
105
110
  }
106
- const status = await httpGet(`http://localhost:${port}/`);
111
+ const status = await probeServerStatus(port);
107
112
  if (status !== null) {
108
113
  if (status === 200) {
109
114
  console.log(`Dev server ready on port ${port} (HTTP ${status})`);
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./tier-policy.js";
3
+ export * from "./report.js";
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types.js"), exports);
18
+ __exportStar(require("./tier-policy.js"), exports);
19
+ __exportStar(require("./report.js"), exports);
@@ -0,0 +1,14 @@
1
+ import type { LighthouseThresholds, TierVerificationView, VerificationEvidence, VerificationFinding, VerificationGrade, VerificationInput, VerificationReport, VerificationTier } from "./types.js";
2
+ export declare const DEFAULT_LH_THRESHOLDS: LighthouseThresholds;
3
+ export declare function getLighthousePass(lighthouseScores?: VerificationInput["lighthouseScores"], thresholds?: LighthouseThresholds): boolean;
4
+ export declare function getVerificationGrade(input: VerificationInput, thresholds?: LighthouseThresholds): VerificationGrade;
5
+ export declare function buildVerificationEvidence(input: VerificationInput, thresholds?: LighthouseThresholds): VerificationEvidence;
6
+ export declare function getImprovementRecommendations(input: VerificationInput, thresholds?: LighthouseThresholds): VerificationFinding[];
7
+ export declare function buildVerificationReport(input: VerificationInput, options?: {
8
+ tier?: VerificationTier;
9
+ thresholds?: LighthouseThresholds;
10
+ }): VerificationReport;
11
+ export declare function buildTierVerificationView(input: VerificationInput, options?: {
12
+ tier?: VerificationTier;
13
+ thresholds?: LighthouseThresholds;
14
+ }): TierVerificationView;
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_LH_THRESHOLDS = void 0;
4
+ exports.getLighthousePass = getLighthousePass;
5
+ exports.getVerificationGrade = getVerificationGrade;
6
+ exports.buildVerificationEvidence = buildVerificationEvidence;
7
+ exports.getImprovementRecommendations = getImprovementRecommendations;
8
+ exports.buildVerificationReport = buildVerificationReport;
9
+ exports.buildTierVerificationView = buildTierVerificationView;
10
+ const tier_policy_js_1 = require("./tier-policy.js");
11
+ exports.DEFAULT_LH_THRESHOLDS = {
12
+ performance: 70,
13
+ accessibility: 85,
14
+ seo: 80,
15
+ bestPractices: 80,
16
+ };
17
+ function getLighthousePass(lighthouseScores, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
18
+ if (!lighthouseScores)
19
+ return false;
20
+ return (lighthouseScores.performance >= thresholds.performance &&
21
+ lighthouseScores.accessibility >= thresholds.accessibility &&
22
+ lighthouseScores.seo >= thresholds.seo &&
23
+ lighthouseScores.bestPractices >= thresholds.bestPractices);
24
+ }
25
+ function getVerificationGrade(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
26
+ const buildPassed = input.buildSuccess === true;
27
+ const e2ePassedAll = typeof input.e2ePassed === "number" &&
28
+ typeof input.e2eTotal === "number" &&
29
+ input.e2eTotal > 0 &&
30
+ input.e2ePassed === input.e2eTotal;
31
+ const lighthousePassed = getLighthousePass(input.lighthouseScores, thresholds);
32
+ if (buildPassed && e2ePassedAll && lighthousePassed)
33
+ return "gold";
34
+ if (buildPassed && e2ePassedAll)
35
+ return "silver";
36
+ if (buildPassed)
37
+ return "bronze";
38
+ return "unverified";
39
+ }
40
+ function buildVerificationEvidence(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
41
+ const buildPassed = input.buildSuccess === true;
42
+ const hasE2EData = typeof input.e2eTotal === "number" && input.e2eTotal > 0;
43
+ const e2ePassedAll = hasE2EData &&
44
+ typeof input.e2ePassed === "number" &&
45
+ typeof input.e2eTotal === "number" &&
46
+ input.e2ePassed === input.e2eTotal;
47
+ const hasMultiViewportData = typeof input.viewportIssues === "number" || typeof input.multiViewportPassed === "boolean";
48
+ const multiViewportPassed = hasMultiViewportData
49
+ ? input.multiViewportPassed === true ||
50
+ (input.multiViewportPassed !== false && (input.viewportIssues ?? 0) <= 0)
51
+ : false;
52
+ const hasVisualDiffData = typeof input.visualDiffVerdict === "string";
53
+ const visualDiffPassed = hasVisualDiffData &&
54
+ input.visualDiffVerdict !== "warn" &&
55
+ input.visualDiffVerdict !== "rollback";
56
+ const lighthousePassed = getLighthousePass(input.lighthouseScores, thresholds);
57
+ return {
58
+ input,
59
+ thresholds,
60
+ buildPassed,
61
+ hasE2EData,
62
+ e2ePassedAll,
63
+ hasMultiViewportData,
64
+ multiViewportPassed,
65
+ hasVisualDiffData,
66
+ visualDiffPassed,
67
+ lighthousePassed,
68
+ };
69
+ }
70
+ function getImprovementRecommendations(input, thresholds = exports.DEFAULT_LH_THRESHOLDS) {
71
+ const findings = [];
72
+ const errors = input.buildErrors ?? [];
73
+ if (input.buildSuccess === false) {
74
+ if (errors.some((error) => /TS\d+|type/i.test(error))) {
75
+ findings.push({
76
+ category: "build",
77
+ severity: "critical",
78
+ title: "TypeScript build errors",
79
+ description: "Type errors are blocking a clean production build.",
80
+ action: "Fix the TypeScript errors first, then rerun verification.",
81
+ });
82
+ }
83
+ if (errors.some((error) => /Module not found|Cannot find module|Failed to resolve/i.test(error))) {
84
+ findings.push({
85
+ category: "build",
86
+ severity: "critical",
87
+ title: "Missing or unresolved modules",
88
+ description: "The build cannot resolve one or more imports or packages.",
89
+ action: "Check import paths, package installation, and package.json consistency.",
90
+ });
91
+ }
92
+ if (errors.some((error) => /SyntaxError|Unexpected token/i.test(error))) {
93
+ findings.push({
94
+ category: "build",
95
+ severity: "critical",
96
+ title: "Syntax errors in source code",
97
+ description: "The code contains syntax issues that stop the build from completing.",
98
+ action: "Fix the syntax errors, then rerun the build verification.",
99
+ });
100
+ }
101
+ if (findings.every((finding) => finding.category !== "build")) {
102
+ findings.push({
103
+ category: "build",
104
+ severity: "critical",
105
+ title: "Build failed",
106
+ description: "Production build verification did not pass.",
107
+ action: "Inspect the build logs and resolve the blocking errors before release.",
108
+ });
109
+ }
110
+ }
111
+ if (typeof input.e2ePassed === "number" &&
112
+ typeof input.e2eTotal === "number" &&
113
+ input.e2eTotal > 0 &&
114
+ input.e2ePassed < input.e2eTotal) {
115
+ findings.push({
116
+ category: "e2e",
117
+ severity: "high",
118
+ title: `E2E failures (${input.e2ePassed}/${input.e2eTotal})`,
119
+ description: "One or more verification scenarios failed.",
120
+ action: "Fix the broken user flow and rerun the verification scenarios.",
121
+ });
122
+ }
123
+ const hasMultiViewportData = typeof input.viewportIssues === "number" || typeof input.multiViewportPassed === "boolean";
124
+ const multiViewportPassed = input.multiViewportPassed === true ||
125
+ (input.multiViewportPassed !== false && (input.viewportIssues ?? 0) <= 0);
126
+ if (hasMultiViewportData && !multiViewportPassed) {
127
+ findings.push({
128
+ category: "viewport",
129
+ severity: "high",
130
+ title: `Multi-viewport issues detected (${input.viewportIssues ?? 0})`,
131
+ description: input.multiViewportSummary ||
132
+ "One or more responsive layout or viewport-specific verification issues were found.",
133
+ action: "Fix the responsive layout issues and rerun the multi-viewport verification pass.",
134
+ });
135
+ }
136
+ if (input.visualDiffVerdict === "rollback") {
137
+ findings.push({
138
+ category: "visual",
139
+ severity: "high",
140
+ title: `Visual regression detected (${input.visualDiffPercentage ?? 0}%)`,
141
+ description: "The visual diff is large enough to recommend a rollback or release hold.",
142
+ action: "Review the visual diff artifacts and fix the unintended UI regression before release.",
143
+ });
144
+ }
145
+ else if (input.visualDiffVerdict === "warn") {
146
+ findings.push({
147
+ category: "visual",
148
+ severity: "medium",
149
+ title: `Visual change needs review (${input.visualDiffPercentage ?? 0}%)`,
150
+ description: "The visual diff changed enough to require a manual review before release.",
151
+ action: "Check the visual diff and confirm the UI change is intentional.",
152
+ });
153
+ }
154
+ const lighthouseScores = input.lighthouseScores;
155
+ if (!lighthouseScores) {
156
+ return findings;
157
+ }
158
+ const lighthouseFinding = (category, actual, required, title, description, action) => ({
159
+ category,
160
+ severity: required - actual >= 20 ? "high" : "medium",
161
+ title: `${title} (${actual} / ${required})`,
162
+ description,
163
+ action,
164
+ });
165
+ if (lighthouseScores.performance < thresholds.performance) {
166
+ findings.push(lighthouseFinding("performance", lighthouseScores.performance, thresholds.performance, "Performance below threshold", "Runtime performance is below the minimum verification threshold.", "Reduce heavy assets, expensive scripts, and blocking work on initial load."));
167
+ }
168
+ if (lighthouseScores.accessibility < thresholds.accessibility) {
169
+ findings.push(lighthouseFinding("accessibility", lighthouseScores.accessibility, thresholds.accessibility, "Accessibility below threshold", "Accessibility checks are below the minimum verification threshold.", "Fix labels, semantics, contrast, and keyboard accessibility issues."));
170
+ }
171
+ if (lighthouseScores.seo < thresholds.seo) {
172
+ findings.push(lighthouseFinding("seo", lighthouseScores.seo, thresholds.seo, "SEO below threshold", "SEO checks are below the minimum verification threshold.", "Fix title, description, crawl settings, and indexable metadata."));
173
+ }
174
+ if (lighthouseScores.bestPractices < thresholds.bestPractices) {
175
+ findings.push(lighthouseFinding("bestPractices", lighthouseScores.bestPractices, thresholds.bestPractices, "Best practices below threshold", "Best practices checks are below the minimum verification threshold.", "Fix browser warnings, unsafe patterns, and platform-level issues."));
176
+ }
177
+ return findings.sort((a, b) => {
178
+ const priority = { critical: 0, high: 1, medium: 2 };
179
+ return priority[a.severity] - priority[b.severity];
180
+ });
181
+ }
182
+ function buildVerificationReport(input, options) {
183
+ const thresholds = options?.thresholds ?? exports.DEFAULT_LH_THRESHOLDS;
184
+ const tier = options?.tier ?? "free";
185
+ const evidence = buildVerificationEvidence(input, thresholds);
186
+ const findings = getImprovementRecommendations(input, thresholds);
187
+ const blockers = findings.filter((finding) => finding.severity === "critical" || finding.severity === "high");
188
+ const warnings = findings.filter((finding) => finding.severity === "medium");
189
+ const grade = getVerificationGrade(input, thresholds);
190
+ const failureEvidence = (input.failureEvidence ?? []).filter(Boolean).slice(0, 5);
191
+ let verdict;
192
+ let confidence;
193
+ let summary;
194
+ if (!evidence.buildPassed) {
195
+ verdict = "build-failed";
196
+ confidence = "low";
197
+ summary = "Build failed. Fix the blocking build errors before relying on this verification result.";
198
+ }
199
+ else if (blockers.length > 0) {
200
+ verdict = "hold";
201
+ confidence = tier === "free" ? "low" : "medium";
202
+ summary = "Blocking verification issues were found. Hold release until the blockers are fixed.";
203
+ }
204
+ else if (tier === "pro_plus" &&
205
+ evidence.buildPassed &&
206
+ evidence.e2ePassedAll &&
207
+ evidence.lighthousePassed &&
208
+ evidence.hasMultiViewportData &&
209
+ evidence.multiViewportPassed) {
210
+ verdict = "release-ready";
211
+ confidence = "high";
212
+ summary = "Core verification checks passed. This run supports a release-ready call.";
213
+ }
214
+ else if (tier === "pro_plus" &&
215
+ evidence.buildPassed &&
216
+ evidence.e2ePassedAll &&
217
+ evidence.lighthousePassed &&
218
+ !evidence.hasMultiViewportData) {
219
+ verdict = "investigate";
220
+ confidence = "medium";
221
+ summary = "Core checks passed, but release-ready confidence still needs multi-viewport verification evidence.";
222
+ }
223
+ else if (tier === "free") {
224
+ verdict = "quick-pass";
225
+ confidence = evidence.lighthousePassed ? "medium" : "low";
226
+ summary = "No immediate hard blockers were found in the quick verification pass.";
227
+ }
228
+ else {
229
+ verdict = "investigate";
230
+ confidence = "medium";
231
+ summary = "The build is standing, but deeper verification evidence should be reviewed before release.";
232
+ }
233
+ const passes = [
234
+ { key: "build", label: "Production build", passed: evidence.buildPassed },
235
+ ...(evidence.hasE2EData
236
+ ? [{ key: "e2e", label: `E2E ${input.e2ePassed ?? 0}/${input.e2eTotal ?? 0}`, passed: evidence.e2ePassedAll }]
237
+ : []),
238
+ ...(evidence.hasMultiViewportData
239
+ ? [{
240
+ key: "viewport",
241
+ label: `Viewport ${input.viewportIssues ?? 0} issues`,
242
+ passed: evidence.multiViewportPassed,
243
+ }]
244
+ : []),
245
+ ...(evidence.hasVisualDiffData
246
+ ? [{
247
+ key: "visual",
248
+ label: input.hasVisualBaseline
249
+ ? `Visual diff ${input.visualDiffPercentage ?? 0}%`
250
+ : "Visual baseline seeded",
251
+ passed: input.visualDiffVerdict !== "rollback",
252
+ }]
253
+ : []),
254
+ { key: "lighthouse", label: "Lighthouse thresholds", passed: evidence.lighthousePassed },
255
+ ];
256
+ const nextActions = [...blockers, ...warnings].slice(0, 4).map((finding) => finding.action);
257
+ return {
258
+ tier,
259
+ verdict,
260
+ confidence,
261
+ summary,
262
+ grade,
263
+ blockers,
264
+ warnings,
265
+ passes,
266
+ nextActions,
267
+ failureEvidence,
268
+ evidence,
269
+ };
270
+ }
271
+ function buildTierVerificationView(input, options) {
272
+ return (0, tier_policy_js_1.getTierVerificationView)(buildVerificationReport(input, options));
273
+ }
@@ -0,0 +1,13 @@
1
+ import type { TierVerificationView, VerificationReport, VerificationTier } from "./types.js";
2
+ export interface TierPolicy {
3
+ tier: VerificationTier;
4
+ showDetailedLighthouse: boolean;
5
+ showDetailedE2E: boolean;
6
+ showReportExport: boolean;
7
+ maxBlockers: number;
8
+ maxWarnings: number;
9
+ }
10
+ export declare function getTierPolicy(tier?: VerificationTier): TierPolicy;
11
+ export declare function planToVerificationTier(plan?: string | null): VerificationTier;
12
+ export declare function getVerificationTierQuestion(tier: VerificationTier): string;
13
+ export declare function getTierVerificationView(report: VerificationReport): TierVerificationView;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTierPolicy = getTierPolicy;
4
+ exports.planToVerificationTier = planToVerificationTier;
5
+ exports.getVerificationTierQuestion = getVerificationTierQuestion;
6
+ exports.getTierVerificationView = getTierVerificationView;
7
+ const TIER_POLICIES = {
8
+ free: {
9
+ tier: "free",
10
+ showDetailedLighthouse: false,
11
+ showDetailedE2E: false,
12
+ showReportExport: false,
13
+ maxBlockers: 1,
14
+ maxWarnings: 2,
15
+ },
16
+ pro: {
17
+ tier: "pro",
18
+ showDetailedLighthouse: true,
19
+ showDetailedE2E: true,
20
+ showReportExport: true,
21
+ maxBlockers: 5,
22
+ maxWarnings: 5,
23
+ },
24
+ pro_plus: {
25
+ tier: "pro_plus",
26
+ showDetailedLighthouse: true,
27
+ showDetailedE2E: true,
28
+ showReportExport: true,
29
+ maxBlockers: 8,
30
+ maxWarnings: 8,
31
+ },
32
+ };
33
+ function getTierPolicy(tier = "free") {
34
+ return TIER_POLICIES[tier];
35
+ }
36
+ function planToVerificationTier(plan) {
37
+ if (plan === "pro")
38
+ return "pro";
39
+ if (plan === "pro_plus" || plan === "team" || plan === "enterprise")
40
+ return "pro_plus";
41
+ return "free";
42
+ }
43
+ function getVerificationTierQuestion(tier) {
44
+ switch (tier) {
45
+ case "pro":
46
+ return "Is this strong enough to send to a client?";
47
+ case "pro_plus":
48
+ return "Can I call this release-ready with confidence?";
49
+ default:
50
+ return "Is this likely to break right now?";
51
+ }
52
+ }
53
+ function getTierVerificationView(report) {
54
+ const policy = getTierPolicy(report.tier);
55
+ return {
56
+ tier: report.tier,
57
+ question: getVerificationTierQuestion(report.tier),
58
+ verdict: report.verdict,
59
+ confidence: report.confidence,
60
+ summary: report.summary,
61
+ grade: report.grade,
62
+ blockers: report.blockers.slice(0, policy.maxBlockers),
63
+ warnings: report.warnings.slice(0, policy.maxWarnings),
64
+ passes: report.passes,
65
+ nextActions: report.nextActions.slice(0, Math.max(2, policy.maxWarnings)),
66
+ failureEvidence: report.failureEvidence.slice(0, Math.max(2, policy.maxWarnings)),
67
+ showDetailedLighthouse: policy.showDetailedLighthouse,
68
+ showDetailedE2E: policy.showDetailedE2E,
69
+ showReportExport: policy.showReportExport,
70
+ };
71
+ }
@@ -0,0 +1,82 @@
1
+ export type VerificationGrade = "gold" | "silver" | "bronze" | "unverified";
2
+ export type VerificationTier = "free" | "pro" | "pro_plus";
3
+ export type ReleaseVerdict = "quick-pass" | "investigate" | "hold" | "release-ready" | "build-failed";
4
+ export interface LighthouseThresholds {
5
+ performance: number;
6
+ accessibility: number;
7
+ seo: number;
8
+ bestPractices: number;
9
+ }
10
+ export interface LighthouseScores {
11
+ performance: number;
12
+ accessibility: number;
13
+ bestPractices: number;
14
+ seo: number;
15
+ }
16
+ export interface VerificationInput {
17
+ buildSuccess?: boolean;
18
+ buildErrors?: string[];
19
+ e2ePassed?: number;
20
+ e2eTotal?: number;
21
+ viewportIssues?: number;
22
+ multiViewportPassed?: boolean;
23
+ multiViewportSummary?: string;
24
+ visualDiffVerdict?: "pass" | "warn" | "rollback";
25
+ visualDiffPercentage?: number;
26
+ hasVisualBaseline?: boolean;
27
+ failureEvidence?: string[];
28
+ lighthouseScores?: LighthouseScores;
29
+ }
30
+ export interface VerificationCheck {
31
+ key: "build" | "e2e" | "lighthouse" | "viewport" | "visual";
32
+ label: string;
33
+ passed: boolean;
34
+ }
35
+ export interface VerificationFinding {
36
+ category: "build" | "performance" | "accessibility" | "seo" | "bestPractices" | "e2e" | "viewport" | "visual";
37
+ severity: "critical" | "high" | "medium";
38
+ title: string;
39
+ description: string;
40
+ action: string;
41
+ }
42
+ export interface VerificationEvidence {
43
+ input: VerificationInput;
44
+ thresholds: LighthouseThresholds;
45
+ buildPassed: boolean;
46
+ e2ePassedAll: boolean;
47
+ hasE2EData: boolean;
48
+ hasMultiViewportData: boolean;
49
+ multiViewportPassed: boolean;
50
+ hasVisualDiffData: boolean;
51
+ visualDiffPassed: boolean;
52
+ lighthousePassed: boolean;
53
+ }
54
+ export interface VerificationReport {
55
+ tier: VerificationTier;
56
+ verdict: ReleaseVerdict;
57
+ confidence: "low" | "medium" | "high";
58
+ summary: string;
59
+ grade: VerificationGrade;
60
+ blockers: VerificationFinding[];
61
+ warnings: VerificationFinding[];
62
+ passes: VerificationCheck[];
63
+ nextActions: string[];
64
+ failureEvidence: string[];
65
+ evidence: VerificationEvidence;
66
+ }
67
+ export interface TierVerificationView {
68
+ tier: VerificationTier;
69
+ question: string;
70
+ verdict: ReleaseVerdict;
71
+ confidence: "low" | "medium" | "high";
72
+ summary: string;
73
+ grade: VerificationGrade;
74
+ blockers: VerificationFinding[];
75
+ warnings: VerificationFinding[];
76
+ passes: VerificationCheck[];
77
+ nextActions: string[];
78
+ failureEvidence: string[];
79
+ showDetailedLighthouse: boolean;
80
+ showDetailedE2E: boolean;
81
+ showReportExport: boolean;
82
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laxy-verify",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Frontend quality gate: build + Lighthouse verification",
5
5
  "type": "commonjs",
6
6
  "bin": {