@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
package/src/router.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { listResources, readResource } from "./resources/index.js";
|
|
2
|
+
import { PROMPTS } from "./prompts/registry.js";
|
|
3
|
+
import { TOOL_DEFINITIONS } from "./tools/schemas.js";
|
|
4
|
+
import { SQLiteStore } from "./storage/sqlite.js";
|
|
5
|
+
import { VectorStore } from "./types.js";
|
|
6
|
+
import { handleMemoryStore } from "./tools/memory.store.js";
|
|
7
|
+
import { handleMemoryUpdate } from "./tools/memory.update.js";
|
|
8
|
+
import { handleMemorySearch } from "./tools/memory.search.js";
|
|
9
|
+
import { handleMemorySummarize } from "./tools/memory.summarize.js";
|
|
10
|
+
import { handleMemoryDelete } from "./tools/memory.delete.js";
|
|
11
|
+
import { handleMemoryRecap } from "./tools/memory.recap.js";
|
|
12
|
+
|
|
13
|
+
export function createRouter(
|
|
14
|
+
db: SQLiteStore,
|
|
15
|
+
vectors: VectorStore
|
|
16
|
+
): (method: string, params: any) => Promise<any> {
|
|
17
|
+
async function handleMethod(method: string, params: any): Promise<any> {
|
|
18
|
+
switch (method) {
|
|
19
|
+
// ---- tools ----
|
|
20
|
+
case "tools/list":
|
|
21
|
+
return { tools: TOOL_DEFINITIONS };
|
|
22
|
+
|
|
23
|
+
case "tools/call":
|
|
24
|
+
return await handleToolCall(params);
|
|
25
|
+
|
|
26
|
+
// ---- resources ----
|
|
27
|
+
case "resources/list":
|
|
28
|
+
return listResources();
|
|
29
|
+
|
|
30
|
+
case "resources/read":
|
|
31
|
+
return readResource(params?.uri, db);
|
|
32
|
+
|
|
33
|
+
// ---- prompts ----
|
|
34
|
+
case "prompts/list":
|
|
35
|
+
return { prompts: Object.values(PROMPTS) };
|
|
36
|
+
|
|
37
|
+
case "prompts/get": {
|
|
38
|
+
const prompt = PROMPTS[params?.name as keyof typeof PROMPTS];
|
|
39
|
+
if (!prompt) {
|
|
40
|
+
throw new Error(`Unknown prompt: ${params?.name}`);
|
|
41
|
+
}
|
|
42
|
+
return prompt;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
default:
|
|
46
|
+
throw new Error(`Unsupported method: ${method}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleToolCall(params: any): Promise<any> {
|
|
51
|
+
const { name, arguments: args } = params;
|
|
52
|
+
// Normalize tool naming: accept both dot (memory.store) and hyphen (memory-store)
|
|
53
|
+
const toolName = String(name).replace(/\./g, "-");
|
|
54
|
+
|
|
55
|
+
switch (toolName) {
|
|
56
|
+
case "memory-store":
|
|
57
|
+
return await handleMemoryStore(args, db, vectors);
|
|
58
|
+
|
|
59
|
+
case "memory-update":
|
|
60
|
+
return await handleMemoryUpdate(args, db, vectors);
|
|
61
|
+
|
|
62
|
+
case "memory-recap":
|
|
63
|
+
return await handleMemoryRecap(args, db);
|
|
64
|
+
|
|
65
|
+
case "memory-search":
|
|
66
|
+
return await handleMemorySearch(args, db, vectors);
|
|
67
|
+
|
|
68
|
+
case "memory-summarize":
|
|
69
|
+
return await handleMemorySummarize(args, db);
|
|
70
|
+
|
|
71
|
+
case "memory-delete":
|
|
72
|
+
return await handleMemoryDelete(args, db, vectors);
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return handleMethod;
|
|
80
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { createRouter } from "./router.js";
|
|
4
|
+
import { SQLiteStore } from "./storage/sqlite.js";
|
|
5
|
+
import { StubVectorStore } from "./storage/vectors.stub.js";
|
|
6
|
+
import { CAPABILITIES } from "./capabilities.js";
|
|
7
|
+
import { logger } from "./utils/logger.js";
|
|
8
|
+
|
|
9
|
+
// Create storage instances
|
|
10
|
+
const db = new SQLiteStore();
|
|
11
|
+
const vectors = new StubVectorStore(db);
|
|
12
|
+
|
|
13
|
+
// Wire router with injected storage
|
|
14
|
+
const handleMethod = createRouter(db, vectors);
|
|
15
|
+
|
|
16
|
+
// Cleanup on exit
|
|
17
|
+
process.on("SIGINT", () => {
|
|
18
|
+
db.close();
|
|
19
|
+
process.exit(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.on("SIGTERM", () => {
|
|
23
|
+
db.close();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout,
|
|
30
|
+
terminal: false
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function reply(payload: unknown) {
|
|
34
|
+
try {
|
|
35
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
36
|
+
} catch (err: any) {
|
|
37
|
+
// Ignore EPIPE errors (broken pipe when client disconnects)
|
|
38
|
+
if (err.code !== "EPIPE") {
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
rl.on("line", async (line) => {
|
|
45
|
+
if (!line.trim()) return;
|
|
46
|
+
|
|
47
|
+
let msg;
|
|
48
|
+
try {
|
|
49
|
+
msg = JSON.parse(line);
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { id, method, params } = msg;
|
|
55
|
+
|
|
56
|
+
// --- initialize ---
|
|
57
|
+
if (method === "initialize") {
|
|
58
|
+
reply({
|
|
59
|
+
jsonrpc: "2.0",
|
|
60
|
+
id,
|
|
61
|
+
result: {
|
|
62
|
+
protocolVersion: "2024-11-05",
|
|
63
|
+
serverInfo: CAPABILITIES.serverInfo,
|
|
64
|
+
capabilities: CAPABILITIES.capabilities
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
reply({
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
method: "notifications/initialized",
|
|
71
|
+
params: {}
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- ignore notification ---
|
|
77
|
+
if (method === "notifications/initialized") return;
|
|
78
|
+
|
|
79
|
+
// --- route method ---
|
|
80
|
+
try {
|
|
81
|
+
const result = await handleMethod(method, params);
|
|
82
|
+
|
|
83
|
+
reply({
|
|
84
|
+
jsonrpc: "2.0",
|
|
85
|
+
id,
|
|
86
|
+
result
|
|
87
|
+
});
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
logger.error("Method handler error", { method, id, message: err.message });
|
|
90
|
+
reply({
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
id,
|
|
93
|
+
error: {
|
|
94
|
+
code: -32603,
|
|
95
|
+
message: err.message || "Internal error"
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// Feature: memory-mcp-optimization
|
|
2
|
+
// Property tests for SQLiteStore — Properties 1, 6, 7, 8, 9, 10, 18
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import * as fc from "fast-check";
|
|
6
|
+
import { SQLiteStore } from "./sqlite.js";
|
|
7
|
+
import type { MemoryEntry } from "../types.js";
|
|
8
|
+
|
|
9
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
type MemoryType = "code_fact" | "decision" | "mistake" | "pattern";
|
|
12
|
+
|
|
13
|
+
function makeEntry(overrides: Partial<{
|
|
14
|
+
id: string;
|
|
15
|
+
repo: string;
|
|
16
|
+
type: MemoryType;
|
|
17
|
+
title: string;
|
|
18
|
+
content: string;
|
|
19
|
+
importance: number;
|
|
20
|
+
created_at: string;
|
|
21
|
+
expires_at: string | null;
|
|
22
|
+
}>): MemoryEntry {
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
return {
|
|
25
|
+
id: overrides.id ?? `id-${Math.random().toString(36).slice(2)}`,
|
|
26
|
+
type: overrides.type ?? "code_fact",
|
|
27
|
+
title: overrides.title ?? "Test Memory Title",
|
|
28
|
+
content: overrides.content ?? "sample content for testing purposes",
|
|
29
|
+
importance: overrides.importance ?? 3,
|
|
30
|
+
scope: { repo: overrides.repo ?? "test-repo" },
|
|
31
|
+
created_at: overrides.created_at ?? now,
|
|
32
|
+
updated_at: now,
|
|
33
|
+
hit_count: 0,
|
|
34
|
+
recall_count: 0,
|
|
35
|
+
last_used_at: null,
|
|
36
|
+
expires_at: overrides.expires_at !== undefined ? overrides.expires_at : null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function freshStore(): SQLiteStore {
|
|
41
|
+
return new SQLiteStore(":memory:");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Property 1: Pre-filter SQL membatasi kandidat searchBySimilarity ────────
|
|
45
|
+
// Validates: Requirements 1.1, 1.2, 1.3, 1.4
|
|
46
|
+
|
|
47
|
+
describe("Property 1: Pre-filter SQL membatasi kandidat searchBySimilarity", () => {
|
|
48
|
+
// Feature: memory-mcp-optimization, Property 1: Pre-filter SQL membatasi kandidat searchBySimilarity
|
|
49
|
+
it("result.length <= limit for any repo with N > limit memories", () => {
|
|
50
|
+
fc.assert(
|
|
51
|
+
fc.property(
|
|
52
|
+
fc.integer({ min: 5, max: 30 }), // N memories
|
|
53
|
+
fc.integer({ min: 1, max: 4 }), // limit < N
|
|
54
|
+
fc.string({ minLength: 5, maxLength: 30 }), // query
|
|
55
|
+
(n: number, limit: number, query: string) => {
|
|
56
|
+
const store = freshStore();
|
|
57
|
+
const repo = "repo-p1";
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
store.insert(makeEntry({
|
|
61
|
+
id: `p1-${i}`,
|
|
62
|
+
repo,
|
|
63
|
+
content: `memory content item number ${i} about coding patterns`,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const results = store.searchBySimilarity(query, repo, limit);
|
|
68
|
+
store.close();
|
|
69
|
+
|
|
70
|
+
return results.length <= limit;
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
{ numRuns: 50 }
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── Property 6: Inisialisasi SQLiteStore berulang tidak melempar exception ──
|
|
79
|
+
// Validates: Requirements 7.1, 7.3, 7.4
|
|
80
|
+
|
|
81
|
+
describe("Property 6: Inisialisasi SQLiteStore berulang tidak melempar exception", () => {
|
|
82
|
+
// Feature: memory-mcp-optimization, Property 6: Inisialisasi SQLiteStore berulang tidak melempar exception
|
|
83
|
+
it("constructing SQLiteStore twice on :memory: does not throw", () => {
|
|
84
|
+
fc.assert(
|
|
85
|
+
fc.property(
|
|
86
|
+
fc.constant(":memory:"),
|
|
87
|
+
(_path: string) => {
|
|
88
|
+
// Each :memory: DB is independent, so we test the migration logic
|
|
89
|
+
// by creating two stores (each runs migrate() on a fresh in-memory DB)
|
|
90
|
+
expect(() => {
|
|
91
|
+
const s1 = new SQLiteStore(":memory:");
|
|
92
|
+
s1.close();
|
|
93
|
+
}).not.toThrow();
|
|
94
|
+
|
|
95
|
+
expect(() => {
|
|
96
|
+
const s2 = new SQLiteStore(":memory:");
|
|
97
|
+
s2.close();
|
|
98
|
+
}).not.toThrow();
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
{ numRuns: 20 }
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("migrate() is idempotent — inserting then re-opening same store does not throw", () => {
|
|
108
|
+
// We simulate re-initialization by calling migrate logic twice via two stores
|
|
109
|
+
// on the same in-memory path (each :memory: is isolated, so we verify no throw)
|
|
110
|
+
const store1 = new SQLiteStore(":memory:");
|
|
111
|
+
store1.insert(makeEntry({ id: "m1", repo: "r1" }));
|
|
112
|
+
// A second store on :memory: is a fresh DB — migration runs again safely
|
|
113
|
+
const store2 = new SQLiteStore(":memory:");
|
|
114
|
+
store2.insert(makeEntry({ id: "m2", repo: "r2" }));
|
|
115
|
+
store1.close();
|
|
116
|
+
store2.close();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── Property 7: Pagination non-overlapping ──────────────────────────────────
|
|
121
|
+
// Validates: Requirements 8.2, 8.4
|
|
122
|
+
|
|
123
|
+
describe("Property 7: Pagination non-overlapping", () => {
|
|
124
|
+
// Feature: memory-mcp-optimization, Property 7: Pagination non-overlapping
|
|
125
|
+
it("pages i and j (i ≠ j) share no memory ids", () => {
|
|
126
|
+
fc.assert(
|
|
127
|
+
fc.property(
|
|
128
|
+
fc.integer({ min: 10, max: 40 }), // total memories N
|
|
129
|
+
fc.integer({ min: 2, max: 5 }), // page size L
|
|
130
|
+
fc.integer({ min: 0, max: 3 }), // page index i
|
|
131
|
+
fc.integer({ min: 0, max: 3 }), // page index j
|
|
132
|
+
(n: number, pageSize: number, i: number, j: number) => {
|
|
133
|
+
fc.pre(i !== j);
|
|
134
|
+
|
|
135
|
+
const store = freshStore();
|
|
136
|
+
const repo = "repo-p7";
|
|
137
|
+
|
|
138
|
+
for (let k = 0; k < n; k++) {
|
|
139
|
+
store.insert(makeEntry({
|
|
140
|
+
id: `p7-${k}`,
|
|
141
|
+
repo,
|
|
142
|
+
content: `memory item ${k} for pagination test`,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const pageI = store.getRecentMemories(repo, pageSize, i * pageSize);
|
|
147
|
+
const pageJ = store.getRecentMemories(repo, pageSize, j * pageSize);
|
|
148
|
+
store.close();
|
|
149
|
+
|
|
150
|
+
const idsI = new Set(pageI.map((m) => m.id));
|
|
151
|
+
const idsJ = new Set(pageJ.map((m) => m.id));
|
|
152
|
+
|
|
153
|
+
// No overlap
|
|
154
|
+
for (const id of idsI) {
|
|
155
|
+
if (idsJ.has(id)) return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
{ numRuns: 100 }
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ─── Property 8: TTL menyimpan expires_at yang benar ─────────────────────────
|
|
166
|
+
// Validates: Requirement 9.2
|
|
167
|
+
|
|
168
|
+
describe("Property 8: TTL menyimpan expires_at yang benar", () => {
|
|
169
|
+
// Feature: memory-mcp-optimization, Property 8: TTL menyimpan expires_at yang benar
|
|
170
|
+
it("expires_at equals created_at + ttlDays * 86400 seconds", () => {
|
|
171
|
+
fc.assert(
|
|
172
|
+
fc.property(
|
|
173
|
+
fc.integer({ min: 1, max: 365 }), // ttlDays
|
|
174
|
+
(ttlDays: number) => {
|
|
175
|
+
const store = freshStore();
|
|
176
|
+
const createdAt = new Date("2025-01-01T00:00:00.000Z");
|
|
177
|
+
const expectedExpiresAt = new Date(
|
|
178
|
+
createdAt.getTime() + ttlDays * 86400 * 1000
|
|
179
|
+
).toISOString();
|
|
180
|
+
|
|
181
|
+
const entry = makeEntry({
|
|
182
|
+
id: `p8-${ttlDays}`,
|
|
183
|
+
repo: "repo-p8",
|
|
184
|
+
created_at: createdAt.toISOString(),
|
|
185
|
+
expires_at: expectedExpiresAt,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
store.insert(entry);
|
|
189
|
+
const retrieved = store.getById(entry.id);
|
|
190
|
+
store.close();
|
|
191
|
+
|
|
192
|
+
return retrieved?.expires_at === expectedExpiresAt;
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
{ numRuns: 100 }
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ─── Property 9: Expired memories dikecualikan dari hasil pencarian ───────────
|
|
201
|
+
// Validates: Requirements 9.3, 9.6
|
|
202
|
+
|
|
203
|
+
describe("Property 9: Expired memories dikecualikan dari hasil pencarian", () => {
|
|
204
|
+
// Feature: memory-mcp-optimization, Property 9: Expired memories dikecualikan dari hasil pencarian
|
|
205
|
+
it("searchBySimilarity does not return expired memories", () => {
|
|
206
|
+
fc.assert(
|
|
207
|
+
fc.property(
|
|
208
|
+
fc.string({ minLength: 5, maxLength: 20 }), // query
|
|
209
|
+
(query: string) => {
|
|
210
|
+
const store = freshStore();
|
|
211
|
+
const repo = "repo-p9-sim";
|
|
212
|
+
const pastDate = new Date(Date.now() - 86400 * 1000).toISOString(); // yesterday
|
|
213
|
+
|
|
214
|
+
store.insert(makeEntry({
|
|
215
|
+
id: "expired-sim",
|
|
216
|
+
repo,
|
|
217
|
+
content: `${query} expired content that should not appear`,
|
|
218
|
+
expires_at: pastDate,
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
const results = store.searchBySimilarity(query, repo, 10);
|
|
222
|
+
store.close();
|
|
223
|
+
|
|
224
|
+
return !results.some((r) => r.id === "expired-sim");
|
|
225
|
+
}
|
|
226
|
+
),
|
|
227
|
+
{ numRuns: 50 }
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("searchByRepo does not return expired memories", () => {
|
|
232
|
+
fc.assert(
|
|
233
|
+
fc.property(
|
|
234
|
+
fc.integer({ min: 1, max: 5 }), // importance
|
|
235
|
+
(importance: number) => {
|
|
236
|
+
const store = freshStore();
|
|
237
|
+
const repo = "repo-p9-byrepo";
|
|
238
|
+
const pastDate = new Date(Date.now() - 86400 * 1000).toISOString();
|
|
239
|
+
|
|
240
|
+
store.insert(makeEntry({
|
|
241
|
+
id: "expired-byrepo",
|
|
242
|
+
repo,
|
|
243
|
+
importance,
|
|
244
|
+
expires_at: pastDate,
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
const results = store.searchByRepo(repo);
|
|
248
|
+
store.close();
|
|
249
|
+
|
|
250
|
+
return !results.some((r) => r.id === "expired-byrepo");
|
|
251
|
+
}
|
|
252
|
+
),
|
|
253
|
+
{ numRuns: 50 }
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── Property 10: archiveExpiredMemories() idempoten ─────────────────────────
|
|
259
|
+
// Validates: Requirement 9.5
|
|
260
|
+
|
|
261
|
+
describe("Property 10: archiveExpiredMemories() idempoten", () => {
|
|
262
|
+
// Feature: memory-mcp-optimization, Property 10: archiveExpiredMemories() idempoten
|
|
263
|
+
it("second call returns 0 when no new memories expired between calls", () => {
|
|
264
|
+
fc.assert(
|
|
265
|
+
fc.property(
|
|
266
|
+
fc.integer({ min: 1, max: 10 }), // number of expired memories
|
|
267
|
+
(n: number) => {
|
|
268
|
+
const store = freshStore();
|
|
269
|
+
const repo = "repo-p10";
|
|
270
|
+
const pastDate = new Date(Date.now() - 86400 * 1000).toISOString();
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < n; i++) {
|
|
273
|
+
store.insert(makeEntry({
|
|
274
|
+
id: `p10-${i}`,
|
|
275
|
+
repo,
|
|
276
|
+
expires_at: pastDate,
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const first = store.archiveExpiredMemories();
|
|
281
|
+
const second = store.archiveExpiredMemories();
|
|
282
|
+
store.close();
|
|
283
|
+
|
|
284
|
+
return first === n && second === 0;
|
|
285
|
+
}
|
|
286
|
+
),
|
|
287
|
+
{ numRuns: 50 }
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ─── Property 18: listRepos() mengembalikan daftar unik dan terurut ───────────
|
|
293
|
+
// Validates: Requirements 19.5, 19.6
|
|
294
|
+
|
|
295
|
+
describe("Property 18: listRepos() mengembalikan daftar unik dan terurut", () => {
|
|
296
|
+
// Feature: memory-mcp-optimization, Property 18: listRepos() mengembalikan daftar unik dan terurut
|
|
297
|
+
it("no duplicates and sorted ascending", () => {
|
|
298
|
+
fc.assert(
|
|
299
|
+
fc.property(
|
|
300
|
+
fc.array(
|
|
301
|
+
fc.stringMatching(/^[a-z][a-z0-9-]{1,10}$/),
|
|
302
|
+
{ minLength: 2, maxLength: 10 }
|
|
303
|
+
),
|
|
304
|
+
(repos: string[]) => {
|
|
305
|
+
const store = freshStore();
|
|
306
|
+
|
|
307
|
+
repos.forEach((repo: string, i: number) => {
|
|
308
|
+
store.insert(makeEntry({
|
|
309
|
+
id: `p18-${i}-${repo}`,
|
|
310
|
+
repo,
|
|
311
|
+
}));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = store.listRepos();
|
|
315
|
+
store.close();
|
|
316
|
+
|
|
317
|
+
// No duplicates
|
|
318
|
+
const unique = new Set(result);
|
|
319
|
+
if (unique.size !== result.length) return false;
|
|
320
|
+
|
|
321
|
+
// Sorted ascending
|
|
322
|
+
for (let i = 1; i < result.length; i++) {
|
|
323
|
+
if (result[i] < result[i - 1]) return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
),
|
|
329
|
+
{ numRuns: 100 }
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Property 19: Action Log functionality
|
|
335
|
+
describe("Property 19: Action Log stores and retrieves recent actions", () => {
|
|
336
|
+
it("logAction stores action with correct metadata", () => {
|
|
337
|
+
const store = freshStore();
|
|
338
|
+
|
|
339
|
+
store.logAction('search', "test-repo", { query: "test query", resultCount: 5 });
|
|
340
|
+
|
|
341
|
+
const actions = store.getRecentActions("test-repo", 10);
|
|
342
|
+
|
|
343
|
+
expect(actions).toHaveLength(1);
|
|
344
|
+
expect(actions[0].action).toBe("search");
|
|
345
|
+
expect(actions[0].query).toBe("test query");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("getRecentActions returns actions in descending order by created_at", () => {
|
|
349
|
+
const store = freshStore();
|
|
350
|
+
|
|
351
|
+
store.logAction('search', "test-repo", { query: "first query", resultCount: 3 });
|
|
352
|
+
store.logAction('read', "test-repo", { memoryId: "id-1", resultCount: 2 });
|
|
353
|
+
store.logAction('write', "test-repo", { query: "third query", resultCount: 1 });
|
|
354
|
+
|
|
355
|
+
const actions = store.getRecentActions("test-repo", 10);
|
|
356
|
+
|
|
357
|
+
expect(actions[0].action).toBe("write");
|
|
358
|
+
expect(actions[1].action).toBe("read");
|
|
359
|
+
expect(actions[2].action).toBe("search");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("getRecentActions limits results correctly", () => {
|
|
363
|
+
const store = freshStore();
|
|
364
|
+
|
|
365
|
+
for (let i = 0; i < 25; i++) {
|
|
366
|
+
store.logAction('search', "test-repo", { query: `query-${i}`, resultCount: i });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const actions = store.getRecentActions("test-repo", 10);
|
|
370
|
+
|
|
371
|
+
expect(actions).toHaveLength(10);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("getRecentActions filters by repo correctly", () => {
|
|
375
|
+
const store = freshStore();
|
|
376
|
+
|
|
377
|
+
store.logAction('search', "repo1", { query: "repo1 query", resultCount: 1 });
|
|
378
|
+
store.logAction('search', "repo2", { query: "repo2 query", resultCount: 1 });
|
|
379
|
+
store.logAction('read', "repo1", { memoryId: "id-1", resultCount: 1 });
|
|
380
|
+
|
|
381
|
+
const repo1Actions = store.getRecentActions("repo1", 10);
|
|
382
|
+
const repo2Actions = store.getRecentActions("repo2", 10);
|
|
383
|
+
|
|
384
|
+
expect(repo1Actions).toHaveLength(2);
|
|
385
|
+
expect(repo2Actions).toHaveLength(1);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("getRecentActions returns empty array when no actions exist", () => {
|
|
389
|
+
const store = freshStore();
|
|
390
|
+
|
|
391
|
+
const actions = store.getRecentActions("nonexistent-repo", 10);
|
|
392
|
+
|
|
393
|
+
expect(actions).toHaveLength(0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("getRecentActions without repo parameter returns all actions", () => {
|
|
397
|
+
const store = freshStore();
|
|
398
|
+
|
|
399
|
+
store.logAction('search', "repo1", { query: "query1", resultCount: 1 });
|
|
400
|
+
store.logAction('read', "repo2", { memoryId: "id-1", resultCount: 1 });
|
|
401
|
+
|
|
402
|
+
const allActions = store.getRecentActions(undefined, 10);
|
|
403
|
+
|
|
404
|
+
expect(allActions).toHaveLength(2);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("logAction stores result_count correctly", () => {
|
|
408
|
+
const store = freshStore();
|
|
409
|
+
|
|
410
|
+
store.logAction('search', "test-repo", { query: "high results", resultCount: 100 });
|
|
411
|
+
store.logAction('search', "test-repo", { query: "low results", resultCount: 1 });
|
|
412
|
+
|
|
413
|
+
const actions = store.getRecentActions("test-repo", 10);
|
|
414
|
+
|
|
415
|
+
expect(actions[0].result_count).toBe(1);
|
|
416
|
+
expect(actions[1].result_count).toBe(100);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("different action types are logged correctly", () => {
|
|
420
|
+
const store = freshStore();
|
|
421
|
+
|
|
422
|
+
store.logAction('search', "test-repo", { query: "search query", resultCount: 5 });
|
|
423
|
+
store.logAction('read', "test-repo", { memoryId: "mem-1", resultCount: 1 });
|
|
424
|
+
store.logAction('write', "test-repo", { query: "new memory", resultCount: 1 });
|
|
425
|
+
store.logAction('update', "test-repo", { memoryId: "mem-2", resultCount: 1 });
|
|
426
|
+
store.logAction('delete', "test-repo", { memoryId: "mem-3", resultCount: 1 });
|
|
427
|
+
|
|
428
|
+
const actions = store.getRecentActions("test-repo", 10);
|
|
429
|
+
|
|
430
|
+
expect(actions).toHaveLength(5);
|
|
431
|
+
expect(actions.map(a => a.action)).toEqual(['delete', 'update', 'write', 'read', 'search']);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("Dashboard memory queries", () => {
|
|
436
|
+
it("listMemoriesForDashboard paginates, searches, and sorts server-side", () => {
|
|
437
|
+
const store = freshStore();
|
|
438
|
+
|
|
439
|
+
store.insert(makeEntry({
|
|
440
|
+
id: "dash-1",
|
|
441
|
+
repo: "repo-dashboard",
|
|
442
|
+
title: "Alpha Memory",
|
|
443
|
+
content: "alpha content",
|
|
444
|
+
importance: 5,
|
|
445
|
+
}));
|
|
446
|
+
store.insert(makeEntry({
|
|
447
|
+
id: "dash-2",
|
|
448
|
+
repo: "repo-dashboard",
|
|
449
|
+
title: "Beta Memory",
|
|
450
|
+
content: "beta content",
|
|
451
|
+
importance: 3,
|
|
452
|
+
}));
|
|
453
|
+
store.insert(makeEntry({
|
|
454
|
+
id: "dash-3",
|
|
455
|
+
repo: "repo-dashboard",
|
|
456
|
+
title: "Gamma Memory",
|
|
457
|
+
content: "gamma content",
|
|
458
|
+
importance: 4,
|
|
459
|
+
}));
|
|
460
|
+
|
|
461
|
+
store.incrementHitCount("dash-2");
|
|
462
|
+
store.incrementHitCount("dash-2");
|
|
463
|
+
store.incrementRecallCount("dash-2");
|
|
464
|
+
|
|
465
|
+
const result = store.listMemoriesForDashboard({
|
|
466
|
+
repo: "repo-dashboard",
|
|
467
|
+
search: "memory",
|
|
468
|
+
sortBy: "title",
|
|
469
|
+
sortOrder: "asc",
|
|
470
|
+
limit: 2,
|
|
471
|
+
offset: 0,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(result.total).toBe(3);
|
|
475
|
+
expect(result.items).toHaveLength(2);
|
|
476
|
+
expect(result.items[0].title).toBe("Alpha Memory");
|
|
477
|
+
expect(result.items[1].title).toBe("Beta Memory");
|
|
478
|
+
|
|
479
|
+
store.close();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("getByIdWithStats returns recall_rate for detail views", () => {
|
|
483
|
+
const store = freshStore();
|
|
484
|
+
|
|
485
|
+
store.insert(makeEntry({
|
|
486
|
+
id: "detail-1",
|
|
487
|
+
repo: "repo-detail",
|
|
488
|
+
title: "Detail Memory",
|
|
489
|
+
content: "detail content",
|
|
490
|
+
}));
|
|
491
|
+
|
|
492
|
+
store.incrementHitCount("detail-1");
|
|
493
|
+
store.incrementHitCount("detail-1");
|
|
494
|
+
store.incrementRecallCount("detail-1");
|
|
495
|
+
|
|
496
|
+
const memory = store.getByIdWithStats("detail-1");
|
|
497
|
+
|
|
498
|
+
expect(memory).not.toBeNull();
|
|
499
|
+
expect(memory?.title).toBe("Detail Memory");
|
|
500
|
+
expect(memory?.recall_rate).toBe(0.5);
|
|
501
|
+
|
|
502
|
+
store.close();
|
|
503
|
+
});
|
|
504
|
+
});
|