rail_verdict 1.2.0 → 1.4.0
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
- data/README.md +151 -18
- data/lib/rail_verdict/analyzers/bundler_audit.rb +2 -1
- data/lib/rail_verdict/analyzers/minitest.rb +47 -34
- data/lib/rail_verdict/analyzers/rspec.rb +120 -42
- data/lib/rail_verdict/analyzers/rubocop.rb +2 -1
- data/lib/rail_verdict/analyzers/simplecov.rb +71 -75
- data/lib/rail_verdict/check.rb +1 -1
- data/lib/rail_verdict/cli.rb +365 -17
- data/lib/rail_verdict/doctor.rb +2 -1
- data/lib/rail_verdict/handoff.rb +122 -0
- data/lib/rail_verdict/mcp/cache.rb +50 -6
- data/lib/rail_verdict/mcp/server.rb +7 -1
- data/lib/rail_verdict/mcp/tools/create_handoff.rb +115 -0
- data/lib/rail_verdict/mcp/tools/get_pr_intelligence.rb +16 -3
- data/lib/rail_verdict/mcp/tools/get_verification_receipt.rb +17 -3
- data/lib/rail_verdict/mcp/tools/inspect_handoff.rb +72 -0
- data/lib/rail_verdict/mcp/tools/verify_handoff.rb +117 -0
- data/lib/rail_verdict/receipt.rb +163 -22
- data/lib/rail_verdict/reuse.rb +168 -0
- data/lib/rail_verdict/schema_validator.rb +5 -0
- data/lib/rail_verdict/verification_environment.rb +180 -0
- data/lib/rail_verdict/verification_identity.rb +95 -0
- data/lib/rail_verdict/version.rb +1 -1
- data/lib/rail_verdict.rb +4 -0
- data/schemas/receipt-validation-v1.schema.json +6 -0
- data/schemas/verification-handoff-v1.schema.json +69 -0
- data/schemas/verification-receipt-v1.schema.json +1 -0
- metadata +9 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "pathname"
|
|
4
5
|
|
|
5
6
|
require_relative "_shared"
|
|
6
7
|
|
|
@@ -18,9 +19,9 @@ module RailVerdict
|
|
|
18
19
|
|
|
19
20
|
def probe(repository_root, runner: nil, timeout_seconds: 5.0)
|
|
20
21
|
config = load_config(repository_root)
|
|
21
|
-
|
|
22
|
-
full_path =
|
|
23
|
-
return Probe.new(status: "unavailable", message: "coverage file is absent: #{
|
|
22
|
+
raw_path = config.fetch("coverage_path") { DEFAULT_COVERAGE_PATH }
|
|
23
|
+
full_path = resolve_contained_coverage_path(repository_root, raw_path)
|
|
24
|
+
return Probe.new(status: "unavailable", message: "coverage file is absent: #{raw_path}") unless File.file?(full_path)
|
|
24
25
|
|
|
25
26
|
text = File.binread(full_path).force_encoding(Encoding::UTF_8)
|
|
26
27
|
return Probe.new(status: "parse_failed", message: "coverage file is not valid UTF-8") unless text.valid_encoding?
|
|
@@ -30,11 +31,18 @@ module RailVerdict
|
|
|
30
31
|
case detected
|
|
31
32
|
when :coverage_v1
|
|
32
33
|
version = document["version"].to_s
|
|
33
|
-
|
|
34
|
+
v = Gem::Version.new(version) rescue nil
|
|
35
|
+
if v.nil? || !SUPPORTED_VERSION_RANGE.satisfied_by?(v)
|
|
36
|
+
return Probe.new(status: "unsupported", version: version, message: "unsupported SimpleCov version #{version}")
|
|
37
|
+
end
|
|
34
38
|
Probe.new(status: "succeeded", version: version)
|
|
35
39
|
when :native_simplecov
|
|
36
40
|
version = extract_native_version(document)
|
|
37
|
-
|
|
41
|
+
return Probe.new(status: "unsupported", message: "native SimpleCov version could not be determined") unless version
|
|
42
|
+
v = Gem::Version.new(version) rescue nil
|
|
43
|
+
if v.nil? || !SUPPORTED_VERSION_RANGE.satisfied_by?(v)
|
|
44
|
+
return Probe.new(status: "unsupported", version: version, message: "unsupported SimpleCov version #{version}")
|
|
45
|
+
end
|
|
38
46
|
Probe.new(status: "succeeded", version: version)
|
|
39
47
|
when :unsupported
|
|
40
48
|
Probe.new(status: "unsupported", message: "unsupported coverage document shape")
|
|
@@ -51,13 +59,18 @@ module RailVerdict
|
|
|
51
59
|
|
|
52
60
|
def run(repository_root, runner: nil, timeout_seconds: 10.0, probe_result: nil, configuration: nil)
|
|
53
61
|
config = load_config(repository_root, configuration: configuration)
|
|
54
|
-
|
|
62
|
+
raw_path = config.fetch("coverage_path") { DEFAULT_COVERAGE_PATH }
|
|
55
63
|
freshness_window = config.fetch("freshness_window_seconds") { DEFAULT_FRESHNESS_WINDOW_SECONDS }
|
|
56
|
-
invocation = { "executable" => "simplecov", "argv" => ["read",
|
|
57
|
-
|
|
64
|
+
invocation = { "executable" => "simplecov", "argv" => ["read", raw_path] }
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
full_path = resolve_contained_coverage_path(repository_root, raw_path)
|
|
68
|
+
rescue MalformedOutput => error
|
|
69
|
+
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "malformed", message: Shared.bounded_message(error.message)), []]
|
|
70
|
+
end
|
|
58
71
|
|
|
59
72
|
unless File.file?(full_path)
|
|
60
|
-
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "unavailable", message: "coverage file is absent: #{
|
|
73
|
+
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "unavailable", message: "coverage file is absent: #{raw_path}"), []]
|
|
61
74
|
end
|
|
62
75
|
|
|
63
76
|
begin
|
|
@@ -92,24 +105,29 @@ module RailVerdict
|
|
|
92
105
|
version = nil
|
|
93
106
|
if detected == :coverage_v1
|
|
94
107
|
version = document["version"].to_s
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "unsupported", message: "unsupported SimpleCov version #{version}", tool_version:
|
|
108
|
+
v = Gem::Version.new(version) rescue nil
|
|
109
|
+
if v.nil? || !SUPPORTED_VERSION_RANGE.satisfied_by?(v)
|
|
110
|
+
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "unsupported", message: "unsupported SimpleCov version #{version}", tool_version: version), []]
|
|
98
111
|
end
|
|
99
112
|
errors = validate_coverage_schema(document)
|
|
100
113
|
unless errors.empty?
|
|
101
|
-
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "malformed", message: errors.first, tool_version:
|
|
114
|
+
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "malformed", message: errors.first, tool_version: version), []]
|
|
102
115
|
end
|
|
103
116
|
normalized = normalize_coverage_v1(document)
|
|
104
|
-
version = document["version"].to_s
|
|
105
117
|
elsif detected == :native_simplecov
|
|
118
|
+
version = extract_native_version(document)
|
|
119
|
+
unless version
|
|
120
|
+
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "unsupported", message: "native SimpleCov version could not be determined", tool_version: nil), []]
|
|
121
|
+
end
|
|
122
|
+
v = Gem::Version.new(version) rescue nil
|
|
123
|
+
if v.nil? || !SUPPORTED_VERSION_RANGE.satisfied_by?(v)
|
|
124
|
+
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "unsupported", message: "unsupported SimpleCov version #{version}", tool_version: version), []]
|
|
125
|
+
end
|
|
106
126
|
begin
|
|
107
127
|
normalized = normalize_native_document(document, repository_root)
|
|
108
|
-
version = extract_native_version(document)
|
|
109
128
|
rescue MalformedOutput => error
|
|
110
|
-
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "malformed", message: Shared.bounded_message(error.message), tool_version:
|
|
129
|
+
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "malformed", message: Shared.bounded_message(error.message), tool_version: version), []]
|
|
111
130
|
end
|
|
112
|
-
# Validate normalized shape via same schema
|
|
113
131
|
errors = validate_coverage_schema(normalized)
|
|
114
132
|
unless errors.empty?
|
|
115
133
|
return [Shared.failure_result(analyzer_id: ANALYZER_ID, invocation: invocation, status: "malformed", message: errors.first, tool_version: version), []]
|
|
@@ -129,7 +147,7 @@ module RailVerdict
|
|
|
129
147
|
"percent" => executable == 0 ? 100.0 : ((covered.to_f / executable) * 100).round(2),
|
|
130
148
|
"stale" => stale,
|
|
131
149
|
"freshness_window_seconds" => Integer(freshness_window),
|
|
132
|
-
"coverage_path" =>
|
|
150
|
+
"coverage_path" => raw_path.to_s[0, 512],
|
|
133
151
|
"files" => normalized["files"],
|
|
134
152
|
"_coverage_document" => normalized
|
|
135
153
|
}
|
|
@@ -150,47 +168,38 @@ module RailVerdict
|
|
|
150
168
|
|
|
151
169
|
private
|
|
152
170
|
|
|
153
|
-
def
|
|
154
|
-
|
|
171
|
+
def resolve_contained_coverage_path(repository_root, configured_path)
|
|
172
|
+
rel_path = configured_path.to_s.strip
|
|
173
|
+
rel_path = DEFAULT_COVERAGE_PATH if rel_path.empty?
|
|
155
174
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return :coverage_v1
|
|
159
|
-
end
|
|
175
|
+
root = File.realpath(repository_root) rescue File.expand_path(repository_root)
|
|
176
|
+
full_path = File.expand_path(rel_path, root)
|
|
160
177
|
|
|
161
|
-
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
# Native values are hashes with "lines" key
|
|
165
|
-
if sample.is_a?(Hash) && sample.key?("lines")
|
|
166
|
-
return :native_simplecov
|
|
167
|
-
end
|
|
178
|
+
rel = Pathname.new(full_path).relative_path_from(Pathname.new(root)).to_s rescue nil
|
|
179
|
+
if rel.nil? || rel.start_with?("..")
|
|
180
|
+
raise MalformedOutput, "configured coverage_path escapes repository root: #{configured_path}"
|
|
168
181
|
end
|
|
169
182
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return :native_simplecov
|
|
173
|
-
end
|
|
183
|
+
full_path
|
|
184
|
+
end
|
|
174
185
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
def detect_format(document)
|
|
187
|
+
return :malformed unless document.is_a?(Hash)
|
|
188
|
+
|
|
189
|
+
if document.key?("version") && document.key?("timestamp") && document.key?("files")
|
|
190
|
+
return :coverage_v1 if document["files"].is_a?(Array)
|
|
191
|
+
return :malformed if document.key?("files")
|
|
180
192
|
return :unsupported
|
|
181
193
|
end
|
|
182
194
|
|
|
183
|
-
# If document has no recognizable shape but is Hash with coverage key mismatched
|
|
184
195
|
if document.key?("coverage") || document.key?("meta") || document.key?("groups")
|
|
185
196
|
return :native_simplecov if document["coverage"].is_a?(Hash)
|
|
186
197
|
end
|
|
187
198
|
|
|
188
|
-
# If it has version but not files, or files but wrong shape => unsupported/malformed via validator
|
|
189
199
|
if document.key?("version") || document.key?("files") || document.key?("timestamp")
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return :unsupported
|
|
200
|
+
if document["version"].is_a?(String)
|
|
201
|
+
v = Gem::Version.new(document["version"]) rescue nil
|
|
202
|
+
return :unsupported if v.nil? || !SUPPORTED_VERSION_RANGE.satisfied_by?(v)
|
|
194
203
|
end
|
|
195
204
|
return :malformed if document.key?("files") || document.key?("version")
|
|
196
205
|
end
|
|
@@ -206,8 +215,7 @@ module RailVerdict
|
|
|
206
215
|
if document["version"].is_a?(String) && !document["version"].empty?
|
|
207
216
|
return document["version"].to_s[0, 64]
|
|
208
217
|
end
|
|
209
|
-
|
|
210
|
-
"1.0"
|
|
218
|
+
nil
|
|
211
219
|
end
|
|
212
220
|
|
|
213
221
|
def normalize_native_document(document, repository_root)
|
|
@@ -229,7 +237,6 @@ module RailVerdict
|
|
|
229
237
|
when NilClass
|
|
230
238
|
nil
|
|
231
239
|
when String
|
|
232
|
-
# SimpleCov uses "ignored" for skipped lines
|
|
233
240
|
hit == "ignored" ? nil : (raise MalformedOutput, "unexpected string in coverage lines: #{hit.inspect}")
|
|
234
241
|
else
|
|
235
242
|
raise MalformedOutput, "coverage lines must be integers, null, or \"ignored\""
|
|
@@ -237,8 +244,6 @@ module RailVerdict
|
|
|
237
244
|
end
|
|
238
245
|
|
|
239
246
|
filename = normalize_native_path(raw_path, repository_root)
|
|
240
|
-
|
|
241
|
-
# Skip empty paths after normalization
|
|
242
247
|
raise MalformedOutput, "normalized path is empty for #{raw_path}" if filename.empty?
|
|
243
248
|
unless filename.match?(RailVerdict::Finding::LOCATION_PATH_PATTERN)
|
|
244
249
|
raise MalformedOutput, "coverage filename is not a clean repository-relative path: #{filename.inspect}"
|
|
@@ -251,7 +256,7 @@ module RailVerdict
|
|
|
251
256
|
|
|
252
257
|
{
|
|
253
258
|
"version" => "1.0",
|
|
254
|
-
"timestamp" => document["timestamp"].is_a?(Integer) ? document["timestamp"] :
|
|
259
|
+
"timestamp" => document["timestamp"].is_a?(Integer) ? document["timestamp"] : 0,
|
|
255
260
|
"files" => files
|
|
256
261
|
}
|
|
257
262
|
end
|
|
@@ -259,33 +264,24 @@ module RailVerdict
|
|
|
259
264
|
def normalize_native_path(raw_path, repository_root)
|
|
260
265
|
str = raw_path.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "?").scrub("?").strip
|
|
261
266
|
str = str.delete("\u0000")
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
require "pathname"
|
|
271
|
-
rel = Pathname.new(abs).relative_path_from(Pathname.new(root)).to_s rescue nil
|
|
272
|
-
if rel && !rel.start_with?("..") && rel.match?(RailVerdict::Finding::LOCATION_PATH_PATTERN)
|
|
273
|
-
return rel
|
|
274
|
-
end
|
|
275
|
-
rescue StandardError
|
|
276
|
-
nil
|
|
277
|
-
end
|
|
278
|
-
# Fallback: strip leading slash
|
|
279
|
-
str = str.delete_prefix("/")
|
|
267
|
+
raise MalformedOutput, "coverage path is empty" if str.empty?
|
|
268
|
+
|
|
269
|
+
root = File.realpath(repository_root) rescue File.expand_path(repository_root)
|
|
270
|
+
full = File.expand_path(str, root)
|
|
271
|
+
|
|
272
|
+
rel = Pathname.new(full).relative_path_from(Pathname.new(root)).to_s rescue nil
|
|
273
|
+
if rel.nil? || rel.start_with?("..")
|
|
274
|
+
raise MalformedOutput, "coverage path is outside repository root: #{str}"
|
|
280
275
|
end
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
276
|
+
|
|
277
|
+
unless rel.match?(RailVerdict::Finding::LOCATION_PATH_PATTERN)
|
|
278
|
+
raise MalformedOutput, "coverage path is not a clean repository-relative path: #{rel}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
rel
|
|
285
282
|
end
|
|
286
283
|
|
|
287
284
|
def normalize_coverage_v1(document)
|
|
288
|
-
# Already validated; ensure deterministic ordering by filename
|
|
289
285
|
files = document["files"].map do |f|
|
|
290
286
|
{ "filename" => f["filename"].to_s, "coverage" => { "lines" => f.dig("coverage", "lines") } }
|
|
291
287
|
end.sort_by { |f| f["filename"] }
|
data/lib/rail_verdict/check.rb
CHANGED
|
@@ -499,7 +499,7 @@ module RailVerdict
|
|
|
499
499
|
adapter = build_adapter(name, rubocop_command_resolver)
|
|
500
500
|
next unless adapter
|
|
501
501
|
|
|
502
|
-
timeout_seconds = resolve_timeout_seconds(configuration, name, default_timeout_seconds)
|
|
502
|
+
timeout_seconds = [[resolve_timeout_seconds(configuration, name, default_timeout_seconds).to_f, 5.0].min, 1.0].max
|
|
503
503
|
probes[name] = adapter.probe(root, runner: runner, timeout_seconds: timeout_seconds)
|
|
504
504
|
end
|
|
505
505
|
probes
|