@hifullmoon/aicommit 2.4.1 → 2.5.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.
- package/.aicommit.config.example.json +7 -1
- package/CHANGELOG.md +13 -1
- package/README.md +27 -18
- package/README.zh-CN.md +27 -18
- package/docs/distribution.md +1 -1
- package/docs/large-change-implementation-plan.md +14 -14
- package/docs/privacy.md +2 -2
- package/docs/troubleshooting.md +7 -3
- package/package.json +1 -1
- package/src/analysis-budget.js +17 -7
- package/src/analysis-cache.js +246 -0
- package/src/change-analysis.js +206 -54
- package/src/cli.js +15 -0
- package/src/completion.js +4 -1
- package/src/config.js +30 -2
- package/src/local-analysis.js +46 -16
- package/src/main.js +27 -2
- package/src/split.js +114 -15
package/src/split.js
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
summarizeChanges,
|
|
75
75
|
} from './change-analysis.js';
|
|
76
76
|
import { updateGitHash, spoolGit } from './git-spool.js';
|
|
77
|
+
import { createAnalysisCache } from './analysis-cache.js';
|
|
77
78
|
|
|
78
79
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
80
|
// Split mode: group changes into multiple logical commits
|
|
@@ -468,6 +469,51 @@ function groupMessage(g, policy) {
|
|
|
468
469
|
return validateCommitCandidate(message, { policy }).valid ? message : '';
|
|
469
470
|
}
|
|
470
471
|
|
|
472
|
+
function fallbackCommitMessage(policy) {
|
|
473
|
+
const type = policy.types.includes('chore') ? 'chore' : policy.types[0];
|
|
474
|
+
let scope = '';
|
|
475
|
+
if (policy.scope.mode === 'required') {
|
|
476
|
+
if (policy.scope.values.length) scope = policy.scope.values[0];
|
|
477
|
+
else {
|
|
478
|
+
const disallowed = new Set(policy.scope.disallowedValues || []);
|
|
479
|
+
const preferred = ['changes', 'repository', 'all'];
|
|
480
|
+
scope = preferred.find((candidate) => !disallowed.has(candidate)) || '';
|
|
481
|
+
for (let index = 1; !scope; index++) {
|
|
482
|
+
const candidate = `fallback-${index}`;
|
|
483
|
+
if (!disallowed.has(candidate)) scope = candidate;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const breaking = policy.breakingChange === 'require' ? '!' : '';
|
|
488
|
+
const prefix = `${type}${scope ? `(${scope})` : ''}${breaking}: `;
|
|
489
|
+
const preferred = policy.effectiveLanguage === 'zh' ? '更新其余文件' : 'update remaining files';
|
|
490
|
+
const available = Math.max(
|
|
491
|
+
1,
|
|
492
|
+
Math.min(
|
|
493
|
+
policy.subject.maxLength,
|
|
494
|
+
policy.subject.headerMaxLength
|
|
495
|
+
? policy.subject.headerMaxLength - [...prefix].length
|
|
496
|
+
: policy.subject.maxLength,
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
const subject = [...preferred].slice(0, available).join('');
|
|
500
|
+
const body =
|
|
501
|
+
policy.body.mode === 'required'
|
|
502
|
+
? policy.effectiveLanguage === 'zh'
|
|
503
|
+
? '- 包含本次已审核的全部文件变更'
|
|
504
|
+
: '- Include all reviewed file changes'
|
|
505
|
+
: '';
|
|
506
|
+
const message = cleanCommitMessage(`${prefix}${subject}${body ? `\n\n${body}` : ''}`);
|
|
507
|
+
const validation = validateCommitCandidate(message, { policy });
|
|
508
|
+
if (!validation.valid) {
|
|
509
|
+
throw fail(
|
|
510
|
+
ERROR_CATEGORIES.CONFIG,
|
|
511
|
+
`Cannot construct a conservative fallback message under commitPolicy: ${validation.errors.map((item) => item.message).join(' ')}`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return message;
|
|
515
|
+
}
|
|
516
|
+
|
|
471
517
|
// Clean up the model's plan: drop unknown/duplicate files, drop empty
|
|
472
518
|
// groups, and sweep any file the model forgot into a final catch-all group.
|
|
473
519
|
export function normalizePlan(groups, allFiles, language, commitPolicy = null) {
|
|
@@ -531,12 +577,8 @@ export function normalizePlan(groups, allFiles, language, commitPolicy = null) {
|
|
|
531
577
|
if (ids.length) leftoverHunks.push({ path: change.path, ids });
|
|
532
578
|
}
|
|
533
579
|
if (leftoverFiles.length || leftoverHunks.length) {
|
|
534
|
-
const type = policy.types.includes('chore') ? 'chore' : policy.types[0];
|
|
535
580
|
result.push({
|
|
536
|
-
message:
|
|
537
|
-
policy.effectiveLanguage === 'zh'
|
|
538
|
-
? `${type}: 更新其余文件`
|
|
539
|
-
: `${type}: update remaining files`,
|
|
581
|
+
message: fallbackCommitMessage(policy),
|
|
540
582
|
files: leftoverFiles,
|
|
541
583
|
...(leftoverHunks.length ? { hunks: leftoverHunks } : {}),
|
|
542
584
|
});
|
|
@@ -1421,6 +1463,7 @@ export async function splitFlow(
|
|
|
1421
1463
|
scope = 'prompt',
|
|
1422
1464
|
dryRun = false,
|
|
1423
1465
|
yes = false,
|
|
1466
|
+
allowSingleFallback = false,
|
|
1424
1467
|
machineOutput = false,
|
|
1425
1468
|
provider = null,
|
|
1426
1469
|
exportPlanPath = null,
|
|
@@ -1621,6 +1664,7 @@ export async function splitFlow(
|
|
|
1621
1664
|
|
|
1622
1665
|
let raw, reasoningText, elapsed, usage;
|
|
1623
1666
|
let analysis;
|
|
1667
|
+
let persistentAnalysisCache = null;
|
|
1624
1668
|
const large =
|
|
1625
1669
|
needsAnalysis(captured, config, true) ||
|
|
1626
1670
|
untrackedSnapshot.previews.sources?.size > 0 ||
|
|
@@ -1634,7 +1678,22 @@ export async function splitFlow(
|
|
|
1634
1678
|
ERROR_CATEGORIES.CONFIG,
|
|
1635
1679
|
'Large-change analysis does not support experimental hunk plans; use file-level split.',
|
|
1636
1680
|
);
|
|
1637
|
-
planningConfig = analysisConfig(planningConfig);
|
|
1681
|
+
planningConfig = analysisConfig({ ...planningConfig, analysisTask: 'split' });
|
|
1682
|
+
persistentAnalysisCache = createAnalysisCache({
|
|
1683
|
+
projectRoot,
|
|
1684
|
+
snapshotFingerprint: plannedStateFingerprint,
|
|
1685
|
+
config: planningConfig,
|
|
1686
|
+
protect: protectModelInput,
|
|
1687
|
+
});
|
|
1688
|
+
if (
|
|
1689
|
+
planningConfig.largeChange?.strategy === 'deep' &&
|
|
1690
|
+
planningConfig.largeChange.cache?.enabled &&
|
|
1691
|
+
!protectModelInput &&
|
|
1692
|
+
!planningConfig.largeChange.cache.allowUnprotected
|
|
1693
|
+
) {
|
|
1694
|
+
warnings.push('Deep-analysis recovery cache was disabled for unprotected input.');
|
|
1695
|
+
console.log(chalk.dim(' Recovery cache: disabled for unprotected input.'));
|
|
1696
|
+
}
|
|
1638
1697
|
console.log(
|
|
1639
1698
|
chalk.dim(
|
|
1640
1699
|
planningConfig.largeChange?.strategy === 'deep'
|
|
@@ -1657,15 +1716,53 @@ export async function splitFlow(
|
|
|
1657
1716
|
failureMessage: 'API call failed',
|
|
1658
1717
|
task: async (stream) => {
|
|
1659
1718
|
if (large) {
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1719
|
+
let plan;
|
|
1720
|
+
try {
|
|
1721
|
+
analysis = await analyzeChanges(
|
|
1722
|
+
planningConfig,
|
|
1723
|
+
captured,
|
|
1724
|
+
protectModelInput,
|
|
1725
|
+
untrackedSnapshot.previews,
|
|
1726
|
+
({ completedChunks, cachedChunks }) =>
|
|
1727
|
+
console.error(
|
|
1728
|
+
` Analysis: ${completedChunks} chunks completed${cachedChunks ? ` (${cachedChunks} cached)` : ''}`,
|
|
1729
|
+
),
|
|
1730
|
+
persistentAnalysisCache,
|
|
1731
|
+
);
|
|
1732
|
+
plan = await planAnalyzedChanges(planningConfig, analysis.facts, analysis.coverage);
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
const exhausted =
|
|
1735
|
+
err.data?.analysis?.exhausted ||
|
|
1736
|
+
(!planningConfig.analysisBudget.remainingMs() ? 'time' : null);
|
|
1737
|
+
if (!exhausted && !err.data?.fallbackPlan) throw err;
|
|
1738
|
+
if (yes && !dryRun && !allowSingleFallback) {
|
|
1739
|
+
throw fail(
|
|
1740
|
+
err.category || ERROR_CATEGORIES.PROVIDER,
|
|
1741
|
+
'Large-change planning requires a conservative single-commit fallback. No commit was created; review interactively or rerun with --allow-single-fallback.',
|
|
1742
|
+
{ cause: err, data: err.data },
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const fallbackConfig = {
|
|
1746
|
+
...planningConfig,
|
|
1747
|
+
largeChange: { ...planningConfig.largeChange, strategy: 'auto' },
|
|
1748
|
+
};
|
|
1749
|
+
analysis = await analyzeChanges(
|
|
1750
|
+
fallbackConfig,
|
|
1751
|
+
captured,
|
|
1752
|
+
protectModelInput,
|
|
1753
|
+
untrackedSnapshot.previews,
|
|
1754
|
+
);
|
|
1755
|
+
analysis.coverage = {
|
|
1756
|
+
...analysis.coverage,
|
|
1757
|
+
strategy: 'fallback',
|
|
1758
|
+
degradedFrom: planningConfig.largeChange.strategy,
|
|
1759
|
+
fallbackReason: exhausted || 'planning_capacity',
|
|
1760
|
+
};
|
|
1761
|
+
plan = normalizePlan([], allFiles, config.language, config.commitPolicy);
|
|
1762
|
+
const warning = `Large-change planning used one conservative all-files commit after ${exhausted || 'planning capacity'} exhaustion; review the fallback message and grouping.`;
|
|
1763
|
+
warnings.push(warning);
|
|
1764
|
+
console.error(` Fallback: ${warning}`);
|
|
1765
|
+
}
|
|
1669
1766
|
if (analysis.coverage.strategy === 'auto')
|
|
1670
1767
|
warnings.push(
|
|
1671
1768
|
'Split candidates used local metadata and selected excerpts; review the grouping because content was not fully analyzed.',
|
|
@@ -1736,6 +1833,8 @@ export async function splitFlow(
|
|
|
1736
1833
|
reported: true,
|
|
1737
1834
|
});
|
|
1738
1835
|
}
|
|
1836
|
+
persistentAnalysisCache?.clear();
|
|
1837
|
+
persistentAnalysisCache = null;
|
|
1739
1838
|
|
|
1740
1839
|
// Review / edit / regenerate loop
|
|
1741
1840
|
let regenCounts = groups.map(() => 0);
|