@pentatonic-ai/ai-agent-sdk 0.5.11 → 0.7.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 (119) hide show
  1. package/README.md +345 -174
  2. package/bin/__tests__/callback-server.test.js +70 -0
  3. package/bin/__tests__/credentials.test.js +58 -0
  4. package/bin/__tests__/login.test.js +210 -0
  5. package/bin/__tests__/pkce.test.js +39 -0
  6. package/bin/__tests__/whoami.test.js +77 -0
  7. package/bin/cli.js +109 -440
  8. package/bin/commands/config.js +251 -0
  9. package/bin/commands/login.js +219 -0
  10. package/bin/commands/whoami.js +41 -0
  11. package/bin/lib/callback-server.js +137 -0
  12. package/bin/lib/credentials.js +100 -0
  13. package/bin/lib/pkce.js +26 -0
  14. package/package.json +4 -2
  15. package/packages/doctor/__tests__/detect.test.js +2 -6
  16. package/packages/doctor/src/checks/local-memory.js +164 -196
  17. package/packages/doctor/src/detect.js +11 -3
  18. package/packages/memory/src/__tests__/corpus-chunkers.test.js +143 -0
  19. package/packages/memory/src/__tests__/corpus-discover.test.js +175 -0
  20. package/packages/memory/src/__tests__/corpus-ingest.test.js +236 -0
  21. package/packages/memory/src/__tests__/corpus-signatures.test.js +175 -0
  22. package/packages/memory/src/__tests__/corpus-state.test.js +161 -0
  23. package/packages/memory/src/__tests__/ingest-corpus-opts.test.js +129 -0
  24. package/packages/memory/src/__tests__/search-kind.test.js +108 -0
  25. package/packages/memory/src/corpus/adapters.js +398 -0
  26. package/packages/memory/src/corpus/chunkers.js +328 -0
  27. package/packages/memory/src/corpus/cli.js +613 -0
  28. package/packages/memory/src/corpus/discover.js +379 -0
  29. package/packages/memory/src/corpus/index.js +68 -0
  30. package/packages/memory/src/corpus/ingest.js +356 -0
  31. package/packages/memory/src/corpus/signatures.js +280 -0
  32. package/packages/memory/src/corpus/state.js +134 -0
  33. package/packages/memory/src/index.js +18 -0
  34. package/packages/memory/src/ingest.js +20 -11
  35. package/packages/memory/src/openclaw/index.js +39 -1
  36. package/packages/memory/src/search.js +30 -7
  37. package/packages/memory-engine/.env.example +13 -0
  38. package/packages/memory-engine/README.md +131 -0
  39. package/packages/memory-engine/bench/README.md +99 -0
  40. package/packages/memory-engine/bench/scorecards-engine/agent-coding__pentatonic-baseline__20260427-142523.json +1115 -0
  41. package/packages/memory-engine/bench/scorecards-engine/chat-recall__pentatonic-baseline__20260427-142648.json +819 -0
  42. package/packages/memory-engine/bench/scorecards-engine/circular-economy__pentatonic-baseline__20260427-142757.json +1278 -0
  43. package/packages/memory-engine/bench/scorecards-engine/customer-support__pentatonic-baseline__20260427-142900.json +1018 -0
  44. package/packages/memory-engine/bench/scorecards-engine/marketplace-ops__pentatonic-baseline__20260427-142957.json +1038 -0
  45. package/packages/memory-engine/bench/scorecards-engine/product-catalogue__pentatonic-baseline__20260427-143122.json +961 -0
  46. package/packages/memory-engine/bench/scorecards-engine-via-docker/agent-coding__pentatonic-memory__20260427-161812.json +1115 -0
  47. package/packages/memory-engine/bench/scorecards-engine-via-docker/chat-recall__pentatonic-memory__20260427-161701.json +819 -0
  48. package/packages/memory-engine/bench/scorecards-engine-via-docker/circular-economy__pentatonic-memory__20260427-161713.json +1278 -0
  49. package/packages/memory-engine/bench/scorecards-engine-via-docker/customer-support__pentatonic-memory__20260427-161723.json +1018 -0
  50. package/packages/memory-engine/bench/scorecards-engine-via-docker/marketplace-ops__pentatonic-memory__20260427-161732.json +1038 -0
  51. package/packages/memory-engine/bench/scorecards-engine-via-docker/product-catalogue__pentatonic-memory__20260427-161741.json +937 -0
  52. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/agent-coding__pentatonic-memory__20260427-184718.json +1115 -0
  53. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/chat-recall__pentatonic-memory__20260427-184614.json +819 -0
  54. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/circular-economy__pentatonic-memory__20260427-184809.json +1278 -0
  55. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/customer-support__pentatonic-memory__20260427-184854.json +1018 -0
  56. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/marketplace-ops__pentatonic-memory__20260427-184929.json +1038 -0
  57. package/packages/memory-engine/bench/scorecards-engine-via-l2-7-layer-populated/product-catalogue__pentatonic-memory__20260427-185015.json +961 -0
  58. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/agent-coding__pentatonic-memory__20260427-175252.json +1115 -0
  59. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/chat-recall__pentatonic-memory__20260427-175312.json +819 -0
  60. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/circular-economy__pentatonic-memory__20260427-175335.json +1278 -0
  61. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/customer-support__pentatonic-memory__20260427-175355.json +1018 -0
  62. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/marketplace-ops__pentatonic-memory__20260427-175413.json +1038 -0
  63. package/packages/memory-engine/bench/scorecards-engine-via-l2-empty-layers/product-catalogue__pentatonic-memory__20260427-175430.json +883 -0
  64. package/packages/memory-engine/bench/scorecards-engine-via-shim/agent-coding__pentatonic-memory__20260427-155409.json +1115 -0
  65. package/packages/memory-engine/bench/scorecards-engine-via-shim/chat-recall__pentatonic-memory__20260427-155421.json +819 -0
  66. package/packages/memory-engine/bench/scorecards-engine-via-shim/circular-economy__pentatonic-memory__20260427-155433.json +1278 -0
  67. package/packages/memory-engine/bench/scorecards-engine-via-shim/customer-support__pentatonic-memory__20260427-155443.json +1018 -0
  68. package/packages/memory-engine/bench/scorecards-engine-via-shim/marketplace-ops__pentatonic-memory__20260427-155453.json +1038 -0
  69. package/packages/memory-engine/bench/scorecards-engine-via-shim/product-catalogue__pentatonic-memory__20260427-155503.json +937 -0
  70. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory-latest__20260427-145103.json +1115 -0
  71. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/agent-coding__pentatonic-memory__20260427-144909.json +1115 -0
  72. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory-latest__20260427-145153.json +819 -0
  73. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/chat-recall__pentatonic-memory__20260427-145120.json +542 -0
  74. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory-latest__20260427-145313.json +1278 -0
  75. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/circular-economy__pentatonic-memory__20260427-145207.json +894 -0
  76. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory-latest__20260427-145412.json +1018 -0
  77. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/customer-support__pentatonic-memory__20260427-145327.json +680 -0
  78. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory-latest__20260427-145517.json +1038 -0
  79. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/marketplace-ops__pentatonic-memory__20260427-145422.json +693 -0
  80. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory-latest__20260427-145616.json +961 -0
  81. package/packages/memory-engine/bench/scorecards-pentatonic-baseline/product-catalogue__pentatonic-memory__20260427-145528.json +727 -0
  82. package/packages/memory-engine/compat/Dockerfile +11 -0
  83. package/packages/memory-engine/compat/server.py +680 -0
  84. package/packages/memory-engine/docker-compose.yml +243 -0
  85. package/packages/memory-engine/docs/MIGRATION.md +178 -0
  86. package/packages/memory-engine/docs/RUNBOOK-AWS.md +375 -0
  87. package/packages/memory-engine/docs/why-v05-underperforms.md +138 -0
  88. package/packages/memory-engine/engine/README.md +52 -0
  89. package/packages/memory-engine/engine/l2-hybridrag-proxy.py +1543 -0
  90. package/packages/memory-engine/engine/l5-comms-layer.py +663 -0
  91. package/packages/memory-engine/engine/l6-document-store.py +1018 -0
  92. package/packages/memory-engine/engine/services/l2/Dockerfile +41 -0
  93. package/packages/memory-engine/engine/services/l2/init_databases.py +81 -0
  94. package/packages/memory-engine/engine/services/l2/l2-hybridrag-proxy.py +1543 -0
  95. package/packages/memory-engine/engine/services/l4/Dockerfile +15 -0
  96. package/packages/memory-engine/engine/services/l4/server.py +235 -0
  97. package/packages/memory-engine/engine/services/l5/Dockerfile +9 -0
  98. package/packages/memory-engine/engine/services/l5/l5-comms-layer.py +678 -0
  99. package/packages/memory-engine/engine/services/l6/Dockerfile +11 -0
  100. package/packages/memory-engine/engine/services/l6/l6-document-store.py +1016 -0
  101. package/packages/memory-engine/engine/services/nv-embed/Dockerfile +28 -0
  102. package/packages/memory-engine/engine/services/nv-embed/server.py +152 -0
  103. package/packages/memory-engine/pme_memory/__init__.py +0 -0
  104. package/packages/memory-engine/pme_memory/__main__.py +129 -0
  105. package/packages/memory-engine/pme_memory/artifacts.py +95 -0
  106. package/packages/memory-engine/pme_memory/embed.py +74 -0
  107. package/packages/memory-engine/pme_memory/health.py +36 -0
  108. package/packages/memory-engine/pme_memory/hygiene.py +159 -0
  109. package/packages/memory-engine/pme_memory/indexer.py +200 -0
  110. package/packages/memory-engine/pme_memory/needs.py +55 -0
  111. package/packages/memory-engine/pme_memory/provenance.py +80 -0
  112. package/packages/memory-engine/pme_memory/scoring.py +168 -0
  113. package/packages/memory-engine/pme_memory/search.py +52 -0
  114. package/packages/memory-engine/pme_memory/store.py +86 -0
  115. package/packages/memory-engine/pme_memory/synthesis.py +114 -0
  116. package/packages/memory-engine/pyproject.toml +65 -0
  117. package/packages/memory-engine/scripts/kg-extractor.py +557 -0
  118. package/packages/memory-engine/scripts/kg-preflexor-v2.py +738 -0
  119. package/packages/memory-engine/tests/test_api_contract.sh +57 -0
