@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
|
@@ -1,223 +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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const now = Date.now();
|
|
29
|
-
return Math.max(0, (now - timestamp) / (1000 * 60 * 60 * 24));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function recencyScore(isoDate: string): number {
|
|
33
|
-
const days = daysSince(isoDate);
|
|
34
|
-
return 1 / (1 + days / 30);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function semanticScore(query: string, text: string): number {
|
|
38
|
-
const queryTokens = tokenize(query);
|
|
39
|
-
if (queryTokens.length === 0) {
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const haystack = text.toLowerCase();
|
|
44
|
-
let matched = 0;
|
|
45
|
-
for (const token of queryTokens) {
|
|
46
|
-
if (haystack.includes(token)) {
|
|
47
|
-
matched += 1;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const overlap = matched / queryTokens.length;
|
|
52
|
-
const phraseBonus = haystack.includes(query.toLowerCase()) ? 0.25 : 0;
|
|
53
|
-
return Math.min(1, overlap * 0.85 + phraseBonus);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function cosineSimilarity(a: number[], b: number[]): number {
|
|
57
|
-
if (a.length === 0 || b.length === 0 || a.length !== b.length) {
|
|
58
|
-
return 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let dot = 0;
|
|
62
|
-
let normA = 0;
|
|
63
|
-
let normB = 0;
|
|
64
|
-
for (let index = 0; index < a.length; index += 1) {
|
|
65
|
-
const av = a[index];
|
|
66
|
-
const bv = b[index];
|
|
67
|
-
dot += av * bv;
|
|
68
|
-
normA += av * av;
|
|
69
|
-
normB += bv * bv;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (normA === 0 || normB === 0) {
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function groupRuleLinks(relations: RelationRecord[]): Map<string, string[]> {
|
|
79
|
-
const links = new Map<string, string[]>();
|
|
80
|
-
for (const relation of relations) {
|
|
81
|
-
if (relation.relation !== "CONSTRAINS" && relation.relation !== "IMPLEMENTS") {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (relation.relation === "CONSTRAINS") {
|
|
86
|
-
const list = links.get(relation.to) ?? [];
|
|
87
|
-
list.push(relation.from);
|
|
88
|
-
links.set(relation.to, list);
|
|
89
|
-
} else {
|
|
90
|
-
const list = links.get(relation.from) ?? [];
|
|
91
|
-
list.push(relation.to);
|
|
92
|
-
links.set(relation.from, list);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return links;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function buildSearchEntities(data: ContextData, includeContent: boolean): SearchEntity[] {
|
|
99
|
-
const entities: SearchEntity[] = [];
|
|
100
|
-
const ruleLinks = groupRuleLinks(data.relations);
|
|
101
|
-
const adrPathSet = new Set(
|
|
102
|
-
data.adrs
|
|
103
|
-
.map((adr) => adr.path.trim().toLowerCase())
|
|
104
|
-
.filter((adrPath) => adrPath.length > 0)
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
for (const document of data.documents) {
|
|
108
|
-
const normalizedPath = document.path.trim().toLowerCase();
|
|
109
|
-
// ADR content is represented by ADR entities below; avoid duplicate results.
|
|
110
|
-
if (document.kind === "ADR" && adrPathSet.has(normalizedPath)) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
entities.push({
|
|
115
|
-
id: document.id,
|
|
116
|
-
entity_type: "File",
|
|
117
|
-
kind: document.kind,
|
|
118
|
-
label: document.path,
|
|
119
|
-
path: document.path,
|
|
120
|
-
text: `${document.path}\n${document.excerpt}\n${document.content}`,
|
|
121
|
-
status: document.status,
|
|
122
|
-
source_of_truth: document.source_of_truth,
|
|
123
|
-
trust_level: document.trust_level,
|
|
124
|
-
updated_at: document.updated_at,
|
|
125
|
-
snippet: document.excerpt,
|
|
126
|
-
matched_rules: ruleLinks.get(document.id) ?? [],
|
|
127
|
-
content: includeContent ? document.content : undefined
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
for (const rule of data.rules) {
|
|
132
|
-
entities.push({
|
|
133
|
-
id: rule.id,
|
|
134
|
-
entity_type: "Rule",
|
|
135
|
-
kind: "RULE",
|
|
136
|
-
label: rule.title || rule.id,
|
|
137
|
-
path: "",
|
|
138
|
-
text: `${rule.id}\n${rule.title}\n${rule.body}`,
|
|
139
|
-
status: rule.status,
|
|
140
|
-
source_of_truth: rule.source_of_truth,
|
|
141
|
-
trust_level: rule.trust_level,
|
|
142
|
-
updated_at: rule.updated_at,
|
|
143
|
-
snippet: rule.body.slice(0, 500),
|
|
144
|
-
matched_rules: [rule.id],
|
|
145
|
-
content: includeContent ? rule.body : undefined
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (const adr of data.adrs) {
|
|
150
|
-
entities.push({
|
|
151
|
-
id: adr.id,
|
|
152
|
-
entity_type: "ADR",
|
|
153
|
-
kind: "ADR",
|
|
154
|
-
label: adr.title || adr.id,
|
|
155
|
-
path: adr.path,
|
|
156
|
-
text: `${adr.path}\n${adr.title}\n${adr.body}`,
|
|
157
|
-
status: adr.status,
|
|
158
|
-
source_of_truth: adr.source_of_truth,
|
|
159
|
-
trust_level: adr.trust_level,
|
|
160
|
-
updated_at: adr.decision_date,
|
|
161
|
-
snippet: adr.body.slice(0, 500),
|
|
162
|
-
matched_rules: [],
|
|
163
|
-
content: includeContent ? adr.body : undefined
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return entities;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function relationDegree(relations: RelationRecord[]): Map<string, number> {
|
|
171
|
-
const degrees = new Map<string, number>();
|
|
172
|
-
|
|
173
|
-
for (const relation of relations) {
|
|
174
|
-
degrees.set(relation.from, (degrees.get(relation.from) ?? 0) + 1);
|
|
175
|
-
degrees.set(relation.to, (degrees.get(relation.to) ?? 0) + 1);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return degrees;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function entityCatalog(data: ContextData): Map<string, JsonObject> {
|
|
182
|
-
const catalog = new Map<string, JsonObject>();
|
|
183
|
-
|
|
184
|
-
for (const file of data.documents) {
|
|
185
|
-
catalog.set(file.id, {
|
|
186
|
-
id: file.id,
|
|
187
|
-
type: "File",
|
|
188
|
-
label: file.path,
|
|
189
|
-
status: file.status,
|
|
190
|
-
source_of_truth: file.source_of_truth
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
for (const rule of data.rules) {
|
|
195
|
-
catalog.set(rule.id, {
|
|
196
|
-
id: rule.id,
|
|
197
|
-
type: "Rule",
|
|
198
|
-
label: rule.title,
|
|
199
|
-
status: rule.status,
|
|
200
|
-
source_of_truth: rule.source_of_truth
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
for (const adr of data.adrs) {
|
|
205
|
-
catalog.set(adr.id, {
|
|
206
|
-
id: adr.id,
|
|
207
|
-
type: "ADR",
|
|
208
|
-
label: adr.title || adr.id,
|
|
209
|
-
status: adr.status,
|
|
210
|
-
source_of_truth: adr.source_of_truth
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return catalog;
|
|
215
|
-
}
|
|
46
|
+
const MIN_LEXICAL_RELEVANCE = 0.05;
|
|
47
|
+
const MIN_VECTOR_RELEVANCE = 0.2;
|
|
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
|
+
]);
|
|
216
59
|
|
|
217
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;
|
|
218
66
|
const data = await loadContextData();
|
|
219
|
-
const
|
|
220
|
-
const
|
|
67
|
+
const allRelations = [...data.relations, ...buildChunkPartOfRelations(data)];
|
|
68
|
+
const degreeByEntity = relationDegree(allRelations);
|
|
69
|
+
const queryTokens = expandQueryTokens(Array.from(new Set(tokenize(parsed.query))));
|
|
70
|
+
const queryPhrase = normalizeText(parsed.query).trim();
|
|
71
|
+
const candidates = buildSearchEntities(data, includeContent).filter(
|
|
221
72
|
(entity) => parsed.include_deprecated || entity.status.toLowerCase() !== "deprecated"
|
|
222
73
|
);
|
|
223
74
|
const embeddings = loadEmbeddingIndex();
|
|
@@ -226,58 +77,35 @@ export async function runContextSearch(parsed: SearchParams): Promise<ToolPayloa
|
|
|
226
77
|
? await embedQuery(parsed.query, embeddings.model)
|
|
227
78
|
: null;
|
|
228
79
|
|
|
229
|
-
const results =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (entity.source_of_truth) {
|
|
250
|
-
score += 0.1;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
id: entity.id,
|
|
255
|
-
entity_type: entity.entity_type,
|
|
256
|
-
kind: entity.kind,
|
|
257
|
-
title: entity.label,
|
|
258
|
-
path: entity.path || undefined,
|
|
259
|
-
score: Number(score.toFixed(4)),
|
|
260
|
-
semantic_score: Number(semantic.toFixed(4)),
|
|
261
|
-
embedding_score: Number(vectorSemantic.toFixed(4)),
|
|
262
|
-
lexical_score: Number(lexicalSemantic.toFixed(4)),
|
|
263
|
-
graph_score: Number(graphScore.toFixed(4)),
|
|
264
|
-
source_of_truth: entity.source_of_truth,
|
|
265
|
-
status: entity.status,
|
|
266
|
-
updated_at: entity.updated_at,
|
|
267
|
-
matched_rules: entity.matched_rules,
|
|
268
|
-
excerpt: entity.snippet,
|
|
269
|
-
content: parsed.include_content ? entity.content : undefined
|
|
270
|
-
};
|
|
271
|
-
})
|
|
272
|
-
.filter((result) => result.score > 0)
|
|
273
|
-
.sort((a, b) => b.score - a.score)
|
|
274
|
-
.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
|
+
});
|
|
275
99
|
|
|
276
100
|
const warningMessages = [data.warning, embeddings.warning, getEmbeddingRuntimeWarning()].filter(Boolean);
|
|
277
101
|
|
|
278
102
|
return {
|
|
279
103
|
query: parsed.query,
|
|
280
104
|
top_k: parsed.top_k,
|
|
105
|
+
response_preset: responsePreset,
|
|
106
|
+
include_scores: includeScores,
|
|
107
|
+
include_matched_rules: includeMatchedRules,
|
|
108
|
+
include_content: includeContent,
|
|
281
109
|
ranking: data.ranking,
|
|
282
110
|
total_candidates: candidates.length,
|
|
283
111
|
context_source: data.source,
|
|
@@ -289,124 +117,132 @@ export async function runContextSearch(parsed: SearchParams): Promise<ToolPayloa
|
|
|
289
117
|
}
|
|
290
118
|
|
|
291
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;
|
|
292
124
|
const data = await loadContextData();
|
|
293
125
|
const catalog = entityCatalog(data);
|
|
126
|
+
const relations = [...data.relations, ...buildChunkPartOfRelations(data)];
|
|
127
|
+
const relatedResponseMeta = buildRelatedResponseMeta({
|
|
128
|
+
parsed,
|
|
129
|
+
responsePreset,
|
|
130
|
+
includeEdges,
|
|
131
|
+
includeEntityMetadata,
|
|
132
|
+
contextSource: data.source
|
|
133
|
+
});
|
|
294
134
|
|
|
295
135
|
if (!catalog.has(parsed.entity_id)) {
|
|
296
|
-
return {
|
|
297
|
-
|
|
298
|
-
depth: parsed.depth,
|
|
299
|
-
related: [],
|
|
300
|
-
edges: [],
|
|
301
|
-
context_source: data.source,
|
|
136
|
+
return buildEmptyRelatedResponse({
|
|
137
|
+
meta: relatedResponseMeta,
|
|
302
138
|
warning: "Entity not found in indexed context."
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const outgoing = new Map<string, RelationRecord[]>();
|
|
307
|
-
const incoming = new Map<string, RelationRecord[]>();
|
|
308
|
-
|
|
309
|
-
for (const relation of data.relations) {
|
|
310
|
-
const outList = outgoing.get(relation.from) ?? [];
|
|
311
|
-
outList.push(relation);
|
|
312
|
-
outgoing.set(relation.from, outList);
|
|
313
|
-
|
|
314
|
-
const inList = incoming.get(relation.to) ?? [];
|
|
315
|
-
inList.push(relation);
|
|
316
|
-
incoming.set(relation.to, inList);
|
|
139
|
+
});
|
|
317
140
|
}
|
|
318
141
|
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const current = queue.shift() as { id: string; hop: number };
|
|
327
|
-
if (current.hop >= parsed.depth) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const neighbors = [
|
|
332
|
-
...(outgoing.get(current.id) ?? []).map((edge) => ({
|
|
333
|
-
edge,
|
|
334
|
-
next: edge.to,
|
|
335
|
-
direction: "outgoing"
|
|
336
|
-
})),
|
|
337
|
-
...(incoming.get(current.id) ?? []).map((edge) => ({
|
|
338
|
-
edge,
|
|
339
|
-
next: edge.from,
|
|
340
|
-
direction: "incoming"
|
|
341
|
-
}))
|
|
342
|
-
];
|
|
343
|
-
|
|
344
|
-
for (const neighbor of neighbors) {
|
|
345
|
-
const target = neighbor.next;
|
|
346
|
-
if (!seen.has(target)) {
|
|
347
|
-
seen.add(target);
|
|
348
|
-
queue.push({ id: target, hop: current.hop + 1 });
|
|
349
|
-
|
|
350
|
-
const entity = catalog.get(target) ?? {
|
|
351
|
-
id: target,
|
|
352
|
-
type: "Unknown",
|
|
353
|
-
label: target,
|
|
354
|
-
status: "unknown",
|
|
355
|
-
source_of_truth: false
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
related.push({
|
|
359
|
-
...entity,
|
|
360
|
-
hops: current.hop + 1,
|
|
361
|
-
via_relation: neighbor.edge.relation,
|
|
362
|
-
direction: neighbor.direction
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const edgeKey = `${neighbor.edge.from}|${neighbor.edge.relation}|${neighbor.edge.to}|${neighbor.edge.note}`;
|
|
367
|
-
if (!traversedEdgeKeys.has(edgeKey)) {
|
|
368
|
-
traversedEdgeKeys.add(edgeKey);
|
|
369
|
-
traversedEdges.push({
|
|
370
|
-
from: neighbor.edge.from,
|
|
371
|
-
to: neighbor.edge.to,
|
|
372
|
-
relation: neighbor.edge.relation,
|
|
373
|
-
note: neighbor.edge.note
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
142
|
+
const { related, traversedEdges } = traverseRelatedGraph({
|
|
143
|
+
entityId: parsed.entity_id,
|
|
144
|
+
relations,
|
|
145
|
+
depth: parsed.depth,
|
|
146
|
+
catalog,
|
|
147
|
+
includeEntityMetadata
|
|
148
|
+
});
|
|
378
149
|
|
|
379
150
|
return {
|
|
380
|
-
|
|
381
|
-
depth: parsed.depth,
|
|
382
|
-
context_source: data.source,
|
|
151
|
+
...relatedResponseMeta,
|
|
383
152
|
warning: data.warning,
|
|
384
153
|
related,
|
|
385
|
-
edges:
|
|
154
|
+
edges: includeEdges ? traversedEdges : []
|
|
386
155
|
};
|
|
387
156
|
}
|
|
388
157
|
|
|
389
|
-
export async function
|
|
158
|
+
export async function runContextImpact(parsed: ImpactParams): Promise<ToolPayload> {
|
|
390
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
|
+
}
|
|
391
199
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
200
|
+
if (!catalog.has(seedId)) {
|
|
201
|
+
return buildEmptyImpactResponse({
|
|
202
|
+
meta: impactResponseMeta,
|
|
203
|
+
warning: "Seed entity not found in indexed context."
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
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
|
+
});
|
|
404
237
|
|
|
405
238
|
return {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
239
|
+
...impactResponseMeta,
|
|
240
|
+
resolved_seed_id: seedId,
|
|
241
|
+
resolved_from_query: !parsed.entity_id,
|
|
242
|
+
seed: catalog.get(seedId),
|
|
409
243
|
warning: data.warning,
|
|
410
|
-
|
|
244
|
+
query_results: seedResolution.query_results,
|
|
245
|
+
results,
|
|
246
|
+
edges: parsed.include_edges && verbosePaths ? traversedEdges : []
|
|
411
247
|
};
|
|
412
248
|
}
|