/**
* Service for detecting session patterns (new feature, bug fix, refactoring, etc.)
*/
import type { SessionNote, SessionPattern } from '../types/session.js';
interface PatternScore {
pattern: SessionPattern;
score: number;
reasons: string[];
}
/**
* Detect the primary pattern of the session
*/
export function detectPattern(note: SessionNote): { pattern: SessionPattern; confidence: number } {
const scores: PatternScore[] = [
analyzeNewFeature(note),
analyzeBugFix(note),
analyzeRefactoring(note),
analyzeDocumentation(note),
analyzeConfiguration(note),
analyzeTesting(note),
];
// Sort by score
scores.sort((a, b) => b.score - a.score);
const topScore = scores[0].score;
const secondScore = scores[1].score;
// If top two scores are close, it's mixed
if (topScore > 0 && secondScore > 0 && (topScore - secondScore) < 3) {
return { pattern: 'mixed', confidence: 0.6 };
}
// Calculate confidence (0-1)
const maxPossibleScore = 10;
const confidence = Math.min(topScore / maxPossibleScore, 1);
return {
pattern: scores[0].pattern,
confidence: Math.max(confidence, 0.1), // Minimum 0.1 confidence
};
}
function analyzeNewFeature(note: SessionNote): PatternScore {
let score = 0;
const reasons: string[] = [];
// Check summary/topic for keywords
const text = `${note.summary} ${note.topic || ''}`.toLowerCase();
if (text.includes('implement') || text.includes('add') || text.includes('create') || text.includes('new')) {
score += 3;
reasons.push('Contains implementation keywords');
}
// Check file changes
if (note.fileChanges) {
const created = note.fileChanges.filter(f => f.type === 'created');
if (created.length >= 3) {
score += 4;
reasons.push(`Created ${created.length} new files`);
} else if (created.length > 0) {
score += 2;
}
}
// Check tags
if (note.tags?.some(t => t.includes('feature') || t.includes('new'))) {
score += 2;
reasons.push('Tagged as feature');
}
return { pattern: 'new-feature', score, reasons };
}
function analyzeBugFix(note: SessionNote): PatternScore {
let score = 0;
const reasons: string[] = [];
const text = `${note.summary} ${note.topic || ''}`.toLowerCase();
if (text.includes('fix') || text.includes('bug') || text.includes('issue') || text.includes('error')) {
score += 4;
reasons.push('Contains bug fix keywords');
}
// Bug fixes usually modify existing files
if (note.fileChanges) {
const modified = note.fileChanges.filter(f => f.type === 'modified');
const created = note.fileChanges.filter(f => f.type === 'created');
if (modified.length > created.length) {
score += 3;
reasons.push('Mostly modified existing files');
}
}
if (note.tags?.some(t => t.includes('bug') || t.includes('fix'))) {
score += 2;
reasons.push('Tagged as bug fix');
}
return { pattern: 'bug-fix', score, reasons };
}
function analyzeRefactoring(note: SessionNote): PatternScore {
let score = 0;
const reasons: string[] = [];
const text = `${note.summary} ${note.topic || ''}`.toLowerCase();
if (text.includes('refactor') || text.includes('restructure') || text.includes('clean') || text.includes('improve')) {
score += 4;
reasons.push('Contains refactoring keywords');
}
// Refactoring often involves many file modifications
if (note.fileChanges) {
const modified = note.fileChanges.filter(f => f.type === 'modified');
if (modified.length >= 4) {
score += 3;
reasons.push(`Modified ${modified.length} files`);
}
}
if (note.tags?.some(t => t.includes('refactor'))) {
score += 2;
reasons.push('Tagged as refactoring');
}
return { pattern: 'refactoring', score, reasons };
}
function analyzeDocumentation(note: SessionNote): PatternScore {
let score = 0;
const reasons: string[] = [];
const text = `${note.summary} ${note.topic || ''}`.toLowerCase();
if (text.includes('document') || text.includes('readme') || text.includes('docs') || text.includes('comment')) {
score += 4;
reasons.push('Contains documentation keywords');
}
// Check for markdown files
if (note.fileChanges) {
const docFiles = note.fileChanges.filter(f =>
f.path.endsWith('.md') || f.path.endsWith('.mdx') || f.path.includes('README')
);
if (docFiles.length > 0) {
score += 4;
reasons.push(`Modified ${docFiles.length} documentation files`);
}
}
if (note.tags?.some(t => t.includes('doc') || t.includes('readme'))) {
score += 2;
reasons.push('Tagged as documentation');
}
return { pattern: 'documentation', score, reasons };
}
function analyzeConfiguration(note: SessionNote): PatternScore {
let score = 0;
const reasons: string[] = [];
const text = `${note.summary} ${note.topic || ''}`.toLowerCase();
if (text.includes('config') || text.includes('setup') || text.includes('install') || text.includes('dependencies')) {
score += 3;
reasons.push('Contains configuration keywords');
}
// Check for config files
if (note.fileChanges) {
const configFiles = note.fileChanges.filter(f =>
f.path.includes('package.json') ||
f.path.includes('tsconfig') ||
f.path.includes('config') ||
f.path.includes('.yml') ||
f.path.includes('.yaml') ||
f.path.includes('.json') ||
f.path.includes('.env')
);
if (configFiles.length >= 2) {
score += 4;
reasons.push(`Modified ${configFiles.length} config files`);
} else if (configFiles.length > 0) {
score += 2;
}
}
// Check for install/setup commands
if (note.commands) {
const setupCommands = note.commands.filter(c =>
c.command.includes('install') ||
c.command.includes('init') ||
c.command.includes('setup')
);
if (setupCommands.length > 0) {
score += 3;
reasons.push('Ran setup commands');
}
}
if (note.tags?.some(t => t.includes('config') || t.includes('setup'))) {
score += 2;
reasons.push('Tagged as configuration');
}
return { pattern: 'configuration', score, reasons };
}
function analyzeTesting(note: SessionNote): PatternScore {
let score = 0;
const reasons: string[] = [];
const text = `${note.summary} ${note.topic || ''}`.toLowerCase();
if (text.includes('test') || text.includes('spec')) {
score += 4;
reasons.push('Contains testing keywords');
}
// Check for test files
if (note.fileChanges) {
const testFiles = note.fileChanges.filter(f =>
f.path.includes('.test.') ||
f.path.includes('.spec.') ||
f.path.includes('__tests__') ||
f.path.includes('/test/')
);
if (testFiles.length > 0) {
score += 4;
reasons.push(`Modified ${testFiles.length} test files`);
}
}
// Check for test commands
if (note.commands) {
const testCommands = note.commands.filter(c =>
c.command.includes('test') || c.command.includes('jest') || c.command.includes('vitest')
);
if (testCommands.length > 0) {
score += 3;
reasons.push('Ran test commands');
}
}
if (note.tags?.some(t => t.includes('test'))) {
score += 2;
reasons.push('Tagged as testing');
}
return { pattern: 'testing', score, reasons };
}