@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.
Files changed (196) hide show
  1. package/DASHBOARD.md +129 -0
  2. package/HYBRID_SEARCH.md +204 -0
  3. package/IMPLEMENTATION.md +159 -0
  4. package/README.md +175 -0
  5. package/dist/capabilities.d.ts +22 -0
  6. package/dist/capabilities.d.ts.map +1 -0
  7. package/dist/capabilities.js +23 -0
  8. package/dist/capabilities.js.map +1 -0
  9. package/dist/dashboard/dashboard.test.d.ts +2 -0
  10. package/dist/dashboard/dashboard.test.d.ts.map +1 -0
  11. package/dist/dashboard/dashboard.test.js +362 -0
  12. package/dist/dashboard/dashboard.test.js.map +1 -0
  13. package/dist/dashboard/public/app.js +1187 -0
  14. package/dist/dashboard/public/chart.js +0 -0
  15. package/dist/dashboard/public/index.html +967 -0
  16. package/dist/dashboard/server.d.ts +3 -0
  17. package/dist/dashboard/server.d.ts.map +1 -0
  18. package/dist/dashboard/server.js +297 -0
  19. package/dist/dashboard/server.js.map +1 -0
  20. package/dist/mcp/client.d.ts +34 -0
  21. package/dist/mcp/client.d.ts.map +1 -0
  22. package/dist/mcp/client.js +181 -0
  23. package/dist/mcp/client.js.map +1 -0
  24. package/dist/mcp/client.test.d.ts +2 -0
  25. package/dist/mcp/client.test.d.ts.map +1 -0
  26. package/dist/mcp/client.test.js +130 -0
  27. package/dist/mcp/client.test.js.map +1 -0
  28. package/dist/prompts/registry.d.ts +39 -0
  29. package/dist/prompts/registry.d.ts.map +1 -0
  30. package/dist/prompts/registry.js +90 -0
  31. package/dist/prompts/registry.js.map +1 -0
  32. package/dist/resources/index.d.ts +17 -0
  33. package/dist/resources/index.d.ts.map +1 -0
  34. package/dist/resources/index.js +100 -0
  35. package/dist/resources/index.js.map +1 -0
  36. package/dist/resources/index.test.d.ts +2 -0
  37. package/dist/resources/index.test.d.ts.map +1 -0
  38. package/dist/resources/index.test.js +96 -0
  39. package/dist/resources/index.test.js.map +1 -0
  40. package/dist/router.d.ts +4 -0
  41. package/dist/router.d.ts.map +1 -0
  42. package/dist/router.js +60 -0
  43. package/dist/router.js.map +1 -0
  44. package/dist/router.test.d.ts +2 -0
  45. package/dist/router.test.d.ts.map +1 -0
  46. package/dist/router.test.js +113 -0
  47. package/dist/router.test.js.map +1 -0
  48. package/dist/search_memory_example.d.ts +3 -0
  49. package/dist/search_memory_example.d.ts.map +1 -0
  50. package/dist/search_memory_example.js +56 -0
  51. package/dist/search_memory_example.js.map +1 -0
  52. package/dist/server.d.ts +3 -0
  53. package/dist/server.d.ts.map +1 -0
  54. package/dist/server.js +91 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/storage/sqlite.d.ts +95 -0
  57. package/dist/storage/sqlite.d.ts.map +1 -0
  58. package/dist/storage/sqlite.js +537 -0
  59. package/dist/storage/sqlite.js.map +1 -0
  60. package/dist/storage/sqlite.test.d.ts +2 -0
  61. package/dist/storage/sqlite.test.d.ts.map +1 -0
  62. package/dist/storage/sqlite.test.js +358 -0
  63. package/dist/storage/sqlite.test.js.map +1 -0
  64. package/dist/storage/vectors.stub.d.ts +12 -0
  65. package/dist/storage/vectors.stub.d.ts.map +1 -0
  66. package/dist/storage/vectors.stub.js +88 -0
  67. package/dist/storage/vectors.stub.js.map +1 -0
  68. package/dist/store_memory_example.d.ts +3 -0
  69. package/dist/store_memory_example.d.ts.map +1 -0
  70. package/dist/store_memory_example.js +69 -0
  71. package/dist/store_memory_example.js.map +1 -0
  72. package/dist/test_quotes_client.d.ts +3 -0
  73. package/dist/test_quotes_client.d.ts.map +1 -0
  74. package/dist/test_quotes_client.js +72 -0
  75. package/dist/test_quotes_client.js.map +1 -0
  76. package/dist/tools/memory.delete.d.ts +9 -0
  77. package/dist/tools/memory.delete.d.ts.map +1 -0
  78. package/dist/tools/memory.delete.js +22 -0
  79. package/dist/tools/memory.delete.js.map +1 -0
  80. package/dist/tools/memory.recap.d.ts +4 -0
  81. package/dist/tools/memory.recap.d.ts.map +1 -0
  82. package/dist/tools/memory.recap.js +42 -0
  83. package/dist/tools/memory.recap.js.map +1 -0
  84. package/dist/tools/memory.search.d.ts +5 -0
  85. package/dist/tools/memory.search.d.ts.map +1 -0
  86. package/dist/tools/memory.search.js +192 -0
  87. package/dist/tools/memory.search.js.map +1 -0
  88. package/dist/tools/memory.search.test.d.ts +2 -0
  89. package/dist/tools/memory.search.test.d.ts.map +1 -0
  90. package/dist/tools/memory.search.test.js +181 -0
  91. package/dist/tools/memory.search.test.js.map +1 -0
  92. package/dist/tools/memory.store.d.ts +5 -0
  93. package/dist/tools/memory.store.d.ts.map +1 -0
  94. package/dist/tools/memory.store.js +41 -0
  95. package/dist/tools/memory.store.js.map +1 -0
  96. package/dist/tools/memory.summarize.d.ts +4 -0
  97. package/dist/tools/memory.summarize.d.ts.map +1 -0
  98. package/dist/tools/memory.summarize.js +13 -0
  99. package/dist/tools/memory.summarize.js.map +1 -0
  100. package/dist/tools/memory.update.d.ts +5 -0
  101. package/dist/tools/memory.update.d.ts.map +1 -0
  102. package/dist/tools/memory.update.js +31 -0
  103. package/dist/tools/memory.update.js.map +1 -0
  104. package/dist/tools/schemas.d.ts +334 -0
  105. package/dist/tools/schemas.d.ts.map +1 -0
  106. package/dist/tools/schemas.js +251 -0
  107. package/dist/tools/schemas.js.map +1 -0
  108. package/dist/types.d.ts +31 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +3 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/utils/git-scope.d.ts +8 -0
  113. package/dist/utils/git-scope.d.ts.map +1 -0
  114. package/dist/utils/git-scope.js +38 -0
  115. package/dist/utils/git-scope.js.map +1 -0
  116. package/dist/utils/logger.d.ts +7 -0
  117. package/dist/utils/logger.d.ts.map +1 -0
  118. package/dist/utils/logger.js +40 -0
  119. package/dist/utils/logger.js.map +1 -0
  120. package/dist/utils/logger.test.d.ts +2 -0
  121. package/dist/utils/logger.test.d.ts.map +1 -0
  122. package/dist/utils/logger.test.js +84 -0
  123. package/dist/utils/logger.test.js.map +1 -0
  124. package/dist/utils/mcp-response.d.ts +44 -0
  125. package/dist/utils/mcp-response.d.ts.map +1 -0
  126. package/dist/utils/mcp-response.js +81 -0
  127. package/dist/utils/mcp-response.js.map +1 -0
  128. package/dist/utils/normalize.d.ts +4 -0
  129. package/dist/utils/normalize.d.ts.map +1 -0
  130. package/dist/utils/normalize.js +51 -0
  131. package/dist/utils/normalize.js.map +1 -0
  132. package/dist/utils/normalize.test.d.ts +2 -0
  133. package/dist/utils/normalize.test.d.ts.map +1 -0
  134. package/dist/utils/normalize.test.js +159 -0
  135. package/dist/utils/normalize.test.js.map +1 -0
  136. package/dist/utils/query-expander.d.ts +2 -0
  137. package/dist/utils/query-expander.d.ts.map +1 -0
  138. package/dist/utils/query-expander.js +50 -0
  139. package/dist/utils/query-expander.js.map +1 -0
  140. package/dist/utils/query-expander.test.d.ts +2 -0
  141. package/dist/utils/query-expander.test.d.ts.map +1 -0
  142. package/dist/utils/query-expander.test.js +35 -0
  143. package/dist/utils/query-expander.test.js.map +1 -0
  144. package/docs/PRD.md +199 -0
  145. package/docs/PROMPT-agent.md +139 -0
  146. package/docs/SPEC-git-scope.md +172 -0
  147. package/docs/SPEC-heuristics.md +199 -0
  148. package/docs/SPEC-server.md +243 -0
  149. package/docs/SPEC-skeleton.md +255 -0
  150. package/docs/SPEC-sqlite-schema.md +183 -0
  151. package/docs/SPEC-tool-schema.md +201 -0
  152. package/docs/SPEC-vector-search.md +198 -0
  153. package/docs/TEST-scenarios.md +179 -0
  154. package/package.json +43 -0
  155. package/scripts/update-null-titles-ai.mjs +272 -0
  156. package/scripts/update-titles-batch.mjs +71 -0
  157. package/scripts/update-titles.mjs +66 -0
  158. package/seed-data.mjs +151 -0
  159. package/src/capabilities.ts +22 -0
  160. package/src/dashboard/dashboard.test.ts +546 -0
  161. package/src/dashboard/public/app.js +1187 -0
  162. package/src/dashboard/public/chart.js +0 -0
  163. package/src/dashboard/public/index.html +967 -0
  164. package/src/dashboard/server.ts +347 -0
  165. package/src/mcp/client.test.ts +164 -0
  166. package/src/mcp/client.ts +212 -0
  167. package/src/prompts/registry.ts +89 -0
  168. package/src/resources/index.test.ts +132 -0
  169. package/src/resources/index.ts +113 -0
  170. package/src/router.test.ts +145 -0
  171. package/src/router.ts +80 -0
  172. package/src/server.ts +99 -0
  173. package/src/storage/sqlite.test.ts +504 -0
  174. package/src/storage/sqlite.ts +688 -0
  175. package/src/storage/vectors.stub.ts +101 -0
  176. package/src/tools/memory.delete.ts +37 -0
  177. package/src/tools/memory.recap.ts +61 -0
  178. package/src/tools/memory.search.test.ts +276 -0
  179. package/src/tools/memory.search.ts +244 -0
  180. package/src/tools/memory.store.ts +56 -0
  181. package/src/tools/memory.summarize.ts +23 -0
  182. package/src/tools/memory.update.ts +46 -0
  183. package/src/tools/schemas.ts +261 -0
  184. package/src/types.ts +36 -0
  185. package/src/utils/git-scope.ts +42 -0
  186. package/src/utils/logger.test.ts +125 -0
  187. package/src/utils/logger.ts +53 -0
  188. package/src/utils/mcp-response.ts +116 -0
  189. package/src/utils/normalize.test.ts +203 -0
  190. package/src/utils/normalize.ts +53 -0
  191. package/src/utils/query-expander.test.ts +40 -0
  192. package/src/utils/query-expander.ts +60 -0
  193. package/storage/.gitkeep +5 -0
  194. package/test.sh +48 -0
  195. package/tsconfig.json +21 -0
  196. 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
+ });