@@ -0,0 +1,175 @@
1
+ import { extractReferences } from "../corpus/signatures.js";
2
+
3
+ function file(relPath, content, ext) {
4
+ return {
5
+ relPath,
6
+ content,
7
+ ext: ext ?? "." + relPath.split(".").pop(),
8
+ };
9
+ }
10
+
11
+ describe("corpus/signatures.js — reference extraction", () => {
12
+ describe("metadata shape (universal)", () => {
13
+ it("every reference carries kind=code_reference + path + lines + language", () => {
14
+ const refs = extractReferences(
15
+ file("src/x.ts", "export function foo() { return 1; }")
16
+ );
17
+ expect(refs.length).toBeGreaterThan(0);
18
+ for (const r of refs) {
19
+ expect(r.metadata.kind).toBe("code_reference");
20
+ expect(r.metadata.path).toBe("src/x.ts");
21
+ expect(typeof r.metadata.start_line).toBe("number");
22
+ expect(typeof r.metadata.end_line).toBe("number");
23
+ expect(r.metadata.language).toBe("typescript");
24
+ expect(r.metadata.lines).toBe(
25
+ `${r.metadata.start_line}-${r.metadata.end_line}`
26
+ );
27
+ }
28
+ });
29
+ });
30
+
31
+ describe("markdown", () => {
32
+ it("emits one reference per H1/H2 section", () => {
33
+ const md = [
34
+ "# Top",
35
+ "intro paragraph one.",
36
+ "",
37
+ "## Auth",
38
+ "JWT verification middleware lives here.",
39
+ "",
40
+ "## Storage",
41
+ "Postgres + R2.",
42
+ ].join("\n");
43
+ const refs = extractReferences(file("docs/arch.md", md));
44
+ expect(refs.length).toBe(3);
45
+ expect(refs[0].metadata.symbol).toBe("Top");
46
+ expect(refs[1].metadata.symbol).toBe("Auth");
47
+ expect(refs[2].metadata.symbol).toBe("Storage");
48
+ expect(refs[1].content).toMatch(/JWT verification/);
49
+ });
50
+
51
+ it("falls back to a file-level reference when there are no headings", () => {
52
+ const refs = extractReferences(
53
+ file("README.txt", "no headings here, just text", ".md")
54
+ );
55
+ expect(refs.length).toBe(1);
56
+ expect(refs[0].metadata.start_line).toBe(1);
57
+ expect(refs[0].content).toMatch(/README\.txt/);
58
+ });
59
+ });
60
+
61
+ describe("javascript / typescript", () => {
62
+ it("extracts top-level function declarations", () => {
63
+ const code = [
64
+ "import x from 'y';",
65
+ "",
66
+ "export function authenticate(req) {",
67
+ " return verify(req);",
68
+ "}",
69
+ "",
70
+ "function helper() { return 1; }",
71
+ ].join("\n");
72
+ const refs = extractReferences(file("src/auth.js", code));
73
+ const symbols = refs.map((r) => r.metadata.symbol);
74
+ expect(symbols).toContain("authenticate");
75
+ expect(symbols).toContain("helper");
76
+ });
77
+
78
+ it("extracts class declarations", () => {
79
+ const code = "export class UserService {\n ping() {}\n}";
80
+ const refs = extractReferences(file("src/svc.ts", code));
81
+ expect(refs.find((r) => r.metadata.symbol === "UserService")).toBeDefined();
82
+ });
83
+
84
+ it("extracts top-level const/let/var bindings", () => {
85
+ const code = "export const handler = async (req) => req;";
86
+ const refs = extractReferences(file("src/h.js", code));
87
+ expect(refs[0].metadata.symbol).toBe("handler");
88
+ });
89
+
90
+ it("ignores indented (non-top-level) declarations", () => {
91
+ const code = [
92
+ "function outer() {",
93
+ " function inner() { return 1; }",
94
+ "}",
95
+ ].join("\n");
96
+ const refs = extractReferences(file("src/x.js", code));
97
+ const symbols = refs.map((r) => r.metadata.symbol);
98
+ expect(symbols).toContain("outer");
99
+ expect(symbols).not.toContain("inner");
100
+ });
101
+
102
+ it("falls back to file-level reference when nothing matches", () => {
103
+ const code = "// just a comment\nconsole.log('side effect');";
104
+ const refs = extractReferences(file("src/y.js", code));
105
+ expect(refs.length).toBe(1);
106
+ expect(refs[0].metadata.start_line).toBe(1);
107
+ expect(refs[0].content).toMatch(/src\/y\.js/);
108
+ });
109
+
110
+ it("captures path:line + symbol in the embedded content", () => {
111
+ const code = "\n\nexport function foo() {}\n";
112
+ const refs = extractReferences(file("a/b.ts", code));
113
+ expect(refs[0].content).toMatch(/a\/b\.ts:3 — foo/);
114
+ });
115
+ });
116
+
117
+ describe("python", () => {
118
+ it("extracts top-level def and class declarations", () => {
119
+ const py = [
120
+ "import os",
121
+ "",
122
+ "def authenticate(req):",
123
+ " return True",
124
+ "",
125
+ "class UserService:",
126
+ " def ping(self): pass",
127
+ ].join("\n");
128
+ const refs = extractReferences(file("svc/auth.py", py));
129
+ const symbols = refs.map((r) => r.metadata.symbol);
130
+ expect(symbols).toContain("authenticate");
131
+ expect(symbols).toContain("UserService");
132
+ // Methods of the class should NOT be separately extracted
133
+ expect(symbols).not.toContain("ping");
134
+ });
135
+ });
136
+
137
+ describe("json / yaml configs", () => {
138
+ it("collapses to a single reference of top-level keys (json)", () => {
139
+ const json = JSON.stringify(
140
+ { name: "x", version: "1.0", dependencies: {} },
141
+ null,
142
+ 2
143
+ );
144
+ const refs = extractReferences(file("package.json", json));
145
+ expect(refs.length).toBe(1);
146
+ expect(refs[0].content).toMatch(/top-level keys/);
147
+ expect(refs[0].content).toMatch(/name/);
148
+ expect(refs[0].content).toMatch(/version/);
149
+ expect(refs[0].content).toMatch(/dependencies/);
150
+ });
151
+
152
+ it("collapses to a single reference of top-level keys (yaml)", () => {
153
+ const yaml = "name: x\nversion: 1.0\nbuild:\n steps: []\n";
154
+ const refs = extractReferences(file("ci.yml", yaml));
155
+ expect(refs.length).toBe(1);
156
+ expect(refs[0].content).toMatch(/name/);
157
+ expect(refs[0].content).toMatch(/version/);
158
+ expect(refs[0].content).toMatch(/build/);
159
+ // Indented `steps:` should not appear as a top-level key
160
+ expect(refs[0].content).not.toMatch(/\bsteps\b/);
161
+ });
162
+ });
163
+
164
+ describe("unknown languages", () => {
165
+ it("emits a single file-level reference with the head of the content", () => {
166
+ const refs = extractReferences(
167
+ file("data.txt", "some random text\nover multiple lines", ".txt")
168
+ );
169
+ expect(refs.length).toBe(1);
170
+ expect(refs[0].metadata.language).toBe("text");
171
+ expect(refs[0].content).toMatch(/data\.txt/);
172
+ expect(refs[0].content).toMatch(/some random text/);
173
+ });
174
+ });
175
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Tests for corpus state persistence — atomic writes, version handling,
3
+ * and the upsert/remove API used by ingest.js.
4
+ */
5
+
6
+ import { promises as fsp, existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import { mkdtemp } from "node:fs/promises";
10
+
11
+ import {
12
+ loadState,
13
+ saveState,
14
+ upsertSource,
15
+ removeSource,
16
+ getSource,
17
+ recordFile,
18
+ forgetFile,
19
+ recomputeStats,
20
+ emptyState,
21
+ STATE_VERSION,
22
+ } from "../corpus/state.js";
23
+
24
+ let tmp;
25
+ let statePath;
26
+
27
+ beforeEach(async () => {
28
+ tmp = await mkdtemp(join(tmpdir(), "tes-state-"));
29
+ statePath = join(tmp, "corpus.json");
30
+ });
31
+
32
+ afterEach(async () => {
33
+ await fsp.rm(tmp, { recursive: true, force: true });
34
+ });
35
+
36
+ describe("loadState", () => {
37
+ it("returns empty state when file does not exist", async () => {
38
+ const state = await loadState(statePath);
39
+ expect(state.version).toBe(STATE_VERSION);
40
+ expect(state.sources).toEqual({});
41
+ });
42
+
43
+ it("loads previously-saved state", async () => {
44
+ const initial = emptyState();
45
+ upsertSource(initial, "/repo/x", { sourceType: "git" });
46
+ await saveState(initial, statePath);
47
+
48
+ const loaded = await loadState(statePath);
49
+ expect(loaded.sources["/repo/x"]).toBeDefined();
50
+ expect(loaded.sources["/repo/x"].sourceType).toBe("git");
51
+ });
52
+
53
+ it("rejects state with newer version than supported", async () => {
54
+ await fsp.writeFile(
55
+ statePath,
56
+ JSON.stringify({ version: STATE_VERSION + 99, sources: {} })
57
+ );
58
+ await expect(loadState(statePath)).rejects.toThrow(/unsupported version/);
59
+ });
60
+
61
+ it("rejects corrupt JSON with a clear error", async () => {
62
+ await fsp.writeFile(statePath, "{ not valid json");
63
+ await expect(loadState(statePath)).rejects.toThrow(/corrupt JSON/);
64
+ });
65
+ });
66
+
67
+ describe("saveState", () => {
68
+ it("creates parent directory if missing", async () => {
69
+ const nested = join(tmp, "a", "b", "c", "corpus.json");
70
+ await saveState(emptyState(), nested);
71
+ expect(existsSync(nested)).toBe(true);
72
+ });
73
+
74
+ it("writes with mode 0600 (owner-only)", async () => {
75
+ await saveState(emptyState(), statePath);
76
+ const stat = await fsp.stat(statePath);
77
+ // mode bits: only check owner read+write set, group/other empty
78
+ const mode = stat.mode & 0o777;
79
+ expect(mode).toBe(0o600);
80
+ });
81
+
82
+ it("uses atomic rename (no half-written file on crash)", async () => {
83
+ // Save twice; the second save shouldn't leave a .tmp behind
84
+ await saveState(emptyState(), statePath);
85
+ await saveState(emptyState(), statePath);
86
+ const dir = await fsp.readdir(tmp);
87
+ expect(dir.filter((f) => f.endsWith(".tmp"))).toHaveLength(0);
88
+ });
89
+ });
90
+
91
+ describe("upsertSource / removeSource / getSource", () => {
92
+ it("upsert merges patches and resolves to absolute path", async () => {
93
+ const state = emptyState();
94
+ upsertSource(state, "./relative-path", { sourceType: "directory" });
95
+ const keys = Object.keys(state.sources);
96
+ expect(keys).toHaveLength(1);
97
+ expect(keys[0]).toMatch(/^\//); // absolute
98
+ });
99
+
100
+ it("upsert preserves existing fields when patching", async () => {
101
+ const state = emptyState();
102
+ upsertSource(state, "/repo", { sourceType: "git" });
103
+ upsertSource(state, "/repo", { lastSyncedCommit: "abc" });
104
+ expect(state.sources["/repo"].sourceType).toBe("git");
105
+ expect(state.sources["/repo"].lastSyncedCommit).toBe("abc");
106
+ });
107
+
108
+ it("removeSource returns false for unknown paths", () => {
109
+ const state = emptyState();
110
+ expect(removeSource(state, "/never-added")).toBe(false);
111
+ });
112
+
113
+ it("removeSource returns true and deletes when present", () => {
114
+ const state = emptyState();
115
+ upsertSource(state, "/repo", {});
116
+ expect(removeSource(state, "/repo")).toBe(true);
117
+ expect(state.sources["/repo"]).toBeUndefined();
118
+ });
119
+
120
+ it("getSource resolves relative paths to absolute lookups", () => {
121
+ const state = emptyState();
122
+ upsertSource(state, "/abs/repo", {});
123
+ // Looking up a different relative form that resolves to same abs
124
+ // returns the same record (assuming cwd matches when the test runs
125
+ // — we use an absolute path here to avoid flakiness)
126
+ expect(getSource(state, "/abs/repo")).toBeDefined();
127
+ expect(getSource(state, "/abs/other")).toBeNull();
128
+ });
129
+ });
130
+
131
+ describe("recordFile / forgetFile / recomputeStats", () => {
132
+ it("recordFile sets hash, chunks, indexedAt", () => {
133
+ const state = emptyState();
134
+ const src = upsertSource(state, "/repo", {});
135
+ recordFile(src, "src/x.ts", "hash123", 4);
136
+ expect(src.files["src/x.ts"]).toMatchObject({
137
+ hash: "hash123",
138
+ chunks: 4,
139
+ });
140
+ expect(src.files["src/x.ts"].indexedAt).toBeTruthy();
141
+ });
142
+
143
+ it("forgetFile removes the entry", () => {
144
+ const state = emptyState();
145
+ const src = upsertSource(state, "/repo", {});
146
+ recordFile(src, "src/x.ts", "h", 1);
147
+ expect(forgetFile(src, "src/x.ts")).toBe(true);
148
+ expect(src.files["src/x.ts"]).toBeUndefined();
149
+ expect(forgetFile(src, "src/x.ts")).toBe(false);
150
+ });
151
+
152
+ it("recomputeStats reflects current files", () => {
153
+ const state = emptyState();
154
+ const src = upsertSource(state, "/repo", {});
155
+ recordFile(src, "a", "h1", 3);
156
+ recordFile(src, "b", "h2", 7);
157
+ const stats = recomputeStats(src);
158
+ expect(stats.fileCount).toBe(2);
159
+ expect(stats.chunkCount).toBe(10);
160
+ });
161
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Tests for ingest opts.distill / opts.hyde — both must be skippable
3
+ * for corpus ingest so code references don't trigger conversation-shaped
4
+ * LLM enrichment (distillation hallucinates "user facts" from code,
5
+ * HyDE generates expansion that won't match how callers query symbols).
6
+ */
7
+
8
+ import { ingest } from "../ingest.js";
9
+
10
+ function mockDb() {
11
+ const queries = [];
12
+ const db = async (sql, params) => {
13
+ queries.push({ sql, params });
14
+ if (sql.includes("FROM memory_layers")) {
15
+ return { rows: [{ id: "layer_xyz" }] };
16
+ }
17
+ return { rows: [] };
18
+ };
19
+ db.queries = queries;
20
+ return db;
21
+ }
22
+
23
+ function mockAi() {
24
+ let embedCalls = 0;
25
+ return {
26
+ embed: async () => {
27
+ embedCalls++;
28
+ return {
29
+ embedding: new Array(8).fill(0.1),
30
+ dimensions: 8,
31
+ model: "mock-embed",
32
+ };
33
+ },
34
+ chat: async () => "",
35
+ get embedCalls() {
36
+ return embedCalls;
37
+ },
38
+ };
39
+ }
40
+
41
+ function mockLlm() {
42
+ let chatCalls = 0;
43
+ const calls = [];
44
+ return {
45
+ chat: async (...args) => {
46
+ chatCalls++;
47
+ calls.push(args);
48
+ // Fake hypothetical-question response (3 lines)
49
+ return "What does this do?\nHow does this work?\nWhen is this called?";
50
+ },
51
+ get chatCalls() {
52
+ return chatCalls;
53
+ },
54
+ get calls() {
55
+ return calls;
56
+ },
57
+ };
58
+ }
59
+
60
+ describe("ingest — corpus opt-outs", () => {
61
+ it("runs HyDE by default (chat is invoked)", async () => {
62
+ const db = mockDb();
63
+ const ai = mockAi();
64
+ const llm = mockLlm();
65
+ await ingest(db, ai, llm, "user said hello", {
66
+ clientId: "acme",
67
+ distill: false, // isolate HyDE behavior
68
+ });
69
+ expect(llm.chatCalls).toBeGreaterThan(0);
70
+ // The HyDE update should land in metadata
71
+ const hydeUpdate = db.queries.find((q) =>
72
+ q.sql.includes("hypothetical_queries")
73
+ );
74
+ expect(hydeUpdate).toBeDefined();
75
+ });
76
+
77
+ it("skips HyDE when opts.hyde === false", async () => {
78
+ const db = mockDb();
79
+ const ai = mockAi();
80
+ const llm = mockLlm();
81
+ await ingest(db, ai, llm, "function authenticate(req)", {
82
+ clientId: "acme",
83
+ distill: false,
84
+ hyde: false,
85
+ });
86
+ expect(llm.chatCalls).toBe(0);
87
+ const hydeUpdate = db.queries.find((q) =>
88
+ q.sql.includes("hypothetical_queries")
89
+ );
90
+ expect(hydeUpdate).toBeUndefined();
91
+ });
92
+
93
+ it("skips both HyDE and distill for code references", async () => {
94
+ const db = mockDb();
95
+ const ai = mockAi();
96
+ const llm = mockLlm();
97
+ await ingest(db, ai, llm, "function authenticate(req)", {
98
+ clientId: "acme",
99
+ distill: false,
100
+ hyde: false,
101
+ metadata: {
102
+ kind: "code_reference",
103
+ path: "src/auth.js",
104
+ symbol: "authenticate",
105
+ },
106
+ });
107
+ // No LLM calls at all (only embedding)
108
+ expect(llm.chatCalls).toBe(0);
109
+ expect(ai.embedCalls).toBe(1);
110
+ });
111
+
112
+ it("preserves the metadata.kind field through to the insert", async () => {
113
+ const db = mockDb();
114
+ const ai = mockAi();
115
+ const llm = mockLlm();
116
+ await ingest(db, ai, llm, "fn x()", {
117
+ clientId: "acme",
118
+ hyde: false,
119
+ distill: false,
120
+ metadata: { kind: "code_reference", path: "x.js" },
121
+ });
122
+ const insert = db.queries.find((q) =>
123
+ q.sql.includes("INSERT INTO memory_nodes")
124
+ );
125
+ expect(insert).toBeDefined();
126
+ const metadataParam = JSON.parse(insert.params[4]);
127
+ expect(metadataParam.kind).toBe("code_reference");
128
+ });
129
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Tests for search opts.kind — must add a metadata->>'kind' filter to
3
+ * the SQL so corpus-shaped queries can scope to code references and
4
+ * not compete with conversational memory for top-K slots.
5
+ */
6
+
7
+ import { search, textSearch } from "../search.js";
8
+
9
+ function makeDb() {
10
+ const calls = [];
11
+ const db = async (sql, params) => {
12
+ calls.push({ sql, params });
13
+ // Vector column existence check — pretend it's there
14
+ if (sql.includes("information_schema.columns")) {
15
+ return { rows: [{}] };
16
+ }
17
+ // Search SQL — return empty rows; we only assert SQL shape
18
+ return { rows: [] };
19
+ };
20
+ db.calls = calls;
21
+ return db;
22
+ }
23
+
24
+ function makeAi() {
25
+ return {
26
+ embed: async () => ({
27
+ embedding: new Array(8).fill(0.1),
28
+ dimensions: 8,
29
+ model: "mock",
30
+ }),
31
+ };
32
+ }
33
+
34
+ describe("search — kind filter", () => {
35
+ it("adds metadata->>'kind' filter when opts.kind is provided", async () => {
36
+ const db = makeDb();
37
+ await search(db, makeAi(), "auth", {
38
+ clientId: "acme",
39
+ kind: "code_reference",
40
+ });
41
+ const searchCall = db.calls.find(
42
+ (c) =>
43
+ c.sql.includes("FROM memory_nodes mn") &&
44
+ c.sql.includes("ORDER BY final_score")
45
+ );
46
+ expect(searchCall).toBeDefined();
47
+ expect(searchCall.sql).toMatch(/metadata->>'kind' = \$\d+/);
48
+ expect(searchCall.params).toContain("code_reference");
49
+ });
50
+
51
+ it("omits the kind filter entirely when not provided", async () => {
52
+ const db = makeDb();
53
+ await search(db, makeAi(), "auth", { clientId: "acme" });
54
+ const searchCall = db.calls.find(
55
+ (c) =>
56
+ c.sql.includes("FROM memory_nodes mn") &&
57
+ c.sql.includes("ORDER BY final_score")
58
+ );
59
+ expect(searchCall.sql).not.toMatch(/metadata->>'kind'/);
60
+ });
61
+
62
+ it("supports kind together with userId (param numbering stays valid)", async () => {
63
+ const db = makeDb();
64
+ await search(db, makeAi(), "auth", {
65
+ clientId: "acme",
66
+ userId: "u1",
67
+ kind: "code_reference",
68
+ });
69
+ const searchCall = db.calls.find(
70
+ (c) =>
71
+ c.sql.includes("FROM memory_nodes mn") &&
72
+ c.sql.includes("ORDER BY final_score")
73
+ );
74
+ expect(searchCall.sql).toMatch(/mn.user_id = \$\d+/);
75
+ expect(searchCall.sql).toMatch(/metadata->>'kind' = \$\d+/);
76
+ // Check params include both
77
+ expect(searchCall.params).toContain("u1");
78
+ expect(searchCall.params).toContain("code_reference");
79
+ });
80
+ });
81
+
82
+ describe("textSearch fallback — kind filter", () => {
83
+ it("propagates kind to the fallback path SQL", async () => {
84
+ const db = makeDb();
85
+ await textSearch(db, "auth", {
86
+ clientId: "acme",
87
+ kind: "code_reference",
88
+ });
89
+ const searchCall = db.calls.find(
90
+ (c) =>
91
+ c.sql.includes("FROM memory_nodes mn") &&
92
+ c.sql.includes("plainto_tsquery")
93
+ );
94
+ expect(searchCall.sql).toMatch(/metadata->>'kind' = \$\d+/);
95
+ expect(searchCall.params).toContain("code_reference");
96
+ });
97
+
98
+ it("omits kind from textSearch when not provided", async () => {
99
+ const db = makeDb();
100
+ await textSearch(db, "auth", { clientId: "acme" });
101
+ const searchCall = db.calls.find(
102
+ (c) =>
103
+ c.sql.includes("FROM memory_nodes mn") &&
104
+ c.sql.includes("plainto_tsquery")
105
+ );
106
+ expect(searchCall.sql).not.toMatch(/metadata->>'kind'/);
107
+ });
108
+ });