klee 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ad4060f69da403186a89fb198ddfdb917606eac5e3344dcb5b0cc58f3d1bdd8
4
- data.tar.gz: 90b540e449684b48e71714194b3c622f9fd35ecae931cb4b94fb53545f338958
3
+ metadata.gz: c8efdb5e1472295c0778a374828e8af7cb49e741a281d4d7558790863221ee0b
4
+ data.tar.gz: 99c547540505671159ace42750ba0ba6708aeb16f2888276a60426fc11316176
5
5
  SHA512:
6
- metadata.gz: 46ed4f0a4469aee13b165138f3ff04da7b30819e6e37b01524e750df591a52cf6da53dc2f082a8c353a1bfa6a47e37111814baf8bbcdd377f246797ea12ec283
7
- data.tar.gz: 77af3a015eb3e3fd8267b78525e46f53bb51694b2160d12a04e05bf6c04734549ed628a9cb5a8955cf4c9d6d4ec3ac1a544b011556125fdd5d07d2e1b6b71037
6
+ metadata.gz: 4009d050d3439f95c2d451f7ec9a5c56f407c0fe95b269fb802335c2262886cb2bb0e159d71f7068aaa4f35c87836c366ee8745421d55e425e5791d110530df0
7
+ data.tar.gz: 68ffb23b66ae05b66874e677ca47bc3ed96274f681d9d0e9f3f1c84726db35851cb2f3657404690b80189284c8b1460a96d57339b9106175146f3200dae97016
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [1.0.0] - 2026-09-19
9
+
10
+ ### Added
11
+
12
+ - Shared Words tokenizer with acronym splitting and predicate stripping (acff7d2)
13
+ - Nested class names, attr_* methods, and constant collaborators (18d27de)
14
+ - Klee.scan(profile: :rails) for models/lib globs and English stopwords (801fab2)
15
+ - ConceptIndex#catalog, #gaps, and #units (801fab2)
16
+
17
+ ### Changed
18
+
19
+ - Concepts#[] returns a Hash of concept words to method names (acff7d2)
20
+ - FileAnalyzer records each message receiver, not only locals and ivars (18d27de)
21
+ - Klee::Collaborators wraps FileAnalyzer instead of lexing the file (18d27de)
22
+
23
+ ### Fixed
24
+
25
+ - Klee.concepts now forwards the modifiers: keyword (acff7d2)
26
+ - Operator messages, block params, and conversion chains are ignored (18d27de)
27
+
8
28
  ## [0.1.2] - 2026-02-01
9
29
 
10
30
  ### Added
@@ -20,22 +40,3 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
20
40
  ### Changed
21
41
 
22
42
  - README now includes MCP server setup and available tools (f84bdc5)
23
-
24
- ## [0.1.1] - 2026-01-29
25
-
26
- ### Changed
27
-
28
- - Switch to supporting ruby 3.4 and up. (84fdd9c)
29
- - Require ruby 3.4 and above (067ec87)
30
-
31
- ### Added
32
-
33
- - Reissue for version and release support. (885fe28)
34
- - SimpleCov for coverage reporting (889d169)
35
- - Coverage comment from PRs (067ec87)
36
- - Klee.scan() method for codebase-wide analysis with glob patterns (3e509a1)
37
- - ConceptIndex for aggregating domain concepts across files (3e509a1)
38
- - CollaboratorIndex for pairwise co-occurrence and clustering (3e509a1)
39
- - FileAnalyzer using Prism AST to extract classes, methods, and collaborators (3e509a1)
40
- - Configurable threshold and ignore list for filtering noise (3e509a1)
41
- - Method-level scope option for tighter coupling analysis (3e509a1)
data/README.md CHANGED
@@ -45,11 +45,14 @@ end
45
45
 
46
46
  concept = Klee.object_concepts(Something)
47
47
 
48
- concept[4] #=> Set of words that appear at least 4 times
49
- concept[2] #=> larger Set of words that appear at least 2 times
48
+ concept[4]
49
+ # => { banana: [:banana_berry, :banana_boat, :banana_bunch, :banana_hammock], ... }
50
+
51
+ concept[3][:address]
52
+ # => [:address_informal_name, :address_street, :address_zip]
50
53
 
