llmed 0.4.4 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48b4c352f5fd54aa841fe411fd7f5025f1c98f40b8a287589aeb46ad213def68
4
- data.tar.gz: c53e2efec6164261d6985f9297b101b363d1e9da432123ddeac1e369a7f045d2
3
+ metadata.gz: c47ebb44059bae3a1ffc2d27dd064098036ebd390cb677a488761845e13d4b6b
4
+ data.tar.gz: 1442dec8d3c594b8610af95d7edfbdbdee8881ac00a046001cc6fd4eb9c2e68c
5
5
  SHA512:
6
- metadata.gz: 1b228350f691be1762449160bafde3f3c3eba0cc180fd918207f09a855939d0ec784f53861f72304a61d6c4c2adfd525d4e29cf868c1bc1d1dbfb9aa0ff7ad15
7
- data.tar.gz: 7eea2cc52879f589c1b4612409c7c0923aa88135bcb705817966fb4827d449032cc528891140e37a088ff0145fe792982f068162b716785d41e5c3ac736a8bfd
6
+ metadata.gz: 80a7c93eed1106305b848bae99b0fcce744c55bc933538465041e19329ed1e6fe01f398c38ec849f2ae4981ec260024d3a9ded96f1465224a0b9f60cc8924b53
7
+ data.tar.gz: 7c3df54bec3ee2e2f02bd7108c92004e6969bb8019ab5209bda32327cc916492ac2946948b2ce835325b93d33238292187870e68bc35bfa50feefb0b38164263
@@ -1,6 +1,8 @@
1
1
  # Copyright 2025 Jovany Leandro G.C <bit4bit@riseup.net>
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'set'
5
+
4
6
  class LLMed
5
7
  class Application
6
8
  attr_reader :contexts, :name, :language
@@ -29,6 +31,8 @@ class LLMed
29
31
  end
30
32
 
31
33
  def initialize(name:, language:, output_file:, block:, logger:, release:, release_dir:, output_dir:)
34
+ snapshot_file = Pathname.new(release_dir) + "#{output_file}.snapshot"
35
+
32
36
  @name = name
33
37
  @output_file = output_file
34
38
  @language = language.to_sym
@@ -39,6 +43,7 @@ class LLMed
39
43
  @release = release
40
44
  @release_dir = release_dir
41
45
  @output_dir = output_dir
46
+ @snapshot = Snapshot.new(snapshot_file)
42
47
  end
43
48
 
44
49
  # Example:
@@ -56,7 +61,113 @@ class LLMed
56
61
  instance_eval(&@block)
57
62
  end
58
63
 
