laxy-verify 1.3.3 → 1.4.1

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.
@@ -0,0 +1,66 @@
1
+ export interface AutoFixContext {
2
+ grade: string;
3
+ blockers: Array<{
4
+ title: string;
5
+ severity: string;
6
+ action: string;
7
+ }>;
8
+ lighthouseScores: {
9
+ performance: number;
10
+ accessibility: number;
11
+ seo: number;
12
+ bestPractices: number;
13
+ } | null;
14
+ thresholds: {
15
+ performance: number;
16
+ accessibility: number;
17
+ seo: number;
18
+ bestPractices: number;
19
+ };
20
+ buildErrors: string[];
21
+ e2eFailed: number;
22
+ e2eTotal: number;
23
+ securitySummary?: string;
24
+ /** Source file snippets keyed by relative path */
25
+ fileSnippets: Array<{
26
+ path: string;
27
+ content: string;
28
+ }>;
29
+ }
30
+ export interface CodeFix {
31
+ /** Relative file path to apply the fix to */
32
+ filePath: string;
33
+ /** Description of what this fix does */
34
+ description: string;
35
+ /** The complete replacement content for the file (or section) */
36
+ code: string;
37
+ /** Whether this is a full-file replacement or a targeted patch */
38
+ type: "full" | "patch";
39
+ /** Line range for patch-type fixes */
40
+ startLine?: number;
41
+ endLine?: number;
42
+ }
43
+ export interface AutoFixResult {
44
+ rootCause: string;
45
+ fixes: CodeFix[];
46
+ confidence: "high" | "medium" | "low";
47
+ }
48
+ /**
49
+ * Collect relevant source file snippets for AI context.
50
+ * Picks up to 5 files that are most likely related to the failures.
51
+ */
52
+ export declare function collectFileSnippets(projectDir: string, buildErrors: string[]): Array<{
53
+ path: string;
54
+ content: string;
55
+ }>;
56
+ /**
57
+ * Apply a code fix to the project directory.
58
+ */
59
+ export declare function applyCodeFix(projectDir: string, fix: CodeFix): {
60
+ success: boolean;
61
+ error?: string;
62
+ };
63
+ /**
64
+ * Request AI auto-fix from the Laxy API.
65
+ */
66
+ export declare function requestAutoFix(context: AutoFixContext): Promise<AutoFixResult | null>;
@@ -0,0 +1,178 @@
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.collectFileSnippets = collectFileSnippets;
37
+ exports.applyCodeFix = applyCodeFix;
38
+ exports.requestAutoFix = requestAutoFix;
39
+ /**
40
+ * AI Auto-Fix client (Pro feature).
41
+ *
42
+ * Sends verification failure context plus relevant source files
43
+ * to the Laxy API, which returns code fix suggestions that can
44
+ * be applied directly or pushed as a PR.
45
+ */
46
+ const fs = __importStar(require("node:fs"));
47
+ const path = __importStar(require("node:path"));
48
+ const auth_js_1 = require("./auth.js");
49
+ /**
50
+ * Collect relevant source file snippets for AI context.
51
+ * Picks up to 5 files that are most likely related to the failures.
52
+ */
53
+ function collectFileSnippets(projectDir, buildErrors) {
54
+ const snippets = [];
55
+ const seen = new Set();
56
+ // Extract file paths from build errors
57
+ const errorFilePattern = /(?:^|\n)(\.?[^\s:]+(?:\/[^\s:]+)*\.[a-zA-Z]+)(?::|\s)/g;
58
+ const errorFiles = new Set();
59
+ for (const err of buildErrors) {
60
+ let match;
61
+ const localPattern = new RegExp(errorFilePattern.source, "g");
62
+ while ((match = localPattern.exec(err)) !== null) {
63
+ errorFiles.add(match[1]);
64
+ }
65
+ }
66
+ // Add files from build errors first
67
+ for (const filePath of errorFiles) {
68
+ if (seen.has(filePath) || snippets.length >= 5)
69
+ continue;
70
+ const absPath = path.join(projectDir, filePath);
71
+ if (fs.existsSync(absPath)) {
72
+ try {
73
+ const content = fs.readFileSync(absPath, "utf-8");
74
+ if (content.length <= 20000) {
75
+ snippets.push({ path: filePath, content: content.slice(0, 4000) });
76
+ seen.add(filePath);
77
+ }
78
+ }
79
+ catch { /* ignore */ }
80
+ }
81
+ }
82
+ // Fill remaining slots with common config/entry files
83
+ const commonFiles = [
84
+ "package.json",
85
+ "tsconfig.json",
86
+ "vite.config.ts",
87
+ "vite.config.js",
88
+ "next.config.js",
89
+ "next.config.mjs",
90
+ "src/App.tsx",
91
+ "src/App.jsx",
92
+ "src/main.tsx",
93
+ "src/main.jsx",
94
+ "src/pages/index.tsx",
95
+ "src/pages/_app.tsx",
96
+ "app/page.tsx",
97
+ "app/layout.tsx",
98
+ "index.html",
99
+ ];
100
+ for (const filePath of commonFiles) {
101
+ if (seen.has(filePath) || snippets.length >= 5)
102
+ continue;
103
+ const absPath = path.join(projectDir, filePath);
104
+ if (fs.existsSync(absPath)) {
105
+ try {
106
+ const content = fs.readFileSync(absPath, "utf-8");
107
+ if (content.length <= 20000) {
108
+ snippets.push({ path: filePath, content: content.slice(0, 4000) });
109
+ seen.add(filePath);
110
+ }
111
+ }
112
+ catch { /* ignore */ }
113
+ }
114
+ }
115
+ return snippets;
116
+ }
117
+ /**
118
+ * Apply a code fix to the project directory.
119
+ */
120
+ function applyCodeFix(projectDir, fix) {
121
+ const absPath = path.resolve(projectDir, fix.filePath);
122
+ // Prevent path traversal outside the project directory (case-insensitive on Windows)
123
+ const resolvedRoot = path.resolve(projectDir);
124
+ const relative = path.relative(resolvedRoot, absPath);
125
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
126
+ return { success: false, error: `Path traversal detected: ${fix.filePath}` };
127
+ }
128
+ if (fix.type === "full") {
129
+ try {
130
+ fs.writeFileSync(absPath, fix.code, "utf-8");
131
+ return { success: true };
132
+ }
133
+ catch (err) {
134
+ return { success: false, error: err instanceof Error ? err.message : "Write failed" };
135
+ }
136
+ }
137
+ // Patch type: replace specific lines
138
+ if (!fs.existsSync(absPath)) {
139
+ return { success: false, error: `File not found: ${fix.filePath}` };
140
+ }
141
+ try {
142
+ const lines = fs.readFileSync(absPath, "utf-8").split("\n");
143
+ const start = (fix.startLine ?? 1) - 1;
144
+ const end = fix.endLine ?? lines.length;
145
+ const newLines = fix.code.split("\n");
146
+ lines.splice(start, end - start, ...newLines);
147
+ fs.writeFileSync(absPath, lines.join("\n"), "utf-8");
148
+ return { success: true };
149
+ }
150
+ catch (err) {
151
+ return { success: false, error: err instanceof Error ? err.message : "Patch failed" };
152
+ }
153
+ }
154
+ /**
155
+ * Request AI auto-fix from the Laxy API.
156
+ */
157
+ async function requestAutoFix(context) {
158
+ const token = (0, auth_js_1.loadToken)();
159
+ if (!token)
160
+ return null;
161
+ try {
162
+ const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/auto-fix`, {
163
+ method: "POST",
164
+ headers: {
165
+ Authorization: `Bearer ${token}`,
166
+ "Content-Type": "application/json",
167
+ },
168
+ body: JSON.stringify(context),
169
+ signal: AbortSignal.timeout(60_000),
170
+ });
171
+ if (!res.ok)
172
+ return null;
173
+ return (await res.json());
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
@@ -0,0 +1,58 @@
1
+ import type { TrendDelta } from "./trend.js";
2
+ interface CheckRunResult {
3
+ grade: string;
4
+ verdict: string;
5
+ exitCode: number;
6
+ lighthouse: {
7
+ performance: number;
8
+ accessibility: number;
9
+ seo: number;
10
+ bestPractices: number;
11
+ runs: number;
12
+ } | null;
13
+ thresholds: {
14
+ performance: number;
15
+ accessibility: number;
16
+ seo: number;
17
+ bestPractices: number;
18
+ };
19
+ e2e?: {
20
+ passed: number;
21
+ failed: number;
22
+ total: number;
23
+ } | null;
24
+ security?: {
25
+ critical: number;
26
+ high: number;
27
+ total: number;
28
+ } | null;
29
+ build?: {
30
+ success: boolean;
31
+ durationMs: number;
32
+ };
33
+ blockers: Array<{
34
+ title: string;
35
+ severity: string;
36
+ }>;
37
+ typecheck?: {
38
+ errorCount: number;
39
+ } | null;
40
+ secretScan?: {
41
+ findingsCount: number;
42
+ } | null;
43
+ bundleSize?: {
44
+ advisory: string;
45
+ } | null;
46
+ a11yDeep?: {
47
+ criticalCount: number;
48
+ seriousCount: number;
49
+ } | null;
50
+ brokenLinks?: {
51
+ broken: number;
52
+ checked: number;
53
+ } | null;
54
+ consoleErrors?: number;
55
+ config_fail_on: string;
56
+ }
57
+ export declare function createCheckRun(result: CheckRunResult, trend?: TrendDelta | null): Promise<void>;
58
+ export {};
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCheckRun = createCheckRun;
4
+ /**
5
+ * GitHub Check Run API integration (Pro feature).
6
+ *
7
+ * Creates a rich Check Run on the PR that appears in the merge box
8
+ * with expandable details: grade, blockers, Lighthouse scores, E2E,
9
+ * security, and verification verdict.
10
+ */
11
+ const github_js_1 = require("./github.js");
12
+ const trend_js_1 = require("./trend.js");
13
+ function renderCheckRunSummary(result, trend) {
14
+ const lines = [];
15
+ const emoji = result.grade === "Gold" ? "🏆" :
16
+ result.grade === "Silver" ? "✅" :
17
+ result.grade === "Bronze" ? "🔨" : "⚠️";
18
+ lines.push(`## ${emoji} ${result.grade} — ${result.verdict}`);
19
+ lines.push("");
20
+ // Trend table
21
+ if (trend) {
22
+ lines.push("### vs Base Branch");
23
+ lines.push("| Check | This PR | Base | Delta |");
24
+ lines.push("|---|---|---|---|");
25
+ const gradeUp = (0, trend_js_1.gradeIndex)(trend.grade.current) > (0, trend_js_1.gradeIndex)(trend.grade.base);
26
+ const gradeDown = (0, trend_js_1.gradeIndex)(trend.grade.current) < (0, trend_js_1.gradeIndex)(trend.grade.base);
27
+ const gradeDelta = gradeUp ? " ↑" : gradeDown ? " ↓" : "";
28
+ lines.push(`| Grade | **${trend.grade.current}** | ${trend.grade.base} |${gradeDelta} |`);
29
+ const fmtD = (d) => d === null ? "—" : d > 0 ? `+${d}` : d < 0 ? `${d}` : "—";
30
+ if (trend.performance.current !== null)
31
+ lines.push(`| Performance | ${trend.performance.current} | ${trend.performance.base ?? "—"} | ${fmtD(trend.performance.delta)} |`);
32
+ if (trend.accessibility.current !== null)
33
+ lines.push(`| Accessibility | ${trend.accessibility.current} | ${trend.accessibility.base ?? "—"} | ${fmtD(trend.accessibility.delta)} |`);
34
+ if (trend.seo.current !== null)
35
+ lines.push(`| SEO | ${trend.seo.current} | ${trend.seo.base ?? "—"} | ${fmtD(trend.seo.delta)} |`);
36
+ if (trend.bestPractices.current !== null)
37
+ lines.push(`| Best Practices | ${trend.bestPractices.current} | ${trend.bestPractices.base ?? "—"} | ${fmtD(trend.bestPractices.delta)} |`);
38
+ if (trend.e2e.current !== null)
39
+ lines.push(`| E2E | ${trend.e2e.current} | ${trend.e2e.base ?? "—"} | — |`);
40
+ lines.push("");
41
+ }
42
+ // Build
43
+ if (result.build) {
44
+ const dur = (result.build.durationMs / 1000).toFixed(1);
45
+ lines.push(`### Build: ${result.build.success ? "✅ Passed" : "❌ Failed"} (${dur}s)`);
46
+ lines.push("");
47
+ }
48
+ // Lighthouse
49
+ if (result.lighthouse) {
50
+ const lh = result.lighthouse;
51
+ const t = result.thresholds;
52
+ lines.push("### Lighthouse");
53
+ lines.push("| Category | Score | Threshold | Status |");
54
+ lines.push("|---|---|---|---|");
55
+ lines.push(`| Performance | ${lh.performance} | ≥${t.performance} | ${lh.performance >= t.performance ? "✅" : "❌"} |`);
56
+ lines.push(`| Accessibility | ${lh.accessibility} | ≥${t.accessibility} | ${lh.accessibility >= t.accessibility ? "✅" : "❌"} |`);
57
+ lines.push(`| SEO | ${lh.seo} | ≥${t.seo} | ${lh.seo >= t.seo ? "✅" : "❌"} |`);
58
+ lines.push(`| Best Practices | ${lh.bestPractices} | ≥${t.bestPractices} | ${lh.bestPractices >= t.bestPractices ? "✅" : "❌"} |`);
59
+ lines.push("");
60
+ }
61
+ // E2E
62
+ if (result.e2e && result.e2e.total > 0) {
63
+ const e2e = result.e2e;
64
+ lines.push(`### E2E: ${e2e.failed === 0 ? "✅" : "❌"} ${e2e.passed}/${e2e.total} passed`);
65
+ lines.push("");
66
+ }
67
+ // Blockers
68
+ if (result.blockers.length > 0) {
69
+ lines.push("### Blockers");
70
+ for (const b of result.blockers.slice(0, 8)) {
71
+ lines.push(`- **[${b.severity}]** ${b.title}`);
72
+ }
73
+ lines.push("");
74
+ }
75
+ // Extra checks
76
+ const extras = [];
77
+ if (result.security && result.security.total > 0)
78
+ extras.push(`Security: ${result.security.critical} critical, ${result.security.high} high`);
79
+ if (result.typecheck && result.typecheck.errorCount > 0)
80
+ extras.push(`TypeScript: ${result.typecheck.errorCount} errors`);
81
+ if (result.secretScan && result.secretScan.findingsCount > 0)
82
+ extras.push(`Secrets: ${result.secretScan.findingsCount} findings`);
83
+ if (result.a11yDeep && (result.a11yDeep.criticalCount > 0 || result.a11yDeep.seriousCount > 0))
84
+ extras.push(`WCAG: ${result.a11yDeep.criticalCount} critical, ${result.a11yDeep.seriousCount} serious`);
85
+ if (result.brokenLinks && result.brokenLinks.broken > 0)
86
+ extras.push(`Broken links: ${result.brokenLinks.broken}/${result.brokenLinks.checked}`);
87
+ if (result.consoleErrors && result.consoleErrors > 0)
88
+ extras.push(`Console errors: ${result.consoleErrors}`);
89
+ if (extras.length > 0) {
90
+ lines.push("### Additional findings");
91
+ for (const e of extras)
92
+ lines.push(`- ${e}`);
93
+ lines.push("");
94
+ }
95
+ lines.push(`**Fail-on threshold**: ${result.config_fail_on ?? "bronze"}`);
96
+ lines.push("");
97
+ lines.push("---");
98
+ lines.push("[laxy-verify docs](https://github.com/SUNgm24/Laxy/tree/main/laxy-verify)");
99
+ return lines.join("\n");
100
+ }
101
+ async function createCheckRun(result, trend) {
102
+ const ctx = (0, github_js_1.getGitHubContext)();
103
+ if (!ctx)
104
+ return;
105
+ const [owner, repo] = ctx.repository.split("/");
106
+ const conclusion = result.exitCode === 0 ? "success" : "failure";
107
+ const title = result.exitCode === 0
108
+ ? `Laxy Verify: ${result.grade} — no blockers`
109
+ : `Laxy Verify: ${result.grade} — ${result.blockers.length} blocker(s)`;
110
+ const summary = renderCheckRunSummary(result, trend);
111
+ // GitHub Check Runs API
112
+ const url = `https://api.github.com/repos/${owner}/${repo}/check-runs`;
113
+ try {
114
+ const res = await fetch(url, {
115
+ method: "POST",
116
+ headers: {
117
+ Authorization: `Bearer ${ctx.token}`,
118
+ Accept: "application/vnd.github.v3+json",
119
+ "Content-Type": "application/json",
120
+ },
121
+ body: JSON.stringify({
122
+ name: "laxy-verify",
123
+ head_sha: ctx.sha,
124
+ status: "completed",
125
+ conclusion,
126
+ output: {
127
+ title,
128
+ summary,
129
+ },
130
+ }),
131
+ });
132
+ if (!res.ok) {
133
+ console.warn(`GitHub Check Run API returned ${res.status}; falling back to status check.`);
134
+ }
135
+ }
136
+ catch (err) {
137
+ console.warn(`GitHub Check Run request failed: ${err instanceof Error ? err.message : String(err)}`);
138
+ }
139
+ }