coding-friend-cli 1.36.4 → 1.38.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 (71) hide show
  1. package/README.md +8 -3
  2. package/dist/{chunk-GZT3YPAP.js → chunk-32UAW3NX.js} +1 -1
  3. package/dist/chunk-5QCGEPQO.js +238 -0
  4. package/dist/{chunk-YIXAQ4R2.js → chunk-6NXZKAOV.js} +1 -1
  5. package/dist/{chunk-XHP3EBR3.js → chunk-776ZY62N.js} +112 -4
  6. package/dist/{chunk-N4WZWRCI.js → chunk-BXSCYGWE.js} +85 -37
  7. package/dist/{chunk-AK6RMCAC.js → chunk-CGEMETKQ.js} +44 -30
  8. package/dist/{chunk-XEKQW4RN.js → chunk-ETTZRJ55.js} +1 -1
  9. package/dist/{chunk-IKLMHJH6.js → chunk-GIH6E7GJ.js} +62 -1
  10. package/dist/{chunk-56YJWGAN.js → chunk-ONG4KRYP.js} +60 -47
  11. package/dist/{chunk-X2H7JJMR.js → chunk-PLSJW7QJ.js} +29 -1
  12. package/dist/{chunk-O5ZKLFSL.js → chunk-RCE4OWNX.js} +1 -1
  13. package/dist/{chunk-K4WSC62G.js → chunk-RFNKPAJW.js} +42 -2
  14. package/dist/{clean-Q4VU662V.js → clean-YWEMYDE5.js} +3 -2
  15. package/dist/{config-CTTHWJH5.js → config-NG7SICM4.js} +15 -27
  16. package/dist/{dev-IGHMMEP4.js → dev-PTBNMKZT.js} +3 -3
  17. package/dist/{disable-HL5OEURT.js → disable-MO7AXYYF.js} +20 -3
  18. package/dist/{enable-CZIOCKKJ.js → enable-4GXLQEHD.js} +20 -3
  19. package/dist/{guide-CO2V4ZLZ.js → guide-HCR43LJN.js} +1 -1
  20. package/dist/{host-NCJMUSME.js → host-EV6QIVTI.js} +3 -3
  21. package/dist/index.js +62 -48
  22. package/dist/{init-UABKVDVT.js → init-KYE5CJMW.js} +135 -17
  23. package/dist/{install-R2HDM3N4.js → install-XAFQY5YF.js} +80 -5
  24. package/dist/{learn-K64AUUFB.js → learn-MC4VDHZH.js} +2 -2
  25. package/dist/{mcp-OMQFSJP4.js → mcp-FV6YX3ZT.js} +24 -24
  26. package/dist/{mcp-serve-3FEPFVVQ.js → mcp-serve-PXREYQRZ.js} +10 -1
  27. package/dist/{mcp-serve-learn-OF3ZJUJC.js → mcp-serve-learn-UVXIDOEZ.js} +1 -1
  28. package/dist/{memory-Y6GNALG3.js → memory-DF7OUOTV.js} +7 -7
  29. package/dist/{permission-I3OFJUYA.js → permission-VISPGMUK.js} +38 -4
  30. package/dist/{permissions-J46VP6US.js → permissions-2SBACQHV.js} +2 -2
  31. package/dist/{session-Q2DLQPYP.js → session-QK6OZO3X.js} +3 -2
  32. package/dist/{status-ML4XEDYL.js → status-FVFTJ6GS.js} +18 -14
  33. package/dist/{statusline-KKFJPTI7.js → statusline-AOZHEQSV.js} +2 -2
  34. package/dist/{uninstall-ASLHT3MH.js → uninstall-MF6FRUBO.js} +64 -3
  35. package/dist/{update-DIOH45TE.js → update-QNUCSDSA.js} +7 -4
  36. package/dist/{update-check-25SCLHZO.js → update-check-7LKJLJPM.js} +1 -1
  37. package/lib/cf-memory/src/backends/sqlite/index.ts +142 -85
  38. package/lib/cf-memory/src/lib/claude-md.ts +82 -70
  39. package/lib/cf-memory/src/lib/status-frame.ts +2 -2
  40. package/lib/cf-memory/src/tools/delete.ts +1 -1
  41. package/lib/cf-memory/src/tools/store.ts +6 -6
  42. package/lib/cf-memory/src/tools/update.ts +5 -11
  43. package/package.json +32 -4
  44. package/lib/cf-memory/README.md +0 -317
  45. package/lib/cf-memory/src/__tests__/claude-md.test.ts +0 -343
  46. package/lib/cf-memory/src/__tests__/daemon-client.test.ts +0 -146
  47. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +0 -223
  48. package/lib/cf-memory/src/__tests__/daemon.test.ts +0 -407
  49. package/lib/cf-memory/src/__tests__/dedup.test.ts +0 -103
  50. package/lib/cf-memory/src/__tests__/embeddings.test.ts +0 -292
  51. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +0 -210
  52. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +0 -450
  53. package/lib/cf-memory/src/__tests__/migration.test.ts +0 -255
  54. package/lib/cf-memory/src/__tests__/migrations.test.ts +0 -288
  55. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +0 -262
  56. package/lib/cf-memory/src/__tests__/ollama.test.ts +0 -48
  57. package/lib/cf-memory/src/__tests__/project-id.test.ts +0 -43
  58. package/lib/cf-memory/src/__tests__/schema.test.ts +0 -128
  59. package/lib/cf-memory/src/__tests__/search.test.ts +0 -115
  60. package/lib/cf-memory/src/__tests__/status-frame.test.ts +0 -136
  61. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +0 -54
  62. package/lib/cf-memory/src/__tests__/tier.test.ts +0 -367
  63. package/lib/cf-memory/src/__tests__/tools.test.ts +0 -83
  64. package/lib/cf-memory/vitest.config.ts +0 -7
  65. package/lib/learn-host/.prettierignore +0 -4
  66. package/lib/learn-host/.prettierrc +0 -8
  67. package/lib/learn-host/README.md +0 -113
  68. package/lib/learn-host/eslint.config.mjs +0 -6
  69. package/lib/learn-mcp/README.md +0 -187
  70. package/lib/learn-mcp/src/__tests__/docs.test.ts +0 -117
  71. 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,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
- });