Skip to content

dowsing.json schema

dowsing analyze writes its result to .dowsing/dowsing.json. The CLI display, the HTML report, and the CI integration all consume that single file — so you can get several outputs without re-analysing.

The schema is defined with zod and self-validated on every write: the output is the schema.

No compatibility guarantee while schemaVersion is 0

Fields may be added or changed without notice. Be careful about building automation on it until it stabilises.

Overall shape

ts
{
  schemaVersion: 0,
  meta: { ... },          // generation time, git window, exclusion counts
  workspace: { ... },     // package list and detected tooling
  files: [ ... ],         // descending hotspot score
  packages: [ ... ],      // descending hotspotSum
  packageGraph: { ... },  // declared dependencies + actual imports
  coupling: { ... },      // change coupling (null without git history)
  findings: [ ... ],      // synthesised insights, descending severity
  authors: [ ... ],       // contributors, after mailmap and bot removal
  config: { ... }         // the formulas, thresholds, and weights used
}

meta

ts
{
  generatedAt: string,      // ISO 8601
  repoRoot: string,
  toolVersion: string,
  git: {
    since: string,
    until: string | null,
    commitCount: number,    // after exclusions
    shallow: boolean,       // if true, do not trust the result
    exclusions: {
      merges: number,
      bots: number,
      formatOnly: number,
      ignoreRevs: number,
      largeChangesets: number   // excluded from coupling only, kept in churn
    }
  } | null                  // null when there is no git history
}

Exclusion counts are always included because a number whose scope you cannot see is not a number you can act on.

files[]

ts
{
  path: string,             // repo-root relative, posix
  package: string | null,
  lines: number,
  loc: number,              // effective lines, comments and blanks removed
  complexity: {
    cyclomatic: number,     // file total
    cyclomaticMax: number,  // the most complex function
    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,   // raw
      complexity: number,        // raw
      frequencyRank: number,     // percentile
      complexityRank: number,
      frequencyMinmax: number,   // min-max normalisation kept too
      complexityMinmax: number
    }
  },
  churnVerification: {      // only with --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,      // points this smell cost
      value: number,        // measured value
      threshold: number,
      locations: string[],  // function names and line numbers
      explanation: string   // why it is a problem
    }],
    locBaselineScore: number,  // the score you would get from LoC alone
    baselineDelta: number      // score − locBaselineScore
  },
  ownership: {
    mainDeveloper: string | null,   // by added lines
    mainDeveloperShare: number,
    minorContributors: number,      // ownership < 5%
    contributors: number
  } | null
}

hotspot.components keeps the raw values and both normalisations so that you can recompute the score by hand.

packages[]

ts
{
  package: string,
  files: number,
  loc: number,
  churnCommits: number,
  hotspotSum: number,
  hotspotLocWeighted: number,
  health: number | null,          // LoC-weighted mean
  busFactor: {
    busFactor: number,
    files: number,
    orphanedRatio: number,
    keyDevelopers: [{ email: string, ownedFiles: number }]
  } | null
}

packageGraph

ts
{
  nodes: string[],
  edges: [{
    from: string,
    to: string,
    declared: boolean,        // declared in package.json
    imported: boolean,        // actually imported
    importingFiles: number,
    typeOnly: boolean         // every import is type-only
  }]
}

declared && !imported is an unused dependency; !declared && imported is a phantom dependency.

coupling

ts
{
  filters: {                  // the filters applied — formula transparency
    minRevisions: number,
    minShared: number,
    minDegree: number,
    maxChangesetSize: number,
    limit: number
  },
  excludedLargeChangesets: number,
  files: [{ a, b, shared, revisionsA, revisionsB,
            degree, confidenceAtoB, confidenceBtoA }],
  packages: [{ ...same as above,
    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>   // the numbers behind it
}

evidence always carries the basis for the score. A finding without evidence is not emitted.

config

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

Thresholds and weights are part of the output so that everything needed to reproduce the score is in the file.

Examples

bash
dowsing analyze --json | jq '.files[0:5] | .[] | {path, score: .hotspot.score}'
bash
# the ten lowest health scores
dowsing analyze --json | jq '[.files[] | {path, health: .health.score}] | sort_by(.health) | .[0:10]'

Released under the MIT License