@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.
Files changed (64) hide show
  1. package/README.md +125 -42
  2. package/bin/cortex.mjs +36 -63
  3. package/bin/wsl.mjs +30 -0
  4. package/package.json +15 -3
  5. package/scaffold/.context/ontology.cypher +47 -0
  6. package/scaffold/.githooks/post-commit +14 -0
  7. package/scaffold/.githooks/post-rewrite +23 -0
  8. package/scaffold/mcp/package-lock.json +16 -16
  9. package/scaffold/mcp/package.json +4 -1
  10. package/scaffold/mcp/src/contextEntities.ts +311 -0
  11. package/scaffold/mcp/src/defaults.ts +6 -0
  12. package/scaffold/mcp/src/embed.ts +163 -37
  13. package/scaffold/mcp/src/frontmatter.ts +39 -0
  14. package/scaffold/mcp/src/graph.ts +253 -130
  15. package/scaffold/mcp/src/graphMetrics.ts +12 -0
  16. package/scaffold/mcp/src/impactPresentation.ts +202 -0
  17. package/scaffold/mcp/src/impactRanking.ts +237 -0
  18. package/scaffold/mcp/src/impactResponse.ts +47 -0
  19. package/scaffold/mcp/src/impactResults.ts +173 -0
  20. package/scaffold/mcp/src/impactSeed.ts +33 -0
  21. package/scaffold/mcp/src/impactTraversal.ts +83 -0
  22. package/scaffold/mcp/src/jsonl.ts +34 -0
  23. package/scaffold/mcp/src/loadGraph.ts +345 -86
  24. package/scaffold/mcp/src/paths.ts +33 -2
  25. package/scaffold/mcp/src/presets.ts +137 -0
  26. package/scaffold/mcp/src/relatedResponse.ts +30 -0
  27. package/scaffold/mcp/src/relatedTraversal.ts +101 -0
  28. package/scaffold/mcp/src/rules.ts +27 -0
  29. package/scaffold/mcp/src/search.ts +186 -455
  30. package/scaffold/mcp/src/searchCore.ts +274 -0
  31. package/scaffold/mcp/src/searchResults.ts +133 -0
  32. package/scaffold/mcp/src/server.ts +95 -3
  33. package/scaffold/mcp/src/types.ts +82 -3
  34. package/scaffold/scripts/context.sh +12 -46
  35. package/scaffold/scripts/dashboard.mjs +797 -0
  36. package/scaffold/scripts/dashboard.sh +13 -0
  37. package/scaffold/scripts/ingest.mjs +2227 -59
  38. package/scaffold/scripts/install-git-hooks.sh +3 -1
  39. package/scaffold/scripts/memory-compile.mjs +241 -0
  40. package/scaffold/scripts/memory-compile.sh +20 -0
  41. package/scaffold/scripts/memory-lint.mjs +384 -0
  42. package/scaffold/scripts/memory-lint.sh +20 -0
  43. package/scaffold/scripts/parsers/config.mjs +178 -0
  44. package/scaffold/scripts/parsers/cpp.mjs +316 -0
  45. package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
  46. package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
  47. package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
  48. package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
  49. package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
  50. package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
  51. package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
  52. package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
  53. package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
  54. package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
  55. package/scaffold/scripts/parsers/javascript.mjs +27 -350
  56. package/scaffold/scripts/parsers/resources.mjs +166 -0
  57. package/scaffold/scripts/parsers/rust.mjs +515 -0
  58. package/scaffold/scripts/parsers/sql.mjs +137 -0
  59. package/scaffold/scripts/parsers/vbnet.mjs +143 -0
  60. package/scaffold/scripts/status.sh +0 -7
  61. package/scaffold/scripts/watch.sh +9 -1
  62. package/scaffold/scripts/capture-note.sh +0 -55
  63. package/scaffold/scripts/plan-state-engine.cjs +0 -310
  64. package/scaffold/scripts/plan-state.sh +0 -71
