Repository Analysis
Production-ready repository analysis with AI-ready context and staging environment detection
📋 Overview
Repository Analysis is the foundation for intelligent test generation. It analyzes your codebase structure, detects frameworks and libraries, identifies test candidates, and stores this information for use by the AI-powered test generation system.
Key Features:
- ✅ Production Ready: Enhanced analyzer with AI-ready context generation
- ✅ Staging Environment Detection: Automatically detects staging URLs from configuration files
- ✅ 20+ Language Support: Comprehensive language and framework detection
- ✅ Plugin Architecture: Extensible analysis with 4+ built-in plugins
- ✅ AI-Optimized Output: Structured context perfect for AI agents
🎯 User Stories
- As a developer, I want to analyze my repository so the system understands my codebase structure
- As a user, I want automatic detection of frameworks and languages
- As a developer, I want to see which files are candidates for test generation
- As a system, I need to track changes between commits to generate tests for modified code
🏗️ Architecture
Database Schema
projectAnalyses table:
{
id: string // Primary key (ULID)
projectId: string // Foreign key to projects (ULID)
repoUrl: string // Repository URL
branch: string // Branch analyzed
commitSha: string // Git commit SHA
analysis: object // Full analysis results (JSONB)
createdAt: Date // Analysis timestamp
}
Indexes:
CREATE INDEX project_analyses_project_created_idx ON project_analyses(project_id, created_at);
CREATE UNIQUE INDEX project_analyses_project_id_commit_sha_key ON project_analyses(project_id, commit_sha);
Unique constraint: One analysis per project per commit
Analysis Result Structure
interface AnalysisResult {
// Metadata
commitSha: string
branch: string
analyzedAt: string
// Repository structure
files: Array<{
path: string
type: 'file' | 'directory'
size: number
extension: string
hash?: string
}>
// Language detection
languages: Record<string, {
files: number
bytes: number
percentage: number
}>
// Framework detection
frameworks: Array<{
name: string
version?: string
configFiles: string[]
}>
// Dependencies
dependencies: {
runtime: Record<string, string>
dev: Record<string, string>
}
// Test candidates
testCandidates: Array<{
file: string
reason: string
priority: 'high' | 'medium' | 'low'
existingTests: string[]
}>
// Staging environment detection
staging: {
urls: Array<{
url: string
source: string
confidence: number
}>
environments: string[]
configurations: Array<{
type: string
file: string
content: any
}>
recommendedUrl?: string
hasStagingConfig: boolean
}
// Statistics
stats: {
totalFiles: number
totalDirectories: number
totalSize: number
averageFileSize: number
testCoverage?: number
}
}
🔌 API Reference
Analyze Repository
// tRPC endpoint: analysis.analyzeRepository
const result = await trpc.analysis.analyzeRepository.mutate({
projectId: 'proj_123',
repoUrl: 'https://github.com/username/repo',
branch: 'main',
githubToken: 'ghp_...', // Optional, for private repos
userEmail: 'user@example.com',
options: {
maxFiles: 1000,
maxFileSizeBytes: 1024 * 1024, // 1MB
includeHashes: true
}
})
// Response
{
success: true,
message: 'Repository analyzed successfully',
analysis: {
id: 'analysis_456',
projectId: 'proj_123',
repoUrl: 'https://github.com/username/repo',
branch: 'main',
commitSha: 'abc123...',
analysis: {
// Full analysis results
commitSha: 'abc123...',
files: [...],
languages: {...},
frameworks: [...],
dependencies: {...},
testCandidates: [...],
stats: {...}
},
createdAt: '2024-01-01T00:00:00Z'
}
}
Input validation:
- projectId: required, must be ULID (26 characters)
- repoUrl: required, must be valid URL
- branch: optional, defaults to 'main'
- userEmail: required, must be valid email
- githubToken: optional, for private repositories
Authorization:
- User must own the project
- Returns 403 if project access denied
Behavior:
- Clones repository (or uses existing clone)
- Analyzes file structure
- Detects languages and frameworks
- Identifies test candidates
- Stores results as JSONB
- Uses upsert: updates if same commit already analyzed
Get Latest Analysis
// Get most recent analysis for a project
const analysis = await trpc.analysis.getLatestAnalysis.query({
projectId: 'proj_123'
})
// Response
{
analysis: {
id: 'analysis_456',
projectId: 'proj_123',
commitSha: 'abc123...',
branch: 'main',
analysis: {
// Full analysis results
},
createdAt: '2024-01-01T00:00:00Z'
}
}
List Analyses
// Get analysis history for a project
const analyses = await trpc.analysis.listAnalyses.query({
projectId: 'proj_123',
limit: 10,
offset: 0
})
// Response
{
analyses: [
{
id: 'analysis_456',
commitSha: 'abc123...',
branch: 'main',
createdAt: '2024-01-01T00:00:00Z',
stats: {
totalFiles: 250,
testCandidates: 45
}
}
],
total: 15
}
🔬 Analysis Process
1. Repository Cloning
import { RepositoryAnalyzer } from '@izri/repo-analyzer'
const analyzer = new RepositoryAnalyzer()
// Clone repository
const analysis = await analyzer.analyzeRepository(
'https://github.com/username/repo',
{
branch: 'main',
githubToken: process.env.GITHUB_TOKEN
}
)
Steps:
- Check if repository already cloned
- If not, clone to temporary directory
- Checkout specified branch
- Get current commit SHA
- Run analysis
- Clean up temporary files
2. File Structure Analysis
// Detected files structure
{
files: [
{
path: 'src/components/Button.tsx',
type: 'file',
size: 1024,
extension: '.tsx',
hash: 'abc123...'
},
{
path: 'src/components/',
type: 'directory',
size: 0,
extension: ''
}
]
}
Detection logic:
- Traverse directory tree
- Filter node_modules, .git, dist, build
- Calculate file sizes
- Optionally compute file hashes
- Identify file types by extension
3. Language Detection
{
languages: {
'TypeScript': {
files: 45,
bytes: 125000,
percentage: 65.5
},
'JavaScript': {
files: 12,
bytes: 35000,
percentage: 18.3
},
'CSS': {
files: 8,
bytes: 15000,
percentage: 7.9
}
}
}
Detection by:
- File extensions (.ts, .js, .py, etc.)
- Shebang lines (#!/usr/bin/env python)
- File content patterns
4. Framework Detection
{
frameworks: [
{
name: 'React',
version: '18.2.0',
configFiles: ['package.json']
},
{
name: 'Vite',
version: '5.0.0',
configFiles: ['vite.config.ts']
},
{
name: 'Tailwind CSS',
version: '3.4.0',
configFiles: ['tailwind.config.js', 'postcss.config.js']
}
]
}
Detection strategy:
- Check package.json dependencies
- Look for config files (vite.config.ts, next.config.js, etc.)
- Analyze import patterns
- Check for framework-specific file structures
5. Dependency Analysis
{
dependencies: {
runtime: {
'react': '^18.2.0',
'react-dom': '^18.2.0',
'@tanstack/react-query': '^5.0.0'
},
dev: {
'vitest': '^1.0.0',
'@testing-library/react': '^14.0.0',
'typescript': '^5.3.0'
}
}
}
Sources:
- package.json (JavaScript/TypeScript)
- requirements.txt, pyproject.toml (Python)
- Cargo.toml (Rust)
- go.mod (Go)
6. Test Candidate Identification
{
testCandidates: [
{
file: 'src/utils/validation.ts',
reason: 'Utility functions with no existing tests',
priority: 'high',
existingTests: []
},
{
file: 'src/components/Button.tsx',
reason: 'React component with props validation',
priority: 'medium',
existingTests: ['src/components/__tests__/Button.test.tsx']
}
]
}
Prioritization logic:
High priority:
- Utility functions without tests
- API route handlers
- Database models
- Critical business logic
Medium priority:
- UI components
- Services with some tests
- Middleware functions
Low priority:
- Configuration files
- Type definitions
- Files with comprehensive tests
🎨 Frontend Implementation
Analyze Button Component
import { trpc } from '~/lib/trpc'
import { Button } from '~/components/ui/button'
interface AnalyzeButtonProps {
projectId: string
repoUrl: string
branch?: string
}
export function AnalyzeButton({ projectId, repoUrl, branch }: AnalyzeButtonProps) {
const utils = trpc.useUtils()
const user = useAuth()
const analyze = trpc.analysis.analyzeRepository.useMutation({
onSuccess: () => {
utils.analysis.getLatestAnalysis.invalidate({ projectId })
utils.projects.getProject.invalidate({ projectId })
}
})
const handleAnalyze = () => {
analyze.mutate({
projectId,
repoUrl,
branch: branch || 'main',
userEmail: user.email
})
}
return (
<Button
onClick={handleAnalyze}
disabled={analyze.isLoading}
>
{analyze.isLoading ? (
<>
<LoaderIcon className="animate-spin" />
Analyzing...
</>
) : (
<>
<AnalyzeIcon />
Analyze Repository
</>
)}
</Button>
)
}
Analysis Results Display
interface AnalysisResultsProps {
analysis: AnalysisResult
}
export function AnalysisResults({ analysis }: AnalysisResultsProps) {
return (
<div className="space-y-6">
{/* Summary Statistics */}
<Card>
<CardHeader>
<CardTitle>Analysis Summary</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-4">
<StatCard
label="Total Files"
value={analysis.stats.totalFiles}
icon={<FileIcon />}
/>
<StatCard
label="Languages"
value={Object.keys(analysis.languages).length}
icon={<CodeIcon />}
/>
<StatCard
label="Frameworks"
value={analysis.frameworks.length}
icon={<LayersIcon />}
/>
<StatCard
label="Test Candidates"
value={analysis.testCandidates.length}
icon={<TestTubeIcon />}
/>
</CardContent>
</Card>
{/* Language Breakdown */}
<Card>
<CardHeader>
<CardTitle>Languages</CardTitle>
</CardHeader>
<CardContent>
{Object.entries(analysis.languages).map(([lang, data]) => (
<div key={lang} className="mb-2">
<div className="flex justify-between text-sm mb-1">
<span>{lang}</span>
<span>{data.percentage.toFixed(1)}%</span>
</div>
<Progress value={data.percentage} />
</div>
))}
</CardContent>
</Card>
{/* Frameworks */}
<Card>
<CardHeader>
<CardTitle>Detected Frameworks</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{analysis.frameworks.map((framework) => (
<div key={framework.name} className="flex items-center justify-between">
<span>{framework.name}</span>
{framework.version && (
<Badge variant="secondary">{framework.version}</Badge>
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* Test Candidates */}
<Card>
<CardHeader>
<CardTitle>Test Candidates</CardTitle>
<CardDescription>
Files that could benefit from tests
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{analysis.testCandidates.map((candidate) => (
<div key={candidate.file} className="border rounded p-3">
<div className="flex items-center justify-between mb-1">
<code className="text-sm">{candidate.file}</code>
<Badge
variant={
candidate.priority === 'high' ? 'destructive' :
candidate.priority === 'medium' ? 'default' :
'secondary'
}
>
{candidate.priority}
</Badge>
</div>
<p className="text-sm text-muted-foreground">{candidate.reason}</p>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}
🔧 Configuration
Analysis Options
interface AnalysisOptions {
// Maximum number of files to analyze
maxFiles?: number // default: 10000
// Maximum file size to include
maxFileSizeBytes?: number // default: 1MB
// Include file content hashes
includeHashes?: boolean // default: false
// Exclude patterns
excludePatterns?: string[] // default: ['node_modules', '.git', 'dist']
// Custom language detection
customExtensions?: Record<string, string>
}
GitHub Token Configuration
For private repositories:
# .env
GITHUB_TOKEN=ghp_your_token_here
const analysis = await analyzer.analyzeRepository(repoUrl, {
githubToken: process.env.GITHUB_TOKEN
})
✅ Testing
Unit Tests
import { describe, expect, it } from 'vitest'
import { RepositoryAnalyzer } from '@izri/repo-analyzer'
describe('RepositoryAnalyzer', () => {
it('analyzes a public repository', async () => {
const analyzer = new RepositoryAnalyzer()
const result = await analyzer.analyzeRepository(
'https://github.com/octocat/Hello-World'
)
expect(result.commitSha).toBeDefined()
expect(result.files.length).toBeGreaterThan(0)
expect(result.languages).toBeDefined()
})
it('detects TypeScript projects', async () => {
const analyzer = new RepositoryAnalyzer()
const result = await analyzer.analyzeRepository(
'https://github.com/example/typescript-project'
)
expect(result.languages['TypeScript']).toBeDefined()
expect(result.languages['TypeScript'].percentage).toBeGreaterThan(50)
})
})
🔗 Related Documentation
- Projects: Project management
- Test Generation: Generating tests from analysis
- Repo Analyzer Package: @izri/repo-analyzer
Next: Test Generation →