jev-feels 1.1.1 → 1.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23595f1ad313a8a80834c59a33c8c4e5216e25a52319ec98bad95d1d56980702
4
- data.tar.gz: aa494f3e27c534918c7c8ece6b2be0eec941ae85501e016476795a761a7bf9c3
3
+ metadata.gz: a3eece3f0263fb96e25774fc923368f688a0d9f9fa20d753b2def6fea8bd218c
4
+ data.tar.gz: 3c2c4aba985b8110c1911d7e3de1a75df6c7556a40925739c7b79edc576cc1e7
5
5
  SHA512:
6
- metadata.gz: 15b77459470ea59576b87b94f66b1e3ff2ebd22944b81f15ab927a98f359714b6a9e7c717c7de01dea71d0b892c0ac68cf6655aab03c3ca80f6344cfb66cd404
7
- data.tar.gz: 19f2f042a791762bcc11341a82f87a8e08b9af0b8fb104f9bf432d45c7361b991b1bc922e57f0ad081864afff9079c0747c9729e7e48877797b8754ac9b6a87f
6
+ metadata.gz: 01c48dd2cf71d607b8fb0b6fd26c07ef29db4730c3875c21f8a41c75a6522ab32fcaedf12f1c2c270267f12b7b05b414112abc7df1a6d99e1b2792a59e7746cc
7
+ data.tar.gz: 5b2125b7d7055fed2b99dc65694043f42af4cc8071c4e516803e960304172ccf6a632a69f11f82ac52ca679258b8c003784a2c1be3e83791f0b2cfa3fd0c0043
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.2
4
+
5
+ ### Fixed
6
+
7
+ - Enforce a maximum of 255 Choice options and 10 Score levels for named and ad-hoc questions.
8
+ - Generate Score stub probabilities between adjacent levels so their weighted mean matches the supplied score; reject out-of-range scores when generating probabilities.
9
+ - Preserve explicitly supplied stub probabilities and confidence.
10
+
11
+ ### Changed
12
+
13
+ - Simplify response parsing to JSON number and string key types, and require a definition when parsing Choice answers.
14
+ - Read HTTP error details from `detail.message`; remove error-message redaction and truncation.
15
+
3
16
  ## 1.1.1
4
17
 
5
18
  ### Fixed
data/README.md CHANGED
@@ -85,7 +85,7 @@ Jev.define :urgent, "Requires immediate attention or action"
85
85
  Jev.define :spam, "Unsolicited or unwanted promotional content"
86
86
  ```
87
87
 
88
- A decision (`decide`):
88
+ A decision (`decide`), with up to 255 choices:
89
89
 
90
90
  ```ruby
91
91
  Jev.define :support_team,
@@ -98,7 +98,7 @@ Jev.define :support_team,
98
98
  }
99
99
  ```
100
100
 
101
- A scale (`score`). Hash order is low to high:
101
+ A scale (`score`), with 2 to 10 levels. Hash order is low to high:
102
102
 
103
103
  ```ruby
104
104
  Jev.define :severity,
@@ -297,6 +297,8 @@ end
297
297
 
298
298
  A full `decide` / `score` stub is a hash with `choice` or `score`, plus `confidence` and `probabilities`. To own the HTTP shape, set `config.transport`.
299
299
 
300
+ When Score probabilities are omitted, the stub distributes probability between adjacent levels so their weighted mean equals the score. The score must be within the declared scale (`0..levels.size - 1`); otherwise it raises `ArgumentError`. Explicit probabilities and confidence are preserved; confidence defaults to `1.0`.
301
+
300
302
  Record real answers and replay them later. Replay never uses the network. The tape matches the request (state and questions), not a queue index. API keys and Authorization headers are not stored.
301
303
 
