coding-friend-cli 1.37.0 → 1.38.1
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/README.md +7 -2
- package/dist/{chunk-GZT3YPAP.js → chunk-32UAW3NX.js} +1 -1
- package/dist/chunk-5QCGEPQO.js +238 -0
- package/dist/{chunk-YIXAQ4R2.js → chunk-6NXZKAOV.js} +1 -1
- package/dist/{chunk-DP6DFQ4G.js → chunk-BXSCYGWE.js} +2 -2
- package/dist/{chunk-W6D3J5DX.js → chunk-CGEMETKQ.js} +8 -6
- package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
- package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
- package/dist/{chunk-6S7352JG.js → chunk-ONG4KRYP.js} +1 -1
- package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
- package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
- package/dist/{chunk-PI4P34KF.js → chunk-RFNKPAJW.js} +3 -1
- package/dist/{chunk-676YCW6T.js → chunk-XAYWGZQT.js} +101 -8
- package/dist/{clean-WEYQZB34.js → clean-YWEMYDE5.js} +2 -2
- package/dist/{config-ZMR2AOBB.js → config-NG7SICM4.js} +12 -24
- package/dist/{dev-IGHMMEP4.js → dev-PTBNMKZT.js} +3 -3
- package/dist/{disable-HL5OEURT.js → disable-MO7AXYYF.js} +20 -3
- package/dist/{enable-CZIOCKKJ.js → enable-4GXLQEHD.js} +20 -3
- package/dist/{guide-CO2V4ZLZ.js → guide-HCR43LJN.js} +1 -1
- package/dist/{host-SSRI4JZV.js → host-EV6QIVTI.js} +2 -2
- package/dist/index.js +59 -47
- package/dist/{init-6MFQWPOP.js → init-KYE5CJMW.js} +115 -12
- package/dist/{install-37NN4CQH.js → install-3YEPLDMD.js} +79 -6
- package/dist/{learn-OPH3CI67.js → learn-MC4VDHZH.js} +2 -2
- package/dist/{mcp-DWKVMNC7.js → mcp-FV6YX3ZT.js} +7 -7
- package/dist/{mcp-serve-5GODJ4DJ.js → mcp-serve-PXREYQRZ.js} +2 -2
- package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
- package/dist/{memory-TRDLADUM.js → memory-DF7OUOTV.js} +6 -6
- package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
- package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
- package/dist/{session-S4WKA4P5.js → session-QK6OZO3X.js} +2 -2
- package/dist/{status-YZEOUX46.js → status-6HQRVZKR.js} +11 -10
- package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
- package/dist/{uninstall-AVA3J554.js → uninstall-MF6FRUBO.js} +51 -4
- package/dist/{update-Q3ACCK4Z.js → update-SMWXKJ3R.js} +6 -5
- package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
- package/lib/cf-memory/src/lib/claude-md.ts +82 -70
- package/lib/cf-memory/src/lib/status-frame.ts +2 -2
- package/lib/cf-memory/src/tools/delete.ts +1 -1
- package/lib/cf-memory/src/tools/store.ts +6 -6
- package/lib/cf-memory/src/tools/update.ts +5 -11
- package/package.json +32 -4
- package/lib/cf-memory/README.md +0 -317
- package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
- package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
- package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
- package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
- package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
- package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
- package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
- package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
- package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
- package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
- package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
- package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
- package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
- package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
- package/lib/cf-memory/src/__tests__/sqlite-lazy-db.test.ts +0 -213
- package/lib/cf-memory/src/__tests__/status-frame.test.ts +0 -136
- package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +0 -54
- package/lib/cf-memory/src/__tests__/tier.test.ts +0 -388
- package/lib/cf-memory/src/__tests__/tools.test.ts +0 -83
- package/lib/cf-memory/vitest.config.ts +0 -7
- package/lib/learn-host/.prettierignore +0 -4
- package/lib/learn-host/.prettierrc +0 -8
- package/lib/learn-host/README.md +0 -113
- package/lib/learn-host/eslint.config.mjs +0 -6
- package/lib/learn-mcp/README.md +0 -187
- package/lib/learn-mcp/src/__tests__/docs.test.ts +0 -117
- package/lib/learn-mcp/vitest.config.ts +0 -8
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
SCHEMA_V1,
|
|
4
|
-
SCHEMA_VERSION,
|
|
5
|
-
PRAGMA_SETTINGS,
|
|
6
|
-
EMBEDDING_DIMS,
|
|
7
|
-
SCHEMA_V2_METADATA,
|
|
8
|
-
getVecTableSQL,
|
|
9
|
-
} from "../backends/sqlite/schema.js";
|
|
10
|
-
|
|
11
|
-
describe("Schema constants", () => {
|
|
12
|
-
it("SCHEMA_VERSION is 2", () => {
|
|
13
|
-
expect(SCHEMA_VERSION).toBe(2);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("EMBEDDING_DIMS is 384 (all-MiniLM-L6-v2)", () => {
|
|
17
|
-
expect(EMBEDDING_DIMS).toBe(384);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("SCHEMA_V1 contains all required table creation statements", () => {
|
|
21
|
-
const combined = SCHEMA_V1.join(" ");
|
|
22
|
-
|
|
23
|
-
// Main tables
|
|
24
|
-
expect(combined).toContain("CREATE TABLE IF NOT EXISTS schema_version");
|
|
25
|
-
expect(combined).toContain("CREATE TABLE IF NOT EXISTS memories");
|
|
26
|
-
expect(combined).toContain("CREATE TABLE IF NOT EXISTS embedding_cache");
|
|
27
|
-
|
|
28
|
-
// FTS5 virtual table
|
|
29
|
-
expect(combined).toContain(
|
|
30
|
-
"CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5",
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// FTS5 sync triggers
|
|
34
|
-
expect(combined).toContain("CREATE TRIGGER IF NOT EXISTS memories_ai");
|
|
35
|
-
expect(combined).toContain("CREATE TRIGGER IF NOT EXISTS memories_ad");
|
|
36
|
-
expect(combined).toContain("CREATE TRIGGER IF NOT EXISTS memories_au");
|
|
37
|
-
|
|
38
|
-
// Indexes
|
|
39
|
-
expect(combined).toContain("CREATE INDEX IF NOT EXISTS idx_memories_type");
|
|
40
|
-
expect(combined).toContain(
|
|
41
|
-
"CREATE INDEX IF NOT EXISTS idx_memories_category",
|
|
42
|
-
);
|
|
43
|
-
expect(combined).toContain(
|
|
44
|
-
"CREATE INDEX IF NOT EXISTS idx_memories_updated",
|
|
45
|
-
);
|
|
46
|
-
expect(combined).toContain(
|
|
47
|
-
"CREATE INDEX IF NOT EXISTS idx_memories_content_hash",
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("memories table has all required columns", () => {
|
|
52
|
-
const memoriesTable = SCHEMA_V1.find((s) =>
|
|
53
|
-
s.includes("CREATE TABLE IF NOT EXISTS memories"),
|
|
54
|
-
)!;
|
|
55
|
-
|
|
56
|
-
const requiredColumns = [
|
|
57
|
-
"id TEXT PRIMARY KEY",
|
|
58
|
-
"slug TEXT NOT NULL",
|
|
59
|
-
"category TEXT NOT NULL",
|
|
60
|
-
"title TEXT NOT NULL",
|
|
61
|
-
"description TEXT NOT NULL",
|
|
62
|
-
"type TEXT NOT NULL",
|
|
63
|
-
"tags TEXT NOT NULL",
|
|
64
|
-
"importance INTEGER NOT NULL",
|
|
65
|
-
"created TEXT NOT NULL",
|
|
66
|
-
"updated TEXT NOT NULL",
|
|
67
|
-
"source TEXT NOT NULL",
|
|
68
|
-
"content TEXT NOT NULL",
|
|
69
|
-
"content_hash TEXT NOT NULL",
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
for (const col of requiredColumns) {
|
|
73
|
-
expect(memoriesTable).toContain(col);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("FTS5 table uses porter tokenizer", () => {
|
|
78
|
-
const ftsTable = SCHEMA_V1.find((s) => s.includes("memories_fts"))!;
|
|
79
|
-
expect(ftsTable).toContain("tokenize='porter unicode61'");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("embedding_cache table has required columns", () => {
|
|
83
|
-
const cacheTable = SCHEMA_V1.find((s) =>
|
|
84
|
-
s.includes("CREATE TABLE IF NOT EXISTS embedding_cache"),
|
|
85
|
-
)!;
|
|
86
|
-
expect(cacheTable).toContain("content_hash TEXT PRIMARY KEY");
|
|
87
|
-
expect(cacheTable).toContain("embedding BLOB NOT NULL");
|
|
88
|
-
expect(cacheTable).toContain("model TEXT NOT NULL");
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("PRAGMA_SETTINGS", () => {
|
|
93
|
-
it("enables WAL mode", () => {
|
|
94
|
-
expect(PRAGMA_SETTINGS).toContain("PRAGMA journal_mode = WAL");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("sets synchronous to NORMAL", () => {
|
|
98
|
-
expect(PRAGMA_SETTINGS).toContain("PRAGMA synchronous = NORMAL");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("sets cache size", () => {
|
|
102
|
-
const cachePragma = PRAGMA_SETTINGS.find((p) => p.includes("cache_size"));
|
|
103
|
-
expect(cachePragma).toBeDefined();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("getVecTableSQL()", () => {
|
|
108
|
-
it("creates vec0 table with default dims", () => {
|
|
109
|
-
const sql = getVecTableSQL();
|
|
110
|
-
expect(sql).toContain(
|
|
111
|
-
"CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0",
|
|
112
|
-
);
|
|
113
|
-
expect(sql).toContain("float[384]");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("supports custom dimensions", () => {
|
|
117
|
-
const sql = getVecTableSQL(768);
|
|
118
|
-
expect(sql).toContain("float[768]");
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe("SCHEMA_V2_METADATA", () => {
|
|
123
|
-
it("creates metadata table with key-value structure", () => {
|
|
124
|
-
expect(SCHEMA_V2_METADATA).toContain("CREATE TABLE IF NOT EXISTS metadata");
|
|
125
|
-
expect(SCHEMA_V2_METADATA).toContain("key TEXT PRIMARY KEY");
|
|
126
|
-
expect(SCHEMA_V2_METADATA).toContain("value TEXT NOT NULL");
|
|
127
|
-
});
|
|
128
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { detectSearchMode, rrfFuse } from "../backends/sqlite/search.js";
|
|
3
|
-
|
|
4
|
-
describe("detectSearchMode()", () => {
|
|
5
|
-
it("returns 'keyword' for quoted strings", () => {
|
|
6
|
-
expect(detectSearchMode('"exact phrase"')).toBe("keyword");
|
|
7
|
-
expect(detectSearchMode("'single quoted'")).toBe("keyword");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("returns 'keyword' for code-like patterns", () => {
|
|
11
|
-
expect(detectSearchMode("auth.middleware")).toBe("keyword");
|
|
12
|
-
expect(detectSearchMode("std::vector")).toBe("keyword");
|
|
13
|
-
expect(detectSearchMode("user->name")).toBe("keyword");
|
|
14
|
-
expect(detectSearchMode("src/lib/auth")).toBe("keyword");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("returns 'semantic' for questions", () => {
|
|
18
|
-
expect(detectSearchMode("how does auth work")).toBe("semantic");
|
|
19
|
-
expect(detectSearchMode("What is the login flow")).toBe("semantic");
|
|
20
|
-
expect(detectSearchMode("why did we choose JWT")).toBe("semantic");
|
|
21
|
-
expect(detectSearchMode("where is the config stored")).toBe("semantic");
|
|
22
|
-
expect(detectSearchMode("when was this added")).toBe("semantic");
|
|
23
|
-
expect(detectSearchMode("who implemented this")).toBe("semantic");
|
|
24
|
-
expect(detectSearchMode("is this deprecated")).toBe("semantic");
|
|
25
|
-
expect(detectSearchMode("can we use OAuth")).toBe("semantic");
|
|
26
|
-
expect(detectSearchMode("does this support SSO")).toBe("semantic");
|
|
27
|
-
expect(detectSearchMode("should we migrate")).toBe("semantic");
|
|
28
|
-
expect(detectSearchMode("which database to use")).toBe("semantic");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("returns 'hybrid' for general queries", () => {
|
|
32
|
-
expect(detectSearchMode("authentication")).toBe("hybrid");
|
|
33
|
-
expect(detectSearchMode("JWT tokens")).toBe("hybrid");
|
|
34
|
-
expect(detectSearchMode("database migration")).toBe("hybrid");
|
|
35
|
-
expect(detectSearchMode("CORS fix")).toBe("hybrid");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("handles edge cases", () => {
|
|
39
|
-
expect(detectSearchMode(" authentication ")).toBe("hybrid");
|
|
40
|
-
expect(detectSearchMode("How")).toBe("semantic");
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("rrfFuse()", () => {
|
|
45
|
-
it("merges two ranked lists using RRF", () => {
|
|
46
|
-
const list1 = [
|
|
47
|
-
{ id: "a", score: 10, matchedOn: ["title"] },
|
|
48
|
-
{ id: "b", score: 5, matchedOn: ["content"] },
|
|
49
|
-
];
|
|
50
|
-
const list2 = [
|
|
51
|
-
{ id: "b", score: 0.9, matchedOn: ["semantic"] },
|
|
52
|
-
{ id: "c", score: 0.5, matchedOn: ["semantic"] },
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const fused = rrfFuse(list1, list2);
|
|
56
|
-
|
|
57
|
-
// "b" appears in both lists, should have highest RRF score
|
|
58
|
-
expect(fused[0].id).toBe("b");
|
|
59
|
-
expect(fused[0].matchedOn).toContain("content");
|
|
60
|
-
expect(fused[0].matchedOn).toContain("semantic");
|
|
61
|
-
|
|
62
|
-
// All three items should be in the result
|
|
63
|
-
expect(fused.map((r) => r.id).sort()).toEqual(["a", "b", "c"]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("handles single list", () => {
|
|
67
|
-
const list = [
|
|
68
|
-
{ id: "a", score: 10, matchedOn: ["title"] },
|
|
69
|
-
{ id: "b", score: 5, matchedOn: ["content"] },
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
const fused = rrfFuse(list);
|
|
73
|
-
expect(fused).toHaveLength(2);
|
|
74
|
-
expect(fused[0].id).toBe("a"); // rank 0 in first list
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("handles empty lists", () => {
|
|
78
|
-
expect(rrfFuse([], [])).toEqual([]);
|
|
79
|
-
expect(rrfFuse([])).toEqual([]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("handles three lists", () => {
|
|
83
|
-
const list1 = [{ id: "a", score: 10, matchedOn: ["title"] }];
|
|
84
|
-
const list2 = [{ id: "a", score: 0.9, matchedOn: ["semantic"] }];
|
|
85
|
-
const list3 = [{ id: "b", score: 5, matchedOn: ["tags"] }];
|
|
86
|
-
|
|
87
|
-
const fused = rrfFuse(list1, list2, list3);
|
|
88
|
-
|
|
89
|
-
// "a" appears in 2 lists, should rank higher than "b" in 1 list
|
|
90
|
-
expect(fused[0].id).toBe("a");
|
|
91
|
-
expect(fused[0].score).toBeGreaterThan(fused[1].score);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("RRF scores are positive", () => {
|
|
95
|
-
const list = [
|
|
96
|
-
{ id: "a", score: 10, matchedOn: ["title"] },
|
|
97
|
-
{ id: "b", score: 5, matchedOn: ["content"] },
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
const fused = rrfFuse(list);
|
|
101
|
-
for (const item of fused) {
|
|
102
|
-
expect(item.score).toBeGreaterThan(0);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("merges matchedOn from multiple lists", () => {
|
|
107
|
-
const list1 = [{ id: "a", score: 10, matchedOn: ["title", "tags"] }];
|
|
108
|
-
const list2 = [{ id: "a", score: 0.9, matchedOn: ["semantic"] }];
|
|
109
|
-
|
|
110
|
-
const fused = rrfFuse(list1, list2);
|
|
111
|
-
expect(fused[0].matchedOn).toContain("title");
|
|
112
|
-
expect(fused[0].matchedOn).toContain("tags");
|
|
113
|
-
expect(fused[0].matchedOn).toContain("semantic");
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for lazy DB creation in SqliteBackend.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that constructing a SqliteBackend (or performing read operations)
|
|
5
|
-
* on a project with no existing memories does NOT create the DB file or dir,
|
|
6
|
-
* and that the first write (store) creates the DB and the item is findable.
|
|
7
|
-
*/
|
|
8
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
9
|
-
import { mkdirSync, rmSync, existsSync } from "fs";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
import { tmpdir } from "os";
|
|
12
|
-
import { areSqliteDepsAvailable } from "../lib/lazy-install.js";
|
|
13
|
-
import { SqliteBackend } from "../backends/sqlite/index.js";
|
|
14
|
-
import { createBackendForTier } from "../lib/tier.js";
|
|
15
|
-
import type { StoreInput } from "../lib/types.js";
|
|
16
|
-
|
|
17
|
-
let testDir: string;
|
|
18
|
-
let dbPath: string;
|
|
19
|
-
let docsDir: string;
|
|
20
|
-
let counter = 0;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
testDir = join(
|
|
24
|
-
tmpdir(),
|
|
25
|
-
`cf-memory-lazy-db-test-${Date.now()}-${++counter}`,
|
|
26
|
-
);
|
|
27
|
-
mkdirSync(testDir, { recursive: true });
|
|
28
|
-
docsDir = join(testDir, "docs", "memory");
|
|
29
|
-
dbPath = join(testDir, "db.sqlite");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const sampleInput: StoreInput = {
|
|
37
|
-
title: "Lazy DB Test Memory",
|
|
38
|
-
description: "Created to test lazy DB init",
|
|
39
|
-
type: "fact",
|
|
40
|
-
tags: ["test"],
|
|
41
|
-
content: "# Lazy DB\n\nThis memory tests deferred DB creation.",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const depsAvailable = areSqliteDepsAvailable();
|
|
45
|
-
|
|
46
|
-
describe("SqliteBackend — lazy DB creation", () => {
|
|
47
|
-
it.skipIf(!depsAvailable)(
|
|
48
|
-
"(a) constructing the backend does NOT create the DB file",
|
|
49
|
-
() => {
|
|
50
|
-
new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
51
|
-
|
|
52
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
53
|
-
},
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
it.skipIf(!depsAvailable)(
|
|
57
|
-
"(b) search() on empty backend returns [] without creating the DB",
|
|
58
|
-
async () => {
|
|
59
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
60
|
-
|
|
61
|
-
const results = await backend.search({ query: "anything" });
|
|
62
|
-
|
|
63
|
-
expect(results).toEqual([]);
|
|
64
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
65
|
-
|
|
66
|
-
await backend.close();
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
it.skipIf(!depsAvailable)(
|
|
71
|
-
"(b) list() on empty backend returns [] without creating the DB",
|
|
72
|
-
async () => {
|
|
73
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
74
|
-
|
|
75
|
-
const metas = await backend.list({});
|
|
76
|
-
|
|
77
|
-
expect(metas).toEqual([]);
|
|
78
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
79
|
-
|
|
80
|
-
await backend.close();
|
|
81
|
-
},
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
it.skipIf(!depsAvailable)(
|
|
85
|
-
"(b) stats() on empty backend returns zeroed stats without creating the DB",
|
|
86
|
-
async () => {
|
|
87
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
88
|
-
|
|
89
|
-
const stats = await backend.stats();
|
|
90
|
-
|
|
91
|
-
expect(stats).toEqual({ total: 0, byCategory: {}, byType: {} });
|
|
92
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
93
|
-
|
|
94
|
-
await backend.close();
|
|
95
|
-
},
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
it.skipIf(!depsAvailable)(
|
|
99
|
-
"(c) after store(), DB file exists and search() finds the item",
|
|
100
|
-
async () => {
|
|
101
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
102
|
-
|
|
103
|
-
// DB should not exist before write
|
|
104
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
105
|
-
|
|
106
|
-
await backend.store(sampleInput);
|
|
107
|
-
|
|
108
|
-
// DB should now exist
|
|
109
|
-
expect(existsSync(dbPath)).toBe(true);
|
|
110
|
-
|
|
111
|
-
// Item should be findable via empty-query search (routes through list)
|
|
112
|
-
const results = await backend.search({ query: "" });
|
|
113
|
-
expect(results.length).toBe(1);
|
|
114
|
-
expect(results[0].memory.frontmatter.title).toBe("Lazy DB Test Memory");
|
|
115
|
-
|
|
116
|
-
await backend.close();
|
|
117
|
-
},
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
it.skipIf(!depsAvailable)(
|
|
121
|
-
"close() on a never-opened backend does not throw",
|
|
122
|
-
async () => {
|
|
123
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
124
|
-
|
|
125
|
-
await expect(backend.close()).resolves.toBeUndefined();
|
|
126
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
127
|
-
},
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
it.skipIf(!depsAvailable)(
|
|
131
|
-
"getDbPath() returns the computed path without touching the filesystem",
|
|
132
|
-
() => {
|
|
133
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
134
|
-
|
|
135
|
-
expect(backend.getDbPath()).toBe(dbPath);
|
|
136
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
it.skipIf(!depsAvailable)(
|
|
141
|
-
"delete(nonexistentId) returns false and does not create the DB",
|
|
142
|
-
async () => {
|
|
143
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
144
|
-
|
|
145
|
-
const result = await backend.delete("nonexistent/id");
|
|
146
|
-
|
|
147
|
-
expect(result).toBe(false);
|
|
148
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
149
|
-
|
|
150
|
-
await backend.close();
|
|
151
|
-
},
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
it.skipIf(!depsAvailable)(
|
|
155
|
-
"update(nonexistentId) returns null and does not create the DB",
|
|
156
|
-
async () => {
|
|
157
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
158
|
-
|
|
159
|
-
const result = await backend.update({ id: "nonexistent/id", title: "New Title" });
|
|
160
|
-
|
|
161
|
-
expect(result).toBeNull();
|
|
162
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
163
|
-
|
|
164
|
-
await backend.close();
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
it.skipIf(!depsAvailable)(
|
|
169
|
-
"rebuild() on empty docs/memory does not create the DB file",
|
|
170
|
-
async () => {
|
|
171
|
-
// docsDir does not exist (no memories)
|
|
172
|
-
const backend = new SqliteBackend(docsDir, { dbPath, skipVec: true });
|
|
173
|
-
|
|
174
|
-
await backend.rebuild();
|
|
175
|
-
|
|
176
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
177
|
-
|
|
178
|
-
await backend.close();
|
|
179
|
-
},
|
|
180
|
-
);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("SqliteBackend — lazy DB via createBackendForTier (integration)", () => {
|
|
184
|
-
it.skipIf(!depsAvailable)(
|
|
185
|
-
"createBackendForTier 'full' on empty project does not create DB file before or after read ops",
|
|
186
|
-
async () => {
|
|
187
|
-
// Use explicit dbPath so we do not pollute ~/.coding-friend
|
|
188
|
-
const { backend, tier } = await createBackendForTier(
|
|
189
|
-
docsDir,
|
|
190
|
-
"full",
|
|
191
|
-
undefined,
|
|
192
|
-
{ dbPath },
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
expect(tier.name).toBe("full");
|
|
196
|
-
|
|
197
|
-
// (1) No DB file after construction
|
|
198
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
199
|
-
|
|
200
|
-
// (2) Read ops must not create the DB file
|
|
201
|
-
await backend.list({});
|
|
202
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
203
|
-
|
|
204
|
-
await backend.search({ query: "anything" });
|
|
205
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
206
|
-
|
|
207
|
-
await backend.stats();
|
|
208
|
-
expect(existsSync(dbPath)).toBe(false);
|
|
209
|
-
|
|
210
|
-
await backend.close();
|
|
211
|
-
},
|
|
212
|
-
);
|
|
213
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { buildStoreStatus, buildUpdateStatus } from "../lib/status-frame.js";
|
|
3
|
-
|
|
4
|
-
describe("buildStoreStatus", () => {
|
|
5
|
-
it("returns a list with markdown and database info", () => {
|
|
6
|
-
const result = buildStoreStatus({
|
|
7
|
-
id: "features/auth-pattern",
|
|
8
|
-
title: "Auth Pattern",
|
|
9
|
-
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
10
|
-
dbPath: "/home/.coding-friend/memory/projects/-foo/db.sqlite",
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
expect(result).toContain("features/auth-pattern");
|
|
14
|
-
expect(result).toContain("Auth Pattern");
|
|
15
|
-
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
16
|
-
expect(result).toContain("db.sqlite");
|
|
17
|
-
// No box-drawing characters
|
|
18
|
-
expect(result).not.toContain("╭");
|
|
19
|
-
expect(result).not.toContain("╰");
|
|
20
|
-
expect(result).not.toContain("│");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("shows markdown-only when dbPath is null", () => {
|
|
24
|
-
const result = buildStoreStatus({
|
|
25
|
-
id: "features/auth-pattern",
|
|
26
|
-
title: "Auth Pattern",
|
|
27
|
-
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
28
|
-
dbPath: null,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
32
|
-
expect(result).not.toContain("db.sqlite");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("shows CLAUDE.md updated message when claudeMdUpdated is true", () => {
|
|
36
|
-
const result = buildStoreStatus({
|
|
37
|
-
id: "conventions/code-style",
|
|
38
|
-
title: "Code Style",
|
|
39
|
-
markdownPath: "/docs/memory/conventions/code-style.md",
|
|
40
|
-
dbPath: null,
|
|
41
|
-
claudeMdUpdated: true,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
expect(result).toContain("CLAUDE.md updated");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("omits CLAUDE.md line when claudeMdUpdated is false", () => {
|
|
48
|
-
const result = buildStoreStatus({
|
|
49
|
-
id: "features/auth-pattern",
|
|
50
|
-
title: "Auth Pattern",
|
|
51
|
-
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
52
|
-
dbPath: null,
|
|
53
|
-
claudeMdUpdated: false,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
expect(result).not.toContain("CLAUDE.md");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("omits CLAUDE.md line when claudeMdUpdated is not provided", () => {
|
|
60
|
-
const result = buildStoreStatus({
|
|
61
|
-
id: "features/auth-pattern",
|
|
62
|
-
title: "Auth Pattern",
|
|
63
|
-
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
64
|
-
dbPath: null,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(result).not.toContain("CLAUDE.md");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("includes duplicate warning when provided", () => {
|
|
71
|
-
const result = buildStoreStatus({
|
|
72
|
-
id: "features/auth-pattern",
|
|
73
|
-
title: "Auth Pattern",
|
|
74
|
-
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
75
|
-
dbPath: null,
|
|
76
|
-
warning: "Near-duplicate found: features/old-auth (similarity: 0.82)",
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
expect(result).toContain("Near-duplicate");
|
|
80
|
-
expect(result).toContain("⚠");
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("buildUpdateStatus", () => {
|
|
85
|
-
it("returns a list with markdown and database info", () => {
|
|
86
|
-
const result = buildUpdateStatus({
|
|
87
|
-
id: "features/auth-pattern",
|
|
88
|
-
title: "Auth Pattern",
|
|
89
|
-
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
90
|
-
dbPath: "/home/.coding-friend/memory/projects/-foo/db.sqlite",
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
expect(result).toContain("features/auth-pattern");
|
|
94
|
-
expect(result).toContain("Auth Pattern");
|
|
95
|
-
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
96
|
-
expect(result).toContain("db.sqlite");
|
|
97
|
-
expect(result).not.toContain("╭");
|
|
98
|
-
expect(result).not.toContain("╰");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("shows markdown-only when dbPath is null", () => {
|
|
102
|
-
const result = buildUpdateStatus({
|
|
103
|
-
id: "features/auth-pattern",
|
|
104
|
-
title: "Auth Pattern",
|
|
105
|
-
markdownPath: "/docs/memory/features/auth-pattern.md",
|
|
106
|
-
dbPath: null,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
expect(result).toContain("/docs/memory/features/auth-pattern.md");
|
|
110
|
-
expect(result).not.toContain("db.sqlite");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("shows CLAUDE.md updated message when claudeMdUpdated is true", () => {
|
|
114
|
-
const result = buildUpdateStatus({
|
|
115
|
-
id: "conventions/code-style",
|
|
116
|
-
title: "Code Style",
|
|
117
|
-
markdownPath: "/docs/memory/conventions/code-style.md",
|
|
118
|
-
dbPath: null,
|
|
119
|
-
claudeMdUpdated: true,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
expect(result).toContain("CLAUDE.md updated");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("omits CLAUDE.md line when claudeMdUpdated is false", () => {
|
|
126
|
-
const result = buildUpdateStatus({
|
|
127
|
-
id: "conventions/code-style",
|
|
128
|
-
title: "Code Style",
|
|
129
|
-
markdownPath: "/docs/memory/conventions/code-style.md",
|
|
130
|
-
dbPath: null,
|
|
131
|
-
claudeMdUpdated: false,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(result).not.toContain("CLAUDE.md");
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { applyTemporalDecay } from "../lib/temporal-decay.js";
|
|
3
|
-
|
|
4
|
-
describe("applyTemporalDecay()", () => {
|
|
5
|
-
it("keeps ~100% of score for today's date", () => {
|
|
6
|
-
const today = new Date().toISOString().split("T")[0];
|
|
7
|
-
const result = applyTemporalDecay(10, today);
|
|
8
|
-
// Should be very close to original score
|
|
9
|
-
expect(result).toBeGreaterThan(9.9);
|
|
10
|
-
expect(result).toBeLessThanOrEqual(10);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("decays score for 90-day-old memories", () => {
|
|
14
|
-
const date90ago = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
|
|
15
|
-
.toISOString()
|
|
16
|
-
.split("T")[0];
|
|
17
|
-
const result = applyTemporalDecay(10, date90ago);
|
|
18
|
-
// 0.7 + 0.3 * exp(-1) ≈ 0.7 + 0.3 * 0.368 ≈ 0.81
|
|
19
|
-
expect(result).toBeGreaterThan(7.5);
|
|
20
|
-
expect(result).toBeLessThan(8.5);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("keeps at least 70% of score for very old memories", () => {
|
|
24
|
-
const result = applyTemporalDecay(10, "2020-01-01");
|
|
25
|
-
// 0.7 + 0.3 * exp(-very_large) ≈ 0.7
|
|
26
|
-
expect(result).toBeGreaterThanOrEqual(6.9);
|
|
27
|
-
expect(result).toBeLessThan(7.2);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("boosts score based on access count", () => {
|
|
31
|
-
const today = new Date().toISOString().split("T")[0];
|
|
32
|
-
const withoutAccess = applyTemporalDecay(10, today);
|
|
33
|
-
const withAccess = applyTemporalDecay(10, today, 5);
|
|
34
|
-
expect(withAccess).toBeGreaterThan(withoutAccess);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("caps access boost at 10", () => {
|
|
38
|
-
const today = new Date().toISOString().split("T")[0];
|
|
39
|
-
const at10 = applyTemporalDecay(10, today, 10);
|
|
40
|
-
const at100 = applyTemporalDecay(10, today, 100);
|
|
41
|
-
expect(at10).toBe(at100);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("handles zero score", () => {
|
|
45
|
-
const result = applyTemporalDecay(0, "2026-01-01");
|
|
46
|
-
expect(result).toBe(0);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("handles negative score (BM25 negated)", () => {
|
|
50
|
-
const today = new Date().toISOString().split("T")[0];
|
|
51
|
-
const result = applyTemporalDecay(-5, today);
|
|
52
|
-
expect(result).toBeLessThan(0);
|
|
53
|
-
});
|
|
54
|
-
});
|