@hifullmoon/aicommit 2.6.6 → 2.6.7

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.7] - 2026-10-05
8
+
9
+ ### Changed
10
+
11
+ - Large-change local analysis now selects bounded excerpts across change hunks, prioritizing behavior changes over imports and comments.
12
+
13
+ ### Fixed
14
+
15
+ - Local change summaries now fit the complete commit request input budget, including system policy, repository context, and JSON envelopes.
16
+
7
17
  ## [2.6.6] - 2026-09-21
8
18
 
9
19
  ### Fixed
@@ -273,7 +283,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
273
283
  - Added file-level split planning and execution with Git-state concurrency checks.
274
284
  - Added provider presets and user/project configuration boundaries.
275
285
 
276
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.6...HEAD
286
+ [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.7...HEAD
287
+ [2.6.7]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.7
277
288
  [2.6.6]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.6
278
289
  [2.6.5]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.5
279
290
  [2.6.4]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.4
@@ -33,7 +33,7 @@ The release workflow uses npm Trusted Publishing without a long-lived `NPM_TOKEN
33
33
  ```bash
34
34
  workdir=$(mktemp -d)
35
35
  cd "$workdir"
36
- npm install --package-lock-only @hifullmoon/aicommit@2.6.6
36
+ npm install --package-lock-only @hifullmoon/aicommit@2.6.7
37
37
  npm audit signatures
38
38
  ```
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifullmoon/aicommit",
3
- "version": "2.6.6",
3
+ "version": "2.6.7",
4
4
  "description": "Safe, local-first AI commit message generator for Git workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { isLockFile, matchStripPattern } from './git.js';
3
3
  import { ERROR_CATEGORIES, fail } from './errors.js';
4
+ import { buildCommitMessages } from './api.js';
5
+ import { estimateTokens } from './analysis-budget.js';
4
6
 
5
7
  // These are evidence-selection rules, not exclusions from Git or secret scans.
6
8
  export function isGeneratedFile(path) {
@@ -117,6 +119,16 @@ export function localOverview(config, facts) {
117
119
  samples: [],
118
120
  };
119
121
  const cap = localInputBytes(config);
122
+ const fits = () => {
123
+ const text = JSON.stringify(data);
124
+ return (
125
+ Buffer.byteLength(text) <= cap &&
126
+ estimateTokens(JSON.stringify(buildCommitMessages(config, text).messages)) <=
127
+ (config.analysisBudget?.limits.chunkInputTokens ||
128
+ config.largeChange?.chunkInputTokens ||
129
+ 12000)
130
+ );
131
+ };
120
132
  let sampledFiles = 0;
121
133
  const seen = new Set();
122
134
  const ordered = ranked(facts);
@@ -145,17 +157,18 @@ export function localOverview(config, facts) {
145
157
  if (data.samples.length >= 16) break;
146
158
  const sample = summaryOf(fact);
147
159
  data.samples.push(sample);
148
- if (Buffer.byteLength(JSON.stringify(data)) > cap) {
160
+ data.omittedGroups--;
161
+ if (!fits()) {
149
162
  data.samples.pop();
163
+ data.omittedGroups++;
150
164
  continue;
151
165
  }
152
166
  if (fact.evidence) sampledFiles++;
153
- data.omittedGroups--;
154
167
  }
155
168
  // A tiny configured budget may not even fit aggregate metadata. Fail before
156
169
  // sending rather than silently slicing JSON or dropping the uncertainty notice.
157
170
  const text = JSON.stringify(data);
158
- if (Buffer.byteLength(text) > cap)
171
+ if (!fits())
159
172
  throw fail(
160
173
  ERROR_CATEGORIES.CONFIG,
161
174
  'Local change inventory does not fit the input budget. Increase largeChange.chunkInputTokens or maxDiffChars.',
@@ -172,30 +185,61 @@ export function analyzeLocally(config, capture, protect, previews) {
172
185
  hash: createHash('sha256'),
173
186
  evidence: '',
174
187
  textual: false,
188
+ excerpts: [],
189
+ excerpt: '',
190
+ score: 0,
175
191
  },
176
192
  ]),
177
193
  );
194
+ const finishExcerpt = (record) => {
195
+ if (!record.excerpt) return;
196
+ record.excerpts.push({ text: record.excerpt, score: record.score });
197
+ record.excerpts.sort((a, b) => b.score - a.score);
198
+ record.excerpts.length = Math.min(record.excerpts.length, 8);
199
+ record.excerpt = '';
200
+ record.score = 0;
201
+ };
178
202
  for (const unit of capture.units(protect, previews)) {
179
203
  if (unit.metadataOnly) continue;
180
204
  const record = records.get(unit.fileId);
181
205
  // Tracked fragments carry Git headers; untracked fragments are plain text.
182
206
  const section = unit.id.match(/S\d+(?=P\d+$)/)?.[0];
183
207
  if (section && section !== record.section) {
208
+ finishExcerpt(record);
184
209
  record.section = section;
185
210
  record.inHunk = false;
186
211
  }
187
212
  for (const line of unit.text.split(/(?<=\n)/)) {
188
213
  if (section) {
189
- if (line.startsWith('@@')) record.inHunk = true;
214
+ if (line.startsWith('@@')) {
215
+ finishExcerpt(record);
216
+ record.inHunk = true;
217
+ }
190
218
  if (!record.inHunk || !/^[+-]/.test(line)) continue;
191
219
  }
192
220
  record.hash.update(line);
193
221
  record.textual = true;
194
- const remaining = 900 - Buffer.byteLength(record.evidence);
195
- if (remaining > 3) record.evidence += clip(line, remaining);
222
+ // Keep bounded candidates across hunks instead of spending the entire
223
+ // evidence allowance on imports at the beginning of a file.
224
+ const remaining = 450 - Buffer.byteLength(record.excerpt);
225
+ if (remaining > 3) {
226
+ record.excerpt += clip(line, remaining);
227
+ record.score = Math.max(
228
+ record.score,
229
+ /^[+-]\s*(?:import\b|export\s+\{|\/\/|\*|$)/.test(line) ? 1 : 4,
230
+ );
231
+ }
232
+ if (Buffer.byteLength(record.excerpt) >= 447) finishExcerpt(record);
196
233
  }
197
234
  }
198
235
  const grouped = new Map();
236
+ for (const record of records.values()) {
237
+ finishExcerpt(record);
238
+ record.evidence = record.excerpts
239
+ .slice(0, 3)
240
+ .map((excerpt) => clip(excerpt.text, 300))
241
+ .join('');
242
+ }
199
243
  for (const { file, hash, evidence, textual } of records.values()) {
200
244
  const kind = category(file, config);
201
245
  const module = moduleOf(file.addPaths?.at(-1) || file.path);