@rce-mcp/retrieval-core 0.1.0 → 0.1.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.
@@ -0,0 +1,322 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { SqliteIndexRepository, SqliteQueryCache } from "@rce-mcp/data-plane";
6
+ import { RetrievalCore } from "../src/index.js";
7
+
8
+ function firstRank(results: Array<{ path: string }>, path: string): number {
9
+ const index = results.findIndex((row) => row.path === path);
10
+ return index >= 0 ? index + 1 : Number.POSITIVE_INFINITY;
11
+ }
12
+
13
+ function buildLongCircuitBreakerFixture(): string {
14
+ const lines: string[] = [
15
+ "export interface RiskState {",
16
+ " accountId: string;",
17
+ " pnlSeries: number[];",
18
+ " window: number[];",
19
+ "}",
20
+ "",
21
+ "const HARD_DRAWDOWN_LIMIT_BPS = 650;",
22
+ "const RISK_ACCUMULATOR_LIMIT = 25_000;",
23
+ "",
24
+ "function calculateDrawdownBps(values: number[], checksum: number): number {",
25
+ " return values.length > 0 ? Math.max(...values) + (checksum % 7) : checksum % 7;",
26
+ "}",
27
+ "",
28
+ "function captureWindowChecksum(values: number[]): number {",
29
+ " return values.reduce((sum, value) => sum + value, 0);",
30
+ "}",
31
+ "",
32
+ "function publishTripEvent(input: { reason: string; drawdownBps: number; riskAccumulator: number }) {",
33
+ " return { eventId: `${input.reason}-${input.drawdownBps}-${input.riskAccumulator}` };",
34
+ "}",
35
+ "",
36
+ "function freezeOrderEntry(accountId: string, eventId: string): void {",
37
+ " void accountId;",
38
+ " void eventId;",
39
+ "}",
40
+ "",
41
+ "function recordTripAuditLog(accountId: string, eventId: string, checksum: number): string {",
42
+ " void accountId;",
43
+ " void eventId;",
44
+ " return `${eventId}-${checksum}`;",
45
+ "}",
46
+ "",
47
+ "export function evaluateCircuitBreaker(state: RiskState): boolean {",
48
+ " const startWindowChecksum = captureWindowChecksum(state.window);",
49
+ " const drawdownBps = calculateDrawdownBps(state.pnlSeries, startWindowChecksum);",
50
+ " const circuitActivationSeed = state.window.length + state.accountId.length;",
51
+ " let riskAccumulator = 0;"
52
+ ];
53
+ for (let i = 0; i < 35; i += 1) {
54
+ lines.push(` riskAccumulator += state.window[${i}] ?? 0;`);
55
+ }
56
+ lines.push(
57
+ " const shouldTrip = drawdownBps >= HARD_DRAWDOWN_LIMIT_BPS || riskAccumulator >= RISK_ACCUMULATOR_LIMIT;",
58
+ " if (!shouldTrip) {",
59
+ " return false;",
60
+ " }",
61
+ " const tripEvent = publishTripEvent({",
62
+ " reason: 'hard-drawdown',",
63
+ " drawdownBps,",
64
+ " riskAccumulator",
65
+ " });",
66
+ " const tripAuditDigest = recordTripAuditLog(state.accountId, tripEvent.eventId, startWindowChecksum);",
67
+ " freezeOrderEntry(state.accountId, tripAuditDigest);",
68
+ " return true;",
69
+ "}"
70
+ );
71
+ return lines.join("\n");
72
+ }
73
+
74
+ describe("mcp search quality regressions", () => {
75
+ const dirs: string[] = [];
76
+
77
+ afterEach(async () => {
78
+ while (dirs.length > 0) {
79
+ const dir = dirs.pop();
80
+ if (dir) {
81
+ await rm(dir, { recursive: true, force: true });
82
+ }
83
+ }
84
+ });
85
+
86
+ it("keeps MCP enforcement/config targets in top results and suppresses guidance noise", async () => {
87
+ const root = await mkdtemp(join(tmpdir(), "rce-mcp-quality-"));
88
+ dirs.push(root);
89
+ const sqlitePath = join(root, "mcp-quality.sqlite");
90
+
91
+ const repo = new SqliteIndexRepository(sqlitePath);
92
+ await repo.migrate();
93
+ await repo.upsertWorkspace({
94
+ workspace_id: "ws-mcp",
95
+ tenant_id: "tenant-mcp",
96
+ name: "mcp",
97
+ project_root_path: "/workspace/mcp"
98
+ });
99
+
100
+ const cache = new SqliteQueryCache(sqlitePath);
101
+ const core = new RetrievalCore(repo, cache);
102
+
103
+ try {
104
+ await core.indexArtifact({
105
+ tenant_id: "tenant-mcp",
106
+ workspace_id: "ws-mcp",
107
+ index_version: "idx-mcp-v1",
108
+ files: [
109
+ {
110
+ path: "apps/mcp-api/src/server.ts",
111
+ language: "typescript",
112
+ content: `
113
+ export function resolveMcpAuthContext() {}
114
+ export function assertSessionMatchesAuth() {}
115
+ export function enforceMcpTenantBinding() {}
116
+ export function streamableHttpMcpRoute() {}
117
+ `
118
+ },
119
+ {
120
+ path: "apps/mcp-api/src/state.ts",
121
+ language: "typescript",
122
+ content: `
123
+ export const RCE_MCP_TOKEN_BINDINGS_JSON = "RCE_MCP_TOKEN_BINDINGS_JSON";
124
+ export function parseMcpTokenBindingsJson(raw: string) { return raw; }
125
+ export function resolveMcpAuthConfigFromEnv() { return { tenant_id: "t", workspace_id: "w", project_root_path: "/p" }; }
126
+ `
127
+ },
128
+ {
129
+ path: "apps/mcp-api/src/mcp-stdio.ts",
130
+ language: "typescript",
131
+ content: `
132
+ export function bootstrapRemoteProxy() {}
133
+ export function runRemoteMcpStdioServer() {}
134
+ export const REMOTE_BINDING_FIELDS = ["workspace_id", "project_root_path"];
135
+ `
136
+ },
137
+ {
138
+ path: "apps/mcp-api/src/mcp-tool-guidance.ts",
139
+ language: "typescript",
140
+ content: `
141
+ // guidance-only text containing overlapping auth/config terms
142
+ export const GUIDE = "tenant_id workspace_id project_root_path token binding mcp security config";
143
+ `
144
+ },
145
+ {
146
+ path: "apps/mcp-api/src/tools/search-context.ts",
147
+ language: "typescript",
148
+ content: "export function searchContextTool() { return 'tool'; }"
149
+ },
150
+ {
151
+ path: "apps/mcp-api/src/tools/enhance-prompt.ts",
152
+ language: "typescript",
153
+ content: "export function enhancePromptTool() { return 'tool'; }"
154
+ },
155
+ {
156
+ path: "apps/mcp-api/src/docs/reference.md",
157
+ language: "markdown",
158
+ content: "MCP docs reference for tokens, tenant_id, workspace_id, and project_root_path."
159
+ }
160
+ ]
161
+ });
162
+
163
+ const enforcement = await core.searchContext({
164
+ trace_id: "trc-mcp-enforcement",
165
+ tenant_id: "tenant-mcp",
166
+ workspace_id: "ws-mcp",
167
+ request: {
168
+ project_root_path: "/workspace/mcp",
169
+ query: "resolveMcpAuthContext enforceMcpTenantBinding assertSessionMatchesAuth streamable HTTP /mcp",
170
+ top_k: 8,
171
+ filters: { path_prefix: "apps/mcp-api/src" }
172
+ }
173
+ });
174
+
175
+ const config = await core.searchContext({
176
+ trace_id: "trc-mcp-config",
177
+ tenant_id: "tenant-mcp",
178
+ workspace_id: "ws-mcp",
179
+ request: {
180
+ project_root_path: "/workspace/mcp",
181
+ query: "RCE_MCP_TOKEN_BINDINGS_JSON parse token bindings tenant_id workspace_id project_root_path",
182
+ top_k: 6,
183
+ filters: { path_prefix: "apps/mcp-api/src" }
184
+ }
185
+ });
186
+
187
+ const stdio = await core.searchContext({
188
+ trace_id: "trc-mcp-stdio",
189
+ tenant_id: "tenant-mcp",
190
+ workspace_id: "ws-mcp",
191
+ request: {
192
+ project_root_path: "/workspace/mcp",
193
+ query: "mcp-stdio remote bootstrap token binding workspace_id project_root_path",
194
+ top_k: 6,
195
+ filters: { path_prefix: "apps/mcp-api/src" }
196
+ }
197
+ });
198
+
199
+ const enforcementTarget = firstRank(enforcement.results, "apps/mcp-api/src/server.ts");
200
+ const configTarget = firstRank(config.results, "apps/mcp-api/src/state.ts");
201
+ const stdioTarget = firstRank(stdio.results, "apps/mcp-api/src/mcp-stdio.ts");
202
+ const enforcementGuidance = firstRank(enforcement.results, "apps/mcp-api/src/mcp-tool-guidance.ts");
203
+ const configGuidance = firstRank(config.results, "apps/mcp-api/src/mcp-tool-guidance.ts");
204
+
205
+ expect(enforcementTarget).toBeLessThanOrEqual(3);
206
+ expect(configTarget).toBeLessThanOrEqual(3);
207
+ expect(stdioTarget).toBeLessThanOrEqual(3);
208
+ expect(enforcementGuidance).toBeGreaterThan(enforcementTarget);
209
+ expect(configGuidance).toBeGreaterThan(configTarget);
210
+ } finally {
211
+ cache.close();
212
+ repo.close();
213
+ }
214
+ });
215
+
216
+ it("returns larger, more complete snippets with upgraded chunk windows", async () => {
217
+ const root = await mkdtemp(join(tmpdir(), "rce-mcp-snippet-quality-"));
218
+ dirs.push(root);
219
+ const sqlitePath = join(root, "mcp-snippet-quality.sqlite");
220
+
221
+ const repo = new SqliteIndexRepository(sqlitePath);
222
+ await repo.migrate();
223
+ await repo.upsertWorkspace({
224
+ workspace_id: "ws-snippet-legacy",
225
+ tenant_id: "tenant-snippet",
226
+ name: "snippet-quality-legacy",
227
+ project_root_path: "/workspace/snippet-legacy"
228
+ });
229
+ await repo.upsertWorkspace({
230
+ workspace_id: "ws-snippet-upgraded",
231
+ tenant_id: "tenant-snippet",
232
+ name: "snippet-quality-upgraded",
233
+ project_root_path: "/workspace/snippet-upgraded"
234
+ });
235
+
236
+ const cache = new SqliteQueryCache(sqlitePath);
237
+ const legacyCore = new RetrievalCore(repo, cache, {
238
+ chunkingConfig: {
239
+ strategy: "sliding",
240
+ target_chunk_tokens: 220,
241
+ chunk_overlap_tokens: 40
242
+ }
243
+ });
244
+ const upgradedCore = new RetrievalCore(repo, cache, {
245
+ chunkingConfig: {
246
+ strategy: "sliding",
247
+ target_chunk_tokens: 420,
248
+ chunk_overlap_tokens: 90
249
+ }
250
+ });
251
+
252
+ try {
253
+ const files = [
254
+ {
255
+ path: "packages/shared/src/risk/circuit-breaker.ts",
256
+ language: "typescript",
257
+ content: buildLongCircuitBreakerFixture()
258
+ },
259
+ {
260
+ path: "docs/risk-architecture.md",
261
+ language: "markdown",
262
+ content: "High-level risk architecture notes: circuit breaker policy, drawdown thresholds, and account controls."
263
+ }
264
+ ] as const;
265
+
266
+ await legacyCore.indexArtifact({
267
+ tenant_id: "tenant-snippet",
268
+ workspace_id: "ws-snippet-legacy",
269
+ index_version: "idx-snippet-legacy-v1",
270
+ files: [...files]
271
+ });
272
+ await upgradedCore.indexArtifact({
273
+ tenant_id: "tenant-snippet",
274
+ workspace_id: "ws-snippet-upgraded",
275
+ index_version: "idx-snippet-upgraded-v1",
276
+ files: [...files]
277
+ });
278
+
279
+ const query = "evaluateCircuitBreaker circuitActivationSeed tripAuditDigest freezeOrderEntry hard drawdown";
280
+ const legacyRetrieval = await legacyCore.searchContext({
281
+ trace_id: "trc-snippet-completeness-legacy",
282
+ tenant_id: "tenant-snippet",
283
+ workspace_id: "ws-snippet-legacy",
284
+ request: {
285
+ project_root_path: "/workspace/snippet-legacy",
286
+ query,
287
+ top_k: 5
288
+ }
289
+ });
290
+ const upgradedRetrieval = await upgradedCore.searchContext({
291
+ trace_id: "trc-snippet-completeness-upgraded",
292
+ tenant_id: "tenant-snippet",
293
+ workspace_id: "ws-snippet-upgraded",
294
+ request: {
295
+ project_root_path: "/workspace/snippet-upgraded",
296
+ query,
297
+ top_k: 5
298
+ }
299
+ });
300
+
301
+ const targetPath = "packages/shared/src/risk/circuit-breaker.ts";
302
+ const legacyTop = legacyRetrieval.results.find((row) => row.path === targetPath);
303
+ const upgradedTop = upgradedRetrieval.results.find((row) => row.path === targetPath);
304
+ expect(legacyTop).toBeDefined();
305
+ expect(upgradedTop).toBeDefined();
306
+
307
+ const legacySpan = (legacyTop?.end_line ?? 0) - (legacyTop?.start_line ?? 0);
308
+ const upgradedSpan = (upgradedTop?.end_line ?? 0) - (upgradedTop?.start_line ?? 0);
309
+ expect(upgradedSpan).toBeGreaterThan(legacySpan);
310
+ expect((upgradedTop?.snippet.length ?? 0)).toBeGreaterThan(legacyTop?.snippet.length ?? 0);
311
+
312
+ const tokenCoverage = (snippet: string | undefined): number =>
313
+ ["circuitActivationSeed", "tripAuditDigest", "freezeOrderEntry"].filter((token) => snippet?.includes(token))
314
+ .length;
315
+ expect(tokenCoverage(upgradedTop?.snippet)).toBeGreaterThanOrEqual(tokenCoverage(legacyTop?.snippet));
316
+ expect(tokenCoverage(upgradedTop?.snippet)).toBeGreaterThanOrEqual(2);
317
+ } finally {
318
+ cache.close();
319
+ repo.close();
320
+ }
321
+ });
322
+ });
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
5
5
  import {
6
6
  RemoteSyncHttpResponseError,
7
7
  buildRemoteSyncDeltaFromState,
8
+ collectProjectFileStats,
8
9
  runRemoteDeltaSync,
9
10
  type RemoteSyncStateFile
10
11
  } from "../src/index.js";
