@elmoorx/code-review 2.0.0-alpha.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/package.json +34 -0
  4. package/src/index.ts +605 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wafra Framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @wafra/code-review
2
+
3
+ > AI code review: PR analysis, security scan, performance suggestions
4
+
5
+ Part of the [Wafra Framework](https://github.com/wafra/framework) — Build fast. Run anywhere. Stay secure.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @wafra/code-review
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { /* exports */ } from '@wafra/code-review';
17
+ ```
18
+
19
+ ## Features
20
+
21
+ - Zero external dependencies
22
+ - Full TypeScript support
23
+ - Tree-shakeable
24
+ - Edge-runtime compatible
25
+ - Arabic/RTL friendly
26
+
27
+ ## Documentation
28
+
29
+ See [https://wafra.dev/docs/code-review](https://wafra.dev/docs/code-review)
30
+
31
+ ## License
32
+
33
+ MIT © Wafra Framework
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@elmoorx/code-review",
3
+ "version": "2.0.0-alpha.25",
4
+ "description": "AI code review: PR analysis, security scan, performance suggestions",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "scripts": {
9
+ "build": "tsc"
10
+ },
11
+ "dependencies": {
12
+ "@elmoorx/runtime": "^1.0.0"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Wafra Framework",
16
+ "homepage": "https://wafra.dev/packages/code-review",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/wafra/framework",
20
+ "directory": "packages/code-review"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/wafra/framework/issues"
24
+ },
25
+ "keywords": [
26
+ "wafra",
27
+ "framework",
28
+ "code-review"
29
+ ],
30
+ "sideEffects": false,
31
+ "exports": {
32
+ ".": "./src/index.ts"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,605 @@
1
+ /**
2
+ * @wafra/code-review — AI Code Review System
3
+ * ============================================
4
+ * AI-powered code review that finds bugs, security issues,
5
+ * and suggests improvements — before you deploy.
6
+ *
7
+ * import { reviewCode } from "@wafra/code-review";
8
+ * const report = await reviewCode(myComponentCode);
9
+ * // → { score: 92, issues: [...], suggestions: [...] }
10
+ */
11
+
12
+ import { $state, type WafraNode } from "@wafra/runtime";
13
+
14
+ // ============ REVIEW TYPES ============
15
+
16
+ export interface CodeReviewReport {
17
+ score: number;
18
+ issues: CodeIssue[];
19
+ suggestions: CodeSuggestion[];
20
+ metrics: CodeMetrics;
21
+ summary: string;
22
+ }
23
+
24
+ export interface CodeIssue {
25
+ id: string;
26
+ severity: "critical" | "warning" | "info" | "style";
27
+ category: "bug" | "security" | "performance" | "accessibility" | "best-practice" | "typescript";
28
+ title: string;
29
+ description: string;
30
+ line?: number;
31
+ fix?: string;
32
+ }
33
+
34
+ export interface CodeSuggestion {
35
+ id: string;
36
+ type: "refactor" | "optimize" | "modernize" | "simplify";
37
+ title: string;
38
+ description: string;
39
+ before: string;
40
+ after: string;
41
+ impact: "low" | "medium" | "high";
42
+ }
43
+
44
+ export interface CodeMetrics {
45
+ lines: number;
46
+ complexity: number;
47
+ maintainability: number;
48
+ readability: number;
49
+ testability: number;
50
+ bundleImpact: number;
51
+ }
52
+
53
+ // ============ CODE REVIEWER ============
54
+
55
+ class CodeReviewer {
56
+ async review(code: string): Promise<CodeReviewReport> {
57
+ const issues: CodeIssue[] = [];
58
+ const suggestions: CodeSuggestion[] = [];
59
+
60
+ // === SECURITY CHECKS ===
61
+ issues.push(...this.checkSecurity(code));
62
+
63
+ // === BUG CHECKS ===
64
+ issues.push(...this.checkBugs(code));
65
+
66
+ // === PERFORMANCE CHECKS ===
67
+ issues.push(...this.checkPerformance(code));
68
+
69
+ // === ACCESSIBILITY CHECKS ===
70
+ issues.push(...this.checkAccessibility(code));
71
+
72
+ // === BEST PRACTICES ===
73
+ issues.push(...this.checkBestPractices(code));
74
+
75
+ // === SUGGESTIONS ===
76
+ suggestions.push(...this.generateSuggestions(code));
77
+
78
+ // === METRICS ===
79
+ const metrics = this.calculateMetrics(code);
80
+
81
+ // === SCORE ===
82
+ const score = this.calculateScore(issues, metrics);
83
+
84
+ // === SUMMARY ===
85
+ const summary = this.generateSummary(score, issues, metrics);
86
+
87
+ return { score, issues, suggestions, metrics, summary };
88
+ }
89
+
90
+ // ============ SECURITY CHECKS ============
91
+
92
+ private checkSecurity(code: string): CodeIssue[] {
93
+ const issues: CodeIssue[] = [];
94
+
95
+ // dangerouslySetInnerHTML
96
+ if (code.includes("dangerouslySetInnerHTML")) {
97
+ issues.push({
98
+ id: "sec-xss-dangerous",
99
+ severity: "critical",
100
+ category: "security",
101
+ title: "XSS vulnerability: dangerouslySetInnerHTML",
102
+ description: "Using dangerouslySetInnerHTML allows XSS attacks. Use $html() from @wafra/runtime which auto-sanitizes.",
103
+ fix: 'Replace: dangerouslySetInnerHTML={{ __html: x }}\nWith: $html(x)',
104
+ });
105
+ }
106
+
107
+ // v-html
108
+ if (code.includes("v-html")) {
109
+ issues.push({
110
+ id: "sec-xss-vhtml",
111
+ severity: "critical",
112
+ category: "security",
113
+ title: "XSS vulnerability: v-html",
114
+ description: "Using v-html allows XSS attacks. Use $html() instead.",
115
+ });
116
+ }
117
+
118
+ // innerHTML
119
+ if (code.includes(".innerHTML") && !code.includes("$html")) {
120
+ issues.push({
121
+ id: "sec-xss-innerhtml",
122
+ severity: "warning",
123
+ category: "security",
124
+ title: "Potential XSS: innerHTML",
125
+ description: "Direct innerHTML assignment can cause XSS. Consider using $html() for auto-sanitization.",
126
+ });
127
+ }
128
+
129
+ // eval
130
+ if (code.includes("eval(")) {
131
+ issues.push({
132
+ id: "sec-eval",
133
+ severity: "critical",
134
+ category: "security",
135
+ title: "Security risk: eval()",
136
+ description: "eval() allows arbitrary code execution. Remove it and use safer alternatives.",
137
+ });
138
+ }
139
+
140
+ // Hardcoded secrets
141
+ if (/api[_-]?key\s*=\s*["'][a-zA-Z0-9]{20,}["']/i.test(code)) {
142
+ issues.push({
143
+ id: "sec-hardcoded-key",
144
+ severity: "critical",
145
+ category: "security",
146
+ title: "Hardcoded API key detected",
147
+ description: "Never hardcode API keys in source code. Use environment variables.",
148
+ fix: 'Use: const apiKey = process.env.API_KEY;',
149
+ });
150
+ }
151
+
152
+ // HTTP (not HTTPS)
153
+ if (/http:\/\/(?!localhost|127\.0\.0\.1)/.test(code)) {
154
+ issues.push({
155
+ id: "sec-http",
156
+ severity: "warning",
157
+ category: "security",
158
+ title: "Insecure HTTP URL",
159
+ description: "Use HTTPS for all external URLs to prevent man-in-the-middle attacks.",
160
+ });
161
+ }
162
+
163
+ return issues;
164
+ }
165
+
166
+ // ============ BUG CHECKS ============
167
+
168
+ private checkBugs(code: string): CodeIssue[] {
169
+ const issues: CodeIssue[] = [];
170
+
171
+ // Missing key in list
172
+ if (code.includes(".map(") && !code.includes("key=") && !code.includes("key:")) {
173
+ issues.push({
174
+ id: "bug-missing-key",
175
+ severity: "warning",
176
+ category: "bug",
177
+ title: "Missing key prop in list",
178
+ description: "When rendering lists with .map(), each item should have a stable key prop for optimal rendering.",
179
+ fix: 'items.map(item => h("li", { key: item.id }, item.name))',
180
+ });
181
+ }
182
+
183
+ // useEffect without cleanup
184
+ if (code.includes("$effect(") || code.includes("useEffect(")) {
185
+ if (code.includes("setInterval") && !code.includes("clearInterval")) {
186
+ issues.push({
187
+ id: "bug-interval-leak",
188
+ severity: "warning",
189
+ category: "bug",
190
+ title: "Memory leak: interval without cleanup",
191
+ description: "setInterval inside $effect should return a cleanup function that clears the interval.",
192
+ fix: '$effect(() => {\n const id = setInterval(tick, 1000);\n return () => clearInterval(id);\n});',
193
+ });
194
+ }
195
+ if (code.includes("addEventListener") && !code.includes("removeEventListener")) {
196
+ issues.push({
197
+ id: "bug-listener-leak",
198
+ severity: "warning",
199
+ category: "bug",
200
+ title: "Memory leak: event listener without cleanup",
201
+ description: "addEventListener should be paired with removeEventListener in cleanup.",
202
+ });
203
+ }
204
+ }
205
+
206
+ // State update on unmounted component
207
+ if (code.includes("setTimeout") && code.includes(".set(") && !code.includes("onCleanup")) {
208
+ issues.push({
209
+ id: "bug-timeout-leak",
210
+ severity: "info",
211
+ category: "bug",
212
+ title: "Potential state update after unmount",
213
+ description: "setTimeout callbacks may fire after component unmounts. Clear timeouts in onCleanup.",
214
+ });
215
+ }
216
+
217
+ // Empty dependency array with external vars
218
+ if (/\$effect\(\(\)\s*=>\s*\{[^}]+\},\s*\[\]\)/.test(code)) {
219
+ issues.push({
220
+ id: "bug-empty-deps",
221
+ severity: "info",
222
+ category: "bug",
223
+ title: "Effect with empty dependencies",
224
+ description: "This effect has empty deps but may reference external variables. Verify this is intentional.",
225
+ });
226
+ }
227
+
228
+ return issues;
229
+ }
230
+
231
+ // ============ PERFORMANCE CHECKS ============
232
+
233
+ private checkPerformance(code: string): CodeIssue[] {
234
+ const issues: CodeIssue[] = [];
235
+
236
+ // Large inline objects
237
+ const styleMatches = code.match(/style\s*[:=]\s*\{[^}]{200,}\}/g);
238
+ if (styleMatches) {
239
+ issues.push({
240
+ id: "perf-large-style",
241
+ severity: "info",
242
+ category: "performance",
243
+ title: "Large inline style object",
244
+ description: "Consider extracting large style objects to a constant or using CSS classes.",
245
+ });
246
+ }
247
+
248
+ // Missing memo
249
+ if (code.includes(".map(") && code.includes("filter(") && !code.includes("memo(") && !code.includes("useMemo")) {
250
+ issues.push({
251
+ id: "perf-missing-memo",
252
+ severity: "info",
253
+ category: "performance",
254
+ title: "Consider memoizing computed list",
255
+ description: "Filtering + mapping on every render is expensive. Wrap in useMemo or memo().",
256
+ fix: 'const filtered = useMemo(() => items.filter(pred).map(fn), [items]);',
257
+ });
258
+ }
259
+
260
+ // Console.log in production
261
+ if (code.includes("console.log")) {
262
+ issues.push({
263
+ id: "perf-console-log",
264
+ severity: "style",
265
+ category: "performance",
266
+ title: "console.log in production code",
267
+ description: "Remove console.log statements before deploying to production.",
268
+ });
269
+ }
270
+
271
+ // Inline function creation
272
+ if ((code.match(/onClick\s*[:=]\s*\(/g) || []).length > 5) {
273
+ issues.push({
274
+ id: "perf-inline-functions",
275
+ severity: "info",
276
+ category: "performance",
277
+ title: "Many inline event handlers",
278
+ description: "Creating many inline functions on each render can cause unnecessary re-renders. Consider useCallback.",
279
+ });
280
+ }
281
+
282
+ return issues;
283
+ }
284
+
285
+ // ============ ACCESSIBILITY CHECKS ============
286
+
287
+ private checkAccessibility(code: string): CodeIssue[] {
288
+ const issues: CodeIssue[] = [];
289
+
290
+ // img without alt
291
+ if (code.includes("<img") && !code.includes("alt=")) {
292
+ issues.push({
293
+ id: "a11y-img-alt",
294
+ severity: "warning",
295
+ category: "accessibility",
296
+ title: "Images without alt text",
297
+ description: "All images should have alt text for screen readers.",
298
+ fix: 'h("img", { src: url, alt: "Description" })',
299
+ });
300
+ }
301
+
302
+ // button without accessible name
303
+ if (code.includes('h("button"') && !code.includes("aria-label")) {
304
+ const emptyButtons = code.match(/h\(["']button["'],\s*\{[^}]*\}\s*,\s*["']["']\)/g);
305
+ if (emptyButtons) {
306
+ issues.push({
307
+ id: "a11y-button-name",
308
+ severity: "warning",
309
+ category: "accessibility",
310
+ title: "Buttons without accessible names",
311
+ description: "Buttons should have text content or aria-label for screen readers.",
312
+ });
313
+ }
314
+ }
315
+
316
+ // onClick without onKeyDown
317
+ if (code.includes("onClick") && !code.includes("onKeyDown") && !code.includes("onKeyPress")) {
318
+ issues.push({
319
+ id: "a11y-keyboard",
320
+ severity: "info",
321
+ category: "accessibility",
322
+ title: "Click handler without keyboard support",
323
+ description: "Interactive elements should also respond to keyboard events (Enter, Space).",
324
+ });
325
+ }
326
+
327
+ return issues;
328
+ }
329
+
330
+ // ============ BEST PRACTICES ============
331
+
332
+ private checkBestPractices(code: string): CodeIssue[] {
333
+ const issues: CodeIssue[] = [];
334
+
335
+ // var instead of const/let
336
+ if (code.includes("var ")) {
337
+ issues.push({
338
+ id: "bp-var",
339
+ severity: "style",
340
+ category: "best-practice",
341
+ title: "Use const/let instead of var",
342
+ description: "var has function scope and can cause bugs. Use const (default) or let (when reassigning).",
343
+ });
344
+ }
345
+
346
+ // == instead of ===
347
+ if (code.includes(" == ") && !code.includes(" === ")) {
348
+ issues.push({
349
+ id: "bp-strict-equality",
350
+ severity: "style",
351
+ category: "best-practice",
352
+ title: "Use strict equality (===)",
353
+ description: "== does type coercion which can cause unexpected behavior. Use === instead.",
354
+ });
355
+ }
356
+
357
+ // any type
358
+ if (code.includes(": any") || code.includes("as any")) {
359
+ issues.push({
360
+ id: "bp-any-type",
361
+ severity: "warning",
362
+ category: "typescript",
363
+ title: "Avoid 'any' type",
364
+ description: "Using 'any' defeats TypeScript's type safety. Use 'unknown' or specific types.",
365
+ });
366
+ }
367
+
368
+ // TODO/FIXME
369
+ if (code.includes("TODO") || code.includes("FIXME") || code.includes("HACK")) {
370
+ issues.push({
371
+ id: "bp-todo",
372
+ severity: "info",
373
+ category: "best-practice",
374
+ title: "Unresolved TODO/FIXME",
375
+ description: "Code contains TODO or FIXME comments that should be resolved before production.",
376
+ });
377
+ }
378
+
379
+ return issues;
380
+ }
381
+
382
+ // ============ SUGGESTIONS ============
383
+
384
+ private generateSuggestions(code: string): CodeSuggestion[] {
385
+ const suggestions: CodeSuggestion[] = [];
386
+
387
+ // Suggest memo
388
+ if (code.includes("function") && !code.includes("memo(")) {
389
+ suggestions.push({
390
+ id: "sug-memo",
391
+ type: "optimize",
392
+ title: "Wrap pure components in memo()",
393
+ description: "Components that don't depend on external state can be memoized to skip unnecessary re-renders.",
394
+ before: 'function ExpensiveList(props) {\n return h("ul", null, props.items.map(...));\n}',
395
+ after: 'const ExpensiveList = memo(function(props) {\n return h("ul", null, props.items.map(...));\n});',
396
+ impact: "medium",
397
+ });
398
+ }
399
+
400
+ // Suggest $store instead of multiple $state
401
+ const stateCount = (code.match(/\$state\(/g) || []).length;
402
+ if (stateCount >= 3) {
403
+ suggestions.push({
404
+ id: "sug-store",
405
+ type: "refactor",
406
+ title: `Consider using $store instead of ${stateCount} $state calls`,
407
+ description: "Multiple related $state calls can be combined into a single $store for cleaner code.",
408
+ before: 'const name = $state("");\nconst email = $state("");\nconst age = $state(0);',
409
+ after: 'const form = $store({\n name: "",\n email: "",\n age: 0\n});',
410
+ impact: "medium",
411
+ });
412
+ }
413
+
414
+ // Suggest lazy loading
415
+ if (code.includes("import(") && !code.includes("lazy(")) {
416
+ suggestions.push({
417
+ id: "sug-lazy",
418
+ type: "optimize",
419
+ title: "Use lazy() for dynamic imports",
420
+ description: "Wrap dynamic imports in lazy() for better code splitting and prefetching.",
421
+ before: 'const Chart = () => import("./Chart");',
422
+ after: 'const Chart = lazy(() => import("./Chart"));',
423
+ impact: "high",
424
+ });
425
+ }
426
+
427
+ // Suggest async/await over .then()
428
+ if (code.includes(".then(") && !code.includes("async")) {
429
+ suggestions.push({
430
+ id: "sug-async",
431
+ type: "modernize",
432
+ title: "Use async/await instead of .then()",
433
+ description: "async/await is more readable and easier to debug than promise chains.",
434
+ before: 'fetch(url).then(res => res.json()).then(data => console.log(data));',
435
+ after: 'const res = await fetch(url);\nconst data = await res.json();\nconsole.log(data);',
436
+ impact: "low",
437
+ });
438
+ }
439
+
440
+ return suggestions;
441
+ }
442
+
443
+ // ============ METRICS ============
444
+
445
+ private calculateMetrics(code: string): CodeMetrics {
446
+ const lines = code.split("\n").length;
447
+ const conditions = (code.match(/if\s*\(/g) || []).length;
448
+ const loops = (code.match(/for\s*\(|while\s*\(|\.map\(|\.forEach\(|\.filter\(/g) || []).length;
449
+ const functions = (code.match(/function\s+\w+|=>\s*[{(]/g) || []).length;
450
+
451
+ const complexity = conditions + loops * 2;
452
+ const maintainability = Math.max(0, 100 - complexity * 3);
453
+ const readability = Math.max(0, 100 - (lines > 200 ? 30 : 0) - (functions > 10 ? 20 : 0));
454
+ const testability = Math.max(0, 100 - (complexity > 10 ? 30 : 0));
455
+ const bundleImpact = Math.min(100, lines * 0.5);
456
+
457
+ return {
458
+ lines,
459
+ complexity,
460
+ maintainability: Math.round(maintainability),
461
+ readability: Math.round(readability),
462
+ testability: Math.round(testability),
463
+ bundleImpact: Math.round(bundleImpact),
464
+ };
465
+ }
466
+
467
+ // ============ SCORE ============
468
+
469
+ private calculateScore(issues: CodeIssue[], metrics: CodeMetrics): number {
470
+ let score = 100;
471
+
472
+ for (const issue of issues) {
473
+ switch (issue.severity) {
474
+ case "critical": score -= 20; break;
475
+ case "warning": score -= 10; break;
476
+ case "info": score -= 3; break;
477
+ case "style": score -= 1; break;
478
+ }
479
+ }
480
+
481
+ // Factor in metrics
482
+ score = (score + metrics.maintainability + metrics.readability + metrics.testability) / 4;
483
+
484
+ return Math.max(0, Math.min(100, Math.round(score)));
485
+ }
486
+
487
+ // ============ SUMMARY ============
488
+
489
+ private generateSummary(score: number, issues: CodeIssue[], metrics: CodeMetrics): string {
490
+ const critical = issues.filter(i => i.severity === "critical").length;
491
+ const warnings = issues.filter(i => i.severity === "warning").length;
492
+
493
+ if (score >= 90) {
494
+ return `Excellent code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Ready for production.`;
495
+ } else if (score >= 70) {
496
+ return `Good code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Address critical issues before deploy.`;
497
+ } else if (score >= 50) {
498
+ return `Fair code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Needs improvement before production.`;
499
+ } else {
500
+ return `Poor code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Major refactoring needed.`;
501
+ }
502
+ }
503
+ }
504
+
505
+ export const codeReviewer = new CodeReviewer();
506
+
507
+ // ============ CONVENIENCE FUNCTION ============
508
+
509
+ export async function reviewCode(code: string): Promise<CodeReviewReport> {
510
+ return codeReviewer.review(code);
511
+ }
512
+
513
+ // ============ REVIEW DASHBOARD ============
514
+
515
+ export function CodeReviewDashboard(props: { report: CodeReviewReport }): WafraNode {
516
+ const r = props.report;
517
+
518
+ return h("div", {
519
+ style: "background:#0A0A0F;color:#E4E4E7;font-family:Inter,sans-serif;padding:32px;border-radius:12px;",
520
+ },
521
+ // Score
522
+ h("div", { style: "display:flex;align-items:center;gap:20px;margin-bottom:24px;" },
523
+ h("div", {
524
+ style: `
525
+ width:80px;height:80px;border-radius:50%;display:flex;align-items:center;justify-content:center;
526
+ font-size:28px;font-weight:700;font-family:'Space Grotesk',sans-serif;
527
+ background:conic-gradient(${r.score >= 80 ? "#10B981" : r.score >= 50 ? "#F59E0B" : "#EF4444"} ${r.score * 3.6}deg, #2A2A38 0deg);
528
+ `,
529
+ },
530
+ h("div", { style: "width:64px;height:64px;border-radius:50%;background:#0A0A0F;display:flex;align-items:center;justify-content:center;color:#E4E4E7;" }, String(r.score))
531
+ ),
532
+ h("div", null,
533
+ h("div", { style: "font-size:14px;color:#A1A1AA;" }, "Code Review Score"),
534
+ h("div", { style: `font-size:20px;font-weight:600;color:${r.score >= 80 ? "#10B981" : r.score >= 50 ? "#F59E0B" : "#EF4444"};` },
535
+ r.score >= 90 ? "Excellent" : r.score >= 70 ? "Good" : r.score >= 50 ? "Fair" : "Poor"
536
+ ),
537
+ h("div", { style: "font-size:12px;color:#71717A;margin-top:4px;" }, r.summary),
538
+ ),
539
+ ),
540
+
541
+ // Issues
542
+ r.issues.length > 0 ? h("div", { style: "margin-bottom:24px;" },
543
+ h("div", { style: "font-family:monospace;font-size:10px;color:#71717A;text-transform:uppercase;margin-bottom:8px;" }, `Issues (${r.issues.length})`),
544
+ ...r.issues.map(issue =>
545
+ h("div", {
546
+ key: issue.id,
547
+ style: `background:#14141B;border:1px solid #2A2A38;border-left:3px solid ${issue.severity === "critical" ? "#EF4444" : issue.severity === "warning" ? "#F59E0B" : issue.severity === "info" ? "#06B6D4" : "#71717A"};border-radius:6px;padding:12px;margin-bottom:8px;`,
548
+ },
549
+ h("div", { style: "display:flex;align-items:center;gap:8px;margin-bottom:4px;" },
550
+ h("span", {
551
+ style: `padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600;text-transform:uppercase;background:${issue.severity === "critical" ? "rgba(239,68,68,0.15);color:#EF4444" : issue.severity === "warning" ? "rgba(245,158,11,0.15);color:#F59E0B" : issue.severity === "info" ? "rgba(6,182,212,0.15);color:#06B6D4" : "rgba(113,113,122,0.15);color:#71717A"};`,
552
+ }, issue.severity),
553
+ h("span", { style: "font-size:13px;font-weight:600;color:#E4E4E7;" }, issue.title),
554
+ ),
555
+ h("div", { style: "font-size:12px;color:#A1A1AA;margin-bottom:8px;" }, issue.description),
556
+ issue.fix ? h("pre", { style: "font-size:11px;color:#10B981;font-family:monospace;background:#0F0F17;padding:8px;border-radius:4px;white-space:pre-wrap;" }, issue.fix) : null,
557
+ )
558
+ )
559
+ ) : null,
560
+
561
+ // Suggestions
562
+ r.suggestions.length > 0 ? h("div", { style: "margin-bottom:24px;" },
563
+ h("div", { style: "font-family:monospace;font-size:10px;color:#71717A;text-transform:uppercase;margin-bottom:8px;" }, `Suggestions (${r.suggestions.length})`),
564
+ ...r.suggestions.map(sug =>
565
+ h("div", {
566
+ key: sug.id,
567
+ style: "background:#14141B;border:1px solid #2A2A38;border-radius:6px;padding:12px;margin-bottom:8px;",
568
+ },
569
+ h("div", { style: "display:flex;align-items:center;gap:8px;margin-bottom:4px;" },
570
+ h("span", { style: `padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600;text-transform:uppercase;background:rgba(168,85,247,0.15);color:#A855F7;` }, sug.type),
571
+ h("span", { style: "font-size:13px;font-weight:600;color:#E4E4E7;" }, sug.title),
572
+ h("span", { style: `margin-left:auto;font-size:10px;color:${sug.impact === "high" ? "#10B981" : sug.impact === "medium" ? "#F59E0B" : "#71717A"};` }, sug.impact + " impact"),
573
+ ),
574
+ h("div", { style: "font-size:12px;color:#A1A1AA;margin-bottom:8px;" }, sug.description),
575
+ h("div", { style: "display:grid;grid-template-columns:1fr 1fr;gap:8px;" },
576
+ h("div", null,
577
+ h("div", { style: "font-size:9px;color:#EF4444;text-transform:uppercase;margin-bottom:2px;" }, "Before"),
578
+ h("pre", { style: "font-size:10px;color:#A1A1AA;font-family:monospace;background:#0F0F17;padding:6px;border-radius:4px;white-space:pre-wrap;" }, sug.before),
579
+ ),
580
+ h("div", null,
581
+ h("div", { style: "font-size:9px;color:#10B981;text-transform:uppercase;margin-bottom:2px;" }, "After"),
582
+ h("pre", { style: "font-size:10px;color:#10B981;font-family:monospace;background:#0F0F17;padding:6px;border-radius:4px;white-space:pre-wrap;" }, sug.after),
583
+ ),
584
+ ),
585
+ )
586
+ )
587
+ ) : null,
588
+
589
+ // Metrics
590
+ h("div", null,
591
+ h("div", { style: "font-family:monospace;font-size:10px;color:#71717A;text-transform:uppercase;margin-bottom:8px;" }, "Metrics"),
592
+ h("div", { style: "display:grid;grid-template-columns:repeat(3,1fr);gap:8px;" },
593
+ ...Object.entries(r.metrics).map(([key, value]) =>
594
+ h("div", {
595
+ key,
596
+ style: "background:#14141B;border:1px solid #2A2A38;border-radius:6px;padding:8px;text-align:center;",
597
+ },
598
+ h("div", { style: "font-size:20px;font-weight:700;color:#A855F7;" }, String(value)),
599
+ h("div", { style: "font-size:10px;color:#71717A;text-transform:uppercase;" }, key),
600
+ )
601
+ )
602
+ ),
603
+ ),
604
+ );
605
+ }