Skip to content

dowsing.json 结构

dowsing analyze 把结果写入 .dowsing/dowsing.json。CLI 显示、HTML 报告与 CI 集成全都消费这一个文件——因此无需重新分析就能得到多种输出。

结构用 zod 定义,并在每次写出时自我校验:输出即结构。

schemaVersion 为 0 期间没有兼容性保证

字段可能在没有预告的情况下增加或变更。在它稳定之前,把它接入自动化时请谨慎。

整体形状

ts
{
  schemaVersion: 0,
  meta: { ... },          // 生成时间、git 窗口、排除计数
  workspace: { ... },     // 包列表与检出的工具
  files: [ ... ],         // 按 hotspot 分数降序
  packages: [ ... ],      // 按 hotspotSum 降序
  packageGraph: { ... },  // 声明的依赖 + 实际的 import
  coupling: { ... },      // 变更耦合(无 git 历史时为 null)
  findings: [ ... ],      // 合成洞察,按 severity 降序
  authors: [ ... ],       // 贡献者,已做 mailmap 归一与 bot 剔除
  config: { ... }         // 所用的公式、阈值与权重
}

meta

ts
{
  generatedAt: string,      // ISO 8601
  repoRoot: string,
  toolVersion: string,
  git: {
    since: string,
    until: string | null,
    commitCount: number,    // 排除之后
    shallow: boolean,       // 为 true 时不要相信结果
    exclusions: {
      merges: number,
      bots: number,
      formatOnly: number,
      ignoreRevs: number,
      largeChangesets: number   // 只从耦合中排除,churn 中保留
    }
  } | null                  // 没有 git 历史时为 null
}

排除计数总是包含在内,因为一个你看不出范围的数字,是无法据以行动的

files[]

ts
{
  path: string,             // 相对仓库根目录,posix 风格
  package: string | null,
  lines: number,
  loc: number,              // 有效行数,剔除注释与空行
  complexity: {
    cyclomatic: number,     // 文件合计
    cyclomaticMax: number,  // 最复杂的函数
    cognitive: number,
    cognitiveMax: number
  },
  churn: {
    commits: number,
    authors: number,
    addedLines: number,
    deletedLines: number,
    firstSeen: string,
    lastChanged: string
  } | null,
  hotspot: {
    score: number,          // rank(freq) × rank(complexity)
    components: {
      changeFrequency: number,   // 原始值
      complexity: number,        // 原始值
      frequencyRank: number,     // 百分位
      complexityRank: number,
      frequencyMinmax: number,   // 同时保留 min-max 归一化
      complexityMinmax: number
    }
  },
  churnVerification: {      // 仅在 --verify-churn 时存在
    rawCommits: number,
    effectiveCommits: number,
    effectiveRatio: number,
    breakdown: {
      changed: number,
      "rename-only": number,
      "format-only": number,
      identical: number,
      unknown: number
    }
  } | null,
  health: {
    score: number,          // 1.0–10.0
    smells: [{
      kind: "complex-method" | "high-cognitive-complexity"
          | "large-file" | "many-arguments",
      severity: number,     // 0–1
      penalty: number,      // 这个坏味道扣掉的分数
      value: number,        // 实测值
      threshold: number,
      locations: string[],  // 函数名与行号
      explanation: string   // 为什么是问题
    }],
    locBaselineScore: number,  // 只用 LoC 会得到的分数
    baselineDelta: number      // score − locBaselineScore
  },
  ownership: {
    mainDeveloper: string | null,   // 按新增行数
    mainDeveloperShare: number,
    minorContributors: number,      // ownership < 5%
    contributors: number
  } | null
}

hotspot.components 保留原始值与两种归一化,是为了让你可以手工复算这个分数

packages[]

ts
{
  package: string,
  files: number,
  loc: number,
  churnCommits: number,
  hotspotSum: number,
  hotspotLocWeighted: number,
  health: number | null,          // LoC 加权平均
  busFactor: {
    busFactor: number,
    files: number,
    orphanedRatio: number,
    keyDevelopers: [{ email: string, ownedFiles: number }]
  } | null
}

packageGraph

ts
{
  nodes: string[],
  edges: [{
    from: string,
    to: string,
    declared: boolean,        // 在 package.json 中声明
    imported: boolean,        // 实际被 import
    importingFiles: number,
    typeOnly: boolean         // 全部是 type-only import
  }]
}

declared && !imported 是无用依赖;!declared && imported 是幽灵依赖。

coupling

ts
{
  filters: {                  // 应用的过滤条件——公式透明性
    minRevisions: number,
    minShared: number,
    minDegree: number,
    maxChangesetSize: number,
    limit: number
  },
  excludedLargeChangesets: number,
  files: [{ a, b, shared, revisionsA, revisionsB,
            degree, confidenceAtoB, confidenceBtoA }],
  packages: [{ ...同上,
    kind: "hidden" | "type-only" | "declared-only" | "obvious",
    dependencyDirection: "a->b" | "b->a" | "both" | null
  }]
} | null

findings[]

ts
{
  kind: "hotspot-file" | "hidden-coupling" | "hotspot-package"
      | "bus-factor" | "unhealthy-package" | "unused-dependency",
  target: string,
  severity: number,
  summary: string,
  evidence: Record<string, string | number>   // 背后的数字
}

evidence 总是携带分数的依据。没有依据的 finding 不会被输出。

config

ts
{
  complexity: "loc" | "cyclomatic" | "cognitive",
  healthThresholds: { cyclomatic, cognitive, fileLoc, arguments },
  healthWeights: Record<string, number>
}

阈值与权重是输出的一部分,这样复现这个分数所需的一切都在文件里

示例

bash
dowsing analyze --json | jq '.files[0:5] | .[] | {path, score: .hotspot.score}'
bash
# health 最低的十个
dowsing analyze --json | jq '[.files[] | {path, health: .health.score}] | sort_by(.health) | .[0:10]'

基于 MIT 许可发布