git-coco 0.43.0 → 0.44.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/dist/index.d.ts +1 -0
- package/dist/index.esm.mjs +183 -80
- package/dist/index.js +183 -80
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.esm.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import { pathToFileURL } from 'url';
|
|
|
54
54
|
/**
|
|
55
55
|
* Current build version from package.json
|
|
56
56
|
*/
|
|
57
|
-
const BUILD_VERSION = "0.
|
|
57
|
+
const BUILD_VERSION = "0.44.0";
|
|
58
58
|
|
|
59
59
|
const isInteractive = (config) => {
|
|
60
60
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -12415,6 +12415,164 @@ const CommitSplitPlanSchema = objectType({
|
|
|
12415
12415
|
}))
|
|
12416
12416
|
.min(1),
|
|
12417
12417
|
});
|
|
12418
|
+
|
|
12419
|
+
const getGroupFiles$1 = (group) => group.files || [];
|
|
12420
|
+
const getGroupHunks$1 = (group) => group.hunks || [];
|
|
12421
|
+
function getPlanValidationIssues(plan, staged, hunkInventory) {
|
|
12422
|
+
const stagedFiles = new Set(staged.map((change) => change.filePath));
|
|
12423
|
+
const seen = new Set();
|
|
12424
|
+
const seenHunks = new Set();
|
|
12425
|
+
const unknownFiles = [];
|
|
12426
|
+
const duplicateFiles = [];
|
|
12427
|
+
const unknownHunks = [];
|
|
12428
|
+
const duplicateHunks = [];
|
|
12429
|
+
plan.groups.forEach((group) => {
|
|
12430
|
+
getGroupFiles$1(group).forEach((file) => {
|
|
12431
|
+
if (!stagedFiles.has(file)) {
|
|
12432
|
+
unknownFiles.push(file);
|
|
12433
|
+
return;
|
|
12434
|
+
}
|
|
12435
|
+
if (seen.has(file)) {
|
|
12436
|
+
duplicateFiles.push(file);
|
|
12437
|
+
return;
|
|
12438
|
+
}
|
|
12439
|
+
seen.add(file);
|
|
12440
|
+
});
|
|
12441
|
+
getGroupHunks$1(group).forEach((hunkId) => {
|
|
12442
|
+
const hunk = hunkInventory?.byId.get(hunkId);
|
|
12443
|
+
if (!hunk) {
|
|
12444
|
+
unknownHunks.push(hunkId);
|
|
12445
|
+
return;
|
|
12446
|
+
}
|
|
12447
|
+
if (seenHunks.has(hunkId)) {
|
|
12448
|
+
duplicateHunks.push(hunkId);
|
|
12449
|
+
return;
|
|
12450
|
+
}
|
|
12451
|
+
seenHunks.add(hunkId);
|
|
12452
|
+
});
|
|
12453
|
+
});
|
|
12454
|
+
const hunkCoveredFiles = new Set([...seenHunks].map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath));
|
|
12455
|
+
const mixedFiles = [...seen].filter((file) => hunkCoveredFiles.has(file));
|
|
12456
|
+
const partiallyCoveredFiles = [...hunkCoveredFiles]
|
|
12457
|
+
.filter((file) => Boolean(file))
|
|
12458
|
+
.filter((file) => {
|
|
12459
|
+
const fileHunks = hunkInventory?.byFile.get(file) || [];
|
|
12460
|
+
return fileHunks.some((hunk) => !seenHunks.has(hunk.id));
|
|
12461
|
+
});
|
|
12462
|
+
const missingFiles = [...stagedFiles].filter((file) => !seen.has(file) && !hunkCoveredFiles.has(file));
|
|
12463
|
+
return {
|
|
12464
|
+
unknownFiles,
|
|
12465
|
+
duplicateFiles,
|
|
12466
|
+
unknownHunks,
|
|
12467
|
+
duplicateHunks,
|
|
12468
|
+
mixedFiles,
|
|
12469
|
+
partiallyCoveredFiles,
|
|
12470
|
+
missingFiles,
|
|
12471
|
+
};
|
|
12472
|
+
}
|
|
12473
|
+
function hasPlanValidationIssues(issues) {
|
|
12474
|
+
return (issues.unknownFiles.length > 0 ||
|
|
12475
|
+
issues.duplicateFiles.length > 0 ||
|
|
12476
|
+
issues.unknownHunks.length > 0 ||
|
|
12477
|
+
issues.duplicateHunks.length > 0 ||
|
|
12478
|
+
issues.mixedFiles.length > 0 ||
|
|
12479
|
+
issues.partiallyCoveredFiles.length > 0 ||
|
|
12480
|
+
issues.missingFiles.length > 0);
|
|
12481
|
+
}
|
|
12482
|
+
function formatPlanValidationIssuesError(issues) {
|
|
12483
|
+
return [
|
|
12484
|
+
issues.unknownFiles.length ? `unknown files: ${issues.unknownFiles.join(', ')}` : undefined,
|
|
12485
|
+
issues.duplicateFiles.length
|
|
12486
|
+
? `duplicate files: ${issues.duplicateFiles.join(', ')}`
|
|
12487
|
+
: undefined,
|
|
12488
|
+
issues.unknownHunks.length ? `unknown hunks: ${issues.unknownHunks.join(', ')}` : undefined,
|
|
12489
|
+
issues.duplicateHunks.length
|
|
12490
|
+
? `duplicate hunks: ${issues.duplicateHunks.join(', ')}`
|
|
12491
|
+
: undefined,
|
|
12492
|
+
issues.mixedFiles.length
|
|
12493
|
+
? `files assigned both as whole files and hunks: ${issues.mixedFiles.join(', ')}`
|
|
12494
|
+
: undefined,
|
|
12495
|
+
issues.partiallyCoveredFiles.length
|
|
12496
|
+
? `files with only some hunks assigned: ${issues.partiallyCoveredFiles.join(', ')}`
|
|
12497
|
+
: undefined,
|
|
12498
|
+
issues.missingFiles.length ? `missing files: ${issues.missingFiles.join(', ')}` : undefined,
|
|
12499
|
+
]
|
|
12500
|
+
.filter(Boolean)
|
|
12501
|
+
.join('; ');
|
|
12502
|
+
}
|
|
12503
|
+
function formatPlanValidationFeedback(issues) {
|
|
12504
|
+
const sections = [];
|
|
12505
|
+
if (issues.unknownFiles.length) {
|
|
12506
|
+
sections.push(`Files referenced that are NOT in the staged file inventory (remove or replace): ${issues.unknownFiles.join(', ')}`);
|
|
12507
|
+
}
|
|
12508
|
+
if (issues.duplicateFiles.length) {
|
|
12509
|
+
sections.push(`Files assigned to more than one group (each file may appear at most once): ${issues.duplicateFiles.join(', ')}`);
|
|
12510
|
+
}
|
|
12511
|
+
if (issues.unknownHunks.length) {
|
|
12512
|
+
sections.push(`Hunk IDs referenced that are NOT in the staged hunk inventory: ${issues.unknownHunks.join(', ')}`);
|
|
12513
|
+
}
|
|
12514
|
+
if (issues.duplicateHunks.length) {
|
|
12515
|
+
sections.push(`Hunk IDs assigned to more than one group (each hunk may appear at most once): ${issues.duplicateHunks.join(', ')}`);
|
|
12516
|
+
}
|
|
12517
|
+
if (issues.mixedFiles.length) {
|
|
12518
|
+
sections.push(`Files assigned BOTH as whole files and via hunks (pick one mode per file): ${issues.mixedFiles.join(', ')}`);
|
|
12519
|
+
}
|
|
12520
|
+
if (issues.partiallyCoveredFiles.length) {
|
|
12521
|
+
sections.push(`Files with only some hunks assigned (every hunk for these files must be covered): ${issues.partiallyCoveredFiles.join(', ')}`);
|
|
12522
|
+
}
|
|
12523
|
+
if (issues.missingFiles.length) {
|
|
12524
|
+
sections.push(`Staged files missing from every group (must appear exactly once): ${issues.missingFiles.join(', ')}`);
|
|
12525
|
+
}
|
|
12526
|
+
return sections.map((section) => `- ${section}`).join('\n');
|
|
12527
|
+
}
|
|
12528
|
+
|
|
12529
|
+
const NO_PREVIOUS_FEEDBACK_PLACEHOLDER = 'None — this is the first attempt.';
|
|
12530
|
+
const DEFAULT_MAX_PLAN_ATTEMPTS = 3;
|
|
12531
|
+
/**
|
|
12532
|
+
* Generate a commit-split plan with self-correcting retries on validator failures.
|
|
12533
|
+
*
|
|
12534
|
+
* The first attempt runs as normal. If `validatePlanForStagedFiles` rejects the result,
|
|
12535
|
+
* the validator's complaints are formatted as natural-language feedback and fed back
|
|
12536
|
+
* into the same prompt template (`previous_attempt_feedback` slot) so the model can
|
|
12537
|
+
* fix its own mistakes without re-running pre-processing.
|
|
12538
|
+
*/
|
|
12539
|
+
async function generateValidatedCommitSplitPlan({ llm, prompt, variables, staged, hunkInventory, logger, tokenizer, metadata = {}, maxAttempts = DEFAULT_MAX_PLAN_ATTEMPTS, }) {
|
|
12540
|
+
let lastIssues = null;
|
|
12541
|
+
let attempt = 0;
|
|
12542
|
+
while (attempt < maxAttempts) {
|
|
12543
|
+
attempt++;
|
|
12544
|
+
const previousFeedback = lastIssues
|
|
12545
|
+
? formatPlanValidationFeedback(lastIssues)
|
|
12546
|
+
: NO_PREVIOUS_FEEDBACK_PLACEHOLDER;
|
|
12547
|
+
const plan = await executeChainWithSchema(CommitSplitPlanSchema, llm, prompt, {
|
|
12548
|
+
...variables,
|
|
12549
|
+
previous_attempt_feedback: previousFeedback,
|
|
12550
|
+
}, {
|
|
12551
|
+
logger,
|
|
12552
|
+
tokenizer,
|
|
12553
|
+
metadata: {
|
|
12554
|
+
task: 'commit-split-plan',
|
|
12555
|
+
...metadata,
|
|
12556
|
+
planAttempt: attempt,
|
|
12557
|
+
},
|
|
12558
|
+
});
|
|
12559
|
+
const issues = getPlanValidationIssues(plan, staged, hunkInventory);
|
|
12560
|
+
if (!hasPlanValidationIssues(issues)) {
|
|
12561
|
+
if (attempt > 1 && logger) {
|
|
12562
|
+
logger.verbose(`Plan validated after ${attempt} attempts.`, { color: 'green' });
|
|
12563
|
+
}
|
|
12564
|
+
return { plan, attempts: attempt };
|
|
12565
|
+
}
|
|
12566
|
+
lastIssues = issues;
|
|
12567
|
+
if (logger) {
|
|
12568
|
+
logger.verbose(`Plan attempt ${attempt}/${maxAttempts} failed validation: ${formatPlanValidationIssuesError(issues)}`, { color: 'yellow' });
|
|
12569
|
+
}
|
|
12570
|
+
}
|
|
12571
|
+
throw new Error(lastIssues
|
|
12572
|
+
? `Failed to produce a valid commit-split plan after ${maxAttempts} attempts. Final validator issues: ${formatPlanValidationIssuesError(lastIssues)}`
|
|
12573
|
+
: `Failed to produce a valid commit-split plan after ${maxAttempts} attempts.`);
|
|
12574
|
+
}
|
|
12575
|
+
|
|
12418
12576
|
const COMMIT_SPLIT_PROMPT = PromptTemplate.fromTemplate(`You are helping split staged git changes into a small sequence of coherent commits.
|
|
12419
12577
|
|
|
12420
12578
|
Return ONLY valid JSON matching this schema:
|
|
@@ -12431,14 +12589,13 @@ Return ONLY valid JSON matching this schema:
|
|
|
12431
12589
|
}}
|
|
12432
12590
|
|
|
12433
12591
|
Rules:
|
|
12434
|
-
-
|
|
12435
|
-
- If
|
|
12436
|
-
- Do not list the same file in "files"
|
|
12437
|
-
- Only use file paths listed in the staged file inventory.
|
|
12438
|
-
- Only use hunk IDs listed in the staged hunk inventory.
|
|
12592
|
+
- Every staged file MUST be assigned exactly once across all groups, either via "files" OR via every one of its hunk IDs (never both).
|
|
12593
|
+
- If you assign any hunk for a file, you MUST assign EVERY hunk for that file across the groups — partial coverage is invalid.
|
|
12594
|
+
- Do not list the same file in "files" of more than one group, and do not assign the same hunk ID to more than one group.
|
|
12595
|
+
- Only use file paths listed in the staged file inventory. Do not invent files.
|
|
12596
|
+
- Only use hunk IDs listed in the staged hunk inventory. Do not invent hunk IDs.
|
|
12439
12597
|
- Prefer 2-5 commits unless the changes are truly all one topic.
|
|
12440
12598
|
- Keep commit titles concise and understandable.
|
|
12441
|
-
- Do not invent files.
|
|
12442
12599
|
|
|
12443
12600
|
Staged file inventory:
|
|
12444
12601
|
{file_inventory}
|
|
@@ -12450,7 +12607,10 @@ Condensed staged diff:
|
|
|
12450
12607
|
{summary}
|
|
12451
12608
|
|
|
12452
12609
|
Additional context:
|
|
12453
|
-
{additional_context}
|
|
12610
|
+
{additional_context}
|
|
12611
|
+
|
|
12612
|
+
Feedback on previous attempt (fix every item before responding):
|
|
12613
|
+
{previous_attempt_feedback}`);
|
|
12454
12614
|
function isCommitSplitCommand(argv) {
|
|
12455
12615
|
return Boolean(argv.split || argv.plan || argv.apply || argv._.includes('split'));
|
|
12456
12616
|
}
|
|
@@ -12469,9 +12629,6 @@ function formatCommitSplitPlan(plan) {
|
|
|
12469
12629
|
})
|
|
12470
12630
|
.join('\n\n---\n\n');
|
|
12471
12631
|
}
|
|
12472
|
-
function getStagedFileSet(changes) {
|
|
12473
|
-
return new Set(changes.map((change) => change.filePath));
|
|
12474
|
-
}
|
|
12475
12632
|
function getGroupFiles(group) {
|
|
12476
12633
|
return group.files || [];
|
|
12477
12634
|
}
|
|
@@ -12528,67 +12685,9 @@ function formatHunkInventory(inventory) {
|
|
|
12528
12685
|
.join('\n');
|
|
12529
12686
|
}
|
|
12530
12687
|
function validatePlanForStagedFiles(plan, staged, hunkInventory) {
|
|
12531
|
-
const
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
const unknown = [];
|
|
12535
|
-
const duplicate = [];
|
|
12536
|
-
const unknownHunks = [];
|
|
12537
|
-
const duplicateHunks = [];
|
|
12538
|
-
plan.groups.forEach((group) => {
|
|
12539
|
-
getGroupFiles(group).forEach((file) => {
|
|
12540
|
-
if (!stagedFiles.has(file)) {
|
|
12541
|
-
unknown.push(file);
|
|
12542
|
-
return;
|
|
12543
|
-
}
|
|
12544
|
-
if (seen.has(file)) {
|
|
12545
|
-
duplicate.push(file);
|
|
12546
|
-
return;
|
|
12547
|
-
}
|
|
12548
|
-
seen.add(file);
|
|
12549
|
-
});
|
|
12550
|
-
getGroupHunks(group).forEach((hunkId) => {
|
|
12551
|
-
const hunk = hunkInventory?.byId.get(hunkId);
|
|
12552
|
-
if (!hunk) {
|
|
12553
|
-
unknownHunks.push(hunkId);
|
|
12554
|
-
return;
|
|
12555
|
-
}
|
|
12556
|
-
if (seenHunks.has(hunkId)) {
|
|
12557
|
-
duplicateHunks.push(hunkId);
|
|
12558
|
-
return;
|
|
12559
|
-
}
|
|
12560
|
-
seenHunks.add(hunkId);
|
|
12561
|
-
});
|
|
12562
|
-
});
|
|
12563
|
-
const hunkCoveredFiles = new Set([...seenHunks].map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath));
|
|
12564
|
-
const mixedFiles = [...seen].filter((file) => hunkCoveredFiles.has(file));
|
|
12565
|
-
const partiallyCoveredFiles = [...hunkCoveredFiles]
|
|
12566
|
-
.filter((file) => Boolean(file))
|
|
12567
|
-
.filter((file) => {
|
|
12568
|
-
const fileHunks = hunkInventory?.byFile.get(file) || [];
|
|
12569
|
-
return fileHunks.some((hunk) => !seenHunks.has(hunk.id));
|
|
12570
|
-
});
|
|
12571
|
-
const missing = [...stagedFiles].filter((file) => !seen.has(file) && !hunkCoveredFiles.has(file));
|
|
12572
|
-
if (unknown.length ||
|
|
12573
|
-
duplicate.length ||
|
|
12574
|
-
unknownHunks.length ||
|
|
12575
|
-
duplicateHunks.length ||
|
|
12576
|
-
mixedFiles.length ||
|
|
12577
|
-
partiallyCoveredFiles.length ||
|
|
12578
|
-
missing.length) {
|
|
12579
|
-
throw new Error([
|
|
12580
|
-
unknown.length ? `unknown files: ${unknown.join(', ')}` : undefined,
|
|
12581
|
-
duplicate.length ? `duplicate files: ${duplicate.join(', ')}` : undefined,
|
|
12582
|
-
unknownHunks.length ? `unknown hunks: ${unknownHunks.join(', ')}` : undefined,
|
|
12583
|
-
duplicateHunks.length ? `duplicate hunks: ${duplicateHunks.join(', ')}` : undefined,
|
|
12584
|
-
mixedFiles.length ? `files assigned both as whole files and hunks: ${mixedFiles.join(', ')}` : undefined,
|
|
12585
|
-
partiallyCoveredFiles.length
|
|
12586
|
-
? `files with only some hunks assigned: ${partiallyCoveredFiles.join(', ')}`
|
|
12587
|
-
: undefined,
|
|
12588
|
-
missing.length ? `missing files: ${missing.join(', ')}` : undefined,
|
|
12589
|
-
]
|
|
12590
|
-
.filter(Boolean)
|
|
12591
|
-
.join('; '));
|
|
12688
|
+
const issues = getPlanValidationIssues(plan, staged, hunkInventory);
|
|
12689
|
+
if (hasPlanValidationIssues(issues)) {
|
|
12690
|
+
throw new Error(formatPlanValidationIssuesError(issues));
|
|
12592
12691
|
}
|
|
12593
12692
|
}
|
|
12594
12693
|
function assertNoUnstagedOverlap(plan, changes, hunkInventory) {
|
|
@@ -12692,22 +12791,26 @@ async function handleCommitSplit({ argv, config, git, logger, tokenizer, llm, })
|
|
|
12692
12791
|
.map((change) => `- ${change.filePath}: ${change.status} - ${change.summary}`)
|
|
12693
12792
|
.join('\n');
|
|
12694
12793
|
const hunkInventoryText = formatHunkInventory(hunkInventory);
|
|
12695
|
-
const plan = await
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12794
|
+
const { plan } = await generateValidatedCommitSplitPlan({
|
|
12795
|
+
llm,
|
|
12796
|
+
prompt: COMMIT_SPLIT_PROMPT,
|
|
12797
|
+
variables: {
|
|
12798
|
+
file_inventory: fileInventory,
|
|
12799
|
+
hunk_inventory: hunkInventoryText,
|
|
12800
|
+
summary,
|
|
12801
|
+
additional_context: argv.additional || '',
|
|
12802
|
+
},
|
|
12803
|
+
staged: changes.staged,
|
|
12804
|
+
hunkInventory,
|
|
12701
12805
|
logger,
|
|
12702
12806
|
tokenizer,
|
|
12703
12807
|
metadata: {
|
|
12704
|
-
task: 'commit-split-plan',
|
|
12705
12808
|
command: 'commit',
|
|
12706
12809
|
provider: config.service.provider,
|
|
12707
12810
|
model: String(config.service.model),
|
|
12708
12811
|
},
|
|
12812
|
+
maxAttempts: DEFAULT_MAX_PLAN_ATTEMPTS,
|
|
12709
12813
|
});
|
|
12710
|
-
validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
|
|
12711
12814
|
if (argv.apply) {
|
|
12712
12815
|
return await applyCommitSplitPlan({
|
|
12713
12816
|
plan,
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
78
78
|
/**
|
|
79
79
|
* Current build version from package.json
|
|
80
80
|
*/
|
|
81
|
-
const BUILD_VERSION = "0.
|
|
81
|
+
const BUILD_VERSION = "0.44.0";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -12439,6 +12439,164 @@ const CommitSplitPlanSchema = objectType({
|
|
|
12439
12439
|
}))
|
|
12440
12440
|
.min(1),
|
|
12441
12441
|
});
|
|
12442
|
+
|
|
12443
|
+
const getGroupFiles$1 = (group) => group.files || [];
|
|
12444
|
+
const getGroupHunks$1 = (group) => group.hunks || [];
|
|
12445
|
+
function getPlanValidationIssues(plan, staged, hunkInventory) {
|
|
12446
|
+
const stagedFiles = new Set(staged.map((change) => change.filePath));
|
|
12447
|
+
const seen = new Set();
|
|
12448
|
+
const seenHunks = new Set();
|
|
12449
|
+
const unknownFiles = [];
|
|
12450
|
+
const duplicateFiles = [];
|
|
12451
|
+
const unknownHunks = [];
|
|
12452
|
+
const duplicateHunks = [];
|
|
12453
|
+
plan.groups.forEach((group) => {
|
|
12454
|
+
getGroupFiles$1(group).forEach((file) => {
|
|
12455
|
+
if (!stagedFiles.has(file)) {
|
|
12456
|
+
unknownFiles.push(file);
|
|
12457
|
+
return;
|
|
12458
|
+
}
|
|
12459
|
+
if (seen.has(file)) {
|
|
12460
|
+
duplicateFiles.push(file);
|
|
12461
|
+
return;
|
|
12462
|
+
}
|
|
12463
|
+
seen.add(file);
|
|
12464
|
+
});
|
|
12465
|
+
getGroupHunks$1(group).forEach((hunkId) => {
|
|
12466
|
+
const hunk = hunkInventory?.byId.get(hunkId);
|
|
12467
|
+
if (!hunk) {
|
|
12468
|
+
unknownHunks.push(hunkId);
|
|
12469
|
+
return;
|
|
12470
|
+
}
|
|
12471
|
+
if (seenHunks.has(hunkId)) {
|
|
12472
|
+
duplicateHunks.push(hunkId);
|
|
12473
|
+
return;
|
|
12474
|
+
}
|
|
12475
|
+
seenHunks.add(hunkId);
|
|
12476
|
+
});
|
|
12477
|
+
});
|
|
12478
|
+
const hunkCoveredFiles = new Set([...seenHunks].map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath));
|
|
12479
|
+
const mixedFiles = [...seen].filter((file) => hunkCoveredFiles.has(file));
|
|
12480
|
+
const partiallyCoveredFiles = [...hunkCoveredFiles]
|
|
12481
|
+
.filter((file) => Boolean(file))
|
|
12482
|
+
.filter((file) => {
|
|
12483
|
+
const fileHunks = hunkInventory?.byFile.get(file) || [];
|
|
12484
|
+
return fileHunks.some((hunk) => !seenHunks.has(hunk.id));
|
|
12485
|
+
});
|
|
12486
|
+
const missingFiles = [...stagedFiles].filter((file) => !seen.has(file) && !hunkCoveredFiles.has(file));
|
|
12487
|
+
return {
|
|
12488
|
+
unknownFiles,
|
|
12489
|
+
duplicateFiles,
|
|
12490
|
+
unknownHunks,
|
|
12491
|
+
duplicateHunks,
|
|
12492
|
+
mixedFiles,
|
|
12493
|
+
partiallyCoveredFiles,
|
|
12494
|
+
missingFiles,
|
|
12495
|
+
};
|
|
12496
|
+
}
|
|
12497
|
+
function hasPlanValidationIssues(issues) {
|
|
12498
|
+
return (issues.unknownFiles.length > 0 ||
|
|
12499
|
+
issues.duplicateFiles.length > 0 ||
|
|
12500
|
+
issues.unknownHunks.length > 0 ||
|
|
12501
|
+
issues.duplicateHunks.length > 0 ||
|
|
12502
|
+
issues.mixedFiles.length > 0 ||
|
|
12503
|
+
issues.partiallyCoveredFiles.length > 0 ||
|
|
12504
|
+
issues.missingFiles.length > 0);
|
|
12505
|
+
}
|
|
12506
|
+
function formatPlanValidationIssuesError(issues) {
|
|
12507
|
+
return [
|
|
12508
|
+
issues.unknownFiles.length ? `unknown files: ${issues.unknownFiles.join(', ')}` : undefined,
|
|
12509
|
+
issues.duplicateFiles.length
|
|
12510
|
+
? `duplicate files: ${issues.duplicateFiles.join(', ')}`
|
|
12511
|
+
: undefined,
|
|
12512
|
+
issues.unknownHunks.length ? `unknown hunks: ${issues.unknownHunks.join(', ')}` : undefined,
|
|
12513
|
+
issues.duplicateHunks.length
|
|
12514
|
+
? `duplicate hunks: ${issues.duplicateHunks.join(', ')}`
|
|
12515
|
+
: undefined,
|
|
12516
|
+
issues.mixedFiles.length
|
|
12517
|
+
? `files assigned both as whole files and hunks: ${issues.mixedFiles.join(', ')}`
|
|
12518
|
+
: undefined,
|
|
12519
|
+
issues.partiallyCoveredFiles.length
|
|
12520
|
+
? `files with only some hunks assigned: ${issues.partiallyCoveredFiles.join(', ')}`
|
|
12521
|
+
: undefined,
|
|
12522
|
+
issues.missingFiles.length ? `missing files: ${issues.missingFiles.join(', ')}` : undefined,
|
|
12523
|
+
]
|
|
12524
|
+
.filter(Boolean)
|
|
12525
|
+
.join('; ');
|
|
12526
|
+
}
|
|
12527
|
+
function formatPlanValidationFeedback(issues) {
|
|
12528
|
+
const sections = [];
|
|
12529
|
+
if (issues.unknownFiles.length) {
|
|
12530
|
+
sections.push(`Files referenced that are NOT in the staged file inventory (remove or replace): ${issues.unknownFiles.join(', ')}`);
|
|
12531
|
+
}
|
|
12532
|
+
if (issues.duplicateFiles.length) {
|
|
12533
|
+
sections.push(`Files assigned to more than one group (each file may appear at most once): ${issues.duplicateFiles.join(', ')}`);
|
|
12534
|
+
}
|
|
12535
|
+
if (issues.unknownHunks.length) {
|
|
12536
|
+
sections.push(`Hunk IDs referenced that are NOT in the staged hunk inventory: ${issues.unknownHunks.join(', ')}`);
|
|
12537
|
+
}
|
|
12538
|
+
if (issues.duplicateHunks.length) {
|
|
12539
|
+
sections.push(`Hunk IDs assigned to more than one group (each hunk may appear at most once): ${issues.duplicateHunks.join(', ')}`);
|
|
12540
|
+
}
|
|
12541
|
+
if (issues.mixedFiles.length) {
|
|
12542
|
+
sections.push(`Files assigned BOTH as whole files and via hunks (pick one mode per file): ${issues.mixedFiles.join(', ')}`);
|
|
12543
|
+
}
|
|
12544
|
+
if (issues.partiallyCoveredFiles.length) {
|
|
12545
|
+
sections.push(`Files with only some hunks assigned (every hunk for these files must be covered): ${issues.partiallyCoveredFiles.join(', ')}`);
|
|
12546
|
+
}
|
|
12547
|
+
if (issues.missingFiles.length) {
|
|
12548
|
+
sections.push(`Staged files missing from every group (must appear exactly once): ${issues.missingFiles.join(', ')}`);
|
|
12549
|
+
}
|
|
12550
|
+
return sections.map((section) => `- ${section}`).join('\n');
|
|
12551
|
+
}
|
|
12552
|
+
|
|
12553
|
+
const NO_PREVIOUS_FEEDBACK_PLACEHOLDER = 'None — this is the first attempt.';
|
|
12554
|
+
const DEFAULT_MAX_PLAN_ATTEMPTS = 3;
|
|
12555
|
+
/**
|
|
12556
|
+
* Generate a commit-split plan with self-correcting retries on validator failures.
|
|
12557
|
+
*
|
|
12558
|
+
* The first attempt runs as normal. If `validatePlanForStagedFiles` rejects the result,
|
|
12559
|
+
* the validator's complaints are formatted as natural-language feedback and fed back
|
|
12560
|
+
* into the same prompt template (`previous_attempt_feedback` slot) so the model can
|
|
12561
|
+
* fix its own mistakes without re-running pre-processing.
|
|
12562
|
+
*/
|
|
12563
|
+
async function generateValidatedCommitSplitPlan({ llm, prompt, variables, staged, hunkInventory, logger, tokenizer, metadata = {}, maxAttempts = DEFAULT_MAX_PLAN_ATTEMPTS, }) {
|
|
12564
|
+
let lastIssues = null;
|
|
12565
|
+
let attempt = 0;
|
|
12566
|
+
while (attempt < maxAttempts) {
|
|
12567
|
+
attempt++;
|
|
12568
|
+
const previousFeedback = lastIssues
|
|
12569
|
+
? formatPlanValidationFeedback(lastIssues)
|
|
12570
|
+
: NO_PREVIOUS_FEEDBACK_PLACEHOLDER;
|
|
12571
|
+
const plan = await executeChainWithSchema(CommitSplitPlanSchema, llm, prompt, {
|
|
12572
|
+
...variables,
|
|
12573
|
+
previous_attempt_feedback: previousFeedback,
|
|
12574
|
+
}, {
|
|
12575
|
+
logger,
|
|
12576
|
+
tokenizer,
|
|
12577
|
+
metadata: {
|
|
12578
|
+
task: 'commit-split-plan',
|
|
12579
|
+
...metadata,
|
|
12580
|
+
planAttempt: attempt,
|
|
12581
|
+
},
|
|
12582
|
+
});
|
|
12583
|
+
const issues = getPlanValidationIssues(plan, staged, hunkInventory);
|
|
12584
|
+
if (!hasPlanValidationIssues(issues)) {
|
|
12585
|
+
if (attempt > 1 && logger) {
|
|
12586
|
+
logger.verbose(`Plan validated after ${attempt} attempts.`, { color: 'green' });
|
|
12587
|
+
}
|
|
12588
|
+
return { plan, attempts: attempt };
|
|
12589
|
+
}
|
|
12590
|
+
lastIssues = issues;
|
|
12591
|
+
if (logger) {
|
|
12592
|
+
logger.verbose(`Plan attempt ${attempt}/${maxAttempts} failed validation: ${formatPlanValidationIssuesError(issues)}`, { color: 'yellow' });
|
|
12593
|
+
}
|
|
12594
|
+
}
|
|
12595
|
+
throw new Error(lastIssues
|
|
12596
|
+
? `Failed to produce a valid commit-split plan after ${maxAttempts} attempts. Final validator issues: ${formatPlanValidationIssuesError(lastIssues)}`
|
|
12597
|
+
: `Failed to produce a valid commit-split plan after ${maxAttempts} attempts.`);
|
|
12598
|
+
}
|
|
12599
|
+
|
|
12442
12600
|
const COMMIT_SPLIT_PROMPT = prompts.PromptTemplate.fromTemplate(`You are helping split staged git changes into a small sequence of coherent commits.
|
|
12443
12601
|
|
|
12444
12602
|
Return ONLY valid JSON matching this schema:
|
|
@@ -12455,14 +12613,13 @@ Return ONLY valid JSON matching this schema:
|
|
|
12455
12613
|
}}
|
|
12456
12614
|
|
|
12457
12615
|
Rules:
|
|
12458
|
-
-
|
|
12459
|
-
- If
|
|
12460
|
-
- Do not list the same file in "files"
|
|
12461
|
-
- Only use file paths listed in the staged file inventory.
|
|
12462
|
-
- Only use hunk IDs listed in the staged hunk inventory.
|
|
12616
|
+
- Every staged file MUST be assigned exactly once across all groups, either via "files" OR via every one of its hunk IDs (never both).
|
|
12617
|
+
- If you assign any hunk for a file, you MUST assign EVERY hunk for that file across the groups — partial coverage is invalid.
|
|
12618
|
+
- Do not list the same file in "files" of more than one group, and do not assign the same hunk ID to more than one group.
|
|
12619
|
+
- Only use file paths listed in the staged file inventory. Do not invent files.
|
|
12620
|
+
- Only use hunk IDs listed in the staged hunk inventory. Do not invent hunk IDs.
|
|
12463
12621
|
- Prefer 2-5 commits unless the changes are truly all one topic.
|
|
12464
12622
|
- Keep commit titles concise and understandable.
|
|
12465
|
-
- Do not invent files.
|
|
12466
12623
|
|
|
12467
12624
|
Staged file inventory:
|
|
12468
12625
|
{file_inventory}
|
|
@@ -12474,7 +12631,10 @@ Condensed staged diff:
|
|
|
12474
12631
|
{summary}
|
|
12475
12632
|
|
|
12476
12633
|
Additional context:
|
|
12477
|
-
{additional_context}
|
|
12634
|
+
{additional_context}
|
|
12635
|
+
|
|
12636
|
+
Feedback on previous attempt (fix every item before responding):
|
|
12637
|
+
{previous_attempt_feedback}`);
|
|
12478
12638
|
function isCommitSplitCommand(argv) {
|
|
12479
12639
|
return Boolean(argv.split || argv.plan || argv.apply || argv._.includes('split'));
|
|
12480
12640
|
}
|
|
@@ -12493,9 +12653,6 @@ function formatCommitSplitPlan(plan) {
|
|
|
12493
12653
|
})
|
|
12494
12654
|
.join('\n\n---\n\n');
|
|
12495
12655
|
}
|
|
12496
|
-
function getStagedFileSet(changes) {
|
|
12497
|
-
return new Set(changes.map((change) => change.filePath));
|
|
12498
|
-
}
|
|
12499
12656
|
function getGroupFiles(group) {
|
|
12500
12657
|
return group.files || [];
|
|
12501
12658
|
}
|
|
@@ -12552,67 +12709,9 @@ function formatHunkInventory(inventory) {
|
|
|
12552
12709
|
.join('\n');
|
|
12553
12710
|
}
|
|
12554
12711
|
function validatePlanForStagedFiles(plan, staged, hunkInventory) {
|
|
12555
|
-
const
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
const unknown = [];
|
|
12559
|
-
const duplicate = [];
|
|
12560
|
-
const unknownHunks = [];
|
|
12561
|
-
const duplicateHunks = [];
|
|
12562
|
-
plan.groups.forEach((group) => {
|
|
12563
|
-
getGroupFiles(group).forEach((file) => {
|
|
12564
|
-
if (!stagedFiles.has(file)) {
|
|
12565
|
-
unknown.push(file);
|
|
12566
|
-
return;
|
|
12567
|
-
}
|
|
12568
|
-
if (seen.has(file)) {
|
|
12569
|
-
duplicate.push(file);
|
|
12570
|
-
return;
|
|
12571
|
-
}
|
|
12572
|
-
seen.add(file);
|
|
12573
|
-
});
|
|
12574
|
-
getGroupHunks(group).forEach((hunkId) => {
|
|
12575
|
-
const hunk = hunkInventory?.byId.get(hunkId);
|
|
12576
|
-
if (!hunk) {
|
|
12577
|
-
unknownHunks.push(hunkId);
|
|
12578
|
-
return;
|
|
12579
|
-
}
|
|
12580
|
-
if (seenHunks.has(hunkId)) {
|
|
12581
|
-
duplicateHunks.push(hunkId);
|
|
12582
|
-
return;
|
|
12583
|
-
}
|
|
12584
|
-
seenHunks.add(hunkId);
|
|
12585
|
-
});
|
|
12586
|
-
});
|
|
12587
|
-
const hunkCoveredFiles = new Set([...seenHunks].map((hunkId) => hunkInventory?.byId.get(hunkId)?.filePath));
|
|
12588
|
-
const mixedFiles = [...seen].filter((file) => hunkCoveredFiles.has(file));
|
|
12589
|
-
const partiallyCoveredFiles = [...hunkCoveredFiles]
|
|
12590
|
-
.filter((file) => Boolean(file))
|
|
12591
|
-
.filter((file) => {
|
|
12592
|
-
const fileHunks = hunkInventory?.byFile.get(file) || [];
|
|
12593
|
-
return fileHunks.some((hunk) => !seenHunks.has(hunk.id));
|
|
12594
|
-
});
|
|
12595
|
-
const missing = [...stagedFiles].filter((file) => !seen.has(file) && !hunkCoveredFiles.has(file));
|
|
12596
|
-
if (unknown.length ||
|
|
12597
|
-
duplicate.length ||
|
|
12598
|
-
unknownHunks.length ||
|
|
12599
|
-
duplicateHunks.length ||
|
|
12600
|
-
mixedFiles.length ||
|
|
12601
|
-
partiallyCoveredFiles.length ||
|
|
12602
|
-
missing.length) {
|
|
12603
|
-
throw new Error([
|
|
12604
|
-
unknown.length ? `unknown files: ${unknown.join(', ')}` : undefined,
|
|
12605
|
-
duplicate.length ? `duplicate files: ${duplicate.join(', ')}` : undefined,
|
|
12606
|
-
unknownHunks.length ? `unknown hunks: ${unknownHunks.join(', ')}` : undefined,
|
|
12607
|
-
duplicateHunks.length ? `duplicate hunks: ${duplicateHunks.join(', ')}` : undefined,
|
|
12608
|
-
mixedFiles.length ? `files assigned both as whole files and hunks: ${mixedFiles.join(', ')}` : undefined,
|
|
12609
|
-
partiallyCoveredFiles.length
|
|
12610
|
-
? `files with only some hunks assigned: ${partiallyCoveredFiles.join(', ')}`
|
|
12611
|
-
: undefined,
|
|
12612
|
-
missing.length ? `missing files: ${missing.join(', ')}` : undefined,
|
|
12613
|
-
]
|
|
12614
|
-
.filter(Boolean)
|
|
12615
|
-
.join('; '));
|
|
12712
|
+
const issues = getPlanValidationIssues(plan, staged, hunkInventory);
|
|
12713
|
+
if (hasPlanValidationIssues(issues)) {
|
|
12714
|
+
throw new Error(formatPlanValidationIssuesError(issues));
|
|
12616
12715
|
}
|
|
12617
12716
|
}
|
|
12618
12717
|
function assertNoUnstagedOverlap(plan, changes, hunkInventory) {
|
|
@@ -12716,22 +12815,26 @@ async function handleCommitSplit({ argv, config, git, logger, tokenizer, llm, })
|
|
|
12716
12815
|
.map((change) => `- ${change.filePath}: ${change.status} - ${change.summary}`)
|
|
12717
12816
|
.join('\n');
|
|
12718
12817
|
const hunkInventoryText = formatHunkInventory(hunkInventory);
|
|
12719
|
-
const plan = await
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12818
|
+
const { plan } = await generateValidatedCommitSplitPlan({
|
|
12819
|
+
llm,
|
|
12820
|
+
prompt: COMMIT_SPLIT_PROMPT,
|
|
12821
|
+
variables: {
|
|
12822
|
+
file_inventory: fileInventory,
|
|
12823
|
+
hunk_inventory: hunkInventoryText,
|
|
12824
|
+
summary,
|
|
12825
|
+
additional_context: argv.additional || '',
|
|
12826
|
+
},
|
|
12827
|
+
staged: changes.staged,
|
|
12828
|
+
hunkInventory,
|
|
12725
12829
|
logger,
|
|
12726
12830
|
tokenizer,
|
|
12727
12831
|
metadata: {
|
|
12728
|
-
task: 'commit-split-plan',
|
|
12729
12832
|
command: 'commit',
|
|
12730
12833
|
provider: config.service.provider,
|
|
12731
12834
|
model: String(config.service.model),
|
|
12732
12835
|
},
|
|
12836
|
+
maxAttempts: DEFAULT_MAX_PLAN_ATTEMPTS,
|
|
12733
12837
|
});
|
|
12734
|
-
validatePlanForStagedFiles(plan, changes.staged, hunkInventory);
|
|
12735
12838
|
if (argv.apply) {
|
|
12736
12839
|
return await applyCommitSplitPlan({
|
|
12737
12840
|
plan,
|