gemchat 0.1.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.
data/exe/gemchat ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/gemchat"
5
+ require_relative "../lib/gemchat/cli"
6
+
7
+ exit Gemchat::CLI.start(ARGV)
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemchat
4
+ # Turns ri structs into indexable chunks. One chunk per method (signature +
5
+ # description is the unit an agent acts on) and one per class, so a query for
6
+ # an API name lands on something directly usable.
7
+ #
8
+ # Plain Ruby only: this gem does not depend on ActiveSupport.
9
+ class Chunker
10
+ MIN_CHARS = 20
11
+ MAX_BODY_CHARS = 2000
12
+ MAX_SIGNATURE_CHARS = 200
13
+
14
+ # Labelled fields, so BM25 can weight a signature match above a prose match.
15
+ # Blank fields are dropped by Chunk#searchable_text rather than emitted as
16
+ # "method_name: ", which would pad every class-level chunk with dead tokens.
17
+ SEARCH_FIELDS = {
18
+ "title" => :title,
19
+ "source_type" => :source_type,
20
+ "class_name" => :class_name,
21
+ "method_name" => :method_name,
22
+ "signature" => :signature
23
+ }.freeze
24
+
25
+ # `source_path` and `source_line` come from the Prism pass, not from ri --
26
+ # ri cannot tell us which file a method lives in. See `Gemchat::Symbols`.
27
+ Chunk = Struct.new(
28
+ :gem_name, :gem_version, :source_type, :title, :body,
29
+ :class_name, :method_name, :method_type, :signature,
30
+ :source_path, :source_line
31
+ ) do
32
+ def content
33
+ [signature.to_s, body.to_s].reject { |s| s.strip.empty? }.join("\n\n")
34
+ end
35
+
36
+ def searchable_text
37
+ labelled = SEARCH_FIELDS.filter_map { |label, attr|
38
+ value = public_send(attr).to_s.strip
39
+ "#{label}: #{value}" unless value.empty?
40
+ }
41
+
42
+ (labelled << content).reject { |s| s.to_s.strip.empty? }.join("\n")
43
+ end
44
+
45
+ # Includes the source location. A chunk whose definition moved to another
46
+ # file is a changed chunk, and reusing the old row would leave a citation
47
+ # pointing at the wrong line.
48
+ def content_digest
49
+ Digest::SHA256.hexdigest([searchable_text, source_path.to_s, source_line.to_s].join("\n"))
50
+ end
51
+ end
52
+
53
+ def self.from_ri(gem_name, gem_version, classes)
54
+ new(gem_name, gem_version).from_ri(classes)
55
+ end
56
+
57
+ def initialize(gem_name, gem_version)
58
+ @gem_name = gem_name
59
+ @gem_version = gem_version
60
+ end
61
+
62
+ # An unreadable or empty ri store yields nil rather than raising, so this
63
+ # has to tolerate a nil argument.
64
+ def from_ri(classes)
65
+ classes.to_a.flat_map { |klass| chunks_for(klass) }.compact
66
+ end
67
+
68
+ private
69
+
70
+ def chunks_for(klass)
71
+ class_name = klass[:class_name].to_s
72
+ out = []
73
+
74
+ description = klass[:description].to_s
75
+ if description.length >= MIN_CHARS
76
+ out << build(
77
+ title: class_name,
78
+ body: description,
79
+ class_name: class_name,
80
+ method_name: nil,
81
+ method_type: nil,
82
+ signature: nil
83
+ )
84
+ end
85
+
86
+ (klass[:methods] || []).each do |method|
87
+ separator = (method[:type] == "class") ? "::" : "#"
88
+ qualified = "#{class_name}#{separator}#{method[:name]}"
89
+ body = [method[:description].to_s, method[:signature].to_s]
90
+ .reject { |s| s.strip.empty? }.join("\n")
91
+ next if body.length < MIN_CHARS
92
+
93
+ out << build(
94
+ title: qualified,
95
+ body: body,
96
+ class_name: class_name,
97
+ method_name: method[:name],
98
+ method_type: method[:type],
99
+ signature: method[:signature]
100
+ )
101
+ end
102
+
103
+ out
104
+ end
105
+
106
+ def build(title:, body:, class_name:, method_name:, method_type:, signature:)
107
+ Chunk.new(
108
+ gem_name: @gem_name,
109
+ gem_version: @gem_version,
110
+ source_type: "ri",
111
+ title: title,
112
+ body: body[0, MAX_BODY_CHARS],
113
+ class_name: class_name,
114
+ method_name: method_name,
115
+ method_type: method_type,
116
+ signature: signature.to_s[0, MAX_SIGNATURE_CHARS]
117
+ )
118
+ end
119
+ end
120
+ end