@grafema/mcp 0.3.21 → 0.3.23
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/dist/definitions/query-tools.d.ts.map +1 -1
- package/dist/definitions/query-tools.js +102 -1
- package/dist/definitions/query-tools.js.map +1 -1
- package/dist/handlers/analysis-handlers.d.ts.map +1 -1
- package/dist/handlers/analysis-handlers.js +5 -1
- package/dist/handlers/analysis-handlers.js.map +1 -1
- package/dist/handlers/context-handlers.d.ts +4 -0
- package/dist/handlers/context-handlers.d.ts.map +1 -1
- package/dist/handlers/context-handlers.js +76 -1
- package/dist/handlers/context-handlers.js.map +1 -1
- package/dist/handlers/dataflow-handlers.d.ts +8 -1
- package/dist/handlers/dataflow-handlers.d.ts.map +1 -1
- package/dist/handlers/dataflow-handlers.js +143 -1
- package/dist/handlers/dataflow-handlers.js.map +1 -1
- package/dist/handlers/index.d.ts +3 -2
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +2 -2
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/query-handlers.d.ts.map +1 -1
- package/dist/handlers/query-handlers.js +235 -7
- package/dist/handlers/query-handlers.js.map +1 -1
- package/dist/server.js +51 -15
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/definitions/query-tools.ts +102 -1
- package/src/handlers/analysis-handlers.ts +7 -1
- package/src/handlers/context-handlers.ts +80 -1
- package/src/handlers/dataflow-handlers.ts +164 -0
- package/src/handlers/index.ts +3 -2
- package/src/handlers/query-handlers.ts +239 -14
- package/src/server.ts +59 -14
- package/src/types.ts +12 -0
|
@@ -113,7 +113,7 @@ Use this when you need to:
|
|
|
113
113
|
|
|
114
114
|
Returns semantic IDs that you can pass to get_context, get_node, get_neighbors, or find_guards.
|
|
115
115
|
|
|
116
|
-
Supports partial matches on name and file. Use limit/offset for pagination.`,
|
|
116
|
+
Supports partial matches on name and file. When a name filter returns no exact matches, automatically falls back to fuzzy name matching using token similarity (CamelCase/snake_case aware). Use limit/offset for pagination.`,
|
|
117
117
|
inputSchema: {
|
|
118
118
|
type: 'object',
|
|
119
119
|
properties: {
|
|
@@ -214,6 +214,107 @@ Tip: Start with max_depth=5, increase if needed.`,
|
|
|
214
214
|
required: ['source'],
|
|
215
215
|
},
|
|
216
216
|
},
|
|
217
|
+
{
|
|
218
|
+
name: 'trace_calls',
|
|
219
|
+
description: `Trace call chains from or to a function/method, following CALLS and CALLS_REMOTE edges transitively.
|
|
220
|
+
|
|
221
|
+
Use this when you need to:
|
|
222
|
+
- "What does this function eventually call?" (forward) — full call tree including cross-language hops
|
|
223
|
+
- "Who calls this function?" (backward) — all callers up the stack
|
|
224
|
+
- "Show the full call chain from handler to database" (forward with depth)
|
|
225
|
+
|
|
226
|
+
Unlike trace_dataflow (which follows data assignments), this follows function CALLS edges:
|
|
227
|
+
- CALLS: same-language function/method invocation
|
|
228
|
+
- CALLS_REMOTE: cross-process/language boundary (IPC, HTTP, socket)
|
|
229
|
+
|
|
230
|
+
Returns: Indented call tree showing each hop with file:line location.`,
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
source: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Function/method name or semantic ID to trace from',
|
|
237
|
+
},
|
|
238
|
+
file: {
|
|
239
|
+
type: 'string',
|
|
240
|
+
description: 'File path to disambiguate (optional)',
|
|
241
|
+
},
|
|
242
|
+
direction: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
description: 'forward (callees), backward (callers), or both (default: forward)',
|
|
245
|
+
enum: ['forward', 'backward', 'both'],
|
|
246
|
+
},
|
|
247
|
+
max_depth: {
|
|
248
|
+
type: 'number',
|
|
249
|
+
description: 'Maximum chain depth (default: 10)',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
required: ['source'],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'get_shape',
|
|
257
|
+
description: `Get the shape (methods + properties) of a CLASS, INTERFACE, or typed variable.
|
|
258
|
+
|
|
259
|
+
Shows all members including inherited ones via EXTENDS chain. For variables,
|
|
260
|
+
follows INSTANCE_OF to find the type, then returns its shape.
|
|
261
|
+
|
|
262
|
+
Use this to understand:
|
|
263
|
+
- "What methods does GraphBackend have?" → get_shape(target="GraphBackend")
|
|
264
|
+
- "What can I call on this variable?" → get_shape(target="db", file="handlers.ts")
|
|
265
|
+
- "What does this interface require?" → get_shape(target="NodeRecord")
|
|
266
|
+
|
|
267
|
+
Returns: members (methods + properties), extends chain, implements list.`,
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
target: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: 'CLASS, INTERFACE, or variable name (or semantic ID)',
|
|
274
|
+
},
|
|
275
|
+
file: {
|
|
276
|
+
type: 'string',
|
|
277
|
+
description: 'File path to disambiguate (optional)',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
required: ['target'],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'explain',
|
|
285
|
+
description: `Explain a code element using graph data — returns structured context + prompt for the LLM to summarize.
|
|
286
|
+
|
|
287
|
+
Unlike other tools that return raw data, this tool returns graph query results
|
|
288
|
+
PLUS a natural-language prompt asking the calling LLM to explain the results
|
|
289
|
+
to the user. The LLM uses its own reasoning to produce a human-readable summary.
|
|
290
|
+
|
|
291
|
+
No extra API calls needed — the calling model (Claude, GPT, etc.) does the summarization.
|
|
292
|
+
|
|
293
|
+
Use cases:
|
|
294
|
+
- "Explain where this value comes from" → dataflow trace + summarization prompt
|
|
295
|
+
- "What does this function do?" → structure + calls + prompt to describe
|
|
296
|
+
- "How is this variable used?" → forward trace + prompt to explain usage patterns
|
|
297
|
+
|
|
298
|
+
The question parameter guides what graph data to fetch and how to frame the summary.`,
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: {
|
|
302
|
+
target: {
|
|
303
|
+
type: 'string',
|
|
304
|
+
description: 'Variable, function, or node name to explain',
|
|
305
|
+
},
|
|
306
|
+
file: {
|
|
307
|
+
type: 'string',
|
|
308
|
+
description: 'File path to narrow scope',
|
|
309
|
+
},
|
|
310
|
+
question: {
|
|
311
|
+
type: 'string',
|
|
312
|
+
description: 'What to explain: "where does this value come from?", "what does this function do?", "how is this used?" (default: general explanation)',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
required: ['target'],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
217
318
|
{
|
|
218
319
|
name: 'check_invariant',
|
|
219
320
|
description: `Check a one-off code invariant using a Datalog rule. Returns violations if broken.
|
|
@@ -99,13 +99,19 @@ export async function handleGetStats(): Promise<ToolResult> {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const actionHint =
|
|
103
|
+
nodeCount > 0
|
|
104
|
+
? `\n\nGraph is ready — use find_nodes, find_calls, trace_dataflow to query it.`
|
|
105
|
+
: `\n\nGraph is empty — call analyze_project to build the graph first.`;
|
|
106
|
+
|
|
102
107
|
return textResult(
|
|
103
108
|
`Graph Statistics:\n\n` +
|
|
104
109
|
`Total nodes: ${nodeCount.toLocaleString()}\n` +
|
|
105
110
|
`Total edges: ${edgeCount.toLocaleString()}\n\n` +
|
|
106
111
|
`Nodes by type:\n${JSON.stringify(nodesByType, null, 2)}\n\n` +
|
|
107
112
|
`Edges by type:\n${JSON.stringify(edgesByType, null, 2)}` +
|
|
108
|
-
shardSection
|
|
113
|
+
shardSection +
|
|
114
|
+
actionHint
|
|
109
115
|
);
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { ensureAnalyzed } from '../analysis.js';
|
|
6
6
|
import { getProjectPath } from '../state.js';
|
|
7
|
-
import { findCallsInFunction, findContainingFunction, FileOverview, buildNodeContext, getNodeDisplayName, formatEdgeMetadata, STRUCTURAL_EDGE_TYPES, isGrafemaUri, toCompactSemanticId } from '@grafema/util';
|
|
7
|
+
import { findCallsInFunction, findContainingFunction, FileOverview, buildNodeContext, getNodeDisplayName, formatEdgeMetadata, STRUCTURAL_EDGE_TYPES, isGrafemaUri, toCompactSemanticId, getShape } from '@grafema/util';
|
|
8
|
+
import type { ClassIndex } from '@grafema/util';
|
|
8
9
|
import type { CallInfo, CallerInfo, NodeContext } from '@grafema/util';
|
|
9
10
|
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
10
11
|
import { isAbsolute, join, relative } from 'path';
|
|
@@ -433,3 +434,81 @@ export async function handleGetFileOverview(
|
|
|
433
434
|
return errorResult(`Failed to get file overview: ${message}`);
|
|
434
435
|
}
|
|
435
436
|
}
|
|
437
|
+
|
|
438
|
+
// === GET SHAPE ===
|
|
439
|
+
|
|
440
|
+
export async function handleGetShape(args: { target: string; file?: string }): Promise<ToolResult> {
|
|
441
|
+
const db = await ensureAnalyzed();
|
|
442
|
+
const { target, file } = args;
|
|
443
|
+
|
|
444
|
+
// Find the target node
|
|
445
|
+
let targetNode: GraphNode | null = await db.getNode(target);
|
|
446
|
+
if (!targetNode) {
|
|
447
|
+
// Search by name, preferring CLASS/INTERFACE
|
|
448
|
+
for (const type of ['CLASS', 'INTERFACE', 'VARIABLE', 'CONSTANT', 'PARAMETER']) {
|
|
449
|
+
for await (const node of db.queryNodes({ type, name: target })) {
|
|
450
|
+
if (file && !node.file?.includes(file)) continue;
|
|
451
|
+
targetNode = node;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
if (targetNode) break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (!targetNode) {
|
|
458
|
+
return errorResult(`Target "${target}" not found. Provide a CLASS, INTERFACE, or typed variable name.`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Build class index for EXTENDS chain walking
|
|
462
|
+
const classIndex: ClassIndex = new Map();
|
|
463
|
+
for await (const node of db.queryNodes({ type: 'CLASS' })) {
|
|
464
|
+
if (node.name) classIndex.set(node.name, String(node.id));
|
|
465
|
+
}
|
|
466
|
+
for await (const node of db.queryNodes({ type: 'INTERFACE' })) {
|
|
467
|
+
if (node.name) classIndex.set(node.name, String(node.id));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const backend = {
|
|
471
|
+
getNode: (id: string) => db.getNode(id),
|
|
472
|
+
getOutgoingEdges: (id: string) => db.getOutgoingEdges(id),
|
|
473
|
+
getIncomingEdges: (id: string) => db.getIncomingEdges(id),
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const shape = await getShape(backend as never, String(targetNode.id), classIndex);
|
|
477
|
+
if (!shape) {
|
|
478
|
+
return errorResult(`Could not determine shape for "${target}". It may not be a CLASS, INTERFACE, or typed variable.`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Format output
|
|
482
|
+
const lines: string[] = [];
|
|
483
|
+
lines.push(`## Shape: ${shape.name} (${shape.nodeType})`);
|
|
484
|
+
if (shape.file) lines.push(`File: ${shape.file}`);
|
|
485
|
+
if (shape.extends.length > 0) lines.push(`Extends: ${shape.extends.join(' → ')}`);
|
|
486
|
+
if (shape.implements.length > 0) lines.push(`Implements: ${shape.implements.join(', ')}`);
|
|
487
|
+
lines.push(`Confidence: ${shape.confidence}`);
|
|
488
|
+
lines.push('');
|
|
489
|
+
|
|
490
|
+
// Group members by source
|
|
491
|
+
const bySource = new Map<string, typeof shape.members>();
|
|
492
|
+
for (const m of shape.members) {
|
|
493
|
+
if (!bySource.has(m.from)) bySource.set(m.from, []);
|
|
494
|
+
bySource.get(m.from)!.push(m);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
for (const [source, members] of bySource) {
|
|
498
|
+
const label = source === shape.name ? 'Own members' : `Inherited from ${source}`;
|
|
499
|
+
lines.push(`### ${label} (${members.length})`);
|
|
500
|
+
const methods = members.filter(m => m.kind === 'method' || m.kind === 'method_signature');
|
|
501
|
+
const props = members.filter(m => m.kind === 'property' || m.kind === 'property_signature');
|
|
502
|
+
if (methods.length > 0) {
|
|
503
|
+
lines.push(`Methods: ${methods.map(m => m.name).join(', ')}`);
|
|
504
|
+
}
|
|
505
|
+
if (props.length > 0) {
|
|
506
|
+
lines.push(`Properties: ${props.map(m => m.name).join(', ')}`);
|
|
507
|
+
}
|
|
508
|
+
lines.push('');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
lines.push(`Total: ${shape.members.length} members`);
|
|
512
|
+
|
|
513
|
+
return textResult(lines.join('\n'));
|
|
514
|
+
}
|
|
@@ -16,12 +16,14 @@ import type {
|
|
|
16
16
|
ToolResult,
|
|
17
17
|
TraceAliasArgs,
|
|
18
18
|
TraceDataFlowArgs,
|
|
19
|
+
TraceCallChainArgs,
|
|
19
20
|
CheckInvariantArgs,
|
|
20
21
|
GraphNode,
|
|
21
22
|
} from '../types.js';
|
|
22
23
|
import {
|
|
23
24
|
traceDataflow,
|
|
24
25
|
renderTraceNarrative,
|
|
26
|
+
traceCallChain,
|
|
25
27
|
type DataflowBackend,
|
|
26
28
|
type TraceDetail,
|
|
27
29
|
} from '@grafema/util';
|
|
@@ -155,6 +157,168 @@ export async function handleTraceDataFlow(args: TraceDataFlowArgs): Promise<Tool
|
|
|
155
157
|
}));
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
// === TRACE CALL CHAIN ===
|
|
161
|
+
|
|
162
|
+
export async function handleTraceCallChain(args: TraceCallChainArgs): Promise<ToolResult> {
|
|
163
|
+
const db = await ensureAnalyzed();
|
|
164
|
+
const { source, file, direction = 'forward', max_depth = 10 } = args;
|
|
165
|
+
|
|
166
|
+
// Find source node (same resolution logic as trace_dataflow)
|
|
167
|
+
let sourceNode: GraphNode | null = await db.getNode(source);
|
|
168
|
+
if (!sourceNode) {
|
|
169
|
+
let fallbackNode: GraphNode | null = null;
|
|
170
|
+
for (const type of ['FUNCTION', 'METHOD']) {
|
|
171
|
+
for await (const node of db.queryNodes({ type, name: source })) {
|
|
172
|
+
if (file && !node.file?.includes(file)) {
|
|
173
|
+
if (!fallbackNode) fallbackNode = node;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
sourceNode = node;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (sourceNode) break;
|
|
180
|
+
}
|
|
181
|
+
if (!sourceNode && fallbackNode) {
|
|
182
|
+
sourceNode = fallbackNode;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!sourceNode) {
|
|
186
|
+
const displaySource = isGrafemaUri(source) ? toCompactSemanticId(source) : source;
|
|
187
|
+
return errorResult(`Source "${displaySource}" not found. Provide a FUNCTION or METHOD name.`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const results = await traceCallChain(db as never, sourceNode.id, {
|
|
191
|
+
direction: direction as 'forward' | 'backward' | 'both',
|
|
192
|
+
maxDepth: max_depth,
|
|
193
|
+
limit: 100,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Render as indented call tree
|
|
197
|
+
const lines: string[] = [];
|
|
198
|
+
const sourceName = sourceNode.name || source;
|
|
199
|
+
const sourceFile = sourceNode.file ? sourceNode.file.split('/').pop() : '';
|
|
200
|
+
|
|
201
|
+
for (const result of results) {
|
|
202
|
+
lines.push(`## ${result.direction === 'forward' ? 'Outgoing calls from' : 'Incoming callers of'} ${sourceName} (${sourceFile}:${sourceNode.line ?? '?'})`);
|
|
203
|
+
lines.push('');
|
|
204
|
+
|
|
205
|
+
if (result.chain.length === 0) {
|
|
206
|
+
lines.push(' (no calls found)');
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const hop of result.chain) {
|
|
211
|
+
const indent = ' '.repeat(hop.depth + 1);
|
|
212
|
+
const prefix = hop.remote ? '> calls_remote' : '> calls';
|
|
213
|
+
const shortFile = hop.file ? hop.file.split('/').pop() : '?';
|
|
214
|
+
const loc = hop.line ? `${shortFile}:${hop.line}` : shortFile;
|
|
215
|
+
const unresolvedTag = hop.resolved ? '' : ' [unresolved]';
|
|
216
|
+
lines.push(`${indent}${prefix} ${hop.name} (${loc})${unresolvedTag}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push(`${result.totalFound} hop(s) found`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push('Legend: > calls = local call, > calls_remote = cross-process/language boundary');
|
|
225
|
+
|
|
226
|
+
return textResult(lines.join('\n'));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// === EXPLAIN (graph data + LLM prompt injection) ===
|
|
230
|
+
|
|
231
|
+
export interface ExplainArgs {
|
|
232
|
+
target: string;
|
|
233
|
+
file?: string;
|
|
234
|
+
question?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function handleExplain(args: ExplainArgs): Promise<ToolResult> {
|
|
238
|
+
const db = await ensureAnalyzed();
|
|
239
|
+
const { target, file, question } = args;
|
|
240
|
+
|
|
241
|
+
// 1. Find the target node
|
|
242
|
+
let targetNode: GraphNode | null = await db.getNode(target);
|
|
243
|
+
if (!targetNode) {
|
|
244
|
+
for await (const node of db.queryNodes({ name: target })) {
|
|
245
|
+
if (file && !node.file?.includes(file)) continue;
|
|
246
|
+
targetNode = node;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!targetNode) {
|
|
251
|
+
// Try PARAMETER, CONSTANT
|
|
252
|
+
for (const type of ['PARAMETER', 'CONSTANT', 'IMPORT_BINDING']) {
|
|
253
|
+
for await (const node of db.queryNodes({ type, name: target })) {
|
|
254
|
+
if (file && !node.file?.includes(file)) continue;
|
|
255
|
+
targetNode = node;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
if (targetNode) break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!targetNode) {
|
|
262
|
+
return errorResult(`Target "${target}" not found`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 2. Gather graph context based on question type
|
|
266
|
+
const dfDb = db as unknown as DataflowBackend;
|
|
267
|
+
const sections: string[] = [];
|
|
268
|
+
|
|
269
|
+
const nodeType = targetNode.nodeType || targetNode.type;
|
|
270
|
+
const nodeName = targetNode.name || target;
|
|
271
|
+
const nodeFile = targetNode.file || '';
|
|
272
|
+
|
|
273
|
+
sections.push(`## ${nodeType} "${nodeName}" in ${nodeFile}`);
|
|
274
|
+
|
|
275
|
+
// Always include dataflow (both directions)
|
|
276
|
+
const trace = await traceDataflow(dfDb, targetNode.id, {
|
|
277
|
+
direction: 'both',
|
|
278
|
+
maxDepth: 8,
|
|
279
|
+
limit: 30,
|
|
280
|
+
});
|
|
281
|
+
const traceText = renderTraceNarrative(trace, nodeName, { detail: 'normal' });
|
|
282
|
+
sections.push(`### Dataflow\n${traceText}`);
|
|
283
|
+
|
|
284
|
+
// Include structure context if it's a function
|
|
285
|
+
if (nodeType === 'FUNCTION') {
|
|
286
|
+
const allOutEdges = await db.getOutgoingEdges(targetNode.id);
|
|
287
|
+
const allInEdges = await db.getIncomingEdges(targetNode.id);
|
|
288
|
+
const outSummary: string[] = [];
|
|
289
|
+
const inSummary: string[] = [];
|
|
290
|
+
for (const edge of allOutEdges.slice(0, 15)) {
|
|
291
|
+
if (['CALLS', 'CONTAINS', 'HAS_SCOPE'].includes(edge.type)) {
|
|
292
|
+
const dst = await db.getNode(edge.dst);
|
|
293
|
+
if (dst) outSummary.push(`${edge.type} → ${dst.nodeType} "${dst.name}"`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const edge of allInEdges.slice(0, 15)) {
|
|
297
|
+
if (['CALLS', 'CONTAINS'].includes(edge.type)) {
|
|
298
|
+
const src = await db.getNode(edge.src);
|
|
299
|
+
if (src) inSummary.push(`${edge.type} ← ${src.nodeType} "${src.name}"`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (outSummary.length) sections.push(`### Outgoing\n${outSummary.join('\n')}`);
|
|
303
|
+
if (inSummary.length) sections.push(`### Incoming\n${inSummary.join('\n')}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 3. Prompt injection — instruct the calling LLM to summarize
|
|
307
|
+
const questionText = question || `what is "${nodeName}" and how is it used?`;
|
|
308
|
+
const prompt = `
|
|
309
|
+
---
|
|
310
|
+
INSTRUCTION: Based on the graph data above, answer the user's question in 2-3 concise sentences.
|
|
311
|
+
Question: "${questionText}"
|
|
312
|
+
Focus on: data origins, transformations, and where the value ends up.
|
|
313
|
+
Use concrete names from the trace (function names, variable names, file paths).
|
|
314
|
+
Do NOT repeat the raw graph data — synthesize it into a human-readable explanation.
|
|
315
|
+
---`;
|
|
316
|
+
|
|
317
|
+
sections.push(prompt);
|
|
318
|
+
|
|
319
|
+
return textResult(sections.join('\n\n'));
|
|
320
|
+
}
|
|
321
|
+
|
|
158
322
|
// === CHECK INVARIANT (unchanged) ===
|
|
159
323
|
|
|
160
324
|
export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<ToolResult> {
|
package/src/handlers/index.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { handleQueryGraph, handleFindCalls, handleFindNodes } from './query-handlers.js';
|
|
6
|
-
export { handleTraceAlias, handleTraceDataFlow, handleCheckInvariant } from './dataflow-handlers.js';
|
|
6
|
+
export { handleTraceAlias, handleTraceDataFlow, handleTraceCallChain, handleCheckInvariant, handleExplain } from './dataflow-handlers.js';
|
|
7
|
+
export type { ExplainArgs } from './dataflow-handlers.js';
|
|
7
8
|
export { handleAnalyzeProject, handleGetAnalysisStatus, handleGetStats, handleGetSchema } from './analysis-handlers.js';
|
|
8
9
|
export { handleCreateGuarantee, handleListGuarantees, handleCheckGuarantees, handleDeleteGuarantee } from './guarantee-handlers.js';
|
|
9
|
-
export { handleGetFunctionDetails, handleGetContext, handleGetFileOverview } from './context-handlers.js';
|
|
10
|
+
export { handleGetFunctionDetails, handleGetContext, handleGetFileOverview, handleGetShape } from './context-handlers.js';
|
|
10
11
|
export { handleReadProjectStructure, handleWriteConfig } from './project-handlers.js';
|
|
11
12
|
export { handleGetCoverage } from './coverage-handlers.js';
|
|
12
13
|
export { handleFindGuards } from './guard-handlers.js';
|