Skip to main content
Glama
metrics.ts14.2 kB
/** * Metrics Calculation Module * * Tracks development metrics and velocity using Git history for timestamps. * Implements DORA metrics, SPACE framework, and cycle time tracking. * * ⚠️ CRITICAL: This module avoids velocity-as-performance metrics. * Focus is on cycle time, quality, and process improvement. */ import { exec } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; import { parseTasksContent } from './taskParser.js'; const execAsync = promisify(exec); export interface DORAMetrics { deploymentFrequency: { specsPerWeek: number; plansPerWeek: number; tasksPerWeek: number; rating: 'Elite' | 'High' | 'Medium' | 'Low'; }; leadTimeForChanges: { averageDays: number; medianDays: number; p95Days: number; rating: 'Elite' | 'High' | 'Medium' | 'Low'; }; changeFailureRate: { specValidationFailures: number; totalSpecs: number; failureRate: number; rating: 'Elite' | 'High' | 'Medium' | 'Low'; }; timeToRestoreService: { applicable: false; reason: 'Not applicable for spec-driven development (no service outages)'; }; } export interface SPACEMetrics { satisfaction: { applicable: false; reason: 'Survey data not collected automatically'; }; performance: { specCompletionRate: number; validationPassRate: number; }; activity: { specsCreated: number; plansCreated: number; tasksCreated: number; clarificationsResolved: number; }; communication: { averageClarificationResolutionDays: number; totalClarifications: number; }; efficiency: { averageSpecToPlanDays: number; averagePlanToTaskDays: number; averageTaskCompletionDays: number; }; } export interface CycleTimeMetrics { spec: { averageDays: number; medianDays: number; p95Days: number; }; plan: { averageDays: number; medianDays: number; p95Days: number; }; task: { averageDays: number; medianDays: number; p95Days: number; }; overall: { averageDays: number; medianDays: number; p95Days: number; }; } export interface TrendAnalysis { cycleTime: { current7DayMA: number; previous7DayMA: number; percentChange: number; trend: 'improving' | 'declining' | 'stable'; }; throughput: { currentWeekTasks: number; previousWeekTasks: number; percentChange: number; trend: 'increasing' | 'decreasing' | 'stable'; }; quality: { currentWeekPassRate: number; previousWeekPassRate: number; percentChange: number; trend: 'improving' | 'declining' | 'stable'; }; } export interface BurndownChart { ideal: number[]; actual: number[]; remaining: number; projectedCompletionDate: string; confidenceInterval: { earliest: string; latest: string; }; } export interface QualityMetrics { specValidationPassRate: number; averageClarificationsPerSpec: number; specRefinementFrequency: number; acceptanceCriteriaCoverage: number; } export interface MetricsReport { generatedAt: string; period: { start: string; end: string; days: number; }; dora: DORAMetrics; space: SPACEMetrics; cycleTime: CycleTimeMetrics; trends: TrendAnalysis; quality: QualityMetrics; burndown: BurndownChart; velocityWarning: string; } /** * Get creation timestamp for a file using Git history */ export async function getFileCreationTime(workspacePath: string, filePath: string): Promise<Date | null> { try { // Get first commit time for file (creation time) const { stdout } = await execAsync( `git log --follow --diff-filter=A --format=%aI -- "${filePath}"`, { cwd: workspacePath } ); const timestamps = stdout.trim().split('\n').filter(Boolean); if (timestamps.length === 0) { return null; } // Return oldest timestamp (last in log) return new Date(timestamps[timestamps.length - 1]); } catch { return null; } } /** * Get last modification timestamp for a file using Git history */ export async function getFileModificationTime(workspacePath: string, filePath: string): Promise<Date | null> { try { const { stdout } = await execAsync( `git log --format=%aI -1 -- "${filePath}"`, { cwd: workspacePath } ); const timestamp = stdout.trim(); return timestamp ? new Date(timestamp) : null; } catch { return null; } } /** * Get task completion timestamp from Git history * Finds the commit where task status changed to completed */ export async function getTaskCompletionTime( workspacePath: string, tasksFilePath: string, taskId: string ): Promise<Date | null> { try { // Get all commits for tasks file const { stdout } = await execAsync( `git log --format=%H|%aI --follow -- "${tasksFilePath}"`, { cwd: workspacePath } ); const commits = stdout.trim().split('\n').map(line => { const [hash, timestamp] = line.split('|'); return { hash, timestamp }; }); // Check each commit to find when task was marked completed for (const commit of commits) { try { const { stdout: content } = await execAsync( `git show ${commit.hash}:"${tasksFilePath}"`, { cwd: workspacePath } ); const tasks = parseTasksContent(content); const task = tasks.find(t => t.id === taskId); if (task && task.status === 'completed') { return new Date(commit.timestamp); } } catch { // Commit may not have this file yet, continue } } return null; } catch { return null; } } /** * Calculate statistics for an array of numbers */ function calculateStats(values: number[]): { average: number; median: number; p95: number } { if (values.length === 0) { return { average: 0, median: 0, p95: 0 }; } const sorted = [...values].sort((a, b) => a - b); const average = values.reduce((sum, v) => sum + v, 0) / values.length; const median = sorted[Math.floor(sorted.length / 2)]; const p95 = sorted[Math.floor(sorted.length * 0.95)]; return { average, median, p95 }; } /** * Calculate days between two dates */ function daysBetween(start: Date, end: Date): number { return (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24); } /** * Rate DORA deployment frequency */ function rateDeploymentFrequency(deploymentsPerDay: number): 'Elite' | 'High' | 'Medium' | 'Low' { if (deploymentsPerDay >= 1) { return 'Elite'; } if (deploymentsPerDay >= 1 / 7) { return 'High'; // Once per week } if (deploymentsPerDay >= 1 / 30) { return 'Medium'; // Once per month } return 'Low'; } /** * Rate DORA lead time */ function rateLeadTime(days: number): 'Elite' | 'High' | 'Medium' | 'Low' { if (days < 1) { return 'Elite'; } if (days < 7) { return 'High'; } if (days < 30) { return 'Medium'; } return 'Low'; } /** * Rate DORA change failure rate */ function rateChangeFailureRate(rate: number): 'Elite' | 'High' | 'Medium' | 'Low' { if (rate < 0.05) { return 'Elite'; // < 5% } if (rate < 0.10) { return 'High'; // < 10% } if (rate < 0.15) { return 'Medium'; // < 15% } return 'Low'; } /** * Calculate DORA metrics */ export async function calculateDORAMetrics( workspacePath: string, startDate: Date, endDate: Date ): Promise<DORAMetrics> { // Find all spec, plan, and task files created in period const dincoderDir = path.join(workspacePath, '.dincoder'); // Get all relevant files const specFiles: string[] = []; const planFiles: string[] = []; const taskFiles: string[] = []; try { // Find spec files const specDirs = ['specs', '.dincoder']; for (const dir of specDirs) { const dirPath = path.join(workspacePath, dir); try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name === 'spec.md') { specFiles.push(path.join(dirPath, entry.name)); } else if (entry.isDirectory() && /^\d{3}-/.test(entry.name)) { const specPath = path.join(dirPath, entry.name, 'spec.md'); try { await fs.access(specPath); specFiles.push(specPath); } catch { // File doesn't exist } } } } catch { // Directory doesn't exist } } // Find plan files const planPath = path.join(dincoderDir, 'plan.md'); try { await fs.access(planPath); planFiles.push(planPath); } catch { // File doesn't exist } // Find task files const tasksPath = path.join(dincoderDir, 'tasks.md'); try { await fs.access(tasksPath); taskFiles.push(tasksPath); } catch { // File doesn't exist } } catch { // Error reading directories } // Count completions in period let specsCompleted = 0; let plansCompleted = 0; let tasksCompleted = 0; const leadTimes: number[] = []; for (const file of specFiles) { const createdAt = await getFileCreationTime(workspacePath, file); const modifiedAt = await getFileModificationTime(workspacePath, file); if (createdAt && modifiedAt && modifiedAt >= startDate && modifiedAt <= endDate) { specsCompleted++; leadTimes.push(daysBetween(createdAt, modifiedAt)); } } for (const file of planFiles) { const createdAt = await getFileCreationTime(workspacePath, file); const modifiedAt = await getFileModificationTime(workspacePath, file); if (createdAt && modifiedAt && modifiedAt >= startDate && modifiedAt <= endDate) { plansCompleted++; } } // Count completed tasks for (const file of taskFiles) { const content = await fs.readFile(file, 'utf-8'); const tasks = parseTasksContent(content); for (const task of tasks) { if (task.status === 'completed') { const completedAt = await getTaskCompletionTime(workspacePath, file, task.id); if (completedAt && completedAt >= startDate && completedAt <= endDate) { tasksCompleted++; } } } } const periodDays = daysBetween(startDate, endDate); const periodWeeks = periodDays / 7; // Calculate deployment frequency (specs/plans/tasks per week) const specsPerWeek = specsCompleted / periodWeeks; const plansPerWeek = plansCompleted / periodWeeks; const tasksPerWeek = tasksCompleted / periodWeeks; // Use tasks per day as deployment frequency metric const deploymentsPerDay = tasksCompleted / periodDays; // Calculate lead time statistics const leadTimeStats = calculateStats(leadTimes); return { deploymentFrequency: { specsPerWeek, plansPerWeek, tasksPerWeek, rating: rateDeploymentFrequency(deploymentsPerDay), }, leadTimeForChanges: { averageDays: leadTimeStats.average, medianDays: leadTimeStats.median, p95Days: leadTimeStats.p95, rating: rateLeadTime(leadTimeStats.median), }, changeFailureRate: { specValidationFailures: 0, // TODO: Track validation failures totalSpecs: specsCompleted, failureRate: 0, rating: rateChangeFailureRate(0), }, timeToRestoreService: { applicable: false, reason: 'Not applicable for spec-driven development (no service outages)', }, }; } /** * Generate comprehensive metrics report */ export async function generateMetricsReport( workspacePath: string, startDate?: Date, endDate?: Date ): Promise<MetricsReport> { const now = new Date(); const start = startDate || new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // Default: last 30 days const end = endDate || now; // Calculate all metrics const dora = await calculateDORAMetrics(workspacePath, start, end); // Placeholder implementations for other metrics // TODO: Implement full SPACE, cycle time, trends, quality, and burndown const space: SPACEMetrics = { satisfaction: { applicable: false, reason: 'Survey data not collected automatically', }, performance: { specCompletionRate: 0, validationPassRate: 0, }, activity: { specsCreated: 0, plansCreated: 0, tasksCreated: 0, clarificationsResolved: 0, }, communication: { averageClarificationResolutionDays: 0, totalClarifications: 0, }, efficiency: { averageSpecToPlanDays: 0, averagePlanToTaskDays: 0, averageTaskCompletionDays: 0, }, }; const cycleTime: CycleTimeMetrics = { spec: { averageDays: 0, medianDays: 0, p95Days: 0 }, plan: { averageDays: 0, medianDays: 0, p95Days: 0 }, task: { averageDays: 0, medianDays: 0, p95Days: 0 }, overall: { averageDays: 0, medianDays: 0, p95Days: 0 }, }; const trends: TrendAnalysis = { cycleTime: { current7DayMA: 0, previous7DayMA: 0, percentChange: 0, trend: 'stable', }, throughput: { currentWeekTasks: 0, previousWeekTasks: 0, percentChange: 0, trend: 'stable', }, quality: { currentWeekPassRate: 0, previousWeekPassRate: 0, percentChange: 0, trend: 'stable', }, }; const quality: QualityMetrics = { specValidationPassRate: 0, averageClarificationsPerSpec: 0, specRefinementFrequency: 0, acceptanceCriteriaCoverage: 0, }; const burndown: BurndownChart = { ideal: [], actual: [], remaining: 0, projectedCompletionDate: '', confidenceInterval: { earliest: '', latest: '', }, }; return { generatedAt: now.toISOString(), period: { start: start.toISOString(), end: end.toISOString(), days: daysBetween(start, end), }, dora, space, cycleTime, trends, quality, burndown, velocityWarning: '⚠️ IMPORTANT: Velocity should never be used as a performance metric. Focus on cycle time, lead time, and quality metrics instead. High velocity with poor quality indicates process problems, not team performance.', }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flight505/MCP_DinCoder'

If you have feedback or need assistance with the MCP directory API, please join our Discord server