@vheins/local-memory-mcp 0.1.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.
- package/DASHBOARD.md +129 -0
- package/HYBRID_SEARCH.md +204 -0
- package/IMPLEMENTATION.md +159 -0
- package/README.md +175 -0
- package/dist/capabilities.d.ts +22 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +23 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/dashboard/dashboard.test.d.ts +2 -0
- package/dist/dashboard/dashboard.test.d.ts.map +1 -0
- package/dist/dashboard/dashboard.test.js +362 -0
- package/dist/dashboard/dashboard.test.js.map +1 -0
- package/dist/dashboard/public/app.js +1187 -0
- package/dist/dashboard/public/chart.js +0 -0
- package/dist/dashboard/public/index.html +967 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +297 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/mcp/client.d.ts +34 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +181 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/client.test.d.ts +2 -0
- package/dist/mcp/client.test.d.ts.map +1 -0
- package/dist/mcp/client.test.js +130 -0
- package/dist/mcp/client.test.js.map +1 -0
- package/dist/prompts/registry.d.ts +39 -0
- package/dist/prompts/registry.d.ts.map +1 -0
- package/dist/prompts/registry.js +90 -0
- package/dist/prompts/registry.js.map +1 -0
- package/dist/resources/index.d.ts +17 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +100 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/index.test.d.ts +2 -0
- package/dist/resources/index.test.d.ts.map +1 -0
- package/dist/resources/index.test.js +96 -0
- package/dist/resources/index.test.js.map +1 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +60 -0
- package/dist/router.js.map +1 -0
- package/dist/router.test.d.ts +2 -0
- package/dist/router.test.d.ts.map +1 -0
- package/dist/router.test.js +113 -0
- package/dist/router.test.js.map +1 -0
- package/dist/search_memory_example.d.ts +3 -0
- package/dist/search_memory_example.d.ts.map +1 -0
- package/dist/search_memory_example.js +56 -0
- package/dist/search_memory_example.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +91 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/sqlite.d.ts +95 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +537 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/storage/sqlite.test.d.ts +2 -0
- package/dist/storage/sqlite.test.d.ts.map +1 -0
- package/dist/storage/sqlite.test.js +358 -0
- package/dist/storage/sqlite.test.js.map +1 -0
- package/dist/storage/vectors.stub.d.ts +12 -0
- package/dist/storage/vectors.stub.d.ts.map +1 -0
- package/dist/storage/vectors.stub.js +88 -0
- package/dist/storage/vectors.stub.js.map +1 -0
- package/dist/store_memory_example.d.ts +3 -0
- package/dist/store_memory_example.d.ts.map +1 -0
- package/dist/store_memory_example.js +69 -0
- package/dist/store_memory_example.js.map +1 -0
- package/dist/test_quotes_client.d.ts +3 -0
- package/dist/test_quotes_client.d.ts.map +1 -0
- package/dist/test_quotes_client.js +72 -0
- package/dist/test_quotes_client.js.map +1 -0
- package/dist/tools/memory.delete.d.ts +9 -0
- package/dist/tools/memory.delete.d.ts.map +1 -0
- package/dist/tools/memory.delete.js +22 -0
- package/dist/tools/memory.delete.js.map +1 -0
- package/dist/tools/memory.recap.d.ts +4 -0
- package/dist/tools/memory.recap.d.ts.map +1 -0
- package/dist/tools/memory.recap.js +42 -0
- package/dist/tools/memory.recap.js.map +1 -0
- package/dist/tools/memory.search.d.ts +5 -0
- package/dist/tools/memory.search.d.ts.map +1 -0
- package/dist/tools/memory.search.js +192 -0
- package/dist/tools/memory.search.js.map +1 -0
- package/dist/tools/memory.search.test.d.ts +2 -0
- package/dist/tools/memory.search.test.d.ts.map +1 -0
- package/dist/tools/memory.search.test.js +181 -0
- package/dist/tools/memory.search.test.js.map +1 -0
- package/dist/tools/memory.store.d.ts +5 -0
- package/dist/tools/memory.store.d.ts.map +1 -0
- package/dist/tools/memory.store.js +41 -0
- package/dist/tools/memory.store.js.map +1 -0
- package/dist/tools/memory.summarize.d.ts +4 -0
- package/dist/tools/memory.summarize.d.ts.map +1 -0
- package/dist/tools/memory.summarize.js +13 -0
- package/dist/tools/memory.summarize.js.map +1 -0
- package/dist/tools/memory.update.d.ts +5 -0
- package/dist/tools/memory.update.d.ts.map +1 -0
- package/dist/tools/memory.update.js +31 -0
- package/dist/tools/memory.update.js.map +1 -0
- package/dist/tools/schemas.d.ts +334 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +251 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/git-scope.d.ts +8 -0
- package/dist/utils/git-scope.d.ts.map +1 -0
- package/dist/utils/git-scope.js +38 -0
- package/dist/utils/git-scope.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.test.d.ts +2 -0
- package/dist/utils/logger.test.d.ts.map +1 -0
- package/dist/utils/logger.test.js +84 -0
- package/dist/utils/logger.test.js.map +1 -0
- package/dist/utils/mcp-response.d.ts +44 -0
- package/dist/utils/mcp-response.d.ts.map +1 -0
- package/dist/utils/mcp-response.js +81 -0
- package/dist/utils/mcp-response.js.map +1 -0
- package/dist/utils/normalize.d.ts +4 -0
- package/dist/utils/normalize.d.ts.map +1 -0
- package/dist/utils/normalize.js +51 -0
- package/dist/utils/normalize.js.map +1 -0
- package/dist/utils/normalize.test.d.ts +2 -0
- package/dist/utils/normalize.test.d.ts.map +1 -0
- package/dist/utils/normalize.test.js +159 -0
- package/dist/utils/normalize.test.js.map +1 -0
- package/dist/utils/query-expander.d.ts +2 -0
- package/dist/utils/query-expander.d.ts.map +1 -0
- package/dist/utils/query-expander.js +50 -0
- package/dist/utils/query-expander.js.map +1 -0
- package/dist/utils/query-expander.test.d.ts +2 -0
- package/dist/utils/query-expander.test.d.ts.map +1 -0
- package/dist/utils/query-expander.test.js +35 -0
- package/dist/utils/query-expander.test.js.map +1 -0
- package/docs/PRD.md +199 -0
- package/docs/PROMPT-agent.md +139 -0
- package/docs/SPEC-git-scope.md +172 -0
- package/docs/SPEC-heuristics.md +199 -0
- package/docs/SPEC-server.md +243 -0
- package/docs/SPEC-skeleton.md +255 -0
- package/docs/SPEC-sqlite-schema.md +183 -0
- package/docs/SPEC-tool-schema.md +201 -0
- package/docs/SPEC-vector-search.md +198 -0
- package/docs/TEST-scenarios.md +179 -0
- package/package.json +43 -0
- package/scripts/update-null-titles-ai.mjs +272 -0
- package/scripts/update-titles-batch.mjs +71 -0
- package/scripts/update-titles.mjs +66 -0
- package/seed-data.mjs +151 -0
- package/src/capabilities.ts +22 -0
- package/src/dashboard/dashboard.test.ts +546 -0
- package/src/dashboard/public/app.js +1187 -0
- package/src/dashboard/public/chart.js +0 -0
- package/src/dashboard/public/index.html +967 -0
- package/src/dashboard/server.ts +347 -0
- package/src/mcp/client.test.ts +164 -0
- package/src/mcp/client.ts +212 -0
- package/src/prompts/registry.ts +89 -0
- package/src/resources/index.test.ts +132 -0
- package/src/resources/index.ts +113 -0
- package/src/router.test.ts +145 -0
- package/src/router.ts +80 -0
- package/src/server.ts +99 -0
- package/src/storage/sqlite.test.ts +504 -0
- package/src/storage/sqlite.ts +688 -0
- package/src/storage/vectors.stub.ts +101 -0
- package/src/tools/memory.delete.ts +37 -0
- package/src/tools/memory.recap.ts +61 -0
- package/src/tools/memory.search.test.ts +276 -0
- package/src/tools/memory.search.ts +244 -0
- package/src/tools/memory.store.ts +56 -0
- package/src/tools/memory.summarize.ts +23 -0
- package/src/tools/memory.update.ts +46 -0
- package/src/tools/schemas.ts +261 -0
- package/src/types.ts +36 -0
- package/src/utils/git-scope.ts +42 -0
- package/src/utils/logger.test.ts +125 -0
- package/src/utils/logger.ts +53 -0
- package/src/utils/mcp-response.ts +116 -0
- package/src/utils/normalize.test.ts +203 -0
- package/src/utils/normalize.ts +53 -0
- package/src/utils/query-expander.test.ts +40 -0
- package/src/utils/query-expander.ts +60 -0
- package/storage/.gitkeep +5 -0
- package/test.sh +48 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { VectorStore, VectorResult } from "../types.js";
|
|
2
|
+
import { SQLiteStore } from "./sqlite.js";
|
|
3
|
+
|
|
4
|
+
// Simple vector store using SQLite - lightweight embeddings without ollama
|
|
5
|
+
export class StubVectorStore implements VectorStore {
|
|
6
|
+
private db: SQLiteStore;
|
|
7
|
+
|
|
8
|
+
constructor(db?: SQLiteStore) {
|
|
9
|
+
if (!db) {
|
|
10
|
+
throw new Error("SQLiteStore required for vector operations");
|
|
11
|
+
}
|
|
12
|
+
this.db = db;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Generate simple text-based vector (TF-IDF style) without external embeddings
|
|
16
|
+
private generateTextVector(text: string): string[] {
|
|
17
|
+
const normalized = text.toLowerCase()
|
|
18
|
+
// Remove punctuation and special characters, but keep Indonesian characters
|
|
19
|
+
.replace(/[^\w\s\u00C0-\u017F]/g, ' ')
|
|
20
|
+
// Normalize multiple spaces to single space
|
|
21
|
+
.replace(/\s+/g, ' ')
|
|
22
|
+
.trim()
|
|
23
|
+
.split(/\s+/)
|
|
24
|
+
.filter(word => word.length > 2);
|
|
25
|
+
|
|
26
|
+
const stopwords = new Set([
|
|
27
|
+
// English stopwords
|
|
28
|
+
'the', 'is', 'at', 'which', 'on', 'and', 'or', 'but', 'for', 'with',
|
|
29
|
+
'to', 'from', 'by', 'as', 'in', 'of', 'a', 'an', 'be', 'this', 'that',
|
|
30
|
+
'are', 'was', 'were', 'been', 'have', 'has', 'had', 'do', 'does', 'did',
|
|
31
|
+
// Indonesian stopwords
|
|
32
|
+
'yang', 'di', 'ke', 'dari', 'untuk', 'dengan', 'oleh', 'pada', 'dalam', 'atas',
|
|
33
|
+
'bawah', 'depan', 'belakang', 'samping', 'sebelah', 'antara', 'diantara', 'melalui',
|
|
34
|
+
'selama', 'sampai', 'hingga', 'sejak', 'sebelum', 'sesudah', 'setelah', 'sebelumnya',
|
|
35
|
+
'kemudian', 'selanjutnya', 'lagi', 'juga', 'pun', 'bahkan', 'malah', 'bahwa',
|
|
36
|
+
'karena', 'sebab', 'oleh', 'karena', 'sehingga', 'maka', 'lalu', 'kemudian',
|
|
37
|
+
'saya', 'kamu', 'dia', 'kami', 'kalian', 'mereka', 'aku', 'engkau', 'ia', 'kita',
|
|
38
|
+
'anda', 'beliau', 'mereka', 'siapa', 'apa', 'dimana', 'kapan', 'bagaimana', 'mengapa',
|
|
39
|
+
'berapa', 'banyak', 'sedikit', 'semua', 'beberapa', 'banyak', 'sedikit', 'hampir',
|
|
40
|
+
'hanya', 'sudah', 'belum', 'masih', 'lagi', 'selalu', 'kadang', 'sering', 'jarang',
|
|
41
|
+
'pernah', 'belum', 'sudah', 'akan', 'sedang', 'telah', 'baru', 'lama', 'cepat',
|
|
42
|
+
'lambat', 'besar', 'kecil', 'panjang', 'pendek', 'tinggi', 'rendah', 'lebar', 'sempit',
|
|
43
|
+
'tebal', 'tipis', 'berat', 'ringan', 'kuat', 'lemah', 'baik', 'buruk', 'benar', 'salah',
|
|
44
|
+
'cantik', 'jelek', 'indah', 'buruk', 'bagus', 'jelek', 'suka', 'tidak', 'bukan',
|
|
45
|
+
'jangan', 'harus', 'boleh', 'bisa', 'mampu', 'dapat', 'mau', 'ingin', 'perlu', 'penting',
|
|
46
|
+
// Additional common Indonesian words
|
|
47
|
+
'dan', 'atau', 'tapi', 'namun', 'lalu', 'kemudian', 'jadi', 'maka', 'yaitu', 'yakni'
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
return normalized.filter(word => !stopwords.has(word));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Calculate similarity between two token sets
|
|
54
|
+
private calculateSimilarity(tokens1: string[], tokens2: string[]): number {
|
|
55
|
+
if (tokens1.length === 0 || tokens2.length === 0) return 0;
|
|
56
|
+
|
|
57
|
+
const set1 = new Set(tokens1);
|
|
58
|
+
const set2 = new Set(tokens2);
|
|
59
|
+
|
|
60
|
+
// Jaccard similarity: intersection / union
|
|
61
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
62
|
+
const union = new Set([...set1, ...set2]);
|
|
63
|
+
|
|
64
|
+
return union.size > 0 ? intersection.size / union.size : 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async upsert(id: string, text: string): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
// Generate simple vector from text tokens
|
|
70
|
+
const tokens = this.generateTextVector(text);
|
|
71
|
+
|
|
72
|
+
// Store tokens as JSON array for better retrieval
|
|
73
|
+
this.db.upsertVectorEmbedding(id, tokens);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error("Error upserting vector:", error);
|
|
76
|
+
// Silently fail - vector is optional for search fallback
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async remove(id: string): Promise<void> {
|
|
81
|
+
// Vectors are automatically removed by CASCADE when memory is deleted
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async search(query: string, limit: number): Promise<VectorResult[]> {
|
|
85
|
+
try {
|
|
86
|
+
// Get all memories and compute similarity to query
|
|
87
|
+
const queryTokens = this.generateTextVector(query);
|
|
88
|
+
|
|
89
|
+
if (queryTokens.length === 0) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// For now, return empty - we'll use similarity search in SQLite instead
|
|
94
|
+
// In production, you could implement approximate nearest neighbor search here
|
|
95
|
+
return [];
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error("Error searching vectors:", error);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SQLiteStore } from "../storage/sqlite.js";
|
|
3
|
+
import { VectorStore } from "../types.js";
|
|
4
|
+
import { createMcpResponse, McpResponse } from "../utils/mcp-response.js";
|
|
5
|
+
|
|
6
|
+
export const MemoryDeleteSchema = z.object({
|
|
7
|
+
id: z.string().uuid()
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export async function handleMemoryDelete(
|
|
11
|
+
params: any,
|
|
12
|
+
db: SQLiteStore,
|
|
13
|
+
vectors: VectorStore
|
|
14
|
+
): Promise<McpResponse> {
|
|
15
|
+
// Validate input
|
|
16
|
+
const validated = MemoryDeleteSchema.parse(params);
|
|
17
|
+
|
|
18
|
+
// Check if memory exists
|
|
19
|
+
const existing = db.getById(validated.id);
|
|
20
|
+
if (!existing) {
|
|
21
|
+
throw new Error(`Memory not found: ${validated.id}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Delete from SQLite
|
|
25
|
+
db.delete(validated.id);
|
|
26
|
+
|
|
27
|
+
// Delete from vector store
|
|
28
|
+
await vectors.remove(validated.id);
|
|
29
|
+
|
|
30
|
+
// Log the delete action
|
|
31
|
+
db.logAction('delete', existing.scope.repo, { memoryId: validated.id, resultCount: 1 });
|
|
32
|
+
|
|
33
|
+
return createMcpResponse(
|
|
34
|
+
{ success: true },
|
|
35
|
+
`Deleted memory ${validated.id.slice(0, 8)}...`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { MemoryRecapSchema } from "./schemas.js";
|
|
2
|
+
import { SQLiteStore } from "../storage/sqlite.js";
|
|
3
|
+
import { createMcpResponse, McpResponse } from "../utils/mcp-response.js";
|
|
4
|
+
|
|
5
|
+
export async function handleMemoryRecap(
|
|
6
|
+
params: any,
|
|
7
|
+
db: SQLiteStore
|
|
8
|
+
): Promise<McpResponse> {
|
|
9
|
+
// Validate input
|
|
10
|
+
const validated = MemoryRecapSchema.parse(params);
|
|
11
|
+
|
|
12
|
+
// Get total count for pagination metadata
|
|
13
|
+
const total = db.getTotalCount(validated.repo);
|
|
14
|
+
|
|
15
|
+
// Get recent memories using public API (no type-unsafe cast)
|
|
16
|
+
const rows = db.getRecentMemories(validated.repo, validated.limit, validated.offset);
|
|
17
|
+
|
|
18
|
+
if (rows.length === 0) {
|
|
19
|
+
return createMcpResponse(
|
|
20
|
+
{
|
|
21
|
+
repo: validated.repo,
|
|
22
|
+
count: 0,
|
|
23
|
+
total,
|
|
24
|
+
offset: validated.offset,
|
|
25
|
+
memories: [],
|
|
26
|
+
message: `No memories found for repo: ${validated.repo}`
|
|
27
|
+
},
|
|
28
|
+
`No memories for repo "${validated.repo}"`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Format memories for recap
|
|
33
|
+
const formattedMemories = rows.map((row, index) => ({
|
|
34
|
+
number: validated.offset + index + 1,
|
|
35
|
+
id: row.id,
|
|
36
|
+
type: row.type,
|
|
37
|
+
importance: row.importance,
|
|
38
|
+
preview: row.content.substring(0, 100) + (row.content.length > 100 ? "..." : ""),
|
|
39
|
+
created_at: row.created_at
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Create summary text
|
|
43
|
+
const summary = formattedMemories
|
|
44
|
+
.map(
|
|
45
|
+
(m) =>
|
|
46
|
+
`${m.number}. [${m.type.toUpperCase()}] (importance: ${m.importance}) ${m.preview}`
|
|
47
|
+
)
|
|
48
|
+
.join("\n");
|
|
49
|
+
|
|
50
|
+
return createMcpResponse(
|
|
51
|
+
{
|
|
52
|
+
repo: validated.repo,
|
|
53
|
+
count: rows.length,
|
|
54
|
+
total,
|
|
55
|
+
offset: validated.offset,
|
|
56
|
+
memories: formattedMemories,
|
|
57
|
+
summary: `Recent ${rows.length} memories:\n\n${summary}`
|
|
58
|
+
},
|
|
59
|
+
`Retrieved ${rows.length} memories for repo "${validated.repo}"`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import * as fc from "fast-check";
|
|
3
|
+
|
|
4
|
+
// ─── Property 2: Recap conditional ───────────────────────────────────────────
|
|
5
|
+
// Feature: memory-mcp-optimization, Property 2: Recap conditional
|
|
6
|
+
|
|
7
|
+
// We test the weight constants directly by importing them from the module.
|
|
8
|
+
// For Property 2 we need to verify the conditional recap call logic.
|
|
9
|
+
// We do this by extracting the logic into a testable helper and verifying
|
|
10
|
+
// the branching behaviour with fast-check.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mirrors the weight selection logic from memory.search.ts.
|
|
14
|
+
* Returns the weights object that would be used given whether vector results
|
|
15
|
+
* are present or not.
|
|
16
|
+
*/
|
|
17
|
+
function selectWeights(vectorResultsEmpty: boolean): {
|
|
18
|
+
similarity: number;
|
|
19
|
+
vector?: number;
|
|
20
|
+
importance: number;
|
|
21
|
+
} {
|
|
22
|
+
if (vectorResultsEmpty) {
|
|
23
|
+
return { similarity: 0.85, importance: 0.15 };
|
|
24
|
+
}
|
|
25
|
+
return { similarity: 0.6, vector: 0.3, importance: 0.1 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mirrors the recap-call decision from memory.search.ts.
|
|
30
|
+
* Returns true if handleMemoryRecap should be called.
|
|
31
|
+
*/
|
|
32
|
+
function shouldCallRecap(includeRecap: boolean | undefined): boolean {
|
|
33
|
+
return includeRecap === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Property 2 tests ────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe("Property 2: Recap dipanggil jika dan hanya jika includeRecap = true", () => {
|
|
39
|
+
// Feature: memory-mcp-optimization, Property 2: Recap conditional
|
|
40
|
+
// Validates: Requirements 2.1, 2.2, 2.3
|
|
41
|
+
|
|
42
|
+
it("should NOT call recap when includeRecap is false or absent", () => {
|
|
43
|
+
fc.assert(
|
|
44
|
+
fc.property(
|
|
45
|
+
fc.oneof(
|
|
46
|
+
fc.constant<boolean | undefined>(false),
|
|
47
|
+
fc.constant<boolean | undefined>(undefined)
|
|
48
|
+
),
|
|
49
|
+
(includeRecap: boolean | undefined) => {
|
|
50
|
+
expect(shouldCallRecap(includeRecap)).toBe(false);
|
|
51
|
+
}
|
|
52
|
+
),
|
|
53
|
+
{ numRuns: 100 }
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should call recap when includeRecap is true", () => {
|
|
58
|
+
fc.assert(
|
|
59
|
+
fc.property(
|
|
60
|
+
fc.constant<boolean>(true),
|
|
61
|
+
(includeRecap: boolean) => {
|
|
62
|
+
expect(shouldCallRecap(includeRecap)).toBe(true);
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
{ numRuns: 100 }
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("recap is called if and only if includeRecap === true (combined property)", () => {
|
|
70
|
+
fc.assert(
|
|
71
|
+
fc.property(
|
|
72
|
+
fc.oneof(
|
|
73
|
+
fc.constant<boolean | undefined>(true),
|
|
74
|
+
fc.constant<boolean | undefined>(false),
|
|
75
|
+
fc.constant<boolean | undefined>(undefined)
|
|
76
|
+
),
|
|
77
|
+
(includeRecap: boolean | undefined) => {
|
|
78
|
+
const called = shouldCallRecap(includeRecap);
|
|
79
|
+
// The iff condition: called ↔ (includeRecap === true)
|
|
80
|
+
expect(called).toBe(includeRecap === true);
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
{ numRuns: 200 }
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ─── Property 3: Hybrid score weights sum to 1.0 ─────────────────────────────
|
|
89
|
+
|
|
90
|
+
describe("Property 3: Total bobot Hybrid Score selalu = 1.0", () => {
|
|
91
|
+
// Feature: memory-mcp-optimization, Property 3: Hybrid score weights sum to 1.0
|
|
92
|
+
// Validates: Requirements 3.1, 3.2, 3.4
|
|
93
|
+
|
|
94
|
+
it("weights sum to 1.0 when vector store is empty (no vector results)", () => {
|
|
95
|
+
fc.assert(
|
|
96
|
+
fc.property(
|
|
97
|
+
fc.constant<boolean>(true), // vectorResultsEmpty = true
|
|
98
|
+
(vectorResultsEmpty: boolean) => {
|
|
99
|
+
const weights = selectWeights(vectorResultsEmpty);
|
|
100
|
+
const sum = weights.similarity + (weights.vector ?? 0) + weights.importance;
|
|
101
|
+
expect(Math.abs(sum - 1.0)).toBeLessThan(1e-10);
|
|
102
|
+
}
|
|
103
|
+
),
|
|
104
|
+
{ numRuns: 100 }
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("weights sum to 1.0 when vector store is active (has vector results)", () => {
|
|
109
|
+
fc.assert(
|
|
110
|
+
fc.property(
|
|
111
|
+
fc.constant<boolean>(false), // vectorResultsEmpty = false
|
|
112
|
+
(vectorResultsEmpty: boolean) => {
|
|
113
|
+
const weights = selectWeights(vectorResultsEmpty);
|
|
114
|
+
const sum = weights.similarity + (weights.vector ?? 0) + weights.importance;
|
|
115
|
+
expect(Math.abs(sum - 1.0)).toBeLessThan(1e-10);
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
{ numRuns: 100 }
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("weights always sum to 1.0 for both vector conditions", () => {
|
|
123
|
+
fc.assert(
|
|
124
|
+
fc.property(
|
|
125
|
+
fc.boolean(), // arbitrary vectorResultsEmpty flag
|
|
126
|
+
(vectorResultsEmpty: boolean) => {
|
|
127
|
+
const weights = selectWeights(vectorResultsEmpty);
|
|
128
|
+
const sum = weights.similarity + (weights.vector ?? 0) + weights.importance;
|
|
129
|
+
expect(Math.abs(sum - 1.0)).toBeLessThan(1e-10);
|
|
130
|
+
}
|
|
131
|
+
),
|
|
132
|
+
{ numRuns: 200 }
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("vector-active weights are exactly 0.6 + 0.3 + 0.1 = 1.0", () => {
|
|
137
|
+
const weights = selectWeights(false);
|
|
138
|
+
expect(weights.similarity).toBe(0.6);
|
|
139
|
+
expect(weights.vector).toBe(0.3);
|
|
140
|
+
expect(weights.importance).toBe(0.1);
|
|
141
|
+
expect(weights.similarity + (weights.vector ?? 0) + weights.importance).toBeCloseTo(1.0, 10);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("vector-empty weights are exactly 0.85 + 0.15 = 1.0", () => {
|
|
145
|
+
const weights = selectWeights(true);
|
|
146
|
+
expect(weights.similarity).toBe(0.85);
|
|
147
|
+
expect(weights.vector).toBeUndefined();
|
|
148
|
+
expect(weights.importance).toBe(0.15);
|
|
149
|
+
expect(weights.similarity + weights.importance).toBe(1.0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ─── Prompt parameter tests ─────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
describe("Property: prompt parameter in memory-search", () => {
|
|
156
|
+
it("prompt is optional - search works without prompt", () => {
|
|
157
|
+
const params = {
|
|
158
|
+
query: "database orm",
|
|
159
|
+
repo: "test-repo",
|
|
160
|
+
limit: 5
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// If prompt is not provided, searchQuery should equal query
|
|
164
|
+
const hasPrompt = 'prompt' in params && params.prompt;
|
|
165
|
+
const searchQuery = hasPrompt
|
|
166
|
+
? `${params.query} ${params.prompt}`
|
|
167
|
+
: params.query;
|
|
168
|
+
|
|
169
|
+
expect(searchQuery).toBe("database orm");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("prompt is combined with query when provided", () => {
|
|
173
|
+
const params = {
|
|
174
|
+
query: "database orm",
|
|
175
|
+
prompt: "I need user authentication",
|
|
176
|
+
repo: "test-repo",
|
|
177
|
+
limit: 5
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const searchQuery = params.prompt
|
|
181
|
+
? `${params.query} ${params.prompt}`
|
|
182
|
+
: params.query;
|
|
183
|
+
|
|
184
|
+
expect(searchQuery).toBe("database orm I need user authentication");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("prompt can be empty string - treated as no prompt (empty string is falsy)", () => {
|
|
188
|
+
const params = {
|
|
189
|
+
query: "database orm",
|
|
190
|
+
prompt: "",
|
|
191
|
+
repo: "test-repo",
|
|
192
|
+
limit: 5
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Empty string is falsy in JS, so it uses just query
|
|
196
|
+
const searchQuery = params.prompt
|
|
197
|
+
? `${params.query} ${params.prompt}`
|
|
198
|
+
: params.query;
|
|
199
|
+
|
|
200
|
+
expect(searchQuery).toBe("database orm");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("matchReason includes prompt when provided", () => {
|
|
204
|
+
const query = "database orm";
|
|
205
|
+
const prompt = "user authentication context";
|
|
206
|
+
|
|
207
|
+
const matchReason = prompt
|
|
208
|
+
? `Results ranked by relevance to "${query}" with context: ${prompt}`
|
|
209
|
+
: `Results ranked by relevance to "${query}"`;
|
|
210
|
+
|
|
211
|
+
expect(matchReason).toContain("database orm");
|
|
212
|
+
expect(matchReason).toContain("user authentication context");
|
|
213
|
+
expect(matchReason).toContain("with context:");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("matchReason does not include prompt when not provided", () => {
|
|
217
|
+
const query = "database orm";
|
|
218
|
+
const prompt = undefined;
|
|
219
|
+
|
|
220
|
+
const matchReason = prompt
|
|
221
|
+
? `Results ranked by relevance to "${query}" with context: ${prompt}`
|
|
222
|
+
: `Results ranked by relevance to "${query}"`;
|
|
223
|
+
|
|
224
|
+
expect(matchReason).toBe(`Results ranked by relevance to "${query}"`);
|
|
225
|
+
expect(matchReason).not.toContain("with context:");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("property: searchQuery combines query and prompt consistently", () => {
|
|
229
|
+
fc.assert(
|
|
230
|
+
fc.property(
|
|
231
|
+
fc.string({ minLength: 3, maxLength: 50 }),
|
|
232
|
+
fc.string({ minLength: 1, maxLength: 100 }),
|
|
233
|
+
(query, prompt) => {
|
|
234
|
+
const searchQuery = `${query} ${prompt}`;
|
|
235
|
+
|
|
236
|
+
// Search query should always start with the original query
|
|
237
|
+
expect(searchQuery.startsWith(query)).toBe(true);
|
|
238
|
+
|
|
239
|
+
// Search query should contain the prompt
|
|
240
|
+
expect(searchQuery.includes(prompt)).toBe(true);
|
|
241
|
+
|
|
242
|
+
// There should be exactly one space between query and prompt
|
|
243
|
+
expect(searchQuery).toBe(`${query} ${prompt}`);
|
|
244
|
+
}
|
|
245
|
+
),
|
|
246
|
+
{ numRuns: 50 }
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("property: matchReason format is correct with and without prompt", () => {
|
|
251
|
+
fc.assert(
|
|
252
|
+
fc.property(
|
|
253
|
+
fc.string({ minLength: 1, maxLength: 30 }),
|
|
254
|
+
fc.option(fc.string({ minLength: 1, maxLength: 50 })),
|
|
255
|
+
(query, prompt) => {
|
|
256
|
+
const matchReason = prompt
|
|
257
|
+
? `Results ranked by relevance to "${query}" with context: ${prompt}`
|
|
258
|
+
: `Results ranked by relevance to "${query}"`;
|
|
259
|
+
|
|
260
|
+
// Should always contain the query
|
|
261
|
+
expect(matchReason).toContain(query);
|
|
262
|
+
|
|
263
|
+
if (prompt) {
|
|
264
|
+
// Should contain "with context:" when prompt exists
|
|
265
|
+
expect(matchReason).toContain("with context:");
|
|
266
|
+
expect(matchReason).toContain(prompt);
|
|
267
|
+
} else {
|
|
268
|
+
// Should NOT contain "with context:" when prompt is absent
|
|
269
|
+
expect(matchReason).not.toContain("with context:");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
),
|
|
273
|
+
{ numRuns: 50 }
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
});
|