302
304
  ```ruby
@@ -330,7 +332,7 @@ end
330
332
 
331
333
  ## Errors
332
334
 
333
- All errors inherit from `Jev::Error`. HTTP failures are wrapped; the original exception is available as `cause`. API keys are redacted from messages.
335
+ All errors inherit from `Jev::Error`. HTTP failures are wrapped; the original exception is available as `cause`.
334
336
 
335
337
  | Error | When |
336
338
  | --------------------------- | --------------------------------------------- |
@@ -346,4 +348,4 @@ Malformed definitions (`choices:` and `levels:` together, empty Choice, too few
346
348
 
347
349
  ## What this talks to
348
350
 
349
- [Jev](https://docs.typesafe.ai/introduction.md) / TypeSafe System One. `POST https://api.typesafe.ai/v1/systemone`. That is an implementation detail. The public API is `Jev.feels` / `Jev.feels?` / `Jev.decide` / `Jev.score` / `Jev.measure` / `Jev.match`.
351
+ [Jev](https://docs.typesafe.ai/introduction.md) / TypeSafe System One. `POST https://api.typesafe.ai/v1/systemone`. That is an implementation detail. The public API is `Jev.feels` / `Jev.feels?` / `Jev.decide` / `Jev.score` / `Jev.measure` / `Jev.match`.
@@ -48,6 +48,7 @@ module Jev
48
48
  def self.normalize_choices(choices)
49
49
  raise ArgumentError, "choices must be a Hash" unless choices.is_a?(Hash)
50
50
  raise ArgumentError, "choices cannot be empty" if choices.empty?
51
+ raise ArgumentError, "choices must have at most 255 entries" if choices.size > 255
51
52
 
52
53
  unique_keys(choices, "choice").freeze
53
54
  end
@@ -56,6 +57,7 @@ module Jev
56
57
  def self.normalize_levels(levels)
57
58
  raise ArgumentError, "levels must be a Hash" unless levels.is_a?(Hash)
58
59
  raise ArgumentError, "levels must have at least 2 entries" if levels.size < 2
60
+ raise ArgumentError, "levels must have at most 10 entries" if levels.size > 10
59
61
 
60
62
  unique_keys(levels, "level").freeze
61
63
  end
data/lib/jev/harness.rb CHANGED
@@ -233,9 +233,13 @@ module Jev
233
233
  end
234
234
 
235
235
  def default_score_probabilities(criteria, score)
236
- size = [criteria.size, 1].max
237
- nearest = Float(score).round.clamp(0, size - 1)
238
- size.times.to_h { |index| [index.to_s, index == nearest ? 1.0 : 0.0] }
236
+ score = Float(score)
237
+ maximum = criteria.size - 1
238
+ unless score.finite? && score.between?(0, maximum)
239
+ raise ArgumentError, "stub score must be between 0 and #{maximum}"
240
+ end
241
+
242
+ criteria.size.times.to_h { |index| [index.to_s, [1.0 - (index - score).abs, 0.0].max] }
239
243
  end
240
244
 
241
245
  def score_probability_hash(probabilities, definition)
data/lib/jev/result.rb CHANGED
@@ -15,14 +15,9 @@ module Jev
15
15
  end
16
16
 
17
17
  def self.finite_unit(value, label)
18
- raise InvalidResponseError, "Jev response is missing #{label}" unless value.is_a?(Numeric)
18
+ raise InvalidResponseError, "Jev response is missing #{label}" unless value.is_a?(Integer) || value.is_a?(Float)
19
19
 
20
- value = Float(value)
21
- raise InvalidResponseError, "Jev #{label} is not finite" unless value.finite?
22
-
23
- value
24
- rescue ArgumentError, TypeError, RangeError
25
- raise InvalidResponseError, "Jev #{label} is not a real number"
20
+ Float(value)
26
21
  end
27
22
 
28
23
  class Noul
@@ -35,13 +30,12 @@ module Jev
35
30
 
36
31
  def self.parse(answer)
37
32
  noul = answer["noul"]
38
- raise InvalidResponseError, "Jev response is missing a noul probability" unless noul.is_a?(Numeric)
39
-
40
- noul = Float(noul)
41
- raise InvalidResponseError, "Jev noul probability is not finite" unless noul.finite?
33
+ unless noul.is_a?(Integer) || noul.is_a?(Float)
34
+ raise InvalidResponseError, "Jev response is missing a noul probability"
35
+ end
42
36
 
43
37
  # ponytail: clamp out-of-range noul; raise if Jev starts returning uncalibrated values
44
- new(probability: noul.clamp(0.0, 1.0))
38
+ new(probability: Float(noul).clamp(0.0, 1.0))
45
39
  end
46
40
 
47
41
  def type
@@ -63,7 +57,7 @@ module Jev
63
57
  freeze
64
58
  end
65
59
 
66
- def self.parse(answer, definition = nil)
60
+ def self.parse(answer, definition)
67
61
  winner = choice_key(answer["choice"], definition)
68
62
  new(
69
63
  choice: winner,
@@ -73,12 +67,8 @@ module Jev
73
67
  end
74
68
 
75
69
  def self.choice_key(key, definition)
76
- unless key.is_a?(String) || key.is_a?(Symbol)
77
- raise InvalidResponseError, "Jev response is missing a valid choice"
78
- end
79
- if definition && !definition.choices.key?(key.to_sym)
80
- raise InvalidResponseError, "Jev response has an unknown choice"
81
- end
70
+ raise InvalidResponseError, "Jev response is missing a valid choice" unless key.is_a?(String)
71
+ raise InvalidResponseError, "Jev response has an unknown choice" unless definition.choices.key?(key.to_sym)
82
72
 
83
73
  key.to_sym
84
74
  end
@@ -132,29 +122,21 @@ module Jev
132
122
 
133
123
  raw.to_h do |key, value|
134
124
  index = level_index(key, names)
135
- [names ? names.fetch(index) : index, Result.finite_unit(value, "score probability")]
125
+ [names.fetch(index), Result.finite_unit(value, "score probability")]
136
126
  end.freeze
137
127
  end
138
128
 
139
129
  def self.level_index(key, names)
140
- index = parse_index(key)
141
- if index.negative? || (names && index >= names.size)
142
- raise InvalidResponseError, "Jev score probability has an unknown level"
143
- end
144
-
145
- index
146
- end
147
- private_class_method :level_index
148
-
149
- def self.parse_index(key)
150
- return key if key.is_a?(Integer)
151
130
  unless key.is_a?(String) && key.match?(/\A\d+\z/)
152
131
  raise InvalidResponseError, "Jev score probabilities are not keyed by level number"
153
132
  end
154
133
 
155
- Integer(key, 10)
134
+ index = Integer(key, 10)
135
+ raise InvalidResponseError, "Jev score probability has an unknown level" if index >= names.size
136
+
137
+ index
156
138
  end
157
- private_class_method :parse_index
139
+ private_class_method :level_index
158
140
 
159
141
  def self.named_level(probabilities, names)
160
142
  return unless names
data/lib/jev/transport.rb CHANGED
@@ -20,7 +20,7 @@ module Jev
20
20
  rescue Timeout::Error
21
21
  raise RequestError, "Jev request timed out"
22
22
  rescue SystemCallError, SocketError, IOError, OpenSSL::SSL::SSLError => e
23
- raise RequestError, redact("Jev request failed: #{e.message}")
23
+ raise RequestError, "Jev request failed: #{e.message}"
24
24
  end
25
25
 
26
26
  private
@@ -104,32 +104,20 @@ module Jev
104
104
  end
105
105
 
106
106
  def error_detail(raw)
107
- text = extract_error_message(raw)
108
- return if text.nil? || text.empty?
109
-
110
- redact(text)[0, 200]
111
- end
112
-
113
- def extract_error_message(raw)
114
107
  parsed = JSON.parse(raw)
115
- return raw.strip unless parsed.is_a?(Hash)
108
+ return unless parsed.is_a?(Hash)
109
+
110
+ detail = parsed["detail"]
111
+ return unless detail.is_a?(Hash)
116
112
 
117
- message = parsed.values_at("error", "message", "detail").compact.first
118
- message = message["message"] if message.is_a?(Hash)
119
- message.to_s
113
+ message = detail["message"]
114
+ message if message.is_a?(String) && !message.empty?
120
115
  rescue JSON::ParserError
121
- raw.strip
116
+ nil
122
117
  end
123
118
 
124
119
  def join_detail(prefix, detail)
125
120
  detail ? "#{prefix}: #{detail}" : prefix
126
121
  end
127
-
128
- def redact(text)
129
- key = @configuration.api_key
130
- return text.to_s if key.nil? || key.empty?
131
-
132
- text.to_s.gsub(key, "[FILTERED]")
133
- end
134
122
  end
135
123
  end
data/lib/jev/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jev
4
- VERSION = "1.1.1"
4
+ VERSION = "1.1.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jev-feels
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxim Veysgeym