@@ -0,0 +1,202 @@
1
+ import type { ImpactParams, JsonObject, SearchEntity } from "./types.js";
2
+
3
+ export type ImpactTraversalStep = {
4
+ hops: number;
5
+ via_relation: string;
6
+ direction: string;
7
+ via_entity: string;
8
+ via_note?: string;
9
+ };
10
+
11
+ function impactEntityLabel(
12
+ entityId: string,
13
+ catalog: Map<string, JsonObject>,
14
+ searchEntities: Map<string, SearchEntity>
15
+ ): string {
16
+ const searchEntity = searchEntities.get(entityId);
17
+ if (searchEntity?.label) {
18
+ return searchEntity.label;
19
+ }
20
+ const catalogEntry = catalog.get(entityId);
21
+ return String(catalogEntry?.label ?? entityId);
22
+ }
23
+
24
+ export function buildImpactPath(
25
+ targetId: string,
26
+ seedId: string,
27
+ visited: Map<string, ImpactTraversalStep>,
28
+ catalog: Map<string, JsonObject>,
29
+ searchEntities: Map<string, SearchEntity>
30
+ ): { summary: string; entities: string[]; edges: JsonObject[] } {
31
+ const entities = [targetId];
32
+ const edges: JsonObject[] = [];
33
+ let currentId = targetId;
34
+
35
+ while (currentId !== seedId) {
36
+ const metadata = visited.get(currentId);
37
+ if (!metadata) {
38
+ break;
39
+ }
40
+
41
+ const from = metadata.direction === "outgoing" ? metadata.via_entity : currentId;
42
+ const to = metadata.direction === "outgoing" ? currentId : metadata.via_entity;
43
+ edges.push({
44
+ from,
45
+ to,
46
+ relation: metadata.via_relation,
47
+ note: metadata.via_note ?? ""
48
+ });
49
+ entities.push(metadata.via_entity);
50
+ currentId = metadata.via_entity;
51
+ }
52
+
53
+ entities.reverse();
54
+ edges.reverse();
55
+
56
+ const labels = entities.map((entityId) => impactEntityLabel(entityId, catalog, searchEntities));
57
+ const summaryParts = [];
58
+ for (let index = 0; index < labels.length; index += 1) {
59
+ summaryParts.push(labels[index]);
60
+ if (index < edges.length) {
61
+ const edge = edges[index];
62
+ const note = edge.note ? `(${String(edge.note)})` : "";
63
+ summaryParts.push(`-[${String(edge.relation)}${note}]->`);
64
+ }
65
+ }
66
+
67
+ return {
68
+ summary: summaryParts.join(" "),
69
+ entities,
70
+ edges
71
+ };
72
+ }
73
+
74
+ export function buildCompactImpactSummary(
75
+ entities: string[],
76
+ edges: JsonObject[],
77
+ catalog: Map<string, JsonObject>,
78
+ searchEntities: Map<string, SearchEntity>,
79
+ maxPathHopsShown: number
80
+ ): string {
81
+ const labels = entities.map((entityId) => impactEntityLabel(entityId, catalog, searchEntities));
82
+ if (labels.length <= 3 || edges.length <= maxPathHopsShown) {
83
+ const summaryParts = [];
84
+ for (let index = 0; index < labels.length; index += 1) {
85
+ summaryParts.push(labels[index]);
86
+ if (index < edges.length) {
87
+ const edge = edges[index];
88
+ const note = edge.note ? `(${String(edge.note)})` : "";
89
+ summaryParts.push(`-[${String(edge.relation)}${note}]->`);
90
+ }
91
+ }
92
+ return summaryParts.join(" ");
93
+ }
94
+
95
+ const headEdgeCount = Math.max(1, Math.ceil(maxPathHopsShown / 2));
96
+ const tailEdgeCount = Math.max(0, Math.floor(maxPathHopsShown / 2));
97
+ const hiddenHopCount = Math.max(0, edges.length - headEdgeCount - tailEdgeCount);
98
+ const hiddenText = hiddenHopCount === 1 ? "1 more hop" : `${hiddenHopCount} more hops`;
99
+ const summaryParts = [labels[0]];
100
+
101
+ for (let index = 0; index < headEdgeCount; index += 1) {
102
+ const edge = edges[index];
103
+ const note = edge.note ? `(${String(edge.note)})` : "";
104
+ summaryParts.push(`-[${String(edge.relation)}${note}]->`);
105
+ summaryParts.push(labels[index + 1]);
106
+ }
107
+
108
+ summaryParts.push(`... ${hiddenText} ...`);
109
+
110
+ const tailStart = edges.length - tailEdgeCount;
111
+ const tailEntityStart = labels.length - tailEdgeCount - 1;
112
+ for (let index = tailStart; index < edges.length; index += 1) {
113
+ const edge = edges[index];
114
+ const note = edge.note ? `(${String(edge.note)})` : "";
115
+ const labelIndex = tailEntityStart + (index - tailStart);
116
+ summaryParts.push(`-[${String(edge.relation)}${note}]->`);
117
+ summaryParts.push(labels[labelIndex + 1]);
118
+ }
119
+
120
+ return summaryParts.join(" ");
121
+ }
122
+
123
+ function formatImpactRelationLabel(relation: string): string {
124
+ return relation.toLowerCase().replaceAll("_", " ");
125
+ }
126
+
127
+ function formatImpactRelationWithNote(edge: JsonObject): string {
128
+ const relationLabel = formatImpactRelationLabel(String(edge.relation ?? ""));
129
+ const note = String(edge.note ?? "").trim();
130
+ if (!note) {
131
+ return relationLabel;
132
+ }
133
+ return `${relationLabel} (${note})`;
134
+ }
135
+
136
+ export function buildImpactWhy(
137
+ seedId: string,
138
+ targetId: string,
139
+ pathEdges: JsonObject[],
140
+ hops: number,
141
+ catalog: Map<string, JsonObject>,
142
+ searchEntities: Map<string, SearchEntity>
143
+ ): string {
144
+ const seedLabel = impactEntityLabel(seedId, catalog, searchEntities);
145
+ const targetLabel = impactEntityLabel(targetId, catalog, searchEntities);
146
+ const relationLabels = [...new Set(pathEdges.map((edge) => formatImpactRelationWithNote(edge)))];
147
+
148
+ if (relationLabels.length === 0) {
149
+ return `${targetLabel} is reachable from ${seedLabel}.`;
150
+ }
151
+
152
+ const relationText =
153
+ relationLabels.length === 1
154
+ ? relationLabels[0]
155
+ : `${relationLabels.slice(0, -1).join(", ")} and ${relationLabels[relationLabels.length - 1]}`;
156
+ const hopText = hops === 1 ? "1 hop" : `${hops} hops`;
157
+ return `${targetLabel} is impacted from ${seedLabel} via ${relationText} in ${hopText}.`;
158
+ }
159
+
160
+ export function buildImpactTopReasons(params: {
161
+ hops: number;
162
+ profile: NonNullable<ImpactParams["profile"]>;
163
+ semanticScore: number;
164
+ noteScore: number;
165
+ profileScore: number;
166
+ impactDomains: string[];
167
+ pathEdges: JsonObject[];
168
+ }): string[] {
169
+ const reasons: string[] = [];
170
+
171
+ reasons.push(params.hops === 1 ? "1-hop path" : `${params.hops}-hop path`);
172
+
173
+ if (params.impactDomains.length > 0) {
174
+ reasons.push(`domains: ${params.impactDomains.join(", ")}`);
175
+ }
176
+
177
+ if (params.noteScore > 0) {
178
+ const notedEdges = params.pathEdges
179
+ .map((edge) => ({
180
+ relation: formatImpactRelationLabel(String(edge.relation ?? "")),
181
+ note: String(edge.note ?? "").trim()
182
+ }))
183
+ .filter((edge) => edge.note.length > 0)
184
+ .slice(0, 2)
185
+ .map((edge) => `${edge.relation} (${edge.note})`);
186
+ if (notedEdges.length > 0) {
187
+ reasons.push(`note match: ${notedEdges.join(", ")}`);
188
+ } else {
189
+ reasons.push("note match");
190
+ }
191
+ }
192
+
193
+ if (params.profileScore > 0) {
194
+ reasons.push(`profile boost: ${params.profile}`);
195
+ }
196
+
197
+ if (params.semanticScore > 0.15) {
198
+ reasons.push("entity text matched query");
199
+ }
200
+
201
+ return reasons.slice(0, 4);
202
+ }
@@ -0,0 +1,237 @@
1
+ import type { ImpactParams, JsonObject, RelationType, 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 IMPACT_RELATION_TYPE_LIST: RelationType[] = [
9
+ "CALLS",
10
+ "CALLS_SQL",
11
+ "IMPORTS",
12
+ "USES_CONFIG_KEY",
13
+ "USES_RESOURCE_KEY",
14
+ "USES_SETTING_KEY",
15
+ "USES_CONFIG",
16
+ "TRANSFORMS_CONFIG",
17
+ "PART_OF"
18
+ ];
19
+ const IMPACT_PROFILE_RELATIONS: Record<
20
+ NonNullable<ImpactParams["profile"]>,
21
+ RelationType[]
22
+ > = {
23
+ all: IMPACT_RELATION_TYPE_LIST,
24
+ config_only: ["USES_CONFIG_KEY", "USES_RESOURCE_KEY", "USES_SETTING_KEY", "USES_CONFIG", "TRANSFORMS_CONFIG", "PART_OF"],
25
+ config_to_sql: ["USES_CONFIG_KEY", "USES_RESOURCE_KEY", "USES_SETTING_KEY", "USES_CONFIG", "TRANSFORMS_CONFIG", "CALLS_SQL", "PART_OF"],
26
+ code_only: ["CALLS", "IMPORTS", "PART_OF"],
27
+ sql_only: ["CALLS_SQL", "PART_OF"]
28
+ };
29
+
30
+ function normalizeText(value: string): string {
31
+ return value.normalize("NFKC").toLowerCase();
32
+ }
33
+
34
+ function pathHasExtension(pathValue: string, extensions: string[]): boolean {
35
+ const normalized = normalizeText(pathValue);
36
+ return extensions.some((extension) => normalized.endsWith(extension));
37
+ }
38
+
39
+ export function impactBaseScore(hops: number, graphScore: number, trustScore: number, semantic = 0): number {
40
+ const hopScore = 1 / (1 + Math.max(0, hops));
41
+ const score = hopScore * 0.55 + graphScore * 0.2 + trustScore * 0.15 + semantic * 0.1;
42
+ return Number(score.toFixed(4));
43
+ }
44
+
45
+ export function resolveImpactRelationTypes(parsed: ImpactParams): Set<RelationType> {
46
+ if (Array.isArray(parsed.relation_types) && parsed.relation_types.length > 0) {
47
+ return new Set(parsed.relation_types);
48
+ }
49
+
50
+ const profile = parsed.profile ?? "all";
51
+ return new Set(IMPACT_PROFILE_RELATIONS[profile]);
52
+ }
53
+
54
+ export function resolveImpactResultDomains(parsed: ImpactParams): Set<string> | null {
55
+ if (!Array.isArray(parsed.result_domains) || parsed.result_domains.length === 0) {
56
+ return null;
57
+ }
58
+ return new Set(parsed.result_domains.map((domain) => normalizeText(domain)));
59
+ }
60
+
61
+ export function resolveImpactResultEntityTypes(parsed: ImpactParams): Set<string> | null {
62
+ if (!Array.isArray(parsed.result_entity_types) || parsed.result_entity_types.length === 0) {
63
+ return null;
64
+ }
65
+ return new Set(parsed.result_entity_types.map((entityType) => normalizeText(entityType)));
66
+ }
67
+
68
+ export function resolveImpactPathMustInclude(parsed: ImpactParams): Set<string> | null {
69
+ if (!Array.isArray(parsed.path_must_include) || parsed.path_must_include.length === 0) {
70
+ return null;
71
+ }
72
+ return new Set(parsed.path_must_include.map((relation) => normalizeText(relation)));
73
+ }
74
+
75
+ export function resolveImpactPathMustExclude(parsed: ImpactParams): Set<string> | null {
76
+ if (!Array.isArray(parsed.path_must_exclude) || parsed.path_must_exclude.length === 0) {
77
+ return null;
78
+ }
79
+ return new Set(parsed.path_must_exclude.map((relation) => normalizeText(relation)));
80
+ }
81
+
82
+ export function impactResultComparator(
83
+ sortBy: NonNullable<ImpactParams["sort_by"]>
84
+ ): (a: Record<string, unknown>, b: Record<string, unknown>) => number {
85
+ return (a, b) => {
86
+ const aHops = Number(a.hops ?? Number.POSITIVE_INFINITY);
87
+ const bHops = Number(b.hops ?? Number.POSITIVE_INFINITY);
88
+ const aImpact = Number(a.impact_score ?? 0);
89
+ const bImpact = Number(b.impact_score ?? 0);
90
+ const aSemantic = Number(a.semantic_score ?? 0);
91
+ const bSemantic = Number(b.semantic_score ?? 0);
92
+ const aGraph = Number(a.graph_score ?? 0);
93
+ const bGraph = Number(b.graph_score ?? 0);
94
+ const aTrust = Number(a.trust_score ?? 0);
95
+ const bTrust = Number(b.trust_score ?? 0);
96
+
97
+ if (sortBy === "shortest_path") {
98
+ return aHops - bHops || bImpact - aImpact || bSemantic - aSemantic;
99
+ }
100
+ if (sortBy === "semantic_score") {
101
+ return bSemantic - aSemantic || bImpact - aImpact || aHops - bHops;
102
+ }
103
+ if (sortBy === "graph_score") {
104
+ return bGraph - aGraph || bImpact - aImpact || aHops - bHops;
105
+ }
106
+ if (sortBy === "trust_score") {
107
+ return bTrust - aTrust || bImpact - aImpact || aHops - bHops;
108
+ }
109
+ return bImpact - aImpact || aHops - bHops || bSemantic - aSemantic;
110
+ };
111
+ }
112
+
113
+ export function impactDomainsForEntity(
114
+ entity: SearchEntity | undefined,
115
+ catalogEntry: JsonObject | undefined
116
+ ): string[] {
117
+ const domains = new Set<string>();
118
+ const normalizedKind = normalizeText(entity?.kind ?? "");
119
+ const normalizedType = normalizeText(entity?.entity_type ?? String(catalogEntry?.type ?? ""));
120
+ const pathValue = String(entity?.path ?? catalogEntry?.path ?? "");
121
+
122
+ if (SQL_ENTITY_KINDS.has(normalizedKind) || pathHasExtension(pathValue, SQL_LIKE_EXTENSIONS)) {
123
+ domains.add("sql");
124
+ }
125
+
126
+ if (
127
+ normalizedKind === "connection_string" ||
128
+ normalizedKind === "database_target" ||
129
+ normalizedKind === "app_setting" ||
130
+ pathHasExtension(pathValue, CONFIG_LIKE_EXTENSIONS)
131
+ ) {
132
+ domains.add("config");
133
+ }
134
+
135
+ if (normalizedKind === "resource_entry" || pathHasExtension(pathValue, RESOURCE_LIKE_EXTENSIONS)) {
136
+ domains.add("resource");
137
+ domains.add("config");
138
+ }
139
+
140
+ if (normalizedKind === "setting_entry" || pathHasExtension(pathValue, SETTINGS_LIKE_EXTENSIONS)) {
141
+ domains.add("settings");
142
+ domains.add("config");
143
+ }
144
+
145
+ if (normalizedType === "project") {
146
+ domains.add("project");
147
+ }
148
+
149
+ if (
150
+ !domains.has("sql") &&
151
+ !domains.has("config") &&
152
+ !domains.has("resource") &&
153
+ !domains.has("settings") &&
154
+ (normalizedType === "file" || normalizedType === "chunk" || normalizedType === "module")
155
+ ) {
156
+ domains.add("code");
157
+ }
158
+
159
+ return [...domains];
160
+ }
161
+
162
+ export function impactProfileBoost(
163
+ profile: NonNullable<ImpactParams["profile"]>,
164
+ domains: string[],
165
+ pathEdges: JsonObject[]
166
+ ): number {
167
+ const relationTypes = new Set(pathEdges.map((edge) => String(edge.relation ?? "")));
168
+ const hasSqlPath = relationTypes.has("CALLS_SQL");
169
+ const hasConfigKeyPath =
170
+ relationTypes.has("USES_CONFIG_KEY") ||
171
+ relationTypes.has("USES_RESOURCE_KEY") ||
172
+ relationTypes.has("USES_SETTING_KEY") ||
173
+ relationTypes.has("USES_CONFIG");
174
+
175
+ let boost = 0;
176
+
177
+ if (profile === "config_to_sql") {
178
+ if (domains.includes("sql")) {
179
+ boost += 0.18;
180
+ }
181
+ if (domains.includes("config")) {
182
+ boost += 0.04;
183
+ }
184
+ if (hasSqlPath) {
185
+ boost += 0.08;
186
+ }
187
+ if (hasConfigKeyPath && hasSqlPath) {
188
+ boost += 0.08;
189
+ }
190
+ } else if (profile === "config_only") {
191
+ if (domains.includes("config")) {
192
+ boost += 0.08;
193
+ }
194
+ if (!hasSqlPath && !relationTypes.has("CALLS")) {
195
+ boost += 0.04;
196
+ }
197
+ } else if (profile === "sql_only") {
198
+ if (domains.includes("sql")) {
199
+ boost += 0.14;
200
+ }
201
+ if (hasSqlPath) {
202
+ boost += 0.08;
203
+ }
204
+ } else if (profile === "code_only") {
205
+ if (domains.includes("code")) {
206
+ boost += 0.08;
207
+ }
208
+ if (relationTypes.has("CALLS") || relationTypes.has("IMPORTS")) {
209
+ boost += 0.05;
210
+ }
211
+ } else if (domains.includes("sql") && hasSqlPath) {
212
+ boost += 0.04;
213
+ }
214
+
215
+ return Number(boost.toFixed(4));
216
+ }
217
+
218
+ export function impactNoteScore(
219
+ queryTokens: string[],
220
+ queryPhrase: string,
221
+ pathEdges: JsonObject[],
222
+ semanticScorer: (queryTokens: string[], queryPhrase: string, text: string) => number
223
+ ): number {
224
+ if (pathEdges.length === 0 || (queryTokens.length === 0 && !queryPhrase)) {
225
+ return 0;
226
+ }
227
+
228
+ const noteText = pathEdges
229
+ .map((edge) => String(edge.note ?? "").trim())
230
+ .filter(Boolean)
231
+ .join("\n");
232
+ if (!noteText) {
233
+ return 0;
234
+ }
235
+
236
+ return Number(semanticScorer(queryTokens, queryPhrase, noteText).toFixed(4));
237
+ }
@@ -0,0 +1,47 @@
1
+ import type { ImpactParams, RelationType, ToolPayload } from "./types.js";
2
+
3
+ export function buildImpactResponseMeta(params: {
4
+ parsed: ImpactParams;
5
+ responsePreset: "full" | "compact" | "minimal";
6
+ includeScores: boolean;
7
+ includeReasons: boolean;
8
+ verbosePaths: boolean;
9
+ maxPathHopsShown: number;
10
+ profile: NonNullable<ImpactParams["profile"]>;
11
+ sortBy: NonNullable<ImpactParams["sort_by"]>;
12
+ allowedRelationTypes: Set<RelationType>;
13
+ contextSource: string;
14
+ }): ToolPayload {
15
+ return {
16
+ entity_id: params.parsed.entity_id,
17
+ query: params.parsed.query,
18
+ depth: params.parsed.depth,
19
+ top_k: params.parsed.top_k,
20
+ response_preset: params.responsePreset,
21
+ include_scores: params.includeScores,
22
+ include_reasons: params.includeReasons,
23
+ verbose_paths: params.verbosePaths,
24
+ max_path_hops_shown: params.maxPathHopsShown,
25
+ profile: params.profile,
26
+ sort_by: params.sortBy,
27
+ relation_types: [...params.allowedRelationTypes],
28
+ path_must_include: params.parsed.path_must_include ?? [],
29
+ path_must_exclude: params.parsed.path_must_exclude ?? [],
30
+ result_domains: params.parsed.result_domains ?? [],
31
+ result_entity_types: params.parsed.result_entity_types ?? [],
32
+ context_source: params.contextSource
33
+ };
34
+ }
35
+
36
+ export function buildEmptyImpactResponse(params: {
37
+ meta: ToolPayload;
38
+ warning: string;
39
+ }): ToolPayload {
40
+ return {
41
+ ...params.meta,
42
+ warning: params.warning,
43
+ seed: null,
44
+ results: [],
45
+ edges: []
46
+ };
47
+ }
@@ -0,0 +1,173 @@
1
+ import {
2
+ buildCompactImpactSummary,
3
+ buildImpactPath,
4
+ buildImpactTopReasons,
5
+ buildImpactWhy
6
+ } from "./impactPresentation.js";
7
+ import type { ImpactTraversalStep } from "./impactPresentation.js";
8
+ import {
9
+ impactBaseScore,
10
+ impactDomainsForEntity,
11
+ impactNoteScore,
12
+ impactProfileBoost,
13
+ impactResultComparator
14
+ } from "./impactRanking.js";
15
+ import type { ImpactParams, JsonObject, SearchEntity } from "./types.js";
16
+
17
+ function normalizeText(value: string): string {
18
+ return value.normalize("NFKC").toLowerCase();
19
+ }
20
+
21
+ function matchesImpactFilters(
22
+ result: Record<string, unknown>,
23
+ resultDomains: Set<string> | null,
24
+ resultEntityTypes: Set<string> | null,
25
+ pathMustInclude: Set<string> | null,
26
+ pathMustExclude: Set<string> | null
27
+ ): boolean {
28
+ const pathRelationTypes = new Set(
29
+ ((result.path_relation_types as string[] | undefined) ?? []).map((relation) => normalizeText(String(relation)))
30
+ );
31
+ if (pathMustInclude && pathMustInclude.size > 0) {
32
+ for (const requiredRelation of pathMustInclude) {
33
+ if (!pathRelationTypes.has(requiredRelation)) {
34
+ return false;
35
+ }
36
+ }
37
+ }
38
+ if (pathMustExclude && pathMustExclude.size > 0) {
39
+ for (const blockedRelation of pathMustExclude) {
40
+ if (pathRelationTypes.has(blockedRelation)) {
41
+ return false;
42
+ }
43
+ }
44
+ }
45
+ if (!resultDomains || resultDomains.size === 0) {
46
+ return !resultEntityTypes || resultEntityTypes.has(normalizeText(String(result.entity_type ?? "")));
47
+ }
48
+ const impactDomains = Array.isArray(result.impact_domains) ? result.impact_domains : [];
49
+ const matchesDomain = impactDomains.some((domain) => resultDomains.has(normalizeText(String(domain))));
50
+ if (!matchesDomain) {
51
+ return false;
52
+ }
53
+ return !resultEntityTypes || resultEntityTypes.has(normalizeText(String(result.entity_type ?? "")));
54
+ }
55
+
56
+ export function buildImpactResults(params: {
57
+ visited: Map<string, ImpactTraversalStep>;
58
+ seedId: string;
59
+ catalog: Map<string, JsonObject>;
60
+ searchEntities: Map<string, SearchEntity>;
61
+ degreeByEntity: Map<string, number>;
62
+ queryTokens: string[];
63
+ queryPhrase: string;
64
+ hasQuery: boolean;
65
+ profile: NonNullable<ImpactParams["profile"]>;
66
+ includeReasons: boolean;
67
+ includeScores: boolean;
68
+ verbosePaths: boolean;
69
+ maxPathHopsShown: number;
70
+ resultDomains: Set<string> | null;
71
+ resultEntityTypes: Set<string> | null;
72
+ pathMustInclude: Set<string> | null;
73
+ pathMustExclude: Set<string> | null;
74
+ sortBy: NonNullable<ImpactParams["sort_by"]>;
75
+ topK: number;
76
+ semanticScorer: (queryTokens: string[], queryPhrase: string, text: string) => number;
77
+ }): Record<string, unknown>[] {
78
+ return [...params.visited.entries()]
79
+ .filter(([id]) => id !== params.seedId)
80
+ .map(([id, metadata]) => {
81
+ const entity = params.searchEntities.get(id);
82
+ const catalogEntry = params.catalog.get(id) ?? {
83
+ id,
84
+ type: "Unknown",
85
+ label: id,
86
+ status: "unknown",
87
+ source_of_truth: false
88
+ };
89
+ const semantic =
90
+ entity && params.hasQuery ? params.semanticScorer(params.queryTokens, params.queryPhrase, entity.text) : 0;
91
+ const graphScore = Math.min(1, (params.degreeByEntity.get(id) ?? 0) / 4);
92
+ const trustScore = entity ? Math.max(0, Math.min(1, entity.trust_level / 100)) : 0.5;
93
+ const impactPath = buildImpactPath(id, params.seedId, params.visited, params.catalog, params.searchEntities);
94
+ const impactDomains = impactDomainsForEntity(entity, catalogEntry);
95
+ const profileScore = impactProfileBoost(params.profile, impactDomains, impactPath.edges);
96
+ const noteScore = impactNoteScore(
97
+ params.queryTokens,
98
+ params.queryPhrase,
99
+ impactPath.edges,
100
+ params.semanticScorer
101
+ );
102
+ const impactScore = Number(
103
+ (impactBaseScore(metadata.hops, graphScore, trustScore, semantic) + profileScore + noteScore * 0.12).toFixed(4)
104
+ );
105
+ const topReasons = buildImpactTopReasons({
106
+ hops: metadata.hops,
107
+ profile: params.profile,
108
+ semanticScore: semantic,
109
+ noteScore,
110
+ profileScore,
111
+ impactDomains,
112
+ pathEdges: impactPath.edges
113
+ });
114
+
115
+ return {
116
+ id,
117
+ entity_type: entity?.entity_type ?? String(catalogEntry.type ?? "Unknown"),
118
+ kind: entity?.kind ?? "",
119
+ title: entity?.label ?? String(catalogEntry.label ?? id),
120
+ path: entity?.path || catalogEntry.path || undefined,
121
+ hops: metadata.hops,
122
+ via_relation: metadata.via_relation,
123
+ direction: metadata.direction,
124
+ via_entity: metadata.via_entity,
125
+ impact_domains: impactDomains,
126
+ why: buildImpactWhy(params.seedId, id, impactPath.edges, metadata.hops, params.catalog, params.searchEntities),
127
+ path_summary: impactPath.summary,
128
+ path_summary_compact: buildCompactImpactSummary(
129
+ impactPath.entities,
130
+ impactPath.edges,
131
+ params.catalog,
132
+ params.searchEntities,
133
+ params.maxPathHopsShown
134
+ ),
135
+ path_relation_types: impactPath.edges.map((edge) => String(edge.relation ?? "")),
136
+ excerpt: entity?.snippet ?? "",
137
+ status: entity?.status ?? String(catalogEntry.status ?? "unknown"),
138
+ source_of_truth: entity?.source_of_truth ?? Boolean(catalogEntry.source_of_truth),
139
+ ...(params.includeReasons
140
+ ? {
141
+ top_reasons: topReasons
142
+ }
143
+ : {}),
144
+ ...(params.includeScores
145
+ ? {
146
+ impact_score: impactScore,
147
+ profile_score: profileScore,
148
+ note_score: noteScore,
149
+ semantic_score: Number(semantic.toFixed(4)),
150
+ graph_score: Number(graphScore.toFixed(4)),
151
+ trust_score: Number(trustScore.toFixed(4))
152
+ }
153
+ : {}),
154
+ ...(params.verbosePaths
155
+ ? {
156
+ path_entities: impactPath.entities,
157
+ path_edges: impactPath.edges
158
+ }
159
+ : {})
160
+ };
161
+ })
162
+ .filter((result) =>
163
+ matchesImpactFilters(
164
+ result,
165
+ params.resultDomains,
166
+ params.resultEntityTypes,
167
+ params.pathMustInclude,
168
+ params.pathMustExclude
169
+ )
170
+ )
171
+ .sort(impactResultComparator(params.sortBy))
172
+ .slice(0, params.topK);
173
+ }
@@ -0,0 +1,33 @@
1
+ import type { ImpactParams, JsonObject, ToolPayload } from "./types.js";
2
+
3
+ export async function resolveImpactSeed(
4
+ parsed: ImpactParams,
5
+ runSearch: (params: {
6
+ query: string;
7
+ top_k: number;
8
+ include_deprecated: boolean;
9
+ include_content: boolean;
10
+ }) => Promise<ToolPayload>
11
+ ): Promise<{ id: string | null; query_results?: JsonObject[]; warning?: string }> {
12
+ if (parsed.entity_id) {
13
+ return { id: parsed.entity_id };
14
+ }
15
+
16
+ if (!parsed.query) {
17
+ return { id: null, warning: "Either entity_id or query is required." };
18
+ }
19
+
20
+ const searchPayload = await runSearch({
21
+ query: parsed.query,
22
+ top_k: Math.max(parsed.top_k, 5),
23
+ include_deprecated: false,
24
+ include_content: false
25
+ });
26
+ const rawResults = Array.isArray(searchPayload.results) ? searchPayload.results : [];
27
+ const firstResult = rawResults[0];
28
+
29
+ return {
30
+ id: typeof firstResult?.id === "string" ? firstResult.id : null,
31
+ query_results: rawResults as JsonObject[]
32
+ };
33
+ }