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,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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mcp/repository_root"
4
+ require_relative "mcp/validators"
5
+ require_relative "mcp/serializers"
6
+ require_relative "mcp/cache"
7
+ require_relative "mcp/server"