ragcode-context-engine 0.1.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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/README.zh-CN.md +363 -0
  4. package/dist/src/cli/configure/app.d.ts +6 -0
  5. package/dist/src/cli/configure/app.js +81 -0
  6. package/dist/src/cli/configure/run.d.ts +5 -0
  7. package/dist/src/cli/configure/run.js +85 -0
  8. package/dist/src/cli/configure/state.d.ts +42 -0
  9. package/dist/src/cli/configure/state.js +174 -0
  10. package/dist/src/cli/configure.d.ts +31 -0
  11. package/dist/src/cli/configure.js +101 -0
  12. package/dist/src/cli/index.d.ts +2 -0
  13. package/dist/src/cli/index.js +503 -0
  14. package/dist/src/cli/tui/index-progress.d.ts +12 -0
  15. package/dist/src/cli/tui/index-progress.js +49 -0
  16. package/dist/src/cli/tui/watch-status.d.ts +10 -0
  17. package/dist/src/cli/tui/watch-status.js +27 -0
  18. package/dist/src/cli/update.d.ts +18 -0
  19. package/dist/src/cli/update.js +111 -0
  20. package/dist/src/config/dotenv.d.ts +1 -0
  21. package/dist/src/config/dotenv.js +14 -0
  22. package/dist/src/config/graph-runtime.d.ts +13 -0
  23. package/dist/src/config/graph-runtime.js +29 -0
  24. package/dist/src/config/runtime-config.d.ts +87 -0
  25. package/dist/src/config/runtime-config.js +215 -0
  26. package/dist/src/config/semantic-runtime.d.ts +24 -0
  27. package/dist/src/config/semantic-runtime.js +89 -0
  28. package/dist/src/context/context-builder.d.ts +20 -0
  29. package/dist/src/context/context-builder.js +277 -0
  30. package/dist/src/context/expansion-policy.d.ts +6 -0
  31. package/dist/src/context/expansion-policy.js +49 -0
  32. package/dist/src/context/skeletonizer.d.ts +2 -0
  33. package/dist/src/context/skeletonizer.js +79 -0
  34. package/dist/src/context/snippet-renderer.d.ts +2 -0
  35. package/dist/src/context/snippet-renderer.js +67 -0
  36. package/dist/src/core/contracts.d.ts +74 -0
  37. package/dist/src/core/contracts.js +1 -0
  38. package/dist/src/core/engine.d.ts +64 -0
  39. package/dist/src/core/engine.js +442 -0
  40. package/dist/src/core/types.d.ts +490 -0
  41. package/dist/src/core/types.js +1 -0
  42. package/dist/src/diagnostics/doctor.d.ts +66 -0
  43. package/dist/src/diagnostics/doctor.js +193 -0
  44. package/dist/src/diagnostics/embedding-test.d.ts +24 -0
  45. package/dist/src/diagnostics/embedding-test.js +83 -0
  46. package/dist/src/graph/diff-files.d.ts +1 -0
  47. package/dist/src/graph/diff-files.js +14 -0
  48. package/dist/src/graph/impact-report.d.ts +10 -0
  49. package/dist/src/graph/impact-report.js +173 -0
  50. package/dist/src/graph/in-memory-graph-store.d.ts +36 -0
  51. package/dist/src/graph/in-memory-graph-store.js +395 -0
  52. package/dist/src/graph/owner-ranking.d.ts +2 -0
  53. package/dist/src/graph/owner-ranking.js +41 -0
  54. package/dist/src/graph/sqlite-graph-store.d.ts +51 -0
  55. package/dist/src/graph/sqlite-graph-store.js +724 -0
  56. package/dist/src/graph/sqlite-statements.d.ts +36 -0
  57. package/dist/src/graph/sqlite-statements.js +105 -0
  58. package/dist/src/graph/target-matcher.d.ts +13 -0
  59. package/dist/src/graph/target-matcher.js +64 -0
  60. package/dist/src/index.d.ts +32 -0
  61. package/dist/src/index.js +32 -0
  62. package/dist/src/indexing/analyzers/fallback-analyzer.d.ts +6 -0
  63. package/dist/src/indexing/analyzers/fallback-analyzer.js +45 -0
  64. package/dist/src/indexing/analyzers/go-treesitter-analyzer.d.ts +2 -0
  65. package/dist/src/indexing/analyzers/go-treesitter-analyzer.js +87 -0
  66. package/dist/src/indexing/analyzers/java-treesitter-analyzer.d.ts +2 -0
  67. package/dist/src/indexing/analyzers/java-treesitter-analyzer.js +88 -0
  68. package/dist/src/indexing/analyzers/python-treesitter-analyzer.d.ts +2 -0
  69. package/dist/src/indexing/analyzers/python-treesitter-analyzer.js +96 -0
  70. package/dist/src/indexing/analyzers/registry.d.ts +5 -0
  71. package/dist/src/indexing/analyzers/registry.js +23 -0
  72. package/dist/src/indexing/analyzers/rust-treesitter-analyzer.d.ts +2 -0
  73. package/dist/src/indexing/analyzers/rust-treesitter-analyzer.js +96 -0
  74. package/dist/src/indexing/analyzers/tree-sitter-base.d.ts +30 -0
  75. package/dist/src/indexing/analyzers/tree-sitter-base.js +163 -0
  76. package/dist/src/indexing/analyzers/types.d.ts +17 -0
  77. package/dist/src/indexing/analyzers/types.js +1 -0
  78. package/dist/src/indexing/analyzers/typescript-analyzer.d.ts +5 -0
  79. package/dist/src/indexing/analyzers/typescript-analyzer.js +199 -0
  80. package/dist/src/indexing/ast-analyzer.d.ts +11 -0
  81. package/dist/src/indexing/ast-analyzer.js +11 -0
  82. package/dist/src/indexing/chunker.d.ts +11 -0
  83. package/dist/src/indexing/chunker.js +157 -0
  84. package/dist/src/indexing/ignore-policy.d.ts +6 -0
  85. package/dist/src/indexing/ignore-policy.js +40 -0
  86. package/dist/src/indexing/indexer.d.ts +13 -0
  87. package/dist/src/indexing/indexer.js +189 -0
  88. package/dist/src/indexing/language.d.ts +3 -0
  89. package/dist/src/indexing/language.js +24 -0
  90. package/dist/src/indexing/scanner.d.ts +13 -0
  91. package/dist/src/indexing/scanner.js +87 -0
  92. package/dist/src/lsp/definition-resolver.d.ts +6 -0
  93. package/dist/src/lsp/definition-resolver.js +60 -0
  94. package/dist/src/lsp/typescript-language-service.d.ts +21 -0
  95. package/dist/src/lsp/typescript-language-service.js +82 -0
  96. package/dist/src/mcp/server.d.ts +11 -0
  97. package/dist/src/mcp/server.js +64 -0
  98. package/dist/src/mcp/tools.d.ts +266 -0
  99. package/dist/src/mcp/tools.js +309 -0
  100. package/dist/src/project/project-identity.d.ts +2 -0
  101. package/dist/src/project/project-identity.js +24 -0
  102. package/dist/src/project/project-registry.d.ts +12 -0
  103. package/dist/src/project/project-registry.js +49 -0
  104. package/dist/src/project/workspace-resolver.d.ts +20 -0
  105. package/dist/src/project/workspace-resolver.js +62 -0
  106. package/dist/src/retrieval/graph-reranker.d.ts +11 -0
  107. package/dist/src/retrieval/graph-reranker.js +0 -0
  108. package/dist/src/retrieval/hybrid-retriever.d.ts +31 -0
  109. package/dist/src/retrieval/hybrid-retriever.js +111 -0
  110. package/dist/src/retrieval/path-classification.d.ts +6 -0
  111. package/dist/src/retrieval/path-classification.js +22 -0
  112. package/dist/src/retrieval/query-matching.d.ts +22 -0
  113. package/dist/src/retrieval/query-matching.js +166 -0
  114. package/dist/src/retrieval/query-planner.d.ts +5 -0
  115. package/dist/src/retrieval/query-planner.js +77 -0
  116. package/dist/src/retrieval/ranking-signals.d.ts +19 -0
  117. package/dist/src/retrieval/ranking-signals.js +97 -0
  118. package/dist/src/retrieval/topology-distance.d.ts +21 -0
  119. package/dist/src/retrieval/topology-distance.js +116 -0
  120. package/dist/src/reuse/reuse-detector.d.ts +12 -0
  121. package/dist/src/reuse/reuse-detector.js +564 -0
  122. package/dist/src/semantic/deterministic-embedding.d.ts +7 -0
  123. package/dist/src/semantic/deterministic-embedding.js +31 -0
  124. package/dist/src/semantic/in-memory-semantic-store.d.ts +11 -0
  125. package/dist/src/semantic/in-memory-semantic-store.js +65 -0
  126. package/dist/src/semantic/lance-semantic-store.d.ts +131 -0
  127. package/dist/src/semantic/lance-semantic-store.js +623 -0
  128. package/dist/src/semantic/openai-compatible-embedding.d.ts +19 -0
  129. package/dist/src/semantic/openai-compatible-embedding.js +75 -0
  130. package/dist/src/service/service-identity.d.ts +13 -0
  131. package/dist/src/service/service-identity.js +48 -0
  132. package/dist/src/service/service-manager.d.ts +29 -0
  133. package/dist/src/service/service-manager.js +231 -0
  134. package/dist/src/service/service-templates.d.ts +22 -0
  135. package/dist/src/service/service-templates.js +101 -0
  136. package/dist/src/subgraph/impact-explainer.d.ts +2 -0
  137. package/dist/src/subgraph/impact-explainer.js +54 -0
  138. package/dist/src/subgraph/node-expander.d.ts +13 -0
  139. package/dist/src/subgraph/node-expander.js +139 -0
  140. package/dist/src/subgraph/output-preset.d.ts +3 -0
  141. package/dist/src/subgraph/output-preset.js +102 -0
  142. package/dist/src/subgraph/subgraph-builder.d.ts +17 -0
  143. package/dist/src/subgraph/subgraph-builder.js +688 -0
  144. package/dist/src/topology/export-index.d.ts +7 -0
  145. package/dist/src/topology/export-index.js +14 -0
  146. package/dist/src/topology/framework-topology.d.ts +3 -0
  147. package/dist/src/topology/framework-topology.js +460 -0
  148. package/dist/src/topology/import-resolver.d.ts +2 -0
  149. package/dist/src/topology/import-resolver.js +29 -0
  150. package/dist/src/topology/orm-topology.d.ts +3 -0
  151. package/dist/src/topology/orm-topology.js +200 -0
  152. package/dist/src/topology/runtime-topology.d.ts +3 -0
  153. package/dist/src/topology/runtime-topology.js +204 -0
  154. package/dist/src/topology/symbol-resolver.d.ts +6 -0
  155. package/dist/src/topology/symbol-resolver.js +74 -0
  156. package/dist/src/topology/test-topology.d.ts +2 -0
  157. package/dist/src/topology/test-topology.js +82 -0
  158. package/dist/src/utils/hash.d.ts +2 -0
  159. package/dist/src/utils/hash.js +7 -0
  160. package/dist/src/utils/path.d.ts +2 -0
  161. package/dist/src/utils/path.js +7 -0
  162. package/dist/src/watch/event-journal.d.ts +17 -0
  163. package/dist/src/watch/event-journal.js +81 -0
  164. package/dist/src/watch/file-event-coalescer.d.ts +9 -0
  165. package/dist/src/watch/file-event-coalescer.js +39 -0
  166. package/dist/src/watch/index-scheduler.d.ts +52 -0
  167. package/dist/src/watch/index-scheduler.js +190 -0
  168. package/dist/src/watch/watch-daemon.d.ts +73 -0
  169. package/dist/src/watch/watch-daemon.js +368 -0
  170. package/dist/src/watch/watcher-liveness.d.ts +47 -0
  171. package/dist/src/watch/watcher-liveness.js +168 -0
  172. package/dist/src/web/server.d.ts +1 -0
  173. package/dist/src/web/server.js +375 -0
  174. package/package.json +94 -0
