rail_verdict 1.0.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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/NOTICE +14 -0
  4. data/README.md +192 -0
  5. data/exe/railverdict +6 -0
  6. data/exe/railverdict-minitest-reporter.rb +90 -0
  7. data/lib/rail_verdict/analyzers/_shared.rb +54 -0
  8. data/lib/rail_verdict/analyzers/bundler_audit.rb +186 -0
  9. data/lib/rail_verdict/analyzers/minitest.rb +312 -0
  10. data/lib/rail_verdict/analyzers/rspec.rb +274 -0
  11. data/lib/rail_verdict/analyzers/rubocop.rb +289 -0
  12. data/lib/rail_verdict/analyzers/simplecov.rb +178 -0
  13. data/lib/rail_verdict/baseline.rb +151 -0
  14. data/lib/rail_verdict/check.rb +415 -0
  15. data/lib/rail_verdict/cli.rb +471 -0
  16. data/lib/rail_verdict/comparison.rb +216 -0
  17. data/lib/rail_verdict/configuration.rb +165 -0
  18. data/lib/rail_verdict/contracts/analyzer_result.rb +138 -0
  19. data/lib/rail_verdict/contracts/finding.rb +154 -0
  20. data/lib/rail_verdict/contracts/gate_result.rb +171 -0
  21. data/lib/rail_verdict/coverage/changed_line_evaluator.rb +70 -0
  22. data/lib/rail_verdict/doctor.rb +132 -0
  23. data/lib/rail_verdict/errors.rb +17 -0
  24. data/lib/rail_verdict/findings_command.rb +24 -0
  25. data/lib/rail_verdict/fingerprint.rb +72 -0
  26. data/lib/rail_verdict/git/command.rb +48 -0
  27. data/lib/rail_verdict/git/context.rb +361 -0
  28. data/lib/rail_verdict/git/diff_parser.rb +200 -0
  29. data/lib/rail_verdict/git/errors.rb +16 -0
  30. data/lib/rail_verdict/init.rb +38 -0
  31. data/lib/rail_verdict/intelligence/ai_analysis.rb +82 -0
  32. data/lib/rail_verdict/intelligence/ai_failure.rb +44 -0
  33. data/lib/rail_verdict/intelligence/budget.rb +36 -0
  34. data/lib/rail_verdict/intelligence/cache.rb +74 -0
  35. data/lib/rail_verdict/intelligence/context_builder.rb +92 -0
  36. data/lib/rail_verdict/intelligence/manifest.rb +70 -0
  37. data/lib/rail_verdict/intelligence/orchestrator.rb +77 -0
  38. data/lib/rail_verdict/intelligence/prompt.rb +28 -0
  39. data/lib/rail_verdict/intelligence/provider.rb +14 -0
  40. data/lib/rail_verdict/intelligence/providers/fake_provider.rb +31 -0
  41. data/lib/rail_verdict/intelligence/providers/openai_compat.rb +119 -0
  42. data/lib/rail_verdict/intelligence/redactor.rb +51 -0
  43. data/lib/rail_verdict/intelligence/secret_detector.rb +45 -0
  44. data/lib/rail_verdict/intelligence/source_reader.rb +49 -0
  45. data/lib/rail_verdict/intelligence.rb +11 -0
  46. data/lib/rail_verdict/mcp/cache.rb +233 -0
  47. data/lib/rail_verdict/mcp/repository_root.rb +31 -0
  48. data/lib/rail_verdict/mcp/serializers.rb +98 -0
  49. data/lib/rail_verdict/mcp/server.rb +110 -0
  50. data/lib/rail_verdict/mcp/tools/build_repair_packet.rb +85 -0
  51. data/lib/rail_verdict/mcp/tools/explain.rb +100 -0
  52. data/lib/rail_verdict/mcp/tools/get_finding.rb +139 -0
  53. data/lib/rail_verdict/mcp/tools/investigate.rb +107 -0
  54. data/lib/rail_verdict/mcp/tools/list_findings.rb +119 -0
  55. data/lib/rail_verdict/mcp/tools/verify.rb +137 -0
  56. data/lib/rail_verdict/mcp/tools/verify_repair.rb +105 -0
  57. data/lib/rail_verdict/mcp/validators.rb +81 -0
  58. data/lib/rail_verdict/mcp.rb +7 -0
  59. data/lib/rail_verdict/process_runner.rb +292 -0
  60. data/lib/rail_verdict/rails_context/classifier.rb +57 -0
  61. data/lib/rail_verdict/rails_context/confidence.rb +13 -0
  62. data/lib/rail_verdict/rails_context/constant_inferencer.rb +56 -0
  63. data/lib/rail_verdict/rails_context/context.rb +115 -0
  64. data/lib/rail_verdict/rails_context/detector.rb +141 -0
  65. data/lib/rail_verdict/rails_context/limits.rb +17 -0
  66. data/lib/rail_verdict/rails_context/resolvers/association_extractor.rb +48 -0
  67. data/lib/rail_verdict/rails_context/resolvers/policy_resolver.rb +45 -0
  68. data/lib/rail_verdict/rails_context/resolvers/route_resolver.rb +40 -0
  69. data/lib/rail_verdict/rails_context/resolvers/route_scanner.rb +55 -0
  70. data/lib/rail_verdict/rails_context/resolvers/schema_resolver.rb +90 -0
  71. data/lib/rail_verdict/rails_context/resolvers/test_candidates.rb +75 -0
  72. data/lib/rail_verdict/rails_context/resolvers/view_resolver.rb +45 -0
  73. data/lib/rail_verdict/rails_context/schema_parser.rb +50 -0
  74. data/lib/rail_verdict/rails_context.rb +21 -0
  75. data/lib/rail_verdict/repair/boundary.rb +36 -0
  76. data/lib/rail_verdict/repair/command.rb +89 -0
  77. data/lib/rail_verdict/repair/constraints.rb +31 -0
  78. data/lib/rail_verdict/repair/context_assembler.rb +301 -0
  79. data/lib/rail_verdict/repair/diff_context.rb +51 -0
  80. data/lib/rail_verdict/repair/packet.rb +158 -0
  81. data/lib/rail_verdict/repair/prompt_renderer.rb +27 -0
  82. data/lib/rail_verdict/repair/verification_plan.rb +45 -0
  83. data/lib/rail_verdict/repair/verifier.rb +84 -0
  84. data/lib/rail_verdict/reporters/console.rb +107 -0
  85. data/lib/rail_verdict/reporters/github_annotations.rb +104 -0
  86. data/lib/rail_verdict/reporters/json_reporter.rb +19 -0
  87. data/lib/rail_verdict/reporters/sarif.rb +138 -0
  88. data/lib/rail_verdict/run_context.rb +99 -0
  89. data/lib/rail_verdict/schema_validator.rb +85 -0
  90. data/lib/rail_verdict/strict_yaml.rb +134 -0
  91. data/lib/rail_verdict/verification/policy.rb +359 -0
  92. data/lib/rail_verdict/version.rb +5 -0
  93. data/lib/rail_verdict/waiver.rb +67 -0
  94. data/lib/rail_verdict/waiver_store.rb +59 -0
  95. data/lib/rail_verdict.rb +64 -0
  96. data/schemas/ai-analysis-v1.schema.json +54 -0
  97. data/schemas/baseline-v1.schema.json +108 -0
  98. data/schemas/configuration-v1.1.schema.json +150 -0
  99. data/schemas/configuration-v1.2.schema.json +182 -0
  100. data/schemas/configuration-v1.3.schema.json +198 -0
  101. data/schemas/configuration-v1.4.schema.json +146 -0
  102. data/schemas/configuration-v1.schema.json +72 -0
  103. data/schemas/coverage-v1.schema.json +51 -0
  104. data/schemas/finding-v1.schema.json +131 -0
  105. data/schemas/minitest-reporter-v1.schema.json +113 -0
  106. data/schemas/repair-packet-v1.schema.json +364 -0
  107. data/schemas/result-v1.schema.json +378 -0
  108. data/schemas/waiver-v1.schema.json +45 -0
  109. data/schemas/waivers-v1.schema.json +67 -0
  110. metadata +192 -0
