@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
|
@@ -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
|
+
}
|