laxy-verify 1.1.7 → 1.1.9

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,23 @@ 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 e2e_js_1 = require("./e2e.js");
56
+ const visual_diff_js_1 = require("./visual-diff.js");
57
+ const index_js_1 = require("./verification-core/index.js");
58
+ const package_json_1 = __importDefault(require("../package.json"));
59
+ function exitGracefully(code) {
60
+ if (process.platform === "win32") {
61
+ setTimeout(() => process.exit(code), 100);
62
+ return;
63
+ }
64
+ process.exit(code);
65
+ }
66
+ async function ensurePortAvailableForVerification(port) {
67
+ const status = await (0, serve_js_1.probeServerStatus)(port);
68
+ if (status === null)
69
+ return;
70
+ 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.`);
71
+ }
52
72
  function parseArgs() {
53
73
  const raw = process.argv.slice(2);
54
74
  // Single-pass: collect flags and first positional
@@ -80,20 +100,20 @@ function parseArgs() {
80
100
  }
81
101
  }
82
102
  }
83
- // 서브커맨드 처리: login [email], logout, whoami
103
+ // ?쒕툕而ㅻ㎤??泥섎━: login [email], logout, whoami
84
104
  let subcommand;
85
105
  let subcommandArg;
86
106
  if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
87
107
  subcommand = projectDir;
88
108
  projectDir = ".";
89
- // login 뒤에 이메일이 올 수 있음
109
+ // login ?ㅼ뿉 ?대찓?쇱씠 ?????덉쓬
90
110
  if (subcommand === "login" && raw.length > 0 && !raw[0].startsWith("-")) {
91
111
  // already consumed via loop above if it was first; re-check flags
92
112
  }
93
- // 번째 non-flag 이메일로 취급
113
+ // 泥?踰덉㎏ non-flag瑜??대찓?쇰줈 痍④툒
94
114
  subcommandArg = flags["email"];
95
115
  if (!subcommandArg) {
96
- // login PSM@example.com 형태 positional already captured in projectDir before reset
116
+ // login PSM@example.com ?뺥깭 ??positional already captured in projectDir before reset
97
117
  }
98
118
  }
99
119
  return {
@@ -107,29 +127,59 @@ function parseArgs() {
107
127
  skipLighthouse: flags["skip-lighthouse"] !== undefined,
108
128
  badge: flags["badge"] !== undefined,
109
129
  init: flags["init"] !== undefined,
130
+ initRun: flags["init"] !== undefined && flags["run"] !== undefined,
110
131
  multiViewport: flags["multi-viewport"] !== undefined,
111
132
  failureAnalysis: flags["failure-analysis"] !== undefined,
133
+ help: flags["help"] !== undefined || flags["h"] !== undefined,
112
134
  };
113
135
  }
114
136
  function writeResultFile(projectDir, result) {
115
137
  const filePath = path.join(projectDir, ".laxy-result.json");
116
138
  fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
117
139
  }
140
+ function summarizeViewportIssues(scores, thresholds) {
141
+ if (!scores)
142
+ return { count: 0 };
143
+ const failed = [];
144
+ for (const [label, viewportScores] of Object.entries(scores)) {
145
+ if (!viewportScores) {
146
+ failed.push(`${label}: missing`);
147
+ continue;
148
+ }
149
+ const passes = viewportScores.performance >= thresholds.performance &&
150
+ viewportScores.accessibility >= thresholds.accessibility &&
151
+ viewportScores.seo >= thresholds.seo &&
152
+ viewportScores.bestPractices >= thresholds.bestPractices;
153
+ if (!passes) {
154
+ failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
155
+ }
156
+ }
157
+ return {
158
+ count: failed.length,
159
+ summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
160
+ };
161
+ }
118
162
  function consoleOutput(result) {
119
163
  const gradeLabel = result.grade;
120
- const checkEmoji = result.grade !== "Unverified" ? " " : "";
121
- console.log(`\n Laxy Verify ${gradeLabel}${checkEmoji}`);
164
+ const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
165
+ console.log(`\n Laxy Verify ${gradeLabel}${checkEmoji}`);
122
166
  console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
123
167
  if (result.build.errors.length > 0) {
124
- const last5 = result.build.errors.slice(-5);
125
168
  console.log(` Errors:`);
126
- for (const e of last5)
169
+ // 泥?踰덉㎏ 'error' ?ㅼ썙?쒓? ?ы븿??以?(?먯씤 ?뚯븙??
170
+ const firstError = result.build.errors.find(e => /error/i.test(e));
171
+ // 留덉?留?5以?(鍮뚮뱶 醫낅즺 而⑦뀓?ㅽ듃)
172
+ const last5 = result.build.errors.slice(-5);
173
+ const toShow = firstError && !last5.includes(firstError)
174
+ ? [firstError, " ...", ...last5]
175
+ : last5;
176
+ for (const e of toShow)
127
177
  console.error(` ${e}`);
128
178
  }
129
179
  if (result.lighthouse !== null) {
130
180
  const lh = result.lighthouse;
131
181
  const t = result.thresholds;
132
- const check = (passed) => passed ? " " : " ";
182
+ const check = (passed) => (passed ? " OK" : " FAIL");
133
183
  console.log(` Lighthouse:`);
134
184
  console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
135
185
  console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
@@ -140,6 +190,34 @@ function consoleOutput(result) {
140
190
  else {
141
191
  console.log(` Lighthouse: skipped`);
142
192
  }
193
+ if (result.e2e) {
194
+ console.log(` E2E: ${result.e2e.passed}/${result.e2e.total} passed`);
195
+ }
196
+ if (result.visualDiff) {
197
+ console.log(` Visual diff: ${result.visualDiff.diffPercentage}% (${result.visualDiff.verdict})`);
198
+ }
199
+ if (result.verification) {
200
+ const view = result.verification.view;
201
+ console.log(` Verification tier: ${view.tier}`);
202
+ console.log(` Question: ${view.question}`);
203
+ console.log(` Verdict: ${view.verdict} (${view.confidence})`);
204
+ console.log(` Summary: ${view.summary}`);
205
+ if (view.blockers.length > 0) {
206
+ console.log(` Blockers:`);
207
+ for (const blocker of view.blockers)
208
+ console.log(` - ${blocker.title}`);
209
+ }
210
+ if (view.nextActions.length > 0) {
211
+ console.log(` Next actions:`);
212
+ for (const action of view.nextActions)
213
+ console.log(` - ${action}`);
214
+ }
215
+ if (view.failureEvidence.length > 0) {
216
+ console.log(` Evidence:`);
217
+ for (const item of view.failureEvidence)
218
+ console.log(` - ${item}`);
219
+ }
220
+ }
143
221
  if (result.github) {
144
222
  if (result.github.status === "comment_posted")
145
223
  console.log(` PR comment: posted`);
@@ -148,45 +226,100 @@ function consoleOutput(result) {
148
226
  }
149
227
  console.log(` Result: .laxy-result.json`);
150
228
  console.log(` Exit code: ${result.exitCode}`);
229
+ // Upsell CTA: Gold ?ъ꽦?????먭퀬 Free ?뚮옖????
230
+ if (result.grade === "silver" || result.grade === "bronze") {
231
+ if (!result._plan || result._plan === "free") {
232
+ console.log(`\n Gold ?깃툒???먰븯?좊떎硫?Pro/Pro+濡??낃렇?덉씠?쒗븯?몄슂:`);
233
+ console.log(` ??https://laxy-blue.vercel.app/pricing`);
234
+ }
235
+ }
151
236
  }