@@ -0,0 +1,309 @@
1
+ import { z } from "zod";
2
+ import { buildExplainImpactReport } from "../subgraph/impact-explainer.js";
3
+ import { expandNode, parseNodeRef } from "../subgraph/node-expander.js";
4
+ import { applyExplainImpactOutputPreset, applySubgraphOutputPreset } from "../subgraph/output-preset.js";
5
+ import { readWatcherLiveness } from "../watch/watcher-liveness.js";
6
+ export const ToolNameSchema = z.enum([
7
+ "index_repo",
8
+ "refresh_index",
9
+ "index_status",
10
+ "watch_status",
11
+ "record_file_events",
12
+ "search_code",
13
+ "get_context",
14
+ "topology_map",
15
+ "find_symbol",
16
+ "explain_file",
17
+ "expand_node",
18
+ "find_owner",
19
+ "find_reuse_candidates",
20
+ "impact_analysis",
21
+ "explain_impact",
22
+ "related_tests",
23
+ "trace_flow",
24
+ "trace_request_flow",
25
+ "review_diff"
26
+ ]);
27
+ export const IndexRepoInput = z.object({ repoRoot: z.string().min(1) });
28
+ export const ContextModeSchema = z.enum(["auto", "debug", "feature", "refactor", "review", "explain"]);
29
+ export const ExpansionLevelSchema = z.enum(["file_card", "skeleton", "focused_body", "full_body"]);
30
+ export const SubgraphOutputPresetSchema = z.enum(["compact", "agent_edit", "debug_trace", "review_risk"]);
31
+ export const WorkspaceHintInput = z.object({ root: z.string().min(1).optional(), filePath: z.string().min(1).optional() }).optional();
32
+ export const RefreshIndexInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput });
33
+ export const IndexStatusInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput });
34
+ export const WatchStatusInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput });
35
+ export const RecordFileEventsInput = z.object({
36
+ repoRoot: z.string().min(1).optional(),
37
+ workspace: WorkspaceHintInput,
38
+ filePaths: z.array(z.string().min(1)).min(1),
39
+ burstThreshold: z.number().int().positive().optional(),
40
+ maxDirtyFiles: z.number().int().positive().optional()
41
+ });
42
+ export const SearchCodeInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput, query: z.string().min(1), limit: z.number().int().positive().optional(), mode: ContextModeSchema.optional() });
43
+ export const GetContextInput = SearchCodeInput.extend({ budgetChars: z.number().int().positive().optional() });
44
+ export const TopologyMapInput = SearchCodeInput.extend({ budgetChars: z.number().int().positive().optional(), maxEdges: z.number().int().positive().optional() });
45
+ export const FindSymbolInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput, name: z.string().min(1) });
46
+ export const ExplainFileInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput, filePath: z.string().min(1) });
47
+ export const ExpandNodeInput = z.object({
48
+ repoRoot: z.string().min(1).optional(),
49
+ workspace: WorkspaceHintInput,
50
+ nodeRef: z.string().min(1),
51
+ expansionLevel: ExpansionLevelSchema.optional(),
52
+ budgetChars: z.number().int().positive().optional()
53
+ });
54
+ export const FindOwnerInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput, query: z.string().min(1), limit: z.number().int().positive().optional() });
55
+ export const FindReuseCandidatesInput = FindOwnerInput.extend({ reuseGuard: z.boolean().optional() });
56
+ export const ImpactAnalysisInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput, target: z.string().min(1) });
57
+ export const ExplainImpactInput = ImpactAnalysisInput.extend({ budgetChars: z.number().int().positive().optional(), maxHops: z.number().int().positive().optional(), preset: SubgraphOutputPresetSchema.optional() });
58
+ export const RelatedTestsInput = ImpactAnalysisInput;
59
+ export const TraceFlowInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput, entry: z.string().min(1), maxSteps: z.number().int().positive().optional() });
60
+ export const TraceRequestFlowInput = z.object({
61
+ repoRoot: z.string().min(1).optional(),
62
+ workspace: WorkspaceHintInput,
63
+ entry: z.string().min(1),
64
+ query: z.string().min(1).optional(),
65
+ budgetChars: z.number().int().positive().optional(),
66
+ maxHops: z.number().int().positive().optional(),
67
+ preset: SubgraphOutputPresetSchema.optional()
68
+ });
69
+ export const ReviewDiffInput = z.object({ repoRoot: z.string().min(1).optional(), workspace: WorkspaceHintInput, diff: z.string().optional(), changedFiles: z.array(z.string()).optional() });
70
+ export function listToolDefinitions() {
71
+ return listRuntimeToolDefinitions().map((tool) => ({
72
+ name: tool.name,
73
+ description: tool.description,
74
+ inputSchema: zodToJsonShape(tool.inputSchema)
75
+ }));
76
+ }
77
+ export function listRuntimeToolDefinitions() {
78
+ return [
79
+ {
80
+ name: "index_repo",
81
+ description: "Index or re-index a local repository into the structural graph and semantic store.",
82
+ inputSchema: IndexRepoInput
83
+ },
84
+ {
85
+ name: "refresh_index",
86
+ description: "Force refresh the active indexed repository. Currently performs a full reindex; future versions can narrow to changed files.",
87
+ inputSchema: RefreshIndexInput
88
+ },
89
+ {
90
+ name: "index_status",
91
+ description: "Report indexed file/chunk/symbol/edge counts plus freshness, stale, pending, and skipped file state for the active repository.",
92
+ inputSchema: IndexStatusInput
93
+ },
94
+ {
95
+ name: "watch_status",
96
+ description: "Report whether a background watcher is alive for the active repository (lock + heartbeat liveness) plus current dirty/pending backlog. Read-only: never starts a watcher.",
97
+ inputSchema: WatchStatusInput
98
+ },
99
+ {
100
+ name: "record_file_events",
101
+ description: "Record watcher file events as coalesced dirty-file state without indexing immediately.",
102
+ inputSchema: RecordFileEventsInput
103
+ },
104
+ {
105
+ name: "search_code",
106
+ description: "Run hybrid code search over keyword and semantic indexes.",
107
+ inputSchema: SearchCodeInput
108
+ },
109
+ {
110
+ name: "get_context",
111
+ description: "Build an agent-ready context pack for a code question under a character budget.",
112
+ inputSchema: GetContextInput
113
+ },
114
+ {
115
+ name: "topology_map",
116
+ description: "Return owner-chain and topology edges for a feature/domain query without full evidence snippets.",
117
+ inputSchema: TopologyMapInput
118
+ },
119
+ {
120
+ name: "find_symbol",
121
+ description: "Find indexed symbols by name.",
122
+ inputSchema: FindSymbolInput
123
+ },
124
+ {
125
+ name: "explain_file",
126
+ description: "Return indexed file metadata, chunks, and symbols for a file.",
127
+ inputSchema: ExplainFileInput
128
+ },
129
+ {
130
+ name: "expand_node",
131
+ description: "Expand one node from a compact subgraph as a focused body, skeleton, file card, or full body under budget.",
132
+ inputSchema: ExpandNodeInput
133
+ },
134
+ {
135
+ name: "find_owner",
136
+ description: "Find likely owner files and symbols for a feature, bug, or architecture question.",
137
+ inputSchema: FindOwnerInput
138
+ },
139
+ {
140
+ name: "find_reuse_candidates",
141
+ description: "Find existing helpers, services, hooks, components, wrappers, schemas, or fixtures that should be reused before writing new code.",
142
+ inputSchema: FindReuseCandidatesInput
143
+ },
144
+ {
145
+ name: "impact_analysis",
146
+ description: "Estimate direct structural impact for a file or symbol using graph edges.",
147
+ inputSchema: ImpactAnalysisInput
148
+ },
149
+ {
150
+ name: "explain_impact",
151
+ description: "Return a verified minimal blast-radius subgraph with coverage signals, risk score, and edit-readiness guidance.",
152
+ inputSchema: ExplainImpactInput
153
+ },
154
+ {
155
+ name: "related_tests",
156
+ description: "Find likely related test files for a file or symbol target.",
157
+ inputSchema: RelatedTestsInput
158
+ },
159
+ {
160
+ name: "trace_flow",
161
+ description: "Trace outgoing call edges from an entry symbol or file hint.",
162
+ inputSchema: TraceFlowInput
163
+ },
164
+ {
165
+ name: "trace_request_flow",
166
+ description: "Return an ordered verified request/data-flow subgraph from an entry symbol or file hint.",
167
+ inputSchema: TraceRequestFlowInput
168
+ },
169
+ {
170
+ name: "review_diff",
171
+ description: "Review changed files or a unified diff for risk and related tests.",
172
+ inputSchema: ReviewDiffInput
173
+ }
174
+ ];
175
+ }
176
+ export async function callTool(engine, name, rawInput) {
177
+ switch (name) {
178
+ case "index_repo": {
179
+ const input = IndexRepoInput.parse(rawInput);
180
+ return engine.indexRepo(input.repoRoot);
181
+ }
182
+ case "refresh_index": {
183
+ const input = RefreshIndexInput.parse(rawInput);
184
+ return engine.refreshIndex(input.repoRoot ?? input.workspace?.root);
185
+ }
186
+ case "index_status": {
187
+ const input = IndexStatusInput.parse(rawInput);
188
+ return engine.indexStatus(input.repoRoot ?? input.workspace?.root);
189
+ }
190
+ case "watch_status": {
191
+ const input = WatchStatusInput.parse(rawInput);
192
+ // Resolve the repo via indexStatus (which applies workspace resolution), then read liveness
193
+ // from the on-disk lock/heartbeat. Pure read — never spawns a watcher; clients that want one
194
+ // running must install the OS service (`ragcode service install`) or run `ragcode watch`.
195
+ const status = await engine.indexStatus(input.repoRoot ?? input.workspace?.root);
196
+ const liveness = await readWatcherLiveness(status.repoRoot);
197
+ return {
198
+ repoRoot: status.repoRoot,
199
+ projectId: status.projectId,
200
+ watcher: liveness,
201
+ backlog: {
202
+ pendingFiles: status.pendingFileCount,
203
+ indexingFiles: status.indexingFileCount,
204
+ staleFiles: status.staleFileCount
205
+ },
206
+ hint: liveness.state === "running"
207
+ ? undefined
208
+ : "No live watcher; the index will not auto-refresh. Run `ragcode service install <repo>` for a background watcher, or `ragcode watch <repo>` manually."
209
+ };
210
+ }
211
+ case "record_file_events": {
212
+ const input = RecordFileEventsInput.parse(rawInput);
213
+ return engine.recordFileEvents(input.repoRoot ?? input.workspace?.root, input.filePaths, {
214
+ burstThreshold: input.burstThreshold,
215
+ maxDirtyFiles: input.maxDirtyFiles
216
+ });
217
+ }
218
+ case "search_code": {
219
+ const input = SearchCodeInput.parse(rawInput);
220
+ return engine.searchCode(input);
221
+ }
222
+ case "get_context": {
223
+ const input = GetContextInput.parse(rawInput);
224
+ return engine.getContext(input);
225
+ }
226
+ case "topology_map": {
227
+ const input = TopologyMapInput.parse(rawInput);
228
+ return engine.topologyMap(input);
229
+ }
230
+ case "find_symbol": {
231
+ const input = FindSymbolInput.parse(rawInput);
232
+ return engine.findSymbol(input.repoRoot ?? input.workspace?.root, input.name);
233
+ }
234
+ case "explain_file": {
235
+ const input = ExplainFileInput.parse(rawInput);
236
+ return engine.explainFile(input.repoRoot ?? input.workspace?.root, input.filePath);
237
+ }
238
+ case "expand_node": {
239
+ const input = ExpandNodeInput.parse(rawInput);
240
+ const parsed = parseNodeRef(input.nodeRef);
241
+ const indexedFile = await engine.explainFile(input.repoRoot ?? input.workspace?.root, parsed.filePath);
242
+ return expandNode({
243
+ nodeRef: input.nodeRef,
244
+ chunks: indexedFile.chunks,
245
+ symbols: indexedFile.symbols,
246
+ expansionLevel: input.expansionLevel,
247
+ budgetChars: input.budgetChars
248
+ });
249
+ }
250
+ case "find_owner": {
251
+ const input = FindOwnerInput.parse(rawInput);
252
+ return engine.findOwner(input.repoRoot ?? input.workspace?.root, input.query, input.limit);
253
+ }
254
+ case "find_reuse_candidates": {
255
+ const input = FindReuseCandidatesInput.parse(rawInput);
256
+ return engine.findReuseCandidates(input);
257
+ }
258
+ case "impact_analysis": {
259
+ const input = ImpactAnalysisInput.parse(rawInput);
260
+ return engine.impactAnalysis(input.repoRoot ?? input.workspace?.root, input.target);
261
+ }
262
+ case "explain_impact": {
263
+ const input = ExplainImpactInput.parse(rawInput);
264
+ const subgraph = await engine.verifiedSubgraph({
265
+ repoRoot: input.repoRoot ?? input.workspace?.root,
266
+ workspace: input.workspace,
267
+ query: input.target,
268
+ seed: input.target,
269
+ mode: "impact",
270
+ budgetChars: input.budgetChars,
271
+ maxHops: input.maxHops
272
+ });
273
+ return applyExplainImpactOutputPreset(buildExplainImpactReport(input.target, subgraph), input.preset ?? "compact");
274
+ }
275
+ case "related_tests": {
276
+ const input = RelatedTestsInput.parse(rawInput);
277
+ return engine.relatedTests(input.repoRoot ?? input.workspace?.root, input.target);
278
+ }
279
+ case "trace_flow": {
280
+ const input = TraceFlowInput.parse(rawInput);
281
+ return engine.traceFlow(input.repoRoot ?? input.workspace?.root, input.entry, input.maxSteps);
282
+ }
283
+ case "trace_request_flow": {
284
+ const input = TraceRequestFlowInput.parse(rawInput);
285
+ const subgraph = await engine.verifiedSubgraph({
286
+ repoRoot: input.repoRoot ?? input.workspace?.root,
287
+ workspace: input.workspace,
288
+ query: input.query ?? input.entry,
289
+ seed: input.entry,
290
+ mode: "flow",
291
+ budgetChars: input.budgetChars,
292
+ maxHops: input.maxHops
293
+ });
294
+ return applySubgraphOutputPreset(subgraph, input.preset ?? "compact");
295
+ }
296
+ case "review_diff": {
297
+ const input = ReviewDiffInput.parse(rawInput);
298
+ return engine.reviewDiff(input.repoRoot ?? input.workspace?.root, input.diff, input.changedFiles);
299
+ }
300
+ }
301
+ }
302
+ function zodToJsonShape(schema) {
303
+ // Keep this intentionally lightweight for the foundation. The SDK server can
304
+ // later use zod directly; this shape is enough for docs/tests/tool listing.
305
+ return {
306
+ type: "object",
307
+ description: schema.description ?? "See tool input contract in src/mcp/tools.ts"
308
+ };
309
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectIdentity } from "../core/types.js";
2
+ export declare function createProjectIdentity(repoRoot: string): Promise<ProjectIdentity>;
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { sha256 } from "../utils/hash.js";
4
+ export async function createProjectIdentity(repoRoot) {
5
+ const resolvedRoot = path.resolve(repoRoot);
6
+ const canonicalRoot = await fs.realpath(resolvedRoot).catch(() => resolvedRoot);
7
+ const gitRemote = await readGitRemote(canonicalRoot);
8
+ return {
9
+ projectId: sha256([canonicalRoot.toLowerCase(), gitRemote ?? ""].join("::")).slice(0, 24),
10
+ repoRoot: resolvedRoot,
11
+ canonicalRoot,
12
+ displayName: path.basename(canonicalRoot),
13
+ gitRemote,
14
+ createdAtMs: Date.now()
15
+ };
16
+ }
17
+ async function readGitRemote(repoRoot) {
18
+ const configPath = path.join(repoRoot, ".git", "config");
19
+ const config = await fs.readFile(configPath, "utf8").catch(() => undefined);
20
+ if (!config)
21
+ return undefined;
22
+ const match = /\[remote "origin"\][\s\S]*?url\s*=\s*(.+)/.exec(config);
23
+ return match?.[1]?.trim();
24
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProjectIdentity } from "../core/types.js";
2
+ export declare class ProjectRegistry {
3
+ private readonly byId;
4
+ private readonly byRoot;
5
+ register(repoRoot: string): Promise<ProjectIdentity>;
6
+ upsert(project: ProjectIdentity): ProjectIdentity;
7
+ getByProjectId(projectId: string): ProjectIdentity | undefined;
8
+ findByRoot(root: string): ProjectIdentity | undefined;
9
+ findContainingPath(filePath: string): ProjectIdentity[];
10
+ list(): ProjectIdentity[];
11
+ }
12
+ export declare function isInside(root: string, candidate: string): boolean;
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { createProjectIdentity } from "./project-identity.js";
3
+ export class ProjectRegistry {
4
+ byId = new Map();
5
+ byRoot = new Map();
6
+ async register(repoRoot) {
7
+ const identity = await createProjectIdentity(repoRoot);
8
+ const existing = this.byId.get(identity.projectId);
9
+ const merged = {
10
+ ...(existing ?? identity),
11
+ ...identity,
12
+ createdAtMs: existing?.createdAtMs ?? identity.createdAtMs,
13
+ lastIndexedAtMs: Date.now()
14
+ };
15
+ return this.upsert(merged);
16
+ }
17
+ upsert(project) {
18
+ const existing = this.byId.get(project.projectId);
19
+ const merged = {
20
+ ...(existing ?? project),
21
+ ...project,
22
+ createdAtMs: existing?.createdAtMs ?? project.createdAtMs
23
+ };
24
+ this.byId.set(merged.projectId, merged);
25
+ this.byRoot.set(normalizeRoot(merged.repoRoot), merged);
26
+ this.byRoot.set(normalizeRoot(merged.canonicalRoot), merged);
27
+ return merged;
28
+ }
29
+ getByProjectId(projectId) {
30
+ return this.byId.get(projectId);
31
+ }
32
+ findByRoot(root) {
33
+ return this.byRoot.get(normalizeRoot(root));
34
+ }
35
+ findContainingPath(filePath) {
36
+ const absolute = path.resolve(filePath);
37
+ return [...this.byId.values()].filter((project) => isInside(project.canonicalRoot, absolute) || isInside(project.repoRoot, absolute));
38
+ }
39
+ list() {
40
+ return [...this.byId.values()].sort((a, b) => a.canonicalRoot.localeCompare(b.canonicalRoot));
41
+ }
42
+ }
43
+ export function isInside(root, candidate) {
44
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
45
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
46
+ }
47
+ function normalizeRoot(root) {
48
+ return path.resolve(root).toLowerCase();
49
+ }
@@ -0,0 +1,20 @@
1
+ import type { ProjectIdentity, WorkspaceHint, WorkspaceSession } from "../core/types.js";
2
+ import { ProjectRegistry } from "./project-registry.js";
3
+ export interface WorkspaceResolverOptions {
4
+ cwd?: string;
5
+ roots?: string[];
6
+ }
7
+ export declare class WorkspaceResolver {
8
+ private readonly registry;
9
+ private readonly options;
10
+ private active?;
11
+ constructor(registry: ProjectRegistry, options?: WorkspaceResolverOptions);
12
+ setActive(project: ProjectIdentity, resolvedFrom?: WorkspaceSession["resolvedFrom"]): WorkspaceSession;
13
+ resolve(input?: {
14
+ repoRoot?: string;
15
+ workspace?: WorkspaceHint;
16
+ }): WorkspaceSession;
17
+ getActive(): WorkspaceSession | undefined;
18
+ private fromFilePath;
19
+ private fromRoot;
20
+ }
@@ -0,0 +1,62 @@
1
+ import path from "node:path";
2
+ export class WorkspaceResolver {
3
+ registry;
4
+ options;
5
+ active;
6
+ constructor(registry, options = {}) {
7
+ this.registry = registry;
8
+ this.options = options;
9
+ }
10
+ setActive(project, resolvedFrom = "active_session") {
11
+ this.active = {
12
+ activeProjectId: project.projectId,
13
+ activeRepoRoot: project.repoRoot,
14
+ knownProjects: this.registry.list(),
15
+ resolvedFrom
16
+ };
17
+ return this.active;
18
+ }
19
+ resolve(input = {}) {
20
+ if (input.workspace?.filePath)
21
+ return this.fromFilePath(input.workspace.filePath);
22
+ if (input.workspace?.root)
23
+ return this.fromRoot(input.workspace.root, "root");
24
+ if (input.repoRoot)
25
+ return this.fromRoot(input.repoRoot, "repoRoot");
26
+ for (const root of this.options.roots ?? []) {
27
+ const project = this.registry.findByRoot(root);
28
+ if (project)
29
+ return this.setActive(project, "mcp_roots");
30
+ }
31
+ if (this.active)
32
+ return this.active;
33
+ if (this.options.cwd) {
34
+ const project = this.registry.findByRoot(this.options.cwd);
35
+ if (project)
36
+ return this.setActive(project, "cwd");
37
+ }
38
+ const projects = this.registry.list();
39
+ if (projects.length === 1)
40
+ return this.setActive(projects[0], "single_project");
41
+ if (projects.length > 1)
42
+ throw new Error(`Ambiguous workspace: ${projects.length} indexed projects are available. Provide workspace.root or workspace.filePath.`);
43
+ throw new Error("Missing workspace: index a repository or provide workspace.root before retrieval.");
44
+ }
45
+ getActive() {
46
+ return this.active;
47
+ }
48
+ fromFilePath(filePath) {
49
+ const matches = this.registry.findContainingPath(path.resolve(filePath));
50
+ if (matches.length === 1)
51
+ return this.setActive(matches[0], "filePath");
52
+ if (matches.length > 1)
53
+ throw new Error(`Ambiguous workspace for filePath: ${filePath}`);
54
+ throw new Error(`No indexed workspace contains filePath: ${filePath}`);
55
+ }
56
+ fromRoot(root, resolvedFrom) {
57
+ const project = this.registry.findByRoot(root);
58
+ if (!project)
59
+ throw new Error(`Workspace is not indexed: ${root}`);
60
+ return this.setActive(project, resolvedFrom);
61
+ }
62
+ }
@@ -0,0 +1,11 @@
1
+ import type { GraphStore } from "../core/contracts.js";
2
+ import type { SearchHit, SearchQuery } from "../core/types.js";
3
+ import type { ResolvedContextMode } from "./query-planner.js";
4
+ export interface GraphRerankerOptions {
5
+ graphStore: GraphStore;
6
+ maxHops?: number;
7
+ maxSeeds?: number;
8
+ maxExpansionCandidates?: number;
9
+ maxExpansionHops?: number;
10
+ }
11
+ export declare function rerankWithGraph(hits: SearchHit[], query: SearchQuery, mode: ResolvedContextMode, options: GraphRerankerOptions): Promise<SearchHit[]>;
@@ -0,0 +1,31 @@
1
+ import type { EmbeddingProvider, GraphStore, SemanticStore } from "../core/contracts.js";
2
+ import type { SearchHit, SearchQuery } from "../core/types.js";
3
+ export interface HybridRetrieverOptions {
4
+ graphStore: GraphStore;
5
+ semanticStore: SemanticStore;
6
+ embeddingProvider: EmbeddingProvider;
7
+ }
8
+ export interface HybridSearchDiagnostics {
9
+ keywordHitCount: number;
10
+ semantic: {
11
+ status: "ok" | "failed";
12
+ hitCount: number;
13
+ error?: string;
14
+ };
15
+ fusion: {
16
+ semanticTopNParticipation: number;
17
+ };
18
+ }
19
+ export interface HybridSearchResult {
20
+ hits: SearchHit[];
21
+ diagnostics: HybridSearchDiagnostics;
22
+ }
23
+ export declare class HybridRetriever {
24
+ private readonly options;
25
+ constructor(options: HybridRetrieverOptions);
26
+ search(query: SearchQuery): Promise<SearchHit[]>;
27
+ searchWithDiagnostics(query: SearchQuery): Promise<HybridSearchResult>;
28
+ private searchSemantic;
29
+ }
30
+ export declare function fuseHits(keywordHits: SearchHit[], semanticHits: SearchHit[]): SearchHit[];
31
+ export declare function hasSemanticParticipation(hit: SearchHit): boolean;
@@ -0,0 +1,111 @@
1
+ import { rerankWithGraph } from "./graph-reranker.js";
2
+ import { applyModeBoost, resolveContextMode } from "./query-planner.js";
3
+ export class HybridRetriever {
4
+ options;
5
+ constructor(options) {
6
+ this.options = options;
7
+ }
8
+ async search(query) {
9
+ return (await this.searchWithDiagnostics(query)).hits;
10
+ }
11
+ async searchWithDiagnostics(query) {
12
+ const limit = query.limit ?? 20;
13
+ const keywordHits = await this.options.graphStore.searchText({ ...query, limit: limit * 2 });
14
+ const semanticResult = await this.searchSemantic({ ...query, limit: limit * 2 });
15
+ const mode = resolveContextMode(query.query, query.mode);
16
+ const fused = fuseHits(keywordHits, semanticResult.hits).map((hit) => applyModeBoost(hit, mode, query.query));
17
+ const reranked = await rerankWithGraph(fused, query, mode, { graphStore: this.options.graphStore });
18
+ const hits = reranked.filter((hit) => hit.score > 0).slice(0, limit);
19
+ return {
20
+ hits,
21
+ diagnostics: {
22
+ keywordHitCount: keywordHits.length,
23
+ semantic: {
24
+ status: semanticResult.error ? "failed" : "ok",
25
+ hitCount: semanticResult.hits.length,
26
+ error: semanticResult.error
27
+ },
28
+ fusion: {
29
+ semanticTopNParticipation: hits.filter(hasSemanticParticipation).length
30
+ }
31
+ }
32
+ };
33
+ }
34
+ async searchSemantic(query) {
35
+ try {
36
+ return { hits: await this.options.semanticStore.search(query, this.options.embeddingProvider) };
37
+ }
38
+ catch (error) {
39
+ return { hits: [], error: error instanceof Error ? error.message : String(error) };
40
+ }
41
+ }
42
+ }
43
+ export function fuseHits(keywordHits, semanticHits) {
44
+ const byChunk = new Map();
45
+ for (const hit of rankFusionHits(keywordHits, "keyword")) {
46
+ mergeHit(byChunk, hit);
47
+ }
48
+ for (const hit of rankFusionHits(semanticHits, "semantic")) {
49
+ mergeHit(byChunk, hit);
50
+ }
51
+ return [...byChunk.values()].sort((a, b) => {
52
+ if (b.score !== a.score)
53
+ return b.score - a.score;
54
+ return a.chunk.filePath.localeCompare(b.chunk.filePath);
55
+ });
56
+ }
57
+ export function hasSemanticParticipation(hit) {
58
+ return hit.source === "semantic" || /\bsemantic\b|vector similarity/i.test(hit.reason);
59
+ }
60
+ // Reciprocal Rank Fusion (Cormack et al. 2009). Fusion depends only on each
61
+ // hit's rank within its own source list, not on raw score magnitude — this is
62
+ // what lets us combine keyword (bm25, ~thousands) and semantic (cosine, 0..1)
63
+ // scores that live on incompatible scales.
64
+ //
65
+ // Textbook RRF is 1/(k+rank), which tops out around 1/k (~0.016 at k=60) —
66
+ // two orders of magnitude below the downstream modeBoost (~0.2-0.65) and
67
+ // graphAdjustment (~0.15-2.5) that get *added* to the fused score later. To
68
+ // keep the fused score on the same ~0..1 scale those boosts were tuned
69
+ // against, we use the order-preserving variant k/(k+rank), which tops out near
70
+ // 1.0. Scaling by a constant is monotonic, so it does not change RRF ranking.
71
+ const RRF_K = 60;
72
+ function rankFusionHits(hits, label) {
73
+ return hits.map((hit, index) => {
74
+ const rank = index + 1;
75
+ const score = RRF_K / (RRF_K + rank);
76
+ return {
77
+ ...hit,
78
+ score,
79
+ scoreBreakdown: {
80
+ ...hit.scoreBreakdown,
81
+ [label]: hit.score,
82
+ sourceNormalized: score,
83
+ final: score
84
+ },
85
+ reason: `${hit.reason}; rank fusion ${label} rank ${rank} rrf=${score.toFixed(3)}`
86
+ };
87
+ });
88
+ }
89
+ function mergeHit(byChunk, hit) {
90
+ const existing = byChunk.get(hit.chunk.id);
91
+ if (!existing) {
92
+ byChunk.set(hit.chunk.id, hit);
93
+ return;
94
+ }
95
+ byChunk.set(hit.chunk.id, {
96
+ ...existing,
97
+ score: existing.score + hit.score,
98
+ scoreBreakdown: {
99
+ ...existing.scoreBreakdown,
100
+ ...hit.scoreBreakdown,
101
+ keyword: existing.scoreBreakdown?.keyword ?? hit.scoreBreakdown?.keyword,
102
+ semantic: existing.scoreBreakdown?.semantic ?? hit.scoreBreakdown?.semantic,
103
+ sourceNormalized: (existing.scoreBreakdown?.sourceNormalized ?? existing.score) + (hit.scoreBreakdown?.sourceNormalized ?? hit.score),
104
+ modeBoost: (existing.scoreBreakdown?.modeBoost ?? 0) + (hit.scoreBreakdown?.modeBoost ?? 0),
105
+ graphAdjustment: (existing.scoreBreakdown?.graphAdjustment ?? 0) + (hit.scoreBreakdown?.graphAdjustment ?? 0),
106
+ final: existing.score + hit.score
107
+ },
108
+ source: existing.source === hit.source ? existing.source : "graph",
109
+ reason: `${existing.reason}; ${hit.reason}`
110
+ });
111
+ }
@@ -0,0 +1,6 @@
1
+ export type EvidencePathKind = "implementation" | "test" | "docs" | "fixture";
2
+ export declare function classifyEvidencePath(filePath: string): EvidencePathKind;
3
+ export declare function isSupportingEvidencePath(filePath: string): boolean;
4
+ export declare function isExplicitSupportingEvidenceQuery(query: string): boolean;
5
+ export declare function isExplicitTestQuery(query: string): boolean;
6
+ export declare function isTestPath(filePath: string): boolean;
@@ -0,0 +1,22 @@
1
+ export function classifyEvidencePath(filePath) {
2
+ const normalized = filePath.replaceAll("\\", "/").toLowerCase();
3
+ if (isTestPath(normalized))
4
+ return "test";
5
+ if (/(^|\/)(__fixtures__|fixtures?|playground|examples?|samples?|demo)(\/|$)/.test(normalized))
6
+ return "fixture";
7
+ if (/(^|\/)(docs?|documentation)(\/|$)|\.mdx?$/.test(normalized))
8
+ return "docs";
9
+ return "implementation";
10
+ }
11
+ export function isSupportingEvidencePath(filePath) {
12
+ return classifyEvidencePath(filePath) !== "implementation";
13
+ }
14
+ export function isExplicitSupportingEvidenceQuery(query) {
15
+ return /\b(test|tests|spec|coverage|regression|doc|docs|documentation|readme|example|examples|sample|samples|fixture|fixtures|playground|demo)\b/i.test(query);
16
+ }
17
+ export function isExplicitTestQuery(query) {
18
+ return /\b(test|tests|spec|coverage|regression)\b/i.test(query);
19
+ }
20
+ export function isTestPath(filePath) {
21
+ return /(^|\/)(__tests__|tests?)(\/|$)|\.(test|spec)\.[jt]sx?$/.test(filePath.replaceAll("\\", "/").toLowerCase());
22
+ }