Spaces:
Running
Running
Download api/v2/_services/githubValidationEngine.ts from Sarvesh213/getcert-backend: direct link, hf CLI and curl.
- Browser
- Download file 9.71 kB
-
https://huggingface.co/spaces/Sarvesh213/getcert-backend/resolve/main/api/v2/_services/githubValidationEngine.ts
- Command line
-
hf download hf://spaces/Sarvesh213/getcert-backend/api/v2/_services/githubValidationEngine.ts
-
curl -L -o githubValidationEngine.ts https://huggingface.co/spaces/Sarvesh213/getcert-backend/resolve/main/api/v2/_services/githubValidationEngine.ts
9.71 kB
| import type { | |
| CommitValidation, | |
| GitHubRepoMetadata, | |
| GitHubValidationReport, | |
| ReadmeValidation, | |
| StructureValidation, | |
| } from '../_types.js'; | |
| type ParsedRepo = { owner: string; repo: string }; | |
| const DEFAULT_MIN_COMMITS = Number(process.env.MIN_PROJECT_COMMITS || 8); | |
| const CODE_FILE_REGEX = /\.(ts|tsx|js|jsx|py|java|go|rs|php|rb|cs|kt|swift|sql|scala|sh|ipynb)$/i; | |
| const DOC_FILE_REGEX = /\.(md|txt|rst)$/i; | |
| function parseRepoUrl(repoUrl: string): ParsedRepo { | |
| const cleaned = repoUrl.trim().replace(/\.git$/, ''); | |
| const url = new URL(cleaned); | |
| const parts = url.pathname.split('/').filter(Boolean); | |
| if (parts.length < 2) { | |
| throw new Error('Invalid GitHub repository URL'); | |
| } | |
| return { | |
| owner: parts[0], | |
| repo: parts[1], | |
| }; | |
| } | |
| function githubHeaders(): HeadersInit { | |
| const token = process.env.GITHUB_TOKEN; | |
| const headers: HeadersInit = { | |
| Accept: 'application/vnd.github+json', | |
| 'User-Agent': 'GetCert-v2-validator', | |
| }; | |
| if (token) { | |
| headers.Authorization = `Bearer ${token}`; | |
| } | |
| return headers; | |
| } | |
| async function githubGet<T>(path: string): Promise<T> { | |
| const response = await fetch(`https://api.github.com${path}`, { | |
| headers: githubHeaders(), | |
| }); | |
| if (!response.ok) { | |
| const body = await response.text(); | |
| throw new Error(`GitHub API error ${response.status}: ${body}`); | |
| } | |
| return (await response.json()) as T; | |
| } | |
| function decodeBase64(content: string): string { | |
| return Buffer.from(content, 'base64').toString('utf8'); | |
| } | |
| function validateStructure(tree: Array<{ path: string; type?: string; size?: number }>): StructureValidation { | |
| const treePaths = tree.map((entry) => entry.path); | |
| const lower = treePaths.map((path) => path.toLowerCase()); | |
| const fileEntries = tree.filter((entry) => (entry.type ? entry.type === 'blob' : !entry.path.endsWith('/'))); | |
| const filePaths = fileEntries.map((entry) => entry.path.toLowerCase()); | |
| const frontendSignals = [ | |
| 'src/', | |
| 'public/', | |
| 'app/', | |
| 'pages/', | |
| 'components/', | |
| 'index.html', | |
| 'vite.config', | |
| 'next.config', | |
| 'angular.json', | |
| ]; | |
| const backendSignals = [ | |
| 'api/', | |
| 'server/', | |
| 'backend/', | |
| 'controllers/', | |
| 'routes/', | |
| 'main.py', | |
| 'app.py', | |
| 'manage.py', | |
| 'pom.xml', | |
| 'go.mod', | |
| 'dockerfile', | |
| 'vercel.json', | |
| ]; | |
| const hasFrontend = lower.some((path) => frontendSignals.some((signal) => path.includes(signal))); | |
| const hasBackend = lower.some((path) => backendSignals.some((signal) => path.includes(signal))); | |
| const hasPython = lower.some((path) => path.endsWith('.py')); | |
| const hasApiUsage = lower.some((path) => | |
| path.includes('routes') || | |
| path.includes('controller') || | |
| path.includes('endpoint') || | |
| path.includes('api/') || | |
| path.includes('graphql') || | |
| path.includes('openapi') | |
| ); | |
| const keySignals = lower | |
| .filter((path) => | |
| path.includes('auth') || | |
| path.includes('api') || | |
| path.includes('db') || | |
| path.includes('schema') || | |
| path.includes('model') || | |
| path.includes('migration') | |
| ) | |
| .slice(0, 30); | |
| const meaningfulFileCount = filePaths.filter((path) => { | |
| if (CODE_FILE_REGEX.test(path)) return true; | |
| if (path.endsWith('dockerfile')) return true; | |
| if (path.endsWith('package.json') || path.endsWith('pyproject.toml') || path.endsWith('requirements.txt')) return true; | |
| if (path.endsWith('tsconfig.json') || path.endsWith('vite.config.ts') || path.endsWith('vercel.json')) return true; | |
| return false; | |
| }).length; | |
| const docLikeCount = filePaths.filter((path) => DOC_FILE_REGEX.test(path) || path.includes('license')).length; | |
| const emptyCodeFileCount = fileEntries.filter((entry) => { | |
| const path = entry.path.toLowerCase(); | |
| const size = typeof entry.size === 'number' ? entry.size : 1; | |
| return CODE_FILE_REGEX.test(path) && size === 0; | |
| }).length; | |
| const suspiciousPatterns: string[] = []; | |
| if (fileEntries.length < 8) { | |
| suspiciousPatterns.push(`Repository has too few files (${fileEntries.length})`); | |
| } | |
| if (meaningfulFileCount < 3) { | |
| suspiciousPatterns.push('Repository has too few meaningful source/config files'); | |
| } | |
| if (docLikeCount >= Math.max(3, Math.floor(fileEntries.length * 0.7))) { | |
| suspiciousPatterns.push('Repository appears mostly documentation with little implementation'); | |
| } | |
| if (emptyCodeFileCount >= 3) { | |
| suspiciousPatterns.push('Repository contains multiple empty code files (possible fake scaffold)'); | |
| } | |
| if (!(hasFrontend || hasBackend || hasPython || hasApiUsage) && keySignals.length < 2) { | |
| suspiciousPatterns.push('No clear implementation architecture signals detected'); | |
| } | |
| const passed = suspiciousPatterns.length === 0; | |
| return { | |
| passed, | |
| hasFrontend, | |
| hasBackend, | |
| hasPython, | |
| hasApiUsage, | |
| fileCount: fileEntries.length, | |
| meaningfulFileCount, | |
| suspiciousPatterns, | |
| keySignals, | |
| }; | |
| } | |
| function validateCommits(commitMessages: string[], minRequired = DEFAULT_MIN_COMMITS): CommitValidation { | |
| const normalized = commitMessages | |
| .map((message) => message.toLowerCase().trim()) | |
| .filter(Boolean); | |
| const commitCount = normalized.length; | |
| const suspiciousPatterns: string[] = []; | |
| if (commitCount < minRequired) { | |
| suspiciousPatterns.push(`Too few commits (${commitCount})`); | |
| } | |
| const genericWords = ['update', 'fix', 'changes', 'wip', 'temp', 'misc', 'final']; | |
| const repeatedGeneric = normalized.filter((message) => genericWords.includes(message)).length; | |
| if (repeatedGeneric >= Math.max(3, Math.floor(commitCount * 0.45))) { | |
| suspiciousPatterns.push('Commit messages are excessively generic'); | |
| } | |
| const singleWordMessages = normalized.filter((message) => message.split(/\s+/).length === 1).length; | |
| if (singleWordMessages >= Math.max(4, Math.floor(commitCount * 0.5))) { | |
| suspiciousPatterns.push('High ratio of one-word commit messages detected'); | |
| } | |
| const uniqueCount = new Set(normalized).size; | |
| const uniqueRatio = commitCount > 0 ? uniqueCount / commitCount : 0; | |
| if (commitCount > 0 && uniqueRatio < 0.45) { | |
| suspiciousPatterns.push('High commit message repetition detected'); | |
| } | |
| const suspiciousBulkMarkers = normalized.filter((message) => | |
| message.includes('initial commit') || message.includes('upload project') || message.includes('final submission') | |
| ).length; | |
| if (commitCount <= 3 && suspiciousBulkMarkers > 0) { | |
| suspiciousPatterns.push('Possible bulk upload pattern detected'); | |
| } | |
| const repeatedGenericRatio = commitCount > 0 ? repeatedGeneric / commitCount : 1; | |
| const singleWordRatio = commitCount > 0 ? singleWordMessages / commitCount : 1; | |
| let score = 100; | |
| if (commitCount < minRequired) { | |
| score -= Math.min(45, (minRequired - commitCount) * 6); | |
| } | |
| score -= Math.round(repeatedGenericRatio * 25); | |
| score -= Math.round(singleWordRatio * 20); | |
| if (uniqueRatio < 0.5) { | |
| score -= Math.round((0.5 - uniqueRatio) * 40); | |
| } | |
| if (suspiciousBulkMarkers > 0) { | |
| score -= 15; | |
| } | |
| score = Math.max(0, Math.min(100, score)); | |
| const passed = commitCount >= minRequired && score >= 55; | |
| return { | |
| passed, | |
| score, | |
| commitCount, | |
| minRequired, | |
| suspiciousPatterns, | |
| }; | |
| } | |
| function validateReadme(readme: string): ReadmeValidation { | |
| const text = readme || ''; | |
| const lowered = text.toLowerCase(); | |
| const requiredSections = ['overview', 'setup', 'architecture', 'usage', 'proof']; | |
| const missingSections = requiredSections.filter((section) => !lowered.includes(section)); | |
| const richnessSignals = [ | |
| 'api', | |
| 'database', | |
| 'authentication', | |
| 'deployment', | |
| 'testing', | |
| 'architecture', | |
| 'trade-off', | |
| 'validation', | |
| 'error handling', | |
| ]; | |
| const richnessScore = richnessSignals.filter((signal) => lowered.includes(signal)).length; | |
| const hasMinimumLength = text.length >= 800; | |
| const passed = hasMinimumLength && missingSections.length <= 2 && richnessScore >= 4; | |
| return { | |
| passed, | |
| length: text.length, | |
| requiredSections, | |
| missingSections, | |
| richnessScore, | |
| }; | |
| } | |
| export async function validateGitHubRepository(repoUrl: string): Promise<GitHubValidationReport> { | |
| const parsed = parseRepoUrl(repoUrl); | |
| const repoData = await githubGet<{ | |
| default_branch: string; | |
| language: string | null; | |
| stargazers_count: number; | |
| forks_count: number; | |
| }>(`/repos/${parsed.owner}/${parsed.repo}`); | |
| const treeData = await githubGet<{ tree: Array<{ path: string; type?: string; size?: number }> }>( | |
| `/repos/${parsed.owner}/${parsed.repo}/git/trees/${repoData.default_branch}?recursive=1` | |
| ); | |
| const commitsData = await githubGet<Array<{ commit: { message: string } }>>( | |
| `/repos/${parsed.owner}/${parsed.repo}/commits?per_page=100` | |
| ); | |
| const readmeData = await githubGet<{ content: string }>(`/repos/${parsed.owner}/${parsed.repo}/readme`); | |
| const languagesData = await githubGet<Record<string, number>>(`/repos/${parsed.owner}/${parsed.repo}/languages`); | |
| const metadata: GitHubRepoMetadata = { | |
| owner: parsed.owner, | |
| repo: parsed.repo, | |
| defaultBranch: repoData.default_branch, | |
| language: repoData.language, | |
| stars: repoData.stargazers_count, | |
| forks: repoData.forks_count, | |
| treePaths: treeData.tree.map((item) => item.path), | |
| commitMessages: commitsData.map((item) => item.commit.message), | |
| readme: decodeBase64(readmeData.content), | |
| languages: languagesData, | |
| }; | |
| const structure = validateStructure(treeData.tree); | |
| const commits = validateCommits(metadata.commitMessages); | |
| const readme = validateReadme(metadata.readme); | |
| return { | |
| metadata, | |
| structure, | |
| commits, | |
| readme, | |
| overallPassed: commits.passed && readme.passed && structure.passed, | |
| }; | |
| } | |