152
237
  async function run() {
153
238
  const args = parseArgs();
154
- // ── 서브커맨드 처리 ──────────────────────────────────────────────────────
239
+ // ?€?€ --help ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
240
+ if (args.help) {
241
+ console.log(`
242
+ laxy-verify v${package_json_1.default.version}
243
+ Frontend quality gate: build + Lighthouse verification
244
+
245
+ Usage:
246
+ npx laxy-verify [project-dir] [options]
247
+ npx laxy-verify <subcommand>
248
+
249
+ Subcommands:
250
+ login [email] Log in to unlock Pro/Pro+ features
251
+ logout Remove saved credentials
252
+ whoami Show current login status
253
+
254
+ Options:
255
+ --init Generate .laxy.yml + GitHub workflow file
256
+ --init --run Generate config and immediately run verification
257
+ --format console | json (default: console)
258
+ --ci CI mode: -10 Performance threshold, runs=3
259
+ --config <path> Path to .laxy.yml
260
+ --fail-on unverified | bronze | silver | gold
261
+ --skip-lighthouse Build-only (max Bronze grade)
262
+ --multi-viewport Pro+: Lighthouse on desktop/tablet/mobile
263
+ --badge Print shields.io badge markdown
264
+ --help Show this help
265
+
266
+ Exit codes:
267
+ 0 Grade meets or exceeds fail_on threshold
268
+ 1 Grade worse than fail_on, or build failed
269
+ 2 Configuration error
270
+
271
+ Examples:
272
+ npx laxy-verify --init --run # Setup + first verification
273
+ npx laxy-verify . # Run in current directory
274
+ npx laxy-verify . --ci # CI mode
275
+ npx laxy-verify . --fail-on silver # Require Silver or better
276
+
277
+ Docs: https://github.com/psungmin24/laxy-verify
278
+ `);
279
+ exitGracefully(0);
280
+ return;
281
+ }
282
+ // ?€?€ ?쒕툕而ㅻ㎤??泥섎━ ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
155
283
  if (args.subcommand === "login") {
156
284
  await (0, auth_js_1.login)(args.subcommandArg);
157
- // Windows: fetch()TCP 소켓이 closing 중에 process.exit() 즉시 호출하면
158
- // UV_HANDLE_CLOSING Assertion 발생. setTimeout으로 UV 루프에 정리 시간을 준다.
159
- setTimeout(() => process.exit(0), 100);
285
+ // Windows: fetch()??TCP ?뚯폆??closing 以묒뿉 process.exit()??利됱떆 ?몄텧?섎㈃
286
+ // UV_HANDLE_CLOSING Assertion 諛쒖깮. setTimeout?쇰줈 UV 猷⑦봽???뺣━ ?쒓컙??以€??
287
+ exitGracefully(0);
160
288
  return;
161
289
  }
162
290
  if (args.subcommand === "logout") {
163
291
  (0, auth_js_1.clearToken)();
164
- process.exit(0);
292
+ exitGracefully(0);
165
293
  return;
166
294
  }
167
295
  if (args.subcommand === "whoami") {
168
296
  (0, auth_js_1.whoami)();
169
- process.exit(0);
297
+ exitGracefully(0);
170
298
  return;
171
299
  }
172
300
  // --init
173
301
  if (args.init) {
174
302
  (0, init_js_1.runInit)(args.projectDir);
175
- process.exit(0);
176
- return;
303
+ if (!args.initRun) {
304
+ console.log(`\n ?ㅼ쓬 ?④퀎: npx laxy-verify . (?먮뒗 --init --run ?쇰줈 諛붾줈 ?ㅽ뻾)`);
305
+ exitGracefully(0);
306
+ return;
307
+ }
308
+ console.log("\n 泥?踰덉㎏ 寃€利앹쓣 ?쒖옉?⑸땲??..\n");
309
+ // initRun: fall through to run verification below
177
310
  }
178
311
  // --badge
179
312
  if (args.badge) {
180
313
  const resultPath = path.join(args.projectDir, ".laxy-result.json");
181
314
  if (!fs.existsSync(resultPath)) {
182
315
  console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
183
- process.exit(2);
316
+ exitGracefully(2);
184
317
  return;
185
318
  }
186
319
  const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
187
320
  const badge = (0, badge_js_1.generateBadge)(content.grade);
188
321
  console.log(badge);
189
- process.exit(0);
322
+ exitGracefully(0);
190
323
  return;
191
324
  }
192
325
  // Load config
@@ -204,7 +337,7 @@ async function run() {
204
337
  }
205
338
  catch (err) {
206
339
  console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
207
- process.exit(2);
340
+ exitGracefully(2);
208
341
  return;
209
342
  }
210
343
  // Auto-detect framework + package manager
@@ -214,17 +347,25 @@ async function run() {
214
347
  }
215
348
  catch (err) {
216
349
  console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
217
- process.exit(2);
350
+ exitGracefully(2);
218
351
  return;
219
352
  }
220
353
  // Merge config overrides
221
354
  const buildCmd = config.build_command || detected.buildCmd;
222
355
  const devCmd = config.dev_command || detected.devCmd;
223
356
  const port = config.port;
357
+ try {
358
+ await ensurePortAvailableForVerification(port);
359
+ }
360
+ catch (err) {
361
+ console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
362
+ exitGracefully(2);
363
+ return;
364
+ }
224
365
  // Phase 1: Build
225
366
  let buildResult;
226
367
  try {
227
- buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout);
368
+ buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
228
369
  }
