@entro314labs/ai-changelog-generator 3.0.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +801 -0
  2. package/LICENSE +21 -0
  3. package/README.md +393 -0
  4. package/ai-changelog-mcp.sh +93 -0
  5. package/ai-changelog.sh +103 -0
  6. package/bin/ai-changelog-dxt.js +35 -0
  7. package/bin/ai-changelog-mcp.js +34 -0
  8. package/bin/ai-changelog.js +18 -0
  9. package/package.json +135 -0
  10. package/src/ai-changelog-generator.js +258 -0
  11. package/src/application/orchestrators/changelog.orchestrator.js +730 -0
  12. package/src/application/services/application.service.js +301 -0
  13. package/src/cli.js +157 -0
  14. package/src/domains/ai/ai-analysis.service.js +486 -0
  15. package/src/domains/analysis/analysis.engine.js +445 -0
  16. package/src/domains/changelog/changelog.service.js +1761 -0
  17. package/src/domains/changelog/workspace-changelog.service.js +505 -0
  18. package/src/domains/git/git-repository.analyzer.js +588 -0
  19. package/src/domains/git/git.service.js +302 -0
  20. package/src/infrastructure/cli/cli.controller.js +517 -0
  21. package/src/infrastructure/config/configuration.manager.js +538 -0
  22. package/src/infrastructure/interactive/interactive-workflow.service.js +444 -0
  23. package/src/infrastructure/mcp/mcp-server.service.js +540 -0
  24. package/src/infrastructure/metrics/metrics.collector.js +362 -0
  25. package/src/infrastructure/providers/core/base-provider.js +184 -0
  26. package/src/infrastructure/providers/implementations/anthropic.js +329 -0
  27. package/src/infrastructure/providers/implementations/azure.js +296 -0
  28. package/src/infrastructure/providers/implementations/bedrock.js +393 -0
  29. package/src/infrastructure/providers/implementations/dummy.js +112 -0
  30. package/src/infrastructure/providers/implementations/google.js +320 -0
  31. package/src/infrastructure/providers/implementations/huggingface.js +301 -0
  32. package/src/infrastructure/providers/implementations/lmstudio.js +189 -0
  33. package/src/infrastructure/providers/implementations/mock.js +275 -0
  34. package/src/infrastructure/providers/implementations/ollama.js +151 -0
  35. package/src/infrastructure/providers/implementations/openai.js +273 -0
  36. package/src/infrastructure/providers/implementations/vertex.js +438 -0
  37. package/src/infrastructure/providers/provider-management.service.js +415 -0
  38. package/src/infrastructure/providers/provider-manager.service.js +363 -0
  39. package/src/infrastructure/providers/utils/base-provider-helpers.js +660 -0
  40. package/src/infrastructure/providers/utils/model-config.js +610 -0
  41. package/src/infrastructure/providers/utils/provider-utils.js +286 -0
  42. package/src/shared/constants/colors.js +370 -0
  43. package/src/shared/utils/cli-entry-utils.js +525 -0
  44. package/src/shared/utils/error-classes.js +423 -0
  45. package/src/shared/utils/json-utils.js +318 -0
  46. package/src/shared/utils/utils.js +1997 -0
  47. package/types/index.d.ts +464 -0