@@ -29,6 +30,56 @@ afterEach(async () => {
29
30
  });
30
31
 
31
32
  describe("runRemoteDeltaSync", () => {
33
+ it("excludes .tmp directories from remote sync deltas", async () => {
34
+ const root = await createProject();
35
+ await mkdir(join(root, ".tmp", "cache"), { recursive: true });
36
+ await writeFile(join(root, "src", "keep.ts"), "export const KEEP = true;\n");
37
+ await writeFile(join(root, ".tmp", "cache", "noise.ts"), "export const NOISE = true;\n");
38
+
39
+ const result = await buildRemoteSyncDeltaFromState({
40
+ project_root_path: root,
41
+ force_full_upsert: true
42
+ });
43
+
44
+ expect(result.delta.upsert_files.map((file) => file.path)).toEqual(["src/keep.ts"]);
45
+ expect(Object.keys(result.next_files)).toEqual(["src/keep.ts"]);
46
+ });
47
+
48
+ it("applies .contextignore and .rceignore patterns to remote sync deltas", async () => {
49
+ const root = await createProject();
50
+ await mkdir(join(root, "src", "generated"), { recursive: true });
51
+ await mkdir(join(root, "generated"), { recursive: true });
52
+ await mkdir(join(root, "build"), { recursive: true });
53
+
54
+ await writeFile(
55
+ join(root, ".contextignore"),
56
+ "# comments and blank lines are ignored\n\n**/generated/**\n"
57
+ );
58
+ await writeFile(join(root, ".rceignore"), "build/\n");
59
+ await writeFile(join(root, "src", "keep.ts"), "export const KEEP = true;\n");
60
+ await writeFile(join(root, "src", "generated", "types.ts"), "export const GENERATED = true;\n");
61
+ await writeFile(join(root, "generated", "root.ts"), "export const ROOT_GENERATED = true;\n");
62
+ await writeFile(join(root, "build", "bundle.ts"), "export const BUNDLE = true;\n");
63
+
64
+ const result = await buildRemoteSyncDeltaFromState({
65
+ project_root_path: root,
66
+ force_full_upsert: true
67
+ });
68
+
69
+ expect(result.delta.upsert_files.map((file) => file.path)).toEqual(["src/keep.ts"]);
70
+ expect(Object.keys(result.next_files)).toEqual(["src/keep.ts"]);
71
+ });
72
+
73
+ it("maps .mjs and .cjs files to javascript language metadata during scans", async () => {
74
+ const root = await createProject();
75
+ await writeFile(join(root, "src", "runtime.mjs"), "export const runtime = true;\n");
76
+ await writeFile(join(root, "src", "loader.cjs"), "module.exports = { loader: true };\n");
77
+
78
+ const stats = await collectProjectFileStats(root);
79
+ expect(stats.get("src/runtime.mjs")?.language).toBe("javascript");
80
+ expect(stats.get("src/loader.cjs")?.language).toBe("javascript");
81
+ });
82
+
32
83
  it("applies add/modify/delete changes incrementally", async () => {
33
84
  const root = await createProject();
34
85
  await writeFile(join(root, "src", "a.ts"), "export const A = 1;\n");