// --------------------------------------------------------------------------- // Ninja Code Guard – API client + mock data for development // --------------------------------------------------------------------------- import type { Finding, PRReviewRecord, RepoStats, SynthesizedReview, } from "./types"; const API_URL = process.env.NEXT_PUBLIC_API_URL ?? ""; // --------------------------------------------------------------------------- // Generic fetcher // --------------------------------------------------------------------------- async function apiFetch(path: string): Promise { if (!API_URL) return null as unknown as T; // fall through to mock const res = await fetch(`${API_URL}${path}`, { next: { revalidate: 60 } }); if (!res.ok) throw new Error(`API ${res.status}: ${res.statusText}`); return res.json() as Promise; } // --------------------------------------------------------------------------- // Mock findings // --------------------------------------------------------------------------- const MOCK_FINDINGS: Finding[] = [ { agent: "security", file_path: "src/auth/login.ts", line_start: 42, line_end: 48, severity: "critical", category: "SQL Injection", title: "Unsanitized user input in SQL query", description: "User-supplied `username` is interpolated directly into a SQL query string without parameterisation. An attacker can inject arbitrary SQL to bypass authentication or exfiltrate data.", suggested_fix: 'Use parameterised queries: `db.query("SELECT * FROM users WHERE username = $1", [username])`', cwe_id: "CWE-89", confidence: 0.95, }, { agent: "security", file_path: "src/api/middleware.ts", line_start: 15, line_end: 22, severity: "high", category: "Authentication", title: "Missing JWT expiry validation", description: "The JWT verification step does not check the `exp` claim, allowing expired tokens to grant access indefinitely.", suggested_fix: "Pass `{ algorithms: ['HS256'], ignoreExpiration: false }` to `jwt.verify()`.", cwe_id: "CWE-613", confidence: 0.88, }, { agent: "performance", file_path: "src/services/dataLoader.ts", line_start: 78, line_end: 95, severity: "high", category: "N+1 Query", title: "Sequential database queries inside loop", description: "Each iteration of the `for` loop executes a separate `SELECT` query. For 1 000 records this produces 1 001 queries instead of a single batch query.", suggested_fix: "Collect IDs first, then fetch all records in a single `WHERE id IN (...)` query.", cwe_id: null, confidence: 0.92, }, { agent: "performance", file_path: "src/utils/imageProcessor.ts", line_start: 12, line_end: 30, severity: "medium", category: "Memory", title: "Large buffer allocated synchronously", description: "A 50 MB buffer is allocated on the main thread for image processing. This can cause the event loop to stall and trigger OOM errors under load.", suggested_fix: "Stream the image in chunks or offload processing to a worker thread.", cwe_id: null, confidence: 0.78, }, { agent: "style", file_path: "src/components/Dashboard.tsx", line_start: 5, line_end: 5, severity: "low", category: "Naming", title: "Component file uses default export without matching name", description: "The file exports `export default function Dash()` which does not match the filename `Dashboard.tsx`. This hurts discoverability in IDEs and stack traces.", suggested_fix: "Rename the function to `Dashboard` or rename the file to `Dash.tsx`.", cwe_id: null, confidence: 0.99, }, { agent: "style", file_path: "src/hooks/useData.ts", line_start: 18, line_end: 45, severity: "low", category: "Complexity", title: "Function exceeds recommended cyclomatic complexity", description: "The `transformPayload` function has a cyclomatic complexity of 14. Consider extracting branches into helper functions for readability.", suggested_fix: "Extract the nested conditionals into separate pure functions (e.g. `normaliseDate`, `mapStatus`).", cwe_id: null, confidence: 0.85, }, { agent: "security", file_path: "src/config/cors.ts", line_start: 3, line_end: 8, severity: "medium", category: "CORS", title: "Wildcard CORS origin in production config", description: "The CORS configuration uses `origin: '*'` which allows any website to make credentialed requests to the API.", suggested_fix: "Restrict the origin to your frontend domain(s): `origin: ['https://app.example.com']`.", cwe_id: "CWE-942", confidence: 0.91, }, ]; // --------------------------------------------------------------------------- // Mock PR review records // --------------------------------------------------------------------------- function makeMockReviews( repo: string, count: number ): PRReviewRecord[] { const base = Date.now(); return Array.from({ length: count }, (_, i) => { const score = Math.min(100, Math.max(35, 72 + Math.round(Math.sin(i) * 18))); const crit = score < 50 ? 2 : score < 70 ? 1 : 0; const high = Math.max(0, 3 - Math.floor(score / 30)); const med = Math.max(0, 4 - Math.floor(score / 25)); const low = 2; return { id: `pr-${repo}-${i}`, repo_full_name: repo, pr_number: 100 + count - i, commit_sha: `abc${String(i).padStart(4, "0")}`, health_score: score, critical_count: crit, high_count: high, medium_count: med, low_count: low, summary: `Automated review for PR #${100 + count - i}`, findings: MOCK_FINDINGS.slice(0, 3 + (i % 4)), duration_ms: 1200 + i * 300, created_at: new Date(base - i * 86_400_000).toISOString(), }; }); } // --------------------------------------------------------------------------- // Mock repos // --------------------------------------------------------------------------- export interface MockRepo { owner: string; repo: string; full_name: string; health_score: number; open_prs: number; last_review: string; } export const MOCK_REPOS: MockRepo[] = [ { owner: "acme", repo: "web-app", full_name: "acme/web-app", health_score: 87, open_prs: 4, last_review: "2 hours ago", }, { owner: "acme", repo: "api-server", full_name: "acme/api-server", health_score: 64, open_prs: 7, last_review: "35 minutes ago", }, { owner: "acme", repo: "mobile-sdk", full_name: "acme/mobile-sdk", health_score: 93, open_prs: 2, last_review: "1 day ago", }, { owner: "acme", repo: "infra-tools", full_name: "acme/infra-tools", health_score: 51, open_prs: 11, last_review: "10 minutes ago", }, ]; // --------------------------------------------------------------------------- // Mock synthesized review // --------------------------------------------------------------------------- const MOCK_SYNTH_REVIEW: SynthesizedReview = { health_score: 64, executive_summary: "This PR introduces several new API endpoints and a database migration. While the feature logic is sound, there are critical security issues — most notably an SQL injection vulnerability in the login flow — and performance concerns around N+1 queries in the data-loading layer. Style issues are minor but should be addressed for long-term maintainability.", recommendation: "request_changes", findings: MOCK_FINDINGS, critical_count: 1, high_count: 2, medium_count: 2, low_count: 2, duration_ms: 3420, }; // --------------------------------------------------------------------------- // Public API functions // --------------------------------------------------------------------------- export async function getRepoReviews( owner: string, repo: string ): Promise { try { if (API_URL) return await apiFetch(`/repos/${owner}/${repo}/reviews`); } catch { /* fall through to mock */ } return makeMockReviews(`${owner}/${repo}`, 10); } export async function getReviewDetail( owner: string, repo: string, prNumber: number ): Promise<{ review: SynthesizedReview; record: PRReviewRecord }> { try { if (API_URL) return await apiFetch(`/repos/${owner}/${repo}/prs/${prNumber}`); } catch { /* fall through to mock */ } const records = makeMockReviews(`${owner}/${repo}`, 10); const record = records.find((r) => r.pr_number === prNumber) ?? records[0]; return { review: { ...MOCK_SYNTH_REVIEW, health_score: record.health_score }, record, }; } export async function getRepoStats( owner: string, repo: string ): Promise { try { if (API_URL) return await apiFetch(`/repos/${owner}/${repo}/stats`); } catch { /* fall through to mock */ } const reviews = makeMockReviews(`${owner}/${repo}`, 10); const scores = reviews.map((r) => r.health_score).reverse(); return { repo_full_name: `${owner}/${repo}`, total_reviews: reviews.length, average_health_score: Math.round( scores.reduce((a, b) => a + b, 0) / scores.length ), total_findings: reviews.reduce((s, r) => s + r.findings.length, 0), recent_scores: scores, top_categories: [ { category: "SQL Injection", count: 4 }, { category: "N+1 Query", count: 3 }, { category: "Naming", count: 6 }, { category: "Authentication", count: 2 }, { category: "Complexity", count: 5 }, ], }; }