51
54
  filtered = Klee.object_concepts(Something, modifiers: %i[fill_in_ hover_over_ _message])
52
- filtered[4] #=> Set of concepts excluding any common modifiers
55
+ filtered[4] # modifiers stripped before words are counted
53
56
  ```
54
57
 
55
58
  ### Scan a codebase for domain concepts
@@ -77,6 +80,31 @@ codebase = Klee.scan("app/**/*.rb",
77
80
  threshold: 3)
78
81
  ```
79
82
 
83
+ ### Rails profile
84
+
85
+ Skip views, drop English function words, and get the extract-type read model:
86
+
87
+ ```ruby
88
+ cb = Klee.scan(profile: :rails) # app/models/**/*.rb + lib/**/*.rb from cwd
89
+
90
+ cb.concepts.catalog.first
91
+ # => #<data Klee::ConceptIndex::Entry word="intensity" class_count=71 method_count=29 bias=:type>
92
+
93
+ cb.concepts.gaps
94
+ # => { "seconds" => { classes: 0, methods: 95, names: #<Set: {"trailing_rest_seconds", ...}> }, ... }
95
+
96
+ cb.concepts.units
97
+ # => { "seconds" => ["cap_clock_seconds", "trailing_rest_seconds", ...], "minutes" => [...], "kg" => [...] }
98
+ ```
99
+
100
+ `catalog` bias is `:type` (class-heavy), `:verb` (`build`/`calculate`/`extract`/…), `:gap` (many methods, almost no classes), or `:mixed`.
101
+
102
+ Pass explicit globs with the profile to keep the stopwords:
103
+
104
+ ```ruby
105
+ Klee.scan("app/models/**/*.rb", profile: :rails, threshold: 4)
106
+ ```
107
+
80
108
  ### Find collaborator clusters
81
109
 
82
110
  Discover which objects frequently work together across your codebase.
@@ -1,41 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "prism"
4
-
5
3
  module Klee
6
4
  class Collaborators
7
5
  def initialize(const)
8
- @const = const
9
- end
10
- attr_reader :const
11
-
12
- def lexed
13
- Prism.lex_file(Object.const_source_location(const.name).first)
14
- end
15
-
16
- def additional_processing_types = [:DOT, :BRACKET_LEFT]
6
+ unless const.is_a?(Module)
7
+ raise ArgumentError, "expected a class or module, got #{const.inspect}"
8
+ end
17
9
 
18
- def identifier_with_additional_processing
19
- lexed.value.each_cons(2).filter_map do |(token, next_token)|
20
- if token.first.type == :IDENTIFIER &&
21
- additional_processing_types.include?(next_token.first.type)
22
- token.first.value
23
- end
10
+ path = Object.const_source_location(const.name)&.first
11
+ unless path && File.file?(path)
12
+ raise Error, "cannot locate source for #{const}"
24
13
  end
14
+
15
+ @const = const
16
+ @analyzer = FileAnalyzer.new(path)
25
17
  end
18
+ attr_reader :const
26
19
 
27
20
  def tally
28
- identifier_with_additional_processing.tally
21
+ @analyzer.collaborators.tally
29
22
  end
30
23
 
31
24
  def rank
32
- tally.group_by do |_, count|
33
- count
34
- end.sort.reverse.to_h.transform_values do |value|
35
- value.map(&:first)
36
- end.reject do |key, value|
37
- value.empty? || key < 3
38
- end
25
+ tally.group_by { |_, count| count }
26
+ .sort.reverse.to_h
27
+ .transform_values { |value| value.map(&:first) }
28
+ .reject { |key, _value| key < 3 }
39
29
  end
40
30
  end
41
31
  end
@@ -1,9 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "words"
4
+
3
5
  module Klee
4
6
  class ConceptIndex
5
7
  include Enumerable
6
8
 
