gsd-pi 2.75.0-dev.b6ad8c5f7 → 2.75.0-dev.e41b70b10

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 (181) hide show
  1. package/dist/resources/extensions/gsd/auto/phases.js +2 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.js +22 -1
  3. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +8 -2
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -11
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +3 -1
  6. package/dist/resources/extensions/gsd/auto-prompts.js +19 -9
  7. package/dist/resources/extensions/gsd/auto-worktree.js +16 -1
  8. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
  9. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  10. package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
  11. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -92
  12. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  13. package/dist/resources/extensions/gsd/commands-memory.js +462 -0
  14. package/dist/resources/extensions/gsd/doctor-git-checks.js +22 -2
  15. package/dist/resources/extensions/gsd/gsd-db.js +237 -4
  16. package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
  17. package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
  18. package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
  19. package/dist/resources/extensions/gsd/memory-relations.js +189 -0
  20. package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
  21. package/dist/resources/extensions/gsd/memory-store.js +299 -6
  22. package/dist/resources/extensions/gsd/pre-execution-checks.js +12 -8
  23. package/dist/resources/extensions/gsd/prompts/add-tests.md +1 -0
  24. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  25. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  26. package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
  27. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +14 -0
  28. package/dist/resources/extensions/search-the-web/command-search-provider.js +4 -1
  29. package/dist/resources/extensions/search-the-web/native-search.js +13 -2
  30. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  31. package/dist/web/standalone/.next/BUILD_ID +1 -1
  32. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  33. package/dist/web/standalone/.next/build-manifest.json +2 -2
  34. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  35. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.html +1 -1
  52. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  61. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  62. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  63. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  66. package/packages/mcp-server/dist/server.js +12 -10
  67. package/packages/mcp-server/dist/server.js.map +1 -1
  68. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  69. package/packages/mcp-server/dist/session-manager.js +8 -1
  70. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  71. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
  72. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  73. package/packages/mcp-server/dist/workflow-tools.js +207 -71
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/src/mcp-server.test.ts +40 -4
  76. package/packages/mcp-server/src/server.ts +12 -10
  77. package/packages/mcp-server/src/session-manager.ts +10 -3
  78. package/packages/mcp-server/src/workflow-tools.test.ts +346 -1
  79. package/packages/mcp-server/src/workflow-tools.ts +228 -75
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  81. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  82. package/packages/pi-ai/dist/index.d.ts +1 -0
  83. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  84. package/packages/pi-ai/dist/index.js +1 -0
  85. package/packages/pi-ai/dist/index.js.map +1 -1
  86. package/packages/pi-ai/dist/providers/api-family.d.ts +27 -0
  87. package/packages/pi-ai/dist/providers/api-family.d.ts.map +1 -0
  88. package/packages/pi-ai/dist/providers/api-family.js +47 -0
  89. package/packages/pi-ai/dist/providers/api-family.js.map +1 -0
  90. package/packages/pi-ai/dist/providers/api-family.test.d.ts +2 -0
  91. package/packages/pi-ai/dist/providers/api-family.test.d.ts.map +1 -0
  92. package/packages/pi-ai/dist/providers/api-family.test.js +101 -0
  93. package/packages/pi-ai/dist/providers/api-family.test.js.map +1 -0
  94. package/packages/pi-ai/src/index.ts +1 -0
  95. package/packages/pi-ai/src/providers/api-family.test.ts +129 -0
  96. package/packages/pi-ai/src/providers/api-family.ts +57 -0
  97. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  98. package/packages/pi-coding-agent/dist/cli/args.d.ts +6 -0
  99. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/cli/args.js +14 -4
  101. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/cli/args.test.d.ts +2 -0
  103. package/packages/pi-coding-agent/dist/cli/args.test.d.ts.map +1 -0
  104. package/packages/pi-coding-agent/dist/cli/args.test.js +38 -0
  105. package/packages/pi-coding-agent/dist/cli/args.test.js.map +1 -0
  106. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  107. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
  110. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/retry-handler.js +4 -1
  114. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -0
  116. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/sdk.js +7 -1
  118. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.js +3 -0
  121. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  122. package/packages/pi-coding-agent/src/cli/args.test.ts +44 -0
  123. package/packages/pi-coding-agent/src/cli/args.ts +21 -6
  124. package/packages/pi-coding-agent/src/core/extensions/runner.ts +4 -1
  125. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -2
  126. package/packages/pi-coding-agent/src/core/retry-handler.ts +4 -1
  127. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  128. package/packages/pi-coding-agent/src/main.ts +4 -0
  129. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  130. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -10
  131. package/src/resources/extensions/gsd/auto/phases.ts +3 -0
  132. package/src/resources/extensions/gsd/auto-dashboard.ts +25 -1
  133. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +15 -2
  134. package/src/resources/extensions/gsd/auto-dispatch.ts +21 -7
  135. package/src/resources/extensions/gsd/auto-model-selection.ts +3 -1
  136. package/src/resources/extensions/gsd/auto-prompts.ts +33 -9
  137. package/src/resources/extensions/gsd/auto-worktree.ts +16 -1
  138. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
  139. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  140. package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
  141. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -131
  142. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  143. package/src/resources/extensions/gsd/commands-memory.ts +551 -0
  144. package/src/resources/extensions/gsd/doctor-git-checks.ts +23 -2
  145. package/src/resources/extensions/gsd/gsd-db.ts +273 -4
  146. package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
  147. package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
  148. package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
  149. package/src/resources/extensions/gsd/memory-relations.ts +240 -0
  150. package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
  151. package/src/resources/extensions/gsd/memory-store.ts +351 -7
  152. package/src/resources/extensions/gsd/pre-execution-checks.ts +12 -8
  153. package/src/resources/extensions/gsd/prompts/add-tests.md +1 -0
  154. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  155. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  156. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +49 -0
  157. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  158. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  159. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
  160. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  161. package/src/resources/extensions/gsd/tests/integration/doctor-git-symlink-cwd.test.ts +79 -0
  162. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  163. package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
  164. package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
  165. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
  166. package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
  167. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  168. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
  169. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +66 -0
  170. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +132 -8
  171. package/src/resources/extensions/gsd/tests/prompts-no-gitignored-test-refs.test.ts +56 -0
  172. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +54 -0
  173. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +97 -0
  174. package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
  175. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +14 -0
  176. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  177. package/src/resources/extensions/search-the-web/command-search-provider.ts +4 -1
  178. package/src/resources/extensions/search-the-web/native-search.ts +13 -3
  179. package/src/resources/extensions/gsd/tests/onboarding-handler-loader.test.ts +0 -41
  180. /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_buildManifest.js +0 -0
  181. /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_ssgManifest.js +0 -0
