@hifullmoon/aicommit 2.6.3 → 2.6.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.4] - 2026-09-19
8
+
9
+ ### Fixed
10
+
11
+ - Large split planning now sizes batches for the model response limit and preserves valid file groupings when the provider omits a group summary.
12
+
7
13
  ## [2.6.3] - 2026-09-15
8
14
 
9
15
  ### Changed
@@ -255,7 +261,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
255
261
  - Added file-level split planning and execution with Git-state concurrency checks.
256
262
  - Added provider presets and user/project configuration boundaries.
257
263
 
258
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.3...HEAD
264
+ [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.4...HEAD
265
+ [2.6.4]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.4
259
266
  [2.6.3]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.3
260
267
  [2.6.2]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.2
261
268
  [2.6.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.1
@@ -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.3
36
+ npm install --package-lock-only @hifullmoon/aicommit@2.6.4
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.3",
3
+ "version": "2.6.4",
4
4
  "description": "Safe, local-first AI commit message generator for Git workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -351,17 +351,17 @@ async function jsonCall(config, instruction, items, validate = null, stream = nu
351
351
  }
352
352
  }
353
353
 
354
- export function validatePartition(groups, ids) {
354
+ export function validatePartition(groups, ids, requireSummary = true) {
355
355
  const remaining = new Set(ids);
356
356
  if (!Array.isArray(groups) || !groups.length)
357
357
  throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, 'Analysis returned an empty partition.');
358
358
  for (const group of groups) {
359
359
  if (
360
+ !group ||
360
361
  !Array.isArray(group.ids) ||
361
362
  !group.ids.length ||
362
- typeof group.summary !== 'string' ||
363
- !group.summary.trim() ||
364
- group.summary.length > 2000
363
+ (requireSummary &&
364
+ (typeof group.summary !== 'string' || !group.summary.trim() || group.summary.length > 2000))
365
365
  )
366
366
  throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, 'Analysis returned an invalid group.');
367
367
  for (const id of group.ids)
@@ -729,15 +729,23 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
729
729
  config.splitMaxDiffChars || 16000,
730
730
  Math.floor(config.analysisBudget.limits.chunkInputTokens * 0.6),
731
731
  );
732
+ // A model may keep every input as a separate commit. Limit candidate count
733
+ // by the response budget too, leaving room for a complete JSON plan.
734
+ const outputTokens = Math.min(2048, config.maxTokens || 1024);
735
+ const maxPlanItems = Math.max(
736
+ 1,
737
+ Math.min(config.splitMaxPlanFiles || 100, Math.floor((outputTokens - 224) / 80)),
738
+ );
739
+ const batchConfig = { ...config, splitMaxPlanFiles: maxPlanItems };
732
740
  for (let level = 0; level < 8; level++) {
733
741
  const byId = new Map(candidates.map((x) => [x.id, x]));
734
742
  const batches =
735
743
  !deep && level === 0
736
- ? localPlanBatches(config, candidates, cap)
744
+ ? localPlanBatches(batchConfig, candidates, cap)
737
745
  : packItems(
738
746
  candidates.map(({ id, path, summary }) => ({ id, path, summary })),
739
747
  cap,
740
- config.splitMaxPlanFiles || 100,
748
+ maxPlanItems,
741
749
  );
742
750
  if (!deep && level === 0 && coverage) {
743
751
  coverage.sampledFiles = batches.flat().filter((item) => item.representativeExcerpt).length;
@@ -769,10 +777,10 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
769
777
  };
770
778
  const groups = await jsonCall(
771
779
  config,
772
- `Each commit message must follow this policy: ${JSON.stringify(policy)}.\nGroup related changes into logical commits, including related implementation and tests across directories. Return [{"ids":[input IDs],"summary":"factual combined change summary","subject":"commit subject","body":"optional commit body"}]. Assign every input ID exactly once. Do not merge unrelated changes just to reduce group count.${!deep && level === 0 ? ' Inputs are a compact local inventory, not full semantic summaries. One ID may represent multiple files and must remain atomic; use kind, module, status, fileCount, examples, and any representativeExcerpt conservatively.' : ''}`,
780
+ `Each commit message must follow this policy: ${JSON.stringify(policy)}.\nGroup related changes into logical commits, including related implementation and tests across directories. Return [{"ids":[input IDs],"summary":"factual combined change summary","subject":"commit subject","body":"optional commit body"}]. Assign every input ID exactly once. Do not merge unrelated changes just to reduce group count.${finalPlan ? ' The summary field is optional in this final plan.' : ''}${!deep && level === 0 ? ' Inputs are a compact local inventory, not full semantic summaries. One ID may represent multiple files and must remain atomic; use kind, module, status, fileCount, examples, and any representativeExcerpt conservatively.' : ''}`,
773
781
  batch,
774
782
  (candidate) => {
775
- validatePartition(candidate, ids);
783
+ validatePartition(candidate, ids, false);
776
784
  if (candidate.some((group) => typeof group.subject !== 'string' || !group.subject.trim()))
777
785
  throw fail(
778
786
  ERROR_CATEGORIES.RESPONSE_FORMAT,
@@ -782,8 +790,23 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
782
790
  batchStream,
783
791
  );
784
792
  for (const group of groups) {
793
+ const sourceSummary = group.ids
794
+ .map((id) => {
795
+ const fact = byId.get(id);
796
+ return (
797
+ fact.summary ||
798
+ `${fact.status || 'Changed'} ${fact.kind || 'files'} in ${fact.module || fact.path || 'repository'} (${fact.files.length} files)`
799
+ ).slice(0, 160);
800
+ })
801
+ .join('; ');
785
802
  next.push({
786
803
  ...group,
804
+ summary:
805
+ typeof group.summary === 'string' &&
806
+ group.summary.trim() &&
807
+ group.summary.length <= 2000
808
+ ? group.summary
809
+ : sourceSummary.slice(0, 2000),
787
810
  id: `L${level}G${next.length}`,
788
811
  files: group.ids.flatMap((id) => byId.get(id).files),
789
812
  });