simcov-ai-formatter

Ask DeepWiki

A SimpleCov formatter that emits a JSON format optimized for AI / LLM consumption — per-file summaries, uncovered ranges, and optional source snippets.

Why this exists

SimpleCov's .resultset.json records coverage as position-dependent arrays:

{ "RSpec": { "coverage": { "/abs/path/foo.rb": { "lines": [null, 1, 0, 0, null, 5] } } } }

lines is a 1-indexed hit-count array (null = irrelevant, 0 = uncovered, Integer >= 1 = hit count). For an LLM to figure out "which file has which uncovered lines" from this shape, it has to scan arrays, compute summaries, and normalize paths every single time.

This gem does that preprocessing once, deterministically, and in a token-efficient way.

Installation

gem install simcov-ai-formatter

Or in Gemfile:

group :development, :test do
  gem "simcov-ai-formatter"
end

Output schema

Default

{
  "schema_version": 1,
  "suite": "RSpec",
  "root": "/Users/me/proj",
  "summary": {
    "total_files": 42,
    "relevant_lines": 1830,
    "covered_lines": 1644,
    "missed_lines": 186,
    "coverage_percentage": 89.84
  },
  "files": {
    "lib/foo.rb": {
      "relevant_lines": 50,
      "covered_lines": 45,
      "missed_lines": 5,
      "coverage_percentage": 90.0,
      "uncovered_ranges": [
        { "start": 12, "end": 14 },
        { "start": 88, "end": 88 }
      ],
      "uncovered_lines": [12, 13, 14, 88]
    }
  }
}

When multiple suites are merged, a top-level "suites_merged": ["RSpec", "Cucumber"] is emitted.

With with_source: true, context: 2

Each uncovered_ranges entry gains a source array:

{
  "start": 12, "end": 14,
  "source": [
    { "line": 10, "text": "def parse(input)",        "covered": true },
    { "line": 11, "text": "  return nil if input.nil?", "covered": true },
    { "line": 12, "text": "  raise ArgumentError",     "covered": false },
    { "line": 13, "text": "  log_error(input)",        "covered": false },
    { "line": 14, "text": "  nil",                     "covered": false },
    { "line": 15, "text": "end",                       "covered": true }
  ]
}

If the source file is missing, the range gets "source": null, "source_error": "missing" and a warning summary is emitted to stderr (processing continues).

Branch coverage

If branches exists in the resultset, each file gets a branches_raw field containing the resultset's original key shape unchanged. Structured form ({ type: "if", line: ..., then_hits: ..., else_hits: ... }) is planned for v0.2.0.

Schema details

  • relevant_lines excludes null entries (matches SimpleCov convention).
  • Files with all null lines (comments / blanks only) report relevant_lines: 0, coverage_percentage: 100.0 and are excluded from the project-level denominator.
  • coverage_percentage is rounded to 2 decimal places.
  • Files outside root (e.g. third-party gems) are kept under keys of the form !abs:/abs/path.
  • The output contains no timestamp — the JSON is deterministic.

As a SimpleCov formatter

Plug simcov-ai-formatter into SimpleCov's formatter pipeline to emit the AI-friendly JSON automatically as part of your test run.

# spec/spec_helper.rb (or .simplecov)
require "simplecov"
require "simcov_ai_formatter/simple_cov_formatter"

SimpleCov.start do
  # ... your usual SimpleCov config
end

# Replace the default formatter
SimpleCov.formatter = SimcovAiFormatter::SimpleCovFormatter

# Or run alongside the HTML formatter
SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.create([
  SimpleCov::Formatter::HTMLFormatter,
  SimcovAiFormatter::SimpleCovFormatter
])

After tests finish, the AI-friendly JSON is written to coverage/.resultset.ai.json (or wherever SimpleCov.coverage_path points).

Configuration

Set class-level attributes before SimpleCov.start:

SimcovAiFormatter::SimpleCovFormatter.with_source = true
SimcovAiFormatter::SimpleCovFormatter.context     = 3
SimcovAiFormatter::SimpleCovFormatter.pretty      = false
SimcovAiFormatter::SimpleCovFormatter.output_path = "tmp/coverage.ai.json"  # nil = coverage/.resultset.ai.json

Programmatic use

The same logic is callable from Ruby:

require "simcov_ai_formatter"

result = SimcovAiFormatter.format(
  "coverage/.resultset.json",
  root: Dir.pwd,
  with_source: true,
  context: 2
)

# result is a Hash
puts result["summary"]["coverage_percentage"]

Development

bundle install
bundle exec rake test                   # run all tests
UPDATE_GOLDEN=1 bundle exec rake test   # regenerate golden files

License

MIT