@@ -0,0 +1,79 @@
1
+ import assert from "node:assert/strict";
2
+ import { execSync } from "node:child_process";
3
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { test } from "node:test";
7
+
8
+ import { runGSDDoctor } from "../../doctor.ts";
9
+
10
+ function run(cmd: string, cwd: string): string {
11
+ return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
12
+ }
13
+
14
+ function createRepoWithCompletedMilestone(): string {
15
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-git-symlink-cwd-")));
16
+ run("git init", dir);
17
+ run("git config user.email test@test.com", dir);
18
+ run("git config user.name Test", dir);
19
+
20
+ writeFileSync(join(dir, "README.md"), "# test\n");
21
+ run("git add .", dir);
22
+ run("git commit -m init", dir);
23
+ run("git branch -M main", dir);
24
+
25
+ const milestoneDir = join(dir, ".gsd", "milestones", "M001");
26
+ mkdirSync(milestoneDir, { recursive: true });
27
+ writeFileSync(join(milestoneDir, "ROADMAP.md"), `---
28
+ id: M001
29
+ title: "Test Milestone"
30
+ ---
31
+
32
+ # M001: Test Milestone
33
+
34
+ ## Vision
35
+ Test
36
+
37
+ ## Success Criteria
38
+ - Done
39
+
40
+ ## Slices
41
+ - [x] **S01: Test slice** \`risk:low\` \`depends:[]\`
42
+ > After this: done
43
+
44
+ ## Boundary Map
45
+ _None_
46
+ `);
47
+
48
+ run("git add -A", dir);
49
+ run("git commit -m \"add milestone\"", dir);
50
+
51
+ return dir;
52
+ }
53
+
54
+ test("doctor removes orphaned milestone worktree when cwd uses a symlink alias", { skip: process.platform === "win32" }, async (t) => {
55
+ const previousCwd = process.cwd();
56
+ const dir = createRepoWithCompletedMilestone();
57
+ const alias = join(tmpdir(), `doc-git-alias-${Date.now()}-${Math.random().toString(16).slice(2)}`);
58
+
59
+ t.after(() => {
60
+ try { process.chdir(previousCwd); } catch { process.chdir(tmpdir()); }
61
+ rmSync(alias, { recursive: true, force: true });
62
+ rmSync(dir, { recursive: true, force: true });
63
+ });
64
+
65
+ mkdirSync(join(dir, ".gsd", "worktrees"), { recursive: true });
66
+ run("git worktree add -b milestone/M001 .gsd/worktrees/M001", dir);
67
+
68
+ symlinkSync(dir, alias);
69
+ process.chdir(join(alias, ".gsd", "worktrees", "M001"));
70
+
71
+ const fixed = await runGSDDoctor(dir, { fix: true, isolationMode: "worktree" });
72
+ assert.ok(
73
+ fixed.fixesApplied.some(f => f.includes("removed orphaned worktree")),
74
+ `removes orphaned worktree even when cwd uses a symlink alias (got: ${JSON.stringify(fixed.fixesApplied)})`,
75
+ );
76
+
77
+ const wtList = run("git worktree list", dir);
78
+ assert.ok(!wtList.includes("milestone/M001"), "worktree removed after symlink-cwd fix");
79
+ });
@@ -363,7 +363,7 @@ test('md-importer: schema v1→v2 migration', () => {
363
363
  openDatabase(':memory:');
364
364
  const adapter = _getAdapter();
365
365
  const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
366
- assert.deepStrictEqual(version?.v, 17, 'new DB should be at schema version 17');
366
+ assert.deepStrictEqual(version?.v, 20, 'new DB should be at schema version 20');
367
367
 
368
368
  // Artifacts table should exist
369
369
  const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
@@ -0,0 +1,213 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { _getAdapter, closeDatabase, openDatabase } from '../gsd-db.ts';
5
+ import { createMemory, queryMemoriesRanked, supersedeMemory } from '../memory-store.ts';
6
+ import {
7
+ cosineSimilarity,
8
+ embedMemories,
9
+ getEmbeddingForMemory,
10
+ loadAllEmbeddings,
11
+ packFloat32,
12
+ removeEmbedding,
13
+ saveEmbedding,
14
+ unpackFloat32,
15
+ } from '../memory-embeddings.ts';
16
+
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ // Float32 packing + cosine math
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ test('memory-embeddings: packFloat32 / unpackFloat32 round-trip', () => {
22
+ const vec = new Float32Array([0.1, -0.5, 0.25, 0.75]);
23
+ const packed = packFloat32(vec);
24
+ const restored = unpackFloat32(packed);
25
+ assert.ok(restored);
26
+ assert.equal(restored!.length, vec.length);
27
+ for (let i = 0; i < vec.length; i++) {
28
+ assert.ok(Math.abs(restored![i] - vec[i]) < 1e-6);
29
+ }
30
+ });
31
+
32
+ test('memory-embeddings: cosineSimilarity identities', () => {
33
+ const a = new Float32Array([1, 0, 0]);
34
+ const b = new Float32Array([1, 0, 0]);
35
+ const c = new Float32Array([0, 1, 0]);
36
+ assert.ok(Math.abs(cosineSimilarity(a, b) - 1) < 1e-6);
37
+ assert.ok(Math.abs(cosineSimilarity(a, c)) < 1e-6);
38
+ assert.equal(cosineSimilarity(new Float32Array([]), new Float32Array([])), 0);
39
+ });
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+ // memory_embeddings table round-trip
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ test('memory-embeddings: saveEmbedding then getEmbeddingForMemory', () => {
46
+ openDatabase(':memory:');
47
+ const id = createMemory({ category: 'pattern', content: 'embedding persistence test' });
48
+ assert.ok(id);
49
+
50
+ const vec = new Float32Array([0.1, 0.2, 0.3]);
51
+ assert.ok(saveEmbedding(id!, vec, 'test-model'));
52
+
53
+ const row = getEmbeddingForMemory(id!);
54
+ assert.ok(row);
55
+ assert.equal(row!.model, 'test-model');
56
+ assert.equal(row!.dim, 3);
57
+ assert.equal(row!.vector.length, 3);
58
+
59
+ assert.ok(removeEmbedding(id!));
60
+ assert.equal(getEmbeddingForMemory(id!), null);
61
+
62
+ closeDatabase();
63
+ });
64
+
65
+ test('memory-embeddings: loadAllEmbeddings skips superseded memories', () => {
66
+ openDatabase(':memory:');
67
+ createMemory({ category: 'pattern', content: 'mem A' });
68
+ createMemory({ category: 'pattern', content: 'mem B' });
69
+ saveEmbedding('MEM001', new Float32Array([1, 0, 0]), 'm');
70
+ saveEmbedding('MEM002', new Float32Array([0, 1, 0]), 'm');
71
+
72
+ const all = loadAllEmbeddings();
73
+ assert.equal(all.length, 2);
74
+
75
+ // supersede MEM001
76
+ supersedeMemory('MEM001', 'MEM002');
77
+
78
+ const remaining = loadAllEmbeddings();
79
+ assert.equal(remaining.length, 1);
80
+ assert.equal(remaining[0].memoryId, 'MEM002');
81
+
82
+ closeDatabase();
83
+ });
84
+
85
+ test('memory-embeddings: embedMemories returns 0 when embedFn is null', async () => {
86
+ const n = await embedMemories([{ id: 'MEM001', content: 'x' }], null, 'none');
87
+ assert.equal(n, 0);
88
+ });
89
+
90
+ test('memory-embeddings: embedMemories persists fetched vectors', async () => {
91
+ openDatabase(':memory:');
92
+ createMemory({ category: 'pattern', content: 'first' });
93
+ createMemory({ category: 'pattern', content: 'second' });
94
+
95
+ const fn = async (texts: string[]): Promise<Float32Array[]> =>
96
+ texts.map((_, i) => new Float32Array([i, i + 1, i + 2]));
97
+
98
+ const count = await embedMemories(
99
+ [
100
+ { id: 'MEM001', content: 'first' },
101
+ { id: 'MEM002', content: 'second' },
102
+ ],
103
+ fn,
104
+ 'mock-embedder',
105
+ );
106
+ assert.equal(count, 2);
107
+
108
+ const first = getEmbeddingForMemory('MEM001');
109
+ assert.ok(first);
110
+ assert.equal(first!.model, 'mock-embedder');
111
+ assert.equal(first!.vector.length, 3);
112
+
113
+ closeDatabase();
114
+ });
115
+
116
+ // ═══════════════════════════════════════════════════════════════════════════
117
+ // queryMemoriesRanked + FTS5 keyword path
118
+ // ═══════════════════════════════════════════════════════════════════════════
119
+
120
+ test('memory-store: queryMemoriesRanked returns keyword hits', () => {
121
+ openDatabase(':memory:');
122
+ createMemory({ category: 'gotcha', content: 'sql.js FTS5 virtual tables need triggers' });
123
+ createMemory({ category: 'pattern', content: 'prefer prepared statements for hot paths' });
124
+ createMemory({ category: 'convention', content: 'atomic writes use tmp + rename' });
125
+
126
+ const ranked = queryMemoriesRanked({ query: 'sql triggers', k: 5 });
127
+ assert.ok(ranked.length >= 1);
128
+ assert.equal(ranked[0].memory.id, 'MEM001');
129
+ assert.ok(ranked[0].keywordRank !== null);
130
+ assert.equal(ranked[0].reason, 'keyword');
131
+
132
+ closeDatabase();
133
+ });
134
+
135
+ test('memory-store: queryMemoriesRanked respects scope + tag filters', () => {
136
+ openDatabase(':memory:');
137
+ createMemory({ category: 'gotcha', content: 'scoped fact one', scope: 'project', tags: ['a'] });
138
+ createMemory({ category: 'gotcha', content: 'scoped fact two', scope: 'global', tags: ['a', 'b'] });
139
+
140
+ const projectOnly = queryMemoriesRanked({ query: 'scoped fact', scope: 'project' });
141
+ assert.equal(projectOnly.length, 1);
142
+ assert.equal(projectOnly[0].memory.scope, 'project');
143
+
144
+ const tagged = queryMemoriesRanked({ query: 'scoped fact', tag: 'b' });
145
+ assert.equal(tagged.length, 1);
146
+ assert.equal(tagged[0].memory.id, 'MEM002');
147
+
148
+ closeDatabase();
149
+ });
150
+
151
+ test('memory-store: queryMemoriesRanked fuses keyword + semantic hits', () => {
152
+ openDatabase(':memory:');
153
+ createMemory({ category: 'pattern', content: 'foo bar baz' });
154
+ createMemory({ category: 'pattern', content: 'completely unrelated' });
155
+
156
+ // Embed memory 2 so semantic scoring picks it up even though the keyword
157
+ // "foo" does not appear in its content.
158
+ saveEmbedding('MEM001', new Float32Array([1, 0, 0, 0]), 'mock');
159
+ saveEmbedding('MEM002', new Float32Array([0.9, 0.1, 0, 0]), 'mock');
160
+
161
+ const queryVec = new Float32Array([0.95, 0.05, 0, 0]);
162
+ const ranked = queryMemoriesRanked({ query: 'foo', queryVector: queryVec, k: 5 });
163
+ const reasons = ranked.map((r) => r.reason);
164
+ assert.ok(reasons.includes('both') || reasons.includes('keyword'));
165
+ const ids = ranked.map((r) => r.memory.id);
166
+ assert.ok(ids.includes('MEM002'), 'semantic-only hit should surface even without keyword match');
167
+
168
+ closeDatabase();
169
+ });
170
+
171
+ test('memory-store: queryMemoriesRanked falls back to ranked listing when query is empty', () => {
172
+ openDatabase(':memory:');
173
+ createMemory({ category: 'gotcha', content: 'first', confidence: 0.9 });
174
+ createMemory({ category: 'pattern', content: 'second', confidence: 0.5 });
175
+
176
+ const ranked = queryMemoriesRanked({ query: ' ' });
177
+ assert.equal(ranked.length, 2);
178
+ assert.equal(ranked[0].memory.id, 'MEM001');
179
+ assert.equal(ranked[0].reason, 'ranked');
180
+
181
+ closeDatabase();
182
+ });
183
+
184
+ test('memory-store: queryMemoriesRanked excludes superseded by default', () => {
185
+ openDatabase(':memory:');
186
+ createMemory({ category: 'gotcha', content: 'stale insight' });
187
+ createMemory({ category: 'gotcha', content: 'fresh insight' });
188
+
189
+ supersedeMemory('MEM001', 'MEM002');
190
+
191
+ const active = queryMemoriesRanked({ query: 'insight' });
192
+ assert.ok(active.every((r) => r.memory.superseded_by == null));
193
+ });
194
+
195
+ // ═══════════════════════════════════════════════════════════════════════════
196
+ // FTS5 triggers keep memories_fts in sync with memories
197
+ // ═══════════════════════════════════════════════════════════════════════════
198
+
199
+ test('memory-store schema: memories_fts stays in sync with memories', () => {
200
+ openDatabase(':memory:');
201
+ createMemory({ category: 'pattern', content: 'alpha beta gamma' });
202
+ const adapter = _getAdapter();
203
+ assert.ok(adapter);
204
+ const fts = adapter!.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
205
+ if (!fts) {
206
+ // Build without FTS5 — skip.
207
+ closeDatabase();
208
+ return;
209
+ }
210
+ const rows = adapter!.prepare(`SELECT content FROM memories_fts WHERE memories_fts MATCH 'beta'`).all();
211
+ assert.ok(rows.some((row) => (row['content'] as string).includes('alpha beta gamma')));
212
+ closeDatabase();
213
+ });
@@ -0,0 +1,153 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { _getAdapter, closeDatabase, openDatabase } from '../gsd-db.ts';
8
+ import {
9
+ createMemorySource,
10
+ deleteMemorySource,
11
+ getMemorySource,
12
+ hashContent,
13
+ listMemorySources,
14
+ } from '../memory-source-store.ts';
15
+ import { ingestFile, ingestNote, summarizeIngest } from '../memory-ingest.ts';
16
+
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ // memory-source-store
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ test('memory-sources: content_hash is stable', () => {
22
+ const a = hashContent('hello');
23
+ const b = hashContent('hello');
24
+ assert.equal(a, b);
25
+ assert.notEqual(a, hashContent('world'));
26
+ });
27
+
28
+ test('memory-sources: createMemorySource is idempotent on content_hash', () => {
29
+ openDatabase(':memory:');
30
+
31
+ const first = createMemorySource({
32
+ kind: 'note',
33
+ content: 'sql.js FTS5 triggers must be kept in sync with the base table',
34
+ tags: ['db', 'fts'],
35
+ });
36
+ assert.ok(first);
37
+ assert.equal(first!.duplicate, false);
38
+
39
+ const second = createMemorySource({
40
+ kind: 'note',
41
+ content: 'sql.js FTS5 triggers must be kept in sync with the base table',
42
+ tags: ['db', 'fts'],
43
+ });
44
+ assert.ok(second);
45
+ assert.equal(second!.duplicate, true);
46
+ assert.equal(second!.id, first!.id);
47
+
48
+ const sources = listMemorySources();
49
+ assert.equal(sources.length, 1);
50
+ assert.equal(sources[0].kind, 'note');
51
+ assert.deepEqual(sources[0].tags, ['db', 'fts']);
52
+
53
+ closeDatabase();
54
+ });
55
+
56
+ test('memory-sources: getMemorySource and deleteMemorySource round-trip', () => {
57
+ openDatabase(':memory:');
58
+
59
+ const created = createMemorySource({ kind: 'note', content: 'note to delete' });
60
+ assert.ok(created);
61
+ const fetched = getMemorySource(created!.id);
62
+ assert.ok(fetched);
63
+ assert.equal(fetched!.content, 'note to delete');
64
+
65
+ const removed = deleteMemorySource(created!.id);
66
+ assert.ok(removed);
67
+ assert.equal(getMemorySource(created!.id), null);
68
+
69
+ closeDatabase();
70
+ });
71
+
72
+ // ═══════════════════════════════════════════════════════════════════════════
73
+ // memory-ingest
74
+ // ═══════════════════════════════════════════════════════════════════════════
75
+
76
+ test('memory-ingest: ingestNote persists a note source', async () => {
77
+ openDatabase(':memory:');
78
+
79
+ const result = await ingestNote('The CAPTURES.md pipeline runs on triage, not dispatch.', null, {
80
+ tags: ['captures'],
81
+ scope: 'project',
82
+ extract: false,
83
+ });
84
+
85
+ assert.ok(result.sourceId.startsWith('SRC-'));
86
+ assert.equal(result.duplicate, false);
87
+ assert.equal(result.kind, 'note');
88
+ assert.equal(result.extracted.length, 0);
89
+ assert.match(summarizeIngest(result), /no memories extracted/);
90
+
91
+ closeDatabase();
92
+ });
93
+
94
+ test('memory-ingest: ingestNote rejects empty input', async () => {
95
+ openDatabase(':memory:');
96
+
97
+ const result = await ingestNote(' ', null, { extract: false });
98
+ assert.equal(result.sourceId, '');
99
+
100
+ closeDatabase();
101
+ });
102
+
103
+ test('memory-ingest: ingestNote marks duplicates on replay', async () => {
104
+ openDatabase(':memory:');
105
+
106
+ const first = await ingestNote('same note body', null, { extract: false });
107
+ const second = await ingestNote('same note body', null, { extract: false });
108
+ assert.equal(first.sourceId, second.sourceId);
109
+ assert.equal(second.duplicate, true);
110
+
111
+ closeDatabase();
112
+ });
113
+
114
+ test('memory-ingest: ingestFile persists a file source', async () => {
115
+ openDatabase(':memory:');
116
+
117
+ const dir = mkdtempSync(join(tmpdir(), 'gsd-memory-ingest-'));
118
+ const file = join(dir, 'sample.md');
119
+ writeFileSync(file, '# Architecture\n\nWe use SQLite with WAL journaling.', 'utf-8');
120
+
121
+ try {
122
+ const result = await ingestFile(file, null, { tags: ['arch'], extract: false });
123
+ assert.ok(result.sourceId.startsWith('SRC-'));
124
+ assert.equal(result.kind, 'file');
125
+ assert.equal(result.title, 'sample.md');
126
+ assert.equal(result.uri, file);
127
+ } finally {
128
+ rmSync(dir, { recursive: true, force: true });
129
+ closeDatabase();
130
+ }
131
+ });
132
+
133
+ test('memory-ingest: ingestFile rejects missing paths', async () => {
134
+ openDatabase(':memory:');
135
+
136
+ await assert.rejects(() => ingestFile('/no/such/file/exists.md', null, { extract: false }), /File not found/);
137
+
138
+ closeDatabase();
139
+ });
140
+
141
+ // ═══════════════════════════════════════════════════════════════════════════
142
+ // schema migration + scope/tags columns on memories
143
+ // ═══════════════════════════════════════════════════════════════════════════
144
+
145
+ test('memory-store schema: scope and tags columns exist on memories', () => {
146
+ openDatabase(':memory:');
147
+ const adapter = _getAdapter();
148
+ const info = adapter!.prepare('PRAGMA table_info(memories)').all();
149
+ const names = info.map((row: Record<string, unknown>) => row.name);
150
+ assert.ok(names.includes('scope'));
151
+ assert.ok(names.includes('tags'));
152
+ closeDatabase();
153
+ });
@@ -0,0 +1,107 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { _getAdapter, closeDatabase, openDatabase } from '../gsd-db.ts';
5
+ import {
6
+ createMemory,
7
+ decayStaleMemories,
8
+ enforceMemoryCap,
9
+ markUnitProcessed,
10
+ } from '../memory-store.ts';
11
+ import { createMemoryRelation, listRelationsFor } from '../memory-relations.ts';
12
+ import { saveEmbedding, getEmbeddingForMemory } from '../memory-embeddings.ts';
13
+
14
+ // ═══════════════════════════════════════════════════════════════════════════
15
+ // enforceMemoryCap — cascade cleanup of embeddings and relations
16
+ // ═══════════════════════════════════════════════════════════════════════════
17
+
18
+ test('memory-cap: supersedes lowest-ranked memories and cascades cleanup', () => {
19
+ openDatabase(':memory:');
20
+
21
+ // Create 6 memories with descending confidence so the first is lowest.
22
+ createMemory({ category: 'pattern', content: 'weakest', confidence: 0.2 });
23
+ createMemory({ category: 'pattern', content: 'mid-1', confidence: 0.5 });
24
+ createMemory({ category: 'pattern', content: 'mid-2', confidence: 0.6 });
25
+ createMemory({ category: 'pattern', content: 'strong-1', confidence: 0.9 });
26
+ createMemory({ category: 'pattern', content: 'strong-2', confidence: 0.92 });
27
+ createMemory({ category: 'pattern', content: 'strongest', confidence: 0.95 });
28
+
29
+ saveEmbedding('MEM001', new Float32Array([1, 0, 0]), 'm');
30
+ saveEmbedding('MEM006', new Float32Array([0, 1, 0]), 'm');
31
+ createMemoryRelation('MEM001', 'MEM002', 'related_to');
32
+
33
+ // Cap at 5 — MEM001 should be superseded and its embedding + relations purged.
34
+ enforceMemoryCap(5);
35
+
36
+ const adapter = _getAdapter();
37
+ const row = adapter!
38
+ .prepare("SELECT superseded_by FROM memories WHERE id = 'MEM001'")
39
+ .get();
40
+ assert.equal(row?.['superseded_by'], 'CAP_EXCEEDED');
41
+ assert.equal(getEmbeddingForMemory('MEM001'), null);
42
+ assert.equal(listRelationsFor('MEM001').length, 0);
43
+
44
+ // MEM006 was not victimised — its embedding should still exist.
45
+ assert.ok(getEmbeddingForMemory('MEM006'));
46
+
47
+ closeDatabase();
48
+ });
49
+
50
+ test('memory-cap: is a no-op when count is already under the cap', () => {
51
+ openDatabase(':memory:');
52
+ createMemory({ category: 'pattern', content: 'one' });
53
+ createMemory({ category: 'pattern', content: 'two' });
54
+
55
+ enforceMemoryCap(10);
56
+
57
+ const adapter = _getAdapter();
58
+ const count = adapter!
59
+ .prepare("SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL")
60
+ .get();
61
+ assert.equal(count?.['cnt'], 2);
62
+
63
+ closeDatabase();
64
+ });
65
+
66
+ // ═══════════════════════════════════════════════════════════════════════════
67
+ // decayStaleMemories — returns list of decayed IDs
68
+ // ═══════════════════════════════════════════════════════════════════════════
69
+
70
+ test('memory-decay: returns decayed memory IDs', () => {
71
+ openDatabase(':memory:');
72
+
73
+ // Insert processed units — decayStaleMemories needs at least N rows.
74
+ const now = Date.now();
75
+ for (let i = 0; i < 21; i++) {
76
+ markUnitProcessed(`unit/${i}`, `file-${i}`);
77
+ // small spacing to create deterministic ordering
78
+ const row = _getAdapter()!
79
+ .prepare('UPDATE memory_processed_units SET processed_at = :ts WHERE unit_key = :key');
80
+ row.run({ ':ts': new Date(now + i * 1000).toISOString(), ':key': `unit/${i}` });
81
+ }
82
+
83
+ // Create memory with updated_at in the distant past
84
+ createMemory({ category: 'pattern', content: 'stale entry', confidence: 0.9 });
85
+ _getAdapter()!
86
+ .prepare("UPDATE memories SET updated_at = '2000-01-01T00:00:00Z' WHERE id = 'MEM001'")
87
+ .run({});
88
+
89
+ const decayed = decayStaleMemories(20);
90
+ assert.ok(decayed.includes('MEM001'));
91
+
92
+ // Confidence should have dropped.
93
+ const row = _getAdapter()!
94
+ .prepare("SELECT confidence FROM memories WHERE id = 'MEM001'")
95
+ .get();
96
+ assert.ok((row?.['confidence'] as number) < 0.9);
97
+
98
+ closeDatabase();
99
+ });
100
+
101
+ test('memory-decay: returns empty when there are fewer processed units than the threshold', () => {
102
+ openDatabase(':memory:');
103
+ createMemory({ category: 'pattern', content: 'fresh' });
104
+ const decayed = decayStaleMemories(20);
105
+ assert.deepEqual(decayed, []);
106
+ closeDatabase();
107
+ });