analyze_adr_timeline
Analyze architectural decision timelines to detect staleness, implementation lag, and technical debt, then generate prioritized work items with effort estimates.
Instructions
Analyze ADR timeline with smart time tracking, adaptive thresholds, and actionable recommendations. Auto-detects project context (startup/growth/mature) and generates prioritized work queue based on staleness, implementation lag, and technical debt.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectPath | No | Path to the project directory | . |
| adrDirectory | No | Directory containing ADR files | docs/adrs |
| generateActions | No | Generate actionable work items with priority and effort estimates | |
| thresholdProfile | No | Threshold profile for action generation (auto-detected if not specified) | |
| autoDetectContext | No | Auto-detect project phase from git activity and ADR patterns | |
| includeContent | No | Include ADR content for better analysis | |
| forceExtract | No | Force timeline extraction even if ADRs have dates |
Implementation Reference
- src/tools/tool-catalog.ts:361-379 (registration)Registration and input schema definition for the analyze_adr_timeline tool in the central tool catalog. Includes metadata, description, token cost estimates, related tools, and input schema.TOOL_CATALOG.set('analyze_adr_timeline', { name: 'analyze_adr_timeline', shortDescription: 'Analyze ADR evolution over time', fullDescription: 'Analyzes the timeline of ADR decisions to understand architectural evolution.', category: 'adr', complexity: 'moderate', tokenCost: { min: 1500, max: 3000 }, hasCEMCPDirective: true, // Phase 4.3: Moderate tool - timeline analysis relatedTools: ['discover_existing_adrs', 'compare_adr_progress'], keywords: ['adr', 'timeline', 'history', 'evolution'], requiresAI: true, inputSchema: { type: 'object', properties: { startDate: { type: 'string', description: 'Start date for analysis' }, endDate: { type: 'string', description: 'End date for analysis' }, }, }, });
- Core timeline extraction logic that analyzes ADR creation/update dates using git history, content parsing, or filesystem metadata with intelligent caching and conditional extraction. This is the primary helper function for ADR timeline analysis.export async function extractBasicTimeline( adrPath: string, adrContent: string, options: TimelineExtractionOptions = {} ): Promise<BasicTimeline> { const { useCache = true, cacheTTL = 3600, // 1 hour default forceExtract = false, } = options; // Check if extraction can be skipped (unless forced) if (!forceExtract) { const decision = await shouldExtractTimeline(adrPath, adrContent); if (!decision.shouldExtract && decision.useExisting) { // Use existing date from content return createTimelineFromContent(adrContent, decision.useExisting, decision.reason); } if (!decision.shouldExtract && !decision.useExisting) { // Use cached data const cached = getCachedTimeline(`timeline:${adrPath}`); if (cached) { return cached as BasicTimeline; } } // Log extraction reason for debugging // NOTE: All console output goes to stderr to preserve stdout for MCP JSON-RPC if (decision.shouldExtract) { console.error(`[Timeline] Extracting for ${adrPath}: ${decision.reason}`); } } // Perform extraction using priority order: git → content → filesystem let timeline: Partial<BasicTimeline> | null = null; // Try git extraction first timeline = await tryGitExtraction(adrPath); // Fallback to content parsing if (!timeline || !timeline.created_at) { const contentDate = extractDateFromContent(adrContent); if (contentDate) { timeline = { created_at: contentDate, updated_at: contentDate, extraction_method: 'content', }; } } // Final fallback to filesystem if (!timeline || !timeline.created_at) { timeline = await fallbackToFilesystem(adrPath); } // Calculate derived fields const stats = await stat(adrPath); const ageDays = calculateAgeDays(timeline.created_at!); const daysSinceUpdate = calculateAgeDays(timeline.updated_at!); const fullTimeline: BasicTimeline = { created_at: timeline.created_at!, updated_at: timeline.updated_at!, age_days: ageDays, days_since_update: daysSinceUpdate, staleness_warnings: generateBasicWarnings(ageDays, daysSinceUpdate), extraction_method: timeline.extraction_method as 'git' | 'content' | 'filesystem', }; // Cache result with file modification time for future checks if (useCache) { setCachedTimeline( `timeline:${adrPath}`, { ...fullTimeline, _fileModifiedAt: stats.mtime.toISOString(), _cachedAt: new Date().toISOString(), }, cacheTTL ); } return fullTimeline; } /** * Determine if timeline extraction should run */ async function shouldExtractTimeline( adrPath: string, adrContent: string ): Promise<{ shouldExtract: boolean; reason: string; useExisting?: string; }> { // Check if ADR content has a date field const dateInContent = extractDateFromContent(adrContent); // Check file modification time const stats = await stat(adrPath); const fileModifiedAt = stats.mtime; // Check cache const cacheKey = `timeline:${adrPath}`; const cached = getCachedTimeline(cacheKey); // CONDITION 1: No date in content → MUST extract if (!dateInContent) { return { shouldExtract: true, reason: 'No timestamp found in ADR content', }; } // CONDITION 2: Has date + cached + file unchanged → SKIP if (cached && cached._fileModifiedAt) { const cachedModTime = new Date(cached._fileModifiedAt); if (fileModifiedAt <= cachedModTime) { return { shouldExtract: false, reason: 'File unchanged since last extraction', useExisting: dateInContent, }; } } // CONDITION 3: Has date + file modified recently → RE-EXTRACT const daysSinceModification = (Date.now() - fileModifiedAt.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceModification < 7) { // Modified within last week return { shouldExtract: true, reason: `File modified ${Math.floor(daysSinceModification)} days ago`, }; } // CONDITION 4: Has date + old file → USE CONTENT DATE return { shouldExtract: false, reason: 'File has valid date and is stable (not recently modified)', useExisting: dateInContent, }; } /** * Extract date from ADR content using regex */ function extractDateFromContent(content: string): string | null { const dateMatch = content.match(/(?:##?\s*date|date:|\*\*date\*\*:)\s*(.+?)(?:\n|$)/i); if (dateMatch && dateMatch[1]) { const dateStr = dateMatch[1].trim(); try { // Validate it's a parseable date const date = new Date(dateStr); if (!isNaN(date.getTime())) { return date.toISOString(); } } catch { // Invalid date format } } return null; } /** * Create timeline from ADR content date (no git extraction needed) */ function createTimelineFromContent( _content: string, dateStr: string, reason: string ): BasicTimeline { let decisionDate: Date; try { decisionDate = new Date(dateStr); } catch { // Fallback to current time if parsing fails decisionDate = new Date(); } const isoDate = decisionDate.toISOString(); const ageDays = calculateAgeDays(isoDate); return { created_at: isoDate, updated_at: isoDate, // Assume same as creation age_days: ageDays, days_since_update: ageDays, staleness_warnings: generateBasicWarnings(ageDays, ageDays), extraction_method: 'content', extraction_skipped: true, skip_reason: reason, }; } /** * Try to extract timeline from git history */ async function tryGitExtraction(adrPath: string): Promise<Partial<BasicTimeline> | null> { try { // Creation date (first commit) const { stdout: created } = await execAsync( `git log --diff-filter=A --follow --format=%aI -- "${adrPath}" | tail -1`, { timeout: 5000 } ); // Last modification date const { stdout: updated } = await execAsync(`git log -1 --format=%aI -- "${adrPath}"`, { timeout: 5000, }); if (!created.trim() || !updated.trim()) { return null; } return { created_at: created.trim(), updated_at: updated.trim(), extraction_method: 'git', }; } catch (error) { // Git not available, not a git repo, or command failed console.warn(`[Timeline] Git extraction failed for ${adrPath}:`, error); return null; } } /** * Fallback to filesystem timestamps */ async function fallbackToFilesystem(adrPath: string): Promise<Partial<BasicTimeline>> { try { const stats = await stat(adrPath); return { created_at: stats.birthtime.toISOString(), updated_at: stats.mtime.toISOString(), extraction_method: 'filesystem', }; } catch { // Last resort: use current time const now = new Date().toISOString(); return { created_at: now, updated_at: now, extraction_method: 'filesystem', }; } } /** * Calculate age in days from ISO timestamp */ function calculateAgeDays(isoTimestamp: string): number { const date = new Date(isoTimestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); return Math.floor(diffMs / (1000 * 60 * 60 * 24)); } /** * Generate basic staleness warnings */ function generateBasicWarnings(ageDays: number, daysSinceUpdate: number): string[] { const warnings: string[] = []; // Warning: Old ADR if (ageDays > 180) { warnings.push(`ADR is ${ageDays} days old (>6 months)`); } // Warning: No recent updates if (daysSinceUpdate > 365) { warnings.push(`No updates in ${daysSinceUpdate} days (>1 year)`); } // Warning: Very old dormant ADR if (daysSinceUpdate > 730) { warnings.push(`Dormant for ${Math.floor(daysSinceUpdate / 365)} years - may be obsolete`); } return warnings; } /** * Helper to extract keywords from ADR content for implementation tracking */ export function extractKeywordsFromAdr(title: string, decision?: string): string[] { const text = `${title} ${decision || ''}`.toLowerCase(); const keywords: string[] = []; // Extract technology names const techPatterns = [ /\b(redis|postgres|mongodb|mysql|kafka|rabbitmq|docker|kubernetes|aws|gcp|azure)\b/gi, /\b(react|vue|angular|node|express|fastapi|django|flask|spring)\b/gi, /\b(graphql|rest|grpc|websocket|oauth|jwt|saml)\b/gi, ]; techPatterns.forEach(pattern => { const matches = text.match(pattern); if (matches) { keywords.push(...matches.map(m => m.toLowerCase())); } }); // Extract quoted terms (likely important concepts) const quoted = text.match(/"([^"]+)"/g); if (quoted) { keywords.push(...quoted.map(q => q.replace(/"/g, '').toLowerCase())); } // Remove duplicates return [...new Set(keywords)]; }
- src/utils/adr-timeline-types.ts:5-25 (helper)Type definitions for ADR timeline data structures used by the timeline extractor and analyzer.*/ /** * Basic timeline information for an ADR * Extracted automatically from git history, content, or filesystem */ export interface BasicTimeline { /** When the ADR was created (ISO timestamp) */ created_at: string; /** When the ADR was last updated (ISO timestamp) */ updated_at: string; /** Age of the ADR in days */ age_days: number; /** Days since last update */ days_since_update: number; /** Staleness warnings based on basic timeline analysis */ staleness_warnings: string[]; /** How the timeline was extracted */ extraction_method: 'git' | 'content' | 'filesystem'; /** Whether extraction was skipped due to smart detection */ extraction_skipped?: boolean;
- src/utils/adr-discovery.ts:210-220 (helper)Usage of the timeline extractor in ADR discovery utility, demonstrating integration point for timeline analysis.const { extractBasicTimeline } = await import('./adr-timeline-extractor.js'); adr.timeline = await extractBasicTimeline(filePath, content, timelineOptions); } catch (error) { console.warn(`[Timeline] Failed to extract timeline for ${filename}:`, error); // Continue without timeline data } } discoveredAdrs.push(adr); } } catch (error) {