Skip to content

TDI 算法

TDI(Technical Debt Index)是 Git Doctor 创新的技术债务量化算法。

算法概述

TDI = Σ(Churn × BugFactor × Complexity × TimeDecay)

TDI 综合考虑四个维度,将抽象的"技术债务"转化为可量化的指数。

各维度详解

1. Churn(修改频率)- 权重 40%

文件的修改次数反映其稳定性。

typescript
function calculateChurn(fileChurnCount: number, maxChurnCount: number): number {
  // 归一化到 0-1
  return fileChurnCount / maxChurnCount
}

理论依据:

  • 频繁修改的文件往往存在设计问题
  • 修改次数越多,引入 Bug 的概率越高
  • "热点"文件通常是重构的重点目标

2. BugFactor(Bug 关联度)- 权重 35%

与 Bug 修复相关的提交占比。

typescript
function calculateBugFactor(bugFixCount: number, totalChanges: number): number {
  if (totalChanges === 0) return 0
  return bugFixCount / totalChanges
}

function isFixCommit(message: string): boolean {
  const lowerMessage = message.toLowerCase()

  // Conventional Commits 格式
  if (/^(fix|hotfix|bugfix)(\(.+\))?:/.test(lowerMessage)) {
    return true
  }

  // 关键词匹配
  const fixKeywords = [
    'fix', 'bug', 'issue', '修复', '修正', '解决',
    'patch', 'hotfix', 'bugfix'
  ]
  return fixKeywords.some(kw => lowerMessage.includes(kw))
}

理论依据:

  • Bug 修复率高说明代码质量有问题
  • 反复修复同一区域说明存在深层设计缺陷
  • 高 BugFactor 文件应优先重构

3. Complexity(复杂度)- 权重 15%

文件大小作为复杂度的近似指标。

typescript
function calculateComplexity(fileSize: number): number {
  const baseSize = 10 * 1024  // 10KB 作为基准
  const ratio = fileSize / baseSize

  // 限制在 0-1 范围,超大文件封顶
  return Math.min(ratio, 3) / 3
}

理论依据:

  • 大文件通常包含更多逻辑,更难维护
  • 文件过大可能违反单一职责原则
  • 未来可扩展为圈复杂度等更精确指标

4. TimeDecay(时间衰减)- 权重 10%

近期修改的权重更高。

typescript
function calculateTimeDecay(lastModified: Date): number {
  const now = new Date()
  const msPerDay = 1000 * 60 * 60 * 24
  const daysSinceModified = (now.getTime() - lastModified.getTime()) / msPerDay

  // 180天内线性衰减:1.0 → 0.5
  if (daysSinceModified <= 180) {
    return 1.0 - (daysSinceModified / 180) * 0.5
  }

  return 0.5  // 180天后保持最低权重
}

理论依据:

  • 近期活跃的问题更紧迫
  • 长期未修改的文件可能已稳定
  • 但也不能完全忽视历史问题

完整计算流程

typescript
interface FileStats {
  path: string
  churnCount: number
  bugFixCount: number
  fileSize: number
  lastModified: Date
}

function calculateTDI(
  file: FileStats,
  maxChurnCount: number
): number {
  const weights = {
    churn: 0.40,
    bugFactor: 0.35,
    complexity: 0.15,
    timeDecay: 0.10
  }

  const churn = calculateChurn(file.churnCount, maxChurnCount)
  const bugFactor = calculateBugFactor(file.bugFixCount, file.churnCount)
  const complexity = calculateComplexity(file.fileSize)
  const timeDecay = calculateTimeDecay(file.lastModified)

  const tdi = (
    churn * weights.churn +
    bugFactor * weights.bugFactor +
    complexity * weights.complexity +
    timeDecay * weights.timeDecay
  ) * 100  // 转换为 0-100 分数

  return Math.round(tdi * 10) / 10  // 保留一位小数
}

风险级别划分

TDI 范围风险级别颜色行动建议
0-30🟢正常维护
31-60🟡持续关注
61-80🟠计划重构
81-100严重🔴优先处理

计算示例

示例文件

文件: src/core/parser.ts
修改次数: 45 次(最大 50 次)
Bug 修复: 12 次
文件大小: 25KB
最后修改: 15 天前

计算过程

Churn = 45/50 = 0.90
BugFactor = 12/45 = 0.27
Complexity = min(25/10, 3)/3 = 0.83
TimeDecay = 1.0 - (15/180)*0.5 = 0.96

TDI = (0.90×0.40 + 0.27×0.35 + 0.83×0.15 + 0.96×0.10) × 100
    = (0.36 + 0.09 + 0.12 + 0.10) × 100
    = 67.0

风险级别: 🟠 高风险

聚合分析

模块级 TDI

typescript
function calculateModuleTDI(files: FileStats[]): number {
  const tdis = files.map(f => calculateTDI(f, getMaxChurn(files)))
  return tdis.reduce((sum, t) => sum + t, 0) / tdis.length
}

仓库级 TDI

typescript
function calculateRepoTDI(allFiles: FileStats[]): {
  average: number
  critical: FileStats[]
  trend: 'improving' | 'stable' | 'degrading'
} {
  const tdis = allFiles.map(f => ({
    file: f,
    tdi: calculateTDI(f, getMaxChurn(allFiles))
  }))

  return {
    average: tdis.reduce((sum, t) => sum + t.tdi, 0) / tdis.length,
    critical: tdis.filter(t => t.tdi > 80).map(t => t.file),
    trend: calculateTrend(tdis)
  }
}

与其他指标对比

指标优点缺点
代码行数简单不反映质量
圈复杂度精确计算成本高
代码覆盖率直观不反映修改频率
TDI综合、基于历史需要提交历史

局限性

  1. 依赖提交历史质量 - 如果提交消息不规范,BugFactor 可能不准确
  2. 文件大小近似复杂度 - 不如圈复杂度精确
  3. 无法检测未修改的坏代码 - 只关注活跃区域

扩展方向

未来可考虑引入:

  • 圈复杂度(需要代码解析)
  • 代码重复率
  • 测试覆盖率
  • 依赖复杂度

基于 MIT 许可发布