@danielblomma/cortex-mcp 0.4.2 → 0.6.4
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/README.md +64 -16
- package/bin/cortex.mjs +32 -60
- package/package.json +17 -3
- package/scaffold/.context/ontology.cypher +47 -0
- package/scaffold/.githooks/post-commit +14 -0
- package/scaffold/.githooks/post-rewrite +23 -0
- package/scaffold/mcp/package-lock.json +19 -23
- package/scaffold/mcp/package.json +3 -1
- package/scaffold/mcp/src/contextEntities.ts +311 -0
- package/scaffold/mcp/src/defaults.ts +6 -0
- package/scaffold/mcp/src/embed.ts +163 -37
- package/scaffold/mcp/src/frontmatter.ts +39 -0
- package/scaffold/mcp/src/graph.ts +330 -109
- package/scaffold/mcp/src/graphMetrics.ts +12 -0
- package/scaffold/mcp/src/impactPresentation.ts +202 -0
- package/scaffold/mcp/src/impactRanking.ts +237 -0
- package/scaffold/mcp/src/impactResponse.ts +47 -0
- package/scaffold/mcp/src/impactResults.ts +173 -0
- package/scaffold/mcp/src/impactSeed.ts +33 -0
- package/scaffold/mcp/src/impactTraversal.ts +83 -0
- package/scaffold/mcp/src/jsonl.ts +34 -0
- package/scaffold/mcp/src/loadGraph.ts +345 -86
- package/scaffold/mcp/src/paths.ts +24 -2
- package/scaffold/mcp/src/presets.ts +137 -0
- package/scaffold/mcp/src/relatedResponse.ts +30 -0
- package/scaffold/mcp/src/relatedTraversal.ts +101 -0
- package/scaffold/mcp/src/rules.ts +27 -0
- package/scaffold/mcp/src/search.ts +191 -355
- package/scaffold/mcp/src/searchCore.ts +274 -0
- package/scaffold/mcp/src/searchResults.ts +133 -0
- package/scaffold/mcp/src/server.ts +95 -3
- package/scaffold/mcp/src/types.ts +99 -3
- package/scaffold/scripts/context.sh +12 -46
- package/scaffold/scripts/dashboard.mjs +797 -0
- package/scaffold/scripts/dashboard.sh +13 -0
- package/scaffold/scripts/ingest.mjs +2219 -59
- package/scaffold/scripts/install-git-hooks.sh +3 -1
- package/scaffold/scripts/memory-compile.mjs +232 -0
- package/scaffold/scripts/memory-compile.sh +20 -0
- package/scaffold/scripts/memory-lint.mjs +375 -0
- package/scaffold/scripts/memory-lint.sh +20 -0
- package/scaffold/scripts/parsers/config.mjs +178 -0
- package/scaffold/scripts/parsers/cpp.mjs +316 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
- package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
- package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
- package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
- package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
- package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
- package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
- package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
- package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
- package/scaffold/scripts/parsers/javascript.mjs +27 -350
- package/scaffold/scripts/parsers/resources.mjs +166 -0
- package/scaffold/scripts/parsers/sql.mjs +137 -0
- package/scaffold/scripts/parsers/vbnet.mjs +143 -0
- package/scaffold/scripts/status.sh +15 -8
- package/scaffold/scripts/capture-note.sh +0 -55
- package/scaffold/scripts/plan-state-engine.cjs +0 -310
- package/scaffold/scripts/plan-state.sh +0 -71
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { SearchEntity } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const SQL_ENTITY_KINDS = new Set(["procedure", "view", "function", "table", "trigger"]);
|
|
4
|
+
const SQL_LIKE_EXTENSIONS = [".sql"];
|
|
5
|
+
const CONFIG_LIKE_EXTENSIONS = [".config"];
|
|
6
|
+
const RESOURCE_LIKE_EXTENSIONS = [".resx"];
|
|
7
|
+
const SETTINGS_LIKE_EXTENSIONS = [".settings"];
|
|
8
|
+
const CONFIG_ENVIRONMENT_TOKENS = [
|
|
9
|
+
"release",
|
|
10
|
+
"debug",
|
|
11
|
+
"prod",
|
|
12
|
+
"production",
|
|
13
|
+
"staging",
|
|
14
|
+
"stage",
|
|
15
|
+
"dev",
|
|
16
|
+
"development",
|
|
17
|
+
"test",
|
|
18
|
+
"qa",
|
|
19
|
+
"uat"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const QUERY_TOKEN_EXPANSIONS: Record<string, string[]> = {
|
|
23
|
+
semantisk: ["semantic"],
|
|
24
|
+
sökning: ["search"],
|
|
25
|
+
sokning: ["search"],
|
|
26
|
+
regel: ["rule"],
|
|
27
|
+
regler: ["rules"],
|
|
28
|
+
relaterad: ["related"],
|
|
29
|
+
meddelande: ["message"],
|
|
30
|
+
avvikelse: ["deviation"]
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function normalizeText(value: string): string {
|
|
34
|
+
return value.normalize("NFKC").toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function tokenize(value: string): string[] {
|
|
38
|
+
return normalizeText(value)
|
|
39
|
+
.split(/[^\p{L}\p{N}]+/gu)
|
|
40
|
+
.map((part) => part.trim())
|
|
41
|
+
.filter((part) => part.length >= 2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function expandQueryTokens(tokens: string[]): string[] {
|
|
45
|
+
const expanded = new Set<string>(tokens);
|
|
46
|
+
for (const token of tokens) {
|
|
47
|
+
const aliases = QUERY_TOKEN_EXPANSIONS[token];
|
|
48
|
+
if (!aliases) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const alias of aliases) {
|
|
52
|
+
expanded.add(alias);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Array.from(expanded);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function daysSince(isoDate: string): number {
|
|
59
|
+
const timestamp = Date.parse(isoDate);
|
|
60
|
+
if (Number.isNaN(timestamp)) {
|
|
61
|
+
return 3650;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
return Math.max(0, (now - timestamp) / (1000 * 60 * 60 * 24));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function recencyScore(isoDate: string): number {
|
|
69
|
+
const days = daysSince(isoDate);
|
|
70
|
+
return 1 / (1 + days / 30);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function semanticScore(queryTokens: string[], queryPhrase: string, text: string): number {
|
|
74
|
+
if (queryTokens.length === 0) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const textTokenSet = new Set(tokenize(text));
|
|
79
|
+
if (textTokenSet.size === 0) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let matched = 0;
|
|
84
|
+
for (const token of queryTokens) {
|
|
85
|
+
if (textTokenSet.has(token)) {
|
|
86
|
+
matched += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const overlap = matched / queryTokens.length;
|
|
91
|
+
if (overlap <= 0) {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const normalizedText = normalizeText(text);
|
|
96
|
+
const phraseBonus = queryPhrase && normalizedText.includes(queryPhrase) ? 0.15 : 0;
|
|
97
|
+
return Math.min(1, overlap * 0.85 + phraseBonus);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function queryHasAnyToken(queryTokens: string[], candidates: string[]): boolean {
|
|
101
|
+
return candidates.some((candidate) => queryTokens.includes(candidate));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pathHasExtension(pathValue: string, extensions: string[]): boolean {
|
|
105
|
+
const normalizedPath = normalizeText(pathValue);
|
|
106
|
+
return extensions.some((extension) => normalizedPath.endsWith(extension));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function legacyDataAccessBoost(entity: SearchEntity, queryTokens: string[], queryPhrase: string): number {
|
|
110
|
+
const normalizedKind = normalizeText(entity.kind);
|
|
111
|
+
const wantsSql =
|
|
112
|
+
queryHasAnyToken(queryTokens, [
|
|
113
|
+
"sql",
|
|
114
|
+
"database",
|
|
115
|
+
"db",
|
|
116
|
+
"provider",
|
|
117
|
+
"providername",
|
|
118
|
+
"sqlclient",
|
|
119
|
+
"sqlserver",
|
|
120
|
+
"oracle",
|
|
121
|
+
"postgres",
|
|
122
|
+
"postgresql",
|
|
123
|
+
"pgsql",
|
|
124
|
+
"mysql",
|
|
125
|
+
"sqlite",
|
|
126
|
+
"stored",
|
|
127
|
+
"procedure",
|
|
128
|
+
"proc",
|
|
129
|
+
"query",
|
|
130
|
+
"queries",
|
|
131
|
+
"view",
|
|
132
|
+
"table",
|
|
133
|
+
"trigger",
|
|
134
|
+
"report",
|
|
135
|
+
"reporting",
|
|
136
|
+
"data",
|
|
137
|
+
"dataflow"
|
|
138
|
+
]) || queryPhrase.includes("stored procedure");
|
|
139
|
+
const wantsConfig =
|
|
140
|
+
queryHasAnyToken(queryTokens, [
|
|
141
|
+
"config",
|
|
142
|
+
"configuration",
|
|
143
|
+
"connection",
|
|
144
|
+
"connectionstring",
|
|
145
|
+
"connectionstrings",
|
|
146
|
+
"appsettings",
|
|
147
|
+
"setting",
|
|
148
|
+
"settings"
|
|
149
|
+
]) || queryPhrase.includes("connection string");
|
|
150
|
+
const wantsResource = queryHasAnyToken(queryTokens, ["resource", "resources", "resx"]);
|
|
151
|
+
const wantsSettings = queryHasAnyToken(queryTokens, ["setting", "settings", "appsettings"]);
|
|
152
|
+
const wantsConfigTransform =
|
|
153
|
+
queryHasAnyToken(queryTokens, [...CONFIG_ENVIRONMENT_TOKENS, "transform", "xdt", "override"]) ||
|
|
154
|
+
queryPhrase.includes("web.release.config") ||
|
|
155
|
+
queryPhrase.includes("web.debug.config");
|
|
156
|
+
const wantsMachineConfig = queryHasAnyToken(queryTokens, ["machine", "machineconfig"]);
|
|
157
|
+
const wantsImpact = queryHasAnyToken(queryTokens, [
|
|
158
|
+
"impact",
|
|
159
|
+
"affect",
|
|
160
|
+
"affected",
|
|
161
|
+
"affects",
|
|
162
|
+
"change",
|
|
163
|
+
"changes",
|
|
164
|
+
"changing",
|
|
165
|
+
"override",
|
|
166
|
+
"overrides"
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
let boost = 0;
|
|
170
|
+
|
|
171
|
+
if (entity.entity_type === "Chunk") {
|
|
172
|
+
if (normalizedKind === "connection_string" && (wantsConfig || wantsSql)) {
|
|
173
|
+
boost += 0.16;
|
|
174
|
+
} else if (
|
|
175
|
+
normalizedKind === "database_target" &&
|
|
176
|
+
(wantsConfig ||
|
|
177
|
+
wantsSql ||
|
|
178
|
+
queryHasAnyToken(queryTokens, [
|
|
179
|
+
"database",
|
|
180
|
+
"server",
|
|
181
|
+
"catalog",
|
|
182
|
+
"provider",
|
|
183
|
+
"providername",
|
|
184
|
+
"sqlclient",
|
|
185
|
+
"sqlserver",
|
|
186
|
+
"oracle",
|
|
187
|
+
"postgres",
|
|
188
|
+
"postgresql",
|
|
189
|
+
"pgsql",
|
|
190
|
+
"mysql",
|
|
191
|
+
"sqlite"
|
|
192
|
+
]))
|
|
193
|
+
) {
|
|
194
|
+
boost += 0.18;
|
|
195
|
+
} else if (normalizedKind === "app_setting" && (wantsConfig || wantsSettings)) {
|
|
196
|
+
boost += 0.12;
|
|
197
|
+
} else if (normalizedKind === "resource_entry" && (wantsResource || wantsSql)) {
|
|
198
|
+
boost += 0.1;
|
|
199
|
+
} else if (normalizedKind === "setting_entry" && (wantsSettings || wantsConfig || wantsSql)) {
|
|
200
|
+
boost += 0.1;
|
|
201
|
+
} else if (SQL_ENTITY_KINDS.has(normalizedKind) && wantsSql) {
|
|
202
|
+
boost += 0.12;
|
|
203
|
+
}
|
|
204
|
+
if (
|
|
205
|
+
wantsImpact &&
|
|
206
|
+
(normalizedKind === "connection_string" ||
|
|
207
|
+
normalizedKind === "database_target" ||
|
|
208
|
+
normalizedKind === "app_setting" ||
|
|
209
|
+
SQL_ENTITY_KINDS.has(normalizedKind))
|
|
210
|
+
) {
|
|
211
|
+
boost += 0.08;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (entity.entity_type === "File") {
|
|
216
|
+
if (pathHasExtension(entity.path, SQL_LIKE_EXTENSIONS) && wantsSql) {
|
|
217
|
+
boost += 0.04;
|
|
218
|
+
}
|
|
219
|
+
if (pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) && wantsConfig) {
|
|
220
|
+
boost += 0.06;
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) &&
|
|
224
|
+
wantsConfigTransform &&
|
|
225
|
+
CONFIG_ENVIRONMENT_TOKENS.some((token) => normalizeText(entity.path).includes(`.${token}.config`))
|
|
226
|
+
) {
|
|
227
|
+
boost += 0.12;
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) &&
|
|
231
|
+
wantsMachineConfig &&
|
|
232
|
+
normalizeText(entity.path).endsWith("machine.config")
|
|
233
|
+
) {
|
|
234
|
+
boost += 0.12;
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
pathHasExtension(entity.path, CONFIG_LIKE_EXTENSIONS) &&
|
|
238
|
+
wantsImpact &&
|
|
239
|
+
(wantsConfig || wantsConfigTransform || wantsSql)
|
|
240
|
+
) {
|
|
241
|
+
boost += 0.08;
|
|
242
|
+
}
|
|
243
|
+
if (pathHasExtension(entity.path, RESOURCE_LIKE_EXTENSIONS) && (wantsResource || wantsSql)) {
|
|
244
|
+
boost += 0.05;
|
|
245
|
+
}
|
|
246
|
+
if (pathHasExtension(entity.path, SETTINGS_LIKE_EXTENSIONS) && (wantsSettings || wantsConfig)) {
|
|
247
|
+
boost += 0.05;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return boost;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
255
|
+
if (a.length === 0 || b.length === 0 || a.length !== b.length) {
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let dot = 0;
|
|
260
|
+
let normA = 0;
|
|
261
|
+
let normB = 0;
|
|
262
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
263
|
+
const av = a[index];
|
|
264
|
+
const bv = b[index];
|
|
265
|
+
dot += av * bv;
|
|
266
|
+
normA += av * av;
|
|
267
|
+
normB += bv * bv;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (normA === 0 || normB === 0) {
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
274
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { RankingWeights, SearchEntity } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function isWindowChunkId(id: string): boolean {
|
|
4
|
+
return id.includes(":window:");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function baseChunkId(id: string): string {
|
|
8
|
+
const markerIndex = id.indexOf(":window:");
|
|
9
|
+
return markerIndex === -1 ? id : id.slice(0, markerIndex);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function baseChunkLabel(label: string): string {
|
|
13
|
+
const markerIndex = label.indexOf("#window");
|
|
14
|
+
return markerIndex === -1 ? label : label.slice(0, markerIndex);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildSearchResults(params: {
|
|
18
|
+
candidates: SearchEntity[];
|
|
19
|
+
degreeByEntity: Map<string, number>;
|
|
20
|
+
queryTokens: string[];
|
|
21
|
+
queryPhrase: string;
|
|
22
|
+
ranking: RankingWeights;
|
|
23
|
+
includeScores: boolean;
|
|
24
|
+
includeMatchedRules: boolean;
|
|
25
|
+
includeContent: boolean;
|
|
26
|
+
queryVector: number[] | null;
|
|
27
|
+
embeddingVectors: Map<string, number[]>;
|
|
28
|
+
topK: number;
|
|
29
|
+
minLexicalRelevance: number;
|
|
30
|
+
minVectorRelevance: number;
|
|
31
|
+
semanticScorer: (queryTokens: string[], queryPhrase: string, text: string) => number;
|
|
32
|
+
vectorScorer: (a: number[], b: number[]) => number;
|
|
33
|
+
recencyScorer: (isoDate: string) => number;
|
|
34
|
+
legacyDataAccessBooster: (entity: SearchEntity, queryTokens: string[], queryPhrase: string) => number;
|
|
35
|
+
}): Record<string, unknown>[] {
|
|
36
|
+
const rawResults = params.candidates
|
|
37
|
+
.map((entity) => {
|
|
38
|
+
const lexicalSemantic = params.semanticScorer(params.queryTokens, params.queryPhrase, entity.text);
|
|
39
|
+
const entityVector = params.embeddingVectors.get(entity.id);
|
|
40
|
+
const vectorSemantic =
|
|
41
|
+
params.queryVector && entityVector
|
|
42
|
+
? Math.max(0, Math.min(1, params.vectorScorer(params.queryVector, entityVector)))
|
|
43
|
+
: 0;
|
|
44
|
+
const hasRelevanceSignal =
|
|
45
|
+
lexicalSemantic >= params.minLexicalRelevance || vectorSemantic >= params.minVectorRelevance;
|
|
46
|
+
if (!hasRelevanceSignal) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const semantic =
|
|
51
|
+
vectorSemantic > 0 ? vectorSemantic * 0.75 + lexicalSemantic * 0.25 : lexicalSemantic;
|
|
52
|
+
const graphScore = Math.min(1, (params.degreeByEntity.get(entity.id) ?? 0) / 4);
|
|
53
|
+
const trustScore = Math.max(0, Math.min(1, entity.trust_level / 100));
|
|
54
|
+
const dateScore = params.recencyScorer(entity.updated_at);
|
|
55
|
+
|
|
56
|
+
let score = 0;
|
|
57
|
+
score += params.ranking.semantic * semantic;
|
|
58
|
+
score += params.ranking.graph * graphScore;
|
|
59
|
+
score += params.ranking.trust * trustScore;
|
|
60
|
+
score += params.ranking.recency * dateScore;
|
|
61
|
+
score += params.legacyDataAccessBooster(entity, params.queryTokens, params.queryPhrase);
|
|
62
|
+
|
|
63
|
+
if (entity.source_of_truth) {
|
|
64
|
+
score += 0.1 * semantic;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
id: entity.id,
|
|
69
|
+
entity_type: entity.entity_type,
|
|
70
|
+
kind: entity.kind,
|
|
71
|
+
title: entity.label,
|
|
72
|
+
path: entity.path || undefined,
|
|
73
|
+
source_of_truth: entity.source_of_truth,
|
|
74
|
+
status: entity.status,
|
|
75
|
+
updated_at: entity.updated_at,
|
|
76
|
+
excerpt: entity.snippet,
|
|
77
|
+
...(params.includeScores
|
|
78
|
+
? {
|
|
79
|
+
score: Number(score.toFixed(4)),
|
|
80
|
+
semantic_score: Number(semantic.toFixed(4)),
|
|
81
|
+
embedding_score: Number(vectorSemantic.toFixed(4)),
|
|
82
|
+
lexical_score: Number(lexicalSemantic.toFixed(4)),
|
|
83
|
+
graph_score: Number(graphScore.toFixed(4))
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
...(params.includeMatchedRules
|
|
87
|
+
? {
|
|
88
|
+
matched_rules: entity.matched_rules
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
91
|
+
...(params.includeContent
|
|
92
|
+
? {
|
|
93
|
+
content: entity.content
|
|
94
|
+
}
|
|
95
|
+
: {})
|
|
96
|
+
};
|
|
97
|
+
})
|
|
98
|
+
.filter((result): result is NonNullable<typeof result> => result !== null)
|
|
99
|
+
.sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0));
|
|
100
|
+
|
|
101
|
+
const chunkCandidatesById = new Map(
|
|
102
|
+
params.candidates.filter((entity) => entity.entity_type === "Chunk").map((entity) => [entity.id, entity])
|
|
103
|
+
);
|
|
104
|
+
const normalizedById = new Map<string, (typeof rawResults)[number]>();
|
|
105
|
+
|
|
106
|
+
for (const result of rawResults) {
|
|
107
|
+
if (result.entity_type !== "Chunk" || !isWindowChunkId(String(result.id))) {
|
|
108
|
+
if (!normalizedById.has(String(result.id))) {
|
|
109
|
+
normalizedById.set(String(result.id), result);
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const canonicalId = baseChunkId(String(result.id));
|
|
115
|
+
const baseChunk = chunkCandidatesById.get(canonicalId);
|
|
116
|
+
const normalizedResult = baseChunk
|
|
117
|
+
? {
|
|
118
|
+
...result,
|
|
119
|
+
id: canonicalId,
|
|
120
|
+
title: baseChunkLabel(baseChunk.label),
|
|
121
|
+
path: baseChunk.path || undefined
|
|
122
|
+
}
|
|
123
|
+
: result;
|
|
124
|
+
const existing = normalizedById.get(String(normalizedResult.id));
|
|
125
|
+
if (!existing || Number(normalizedResult.score ?? 0) > Number(existing.score ?? 0)) {
|
|
126
|
+
normalizedById.set(String(normalizedResult.id), normalizedResult);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return [...normalizedById.values()]
|
|
131
|
+
.sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0))
|
|
132
|
+
.slice(0, params.topK);
|
|
133
|
+
}
|
|
@@ -2,7 +2,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { reloadContextGraph } from "./graph.js";
|
|
5
|
-
import {
|
|
5
|
+
import { runContextRules } from "./rules.js";
|
|
6
|
+
import { runContextImpact, runContextRelated, runContextSearch } from "./search.js";
|
|
6
7
|
|
|
7
8
|
type ToolPayload = Record<string, unknown>;
|
|
8
9
|
|
|
@@ -10,15 +11,97 @@ const SearchInput = z.object({
|
|
|
10
11
|
query: z.string().min(1),
|
|
11
12
|
top_k: z.number().int().positive().max(20).default(5),
|
|
12
13
|
include_deprecated: z.boolean().default(false),
|
|
13
|
-
|
|
14
|
+
response_preset: z.enum(["full", "compact", "minimal"]).optional(),
|
|
15
|
+
include_scores: z.boolean().optional(),
|
|
16
|
+
include_matched_rules: z.boolean().optional(),
|
|
17
|
+
include_content: z.boolean().optional()
|
|
14
18
|
});
|
|
15
19
|
|
|
16
20
|
const RelatedInput = z.object({
|
|
17
21
|
entity_id: z.string().min(1),
|
|
18
22
|
depth: z.number().int().positive().max(3).default(1),
|
|
19
|
-
include_edges: z.boolean().
|
|
23
|
+
include_edges: z.boolean().optional(),
|
|
24
|
+
response_preset: z.enum(["full", "compact", "minimal"]).optional(),
|
|
25
|
+
include_entity_metadata: z.boolean().optional()
|
|
20
26
|
});
|
|
21
27
|
|
|
28
|
+
const ImpactInput = z
|
|
29
|
+
.object({
|
|
30
|
+
entity_id: z.string().min(1).optional(),
|
|
31
|
+
query: z.string().min(1).optional(),
|
|
32
|
+
depth: z.number().int().positive().max(4).default(2),
|
|
33
|
+
top_k: z.number().int().positive().max(20).default(8),
|
|
34
|
+
include_edges: z.boolean().default(true),
|
|
35
|
+
response_preset: z.enum(["full", "compact", "minimal"]).optional(),
|
|
36
|
+
include_scores: z.boolean().optional(),
|
|
37
|
+
include_reasons: z.boolean().optional(),
|
|
38
|
+
verbose_paths: z.boolean().optional(),
|
|
39
|
+
max_path_hops_shown: z.number().int().positive().max(8).optional(),
|
|
40
|
+
profile: z.enum(["all", "config_only", "config_to_sql", "code_only", "sql_only"]).default("all"),
|
|
41
|
+
sort_by: z
|
|
42
|
+
.enum(["impact_score", "shortest_path", "semantic_score", "graph_score", "trust_score"])
|
|
43
|
+
.default("impact_score"),
|
|
44
|
+
relation_types: z
|
|
45
|
+
.array(
|
|
46
|
+
z.enum([
|
|
47
|
+
"CALLS",
|
|
48
|
+
"CALLS_SQL",
|
|
49
|
+
"IMPORTS",
|
|
50
|
+
"USES_CONFIG_KEY",
|
|
51
|
+
"USES_RESOURCE_KEY",
|
|
52
|
+
"USES_SETTING_KEY",
|
|
53
|
+
"USES_CONFIG",
|
|
54
|
+
"TRANSFORMS_CONFIG",
|
|
55
|
+
"PART_OF"
|
|
56
|
+
])
|
|
57
|
+
)
|
|
58
|
+
.max(9)
|
|
59
|
+
.optional(),
|
|
60
|
+
path_must_include: z
|
|
61
|
+
.array(
|
|
62
|
+
z.enum([
|
|
63
|
+
"CALLS",
|
|
64
|
+
"CALLS_SQL",
|
|
65
|
+
"IMPORTS",
|
|
66
|
+
"USES_CONFIG_KEY",
|
|
67
|
+
"USES_RESOURCE_KEY",
|
|
68
|
+
"USES_SETTING_KEY",
|
|
69
|
+
"USES_CONFIG",
|
|
70
|
+
"TRANSFORMS_CONFIG",
|
|
71
|
+
"PART_OF"
|
|
72
|
+
])
|
|
73
|
+
)
|
|
74
|
+
.max(9)
|
|
75
|
+
.optional(),
|
|
76
|
+
path_must_exclude: z
|
|
77
|
+
.array(
|
|
78
|
+
z.enum([
|
|
79
|
+
"CALLS",
|
|
80
|
+
"CALLS_SQL",
|
|
81
|
+
"IMPORTS",
|
|
82
|
+
"USES_CONFIG_KEY",
|
|
83
|
+
"USES_RESOURCE_KEY",
|
|
84
|
+
"USES_SETTING_KEY",
|
|
85
|
+
"USES_CONFIG",
|
|
86
|
+
"TRANSFORMS_CONFIG",
|
|
87
|
+
"PART_OF"
|
|
88
|
+
])
|
|
89
|
+
)
|
|
90
|
+
.max(9)
|
|
91
|
+
.optional(),
|
|
92
|
+
result_domains: z
|
|
93
|
+
.array(z.enum(["code", "config", "resource", "settings", "sql", "project"]))
|
|
94
|
+
.max(6)
|
|
95
|
+
.optional(),
|
|
96
|
+
result_entity_types: z
|
|
97
|
+
.array(z.enum(["File", "Chunk", "Module", "Project", "ADR", "Rule"]))
|
|
98
|
+
.max(6)
|
|
99
|
+
.optional()
|
|
100
|
+
})
|
|
101
|
+
.refine((value) => Boolean(value.entity_id || value.query), {
|
|
102
|
+
message: "Either entity_id or query is required."
|
|
103
|
+
});
|
|
104
|
+
|
|
22
105
|
const RulesInput = z.object({
|
|
23
106
|
scope: z.string().optional(),
|
|
24
107
|
include_inactive: z.boolean().default(false)
|
|
@@ -59,6 +142,15 @@ function registerTools(server: McpServer): void {
|
|
|
59
142
|
async (input) => buildToolResult(await runContextRelated(RelatedInput.parse(input ?? {})))
|
|
60
143
|
);
|
|
61
144
|
|
|
145
|
+
server.registerTool(
|
|
146
|
+
"context.impact",
|
|
147
|
+
{
|
|
148
|
+
description: "Traverse likely impact paths across config, code and SQL starting from an entity id or query.",
|
|
149
|
+
inputSchema: ImpactInput
|
|
150
|
+
},
|
|
151
|
+
async (input) => buildToolResult(await runContextImpact(ImpactInput.parse(input ?? {})))
|
|
152
|
+
);
|
|
153
|
+
|
|
62
154
|
server.registerTool(
|
|
63
155
|
"context.get_rules",
|
|
64
156
|
{
|
|
@@ -38,13 +38,81 @@ export type AdrRecord = {
|
|
|
38
38
|
status: string;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
export type RelationType =
|
|
42
|
+
| "CONSTRAINS"
|
|
43
|
+
| "IMPLEMENTS"
|
|
44
|
+
| "SUPERSEDES"
|
|
45
|
+
| "DEFINES"
|
|
46
|
+
| "CALLS"
|
|
47
|
+
| "IMPORTS"
|
|
48
|
+
| "CALLS_SQL"
|
|
49
|
+
| "USES_CONFIG_KEY"
|
|
50
|
+
| "USES_RESOURCE_KEY"
|
|
51
|
+
| "USES_SETTING_KEY"
|
|
52
|
+
| "PART_OF"
|
|
53
|
+
| "CONTAINS"
|
|
54
|
+
| "CONTAINS_MODULE"
|
|
55
|
+
| "EXPORTS"
|
|
56
|
+
| "INCLUDES_FILE"
|
|
57
|
+
| "REFERENCES_PROJECT"
|
|
58
|
+
| "USES_RESOURCE"
|
|
59
|
+
| "USES_SETTING"
|
|
60
|
+
| "USES_CONFIG"
|
|
61
|
+
| "TRANSFORMS_CONFIG";
|
|
62
|
+
|
|
41
63
|
export type RelationRecord = {
|
|
42
64
|
from: string;
|
|
43
65
|
to: string;
|
|
44
|
-
relation:
|
|
66
|
+
relation: RelationType;
|
|
45
67
|
note: string;
|
|
46
68
|
};
|
|
47
69
|
|
|
70
|
+
export type ChunkRecord = {
|
|
71
|
+
id: string;
|
|
72
|
+
file_id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
kind: string;
|
|
75
|
+
signature: string;
|
|
76
|
+
body: string;
|
|
77
|
+
description: string;
|
|
78
|
+
start_line: number;
|
|
79
|
+
end_line: number;
|
|
80
|
+
language: string;
|
|
81
|
+
exported: boolean;
|
|
82
|
+
updated_at: string;
|
|
83
|
+
source_of_truth: boolean;
|
|
84
|
+
trust_level: number;
|
|
85
|
+
status: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ModuleRecord = {
|
|
89
|
+
id: string;
|
|
90
|
+
path: string;
|
|
91
|
+
name: string;
|
|
92
|
+
summary: string;
|
|
93
|
+
file_count: number;
|
|
94
|
+
exported_symbols: string;
|
|
95
|
+
updated_at: string;
|
|
96
|
+
source_of_truth: boolean;
|
|
97
|
+
trust_level: number;
|
|
98
|
+
status: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type ProjectRecord = {
|
|
102
|
+
id: string;
|
|
103
|
+
path: string;
|
|
104
|
+
name: string;
|
|
105
|
+
kind: string;
|
|
106
|
+
language: string;
|
|
107
|
+
target_framework: string;
|
|
108
|
+
summary: string;
|
|
109
|
+
file_count: number;
|
|
110
|
+
updated_at: string;
|
|
111
|
+
source_of_truth: boolean;
|
|
112
|
+
trust_level: number;
|
|
113
|
+
status: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
48
116
|
export type RankingWeights = {
|
|
49
117
|
semantic: number;
|
|
50
118
|
graph: number;
|
|
@@ -56,6 +124,9 @@ export type ContextData = {
|
|
|
56
124
|
documents: DocumentRecord[];
|
|
57
125
|
adrs: AdrRecord[];
|
|
58
126
|
rules: RuleRecord[];
|
|
127
|
+
chunks: ChunkRecord[];
|
|
128
|
+
modules: ModuleRecord[];
|
|
129
|
+
projects: ProjectRecord[];
|
|
59
130
|
relations: RelationRecord[];
|
|
60
131
|
ranking: RankingWeights;
|
|
61
132
|
source: "cache" | "ryu";
|
|
@@ -64,7 +135,7 @@ export type ContextData = {
|
|
|
64
135
|
|
|
65
136
|
export type SearchEntity = {
|
|
66
137
|
id: string;
|
|
67
|
-
entity_type: "File" | "Rule" | "ADR";
|
|
138
|
+
entity_type: "File" | "Rule" | "ADR" | "Chunk" | "Module" | "Project";
|
|
68
139
|
kind: string;
|
|
69
140
|
label: string;
|
|
70
141
|
path: string;
|
|
@@ -88,13 +159,38 @@ export type SearchParams = {
|
|
|
88
159
|
query: string;
|
|
89
160
|
top_k: number;
|
|
90
161
|
include_deprecated: boolean;
|
|
91
|
-
|
|
162
|
+
response_preset?: "full" | "compact" | "minimal";
|
|
163
|
+
include_scores?: boolean;
|
|
164
|
+
include_matched_rules?: boolean;
|
|
165
|
+
include_content?: boolean;
|
|
92
166
|
};
|
|
93
167
|
|
|
94
168
|
export type RelatedParams = {
|
|
95
169
|
entity_id: string;
|
|
96
170
|
depth: number;
|
|
171
|
+
include_edges?: boolean;
|
|
172
|
+
response_preset?: "full" | "compact" | "minimal";
|
|
173
|
+
include_entity_metadata?: boolean;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export type ImpactParams = {
|
|
177
|
+
entity_id?: string;
|
|
178
|
+
query?: string;
|
|
179
|
+
depth: number;
|
|
180
|
+
top_k: number;
|
|
97
181
|
include_edges: boolean;
|
|
182
|
+
response_preset?: "full" | "compact" | "minimal";
|
|
183
|
+
include_scores?: boolean;
|
|
184
|
+
include_reasons?: boolean;
|
|
185
|
+
verbose_paths?: boolean;
|
|
186
|
+
max_path_hops_shown?: number;
|
|
187
|
+
profile?: "all" | "config_only" | "config_to_sql" | "code_only" | "sql_only";
|
|
188
|
+
sort_by?: "impact_score" | "shortest_path" | "semantic_score" | "graph_score" | "trust_score";
|
|
189
|
+
relation_types?: RelationType[];
|
|
190
|
+
path_must_include?: RelationType[];
|
|
191
|
+
path_must_exclude?: RelationType[];
|
|
192
|
+
result_domains?: ("code" | "config" | "resource" | "settings" | "sql" | "project")[];
|
|
193
|
+
result_entity_types?: ("File" | "Chunk" | "Module" | "Project" | "ADR" | "Rule")[];
|
|
98
194
|
};
|
|
99
195
|
|
|
100
196
|
export type RulesParams = {
|