@gethmy/mcp 2.5.0 → 2.5.2

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.
@@ -1,213 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- fitToBudget,
4
- type ParkInput,
5
- relevanceFromRank,
6
- rescore,
7
- } from "../memory-park.js";
8
-
9
- const NOW = new Date("2026-05-08T12:00:00Z");
10
-
11
- function entity(
12
- partial: Partial<ParkInput> & {
13
- id: string;
14
- title?: string;
15
- content?: string;
16
- },
17
- ): ParkInput & { id: string; title?: string; content?: string } {
18
- return {
19
- id: partial.id,
20
- type: partial.type ?? "pattern",
21
- importance: partial.importance,
22
- last_accessed_at: partial.last_accessed_at,
23
- created_at: partial.created_at,
24
- title: partial.title,
25
- content: partial.content,
26
- };
27
- }
28
-
29
- describe("memory-park rescore", () => {
30
- test("fresh + high-importance pattern beats stale + low-importance pattern", () => {
31
- const fresh = entity({
32
- id: "fresh",
33
- type: "pattern",
34
- importance: 9,
35
- last_accessed_at: "2026-05-08T11:00:00Z",
36
- });
37
- const stale = entity({
38
- id: "stale",
39
- type: "pattern",
40
- importance: 4,
41
- last_accessed_at: "2024-05-08T11:00:00Z", // 2y old
42
- });
43
- const out = rescore([stale, fresh], { now: NOW });
44
- expect(out[0]?.entity.id).toBe("fresh");
45
- expect(out[1]?.entity.id).toBe("stale");
46
- expect(out[0]?.score).toBeGreaterThan(out[1]?.score ?? 0);
47
- });
48
-
49
- test("preferences never decay (tau=Infinity → recency=1)", () => {
50
- const ancient = entity({
51
- id: "p",
52
- type: "preference",
53
- importance: 9,
54
- last_accessed_at: "2020-01-01T00:00:00Z",
55
- });
56
- const out = rescore([ancient], { now: NOW });
57
- expect(out[0]?.recency).toBe(1);
58
- });
59
-
60
- test("caller-supplied relevance dominates when weight is high", () => {
61
- const a = entity({
62
- id: "a",
63
- type: "pattern",
64
- importance: 5,
65
- last_accessed_at: "2026-04-01T00:00:00Z",
66
- });
67
- const b = entity({
68
- id: "b",
69
- type: "pattern",
70
- importance: 9,
71
- last_accessed_at: "2026-04-01T00:00:00Z",
72
- });
73
- const relevance = new Map([
74
- ["a", 1.0],
75
- ["b", 0.0],
76
- ]);
77
- const out = rescore([a, b], { now: NOW, relevance });
78
- expect(out[0]?.entity.id).toBe("a");
79
- });
80
-
81
- test("missing importance falls back to per-type default", () => {
82
- const e = entity({
83
- id: "p",
84
- type: "preference", // default 9
85
- importance: null,
86
- last_accessed_at: "2026-05-08T11:00:00Z",
87
- });
88
- const out = rescore([e], { now: NOW });
89
- expect(out[0]?.importance).toBeCloseTo(0.9);
90
- });
91
-
92
- test("clamps importance to [1,10]", () => {
93
- const tooLow = entity({ id: "lo", type: "pattern", importance: -5 });
94
- const tooHigh = entity({ id: "hi", type: "pattern", importance: 99 });
95
- const out = rescore([tooLow, tooHigh], { now: NOW });
96
- const lo = out.find((s) => s.entity.id === "lo");
97
- const hi = out.find((s) => s.entity.id === "hi");
98
- expect(lo?.importance).toBeCloseTo(0.1);
99
- expect(hi?.importance).toBeCloseTo(1.0);
100
- });
101
-
102
- test("score is α·relevance + β·recency + γ·importance", () => {
103
- const e = entity({
104
- id: "x",
105
- type: "pattern",
106
- importance: 5,
107
- last_accessed_at: "2026-05-08T11:00:00Z", // very fresh
108
- });
109
- const out = rescore([e], {
110
- now: NOW,
111
- relevance: new Map([["x", 1.0]]),
112
- });
113
- const r = out[0]!;
114
- const expected = 0.55 * r.relevance + 0.25 * r.recency + 0.2 * r.importance;
115
- expect(r.score).toBeCloseTo(expected, 5);
116
- });
117
- });
118
-
119
- describe("memory-park relevanceFromRank (hybrid retrieval bridge)", () => {
120
- test("top-ranked entity gets relevance 1.0", () => {
121
- const ranked = [{ id: "a" }, { id: "b" }, { id: "c" }];
122
- const map = relevanceFromRank(ranked);
123
- expect(map.get("a")).toBeCloseTo(1.0);
124
- });
125
-
126
- test("relevance decays exponentially with rank", () => {
127
- const ranked = Array.from({ length: 20 }, (_, i) => ({ id: `r${i}` }));
128
- const map = relevanceFromRank(ranked, 10);
129
- expect(map.get("r0")).toBeCloseTo(1.0);
130
- expect(map.get("r10")).toBeCloseTo(Math.exp(-1));
131
- expect(map.get("r20")).toBeUndefined();
132
- });
133
-
134
- test("hybrid-ranked top-1 dominates Park rescore over a stale high-importance pattern", () => {
135
- const matchedByQuery = entity({
136
- id: "match",
137
- type: "pattern",
138
- importance: 5,
139
- last_accessed_at: "2025-05-08T11:00:00Z",
140
- });
141
- const stalePopular = entity({
142
- id: "popular",
143
- type: "pattern",
144
- importance: 9,
145
- last_accessed_at: "2024-05-08T11:00:00Z",
146
- });
147
- const ranked = [{ id: "match" }, { id: "popular" }];
148
- const relevance = relevanceFromRank(ranked);
149
- const scored = rescore([stalePopular, matchedByQuery], {
150
- now: NOW,
151
- relevance,
152
- });
153
- expect(scored[0]?.entity.id).toBe("match");
154
- });
155
-
156
- test("custom decay scales the falloff curve", () => {
157
- const ranked = Array.from({ length: 5 }, (_, i) => ({ id: `r${i}` }));
158
- const tight = relevanceFromRank(ranked, 1);
159
- const loose = relevanceFromRank(ranked, 100);
160
- expect(tight.get("r1")).toBeLessThan(loose.get("r1") ?? 0);
161
- });
162
- });
163
-
164
- describe("memory-park fitToBudget", () => {
165
- test("returns prefix that fits within token budget", () => {
166
- const ents = [
167
- entity({
168
- id: "1",
169
- type: "pattern",
170
- title: "x".repeat(40),
171
- content: "y".repeat(40),
172
- }),
173
- entity({
174
- id: "2",
175
- type: "pattern",
176
- title: "x".repeat(40),
177
- content: "y".repeat(40),
178
- }),
179
- entity({
180
- id: "3",
181
- type: "pattern",
182
- title: "x".repeat(40),
183
- content: "y".repeat(40),
184
- }),
185
- ];
186
- const scored = rescore(ents, { now: NOW });
187
- // Each entity ~ (40/4) + (40/4) + 4 = 24 tokens
188
- const fit = fitToBudget(scored, 50);
189
- expect(fit.length).toBe(2);
190
- });
191
-
192
- test("always includes top result even if over budget", () => {
193
- const big = entity({
194
- id: "big",
195
- type: "pattern",
196
- title: "x".repeat(2000),
197
- content: "y".repeat(2000),
198
- });
199
- const scored = rescore([big], { now: NOW });
200
- const fit = fitToBudget(scored, 10);
201
- expect(fit.length).toBe(1);
202
- expect(fit[0]?.entity.id).toBe("big");
203
- });
204
-
205
- test("zero or negative budget returns empty", () => {
206
- const ents = [
207
- entity({ id: "1", type: "pattern", title: "abc", content: "def" }),
208
- ];
209
- const scored = rescore(ents, { now: NOW });
210
- expect(fitToBudget(scored, 0)).toHaveLength(0);
211
- expect(fitToBudget(scored, -10)).toHaveLength(0);
212
- });
213
- });
@@ -1,77 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import {
4
- isSessionScope,
5
- resolveSessionScope,
6
- sessionScopeFor,
7
- } from "../memory-session.js";
8
-
9
- describe("resolveSessionScope", () => {
10
- it("returns undefined when caller did not pass a scope", () => {
11
- expect(resolveSessionScope(undefined, "abc-123")).toBeUndefined();
12
- });
13
-
14
- it("passes a non-alias scope through unchanged", () => {
15
- expect(resolveSessionScope("project", "abc-123")).toBe("project");
16
- expect(resolveSessionScope("private", undefined)).toBe("private");
17
- expect(resolveSessionScope("workspace", "abc-123")).toBe("workspace");
18
- expect(resolveSessionScope("global", undefined)).toBe("global");
19
- });
20
-
21
- it("resolves the 'session' alias to session:<id> when a session is active", () => {
22
- expect(
23
- resolveSessionScope("session", "11111111-1111-1111-1111-111111111111"),
24
- ).toBe("session:11111111-1111-1111-1111-111111111111");
25
- });
26
-
27
- it("throws when 'session' alias is used without an active session", () => {
28
- expect(() => resolveSessionScope("session", undefined)).toThrow(
29
- /requires an active agent session/i,
30
- );
31
- expect(() => resolveSessionScope("session", "")).toThrow(
32
- /requires an active agent session/i,
33
- );
34
- });
35
-
36
- it("does not auto-bind any other scope to the active session", () => {
37
- expect(resolveSessionScope("project", "abc-123")).toBe("project");
38
- expect(resolveSessionScope("private", "abc-123")).toBe("private");
39
- });
40
- });
41
-
42
- describe("isSessionScope", () => {
43
- it("recognizes concrete session:<id> values", () => {
44
- expect(isSessionScope("session:abc-123")).toBe(true);
45
- expect(isSessionScope("session:11111111-1111-1111-1111-111111111111")).toBe(
46
- true,
47
- );
48
- });
49
-
50
- it("rejects the bare 'session' alias (not yet resolved)", () => {
51
- expect(isSessionScope("session")).toBe(false);
52
- expect(isSessionScope("session:")).toBe(false);
53
- });
54
-
55
- it("rejects standard scopes", () => {
56
- expect(isSessionScope("private")).toBe(false);
57
- expect(isSessionScope("project")).toBe(false);
58
- expect(isSessionScope("workspace")).toBe(false);
59
- expect(isSessionScope("global")).toBe(false);
60
- });
61
-
62
- it("rejects non-string inputs", () => {
63
- expect(isSessionScope(undefined)).toBe(false);
64
- expect(isSessionScope(null)).toBe(false);
65
- });
66
- });
67
-
68
- describe("sessionScopeFor", () => {
69
- it("constructs the canonical scope value for a session id", () => {
70
- expect(sessionScopeFor("abc-123")).toBe("session:abc-123");
71
- });
72
-
73
- it("output round-trips through isSessionScope", () => {
74
- const scope = sessionScopeFor("11111111-1111-1111-1111-111111111111");
75
- expect(isSessionScope(scope)).toBe(true);
76
- });
77
- });