@@ -0,0 +1,505 @@
1
+ import { getWorkingDirectoryChanges, summarizeFileChanges, categorizeFile, detectLanguage, assessFileImportance } from '../../shared/utils/utils.js';
2
+ import colors from '../../shared/constants/colors.js';
3
+
4
+ /**
5
+ * Workspace Changelog Service
6
+ *
7
+ * Handles changelog generation from working directory changes
8
+ */
9
+ export class WorkspaceChangelogService {
10
+ constructor(aiAnalysisService, gitService = null) {
11
+ this.aiAnalysisService = aiAnalysisService;
12
+ this.gitService = gitService;
13
+ }
14
+
15
+ async generateComprehensiveWorkspaceChangelog(options = {}) {
16
+ try {
17
+
18
+ // Get working directory changes as raw array
19
+ const rawChanges = getWorkingDirectoryChanges();
20
+
21
+
22
+ if (!rawChanges || !Array.isArray(rawChanges) || rawChanges.length === 0) {
23
+ console.log(colors.infoMessage('No changes detected in working directory.'));
24
+ return null;
25
+ }
26
+
27
+ // Enhanced analysis of changes with diff content for AI analysis
28
+ const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges);
29
+ const changesSummary = summarizeFileChanges(enhancedChanges);
30
+
31
+ // Generate workspace context
32
+ const workspaceContext = this.generateWorkspaceContext(enhancedChanges, changesSummary);
33
+
34
+ // Generate changelog content
35
+ const changelog = await this.generateChangelogContent(
36
+ enhancedChanges,
37
+ changesSummary,
38
+ workspaceContext,
39
+ options.analysisMode || 'standard'
40
+ );
41
+
42
+ return {
43
+ changelog,
44
+ changes: enhancedChanges,
45
+ summary: changesSummary,
46
+ context: workspaceContext
47
+ };
48
+
49
+ } catch (error) {
50
+ console.error(colors.errorMessage('Workspace changelog generation failed:'), error.message);
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ async generateAIChangelogContentFromChanges(changes, changesSummary, analysisMode = 'standard') {
56
+ if (!this.aiAnalysisService.hasAI) {
57
+ console.log(colors.infoMessage('AI not available, using rule-based analysis...'));
58
+ return this.generateBasicChangelogContentFromChanges(changes, changesSummary);
59
+ }
60
+
61
+ try {
62
+ // Build comprehensive prompt with ALL change details
63
+ const prompt = `Generate a comprehensive AI changelog for the following working directory changes:
64
+
65
+ **Analysis Mode**: ${analysisMode}
66
+ **Total Files**: ${changesSummary.totalFiles}
67
+ **Categories**: ${Object.keys(changesSummary.categories).join(', ')}
68
+
69
+ **Files by category with details**:
70
+ ${Object.entries(changesSummary.categories).map(([cat, files]) =>
71
+ `**${cat}**: ${files.map(f => {
72
+ // Find the full change object with diff content
73
+ const fullChange = changes.find(change => (change.path || change.filePath) === (f.path || f.filePath));
74
+ const diffPreview = fullChange?.diff ?
75
+ (fullChange.diff.length > 200 ? fullChange.diff.substring(0, 200) + '...' : fullChange.diff) :
76
+ 'No diff available';
77
+ return `${f.status} ${f.path} (${diffPreview.replace(/\n/g, ' ')})`;
78
+ }).join('\n ')}`
79
+ ).join('\n')}
80
+
81
+ CRITICAL INSTRUCTIONS FOR ANALYSIS:
82
+ 1. **ONLY DESCRIBE CHANGES VISIBLE IN THE DIFF CONTENT** - Do not invent or assume changes
83
+ 2. **BE FACTUAL AND PRECISE** - Only mention specific lines, functions, imports that you can see
84
+ 3. **NO ASSUMPTIONS OR SPECULATION** - If you can't see it in the diff, don't mention it
85
+ 4. **STICK TO OBSERVABLE FACTS** - Describe what was added, removed, or modified line by line
86
+ 5. **DO NOT MAKE UP INTEGRATION DETAILS** - Don't assume files work together unless explicitly shown
87
+
88
+ STRICT FORMATTING REQUIREMENTS:
89
+ Generate working directory change entries based ONLY on visible diff content:
90
+ - (type) brief factual description - List only the specific changes you can see in the diffs
91
+
92
+ EXAMPLES of CORRECT FACTUAL FORMAT:
93
+ ✅ (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude and Llama models.
94
+
95
+ ✅ (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-3-5-sonnet-20241022' to 'claude-sonnet-4-20250514'. Added new model entries with updated capabilities.
96
+
97
+ EXAMPLES of FORBIDDEN ASSUMPTIONS:
98
+ ❌ "Updated other providers to recognize bedrock" (not visible in diff)
99
+ ❌ "Refactored provider selection logic" (not shown in actual changes)
100
+ ❌ "Improved integration across the system" (speculation)
101
+ ❌ "Enhanced error handling throughout" (assumption)
102
+
103
+ ONLY describe what you can literally see in the diff content. Do not invent connections or integrations.`;
104
+
105
+ // Make AI call with all the context
106
+ const messages = [
107
+ {
108
+ role: "system",
109
+ content: "You are an expert at analyzing code changes and generating factual changelog entries. You MUST only describe changes that are visible in the provided diff content. Never make assumptions, never invent integrations, never speculate about how files work together. Be precise and factual - only describe what you can literally see in the diffs."
110
+ },
111
+ {
112
+ role: "user",
113
+ content: prompt
114
+ }
115
+ ];
116
+
117
+ const options = {
118
+ max_tokens: analysisMode === 'enterprise' ? 2000 : analysisMode === 'detailed' ? 1500 : 1000,
119
+ temperature: 0.3
120
+ };
121
+
122
+
123
+ const response = await this.aiAnalysisService.aiProvider.generateCompletion(messages, options);
124
+
125
+ let changelog = response.content || response.text;
126
+
127
+ // Add metadata
128
+ const timestamp = new Date().toISOString().split('T')[0];
129
+
130
+ // Ensure proper changelog format
131
+ if (!changelog.includes('# ')) {
132
+ changelog = `# Working Directory Changelog - ${timestamp}\n\n${changelog}`;
133
+ }
134
+
135
+ // Add generation metadata
136
+ changelog += `\n\n---\n\n*Generated from ${changesSummary.totalFiles} working directory changes*\n`;
137
+
138
+ return changelog;
139
+
140
+ } catch (error) {
141
+ // Specific error guidance for AI failures
142
+ if (error.message.includes('fetch failed') || error.message.includes('ECONNREFUSED')) {
143
+ console.warn(colors.warningMessage('⚠️ Cannot connect to AI provider'));
144
+ console.warn(colors.infoMessage('💡 Check internet connection and provider service status'));
145
+ } else if (error.message.includes('API key') || error.message.includes('401')) {
146
+ console.warn(colors.warningMessage('⚠️ API authentication failed'));
147
+ console.warn(colors.infoMessage('💡 Run: ai-changelog init'));
148
+ } else if (error.message.includes('rate limit')) {
149
+ console.warn(colors.warningMessage('⚠️ Rate limit exceeded'));
150
+ console.warn(colors.infoMessage('💡 Wait a moment before retrying'));
151
+ } else {
152
+ console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`));
153
+ }
154
+
155
+ console.warn(colors.infoMessage('🔄 Falling back to pattern-based analysis'));
156
+ return this.generateBasicChangelogContentFromChanges(changes, changesSummary);
157
+ }
158
+ }
159
+
160
+ generateBasicChangelogContentFromChanges(changes, changesSummary) {
161
+ const timestamp = new Date().toISOString().split('T')[0];
162
+
163
+ let changelog = `# Working Directory Changes - ${timestamp}\n\n`;
164
+
165
+ // Basic summary
166
+ changelog += `## Summary\n`;
167
+ changelog += `${changes.length} files modified across ${Object.keys(changesSummary.categories).length} categories.\n\n`;
168
+
169
+ // Changes by category
170
+ changelog += this.buildChangesByCategory(changes, changesSummary);
171
+
172
+ // Basic recommendations
173
+ changelog += `## Recommendations\n`;
174
+ changelog += `- Review changes before committing\n`;
175
+ changelog += `- Consider adding tests for new functionality\n`;
176
+ changelog += `- Update documentation if needed\n\n`;
177
+
178
+ return changelog;
179
+ }
180
+
181
+ async enhanceChangesWithDiff(changes) {
182
+ const enhancedChanges = [];
183
+
184
+ for (const change of changes) {
185
+ let enhancedChange = {
186
+ ...change,
187
+ category: categorizeFile(change.path || change.filePath),
188
+ language: detectLanguage(change.path || change.filePath),
189
+ importance: assessFileImportance(change.path || change.filePath, change.status),
190
+ enhanced: true
191
+ };
192
+
193
+ // Get diff content if git service is available
194
+ if (this.gitService) {
195
+ try {
196
+ const diffAnalysis = await this.gitService.analyzeWorkingDirectoryFileChange(
197
+ change.status,
198
+ change.path || change.filePath
199
+ );
200
+
201
+ if (diffAnalysis) {
202
+ enhancedChange.diff = diffAnalysis.diff;
203
+ enhancedChange.beforeContent = diffAnalysis.beforeContent;
204
+ enhancedChange.afterContent = diffAnalysis.afterContent;
205
+ enhancedChange.semanticChanges = diffAnalysis.semanticChanges;
206
+ enhancedChange.functionalImpact = diffAnalysis.functionalImpact;
207
+ enhancedChange.complexity = diffAnalysis.complexity;
208
+ }
209
+ } catch (error) {
210
+ console.warn(`Failed to get diff for ${change.path || change.filePath}:`, error.message);
211
+ }
212
+ }
213
+
214
+ enhancedChanges.push(enhancedChange);
215
+ }
216
+
217
+ return enhancedChanges;
218
+ }
219
+
220
+
221
+ generateWorkspaceContext(changes, summary) {
222
+ const context = {
223
+ totalFiles: changes.length,
224
+ categories: Object.keys(summary.categories),
225
+ primaryCategory: this.getPrimaryCategory(summary.categories),
226
+ riskLevel: this.assessWorkspaceRisk(changes),
227
+ complexity: this.assessWorkspaceComplexity(changes),
228
+ recommendations: this.generateRecommendations(changes)
229
+ };
230
+
231
+ return context;
232
+ }
233
+
234
+ getPrimaryCategory(categories) {
235
+ return Object.entries(categories)
236
+ .sort(([,a], [,b]) => b.length - a.length)[0]?.[0] || 'other';
237
+ }
238
+
239
+ assessWorkspaceRisk(changes) {
240
+ const highRiskFiles = changes.filter(change =>
241
+ change.importance === 'critical' ||
242
+ change.category === 'configuration' ||
243
+ change.status === 'D'
244
+ );
245
+
246
+ if (highRiskFiles.length > changes.length * 0.3) return 'high';
247
+ if (highRiskFiles.length > 0) return 'medium';
248
+ return 'low';
249
+ }
250
+
251
+ assessWorkspaceComplexity(changes) {
252
+ if (changes.length > 20) return 'high';
253
+ if (changes.length > 5) return 'medium';
254
+ return 'low';
255
+ }
256
+
257
+ generateRecommendations(changes) {
258
+ const recommendations = [];
259
+
260
+ const hasTests = changes.some(change => change.category === 'tests');
261
+ const hasSource = changes.some(change => change.category === 'source');
262
+ const hasConfig = changes.some(change => change.category === 'configuration');
263
+ const hasDocs = changes.some(change => change.category === 'documentation');
264
+
265
+ if (hasSource && !hasTests) {
266
+ recommendations.push('Consider adding tests for source code changes');
267
+ }
268
+
269
+ if (hasConfig) {
270
+ recommendations.push('Review configuration changes carefully');
271
+ }
272
+
273
+ if (hasSource && !hasDocs) {
274
+ recommendations.push('Update documentation for new features');
275
+ }
276
+
277
+ if (changes.length > 15) {
278
+ recommendations.push('Consider breaking this into smaller commits');
279
+ }
280
+
281
+ const deletedFiles = changes.filter(change => change.status === 'D');
282
+ if (deletedFiles.length > 0) {
283
+ recommendations.push(`Review ${deletedFiles.length} deleted files before committing`);
284
+ }
285
+
286
+ return recommendations;
287
+ }
288
+
289
+ buildChangesByCategory(changes, changesSummary) {
290
+ let content = `## Changes by Category\n\n`;
291
+
292
+ Object.entries(changesSummary.categories).forEach(([category, files]) => {
293
+ const categoryIcon = this.getCategoryIcon(category);
294
+ content += `### ${categoryIcon} ${category.charAt(0).toUpperCase() + category.slice(1)} (${files.length} files)\n\n`;
295
+
296
+ files.forEach(file => {
297
+ const statusIcon = this.getStatusIcon(file.status);
298
+ content += `- ${statusIcon} ${file.path}\n`;
299
+ });
300
+
301
+ content += '\n';
302
+ });
303
+
304
+ return content;
305
+ }
306
+
307
+ getCategoryIcon(category) {
308
+ const icons = {
309
+ 'source': '💻',
310
+ 'tests': '🧪',
311
+ 'documentation': '📚',
312
+ 'configuration': '⚙️',
313
+ 'frontend': '🎨',
314
+ 'assets': '🖼️',
315
+ 'build': '🔧',
316
+ 'other': '📄'
317
+ };
318
+ return icons[category] || '📄';
319
+ }
320
+
321
+ getStatusIcon(status) {
322
+ const icons = {
323
+ 'A': '➕', // Added
324
+ 'M': '✏️', // Modified
325
+ 'D': '❌', // Deleted
326
+ 'R': '📝', // Renamed
327
+ 'C': '📋' // Copied
328
+ };
329
+ return icons[status] || '📄';
330
+ }
331
+
332
+ async generateChangelogContent(changes, summary, context, analysisMode) {
333
+ if (analysisMode === 'detailed' || analysisMode === 'enterprise') {
334
+ return await this.generateAIChangelogContentFromChanges(changes, summary, analysisMode);
335
+ } else {
336
+ return this.generateBasicChangelogContentFromChanges(changes, summary);
337
+ }
338
+ }
339
+
340
+ // Integration with main changelog service
341
+ async generateCommitStyleWorkingDirectoryEntries(options = {}) {
342
+ try {
343
+ // Use provided working directory analysis or get current changes
344
+ let rawChanges;
345
+ if (options.workingDirAnalysis && options.workingDirAnalysis.changes) {
346
+ rawChanges = options.workingDirAnalysis.changes;
347
+ } else {
348
+ rawChanges = getWorkingDirectoryChanges();
349
+ }
350
+
351
+ if (!rawChanges || !Array.isArray(rawChanges) || rawChanges.length === 0) {
352
+ return { entries: [] };
353
+ }
354
+
355
+ // Enhanced analysis of changes with diff content for AI analysis
356
+ const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges);
357
+ const changesSummary = summarizeFileChanges(enhancedChanges);
358
+
359
+ // Build prompt for commit-style entries
360
+ const prompt = `Generate working directory change entries in the SAME FORMAT as git commits:
361
+
362
+ **Analysis Mode**: ${options.analysisMode || 'standard'}
363
+ **Total Files**: ${changesSummary.totalFiles}
364
+ **Categories**: ${Object.keys(changesSummary.categories).join(', ')}
365
+
366
+ **Files by category with details**:
367
+ ${Object.entries(changesSummary.categories).map(([cat, files]) =>
368
+ `**${cat}**: ${files.map(f => {
369
+ // Find the full change object with diff content
370
+ const fullChange = enhancedChanges.find(change => (change.path || change.filePath) === (f.path || f.filePath));
371
+ const diffPreview = fullChange?.diff ?
372
+ (fullChange.diff.length > 200 ? fullChange.diff.substring(0, 200) + '...' : fullChange.diff) :
373
+ 'No diff available';
374
+ return `${f.status} ${f.path} (${diffPreview.replace(/\n/g, ' ')})`;
375
+ }).join('\n ')}`
376
+ ).join('\n')}
377
+
378
+ STRICT FORMATTING REQUIREMENTS:
379
+ Generate working directory change entries based ONLY on visible diff content:
380
+ - (type) brief factual description - List only the specific changes you can see in the diffs
381
+
382
+ Where:
383
+ - type = feature, fix, refactor, docs, chore, etc. based on the actual changes
384
+ - brief description = concise summary of what was actually changed
385
+ - Detailed explanation = specific lines, functions, imports that were added/removed/modified
386
+
387
+ EXAMPLES of CORRECT FACTUAL FORMAT:
388
+ - (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude and Llama models.
389
+
390
+ - (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-3-5-sonnet-20241022' to 'claude-sonnet-4-20250514'. Added new model entries in getAvailableModels() method.
391
+
392
+ FORBIDDEN - DO NOT MAKE ASSUMPTIONS:
393
+ ❌ Do not mention "integration" unless you see actual integration code
394
+ ❌ Do not mention "provider selection logic" unless you see that specific code
395
+ ❌ Do not assume files work together unless explicitly shown in diffs
396
+
397
+ Generate one entry per file or logical change group. Only describe what you can literally see.`;
398
+
399
+ // Make AI call
400
+ const messages = [
401
+ {
402
+ role: "system",
403
+ content: "You are an expert at analyzing code changes and generating factual commit-style changelog entries. You MUST only describe changes that are visible in the provided diff content. Never make assumptions, never invent integrations, never speculate. Be precise and factual - only describe what you can literally see in the diffs."
404
+ },
405
+ {
406
+ role: "user",
407
+ content: prompt
408
+ }
409
+ ];
410
+
411
+ const options_ai = {
412
+ max_tokens: 1000,
413
+ temperature: 0.3
414
+ };
415
+
416
+
417
+ const response = await this.aiAnalysisService.aiProvider.generateCompletion(messages, options_ai);
418
+
419
+ let content = response.content || response.text;
420
+
421
+ // Parse entries from response
422
+ const entries = content.split('\n')
423
+ .filter(line => line.trim().startsWith('-'))
424
+ .map(line => line.trim());
425
+
426
+ return {
427
+ entries,
428
+ changes: enhancedChanges,
429
+ summary: changesSummary
430
+ };
431
+
432
+ } catch (error) {
433
+ // Provide specific guidance based on error type
434
+ if (error.message.includes('fetch failed') || error.message.includes('connection')) {
435
+ console.warn(colors.warningMessage('⚠️ AI provider connection failed'));
436
+ console.warn(colors.infoMessage('💡 Check your internet connection and provider configuration'));
437
+ } else if (error.message.includes('API key') || error.message.includes('401')) {
438
+ console.warn(colors.warningMessage('⚠️ Authentication failed'));
439
+ console.warn(colors.infoMessage('💡 Run `ai-changelog init` to configure your API key'));
440
+ } else {
441
+ console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`));
442
+ console.warn(colors.infoMessage('💡 Using basic file change detection instead'));
443
+ }
444
+
445
+ // Return basic entries from the changes we were given instead of getting fresh ones
446
+ const fallbackChanges = rawChanges || getWorkingDirectoryChanges();
447
+ const basicEntries = fallbackChanges.map(change => {
448
+ const filePath = change.filePath || change.path || 'unknown file';
449
+ const status = change.status || 'M';
450
+ const changeType = status === 'M' ? 'update' : status === 'A' ? 'feature' : status === 'D' ? 'remove' : 'chore';
451
+ const changeDesc = status === 'M' ? 'updated' : status === 'A' ? 'added' : status === 'D' ? 'deleted' : 'changed';
452
+ return `- (${changeType}) Modified ${filePath} - File ${changeDesc} (pattern-based analysis)`;
453
+ });
454
+
455
+ return { entries: basicEntries };
456
+ }
457
+ }
458
+
459
+ async generateWorkspaceChangelog(version = null, options = {}) {
460
+ const result = await this.generateComprehensiveWorkspaceChangelog(options);
461
+
462
+ if (!result) {
463
+ return null;
464
+ }
465
+
466
+ let changelog = result.changelog;
467
+
468
+ // Add version information if provided
469
+ if (version) {
470
+ changelog = changelog.replace(
471
+ /# Working Directory Changelog/,
472
+ `# Working Directory Changelog - Version ${version}`
473
+ );
474
+ }
475
+
476
+ // Add context information for detailed modes
477
+ if (options.analysisMode === 'detailed' || options.analysisMode === 'enterprise') {
478
+ changelog += this.generateContextSection(result.context);
479
+ }
480
+
481
+ return {
482
+ ...result,
483
+ changelog,
484
+ version
485
+ };
486
+ }
487
+
488
+ generateContextSection(context) {
489
+ let section = `## Context Analysis\n\n`;
490
+ section += `- **Total Files:** ${context.totalFiles}\n`;
491
+ section += `- **Primary Category:** ${context.primaryCategory}\n`;
492
+ section += `- **Risk Level:** ${context.riskLevel}\n`;
493
+ section += `- **Complexity:** ${context.complexity}\n\n`;
494
+
495
+ if (context.recommendations.length > 0) {
496
+ section += `### Recommendations\n\n`;
497
+ context.recommendations.forEach(rec => {
498
+ section += `- ${rec}\n`;
499
+ });
500
+ section += '\n';
501
+ }
502
+
503
+ return section;
504
+ }
505
+ }