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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2e30c960afc4038c2b8f08bf3d24d10885fff4337bee37dd9801d7d20c34472
4
- data.tar.gz: e304453e246016cc9103929ac502163c3b00cd3b1c1362cea2948b016a16c6ba
3
+ metadata.gz: eed547e1328af8116c12a43b51f3dc68ddafdc0d1d17bd7736df863c126c6fba
4
+ data.tar.gz: 275a664f737f601f95ad61ae9b73c10a4274ec36f8915dddc555e9ac9e966093
5
5
  SHA512:
6
- metadata.gz: ed9d68924f40f3cfe096c5f6e5588afabdfe4e4aa746ec1b321343021e9cf51b694d59ebe058f84835c680fe375ff59a98d13393847b5c9215b456db78a0c760
7
- data.tar.gz: 1fb2feefe815838c4e9211f7b1b3eb463afd2dc644d19a9a5cafa4a2f9821822c3d251fcef377ca25d37f40d713f5fb3879f2ba82762f817c504bbf7d0071da4
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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Codegen
6
- VERSION = '0.2.1'
6
+ VERSION = '0.2.4'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-codegen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity