laxy-verify 1.1.7 → 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 +14 -4
- package/dist/build.d.ts +1 -1
- package/dist/build.js +7 -3
- package/dist/cli.js +190 -30
- package/dist/serve.d.ts +2 -1
- package/dist/serve.js +8 -3
- package/dist/verification-core/index.d.ts +3 -0
- package/dist/verification-core/index.js +19 -0
- package/dist/verification-core/report.d.ts +14 -0
- package/dist/verification-core/report.js +273 -0
- package/dist/verification-core/tier-policy.d.ts +13 -0
- package/dist/verification-core/tier-policy.js +71 -0
- package/dist/verification-core/types.d.ts +82 -0
- package/dist/verification-core/types.js +2 -0
- package/package.json +1 -1
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
|
|
7
|
-
npx laxy-verify
|
|
8
|
-
npx laxy-verify
|
|
9
|
-
npx laxy-verify
|
|
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, {
|
|
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
|
-
//
|
|
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
|
-
//
|
|
111
|
+
// 泥?踰덉㎏ non-flag瑜??대찓?쇰줈 痍④툒
|
|
94
112
|
subcommandArg = flags["email"];
|
|
95
113
|
if (!subcommandArg) {
|
|
96
|
-
// login PSM@example.com
|
|
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
|
|
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
|
-
|
|
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,45 +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
|
-
// Windows: fetch()
|
|
158
|
-
// UV_HANDLE_CLOSING Assertion
|
|
159
|
-
|
|
277
|
+
// Windows: fetch()??TCP ?뚯폆??closing 以묒뿉 process.exit()??利됱떆 ?몄텧?섎㈃
|
|
278
|
+
// UV_HANDLE_CLOSING Assertion 諛쒖깮. setTimeout?쇰줈 UV 猷⑦봽???뺣━ ?쒓컙??以??
|
|
279
|
+
exitGracefully(0);
|
|
160
280
|
return;
|
|
161
281
|
}
|
|
162
282
|
if (args.subcommand === "logout") {
|
|
163
283
|
(0, auth_js_1.clearToken)();
|
|
164
|
-
|
|
284
|
+
exitGracefully(0);
|
|
165
285
|
return;
|
|
166
286
|
}
|
|
167
287
|
if (args.subcommand === "whoami") {
|
|
168
288
|
(0, auth_js_1.whoami)();
|
|
169
|
-
|
|
289
|
+
exitGracefully(0);
|
|
170
290
|
return;
|
|
171
291
|
}
|
|
172
292
|
// --init
|
|
173
293
|
if (args.init) {
|
|
174
294
|
(0, init_js_1.runInit)(args.projectDir);
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
177
302
|
}
|
|
178
303
|
// --badge
|
|
179
304
|
if (args.badge) {
|
|
180
305
|
const resultPath = path.join(args.projectDir, ".laxy-result.json");
|
|
181
306
|
if (!fs.existsSync(resultPath)) {
|
|
182
307
|
console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
|
|
183
|
-
|
|
308
|
+
exitGracefully(2);
|
|
184
309
|
return;
|
|
185
310
|
}
|
|
186
311
|
const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
187
312
|
const badge = (0, badge_js_1.generateBadge)(content.grade);
|
|
188
313
|
console.log(badge);
|
|
189
|
-
|
|
314
|
+
exitGracefully(0);
|
|
190
315
|
return;
|
|
191
316
|
}
|
|
192
317
|
// Load config
|
|
@@ -204,7 +329,7 @@ async function run() {
|
|
|
204
329
|
}
|
|
205
330
|
catch (err) {
|
|
206
331
|
console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
|
|
207
|
-
|
|
332
|
+
exitGracefully(2);
|
|
208
333
|
return;
|
|
209
334
|
}
|
|
210
335
|
// Auto-detect framework + package manager
|
|
@@ -214,17 +339,25 @@ async function run() {
|
|
|
214
339
|
}
|
|
215
340
|
catch (err) {
|
|
216
341
|
console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
|
|
217
|
-
|
|
342
|
+
exitGracefully(2);
|
|
218
343
|
return;
|
|
219
344
|
}
|
|
220
345
|
// Merge config overrides
|
|
221
346
|
const buildCmd = config.build_command || detected.buildCmd;
|
|
222
347
|
const devCmd = config.dev_command || detected.devCmd;
|
|
223
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
|
+
}
|
|
224
357
|
// Phase 1: Build
|
|
225
358
|
let buildResult;
|
|
226
359
|
try {
|
|
227
|
-
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);
|
|
228
361
|
}
|
|
229
362
|
catch (err) {
|
|
230
363
|
buildResult = {
|
|
@@ -243,14 +376,14 @@ async function run() {
|
|
|
243
376
|
seo: config.thresholds.seo,
|
|
244
377
|
bestPractices: config.thresholds.bestPractices,
|
|
245
378
|
};
|
|
246
|
-
//
|
|
379
|
+
// ?? ?뚮옖 湲곕뒫 議고쉶 (?좏겙 ?놁쑝硫?Free ?대갚) ??????????????????????????????
|
|
247
380
|
let entitlements = null;
|
|
248
381
|
try {
|
|
249
382
|
entitlements = await (0, entitlement_js_1.getEntitlements)();
|
|
250
383
|
(0, entitlement_js_1.printPlanBanner)(entitlements);
|
|
251
384
|
}
|
|
252
385
|
catch {
|
|
253
|
-
//
|
|
386
|
+
// ?ㅽ듃?뚰겕 ?ㅻ쪟 ??Free濡?吏꾪뻾
|
|
254
387
|
}
|
|
255
388
|
const features = entitlements ?? {
|
|
256
389
|
plan: "free",
|
|
@@ -261,7 +394,7 @@ async function run() {
|
|
|
261
394
|
failure_analysis: false,
|
|
262
395
|
fast_lane: false,
|
|
263
396
|
};
|
|
264
|
-
// Pro
|
|
397
|
+
// Pro ?댁긽?대㈃ Lighthouse 3???ㅽ뻾 (???뺣?)
|
|
265
398
|
if (features.lighthouse_runs_3 && config.lighthouse_runs < 3) {
|
|
266
399
|
config = { ...config, lighthouse_runs: 3 };
|
|
267
400
|
}
|
|
@@ -271,7 +404,7 @@ async function run() {
|
|
|
271
404
|
if (buildResult.success && !args.skipLighthouse) {
|
|
272
405
|
let servePid;
|
|
273
406
|
try {
|
|
274
|
-
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);
|
|
275
408
|
servePid = serve.pid;
|
|
276
409
|
try {
|
|
277
410
|
const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
|
|
@@ -289,9 +422,9 @@ async function run() {
|
|
|
289
422
|
catch (lhErr) {
|
|
290
423
|
console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
|
|
291
424
|
}
|
|
292
|
-
//
|
|
425
|
+
// ?? Pro+: 硫??酉고룷??(dev server媛 ?뚮━??以? ?????????????????????
|
|
293
426
|
if (args.multiViewport && !features.multi_viewport) {
|
|
294
|
-
console.log("\n
|
|
427
|
+
console.log("\n ?좑툘 --multi-viewport??Pro+ ?뚮옖 ?꾩슜?낅땲?? laxy-verify login ?쇰줈 濡쒓렇?명븯?몄슂.");
|
|
295
428
|
}
|
|
296
429
|
else if (features.multi_viewport) {
|
|
297
430
|
try {
|
|
@@ -321,6 +454,27 @@ async function run() {
|
|
|
321
454
|
failOn: config.fail_on,
|
|
322
455
|
goldEligible: features.gold_grade && allViewportsOk,
|
|
323
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);
|
|
324
478
|
// Build result object
|
|
325
479
|
const resultObj = {
|
|
326
480
|
grade: gradeResult.grade.charAt(0).toUpperCase() + gradeResult.grade.slice(1), // Capitalize
|
|
@@ -336,6 +490,12 @@ async function run() {
|
|
|
336
490
|
framework: detected.framework,
|
|
337
491
|
exitCode: gradeResult.exitCode,
|
|
338
492
|
config_fail_on: config.fail_on,
|
|
493
|
+
_plan: features.plan,
|
|
494
|
+
verification: {
|
|
495
|
+
tier: verificationTier,
|
|
496
|
+
report: verificationReport,
|
|
497
|
+
view: verificationView,
|
|
498
|
+
},
|
|
339
499
|
};
|
|
340
500
|
// GitHub integration (only in Actions)
|
|
341
501
|
const inGitHubActions = !!process.env.GITHUB_ACTIONS;
|
|
@@ -364,9 +524,9 @@ async function run() {
|
|
|
364
524
|
if (inGitHubActions && process.env.GITHUB_OUTPUT) {
|
|
365
525
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
|
|
366
526
|
}
|
|
367
|
-
|
|
527
|
+
exitGracefully(gradeResult.exitCode);
|
|
368
528
|
}
|
|
369
529
|
run().catch((err) => {
|
|
370
530
|
console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
371
|
-
|
|
531
|
+
exitGracefully(1);
|
|
372
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
|
|
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
|
-
|
|
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
|
|
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,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
|
+
}
|