ruby_llm-contract 0.2.3 → 0.3.6

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +27 -2
  5. data/lib/ruby_llm/contract/adapters/response.rb +4 -2
  6. data/lib/ruby_llm/contract/adapters/ruby_llm.rb +3 -3
  7. data/lib/ruby_llm/contract/adapters/test.rb +3 -2
  8. data/lib/ruby_llm/contract/concerns/deep_freeze.rb +23 -0
  9. data/lib/ruby_llm/contract/concerns/eval_host.rb +11 -2
  10. data/lib/ruby_llm/contract/contract/schema_validator.rb +70 -3
  11. data/lib/ruby_llm/contract/eval/baseline_diff.rb +92 -0
  12. data/lib/ruby_llm/contract/eval/dataset.rb +11 -4
  13. data/lib/ruby_llm/contract/eval/eval_definition.rb +36 -14
  14. data/lib/ruby_llm/contract/eval/model_comparison.rb +1 -1
  15. data/lib/ruby_llm/contract/eval/report.rb +71 -2
  16. data/lib/ruby_llm/contract/eval/runner.rb +5 -3
  17. data/lib/ruby_llm/contract/eval/trait_evaluator.rb +6 -0
  18. data/lib/ruby_llm/contract/eval.rb +1 -0
  19. data/lib/ruby_llm/contract/pipeline/base.rb +1 -1
  20. data/lib/ruby_llm/contract/pipeline/result.rb +1 -1
  21. data/lib/ruby_llm/contract/pipeline/runner.rb +1 -1
  22. data/lib/ruby_llm/contract/pipeline/trace.rb +3 -2
  23. data/lib/ruby_llm/contract/prompt/builder.rb +2 -1
  24. data/lib/ruby_llm/contract/prompt/node.rb +2 -2
  25. data/lib/ruby_llm/contract/prompt/nodes/example_node.rb +2 -2
  26. data/lib/ruby_llm/contract/rake_task.rb +31 -4
  27. data/lib/ruby_llm/contract/rspec/helpers.rb +28 -8
  28. data/lib/ruby_llm/contract/rspec/pass_eval.rb +23 -2
  29. data/lib/ruby_llm/contract/step/base.rb +10 -5
  30. data/lib/ruby_llm/contract/step/dsl.rb +1 -1
  31. data/lib/ruby_llm/contract/step/limit_checker.rb +1 -1
  32. data/lib/ruby_llm/contract/step/retry_executor.rb +3 -2
  33. data/lib/ruby_llm/contract/step/retry_policy.rb +7 -1
  34. data/lib/ruby_llm/contract/step/runner.rb +10 -2
  35. data/lib/ruby_llm/contract/step/trace.rb +5 -4
  36. data/lib/ruby_llm/contract/version.rb +1 -1
  37. data/lib/ruby_llm/contract.rb +36 -17
  38. metadata +3 -1
@@ -40,25 +40,25 @@ module RubyLLM
40
40
  @eval_hosts = []
41
41
  end
42
42
 
43
- def load_evals!(dir = nil)
44
- dirs = if dir
45
- [dir]
46
- elsif defined?(::Rails)
47
- %w[app/steps/eval app/contracts/eval].filter_map do |path|
48
- full = ::Rails.root.join(path)
49
- full.to_s if full.exist?
50
- end
51
- else
52
- []
53
- end
43
+ def load_evals!(*dirs)
44
+ dirs = dirs.flatten.compact
45
+ if dirs.empty? && defined?(::Rails)
46
+ dirs = %w[app/steps/eval app/contracts/eval].filter_map do |path|
47
+ full = ::Rails.root.join(path)
48
+ full.to_s if full.exist?
49
+ end
50
+ end
54
51
 
55
52
  return if dirs.empty?
56
53
 
57
- # Clear existing eval definitions before reload to prevent stale state.
58
- # Thread-local flag suppresses the "redefining" warning during reload.
54
+ # In Rails, eager-load parent directories so contract classes
55
+ # are available when eval files reference them.
56
+ eager_load_contract_dirs! if defined?(::Rails)
57
+
58
+ # Clear file-sourced evals ONCE, then load ALL dirs.
59
59
  Thread.current[:ruby_llm_contract_reloading] = true
60
60
  eval_hosts.each do |host|
61
- host.clear_eval_definitions! if host.respond_to?(:clear_eval_definitions!)
61
+ host.clear_file_sourced_evals! if host.respond_to?(:clear_file_sourced_evals!)
62
62
  end
63
63
 
64
64
  dirs.each do |d|
@@ -70,10 +70,28 @@ module RubyLLM
70
70
 
71
71
  private
72
72
 
73
- # Filter out GC'd anonymous classes and classes that no longer have evals
73
+ # Filter stale hosts, deduplicate by name (last wins), prune registry in-place
74
74
  def live_eval_hosts
75
- eval_hosts.select do |host|
76
- host.respond_to?(:eval_defined?) && host.eval_defined?
75
+ # Remove hosts without evals
76
+ @eval_hosts&.reject! { |h| !h.respond_to?(:eval_defined?) || !h.eval_defined? }
77
+
78
+ # Deduplicate: if two classes share a name (reload), keep the latest
79
+ seen = {}
80
+ @eval_hosts&.each { |h| seen[h.name || h.object_id] = h }
81
+ @eval_hosts = seen.values
82
+
83
+ @eval_hosts || []
84
+ end
85
+
86
+ def eager_load_contract_dirs!
87
+ %w[app/contracts app/steps].each do |path|
88
+ full = ::Rails.root.join(path)
89
+ next unless full.exist?
90
+
91
+ ::Rails.autoloaders.main.eager_load_dir(full.to_s)
92
+ rescue StandardError
93
+ # Zeitwerk not available or dir not managed — skip
94
+ nil
77
95
  end
78
96
  end
79
97
 
@@ -87,6 +105,7 @@ module RubyLLM
87
105
  end
88
106
  end
89
107
 
108
+ require_relative "contract/concerns/deep_freeze"
90
109
  require_relative "contract/concerns/deep_symbolize"
91
110
  require_relative "contract/concerns/eval_host"
92
111
  require_relative "contract/concerns/trace_equality"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-contract
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justyna
@@ -82,6 +82,7 @@ files:
82
82
  - lib/ruby_llm/contract/adapters/response.rb
83
83
  - lib/ruby_llm/contract/adapters/ruby_llm.rb
84
84
  - lib/ruby_llm/contract/adapters/test.rb
85
+ - lib/ruby_llm/contract/concerns/deep_freeze.rb
85
86
  - lib/ruby_llm/contract/concerns/deep_symbolize.rb
86
87
  - lib/ruby_llm/contract/concerns/eval_host.rb
87
88
  - lib/ruby_llm/contract/concerns/trace_equality.rb
@@ -97,6 +98,7 @@ files:
97
98
  - lib/ruby_llm/contract/dsl.rb
98
99
  - lib/ruby_llm/contract/errors.rb
99
100
  - lib/ruby_llm/contract/eval.rb
101
+ - lib/ruby_llm/contract/eval/baseline_diff.rb
100
102
  - lib/ruby_llm/contract/eval/case_result.rb
101
103
  - lib/ruby_llm/contract/eval/contract_detail_builder.rb
102
104
  - lib/ruby_llm/contract/eval/dataset.rb