simplecov-ai 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +88 -0
- data/lib/simplecov-ai/ast_resolver.rb +65 -35
- data/lib/simplecov-ai/markdown_builder/bypass_compiler.rb +64 -0
- data/lib/simplecov-ai/markdown_builder/deficit_compiler.rb +129 -0
- data/lib/simplecov-ai/markdown_builder/deficit_grouper.rb +65 -0
- data/lib/simplecov-ai/markdown_builder/snippet_formatter.rb +76 -0
- data/lib/simplecov-ai/markdown_builder.rb +51 -117
- data/lib/simplecov-ai/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +47 -15
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b180d86c54ffab95578a779a8765ec546495b6bc3bd1e353674cf81697b9afe
|
|
4
|
+
data.tar.gz: f86ba3e7b0b6e5b3ded6c40799424fded61d2f2d980cb92e784685d340ac31f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cd0db7e047ccb21fdfc6ccf1c4e2855542c209051b6a0f663ec8037deddf6a4b8481865c492343ccee81f9ed42e5b937d0286b5d5f18dec0e07897aaf6cd902
|
|
7
|
+
data.tar.gz: 92f1698f205c2ff941287b45c53290e52306e8e9b1ae3916e84477917c8045d3ed17c5a2653c33a4ea842e837b9c96521a3bb85bc44799e8135962ae65346f1f
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -1 +1,89 @@
|
|
|
1
1
|
# simplecov-ai
|
|
2
|
+
|
|
3
|
+
A custom `SimpleCov::Formatter` designed explicitly for consumption by Large Language Models (LLMs) and autonomous engineering agents.
|
|
4
|
+
|
|
5
|
+
Standard coverage reporters generate massive HTML files or exhaustive JSON/console outputs detailing every line number. This overwhelms strict LLM token constraints and relies on highly volatile line numbers. `simplecov-ai` solves this by generating a concise, structurally optimized **Markdown document** containing only the exact missing semantic coverage blocks.
|
|
6
|
+
|
|
7
|
+
## Why use simplecov-ai?
|
|
8
|
+
|
|
9
|
+
- **Semantic Resolution:** Instead of volatile line numbers, missing coverage is resolved via Abstract Syntax Tree (AST) mapping into immutable semantic groupings (e.g., Class, Module, Instance Method).
|
|
10
|
+
- **Maximum Token Conservation:** Fully covered files are completely omitted. If the report exceeds size limits, it safely truncates the output prioritizing the lowest-coverage files.
|
|
11
|
+
- **Actionable Delta Directives:** Missing branches and lines are mapped directly to method names, letting the AI instantly search the code and write targeted specs.
|
|
12
|
+
- **Directive Auditing:** Explicitly reports `:nocov:` bypasses, ensuring artificial metric inflation is completely transparent to the reviewing AI.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's `Gemfile` strictly in the `test` group:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
group :test do
|
|
20
|
+
gem 'simplecov'
|
|
21
|
+
gem 'simplecov-ai', require: false
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage & Configuration
|
|
26
|
+
|
|
27
|
+
Require and configure the formatter in your test helper (`spec_helper.rb` or `test_helper.rb`) after requiring `simplecov`:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'simplecov'
|
|
31
|
+
require 'simplecov-ai'
|
|
32
|
+
|
|
33
|
+
# Optional Configuration (defaults shown below):
|
|
34
|
+
SimpleCov::Formatter::AIFormatter.configure do |config|
|
|
35
|
+
config.report_path = 'coverage/ai_report.md' # Output location
|
|
36
|
+
config.max_file_size_kb = 50 # Maximum size (Token Ceiling)
|
|
37
|
+
config.max_snippet_lines = 5 # AST context truncation limit
|
|
38
|
+
config.output_to_console = false # Echo digest to STDOUT
|
|
39
|
+
config.granularity = :fine # :fine (statements) or :coarse (methods)
|
|
40
|
+
config.include_bypasses = true # Audit `:nocov:` ignores
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
SimpleCov.start do
|
|
44
|
+
# Combine with your existing formatters
|
|
45
|
+
SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
|
|
46
|
+
SimpleCov::Formatter::HTMLFormatter,
|
|
47
|
+
SimpleCov::Formatter::AIFormatter
|
|
48
|
+
])
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Example Output
|
|
53
|
+
|
|
54
|
+
The output is written to `coverage/ai_report.md` (or your configured path), perfect for providing directly as context to an LLM:
|
|
55
|
+
|
|
56
|
+
```md
|
|
57
|
+
# AI Coverage Digest
|
|
58
|
+
**Status:** FAILED
|
|
59
|
+
**Global Line Coverage:** 92.5%
|
|
60
|
+
**Global Branch Coverage:** 88.0%
|
|
61
|
+
**Generated At:** 2026-04-21T23:40:44+09:00 (Local Timezone)
|
|
62
|
+
**Report File Size:** 1.2 kB
|
|
63
|
+
|
|
64
|
+
## Coverage Deficits
|
|
65
|
+
|
|
66
|
+
### `lib/my_gem/client.rb`
|
|
67
|
+
- `MyGem::Client#authenticate!`
|
|
68
|
+
- **Branch Deficit:** Missing coverage for conditional evaluation handling `ExpiredTokenError`.
|
|
69
|
+
- `MyGem::Client#initialize`
|
|
70
|
+
- **Line Deficit:** Variable initialization state uncovered.
|
|
71
|
+
|
|
72
|
+
### `lib/my_gem/parser/processor.rb`
|
|
73
|
+
- `MyGem::Parser::Processor.parse_stream`
|
|
74
|
+
- **Branch Deficit:** Missing coverage for early-exit condition `break if stream.closed?` (Occurrence 1 of 2).
|
|
75
|
+
|
|
76
|
+
## Ignored Coverage Bypasses
|
|
77
|
+
|
|
78
|
+
### `lib/my_gem/legacy_handler.rb`
|
|
79
|
+
- `MyGem::LegacyHandler#obsolete_action`
|
|
80
|
+
- **Bypass Present:** Contains `:nocov:` directive artificially ignoring coverage (Occurrence 1 of 1).
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Error Handling
|
|
84
|
+
|
|
85
|
+
Adhering to fail-fast principles, if the AST parser encounters structurally unparseable Ruby code or corrupt telemetry, it will gracefully degrade or explicitly fail. It will not silently ignore failures or emit corrupted artifacts.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -78,61 +78,91 @@ module SimpleCov
|
|
|
78
78
|
return [] unless node.is_a?(Parser::AST::Node)
|
|
79
79
|
|
|
80
80
|
nodes = T.let([], T::Array[SemanticNode])
|
|
81
|
-
current_context = context
|
|
81
|
+
current_context, semantic_node = extract_node_metadata(node, comments, context)
|
|
82
|
+
nodes << semantic_node if semantic_node
|
|
82
83
|
|
|
84
|
+
node.children.grep(Parser::AST::Node).each do |child|
|
|
85
|
+
nodes.concat(traverse(child, comments, current_context))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
nodes
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
sig do
|
|
94
|
+
params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
|
|
95
|
+
.returns([String, T.nilable(SemanticNode)])
|
|
96
|
+
end
|
|
97
|
+
def extract_node_metadata(node, comments, context)
|
|
83
98
|
case node.type
|
|
84
99
|
when :class, :module
|
|
85
|
-
|
|
86
|
-
const_node_loc = T.cast(const_node.loc, Parser::Source::Map::Constant)
|
|
87
|
-
const_node_name = T.cast(const_node_loc.name, Parser::Source::Range)
|
|
88
|
-
name = T.cast(const_node_name.source, String)
|
|
89
|
-
current_context = context.empty? ? name : "#{context}::#{name}"
|
|
90
|
-
nodes << build_node(node, comments, current_context, node.type.to_s.capitalize)
|
|
100
|
+
extract_class_metadata(node, comments, context)
|
|
91
101
|
when :def
|
|
92
|
-
|
|
93
|
-
current_context = context.empty? ? "##{name}" : "#{context}##{name}"
|
|
94
|
-
nodes << build_node(node, comments, current_context, 'Instance Method')
|
|
102
|
+
extract_instance_method_metadata(node, comments, context)
|
|
95
103
|
when :defs
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
extract_singleton_method_metadata(node, comments, context)
|
|
105
|
+
else
|
|
106
|
+
[context, nil]
|
|
99
107
|
end
|
|
108
|
+
end
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
sig do
|
|
111
|
+
params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
|
|
112
|
+
.returns([String, T.nilable(SemanticNode)])
|
|
113
|
+
end
|
|
114
|
+
def extract_class_metadata(node, comments, context)
|
|
115
|
+
const_node = T.cast(node.children[0], Parser::AST::Node)
|
|
116
|
+
const_node_name = T.cast(T.cast(const_node.loc, Parser::Source::Map::Constant).name, Parser::Source::Range)
|
|
117
|
+
name = T.cast(const_node_name.source, String)
|
|
118
|
+
ctx = context.empty? ? name : "#{context}::#{name}"
|
|
119
|
+
[ctx, build_node(node, comments, ctx, node.type.to_s.capitalize)]
|
|
120
|
+
end
|
|
107
121
|
|
|
108
|
-
|
|
122
|
+
sig do
|
|
123
|
+
params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
|
|
124
|
+
.returns([String, T.nilable(SemanticNode)])
|
|
125
|
+
end
|
|
126
|
+
def extract_instance_method_metadata(node, comments, context)
|
|
127
|
+
name = T.cast(node.children.first, Symbol).to_s
|
|
128
|
+
ctx = context.empty? ? "##{name}" : "#{context}##{name}"
|
|
129
|
+
[ctx, build_node(node, comments, ctx, 'Instance Method')]
|
|
109
130
|
end
|
|
110
131
|
|
|
111
|
-
|
|
132
|
+
sig do
|
|
133
|
+
params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
|
|
134
|
+
.returns([String, T.nilable(SemanticNode)])
|
|
135
|
+
end
|
|
136
|
+
def extract_singleton_method_metadata(node, comments, context)
|
|
137
|
+
name = T.cast(node.children[1], Symbol).to_s
|
|
138
|
+
ctx = context.empty? ? ".#{name}" : "#{context}.#{name}"
|
|
139
|
+
[ctx, build_node(node, comments, ctx, 'Singleton Method')]
|
|
140
|
+
end
|
|
112
141
|
|
|
113
142
|
sig do
|
|
114
143
|
params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], name: String,
|
|
115
144
|
type: String).returns(SemanticNode)
|
|
116
145
|
end
|
|
117
146
|
def build_node(node, comments, name, type)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
loc = T.cast(node.loc, Parser::Source::Map)
|
|
148
|
+
start_ln = T.cast(loc.line, Integer)
|
|
149
|
+
end_ln = T.cast(loc.last_line, Integer)
|
|
150
|
+
bypasses = extract_bypasses(comments, start_ln, end_ln)
|
|
151
|
+
SemanticNode.new(name: name, type: type, start_line: start_ln, end_line: end_ln, bypasses: bypasses)
|
|
152
|
+
end
|
|
121
153
|
|
|
122
|
-
|
|
154
|
+
sig do
|
|
155
|
+
params(comments: T::Array[Parser::Source::Comment], start_line: Integer, end_line: Integer)
|
|
156
|
+
.returns(T::Array[String])
|
|
157
|
+
end
|
|
158
|
+
def extract_bypasses(comments, start_line, end_line)
|
|
159
|
+
matched = comments.select do |c|
|
|
123
160
|
c_loc = T.cast(c.loc, Parser::Source::Map)
|
|
124
161
|
c_line = T.cast(c_loc.line, Integer)
|
|
125
162
|
c_text = T.cast(c.text, String)
|
|
126
|
-
c_line
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
SemanticNode.new(
|
|
130
|
-
name: name,
|
|
131
|
-
type: type,
|
|
132
|
-
start_line: node_line,
|
|
133
|
-
end_line: node_last_line,
|
|
134
|
-
bypasses: bypasses
|
|
135
|
-
)
|
|
163
|
+
c_line.between?(start_line - 1, end_line + 1) && c_text.include?(':nocov:')
|
|
164
|
+
end
|
|
165
|
+
matched.map { |c| T.cast(c.text, String).strip }
|
|
136
166
|
end
|
|
137
167
|
end
|
|
138
168
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
class MarkdownBuilder
|
|
8
|
+
# Scans resolved AST blocks to report explicitly defined coverage ignores (e.g., :nocov:).
|
|
9
|
+
class BypassCompiler
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(result: SimpleCov::Result, builder: MarkdownBuilder).void }
|
|
13
|
+
def initialize(result, builder)
|
|
14
|
+
@result = result
|
|
15
|
+
@builder = builder
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(buffer: StringIO).void }
|
|
19
|
+
def write_bypasses(buffer)
|
|
20
|
+
bypass_buffer = T.let(StringIO.new, StringIO)
|
|
21
|
+
has_bypasses = compile_all_bypasses(bypass_buffer)
|
|
22
|
+
|
|
23
|
+
return unless has_bypasses
|
|
24
|
+
|
|
25
|
+
buffer.puts "## Ignored Coverage Bypasses\n\n"
|
|
26
|
+
buffer.puts bypass_buffer.string
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
sig { params(buffer: StringIO).returns(T::Boolean) }
|
|
32
|
+
def compile_all_bypasses(buffer)
|
|
33
|
+
has_bypasses = T.let(false, T::Boolean)
|
|
34
|
+
T.let(@result.files.to_a, T::Array[SimpleCov::SourceFile]).each do |file|
|
|
35
|
+
bypassed = fetch_bypassed_nodes(file.filename)
|
|
36
|
+
next if bypassed.empty?
|
|
37
|
+
|
|
38
|
+
has_bypasses = true
|
|
39
|
+
write_file_bypasses(buffer, file, bypassed)
|
|
40
|
+
end
|
|
41
|
+
has_bypasses
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { params(filename: String).returns(T::Array[ASTResolver::SemanticNode]) }
|
|
45
|
+
def fetch_bypassed_nodes(filename)
|
|
46
|
+
nodes = @builder.try_resolve_ast(filename)
|
|
47
|
+
nodes ? nodes.select { |n| n.bypasses.any? } : []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig do
|
|
51
|
+
params(buffer: StringIO, file: SimpleCov::SourceFile, bypasses: T::Array[ASTResolver::SemanticNode]).void
|
|
52
|
+
end
|
|
53
|
+
def write_file_bypasses(buffer, file, bypasses)
|
|
54
|
+
buffer.puts "### `#{file.project_filename}`"
|
|
55
|
+
bypasses.each do |node|
|
|
56
|
+
buffer.puts "- `#{node.name}`\n - **Bypass Present:** Contains `:nocov:` directive."
|
|
57
|
+
end
|
|
58
|
+
buffer.puts ''
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
class MarkdownBuilder
|
|
8
|
+
# Iterates through files with coverage deficits and coordinates their AST parsing and snippet generation.
|
|
9
|
+
class DeficitCompiler
|
|
10
|
+
extend T::Sig
|
|
11
|
+
include SnippetFormatter
|
|
12
|
+
|
|
13
|
+
sig { params(result: SimpleCov::Result, config: Configuration, builder: MarkdownBuilder).void }
|
|
14
|
+
def initialize(result, config, builder)
|
|
15
|
+
@result = result
|
|
16
|
+
@config = config
|
|
17
|
+
@builder = builder
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(buffer: StringIO).void }
|
|
21
|
+
def write_deficits(buffer)
|
|
22
|
+
files = T.let(
|
|
23
|
+
@result.files.reject { |f| f.covered_percent >= 100.0 }.sort_by { |f| [f.covered_percent, f.filename] },
|
|
24
|
+
T::Array[SimpleCov::SourceFile]
|
|
25
|
+
)
|
|
26
|
+
return if files.empty?
|
|
27
|
+
|
|
28
|
+
buffer.puts "## Coverage Deficits\n\n"
|
|
29
|
+
files.each do |file|
|
|
30
|
+
break if @builder.truncate_if_needed?
|
|
31
|
+
|
|
32
|
+
process_file(buffer, file)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
sig { params(buffer: StringIO, file: SimpleCov::SourceFile).void }
|
|
39
|
+
def process_file(buffer, file)
|
|
40
|
+
buffer.puts "### `#{file.project_filename}`"
|
|
41
|
+
nodes = @builder.try_resolve_ast(file.filename)
|
|
42
|
+
nodes ? process_deficits(buffer, file, nodes) : format_raw_deficits(buffer, file)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(buffer: StringIO, file: SimpleCov::SourceFile).void }
|
|
46
|
+
def format_raw_deficits(buffer, file)
|
|
47
|
+
buffer.puts " - **ERROR:** AST Parsing Failed. Showing raw line numbers instead.\n"
|
|
48
|
+
group = MarkdownBuilder::DeficitGroup.new(
|
|
49
|
+
lines: file.missed_lines,
|
|
50
|
+
branches: file.missed_branches
|
|
51
|
+
)
|
|
52
|
+
format_deficit_group(buffer, group, fetch_source_lines(file.filename))
|
|
53
|
+
buffer.puts ''
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig do
|
|
57
|
+
params(buffer: StringIO, file: SimpleCov::SourceFile, nodes: T::Array[ASTResolver::SemanticNode]).void
|
|
58
|
+
end
|
|
59
|
+
def process_deficits(buffer, file, nodes)
|
|
60
|
+
node_deficits = DeficitGrouper.build(file, nodes)
|
|
61
|
+
source_lines = T.let(nil, T.nilable(T::Array[String]))
|
|
62
|
+
|
|
63
|
+
node_deficits.each do |node_name, group|
|
|
64
|
+
break if @builder.truncate_if_needed?
|
|
65
|
+
|
|
66
|
+
source_lines ||= fetch_source_lines(file.filename)
|
|
67
|
+
format_node_deficit(buffer, node_name, group, source_lines)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
buffer.puts ''
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(buffer: StringIO, node_name: String, group: DeficitGroup, source_lines: T::Array[String]).void }
|
|
74
|
+
def format_node_deficit(buffer, node_name, group, source_lines)
|
|
75
|
+
buffer.puts "- `#{node_name}`"
|
|
76
|
+
|
|
77
|
+
if @config.granularity == :coarse
|
|
78
|
+
buffer.puts ' - **Deficit:** Contains unexecuted lines or branches.'
|
|
79
|
+
else
|
|
80
|
+
format_deficit_group(buffer, group, source_lines)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { params(filename: String).returns(T::Array[String]) }
|
|
85
|
+
def fetch_source_lines(filename)
|
|
86
|
+
File.readlines(filename)
|
|
87
|
+
rescue StandardError
|
|
88
|
+
[]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { params(buffer: StringIO, group: DeficitGroup, source_lines: T::Array[String]).void }
|
|
92
|
+
def format_deficit_group(buffer, group, source_lines)
|
|
93
|
+
group.lines.each do |line|
|
|
94
|
+
write_line_snippet(buffer, line, source_lines, group.semantic_node)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
group.branches.each do |branch|
|
|
98
|
+
write_branch_snippet(buffer, branch, source_lines, group.semantic_node)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
sig do
|
|
103
|
+
params(buffer: StringIO, line: SimpleCov::SourceFile::Line, source_lines: T::Array[String],
|
|
104
|
+
node: T.nilable(ASTResolver::SemanticNode)).void
|
|
105
|
+
end
|
|
106
|
+
def write_line_snippet(buffer, line, source_lines, node)
|
|
107
|
+
line_num = line.line_number
|
|
108
|
+
text = truncate_snippet(fetch_snippet_text([line_num], source_lines), @config.max_snippet_lines)
|
|
109
|
+
occurrence_str = calculate_occurrence(line_num, source_lines, node)
|
|
110
|
+
buffer.puts " - **Line Deficit:** #{occurrence_str}`#{text}`"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
sig do
|
|
114
|
+
params(buffer: StringIO, branch: SimpleCov::SourceFile::Branch, source_lines: T::Array[String],
|
|
115
|
+
node: T.nilable(ASTResolver::SemanticNode)).void
|
|
116
|
+
end
|
|
117
|
+
def write_branch_snippet(buffer, branch, source_lines, node)
|
|
118
|
+
start_line = branch.start_line
|
|
119
|
+
end_line = branch.end_line
|
|
120
|
+
lines_range = T.cast((start_line..end_line).to_a, T::Array[Integer])
|
|
121
|
+
text = truncate_snippet(fetch_snippet_text(lines_range, source_lines), @config.max_snippet_lines)
|
|
122
|
+
occurrence_str = calculate_occurrence(start_line, source_lines, node)
|
|
123
|
+
buffer.puts " - **Branch Deficit:** Missing coverage for conditional `#{text}` #{occurrence_str}".rstrip
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
class MarkdownBuilder
|
|
8
|
+
# Groups missed lines and branches into DeficitGroup objects based on AST semantic boundaries.
|
|
9
|
+
class DeficitGrouper
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(T::Hash[String, DeficitGroup]) }
|
|
13
|
+
attr_reader :node_deficits
|
|
14
|
+
|
|
15
|
+
sig { params(nodes: T::Array[ASTResolver::SemanticNode]).void }
|
|
16
|
+
def initialize(nodes)
|
|
17
|
+
@nodes = nodes
|
|
18
|
+
@node_deficits = T.let({}, T::Hash[String, DeficitGroup])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig do
|
|
22
|
+
params(file: SimpleCov::SourceFile, nodes: T::Array[ASTResolver::SemanticNode])
|
|
23
|
+
.returns(T::Hash[String, DeficitGroup])
|
|
24
|
+
end
|
|
25
|
+
def self.build(file, nodes)
|
|
26
|
+
grouper = new(nodes)
|
|
27
|
+
grouper.group_missed_lines(file)
|
|
28
|
+
grouper.group_missed_branches(file)
|
|
29
|
+
grouper.node_deficits
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { params(file: SimpleCov::SourceFile).void }
|
|
33
|
+
def group_missed_lines(file)
|
|
34
|
+
file.missed_lines.each do |line|
|
|
35
|
+
line_num = line.line_number
|
|
36
|
+
node = @nodes.find { |n| line_num.between?(n.start_line, n.end_line) }
|
|
37
|
+
node_name = node ? node.name : "Line #{line_num}"
|
|
38
|
+
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
|
|
39
|
+
T.must(@node_deficits[node_name]).lines << line
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(file: SimpleCov::SourceFile).void }
|
|
44
|
+
def group_missed_branches(file)
|
|
45
|
+
return unless file.respond_to?(:branches) && file.branches.any?
|
|
46
|
+
|
|
47
|
+
file.missed_branches.each do |branch|
|
|
48
|
+
add_missed_branch(branch)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(branch: SimpleCov::SourceFile::Branch).void }
|
|
53
|
+
def add_missed_branch(branch)
|
|
54
|
+
start_line = branch.start_line
|
|
55
|
+
end_line = branch.end_line
|
|
56
|
+
node = @nodes.find { |n| start_line >= n.start_line && end_line <= n.end_line }
|
|
57
|
+
node_name = node ? node.name : "Lines #{start_line}-#{end_line}"
|
|
58
|
+
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
|
|
59
|
+
T.must(@node_deficits[node_name]).branches << branch
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
class MarkdownBuilder
|
|
8
|
+
# Handles extraction and formatting of source code snippets for the markdown digest.
|
|
9
|
+
module SnippetFormatter
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
# Extracts and normalizes exact string literals from the source file arrays.
|
|
13
|
+
#
|
|
14
|
+
# @param line_nums [Array<Integer>] Target line coordinates.
|
|
15
|
+
# @param source_lines [Array<String>] The raw text lines of the file.
|
|
16
|
+
# @return [String] Joined snippet text.
|
|
17
|
+
sig { params(line_nums: T::Array[Integer], source_lines: T::Array[String]).returns(String) }
|
|
18
|
+
def fetch_snippet_text(line_nums, source_lines)
|
|
19
|
+
line_nums.filter_map { |ln| source_lines[ln - 1]&.strip }.reject(&:empty?).join(' ')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Safely limits the character length of a code snippet according to global configurations.
|
|
23
|
+
#
|
|
24
|
+
# @param text [String] The snippet to potentially truncate.
|
|
25
|
+
# @param max_snippet_lines [Integer] The configured max lines.
|
|
26
|
+
# @return [String] Truncated string with trailing ellipses if limit exceeded.
|
|
27
|
+
sig { params(text: String, max_snippet_lines: Integer).returns(String) }
|
|
28
|
+
def truncate_snippet(text, max_snippet_lines)
|
|
29
|
+
max_chars = max_snippet_lines * 80
|
|
30
|
+
text.length > max_chars ? "#{text[0...max_chars]}..." : text
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Disambiguates identical code snippets within the same semantic block (e.g., "(Occurrence 2 of 3)").
|
|
34
|
+
#
|
|
35
|
+
# @param line_num [Integer] The target coordinate of the deficit.
|
|
36
|
+
# @param source_lines [Array<String>] Raw file contents.
|
|
37
|
+
# @param node [ASTResolver::SemanticNode, nil] The semantic node boundary to search within.
|
|
38
|
+
# @return [String] Occurrence label or empty string if unique.
|
|
39
|
+
sig do
|
|
40
|
+
params(line_num: Integer, source_lines: T::Array[String],
|
|
41
|
+
node: T.nilable(ASTResolver::SemanticNode)).returns(String)
|
|
42
|
+
end
|
|
43
|
+
def calculate_occurrence(line_num, source_lines, node)
|
|
44
|
+
return '' if node.nil?
|
|
45
|
+
|
|
46
|
+
first_line_of_snippet = source_lines[line_num - 1]&.strip
|
|
47
|
+
return '' if first_line_of_snippet.nil? || first_line_of_snippet.empty?
|
|
48
|
+
|
|
49
|
+
occurrences, current = count_snippet_occurrences(first_line_of_snippet, line_num, source_lines, node)
|
|
50
|
+
|
|
51
|
+
occurrences > 1 ? "(Occurrence #{current} of #{occurrences}) " : ''
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sig do
|
|
55
|
+
params(snippet: String, target_ln: Integer, source_lines: T::Array[String],
|
|
56
|
+
node: ASTResolver::SemanticNode).returns([Integer, Integer])
|
|
57
|
+
end
|
|
58
|
+
def count_snippet_occurrences(snippet, target_ln, source_lines, node)
|
|
59
|
+
occurrences = 0
|
|
60
|
+
current_occurrence = 1
|
|
61
|
+
|
|
62
|
+
(node.start_line..node.end_line).each do |ln|
|
|
63
|
+
line_content = source_lines[ln - 1]&.strip
|
|
64
|
+
next unless line_content == snippet
|
|
65
|
+
|
|
66
|
+
occurrences += 1
|
|
67
|
+
current_occurrence = occurrences if ln == target_ln
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
[occurrences, current_occurrence]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative 'ast_resolver'
|
|
5
|
+
require_relative 'markdown_builder/snippet_formatter'
|
|
6
|
+
require_relative 'markdown_builder/bypass_compiler'
|
|
7
|
+
require_relative 'markdown_builder/deficit_grouper'
|
|
8
|
+
require_relative 'markdown_builder/deficit_compiler'
|
|
9
|
+
|
|
4
10
|
module SimpleCov
|
|
5
11
|
module Formatter
|
|
6
12
|
class AIFormatter
|
|
@@ -9,6 +15,17 @@ module SimpleCov
|
|
|
9
15
|
# Serves as the primary mutation boundary to format AI consumption targets.
|
|
10
16
|
class MarkdownBuilder
|
|
11
17
|
extend T::Sig
|
|
18
|
+
include SnippetFormatter
|
|
19
|
+
|
|
20
|
+
# Groups unexecuted lines and branches under their common semantic node.
|
|
21
|
+
class DeficitGroup < T::Struct
|
|
22
|
+
# @return [ASTResolver::SemanticNode, nil] The corresponding structural boundary
|
|
23
|
+
prop :semantic_node, T.nilable(ASTResolver::SemanticNode), default: nil
|
|
24
|
+
# @return [Array<SimpleCov::SourceFile::Line>] The missed source lines
|
|
25
|
+
prop :lines, T::Array[SimpleCov::SourceFile::Line], default: []
|
|
26
|
+
# @return [Array<SimpleCov::SourceFile::Branch>] The missed conditional branches
|
|
27
|
+
prop :branches, T::Array[SimpleCov::SourceFile::Branch], default: []
|
|
28
|
+
end
|
|
12
29
|
|
|
13
30
|
# Initializes the Markdown sequence compilation.
|
|
14
31
|
#
|
|
@@ -21,6 +38,7 @@ module SimpleCov
|
|
|
21
38
|
@buffer = T.let(StringIO.new, StringIO)
|
|
22
39
|
@file_count = T.let(0, Integer)
|
|
23
40
|
@truncated = T.let(false, T::Boolean)
|
|
41
|
+
@ast_cache = T.let({}, T::Hash[String, T::Array[ASTResolver::SemanticNode]])
|
|
24
42
|
end
|
|
25
43
|
|
|
26
44
|
# Executes the primary buffer composition logic yielding a monolithic compiled output.
|
|
@@ -30,142 +48,58 @@ module SimpleCov
|
|
|
30
48
|
sig { returns(String) }
|
|
31
49
|
def build
|
|
32
50
|
write_header
|
|
33
|
-
write_deficits
|
|
34
|
-
write_bypasses if @config.include_bypasses
|
|
51
|
+
DeficitCompiler.new(@result, @config, self).write_deficits(@buffer)
|
|
52
|
+
BypassCompiler.new(@result, self).write_bypasses(@buffer) if @config.include_bypasses
|
|
35
53
|
write_truncation_warning if @truncated
|
|
36
54
|
@buffer.string
|
|
37
55
|
end
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
time_str = Time.now.to_s # UI timezone requirement
|
|
45
|
-
|
|
46
|
-
@buffer.puts '# AI Coverage Digest'
|
|
47
|
-
@buffer.puts "**Status:** #{status}"
|
|
48
|
-
@buffer.puts "**Global Line Coverage:** #{T.cast(@result.covered_percent, Float).round(1)}%"
|
|
49
|
-
|
|
50
|
-
branch_pct = begin
|
|
51
|
-
T.cast(@result.covered_branches, Float) / T.cast(@result.total_branches, Numeric) * 100
|
|
52
|
-
rescue StandardError
|
|
53
|
-
0.0
|
|
54
|
-
end
|
|
55
|
-
@buffer.puts "**Global Branch Coverage:** #{T.cast(branch_pct, Float).round(1)}%"
|
|
56
|
-
@buffer.puts "**Generated At:** #{time_str}"
|
|
57
|
-
@buffer.puts ''
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
sig { void }
|
|
61
|
-
def write_deficits
|
|
62
|
-
files_enum = T.cast(@result.files, T::Enumerable[T.untyped])
|
|
63
|
-
files_array = T.let(files_enum.to_a, T::Array[SimpleCov::SourceFile])
|
|
64
|
-
# SCMD-REQ-014: Sort by coverage percent ASC, then by filename
|
|
65
|
-
files = T.let(
|
|
66
|
-
files_array.reject { |f| T.cast(f.covered_percent, Float) >= 100.0 }
|
|
67
|
-
.sort_by { |f| [T.cast(f.covered_percent, Float), T.cast(f.filename, String)] },
|
|
68
|
-
T::Array[SimpleCov::SourceFile]
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
return if files.empty?
|
|
72
|
-
|
|
73
|
-
@buffer.puts "## Coverage Deficits\n\n"
|
|
74
|
-
|
|
75
|
-
files.each do |file|
|
|
76
|
-
# Check size limits SCMD-REQ-012
|
|
77
|
-
if @buffer.size / 1024.0 > @config.max_file_size_kb
|
|
78
|
-
@truncated = true
|
|
79
|
-
break
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
@buffer.puts "### `#{T.cast(file.project_filename, String)}`"
|
|
83
|
-
|
|
84
|
-
begin
|
|
85
|
-
nodes = ASTResolver.resolve(T.cast(file.filename, String))
|
|
86
|
-
rescue StandardError => e
|
|
87
|
-
@buffer.puts "- **ERROR:** AST Parsing Failed (`#{e.class}`)"
|
|
88
|
-
next
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
process_deficits(file, nodes)
|
|
92
|
-
end
|
|
57
|
+
sig { params(filename: String).returns(T.nilable(T::Array[ASTResolver::SemanticNode])) }
|
|
58
|
+
def try_resolve_ast(filename)
|
|
59
|
+
@ast_cache[filename] ||= ASTResolver.resolve(filename)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
93
62
|
end
|
|
94
63
|
|
|
95
|
-
sig {
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
line_num = T.cast(line.line_number, Integer)
|
|
99
|
-
node = nodes.find { |n| line_num >= n.start_line && line_num <= n.end_line }
|
|
100
|
-
node_name = node ? node.name : "Line #{line_num}"
|
|
101
|
-
@buffer.puts "- `#{node_name}`\n - **Line Deficit:** Unexecuted code."
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
process_branch_deficits(file, nodes) if file.respond_to?(:branches)
|
|
64
|
+
sig { returns(T::Boolean) }
|
|
65
|
+
def truncate_if_needed?
|
|
66
|
+
return false unless @buffer.size / 1024.0 > @config.max_file_size_kb
|
|
105
67
|
|
|
106
|
-
@
|
|
68
|
+
@truncated = true
|
|
69
|
+
true
|
|
107
70
|
end
|
|
108
71
|
|
|
109
|
-
|
|
110
|
-
def process_branch_deficits(file, nodes)
|
|
111
|
-
branches = file.branches
|
|
112
|
-
case branches
|
|
113
|
-
when Array
|
|
114
|
-
return unless branches.any?
|
|
115
|
-
|
|
116
|
-
T.cast(file.missed_branches, T::Array[SimpleCov::SourceFile::Branch]).each do |branch|
|
|
117
|
-
start_line = T.cast(branch.start_line, Integer)
|
|
118
|
-
end_line = T.cast(branch.end_line, Integer)
|
|
119
|
-
node = nodes.find { |n| start_line >= n.start_line && end_line <= n.end_line }
|
|
120
|
-
node_name = node ? node.name : "Lines #{start_line}-#{end_line}"
|
|
121
|
-
@buffer.puts "- `#{node_name}`\n - **Branch Deficit:** Missing coverage for conditional."
|
|
122
|
-
end
|
|
123
|
-
end
|
|
72
|
+
private
|
|
124
73
|
|
|
74
|
+
# Writes the summary header containing global coverage percentages and generation metadata.
|
|
75
|
+
sig { void }
|
|
76
|
+
def write_header
|
|
77
|
+
status = @result.covered_percent >= 100.0 ? 'PASSED' : 'FAILED'
|
|
78
|
+
@buffer.puts '# AI Coverage Digest'
|
|
79
|
+
@buffer.puts "**Status:** #{status}"
|
|
80
|
+
@buffer.puts "**Global Line Coverage:** #{@result.covered_percent.round(1)}%"
|
|
81
|
+
@buffer.puts "**Global Branch Coverage:** #{calculate_branch_pct.round(1)}%"
|
|
82
|
+
@buffer.puts "**Generated At:** #{Time.now}"
|
|
125
83
|
@buffer.puts ''
|
|
126
84
|
end
|
|
127
85
|
|
|
128
|
-
sig {
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
bypass_buffer = T.let(StringIO.new, StringIO)
|
|
132
|
-
|
|
133
|
-
files_enum = T.cast(@result.files, T::Enumerable[T.untyped])
|
|
134
|
-
files_array = T.let(files_enum.to_a, T::Array[SimpleCov::SourceFile])
|
|
135
|
-
|
|
136
|
-
files_array.each do |file|
|
|
137
|
-
begin
|
|
138
|
-
nodes = ASTResolver.resolve(T.cast(file.filename, String))
|
|
139
|
-
rescue StandardError
|
|
140
|
-
next
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
nodes_with_bypasses = nodes.select { |n| n.bypasses.any? }
|
|
144
|
-
next if nodes_with_bypasses.empty?
|
|
145
|
-
|
|
146
|
-
has_bypasses = true
|
|
147
|
-
write_file_bypasses(bypass_buffer, file, nodes_with_bypasses)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
return unless has_bypasses
|
|
151
|
-
|
|
152
|
-
@buffer.puts "## Ignored Coverage Bypasses\n\n"
|
|
153
|
-
@buffer.puts bypass_buffer.string
|
|
154
|
-
end
|
|
86
|
+
sig { returns(Float) }
|
|
87
|
+
def calculate_branch_pct
|
|
88
|
+
return 0.0 unless @result.respond_to?(:covered_branches) && @result.respond_to?(:total_branches)
|
|
155
89
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
bypasses.each do |node|
|
|
160
|
-
buffer.puts "- `#{node.name}`\n - **Bypass Present:** Contains `# :nocov:` directive artificially ignoring coverage."
|
|
161
|
-
end
|
|
162
|
-
buffer.puts ''
|
|
90
|
+
T.cast(@result.covered_branches, Float) / @result.total_branches * 100
|
|
91
|
+
rescue StandardError
|
|
92
|
+
0.0
|
|
163
93
|
end
|
|
164
94
|
|
|
95
|
+
# Appends a critical alert if the output hit the token-ceiling constraint and was forcibly terminated.
|
|
165
96
|
sig { void }
|
|
166
97
|
def write_truncation_warning
|
|
167
98
|
@buffer.puts '> **[WARNING] TRUNCATION NOTIFICATION:**'
|
|
168
|
-
|
|
99
|
+
msg = "> The total coverage deficit report exceeded the maximum token constraint (#{@config.max_file_size_kb} kB). " \
|
|
100
|
+
'The report was truncated. The deficits detailed above represent the lowest-coverage files. ' \
|
|
101
|
+
'Please resolve these deficits to reveal the remaining uncovered files.'
|
|
102
|
+
@buffer.puts msg
|
|
169
103
|
end
|
|
170
104
|
end
|
|
171
105
|
end
|
data/lib/simplecov-ai/version.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simplecov-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vitalii Lazebnyi
|
|
@@ -151,44 +151,58 @@ dependencies:
|
|
|
151
151
|
name: rubocop
|
|
152
152
|
requirement: !ruby/object:Gem::Requirement
|
|
153
153
|
requirements:
|
|
154
|
-
- - "
|
|
154
|
+
- - ">="
|
|
155
155
|
- !ruby/object:Gem::Version
|
|
156
156
|
version: '1.28'
|
|
157
157
|
type: :development
|
|
158
158
|
prerelease: false
|
|
159
159
|
version_requirements: !ruby/object:Gem::Requirement
|
|
160
160
|
requirements:
|
|
161
|
-
- - "
|
|
161
|
+
- - ">="
|
|
162
162
|
- !ruby/object:Gem::Version
|
|
163
163
|
version: '1.28'
|
|
164
164
|
- !ruby/object:Gem::Dependency
|
|
165
165
|
name: rubocop-performance
|
|
166
166
|
requirement: !ruby/object:Gem::Requirement
|
|
167
167
|
requirements:
|
|
168
|
-
- - "
|
|
168
|
+
- - ">="
|
|
169
169
|
- !ruby/object:Gem::Version
|
|
170
170
|
version: '1.14'
|
|
171
171
|
type: :development
|
|
172
172
|
prerelease: false
|
|
173
173
|
version_requirements: !ruby/object:Gem::Requirement
|
|
174
174
|
requirements:
|
|
175
|
-
- - "
|
|
175
|
+
- - ">="
|
|
176
176
|
- !ruby/object:Gem::Version
|
|
177
177
|
version: '1.14'
|
|
178
178
|
- !ruby/object:Gem::Dependency
|
|
179
179
|
name: rubocop-rspec
|
|
180
180
|
requirement: !ruby/object:Gem::Requirement
|
|
181
181
|
requirements:
|
|
182
|
-
- - "
|
|
182
|
+
- - ">="
|
|
183
183
|
- !ruby/object:Gem::Version
|
|
184
184
|
version: '2.11'
|
|
185
185
|
type: :development
|
|
186
186
|
prerelease: false
|
|
187
187
|
version_requirements: !ruby/object:Gem::Requirement
|
|
188
188
|
requirements:
|
|
189
|
-
- - "
|
|
189
|
+
- - ">="
|
|
190
190
|
- !ruby/object:Gem::Version
|
|
191
191
|
version: '2.11'
|
|
192
|
+
- !ruby/object:Gem::Dependency
|
|
193
|
+
name: rubocop-sorbet
|
|
194
|
+
requirement: !ruby/object:Gem::Requirement
|
|
195
|
+
requirements:
|
|
196
|
+
- - ">="
|
|
197
|
+
- !ruby/object:Gem::Version
|
|
198
|
+
version: '0'
|
|
199
|
+
type: :development
|
|
200
|
+
prerelease: false
|
|
201
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
202
|
+
requirements:
|
|
203
|
+
- - ">="
|
|
204
|
+
- !ruby/object:Gem::Version
|
|
205
|
+
version: '0'
|
|
192
206
|
- !ruby/object:Gem::Dependency
|
|
193
207
|
name: rubocop-thread_safety
|
|
194
208
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -218,7 +232,7 @@ dependencies:
|
|
|
218
232
|
- !ruby/object:Gem::Version
|
|
219
233
|
version: '0.5'
|
|
220
234
|
- !ruby/object:Gem::Dependency
|
|
221
|
-
name:
|
|
235
|
+
name: standard-sorbet
|
|
222
236
|
requirement: !ruby/object:Gem::Requirement
|
|
223
237
|
requirements:
|
|
224
238
|
- - ">="
|
|
@@ -232,7 +246,7 @@ dependencies:
|
|
|
232
246
|
- !ruby/object:Gem::Version
|
|
233
247
|
version: '0'
|
|
234
248
|
- !ruby/object:Gem::Dependency
|
|
235
|
-
name:
|
|
249
|
+
name: tapioca
|
|
236
250
|
requirement: !ruby/object:Gem::Requirement
|
|
237
251
|
requirements:
|
|
238
252
|
- - ">="
|
|
@@ -246,7 +260,7 @@ dependencies:
|
|
|
246
260
|
- !ruby/object:Gem::Version
|
|
247
261
|
version: '0'
|
|
248
262
|
- !ruby/object:Gem::Dependency
|
|
249
|
-
name:
|
|
263
|
+
name: tsort
|
|
250
264
|
requirement: !ruby/object:Gem::Requirement
|
|
251
265
|
requirements:
|
|
252
266
|
- - ">="
|
|
@@ -260,19 +274,33 @@ dependencies:
|
|
|
260
274
|
- !ruby/object:Gem::Version
|
|
261
275
|
version: '0'
|
|
262
276
|
- !ruby/object:Gem::Dependency
|
|
263
|
-
name:
|
|
277
|
+
name: yard
|
|
264
278
|
requirement: !ruby/object:Gem::Requirement
|
|
265
279
|
requirements:
|
|
266
|
-
- - "
|
|
280
|
+
- - ">="
|
|
267
281
|
- !ruby/object:Gem::Version
|
|
268
|
-
version: '0
|
|
282
|
+
version: '0'
|
|
269
283
|
type: :development
|
|
270
284
|
prerelease: false
|
|
271
285
|
version_requirements: !ruby/object:Gem::Requirement
|
|
272
286
|
requirements:
|
|
273
|
-
- - "
|
|
287
|
+
- - ">="
|
|
274
288
|
- !ruby/object:Gem::Version
|
|
275
|
-
version: '0
|
|
289
|
+
version: '0'
|
|
290
|
+
- !ruby/object:Gem::Dependency
|
|
291
|
+
name: yard-sorbet
|
|
292
|
+
requirement: !ruby/object:Gem::Requirement
|
|
293
|
+
requirements:
|
|
294
|
+
- - ">="
|
|
295
|
+
- !ruby/object:Gem::Version
|
|
296
|
+
version: '0'
|
|
297
|
+
type: :development
|
|
298
|
+
prerelease: false
|
|
299
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
300
|
+
requirements:
|
|
301
|
+
- - ">="
|
|
302
|
+
- !ruby/object:Gem::Version
|
|
303
|
+
version: '0'
|
|
276
304
|
description: Generates highly concise, deterministic Markdown coverage digests tailored
|
|
277
305
|
for LLMs and autonomous agents by matching coverage deficits to their AST semantic
|
|
278
306
|
boundaries rather than line numbers.
|
|
@@ -289,6 +317,10 @@ files:
|
|
|
289
317
|
- lib/simplecov-ai/ast_resolver.rb
|
|
290
318
|
- lib/simplecov-ai/configuration.rb
|
|
291
319
|
- lib/simplecov-ai/markdown_builder.rb
|
|
320
|
+
- lib/simplecov-ai/markdown_builder/bypass_compiler.rb
|
|
321
|
+
- lib/simplecov-ai/markdown_builder/deficit_compiler.rb
|
|
322
|
+
- lib/simplecov-ai/markdown_builder/deficit_grouper.rb
|
|
323
|
+
- lib/simplecov-ai/markdown_builder/snippet_formatter.rb
|
|
292
324
|
- lib/simplecov-ai/version.rb
|
|
293
325
|
homepage: https://github.com/VitaliiLazebnyi/simplecov-ai
|
|
294
326
|
licenses:
|
metadata.gz.sig
CHANGED
|
Binary file
|