@grafema/mcp 0.3.23 → 0.3.27
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 +103 -90
- package/dist/definitions/query-tools.d.ts.map +1 -1
- package/dist/definitions/query-tools.js +71 -0
- package/dist/definitions/query-tools.js.map +1 -1
- package/dist/handlers/analysis-handlers.d.ts +1 -0
- 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/behavior-handlers.d.ts +29 -0
- package/dist/handlers/behavior-handlers.d.ts.map +1 -0
- package/dist/handlers/behavior-handlers.js +127 -0
- package/dist/handlers/behavior-handlers.js.map +1 -0
- package/dist/handlers/dataflow-handlers.d.ts +4 -3
- package/dist/handlers/dataflow-handlers.d.ts.map +1 -1
- package/dist/handlers/dataflow-handlers.js +93 -3
- package/dist/handlers/dataflow-handlers.js.map +1 -1
- package/dist/handlers/graphql-handlers.d.ts +1 -1
- package/dist/handlers/graphql-handlers.js +1 -1
- package/dist/handlers/index.d.ts +4 -3
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +4 -3
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/query-handlers.js +0 -1
- package/dist/handlers/query-handlers.js.map +1 -1
- package/dist/server.js +13 -9
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/definitions/query-tools.ts +71 -0
- package/src/handlers/analysis-handlers.ts +6 -1
- package/src/handlers/behavior-handlers.ts +174 -0
- package/src/handlers/dataflow-handlers.ts +110 -2
- package/src/handlers/graphql-handlers.ts +1 -1
- package/src/handlers/index.ts +4 -3
- package/src/handlers/query-handlers.ts +1 -1
- package/src/server.ts +21 -10
- package/src/types.ts +13 -0
|
@@ -252,6 +252,44 @@ Returns: Indented call tree showing each hop with file:line location.`,
|
|
|
252
252
|
required: ['source'],
|
|
253
253
|
},
|
|
254
254
|
},
|
|
255
|
+
{
|
|
256
|
+
name: 'trace_effects',
|
|
257
|
+
description: `Trace transitive side effects of a function through its call graph.
|
|
258
|
+
|
|
259
|
+
For any function, traverses CALLS edges (DFS) and collects effects from leaf nodes
|
|
260
|
+
using the effects-db (Node.js builtins, npm packages).
|
|
261
|
+
|
|
262
|
+
Use this when you need to:
|
|
263
|
+
- "What side effects does this function have?" → direct + transitive effects
|
|
264
|
+
- "Does this handler do IO?" → trace shows IO:FILE:READ from fs.readFileSync at depth 3
|
|
265
|
+
- "Where does the fetch() call come from?" → leaf_sources shows the origin at depth N
|
|
266
|
+
- "What crosses module boundaries?" → boundary_crossings shows file-to-file effect flow
|
|
267
|
+
|
|
268
|
+
Effect types: PURE, MUTATION, IO (with subtypes like IO:FILE:READ, IO:HTTP:REQUEST),
|
|
269
|
+
THROW, ASYNC, NONDETERMINISTIC, UNKNOWN.
|
|
270
|
+
|
|
271
|
+
UNKNOWN means: unresolved call, external package not in effects-db, or depth limit reached.
|
|
272
|
+
|
|
273
|
+
Returns: direct effects, transitive effects, boundary crossings, leaf sources.`,
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
node: {
|
|
278
|
+
type: 'string',
|
|
279
|
+
description: 'Function/method name or semantic ID',
|
|
280
|
+
},
|
|
281
|
+
file: {
|
|
282
|
+
type: 'string',
|
|
283
|
+
description: 'File path to disambiguate (optional)',
|
|
284
|
+
},
|
|
285
|
+
max_depth: {
|
|
286
|
+
type: 'number',
|
|
287
|
+
description: 'Maximum call graph traversal depth (default: 10)',
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
required: ['node'],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
255
293
|
{
|
|
256
294
|
name: 'get_shape',
|
|
257
295
|
description: `Get the shape (methods + properties) of a CLASS, INTERFACE, or typed variable.
|
|
@@ -351,4 +389,37 @@ Returns: List of nodes violating the rule, with file and line info.`,
|
|
|
351
389
|
required: ['rule'],
|
|
352
390
|
},
|
|
353
391
|
},
|
|
392
|
+
{
|
|
393
|
+
name: 'find_shared_behaviors',
|
|
394
|
+
description: `List clusters of FEATUREs whose entry-points share an identical BEHAVIOR (same forward-slice hash).
|
|
395
|
+
|
|
396
|
+
Surfaces cross-modality duplication — e.g. "this CLI command is a thin wrapper around the
|
|
397
|
+
same library function as that HTTP endpoint" or "this MCP tool and that VS Code command
|
|
398
|
+
delegate to identical logic".
|
|
399
|
+
|
|
400
|
+
Each cluster contains:
|
|
401
|
+
- hash: sha256 of the shared transitive call set (BEHAVIOR.metadata.hash)
|
|
402
|
+
- effects: transitive effects (IO, MUTATION, …) attributed to the shared behavior
|
|
403
|
+
- coreNodeCount: size of the shared forward slice
|
|
404
|
+
- features: array of { id, type, name, file } — the FEATUREs that share this behavior
|
|
405
|
+
|
|
406
|
+
Cluster types are FEATURE node types: cli:command, mcp:tool, vscode:command (and any future
|
|
407
|
+
domain types created by enrichers).
|
|
408
|
+
|
|
409
|
+
Returns clusters ordered by size (largest first), then hash (deterministic tie-break).
|
|
410
|
+
Empty result means every FEATURE has a unique implementation.`,
|
|
411
|
+
inputSchema: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
properties: {
|
|
414
|
+
minClusterSize: {
|
|
415
|
+
type: 'number',
|
|
416
|
+
description: 'Minimum FEATUREs per cluster (default: 2). Values below 2 are clamped to 2.',
|
|
417
|
+
},
|
|
418
|
+
limit: {
|
|
419
|
+
type: 'number',
|
|
420
|
+
description: 'Maximum clusters to return (default: 100).',
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
354
425
|
];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MCP Analysis Handlers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { ensureAnalyzed } from '../analysis.js';
|
|
5
|
+
import { ensureAnalyzed, discoverServices } from '../analysis.js';
|
|
6
6
|
import { getAnalysisStatus, isAnalysisRunning } from '../state.js';
|
|
7
7
|
import {
|
|
8
8
|
textResult,
|
|
@@ -17,6 +17,11 @@ import type { ServerStats } from '@grafema/types';
|
|
|
17
17
|
|
|
18
18
|
// === ANALYSIS HANDLERS ===
|
|
19
19
|
|
|
20
|
+
export async function handleDiscoverServices(): Promise<ToolResult> {
|
|
21
|
+
const services = await discoverServices();
|
|
22
|
+
return textResult(`Found ${services.length} service(s):\n${JSON.stringify(services, null, 2)}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export async function handleAnalyzeProject(args: AnalyzeProjectArgs): Promise<ToolResult> {
|
|
21
26
|
const { service, force } = args;
|
|
22
27
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior handlers — surface cross-modality FEATURE duplication.
|
|
3
|
+
*
|
|
4
|
+
* `find_shared_behaviors` lists clusters of FEATUREs (cli:command, mcp:tool,
|
|
5
|
+
* vscode:command, …) whose entry-point forward-slice hashes match. Mirrors the
|
|
6
|
+
* `grafema features --duplicates` CLI subcommand — same algorithm, same
|
|
7
|
+
* cluster shape — so the two channels return symmetric data.
|
|
8
|
+
*
|
|
9
|
+
* REG-1119.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ensureAnalyzed } from '../analysis.js';
|
|
13
|
+
import { textResult, errorResult } from '../utils.js';
|
|
14
|
+
import type { ToolResult, FindSharedBehaviorsArgs } from '../types.js';
|
|
15
|
+
import type { EdgeType, EdgeRecord } from '@grafema/types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Minimal backend interface used by the logic function. Allows testing with
|
|
19
|
+
* a simple mock without importing the full GraphBackend. Matches the subset
|
|
20
|
+
* of RFDBServerBackend that we need.
|
|
21
|
+
*/
|
|
22
|
+
interface BehaviorBackendLike {
|
|
23
|
+
getNode(id: string): Promise<Record<string, unknown> | null>;
|
|
24
|
+
queryNodes(query: { type?: string; nodeType?: string }): AsyncIterable<Record<string, unknown>>;
|
|
25
|
+
getIncomingEdges(nodeId: string, edgeTypes?: EdgeType[] | null): Promise<EdgeRecord[]>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SharedFeatureRef {
|
|
29
|
+
id: string;
|
|
30
|
+
type: string;
|
|
31
|
+
name: string;
|
|
32
|
+
file: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SharedBehaviorCluster {
|
|
36
|
+
hash: string;
|
|
37
|
+
effects: string[];
|
|
38
|
+
coreNodeCount: number;
|
|
39
|
+
features: SharedFeatureRef[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_MIN_CLUSTER_SIZE = 2;
|
|
43
|
+
const DEFAULT_LIMIT = 100;
|
|
44
|
+
const MAX_LIMIT = 10_000;
|
|
45
|
+
|
|
46
|
+
// === Logic function (testable, accepts backend directly) ===
|
|
47
|
+
|
|
48
|
+
export async function findSharedBehaviorsLogic(
|
|
49
|
+
db: BehaviorBackendLike,
|
|
50
|
+
args: FindSharedBehaviorsArgs,
|
|
51
|
+
): Promise<ToolResult> {
|
|
52
|
+
const { minClusterSize: rawMin, limit: rawLimit } = args ?? {};
|
|
53
|
+
|
|
54
|
+
if (rawMin !== undefined && (!Number.isFinite(rawMin) || rawMin < 0)) {
|
|
55
|
+
return errorResult('minClusterSize must be a non-negative number');
|
|
56
|
+
}
|
|
57
|
+
if (rawLimit !== undefined && (!Number.isFinite(rawLimit) || rawLimit <= 0)) {
|
|
58
|
+
return errorResult('limit must be a positive number');
|
|
59
|
+
}
|
|
60
|
+
if (rawLimit !== undefined && rawLimit > MAX_LIMIT) {
|
|
61
|
+
return errorResult(`limit must be <= ${MAX_LIMIT}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const minClusterSize = Math.max(2, rawMin ?? DEFAULT_MIN_CLUSTER_SIZE);
|
|
65
|
+
const limit = rawLimit ?? DEFAULT_LIMIT;
|
|
66
|
+
|
|
67
|
+
// 1. Collect all BEHAVIOR nodes.
|
|
68
|
+
interface BehaviorRow {
|
|
69
|
+
id: string;
|
|
70
|
+
hash: string;
|
|
71
|
+
effects: string[];
|
|
72
|
+
coreNodeCount: number;
|
|
73
|
+
}
|
|
74
|
+
const behaviors: BehaviorRow[] = [];
|
|
75
|
+
for await (const node of db.queryNodes({ type: 'BEHAVIOR' })) {
|
|
76
|
+
const id = String(node.id ?? '');
|
|
77
|
+
if (!id) continue;
|
|
78
|
+
const hash = readString(node.hash);
|
|
79
|
+
if (!hash) continue;
|
|
80
|
+
behaviors.push({
|
|
81
|
+
id,
|
|
82
|
+
hash,
|
|
83
|
+
effects: readStringArray(node.effects),
|
|
84
|
+
coreNodeCount: readNumber(node.coreNodeCount) ?? 0,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Bucket by hash.
|
|
89
|
+
const byHash = new Map<string, BehaviorRow[]>();
|
|
90
|
+
for (const b of behaviors) {
|
|
91
|
+
let bucket = byHash.get(b.hash);
|
|
92
|
+
if (!bucket) {
|
|
93
|
+
bucket = [];
|
|
94
|
+
byHash.set(b.hash, bucket);
|
|
95
|
+
}
|
|
96
|
+
bucket.push(b);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Resolve features (incoming IMPLEMENTED_BY) for clusters.
|
|
100
|
+
const clusters: SharedBehaviorCluster[] = [];
|
|
101
|
+
for (const [hash, bucket] of byHash) {
|
|
102
|
+
if (bucket.length < minClusterSize) continue;
|
|
103
|
+
|
|
104
|
+
const features: SharedFeatureRef[] = [];
|
|
105
|
+
const seen = new Set<string>();
|
|
106
|
+
for (const beh of bucket) {
|
|
107
|
+
const edges = await db.getIncomingEdges(beh.id, ['IMPLEMENTED_BY'] as EdgeType[]);
|
|
108
|
+
for (const edge of edges) {
|
|
109
|
+
const featureId = String(edge.src);
|
|
110
|
+
if (!featureId || seen.has(featureId)) continue;
|
|
111
|
+
seen.add(featureId);
|
|
112
|
+
const node = await db.getNode(featureId);
|
|
113
|
+
if (!node) continue;
|
|
114
|
+
features.push({
|
|
115
|
+
id: featureId,
|
|
116
|
+
type: readString(node.type) ?? 'UNKNOWN',
|
|
117
|
+
name: readString(node.name) ?? '',
|
|
118
|
+
file: readString(node.file) ?? '',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (features.length < minClusterSize) continue;
|
|
124
|
+
|
|
125
|
+
features.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type.localeCompare(b.type)));
|
|
126
|
+
clusters.push({
|
|
127
|
+
hash,
|
|
128
|
+
effects: bucket[0].effects,
|
|
129
|
+
coreNodeCount: bucket[0].coreNodeCount,
|
|
130
|
+
features,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clusters.sort((a, b) => (b.features.length - a.features.length) || a.hash.localeCompare(b.hash));
|
|
135
|
+
const truncated = clusters.length > limit;
|
|
136
|
+
const limited = clusters.slice(0, limit);
|
|
137
|
+
|
|
138
|
+
return textResult(JSON.stringify({
|
|
139
|
+
count: limited.length,
|
|
140
|
+
truncated,
|
|
141
|
+
minClusterSize,
|
|
142
|
+
clusters: limited,
|
|
143
|
+
}, null, 2));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// === Public handler (calls ensureAnalyzed, used by MCP routing) ===
|
|
147
|
+
|
|
148
|
+
export async function handleFindSharedBehaviors(args: FindSharedBehaviorsArgs): Promise<ToolResult> {
|
|
149
|
+
const db = await ensureAnalyzed();
|
|
150
|
+
return findSharedBehaviorsLogic(db as unknown as BehaviorBackendLike, args);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// === Internal helpers ===
|
|
154
|
+
|
|
155
|
+
function readString(v: unknown): string | undefined {
|
|
156
|
+
return typeof v === 'string' ? v : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readNumber(v: unknown): number | undefined {
|
|
160
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readStringArray(v: unknown): string[] {
|
|
164
|
+
if (Array.isArray(v)) return v.filter((x): x is string => typeof x === 'string');
|
|
165
|
+
if (typeof v === 'string') {
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(v);
|
|
168
|
+
if (Array.isArray(parsed)) return parsed.filter((x): x is string => typeof x === 'string');
|
|
169
|
+
} catch {
|
|
170
|
+
// not JSON
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Delegates BFS tracing to @grafema/util's shared traceDataflow module.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
7
9
|
import { ensureAnalyzed } from '../analysis.js';
|
|
8
10
|
import { getProjectPath } from '../state.js';
|
|
9
11
|
import {
|
|
@@ -17,6 +19,7 @@ import type {
|
|
|
17
19
|
TraceAliasArgs,
|
|
18
20
|
TraceDataFlowArgs,
|
|
19
21
|
TraceCallChainArgs,
|
|
22
|
+
TraceEffectsArgs,
|
|
20
23
|
CheckInvariantArgs,
|
|
21
24
|
GraphNode,
|
|
22
25
|
} from '../types.js';
|
|
@@ -24,6 +27,8 @@ import {
|
|
|
24
27
|
traceDataflow,
|
|
25
28
|
renderTraceNarrative,
|
|
26
29
|
traceCallChain,
|
|
30
|
+
traceEffects,
|
|
31
|
+
EffectsLookup,
|
|
27
32
|
type DataflowBackend,
|
|
28
33
|
type TraceDetail,
|
|
29
34
|
} from '@grafema/util';
|
|
@@ -103,7 +108,7 @@ export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult
|
|
|
103
108
|
|
|
104
109
|
// === TRACE DATAFLOW ===
|
|
105
110
|
|
|
106
|
-
export async function
|
|
111
|
+
export async function handleTraceDataflow(args: TraceDataFlowArgs): Promise<ToolResult> {
|
|
107
112
|
const db = await ensureAnalyzed();
|
|
108
113
|
const { source, file, direction = 'forward', max_depth = 10, limit = 50, detail } = args;
|
|
109
114
|
|
|
@@ -159,7 +164,7 @@ export async function handleTraceDataFlow(args: TraceDataFlowArgs): Promise<Tool
|
|
|
159
164
|
|
|
160
165
|
// === TRACE CALL CHAIN ===
|
|
161
166
|
|
|
162
|
-
export async function
|
|
167
|
+
export async function handleTraceCalls(args: TraceCallChainArgs): Promise<ToolResult> {
|
|
163
168
|
const db = await ensureAnalyzed();
|
|
164
169
|
const { source, file, direction = 'forward', max_depth = 10 } = args;
|
|
165
170
|
|
|
@@ -373,3 +378,106 @@ export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<To
|
|
|
373
378
|
return errorResult(message);
|
|
374
379
|
}
|
|
375
380
|
}
|
|
381
|
+
|
|
382
|
+
// === TRACE EFFECTS ===
|
|
383
|
+
|
|
384
|
+
let _effectsLookup: EffectsLookup | null = null;
|
|
385
|
+
|
|
386
|
+
function getEffectsLookup(): EffectsLookup {
|
|
387
|
+
if (!_effectsLookup) {
|
|
388
|
+
// Find effects-db directory relative to the project
|
|
389
|
+
// The effects-db is at the project root: <repo>/effects-db/
|
|
390
|
+
// Also check node_modules for installed packages
|
|
391
|
+
const candidates = [
|
|
392
|
+
join(getProjectPath(), 'effects-db'),
|
|
393
|
+
join(getProjectPath(), 'node_modules', '@grafema', 'effects-db'),
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
for (const candidate of candidates) {
|
|
397
|
+
if (existsSync(candidate)) {
|
|
398
|
+
_effectsLookup = EffectsLookup.load(candidate);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!_effectsLookup) {
|
|
404
|
+
_effectsLookup = EffectsLookup.empty();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return _effectsLookup;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function handleTraceEffects(args: TraceEffectsArgs): Promise<ToolResult> {
|
|
411
|
+
const db = await ensureAnalyzed();
|
|
412
|
+
const { node, file, max_depth = 10 } = args;
|
|
413
|
+
|
|
414
|
+
// Find source node (same pattern as handleTraceCalls)
|
|
415
|
+
let sourceNode: GraphNode | null = await db.getNode(node);
|
|
416
|
+
if (!sourceNode) {
|
|
417
|
+
let fallbackNode: GraphNode | null = null;
|
|
418
|
+
for (const type of ['FUNCTION', 'METHOD', 'CONSTRUCTOR']) {
|
|
419
|
+
for await (const n of db.queryNodes({ type, name: node })) {
|
|
420
|
+
if (file && !n.file?.includes(file)) {
|
|
421
|
+
if (!fallbackNode) fallbackNode = n;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
sourceNode = n;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
if (sourceNode) break;
|
|
428
|
+
}
|
|
429
|
+
if (!sourceNode && fallbackNode) {
|
|
430
|
+
sourceNode = fallbackNode;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!sourceNode) {
|
|
434
|
+
const displaySource = isGrafemaUri(node) ? toCompactSemanticId(node) : node;
|
|
435
|
+
return errorResult(`Source "${displaySource}" not found. Provide a FUNCTION or METHOD name.`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const effectsLookup = getEffectsLookup();
|
|
439
|
+
const dfDb = db as unknown as DataflowBackend;
|
|
440
|
+
|
|
441
|
+
const result = await traceEffects(dfDb, sourceNode.id, effectsLookup, { maxDepth: max_depth });
|
|
442
|
+
|
|
443
|
+
if (!result) {
|
|
444
|
+
return errorResult(`Could not trace effects for "${sourceNode.name || node}"`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Format as readable text
|
|
448
|
+
const lines: string[] = [];
|
|
449
|
+
const name = result.node.name;
|
|
450
|
+
const fileShort = result.node.file ? result.node.file.split('/').pop() : '?';
|
|
451
|
+
|
|
452
|
+
lines.push(`## Effects of ${name} (${fileShort}:${result.node.line ?? '?'})`);
|
|
453
|
+
lines.push('');
|
|
454
|
+
|
|
455
|
+
lines.push(`**Direct effects:** ${result.direct.join(', ') || 'PURE'}`);
|
|
456
|
+
lines.push(`**Transitive effects:** ${result.transitive.join(', ')}`);
|
|
457
|
+
lines.push('');
|
|
458
|
+
|
|
459
|
+
if (result.leaf_sources.length > 0) {
|
|
460
|
+
lines.push(`### Leaf sources (${result.leaf_sources.length})`);
|
|
461
|
+
for (const leaf of result.leaf_sources) {
|
|
462
|
+
const leafFile = leaf.file ? leaf.file.split('/').pop() : '';
|
|
463
|
+
const loc = leafFile ? ` (${leafFile})` : '';
|
|
464
|
+
lines.push(` depth ${leaf.depth}: ${leaf.node}${loc} → ${leaf.effects.join(', ')}`);
|
|
465
|
+
}
|
|
466
|
+
lines.push('');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (result.boundary_crossings.length > 0) {
|
|
470
|
+
lines.push(`### Boundary crossings (${result.boundary_crossings.length})`);
|
|
471
|
+
for (const bc of result.boundary_crossings) {
|
|
472
|
+
const fromShort = bc.from_file.split('/').pop();
|
|
473
|
+
const toShort = bc.to_file.split('/').pop();
|
|
474
|
+
lines.push(` ${bc.caller} (${fromShort}) → ${bc.callee} (${toShort}): ${bc.effects.join(', ')}`);
|
|
475
|
+
}
|
|
476
|
+
lines.push('');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
lines.push(`---`);
|
|
480
|
+
lines.push(`Nodes visited: ${result.nodes_visited}, max depth: ${result.max_depth}${result.max_depth_reached ? ' (TRUNCATED — some paths marked UNKNOWN)' : ''}`);
|
|
481
|
+
|
|
482
|
+
return textResult(lines.join('\n'));
|
|
483
|
+
}
|
|
@@ -29,7 +29,7 @@ async function getYoga(backend: RFDBServerBackend) {
|
|
|
29
29
|
return yogaInstance;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export async function
|
|
32
|
+
export async function handleQueryGraphql(args: GraphQLQueryArgs): Promise<ToolResult> {
|
|
33
33
|
const { query, variables, operationName } = args;
|
|
34
34
|
|
|
35
35
|
if (!query || query.trim() === '') {
|
package/src/handlers/index.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { handleQueryGraph, handleFindCalls, handleFindNodes } from './query-handlers.js';
|
|
6
|
-
export { handleTraceAlias,
|
|
6
|
+
export { handleTraceAlias, handleTraceDataflow, handleTraceCalls, handleCheckInvariant, handleExplain, handleTraceEffects } from './dataflow-handlers.js';
|
|
7
7
|
export type { ExplainArgs } from './dataflow-handlers.js';
|
|
8
|
-
export { handleAnalyzeProject, handleGetAnalysisStatus, handleGetStats, handleGetSchema } from './analysis-handlers.js';
|
|
8
|
+
export { handleDiscoverServices, handleAnalyzeProject, handleGetAnalysisStatus, handleGetStats, handleGetSchema } from './analysis-handlers.js';
|
|
9
9
|
export { handleCreateGuarantee, handleListGuarantees, handleCheckGuarantees, handleDeleteGuarantee } from './guarantee-handlers.js';
|
|
10
10
|
export { handleGetFunctionDetails, handleGetContext, handleGetFileOverview, handleGetShape } from './context-handlers.js';
|
|
11
11
|
export { handleReadProjectStructure, handleWriteConfig } from './project-handlers.js';
|
|
@@ -18,5 +18,6 @@ export { handleAddKnowledge, handleQueryKnowledge, handleQueryDecisions, handleS
|
|
|
18
18
|
// Disabled: requires git-ingest (US-17). See US-17 in AI-AGENT-STORIES.md
|
|
19
19
|
// export { handleGitChurn, handleGitCoChange, handleGitOwnership, handleGitArchaeology } from './knowledge-handlers.js';
|
|
20
20
|
export { handleDescribe } from './notation-handlers.js';
|
|
21
|
-
export {
|
|
21
|
+
export { handleQueryGraphql } from './graphql-handlers.js';
|
|
22
22
|
export { handleQueryRegistry } from './registry-handlers.js';
|
|
23
|
+
export { handleFindSharedBehaviors } from './behavior-handlers.js';
|
|
@@ -436,7 +436,7 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
|
|
|
436
436
|
// === Rich context enrichment ===
|
|
437
437
|
// For each found node, add structural context (methods, calls, imports)
|
|
438
438
|
// Only enrich when result set is small enough (≤10 nodes) to avoid latency
|
|
439
|
-
|
|
439
|
+
|
|
440
440
|
const enriched = nodes.length <= 10
|
|
441
441
|
? await enrichNodes(db as any, nodes)
|
|
442
442
|
: nodes;
|
package/src/server.ts
CHANGED
|
@@ -35,17 +35,17 @@ import { PROMPTS, getPrompt } from './prompts.js';
|
|
|
35
35
|
|
|
36
36
|
import { TOOLS } from './definitions/index.js';
|
|
37
37
|
import { initializeFromArgs, setupLogging, getProjectPath } from './state.js';
|
|
38
|
-
import {
|
|
38
|
+
import { errorResult, log } from './utils.js';
|
|
39
39
|
import { getSocketPathOverride } from './state.js';
|
|
40
|
-
import { discoverServices } from './analysis.js';
|
|
41
40
|
import {
|
|
42
41
|
handleQueryGraph,
|
|
43
42
|
handleFindCalls,
|
|
44
43
|
handleFindNodes,
|
|
45
44
|
handleTraceAlias,
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
handleTraceDataflow,
|
|
46
|
+
handleTraceCalls,
|
|
48
47
|
handleCheckInvariant,
|
|
48
|
+
handleDiscoverServices,
|
|
49
49
|
handleAnalyzeProject,
|
|
50
50
|
handleGetAnalysisStatus,
|
|
51
51
|
handleGetStats,
|
|
@@ -78,9 +78,11 @@ import {
|
|
|
78
78
|
// handleGitOwnership,
|
|
79
79
|
// handleGitArchaeology,
|
|
80
80
|
handleDescribe,
|
|
81
|
-
|
|
81
|
+
handleQueryGraphql,
|
|
82
82
|
handleQueryRegistry,
|
|
83
83
|
handleExplain,
|
|
84
|
+
handleTraceEffects,
|
|
85
|
+
handleFindSharedBehaviors,
|
|
84
86
|
} from './handlers/index.js';
|
|
85
87
|
import type { ExplainArgs } from './handlers/index.js';
|
|
86
88
|
import type {
|
|
@@ -119,9 +121,11 @@ import type {
|
|
|
119
121
|
// GitCoChangeArgs,
|
|
120
122
|
// GitOwnershipArgs,
|
|
121
123
|
// GitArchaeologyArgs,
|
|
124
|
+
TraceEffectsArgs,
|
|
122
125
|
DescribeArgs,
|
|
123
126
|
GraphQLQueryArgs,
|
|
124
127
|
QueryRegistryArgs,
|
|
128
|
+
FindSharedBehaviorsArgs,
|
|
125
129
|
} from './types.js';
|
|
126
130
|
|
|
127
131
|
/**
|
|
@@ -247,24 +251,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
247
251
|
break;
|
|
248
252
|
|
|
249
253
|
case 'trace_dataflow':
|
|
250
|
-
result = await
|
|
254
|
+
result = await handleTraceDataflow(asArgs<TraceDataFlowArgs>(args));
|
|
251
255
|
break;
|
|
252
256
|
|
|
253
257
|
case 'trace_calls':
|
|
254
|
-
result = await
|
|
258
|
+
result = await handleTraceCalls(asArgs<TraceCallChainArgs>(args));
|
|
255
259
|
break;
|
|
256
260
|
|
|
257
261
|
case 'explain':
|
|
258
262
|
result = await handleExplain(asArgs<ExplainArgs>(args));
|
|
259
263
|
break;
|
|
260
264
|
|
|
265
|
+
case 'trace_effects':
|
|
266
|
+
result = await handleTraceEffects(asArgs<TraceEffectsArgs>(args));
|
|
267
|
+
break;
|
|
268
|
+
|
|
261
269
|
case 'check_invariant':
|
|
262
270
|
result = await handleCheckInvariant(asArgs<CheckInvariantArgs>(args));
|
|
263
271
|
break;
|
|
264
272
|
|
|
265
273
|
case 'discover_services':
|
|
266
|
-
|
|
267
|
-
result = textResult(`Found ${services.length} service(s):\n${JSON.stringify(services, null, 2)}`);
|
|
274
|
+
result = await handleDiscoverServices();
|
|
268
275
|
break;
|
|
269
276
|
|
|
270
277
|
case 'analyze_project':
|
|
@@ -393,13 +400,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
393
400
|
break;
|
|
394
401
|
|
|
395
402
|
case 'query_graphql':
|
|
396
|
-
result = await
|
|
403
|
+
result = await handleQueryGraphql(asArgs<GraphQLQueryArgs>(args));
|
|
397
404
|
break;
|
|
398
405
|
|
|
399
406
|
case 'query_registry':
|
|
400
407
|
result = await handleQueryRegistry(asArgs<QueryRegistryArgs>(args));
|
|
401
408
|
break;
|
|
402
409
|
|
|
410
|
+
case 'find_shared_behaviors':
|
|
411
|
+
result = await handleFindSharedBehaviors(asArgs<FindSharedBehaviorsArgs>(args));
|
|
412
|
+
break;
|
|
413
|
+
|
|
403
414
|
default:
|
|
404
415
|
result = errorResult(`Unknown tool: ${name}`);
|
|
405
416
|
}
|
package/src/types.ts
CHANGED
|
@@ -81,6 +81,12 @@ export interface TraceCallChainArgs {
|
|
|
81
81
|
max_depth?: number;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export interface TraceEffectsArgs {
|
|
85
|
+
node: string;
|
|
86
|
+
file?: string;
|
|
87
|
+
max_depth?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
export interface GetShapeArgs {
|
|
85
91
|
target: string;
|
|
86
92
|
file?: string;
|
|
@@ -108,6 +114,13 @@ export interface FindNodesArgs {
|
|
|
108
114
|
offset?: number;
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
export interface FindSharedBehaviorsArgs {
|
|
118
|
+
/** Minimum cluster size to include. Default 2. */
|
|
119
|
+
minClusterSize?: number;
|
|
120
|
+
/** Maximum number of clusters to return. Default 100. */
|
|
121
|
+
limit?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
export interface AnalyzeProjectArgs {
|
|
112
125
|
service?: string;
|
|
113
126
|
force?: boolean;
|