@juspay/yama 2.0.0 → 2.2.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.
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Knowledge Base Manager
3
+ * Handles reading, writing, and parsing the knowledge base markdown file
4
+ */
5
+ import { readFile, writeFile, mkdir } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { createHash } from "crypto";
9
+ import { execFile } from "child_process";
10
+ import { promisify } from "util";
11
+ const execFileAsync = promisify(execFile);
12
+ import { CATEGORY_SECTION_NAMES, } from "./types.js";
13
+ /**
14
+ * Template for a new knowledge base file
15
+ */
16
+ const KNOWLEDGE_BASE_TEMPLATE = `# Project Knowledge Base
17
+ > Learned patterns, preferences, and guidelines from team feedback
18
+
19
+ ## Metadata
20
+ - Last Updated: {{TIMESTAMP}}
21
+ - Total Learnings: 0
22
+ - Last Summarization: N/A
23
+
24
+ ---
25
+
26
+ ## False Positives (Don't Flag These)
27
+
28
+ Things AI incorrectly flagged as issues. Avoid repeating these mistakes.
29
+
30
+ ---
31
+
32
+ ## Style Preferences (Team Conventions)
33
+
34
+ Project-specific coding conventions that differ from general best practices.
35
+
36
+ ---
37
+
38
+ ## Missed Issues (Should Have Flagged)
39
+
40
+ Patterns AI missed that should be caught in future reviews.
41
+
42
+ ---
43
+
44
+ ## Context & Domain Knowledge
45
+
46
+ Project-specific context AI needs for accurate reviews.
47
+
48
+ ---
49
+
50
+ ## Enhancement Guidelines
51
+
52
+ How AI should provide suggestions for this project.
53
+
54
+ `;
55
+ export class KnowledgeBaseManager {
56
+ config;
57
+ projectRoot;
58
+ constructor(config, projectRoot) {
59
+ this.config = config;
60
+ this.projectRoot = projectRoot || process.cwd();
61
+ }
62
+ /**
63
+ * Get the full path to the knowledge base file
64
+ */
65
+ getFilePath() {
66
+ return join(this.projectRoot, this.config.path);
67
+ }
68
+ /**
69
+ * Check if knowledge base file exists
70
+ */
71
+ exists() {
72
+ return existsSync(this.getFilePath());
73
+ }
74
+ /**
75
+ * Load and parse the knowledge base file
76
+ */
77
+ async load() {
78
+ if (!this.exists()) {
79
+ return this.createEmptyKnowledgeBase();
80
+ }
81
+ const content = await readFile(this.getFilePath(), "utf-8");
82
+ return this.parseMarkdown(content);
83
+ }
84
+ /**
85
+ * Append new learnings to the knowledge base
86
+ * Returns count of learnings actually added (excludes duplicates)
87
+ */
88
+ async append(learnings) {
89
+ const kb = await this.load();
90
+ let addedCount = 0;
91
+ for (const learning of learnings) {
92
+ // Check for duplicates
93
+ if (this.isDuplicate(kb, learning)) {
94
+ continue;
95
+ }
96
+ // Get or create section
97
+ let section = kb.sections.get(learning.category);
98
+ if (!section) {
99
+ section = {
100
+ category: learning.category,
101
+ subcategories: new Map(),
102
+ };
103
+ kb.sections.set(learning.category, section);
104
+ }
105
+ // Get or create subcategory
106
+ const subcatKey = learning.subcategory || "General";
107
+ let learningsList = section.subcategories.get(subcatKey);
108
+ if (!learningsList) {
109
+ learningsList = [];
110
+ section.subcategories.set(subcatKey, learningsList);
111
+ }
112
+ // Add the learning
113
+ learningsList.push(learning.learning);
114
+ addedCount++;
115
+ }
116
+ // Update metadata
117
+ kb.metadata.lastUpdated = new Date().toISOString();
118
+ kb.metadata.totalLearnings += addedCount;
119
+ // Write back
120
+ await this.write(kb);
121
+ return addedCount;
122
+ }
123
+ /**
124
+ * Write the knowledge base back to file
125
+ */
126
+ async write(kb) {
127
+ const content = this.toMarkdown(kb);
128
+ const filePath = this.getFilePath();
129
+ const dir = dirname(filePath);
130
+ // Ensure directory exists
131
+ if (!existsSync(dir)) {
132
+ await mkdir(dir, { recursive: true });
133
+ }
134
+ await writeFile(filePath, content, "utf-8");
135
+ }
136
+ /**
137
+ * Write raw markdown content directly to file
138
+ * Used by summarization to write AI-generated consolidated content
139
+ */
140
+ async writeRaw(content) {
141
+ const filePath = this.getFilePath();
142
+ const dir = dirname(filePath);
143
+ // Ensure directory exists
144
+ if (!existsSync(dir)) {
145
+ await mkdir(dir, { recursive: true });
146
+ }
147
+ await writeFile(filePath, content, "utf-8");
148
+ }
149
+ /**
150
+ * Create a new knowledge base file from template
151
+ */
152
+ async create() {
153
+ const content = KNOWLEDGE_BASE_TEMPLATE.replace("{{TIMESTAMP}}", new Date().toISOString());
154
+ const filePath = this.getFilePath();
155
+ const dir = dirname(filePath);
156
+ if (!existsSync(dir)) {
157
+ await mkdir(dir, { recursive: true });
158
+ }
159
+ await writeFile(filePath, content, "utf-8");
160
+ }
161
+ /**
162
+ * Get knowledge base content formatted for AI prompt injection
163
+ */
164
+ async getForPrompt() {
165
+ if (!this.config.enabled || !this.exists()) {
166
+ return null;
167
+ }
168
+ try {
169
+ const content = await readFile(this.getFilePath(), "utf-8");
170
+ // Remove metadata section for cleaner prompt
171
+ const lines = content.split("\n");
172
+ const filteredLines = [];
173
+ let inMetadata = false;
174
+ for (const line of lines) {
175
+ if (line.startsWith("## Metadata")) {
176
+ inMetadata = true;
177
+ continue;
178
+ }
179
+ if (inMetadata && line.startsWith("---")) {
180
+ inMetadata = false;
181
+ continue;
182
+ }
183
+ if (!inMetadata) {
184
+ filteredLines.push(line);
185
+ }
186
+ }
187
+ return filteredLines.join("\n").trim();
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
193
+ /**
194
+ * Get count of learnings in the knowledge base
195
+ */
196
+ async getLearningCount() {
197
+ const kb = await this.load();
198
+ return kb.metadata.totalLearnings;
199
+ }
200
+ /**
201
+ * Check if summarization is needed based on entry count
202
+ */
203
+ async needsSummarization() {
204
+ const count = await this.getLearningCount();
205
+ return count >= this.config.maxEntriesBeforeSummarization;
206
+ }
207
+ /**
208
+ * Commit the knowledge base file to git
209
+ * Uses execFile with argument arrays to prevent command injection
210
+ */
211
+ async commit(prId, learningsAdded) {
212
+ const filePath = this.config.path; // Relative path for git
213
+ // Validate inputs to prevent injection
214
+ const safePrId = Math.floor(Number(prId));
215
+ const safeLearningsAdded = Math.floor(Number(learningsAdded));
216
+ if (!Number.isFinite(safePrId) || safePrId < 0) {
217
+ throw new Error("Invalid PR ID");
218
+ }
219
+ try {
220
+ // Stage the file using execFile with args array (safe from injection)
221
+ await execFileAsync("git", ["add", filePath], { cwd: this.projectRoot });
222
+ // Create commit message
223
+ const commitMessage = `chore(yama): update knowledge base from PR #${safePrId}
224
+
225
+ Added ${safeLearningsAdded} new learning${safeLearningsAdded !== 1 ? "s" : ""}.
226
+
227
+ 🤖 Generated with Yama`;
228
+ // Commit using execFile with args array (safe from injection)
229
+ await execFileAsync("git", ["commit", "-m", commitMessage], {
230
+ cwd: this.projectRoot,
231
+ });
232
+ }
233
+ catch (error) {
234
+ throw new Error(`Failed to commit knowledge base: ${error instanceof Error ? error.message : String(error)}`);
235
+ }
236
+ }
237
+ /**
238
+ * Generate a hash for deduplication
239
+ */
240
+ generateLearningId(learning) {
241
+ return createHash("md5")
242
+ .update(learning.toLowerCase().trim())
243
+ .digest("hex")
244
+ .substring(0, 12);
245
+ }
246
+ // ============================================================================
247
+ // Private Methods
248
+ // ============================================================================
249
+ /**
250
+ * Create an empty knowledge base structure
251
+ */
252
+ createEmptyKnowledgeBase() {
253
+ return {
254
+ metadata: {
255
+ lastUpdated: new Date().toISOString(),
256
+ totalLearnings: 0,
257
+ },
258
+ sections: new Map(),
259
+ };
260
+ }
261
+ /**
262
+ * Check if a learning already exists in the knowledge base
263
+ */
264
+ isDuplicate(kb, learning) {
265
+ const section = kb.sections.get(learning.category);
266
+ if (!section) {
267
+ return false;
268
+ }
269
+ const normalizedNew = learning.learning.toLowerCase().trim();
270
+ for (const [, learnings] of section.subcategories) {
271
+ for (const existing of learnings) {
272
+ const normalizedExisting = existing.toLowerCase().trim();
273
+ // Check for exact match or high similarity
274
+ if (normalizedExisting === normalizedNew ||
275
+ this.isSimilar(normalizedExisting, normalizedNew)) {
276
+ return true;
277
+ }
278
+ }
279
+ }
280
+ return false;
281
+ }
282
+ /**
283
+ * Check if two learnings are similar (simple similarity check)
284
+ */
285
+ isSimilar(a, b) {
286
+ // Remove common words and check overlap
287
+ const wordsA = new Set(a.split(/\s+/).filter((w) => w.length > 3));
288
+ const wordsB = new Set(b.split(/\s+/).filter((w) => w.length > 3));
289
+ if (wordsA.size === 0 || wordsB.size === 0) {
290
+ return false;
291
+ }
292
+ let overlap = 0;
293
+ for (const word of wordsA) {
294
+ if (wordsB.has(word)) {
295
+ overlap++;
296
+ }
297
+ }
298
+ const similarity = overlap / Math.max(wordsA.size, wordsB.size);
299
+ return similarity > 0.7; // 70% word overlap = similar
300
+ }
301
+ /**
302
+ * Parse markdown content into structured knowledge base
303
+ */
304
+ parseMarkdown(content) {
305
+ const kb = this.createEmptyKnowledgeBase();
306
+ const lines = content.split("\n");
307
+ let currentCategory = null;
308
+ let currentSubcategory = "General";
309
+ let inMetadata = false;
310
+ for (const line of lines) {
311
+ const trimmed = line.trim();
312
+ // Parse metadata
313
+ if (trimmed.startsWith("## Metadata")) {
314
+ inMetadata = true;
315
+ continue;
316
+ }
317
+ if (inMetadata) {
318
+ if (trimmed.startsWith("---")) {
319
+ inMetadata = false;
320
+ continue;
321
+ }
322
+ if (trimmed.startsWith("- Last Updated:")) {
323
+ kb.metadata.lastUpdated = trimmed
324
+ .replace("- Last Updated:", "")
325
+ .trim();
326
+ }
327
+ else if (trimmed.startsWith("- Total Learnings:")) {
328
+ kb.metadata.totalLearnings =
329
+ parseInt(trimmed.replace("- Total Learnings:", "").trim(), 10) || 0;
330
+ }
331
+ else if (trimmed.startsWith("- Last Summarization:")) {
332
+ const value = trimmed.replace("- Last Summarization:", "").trim();
333
+ if (value !== "N/A") {
334
+ kb.metadata.lastSummarization = value;
335
+ }
336
+ }
337
+ continue;
338
+ }
339
+ // Parse category headers (## level)
340
+ if (trimmed.startsWith("## ")) {
341
+ const sectionName = trimmed.substring(3);
342
+ currentCategory = this.categoryFromSectionName(sectionName);
343
+ currentSubcategory = "General";
344
+ if (currentCategory) {
345
+ kb.sections.set(currentCategory, {
346
+ category: currentCategory,
347
+ subcategories: new Map(),
348
+ });
349
+ }
350
+ continue;
351
+ }
352
+ // Parse subcategory headers (### level)
353
+ if (trimmed.startsWith("### ")) {
354
+ currentSubcategory = trimmed.substring(4);
355
+ continue;
356
+ }
357
+ // Parse learning entries (- bullet points)
358
+ if (trimmed.startsWith("- ") && currentCategory) {
359
+ const learning = trimmed.substring(2);
360
+ const section = kb.sections.get(currentCategory);
361
+ if (section) {
362
+ let learnings = section.subcategories.get(currentSubcategory);
363
+ if (!learnings) {
364
+ learnings = [];
365
+ section.subcategories.set(currentSubcategory, learnings);
366
+ }
367
+ learnings.push(learning);
368
+ }
369
+ }
370
+ }
371
+ return kb;
372
+ }
373
+ /**
374
+ * Convert category section name back to category enum
375
+ */
376
+ categoryFromSectionName(name) {
377
+ for (const [category, sectionName] of Object.entries(CATEGORY_SECTION_NAMES)) {
378
+ if (name.includes(sectionName) || sectionName.includes(name)) {
379
+ return category;
380
+ }
381
+ }
382
+ // Fallback matching
383
+ const lowerName = name.toLowerCase();
384
+ if (lowerName.includes("false positive") ||
385
+ lowerName.includes("don't flag")) {
386
+ return "false_positive";
387
+ }
388
+ if (lowerName.includes("missed") || lowerName.includes("should have")) {
389
+ return "missed_issue";
390
+ }
391
+ if (lowerName.includes("style") || lowerName.includes("convention")) {
392
+ return "style_preference";
393
+ }
394
+ if (lowerName.includes("context") || lowerName.includes("domain")) {
395
+ return "domain_context";
396
+ }
397
+ if (lowerName.includes("enhancement") || lowerName.includes("guideline")) {
398
+ return "enhancement_guideline";
399
+ }
400
+ return null;
401
+ }
402
+ /**
403
+ * Convert knowledge base structure to markdown
404
+ */
405
+ toMarkdown(kb) {
406
+ const lines = [];
407
+ // Header
408
+ lines.push("# Project Knowledge Base");
409
+ lines.push("> Learned patterns, preferences, and guidelines from team feedback");
410
+ lines.push("");
411
+ // Metadata
412
+ lines.push("## Metadata");
413
+ lines.push(`- Last Updated: ${kb.metadata.lastUpdated}`);
414
+ lines.push(`- Total Learnings: ${kb.metadata.totalLearnings}`);
415
+ lines.push(`- Last Summarization: ${kb.metadata.lastSummarization || "N/A"}`);
416
+ lines.push("");
417
+ lines.push("---");
418
+ lines.push("");
419
+ // Sections in order
420
+ const categoryOrder = [
421
+ "false_positive",
422
+ "missed_issue",
423
+ "style_preference",
424
+ "domain_context",
425
+ "enhancement_guideline",
426
+ ];
427
+ for (const category of categoryOrder) {
428
+ const sectionName = CATEGORY_SECTION_NAMES[category];
429
+ lines.push(`## ${sectionName}`);
430
+ lines.push("");
431
+ const section = kb.sections.get(category);
432
+ if (section && section.subcategories.size > 0) {
433
+ // Sort subcategories
434
+ const sortedSubcats = Array.from(section.subcategories.entries()).sort(([a], [b]) => a.localeCompare(b));
435
+ for (const [subcategory, learnings] of sortedSubcats) {
436
+ if (subcategory !== "General") {
437
+ lines.push(`### ${subcategory}`);
438
+ }
439
+ for (const learning of learnings) {
440
+ lines.push(`- ${learning}`);
441
+ }
442
+ lines.push("");
443
+ }
444
+ }
445
+ else {
446
+ // Add description placeholder for empty sections
447
+ lines.push(this.getSectionDescription(category));
448
+ lines.push("");
449
+ }
450
+ lines.push("---");
451
+ lines.push("");
452
+ }
453
+ return lines.join("\n");
454
+ }
455
+ /**
456
+ * Get description text for empty sections
457
+ */
458
+ getSectionDescription(category) {
459
+ switch (category) {
460
+ case "false_positive":
461
+ return "Things AI incorrectly flagged as issues. Avoid repeating these mistakes.";
462
+ case "missed_issue":
463
+ return "Patterns AI missed that should be caught in future reviews.";
464
+ case "style_preference":
465
+ return "Project-specific coding conventions that differ from general best practices.";
466
+ case "domain_context":
467
+ return "Project-specific context AI needs for accurate reviews.";
468
+ case "enhancement_guideline":
469
+ return "How AI should provide suggestions for this project.";
470
+ default:
471
+ return "";
472
+ }
473
+ }
474
+ }
475
+ //# sourceMappingURL=KnowledgeBaseManager.js.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Learning Types
3
+ * Type definitions for the knowledge base and learning extraction system
4
+ */
5
+ /**
6
+ * Categories for extracted learnings
7
+ * Maps to sections in the knowledge base file
8
+ */
9
+ export type LearningCategory = "false_positive" | "missed_issue" | "style_preference" | "domain_context" | "enhancement_guideline";
10
+ /**
11
+ * Human-readable category names for knowledge base sections
12
+ */
13
+ export declare const CATEGORY_SECTION_NAMES: Record<LearningCategory, string>;
14
+ /**
15
+ * A single learning extracted from PR feedback
16
+ */
17
+ export interface ExtractedLearning {
18
+ /** Unique hash for deduplication */
19
+ id: string;
20
+ /** Category of the learning */
21
+ category: LearningCategory;
22
+ /** Sub-category within the section (e.g., "Async Patterns", "Security") */
23
+ subcategory?: string;
24
+ /** The actionable, project-level guideline */
25
+ learning: string;
26
+ /** File patterns where this applies (e.g., ["services/*.ts"]) */
27
+ filePatterns?: string[];
28
+ /** Severity for missed_issue learnings */
29
+ severity?: string;
30
+ /** Source info for traceability (not displayed in KB) */
31
+ sourceInfo?: {
32
+ prId: number;
33
+ timestamp: string;
34
+ };
35
+ }
36
+ /**
37
+ * Metadata section of the knowledge base
38
+ */
39
+ export interface KnowledgeBaseMetadata {
40
+ lastUpdated: string;
41
+ totalLearnings: number;
42
+ lastSummarization?: string;
43
+ }
44
+ /**
45
+ * A section in the knowledge base (maps to a category)
46
+ */
47
+ export interface KnowledgeBaseSection {
48
+ category: LearningCategory;
49
+ subcategories: Map<string, string[]>;
50
+ }
51
+ /**
52
+ * Full parsed knowledge base structure
53
+ */
54
+ export interface KnowledgeBase {
55
+ metadata: KnowledgeBaseMetadata;
56
+ sections: Map<LearningCategory, KnowledgeBaseSection>;
57
+ }
58
+ /**
59
+ * Request for the learn command
60
+ */
61
+ export interface LearnRequest {
62
+ workspace: string;
63
+ repository: string;
64
+ pullRequestId: number;
65
+ dryRun?: boolean;
66
+ commit?: boolean;
67
+ summarize?: boolean;
68
+ outputPath?: string;
69
+ outputFormat?: "md" | "json";
70
+ }
71
+ /**
72
+ * Result from the learn command
73
+ */
74
+ export interface LearnResult {
75
+ success: boolean;
76
+ prId: number;
77
+ learningsFound: number;
78
+ learningsAdded: number;
79
+ learningsDuplicate: number;
80
+ learnings: ExtractedLearning[];
81
+ knowledgeBasePath?: string;
82
+ committed?: boolean;
83
+ summarized?: boolean;
84
+ error?: string;
85
+ }
86
+ /**
87
+ * A comment from a PR
88
+ */
89
+ export interface PRComment {
90
+ id: number;
91
+ text: string;
92
+ author: {
93
+ name: string;
94
+ displayName?: string;
95
+ email?: string;
96
+ };
97
+ createdAt: string;
98
+ filePath?: string;
99
+ lineNumber?: number;
100
+ parentId?: number;
101
+ }
102
+ /**
103
+ * A pair of AI comment and developer reply
104
+ */
105
+ export interface CommentPair {
106
+ aiComment: PRComment;
107
+ developerReply: PRComment;
108
+ filePath?: string;
109
+ codeContext?: string;
110
+ }
111
+ /**
112
+ * Output format from AI learning extraction
113
+ */
114
+ export interface AIExtractionOutput {
115
+ category: LearningCategory;
116
+ subcategory?: string;
117
+ learning: string;
118
+ filePatterns?: string[];
119
+ reasoning: string;
120
+ }
121
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Learning Types
3
+ * Type definitions for the knowledge base and learning extraction system
4
+ */
5
+ /**
6
+ * Human-readable category names for knowledge base sections
7
+ */
8
+ export const CATEGORY_SECTION_NAMES = {
9
+ false_positive: "False Positives (Don't Flag These)",
10
+ missed_issue: "Missed Issues (Should Have Flagged)",
11
+ style_preference: "Style Preferences (Team Conventions)",
12
+ domain_context: "Context & Domain Knowledge",
13
+ enhancement_guideline: "Enhancement Guidelines",
14
+ };
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Langfuse Prompt Manager
3
+ * Fetches prompts from Langfuse Prompt Management with local fallbacks
4
+ *
5
+ * Prompt Names in Langfuse:
6
+ * - yama-review: Review system prompt
7
+ * - yama-enhancement: Enhancement system prompt
8
+ */
9
+ export declare class LangfusePromptManager {
10
+ private client;
11
+ private initialized;
12
+ constructor();
13
+ /**
14
+ * Initialize Langfuse client if credentials are available
15
+ */
16
+ private initializeClient;
17
+ /**
18
+ * Get the review system prompt
19
+ * Fetches from Langfuse if available, otherwise returns local fallback
20
+ */
21
+ getReviewPrompt(): Promise<string>;
22
+ /**
23
+ * Get the enhancement system prompt
24
+ * Fetches from Langfuse if available, otherwise returns local fallback
25
+ */
26
+ getEnhancementPrompt(): Promise<string>;
27
+ /**
28
+ * Get the learning extraction prompt
29
+ * Fetches from Langfuse if available, otherwise returns local fallback
30
+ * Langfuse prompt name: "yama-learning"
31
+ */
32
+ getLearningPrompt(): Promise<string>;
33
+ /**
34
+ * Get the summarization prompt
35
+ * Fetches from Langfuse if available, otherwise returns local fallback
36
+ * Langfuse prompt name: "yama-summarization"
37
+ */
38
+ getSummarizationPrompt(): Promise<string>;
39
+ /**
40
+ * Check if Langfuse is enabled
41
+ */
42
+ isEnabled(): boolean;
43
+ /**
44
+ * Shutdown Langfuse client gracefully
45
+ */
46
+ shutdown(): Promise<void>;
47
+ }
48
+ //# sourceMappingURL=LangfusePromptManager.d.ts.map