Skip to main content
Glama
adr-context-detector.ts10.8 kB
/** * ADR Context Detector * * Automatically detects project context (phase, activity level, velocity) * to select appropriate thresholds for timeline analysis */ import { exec } from 'child_process'; import { promisify } from 'util'; import type { ProjectContext, ThresholdProfile, AdrType } from './adr-timeline-types.js'; import type { DiscoveredAdr } from './adr-discovery.js'; const execAsync = promisify(exec); /** * Predefined threshold profiles for different project contexts */ export const THRESHOLD_PROFILES: Record<string, ThresholdProfile> = { // 🚀 STARTUP MODE - Fast iteration, rapid decisions startup: { name: 'Startup / New Project', description: 'Fast-paced development with rapid decision cycles', staleProposedDays: 14, // 2 weeks max acceptedUnimplementedDays: 21, // 3 weeks max outdatedAdrDays: 60, // 2 months dormantAdrDays: 180, // 6 months rapidChangeDays: 3, // Flag if changed within 3 days implementationLagWarning: 14, // Warn if >2 weeks to implement }, // 📈 GROWTH MODE - Active development, structured processes growth: { name: 'Growth / Active Development', description: 'Regular feature development with sprint cycles', staleProposedDays: 30, // 1 sprint cycle acceptedUnimplementedDays: 45, // ~2 sprint cycles outdatedAdrDays: 90, // 1 quarter dormantAdrDays: 365, // 1 year rapidChangeDays: 7, // Flag if changed within 1 week implementationLagWarning: 30, // Warn if >1 month }, // 🏢 MATURE MODE - Deliberate decisions, stable architecture mature: { name: 'Mature / Enterprise', description: 'Established codebase with deliberate architectural changes', staleProposedDays: 90, // 1 quarter acceptedUnimplementedDays: 90, // 1 quarter outdatedAdrDays: 180, // 2 quarters dormantAdrDays: 730, // 2 years rapidChangeDays: 14, // Flag if changed within 2 weeks implementationLagWarning: 60, // Warn if >2 months }, // 🔧 MAINTENANCE MODE - Minimal changes, long-term stability maintenance: { name: 'Maintenance / Legacy', description: 'Stable system with infrequent architectural changes', staleProposedDays: 180, // 6 months (decisions are rare) acceptedUnimplementedDays: 180, // 6 months outdatedAdrDays: 365, // 1 year dormantAdrDays: 1095, // 3 years rapidChangeDays: 30, // Flag if changed within 1 month implementationLagWarning: 120, // Warn if >4 months }, // ⚡ FEATURE DEVELOPMENT - Time-boxed feature work feature_development: { name: 'Feature Development Sprint', description: 'Time-boxed feature development with aggressive timelines', staleProposedDays: 7, // 1 week (fast decisions needed) acceptedUnimplementedDays: 14, // 2 weeks (implement in sprint) outdatedAdrDays: 30, // 1 month dormantAdrDays: 90, // 3 months rapidChangeDays: 2, // Flag if changed within 2 days implementationLagWarning: 7, // Warn if >1 week }, }; /** * ADR type modifiers (multiply thresholds by these values) */ export const ADR_TYPE_MODIFIERS: Record<AdrType, number> = { infrastructure: 1.5, // Infrastructure takes 50% longer feature: 1.0, // Features use base timeline security: 0.8, // Security is urgent (20% faster) refactoring: 1.2, // Refactoring takes 20% longer }; /** * Detect project context from git history and ADR patterns */ export async function detectProjectContext( projectPath: string, adrs: DiscoveredAdr[] ): Promise<ProjectContext> { // Analyze git activity (last 90 days) const gitActivity = await analyzeGitActivity(projectPath, 90); // Analyze ADR patterns const adrPatterns = analyzeAdrPatterns(adrs); // Determine project phase const phase = determineProjectPhase(gitActivity, adrPatterns); // Determine activity level const activityLevel = determineActivityLevel(gitActivity); // Detect deployment cadence const deploymentCadence = await detectDeploymentCadence(projectPath); return { phase, activityLevel, teamVelocity: { avgCommitsPerWeek: gitActivity.commitsPerWeek, avgAdrCreationRate: adrPatterns.adrsPerMonth, avgTimeToImplement: adrPatterns.avgImplementationDays, }, deploymentCadence, adrCharacteristics: { avgAdrAge: adrPatterns.avgAdrAge, proposedToAcceptedAvg: adrPatterns.proposedToAcceptedAvg, acceptedToImplementedAvg: adrPatterns.avgImplementationDays, }, }; } /** * Analyze git activity metrics */ async function analyzeGitActivity( projectPath: string, days: number ): Promise<{ commitsPerWeek: number; activeContributors: number; linesChangedPerWeek: number; }> { try { const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; // Get commit count const { stdout: commits } = await execAsync(`git log --since="${since}" --oneline | wc -l`, { cwd: projectPath, timeout: 5000, }); const totalCommits = parseInt(commits.trim()) || 0; const weeks = days / 7; const commitsPerWeek = totalCommits / weeks; // Get unique contributors const { stdout: contributors } = await execAsync( `git log --since="${since}" --format="%an" | sort -u | wc -l`, { cwd: projectPath, timeout: 5000 } ); const activeContributors = parseInt(contributors.trim()) || 0; // Rough estimate of lines changed const linesChangedPerWeek = commitsPerWeek * 100; // Rough heuristic return { commitsPerWeek, activeContributors, linesChangedPerWeek, }; } catch (error) { // Git not available or not a git repo console.warn('[Context] Git activity analysis failed:', error); return { commitsPerWeek: 0, activeContributors: 0, linesChangedPerWeek: 0, }; } } /** * Analyze ADR patterns */ function analyzeAdrPatterns(adrs: DiscoveredAdr[]): { adrsPerMonth: number; avgAdrAge: number; proposedToAcceptedAvg: number; avgImplementationDays: number; } { if (adrs.length === 0) { return { adrsPerMonth: 0, avgAdrAge: 0, proposedToAcceptedAvg: 0, avgImplementationDays: 0, }; } // Calculate ADR creation rate const now = Date.now(); const adrAges = adrs .filter(adr => adr.timeline?.created_at) .map(adr => { const created = new Date(adr.timeline!.created_at).getTime(); return (now - created) / (1000 * 60 * 60 * 24); // days }); const avgAdrAge = adrAges.length > 0 ? adrAges.reduce((a, b) => a + b, 0) / adrAges.length : 0; // Estimate ADRs per month based on oldest ADR const oldestAdr = Math.max(...adrAges); const adrsPerMonth = oldestAdr > 0 ? (adrs.length / oldestAdr) * 30 : 0; // Rough estimates for other metrics (would need more data to calculate accurately) const proposedToAcceptedAvg = 30; // Default estimate const avgImplementationDays = 45; // Default estimate return { adrsPerMonth, avgAdrAge, proposedToAcceptedAvg, avgImplementationDays, }; } /** * Determine project phase */ function determineProjectPhase( gitActivity: { commitsPerWeek: number }, adrPatterns: { adrsPerMonth: number; avgAdrAge: number } ): 'startup' | 'growth' | 'mature' | 'maintenance' { const { commitsPerWeek } = gitActivity; const { adrsPerMonth, avgAdrAge } = adrPatterns; // Startup: High activity, new ADRs frequently, rapid changes if (commitsPerWeek > 50 && adrsPerMonth > 3 && avgAdrAge < 90) { return 'startup'; } // Growth: Moderate-high activity, regular ADRs, structured development if (commitsPerWeek > 20 && adrsPerMonth > 1 && avgAdrAge < 180) { return 'growth'; } // Maintenance: Low activity, few new ADRs, mostly bug fixes if (commitsPerWeek < 5 && adrsPerMonth < 0.5) { return 'maintenance'; } // Mature: Moderate activity, occasional ADRs, stable architecture return 'mature'; } /** * Determine activity level */ function determineActivityLevel(gitActivity: { commitsPerWeek: number; }): 'very_active' | 'active' | 'moderate' | 'low' { const { commitsPerWeek } = gitActivity; if (commitsPerWeek > 50) return 'very_active'; // Multiple commits per day if (commitsPerWeek > 20) return 'active'; // 3-4 commits per day if (commitsPerWeek > 5) return 'moderate'; // ~1 commit per day return 'low'; // Few commits per week } /** * Detect deployment cadence from git tags/releases */ async function detectDeploymentCadence( projectPath: string ): Promise<'daily' | 'weekly' | 'biweekly' | 'monthly' | 'quarterly'> { try { // Get recent tags (last 90 days) const { stdout } = await execAsync( `git log --tags --simplify-by-decoration --pretty="format:%ai %d" --since="90 days ago"`, { cwd: projectPath, timeout: 5000 } ); const tags = stdout .trim() .split('\n') .filter(line => line.includes('tag:')); if (tags.length === 0) return 'monthly'; // Default const tagCount = tags.length; if (tagCount > 60) return 'daily'; // Multiple tags per day if (tagCount > 12) return 'weekly'; // ~1-2 tags per week if (tagCount > 6) return 'biweekly'; // ~1 tag every 2 weeks if (tagCount > 3) return 'monthly'; // ~1 tag per month return 'quarterly'; } catch { return 'monthly'; // Default fallback } } /** * Select appropriate threshold profile */ export function selectThresholdProfile( context: ProjectContext, override?: string ): ThresholdProfile { // Manual override takes precedence if (override && THRESHOLD_PROFILES[override]) { return THRESHOLD_PROFILES[override]; } // Auto-select based on detected context const profileKey = context.phase; return THRESHOLD_PROFILES[profileKey] || THRESHOLD_PROFILES['mature']!; } /** * Detect ADR type from content */ export function detectAdrType(adr: DiscoveredAdr): AdrType { const content = (adr.title + ' ' + (adr.content || '')).toLowerCase(); if (/infrastructure|deployment|hosting|database migration|ci\/cd/i.test(content)) { return 'infrastructure'; } if (/security|authentication|authorization|encryption|vulnerability/i.test(content)) { return 'security'; } if (/refactor|technical debt|clean up|modernize/i.test(content)) { return 'refactoring'; } return 'feature'; } /** * Apply ADR-type specific adjustments to thresholds */ export function adjustThresholdsForAdrType( baseThresholds: ThresholdProfile, adrType: AdrType ): ThresholdProfile { const multiplier = ADR_TYPE_MODIFIERS[adrType]; return { ...baseThresholds, acceptedUnimplementedDays: Math.round(baseThresholds.acceptedUnimplementedDays * multiplier), implementationLagWarning: Math.round(baseThresholds.implementationLagWarning * multiplier), }; }

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/tosin2013/mcp-adr-analysis-server'

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