272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { AiDoc } from '../classes.aidoc.js';
|
|
import { ProjectContext } from './projectcontext.js';
|
|
import { DiffProcessor } from '../classes.diffprocessor.js';
|
|
import { logger } from '../logging.js';
|
|
|
|
export interface INextCommitObject {
|
|
recommendedNextVersionLevel: 'fix' | 'feat' | 'BREAKING CHANGE'; // the recommended next version level of the project
|
|
recommendedNextVersionScope: string; // the recommended scope name of the next version, like "core" or "cli", or specific class names.
|
|
recommendedNextVersionMessage: string; // the commit message. Don't put fix() feat() or BREAKING CHANGE in the message. Please just the message itself.
|
|
recommendedNextVersionDetails: string[]; // detailed bullet points for the changelog
|
|
recommendedNextVersion: string; // the recommended next version of the project, x.x.x
|
|
changelog?: string; // the changelog for the next version
|
|
}
|
|
|
|
export class Commit {
|
|
private aiDocsRef: AiDoc;
|
|
private projectDir: string;
|
|
|
|
constructor(aiDocsRef: AiDoc, projectDirArg: string) {
|
|
this.aiDocsRef = aiDocsRef;
|
|
this.projectDir = projectDirArg;
|
|
}
|
|
|
|
public async buildNextCommitObject(): Promise<INextCommitObject> {
|
|
const smartgitInstance = new plugins.smartgit.Smartgit();
|
|
await smartgitInstance.init();
|
|
const gitRepo = await plugins.smartgit.GitRepo.fromOpeningRepoDir(
|
|
smartgitInstance,
|
|
this.projectDir
|
|
);
|
|
|
|
// Define comprehensive exclusion patterns
|
|
// smartgit@3.3.0+ supports glob patterns natively
|
|
const excludePatterns = [
|
|
// Lock files
|
|
'pnpm-lock.yaml',
|
|
'package-lock.json',
|
|
'npm-shrinkwrap.json',
|
|
'yarn.lock',
|
|
'deno.lock',
|
|
'bun.lockb',
|
|
|
|
// Build artifacts (main culprit for large diffs!)
|
|
'dist/**',
|
|
'dist_*/**', // dist_ts, dist_web, etc.
|
|
'build/**',
|
|
'.next/**',
|
|
'out/**',
|
|
'public/dist/**',
|
|
|
|
// Compiled/bundled files
|
|
'**/*.js.map',
|
|
'**/*.d.ts.map',
|
|
'**/*.min.js',
|
|
'**/*.bundle.js',
|
|
'**/*.chunk.js',
|
|
|
|
// IDE/Editor directories
|
|
'.claude/**',
|
|
'.cursor/**',
|
|
'.vscode/**',
|
|
'.idea/**',
|
|
'**/*.swp',
|
|
'**/*.swo',
|
|
|
|
// Logs and caches
|
|
'.nogit/**',
|
|
'**/*.log',
|
|
'.cache/**',
|
|
'.rpt2_cache/**',
|
|
'coverage/**',
|
|
'.nyc_output/**',
|
|
];
|
|
|
|
// Pass glob patterns directly to smartgit - it handles matching internally
|
|
const diffStringArray = await gitRepo.getUncommittedDiff(excludePatterns);
|
|
|
|
// Process diffs intelligently using DiffProcessor
|
|
let processedDiffString: string;
|
|
|
|
if (diffStringArray.length > 0) {
|
|
// Diagnostic logging for raw diff statistics
|
|
const totalChars = diffStringArray.join('\n\n').length;
|
|
const estimatedTokens = Math.ceil(totalChars / 4);
|
|
|
|
console.log(`📊 Raw git diff statistics:`);
|
|
console.log(` Files changed: ${diffStringArray.length}`);
|
|
console.log(` Total characters: ${totalChars.toLocaleString()}`);
|
|
console.log(` Estimated tokens: ${estimatedTokens.toLocaleString()}`);
|
|
console.log(` Exclusion patterns: ${excludePatterns.length}`);
|
|
|
|
// Use DiffProcessor to intelligently handle large diffs
|
|
const diffProcessor = new DiffProcessor({
|
|
maxDiffTokens: 100000, // Reserve 100k tokens for diffs
|
|
smallFileLines: 300, // Most source files are under 300 lines
|
|
mediumFileLines: 800, // Only very large files get head/tail treatment
|
|
sampleHeadLines: 75, // When sampling, show more context
|
|
sampleTailLines: 75, // When sampling, show more context
|
|
});
|
|
|
|
const processedDiff = diffProcessor.processDiffs(diffStringArray);
|
|
processedDiffString = diffProcessor.formatForContext(processedDiff);
|
|
|
|
console.log(`📝 Processed diff statistics:`);
|
|
console.log(` Full diffs: ${processedDiff.fullDiffs.length} files`);
|
|
console.log(` Summarized: ${processedDiff.summarizedDiffs.length} files`);
|
|
console.log(` Metadata only: ${processedDiff.metadataOnly.length} files`);
|
|
console.log(` Final tokens: ${processedDiff.totalTokens.toLocaleString()}`);
|
|
|
|
if (estimatedTokens > 50000) {
|
|
console.log(`✅ DiffProcessor reduced token usage: ${estimatedTokens.toLocaleString()} → ${processedDiff.totalTokens.toLocaleString()}`);
|
|
}
|
|
} else {
|
|
processedDiffString = 'No changes.';
|
|
}
|
|
|
|
// Use DualAgentOrchestrator for commit message generation
|
|
// Note: No filesystem tool needed - the diff already contains all change information
|
|
const commitOrchestrator = new plugins.smartagent.DualAgentOrchestrator({
|
|
smartAiInstance: this.aiDocsRef.smartAiInstance,
|
|
defaultProvider: 'openai',
|
|
logPrefix: '[Commit]',
|
|
onProgress: (event) => logger.log(event.logLevel, event.logMessage),
|
|
guardianPolicyPrompt: `
|
|
You validate commit messages for semantic versioning compliance.
|
|
|
|
APPROVE if:
|
|
- Version level (fix/feat/BREAKING CHANGE) matches the scope of changes in the diff
|
|
- Commit message is clear, professional, and follows conventional commit conventions
|
|
- No personal information, licensing details, or AI mentions (Claude/Codex) included
|
|
- JSON structure is valid with all required fields
|
|
- Scope accurately reflects the changed modules/files
|
|
|
|
REJECT with specific feedback if:
|
|
- Version level doesn't match the scope of changes (e.g., "feat" for a typo fix should be "fix")
|
|
- Message is vague, unprofessional, or contains sensitive information
|
|
- JSON is malformed or missing required fields
|
|
`,
|
|
});
|
|
|
|
await commitOrchestrator.start();
|
|
|
|
const commitTaskPrompt = `
|
|
You create a commit message for a git commit.
|
|
Project directory: ${this.projectDir}
|
|
|
|
Analyze the git diff below to understand what changed and generate a commit message.
|
|
|
|
You should not include any licensing information or personal information.
|
|
Never mention CLAUDE code, or codex.
|
|
|
|
Important: Answer only in valid JSON.
|
|
|
|
Your answer should be parseable with JSON.parse() without modifying anything.
|
|
|
|
Here is the structure of the JSON you should return:
|
|
|
|
interface {
|
|
recommendedNextVersionLevel: 'fix' | 'feat' | 'BREAKING CHANGE'; // the recommended next version level
|
|
recommendedNextVersionScope: string; // scope name like "core", "cli", or specific class names
|
|
recommendedNextVersionMessage: string; // the commit message (don't include fix/feat prefix)
|
|
recommendedNextVersionDetails: string[]; // detailed bullet points for the changelog
|
|
recommendedNextVersion: string; // the recommended next version x.x.x
|
|
}
|
|
|
|
For recommendedNextVersionDetails, only add entries that have obvious value to the reader.
|
|
|
|
Here is the git diff showing what changed:
|
|
|
|
${processedDiffString}
|
|
|
|
Generate the commit message based on these changes.
|
|
`;
|
|
|
|
const commitResult = await commitOrchestrator.run(commitTaskPrompt);
|
|
await commitOrchestrator.stop();
|
|
|
|
if (!commitResult.success) {
|
|
throw new Error(`Commit message generation failed: ${commitResult.status}`);
|
|
}
|
|
|
|
const resultObject: INextCommitObject = JSON.parse(
|
|
commitResult.result.replace('```json', '').replace('```', '')
|
|
);
|
|
|
|
const previousChangelogPath = plugins.path.join(this.projectDir, 'changelog.md');
|
|
let previousChangelog: plugins.smartfile.SmartFile;
|
|
if (await plugins.fsInstance.file(previousChangelogPath).exists()) {
|
|
previousChangelog = await plugins.smartfileFactory.fromFilePath(previousChangelogPath);
|
|
}
|
|
|
|
if (!previousChangelog) {
|
|
// lets build the changelog based on that
|
|
const commitMessages = await gitRepo.getAllCommitMessages();
|
|
console.log(JSON.stringify(commitMessages, null, 2));
|
|
|
|
// Use DualAgentOrchestrator for changelog generation with Guardian validation
|
|
const changelogOrchestrator = new plugins.smartagent.DualAgentOrchestrator({
|
|
smartAiInstance: this.aiDocsRef.smartAiInstance,
|
|
defaultProvider: 'openai',
|
|
logPrefix: '[Changelog]',
|
|
onProgress: (event) => logger.log(event.logLevel, event.logMessage),
|
|
guardianPolicyPrompt: `
|
|
You validate changelog generation.
|
|
|
|
APPROVE if:
|
|
- Changelog follows proper markdown format with ## headers for each version
|
|
- Entries are chronologically ordered (newest first)
|
|
- Version ranges for trivial commits are properly summarized
|
|
- No duplicate or empty entries
|
|
- Format matches: ## yyyy-mm-dd - x.x.x - scope
|
|
|
|
REJECT with feedback if:
|
|
- Markdown formatting is incorrect
|
|
- Entries are not meaningful or helpful
|
|
- Dates or versions are malformed
|
|
`,
|
|
});
|
|
|
|
await changelogOrchestrator.start();
|
|
|
|
const changelogTaskPrompt = `
|
|
You are building a changelog.md file for the project.
|
|
Omit commits and versions that lack relevant changes, but make sure to mention them as a range with a summarizing message instead.
|
|
|
|
A changelog entry should look like this:
|
|
|
|
## yyyy-mm-dd - x.x.x - scope here
|
|
main descriptiom here
|
|
|
|
- detailed bullet points follow
|
|
|
|
You are given:
|
|
* the commit messages of the project
|
|
|
|
Only return the changelog file content, so it can be written directly to changelog.md.
|
|
|
|
Here are the commit messages:
|
|
|
|
${JSON.stringify(commitMessages, null, 2)}
|
|
`;
|
|
|
|
const changelogResult = await changelogOrchestrator.run(changelogTaskPrompt);
|
|
await changelogOrchestrator.stop();
|
|
|
|
if (!changelogResult.success) {
|
|
throw new Error(`Changelog generation failed: ${changelogResult.status}`);
|
|
}
|
|
|
|
previousChangelog = plugins.smartfileFactory.fromString(
|
|
previousChangelogPath,
|
|
changelogResult.result.replaceAll('```markdown', '').replaceAll('```', ''),
|
|
'utf8'
|
|
);
|
|
}
|
|
|
|
let oldChangelog = previousChangelog.contents.toString().replace('# Changelog\n\n', '');
|
|
if (oldChangelog.startsWith('\n')) {
|
|
oldChangelog = oldChangelog.replace('\n', '');
|
|
}
|
|
let newDateString = new plugins.smarttime.ExtendedDate().exportToHyphedSortableDate();
|
|
let newChangelog = `# Changelog\n\n${`## ${newDateString} - {{nextVersion}} - {{nextVersionScope}}
|
|
{{nextVersionMessage}}
|
|
|
|
{{nextVersionDetails}}`}\n\n${oldChangelog}`;
|
|
resultObject.changelog = newChangelog;
|
|
|
|
return resultObject;
|
|
}
|
|
}
|