229
370
  catch (err) {
230
371
  buildResult = {
@@ -243,14 +384,14 @@ async function run() {
243
384
  seo: config.thresholds.seo,
244
385
  bestPractices: config.thresholds.bestPractices,
245
386
  };
246
- // ── 플랜 기능 조회 (토큰 없으면 Free 폴백) ──────────────────────────────
387
+ // ?€?€ ?뚮옖 湲곕뒫 議고쉶 (?좏겙 ?놁쑝硫?Free ?대갚) ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
247
388
  let entitlements = null;
248
389
  try {
249
390
  entitlements = await (0, entitlement_js_1.getEntitlements)();
250
391
  (0, entitlement_js_1.printPlanBanner)(entitlements);
251
392
  }
252
393
  catch {
253
- // 네트워크 오류 Free로 진행
394
+ // ?ㅽ듃?뚰겕 ?ㅻ쪟 ??Free濡?吏꾪뻾
254
395
  }
255
396
  const features = entitlements ?? {
256
397
  plan: "free",
@@ -261,18 +402,22 @@ async function run() {
261
402
  failure_analysis: false,
262
403
  fast_lane: false,
263
404
  };
264
- // Pro 이상이면 Lighthouse 3 실행 (더 정밀)
405
+ // Pro ?댁긽?대㈃ Lighthouse 3???ㅽ뻾 (???뺣?)
265
406
  if (features.lighthouse_runs_3 && config.lighthouse_runs < 3) {
266
407
  config = { ...config, lighthouse_runs: 3 };
267
408
  }
268
409
  // Phase 2: Dev server + Lighthouse (only if build succeeded and not skipped)
269
410
  let multiViewportScores = null;
270
411
  let allViewportsOk = false;
412
+ let e2eResult;
413
+ let visualDiffResult = null;
271
414
  if (buildResult.success && !args.skipLighthouse) {
272
415
  let servePid;
273
416
  try {
274
- const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout);
417
+ const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
275
418
  servePid = serve.pid;
419
+ const verifyUrl = `http://127.0.0.1:${port}/`;
420
+ const verificationTier = (0, index_js_1.planToVerificationTier)(features.plan);
276
421
  try {
277
422
  const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
278
423
  scores = lhResult.scores ?? undefined;
@@ -289,9 +434,9 @@ async function run() {
289
434
  catch (lhErr) {
290
435
  console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
291
436
  }
292
- // ── Pro+: 멀티 뷰포트 (dev server 돌리는 중) ─────────────────────
437
+ // ?€?€ Pro+: 硫€??酉고룷??(dev server媛€ ?뚮━??以? ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€
293
438
  if (args.multiViewport && !features.multi_viewport) {
294
- console.log("\n ⚠️ --multi-viewportPro+ 플랜 전용입니다. laxy-verify login 으로 로그인하세요.");
439
+ console.log("\n ?좑툘 --multi-viewport??Pro+ ?뚮옖 ?꾩슜?낅땲?? laxy-verify login ?쇰줈 濡쒓렇?명븯?몄슂.");
295
440
  }
296
441
  else if (features.multi_viewport) {
297
442
  try {
@@ -303,6 +448,26 @@ async function run() {
303
448
  console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
304
449
  }
305
450
  }
451
+ try {
452
+ const e2eRuns = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier);
453
+ e2eResult = {
454
+ passed: e2eRuns.passed,
455
+ failed: e2eRuns.failed,
456
+ total: e2eRuns.results.length,
457
+ results: e2eRuns.results,
458
+ };
459
+ }
460
+ catch (e2eErr) {
461
+ console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
462
+ }
463
+ if (verificationTier === "pro_plus") {
464
+ try {
465
+ visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
466
+ }
467
+ catch (visualErr) {
468
+ console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
469
+ }
470
+ }
306
471
  }
307
472
  catch (serveErr) {
308
473
  console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
@@ -313,29 +478,74 @@ async function run() {
313
478
  }
314
479
  }
315
480
  }
316
- // Calculate grade
317
- const gradeResult = (0, grade_js_1.calculateGrade)({
481
+ const verificationTier = (0, index_js_1.planToVerificationTier)(features.plan);
482
+ const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
483
+ const failureEvidence = [
484
+ ...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`),
485
+ ...(e2eResult
486
+ ? e2eResult.results
487
+ .filter((scenario) => !scenario.passed)
488
+ .slice(0, 2)
489
+ .map((scenario) => `E2E: ${scenario.name}${scenario.error ? ` - ${scenario.error}` : ""}`)
490
+ : []),
491
+ ...(viewportSummary.count > 0 && viewportSummary.summary
492
+ ? [`Viewport: ${viewportSummary.summary}`]
493
+ : []),
494
+ ...(visualDiffResult
495
+ ? [
496
+ visualDiffResult.hasBaseline
497
+ ? `Visual diff: ${visualDiffResult.diffPercentage}% (${visualDiffResult.verdict})`
498
+ : "Visual diff: baseline seeded",
499
+ ]
500
+ : []),
501
+ ];
502
+ const verificationReport = (0, index_js_1.buildVerificationReport)({
318
503
  buildSuccess: buildResult.success,
319
- scores,
504
+ buildErrors: buildResult.errors,
505
+ e2ePassed: e2eResult?.passed,
506
+ e2eTotal: e2eResult?.total,
507
+ viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
508
+ multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
509
+ multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
510
+ visualDiffVerdict: visualDiffResult?.verdict,
511
+ visualDiffPercentage: visualDiffResult?.diffPercentage,
512
+ hasVisualBaseline: visualDiffResult?.hasBaseline,
513
+ lighthouseScores: scores,
514
+ failureEvidence,
515
+ }, {
516
+ tier: verificationTier,
320
517
  thresholds: adjustedThresholds,
321
- failOn: config.fail_on,
322
- goldEligible: features.gold_grade && allViewportsOk,
323
518
  });
519
+ const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
520
+ const unifiedGrade = verificationReport.grade;
521
+ const exitCode = config.fail_on === "unverified"
522
+ ? 0
523
+ : (0, grade_js_1.isWorseOrEqual)(unifiedGrade, config.fail_on)
524
+ ? 1
525
+ : 0;
324
526
  // Build result object
325
527
  const resultObj = {
326
- grade: gradeResult.grade.charAt(0).toUpperCase() + gradeResult.grade.slice(1), // Capitalize
528
+ grade: unifiedGrade.charAt(0).toUpperCase() + unifiedGrade.slice(1),
327
529
  timestamp: new Date().toISOString(),
328
530
  build: {
329
531
  success: buildResult.success,
330
532
  durationMs: buildResult.durationMs,
331
533
  errors: buildResult.errors,
332
534
  },
535
+ e2e: e2eResult,
333
536
  lighthouse: lighthouseResult,
537
+ visualDiff: visualDiffResult,
334
538
  thresholds: adjustedThresholds,
335
539
  ciMode: config.ciMode,
336
540
  framework: detected.framework,
337
- exitCode: gradeResult.exitCode,
541
+ exitCode,
338
542
  config_fail_on: config.fail_on,
543
+ _plan: features.plan,
544
+ verification: {
545
+ tier: verificationTier,
546
+ report: verificationReport,
547
+ view: verificationView,
548
+ },
339
549
  };
340
550
  // GitHub integration (only in Actions)
341
551
  const inGitHubActions = !!process.env.GITHUB_ACTIONS;
@@ -364,9 +574,9 @@ async function run() {
364
574
  if (inGitHubActions && process.env.GITHUB_OUTPUT) {
365
575
  fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
366
576
  }
367
- process.exit(gradeResult.exitCode);
577
+ exitGracefully(exitCode);
368
578
  }
369
579
  run().catch((err) => {
370
580
  console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
371
- process.exit(1);
581
+ exitGracefully(1);
372
582
  });
package/dist/e2e.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { VerificationTier } from "./verification-core/types.js";
2
+ export interface E2EStep {
3
+ type: "click" | "fill" | "check_visible" | "wait" | "scroll" | "clear_fill";
4
+ selector?: string;
5
+ value?: string;
6
+ duration?: number;
7
+ description: string;
8
+ }
9
+ export interface E2EScenario {
10
+ name: string;
11
+ steps: E2EStep[];
12
+ }
13
+ export interface E2EStepResult {
14
+ description: string;
15
+ passed: boolean;
16
+ error?: string;
17
+ }
18
+ export interface E2EScenarioResult {
19
+ name: string;
20
+ passed: boolean;
21
+ steps: E2EStepResult[];
22
+ error?: string;
23
+ }
24
+ export declare function runVerifyE2E(url: string, tier: VerificationTier): Promise<{
25
+ scenarios: E2EScenario[];
26
+ results: E2EScenarioResult[];
27
+ passed: number;
28
+ failed: number;
29
+ }>;