9
+ Entry = Data.define(:word, :class_count, :method_count, :class_names, :method_names, :bias)
10
+
11
+ VERBS = %w[
12
+ build calculate extract apply compute detect update create find get set
13
+ add ensure render save destroy validate serialize
14
+ parse process fetch refresh call fill infer display generate
15
+ normalize coerce prepare predict split sanitize mark
16
+ ].freeze
17
+
18
+ UNIT_PATTERNS = {
19
+ "seconds" => /_seconds\z/,
20
+ "minutes" => /_minutes\z/,
21
+ "kg" => /_kg\z/,
22
+ "m" => /_m\z/,
23
+ "reps" => /_reps\z/
24
+ }.freeze
25
+
7
26
  def initialize(ignore: [], threshold: 2)
8
27
  @ignore = Set.new(ignore.map(&:to_s))
9
28
  @threshold = threshold
@@ -32,16 +51,60 @@ module Klee
32
51
  filtered.sort_by { |word, locs| -(locs[:classes].size + locs[:methods].size) }.to_h
33
52
  end
34
53
 
54
+ def catalog
55
+ rank.map do |word, locs|
56
+ Entry.new(
57
+ word: word,
58
+ class_count: locs[:classes].size,
59
+ method_count: locs[:methods].size,
60
+ class_names: locs[:classes],
61
+ method_names: locs[:methods],
62
+ bias: bias_for(word, locs)
63
+ )
64
+ end
65
+ end
66
+
67
+ def gaps(min_methods: 8, max_classes: 3)
68
+ filtered.each_with_object({}) do |(word, locs), acc|
69
+ next if VERBS.include?(word)
70
+ next if locs[:methods].size < min_methods
71
+ next if locs[:classes].size > max_classes
72
+
73
+ acc[word] = {
74
+ classes: locs[:classes].size,
75
+ methods: locs[:methods].size,
76
+ names: locs[:methods]
77
+ }
78
+ end
79
+ end
80
+
81
+ def units
82
+ names = @data.each_value.flat_map { |locs| locs[:methods].to_a }.uniq
83
+ UNIT_PATTERNS.each_with_object({}) do |(unit, pattern), acc|
84
+ matched = names.grep(pattern)
85
+ acc[unit] = matched.sort if matched.size >= @threshold
86
+ end
87
+ end
88
+
35
89
  private
36
90
 
37
91
  def words_from(name)
38
- name.to_s.gsub(/([a-z])([A-Z])/, '\1_\2')
39
- .downcase.split("_")
40
- .reject { |w| w.empty? || @ignore.include?(w) }
92
+ Words.from(name, ignore: @ignore)
41
93
  end
42
94
 
43
95
  def filtered
44
96
  @data.select { |_, locs| (locs[:classes].size + locs[:methods].size) >= @threshold }
45
97
  end
98
+
99
+ def bias_for(word, locs)
100
+ return :verb if VERBS.include?(word)
101
+
102
+ class_count = locs[:classes].size
103
+ method_count = locs[:methods].size
104
+ return :type if class_count > method_count
105
+ return :gap if method_count >= 8 && class_count <= 3
106
+
107
+ :mixed
108
+ end
46
109
  end
47
110
  end
data/lib/klee/concepts.rb CHANGED
@@ -1,26 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "words"
4
+
1
5
  module Klee
2
6
  class Concepts
3
7
  include Enumerable
4
8
 
5
9
  def initialize(*method_names, modifiers: [])
6
10
  @method_names = method_names
7
- @modifiers = modifiers
11
+ @modifiers = Array(modifiers).map(&:to_s)
8
12
  end
9
13
  attr_reader :method_names, :modifiers
10
14
 
11
15
  def call(threshold)
12
- warn "threshold is beyond the max count of #{max}" if threshold > max
13
- warn "threshold is below the min count of #{min}" if threshold < min
14
- clear
15
- @ideas = Set.new samples
16
- .select { |key, value|
17
- if value.to_i >= threshold
18
- key
19
- end
20
- }
21
- .compact
22
- .transform_keys(&:to_sym)
23
- .keys
16
+ unless samples.empty?
17
+ warn "threshold is beyond the max count of #{max}" if threshold > max
18
+ warn "threshold is below the min count of #{min}" if threshold < min
19
+ end
20
+
21
+ method_groups.select { |_, methods| methods.size >= threshold }
24
22
  end
