Extending Codemetry
Codemetry is designed to be extensible. You can add custom signal providers to incorporate additional metrics into the analysis.
Signal Provider Architecture
Signal providers are the plugin system for Codemetry. Each provider:
- Receives commit data for a time window
- Extracts signals (measurements)
- Returns a
SignalSetwith named values
Git Data → Signal Provider → SignalSet ↓ Normalization → ScoringCreating a Custom Provider
Step 1: Implement the Interface
<?php
namespace App\Codemetry\Providers;
use Codemetry\Core\Signals\SignalProvider;use Codemetry\Core\Domain\RepoSnapshot;use Codemetry\Core\Domain\SignalSet;use Codemetry\Core\Domain\Signal;use DateTimeImmutable;
class TestCoverageProvider implements SignalProvider{ public function id(): string { return 'test_coverage'; }
public function collect( RepoSnapshot $snapshot, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd ): SignalSet { // Your logic to gather coverage data $coverage = $this->getCoverageForWindow($snapshot, $windowStart, $windowEnd);
return new SignalSet([ new Signal( key: 'coverage.percentage', type: 'numeric', value: $coverage['percentage'], description: 'Test coverage percentage' ), new Signal( key: 'coverage.lines_covered', type: 'numeric', value: $coverage['lines_covered'], description: 'Number of lines covered by tests' ), new Signal( key: 'coverage.lines_total', type: 'numeric', value: $coverage['lines_total'], description: 'Total number of lines' ), ]); }
private function getCoverageForWindow( RepoSnapshot $snapshot, DateTimeImmutable $start, DateTimeImmutable $end ): array { // Implementation depends on your coverage tool // Could read from coverage reports, CI artifacts, etc. return [ 'percentage' => 85.5, 'lines_covered' => 1200, 'lines_total' => 1403, ]; }}Step 2: Register the Provider
// In a service provider or bootstrap fileuse Codemetry\Core\Analyzer;use App\Codemetry\Providers\TestCoverageProvider;
$analyzer = app(Analyzer::class);$analyzer->registerProvider(new TestCoverageProvider());Step 3: Use in Analysis
Your provider’s signals will automatically be:
- Collected during analysis
- Normalized against baseline
- Available in output
Provider Requirements
Error Handling
Providers should not throw exceptions. If something fails:
- Return empty or partial signals
- The system adds
provider_skipped:<id>confounder - Analysis continues with available data
public function collect( RepoSnapshot $snapshot, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd): SignalSet { try { $data = $this->fetchData($snapshot); return new SignalSet([/* signals */]); } catch (\Exception $e) { // Log the error but don't break the pipeline Log::warning("TestCoverageProvider failed: " . $e->getMessage()); return new SignalSet([]); // Empty set }}Signal Naming
Follow the naming convention: <category>.<metric>
coverage.percentagecoverage.deltalint.errorslint.warningscomplexity.averagecomplexity.max
Immutability
Signals and SignalSets are immutable. Don’t modify after creation:
// Good: Create new$signals = new SignalSet([ new Signal('coverage.percentage', 'numeric', 85.5, 'Coverage'),]);
// Bad: Never try to modify// $signals->add(...); // This doesn't existExample Providers
PHPStan Integration
class PhpStanProvider implements SignalProvider{ public function id(): string { return 'phpstan'; }
public function collect( RepoSnapshot $snapshot, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd ): SignalSet { // Run PHPStan or read from cached results $result = $this->runPhpStan($snapshot->repoPath());
return new SignalSet([ new Signal('phpstan.errors', 'numeric', $result['errors'], 'PHPStan errors'), new Signal('phpstan.level', 'numeric', $result['level'], 'PHPStan level used'), ]); }}ESLint Integration
class EsLintProvider implements SignalProvider{ public function id(): string { return 'eslint'; }
public function collect( RepoSnapshot $snapshot, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd ): SignalSet { $result = $this->runEsLint($snapshot->repoPath());
return new SignalSet([ new Signal('eslint.errors', 'numeric', $result['errors'], 'ESLint errors'), new Signal('eslint.warnings', 'numeric', $result['warnings'], 'ESLint warnings'), ]); }}CI/CD Metrics
class CiMetricsProvider implements SignalProvider{ public function id(): string { return 'ci_metrics'; }
public function collect( RepoSnapshot $snapshot, DateTimeImmutable $windowStart, DateTimeImmutable $windowEnd ): SignalSet { // Fetch from CI API (GitHub Actions, GitLab CI, etc.) $builds = $this->fetchBuilds($snapshot, $windowStart, $windowEnd);
return new SignalSet([ new Signal('ci.builds_total', 'numeric', count($builds), 'Total builds'), new Signal('ci.builds_failed', 'numeric', $this->countFailed($builds), 'Failed builds'), new Signal('ci.avg_duration', 'numeric', $this->avgDuration($builds), 'Average build duration'), ]); }}Best Practices
1. Keep Providers Focused
Each provider should handle one data source or concern:
- ✅
TestCoverageProvider— just coverage - ✅
PhpStanProvider— just PHPStan - ❌
QualityProvider— too broad
2. Handle Missing Data
Not all windows will have data. Handle gracefully:
if (empty($commits)) { return new SignalSet([ new Signal('coverage.percentage', 'numeric', null, 'No data'), ]);}3. Document Signal Meanings
Use clear descriptions:
new Signal( key: 'coverage.delta', type: 'numeric', value: -2.5, description: 'Coverage change from previous window (percentage points)')4. Consider Performance
Providers run for each analysis window. Optimize:
- Cache expensive computations
- Use efficient queries
- Consider async execution for external APIs