lex-codegen 0.2.1 → 0.2.4
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 +18 -0
- data/lib/legion/extensions/codegen/runners/from_gap.rb +93 -0
- data/lib/legion/extensions/codegen/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eed547e1328af8116c12a43b51f3dc68ddafdc0d1d17bd7736df863c126c6fba
|
|
4
|
+
data.tar.gz: 275a664f737f601f95ad61ae9b73c10a4274ec36f8915dddc555e9ac9e966093
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f38f7774034f1f24428d4181fa3ef593d648ffdc62932d1eb3cba6c216167678c2441f3e8f6769d8016f4e1aeecaca9dbd27506126b6a62bc339277fced8fd5a
|
|
7
|
+
data.tar.gz: 607c285280a1429d2bcdad8ab3be42279a1bb13b3e5816554a178e65c08b5396633b2c0e5510c3b1b0eab766ef331433afe964adf5a76f71e5ac6abeb77d7e4e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.4] - 2026-03-26
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
- `implement_stub`: added `allowed_stub_path?` guard to constrain `file_path` to the configured output directory, reducing the risk of unintended LLM access to local files
|
|
7
|
+
|
|
8
|
+
## [0.2.3] - 2026-03-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `STUB_IMPLEMENTATION_INSTRUCTIONS`: clarified no-comments rule to explicitly exempt existing comments including `# frozen_string_literal: true`, preventing LLM from inadvertently stripping the frozen-string header
|
|
12
|
+
- `extract_code`: updated all four fence-matching regexes to use `\r?\n` instead of `\n`, correctly handling Windows (`\r\n`) line endings in LLM responses
|
|
13
|
+
|
|
14
|
+
## [0.2.2] - 2026-03-26
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `Runners::FromGap.implement_stub` — takes a stub file path and proposal context, calls LLM to fill in the implementation, returns generated code without writing to disk
|
|
18
|
+
- `STUB_IMPLEMENTATION_INSTRUCTIONS` constant with coding rules for LLM code generation
|
|
19
|
+
- Private helpers: `stub_implementation_prompt`, `extract_code` (markdown fence extraction)
|
|
20
|
+
|
|
3
21
|
## [0.2.1] - 2026-03-26
|
|
4
22
|
|
|
5
23
|
### Changed
|
|
@@ -8,8 +8,26 @@ module Legion
|
|
|
8
8
|
module Codegen
|
|
9
9
|
module Runners
|
|
10
10
|
module FromGap
|
|
11
|
+
include Legion::Logging::Helper
|
|
11
12
|
extend self
|
|
12
13
|
|
|
14
|
+
STUB_IMPLEMENTATION_INSTRUCTIONS = <<~INSTRUCTIONS
|
|
15
|
+
You are a Ruby code generator for LegionIO cognitive extensions.
|
|
16
|
+
You receive a stub Ruby file and a description of the extension's purpose.
|
|
17
|
+
Replace stub method bodies with real implementations.
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- Return ONLY the complete Ruby file content, no markdown fencing, no explanation
|
|
21
|
+
- Keep the exact module/class/method structure and signatures
|
|
22
|
+
- Keep `# frozen_string_literal: true` on line 1
|
|
23
|
+
- Runner methods must return `{ success: true/false, ... }` hashes
|
|
24
|
+
- Use in-memory state only (instance variables, no database, no external APIs)
|
|
25
|
+
- Helper classes may use initialize for state setup
|
|
26
|
+
- Follow Ruby style: 2-space indent, snake_case methods
|
|
27
|
+
- Do not add require statements
|
|
28
|
+
- Do not add new comments unless the logic is non-obvious; keep existing comments (including `# frozen_string_literal: true` on line 1)
|
|
29
|
+
INSTRUCTIONS
|
|
30
|
+
|
|
13
31
|
def generate(gap:)
|
|
14
32
|
tier = Helpers::TierClassifier.classify(gap: gap)
|
|
15
33
|
|
|
@@ -22,6 +40,7 @@ module Legion
|
|
|
22
40
|
{ success: false, reason: :unknown_tier, tier: tier }
|
|
23
41
|
end
|
|
24
42
|
rescue StandardError => e
|
|
43
|
+
log.warn("generate failed: #{e.message}")
|
|
25
44
|
{ success: false, reason: :generation_error, error: e.message }
|
|
26
45
|
end
|
|
27
46
|
|
|
@@ -55,6 +74,7 @@ module Legion
|
|
|
55
74
|
spec_code: spec_code
|
|
56
75
|
}
|
|
57
76
|
rescue StandardError => e
|
|
77
|
+
log.warn("generate_runner_method failed: #{e.message}")
|
|
58
78
|
{ success: false, reason: :generation_failed, error: e.message }
|
|
59
79
|
end
|
|
60
80
|
|
|
@@ -80,15 +100,63 @@ module Legion
|
|
|
80
100
|
extension: name
|
|
81
101
|
}
|
|
82
102
|
rescue StandardError => e
|
|
103
|
+
log.warn("generate_full_extension failed: #{e.message}")
|
|
83
104
|
{ success: false, reason: :scaffold_failed, error: e.message }
|
|
84
105
|
end
|
|
85
106
|
|
|
107
|
+
def implement_stub(file_path:, context:)
|
|
108
|
+
return { success: false, reason: :llm_unavailable } unless llm_available?
|
|
109
|
+
return { success: false, reason: :path_not_allowed } unless allowed_stub_path?(file_path)
|
|
110
|
+
|
|
111
|
+
stub_content = ::File.read(file_path)
|
|
112
|
+
prompt = stub_implementation_prompt(stub_content, context)
|
|
113
|
+
|
|
114
|
+
response = Legion::LLM.chat(
|
|
115
|
+
messages: [
|
|
116
|
+
{ role: 'system', content: STUB_IMPLEMENTATION_INSTRUCTIONS },
|
|
117
|
+
{ role: 'user', content: prompt }
|
|
118
|
+
],
|
|
119
|
+
caller: { source: 'lex-codegen', component: 'from_gap', operation: 'implement_stub' }
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
code = extract_code(response&.content)
|
|
123
|
+
return { success: false, reason: :llm_empty_response } if code.nil? || code.strip.empty?
|
|
124
|
+
|
|
125
|
+
{ success: true, code: code, file_path: file_path }
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
log.warn("implement_stub failed: #{e.message}")
|
|
128
|
+
{ success: false, reason: :generation_error, error: e.message }
|
|
129
|
+
end
|
|
130
|
+
|
|
86
131
|
private
|
|
87
132
|
|
|
88
133
|
def llm_available?
|
|
89
134
|
defined?(Legion::LLM) && Legion::LLM.respond_to?(:chat)
|
|
90
135
|
end
|
|
91
136
|
|
|
137
|
+
def allowed_stub_path?(file_path)
|
|
138
|
+
return false unless file_path&.end_with?('.rb')
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
allowed_root = ::File.realpath(output_dir)
|
|
142
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
143
|
+
log.debug("realpath fallback for output_dir: #{e.message}")
|
|
144
|
+
allowed_root = ::File.expand_path(output_dir)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
begin
|
|
148
|
+
# Disallow direct symlink files to avoid exfiltrating arbitrary targets
|
|
149
|
+
return false if ::File.lstat(file_path).symlink?
|
|
150
|
+
|
|
151
|
+
resolved = ::File.realpath(file_path)
|
|
152
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
153
|
+
log.debug("path resolution failed for #{file_path}: #{e.message}")
|
|
154
|
+
return false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
resolved == allowed_root || resolved.start_with?(allowed_root + ::File::SEPARATOR)
|
|
158
|
+
end
|
|
159
|
+
|
|
92
160
|
def build_runner_prompt(gap)
|
|
93
161
|
<<~PROMPT
|
|
94
162
|
Generate a Ruby module for the LegionIO framework that handles this capability:
|
|
@@ -147,6 +215,31 @@ module Legion
|
|
|
147
215
|
def camelize(name)
|
|
148
216
|
name.split('_').map(&:capitalize).join
|
|
149
217
|
end
|
|
218
|
+
|
|
219
|
+
def stub_implementation_prompt(stub_content, context)
|
|
220
|
+
parts = ['Implement this LegionIO extension file.']
|
|
221
|
+
parts << "Extension: #{context[:name]}"
|
|
222
|
+
parts << "Category: #{context[:category]}"
|
|
223
|
+
parts << "Description: #{context[:description]}"
|
|
224
|
+
parts << "Metaphor: #{context[:metaphor]}" if context[:metaphor]
|
|
225
|
+
parts << ''
|
|
226
|
+
parts << 'Current stub:'
|
|
227
|
+
parts << stub_content
|
|
228
|
+
parts.join("\n")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def extract_code(content)
|
|
232
|
+
return nil if content.nil?
|
|
233
|
+
|
|
234
|
+
code = if content.match?(/```ruby\s*\r?\n/)
|
|
235
|
+
content.match(/```ruby\s*\r?\n(.*?)```/m)&.captures&.first || content
|
|
236
|
+
elsif content.match?(/```\s*\r?\n/)
|
|
237
|
+
content.match(/```\s*\r?\n(.*?)```/m)&.captures&.first || content
|
|
238
|
+
else
|
|
239
|
+
content
|
|
240
|
+
end
|
|
241
|
+
"#{code.strip}\n"
|
|
242
|
+
end
|
|
150
243
|
end
|
|
151
244
|
end
|
|
152
245
|
end
|