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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Gemchat
|
|
7
|
+
# The sole source of file:line and signature metadata, per §11 of the plan.
|
|
8
|
+
# ri tells us a class and a method; it does not tell us which file the method is
|
|
9
|
+
# defined in, or on which line. That is the information an agent cannot recover
|
|
10
|
+
# from the result itself, and it is what a citation needs.
|
|
11
|
+
#
|
|
12
|
+
# Ported from the app's `RubySourceDefinitionService`, minus Rails: no
|
|
13
|
+
# `Rails.logger`, no ActiveSupport's `blank?`/`presence`, no `filter_map` on
|
|
14
|
+
# an Array that is sometimes a Hash. The visitor logic is unchanged, including
|
|
15
|
+
# its known imprecision -- a method defined inside `extend SomeModule` is
|
|
16
|
+
# reported under the lexical owner rather than the mixin. §4.4 argues that
|
|
17
|
+
# reporting the lexical owner is arguably *more* precise, since it says the
|
|
18
|
+
# method comes from a mixin.
|
|
19
|
+
class Symbols
|
|
20
|
+
# The same caps the app uses. A generated or vendored tree can contain tens of
|
|
21
|
+
# thousands of .rb files, and this pass runs on every indexed gem.
|
|
22
|
+
MAX_FILES = 2000
|
|
23
|
+
MAX_FILE_BYTES = 1_000_000
|
|
24
|
+
|
|
25
|
+
Definition = Struct.new(
|
|
26
|
+
:class_name, :method_name, :method_type, :signature, :source_path, :source_line
|
|
27
|
+
) do
|
|
28
|
+
# Looked up against an ri chunk, which knows class/method/method_type but
|
|
29
|
+
# not a file. A missing owner on either side means the chunk is a class-level
|
|
30
|
+
# or top-level definition and there is nothing to match.
|
|
31
|
+
def key
|
|
32
|
+
[class_name.to_s, method_name.to_s, method_type.to_s]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Definitions for every .rb under `dir`, keyed by Definition#key.
|
|
37
|
+
def self.for(dir)
|
|
38
|
+
new(dir).index
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(dir)
|
|
42
|
+
@dir = dir.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def index
|
|
46
|
+
return {} unless File.directory?(@dir)
|
|
47
|
+
|
|
48
|
+
files = Dir.glob(File.join(@dir, "**", "*.rb")).sort.first(MAX_FILES)
|
|
49
|
+
defs = files.flat_map { |path| parse_file(path) }
|
|
50
|
+
|
|
51
|
+
# First definition wins. A gem that reopens a class in several files has
|
|
52
|
+
# several definitions for one key, and the first in sorted order is the
|
|
53
|
+
# most predictable choice. Duplicates are counted so the caller can report
|
|
54
|
+
# an ambiguous match rather than pretending there was only one.
|
|
55
|
+
defs.group_by(&:key).transform_values(&:first).tap { |out| @collisions = defs.size - out.size }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def collisions
|
|
59
|
+
@collisions.to_i
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def parse_file(path)
|
|
65
|
+
return [] if File.size(path) > MAX_FILE_BYTES
|
|
66
|
+
|
|
67
|
+
source = File.read(path, encoding: Encoding::UTF_8)
|
|
68
|
+
result = Prism.parse(source)
|
|
69
|
+
relative = Pathname.new(path).relative_path_from(Pathname.new(@dir)).to_s
|
|
70
|
+
Visitor.new(relative).tap { |v| v.visit(result.value) }.definitions
|
|
71
|
+
rescue SystemCallError, ArgumentError, Prism::ParseError => e
|
|
72
|
+
# One unparseable or unreadable file must not cost the whole gem its source
|
|
73
|
+
# locations. A gem with a syntax error in one file still has thousands of
|
|
74
|
+
# parseable ones.
|
|
75
|
+
warn " symbols: skipping #{File.basename(path)} (#{e.class})" if ENV["GEMCHAT_DEBUG"]
|
|
76
|
+
[]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Tracks class/module nesting and `class << self` scope.
|
|
80
|
+
class Visitor < Prism::Visitor
|
|
81
|
+
attr_reader :definitions
|
|
82
|
+
|
|
83
|
+
def initialize(source_path)
|
|
84
|
+
@source_path = source_path
|
|
85
|
+
@definitions = []
|
|
86
|
+
@namespace = []
|
|
87
|
+
@singleton_depth = 0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def visit_module_node(node)
|
|
91
|
+
with_namespace(constant_name(node.constant_path)) { visit_child_nodes(node) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def visit_class_node(node)
|
|
95
|
+
with_namespace(constant_name(node.constant_path)) do
|
|
96
|
+
# `class << self` opens a singleton scope for the rest of the body.
|
|
97
|
+
if node.body.is_a?(Prism::SingletonClassNode)
|
|
98
|
+
@singleton_depth += 1
|
|
99
|
+
begin
|
|
100
|
+
visit_child_nodes(node.body)
|
|
101
|
+
ensure
|
|
102
|
+
@singleton_depth -= 1
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
visit_child_nodes(node)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def visit_singleton_class_node(node)
|
|
111
|
+
@singleton_depth += 1
|
|
112
|
+
begin
|
|
113
|
+
visit_child_nodes(node)
|
|
114
|
+
ensure
|
|
115
|
+
@singleton_depth -= 1
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def visit_def_node(node)
|
|
120
|
+
@definitions << Definition.new(
|
|
121
|
+
current_owner, node.name.to_s,
|
|
122
|
+
class_method?(node) ? "class" : "instance",
|
|
123
|
+
signature_for(node), @source_path, node.location.start_line
|
|
124
|
+
)
|
|
125
|
+
visit_child_nodes(node)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def with_namespace(name)
|
|
131
|
+
@namespace.push(name)
|
|
132
|
+
yield
|
|
133
|
+
ensure
|
|
134
|
+
@namespace.pop
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def current_owner
|
|
138
|
+
full = @namespace.reject { |n| n.to_s.strip.empty? }.join("::")
|
|
139
|
+
full.empty? ? nil : full
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def class_method?(node)
|
|
143
|
+
return true if @singleton_depth.positive?
|
|
144
|
+
|
|
145
|
+
receiver = node.receiver
|
|
146
|
+
receiver.is_a?(Prism::SelfNode) || receiver.is_a?(Prism::ConstantReadNode)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def signature_for(node)
|
|
150
|
+
params = node.parameters&.location&.slice || ""
|
|
151
|
+
"#{node.name}(#{params})"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def constant_name(node)
|
|
155
|
+
node&.slice || ""
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require_relative "paths"
|
|
7
|
+
|
|
8
|
+
module Gemchat
|
|
9
|
+
# Which projects may run the plugin's index hook on their own.
|
|
10
|
+
#
|
|
11
|
+
# A Bundler plugin is checked-in code that executes for anyone who clones the
|
|
12
|
+
# repository. Running `gemchat index` is much less dangerous than arbitrary
|
|
13
|
+
# code, but it still means one machine's bundle install indexes a path derived
|
|
14
|
+
# from someone else's Gemfile, and it writes to the invoking user's home
|
|
15
|
+
# directory. So the hook asks first.
|
|
16
|
+
#
|
|
17
|
+
# The gate is deliberately low-friction for a project's own author: the first
|
|
18
|
+
# *explicit* gemchat command run inside a project grants trust for that
|
|
19
|
+
# project. Someone who just added `plugin "gemchat"` to their own Gemfile gets
|
|
20
|
+
# auto-indexing with no ceremony. Someone who cloned that repository does not,
|
|
21
|
+
# because they have not run a gemchat command.
|
|
22
|
+
#
|
|
23
|
+
# Records live in GEMCHAT_CONFIG_HOME, not the data root, so wiping the index
|
|
24
|
+
# does not also discard these decisions.
|
|
25
|
+
class Trust
|
|
26
|
+
FILENAME = "trusted.json"
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
def path
|
|
30
|
+
File.join(Paths.config_home, FILENAME)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def all
|
|
34
|
+
return {} unless File.file?(path)
|
|
35
|
+
|
|
36
|
+
data = JSON.parse(File.read(path))
|
|
37
|
+
data.is_a?(Hash) ? data : {}
|
|
38
|
+
rescue SystemCallError, JSON::ParserError
|
|
39
|
+
# Unreadable or corrupt trust file means "nothing is trusted", which fails
|
|
40
|
+
# closed. Deliberately narrow: a bare rescue also swallowed a NoMethodError
|
|
41
|
+
# from a missing require and reported every project as untrusted, which
|
|
42
|
+
# looks exactly like a working gate and is very hard to see.
|
|
43
|
+
{}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def trusted?(dir = Dir.pwd)
|
|
47
|
+
all.key?(expand(dir))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def grant!(dir = Dir.pwd, via: "explicit command")
|
|
51
|
+
records = all
|
|
52
|
+
records[expand(dir)] = {"granted_at" => Time.now.utc.iso8601, "via" => via}
|
|
53
|
+
write(records)
|
|
54
|
+
records[expand(dir)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def revoke!(dir = Dir.pwd)
|
|
58
|
+
records = all
|
|
59
|
+
removed = records.delete(expand(dir))
|
|
60
|
+
write(records) if removed
|
|
61
|
+
!removed.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def write(records)
|
|
65
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
66
|
+
File.write(path, JSON.pretty_generate(records))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Project identity is the absolute, symlink-resolved path. Resolving
|
|
70
|
+
# matters: trusting /tmp/x and then arriving at /private/tmp/x on macOS
|
|
71
|
+
# must be the same project, or trust silently never matches.
|
|
72
|
+
def expand(dir)
|
|
73
|
+
File.realpath(dir)
|
|
74
|
+
rescue SystemCallError
|
|
75
|
+
File.expand_path(dir)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/gemchat.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
require "rbconfig"
|
|
8
|
+
require "rdoc"
|
|
9
|
+
require "rdoc/ri/driver"
|
|
10
|
+
require "open3"
|
|
11
|
+
|
|
12
|
+
require_relative "gemchat/version"
|
|
13
|
+
require_relative "gemchat/errors"
|
|
14
|
+
require_relative "gemchat/paths"
|
|
15
|
+
require_relative "gemchat/env"
|
|
16
|
+
require_relative "gemchat/ri"
|
|
17
|
+
require_relative "gemchat/symbols"
|
|
18
|
+
require_relative "gemchat/chunker"
|
|
19
|
+
require_relative "gemchat/markdown"
|
|
20
|
+
require_relative "gemchat/prose"
|
|
21
|
+
require_relative "gemchat/stopwords"
|
|
22
|
+
require_relative "gemchat/hosted"
|
|
23
|
+
require_relative "gemchat/models"
|
|
24
|
+
require_relative "gemchat/embedder"
|
|
25
|
+
require_relative "gemchat/store"
|
|
26
|
+
require_relative "gemchat/lockfile"
|
|
27
|
+
require_relative "gemchat/manifest"
|
|
28
|
+
require_relative "gemchat/trust"
|
|
29
|
+
require_relative "gemchat/hook"
|
|
30
|
+
require_relative "gemchat/plugin_index"
|
|
31
|
+
require_relative "gemchat/init"
|
|
32
|
+
require_relative "gemchat/indexer"
|
|
33
|
+
require_relative "gemchat/cli"
|
|
34
|
+
|
|
35
|
+
module Gemchat
|
|
36
|
+
def self.root
|
|
37
|
+
Paths.root
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.config_home
|
|
41
|
+
Paths.config_home
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.version
|
|
45
|
+
Gemchat::VERSION
|
|
46
|
+
end
|
|
47
|
+
end
|
data/plugins.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bundler plugin entry point. This file executes on every `bundle install` for
|
|
4
|
+
# anyone who clones a project that declares `plugin "gemchat"`, so it is treated
|
|
5
|
+
# as untrusted input.
|
|
6
|
+
#
|
|
7
|
+
# Three rules, in priority order:
|
|
8
|
+
#
|
|
9
|
+
# 1. Never raise. A plugins.rb that raises is wrapped in MalformattedPlugin and
|
|
10
|
+
# fails the whole install (exit 29, no lockfile written). Nothing here may
|
|
11
|
+
# propagate.
|
|
12
|
+
# 2. Carry no logic. Dual declaration installs two physical copies of gemchat
|
|
13
|
+
# and Bundler 4 writes no PLUGINS lockfile section, so nothing pins the
|
|
14
|
+
# plugin copy to the dependency copy. Requiring the *bundled* copy makes
|
|
15
|
+
# drift harmless: this file only locates and loads.
|
|
16
|
+
# 3. Say one line, or nothing. The hook fires on CI and deploy installs too.
|
|
17
|
+
#
|
|
18
|
+
# This file lives in the gem, never in a consuming project. A project-root
|
|
19
|
+
# plugins.rb is silently never executed.
|
|
20
|
+
#
|
|
21
|
+
# Every Bundler reference is root-scoped with `::`. Bundler loads this file with
|
|
22
|
+
# `load(path, true)`, which wraps it in an anonymous module, and `register_plugin`
|
|
23
|
+
# is mid-flight inside `module Bundler::Plugin` while this runs — so a bare
|
|
24
|
+
# `Bundler::Plugin` here can resolve against the wrapper and raise NameError,
|
|
25
|
+
# which then aborts the install. `::Bundler` cannot be shadowed that way.
|
|
26
|
+
module Bundler
|
|
27
|
+
module Gemchat
|
|
28
|
+
HOST_GEM = "gemchat"
|
|
29
|
+
SUPPORTED = Gem::Requirement.new(">= 0.1.0")
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def auto_index
|
|
33
|
+
# Delegates to the bundled copy. This file deliberately holds no logic:
|
|
34
|
+
# a dual `gem` + `plugin` declaration installs two physical copies and
|
|
35
|
+
# Bundler 4 pins neither to the other, so anything implemented here
|
|
36
|
+
# would exist twice and could disagree with the version the project
|
|
37
|
+
# actually depends on. See §15.4.
|
|
38
|
+
spec = host_spec
|
|
39
|
+
return report("#{HOST_GEM} is not in the bundle") unless spec
|
|
40
|
+
return report("skipped: outside supported range") unless SUPPORTED.satisfied_by?(spec.version)
|
|
41
|
+
|
|
42
|
+
lib = File.join(spec.full_gem_path, "lib")
|
|
43
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
44
|
+
require "gemchat/hook"
|
|
45
|
+
::Gemchat::Hook.auto_index
|
|
46
|
+
rescue => e
|
|
47
|
+
report "auto-index skipped (#{e.class}: #{e.message})"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def host_spec
|
|
51
|
+
@host_spec ||= ::Bundler.load.specs.find { |s| s.name == HOST_GEM }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def report(message)
|
|
55
|
+
warn "[gemchat] #{message}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Registration, not execution. A plugins.rb that merely *runs* on load is
|
|
62
|
+
# executed once, during the plugin phase of the very `bundle install` that
|
|
63
|
+
# installed it, and never again: Bundler records which hooks a plugin
|
|
64
|
+
# registered into .bundle/plugin/index, and `hook_plugins` gates every later
|
|
65
|
+
# invocation on that record. Without this line the index shows `hooks:` empty
|
|
66
|
+
# and `bundle add` / `bundle update` are silent no-ops despite the plugin being
|
|
67
|
+
# installed and having printed output once already.
|
|
68
|
+
#
|
|
69
|
+
# The event name must be the string. `Events::GEM_AFTER_INSTALL_ALL` happens to
|
|
70
|
+
# be that same String so it also works, but :after_install_all raises
|
|
71
|
+
# ArgumentError, which register_plugin rewraps as MalformattedPlugin.
|
|
72
|
+
::Bundler::Plugin.add_hook("after-install-all") do
|
|
73
|
+
Bundler::Gemchat.auto_index
|
|
74
|
+
end
|
data/sig/gemchat.rbs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gemchat
|
|
3
|
+
description: Answer questions about the gems in this project's bundle — their API, configuration, and version-specific behaviour — from a local, offline index. Use when asked how a bundled gem works, what a method does, or how to use a library already in the Gemfile. Triggers on gem names, "how do I use X", method or class names from a dependency, and error messages from a gem.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gemchat
|
|
7
|
+
|
|
8
|
+
`gemchat` indexes the documentation of the gems in **this project's own
|
|
9
|
+
`Gemfile.lock`** and answers questions about them offline. It is a local tool: no
|
|
10
|
+
account, no API key, nothing leaves the machine.
|
|
11
|
+
|
|
12
|
+
## Reach for this when
|
|
13
|
+
|
|
14
|
+
- Someone asks how a gem **in this bundle** works, or what a method does.
|
|
15
|
+
- An error message came from a dependency and you need the surrounding API.
|
|
16
|
+
- You need the signature or the configuration options of something installed.
|
|
17
|
+
|
|
18
|
+
## Do not reach for this when
|
|
19
|
+
|
|
20
|
+
- The question is about **this application's** runtime — routes, schema, job
|
|
21
|
+
state, request logs. That is Rails Hyperdrive's job, not a documentation index.
|
|
22
|
+
- The gem is **not in the bundle**. gemchat indexes `Gemfile.lock`; a gem you are
|
|
23
|
+
merely considering is not there.
|
|
24
|
+
|
|
25
|
+
## Pick the right command
|
|
26
|
+
|
|
27
|
+
This is the part worth getting right, because the two search commands fail in
|
|
28
|
+
**opposite** directions and using the wrong one returns nothing at all.
|
|
29
|
+
|
|
30
|
+
| Question shape | Command | Why |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| An exact identifier — `Rake::FileList#exclude`, an error string, a flag | `gemchat search` | Instant, no model needed, and exact. |
|
|
33
|
+
| A natural-language question — "how do I list every task" | `gemchat query` | Needs vectors; use after `embed`. |
|
|
34
|
+
| A paraphrase you want only the vector arm for | `gemchat vsearch` | Same requirement. |
|
|
35
|
+
|
|
36
|
+
**`gemchat search` does not do keyword search.** It matches exact phrases. A
|
|
37
|
+
question like *"how do I list all the available tasks"* returns **no results**,
|
|
38
|
+
because those words do not appear together in any chunk. That is expected
|
|
39
|
+
behaviour, not a broken index — reach for `query` instead.
|
|
40
|
+
|
|
41
|
+
`gemchat query` takes a leading type that routes it:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
gemchat query "how do I list every task" # both engines, fused (default)
|
|
45
|
+
gemchat query "lex: Rake::Task#enhance" # BM25 only
|
|
46
|
+
gemchat query "vec: stop a process on a signal" # vectors only
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`lex:` never loads a model, so it is the cheap choice for a symbol.
|
|
50
|
+
|
|
51
|
+
## Before searching, run `gemchat status`
|
|
52
|
+
|
|
53
|
+
It is one line each, and it tells you whether an answer is even possible:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
index: 5 gems · 1095 chunks # what is indexed
|
|
57
|
+
manifest: up to date # index matches the current lockfile
|
|
58
|
+
vectors: not downloaded (146MB) → gemchat embed
|
|
59
|
+
hosted: not configured → gemchat.org/settings
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `vectors: not downloaded` means `query` and `vsearch` **cannot answer yet**.
|
|
63
|
+
They do not download anything themselves; `gemchat embed` is the explicit step.
|
|
64
|
+
- `manifest: stale` means the lockfile has moved since the last index. Re-run
|
|
65
|
+
`gemchat index`, or answers may be about a version that is no longer installed.
|
|
66
|
+
|
|
67
|
+
## Cost
|
|
68
|
+
|
|
69
|
+
- `gemchat search` — instant, free, no model on disk.
|
|
70
|
+
- `gemchat embed` — one-time, downloads a 146MB model, then embeds the index.
|
|
71
|
+
Every later semantic search is local and fast.
|
|
72
|
+
- If the model is not downloaded, `query` degrades to BM25 and says so on stderr.
|
|
73
|
+
It does not fail silently and it does not download behind your back.
|
|
74
|
+
|
|
75
|
+
## Reading a result
|
|
76
|
+
|
|
77
|
+
Each result names its source, and that changes how you should treat it:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
1. [rake 13.4.2 ri ] Rake::FileList#exclude
|
|
81
|
+
exclude(*patterns, &block) (lib/rake/file_list.rb:150)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `ri` is API reference generated from the installed gem's own source. The
|
|
85
|
+
`path:line` is the real definition site — open it before asserting behaviour.
|
|
86
|
+
- `readme`, `guides` and `doc` are the gem's hand-written prose, split by
|
|
87
|
+
heading. Use these for *why* and *when*, not for signatures.
|
|
88
|
+
|
|
89
|
+
If `path:line` is missing on an `ri` hit, the definition is not recoverable by
|
|
90
|
+
parsing (a C extension, `define_method`, or a macro). Say so rather than
|
|
91
|
+
guessing a file.
|
|
92
|
+
|
|
93
|
+
## Version fidelity
|
|
94
|
+
|
|
95
|
+
The index is built from `Gemfile.lock`, so local answers are about **the versions
|
|
96
|
+
actually installed** — usually what you want. Check `manifest: up to date` before
|
|
97
|
+
relying on that; if it is stale, the answer may describe a version that is no
|
|
98
|
+
longer there.
|
|
99
|
+
|
|
100
|
+
### Hosted backend
|
|
101
|
+
|
|
102
|
+
`gemchat query --hosted` asks gemchat.org instead. It is opt-in and **never
|
|
103
|
+
happens automatically** — there is no route from a failed or unconfigured hosted
|
|
104
|
+
query to a local answer, so you will get an error rather than a local result you
|
|
105
|
+
did not ask for. Do not retry it locally and present that as the answer.
|
|
106
|
+
|
|
107
|
+
It sends this lockfile's exact gem versions, and the response says which ones it
|
|
108
|
+
could scope to:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
note: (scoped to 13.4.2; no per-version index for sidekiq)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Report that note. A gem with no per-version index on the server is answered from
|
|
115
|
+
its full docs, which may not be the pinned release — so a scoped note is fidelity,
|
|
116
|
+
and an absent one is not.
|
|
117
|
+
|
|
118
|
+
Two things that do not exist on the hosted side:
|
|
119
|
+
|
|
120
|
+
- **`lex:` has no hosted equivalent.** It is BM25, which is local-only.
|
|
121
|
+
`gemchat query --hosted lex: …` refuses and points at `gemchat search`. Use
|
|
122
|
+
`gemchat search` for a symbol.
|
|
123
|
+
- **`search` and `vsearch` are local-only**, whatever the backend is configured.
|
|
124
|
+
|
|
125
|
+
If the server does not have a gem you named, the result says so and points at
|
|
126
|
+
`--auto-index`, which asks gemchat.org to index it. That spends the caller's
|
|
127
|
+
quota, so it is opt-in and the message is a suggestion, not something to pass
|
|
128
|
+
through on your own.
|
|
129
|
+
|
|
130
|
+
## Invocation
|
|
131
|
+
|
|
132
|
+
Use `bundle exec gemchat` when gemchat is a development dependency in this
|
|
133
|
+
project's Gemfile (which is what `gemchat init` sets up), so you get the project's
|
|
134
|
+
bundle. Use bare `gemchat` when it is installed globally.
|
|
135
|
+
|
|
136
|
+
## When the index is wrong
|
|
137
|
+
|
|
138
|
+
- `gemchat reindex` discards the index and the generated ri, then builds it
|
|
139
|
+
again. Use it when `status` reports a stale manifest, or after upgrading
|
|
140
|
+
gemchat, whose store format is version-gated and will refuse to open a store
|
|
141
|
+
it did not write.
|
|
142
|
+
- `gemchat version` prints the version, which is worth quoting when a search
|
|
143
|
+
behaves unexpectedly.
|
|
144
|
+
|
|
145
|
+
## Setup
|
|
146
|
+
|
|
147
|
+
`gemchat init` is the only command that modifies a Gemfile. It adds gemchat as a
|
|
148
|
+
development dependency and a Bundler plugin, and grants trust for this project.
|
|
149
|
+
After that, any `bundle install`, `bundle add` or `bundle update` that moves the
|
|
150
|
+
lockfile re-indexes automatically and prints one line. Anything else is silent.
|
|
151
|
+
|
|
152
|
+
`gemchat init --hosted` additionally records the hosted backend as this
|
|
153
|
+
project's default, so later `gemchat query` calls use it without a flag. It still
|
|
154
|
+
needs a key: `GEMCHAT_API_KEY`, or a `credentials` file under the gemchat config
|
|
155
|
+
home. `gemchat status` says which of those is in play.
|
|
156
|
+
|
|
157
|
+
If the project has no `Gemfile.lock`, or the gems are not installed, `index`
|
|
158
|
+
cannot work — those gems have to be on disk to be documented.
|
metadata
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gemchat
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Saroj Maharjan (zoras)
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sqlite3
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rllama
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: prism
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rdoc
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '6.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '6.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: json
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '2.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.0'
|
|
82
|
+
description: |
|
|
83
|
+
gemchat indexes the documentation for the gems in your project's own bundle and
|
|
84
|
+
serves BM25 search over it from the command line, with no account, no API key and
|
|
85
|
+
no network.
|
|
86
|
+
|
|
87
|
+
It is a local tool, not the hosted gemchat.org service. An optional mode can defer
|
|
88
|
+
to gemchat.org if you would rather not keep a local index.
|
|
89
|
+
|
|
90
|
+
Direct dependencies are indexed by default and ri documentation is generated
|
|
91
|
+
locally, because Bundler installs gems without documentation. Results carry the
|
|
92
|
+
exact locked version and real API signatures, which is the point: an agent can
|
|
93
|
+
look up how to call a library without reading a 1500-line lockfile into context.
|
|
94
|
+
email:
|
|
95
|
+
- zoras@users.noreply.github.com
|
|
96
|
+
executables:
|
|
97
|
+
- gemchat
|
|
98
|
+
extensions: []
|
|
99
|
+
extra_rdoc_files: []
|
|
100
|
+
files:
|
|
101
|
+
- AGENTS.md
|
|
102
|
+
- CHANGELOG.md
|
|
103
|
+
- CODE_OF_CONDUCT.md
|
|
104
|
+
- LICENSE.txt
|
|
105
|
+
- README.md
|
|
106
|
+
- Rakefile
|
|
107
|
+
- docs/plans/gemchat-local-bundle-index.md
|
|
108
|
+
- exe/gemchat
|
|
109
|
+
- lib/gemchat.rb
|
|
110
|
+
- lib/gemchat/chunker.rb
|
|
111
|
+
- lib/gemchat/cli.rb
|
|
112
|
+
- lib/gemchat/embedder.rb
|
|
113
|
+
- lib/gemchat/env.rb
|
|
114
|
+
- lib/gemchat/errors.rb
|
|
115
|
+
- lib/gemchat/hook.rb
|
|
116
|
+
- lib/gemchat/hosted.rb
|
|
117
|
+
- lib/gemchat/indexer.rb
|
|
118
|
+
- lib/gemchat/init.rb
|
|
119
|
+
- lib/gemchat/lockfile.rb
|
|
120
|
+
- lib/gemchat/manifest.rb
|
|
121
|
+
- lib/gemchat/markdown.rb
|
|
122
|
+
- lib/gemchat/models.rb
|
|
123
|
+
- lib/gemchat/paths.rb
|
|
124
|
+
- lib/gemchat/plugin_index.rb
|
|
125
|
+
- lib/gemchat/prose.rb
|
|
126
|
+
- lib/gemchat/ri.rb
|
|
127
|
+
- lib/gemchat/stopwords.rb
|
|
128
|
+
- lib/gemchat/store.rb
|
|
129
|
+
- lib/gemchat/symbols.rb
|
|
130
|
+
- lib/gemchat/trust.rb
|
|
131
|
+
- lib/gemchat/version.rb
|
|
132
|
+
- plugins.rb
|
|
133
|
+
- sig/gemchat.rbs
|
|
134
|
+
- skills/gemchat/SKILL.md
|
|
135
|
+
homepage: https://github.com/zoras/gemchat
|
|
136
|
+
licenses:
|
|
137
|
+
- MIT
|
|
138
|
+
metadata:
|
|
139
|
+
homepage_uri: https://github.com/zoras/gemchat
|
|
140
|
+
source_code_uri: https://github.com/zoras/gemchat
|
|
141
|
+
changelog_uri: https://github.com/zoras/gemchat/blob/main/CHANGELOG.md
|
|
142
|
+
rubygems_mfa_required: 'true'
|
|
143
|
+
rdoc_options: []
|
|
144
|
+
require_paths:
|
|
145
|
+
- lib
|
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: 3.2.0
|
|
151
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
|
+
requirements:
|
|
153
|
+
- - ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: '0'
|
|
156
|
+
requirements: []
|
|
157
|
+
rubygems_version: 4.0.22
|
|
158
|
+
specification_version: 4
|
|
159
|
+
summary: Local, offline documentation index for the gems in your own Gemfile.lock
|
|
160
|
+
test_files: []
|