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,22 @@
|
|
|
1
|
+
import type { CodeChunk, SymbolNode } from "../core/types.js";
|
|
2
|
+
export interface QueryMatchProfile {
|
|
3
|
+
queryTerms: string[];
|
|
4
|
+
queryTermVariants: string[];
|
|
5
|
+
expandedTerms: string[];
|
|
6
|
+
expandedSymbolNames: string[];
|
|
7
|
+
ftsTerms: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface TextMatchScore {
|
|
10
|
+
score: number;
|
|
11
|
+
matchedQueryTerms: number;
|
|
12
|
+
matchedExpandedTerms: number;
|
|
13
|
+
matchedSymbolName?: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildQueryMatchProfile(query: string, symbols?: SymbolNode[]): QueryMatchProfile;
|
|
17
|
+
export declare function scoreChunkText(chunk: CodeChunk, profile: QueryMatchProfile): TextMatchScore | undefined;
|
|
18
|
+
export declare function scoreSymbolText(symbol: SymbolNode, profile: QueryMatchProfile): TextMatchScore | undefined;
|
|
19
|
+
export declare function tokenizeQuery(query: string): string[];
|
|
20
|
+
export declare function splitIdentifier(value: string): string[];
|
|
21
|
+
export declare function normalizeIdentifier(value: string): string;
|
|
22
|
+
export declare function textContainsTerm(text: string, term: string): boolean;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import snowball from "snowball-stemmers";
|
|
2
|
+
const englishStemmer = snowball.newStemmer("english");
|
|
3
|
+
export function buildQueryMatchProfile(query, symbols = []) {
|
|
4
|
+
const queryTerms = tokenizeQuery(query);
|
|
5
|
+
const queryTermVariants = expandTermVariants(queryTerms);
|
|
6
|
+
const expandedSymbolNames = matchingSymbolNames(queryTermVariants, symbols);
|
|
7
|
+
const expandedTerms = [...new Set(expandedSymbolNames.map(normalizeIdentifier).filter(Boolean))];
|
|
8
|
+
return {
|
|
9
|
+
queryTerms,
|
|
10
|
+
queryTermVariants,
|
|
11
|
+
expandedTerms,
|
|
12
|
+
expandedSymbolNames,
|
|
13
|
+
ftsTerms: [...new Set([...queryTerms, ...expandedTerms])]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function scoreChunkText(chunk, profile) {
|
|
17
|
+
const text = searchableChunkText(chunk);
|
|
18
|
+
const matchedQueryTerms = countQueryTermMatches(text, profile.queryTerms);
|
|
19
|
+
const matchedExpandedTerms = countMatches(text, profile.expandedTerms);
|
|
20
|
+
const symbolScore = chunk.symbolName ? scoreSymbolName(chunk.symbolName, profile.queryTermVariants) : undefined;
|
|
21
|
+
if (matchedQueryTerms === 0 && matchedExpandedTerms === 0 && !symbolScore)
|
|
22
|
+
return undefined;
|
|
23
|
+
const queryCoverage = profile.queryTerms.length > 0 ? matchedQueryTerms / profile.queryTerms.length : 0;
|
|
24
|
+
const expansionScore = Math.min(0.45, matchedExpandedTerms * 0.15);
|
|
25
|
+
const symbolBoost = symbolScore ? 0.85 * symbolScore.coverage : 0;
|
|
26
|
+
const reasons = [`Matched ${matchedQueryTerms}/${profile.queryTerms.length} query term(s)`];
|
|
27
|
+
if (symbolScore)
|
|
28
|
+
reasons.push(`symbol expansion matched ${symbolScore.name}`);
|
|
29
|
+
else if (matchedExpandedTerms > 0)
|
|
30
|
+
reasons.push(`matched ${matchedExpandedTerms} expanded symbol term(s)`);
|
|
31
|
+
return {
|
|
32
|
+
score: queryCoverage + expansionScore + symbolBoost,
|
|
33
|
+
matchedQueryTerms,
|
|
34
|
+
matchedExpandedTerms,
|
|
35
|
+
matchedSymbolName: symbolScore?.name,
|
|
36
|
+
reason: reasons.join("; ")
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function scoreSymbolText(symbol, profile) {
|
|
40
|
+
const text = searchableSymbolText(symbol);
|
|
41
|
+
const matchedQueryTerms = countQueryTermMatches(text, profile.queryTerms);
|
|
42
|
+
const matchedExpandedTerms = countMatches(text, profile.expandedTerms);
|
|
43
|
+
const symbolScore = scoreSymbolName(symbol.name, profile.queryTermVariants);
|
|
44
|
+
if (matchedQueryTerms === 0 && matchedExpandedTerms === 0 && !symbolScore)
|
|
45
|
+
return undefined;
|
|
46
|
+
const queryCoverage = profile.queryTerms.length > 0 ? matchedQueryTerms / profile.queryTerms.length : 0;
|
|
47
|
+
const symbolBoost = symbolScore ? 1.2 * symbolScore.coverage : 0;
|
|
48
|
+
const expansionScore = Math.min(0.4, matchedExpandedTerms * 0.15);
|
|
49
|
+
return {
|
|
50
|
+
score: queryCoverage + symbolBoost + expansionScore,
|
|
51
|
+
matchedQueryTerms,
|
|
52
|
+
matchedExpandedTerms,
|
|
53
|
+
matchedSymbolName: symbolScore?.name,
|
|
54
|
+
reason: symbolScore ? `Symbol expansion match: ${symbol.name}` : `Symbol match: ${symbol.name}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function tokenizeQuery(query) {
|
|
58
|
+
const tokens = [];
|
|
59
|
+
for (const rawPart of query.split(/[^a-z0-9_./:-]+/i)) {
|
|
60
|
+
const part = rawPart.trim();
|
|
61
|
+
if (!part)
|
|
62
|
+
continue;
|
|
63
|
+
tokens.push(part.toLowerCase());
|
|
64
|
+
if (isCamelOrPascalIdentifier(part))
|
|
65
|
+
tokens.push(...splitIdentifier(part));
|
|
66
|
+
}
|
|
67
|
+
return [...new Set(tokens)];
|
|
68
|
+
}
|
|
69
|
+
export function splitIdentifier(value) {
|
|
70
|
+
return value
|
|
71
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
72
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
73
|
+
.split(/[^a-z0-9]+/i)
|
|
74
|
+
.map((part) => part.trim().toLowerCase())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
export function normalizeIdentifier(value) {
|
|
78
|
+
return splitIdentifier(value).join("");
|
|
79
|
+
}
|
|
80
|
+
export function textContainsTerm(text, term) {
|
|
81
|
+
if (!term)
|
|
82
|
+
return false;
|
|
83
|
+
if (!/^[a-z0-9]+$/i.test(term))
|
|
84
|
+
return text.includes(term.toLowerCase());
|
|
85
|
+
const escaped = term.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
86
|
+
const pluralPattern = term.endsWith("y")
|
|
87
|
+
? `(?:${escaped}|${term.slice(0, -1).toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}ies)`
|
|
88
|
+
: `${escaped}(?:s|es)?`;
|
|
89
|
+
return new RegExp(`(^|[^a-z0-9])${pluralPattern}([^a-z0-9]|$)`).test(text);
|
|
90
|
+
}
|
|
91
|
+
function isCamelOrPascalIdentifier(value) {
|
|
92
|
+
return !/[._/:-]/.test(value) && /(?:[a-z0-9][A-Z]|[A-Z]{2,}[a-z])/.test(value);
|
|
93
|
+
}
|
|
94
|
+
function searchableChunkText(chunk) {
|
|
95
|
+
return searchableText([chunk.filePath, chunk.symbolName, chunk.content]);
|
|
96
|
+
}
|
|
97
|
+
function searchableSymbolText(symbol) {
|
|
98
|
+
return searchableText([symbol.name, symbol.filePath, symbol.signature]);
|
|
99
|
+
}
|
|
100
|
+
function searchableText(parts) {
|
|
101
|
+
const raw = parts.filter(Boolean).join("\n");
|
|
102
|
+
return `${raw}\n${splitIdentifier(raw).join(" ")}`.toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
function matchingSymbolNames(queryTermVariants, symbols) {
|
|
105
|
+
const queryTerms = new Set(queryTermVariants);
|
|
106
|
+
const matches = [];
|
|
107
|
+
for (const symbol of symbols) {
|
|
108
|
+
if (symbol.kind === "file")
|
|
109
|
+
continue;
|
|
110
|
+
const parts = splitIdentifier(symbol.name);
|
|
111
|
+
if (parts.length < 2)
|
|
112
|
+
continue;
|
|
113
|
+
const variants = parts.map((part) => expandTermVariants([part]));
|
|
114
|
+
const matchedParts = variants.filter((partVariants) => partVariants.some((part) => queryTerms.has(part))).length;
|
|
115
|
+
const requiredParts = Math.min(2, parts.length);
|
|
116
|
+
if (matchedParts < requiredParts)
|
|
117
|
+
continue;
|
|
118
|
+
matches.push({ name: symbol.name, score: matchedParts / parts.length });
|
|
119
|
+
}
|
|
120
|
+
return [...new Map(matches
|
|
121
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
122
|
+
.slice(0, 48)
|
|
123
|
+
.map((match) => [match.name, match.name])).values()];
|
|
124
|
+
}
|
|
125
|
+
function scoreSymbolName(name, queryTermVariants) {
|
|
126
|
+
const queryTerms = new Set(queryTermVariants);
|
|
127
|
+
const parts = splitIdentifier(name);
|
|
128
|
+
if (parts.length < 2)
|
|
129
|
+
return undefined;
|
|
130
|
+
const matchedParts = parts.filter((part) => expandTermVariants([part]).some((variant) => queryTerms.has(variant))).length;
|
|
131
|
+
const requiredParts = Math.min(2, parts.length);
|
|
132
|
+
if (matchedParts < requiredParts)
|
|
133
|
+
return undefined;
|
|
134
|
+
return { name, coverage: matchedParts / parts.length };
|
|
135
|
+
}
|
|
136
|
+
function expandTermVariants(terms) {
|
|
137
|
+
const variants = new Set();
|
|
138
|
+
for (const term of terms) {
|
|
139
|
+
variants.add(term);
|
|
140
|
+
const stem = stemTerm(term);
|
|
141
|
+
if (stem && stem !== term)
|
|
142
|
+
variants.add(stem);
|
|
143
|
+
}
|
|
144
|
+
return [...variants];
|
|
145
|
+
}
|
|
146
|
+
function stemTerm(term) {
|
|
147
|
+
if (!/^[a-z]+$/i.test(term))
|
|
148
|
+
return undefined;
|
|
149
|
+
return englishStemmer.stem(term.toLowerCase());
|
|
150
|
+
}
|
|
151
|
+
function countMatches(text, terms) {
|
|
152
|
+
let count = 0;
|
|
153
|
+
for (const term of terms) {
|
|
154
|
+
if (textContainsTerm(text, term))
|
|
155
|
+
count += 1;
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
function countQueryTermMatches(text, terms) {
|
|
160
|
+
let count = 0;
|
|
161
|
+
for (const term of terms) {
|
|
162
|
+
if (expandTermVariants([term]).some((variant) => textContainsTerm(text, variant)))
|
|
163
|
+
count += 1;
|
|
164
|
+
}
|
|
165
|
+
return count;
|
|
166
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ContextMode, SearchHit } from "../core/types.js";
|
|
2
|
+
export type ResolvedContextMode = Exclude<ContextMode, "auto">;
|
|
3
|
+
export declare function resolveContextMode(query: string, explicit: ContextMode | undefined): ResolvedContextMode;
|
|
4
|
+
export declare function applyModeBoost(hit: SearchHit, mode: ResolvedContextMode, query?: string): SearchHit;
|
|
5
|
+
export declare function nextQueriesForMode(query: string, mode: ResolvedContextMode): string[];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { classifyEvidencePath, isExplicitSupportingEvidenceQuery } from "./path-classification.js";
|
|
2
|
+
const MODE_KEYWORDS = {
|
|
3
|
+
debug: ["error", "bug", "fail", "failure", "stack", "trace", "exception", "crash", "log", "fix"],
|
|
4
|
+
feature: ["add", "create", "implement", "feature", "ui", "api", "route", "flow"],
|
|
5
|
+
refactor: ["refactor", "cleanup", "rename", "move", "simplify", "replace", "migrate"],
|
|
6
|
+
review: ["review", "diff", "risk", "regression", "test", "pr"],
|
|
7
|
+
explain: ["explain", "how", "why", "architecture", "overview", "onboard", "understand"]
|
|
8
|
+
};
|
|
9
|
+
export function resolveContextMode(query, explicit) {
|
|
10
|
+
if (explicit && explicit !== "auto")
|
|
11
|
+
return explicit;
|
|
12
|
+
const queryTokens = new Set(query.toLowerCase().split(/[^a-z0-9]+/i).filter(Boolean));
|
|
13
|
+
let best = { mode: "explain", score: 0 };
|
|
14
|
+
for (const [mode, words] of Object.entries(MODE_KEYWORDS)) {
|
|
15
|
+
const score = words.filter((word) => queryTokens.has(word)).length;
|
|
16
|
+
if (score > best.score)
|
|
17
|
+
best = { mode, score };
|
|
18
|
+
}
|
|
19
|
+
return best.mode;
|
|
20
|
+
}
|
|
21
|
+
export function applyModeBoost(hit, mode, query = "") {
|
|
22
|
+
const text = `${hit.chunk.filePath}\n${hit.chunk.symbolName ?? ""}\n${hit.chunk.content}`.toLowerCase();
|
|
23
|
+
const explicitSupportingQuery = isExplicitSupportingEvidenceQuery(query);
|
|
24
|
+
const evidenceKind = classifyEvidencePath(hit.chunk.filePath);
|
|
25
|
+
let boost = 0;
|
|
26
|
+
if (mode === "debug") {
|
|
27
|
+
boost += containsAny(text, ["error", "catch", "throw", "log", "trace", "exception"]) ? 0.35 : 0;
|
|
28
|
+
}
|
|
29
|
+
else if (mode === "feature") {
|
|
30
|
+
boost += containsAny(text, ["route", "handler", "component", "service", "controller", "store"]) ? 0.3 : 0;
|
|
31
|
+
}
|
|
32
|
+
else if (mode === "refactor") {
|
|
33
|
+
boost += containsAny(text, ["export", "interface", "type", "class", "function"]) ? 0.25 : 0;
|
|
34
|
+
}
|
|
35
|
+
else if (mode === "review") {
|
|
36
|
+
boost += containsAny(text, ["test", "spec", "assert", "expect", "mock"]) ? 0.3 : 0;
|
|
37
|
+
}
|
|
38
|
+
else if (mode === "explain") {
|
|
39
|
+
boost += containsAny(text, ["readme", "architecture", "index", "main", "server"]) ? 0.2 : 0;
|
|
40
|
+
}
|
|
41
|
+
if ((mode === "feature" || mode === "refactor" || mode === "explain") && !explicitSupportingQuery) {
|
|
42
|
+
if (evidenceKind === "implementation") {
|
|
43
|
+
boost += 0.25;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const penalty = evidenceKind === "test" ? 0.65 : 0.55;
|
|
47
|
+
boost -= penalty;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (boost === 0)
|
|
51
|
+
return hit;
|
|
52
|
+
const score = Math.max(0, hit.score + boost);
|
|
53
|
+
return {
|
|
54
|
+
...hit,
|
|
55
|
+
score,
|
|
56
|
+
scoreBreakdown: {
|
|
57
|
+
...hit.scoreBreakdown,
|
|
58
|
+
modeBoost: (hit.scoreBreakdown?.modeBoost ?? 0) + boost,
|
|
59
|
+
final: score
|
|
60
|
+
},
|
|
61
|
+
reason: `${hit.reason}; ${mode} mode boost`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function nextQueriesForMode(query, mode) {
|
|
65
|
+
if (mode === "debug")
|
|
66
|
+
return [`owner chain for ${query}`, `related tests for ${query}`, `error handling around ${query}`];
|
|
67
|
+
if (mode === "feature")
|
|
68
|
+
return [`entry points for ${query}`, `state and API owners for ${query}`, `tests for ${query}`];
|
|
69
|
+
if (mode === "refactor")
|
|
70
|
+
return [`callers of ${query}`, `impact analysis for ${query}`, `public exports for ${query}`];
|
|
71
|
+
if (mode === "review")
|
|
72
|
+
return [`changed files impact`, `related tests`, `risk hotspots`];
|
|
73
|
+
return [`module overview for ${query}`, `key symbols for ${query}`, `main execution flow for ${query}`];
|
|
74
|
+
}
|
|
75
|
+
function containsAny(text, needles) {
|
|
76
|
+
return needles.some((needle) => text.includes(needle));
|
|
77
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EdgeKind, SearchHit } from "../core/types.js";
|
|
2
|
+
import type { ResolvedContextMode } from "./query-planner.js";
|
|
3
|
+
import type { TopologyDistance } from "./topology-distance.js";
|
|
4
|
+
export interface RankingSignalInput {
|
|
5
|
+
hit: SearchHit;
|
|
6
|
+
mode: ResolvedContextMode;
|
|
7
|
+
query: string;
|
|
8
|
+
distance?: TopologyDistance;
|
|
9
|
+
hasStructuralSeeds: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface RankingSignalResult {
|
|
12
|
+
score: number;
|
|
13
|
+
adjustment: number;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function applyRankingSignals(input: RankingSignalInput): RankingSignalResult;
|
|
17
|
+
export declare function graphProximityScore(hops: number): number;
|
|
18
|
+
export declare function edgeKindBoost(kinds: EdgeKind[]): number;
|
|
19
|
+
export declare function edgeKindWeight(kind: EdgeKind): number;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { classifyEvidencePath, isExplicitTestQuery, isTestPath } from "./path-classification.js";
|
|
2
|
+
export function applyRankingSignals(input) {
|
|
3
|
+
const reasons = [];
|
|
4
|
+
let adjustment = 0;
|
|
5
|
+
const distance = primaryDistanceFor(input);
|
|
6
|
+
if (distance) {
|
|
7
|
+
const proximity = graphProximityScore(distance.hops) * 1.6;
|
|
8
|
+
const edgeBoost = edgeKindBoost(distance.edgeKinds);
|
|
9
|
+
adjustment += proximity + edgeBoost;
|
|
10
|
+
reasons.push(graphReason(distance, proximity + edgeBoost));
|
|
11
|
+
}
|
|
12
|
+
else if (input.hasStructuralSeeds && disconnectedPenaltyApplies(input.hit)) {
|
|
13
|
+
const penalty = isTestPath(input.hit.chunk.filePath) ? 0.15 : 0.25;
|
|
14
|
+
adjustment -= penalty;
|
|
15
|
+
reasons.push(`graph disconnected from structural seeds (-${penalty.toFixed(2)})`);
|
|
16
|
+
}
|
|
17
|
+
const testAdjustment = testModeAdjustment(input);
|
|
18
|
+
if (testAdjustment !== 0) {
|
|
19
|
+
adjustment += testAdjustment;
|
|
20
|
+
reasons.push(testAdjustment > 0
|
|
21
|
+
? `test relevance boost (+${testAdjustment.toFixed(2)})`
|
|
22
|
+
: `test default demotion (${testAdjustment.toFixed(2)})`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
score: Math.max(0, input.hit.score + adjustment),
|
|
26
|
+
adjustment,
|
|
27
|
+
reason: reasons.length > 0 ? `graph rerank: ${reasons.join("; ")}` : undefined
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function graphProximityScore(hops) {
|
|
31
|
+
if (hops <= 0)
|
|
32
|
+
return 1;
|
|
33
|
+
if (hops === 1)
|
|
34
|
+
return 0.82;
|
|
35
|
+
if (hops === 2)
|
|
36
|
+
return 0.52;
|
|
37
|
+
if (hops === 3)
|
|
38
|
+
return 0.24;
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
export function edgeKindBoost(kinds) {
|
|
42
|
+
if (kinds.length === 0)
|
|
43
|
+
return 0;
|
|
44
|
+
return Math.max(...kinds.map(edgeKindWeight)) * 0.42;
|
|
45
|
+
}
|
|
46
|
+
export function edgeKindWeight(kind) {
|
|
47
|
+
if (kind === "calls" || kind === "calls_api" || kind === "routes_to" || kind === "handles_event" || kind === "handles_webhook")
|
|
48
|
+
return 1;
|
|
49
|
+
if (kind === "tested_by")
|
|
50
|
+
return 0.9;
|
|
51
|
+
if (kind === "uses_middleware")
|
|
52
|
+
return 0.85;
|
|
53
|
+
if (kind === "imports" || kind === "exports")
|
|
54
|
+
return 0.75;
|
|
55
|
+
if (kind === "reads_from" || kind === "writes_to" || kind === "references")
|
|
56
|
+
return 0.55;
|
|
57
|
+
if (kind === "contains")
|
|
58
|
+
return 0.2;
|
|
59
|
+
return 0.15;
|
|
60
|
+
}
|
|
61
|
+
function graphReason(distance, boost) {
|
|
62
|
+
if (distance.hops === 0) {
|
|
63
|
+
const via = unique(distance.edgeKinds).slice(0, 2).join("/");
|
|
64
|
+
return via ? `structural seed via ${via} (+${boost.toFixed(2)})` : `structural seed (+${boost.toFixed(2)})`;
|
|
65
|
+
}
|
|
66
|
+
const via = unique(distance.edgeKinds).slice(0, 3).join("/");
|
|
67
|
+
return `graph proximity ${distance.hops} hop${distance.hops === 1 ? "" : "s"}${via ? ` via ${via}` : ""} (+${boost.toFixed(2)})`;
|
|
68
|
+
}
|
|
69
|
+
function testModeAdjustment(input) {
|
|
70
|
+
if (!isTestPath(input.hit.chunk.filePath))
|
|
71
|
+
return 0;
|
|
72
|
+
const explicitTestQuery = isExplicitTestQuery(input.query);
|
|
73
|
+
if ((input.mode === "review" || input.mode === "debug" || explicitTestQuery) && input.distance)
|
|
74
|
+
return 0.45;
|
|
75
|
+
if (!explicitTestQuery && input.mode !== "review" && input.mode !== "debug")
|
|
76
|
+
return -0.25;
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
function primaryDistanceFor(input) {
|
|
80
|
+
if (!input.distance)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (!isTestPath(input.hit.chunk.filePath))
|
|
83
|
+
return input.distance;
|
|
84
|
+
if (input.mode === "review" || input.mode === "debug" || isExplicitTestQuery(input.query))
|
|
85
|
+
return input.distance;
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function disconnectedPenaltyApplies(hit) {
|
|
89
|
+
if (classifyEvidencePath(hit.chunk.filePath) !== "implementation")
|
|
90
|
+
return true;
|
|
91
|
+
if (hit.chunk.language === "markdown" || hit.chunk.language === "json")
|
|
92
|
+
return true;
|
|
93
|
+
return hit.source === "semantic" || hit.source === "keyword";
|
|
94
|
+
}
|
|
95
|
+
function unique(values) {
|
|
96
|
+
return [...new Set(values)];
|
|
97
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EdgeKind, GraphEdge, SymbolNode } from "../core/types.js";
|
|
2
|
+
export interface FileNeighbor {
|
|
3
|
+
filePath: string;
|
|
4
|
+
kind: EdgeKind;
|
|
5
|
+
}
|
|
6
|
+
export interface FileGraph {
|
|
7
|
+
adjacency: Map<string, FileNeighbor[]>;
|
|
8
|
+
incidentKinds: Map<string, EdgeKind[]>;
|
|
9
|
+
}
|
|
10
|
+
export interface TopologyDistance {
|
|
11
|
+
hops: number;
|
|
12
|
+
edgeKinds: EdgeKind[];
|
|
13
|
+
path: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface TopologyDistanceOptions {
|
|
16
|
+
projectId?: string;
|
|
17
|
+
maxHops?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function buildFileGraph(symbols: SymbolNode[], edges: GraphEdge[], projectId?: string): FileGraph;
|
|
20
|
+
export declare function computeTopologyDistances(graph: FileGraph, seedFiles: string[], candidateFiles: Iterable<string>, options?: TopologyDistanceOptions): Map<string, TopologyDistance>;
|
|
21
|
+
export declare function graphDegree(graph: FileGraph, filePath: string): number;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export function buildFileGraph(symbols, edges, projectId) {
|
|
2
|
+
const scopedSymbols = projectId ? symbols.filter((symbol) => symbol.projectId === projectId) : symbols;
|
|
3
|
+
const symbolById = new Map(scopedSymbols.map((symbol) => [symbol.id, symbol]));
|
|
4
|
+
const adjacency = new Map();
|
|
5
|
+
const incidentKinds = new Map();
|
|
6
|
+
for (const edge of edges) {
|
|
7
|
+
if (projectId && edge.projectId !== projectId)
|
|
8
|
+
continue;
|
|
9
|
+
const sourceFile = endpointFile(edge, symbolById, "source");
|
|
10
|
+
const targetFile = endpointFile(edge, symbolById, "target");
|
|
11
|
+
if (!sourceFile && !targetFile)
|
|
12
|
+
continue;
|
|
13
|
+
if (sourceFile)
|
|
14
|
+
addIncidentKind(incidentKinds, sourceFile, edge.kind);
|
|
15
|
+
if (targetFile)
|
|
16
|
+
addIncidentKind(incidentKinds, targetFile, edge.kind);
|
|
17
|
+
if (!sourceFile || !targetFile || sourceFile === targetFile)
|
|
18
|
+
continue;
|
|
19
|
+
addNeighbor(adjacency, sourceFile, { filePath: targetFile, kind: edge.kind });
|
|
20
|
+
addNeighbor(adjacency, targetFile, { filePath: sourceFile, kind: edge.kind });
|
|
21
|
+
}
|
|
22
|
+
return { adjacency, incidentKinds };
|
|
23
|
+
}
|
|
24
|
+
export function computeTopologyDistances(graph, seedFiles, candidateFiles, options = {}) {
|
|
25
|
+
const maxHops = options.maxHops ?? 3;
|
|
26
|
+
const candidates = new Set(candidateFiles);
|
|
27
|
+
const distances = new Map();
|
|
28
|
+
const visited = new Map();
|
|
29
|
+
const queue = [];
|
|
30
|
+
for (const filePath of unique(seedFiles)) {
|
|
31
|
+
if (!graph.incidentKinds.has(filePath) && !graph.adjacency.has(filePath))
|
|
32
|
+
continue;
|
|
33
|
+
const seedDistance = {
|
|
34
|
+
hops: 0,
|
|
35
|
+
edgeKinds: strongestKinds(graph.incidentKinds.get(filePath) ?? []),
|
|
36
|
+
path: [filePath]
|
|
37
|
+
};
|
|
38
|
+
visited.set(filePath, seedDistance);
|
|
39
|
+
queue.push(seedDistance);
|
|
40
|
+
if (candidates.has(filePath))
|
|
41
|
+
distances.set(filePath, seedDistance);
|
|
42
|
+
}
|
|
43
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
44
|
+
const current = queue[cursor];
|
|
45
|
+
const currentFile = current.path[current.path.length - 1];
|
|
46
|
+
if (!currentFile || current.hops >= maxHops)
|
|
47
|
+
continue;
|
|
48
|
+
for (const neighbor of graph.adjacency.get(currentFile) ?? []) {
|
|
49
|
+
const nextDistance = {
|
|
50
|
+
hops: current.hops + 1,
|
|
51
|
+
edgeKinds: [...current.edgeKinds, neighbor.kind],
|
|
52
|
+
path: [...current.path, neighbor.filePath]
|
|
53
|
+
};
|
|
54
|
+
const existing = visited.get(neighbor.filePath);
|
|
55
|
+
if (existing && compareDistance(existing, nextDistance) <= 0)
|
|
56
|
+
continue;
|
|
57
|
+
visited.set(neighbor.filePath, nextDistance);
|
|
58
|
+
queue.push(nextDistance);
|
|
59
|
+
if (candidates.has(neighbor.filePath))
|
|
60
|
+
distances.set(neighbor.filePath, nextDistance);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return distances;
|
|
64
|
+
}
|
|
65
|
+
export function graphDegree(graph, filePath) {
|
|
66
|
+
return (graph.adjacency.get(filePath)?.length ?? 0) + (graph.incidentKinds.get(filePath)?.length ?? 0);
|
|
67
|
+
}
|
|
68
|
+
function endpointFile(edge, symbolById, endpoint) {
|
|
69
|
+
const symbol = symbolById.get(endpoint === "source" ? edge.sourceId : edge.targetId);
|
|
70
|
+
if (symbol)
|
|
71
|
+
return symbol.filePath;
|
|
72
|
+
const metadataKey = endpoint === "source" ? "sourceFile" : "targetFile";
|
|
73
|
+
const metadataValue = edge.metadata?.[metadataKey];
|
|
74
|
+
return typeof metadataValue === "string" ? metadataValue : undefined;
|
|
75
|
+
}
|
|
76
|
+
function addNeighbor(adjacency, from, neighbor) {
|
|
77
|
+
const neighbors = adjacency.get(from) ?? [];
|
|
78
|
+
if (!neighbors.some((existing) => existing.filePath === neighbor.filePath && existing.kind === neighbor.kind)) {
|
|
79
|
+
neighbors.push(neighbor);
|
|
80
|
+
adjacency.set(from, neighbors);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function addIncidentKind(incidentKinds, filePath, kind) {
|
|
84
|
+
const kinds = incidentKinds.get(filePath) ?? [];
|
|
85
|
+
if (!kinds.includes(kind)) {
|
|
86
|
+
kinds.push(kind);
|
|
87
|
+
incidentKinds.set(filePath, kinds);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function compareDistance(a, b) {
|
|
91
|
+
if (a.hops !== b.hops)
|
|
92
|
+
return a.hops - b.hops;
|
|
93
|
+
return maxEdgeWeight(b.edgeKinds) - maxEdgeWeight(a.edgeKinds);
|
|
94
|
+
}
|
|
95
|
+
function maxEdgeWeight(kinds) {
|
|
96
|
+
return Math.max(0, ...kinds.map(edgeKindStrength));
|
|
97
|
+
}
|
|
98
|
+
function strongestKinds(kinds) {
|
|
99
|
+
return [...kinds].sort((a, b) => edgeKindStrength(b) - edgeKindStrength(a)).slice(0, 3);
|
|
100
|
+
}
|
|
101
|
+
function edgeKindStrength(kind) {
|
|
102
|
+
if (kind === "calls" || kind === "calls_api" || kind === "routes_to" || kind === "handles_event" || kind === "handles_webhook")
|
|
103
|
+
return 1;
|
|
104
|
+
if (kind === "uses_middleware")
|
|
105
|
+
return 0.9;
|
|
106
|
+
if (kind === "imports" || kind === "exports" || kind === "tested_by")
|
|
107
|
+
return 0.8;
|
|
108
|
+
if (kind === "reads_from" || kind === "writes_to" || kind === "references")
|
|
109
|
+
return 0.6;
|
|
110
|
+
if (kind === "contains")
|
|
111
|
+
return 0.25;
|
|
112
|
+
return 0.2;
|
|
113
|
+
}
|
|
114
|
+
function unique(values) {
|
|
115
|
+
return [...new Set(values)];
|
|
116
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CodeChunk, GraphEdge, OwnerCandidate, ReuseCandidateReport, SearchHit, SymbolNode } from "../core/types.js";
|
|
2
|
+
export interface ReuseDetectorInput {
|
|
3
|
+
query: string;
|
|
4
|
+
hits: SearchHit[];
|
|
5
|
+
owners: OwnerCandidate[];
|
|
6
|
+
symbols: SymbolNode[];
|
|
7
|
+
edges: GraphEdge[];
|
|
8
|
+
chunks: CodeChunk[];
|
|
9
|
+
limit?: number;
|
|
10
|
+
reuseGuard?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildReuseCandidateReport(input: ReuseDetectorInput): ReuseCandidateReport;
|