sloprb 0.0.1 → 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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +136 -18
- data/lib/bslop/debug.rb +4 -0
- data/lib/bslop.rb +8 -0
- data/lib/sloprb/configuration.rb +66 -0
- data/lib/sloprb/generator.rb +90 -0
- data/lib/sloprb/parser.rb +112 -0
- data/lib/sloprb/setup.rb +5 -0
- data/lib/sloprb/transformer.rb +59 -0
- data/lib/sloprb/typoeater.rb +89 -0
- data/lib/sloprb/version.rb +1 -1
- data/lib/sloprb.rb +25 -0
- metadata +39 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 506617067b45365d7b15feb11e2cc0dab95787adb139adbf60e5c544a7a2dd0f
|
|
4
|
+
data.tar.gz: 722f67bc7a699d4be54fed15ab204d39d5b68928b2d836d690df958f78a5ca60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23ec911d3f3fd26a939b58bde4171e4bed6579b2326fca4f186c5afd22e3159782cd975228bb09ba6d3d24175d4a39e5b4eb1c5063ceca646f7bc480cbebae0b
|
|
7
|
+
data.tar.gz: 11d5af26a6ea5f78b9bef9c2e199fbc6775966719081fee60b3c1b2ef75b2c02573b95c0060f593461eeb3351f2b26f6753aae4932269c962a20d397b8c41420
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -3,45 +3,163 @@
|
|
|
3
3
|
|
|
4
4
|
# Sloprb
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Stop writing code, let LLMs slop it for you at boot time 🦾.
|
|
7
|
+
|
|
8
|
+
SlopRB uses [Require Hooks][require-hooks] to intercept Ruby's `require` calls and generate some code for you using LLMs. Currently supported scenarios:
|
|
9
|
+
- [Slop method implementations](#slopping-methods) on the fly. Just mark a method with `# :slop:`, describe what it should do in comments, and SlopRB fills in the body when the file is loaded.
|
|
10
|
+
- [Auto-correct syntax errors](#eating-typos). When loading a file fails with a SyntaxError, the LLM fixes it, persists the fix, and retries the load.
|
|
7
11
|
|
|
8
12
|
## Installation
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
Add to your Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "sloprb"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Slopping methods
|
|
21
|
+
|
|
22
|
+
1. You write a method stub with a `# :slop:` marker and optional doc comments describing the desired behavior.
|
|
23
|
+
2. When the file is required, SlopRB parses it with [Prism](https://github.com/ruby/prism), extracts all slop-marked methods, and sends the source to an LLM.
|
|
24
|
+
3. The LLM generates the method bodies, SlopRB splices them into the source, validates syntax, and returns the transformed code to Ruby.
|
|
25
|
+
|
|
26
|
+
Consider an example:
|
|
11
27
|
|
|
12
28
|
```ruby
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
# ...
|
|
29
|
+
class Calculator
|
|
30
|
+
# Evaluates the passed arithmetic expression.
|
|
31
|
+
def evaluate(expr) # :slop:
|
|
32
|
+
end
|
|
18
33
|
end
|
|
34
|
+
|
|
35
|
+
puts Calculator.new.evaluate("2 + 2 * 3") #=> 8
|
|
19
36
|
```
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
You can also provide a sketch of the method or some hints in the body:
|
|
22
39
|
|
|
23
40
|
```ruby
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
class Greeting
|
|
42
|
+
# Generate a friendly greeting message for the given name.
|
|
43
|
+
def greet(name) # :slop:
|
|
44
|
+
stable_index = ... # generate stable index based on name
|
|
45
|
+
samples = [...] # three random greeting phrases (avoid common, most popular options, be creative)
|
|
46
|
+
# return <greeting>, <name> in the end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
puts Greeting.new.greet("Vova")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Setup
|
|
54
|
+
|
|
55
|
+
Load `sloprb/setup` before any code containing slop-marked methods:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require "sloprb/setup"
|
|
26
59
|
```
|
|
27
60
|
|
|
28
|
-
|
|
61
|
+
This configures the LLM provider (auto-detected from environment variables) and registers the Require Hooks source transformer.
|
|
29
62
|
|
|
30
|
-
|
|
63
|
+
### Using with `ruby` executable
|
|
31
64
|
|
|
32
|
-
|
|
65
|
+
You can also setup SlopRB via a command-line switch (which is just a `#require` statement):
|
|
33
66
|
|
|
34
|
-
|
|
67
|
+
```sh
|
|
68
|
+
ruby -rbslop examples/calculator.rb
|
|
69
|
+
```
|
|
35
70
|
|
|
36
|
-
|
|
71
|
+
Use `-rbslop/debug` to also write the generated source to `<path>.slop.rb` files:
|
|
37
72
|
|
|
38
|
-
|
|
73
|
+
```sh
|
|
74
|
+
ruby -rbslop/debug examples/greeting.rb
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Marking methods
|
|
78
|
+
|
|
79
|
+
Add `# :slop:` as a trailing comment on any `def` line:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class Stoplight
|
|
83
|
+
COLORS = %i[green yellow red].freeze
|
|
84
|
+
|
|
85
|
+
# Initialize the stoplight with a starting color.
|
|
86
|
+
# Default to green.
|
|
87
|
+
def initialize(color = :green) # :slop:
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Advance the stoplight to the next color in the cycle:
|
|
91
|
+
# green -> yellow -> red -> green
|
|
92
|
+
def next # :slop:
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Comments immediately above the `def` line are sent to the LLM as guidance. The enclosing class/module context, method parameters, and surrounding source are also included in the prompt.
|
|
39
98
|
|
|
40
|
-
|
|
99
|
+
### Caching with Bootsnap
|
|
41
100
|
|
|
42
|
-
|
|
101
|
+
If you use [Bootsnap][], the generated code is automatically cached and invalidated on every source file change thanks to the Bootsnap support in Require Hooks. You can try it yourself using our examples:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
$ OPENAI_API_KEY=<your key> bundle exec ruby -Ilib -r ./examples/bootsnap -rbslop examples/calculator.rb "42 / 7"
|
|
105
|
+
|
|
106
|
+
6.0
|
|
107
|
+
|
|
108
|
+
# now, you can run it without LLM creds, and it will still work
|
|
109
|
+
bundle exec ruby -Ilib -r ./examples/bootsnap -rbslop examples/calculator.rb "25 + 26"
|
|
110
|
+
|
|
111
|
+
51.0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Eating typos
|
|
115
|
+
|
|
116
|
+
SlopRB's _Typoeater_ extension automatically fixes syntax errors. When a file fails to load with a `SyntaxError`, the LLM attempts to fix it, validates the correction with Prism, and retries the load — transparently.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
require "sloprb/typoeater"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or via command line:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
ruby -r sloprb/typoeater my_script.rb
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
When a required file has a syntax error, Typoeater:
|
|
129
|
+
1. Captures the error and reads the file source
|
|
130
|
+
2. Sends both to the LLM for correction
|
|
131
|
+
3. Validates the fix with Prism (bad LLM output = original error raised, file untouched)
|
|
132
|
+
4. Overwrites the file and retries the load (once)
|
|
133
|
+
|
|
134
|
+
Typoeater works independently from slop method generation — you can use either or both. When both are active, they share the same LLM configuration.
|
|
135
|
+
|
|
136
|
+
## LLM configuration
|
|
137
|
+
|
|
138
|
+
SlopRB auto-detects the LLM provider from environment variables, checked in this order:
|
|
139
|
+
|
|
140
|
+
| Provider | Required env var | Optional env vars |
|
|
141
|
+
|-----------|----------------------|---------------------------|
|
|
142
|
+
| OpenAI | `OPENAI_API_KEY` | `SLOPRB_MODEL` |
|
|
143
|
+
| Anthropic | `ANTHROPIC_API_KEY` | `SLOPRB_MODEL` |
|
|
144
|
+
| Ollama | `OLLAMA_BASE_URL`, `SLOPRB_MODEL` | |
|
|
145
|
+
|
|
146
|
+
For OpenAI and Anthropic, `SLOPRB_MODEL` is optional (sensible defaults are used). For Ollama, it is required.
|
|
147
|
+
|
|
148
|
+
Set `SLOPRB_MODEL` to override the default model:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
SLOPRB_MODEL=claude-sonnet-4-6 ruby -r bslop examples/calculator.rb
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Enable debug output with `SLOPRB_DEBUG=1` to write `.slop.rb` files with the generated source.
|
|
155
|
+
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/sloprb](https://github.com/palkan/sloprb).
|
|
43
159
|
|
|
44
160
|
## License
|
|
45
161
|
|
|
46
162
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
47
163
|
|
|
164
|
+
[require-hooks]: https://github.com/ruby-next/require-hooks
|
|
165
|
+
[Bootsnap]: https://github.com/rails/bootsnap
|
data/lib/bslop/debug.rb
ADDED
data/lib/bslop.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sloprb
|
|
4
|
+
class Configuration
|
|
5
|
+
PROVIDERS = [
|
|
6
|
+
[:openai, ->(env) { env["OPENAI_API_KEY"] }, ->(env) {
|
|
7
|
+
{openai_api_key: env["OPENAI_API_KEY"]}
|
|
8
|
+
}],
|
|
9
|
+
[:anthropic, ->(env) { env["ANTHROPIC_API_KEY"] }, ->(env) {
|
|
10
|
+
{anthropic_api_key: env["ANTHROPIC_API_KEY"]}
|
|
11
|
+
}],
|
|
12
|
+
[:ollama, ->(env) { env["OLLAMA_BASE_URL"] }, ->(env) {
|
|
13
|
+
base = env["OLLAMA_BASE_URL"].chomp("/")
|
|
14
|
+
base += "/v1" unless base.end_with?("/v1")
|
|
15
|
+
{openai_api_key: "ollama", ollama_api_base: base}
|
|
16
|
+
}]
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :provider, :model
|
|
20
|
+
|
|
21
|
+
def initialize(env = ENV)
|
|
22
|
+
@env = env
|
|
23
|
+
@provider, @config = detect_provider(env)
|
|
24
|
+
@model = env.fetch("SLOPRB_MODEL") { default_model }
|
|
25
|
+
@configured = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configure_ruby_llm!
|
|
29
|
+
raise "sloprb: SLOPRB_MODEL is required for the #{@provider} provider." unless @model
|
|
30
|
+
|
|
31
|
+
RubyLLM.configure do |c|
|
|
32
|
+
@config.each { |k, v| c.public_send(:"#{k}=", v) }
|
|
33
|
+
c.log_level = :unknown unless @env["SLOPRB_DEBUG"]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def ensure_ruby_llm_configured!
|
|
38
|
+
return if @configured
|
|
39
|
+
|
|
40
|
+
raise "sloprb: No LLM provider configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OLLAMA_BASE_URL." unless @provider
|
|
41
|
+
|
|
42
|
+
@configured = true
|
|
43
|
+
|
|
44
|
+
configure_ruby_llm!
|
|
45
|
+
RubyLLM::Models.refresh! if provider == :ollama
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def detect_provider(env)
|
|
51
|
+
PROVIDERS.each do |name, detect, config|
|
|
52
|
+
return [name, config.call(env)] if detect.call(env)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_model
|
|
59
|
+
case provider
|
|
60
|
+
when :openai then "gpt-5.3-codex"
|
|
61
|
+
when :anthropic then "claude-opus-4-6"
|
|
62
|
+
when :ollama then nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sloprb
|
|
4
|
+
module Generator
|
|
5
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
6
|
+
You are a Ruby code generator. You will be given a Ruby source file containing method stubs marked with `# :slop:`.
|
|
7
|
+
Your job is to generate the implementation for each marked method.
|
|
8
|
+
|
|
9
|
+
Some methods may contain sketch code, incomplete expressions (like `...`), pseudo-code, or comment instructions in their body.
|
|
10
|
+
Treat these as structural hints and guidance for your implementation — fill in the blanks, replace placeholders,
|
|
11
|
+
and follow the instructions to produce a complete working method body.
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
- Return ONLY the method bodies, not the full method definitions
|
|
15
|
+
- Use `### method_name` as a delimiter before each method body
|
|
16
|
+
- Do not include `def` or `end` lines
|
|
17
|
+
- Use the comments above each method and any sketch code inside the method as guidance
|
|
18
|
+
- Use the class/module context to understand the domain
|
|
19
|
+
- Write idiomatic Ruby
|
|
20
|
+
PROMPT
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def call(source, slop_methods, chat: nil)
|
|
24
|
+
chat ||= default_chat
|
|
25
|
+
|
|
26
|
+
user_prompt = build_prompt(source, slop_methods)
|
|
27
|
+
response = chat.with_instructions(SYSTEM_PROMPT).ask(user_prompt)
|
|
28
|
+
|
|
29
|
+
parse_response(response.content, slop_methods)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def default_chat
|
|
35
|
+
Sloprb.configuration.ensure_ruby_llm_configured!
|
|
36
|
+
RubyLLM.chat(model: Sloprb.configuration.model)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_prompt(source, slop_methods)
|
|
40
|
+
method_list = slop_methods.map { |m|
|
|
41
|
+
desc = m.leading_comments.empty? ? "" : "\n Comments: #{m.leading_comments}"
|
|
42
|
+
sketch = m.body_sketch.empty? ? "" : "\n Sketch:\n#{m.body_sketch.lines.map { |l| " #{l}" }.join}"
|
|
43
|
+
"- #{m.name}#{m.parameters}#{desc}#{sketch}"
|
|
44
|
+
}.join("\n")
|
|
45
|
+
|
|
46
|
+
<<~PROMPT
|
|
47
|
+
Here is the Ruby source file:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
#{source}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Generate implementations for these methods:
|
|
54
|
+
#{method_list}
|
|
55
|
+
|
|
56
|
+
Return only the method bodies using `### method_name` delimiters.
|
|
57
|
+
PROMPT
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_response(content, slop_methods)
|
|
61
|
+
bodies = {}
|
|
62
|
+
current_method = nil
|
|
63
|
+
current_lines = []
|
|
64
|
+
|
|
65
|
+
content.each_line do |line|
|
|
66
|
+
if line.match?(/^```\w*\s*$/)
|
|
67
|
+
next
|
|
68
|
+
elsif line.match?(/^###\s+\S+/)
|
|
69
|
+
if current_method
|
|
70
|
+
bodies[current_method] = current_lines.join.strip
|
|
71
|
+
end
|
|
72
|
+
current_method = line.strip.sub(/^###\s+/, "")
|
|
73
|
+
current_lines = []
|
|
74
|
+
else
|
|
75
|
+
current_lines << line
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
bodies[current_method] = current_lines.join.strip if current_method
|
|
80
|
+
|
|
81
|
+
missing = slop_methods.map(&:name) - bodies.keys
|
|
82
|
+
unless missing.empty?
|
|
83
|
+
raise "sloprb: LLM response missing implementations for: #{missing.join(", ")}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
bodies
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sloprb
|
|
4
|
+
module Parser
|
|
5
|
+
SlopMethod = Data.define(:name, :parameters, :start_offset, :end_offset, :leading_comments, :enclosing_context, :start_column, :body_sketch)
|
|
6
|
+
|
|
7
|
+
class Visitor < Prism::Visitor
|
|
8
|
+
attr_reader :slop_methods
|
|
9
|
+
|
|
10
|
+
def initialize(source, comments)
|
|
11
|
+
super()
|
|
12
|
+
@source = source
|
|
13
|
+
@comments = comments
|
|
14
|
+
@context_stack = []
|
|
15
|
+
@slop_methods = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_class_node(node)
|
|
19
|
+
@context_stack.push(constant_name(node))
|
|
20
|
+
super
|
|
21
|
+
@context_stack.pop
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_module_node(node)
|
|
25
|
+
@context_stack.push(constant_name(node))
|
|
26
|
+
super
|
|
27
|
+
@context_stack.pop
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_def_node(node)
|
|
31
|
+
def_line = node.location.start_line
|
|
32
|
+
|
|
33
|
+
trailing = @comments.find { |c|
|
|
34
|
+
c.location.start_line == def_line &&
|
|
35
|
+
c.location.start_column > node.name_loc.end_column
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if trailing&.location&.slice&.include?(":slop:")
|
|
39
|
+
params = node.parameters ? node.location.slice[/\(.*?\)/] : nil
|
|
40
|
+
leading = collect_leading_comments(def_line)
|
|
41
|
+
sketch = extract_body_sketch(node)
|
|
42
|
+
|
|
43
|
+
@slop_methods << SlopMethod.new(
|
|
44
|
+
name: node.name.to_s,
|
|
45
|
+
parameters: params,
|
|
46
|
+
start_offset: node.location.start_offset,
|
|
47
|
+
end_offset: node.location.end_offset,
|
|
48
|
+
leading_comments: leading,
|
|
49
|
+
enclosing_context: @context_stack.dup,
|
|
50
|
+
start_column: node.location.start_column,
|
|
51
|
+
body_sketch: sketch
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def constant_name(node)
|
|
61
|
+
node.constant_path.full_name
|
|
62
|
+
rescue Prism::DynamicPartsInConstantPathError
|
|
63
|
+
node.constant_path.slice
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def extract_body_sketch(node)
|
|
67
|
+
end_kw = node.end_keyword_loc
|
|
68
|
+
return "" unless end_kw
|
|
69
|
+
|
|
70
|
+
def_line_end = @source.index("\n", node.location.start_offset)
|
|
71
|
+
return "" unless def_line_end
|
|
72
|
+
|
|
73
|
+
body_start = def_line_end + 1
|
|
74
|
+
body_end = end_kw.start_offset
|
|
75
|
+
return "" if body_start >= body_end
|
|
76
|
+
|
|
77
|
+
raw = @source[body_start...body_end]
|
|
78
|
+
dedent(raw).strip
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dedent(text)
|
|
82
|
+
lines = text.lines
|
|
83
|
+
non_blank = lines.reject { |l| l.strip.empty? }
|
|
84
|
+
return text if non_blank.empty?
|
|
85
|
+
|
|
86
|
+
min_indent = non_blank.map { |l| l[/^ */].length }.min
|
|
87
|
+
lines.map { |l| l.sub(/^ {0,#{min_indent}}/, "") }.join
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def collect_leading_comments(def_line)
|
|
91
|
+
lines = []
|
|
92
|
+
line = def_line - 1
|
|
93
|
+
|
|
94
|
+
while line >= 1
|
|
95
|
+
comment = @comments.find { |c| c.location.start_line == line }
|
|
96
|
+
break unless comment
|
|
97
|
+
lines.unshift(comment.location.slice)
|
|
98
|
+
line -= 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
lines.join("\n")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.call(source)
|
|
106
|
+
result = Prism.parse(source)
|
|
107
|
+
visitor = Visitor.new(source, result.comments)
|
|
108
|
+
visitor.visit(result.value)
|
|
109
|
+
visitor.slop_methods
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/sloprb/setup.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sloprb
|
|
4
|
+
module Transformer
|
|
5
|
+
class SyntaxError < ::SyntaxError; end
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def call(path, source, chat: nil)
|
|
9
|
+
source ||= File.read(path)
|
|
10
|
+
|
|
11
|
+
return unless source.include?(":slop:")
|
|
12
|
+
|
|
13
|
+
slop_methods = Parser.call(source)
|
|
14
|
+
return if slop_methods.empty?
|
|
15
|
+
|
|
16
|
+
bodies = Generator.call(source, slop_methods, chat:)
|
|
17
|
+
transformed = splice(source, slop_methods, bodies)
|
|
18
|
+
|
|
19
|
+
validate_syntax!(transformed)
|
|
20
|
+
|
|
21
|
+
if ENV["SLOPRB_DEBUG"]
|
|
22
|
+
File.write("#{path.sub(/\.rb$/, "")}.slop.rb", transformed)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
transformed
|
|
26
|
+
rescue => e
|
|
27
|
+
raise if e.is_a?(SyntaxError)
|
|
28
|
+
warn "sloprb: Failed to generate methods for #{path}: #{e.message}"
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def splice(source, slop_methods, bodies)
|
|
35
|
+
result = source.dup
|
|
36
|
+
|
|
37
|
+
slop_methods.sort_by(&:start_offset).reverse_each do |method|
|
|
38
|
+
body = bodies[method.name]
|
|
39
|
+
indent = " " * (method.start_column + 2)
|
|
40
|
+
indented_body = body.lines.map { |line| "#{indent}#{line}" }.join.rstrip
|
|
41
|
+
|
|
42
|
+
params = method.parameters || ""
|
|
43
|
+
replacement = "def #{method.name}#{params}\n#{indented_body}\n#{" " * method.start_column}end"
|
|
44
|
+
|
|
45
|
+
result[method.start_offset...method.end_offset] = replacement
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_syntax!(source)
|
|
52
|
+
result = Prism.parse(source)
|
|
53
|
+
return if result.errors.empty?
|
|
54
|
+
|
|
55
|
+
raise SyntaxError, "sloprb: Generated source has syntax errors:\n#{result.errors.map(&:message).join("\n")}\n\nSource:\n#{source}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sloprb"
|
|
4
|
+
|
|
5
|
+
module Sloprb
|
|
6
|
+
module Typoeater
|
|
7
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
8
|
+
You are a Ruby syntax error fixer. You will be given a Ruby source file that has a syntax error, along with the error message.
|
|
9
|
+
Your job is to fix the syntax error and return the corrected source file.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- Return ONLY the complete corrected Ruby source file
|
|
13
|
+
- Do not include any explanation, comments about the fix, or markdown formatting
|
|
14
|
+
- Do not change any logic or behavior — only fix the syntax error
|
|
15
|
+
- Preserve all existing comments, whitespace style, and formatting
|
|
16
|
+
PROMPT
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(path, error, chat: nil)
|
|
20
|
+
chat ||= default_chat
|
|
21
|
+
|
|
22
|
+
source = File.read(path)
|
|
23
|
+
error_context = error.respond_to?(:detailed_message) ? error.detailed_message(highlight: false) : error.message
|
|
24
|
+
|
|
25
|
+
user_prompt = <<~PROMPT
|
|
26
|
+
Here is the Ruby source file with a syntax error:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
#{source}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Syntax error:
|
|
33
|
+
#{error_context}
|
|
34
|
+
|
|
35
|
+
Return the complete corrected Ruby source file.
|
|
36
|
+
PROMPT
|
|
37
|
+
|
|
38
|
+
response = chat.with_instructions(SYSTEM_PROMPT).ask(user_prompt)
|
|
39
|
+
corrected = strip_fences(response.content)
|
|
40
|
+
corrected << "\n" unless corrected.end_with?("\n")
|
|
41
|
+
|
|
42
|
+
result = Prism.parse(corrected)
|
|
43
|
+
return if result.errors.any?
|
|
44
|
+
|
|
45
|
+
corrected
|
|
46
|
+
rescue
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def register_hook
|
|
51
|
+
require "require-hooks/setup"
|
|
52
|
+
|
|
53
|
+
RequireHooks.around_load do |path, &load_block|
|
|
54
|
+
result = begin
|
|
55
|
+
load_block.call
|
|
56
|
+
rescue ::SyntaxError => original_error
|
|
57
|
+
corrected = Typoeater.call(path, original_error)
|
|
58
|
+
raise original_error unless corrected
|
|
59
|
+
|
|
60
|
+
File.write(path, corrected)
|
|
61
|
+
|
|
62
|
+
warn "[sloprb] auto-corrected syntax error in #{path}"
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
load_block.call
|
|
66
|
+
rescue ::SyntaxError
|
|
67
|
+
raise original_error
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def default_chat
|
|
77
|
+
Sloprb.configuration.ensure_ruby_llm_configured!
|
|
78
|
+
RubyLLM.chat(model: Sloprb.configuration.model)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def strip_fences(content)
|
|
82
|
+
content.sub(/\A\s*```\w*\n/, "").sub(/\n```\s*\z/, "")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Sloprb.configuration ||= Sloprb::Configuration.new
|
|
89
|
+
Sloprb::Typoeater.register_hook
|
data/lib/sloprb/version.rb
CHANGED
data/lib/sloprb.rb
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
4
|
+
require "ruby_llm"
|
|
5
|
+
|
|
3
6
|
require "sloprb/version"
|
|
7
|
+
require "sloprb/configuration"
|
|
8
|
+
require "sloprb/parser"
|
|
9
|
+
require "sloprb/generator"
|
|
10
|
+
require "sloprb/transformer"
|
|
11
|
+
|
|
12
|
+
module Sloprb
|
|
13
|
+
class << self
|
|
14
|
+
attr_accessor :configuration
|
|
15
|
+
|
|
16
|
+
def setup(env = ENV)
|
|
17
|
+
@configuration = Configuration.new(env)
|
|
18
|
+
register_hook
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register_hook
|
|
22
|
+
require "require-hooks/setup"
|
|
23
|
+
RequireHooks.source_transform do |path, source|
|
|
24
|
+
Transformer.call(path, source)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sloprb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vova Dem
|
|
@@ -9,20 +9,48 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: require-hooks
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ruby_llm
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
12
40
|
- !ruby/object:Gem::Dependency
|
|
13
41
|
name: bundler
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
|
15
43
|
requirements:
|
|
16
44
|
- - ">="
|
|
17
45
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
46
|
+
version: '2.6'
|
|
19
47
|
type: :development
|
|
20
48
|
prerelease: false
|
|
21
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
50
|
requirements:
|
|
23
51
|
- - ">="
|
|
24
52
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
53
|
+
version: '2.6'
|
|
26
54
|
- !ruby/object:Gem::Dependency
|
|
27
55
|
name: rake
|
|
28
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -61,7 +89,15 @@ files:
|
|
|
61
89
|
- CHANGELOG.md
|
|
62
90
|
- LICENSE.txt
|
|
63
91
|
- README.md
|
|
92
|
+
- lib/bslop.rb
|
|
93
|
+
- lib/bslop/debug.rb
|
|
64
94
|
- lib/sloprb.rb
|
|
95
|
+
- lib/sloprb/configuration.rb
|
|
96
|
+
- lib/sloprb/generator.rb
|
|
97
|
+
- lib/sloprb/parser.rb
|
|
98
|
+
- lib/sloprb/setup.rb
|
|
99
|
+
- lib/sloprb/transformer.rb
|
|
100
|
+
- lib/sloprb/typoeater.rb
|
|
65
101
|
- lib/sloprb/version.rb
|
|
66
102
|
homepage: https://github.com/palkan/sloprb
|
|
67
103
|
licenses:
|