25
23
  alias_method :[], :call
26
24
 
@@ -38,10 +36,14 @@ module Klee
38
36
  end
39
37
 
40
38
  def max
39
+ return 0 if samples.empty?
40
+
41
41
  max_by { |_, v| v }.last
42
42
  end
43
43
 
44
44
  def min
45
+ return 0 if samples.empty?
46
+
45
47
  min_by { |_, v| v }.last
46
48
  end
47
49
 
@@ -51,22 +53,22 @@ module Klee
51
53
 
52
54
  private
53
55
 
56
+ def method_groups
57
+ groups = Hash.new { |h, k| h[k] = [] }
58
+ method_names.each do |name|
59
+ words(name).uniq.each { |word| groups[word.to_sym] << name.to_sym }
60
+ end
61
+ groups
62
+ end
63
+
54
64
  def modifier_matcher
55
- @modifier_matcher ||= Regexp.new modifiers.map { Regexp.quote(it) }.join("|")
65
+ @modifier_matcher ||= Regexp.new(modifiers.map { Regexp.quote(it) }.join("|"))
56
66
  end
57
67
 
58
68
  def words(method_name)
59
- method_name.to_s
60
- .then do |string|
61
- if modifiers.empty?
62
- string
63
- else
64
- string.gsub(modifier_matcher, "")
65
- end
66
- end
67
- .gsub(/\s/, "")
68
- .split("_")
69
- .delete_if { it.empty? }
69
+ string = method_name.to_s
70
+ string = string.gsub(modifier_matcher, "") unless modifiers.empty?
71
+ Words.from(string)
70
72
  end
71
73
  end
72
74
  end
@@ -4,6 +4,22 @@ require "prism"
4
4
 
5
5
  module Klee
6
6
  class FileAnalyzer
7
+ ATTR_METHODS = %i[attr attr_reader attr_writer attr_accessor].freeze
8
+ INDEX_OPERATORS = Set[:[], :[]=].freeze
9
+ OPERATORS = Set.new(
10
+ %i[! != !~ + +@ - -@ * / % ** << >> & | ^ ~ <=> > >= < <= == === =~] +
11
+ INDEX_OPERATORS.to_a
12
+ ).freeze
13
+ CHAIN_NOISE = %w[
14
+ to_s to_str to_i to_int to_a to_ary to_h to_hash to_f to_sym to_proc
15
+ intern itself dup clone freeze tap new
16
+ where includes joins order limit offset eager_load preload
17
+ unscope merge distinct group having lock reselect reorder
18
+ first last take count map select collect compact flatten uniq
19
+ sort sort_by reverse each
20
+ Hash Array Set Rails Data Logger Kernel Object
21
+ ].to_set.freeze
22
+
7
23
  attr_reader :path, :class_names, :method_names, :collaborators, :method_collaborators
8
24
 
9
25
  def initialize(path)
@@ -19,38 +35,114 @@ module Klee
19
35
 
20
36
  def parse
21
37
  result = Prism.parse_file(@path)
22
- visit(result.value, current_method: nil)
38
+ visit(result.value, current_method: nil, namespace: [], block_params: Set.new)
23
39
  end
24
40
 
25
- def visit(node, current_method:)
41
+ def visit(node, current_method:, namespace:, block_params:)
26
42
  case node
27
43
  when Prism::ClassNode, Prism::ModuleNode
28
- @class_names << node.name.to_s
44
+ full_name = qualified_name(node, namespace)
45
+ @class_names << full_name
46
+ nested = full_name.split("::")
47
+ node.child_nodes.compact.each do |child|
48
+ visit(child, current_method: current_method, namespace: nested, block_params: block_params)
49
+ end
50
+ return
29
51
  when Prism::DefNode
30
52
  @method_names << node.name.to_s
31
53
  current_method = node.name.to_s
54
+ when Prism::BlockNode, Prism::LambdaNode
55
+ nested_params = block_params | Array(node.locals).map(&:to_s)
56
+ node.child_nodes.compact.each do |child|
57
+ visit(child, current_method: current_method, namespace: namespace, block_params: nested_params)
58
+ end
59
+ return
32
60
  when Prism::CallNode
