@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
|
@@ -1,322 +1,74 @@
|
|
|
1
1
|
import { embedQuery, getEmbeddingRuntimeWarning, loadEmbeddingIndex } from "./embeddings.js";
|
|
2
|
+
import {
|
|
3
|
+
buildChunkPartOfRelations,
|
|
4
|
+
buildEntitySearchMap,
|
|
5
|
+
buildSearchEntities,
|
|
6
|
+
entityCatalog
|
|
7
|
+
} from "./contextEntities.js";
|
|
8
|
+
import { relationDegree } from "./graphMetrics.js";
|
|
2
9
|
import { loadContextData } from "./graph.js";
|
|
10
|
+
import { buildEmptyImpactResponse, buildImpactResponseMeta } from "./impactResponse.js";
|
|
11
|
+
import { buildImpactResults } from "./impactResults.js";
|
|
12
|
+
import { resolveImpactSeed } from "./impactSeed.js";
|
|
13
|
+
import {
|
|
14
|
+
cosineSimilarity,
|
|
15
|
+
expandQueryTokens,
|
|
16
|
+
legacyDataAccessBoost,
|
|
17
|
+
normalizeText,
|
|
18
|
+
recencyScore,
|
|
19
|
+
semanticScore,
|
|
20
|
+
tokenize
|
|
21
|
+
} from "./searchCore.js";
|
|
22
|
+
import { buildSearchResults } from "./searchResults.js";
|
|
23
|
+
import { traverseImpactGraph } from "./impactTraversal.js";
|
|
24
|
+
import { buildEmptyRelatedResponse, buildRelatedResponseMeta } from "./relatedResponse.js";
|
|
25
|
+
import { traverseRelatedGraph } from "./relatedTraversal.js";
|
|
26
|
+
import {
|
|
27
|
+
resolveImpactPathMustExclude,
|
|
28
|
+
resolveImpactPathMustInclude,
|
|
29
|
+
resolveImpactRelationTypes,
|
|
30
|
+
resolveImpactResultDomains,
|
|
31
|
+
resolveImpactResultEntityTypes
|
|
32
|
+
} from "./impactRanking.js";
|
|
33
|
+
import {
|
|
34
|
+
resolveImpactResponsePreset,
|
|
35
|
+
resolveRelatedResponsePreset,
|
|
36
|
+
resolveSearchResponsePreset
|
|
37
|
+
} from "./presets.js";
|
|
3
38
|
import type {
|
|
4
|
-
|
|
5
|
-
JsonObject,
|
|
39
|
+
ImpactParams,
|
|
6
40
|
RelatedParams,
|
|
7
|
-
|
|
8
|
-
RulesParams,
|
|
9
|
-
SearchEntity,
|
|
41
|
+
RelationType,
|
|
10
42
|
SearchParams,
|
|
11
43
|
ToolPayload
|
|
12
44
|
} from "./types.js";
|
|
13
45
|
|
|
14
46
|
const MIN_LEXICAL_RELEVANCE = 0.05;
|
|
15
47
|
const MIN_VECTOR_RELEVANCE = 0.2;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function normalizeText(value: string): string {
|
|
29
|
-
return value.normalize("NFKC").toLowerCase();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function tokenize(value: string): string[] {
|
|
33
|
-
return normalizeText(value)
|
|
34
|
-
.split(/[^\p{L}\p{N}]+/gu)
|
|
35
|
-
.map((part) => part.trim())
|
|
36
|
-
.filter((part) => part.length >= 2);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function expandQueryTokens(tokens: string[]): string[] {
|
|
40
|
-
const expanded = new Set<string>(tokens);
|
|
41
|
-
for (const token of tokens) {
|
|
42
|
-
const aliases = QUERY_TOKEN_EXPANSIONS[token];
|
|
43
|
-
if (!aliases) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
for (const alias of aliases) {
|
|
47
|
-
expanded.add(alias);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return Array.from(expanded);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function daysSince(isoDate: string): number {
|
|
54
|
-
const timestamp = Date.parse(isoDate);
|
|
55
|
-
if (Number.isNaN(timestamp)) {
|
|
56
|
-
return 3650;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
return Math.max(0, (now - timestamp) / (1000 * 60 * 60 * 24));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function recencyScore(isoDate: string): number {
|
|
64
|
-
const days = daysSince(isoDate);
|
|
65
|
-
return 1 / (1 + days / 30);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function semanticScore(queryTokens: string[], queryPhrase: string, text: string): number {
|
|
69
|
-
if (queryTokens.length === 0) {
|
|
70
|
-
return 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const textTokenSet = new Set(tokenize(text));
|
|
74
|
-
if (textTokenSet.size === 0) {
|
|
75
|
-
return 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let matched = 0;
|
|
79
|
-
for (const token of queryTokens) {
|
|
80
|
-
if (textTokenSet.has(token)) {
|
|
81
|
-
matched += 1;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const overlap = matched / queryTokens.length;
|
|
86
|
-
if (overlap <= 0) {
|
|
87
|
-
return 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const normalizedText = normalizeText(text);
|
|
91
|
-
const phraseBonus = queryPhrase && normalizedText.includes(queryPhrase) ? 0.15 : 0;
|
|
92
|
-
return Math.min(1, overlap * 0.85 + phraseBonus);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function cosineSimilarity(a: number[], b: number[]): number {
|
|
96
|
-
if (a.length === 0 || b.length === 0 || a.length !== b.length) {
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let dot = 0;
|
|
101
|
-
let normA = 0;
|
|
102
|
-
let normB = 0;
|
|
103
|
-
for (let index = 0; index < a.length; index += 1) {
|
|
104
|
-
const av = a[index];
|
|
105
|
-
const bv = b[index];
|
|
106
|
-
dot += av * bv;
|
|
107
|
-
normA += av * av;
|
|
108
|
-
normB += bv * bv;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (normA === 0 || normB === 0) {
|
|
112
|
-
return 0;
|
|
113
|
-
}
|
|
114
|
-
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function groupRuleLinks(relations: RelationRecord[]): Map<string, string[]> {
|
|
118
|
-
const links = new Map<string, string[]>();
|
|
119
|
-
for (const relation of relations) {
|
|
120
|
-
if (relation.relation !== "CONSTRAINS" && relation.relation !== "IMPLEMENTS") {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (relation.relation === "CONSTRAINS") {
|
|
125
|
-
const list = links.get(relation.to) ?? [];
|
|
126
|
-
list.push(relation.from);
|
|
127
|
-
links.set(relation.to, list);
|
|
128
|
-
} else {
|
|
129
|
-
const list = links.get(relation.from) ?? [];
|
|
130
|
-
list.push(relation.to);
|
|
131
|
-
links.set(relation.from, list);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return links;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function buildSearchEntities(data: ContextData, includeContent: boolean): SearchEntity[] {
|
|
138
|
-
const entities: SearchEntity[] = [];
|
|
139
|
-
const fileRuleLinks = groupRuleLinks(data.relations);
|
|
140
|
-
const adrPathSet = new Set(
|
|
141
|
-
data.adrs
|
|
142
|
-
.map((adr) => adr.path.trim().toLowerCase())
|
|
143
|
-
.filter((adrPath) => adrPath.length > 0)
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
for (const document of data.documents) {
|
|
147
|
-
const normalizedPath = document.path.trim().toLowerCase();
|
|
148
|
-
// ADR content is represented by ADR entities below; avoid duplicate results.
|
|
149
|
-
if (document.kind === "ADR" && adrPathSet.has(normalizedPath)) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
entities.push({
|
|
154
|
-
id: document.id,
|
|
155
|
-
entity_type: "File",
|
|
156
|
-
kind: document.kind,
|
|
157
|
-
label: document.path,
|
|
158
|
-
path: document.path,
|
|
159
|
-
text: `${document.path}\n${document.excerpt}\n${document.content}`,
|
|
160
|
-
status: document.status,
|
|
161
|
-
source_of_truth: document.source_of_truth,
|
|
162
|
-
trust_level: document.trust_level,
|
|
163
|
-
updated_at: document.updated_at,
|
|
164
|
-
snippet: document.excerpt,
|
|
165
|
-
matched_rules: fileRuleLinks.get(document.id) ?? [],
|
|
166
|
-
content: includeContent ? document.content : undefined
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
for (const rule of data.rules) {
|
|
171
|
-
entities.push({
|
|
172
|
-
id: rule.id,
|
|
173
|
-
entity_type: "Rule",
|
|
174
|
-
kind: "RULE",
|
|
175
|
-
label: rule.title || rule.id,
|
|
176
|
-
path: "",
|
|
177
|
-
text: `${rule.id}\n${rule.title}\n${rule.body}`,
|
|
178
|
-
status: rule.status,
|
|
179
|
-
source_of_truth: rule.source_of_truth,
|
|
180
|
-
trust_level: rule.trust_level,
|
|
181
|
-
updated_at: rule.updated_at,
|
|
182
|
-
snippet: rule.body.slice(0, 500),
|
|
183
|
-
matched_rules: [rule.id],
|
|
184
|
-
content: includeContent ? rule.body : undefined
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
for (const adr of data.adrs) {
|
|
189
|
-
entities.push({
|
|
190
|
-
id: adr.id,
|
|
191
|
-
entity_type: "ADR",
|
|
192
|
-
kind: "ADR",
|
|
193
|
-
label: adr.title || adr.id,
|
|
194
|
-
path: adr.path,
|
|
195
|
-
text: `${adr.path}\n${adr.title}\n${adr.body}`,
|
|
196
|
-
status: adr.status,
|
|
197
|
-
source_of_truth: adr.source_of_truth,
|
|
198
|
-
trust_level: adr.trust_level,
|
|
199
|
-
updated_at: adr.decision_date,
|
|
200
|
-
snippet: adr.body.slice(0, 500),
|
|
201
|
-
matched_rules: [],
|
|
202
|
-
content: includeContent ? adr.body : undefined
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const filePathById = new Map(
|
|
207
|
-
data.documents
|
|
208
|
-
.filter((document) => document.kind === "CODE")
|
|
209
|
-
.map((document) => [document.id, document.path])
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
for (const chunk of data.chunks) {
|
|
213
|
-
const filePath = filePathById.get(chunk.file_id) ?? "";
|
|
214
|
-
entities.push({
|
|
215
|
-
id: chunk.id,
|
|
216
|
-
entity_type: "Chunk",
|
|
217
|
-
kind: chunk.kind || "chunk",
|
|
218
|
-
label: chunk.name || chunk.id,
|
|
219
|
-
path: filePath,
|
|
220
|
-
text: `${filePath}\n${chunk.name}\n${chunk.signature}\n${chunk.body}`,
|
|
221
|
-
status: chunk.status,
|
|
222
|
-
source_of_truth: chunk.source_of_truth,
|
|
223
|
-
trust_level: chunk.trust_level,
|
|
224
|
-
updated_at: chunk.updated_at,
|
|
225
|
-
snippet: chunk.body.slice(0, 500),
|
|
226
|
-
matched_rules: fileRuleLinks.get(chunk.file_id) ?? [],
|
|
227
|
-
content: includeContent ? chunk.body : undefined
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return entities;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function relationDegree(relations: RelationRecord[]): Map<string, number> {
|
|
235
|
-
const degrees = new Map<string, number>();
|
|
236
|
-
|
|
237
|
-
for (const relation of relations) {
|
|
238
|
-
degrees.set(relation.from, (degrees.get(relation.from) ?? 0) + 1);
|
|
239
|
-
degrees.set(relation.to, (degrees.get(relation.to) ?? 0) + 1);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return degrees;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function buildChunkPartOfRelations(data: ContextData): RelationRecord[] {
|
|
246
|
-
const relations: RelationRecord[] = [];
|
|
247
|
-
for (const chunk of data.chunks) {
|
|
248
|
-
if (!chunk.file_id) {
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
relations.push({
|
|
253
|
-
from: chunk.id,
|
|
254
|
-
to: chunk.file_id,
|
|
255
|
-
relation: "PART_OF",
|
|
256
|
-
note: "Chunk belongs to file"
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
return relations;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function entityCatalog(data: ContextData): Map<string, JsonObject> {
|
|
263
|
-
const catalog = new Map<string, JsonObject>();
|
|
264
|
-
const fileById = new Map(data.documents.map((document) => [document.id, document]));
|
|
265
|
-
|
|
266
|
-
for (const file of data.documents) {
|
|
267
|
-
catalog.set(file.id, {
|
|
268
|
-
id: file.id,
|
|
269
|
-
type: "File",
|
|
270
|
-
label: file.path,
|
|
271
|
-
status: file.status,
|
|
272
|
-
source_of_truth: file.source_of_truth
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const rule of data.rules) {
|
|
277
|
-
catalog.set(rule.id, {
|
|
278
|
-
id: rule.id,
|
|
279
|
-
type: "Rule",
|
|
280
|
-
label: rule.title,
|
|
281
|
-
status: rule.status,
|
|
282
|
-
source_of_truth: rule.source_of_truth
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
for (const adr of data.adrs) {
|
|
287
|
-
catalog.set(adr.id, {
|
|
288
|
-
id: adr.id,
|
|
289
|
-
type: "ADR",
|
|
290
|
-
label: adr.title || adr.id,
|
|
291
|
-
status: adr.status,
|
|
292
|
-
source_of_truth: adr.source_of_truth
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
for (const chunk of data.chunks) {
|
|
297
|
-
const filePath = fileById.get(chunk.file_id)?.path ?? "";
|
|
298
|
-
const chunkEntity: JsonObject = {
|
|
299
|
-
id: chunk.id,
|
|
300
|
-
type: "Chunk",
|
|
301
|
-
label: chunk.name || chunk.id,
|
|
302
|
-
status: chunk.status,
|
|
303
|
-
source_of_truth: chunk.source_of_truth
|
|
304
|
-
};
|
|
305
|
-
if (filePath) {
|
|
306
|
-
chunkEntity.path = filePath;
|
|
307
|
-
}
|
|
308
|
-
catalog.set(chunk.id, chunkEntity);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return catalog;
|
|
312
|
-
}
|
|
48
|
+
const IMPACT_RELATION_TYPES = new Set([
|
|
49
|
+
"CALLS",
|
|
50
|
+
"CALLS_SQL",
|
|
51
|
+
"IMPORTS",
|
|
52
|
+
"USES_CONFIG_KEY",
|
|
53
|
+
"USES_RESOURCE_KEY",
|
|
54
|
+
"USES_SETTING_KEY",
|
|
55
|
+
"USES_CONFIG",
|
|
56
|
+
"TRANSFORMS_CONFIG",
|
|
57
|
+
"PART_OF"
|
|
58
|
+
]);
|
|
313
59
|
|
|
314
60
|
export async function runContextSearch(parsed: SearchParams): Promise<ToolPayload> {
|
|
61
|
+
const searchPresetConfig = resolveSearchResponsePreset(parsed);
|
|
62
|
+
const responsePreset = searchPresetConfig.responsePreset;
|
|
63
|
+
const includeScores = searchPresetConfig.includeScores;
|
|
64
|
+
const includeMatchedRules = searchPresetConfig.includeMatchedRules;
|
|
65
|
+
const includeContent = searchPresetConfig.includeContent;
|
|
315
66
|
const data = await loadContextData();
|
|
316
|
-
const
|
|
67
|
+
const allRelations = [...data.relations, ...buildChunkPartOfRelations(data)];
|
|
68
|
+
const degreeByEntity = relationDegree(allRelations);
|
|
317
69
|
const queryTokens = expandQueryTokens(Array.from(new Set(tokenize(parsed.query))));
|
|
318
70
|
const queryPhrase = normalizeText(parsed.query).trim();
|
|
319
|
-
const candidates = buildSearchEntities(data,
|
|
71
|
+
const candidates = buildSearchEntities(data, includeContent).filter(
|
|
320
72
|
(entity) => parsed.include_deprecated || entity.status.toLowerCase() !== "deprecated"
|
|
321
73
|
);
|
|
322
74
|
const embeddings = loadEmbeddingIndex();
|
|
@@ -325,63 +77,35 @@ export async function runContextSearch(parsed: SearchParams): Promise<ToolPayloa
|
|
|
325
77
|
? await embedQuery(parsed.query, embeddings.model)
|
|
326
78
|
: null;
|
|
327
79
|
|
|
328
|
-
const results =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
let score = 0;
|
|
348
|
-
score += data.ranking.semantic * semantic;
|
|
349
|
-
score += data.ranking.graph * graphScore;
|
|
350
|
-
score += data.ranking.trust * trustScore;
|
|
351
|
-
score += data.ranking.recency * dateScore;
|
|
352
|
-
|
|
353
|
-
if (entity.source_of_truth) {
|
|
354
|
-
score += 0.1 * semantic;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return {
|
|
358
|
-
id: entity.id,
|
|
359
|
-
entity_type: entity.entity_type,
|
|
360
|
-
kind: entity.kind,
|
|
361
|
-
title: entity.label,
|
|
362
|
-
path: entity.path || undefined,
|
|
363
|
-
score: Number(score.toFixed(4)),
|
|
364
|
-
semantic_score: Number(semantic.toFixed(4)),
|
|
365
|
-
embedding_score: Number(vectorSemantic.toFixed(4)),
|
|
366
|
-
lexical_score: Number(lexicalSemantic.toFixed(4)),
|
|
367
|
-
graph_score: Number(graphScore.toFixed(4)),
|
|
368
|
-
source_of_truth: entity.source_of_truth,
|
|
369
|
-
status: entity.status,
|
|
370
|
-
updated_at: entity.updated_at,
|
|
371
|
-
matched_rules: entity.matched_rules,
|
|
372
|
-
excerpt: entity.snippet,
|
|
373
|
-
content: parsed.include_content ? entity.content : undefined
|
|
374
|
-
};
|
|
375
|
-
})
|
|
376
|
-
.filter((result): result is NonNullable<typeof result> => result !== null)
|
|
377
|
-
.sort((a, b) => b.score - a.score)
|
|
378
|
-
.slice(0, parsed.top_k);
|
|
80
|
+
const results = buildSearchResults({
|
|
81
|
+
candidates,
|
|
82
|
+
degreeByEntity,
|
|
83
|
+
queryTokens,
|
|
84
|
+
queryPhrase,
|
|
85
|
+
ranking: data.ranking,
|
|
86
|
+
includeScores,
|
|
87
|
+
includeMatchedRules,
|
|
88
|
+
includeContent,
|
|
89
|
+
queryVector,
|
|
90
|
+
embeddingVectors: embeddings.vectors,
|
|
91
|
+
topK: parsed.top_k,
|
|
92
|
+
minLexicalRelevance: MIN_LEXICAL_RELEVANCE,
|
|
93
|
+
minVectorRelevance: MIN_VECTOR_RELEVANCE,
|
|
94
|
+
semanticScorer: semanticScore,
|
|
95
|
+
vectorScorer: cosineSimilarity,
|
|
96
|
+
recencyScorer: recencyScore,
|
|
97
|
+
legacyDataAccessBooster: legacyDataAccessBoost
|
|
98
|
+
});
|
|
379
99
|
|
|
380
100
|
const warningMessages = [data.warning, embeddings.warning, getEmbeddingRuntimeWarning()].filter(Boolean);
|
|
381
101
|
|
|
382
102
|
return {
|
|
383
103
|
query: parsed.query,
|
|
384
104
|
top_k: parsed.top_k,
|
|
105
|
+
response_preset: responsePreset,
|
|
106
|
+
include_scores: includeScores,
|
|
107
|
+
include_matched_rules: includeMatchedRules,
|
|
108
|
+
include_content: includeContent,
|
|
385
109
|
ranking: data.ranking,
|
|
386
110
|
total_candidates: candidates.length,
|
|
387
111
|
context_source: data.source,
|
|
@@ -393,125 +117,132 @@ export async function runContextSearch(parsed: SearchParams): Promise<ToolPayloa
|
|
|
393
117
|
}
|
|
394
118
|
|
|
395
119
|
export async function runContextRelated(parsed: RelatedParams): Promise<ToolPayload> {
|
|
120
|
+
const relatedPresetConfig = resolveRelatedResponsePreset(parsed);
|
|
121
|
+
const responsePreset = relatedPresetConfig.responsePreset;
|
|
122
|
+
const includeEdges = relatedPresetConfig.includeEdges;
|
|
123
|
+
const includeEntityMetadata = relatedPresetConfig.includeEntityMetadata;
|
|
396
124
|
const data = await loadContextData();
|
|
397
125
|
const catalog = entityCatalog(data);
|
|
398
126
|
const relations = [...data.relations, ...buildChunkPartOfRelations(data)];
|
|
127
|
+
const relatedResponseMeta = buildRelatedResponseMeta({
|
|
128
|
+
parsed,
|
|
129
|
+
responsePreset,
|
|
130
|
+
includeEdges,
|
|
131
|
+
includeEntityMetadata,
|
|
132
|
+
contextSource: data.source
|
|
133
|
+
});
|
|
399
134
|
|
|
400
135
|
if (!catalog.has(parsed.entity_id)) {
|
|
401
|
-
return {
|
|
402
|
-
|
|
403
|
-
depth: parsed.depth,
|
|
404
|
-
related: [],
|
|
405
|
-
edges: [],
|
|
406
|
-
context_source: data.source,
|
|
136
|
+
return buildEmptyRelatedResponse({
|
|
137
|
+
meta: relatedResponseMeta,
|
|
407
138
|
warning: "Entity not found in indexed context."
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const outgoing = new Map<string, RelationRecord[]>();
|
|
412
|
-
const incoming = new Map<string, RelationRecord[]>();
|
|
413
|
-
|
|
414
|
-
for (const relation of relations) {
|
|
415
|
-
const outList = outgoing.get(relation.from) ?? [];
|
|
416
|
-
outList.push(relation);
|
|
417
|
-
outgoing.set(relation.from, outList);
|
|
418
|
-
|
|
419
|
-
const inList = incoming.get(relation.to) ?? [];
|
|
420
|
-
inList.push(relation);
|
|
421
|
-
incoming.set(relation.to, inList);
|
|
139
|
+
});
|
|
422
140
|
}
|
|
423
141
|
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const current = queue.shift() as { id: string; hop: number };
|
|
432
|
-
if (current.hop >= parsed.depth) {
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const neighbors = [
|
|
437
|
-
...(outgoing.get(current.id) ?? []).map((edge) => ({
|
|
438
|
-
edge,
|
|
439
|
-
next: edge.to,
|
|
440
|
-
direction: "outgoing"
|
|
441
|
-
})),
|
|
442
|
-
...(incoming.get(current.id) ?? []).map((edge) => ({
|
|
443
|
-
edge,
|
|
444
|
-
next: edge.from,
|
|
445
|
-
direction: "incoming"
|
|
446
|
-
}))
|
|
447
|
-
];
|
|
448
|
-
|
|
449
|
-
for (const neighbor of neighbors) {
|
|
450
|
-
const target = neighbor.next;
|
|
451
|
-
if (!seen.has(target)) {
|
|
452
|
-
seen.add(target);
|
|
453
|
-
queue.push({ id: target, hop: current.hop + 1 });
|
|
454
|
-
|
|
455
|
-
const entity = catalog.get(target) ?? {
|
|
456
|
-
id: target,
|
|
457
|
-
type: "Unknown",
|
|
458
|
-
label: target,
|
|
459
|
-
status: "unknown",
|
|
460
|
-
source_of_truth: false
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
related.push({
|
|
464
|
-
...entity,
|
|
465
|
-
hops: current.hop + 1,
|
|
466
|
-
via_relation: neighbor.edge.relation,
|
|
467
|
-
direction: neighbor.direction
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const edgeKey = `${neighbor.edge.from}|${neighbor.edge.relation}|${neighbor.edge.to}|${neighbor.edge.note}`;
|
|
472
|
-
if (!traversedEdgeKeys.has(edgeKey)) {
|
|
473
|
-
traversedEdgeKeys.add(edgeKey);
|
|
474
|
-
traversedEdges.push({
|
|
475
|
-
from: neighbor.edge.from,
|
|
476
|
-
to: neighbor.edge.to,
|
|
477
|
-
relation: neighbor.edge.relation,
|
|
478
|
-
note: neighbor.edge.note
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
142
|
+
const { related, traversedEdges } = traverseRelatedGraph({
|
|
143
|
+
entityId: parsed.entity_id,
|
|
144
|
+
relations,
|
|
145
|
+
depth: parsed.depth,
|
|
146
|
+
catalog,
|
|
147
|
+
includeEntityMetadata
|
|
148
|
+
});
|
|
483
149
|
|
|
484
150
|
return {
|
|
485
|
-
|
|
486
|
-
depth: parsed.depth,
|
|
487
|
-
context_source: data.source,
|
|
151
|
+
...relatedResponseMeta,
|
|
488
152
|
warning: data.warning,
|
|
489
153
|
related,
|
|
490
|
-
edges:
|
|
154
|
+
edges: includeEdges ? traversedEdges : []
|
|
491
155
|
};
|
|
492
156
|
}
|
|
493
157
|
|
|
494
|
-
export async function
|
|
158
|
+
export async function runContextImpact(parsed: ImpactParams): Promise<ToolPayload> {
|
|
495
159
|
const data = await loadContextData();
|
|
160
|
+
const catalog = entityCatalog(data);
|
|
161
|
+
const relations = [...data.relations, ...buildChunkPartOfRelations(data)];
|
|
162
|
+
const allowedRelationTypes = resolveImpactRelationTypes(parsed);
|
|
163
|
+
const impactRelations = relations.filter((relation) => allowedRelationTypes.has(relation.relation));
|
|
164
|
+
const searchEntities = buildEntitySearchMap(data);
|
|
165
|
+
const degreeByEntity = relationDegree(relations);
|
|
166
|
+
const profile = parsed.profile ?? "all";
|
|
167
|
+
const sortBy = parsed.sort_by ?? "impact_score";
|
|
168
|
+
const responsePresetConfig = resolveImpactResponsePreset(parsed);
|
|
169
|
+
const responsePreset = responsePresetConfig.responsePreset;
|
|
170
|
+
const includeScores = responsePresetConfig.includeScores;
|
|
171
|
+
const includeReasons = responsePresetConfig.includeReasons;
|
|
172
|
+
const verbosePaths = responsePresetConfig.verbosePaths;
|
|
173
|
+
const maxPathHopsShown = responsePresetConfig.maxPathHopsShown;
|
|
174
|
+
const resultDomains = resolveImpactResultDomains(parsed);
|
|
175
|
+
const resultEntityTypes = resolveImpactResultEntityTypes(parsed);
|
|
176
|
+
const pathMustInclude = resolveImpactPathMustInclude(parsed);
|
|
177
|
+
const pathMustExclude = resolveImpactPathMustExclude(parsed);
|
|
178
|
+
const seedResolution = await resolveImpactSeed(parsed, runContextSearch);
|
|
179
|
+
const seedId = seedResolution.id;
|
|
180
|
+
const impactResponseMeta = buildImpactResponseMeta({
|
|
181
|
+
parsed,
|
|
182
|
+
responsePreset,
|
|
183
|
+
includeScores,
|
|
184
|
+
includeReasons,
|
|
185
|
+
verbosePaths,
|
|
186
|
+
maxPathHopsShown,
|
|
187
|
+
profile,
|
|
188
|
+
sortBy,
|
|
189
|
+
allowedRelationTypes,
|
|
190
|
+
contextSource: data.source
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!seedId) {
|
|
194
|
+
return buildEmptyImpactResponse({
|
|
195
|
+
meta: impactResponseMeta,
|
|
196
|
+
warning: seedResolution.warning ?? "No matching seed entity found for impact analysis."
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!catalog.has(seedId)) {
|
|
201
|
+
return buildEmptyImpactResponse({
|
|
202
|
+
meta: impactResponseMeta,
|
|
203
|
+
warning: "Seed entity not found in indexed context."
|
|
204
|
+
});
|
|
205
|
+
}
|
|
496
206
|
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
207
|
+
const { visited, traversedEdges } = traverseImpactGraph({
|
|
208
|
+
seedId,
|
|
209
|
+
relations: impactRelations,
|
|
210
|
+
depth: parsed.depth
|
|
211
|
+
});
|
|
212
|
+
const queryTokens = parsed.query ? expandQueryTokens(Array.from(new Set(tokenize(parsed.query)))) : [];
|
|
213
|
+
const queryPhrase = parsed.query ? normalizeText(parsed.query).trim() : "";
|
|
214
|
+
|
|
215
|
+
const results = buildImpactResults({
|
|
216
|
+
visited,
|
|
217
|
+
seedId,
|
|
218
|
+
catalog,
|
|
219
|
+
searchEntities,
|
|
220
|
+
degreeByEntity,
|
|
221
|
+
queryTokens,
|
|
222
|
+
queryPhrase,
|
|
223
|
+
hasQuery: Boolean(parsed.query),
|
|
224
|
+
profile,
|
|
225
|
+
includeReasons,
|
|
226
|
+
includeScores,
|
|
227
|
+
verbosePaths,
|
|
228
|
+
maxPathHopsShown,
|
|
229
|
+
resultDomains,
|
|
230
|
+
resultEntityTypes,
|
|
231
|
+
pathMustInclude,
|
|
232
|
+
pathMustExclude,
|
|
233
|
+
sortBy,
|
|
234
|
+
topK: parsed.top_k,
|
|
235
|
+
semanticScorer: semanticScore
|
|
236
|
+
});
|
|
509
237
|
|
|
510
238
|
return {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
239
|
+
...impactResponseMeta,
|
|
240
|
+
resolved_seed_id: seedId,
|
|
241
|
+
resolved_from_query: !parsed.entity_id,
|
|
242
|
+
seed: catalog.get(seedId),
|
|
514
243
|
warning: data.warning,
|
|
515
|
-
|
|
244
|
+
query_results: seedResolution.query_results,
|
|
245
|
+
results,
|
|
246
|
+
edges: parsed.include_edges && verbosePaths ? traversedEdges : []
|
|
516
247
|
};
|
|
517
248
|
}
|