jeeves-git-commit 3.0.0 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9dfc1f28648d8afaf7035592aff4dc5cc2bacda8bb2ae760caa04df038fc2359
4
- data.tar.gz: 8d9f768afab1305f0ded8f14192af519f2d0c45cd9bcb7062c876ea113599488
3
+ metadata.gz: c677877fa585dc14ffd287c273bd0a79c82b4592c0f84c5e532c2171b576d061
4
+ data.tar.gz: fcfcd962506dc36115358ae8e5b20afe7273e1a8f531101d4b9c92442bc24b4c
5
5
  SHA512:
6
- metadata.gz: f7bb5569cd7f8b204660bebc3c69f2cf25c96b120b3556a924d6f8bbfe16319a6acaa37550e98d9130966eefb420f1367e4942b1fe4325eb7c1970697c00f4dc
7
- data.tar.gz: c1556ab3fa0eac2a91fd73579f4061a1968a6eae4134b875cff5f165ffb2ce0de5f9aa092106708909acfbccdc379f6d1d86d6c03222ba433ed99684370ac650
6
+ metadata.gz: 0773d6fa8d0504583fbca079ffcb0a418c0de29b09a8ae799b5ac42b0e917fbba3d7f06b2b1a78ab69ae1255a5f36aa118dccdcc06fab41edb10ad452a35aacf
7
+ data.tar.gz: 05a7466354d8ff50a486d4c3bf4487a68a746fdce0c64209133ccb2f52e30a85a4ddf5637f016393293d2fc40e3137c685ab915559f383f1bb6f017f03b964cb
data/README.md CHANGED
@@ -95,6 +95,11 @@ Git and generation failures stop the command with a nonzero exit status.
95
95
  Jeeves checks that staged changes and HEAD still match what it reviewed before
96
96
  committing. See [architecture and Git behavior](docs/architecture.md).
97
97
 
