@danielblomma/cortex-mcp 0.4.5 → 1.0.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/README.md +125 -42
- package/bin/cortex.mjs +36 -63
- package/bin/wsl.mjs +30 -0
- package/package.json +15 -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 +16 -16
- package/scaffold/mcp/package.json +4 -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 +253 -130
- 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 +33 -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 +186 -455
- 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 +82 -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 +2227 -59
- package/scaffold/scripts/install-git-hooks.sh +3 -1
- package/scaffold/scripts/memory-compile.mjs +241 -0
- package/scaffold/scripts/memory-compile.sh +20 -0
- package/scaffold/scripts/memory-lint.mjs +384 -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/rust.mjs +515 -0
- package/scaffold/scripts/parsers/sql.mjs +137 -0
- package/scaffold/scripts/parsers/vbnet.mjs +143 -0
- package/scaffold/scripts/status.sh +0 -7
- package/scaffold/scripts/watch.sh +9 -1
- 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,10 +38,32 @@ 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
|
|
|
@@ -52,9 +74,39 @@ export type ChunkRecord = {
|
|
|
52
74
|
kind: string;
|
|
53
75
|
signature: string;
|
|
54
76
|
body: string;
|
|
77
|
+
description: string;
|
|
55
78
|
start_line: number;
|
|
56
79
|
end_line: number;
|
|
57
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;
|
|
58
110
|
updated_at: string;
|
|
59
111
|
source_of_truth: boolean;
|
|
60
112
|
trust_level: number;
|
|
@@ -73,6 +125,8 @@ export type ContextData = {
|
|
|
73
125
|
adrs: AdrRecord[];
|
|
74
126
|
rules: RuleRecord[];
|
|
75
127
|
chunks: ChunkRecord[];
|
|
128
|
+
modules: ModuleRecord[];
|
|
129
|
+
projects: ProjectRecord[];
|
|
76
130
|
relations: RelationRecord[];
|
|
77
131
|
ranking: RankingWeights;
|
|
78
132
|
source: "cache" | "ryu";
|
|
@@ -81,7 +135,7 @@ export type ContextData = {
|
|
|
81
135
|
|
|
82
136
|
export type SearchEntity = {
|
|
83
137
|
id: string;
|
|
84
|
-
entity_type: "File" | "Rule" | "ADR" | "Chunk";
|
|
138
|
+
entity_type: "File" | "Rule" | "ADR" | "Chunk" | "Module" | "Project";
|
|
85
139
|
kind: string;
|
|
86
140
|
label: string;
|
|
87
141
|
path: string;
|
|
@@ -105,13 +159,38 @@ export type SearchParams = {
|
|
|
105
159
|
query: string;
|
|
106
160
|
top_k: number;
|
|
107
161
|
include_deprecated: boolean;
|
|
108
|
-
|
|
162
|
+
response_preset?: "full" | "compact" | "minimal";
|
|
163
|
+
include_scores?: boolean;
|
|
164
|
+
include_matched_rules?: boolean;
|
|
165
|
+
include_content?: boolean;
|
|
109
166
|
};
|
|
110
167
|
|
|
111
168
|
export type RelatedParams = {
|
|
112
169
|
entity_id: string;
|
|
113
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;
|
|
114
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")[];
|
|
115
194
|
};
|
|
116
195
|
|
|
117
196
|
export type RulesParams = {
|