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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/NOTICE +14 -0
- data/README.md +192 -0
- data/exe/railverdict +6 -0
- data/exe/railverdict-minitest-reporter.rb +90 -0
- data/lib/rail_verdict/analyzers/_shared.rb +54 -0
- data/lib/rail_verdict/analyzers/bundler_audit.rb +186 -0
- data/lib/rail_verdict/analyzers/minitest.rb +312 -0
- data/lib/rail_verdict/analyzers/rspec.rb +274 -0
- data/lib/rail_verdict/analyzers/rubocop.rb +289 -0
- data/lib/rail_verdict/analyzers/simplecov.rb +178 -0
- data/lib/rail_verdict/baseline.rb +151 -0
- data/lib/rail_verdict/check.rb +415 -0
- data/lib/rail_verdict/cli.rb +471 -0
- data/lib/rail_verdict/comparison.rb +216 -0
- data/lib/rail_verdict/configuration.rb +165 -0
- data/lib/rail_verdict/contracts/analyzer_result.rb +138 -0
- data/lib/rail_verdict/contracts/finding.rb +154 -0
- data/lib/rail_verdict/contracts/gate_result.rb +171 -0
- data/lib/rail_verdict/coverage/changed_line_evaluator.rb +70 -0
- data/lib/rail_verdict/doctor.rb +132 -0
- data/lib/rail_verdict/errors.rb +17 -0
- data/lib/rail_verdict/findings_command.rb +24 -0
- data/lib/rail_verdict/fingerprint.rb +72 -0
- data/lib/rail_verdict/git/command.rb +48 -0
- data/lib/rail_verdict/git/context.rb +361 -0
- data/lib/rail_verdict/git/diff_parser.rb +200 -0
- data/lib/rail_verdict/git/errors.rb +16 -0
- data/lib/rail_verdict/init.rb +38 -0
- data/lib/rail_verdict/intelligence/ai_analysis.rb +82 -0
- data/lib/rail_verdict/intelligence/ai_failure.rb +44 -0
- data/lib/rail_verdict/intelligence/budget.rb +36 -0
- data/lib/rail_verdict/intelligence/cache.rb +74 -0
- data/lib/rail_verdict/intelligence/context_builder.rb +92 -0
- data/lib/rail_verdict/intelligence/manifest.rb +70 -0
- data/lib/rail_verdict/intelligence/orchestrator.rb +77 -0
- data/lib/rail_verdict/intelligence/prompt.rb +28 -0
- data/lib/rail_verdict/intelligence/provider.rb +14 -0
- data/lib/rail_verdict/intelligence/providers/fake_provider.rb +31 -0
- data/lib/rail_verdict/intelligence/providers/openai_compat.rb +119 -0
- data/lib/rail_verdict/intelligence/redactor.rb +51 -0
- data/lib/rail_verdict/intelligence/secret_detector.rb +45 -0
- data/lib/rail_verdict/intelligence/source_reader.rb +49 -0
- data/lib/rail_verdict/intelligence.rb +11 -0
- data/lib/rail_verdict/mcp/cache.rb +233 -0
- data/lib/rail_verdict/mcp/repository_root.rb +31 -0
- data/lib/rail_verdict/mcp/serializers.rb +98 -0
- data/lib/rail_verdict/mcp/server.rb +110 -0
- data/lib/rail_verdict/mcp/tools/build_repair_packet.rb +85 -0
- data/lib/rail_verdict/mcp/tools/explain.rb +100 -0
- data/lib/rail_verdict/mcp/tools/get_finding.rb +139 -0
- data/lib/rail_verdict/mcp/tools/investigate.rb +107 -0
- data/lib/rail_verdict/mcp/tools/list_findings.rb +119 -0
- data/lib/rail_verdict/mcp/tools/verify.rb +137 -0
- data/lib/rail_verdict/mcp/tools/verify_repair.rb +105 -0
- data/lib/rail_verdict/mcp/validators.rb +81 -0
- data/lib/rail_verdict/mcp.rb +7 -0
- data/lib/rail_verdict/process_runner.rb +292 -0
- data/lib/rail_verdict/rails_context/classifier.rb +57 -0
- data/lib/rail_verdict/rails_context/confidence.rb +13 -0
- data/lib/rail_verdict/rails_context/constant_inferencer.rb +56 -0
- data/lib/rail_verdict/rails_context/context.rb +115 -0
- data/lib/rail_verdict/rails_context/detector.rb +141 -0
- data/lib/rail_verdict/rails_context/limits.rb +17 -0
- data/lib/rail_verdict/rails_context/resolvers/association_extractor.rb +48 -0
- data/lib/rail_verdict/rails_context/resolvers/policy_resolver.rb +45 -0
- data/lib/rail_verdict/rails_context/resolvers/route_resolver.rb +40 -0
- data/lib/rail_verdict/rails_context/resolvers/route_scanner.rb +55 -0
- data/lib/rail_verdict/rails_context/resolvers/schema_resolver.rb +90 -0
- data/lib/rail_verdict/rails_context/resolvers/test_candidates.rb +75 -0
- data/lib/rail_verdict/rails_context/resolvers/view_resolver.rb +45 -0
- data/lib/rail_verdict/rails_context/schema_parser.rb +50 -0
- data/lib/rail_verdict/rails_context.rb +21 -0
- data/lib/rail_verdict/repair/boundary.rb +36 -0
- data/lib/rail_verdict/repair/command.rb +89 -0
- data/lib/rail_verdict/repair/constraints.rb +31 -0
- data/lib/rail_verdict/repair/context_assembler.rb +301 -0
- data/lib/rail_verdict/repair/diff_context.rb +51 -0
- data/lib/rail_verdict/repair/packet.rb +158 -0
- data/lib/rail_verdict/repair/prompt_renderer.rb +27 -0
- data/lib/rail_verdict/repair/verification_plan.rb +45 -0
- data/lib/rail_verdict/repair/verifier.rb +84 -0
- data/lib/rail_verdict/reporters/console.rb +107 -0
- data/lib/rail_verdict/reporters/github_annotations.rb +104 -0
- data/lib/rail_verdict/reporters/json_reporter.rb +19 -0
- data/lib/rail_verdict/reporters/sarif.rb +138 -0
- data/lib/rail_verdict/run_context.rb +99 -0
- data/lib/rail_verdict/schema_validator.rb +85 -0
- data/lib/rail_verdict/strict_yaml.rb +134 -0
- data/lib/rail_verdict/verification/policy.rb +359 -0
- data/lib/rail_verdict/version.rb +5 -0
- data/lib/rail_verdict/waiver.rb +67 -0
- data/lib/rail_verdict/waiver_store.rb +59 -0
- data/lib/rail_verdict.rb +64 -0
- data/schemas/ai-analysis-v1.schema.json +54 -0
- data/schemas/baseline-v1.schema.json +108 -0
- data/schemas/configuration-v1.1.schema.json +150 -0
- data/schemas/configuration-v1.2.schema.json +182 -0
- data/schemas/configuration-v1.3.schema.json +198 -0
- data/schemas/configuration-v1.4.schema.json +146 -0
- data/schemas/configuration-v1.schema.json +72 -0
- data/schemas/coverage-v1.schema.json +51 -0
- data/schemas/finding-v1.schema.json +131 -0
- data/schemas/minitest-reporter-v1.schema.json +113 -0
- data/schemas/repair-packet-v1.schema.json +364 -0
- data/schemas/result-v1.schema.json +378 -0
- data/schemas/waiver-v1.schema.json +45 -0
- data/schemas/waivers-v1.schema.json +67 -0
- metadata +192 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailVerdict
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ListFindings
|
|
7
|
+
def initialize(server:)
|
|
8
|
+
@server = server
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def tool_name
|
|
12
|
+
"list_findings"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tool_title
|
|
16
|
+
"List findings"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tool_description
|
|
20
|
+
"List deterministic findings from the last verification. Supports filtering by severity/state/blocking and pagination via limit/offset. Stable ordering by path/start_line/analyzer/rule/fingerprint/id. Truncated flag is explicit."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tool_input_schema
|
|
24
|
+
{
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
limit: { type: "integer", description: "Max findings to return (1..100, default 50)" },
|
|
28
|
+
offset: { type: "integer", description: "Offset (default 0)" },
|
|
29
|
+
severity: { type: "string", enum: %w[info low medium high critical], description: "Filter by severity" },
|
|
30
|
+
state: { type: "string", enum: %w[observed introduced existing resolved changed moved suppressed waived], description: "Filter by state" },
|
|
31
|
+
blocking: { type: "boolean", description: "Filter by blocking" }
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: false
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tool_output_schema
|
|
38
|
+
{
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
findings: { type: "array" },
|
|
42
|
+
total: { type: "integer" },
|
|
43
|
+
limit: { type: "integer" },
|
|
44
|
+
offset: { type: "integer" },
|
|
45
|
+
truncated: { type: "boolean" },
|
|
46
|
+
gate: { type: "string" },
|
|
47
|
+
completion_status: { type: "string" }
|
|
48
|
+
},
|
|
49
|
+
required: %w[findings total limit offset truncated]
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def tool_annotations
|
|
54
|
+
{ read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false, title: tool_title }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call(limit: nil, offset: nil, severity: nil, state: nil, blocking: nil, **_rest)
|
|
58
|
+
begin
|
|
59
|
+
limit_v = Validators.validate_limit(limit, default: 50, max: 100)
|
|
60
|
+
offset_v = Validators.validate_offset(offset)
|
|
61
|
+
severity_v = Validators.validate_severity(severity)
|
|
62
|
+
state_v = Validators.validate_state(state)
|
|
63
|
+
|
|
64
|
+
outcome = @server.cache.fetch_outcome
|
|
65
|
+
unless outcome
|
|
66
|
+
outcome = run_verify
|
|
67
|
+
@server.cache.store_outcome(outcome)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
findings = outcome.findings || []
|
|
71
|
+
if severity_v
|
|
72
|
+
findings = findings.select { |f| f.severity == severity_v }
|
|
73
|
+
end
|
|
74
|
+
if state_v
|
|
75
|
+
gate_findings = outcome.result.findings
|
|
76
|
+
allowed_fps = gate_findings.select { |h| h["state"] == state_v }.map { |h| h["fingerprint"] }.to_set
|
|
77
|
+
findings = findings.select { |f| allowed_fps.include?(f.fingerprint) }
|
|
78
|
+
end
|
|
79
|
+
unless blocking.nil?
|
|
80
|
+
gate_findings = outcome.result.findings
|
|
81
|
+
allowed_fps = gate_findings.select { |h| h["blocking"] == blocking }.map { |h| h["fingerprint"] }.to_set
|
|
82
|
+
findings = findings.select { |f| allowed_fps.include?(f.fingerprint) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
findings = findings.sort_by(&:sort_key)
|
|
86
|
+
total = findings.length
|
|
87
|
+
if offset_v > total
|
|
88
|
+
return Serializers.error_response("offset exceeds total (#{total})", code: "invalid_arguments")
|
|
89
|
+
end
|
|
90
|
+
slice = findings.slice(offset_v, limit_v) || []
|
|
91
|
+
truncated = (offset_v + limit_v) < total
|
|
92
|
+
|
|
93
|
+
structured = {
|
|
94
|
+
"findings" => slice.map(&:to_schema_h),
|
|
95
|
+
"total" => total,
|
|
96
|
+
"limit" => limit_v,
|
|
97
|
+
"offset" => offset_v,
|
|
98
|
+
"truncated" => truncated,
|
|
99
|
+
"gate" => outcome.result.gate,
|
|
100
|
+
"completion_status" => outcome.result.completion_status
|
|
101
|
+
}
|
|
102
|
+
structured = Serializers.scrub(structured)
|
|
103
|
+
Serializers.tool_response(structured, error: false)
|
|
104
|
+
rescue ArgumentError => e
|
|
105
|
+
Serializers.error_response(e.message, code: "invalid_arguments")
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
Serializers.error_response("list_findings failed: #{e.message}", code: "internal_error")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def run_verify
|
|
114
|
+
Check.execute(repository_root: @server.repository_root, config_path: File.join(@server.repository_root, ".railverdict.yml"))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailVerdict
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class Verify
|
|
7
|
+
def initialize(server:)
|
|
8
|
+
@server = server
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def tool_name
|
|
12
|
+
"verify"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tool_title
|
|
16
|
+
"Verify repository"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tool_description
|
|
20
|
+
"Deterministic read-only verification. Runs RailVerdict Check and returns GateResult (PASS/WARN/FAIL/INCOMPLETE). FAIL is a successful result (isError false) — not a protocol error. Works offline with no network. Requires no AI."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tool_input_schema
|
|
24
|
+
{
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
changed: { type: "boolean", description: "Use Git changed scope (--changed)" },
|
|
28
|
+
base: { type: "string", description: "Base revision for --changed (hex SHA 7..64)" },
|
|
29
|
+
config_path: { type: "string", description: "Path to .railverdict.yml relative to repository root" },
|
|
30
|
+
baseline_path: { type: "string", description: "Override baseline path" },
|
|
31
|
+
waiver_path: { type: "string", description: "Override waivers path" }
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: false
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tool_output_schema
|
|
38
|
+
{
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
schema_version: { type: "string" },
|
|
42
|
+
completion_status: { type: "string" },
|
|
43
|
+
gate: { type: "string" },
|
|
44
|
+
policy_status: { type: "string" },
|
|
45
|
+
findings: { type: "array" },
|
|
46
|
+
analyzer_results: { type: "array" },
|
|
47
|
+
operational_failures: { type: "array" },
|
|
48
|
+
decision_reasons: { type: "array" }
|
|
49
|
+
},
|
|
50
|
+
required: %w[schema_version completion_status gate policy_status findings]
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tool_annotations
|
|
55
|
+
{ read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false, title: tool_title }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def call(changed: nil, base: nil, config_path: nil, baseline_path: nil, waiver_path: nil, **_rest)
|
|
59
|
+
begin
|
|
60
|
+
changed = changed == true
|
|
61
|
+
base_validated = Validators.validate_base_revision(base)
|
|
62
|
+
if base_validated && !changed
|
|
63
|
+
return Serializers.error_response("--base requires --changed", code: "invalid_arguments")
|
|
64
|
+
end
|
|
65
|
+
config_file = resolve_config_path(config_path)
|
|
66
|
+
if changed
|
|
67
|
+
base_for_check = base_validated || configuration_git_base(config_file)
|
|
68
|
+
if base_for_check.nil? || base_for_check.strip.empty?
|
|
69
|
+
# Let Check handle git_scope_failed as INCOMPLETE — still need a base, but we try check anyway
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
outcome = @server.synchronized_verification { run_check(changed: changed, base: base_validated, config_path: config_file, baseline_path: baseline_path, waiver_path: waiver_path) }
|
|
74
|
+
@server.cache.store_outcome(outcome)
|
|
75
|
+
structured = Serializers.gate_result_to_structured(outcome)
|
|
76
|
+
structured = Validators.scrub_text(structured) if structured.is_a?(String)
|
|
77
|
+
structured = Serializers.scrub(structured)
|
|
78
|
+
Serializers.tool_response(structured, error: false)
|
|
79
|
+
rescue ArgumentError => e
|
|
80
|
+
Serializers.error_response(e.message, code: "invalid_arguments")
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
Serializers.error_response("verify failed: #{e.message}", code: "internal_error")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def run_check(changed:, base:, config_path:, baseline_path:, waiver_path:)
|
|
89
|
+
root = @server.repository_root
|
|
90
|
+
opts = { repository_root: root, config_path: config_path }
|
|
91
|
+
opts[:changed] = true if changed
|
|
92
|
+
opts[:base] = base if base
|
|
93
|
+
baseline_resolved = resolve_contained_path(baseline_path, "baseline_path") if baseline_path && !baseline_path.to_s.strip.empty?
|
|
94
|
+
waiver_resolved = resolve_contained_path(waiver_path, "waiver_path") if waiver_path && !waiver_path.to_s.strip.empty?
|
|
95
|
+
opts[:baseline_path_override] = baseline_resolved if baseline_resolved
|
|
96
|
+
opts[:waiver_path_override] = waiver_resolved if waiver_resolved
|
|
97
|
+
Check.execute(**opts)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def resolve_contained_path(raw_path, field_name)
|
|
101
|
+
raw = raw_path.to_s.strip
|
|
102
|
+
raise ArgumentError, "#{field_name} contains NUL byte" if raw.include?("\u0000")
|
|
103
|
+
|
|
104
|
+
expanded = File.expand_path(raw, @server.repository_root)
|
|
105
|
+
unless RepositoryRoot.contained?(@server.repository_root, expanded)
|
|
106
|
+
raise ArgumentError, "#{field_name} escapes repository root"
|
|
107
|
+
end
|
|
108
|
+
expanded
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def resolve_config_path(config_path)
|
|
112
|
+
return File.join(@server.repository_root, ".railverdict.yml") if config_path.nil? || config_path.to_s.strip.empty?
|
|
113
|
+
|
|
114
|
+
raw = config_path.to_s.strip
|
|
115
|
+
raise ArgumentError, "config_path contains NUL byte" if raw.include?("\u0000")
|
|
116
|
+
|
|
117
|
+
expanded = File.expand_path(raw, @server.repository_root)
|
|
118
|
+
unless RepositoryRoot.contained?(@server.repository_root, expanded)
|
|
119
|
+
raise ArgumentError, "config_path escapes repository root"
|
|
120
|
+
end
|
|
121
|
+
expanded
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def configuration_git_base(config_file)
|
|
125
|
+
return nil unless File.file?(config_file)
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
cfg = Configuration.load(config_file)
|
|
129
|
+
cfg.git_base
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailVerdict
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class VerifyRepair
|
|
7
|
+
def initialize(server:)
|
|
8
|
+
@server = server
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def tool_name
|
|
12
|
+
"verify_repair"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tool_title
|
|
16
|
+
"Verify repair"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tool_description
|
|
20
|
+
"Verify whether a prior RepairPacket target is fixed. Re-runs verification and classifies target_status (fixed/still_present/changed/moved/regressed/incomplete) plus verification_boundary_changed and gate. Never trusts agent-provided fixed claim."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def tool_input_schema
|
|
24
|
+
{
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
packet_id: { type: "string", description: "Packet id (sha256:...); must match a prior build_repair_packet packet_id in this server session" },
|
|
28
|
+
changed: { type: "boolean", description: "Re-verify with Git changed scope" },
|
|
29
|
+
base: { type: "string", description: "Base revision for re-verify (hex SHA) when changed is true" }
|
|
30
|
+
},
|
|
31
|
+
required: ["packet_id"],
|
|
32
|
+
additionalProperties: false
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def tool_output_schema
|
|
37
|
+
{
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
target_status: { type: "string" },
|
|
41
|
+
gate: { type: "string" },
|
|
42
|
+
completion_status: { type: "string" },
|
|
43
|
+
overall_status: { type: "string" },
|
|
44
|
+
new_blocking_findings: { type: "integer" },
|
|
45
|
+
verification_boundary_changed: { type: ["object", "boolean"] },
|
|
46
|
+
regressed: { type: "boolean" },
|
|
47
|
+
gate_result: { type: "object" }
|
|
48
|
+
},
|
|
49
|
+
required: %w[target_status gate completion_status]
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def tool_annotations
|
|
54
|
+
{ read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false, title: tool_title }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call(packet_id: nil, changed: nil, base: nil, **_rest)
|
|
58
|
+
begin
|
|
59
|
+
pid = Validators.validate_packet_id(packet_id)
|
|
60
|
+
changed_v = changed == true
|
|
61
|
+
base_v = Validators.validate_base_revision(base)
|
|
62
|
+
if base_v && !changed_v
|
|
63
|
+
return Serializers.error_response("--base requires --changed", code: "invalid_arguments")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
packet_h = @server.cache.fetch_packet(pid)
|
|
67
|
+
unless packet_h
|
|
68
|
+
return Serializers.error_response("packet not found for packet_id: #{pid}; call build_repair_packet first", code: "stale_target")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
fresh_outcome = @server.synchronized_verification { fresh_check(changed: changed_v, base: base_v) }
|
|
72
|
+
result = Repair::Verifier.verify(packet: packet_h, new_outcome: fresh_outcome)
|
|
73
|
+
|
|
74
|
+
structured = {
|
|
75
|
+
"target_status" => result.target_status,
|
|
76
|
+
"gate" => result.gate,
|
|
77
|
+
"completion_status" => result.completion_status,
|
|
78
|
+
"overall_status" => result.overall_status,
|
|
79
|
+
"new_blocking_findings" => result.new_blocking_findings,
|
|
80
|
+
"verification_boundary_changed" => result.verification_boundary_changed,
|
|
81
|
+
"regressed" => result.regressed,
|
|
82
|
+
"gate_result" => fresh_outcome.result.to_schema_h
|
|
83
|
+
}
|
|
84
|
+
structured = Serializers.scrub(structured)
|
|
85
|
+
Serializers.tool_response(structured, error: false)
|
|
86
|
+
rescue ArgumentError => e
|
|
87
|
+
Serializers.error_response(e.message, code: "invalid_arguments")
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
Serializers.error_response("verify_repair failed: #{e.message}", code: "internal_error")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def fresh_check(changed:, base:)
|
|
96
|
+
root = @server.repository_root
|
|
97
|
+
opts = { repository_root: root, config_path: File.join(root, ".railverdict.yml") }
|
|
98
|
+
opts[:changed] = true if changed
|
|
99
|
+
opts[:base] = base if base
|
|
100
|
+
Check.execute(**opts)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailVerdict
|
|
4
|
+
module MCP
|
|
5
|
+
module Validators
|
|
6
|
+
FINDING_REF_PATTERN = /\A(?:rv:[0-9a-f]{20}|sha256:[0-9a-f]{64})\z/
|
|
7
|
+
BASE_REVISION_PATTERN = /\A[0-9a-f]{7,64}\z/
|
|
8
|
+
MAX_FINDING_REF_BYTES = 512
|
|
9
|
+
|
|
10
|
+
def self.validate_finding_ref(value)
|
|
11
|
+
raise ArgumentError, "finding_ref must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
|
|
12
|
+
raise ArgumentError, "finding_ref exceeds #{MAX_FINDING_REF_BYTES} bytes" if value.bytesize > MAX_FINDING_REF_BYTES
|
|
13
|
+
raise ArgumentError, "finding_ref contains NUL byte" if value.include?("\u0000")
|
|
14
|
+
raise ArgumentError, "finding_ref has invalid format" unless value.strip.match?(FINDING_REF_PATTERN)
|
|
15
|
+
|
|
16
|
+
value.strip
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.validate_limit(value, default: 50, max: 100)
|
|
20
|
+
return default if value.nil?
|
|
21
|
+
|
|
22
|
+
raise ArgumentError, "limit must be an integer" unless value.is_a?(Integer)
|
|
23
|
+
raise ArgumentError, "limit must be between 1 and #{max}" unless value.between?(1, max)
|
|
24
|
+
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.validate_offset(value)
|
|
29
|
+
return 0 if value.nil?
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, "offset must be an integer >= 0" unless value.is_a?(Integer) && value >= 0
|
|
32
|
+
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.validate_base_revision(value)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
str = value.to_s.strip
|
|
39
|
+
return nil if str.empty?
|
|
40
|
+
raise ArgumentError, "base revision contains NUL byte" if str.include?("\u0000")
|
|
41
|
+
raise ArgumentError, "base revision has invalid format" unless str.match?(BASE_REVISION_PATTERN)
|
|
42
|
+
|
|
43
|
+
str
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.validate_boolean(value, name)
|
|
47
|
+
return false if value.nil?
|
|
48
|
+
return value if value == true || value == false
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "#{name} must be a boolean"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.validate_severity(value)
|
|
54
|
+
return nil if value.nil?
|
|
55
|
+
allowed = %w[info low medium high critical]
|
|
56
|
+
raise ArgumentError, "severity must be one of #{allowed.join(', ')}" unless allowed.include?(value)
|
|
57
|
+
|
|
58
|
+
value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.validate_state(value)
|
|
62
|
+
return nil if value.nil?
|
|
63
|
+
allowed = %w[observed introduced existing resolved changed moved suppressed waived]
|
|
64
|
+
raise ArgumentError, "state must be one of #{allowed.join(', ')}" unless allowed.include?(value)
|
|
65
|
+
|
|
66
|
+
value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.validate_packet_id(value)
|
|
70
|
+
raise ArgumentError, "packet_id must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
|
|
71
|
+
raise ArgumentError, "packet_id has invalid format" unless value.strip.match?(/\Asha256:[0-9a-f]{64}\z/)
|
|
72
|
+
|
|
73
|
+
value.strip
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.scrub_text(value)
|
|
77
|
+
value.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD").gsub(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/, "?")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|