@hifullmoon/aicommit 2.6.3 → 2.6.5
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 +15 -1
- package/docs/distribution.md +1 -1
- package/package.json +1 -1
- package/src/api.js +3 -2
- package/src/change-analysis.js +62 -17
- package/src/split.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.6.5] - 2026-09-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Large split plans now validate commit messages during planning and automatically retry with specific policy errors, including missing or empty subjects. Planning failures are labeled accurately instead of being reported as API failures.
|
|
12
|
+
|
|
13
|
+
## [2.6.4] - 2026-09-19
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Large split planning now sizes batches for the model response limit and preserves valid file groupings when the provider omits a group summary.
|
|
18
|
+
|
|
7
19
|
## [2.6.3] - 2026-09-15
|
|
8
20
|
|
|
9
21
|
### Changed
|
|
@@ -255,7 +267,9 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
|
|
|
255
267
|
- Added file-level split planning and execution with Git-state concurrency checks.
|
|
256
268
|
- Added provider presets and user/project configuration boundaries.
|
|
257
269
|
|
|
258
|
-
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.
|
|
270
|
+
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.5...HEAD
|
|
271
|
+
[2.6.5]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.5
|
|
272
|
+
[2.6.4]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.4
|
|
259
273
|
[2.6.3]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.3
|
|
260
274
|
[2.6.2]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.2
|
|
261
275
|
[2.6.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.1
|
package/docs/distribution.md
CHANGED
|
@@ -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.
|
|
36
|
+
npm install --package-lock-only @hifullmoon/aicommit@2.6.5
|
|
37
37
|
npm audit signatures
|
|
38
38
|
```
|
|
39
39
|
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -207,6 +207,7 @@ export async function getResponseText(
|
|
|
207
207
|
: reasoning || '';
|
|
208
208
|
|
|
209
209
|
const partial = text.trim();
|
|
210
|
+
const followUp = typeof followUpPrompt === 'function' ? followUpPrompt() : followUpPrompt;
|
|
210
211
|
const recoveryPrompt =
|
|
211
212
|
truncatedByLimit || invalidResponse
|
|
212
213
|
? `The previous response was ${
|
|
@@ -214,8 +215,8 @@ export async function getResponseText(
|
|
|
214
215
|
}. ` +
|
|
215
216
|
'Reproduce the COMPLETE answer from the beginning; do not continue from the cut-off point. ' +
|
|
216
217
|
'Keep the answer concise.\n\n' +
|
|
217
|
-
|
|
218
|
-
:
|
|
218
|
+
followUp
|
|
219
|
+
: followUp;
|
|
219
220
|
|
|
220
221
|
// With reasoning, its conclusion plus the partial answer is enough to
|
|
221
222
|
// reconstruct the output without paying to send the original diff again.
|
package/src/change-analysis.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createAnalysisBudget, DEFAULT_LARGE_CHANGE, estimateTokens } from './an
|
|
|
4
4
|
import { getResponseText } from './api.js';
|
|
5
5
|
import { encodeUntrustedData } from './trust.js';
|
|
6
6
|
import { ERROR_CATEGORIES, fail } from './errors.js';
|
|
7
|
-
import { normalizeCommitPolicy } from './policy.js';
|
|
7
|
+
import { normalizeCommitPolicy, validateCommitCandidate } from './policy.js';
|
|
8
8
|
import { getProviderAdapter } from './providers.js';
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
10
|
import {
|
|
@@ -315,6 +315,7 @@ function parseAnalysisJson(raw) {
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
async function jsonCall(config, instruction, items, validate = null, stream = null) {
|
|
318
|
+
let validationError = '';
|
|
318
319
|
const parseAndValidate = (raw) => {
|
|
319
320
|
const parsed = parseAnalysisJson(raw);
|
|
320
321
|
validate?.(parsed);
|
|
@@ -328,14 +329,17 @@ async function jsonCall(config, instruction, items, validate = null, stream = nu
|
|
|
328
329
|
],
|
|
329
330
|
0,
|
|
330
331
|
Math.min(2048, config.maxTokens || 1024),
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
() =>
|
|
333
|
+
'Return the complete requested JSON array, preserving every required input ID exactly once. ' +
|
|
334
|
+
`Required IDs: ${JSON.stringify(items.map((item) => item.id))}. ` +
|
|
335
|
+
(validationError ? `Correct these validation errors: ${validationError}` : ''),
|
|
333
336
|
stream,
|
|
334
337
|
(response) => {
|
|
335
338
|
try {
|
|
336
339
|
parseAndValidate(response);
|
|
337
340
|
return true;
|
|
338
|
-
} catch {
|
|
341
|
+
} catch (error) {
|
|
342
|
+
validationError = error.message;
|
|
339
343
|
return false;
|
|
340
344
|
}
|
|
341
345
|
},
|
|
@@ -351,17 +355,39 @@ async function jsonCall(config, instruction, items, validate = null, stream = nu
|
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
export function
|
|
358
|
+
export function validatePlanMessages(groups, policy) {
|
|
359
|
+
const errors = [];
|
|
360
|
+
for (const [index, group] of groups.entries()) {
|
|
361
|
+
const subject = typeof group.subject === 'string' ? group.subject.trim() : '';
|
|
362
|
+
if (!subject) {
|
|
363
|
+
errors.push(`Group ${index + 1}: The subject field must be a non-empty commit header.`);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const body = typeof group.body === 'string' ? group.body.trim() : '';
|
|
367
|
+
const validation = validateCommitCandidate(body ? `${subject}\n\n${body}` : subject, {
|
|
368
|
+
policy,
|
|
369
|
+
});
|
|
370
|
+
if (!validation.valid)
|
|
371
|
+
errors.push(`Group ${index + 1}: ${validation.errors.map((item) => item.message).join(' ')}`);
|
|
372
|
+
}
|
|
373
|
+
if (errors.length)
|
|
374
|
+
throw fail(
|
|
375
|
+
ERROR_CATEGORIES.RESPONSE_FORMAT,
|
|
376
|
+
`Split commit messages violate commitPolicy: ${errors.join(' ')}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function validatePartition(groups, ids, requireSummary = true) {
|
|
355
381
|
const remaining = new Set(ids);
|
|
356
382
|
if (!Array.isArray(groups) || !groups.length)
|
|
357
383
|
throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, 'Analysis returned an empty partition.');
|
|
358
384
|
for (const group of groups) {
|
|
359
385
|
if (
|
|
386
|
+
!group ||
|
|
360
387
|
!Array.isArray(group.ids) ||
|
|
361
388
|
!group.ids.length ||
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
group.summary.length > 2000
|
|
389
|
+
(requireSummary &&
|
|
390
|
+
(typeof group.summary !== 'string' || !group.summary.trim() || group.summary.length > 2000))
|
|
365
391
|
)
|
|
366
392
|
throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, 'Analysis returned an invalid group.');
|
|
367
393
|
for (const id of group.ids)
|
|
@@ -729,15 +755,23 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
|
|
|
729
755
|
config.splitMaxDiffChars || 16000,
|
|
730
756
|
Math.floor(config.analysisBudget.limits.chunkInputTokens * 0.6),
|
|
731
757
|
);
|
|
758
|
+
// A model may keep every input as a separate commit. Limit candidate count
|
|
759
|
+
// by the response budget too, leaving room for a complete JSON plan.
|
|
760
|
+
const outputTokens = Math.min(2048, config.maxTokens || 1024);
|
|
761
|
+
const maxPlanItems = Math.max(
|
|
762
|
+
1,
|
|
763
|
+
Math.min(config.splitMaxPlanFiles || 100, Math.floor((outputTokens - 224) / 80)),
|
|
764
|
+
);
|
|
765
|
+
const batchConfig = { ...config, splitMaxPlanFiles: maxPlanItems };
|
|
732
766
|
for (let level = 0; level < 8; level++) {
|
|
733
767
|
const byId = new Map(candidates.map((x) => [x.id, x]));
|
|
734
768
|
const batches =
|
|
735
769
|
!deep && level === 0
|
|
736
|
-
? localPlanBatches(
|
|
770
|
+
? localPlanBatches(batchConfig, candidates, cap)
|
|
737
771
|
: packItems(
|
|
738
772
|
candidates.map(({ id, path, summary }) => ({ id, path, summary })),
|
|
739
773
|
cap,
|
|
740
|
-
|
|
774
|
+
maxPlanItems,
|
|
741
775
|
);
|
|
742
776
|
if (!deep && level === 0 && coverage) {
|
|
743
777
|
coverage.sampledFiles = batches.flat().filter((item) => item.representativeExcerpt).length;
|
|
@@ -769,21 +803,32 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
|
|
|
769
803
|
};
|
|
770
804
|
const groups = await jsonCall(
|
|
771
805
|
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
|
|
806
|
+
`Each commit message must follow this policy: ${JSON.stringify(policy)}.\nThe subject field must contain the complete Conventional Commit header: <type>[optional scope][optional !]: <description>. Respect the policy's language, scope, length, body, and breaking-change rules; include a body when required and omit it when forbidden.\nGroup related changes into logical commits, including related implementation and tests across directories. Return [{"ids":[input IDs],"summary":"factual combined change summary","subject":"complete commit header","body":"commit body if permitted or required"}]. 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
807
|
batch,
|
|
774
808
|
(candidate) => {
|
|
775
|
-
validatePartition(candidate, ids);
|
|
776
|
-
|
|
777
|
-
throw fail(
|
|
778
|
-
ERROR_CATEGORIES.RESPONSE_FORMAT,
|
|
779
|
-
'Analysis plan is missing a commit subject.',
|
|
780
|
-
);
|
|
809
|
+
validatePartition(candidate, ids, false);
|
|
810
|
+
validatePlanMessages(candidate, policy);
|
|
781
811
|
},
|
|
782
812
|
batchStream,
|
|
783
813
|
);
|
|
784
814
|
for (const group of groups) {
|
|
815
|
+
const sourceSummary = group.ids
|
|
816
|
+
.map((id) => {
|
|
817
|
+
const fact = byId.get(id);
|
|
818
|
+
return (
|
|
819
|
+
fact.summary ||
|
|
820
|
+
`${fact.status || 'Changed'} ${fact.kind || 'files'} in ${fact.module || fact.path || 'repository'} (${fact.files.length} files)`
|
|
821
|
+
).slice(0, 160);
|
|
822
|
+
})
|
|
823
|
+
.join('; ');
|
|
785
824
|
next.push({
|
|
786
825
|
...group,
|
|
826
|
+
summary:
|
|
827
|
+
typeof group.summary === 'string' &&
|
|
828
|
+
group.summary.trim() &&
|
|
829
|
+
group.summary.length <= 2000
|
|
830
|
+
? group.summary
|
|
831
|
+
: sourceSummary.slice(0, 2000),
|
|
787
832
|
id: `L${level}G${next.length}`,
|
|
788
833
|
files: group.ids.flatMap((id) => byId.get(id).files),
|
|
789
834
|
});
|
package/src/split.js
CHANGED
|
@@ -1836,7 +1836,7 @@ export async function splitFlow(
|
|
|
1836
1836
|
reasoning: config.reasoning,
|
|
1837
1837
|
machineOutput,
|
|
1838
1838
|
cancelMessage: 'Split cancelled.',
|
|
1839
|
-
failureMessage: '
|
|
1839
|
+
failureMessage: 'Split planning failed',
|
|
1840
1840
|
task: async (stream) => {
|
|
1841
1841
|
if (large) {
|
|
1842
1842
|
let plan;
|