typesafe-jev 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7645167a374f6815e9a4ee6b96b56d54f611a3a08b274467f963dedd66e786ca
4
+ data.tar.gz: 0d66633fe64b5061b9f1bf7cb59fdf74f73d711c332bb34f7bbf0be1a042b85c
5
+ SHA512:
6
+ metadata.gz: 6de70dbd0412a10808feb2f79e1635462056a0c8069306b3fd063167b15ff0e6706f64fc22719e3cdd47f21a3fff4a70c64d368660eb1491c5abd5b4240e7baf
7
+ data.tar.gz: fd391b3ecd54101ea40a0923d618ee6e9a884ae54c3c9876d9e3ef5ed3f3a8741b81b7ae032ebe3cf9e109cdf96df468bd3741995c94a6d0b949266ed2711622
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - Add question classes: `Typesafe::Noul`, `Typesafe::Choice`, `Typesafe::Score` with the abstract `Typesafe::Question` base. Frozen, immutable value objects that validate their inputs and serialize to the TypeSafe API shape via `#to_h`/`#to_json`.
6
+
7
+ ## 0.1.0
8
+
9
+ - Initial gem scaffold.
data/LICENSE ADDED
File without changes
data/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # typesafe-jev
2
+
3
+ Ruby client for [Jev](https://docs.typesafe.ai), TypeSafe's System One model — starting point, under construction.
4
+
5
+ ## Development
6
+
7
+ ```sh
8
+ bundle install
9
+ bundle exec rake test
10
+ ```
11
+
12
+ ## License
13
+
14
+ MIT
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ # A question that selects one option from a defined, unordered set.
5
+ # The answer includes the selected option, the probability distribution
6
+ # across all options, and a confidence.
7
+ #
8
+ # Typesafe::Choice.new("Which team should handle this?",
9
+ # criteria: {
10
+ # billing: "Payment or subscription issues",
11
+ # technical: "Bugs or integration problems"
12
+ # })
13
+ class Choice < Question
14
+ # @return [Hash] the option keys mapped to their descriptions.
15
+ attr_reader :criteria
16
+
17
+ # @param instructions [String] the question asked about the state.
18
+ # @param criteria [Hash] the options: keys (String or Symbol) mapped to
19
+ # non-empty String descriptions.
20
+ def initialize(instructions, criteria:)
21
+ @criteria = normalize_criteria(criteria)
22
+ super(instructions)
23
+ end
24
+
25
+ def type
26
+ "choice"
27
+ end
28
+
29
+ def to_h
30
+ super.merge(criteria: @criteria)
31
+ end
32
+
33
+ private
34
+
35
+ def normalize_criteria(criteria)
36
+ unless criteria.is_a?(Hash) && !criteria.empty?
37
+ raise ArgumentError, "criteria must be a non-empty Hash of option keys to descriptions"
38
+ end
39
+
40
+ criteria.each_with_object({}) do |(key, value), normalized|
41
+ unless key.is_a?(String) || key.is_a?(Symbol)
42
+ raise ArgumentError, "criteria keys must be Strings or Symbols"
43
+ end
44
+
45
+ normalized[key] = validate_string(value, "criteria[#{key.inspect}]")
46
+ end.freeze
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ # A yes/no question. The answer is the probability that the answer is yes.
5
+ #
6
+ # Typesafe::Noul.new("Does the customer request a refund?")
7
+ #
8
+ # +criteria+ optionally clarifies what yes and no mean:
9
+ #
10
+ # Typesafe::Noul.new("Is the message urgent?",
11
+ # criteria: { true: "Conveys urgency", false: "No time pressure" })
12
+ class Noul < Question
13
+ # @return [Hash, nil] the optional +{ true:, false: }+ descriptions.
14
+ attr_reader :criteria
15
+
16
+ # @param instructions [String] the yes/no question asked about the state.
17
+ # @param criteria [Hash, nil] optional +{ true:, false: }+ descriptions of
18
+ # what yes and no mean. Keys may be Strings or Symbols and are
19
+ # normalized to Symbols.
20
+ def initialize(instructions, criteria: nil)
21
+ @criteria = criteria.nil? ? nil : normalize_criteria(criteria)
22
+ super(instructions)
23
+ end
24
+
25
+ def type
26
+ "noul"
27
+ end
28
+
29
+ # @return [Hash] the API shape; +criteria+ is omitted when not given.
30
+ def to_h
31
+ @criteria ? super.merge(criteria: @criteria) : super
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_criteria(criteria)
37
+ unless criteria.is_a?(Hash)
38
+ raise ArgumentError, "criteria must be a Hash with :true and :false keys"
39
+ end
40
+
41
+ normalized = {}
42
+ criteria.each do |key, value|
43
+ unless key.is_a?(String) || key.is_a?(Symbol)
44
+ raise ArgumentError, "criteria keys must be Strings or Symbols"
45
+ end
46
+
47
+ sym = key.to_sym
48
+ raise ArgumentError, "criteria has duplicate key #{sym.inspect}" if normalized.key?(sym)
49
+
50
+ normalized[sym] = validate_string(value, "criteria[#{key.inspect}]")
51
+ end
52
+
53
+ unless normalized.keys.sort == %i[false true]
54
+ raise ArgumentError, "criteria must have exactly :true and :false keys"
55
+ end
56
+
57
+ normalized.freeze
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ # Abstract base class for the three question types understood by the
5
+ # TypeSafe System One API: {Noul}, {Choice}, and {Score}.
6
+ #
7
+ # A question is an immutable value object. The question ID used to identify
8
+ # its answer is not part of the object: it is the Hash key under which the
9
+ # question is sent in a request, e.g.
10
+ #
11
+ # questions = { refund_requested: Typesafe::Noul.new("Does the customer request a refund?") }
12
+ class Question
13
+ # @return [String] the question asked about the state.
14
+ attr_reader :instructions
15
+
16
+ # @param instructions [String] the question asked about the state.
17
+ # @raise [ArgumentError] if +instructions+ is not a non-empty String.
18
+ def initialize(instructions)
19
+ unless instructions.is_a?(String) && !instructions.strip.empty?
20
+ raise ArgumentError, "instructions must be a non-empty String"
21
+ end
22
+
23
+ @instructions = instructions.dup.freeze
24
+ validate!
25
+ freeze
26
+ end
27
+
28
+ # @return [String] the API question type tag ("noul", "choice", or "score").
29
+ # @raise [NotImplementedError] on the abstract base class.
30
+ def type
31
+ raise NotImplementedError, "#{self.class} must implement #type"
32
+ end
33
+
34
+ # @return [Hash] the question shape expected by the TypeSafe API.
35
+ def to_h
36
+ { type: type, instructions: instructions }
37
+ end
38
+
39
+ # @return [String] the question serialized as JSON for the TypeSafe API.
40
+ def to_json(state = nil)
41
+ JSON.generate(to_h, state)
42
+ end
43
+
44
+ # Questions compare equal when they are of the same class and serialize
45
+ # to the same API shape.
46
+ def ==(other)
47
+ other.class == self.class && other.to_h == to_h
48
+ end
49
+ alias eql? ==
50
+
51
+ def hash
52
+ [self.class, to_h].hash
53
+ end
54
+
55
+ private
56
+
57
+ # Hook for subclasses to validate their own attributes before freezing.
58
+ def validate!; end
59
+
60
+ # Validates that +value+ is a non-empty String and returns a frozen copy.
61
+ def validate_string(value, name)
62
+ unless value.is_a?(String) && !value.strip.empty?
63
+ raise ArgumentError, "#{name} must be a non-empty String"
64
+ end
65
+
66
+ value.dup.freeze
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ # A question that positions the state along an ordered spectrum of levels.
5
+ # The answer includes the score, the legend (levels by number), the
6
+ # probability distribution across levels, and a confidence.
7
+ #
8
+ # Typesafe::Score.new("How frustrated is the customer?",
9
+ # criteria: [
10
+ # "Calm, just stating facts",
11
+ # "Frustrated but civil",
12
+ # "Very angry, strong language"
13
+ # ])
14
+ class Score < Question
15
+ # @return [Array<String>] the ordered levels of the spectrum.
16
+ attr_reader :criteria
17
+
18
+ # @param instructions [String] the question asked about the state.
19
+ # @param criteria [Array<String>] the ordered levels; a non-empty array
20
+ # of non-empty Strings, ordered from lowest to highest.
21
+ def initialize(instructions, criteria:)
22
+ @criteria = normalize_criteria(criteria)
23
+ super(instructions)
24
+ end
25
+
26
+ def type
27
+ "score"
28
+ end
29
+
30
+ def to_h
31
+ super.merge(criteria: @criteria)
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_criteria(criteria)
37
+ unless criteria.is_a?(Array) && !criteria.empty?
38
+ raise ArgumentError, "criteria must be a non-empty Array of Strings"
39
+ end
40
+
41
+ criteria.each_with_index.map do |level, index|
42
+ validate_string(level, "criteria[#{index}]")
43
+ end.freeze
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ VERSION = "0.2.0"
5
+ end
data/lib/typesafe.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "typesafe/version"
6
+ require_relative "typesafe/question"
7
+ require_relative "typesafe/noul"
8
+ require_relative "typesafe/choice"
9
+ require_relative "typesafe/score"
10
+
11
+ module Typesafe
12
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: typesafe-jev
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - dtheo
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.13'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.13'
40
+ description: Use Jev, TypeSafe's System One model, from Ruby or Rails.
41
+ email:
42
+ - dtheo@users.noreply.github.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CHANGELOG.md
48
+ - LICENSE
49
+ - README.md
50
+ - lib/typesafe.rb
51
+ - lib/typesafe/choice.rb
52
+ - lib/typesafe/noul.rb
53
+ - lib/typesafe/question.rb
54
+ - lib/typesafe/score.rb
55
+ - lib/typesafe/version.rb
56
+ homepage: https://docs.typesafe.ai
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ homepage_uri: https://docs.typesafe.ai
61
+ source_code_uri: https://github.com/dtheo/typesafe-jev-ruby
62
+ changelog_uri: https://github.com/dtheo/typesafe-jev-ruby/blob/main/CHANGELOG.md
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '3.1'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.6.9
78
+ specification_version: 4
79
+ summary: Ruby client for Jev, TypeSafe's System One model.
80
+ test_files: []