33
- name = extract_collaborator(node)
34
- if name
35
- @collaborators << name
36
- @method_collaborators[current_method] << name if current_method
61
+ # Walk the receiver first so `user.account` records left-to-right.
62
+ if (receiver = node.receiver)
63
+ visit(receiver, current_method: current_method, namespace: namespace, block_params: block_params)
64
+ end
65
+ record_call(node, current_method, block_params)
66
+ node.child_nodes.compact.each do |child|
67
+ next if child.equal?(receiver)
68
+
69
+ visit(child, current_method: current_method, namespace: namespace, block_params: block_params)
37
70
  end
71
+ return
72
+ end
73
+
74
+ node.child_nodes.compact.each do |child|
75
+ visit(child, current_method: current_method, namespace: namespace, block_params: block_params)
76
+ end
77
+ end
78
+
79
+ def record_call(node, current_method, block_params)
80
+ if attr_call?(node)
81
+ @method_names.concat(attr_names(node))
82
+ return
38
83
  end
39
84
 
40
- node.child_nodes.compact.each { |child| visit(child, current_method: current_method) }
85
+ return if skip_operator_call?(node)
86
+
87
+ name = extract_collaborator(node)
88
+ return if skip_collaborator?(name, block_params)
89
+
90
+ @collaborators << name
91
+ @method_collaborators[current_method] << name if current_method
41
92
  end
42
93
 
94
+ def skip_operator_call?(node)
95
+ OPERATORS.include?(node.name) && !INDEX_OPERATORS.include?(node.name)
96
+ end
97
+
98
+ def skip_collaborator?(name, block_params)
99
+ return true if name.nil? || block_params.include?(name)
100
+
101
+ key = name.to_sym
102
+ OPERATORS.include?(key) || CHAIN_NOISE.include?(name)
103
+ end
104
+
105
+ def attr_call?(node)
106
+ node.receiver.nil? && ATTR_METHODS.include?(node.name)
107
+ end
108
+
109
+ def attr_names(node)
110
+ args = node.arguments&.arguments || []
111
+ args.filter_map { |arg| attr_name_from(arg) }.map do |name|
112
+ (node.name == :attr_writer) ? "#{name}=" : name
113
+ end
114
+ end
115
+
116
+ def attr_name_from(arg)
117
+ case arg
118
+ when Prism::SymbolNode, Prism::StringNode
119
+ arg.unescaped
120
+ end
121
+ end
122
+
123
+ # A collaborator is the immediate receiver of a non-operator message.
43
124
  def extract_collaborator(call_node)
44
- receiver = call_node.receiver
45
- case receiver
125
+ case (receiver = call_node.receiver)
46
126
  when Prism::LocalVariableReadNode
47
127
  receiver.name.to_s
48
128
  when Prism::InstanceVariableReadNode
49
129
  receiver.name.to_s.delete_prefix("@")
50
130
  when Prism::CallNode
51
- # Chain like user.account.name - extract the root
52
- extract_collaborator(receiver)
131
+ receiver.name.to_s
132
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
133
+ constant_name(receiver)
53
134
  end
54
135
  end
136
+
137
+ def qualified_name(node, namespace)
138
+ path = constant_name(node.constant_path)
139
+ return path if node.constant_path.is_a?(Prism::ConstantPathNode)
140
+
141
+ [*namespace, path].join("::")
142
+ end
143
+
144
+ def constant_name(node)
145
+ node.slice.delete_prefix("::")
146
+ end
55
147
  end
56
148
  end
data/lib/klee/gestalt.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Klee
2
4
  class Gestalt
3
5
  def initialize(object, patterns:, ignored:)
@@ -33,7 +35,7 @@ module Klee
33
35
  end
34
36
 
35
37
  plot["unusual"].merge(unusual_set(threshold))
36
- plot["concepts"].merge(concepts(modifiers: modifiers, threshold: concept_threshold))
38
+ plot["concepts"].merge(concepts(modifiers: modifiers, threshold: concept_threshold).keys)
37
39
 
38
40
  self
