diff --git a/src/application/services/MemoryContextLoader.ts b/src/application/services/MemoryContextLoader.ts index 42500e9f..47e1f9ae 100644 --- a/src/application/services/MemoryContextLoader.ts +++ b/src/application/services/MemoryContextLoader.ts @@ -2,17 +2,20 @@ * MemoryContextLoader * * Loads and filters memories for hook context injection. - * Delegates to IMemoryRepository for actual data access. + * Delegates to IMemoryRepository for general loads and + * IMemoryService for query-based searches (notes + trailers). */ import type { IMemoryContextLoader, IMemoryContextOptions, IMemoryContextResult } from '../../domain/interfaces/IMemoryContextLoader'; import type { IMemoryRepository } from '../../domain/interfaces/IMemoryRepository'; +import type { IMemoryService } from '../interfaces/IMemoryService'; import type { ILogger } from '../../domain/interfaces/ILogger'; export class MemoryContextLoader implements IMemoryContextLoader { constructor( private readonly memoryRepository: IMemoryRepository, private readonly logger?: ILogger, + private readonly memoryService?: IMemoryService, ) {} load(options?: IMemoryContextOptions): IMemoryContextResult { @@ -46,4 +49,32 @@ export class MemoryContextLoader implements IMemoryContextLoader { filtered: result.memories.length, }; } + + loadWithQuery(query: string, limit?: number, cwd?: string): IMemoryContextResult { + // Use MemoryService.recall() to search both notes and trailers + if (!this.memoryService) { + this.logger?.warn('MemoryService not available for query search, falling back to repository'); + return this.load({ limit, cwd }); + } + + // Get total count for stats + const allResult = this.memoryRepository.query({ cwd }); + const total = allResult.total; + + // Search with query + const result = this.memoryService.recall(query, { limit, cwd }); + + this.logger?.debug('Memories loaded with query', { + query, + total, + matched: result.memories.length, + limit, + }); + + return { + memories: result.memories, + total, + filtered: result.memories.length, + }; + } } diff --git a/src/domain/interfaces/IMemoryContextLoader.ts b/src/domain/interfaces/IMemoryContextLoader.ts index 738d1e68..eb7d4baa 100644 --- a/src/domain/interfaces/IMemoryContextLoader.ts +++ b/src/domain/interfaces/IMemoryContextLoader.ts @@ -30,4 +30,15 @@ export interface IMemoryContextResult { export interface IMemoryContextLoader { /** Load memories with optional filters. */ load(options?: IMemoryContextOptions): IMemoryContextResult; + + /** + * Load memories filtered by a search query. + * Searches both git notes and commit trailers for matching memories. + * + * @param query - Search keywords (e.g., "authentication, GIT-95, LoginHandler") + * @param limit - Maximum memories to return + * @param cwd - Working directory for git operations + * @returns Matching memories from both notes and trailers + */ + loadWithQuery(query: string, limit?: number, cwd?: string): IMemoryContextResult; } diff --git a/tests/unit/application/services/MemoryContextLoader.test.ts b/tests/unit/application/services/MemoryContextLoader.test.ts index 0280213e..469a3b72 100644 --- a/tests/unit/application/services/MemoryContextLoader.test.ts +++ b/tests/unit/application/services/MemoryContextLoader.test.ts @@ -163,4 +163,107 @@ describe('MemoryContextLoader', () => { assert.equal(result.filtered, 1); }); + + describe('loadWithQuery', () => { + function createMockMemoryService(memories: IMemoryEntity[]) { + return { + remember: () => memories[0]!, + recall: (query?: string, options?: { limit?: number; cwd?: string }) => { + let result = [...memories]; + if (query) { + result = result.filter(m => + m.content.toLowerCase().includes(query.toLowerCase()) + ); + } + if (options?.limit) { + result = result.slice(0, options.limit); + } + return { memories: result, total: memories.length }; + }, + get: () => null, + delete: () => false, + }; + } + + it('should search memories using MemoryService.recall', () => { + const memories = [ + createMemory({ id: '1', content: 'JWT authentication decision' }), + createMemory({ id: '2', content: 'Database migration gotcha' }), + ]; + const repo = createMockRepository(memories); + const service = createMockMemoryService(memories); + const loader = new MemoryContextLoader(repo, undefined, service); + + const result = loader.loadWithQuery('JWT'); + + assert.equal(result.filtered, 1); + assert.ok(result.memories[0]!.content.includes('JWT')); + }); + + it('should fall back to load() when MemoryService not available', () => { + const memories = [ + createMemory({ id: '1', content: 'Test memory' }), + createMemory({ id: '2', content: 'Another memory' }), + ]; + const repo = createMockRepository(memories); + const logger = createMockLogger(); + const loader = new MemoryContextLoader(repo, logger); + + const result = loader.loadWithQuery('anything'); + + assert.equal(result.total, 2); + assert.equal(result.filtered, 2); + assert.ok(logger.warnCalled, 'expected warning about fallback'); + }); + + it('should apply limit when using MemoryService', () => { + const memories = [ + createMemory({ id: '1', content: 'Auth memory' }), + createMemory({ id: '2', content: 'Auth decision' }), + createMemory({ id: '3', content: 'Auth gotcha' }), + ]; + const repo = createMockRepository(memories); + const service = createMockMemoryService(memories); + const loader = new MemoryContextLoader(repo, undefined, service); + + const result = loader.loadWithQuery('Auth', 2); + + assert.equal(result.filtered, 2); + }); + + it('should pass cwd to MemoryService', () => { + let capturedCwd: string | undefined; + const repo = createMockRepository([]); + const service = { + remember: () => createMemory(), + recall: (_query?: string, options?: { cwd?: string }) => { + capturedCwd = options?.cwd; + return { memories: [], total: 0 }; + }, + get: () => null, + delete: () => false, + }; + const loader = new MemoryContextLoader(repo, undefined, service); + + loader.loadWithQuery('test', 10, '/custom/path'); + + assert.equal(capturedCwd, '/custom/path'); + }); + + it('should return total from repository for accurate stats', () => { + const repoMemories = [ + createMemory({ id: '1' }), + createMemory({ id: '2' }), + createMemory({ id: '3' }), + ]; + const repo = createMockRepository(repoMemories); + const service = createMockMemoryService([createMemory({ id: '1' })]); + const loader = new MemoryContextLoader(repo, undefined, service); + + const result = loader.loadWithQuery('test'); + + assert.equal(result.total, 3, 'total should come from repository'); + assert.equal(result.filtered, 1, 'filtered should come from query result'); + }); + }); });