@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.
Files changed (61) hide show
  1. package/README.md +64 -16
  2. package/bin/cortex.mjs +32 -60
  3. package/package.json +17 -3
  4. package/scaffold/.context/ontology.cypher +47 -0
  5. package/scaffold/.githooks/post-commit +14 -0
  6. package/scaffold/.githooks/post-rewrite +23 -0
  7. package/scaffold/mcp/package-lock.json +19 -23
  8. package/scaffold/mcp/package.json +3 -1
  9. package/scaffold/mcp/src/contextEntities.ts +311 -0
  10. package/scaffold/mcp/src/defaults.ts +6 -0
  11. package/scaffold/mcp/src/embed.ts +163 -37
  12. package/scaffold/mcp/src/frontmatter.ts +39 -0
  13. package/scaffold/mcp/src/graph.ts +330 -109
  14. package/scaffold/mcp/src/graphMetrics.ts +12 -0
  15. package/scaffold/mcp/src/impactPresentation.ts +202 -0
  16. package/scaffold/mcp/src/impactRanking.ts +237 -0
  17. package/scaffold/mcp/src/impactResponse.ts +47 -0
  18. package/scaffold/mcp/src/impactResults.ts +173 -0
  19. package/scaffold/mcp/src/impactSeed.ts +33 -0
  20. package/scaffold/mcp/src/impactTraversal.ts +83 -0
  21. package/scaffold/mcp/src/jsonl.ts +34 -0
  22. package/scaffold/mcp/src/loadGraph.ts +345 -86
  23. package/scaffold/mcp/src/paths.ts +24 -2
  24. package/scaffold/mcp/src/presets.ts +137 -0
  25. package/scaffold/mcp/src/relatedResponse.ts +30 -0
  26. package/scaffold/mcp/src/relatedTraversal.ts +101 -0
  27. package/scaffold/mcp/src/rules.ts +27 -0
  28. package/scaffold/mcp/src/search.ts +191 -355
  29. package/scaffold/mcp/src/searchCore.ts +274 -0
  30. package/scaffold/mcp/src/searchResults.ts +133 -0
  31. package/scaffold/mcp/src/server.ts +95 -3
  32. package/scaffold/mcp/src/types.ts +99 -3
  33. package/scaffold/scripts/context.sh +12 -46
  34. package/scaffold/scripts/dashboard.mjs +797 -0
  35. package/scaffold/scripts/dashboard.sh +13 -0
  36. package/scaffold/scripts/ingest.mjs +2219 -59
  37. package/scaffold/scripts/install-git-hooks.sh +3 -1
  38. package/scaffold/scripts/memory-compile.mjs +232 -0
  39. package/scaffold/scripts/memory-compile.sh +20 -0
  40. package/scaffold/scripts/memory-lint.mjs +375 -0
  41. package/scaffold/scripts/memory-lint.sh +20 -0
  42. package/scaffold/scripts/parsers/config.mjs +178 -0
  43. package/scaffold/scripts/parsers/cpp.mjs +316 -0
  44. package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
  45. package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
  46. package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
  47. package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
  48. package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
  49. package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
  50. package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
  51. package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
  52. package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
  53. package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
  54. package/scaffold/scripts/parsers/javascript.mjs +27 -350
  55. package/scaffold/scripts/parsers/resources.mjs +166 -0
  56. package/scaffold/scripts/parsers/sql.mjs +137 -0
  57. package/scaffold/scripts/parsers/vbnet.mjs +143 -0
  58. package/scaffold/scripts/status.sh +15 -8
  59. package/scaffold/scripts/capture-note.sh +0 -55
  60. package/scaffold/scripts/plan-state-engine.cjs +0 -310
  61. 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
- ContextData,
5
- JsonObject,
39
+ ImpactParams,
6
40
  RelatedParams,
7
- RelationRecord,
8
- RulesParams,
9
- SearchEntity,
41
+ RelationType,
10
42
  SearchParams,
11
43
  ToolPayload
12
44
  } from "./types.js";
13
45
 
14
- function tokenize(value: string): string[] {
15
- return value
16
- .toLowerCase()
17
- .split(/[^a-z0-9]+/g)
18
- .map((part) => part.trim())
19
- .filter((part) => part.length >= 2);
20
- }
21
-
22
- function daysSince(isoDate: string): number {
23
- const timestamp = Date.parse(isoDate);
24
- if (Number.isNaN(timestamp)) {
25
- return 3650;
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 degreeByEntity = relationDegree(data.relations);
220
- const candidates = buildSearchEntities(data, parsed.include_content).filter(
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 = candidates
230
- .map((entity) => {
231
- const lexicalSemantic = semanticScore(parsed.query, entity.text);
232
- const entityVector = embeddings.vectors.get(entity.id);
233
- const vectorSemantic =
234
- queryVector && entityVector
235
- ? Math.max(0, Math.min(1, (cosineSimilarity(queryVector, entityVector) + 1) / 2))
236
- : 0;
237
- const semantic =
238
- vectorSemantic > 0 ? vectorSemantic * 0.75 + lexicalSemantic * 0.25 : lexicalSemantic;
239
- const graphScore = Math.min(1, (degreeByEntity.get(entity.id) ?? 0) / 4);
240
- const trustScore = Math.max(0, Math.min(1, entity.trust_level / 100));
241
- const dateScore = recencyScore(entity.updated_at);
242
-
243
- let score = 0;
244
- score += data.ranking.semantic * semantic;
245
- score += data.ranking.graph * graphScore;
246
- score += data.ranking.trust * trustScore;
247
- score += data.ranking.recency * dateScore;
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
- entity_id: parsed.entity_id,
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 seen = new Set<string>([parsed.entity_id]);
320
- const queue: Array<{ id: string; hop: number }> = [{ id: parsed.entity_id, hop: 0 }];
321
- const related: JsonObject[] = [];
322
- const traversedEdges: JsonObject[] = [];
323
- const traversedEdgeKeys = new Set<string>();
324
-
325
- while (queue.length > 0) {
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
- entity_id: parsed.entity_id,
381
- depth: parsed.depth,
382
- context_source: data.source,
151
+ ...relatedResponseMeta,
383
152
  warning: data.warning,
384
153
  related,
385
- edges: parsed.include_edges ? traversedEdges : []
154
+ edges: includeEdges ? traversedEdges : []
386
155
  };
387
156
  }
388
157
 
389
- export async function runContextRules(parsed: RulesParams): Promise<ToolPayload> {
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
- const rules = data.rules
393
- .filter((rule) => parsed.include_inactive || rule.status === "active")
394
- .filter((rule) => !parsed.scope || rule.scope === parsed.scope || rule.scope === "global")
395
- .sort((a, b) => b.priority - a.priority)
396
- .map((rule) => ({
397
- id: rule.id,
398
- title: rule.title,
399
- description: rule.body,
400
- priority: rule.priority,
401
- scope: rule.scope,
402
- status: rule.status
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
- scope: parsed.scope ?? "global",
407
- count: rules.length,
408
- context_source: data.source,
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
- rules
244
+ query_results: seedResolution.query_results,
245
+ results,
246
+ edges: parsed.include_edges && verbosePaths ? traversedEdges : []
411
247
  };
412
248
  }