59
- def prepare
64
+ class Snapshot
65
+ attr_reader :snapshot_file
66
+
67
+ def initialize(snapshot_file)
68
+ @snapshot_file = snapshot_file
69
+ @contexts = []
70
+ end
71
+
72
+ def sync(default)
73
+ load(default)
74
+ dump
75
+ end
76
+
77
+ def refresh(contexts)
78
+ @contexts = contexts.map{ |ctx| [ctx.name, {'name' => ctx.name, 'message' => ctx.raw}]}.to_h
79
+ dump
80
+ end
81
+
82
+ def diff(other_contexts)
83
+ diffs = {}
84
+ other_contexts.each do |other_ctx|
85
+ current_ctx = @contexts[other_ctx.name]
86
+ result = line_diff(current_ctx['message'], other_ctx.raw)
87
+ # omit not changes
88
+ if !result.all?{|op, line| op == '=:'}
89
+ diffs[other_ctx.name] = result
90
+ end
91
+ end
92
+
93
+ diffs
94
+ end
95
+
96
+ private
97
+
98
+ def line_diff(text1, text2)
99
+ lines1 = text1.split("\n")
100
+ lines2 = text2.split("\n")
101
+
102
+ result = []
103
+
104
+ i1 = 0
105
+ i2 = 0
106
+
107
+ while i1 < lines1.size || i2 < lines2.size
108
+ line1 = lines1[i1]
109
+ line2 = lines2[i2]
110
+
111
+ if i1 < lines1.size && i2 < lines2.size && line1 == line2
112
+ result << ["=:", line1]
113
+ i1 += 1
114
+ i2 += 1
115
+ elsif i1 < lines1.size && (i2 >= lines2.size || !lines2[i2..-1].include?(line1))
116
+ result << ["-:", line1]
117
+ i1 += 1
118
+ elsif i2 < lines2.size && (i1 >= lines1.size || !lines1[i1..-1].include?(line2))
119
+ result << ["+:", line2]
120
+ i2 += 1
121
+ else
122
+ # Try to find if one of the lines matches later
123
+ idx1 = lines1[i1+1..-1]&.index(line2)
124
+ idx2 = lines2[i2+1..-1]&.index(line1)
125
+
126
+ if !idx1.nil? && (idx2.nil? || idx1 <= idx2)
127
+ result << ["-:", line1]
128
+ i1 += 1
129
+ elsif !idx2.nil?
130
+ result << ["+:", line2]
131
+ i2 += 1
132
+ else
133
+ # Lines differ, treat both as deleted and added
134
+ result << ["-:", line1]
135
+ result << ["+:", line2]
136
+ i1 += 1
137
+ i2 += 1
138
+ end
139
+ end
140
+ end
141
+
142
+ result
143
+ end
144
+
145
+ def load(default)
146
+ if File.exist?(@snapshot_file)
147
+ File.open(@snapshot_file, 'r') do |f|
148
+ @contexts = JSON.load(f.read)['contexts']
149
+ end
150
+ else
151
+ @contexts = default.map{ |ctx| [ctx.name, {'name' => ctx.name, 'message' => ctx.raw}]}.to_h
152
+ end
153
+ end
154
+
155
+ def dump
156
+ File.open(@snapshot_file, 'w') do |file|
157
+ file.write(JSON.dump({'contexts' => @contexts}))
158
+ end
159
+ end
160
+ end
161
+
162
+ def prepare_snapshot
163
+ raise "snapshot preparation require contexts" if @contexts.empty?
164
+
165
+ @logger.info("APPLICATION #{@name} PREPARING SNAPSHOT #{@snapshot.snapshot_file}")
166
+
167
+ @snapshot.sync(@contexts)
168
+ end
169
+
170
+ def prepare_release
60
171
  @logger.info("APPLICATION #{@name} COMPILING FOR #{@language} RELEASE #{@release}")
61
172
  return unless @output_file.is_a?(String)
62
173
  return unless @release
@@ -64,9 +175,7 @@ class LLMed
64
175
  output_file = Pathname.new(@output_dir) + @output_file
65
176
 
66
177
  if @release && File.exist?(output_file) && !File.exist?(release_source_code)
67
- FileUtils.cp(output_file, release_source_code)
68
- FileUtils.cp(output_file, release_main_source_code)
69
- @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
178
+
70
179
  elsif @release && !File.exist?(output_file) && File.exist?(release_main_source_code)
71
180
  FileUtils.mkdir_p(File.dirname(output_file))
72
181
  FileUtils.cp(release_main_source_code, output_file)
@@ -117,14 +226,35 @@ class LLMed
117
226
  output_file(@output_dir) do |file|
118
227
  file.write(output_content)
119
228
  end
229
+
230
+ # only update snapshot if changes are made
231
+ if !File.exist?(release_source_code)
232
+ @snapshot.refresh(@contexts)
233
+ @logger.info("APPLICATION #{@name} SNAPSHOT REFRESHED")
234
+ end
235
+
236
+ output_file = Pathname.new(@output_dir) + @output_file
237
+ FileUtils.cp(output_file, release_source_code)
238
+ FileUtils.cp(output_file, release_main_source_code)
239
+ @logger.info("APPLICATION #{@name} RELEASE FILE #{release_source_code}")
120
240
  end
121
241
 
122
242
  def system_prompt(configuration)
243
+ contexts_diffs = @snapshot.diff(contexts)
244
+ changes_of_contexts = ''
245
+ if contexts_diffs.any?
246
+ contexts_diffs.each do |context_name, diffs|
247
+ changes_of_contexts += "# Context: #{context_name}\n"
248
+ changes_of_contexts += diffs.map { |op, line| "#{op} #{line}" }.join("\n")
249
+ end
250
+ end
251
+
123
252
  configuration.prompt(language: language,
124
253
  source_code: source_code,
125
254
  code_comment_begin: @code_comment.begin,
126
255
  code_comment_end: @code_comment.end,
127
- update_context_digests: digests_of_context_to_update)
256
+ update_context_digests: digests_of_context_to_update,
257
+ changes_of_contexts: changes_of_contexts)
128
258
  end
