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.
- checksums.yaml +7 -0
- data/AGENTS.md +354 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +144 -0
- data/Rakefile +14 -0
- data/docs/plans/gemchat-local-bundle-index.md +1900 -0
- data/exe/gemchat +7 -0
- data/lib/gemchat/chunker.rb +120 -0
- data/lib/gemchat/cli.rb +731 -0
- data/lib/gemchat/embedder.rb +309 -0
- data/lib/gemchat/env.rb +27 -0
- data/lib/gemchat/errors.rb +10 -0
- data/lib/gemchat/hook.rb +113 -0
- data/lib/gemchat/hosted.rb +195 -0
- data/lib/gemchat/indexer.rb +114 -0
- data/lib/gemchat/init.rb +107 -0
- data/lib/gemchat/lockfile.rb +34 -0
- data/lib/gemchat/manifest.rb +100 -0
- data/lib/gemchat/markdown.rb +103 -0
- data/lib/gemchat/models.rb +241 -0
- data/lib/gemchat/paths.rb +33 -0
- data/lib/gemchat/plugin_index.rb +63 -0
- data/lib/gemchat/prose.rb +211 -0
- data/lib/gemchat/ri.rb +130 -0
- data/lib/gemchat/stopwords.rb +65 -0
- data/lib/gemchat/store.rb +555 -0
- data/lib/gemchat/symbols.rb +159 -0
- data/lib/gemchat/trust.rb +79 -0
- data/lib/gemchat/version.rb +5 -0
- data/lib/gemchat.rb +47 -0
- data/plugins.rb +74 -0
- data/sig/gemchat.rbs +4 -0
- data/skills/gemchat/SKILL.md +158 -0
- metadata +160 -0
data/exe/gemchat
ADDED
|
@@ -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
|