rail_verdict 1.0.1 → 1.1.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 +50 -0
- data/lib/rail_verdict/cli.rb +35 -0
- data/lib/rail_verdict/git/context.rb +25 -6
- data/lib/rail_verdict/git/diff_parser.rb +17 -5
- data/lib/rail_verdict/pr_intelligence.rb +188 -0
- data/lib/rail_verdict/reporters/pr_intelligence.rb +84 -0
- data/lib/rail_verdict/schema_validator.rb +5 -0
- data/lib/rail_verdict/version.rb +1 -1
- data/lib/rail_verdict.rb +2 -0
- data/schemas/pr-intelligence-v1.schema.json +285 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 808405ac6448a462283966840c5b9074afff5cd43f0c2c3b477d9951e7dfe0a8
|
|
4
|
+
data.tar.gz: 2f3aa97c547c6db980c284ca4081bc09230d4e13f63369cced690f60d18ccddd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a70e6b7d070e6d4990b0b7991af2332c572e0ad2c9438b0ada3afb7e095729285b5b5714c8be8be512e1c7d8907e089bec08d5e4b62524a401719d215bc2eb28
|
|
7
|
+
data.tar.gz: 6881881853b9073e8add4c6946d1bdb0da79928a57b6d27a057e9b1646dcbe9e960a9f646c9fb9f62743f5ba3c0d61f93fc2a1fa572b529e8071268f97b69f0b
|
data/README.md
CHANGED
|
@@ -182,6 +182,9 @@ railverdict check
|
|
|
182
182
|
|
|
183
183
|
# 5. Inspect normalized findings
|
|
184
184
|
railverdict findings
|
|
185
|
+
|
|
186
|
+
# 6. Summarize one pull-request change from a single verification run
|
|
187
|
+
railverdict pr --base origin/main
|
|
185
188
|
```
|
|
186
189
|
|
|
187
190
|
### Example: A Passing Gate
|
|
@@ -277,6 +280,53 @@ railverdict check --changed --base HEAD~1
|
|
|
277
280
|
railverdict check --changed --base origin/main --format json
|
|
278
281
|
```
|
|
279
282
|
|
|
283
|
+
### PR Intelligence
|
|
284
|
+
|
|
285
|
+
`railverdict pr --base origin/main` produces a deterministic, versioned summary
|
|
286
|
+
of the change, quality delta, objective Rails path signals, analyzer evidence,
|
|
287
|
+
test metrics, and coverage available from that same verification run. Use
|
|
288
|
+
`--format json` for machine consumers. Signals are deterministic attention
|
|
289
|
+
indicators, not risk probabilities; `GateResult` and policy remain the only
|
|
290
|
+
verification authority.
|
|
291
|
+
|
|
292
|
+
Example console output:
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
RailVerdict PR Intelligence
|
|
296
|
+
|
|
297
|
+
Gate: PASS
|
|
298
|
+
Completion: complete
|
|
299
|
+
Revision: 3f4a2c1d9e00 (base 8a7b6c5d4e33)
|
|
300
|
+
|
|
301
|
+
Change
|
|
302
|
+
4 files
|
|
303
|
+
+38 / -7
|
|
304
|
+
added 1 modified 2 deleted 0 renamed 1
|
|
305
|
+
|
|
306
|
+
Quality Delta
|
|
307
|
+
introduced 0 resolved 2 existing 8
|
|
308
|
+
|
|
309
|
+
Signals
|
|
310
|
+
Database YES
|
|
311
|
+
Authorization YES
|
|
312
|
+
Routes NO
|
|
313
|
+
Dependencies NO
|
|
314
|
+
Configuration NO
|
|
315
|
+
Tests YES
|
|
316
|
+
|
|
317
|
+
Evidence
|
|
318
|
+
rubocop: succeeded
|
|
319
|
+
rspec: succeeded
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The JSON contract is [`pr-intelligence-v1.schema.json`](schemas/pr-intelligence-v1.schema.json).
|
|
323
|
+
It includes `head`, `base`, `merge_base`, and configuration-digest provenance,
|
|
324
|
+
plus a stable projection of the canonical `gate_result` without checkout-local
|
|
325
|
+
paths. Without a compatible baseline, `quality_delta.available` is
|
|
326
|
+
`false` with `reason: "baseline_not_available"`; it does not emit fake zeroes.
|
|
327
|
+
Invalid Git bases and incomplete required evidence remain `INCOMPLETE` with
|
|
328
|
+
exit code `2`.
|
|
329
|
+
|
|
280
330
|
### Key Capabilities
|
|
281
331
|
|
|
282
332
|
- **Merge-Base Resolution:** Computes the exact `merge-base` between `HEAD` and the target branch;
|
data/lib/rail_verdict/cli.rb
CHANGED
|
@@ -20,6 +20,7 @@ module RailVerdict
|
|
|
20
20
|
init Write the default .railverdict.yml configuration
|
|
21
21
|
doctor Report configuration and analyzer observations
|
|
22
22
|
check Run verification and print the gate result
|
|
23
|
+
pr Summarize one changed-scope verification for review
|
|
23
24
|
baseline create Deferred boundary; Phase 3 owns baseline writes
|
|
24
25
|
findings Print normalized findings from the evidence run
|
|
25
26
|
explain Explain a finding with optional AI
|
|
@@ -53,6 +54,8 @@ module RailVerdict
|
|
|
53
54
|
command_doctor(argv.drop(1))
|
|
54
55
|
when "check"
|
|
55
56
|
command_check(argv.drop(1))
|
|
57
|
+
when "pr"
|
|
58
|
+
command_pr(argv.drop(1))
|
|
56
59
|
when "baseline"
|
|
57
60
|
command_baseline(argv.drop(1))
|
|
58
61
|
when "findings"
|
|
@@ -144,6 +147,38 @@ module RailVerdict
|
|
|
144
147
|
exit_code_for(outcome.result, interrupted: interrupted)
|
|
145
148
|
end
|
|
146
149
|
|
|
150
|
+
def command_pr(argv)
|
|
151
|
+
options = { config: DEFAULT_CONFIG_PATH, format: "console", base: nil, baseline: nil, waiver: nil }
|
|
152
|
+
parser = OptionParser.new do |opts|
|
|
153
|
+
opts.banner = "Usage: railverdict pr [--config PATH] [--format console|json] [--base REV] [--baseline PATH] [--waiver PATH]"
|
|
154
|
+
opts.on("--config PATH", String) { |value| options[:config] = value }
|
|
155
|
+
opts.on("--format FORMAT", String) { |value| options[:format] = value }
|
|
156
|
+
opts.on("--base REV", String) { |value| options[:base] = value }
|
|
157
|
+
opts.on("--baseline PATH", String) { |value| options[:baseline] = value }
|
|
158
|
+
opts.on("--waiver PATH", String) { |value| options[:waiver] = value }
|
|
159
|
+
end
|
|
160
|
+
parse!(parser, argv)
|
|
161
|
+
unless %w[console json].include?(options[:format])
|
|
162
|
+
raise RailVerdict::UsageError, "invalid --format #{options[:format].inspect}; expected console or json"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
outcome, interrupted = execute_check(options.merge(changed: true))
|
|
166
|
+
document = PRIntelligence.document(outcome)
|
|
167
|
+
if options[:format] == "json"
|
|
168
|
+
@stdout.write(JSON.generate(document) + "\n")
|
|
169
|
+
else
|
|
170
|
+
@stdout.write(Reporters::PRIntelligence.render(document))
|
|
171
|
+
end
|
|
172
|
+
exit_code_for(outcome.result, interrupted: interrupted)
|
|
173
|
+
rescue RailVerdict::UsageError => error
|
|
174
|
+
@stderr.puts "railverdict pr: #{error.message}"
|
|
175
|
+
@stderr.puts USAGE
|
|
176
|
+
EXIT_NO_GATE
|
|
177
|
+
rescue RailVerdict::Error => error
|
|
178
|
+
@stderr.puts "railverdict pr: #{error.message}"
|
|
179
|
+
EXIT_NO_GATE
|
|
180
|
+
end
|
|
181
|
+
|
|
147
182
|
def command_baseline(argv)
|
|
148
183
|
subcommand = argv.first
|
|
149
184
|
raise RailVerdict::UsageError, "unknown baseline subcommand: #{subcommand.inspect}; only `baseline create` exists" unless subcommand == "create"
|
|
@@ -6,21 +6,32 @@ require "pathname"
|
|
|
6
6
|
module RailVerdict
|
|
7
7
|
module Git
|
|
8
8
|
class ChangedFile
|
|
9
|
-
attr_reader :status, :path, :old_path, :new_path, :score
|
|
9
|
+
attr_reader :status, :path, :old_path, :new_path, :score, :lines_added, :lines_removed
|
|
10
10
|
attr_accessor :binary
|
|
11
11
|
|
|
12
|
-
def initialize(status:, path:, old_path:, new_path:, score:, binary: false)
|
|
12
|
+
def initialize(status:, path:, old_path:, new_path:, score:, binary: false, lines_added: nil, lines_removed: nil)
|
|
13
13
|
@status = status
|
|
14
14
|
@path = path
|
|
15
15
|
@old_path = old_path
|
|
16
16
|
@new_path = new_path
|
|
17
17
|
@score = score
|
|
18
18
|
@binary = binary
|
|
19
|
+
@lines_added = lines_added
|
|
20
|
+
@lines_removed = lines_removed
|
|
19
21
|
freeze
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def to_h
|
|
23
|
-
{
|
|
25
|
+
{
|
|
26
|
+
"status" => status.to_s,
|
|
27
|
+
"path" => path,
|
|
28
|
+
"old_path" => old_path,
|
|
29
|
+
"new_path" => new_path,
|
|
30
|
+
"score" => score,
|
|
31
|
+
"binary" => binary,
|
|
32
|
+
"lines_added" => lines_added,
|
|
33
|
+
"lines_removed" => lines_removed
|
|
34
|
+
}
|
|
24
35
|
end
|
|
25
36
|
end
|
|
26
37
|
|
|
@@ -236,11 +247,19 @@ module RailVerdict
|
|
|
236
247
|
info = numstat[key] if key
|
|
237
248
|
is_binary = !!(info && info[:binary])
|
|
238
249
|
binary_set.add(key) if is_binary
|
|
239
|
-
ChangedFile.new(
|
|
250
|
+
ChangedFile.new(
|
|
251
|
+
status: file.status,
|
|
252
|
+
path: file.path,
|
|
253
|
+
old_path: file.old_path,
|
|
254
|
+
new_path: file.new_path,
|
|
255
|
+
score: file.score,
|
|
256
|
+
binary: is_binary,
|
|
257
|
+
lines_added: info && info[:added],
|
|
258
|
+
lines_removed: info && info[:deleted]
|
|
259
|
+
)
|
|
240
260
|
end
|
|
241
261
|
|
|
242
|
-
|
|
243
|
-
[changed, binary_set]
|
|
262
|
+
[enriched.sort_by { |file| (file.path || file.old_path).to_s }, binary_set]
|
|
244
263
|
end
|
|
245
264
|
private_class_method :resolve_changed_files
|
|
246
265
|
|
|
@@ -62,7 +62,7 @@ module RailVerdict
|
|
|
62
62
|
parts.each do |entry|
|
|
63
63
|
next if entry.empty?
|
|
64
64
|
|
|
65
|
-
fields = entry.split("\t")
|
|
65
|
+
fields = entry.split("\t", 3)
|
|
66
66
|
next unless fields.length == 3
|
|
67
67
|
|
|
68
68
|
added, deleted, path = fields
|
|
@@ -78,15 +78,27 @@ module RailVerdict
|
|
|
78
78
|
parts = raw.split("\0")
|
|
79
79
|
parts.pop if parts.last == ""
|
|
80
80
|
result = {}
|
|
81
|
-
|
|
81
|
+
index = 0
|
|
82
|
+
while index < parts.length
|
|
83
|
+
entry = parts[index]
|
|
84
|
+
index += 1
|
|
82
85
|
next if entry.empty?
|
|
83
86
|
|
|
84
|
-
fields = entry.split("\t")
|
|
85
|
-
next unless fields.length >=
|
|
87
|
+
fields = entry.split("\t", 3)
|
|
88
|
+
next unless fields.length >= 2
|
|
86
89
|
|
|
87
90
|
added, deleted = fields[0], fields[1]
|
|
88
91
|
binary = added == "-" || deleted == "-"
|
|
89
|
-
|
|
92
|
+
if fields.length >= 3 && !fields[2].empty?
|
|
93
|
+
path = fields[2]
|
|
94
|
+
else
|
|
95
|
+
old_path = parts[index]
|
|
96
|
+
new_path = parts[index + 1]
|
|
97
|
+
break if old_path.nil? || new_path.nil?
|
|
98
|
+
|
|
99
|
+
path = new_path
|
|
100
|
+
index += 2
|
|
101
|
+
end
|
|
90
102
|
key = normalize_path(path)
|
|
91
103
|
result[key] = { added: binary ? nil : added.to_i, deleted: binary ? nil : deleted.to_i, binary: binary }
|
|
92
104
|
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RailVerdict
|
|
6
|
+
module PRIntelligence
|
|
7
|
+
SCHEMA_VERSION = "1.0"
|
|
8
|
+
MAX_SIGNAL_EVIDENCE = 20
|
|
9
|
+
SIGNALS = {
|
|
10
|
+
"database_change" => lambda { |path| path == "db/schema.rb" || path == "db/structure.sql" || path.start_with?("db/migrate/") },
|
|
11
|
+
"routes_change" => lambda { |path| path == "config/routes.rb" || path.start_with?("config/routes/") },
|
|
12
|
+
"dependency_change" => lambda { |path| %w[Gemfile Gemfile.lock].include?(path) },
|
|
13
|
+
"configuration_change" => lambda { |path| path == "config" || path.start_with?("config/") },
|
|
14
|
+
"authorization_change" => lambda { |path| path.start_with?("app/policies/") },
|
|
15
|
+
"tests_change" => lambda { |path| path.start_with?("spec/") || path.start_with?("test/") }
|
|
16
|
+
}.freeze
|
|
17
|
+
STATUS_KEYS = %w[added modified deleted renamed].freeze
|
|
18
|
+
TEST_KEYS = %w[tests_total assertions failures errors skips duration_seconds seed runner].freeze
|
|
19
|
+
GATE_RESULT_KEYS = %w[
|
|
20
|
+
schema_version completion_status gate policy_status findings
|
|
21
|
+
operational_failures decision_reasons
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def document(outcome)
|
|
27
|
+
result = outcome.result
|
|
28
|
+
git_context = outcome.context&.git_context
|
|
29
|
+
git = result.git || {}
|
|
30
|
+
|
|
31
|
+
document = {
|
|
32
|
+
"schema_version" => SCHEMA_VERSION,
|
|
33
|
+
"provenance" => provenance(outcome, git_context, git),
|
|
34
|
+
"gate_result" => gate_result(result),
|
|
35
|
+
"change" => change(git_context),
|
|
36
|
+
"signals" => signals(git_context),
|
|
37
|
+
"quality_delta" => quality_delta(result),
|
|
38
|
+
"analyzer_evidence" => analyzer_evidence(result),
|
|
39
|
+
"test_intelligence" => test_intelligence(result),
|
|
40
|
+
"coverage" => coverage(result)
|
|
41
|
+
}
|
|
42
|
+
errors = SchemaValidator.validate_pr_intelligence(document)
|
|
43
|
+
raise RailVerdict::Error, "pr-intelligence-v1 validation failed: #{errors.join('; ')}" unless errors.empty?
|
|
44
|
+
|
|
45
|
+
document
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_json(outcome)
|
|
49
|
+
JSON.generate(document(outcome)) + "\n"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def provenance(outcome, git_context, git)
|
|
53
|
+
context = outcome.context
|
|
54
|
+
{
|
|
55
|
+
"head" => git_context&.head || git["head"],
|
|
56
|
+
"base" => git_context&.base || git["base"],
|
|
57
|
+
"merge_base" => git_context&.merge_base || git["merge_base"],
|
|
58
|
+
"configuration_digest" => context&.configuration_digest
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
private_class_method :provenance
|
|
62
|
+
|
|
63
|
+
def gate_result(result)
|
|
64
|
+
source = result.to_schema_h
|
|
65
|
+
GATE_RESULT_KEYS.to_h { |key| [key, source.fetch(key)] }
|
|
66
|
+
end
|
|
67
|
+
private_class_method :gate_result
|
|
68
|
+
|
|
69
|
+
def change(git_context)
|
|
70
|
+
return { "available" => false, "reason" => "git_scope_unavailable" } unless git_context
|
|
71
|
+
|
|
72
|
+
files = git_context.changed_files
|
|
73
|
+
lines_added = line_total(files, :lines_added)
|
|
74
|
+
lines_removed = line_total(files, :lines_removed)
|
|
75
|
+
status_counts = STATUS_KEYS.to_h { |status| [status, files.count { |file| file.status.to_s == status }] }
|
|
76
|
+
{
|
|
77
|
+
"available" => true,
|
|
78
|
+
"files_changed" => files.length,
|
|
79
|
+
"lines_added" => lines_added,
|
|
80
|
+
"lines_removed" => lines_removed,
|
|
81
|
+
"status_counts" => status_counts
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
private_class_method :change
|
|
85
|
+
|
|
86
|
+
def line_total(files, attribute)
|
|
87
|
+
values = files.map { |file| file.public_send(attribute) }
|
|
88
|
+
return 0 if values.empty?
|
|
89
|
+
return nil unless values.all? { |value| value.is_a?(Integer) && value >= 0 }
|
|
90
|
+
|
|
91
|
+
values.sum
|
|
92
|
+
end
|
|
93
|
+
private_class_method :line_total
|
|
94
|
+
|
|
95
|
+
def signals(git_context)
|
|
96
|
+
paths = if git_context
|
|
97
|
+
git_context.changed_files.flat_map { |file| [file.path, file.old_path, file.new_path] }.compact.uniq.sort
|
|
98
|
+
else
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
available = !git_context.nil?
|
|
102
|
+
|
|
103
|
+
SIGNALS.to_h do |name, matcher|
|
|
104
|
+
evidence = available ? paths.select { |path| matcher.call(path) } : []
|
|
105
|
+
evidence_head = evidence.first(MAX_SIGNAL_EVIDENCE)
|
|
106
|
+
entry = {
|
|
107
|
+
"available" => available,
|
|
108
|
+
"present" => !evidence.empty?,
|
|
109
|
+
"evidence" => evidence_head,
|
|
110
|
+
"additional_evidence_count" => [evidence.length - evidence_head.length, 0].max
|
|
111
|
+
}
|
|
112
|
+
entry["reason"] = "git_scope_unavailable" unless available
|
|
113
|
+
[name, entry]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
private_class_method :signals
|
|
117
|
+
|
|
118
|
+
def quality_delta(result)
|
|
119
|
+
comparison = result.comparison
|
|
120
|
+
baseline = result.baseline
|
|
121
|
+
unless baseline && baseline["loaded"] == true && comparison.is_a?(Hash)
|
|
122
|
+
return { "available" => false, "reason" => baseline && baseline["compatible"] == false ? "baseline_incompatible" : "baseline_not_available" }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
counts = comparison.fetch("counts", {})
|
|
126
|
+
{
|
|
127
|
+
"available" => true,
|
|
128
|
+
"introduced" => counts.fetch("introduced", 0),
|
|
129
|
+
"existing" => counts.fetch("existing", 0),
|
|
130
|
+
"resolved" => counts.fetch("resolved", 0),
|
|
131
|
+
"changed" => counts.fetch("changed", 0),
|
|
132
|
+
"moved" => counts.fetch("moved", 0),
|
|
133
|
+
"waived" => counts.fetch("waived", 0),
|
|
134
|
+
"orphaned_waivers" => Array(comparison["orphaned_waivers"]).length
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
private_class_method :quality_delta
|
|
138
|
+
|
|
139
|
+
def analyzer_evidence(result)
|
|
140
|
+
result.analyzer_results.sort_by(&:analyzer).map do |analyzer|
|
|
141
|
+
entry = {
|
|
142
|
+
"analyzer" => analyzer.analyzer,
|
|
143
|
+
"execution_status" => analyzer.execution_status,
|
|
144
|
+
"evidence_status" => analyzer.evidence_status
|
|
145
|
+
}
|
|
146
|
+
entry["tool_version"] = analyzer.tool_version if analyzer.tool_version
|
|
147
|
+
entry
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
private_class_method :analyzer_evidence
|
|
151
|
+
|
|
152
|
+
def test_intelligence(result)
|
|
153
|
+
analyzers = {}
|
|
154
|
+
result.analyzer_results.sort_by(&:analyzer).each do |analyzer|
|
|
155
|
+
next unless %w[minitest rspec].include?(analyzer.analyzer)
|
|
156
|
+
next unless analyzer.execution_status == "succeeded" && analyzer.evidence_summary.is_a?(Hash)
|
|
157
|
+
|
|
158
|
+
summary = analyzer.evidence_summary
|
|
159
|
+
analyzers[analyzer.analyzer] = TEST_KEYS.each_with_object({}) do |key, values|
|
|
160
|
+
values[key] = summary[key] if summary.key?(key)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
return { "available" => false, "reason" => "test_metrics_unavailable" } if analyzers.empty?
|
|
164
|
+
|
|
165
|
+
{ "available" => true, "analyzers" => analyzers }
|
|
166
|
+
end
|
|
167
|
+
private_class_method :test_intelligence
|
|
168
|
+
|
|
169
|
+
def coverage(result)
|
|
170
|
+
analyzer = result.analyzer_results.find { |item| item.analyzer == "simplecov" }
|
|
171
|
+
summary = analyzer&.evidence_summary
|
|
172
|
+
unless analyzer&.execution_status == "succeeded" && summary.is_a?(Hash)
|
|
173
|
+
return { "available" => false, "reason" => "coverage_evidence_unavailable" }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
output = { "available" => true }
|
|
177
|
+
output["global_percent"] = summary["percent"] if summary["percent"].is_a?(Numeric)
|
|
178
|
+
changed = summary["changed_line_coverage"]
|
|
179
|
+
if changed.is_a?(Hash) && changed["percent"].is_a?(Numeric)
|
|
180
|
+
output["changed_lines_percent"] = changed["percent"]
|
|
181
|
+
output["changed_lines_covered"] = changed["covered_lines"] if changed["covered_lines"].is_a?(Integer)
|
|
182
|
+
output["changed_lines_executable"] = changed["executable_lines"] if changed["executable_lines"].is_a?(Integer)
|
|
183
|
+
end
|
|
184
|
+
output
|
|
185
|
+
end
|
|
186
|
+
private_class_method :coverage
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailVerdict
|
|
4
|
+
module Reporters
|
|
5
|
+
module PRIntelligence
|
|
6
|
+
SIGNAL_LABELS = {
|
|
7
|
+
"database_change" => "Database",
|
|
8
|
+
"authorization_change" => "Authorization",
|
|
9
|
+
"routes_change" => "Routes",
|
|
10
|
+
"dependency_change" => "Dependencies",
|
|
11
|
+
"configuration_change" => "Configuration",
|
|
12
|
+
"tests_change" => "Tests"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def render(document)
|
|
18
|
+
gate_result = document.fetch("gate_result")
|
|
19
|
+
lines = ["RailVerdict PR Intelligence", "", "Gate: #{gate_result.fetch('gate')}", "Completion: #{gate_result.fetch('completion_status')}"]
|
|
20
|
+
provenance = document.fetch("provenance")
|
|
21
|
+
lines << "Revision: #{short(provenance['head'])} (base #{short(provenance['base'])})"
|
|
22
|
+
|
|
23
|
+
change = document.fetch("change")
|
|
24
|
+
lines << ""
|
|
25
|
+
lines << "Change"
|
|
26
|
+
if change["available"]
|
|
27
|
+
lines << " #{change.fetch('files_changed')} files"
|
|
28
|
+
lines << " +#{change['lines_added'] || '?'} / -#{change['lines_removed'] || '?'}"
|
|
29
|
+
counts = change.fetch("status_counts")
|
|
30
|
+
lines << " added #{counts['added']} modified #{counts['modified']} deleted #{counts['deleted']} renamed #{counts['renamed']}"
|
|
31
|
+
else
|
|
32
|
+
lines << " unavailable (#{change.fetch('reason')})"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
delta = document.fetch("quality_delta")
|
|
36
|
+
lines << ""
|
|
37
|
+
lines << "Quality Delta"
|
|
38
|
+
if delta["available"]
|
|
39
|
+
lines << " introduced #{delta['introduced']} resolved #{delta['resolved']} existing #{delta['existing']}"
|
|
40
|
+
lines << " changed #{delta['changed']} moved #{delta['moved']} waived #{delta['waived']} orphaned waivers #{delta['orphaned_waivers']}"
|
|
41
|
+
else
|
|
42
|
+
lines << " unavailable (#{delta.fetch('reason')})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
lines << ""
|
|
46
|
+
lines << "Signals"
|
|
47
|
+
document.fetch("signals").each do |name, signal|
|
|
48
|
+
label = SIGNAL_LABELS.fetch(name, name)
|
|
49
|
+
value = signal["available"] ? (signal["present"] ? "YES" : "NO") : "N/A"
|
|
50
|
+
lines << format(" %-14s %s", label, value)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
lines << ""
|
|
54
|
+
lines << "Evidence"
|
|
55
|
+
evidence = document.fetch("analyzer_evidence")
|
|
56
|
+
if evidence.empty?
|
|
57
|
+
lines << " none"
|
|
58
|
+
else
|
|
59
|
+
evidence.each { |entry| lines << " #{entry.fetch('analyzer')}: #{entry.fetch('execution_status')}" }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
tests = document.fetch("test_intelligence")
|
|
63
|
+
lines << ""
|
|
64
|
+
lines << "Tests: #{tests['available'] ? tests.fetch('analyzers').map { |name, summary| "#{name} #{summary['tests_total']} total, #{summary['failures']} failures" }.join('; ') : tests.fetch('reason')}"
|
|
65
|
+
coverage = document.fetch("coverage")
|
|
66
|
+
coverage_text = if coverage["available"]
|
|
67
|
+
parts = []
|
|
68
|
+
parts << "global #{coverage['global_percent']}%" if coverage.key?("global_percent")
|
|
69
|
+
parts << "changed #{coverage['changed_lines_percent']}%" if coverage.key?("changed_lines_percent")
|
|
70
|
+
parts.join(", ")
|
|
71
|
+
else
|
|
72
|
+
coverage.fetch("reason")
|
|
73
|
+
end
|
|
74
|
+
lines << "Coverage: #{coverage_text}"
|
|
75
|
+
lines.join("\n") + "\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def short(value)
|
|
79
|
+
value ? value.to_s[0, 12] : "unknown"
|
|
80
|
+
end
|
|
81
|
+
private_class_method :short
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -18,6 +18,7 @@ module RailVerdict
|
|
|
18
18
|
WAIVER_SCHEMA = "waiver-v1.schema.json"
|
|
19
19
|
WAIVERS_SCHEMA = "waivers-v1.schema.json"
|
|
20
20
|
REPAIR_PACKET_SCHEMA = "repair-packet-v1.schema.json"
|
|
21
|
+
PR_INTELLIGENCE_SCHEMA = "pr-intelligence-v1.schema.json"
|
|
21
22
|
|
|
22
23
|
def self.schema_dir
|
|
23
24
|
File.expand_path("../../schemas", __dir__)
|
|
@@ -68,6 +69,10 @@ module RailVerdict
|
|
|
68
69
|
validate(data, REPAIR_PACKET_SCHEMA)
|
|
69
70
|
end
|
|
70
71
|
|
|
72
|
+
def self.validate_pr_intelligence(data)
|
|
73
|
+
validate(data, PR_INTELLIGENCE_SCHEMA)
|
|
74
|
+
end
|
|
75
|
+
|
|
71
76
|
def self.validate(data, schema_name)
|
|
72
77
|
schema = load_schema(schema_name)
|
|
73
78
|
JSONSchemer.schema(schema).validate(data).map { |error| format_error(error) }
|
data/lib/rail_verdict/version.rb
CHANGED
data/lib/rail_verdict.rb
CHANGED
|
@@ -36,6 +36,8 @@ require_relative "rail_verdict/reporters/console"
|
|
|
36
36
|
require_relative "rail_verdict/reporters/json_reporter"
|
|
37
37
|
require_relative "rail_verdict/reporters/sarif"
|
|
38
38
|
require_relative "rail_verdict/reporters/github_annotations"
|
|
39
|
+
require_relative "rail_verdict/reporters/pr_intelligence"
|
|
40
|
+
require_relative "rail_verdict/pr_intelligence"
|
|
39
41
|
require_relative "rail_verdict/cli"
|
|
40
42
|
require_relative "rail_verdict/intelligence"
|
|
41
43
|
require_relative "rail_verdict/intelligence/source_reader"
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://railverdict.dev/schemas/pr-intelligence/v1.schema.json",
|
|
4
|
+
"title": "RailVerdict PR Intelligence v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"provenance",
|
|
10
|
+
"gate_result",
|
|
11
|
+
"change",
|
|
12
|
+
"signals",
|
|
13
|
+
"quality_delta",
|
|
14
|
+
"analyzer_evidence",
|
|
15
|
+
"test_intelligence",
|
|
16
|
+
"coverage"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"schema_version": { "const": "1.0" },
|
|
20
|
+
"provenance": { "$ref": "#/$defs/provenance" },
|
|
21
|
+
"gate_result": { "$ref": "#/$defs/gate_result" },
|
|
22
|
+
"change": { "$ref": "#/$defs/change" },
|
|
23
|
+
"signals": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": [
|
|
27
|
+
"database_change",
|
|
28
|
+
"routes_change",
|
|
29
|
+
"dependency_change",
|
|
30
|
+
"configuration_change",
|
|
31
|
+
"authorization_change",
|
|
32
|
+
"tests_change"
|
|
33
|
+
],
|
|
34
|
+
"properties": {
|
|
35
|
+
"database_change": { "$ref": "#/$defs/signal" },
|
|
36
|
+
"routes_change": { "$ref": "#/$defs/signal" },
|
|
37
|
+
"dependency_change": { "$ref": "#/$defs/signal" },
|
|
38
|
+
"configuration_change": { "$ref": "#/$defs/signal" },
|
|
39
|
+
"authorization_change": { "$ref": "#/$defs/signal" },
|
|
40
|
+
"tests_change": { "$ref": "#/$defs/signal" }
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"quality_delta": { "$ref": "#/$defs/quality_delta" },
|
|
44
|
+
"analyzer_evidence": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "$ref": "#/$defs/analyzer_evidence" }
|
|
47
|
+
},
|
|
48
|
+
"test_intelligence": { "$ref": "#/$defs/test_intelligence" },
|
|
49
|
+
"coverage": { "$ref": "#/$defs/coverage" }
|
|
50
|
+
},
|
|
51
|
+
"$defs": {
|
|
52
|
+
"nullable_string": { "type": ["string", "null"] },
|
|
53
|
+
"provenance": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"required": ["head", "base", "merge_base", "configuration_digest"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"head": { "$ref": "#/$defs/nullable_string" },
|
|
59
|
+
"base": { "$ref": "#/$defs/nullable_string" },
|
|
60
|
+
"merge_base": { "$ref": "#/$defs/nullable_string" },
|
|
61
|
+
"configuration_digest": { "$ref": "#/$defs/nullable_string" }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"change": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": ["available"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"available": { "type": "boolean" },
|
|
70
|
+
"reason": { "type": "string" },
|
|
71
|
+
"files_changed": { "type": "integer", "minimum": 0 },
|
|
72
|
+
"lines_added": { "type": ["integer", "null"], "minimum": 0 },
|
|
73
|
+
"lines_removed": { "type": ["integer", "null"], "minimum": 0 },
|
|
74
|
+
"status_counts": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"required": ["added", "modified", "deleted", "renamed"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"added": { "type": "integer", "minimum": 0 },
|
|
80
|
+
"modified": { "type": "integer", "minimum": 0 },
|
|
81
|
+
"deleted": { "type": "integer", "minimum": 0 },
|
|
82
|
+
"renamed": { "type": "integer", "minimum": 0 }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"allOf": [
|
|
87
|
+
{
|
|
88
|
+
"if": { "properties": { "available": { "const": true } } },
|
|
89
|
+
"then": { "required": ["files_changed", "lines_added", "lines_removed", "status_counts"] }
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"if": { "properties": { "available": { "const": false } } },
|
|
93
|
+
"then": { "required": ["reason"] }
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"signal": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"required": ["available", "present", "evidence", "additional_evidence_count"],
|
|
101
|
+
"properties": {
|
|
102
|
+
"available": { "type": "boolean" },
|
|
103
|
+
"present": { "type": "boolean" },
|
|
104
|
+
"evidence": {
|
|
105
|
+
"type": "array",
|
|
106
|
+
"maxItems": 20,
|
|
107
|
+
"items": { "type": "string", "minLength": 1 }
|
|
108
|
+
},
|
|
109
|
+
"additional_evidence_count": { "type": "integer", "minimum": 0 },
|
|
110
|
+
"reason": { "type": "string" }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"quality_delta": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"additionalProperties": false,
|
|
116
|
+
"required": ["available"],
|
|
117
|
+
"properties": {
|
|
118
|
+
"available": { "type": "boolean" },
|
|
119
|
+
"reason": { "type": "string" },
|
|
120
|
+
"introduced": { "type": "integer", "minimum": 0 },
|
|
121
|
+
"existing": { "type": "integer", "minimum": 0 },
|
|
122
|
+
"resolved": { "type": "integer", "minimum": 0 },
|
|
123
|
+
"changed": { "type": "integer", "minimum": 0 },
|
|
124
|
+
"moved": { "type": "integer", "minimum": 0 },
|
|
125
|
+
"waived": { "type": "integer", "minimum": 0 },
|
|
126
|
+
"orphaned_waivers": { "type": "integer", "minimum": 0 }
|
|
127
|
+
},
|
|
128
|
+
"allOf": [
|
|
129
|
+
{
|
|
130
|
+
"if": { "properties": { "available": { "const": true } } },
|
|
131
|
+
"then": { "required": ["introduced", "existing", "resolved", "changed", "moved", "waived", "orphaned_waivers"] }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"if": { "properties": { "available": { "const": false } } },
|
|
135
|
+
"then": { "required": ["reason"] }
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
"analyzer_evidence": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"additionalProperties": false,
|
|
142
|
+
"required": ["analyzer", "execution_status", "evidence_status"],
|
|
143
|
+
"properties": {
|
|
144
|
+
"analyzer": { "type": "string", "minLength": 1 },
|
|
145
|
+
"tool_version": { "type": "string", "minLength": 1 },
|
|
146
|
+
"execution_status": { "type": "string", "minLength": 1 },
|
|
147
|
+
"evidence_status": { "enum": ["complete", "incomplete"] }
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"gate_result": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"additionalProperties": false,
|
|
153
|
+
"required": [
|
|
154
|
+
"schema_version",
|
|
155
|
+
"completion_status",
|
|
156
|
+
"gate",
|
|
157
|
+
"policy_status",
|
|
158
|
+
"findings",
|
|
159
|
+
"operational_failures",
|
|
160
|
+
"decision_reasons"
|
|
161
|
+
],
|
|
162
|
+
"properties": {
|
|
163
|
+
"schema_version": { "const": "1.0" },
|
|
164
|
+
"completion_status": { "enum": ["complete", "incomplete", "interrupted"] },
|
|
165
|
+
"gate": { "enum": ["PASS", "WARN", "FAIL", "INCOMPLETE"] },
|
|
166
|
+
"policy_status": { "enum": ["pass", "warn", "fail", "not_evaluated"] },
|
|
167
|
+
"findings": { "type": "array", "items": { "$ref": "#/$defs/finding_summary" } },
|
|
168
|
+
"operational_failures": { "type": "array", "items": { "$ref": "#/$defs/operational_failure" } },
|
|
169
|
+
"decision_reasons": { "type": "array", "items": { "$ref": "#/$defs/decision_reason" } }
|
|
170
|
+
},
|
|
171
|
+
"allOf": [
|
|
172
|
+
{
|
|
173
|
+
"if": { "properties": { "completion_status": { "const": "complete" } } },
|
|
174
|
+
"then": {
|
|
175
|
+
"properties": {
|
|
176
|
+
"gate": { "enum": ["PASS", "WARN", "FAIL"] },
|
|
177
|
+
"policy_status": { "enum": ["pass", "warn", "fail"] }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"if": { "properties": { "completion_status": { "enum": ["incomplete", "interrupted"] } } },
|
|
183
|
+
"then": {
|
|
184
|
+
"properties": {
|
|
185
|
+
"gate": { "const": "INCOMPLETE" },
|
|
186
|
+
"policy_status": { "const": "not_evaluated" },
|
|
187
|
+
"operational_failures": { "minItems": 1 }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
"finding_summary": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"additionalProperties": false,
|
|
196
|
+
"required": ["id", "fingerprint", "severity", "state", "blocking"],
|
|
197
|
+
"properties": {
|
|
198
|
+
"id": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
199
|
+
"fingerprint": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
|
|
200
|
+
"severity": { "enum": ["info", "low", "medium", "high", "critical"] },
|
|
201
|
+
"state": { "enum": ["observed", "introduced", "existing", "resolved", "changed", "moved", "suppressed", "waived"] },
|
|
202
|
+
"blocking": { "type": "boolean" }
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
"operational_failure": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"additionalProperties": false,
|
|
208
|
+
"required": ["code", "message"],
|
|
209
|
+
"properties": {
|
|
210
|
+
"code": { "enum": ["unavailable", "unsupported", "timed_out", "signaled", "failed", "parse_failed", "truncated", "malformed", "configuration", "interrupted", "incomplete_evidence"] },
|
|
211
|
+
"analyzer": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
212
|
+
"message": { "type": "string", "minLength": 1, "maxLength": 4096 }
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"decision_reason": {
|
|
216
|
+
"type": "object",
|
|
217
|
+
"additionalProperties": false,
|
|
218
|
+
"required": ["code", "message"],
|
|
219
|
+
"properties": {
|
|
220
|
+
"code": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
221
|
+
"message": { "type": "string", "minLength": 1, "maxLength": 4096 }
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"test_intelligence": {
|
|
225
|
+
"type": "object",
|
|
226
|
+
"additionalProperties": false,
|
|
227
|
+
"required": ["available"],
|
|
228
|
+
"properties": {
|
|
229
|
+
"available": { "type": "boolean" },
|
|
230
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
231
|
+
"analyzers": {
|
|
232
|
+
"type": "object",
|
|
233
|
+
"additionalProperties": false,
|
|
234
|
+
"properties": {
|
|
235
|
+
"minitest": { "$ref": "#/$defs/test_summary" },
|
|
236
|
+
"rspec": { "$ref": "#/$defs/test_summary" }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"allOf": [
|
|
241
|
+
{
|
|
242
|
+
"if": { "properties": { "available": { "const": true } } },
|
|
243
|
+
"then": { "required": ["analyzers"] }
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"if": { "properties": { "available": { "const": false } } },
|
|
247
|
+
"then": { "required": ["reason"] }
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
"test_summary": {
|
|
252
|
+
"type": "object",
|
|
253
|
+
"additionalProperties": false,
|
|
254
|
+
"properties": {
|
|
255
|
+
"tests_total": { "type": "integer", "minimum": 0 },
|
|
256
|
+
"assertions": { "type": "integer", "minimum": 0 },
|
|
257
|
+
"failures": { "type": "integer", "minimum": 0 },
|
|
258
|
+
"errors": { "type": "integer", "minimum": 0 },
|
|
259
|
+
"skips": { "type": "integer", "minimum": 0 },
|
|
260
|
+
"duration_seconds": { "type": "number", "minimum": 0 },
|
|
261
|
+
"seed": { "type": ["integer", "null"] },
|
|
262
|
+
"runner": { "type": "string", "minLength": 1, "maxLength": 128 }
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"coverage": {
|
|
266
|
+
"type": "object",
|
|
267
|
+
"additionalProperties": false,
|
|
268
|
+
"required": ["available"],
|
|
269
|
+
"properties": {
|
|
270
|
+
"available": { "type": "boolean" },
|
|
271
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
272
|
+
"global_percent": { "type": "number", "minimum": 0, "maximum": 100 },
|
|
273
|
+
"changed_lines_percent": { "type": "number", "minimum": 0, "maximum": 100 },
|
|
274
|
+
"changed_lines_covered": { "type": "integer", "minimum": 0 },
|
|
275
|
+
"changed_lines_executable": { "type": "integer", "minimum": 0 }
|
|
276
|
+
},
|
|
277
|
+
"allOf": [
|
|
278
|
+
{
|
|
279
|
+
"if": { "properties": { "available": { "const": false } } },
|
|
280
|
+
"then": { "required": ["reason"] }
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rail_verdict
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pedro Dalben
|
|
@@ -113,6 +113,7 @@ files:
|
|
|
113
113
|
- lib/rail_verdict/mcp/tools/verify.rb
|
|
114
114
|
- lib/rail_verdict/mcp/tools/verify_repair.rb
|
|
115
115
|
- lib/rail_verdict/mcp/validators.rb
|
|
116
|
+
- lib/rail_verdict/pr_intelligence.rb
|
|
116
117
|
- lib/rail_verdict/process_runner.rb
|
|
117
118
|
- lib/rail_verdict/rails_context.rb
|
|
118
119
|
- lib/rail_verdict/rails_context/classifier.rb
|
|
@@ -141,6 +142,7 @@ files:
|
|
|
141
142
|
- lib/rail_verdict/reporters/console.rb
|
|
142
143
|
- lib/rail_verdict/reporters/github_annotations.rb
|
|
143
144
|
- lib/rail_verdict/reporters/json_reporter.rb
|
|
145
|
+
- lib/rail_verdict/reporters/pr_intelligence.rb
|
|
144
146
|
- lib/rail_verdict/reporters/sarif.rb
|
|
145
147
|
- lib/rail_verdict/run_context.rb
|
|
146
148
|
- lib/rail_verdict/schema_validator.rb
|
|
@@ -160,6 +162,7 @@ files:
|
|
|
160
162
|
- schemas/coverage-v1.schema.json
|
|
161
163
|
- schemas/finding-v1.schema.json
|
|
162
164
|
- schemas/minitest-reporter-v1.schema.json
|
|
165
|
+
- schemas/pr-intelligence-v1.schema.json
|
|
163
166
|
- schemas/repair-packet-v1.schema.json
|
|
164
167
|
- schemas/result-v1.schema.json
|
|
165
168
|
- schemas/waiver-v1.schema.json
|