129
259
 
130
260
  def rebuild?
@@ -7,36 +7,50 @@ class LLMed
7
7
  @logger = logger
8
8
  # Manual tested, pass 5 times execution
9
9
  @prompt = LLMed::LLM::Template.build(template: "
10
- You are a software developer with knowledge only of the programming language {language}, following the SOLID principles strictly, you always use only imperative and functional programming, design highly isolated components.
11
- Don't make any assumptions/expectations or wait for implementations, always implement the necessary.
12
- The contexts are declarations of how the source code will be (not a file) ensure to follow this always.
13
- The contexts are connected as a flat linked list.
14
- All the contexts represent one source code.
15
- Always exists one-to-one correspondence between context and source code.
16
- Always include the properly escaped comment: LLMED-COMPILED.
10
+ You are a software developer specialized in the programming language {language}. Follow SOLID principles strictly. Use only imperative and functional programming styles and design highly isolated components. You have full access to the standard library and third-party packages only if explicitly allowed.
11
+ Hard requirements: produce complete, executable, and compilable source code — no placeholders, no pseudo-código, no partial implementations, no explanations. If anything below cannot be satisfied, produce a runtime error implementation that clearly fails fast.
17
12
 
18
- You must only modify the following source code:
19
- ```{language}
20
- {source_code}
21
- ```
13
+ The input contexts are functional sections of a single large source file (not separate files). Contexts are linked as a flat linked list. There must be a one-to-one correspondence between each context and the code generated for that context. You must only generate source code for the context(s) whose digest is listed in {update_context_digests}.
22
14
 
23
- Only generate source code of the context who digest belongs to {update_context_digests}.
15
+ Always include the escaped literal comment token LLMED-COMPILED somewhere in the generated code.
24
16
 
25
- Wrap with comment every code that belongs to the indicated context, example in {language}:
26
- {code_comment_begin}<llmed-code context='here context name' digest='....' link-digest='next digest' after='here same value of attribute link-digest'>{code_comment_end}
27
- ...
28
- {code_comment_begin}</llmed-code>{code_comment_end}
17
+ You must only modify the following source code that is provided between the code fences:
18
+ ```{language}
19
+ {source_code}
20
+ ```
29
21
 
30
- !!Your response must contain only the generated source code with all indicated contexts, with no additional text or comments, and you must ensure that runs correctly on the first attempt.
31
- ", input_variables: %w[language source_code code_comment_begin code_comment_end update_context_digests])
22
+ Strict formatting for context wrappers: wrap every context implementation with the exact comment markers below. Use the literal placeholders {code_comment_begin} and {code_comment_end} replaced with the exact comment open/close strings for the target language. Example wrapper (replace placeholders when running the prompt):
23
+ {code_comment_begin}<llmed-code context='context name' digest='CURRENT_DIGEST' link-digest='NEXT_DIGEST' after='NEXT_DIGEST'>{code_comment_end}
24
+ ... COMPLETE, RUNNABLE implementation for that context ...
25
+ {code_comment_begin}</llmed-code>{code_comment_end}
26
+
27
+ Behavioral rules (must be obeyed):
28
+ 1. No comments-only outputs. If your natural answer would be comments, instead implement executable code that performs the described behavior. Do not output explanatory text outside code blocks — output only source code for the indicated contexts.
29
+ 2. All functions/methods must have bodies implementing the intended behavior. If external information is missing, implement a reasonable, deterministic default rather than leaving a stub.
30
+ 3. Fail-fast fallback: if the requested context genuinely cannot be implemented, include a clear runtime failure function implementation_impossible() that raises/prints a single machine-readable error (e.g. throws an exception with message IMPLEMENTATION-IMPOSSIBLE) and still compiles.
31
+ 4. One-to-one mapping: produce exactly one code block per digest requested. Do not add unrelated helper contexts unless they are wrapped and linked to an indicated digest; if helpers are necessary, include them inside the same context wrapper.
32
+ 5. Include the literal LLMED-COMPILED comment somewhere inside the code.
33
+ 6. Do not output any text outside the source code. The assistant response must be only source code for the requested context(s).
34
+
35
+ All behavior described by contexts marked with '-:' in <changes> must be completely removed from the generated source code.
36
+ Do not leave any code, print statements, functions, or references implementing deleted contexts.
37
+ Each context listed in '+:' or '=:' must be implemented exactly according to its description.
38
+ Behavior from removed contexts must not appear anywhere in the output, even indirectly.
39
+ <changes>
40
+ {changes_of_contexts}
41
+ </changes>
42
+
43
+ Output requirement: your response must contain only the generated source code for the indicated context(s), with the required wrapper comments and the test harness; nothing else.
44
+ ", input_variables: %w[language source_code code_comment_begin code_comment_end update_context_digests changes_of_contexts])
32
45
  end
33
46
 
34
- def prompt(language:, source_code:, code_comment_begin:, code_comment_end:, update_context_digests: [])
47
+ def prompt(language:, source_code:, code_comment_begin:, code_comment_end:, update_context_digests: [], changes_of_contexts: '')
35
48
  @prompt.format(language: language,
36
49
  source_code: source_code,
37
50
  code_comment_begin: code_comment_begin,
38
51
  code_comment_end: code_comment_end,
39
- update_context_digests: update_context_digests.join(','))
52
+ update_context_digests: update_context_digests.join(','),
53
+ changes_of_contexts: changes_of_contexts)
40
54
  end
