Docs /features/repository-analysis

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:

  1. Check if repository already cloned
  2. If not, clone to temporary directory
  3. Checkout specified branch
  4. Get current commit SHA
  5. Run analysis
  6. 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


Next: Test Generation

Reading this with an agent? /docs/features/repository-analysis.md serves the raw markdown.

All docs