39
41
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klee
4
+ class Profile
5
+ RAILS_GLOBS = %w[app/models/**/*.rb lib/**/*.rb].freeze
6
+
7
+ STOPWORDS = %w[
8
+ a an the and or not to for from of in on at by as
9
+ is are was were been being have has had do does did
10
+ will would could should may might must can
11
+ this that these those it its they them we our you your
12
+ if else then when
13
+ with without via into over per vs all any
14
+ id ids key keys current value values
15
+ ].freeze
16
+
17
+ def self.resolve(name, patterns:, ignore:, threshold:)
18
+ case name&.to_sym
19
+ when nil
20
+ {patterns: patterns, ignore: Array(ignore), threshold: threshold}
21
+ when :rails
22
+ globs = patterns.empty? ? RAILS_GLOBS : patterns
23
+ {
24
+ patterns: globs,
25
+ ignore: (STOPWORDS + Array(ignore).map(&:to_s)).uniq,
26
+ threshold: threshold
27
+ }
28
+ else
29
+ raise ArgumentError, "unknown profile #{name.inspect}"
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/klee/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Klee
4
- VERSION = "0.1.2"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/klee/words.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klee
4
+ module Words
5
+ def self.from(name, ignore: [])
6
+ ignored = ignore.map(&:to_s)
7
+ tokenize(name).reject { |word| word.empty? || ignored.include?(word) }
8
+ end
9
+
10
+ def self.tokenize(name)
11
+ name.to_s
12
+ .gsub("::", "_")
13
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
14
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
15
+ .downcase
16
+ .gsub(/[?!=]/, "")
17
+ .split(/[_\s]+/)
18
+ end
19
+ end
20
+ end
data/lib/klee.rb CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  # require "classifier-reborn"
4
4
  require_relative "klee/version"
5
+ require_relative "klee/words"
5
6
  require_relative "klee/patterns"
6
7
  require_relative "klee/gestalt"
7
8
  require_relative "klee/concepts"
8
- require_relative "klee/collaborators"
9
9
  require_relative "klee/file_analyzer"
10
+ require_relative "klee/collaborators"
10
11
  require_relative "klee/concept_index"
11
12
  require_relative "klee/collaborator_index"
13
+ require_relative "klee/profile"
12
14
  require_relative "klee/codebase"
13
15
 
14
16
  module Klee
@@ -27,7 +29,7 @@ module Klee
27
29
  end
28
30
 
29
31
  def self.concepts(*method_names, modifiers: [])
30
- Concepts.new(*method_names, modifiers: [])
32
+ Concepts.new(*method_names, modifiers: modifiers)
31
33
  end
32
34
 
33
35
  # def self.classifier
@@ -53,7 +55,8 @@ module Klee
53
55
  Klee::Collaborators.new(const)
54
56
  end
55
57
 
56
- def self.scan(*patterns, ignore: [], threshold: 2)
57
- Codebase.new(*patterns, ignore: ignore, threshold: threshold)
58
+ def self.scan(*patterns, ignore: [], threshold: 2, profile: nil)
59
+ spec = Profile.resolve(profile, patterns: patterns, ignore: ignore, threshold: threshold)
60
+ Codebase.new(*spec[:patterns], ignore: spec[:ignore], threshold: spec[:threshold])
58
61
  end
59
62
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: klee
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -60,7 +60,9 @@ files:
60
60
  - lib/klee/mcp/tools/find_concept_clusters.rb
61
61
  - lib/klee/mcp/version.rb
62
62
  - lib/klee/patterns.rb
63
+ - lib/klee/profile.rb
63
64
  - lib/klee/version.rb
65
+ - lib/klee/words.rb
64
66
  - sig/klee.rbs
65
67
  homepage: https://github.com/saturnflyer/klee
66
68
  licenses:
@@ -90,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
92
  - !ruby/object:Gem::Version
91
93
  version: '0'
92
94
  requirements: []
93
- rubygems_version: 4.0.5
95
+ rubygems_version: 4.0.16
94
96
  specification_version: 4
95
97
  summary: Evaluate the similarities and differences in your objects
96
98
  test_files: []