98
+ Local generation defaults to a 65,536-token context. Oversized diffs are
99
+ automatically shortened, with a warning and omission notices for the model.
100
+ Only the model input is shortened; Git commits the complete staged changes.
101
+ See [input limits](docs/configuration.md#output-checks-and-large-diffs).
102
+
98
103
  ## Develop
99
104
 
100
105
  ```sh
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jeeves
4
+ class Diff
5
+ NOTICE = "[Jeeves: diff shortened. Excerpts may omit files, hunks, or parts of lines. " \
6
+ "Hunk ranges are from the original diff; omitted content is unknown.]\n"
7
+ OMITTED = "\n[... content omitted ...]\n"
8
+
9
+ def initialize(text)
10
+ @text = text
11
+ end
12
+
13
+ def fit(limit)
14
+ return @text if @text.bytesize <= limit
15
+
16
+ budget = limit - NOTICE.bytesize
17
+ if budget < OMITTED.bytesize + 8
18
+ raise Error, 'Too little room for a shortened diff. Shorten the prompt or increase ' \
19
+ 'GIT_COMMIT_LOCAL_CONTEXT and GIT_COMMIT_MAX_DIFF_BYTES.'
20
+ end
21
+ compact = without_context
22
+ return NOTICE + compact if compact.bytesize <= budget
23
+
24
+ files = compact.split(/(?=^diff --(?:git |cc |combined ))/)
25
+ NOTICE + fit_parts(files, budget) { |file, size| fit_file(file, size) }
26
+ end
27
+
28
+ private
29
+
30
+ def without_context
31
+ context_prefix = nil
32
+ @text.each_line.reject do |line|
33
+ context_prefix = nil if line.start_with?('diff --')
34
+ match = line.match(/\A(@{2,}) /)
35
+ context_prefix = ' ' * (match[1].length - 1) if match
36
+ context_prefix && line.start_with?(context_prefix)
37
+ end.join
38
+ end
39
+
40
+ def fit_file(file, budget)
41
+ return file if file.bytesize <= budget
42
+
43
+ header = header_for(file)
44
+ body = file.delete_prefix(header)
45
+ hunks = body.split(/(?=^@@)/)
46
+ header + fit_parts(hunks, budget - header.bytesize) do |hunk, size|
47
+ hunk_header = header_for(hunk)
48
+ hunk_header + excerpt(hunk.delete_prefix(hunk_header), size - hunk_header.bytesize)
49
+ end
50
+ end
51
+
52
+ def header_for(part)
53
+ lines = part.lines
54
+ return lines.first.to_s if lines.first&.start_with?('@@')
55
+
56
+ lines.take_while { |line| !line.start_with?('@@') }.join
57
+ end
58
+
59
+ def fit_parts(parts, budget)
60
+ return parts.join if parts.sum(&:bytesize) <= budget
61
+
62
+ # Keep file metadata and hunk headers before sharing space for changes.
63
+ minimums = parts.map { |part| [part.bytesize, header_for(part).bytesize + OMITTED.bytesize + 8].min }
64
+ return excerpt(parts.join, budget) if minimums.sum > budget
65
+
66
+ sizes = minimums.dup
67
+ remaining = budget - sizes.sum
68
+ order = parts.each_index.sort_by { |index| parts[index].bytesize - sizes[index] }
69
+ order.each_with_index do |index, position|
70
+ extra = [parts[index].bytesize - sizes[index], remaining / (order.length - position)].min
71
+ sizes[index] += extra
72
+ remaining -= extra
73
+ end
74
+ parts.each_with_index.map { |part, index| yield part, sizes[index] }.join
75
+ end
76
+
77
+ def excerpt(text, budget)
78
+ return text if text.bytesize <= budget
79
+
80
+ available = budget - OMITTED.bytesize
81
+ head_size = (available + 1) / 2
82
+ tail_size = available / 2
83
+ head = text.byteslice(0, head_size).scrub('')
84
+ tail = text.byteslice(-tail_size, tail_size).scrub('')
85
+ head = head[0..head.rindex("\n")] if head.include?("\n")
86
+ tail = tail[(tail.index("\n") + 1)..] if tail.include?("\n")
87
+ head + OMITTED + tail
88
+ end
89
+ end
90
+ end
data/lib/jeeves/prompt.rb CHANGED
@@ -10,7 +10,7 @@ module Jeeves
10
10
  @errors = errors
11
11
  end
12
12
 
13
- def render(diff)
13
+ def render(diff, settings:)
14
14
  path = @repository && File.join(@repository, '.jeeves_prompt')
15
15
  unless path && File.file?(path)
16
16
  path = @global_path
@@ -19,7 +19,21 @@ module Jeeves
19
19
  template = File.read(path, encoding: 'UTF-8')
20
20
  raise Error, "Prompt must contain {{DIFF}}: #{path}" unless template.include?('{{DIFF}}')
21
21
 
22
- template.gsub('{{DIFF}}') { diff }
22
+ limit = settings.max_diff_bytes
23
+ if settings.prompt_budget
24
+ overhead = template.gsub('{{DIFF}}', '').bytesize
25
+ available = (settings.prompt_budget - overhead) / template.scan('{{DIFF}}').length
26
+ if available < 1
27
+ raise Error, 'The prompt leaves no room for a diff. Shorten the prompt or increase GIT_COMMIT_LOCAL_CONTEXT.'
28
+ end
29
+ limit = [limit, available].min
30
+ end
31
+ shortened = Diff.new(diff).fit(limit)
32
+ if shortened != diff
33
+ @errors.puts "Warning: diff shortened from #{diff.bytesize} to #{shortened.bytesize} bytes. " \
34
+ 'Some content is omitted; the commit message may miss changes.'
35
+ end
36
+ template.gsub('{{DIFF}}') { shortened }
23
37
  end
24
38
 
25
39
  private
@@ -80,9 +80,7 @@ module Jeeves
80
80
  end
81
81
 
82
82
  def generate(prompt)
83
- # One byte per token is a conservative bound without a model-specific tokenizer.
84
- budget = @settings.context - 1000 - 256
85
- if prompt.bytesize > budget
83
+ if prompt.bytesize > @settings.prompt_budget
86
84
  raise Error, 'Prompt exceeds the conservative local context budget. Split the changes or increase GIT_COMMIT_LOCAL_CONTEXT.'
87
85
  end
88
86
  body = {
@@ -16,7 +16,7 @@ module Jeeves
16
16
  end
17
17
  raise Error, 'The configured model must not be empty' if @model.strip.empty?
18
18
 
19
- @context = integer('GIT_COMMIT_LOCAL_CONTEXT', 32_768) if @provider == 'ollama'
19
+ @context = integer('GIT_COMMIT_LOCAL_CONTEXT', 65_536) if @provider == 'ollama'
20
20
  @max_diff_bytes = integer('GIT_COMMIT_MAX_DIFF_BYTES', 65_536)
21
21
  @message_format = env.fetch('GIT_COMMIT_MESSAGE_FORMAT', 'conventional')
22
22
  return if %w[conventional plain].include?(@message_format)
@@ -27,9 +27,11 @@ module Jeeves
27
27
  def check_diff!(diff)
28
28
  raise Error, 'Diff must be valid UTF-8 text.' unless diff.valid_encoding?
29
29
  raise Error, 'No diff content provided.' if diff.strip.empty?
30
- return if diff.bytesize <= max_diff_bytes
30
+ end
31
31
 
32
- raise Error, "Diff is too large (#{diff.bytesize} bytes; limit #{max_diff_bytes}). Split the changes or increase GIT_COMMIT_MAX_DIFF_BYTES."
32
+ def prompt_budget
33
+ # One byte per token, with room for output and chat framing.
34
+ context - 1000 - 256 if context
33
35
  end
34
36
 
35
37
  private
@@ -1,3 +1,3 @@
1
1
  module Jeeves
2
- VERSION = '3.0.0'
2
+ VERSION = '3.1.0'
3
3
  end
data/lib/jeeves.rb CHANGED
@@ -10,6 +10,7 @@ module Jeeves
10
10
  end
11
11
 
12
12
  require 'jeeves/settings'
13
+ require 'jeeves/diff'
13
14
  require 'jeeves/prompt'
14
15
  require 'jeeves/message'
15
16
  require 'jeeves/providers'
@@ -38,7 +39,7 @@ module Jeeves
38
39
  provider = settings.provider == 'ollama' ? Providers::Ollama.new(settings) : Providers::OpenRouter.new(settings)
39
40
  piped = !@input.tty? && !@input.closed?
40
41
  if piped
41
- diff = (@input.read(settings.max_diff_bytes + 1) || +'').force_encoding(Encoding::UTF_8)
42
+ diff = (@input.read || +'').force_encoding(Encoding::UTF_8)
42
43
  else
43
44
  raise Error, 'Not inside a Git working tree.' unless @git.root
44
45
 
@@ -47,7 +48,8 @@ module Jeeves
47
48
  diff = options[:all] && options[:dry_run] ? @git.preview_all : @git.diff
48
49
  end
49
50
  settings.check_diff!(diff)
50
- prompt = Prompt.new(home: @env.fetch('HOME') { Dir.home }, repository: @git.root, errors: @errors).render(diff)
51
+ prompt = Prompt.new(home: @env.fetch('HOME') { Dir.home }, repository: @git.root, errors: @errors)
52
+ .render(diff, settings: settings)
51
53
  @errors.puts "Using #{settings.provider}: #{settings.model}" unless piped
52
54
  message = Message.prepare(provider.generate(prompt), format: settings.message_format)
53
55
  return show(message) if piped || options[:dry_run]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jeeves-git-commit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Bishop
@@ -79,6 +79,7 @@ files:
79
79
  - bin/jeeves
80
80
  - config/prompt
81
81
  - lib/jeeves.rb
82
+ - lib/jeeves/diff.rb
82
83
  - lib/jeeves/git_repository.rb
83
84
  - lib/jeeves/message.rb
84
85
  - lib/jeeves/prompt.rb