@@ -0,0 +1,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module RailVerdict
6
+ module Check
7
+ Outcome = Struct.new(:result, :context, :configuration, :findings, keyword_init: true)
8
+
9
+ REGISTRY = {
10
+ "rubocop" => RailVerdict::Analyzers::RuboCop,
11
+ "minitest" => RailVerdict::Analyzers::Minitest,
12
+ "rspec" => RailVerdict::Analyzers::RSpec,
13
+ "simplecov" => RailVerdict::Analyzers::SimpleCov,
14
+ "bundler_audit" => RailVerdict::Analyzers::BundlerAudit
15
+ }.freeze
16
+
17
+ def self.registry
18
+ REGISTRY
19
+ end
20
+
21
+ module_function
22
+
23
+ def execute(repository_root:, config_path:, runner: ProcessRunner, rubocop_command_resolver: nil, analyzer_timeout_seconds: 30.0, interrupted: nil, baseline_path_override: nil, waiver_path_override: nil, clock: Time.now.utc, changed: false, base: nil)
24
+ root = File.realpath(repository_root)
25
+ resolved_config = resolve_config_path(root, config_path)
26
+ configuration = Configuration.load(resolved_config)
27
+ return interrupted_outcome if interrupted&.call
28
+
29
+ probes = probe_enabled_analyzers(root, configuration, runner: runner, rubocop_command_resolver: rubocop_command_resolver, timeout_seconds: analyzer_timeout_seconds)
30
+
31
+ analyzer_versions = probes.transform_values(&:version)
32
+
33
+ git_context = nil
34
+ git_error = nil
35
+ if changed
36
+ begin
37
+ git_context = Git::Context.build(repository_root: root, base_override: base, configuration: configuration, runner: runner)
38
+ rescue Git::Error => error
39
+ git_error = error
40
+ end
41
+ if git_error
42
+ git_incomplete = Verification::Policy.incomplete_result(
43
+ analyzer_results: [],
44
+ findings: [],
45
+ operational_failures: [{ "code" => "failed", "message" => git_error.message }],
46
+ code: "git_scope_failed",
47
+ message: git_error.message
48
+ )
49
+ context_for_error = begin
50
+ RunContext.build(repository_root: root, configuration: configuration, analyzer_versions: analyzer_versions, revision_resolver: revision_resolver(root, runner))
51
+ rescue StandardError
52
+ nil
53
+ end
54
+ git_payload = git_error_payload(git_error, base, configuration)
55
+ enriched = GateResult.new(
56
+ completion_status: git_incomplete.completion_status,
57
+ gate: git_incomplete.gate,
58
+ policy_status: git_incomplete.policy_status,
59
+ findings: git_incomplete.findings,
60
+ analyzer_results: git_incomplete.analyzer_results,
61
+ operational_failures: git_incomplete.operational_failures,
62
+ decision_reasons: git_incomplete.decision_reasons,
63
+ git: git_payload
64
+ )
65
+ return Outcome.new(result: enriched, context: context_for_error, configuration: configuration, findings: [].freeze)
66
+ end
67
+ end
68
+
69
+ context = RunContext.build(
70
+ repository_root: root,
71
+ configuration: configuration,
72
+ analyzer_versions: analyzer_versions,
73
+ revision_resolver: revision_resolver(root, runner),
74
+ git_context: git_context
75
+ )
76
+ return interrupted_outcome(context: context, configuration: configuration) if interrupted&.call
77
+
78
+ analyzer_results = []
79
+ findings = []
80
+ configuration.analyzers.each do |name, selection|
81
+ next unless selection.fetch("enabled")
82
+
83
+ adapter_class = REGISTRY[name]
84
+ unless adapter_class
85
+ analyzer_results << AnalyzerResult.new(
86
+ analyzer: name,
87
+ invocation: { "executable" => name, "argv" => [] },
88
+ execution_status: "unavailable",
89
+ finding_ids: [],
90
+ failure: { "code" => "unavailable", "message" => "analyzer #{name} is not implemented in this build" }
91
+ )
92
+ next
93
+ end
94
+
95
+ adapter = build_adapter(name, rubocop_command_resolver)
96
+ probe = probes[name]
97
+ analyzer_result, analyzer_findings = adapter.run(
98
+ root,
99
+ runner: runner,
100
+ probe_result: probe,
101
+ timeout_seconds: analyzer_timeout_seconds
102
+ )
103
+ analyzer_results << analyzer_result
104
+ findings.concat(analyzer_findings)
105
+ end
106
+ return interrupted_outcome(context: context, configuration: configuration, analyzer_results: analyzer_results, findings: findings) if interrupted&.call
107
+
108
+ baseline = nil
109
+ baseline_meta = nil
110
+ comparison = nil
111
+ classified_findings = findings
112
+ waivers = []
113
+ waiver_meta = nil
114
+ resolved_baseline_path = Baseline.resolve_path(repository_root: root, configuration: configuration, output_override: baseline_path_override)
115
+ resolved_waiver_path = WaiverStore.resolve_path(repository_root: root, configuration: configuration, waiver_override: waiver_path_override)
116
+ begin
117
+ waivers = WaiverStore.read_optional(resolved_waiver_path)
118
+ waiver_meta = { "loaded" => !waivers.empty?, "path" => resolved_waiver_path, "count" => waivers.length }
119
+ rescue Waiver::IncompatibleError => error
120
+ incomplete = Verification::Policy.incomplete_result(
121
+ analyzer_results: analyzer_results,
122
+ findings: findings,
123
+ operational_failures: [{ "code" => "failed", "message" => error.message }],
124
+ code: "waiver_incompatible",
125
+ message: error.message
126
+ )
127
+ enriched = GateResult.new(completion_status: incomplete.completion_status, gate: incomplete.gate, policy_status: incomplete.policy_status, findings: incomplete.findings, analyzer_results: incomplete.analyzer_results, operational_failures: incomplete.operational_failures, decision_reasons: incomplete.decision_reasons, baseline: nil, comparison: { "counts" => {}, "waiver_error" => error.message })
128
+ return Outcome.new(result: enriched, context: context, configuration: configuration, findings: findings.freeze)
129
+ end
130
+ rename_map = git_context ? git_context.rename_map : nil
131
+ begin
132
+ if File.file?(resolved_baseline_path)
133
+ loaded = Baseline.read(resolved_baseline_path)
134
+ baseline = loaded
135
+ baseline_meta = { "loaded" => true, "path" => resolved_baseline_path, "schema_version" => Baseline::SCHEMA_VERSION, "fingerprint_version" => Fingerprint::VERSION, "compatible" => true }
136
+ cmp = Comparison.classify(findings: findings, baseline: loaded, waivers: waivers.map(&:to_h), clock: clock, rename_map: rename_map)
137
+ comparison = { "counts" => cmp.counts, "introduced" => cmp.introduced.map(&:fingerprint).sort, "existing" => cmp.existing.map(&:fingerprint).sort, "resolved" => cmp.resolved.map { |entry| entry.fetch("fingerprint") }.sort, "changed" => cmp.changed.map(&:fingerprint).sort, "moved" => cmp.moved.map(&:fingerprint).sort, "waived" => cmp.counts.fetch("waived", 0) == 0 ? [] : cmp.classified_findings.select { |item| item.state == "waived" }.map(&:fingerprint).sort, "orphaned_waivers" => cmp.orphaned_waivers.map { |w| w["fingerprint"] || w[:fingerprint] }.compact.sort }
138
+ if waiver_meta
139
+ comparison["waivers"] = waiver_meta
140
+ end
141
+ classified_findings = cmp.classified_findings
142
+ elsif waiver_meta && waiver_meta["count"] > 0
143
+ cmp = Comparison.classify(findings: findings, baseline: nil, waivers: waivers.map(&:to_h), clock: clock, rename_map: rename_map)
144
+ comparison = { "counts" => cmp.counts, "introduced" => cmp.introduced.map(&:fingerprint).sort, "existing" => cmp.existing.map(&:fingerprint).sort, "resolved" => cmp.resolved.map { |entry| entry.fetch("fingerprint") }.sort, "changed" => cmp.changed.map(&:fingerprint).sort, "moved" => cmp.moved.map(&:fingerprint).sort, "waived" => cmp.counts.fetch("waived", 0) == 0 ? [] : cmp.classified_findings.select { |item| item.state == "waived" }.map(&:fingerprint).sort, "orphaned_waivers" => cmp.orphaned_waivers.map { |w| w["fingerprint"] || w[:fingerprint] }.compact.sort, "waivers" => waiver_meta }
145
+ classified_findings = cmp.classified_findings
146
+ elsif waiver_meta
147
+ comparison = nil
148
+ end
149
+ rescue Baseline::IncompatibleError => error
150
+ incomplete = Verification::Policy.incomplete_result(
151
+ analyzer_results: analyzer_results,
152
+ findings: findings,
153
+ operational_failures: [{ "code" => "failed", "message" => error.message }],
154
+ code: "baseline_incompatible",
155
+ message: error.message
156
+ )
157
+ enriched = GateResult.new(completion_status: incomplete.completion_status, gate: incomplete.gate, policy_status: incomplete.policy_status, findings: incomplete.findings, analyzer_results: incomplete.analyzer_results, operational_failures: incomplete.operational_failures, decision_reasons: incomplete.decision_reasons, baseline: { "loaded" => false, "path" => resolved_baseline_path, "compatible" => false }, comparison: nil)
158
+ return Outcome.new(result: enriched, context: context, configuration: configuration, findings: findings.freeze)
159
+ end
160
+
161
+ unless git_context.nil?
162
+ analyzer_results = attach_changed_coverage(analyzer_results: analyzer_results, git_context: git_context)
163
+ end
164
+
165
+ filtered_findings_for_policy = classified_findings
166
+ changed_filter_active = !git_context.nil?
167
+ if changed_filter_active
168
+ filtered_findings_for_policy = classified_findings
169
+ end
170
+
171
+ result = Verification::Policy.evaluate(
172
+ configuration: configuration,
173
+ analyzer_results: analyzer_results,
174
+ findings: filtered_findings_for_policy,
175
+ comparison: comparison,
176
+ baseline_meta: baseline_meta,
177
+ git_context: git_context
178
+ )
179
+
180
+ git_payload = nil
181
+ if git_context
182
+ git_payload = build_git_payload(git_context, root)
183
+ end
184
+
185
+ rails_context_h = nil
186
+ begin
187
+ rails_ctx = RailsContext::Context.build(repository_root: root, git_context: git_context)
188
+ rails_context_h = rails_ctx.to_h
189
+ rescue StandardError
190
+ rails_context_h = nil
191
+ end
192
+
193
+ if baseline_meta || comparison || git_payload || rails_context_h
194
+ result = GateResult.new(
195
+ completion_status: result.completion_status,
196
+ gate: result.gate,
197
+ policy_status: result.policy_status,
198
+ findings: result.findings,
199
+ analyzer_results: result.analyzer_results,
200
+ operational_failures: result.operational_failures,
201
+ decision_reasons: result.decision_reasons,
202
+ baseline: baseline_meta,
203
+ comparison: comparison,
204
+ git: git_payload,
205
+ rails_context: rails_context_h
206
+ )
207
+ elsif git_payload && result.complete?
208
+ result = GateResult.new(
209
+ completion_status: result.completion_status,
210
+ gate: result.gate,
211
+ policy_status: result.policy_status,
212
+ findings: result.findings,
213
+ analyzer_results: result.analyzer_results,
214
+ operational_failures: result.operational_failures,
215
+ decision_reasons: result.decision_reasons,
216
+ git: git_payload,
217
+ rails_context: rails_context_h
218
+ )
219
+ elsif git_payload
220
+ result = GateResult.new(
221
+ completion_status: result.completion_status,
222
+ gate: result.gate,
223
+ policy_status: result.policy_status,
224
+ findings: result.findings,
225
+ analyzer_results: result.analyzer_results,
226
+ operational_failures: result.operational_failures,
227
+ decision_reasons: result.decision_reasons,
228
+ git: git_payload,
229
+ rails_context: rails_context_h
230
+ )
231
+ elsif rails_context_h
232
+ result = GateResult.new(
233
+ completion_status: result.completion_status,
234
+ gate: result.gate,
235
+ policy_status: result.policy_status,
236
+ findings: result.findings,
237
+ analyzer_results: result.analyzer_results,
238
+ operational_failures: result.operational_failures,
239
+ decision_reasons: result.decision_reasons,
240
+ rails_context: rails_context_h
241
+ )
242
+ end
243
+ Outcome.new(result: result, context: context, configuration: configuration, findings: classified_findings.freeze)
244
+ rescue ConfigurationError => error
245
+ Outcome.new(
246
+ result: Verification::Policy.incomplete_result(
247
+ operational_failures: [{ "code" => "configuration", "message" => canonical_error_message(error, root) }],
248
+ code: "configuration",
249
+ message: "Configuration is invalid; no analyzer evidence was evaluated."
250
+ ),
251
+ context: nil,
252
+ configuration: nil,
253
+ findings: [].freeze
254
+ )
255
+ rescue ProcessRunner::DirectoryError, RailVerdict::Error => error
256
+ Outcome.new(
257
+ result: Verification::Policy.incomplete_result(
258
+ operational_failures: [{ "code" => "failed", "message" => error.message }],
259
+ code: "execution_failed",
260
+ message: "Verification could not complete."
261
+ ),
262
+ context: nil,
263
+ configuration: nil,
264
+ findings: [].freeze
265
+ )
266
+ end
267
+
268
+ def canonical_error_message(error, root)
269
+ return error.message unless error.source_path
270
+
271
+ relative = Pathname.new(error.source_path).relative_path_from(Pathname.new(root)).to_s
272
+ error.message.gsub(error.source_path, relative)
273
+ end
274
+ private_class_method :canonical_error_message
275
+
276
+ def resolve_config_path(root, config_path)
277
+ path = config_path.to_s
278
+ Pathname.new(path).absolute? ? path : File.expand_path(path, root)
279
+ end
280
+ private_class_method :resolve_config_path
281
+
282
+ def revision_resolver(root, runner)
283
+ lambda do |_resolved_root|
284
+ result = runner.run("git", ["rev-parse", "HEAD"], chdir: root, timeout_seconds: 5.0)
285
+ next nil unless result.status == :exited && result.exit_code == 0
286
+
287
+ revision = result.stdout.to_s.strip
288
+ revision.match?(/\A[0-9a-f]{7,64}\z/) ? revision : nil
289
+ end
290
+ end
291
+ private_class_method :revision_resolver
292
+
293
+ def interrupted_outcome(context: nil, configuration: nil, analyzer_results: [], findings: [])
294
+ Outcome.new(
295
+ result: Verification::Policy.interrupted_result(analyzer_results: analyzer_results, findings: findings),
296
+ context: context,
297
+ configuration: configuration,
298
+ findings: findings.freeze
299
+ )
300
+ end
301
+ private_class_method :interrupted_outcome
302
+
303
+ def build_adapter(name, rubocop_command_resolver)
304
+ case name
305
+ when "rubocop"
306
+ RailVerdict::Analyzers::RuboCop.new(command_resolver: rubocop_command_resolver)
307
+ when "minitest"
308
+ RailVerdict::Analyzers::Minitest.new
309
+ when "rspec"
310
+ RailVerdict::Analyzers::RSpec.new
311
+ when "simplecov"
312
+ RailVerdict::Analyzers::SimpleCov.new
313
+ when "bundler_audit"
314
+ RailVerdict::Analyzers::BundlerAudit.new
315
+ end
316
+ end
317
+ private_class_method :build_adapter
318
+
319
+ def git_error_payload(error, base_override, configuration)
320
+ base_for_payload = base_override && !base_override.to_s.strip.empty? ? base_override.to_s.strip : (configuration.respond_to?(:git_base) ? configuration.git_base : nil)
321
+ {
322
+ "head" => nil,
323
+ "base" => base_for_payload,
324
+ "merge_base" => nil,
325
+ "changed_files" => [],
326
+ "changed_line_set" => {},
327
+ "binary_paths" => [],
328
+ "conflicted_paths" => [],
329
+ "error" => error.class.name.split("::").last,
330
+ "error_message" => error.message
331
+ }
332
+ end
333
+ private_class_method :git_error_payload
334
+
335
+ def build_git_payload(git_context, root)
336
+ changed_files = git_context.changed_files.map do |file|
337
+ entry = { "status" => file.status.to_s, "path" => file.path, "score" => file.score }
338
+ entry["old_path"] = file.old_path if file.old_path
339
+ entry["new_path"] = file.new_path if file.new_path
340
+ entry["binary"] = file.binary if file.respond_to?(:binary) && !file.binary.nil?
341
+ entry
342
+ end.sort_by { |entry| entry["path"].to_s }
343
+ {
344
+ "repository_root" => File.basename(root),
345
+ "head" => git_context.head,
346
+ "base" => git_context.base,
347
+ "merge_base" => git_context.merge_base,
348
+ "changed_files" => changed_files,
349
+ "changed_line_set" => git_context.changed_line_set.transform_keys(&:to_s).transform_values { |lines| lines.sort },
350
+ "binary_paths" => git_context.binary_paths.sort,
351
+ "conflicted_paths" => git_context.conflicted_paths.sort
352
+ }
353
+ end
354
+ private_class_method :build_git_payload
355
+
356
+ def attach_changed_coverage(analyzer_results:, git_context:)
357
+ analyzer_results.map do |result|
358
+ next result unless result.analyzer == "simplecov"
359
+ next result unless result.execution_status == "succeeded"
360
+
361
+ summary = result.evidence_summary
362
+ next result unless summary.is_a?(Hash)
363
+
364
+ coverage_doc = summary["_coverage_document"]
365
+ document = coverage_doc || summary_to_coverage_document(summary)
366
+ next result unless document
367
+
368
+ line_set = git_context.changed_line_set
369
+ begin
370
+ changed = Coverage::ChangedLineEvaluator.evaluate(coverage_document: document, line_set: line_set)
371
+ rescue ArgumentError
372
+ next result
373
+ end
374
+ new_summary = summary.dup
375
+ new_summary.delete("_coverage_document")
376
+ new_summary["changed_line_coverage"] = changed
377
+ new_summary["changed_line_set"] = line_set.transform_keys(&:to_s).transform_values { |lines| lines.sort }
378
+ AnalyzerResult.new(
379
+ analyzer: result.analyzer,
380
+ tool_version: result.tool_version,
381
+ invocation: result.invocation,
382
+ execution_status: result.execution_status,
383
+ finding_ids: result.finding_ids,
384
+ evidence_summary: new_summary
385
+ )
386
+ end
387
+ end
388
+ private_class_method :attach_changed_coverage
389
+
390
+ def summary_to_coverage_document(summary)
391
+ files = summary["files"] || summary["_files"]
392
+ return nil unless files.is_a?(Array)
393
+
394
+ { "files" => files }
395
+ end
396
+ private_class_method :summary_to_coverage_document
397
+
398
+ def probe_enabled_analyzers(root, configuration, runner:, rubocop_command_resolver:, timeout_seconds:)
399
+ probes = {}
400
+ configuration.analyzers.each do |name, selection|
401
+ next unless selection.fetch("enabled")
402
+
403
+ adapter_class = REGISTRY[name]
404
+ next unless adapter_class
405
+
406
+ adapter = build_adapter(name, rubocop_command_resolver)
407
+ next unless adapter
408
+
409
+ probes[name] = adapter.probe(root, runner: runner, timeout_seconds: timeout_seconds)
410
+ end
411
+ probes
412
+ end
413
+ private_class_method :probe_enabled_analyzers
414
+ end
415
+ end