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.
- package/LICENSE +21 -0
- package/README.md +366 -0
- package/README.zh-CN.md +363 -0
- package/dist/src/cli/configure/app.d.ts +6 -0
- package/dist/src/cli/configure/app.js +81 -0
- package/dist/src/cli/configure/run.d.ts +5 -0
- package/dist/src/cli/configure/run.js +85 -0
- package/dist/src/cli/configure/state.d.ts +42 -0
- package/dist/src/cli/configure/state.js +174 -0
- package/dist/src/cli/configure.d.ts +31 -0
- package/dist/src/cli/configure.js +101 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +503 -0
- package/dist/src/cli/tui/index-progress.d.ts +12 -0
- package/dist/src/cli/tui/index-progress.js +49 -0
- package/dist/src/cli/tui/watch-status.d.ts +10 -0
- package/dist/src/cli/tui/watch-status.js +27 -0
- package/dist/src/cli/update.d.ts +18 -0
- package/dist/src/cli/update.js +111 -0
- package/dist/src/config/dotenv.d.ts +1 -0
- package/dist/src/config/dotenv.js +14 -0
- package/dist/src/config/graph-runtime.d.ts +13 -0
- package/dist/src/config/graph-runtime.js +29 -0
- package/dist/src/config/runtime-config.d.ts +87 -0
- package/dist/src/config/runtime-config.js +215 -0
- package/dist/src/config/semantic-runtime.d.ts +24 -0
- package/dist/src/config/semantic-runtime.js +89 -0
- package/dist/src/context/context-builder.d.ts +20 -0
- package/dist/src/context/context-builder.js +277 -0
- package/dist/src/context/expansion-policy.d.ts +6 -0
- package/dist/src/context/expansion-policy.js +49 -0
- package/dist/src/context/skeletonizer.d.ts +2 -0
- package/dist/src/context/skeletonizer.js +79 -0
- package/dist/src/context/snippet-renderer.d.ts +2 -0
- package/dist/src/context/snippet-renderer.js +67 -0
- package/dist/src/core/contracts.d.ts +74 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/engine.d.ts +64 -0
- package/dist/src/core/engine.js +442 -0
- package/dist/src/core/types.d.ts +490 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/diagnostics/doctor.d.ts +66 -0
- package/dist/src/diagnostics/doctor.js +193 -0
- package/dist/src/diagnostics/embedding-test.d.ts +24 -0
- package/dist/src/diagnostics/embedding-test.js +83 -0
- package/dist/src/graph/diff-files.d.ts +1 -0
- package/dist/src/graph/diff-files.js +14 -0
- package/dist/src/graph/impact-report.d.ts +10 -0
- package/dist/src/graph/impact-report.js +173 -0
- package/dist/src/graph/in-memory-graph-store.d.ts +36 -0
- package/dist/src/graph/in-memory-graph-store.js +395 -0
- package/dist/src/graph/owner-ranking.d.ts +2 -0
- package/dist/src/graph/owner-ranking.js +41 -0
- package/dist/src/graph/sqlite-graph-store.d.ts +51 -0
- package/dist/src/graph/sqlite-graph-store.js +724 -0
- package/dist/src/graph/sqlite-statements.d.ts +36 -0
- package/dist/src/graph/sqlite-statements.js +105 -0
- package/dist/src/graph/target-matcher.d.ts +13 -0
- package/dist/src/graph/target-matcher.js +64 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.js +32 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.d.ts +6 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.js +45 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.js +87 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.js +88 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/registry.d.ts +5 -0
- package/dist/src/indexing/analyzers/registry.js +23 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.d.ts +30 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.js +163 -0
- package/dist/src/indexing/analyzers/types.d.ts +17 -0
- package/dist/src/indexing/analyzers/types.js +1 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.d.ts +5 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.js +199 -0
- package/dist/src/indexing/ast-analyzer.d.ts +11 -0
- package/dist/src/indexing/ast-analyzer.js +11 -0
- package/dist/src/indexing/chunker.d.ts +11 -0
- package/dist/src/indexing/chunker.js +157 -0
- package/dist/src/indexing/ignore-policy.d.ts +6 -0
- package/dist/src/indexing/ignore-policy.js +40 -0
- package/dist/src/indexing/indexer.d.ts +13 -0
- package/dist/src/indexing/indexer.js +189 -0
- package/dist/src/indexing/language.d.ts +3 -0
- package/dist/src/indexing/language.js +24 -0
- package/dist/src/indexing/scanner.d.ts +13 -0
- package/dist/src/indexing/scanner.js +87 -0
- package/dist/src/lsp/definition-resolver.d.ts +6 -0
- package/dist/src/lsp/definition-resolver.js +60 -0
- package/dist/src/lsp/typescript-language-service.d.ts +21 -0
- package/dist/src/lsp/typescript-language-service.js +82 -0
- package/dist/src/mcp/server.d.ts +11 -0
- package/dist/src/mcp/server.js +64 -0
- package/dist/src/mcp/tools.d.ts +266 -0
- package/dist/src/mcp/tools.js +309 -0
- package/dist/src/project/project-identity.d.ts +2 -0
- package/dist/src/project/project-identity.js +24 -0
- package/dist/src/project/project-registry.d.ts +12 -0
- package/dist/src/project/project-registry.js +49 -0
- package/dist/src/project/workspace-resolver.d.ts +20 -0
- package/dist/src/project/workspace-resolver.js +62 -0
- package/dist/src/retrieval/graph-reranker.d.ts +11 -0
- package/dist/src/retrieval/graph-reranker.js +0 -0
- package/dist/src/retrieval/hybrid-retriever.d.ts +31 -0
- package/dist/src/retrieval/hybrid-retriever.js +111 -0
- package/dist/src/retrieval/path-classification.d.ts +6 -0
- package/dist/src/retrieval/path-classification.js +22 -0
- package/dist/src/retrieval/query-matching.d.ts +22 -0
- package/dist/src/retrieval/query-matching.js +166 -0
- package/dist/src/retrieval/query-planner.d.ts +5 -0
- package/dist/src/retrieval/query-planner.js +77 -0
- package/dist/src/retrieval/ranking-signals.d.ts +19 -0
- package/dist/src/retrieval/ranking-signals.js +97 -0
- package/dist/src/retrieval/topology-distance.d.ts +21 -0
- package/dist/src/retrieval/topology-distance.js +116 -0
- package/dist/src/reuse/reuse-detector.d.ts +12 -0
- package/dist/src/reuse/reuse-detector.js +564 -0
- package/dist/src/semantic/deterministic-embedding.d.ts +7 -0
- package/dist/src/semantic/deterministic-embedding.js +31 -0
- package/dist/src/semantic/in-memory-semantic-store.d.ts +11 -0
- package/dist/src/semantic/in-memory-semantic-store.js +65 -0
- package/dist/src/semantic/lance-semantic-store.d.ts +131 -0
- package/dist/src/semantic/lance-semantic-store.js +623 -0
- package/dist/src/semantic/openai-compatible-embedding.d.ts +19 -0
- package/dist/src/semantic/openai-compatible-embedding.js +75 -0
- package/dist/src/service/service-identity.d.ts +13 -0
- package/dist/src/service/service-identity.js +48 -0
- package/dist/src/service/service-manager.d.ts +29 -0
- package/dist/src/service/service-manager.js +231 -0
- package/dist/src/service/service-templates.d.ts +22 -0
- package/dist/src/service/service-templates.js +101 -0
- package/dist/src/subgraph/impact-explainer.d.ts +2 -0
- package/dist/src/subgraph/impact-explainer.js +54 -0
- package/dist/src/subgraph/node-expander.d.ts +13 -0
- package/dist/src/subgraph/node-expander.js +139 -0
- package/dist/src/subgraph/output-preset.d.ts +3 -0
- package/dist/src/subgraph/output-preset.js +102 -0
- package/dist/src/subgraph/subgraph-builder.d.ts +17 -0
- package/dist/src/subgraph/subgraph-builder.js +688 -0
- package/dist/src/topology/export-index.d.ts +7 -0
- package/dist/src/topology/export-index.js +14 -0
- package/dist/src/topology/framework-topology.d.ts +3 -0
- package/dist/src/topology/framework-topology.js +460 -0
- package/dist/src/topology/import-resolver.d.ts +2 -0
- package/dist/src/topology/import-resolver.js +29 -0
- package/dist/src/topology/orm-topology.d.ts +3 -0
- package/dist/src/topology/orm-topology.js +200 -0
- package/dist/src/topology/runtime-topology.d.ts +3 -0
- package/dist/src/topology/runtime-topology.js +204 -0
- package/dist/src/topology/symbol-resolver.d.ts +6 -0
- package/dist/src/topology/symbol-resolver.js +74 -0
- package/dist/src/topology/test-topology.d.ts +2 -0
- package/dist/src/topology/test-topology.js +82 -0
- package/dist/src/utils/hash.d.ts +2 -0
- package/dist/src/utils/hash.js +7 -0
- package/dist/src/utils/path.d.ts +2 -0
- package/dist/src/utils/path.js +7 -0
- package/dist/src/watch/event-journal.d.ts +17 -0
- package/dist/src/watch/event-journal.js +81 -0
- package/dist/src/watch/file-event-coalescer.d.ts +9 -0
- package/dist/src/watch/file-event-coalescer.js +39 -0
- package/dist/src/watch/index-scheduler.d.ts +52 -0
- package/dist/src/watch/index-scheduler.js +190 -0
- package/dist/src/watch/watch-daemon.d.ts +73 -0
- package/dist/src/watch/watch-daemon.js +368 -0
- package/dist/src/watch/watcher-liveness.d.ts +47 -0
- package/dist/src/watch/watcher-liveness.js +168 -0
- package/dist/src/web/server.d.ts +1 -0
- package/dist/src/web/server.js +375 -0
- 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,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[]>;
|
|
Binary file
|
|
@@ -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
|
+
}
|