41
55
 
42
56
  # Change the default prompt, input variables: language, source_code
data/lib/llmed/context.rb CHANGED
@@ -67,6 +67,10 @@ class LLMed
67
67
  "# Context: \"#{@name}\" Digest: #{digest}\n\n#{@message}"
68
68
  end
69
69
 
70
+ def raw
71
+ @message
72
+ end
73
+
70
74
  def llm(message)
71
75
  @message = message
72
76
  end
data/lib/llmed/release.rb CHANGED
@@ -120,13 +120,19 @@ class LLMed
120
120
  else
121
121
  user_contexts_iter = user_contexts.dup
122
122
  contexts_iter = contexts.dup
123
- rewire_code_contexts(contexts_iter, user_contexts_iter)
124
- contexts_sorted = contexts_iter
123
+ order_digests = rewire_code_contexts(contexts_iter, user_contexts_iter)
124
+
125
+ contexts_on_digests = order_digests.map { |digest| contexts_iter.find { |ctx| ctx.digest == digest } }
126
+ contexts_missing_digests = contexts_iter.select { |ctx| !order_digests.include?(ctx.digest) }
127
+ contexts_sorted = contexts_on_digests + contexts_missing_digests
125
128
  end
126
129
 
130
+ # Sort contexts so that the latest digest (the one whose 'after' is empty) comes last
127
131
  @contexts = contexts_sorted.sort do |a, b|
128
- if a.digest == b.after
132
+ if a.after.empty? && !b.after.empty?
129
133
  1
134
+ elsif !a.after.empty? && b.after.empty?
135
+ -1
130
136
  else
131
137
  0
132
138
  end
@@ -138,13 +144,18 @@ class LLMed
138
144
  private
139
145
 
140
146
  def rewire_code_contexts(code_contexts, user_contexts)
147
+ order_digests = []
141
148
  user_contexts.each_with_next do |user_context, next_user_context|
142
- ctx = code_contexts.find { |ctx| ctx.digest == user_context.digest }
149
+ ctx = code_contexts.find { |ctx| ctx.name == user_context.name }
143
150
  if ctx
151
+ ctx.digest = user_context.digest
152
+ order_digests << user_context.digest
144
153
  ctx.after = '' if user_contexts.count > 1
145
154
  ctx.after = next_user_context.digest if next_user_context
146
155
  end
147
156
  end
157
+
158
+ order_digests
148
159
  end
149
160
 
150
161
  def initialize(origin, code_comment)
data/lib/llmed.rb CHANGED
@@ -66,10 +66,12 @@ class LLMed
66
66
  def compile_application(app)
67
67
  app.notify('COMPILE START')
68
68
 
69
- app.prepare
69
+ app.prepare_release
70
70
  app.evaluate
71
+ app.prepare_snapshot
71
72
  if app.rebuild?
72
73
  llm = @configuration.llm
74
+
73
75
  messages = [LLMed::LLM::Message::System.new(app.system_prompt(@configuration))]
74
76
  app.contexts.each do |ctx|
75
77
  next if ctx.skip?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jovany Leandro G.C