@ttsc/graph 0.16.4 → 0.16.6
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/LICENSE +21 -21
- package/README.md +24 -53
- package/lib/TtscGraphApplication.js +59 -0
- package/lib/TtscGraphApplication.js.map +1 -0
- package/lib/index.js +31 -108
- package/lib/index.js.map +1 -1
- package/lib/model/TtscGraphMemory.js +185 -0
- package/lib/model/TtscGraphMemory.js.map +1 -0
- package/lib/model/loadGraph.js +418 -0
- package/lib/model/loadGraph.js.map +1 -0
- package/lib/reduce.js +101 -0
- package/lib/reduce.js.map +1 -0
- package/lib/resolveGraphBinary.js +42 -0
- package/lib/resolveGraphBinary.js.map +1 -0
- package/lib/server/accessAliases.js +53 -0
- package/lib/server/accessAliases.js.map +1 -0
- package/lib/server/createServer.js +1726 -0
- package/lib/server/createServer.js.map +1 -0
- package/lib/server/instructions.js +80 -0
- package/lib/server/instructions.js.map +1 -0
- package/lib/server/resolveHandle.js +25 -0
- package/lib/server/resolveHandle.js.map +1 -0
- package/lib/server/runDetails.js +413 -0
- package/lib/server/runDetails.js.map +1 -0
- package/lib/server/runEntrypoints.js +211 -0
- package/lib/server/runEntrypoints.js.map +1 -0
- package/lib/server/runLookup.js +234 -0
- package/lib/server/runLookup.js.map +1 -0
- package/lib/server/runOverview.js +160 -0
- package/lib/server/runOverview.js.map +1 -0
- package/lib/server/runTrace.js +314 -0
- package/lib/server/runTrace.js.map +1 -0
- package/lib/server/startServer.js +21 -0
- package/lib/server/startServer.js.map +1 -0
- package/lib/structures/ITtscGraphApplication.js +3 -0
- package/lib/structures/ITtscGraphApplication.js.map +1 -0
- package/lib/structures/ITtscGraphDecorator.js +3 -0
- package/lib/structures/ITtscGraphDecorator.js.map +1 -0
- package/lib/structures/ITtscGraphDetails.js +3 -0
- package/lib/structures/ITtscGraphDetails.js.map +1 -0
- package/lib/structures/ITtscGraphDiagnostic.js +3 -0
- package/lib/structures/ITtscGraphDiagnostic.js.map +1 -0
- package/lib/structures/ITtscGraphDump.js +3 -0
- package/lib/structures/ITtscGraphDump.js.map +1 -0
- package/lib/structures/ITtscGraphEdge.js +3 -0
- package/lib/structures/ITtscGraphEdge.js.map +1 -0
- package/lib/structures/ITtscGraphEntrypoints.js +3 -0
- package/lib/structures/ITtscGraphEntrypoints.js.map +1 -0
- package/lib/structures/ITtscGraphEscape.js +3 -0
- package/lib/structures/ITtscGraphEscape.js.map +1 -0
- package/lib/structures/ITtscGraphEvidence.js +3 -0
- package/lib/structures/ITtscGraphEvidence.js.map +1 -0
- package/lib/structures/ITtscGraphLookup.js +3 -0
- package/lib/structures/ITtscGraphLookup.js.map +1 -0
- package/lib/structures/ITtscGraphNode.js +3 -0
- package/lib/structures/ITtscGraphNode.js.map +1 -0
- package/lib/structures/ITtscGraphOverview.js +3 -0
- package/lib/structures/ITtscGraphOverview.js.map +1 -0
- package/lib/structures/ITtscGraphTrace.js +3 -0
- package/lib/structures/ITtscGraphTrace.js.map +1 -0
- package/lib/structures/TtscGraphEdgeKind.js +3 -0
- package/lib/structures/TtscGraphEdgeKind.js.map +1 -0
- package/lib/structures/TtscGraphNodeKind.js +3 -0
- package/lib/structures/TtscGraphNodeKind.js.map +1 -0
- package/lib/structures/TtscGraphNodeModifier.js +3 -0
- package/lib/structures/TtscGraphNodeModifier.js.map +1 -0
- package/lib/structures/index.js +38 -0
- package/lib/structures/index.js.map +1 -0
- package/lib/view.js +140 -0
- package/lib/view.js.map +1 -0
- package/lib/viewer/index.html +123 -0
- package/lib/viewer/viewer.js +4610 -0
- package/package.json +17 -4
- package/src/TtscGraphApplication.ts +64 -0
- package/src/index.ts +40 -126
- package/src/model/TtscGraphMemory.ts +204 -0
- package/src/model/loadGraph.ts +85 -0
- package/src/reduce.ts +165 -0
- package/src/resolveGraphBinary.ts +40 -0
- package/src/server/accessAliases.ts +55 -0
- package/src/server/createServer.ts +101 -0
- package/src/server/instructions.ts +76 -0
- package/src/server/resolveHandle.ts +34 -0
- package/src/server/runDetails.ts +469 -0
- package/src/server/runEntrypoints.ts +256 -0
- package/src/server/runLookup.ts +257 -0
- package/src/server/runOverview.ts +166 -0
- package/src/server/runTrace.ts +359 -0
- package/src/server/startServer.ts +25 -0
- package/src/structures/ITtscGraphApplication.ts +116 -0
- package/src/structures/ITtscGraphDecorator.ts +31 -0
- package/src/structures/ITtscGraphDetails.ts +134 -0
- package/src/structures/ITtscGraphDiagnostic.ts +34 -0
- package/src/structures/ITtscGraphDump.ts +35 -0
- package/src/structures/ITtscGraphEdge.ts +24 -0
- package/src/structures/ITtscGraphEntrypoints.ts +118 -0
- package/src/structures/ITtscGraphEscape.ts +39 -0
- package/src/structures/ITtscGraphEvidence.ts +26 -0
- package/src/structures/ITtscGraphLookup.ts +61 -0
- package/src/structures/ITtscGraphNode.ts +70 -0
- package/src/structures/ITtscGraphOverview.ts +87 -0
- package/src/structures/ITtscGraphTrace.ts +140 -0
- package/src/structures/TtscGraphEdgeKind.ts +23 -0
- package/src/structures/TtscGraphNodeKind.ts +26 -0
- package/src/structures/TtscGraphNodeModifier.ts +19 -0
- package/src/structures/index.ts +22 -0
- package/src/view.ts +155 -0
- package/src/viewer/index.html +123 -0
- package/src/viewer/main.ts +209 -0
- package/lib/bin.d.ts +0 -2
- package/lib/diagnostics.d.ts +0 -23
- package/lib/diagnostics.js +0 -84
- package/lib/diagnostics.js.map +0 -1
- package/lib/index.d.ts +0 -29
- package/src/diagnostics.ts +0 -93
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
|
+
import { ITtscGraphEvidence } from "../structures/ITtscGraphEvidence";
|
|
3
|
+
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
4
|
+
import { ITtscGraphTrace } from "../structures/ITtscGraphTrace";
|
|
5
|
+
import { accessAliasesFor } from "./accessAliases";
|
|
6
|
+
import { resolveGraphHandle } from "./resolveHandle";
|
|
7
|
+
import { edgeEvidenceOf, edgeEvidenceTextOf, signatureOf } from "./runDetails";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_DEPTH = 2;
|
|
10
|
+
const DEFAULT_MAX_NODES = 6;
|
|
11
|
+
const MAX_OPEN_DEPTH = 2;
|
|
12
|
+
const MAX_OPEN_NODES = 8;
|
|
13
|
+
const MAX_HOPS_PER_NODE = 1;
|
|
14
|
+
const MAX_STEPS = 6;
|
|
15
|
+
const MAX_NEXT_HANDLES = 2;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Breadth-first trace along the dependency graph. Structural
|
|
19
|
+
* (contains/exports/imports) edges are excluded so the path is real call/type
|
|
20
|
+
* flow; forward walks callees, reverse and impact walk callers. Impact
|
|
21
|
+
* additionally tags each reached node's role so the blast radius on the public
|
|
22
|
+
* surface is legible.
|
|
23
|
+
*/
|
|
24
|
+
export function runTrace(
|
|
25
|
+
graph: TtscGraphMemory,
|
|
26
|
+
props: ITtscGraphTrace.IRequest,
|
|
27
|
+
): ITtscGraphTrace {
|
|
28
|
+
const direction = props.direction ?? "forward";
|
|
29
|
+
const focus = props.focus ?? "all";
|
|
30
|
+
const maxDepth = bound(props.maxDepth, DEFAULT_DEPTH, 1, MAX_OPEN_DEPTH);
|
|
31
|
+
const maxNodes = bound(props.maxNodes, DEFAULT_MAX_NODES, 1, MAX_OPEN_NODES);
|
|
32
|
+
const maxHops = maxNodes * MAX_HOPS_PER_NODE;
|
|
33
|
+
const reverse = direction === "reverse" || direction === "impact";
|
|
34
|
+
// Only an impact trace tags reached nodes with their public-surface role; for
|
|
35
|
+
// forward/reverse the role is noise.
|
|
36
|
+
const withRoles = direction === "impact";
|
|
37
|
+
|
|
38
|
+
const start = resolveGraphHandle(graph, props.from);
|
|
39
|
+
if (start.candidates) {
|
|
40
|
+
return {
|
|
41
|
+
type: "trace",
|
|
42
|
+
direction,
|
|
43
|
+
hops: [],
|
|
44
|
+
reached: [],
|
|
45
|
+
truncated: false,
|
|
46
|
+
candidates: start.candidates.map((n) => summary(graph, n)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (start.node === undefined) {
|
|
50
|
+
return {
|
|
51
|
+
type: "trace",
|
|
52
|
+
direction,
|
|
53
|
+
hops: [],
|
|
54
|
+
reached: [],
|
|
55
|
+
truncated: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Path mode: with `to`, return the dependency path from `from` to `to`, the
|
|
60
|
+
// one-call answer for "how does A reach B", instead of an open-ended trace.
|
|
61
|
+
if (props.to !== undefined && props.to !== "") {
|
|
62
|
+
const base = {
|
|
63
|
+
type: "trace" as const,
|
|
64
|
+
direction: "path",
|
|
65
|
+
hops: [],
|
|
66
|
+
reached: [],
|
|
67
|
+
truncated: false,
|
|
68
|
+
};
|
|
69
|
+
const target = resolveGraphHandle(graph, props.to);
|
|
70
|
+
if (target.candidates) {
|
|
71
|
+
return {
|
|
72
|
+
...base,
|
|
73
|
+
start: summary(graph, start.node),
|
|
74
|
+
candidates: target.candidates.map((n) => summary(graph, n)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (target.node === undefined) {
|
|
78
|
+
return { ...base, start: summary(graph, start.node) };
|
|
79
|
+
}
|
|
80
|
+
const found = findPath(
|
|
81
|
+
graph,
|
|
82
|
+
start.node.id,
|
|
83
|
+
target.node.id,
|
|
84
|
+
bound(props.maxDepth, 12, 1, 12),
|
|
85
|
+
focus,
|
|
86
|
+
);
|
|
87
|
+
const path = found?.path ?? [];
|
|
88
|
+
const hops = found?.hops ?? [];
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
start: summary(graph, start.node),
|
|
92
|
+
target: summary(graph, target.node),
|
|
93
|
+
hops,
|
|
94
|
+
path: path.map((node, i) => summary(graph, node, i, false, true)),
|
|
95
|
+
steps: traceSteps(graph, hops),
|
|
96
|
+
next: nextFromPath(path),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hops: ITtscGraphTrace.IHop[] = [];
|
|
101
|
+
const reached = new Map<string, ITtscGraphTrace.INode>();
|
|
102
|
+
const visited = new Set<string>([start.node.id]);
|
|
103
|
+
let queue: Array<{ id: string; depth: number }> = [
|
|
104
|
+
{ id: start.node.id, depth: 0 },
|
|
105
|
+
];
|
|
106
|
+
let truncated = false;
|
|
107
|
+
|
|
108
|
+
while (queue.length > 0) {
|
|
109
|
+
const next: Array<{ id: string; depth: number }> = [];
|
|
110
|
+
for (const { id, depth } of queue) {
|
|
111
|
+
if (depth >= maxDepth) {
|
|
112
|
+
truncated = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const edges = reverse ? graph.incoming(id) : graph.outgoing(id);
|
|
116
|
+
for (const edge of edges) {
|
|
117
|
+
if (!traversable(edge.kind, focus)) continue;
|
|
118
|
+
const otherId = reverse ? edge.from : edge.to;
|
|
119
|
+
const other = graph.node(otherId);
|
|
120
|
+
if (other === undefined || other.kind === "file") continue;
|
|
121
|
+
const hop: ITtscGraphTrace.IHop = {
|
|
122
|
+
from: edge.from,
|
|
123
|
+
to: edge.to,
|
|
124
|
+
kind: edge.kind,
|
|
125
|
+
depth: depth + 1,
|
|
126
|
+
};
|
|
127
|
+
const evidence = edgeEvidenceOf(edge);
|
|
128
|
+
if (evidence !== undefined) hop.evidence = evidence;
|
|
129
|
+
const aliases = accessAliasesFor(
|
|
130
|
+
graph.node(edge.to),
|
|
131
|
+
edgeEvidenceTextOf(edge),
|
|
132
|
+
);
|
|
133
|
+
if (aliases !== undefined) hop.aliases = aliases;
|
|
134
|
+
// A back-edge to the start or an already-reached node: record the hop;
|
|
135
|
+
// its endpoints are already represented.
|
|
136
|
+
if (visited.has(otherId)) {
|
|
137
|
+
if (hops.length >= maxHops) truncated = true;
|
|
138
|
+
else hops.push(hop);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// A new node past the cap is left unrepresented, so drop its hop too:
|
|
142
|
+
// every hop's endpoints stay resolvable in `reached`/`start`.
|
|
143
|
+
if (reached.size >= maxNodes) {
|
|
144
|
+
truncated = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (hops.length >= maxHops) {
|
|
148
|
+
truncated = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
visited.add(otherId);
|
|
152
|
+
reached.set(otherId, summary(graph, other, depth + 1, withRoles));
|
|
153
|
+
next.push({ id: otherId, depth: depth + 1 });
|
|
154
|
+
hops.push(hop);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
queue = next;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
type: "trace",
|
|
162
|
+
start: summary(graph, start.node),
|
|
163
|
+
direction,
|
|
164
|
+
hops,
|
|
165
|
+
reached: [...reached.values()],
|
|
166
|
+
steps: traceSteps(graph, hops),
|
|
167
|
+
next: {
|
|
168
|
+
details: [start.node.id, ...reached.keys()].slice(0, MAX_NEXT_HANDLES),
|
|
169
|
+
traceFrom: [...reached.keys()].slice(0, MAX_NEXT_HANDLES),
|
|
170
|
+
},
|
|
171
|
+
truncated,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function nextFromPath(path: ITtscGraphNode[]): ITtscGraphTrace.INext {
|
|
176
|
+
return {
|
|
177
|
+
details: path.map((node) => node.id),
|
|
178
|
+
traceFrom: path.length > 0 ? [path[path.length - 1]!.id] : [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function traceSteps(
|
|
183
|
+
graph: TtscGraphMemory,
|
|
184
|
+
hops: ITtscGraphTrace.IHop[],
|
|
185
|
+
): string[] {
|
|
186
|
+
return hops.slice(0, MAX_STEPS).map((hop) => {
|
|
187
|
+
const from = graph.node(hop.from);
|
|
188
|
+
const to = graph.node(hop.to);
|
|
189
|
+
const lhs = from?.qualifiedName ?? from?.name ?? hop.from;
|
|
190
|
+
const rhs = to?.qualifiedName ?? to?.name ?? hop.to;
|
|
191
|
+
const evidence =
|
|
192
|
+
hop.evidence === undefined
|
|
193
|
+
? ""
|
|
194
|
+
: ` at ${hop.evidence.file}:${hop.evidence.startLine}`;
|
|
195
|
+
const aliases =
|
|
196
|
+
hop.aliases === undefined ? "" : ` aliases ${hop.aliases.join(", ")}`;
|
|
197
|
+
return `${lhs} -[${hop.kind}${evidence}${aliases}]-> ${rhs}`;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The shortest dependency path from `startId` to `targetId` over real (non-
|
|
203
|
+
* structural) forward edges, breadth-first, or null when `targetId` is not
|
|
204
|
+
* reachable within maxDepth. Returns the nodes in order and the hops between.
|
|
205
|
+
*/
|
|
206
|
+
function findPath(
|
|
207
|
+
graph: TtscGraphMemory,
|
|
208
|
+
startId: string,
|
|
209
|
+
targetId: string,
|
|
210
|
+
maxDepth: number,
|
|
211
|
+
focus: ITtscGraphTrace.IRequest["focus"],
|
|
212
|
+
): { path: ITtscGraphNode[]; hops: ITtscGraphTrace.IHop[] } | null {
|
|
213
|
+
const startNode = graph.node(startId);
|
|
214
|
+
if (startNode === undefined) return null;
|
|
215
|
+
if (startId === targetId) return { path: [startNode], hops: [] };
|
|
216
|
+
const parent = new Map<
|
|
217
|
+
string,
|
|
218
|
+
{
|
|
219
|
+
via: string;
|
|
220
|
+
kind: string;
|
|
221
|
+
evidence?: ITtscGraphEvidence;
|
|
222
|
+
evidenceText?: string;
|
|
223
|
+
}
|
|
224
|
+
>();
|
|
225
|
+
const visited = new Set<string>([startId]);
|
|
226
|
+
let queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
|
|
227
|
+
while (queue.length > 0) {
|
|
228
|
+
const next: Array<{ id: string; depth: number }> = [];
|
|
229
|
+
for (const { id, depth } of queue) {
|
|
230
|
+
if (depth >= maxDepth) continue;
|
|
231
|
+
for (const edge of graph.outgoing(id)) {
|
|
232
|
+
if (!traversable(edge.kind, focus)) continue;
|
|
233
|
+
const otherId = edge.to;
|
|
234
|
+
if (visited.has(otherId)) continue;
|
|
235
|
+
const other = graph.node(otherId);
|
|
236
|
+
if (other === undefined || other.kind === "file") continue;
|
|
237
|
+
visited.add(otherId);
|
|
238
|
+
const evidence = edgeEvidenceOf(edge);
|
|
239
|
+
parent.set(otherId, {
|
|
240
|
+
via: id,
|
|
241
|
+
kind: edge.kind,
|
|
242
|
+
evidence,
|
|
243
|
+
evidenceText: edgeEvidenceTextOf(edge),
|
|
244
|
+
});
|
|
245
|
+
if (otherId === targetId) {
|
|
246
|
+
const ids: string[] = [otherId];
|
|
247
|
+
let cur = otherId;
|
|
248
|
+
while (cur !== startId) {
|
|
249
|
+
const p = parent.get(cur);
|
|
250
|
+
if (p === undefined) break;
|
|
251
|
+
ids.unshift(p.via);
|
|
252
|
+
cur = p.via;
|
|
253
|
+
}
|
|
254
|
+
const path: ITtscGraphNode[] = [];
|
|
255
|
+
for (const nid of ids) {
|
|
256
|
+
const n = graph.node(nid);
|
|
257
|
+
if (n !== undefined) path.push(n);
|
|
258
|
+
}
|
|
259
|
+
const hops: ITtscGraphTrace.IHop[] = [];
|
|
260
|
+
for (let i = 1; i < path.length; i++) {
|
|
261
|
+
const node = path[i]!;
|
|
262
|
+
const parentEdge = parent.get(node.id);
|
|
263
|
+
const hop: ITtscGraphTrace.IHop = {
|
|
264
|
+
from: path[i - 1]!.id,
|
|
265
|
+
to: node.id,
|
|
266
|
+
kind: parentEdge?.kind ?? "calls",
|
|
267
|
+
depth: i,
|
|
268
|
+
};
|
|
269
|
+
if (parentEdge?.evidence !== undefined)
|
|
270
|
+
hop.evidence = parentEdge.evidence;
|
|
271
|
+
const aliases = accessAliasesFor(node, parentEdge?.evidenceText);
|
|
272
|
+
if (aliases !== undefined) hop.aliases = aliases;
|
|
273
|
+
hops.push(hop);
|
|
274
|
+
}
|
|
275
|
+
return { path, hops };
|
|
276
|
+
}
|
|
277
|
+
next.push({ id: otherId, depth: depth + 1 });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
queue = next;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Summarize a node for a trace result. With `withRoles`, tag the public-surface
|
|
287
|
+
* roles (exported / test) an impact trace reports; other directions omit them.
|
|
288
|
+
*/
|
|
289
|
+
function summary(
|
|
290
|
+
graph: TtscGraphMemory,
|
|
291
|
+
node: ITtscGraphNode,
|
|
292
|
+
depth?: number,
|
|
293
|
+
withRoles = false,
|
|
294
|
+
withSignature = false,
|
|
295
|
+
): ITtscGraphTrace.INode {
|
|
296
|
+
const out: ITtscGraphTrace.INode = {
|
|
297
|
+
id: node.id,
|
|
298
|
+
name: node.qualifiedName ?? node.name,
|
|
299
|
+
kind: node.kind,
|
|
300
|
+
file: node.file,
|
|
301
|
+
};
|
|
302
|
+
if (depth !== undefined) out.depth = depth;
|
|
303
|
+
if (withSignature) {
|
|
304
|
+
const sig = signatureOf(graph.project, node);
|
|
305
|
+
if (sig !== undefined) out.signature = sig;
|
|
306
|
+
}
|
|
307
|
+
if (withRoles) {
|
|
308
|
+
const roles: string[] = [];
|
|
309
|
+
if (node.exported) roles.push("exported");
|
|
310
|
+
if (isTestFile(node.file)) roles.push("test");
|
|
311
|
+
if (roles.length > 0) out.roles = roles;
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** An edge the trace should follow: a real dependency, not a structural edge. */
|
|
317
|
+
function traversable(
|
|
318
|
+
kind: string,
|
|
319
|
+
focus: ITtscGraphTrace.IRequest["focus"],
|
|
320
|
+
): boolean {
|
|
321
|
+
if (kind === "contains" || kind === "exports" || kind === "imports") {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (focus === "execution") {
|
|
325
|
+
return (
|
|
326
|
+
kind === "calls" ||
|
|
327
|
+
kind === "instantiates" ||
|
|
328
|
+
kind === "accesses" ||
|
|
329
|
+
kind === "renders"
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (focus === "types") {
|
|
333
|
+
return (
|
|
334
|
+
kind === "type_ref" ||
|
|
335
|
+
kind === "extends" ||
|
|
336
|
+
kind === "implements" ||
|
|
337
|
+
kind === "overrides" ||
|
|
338
|
+
kind === "decorates"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isTestFile(file: string): boolean {
|
|
345
|
+
return (
|
|
346
|
+
/(^|\/)(test|tests|__tests__|spec)\//.test(file) ||
|
|
347
|
+
/\.(test|spec)\.[cm]?tsx?$/.test(file)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function bound(
|
|
352
|
+
value: number | undefined,
|
|
353
|
+
fallback: number,
|
|
354
|
+
min: number,
|
|
355
|
+
max: number,
|
|
356
|
+
): number {
|
|
357
|
+
const n = value === undefined || !Number.isFinite(value) ? fallback : value;
|
|
358
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
359
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
|
|
3
|
+
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
4
|
+
import { loadGraph } from "../model/loadGraph";
|
|
5
|
+
import { createServer } from "./createServer";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Serve the graph tools over MCP on stdio. The server answers the MCP handshake
|
|
9
|
+
* immediately and builds the resident graph on the first tool call, so a large
|
|
10
|
+
* project cannot make the client give up before tools are advertised.
|
|
11
|
+
*/
|
|
12
|
+
export async function startServer(options: {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
tsconfig?: string;
|
|
15
|
+
/** Server version reported in the MCP handshake. */
|
|
16
|
+
version: string;
|
|
17
|
+
}): Promise<void> {
|
|
18
|
+
let graph: TtscGraphMemory | undefined;
|
|
19
|
+
const server = createServer(() => {
|
|
20
|
+
graph ??= loadGraph({ cwd: options.cwd, tsconfig: options.tsconfig });
|
|
21
|
+
return graph;
|
|
22
|
+
}, options.version);
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ITtscGraphDetails } from "./ITtscGraphDetails";
|
|
2
|
+
import { ITtscGraphEntrypoints } from "./ITtscGraphEntrypoints";
|
|
3
|
+
import { ITtscGraphEscape } from "./ITtscGraphEscape";
|
|
4
|
+
import { ITtscGraphLookup } from "./ITtscGraphLookup";
|
|
5
|
+
import { ITtscGraphOverview } from "./ITtscGraphOverview";
|
|
6
|
+
import { ITtscGraphTrace } from "./ITtscGraphTrace";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The MCP tool surface of `@ttsc/graph`, as a typed application.
|
|
10
|
+
*
|
|
11
|
+
* The single method is the single MCP tool. Its parameter object becomes the
|
|
12
|
+
* JSON schema once `typia.llm.controller` reflects this interface.
|
|
13
|
+
* `TtscGraphApplication` implements it over the resident graph.
|
|
14
|
+
*/
|
|
15
|
+
export interface ITtscGraphApplication {
|
|
16
|
+
/**
|
|
17
|
+
* Query the TypeScript project graph for code evidence.
|
|
18
|
+
*
|
|
19
|
+
* Use this before answering TypeScript codebase questions. It returns
|
|
20
|
+
* checker-resolved symbols, dependency paths, edge evidence ranges, and
|
|
21
|
+
* sourceSpan anchors from the resident graph, avoiding assumptions, shell
|
|
22
|
+
* search, or file reads when graph evidence is enough.
|
|
23
|
+
*
|
|
24
|
+
* Fill properties in order: question, graphNeed, draft, review, request.
|
|
25
|
+
* Write `draft.reason` before `draft.type`, then choose `entrypoints`,
|
|
26
|
+
* `lookup`, `trace`, `details`, `overview`, or `escape`.
|
|
27
|
+
*
|
|
28
|
+
* Most answers should need 1-3 calls; four is the hard stop for one answer.
|
|
29
|
+
* Keep slices small, prefer defaults, and stop once file/symbol/range
|
|
30
|
+
* evidence is enough.
|
|
31
|
+
*
|
|
32
|
+
* @param props The reasoning and selected graph request
|
|
33
|
+
* @returns One `result` union member matching the selected request type
|
|
34
|
+
*/
|
|
35
|
+
query(props: ITtscGraphApplication.IProps): ITtscGraphApplication.IResult;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export namespace ITtscGraphApplication {
|
|
39
|
+
/** Think, review, then submit exactly one graph request. */
|
|
40
|
+
export interface IProps {
|
|
41
|
+
/**
|
|
42
|
+
* User's TypeScript code question.
|
|
43
|
+
*
|
|
44
|
+
* Restate the codebase question being answered. Keep this about TypeScript
|
|
45
|
+
* source, symbols, call flow, type flow, or architecture. If the user is
|
|
46
|
+
* asking about scripts, config, generated output, or documentation instead,
|
|
47
|
+
* say that boundary here.
|
|
48
|
+
*/
|
|
49
|
+
question: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Why the resident graph is the next evidence source.
|
|
53
|
+
*
|
|
54
|
+
* State what graph evidence is needed and why assumptions or broad shell
|
|
55
|
+
* search are not the next step for this call. Name the smallest evidence
|
|
56
|
+
* that would let the agent stop. If graph is not actually the right source,
|
|
57
|
+
* say that and use `escape`.
|
|
58
|
+
*/
|
|
59
|
+
graphNeed: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* First request-type decision before arguments are filled.
|
|
63
|
+
*
|
|
64
|
+
* Explain why one operation class is smaller than the alternatives, then
|
|
65
|
+
* name it in `draft.type`. This is only the draft; the final arguments are
|
|
66
|
+
* in `request` after `review`.
|
|
67
|
+
*/
|
|
68
|
+
draft: IRequestDraft;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Critical review of the draft request.
|
|
72
|
+
*
|
|
73
|
+
* Check whether the draft avoids overfetch, non-graph fallback, broad
|
|
74
|
+
* reads, and unnecessary neighbor expansion. If this would be a fourth
|
|
75
|
+
* broad lookup, trace, or details call, prefer answering from evidence in
|
|
76
|
+
* hand or choose `escape`. Do not spend graph calls only to hunt for tests.
|
|
77
|
+
* If the draft is wrong, choose the corrected type in `request`.
|
|
78
|
+
*/
|
|
79
|
+
review: string;
|
|
80
|
+
|
|
81
|
+
/** The graph operation chosen from the reasoning above, or a no-op escape. */
|
|
82
|
+
request:
|
|
83
|
+
| ITtscGraphEntrypoints.IRequest
|
|
84
|
+
| ITtscGraphLookup.IRequest
|
|
85
|
+
| ITtscGraphTrace.IRequest
|
|
86
|
+
| ITtscGraphDetails.IRequest
|
|
87
|
+
| ITtscGraphOverview.IRequest
|
|
88
|
+
| ITtscGraphEscape.IRequest;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** First-pass operation choice before final request arguments. */
|
|
92
|
+
export interface IRequestDraft {
|
|
93
|
+
/** Why this operation type is the smallest useful next step. */
|
|
94
|
+
reason: string;
|
|
95
|
+
|
|
96
|
+
/** Draft discriminator for the intended graph operation. */
|
|
97
|
+
type:
|
|
98
|
+
| ITtscGraphEntrypoints.IRequest["type"]
|
|
99
|
+
| ITtscGraphLookup.IRequest["type"]
|
|
100
|
+
| ITtscGraphTrace.IRequest["type"]
|
|
101
|
+
| ITtscGraphDetails.IRequest["type"]
|
|
102
|
+
| ITtscGraphOverview.IRequest["type"]
|
|
103
|
+
| ITtscGraphEscape.IRequest["type"];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The selected request's output. `result.type` mirrors `request.type`. */
|
|
107
|
+
export interface IResult {
|
|
108
|
+
result:
|
|
109
|
+
| ITtscGraphEntrypoints
|
|
110
|
+
| ITtscGraphLookup
|
|
111
|
+
| ITtscGraphTrace
|
|
112
|
+
| ITtscGraphDetails
|
|
113
|
+
| ITtscGraphOverview
|
|
114
|
+
| ITtscGraphEscape;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decorator as written on a declaration, carried on the decorated
|
|
3
|
+
* {@link ITtscGraphNode}'s `decorators`.
|
|
4
|
+
*
|
|
5
|
+
* The graph reports the decorator faithfully rather than interpreting any
|
|
6
|
+
* framework's convention: the `name` is the decorator as written (`Controller`,
|
|
7
|
+
* `Get`, `TypedRoute.Get`, ...), and statically resolvable literal arguments
|
|
8
|
+
* are preserved so a consumer can apply its own meaning without re-parsing
|
|
9
|
+
* source.
|
|
10
|
+
*/
|
|
11
|
+
export interface ITtscGraphDecorator {
|
|
12
|
+
/**
|
|
13
|
+
* The decorator name as written, qualified through its access path:
|
|
14
|
+
* `Controller`, `Get`, `TypedRoute.Get`, `MessagePattern`.
|
|
15
|
+
*/
|
|
16
|
+
name: string;
|
|
17
|
+
|
|
18
|
+
/** The literal call arguments, in source order. Empty for a bare decorator. */
|
|
19
|
+
arguments: ITtscGraphDecorator.IArgument[];
|
|
20
|
+
}
|
|
21
|
+
export namespace ITtscGraphDecorator {
|
|
22
|
+
/**
|
|
23
|
+
* One argument of an {@link ITtscGraphDecorator}. `literal` is set only when
|
|
24
|
+
* the argument is a string, number, or boolean literal the producer could
|
|
25
|
+
* resolve statically, so a consumer can use it without evaluating code.
|
|
26
|
+
*/
|
|
27
|
+
export interface IArgument {
|
|
28
|
+
/** The statically-resolved literal value, when the argument is a literal. */
|
|
29
|
+
literal?: string | number | boolean;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ITtscGraphDecorator } from "./ITtscGraphDecorator";
|
|
2
|
+
import { ITtscGraphEvidence } from "./ITtscGraphEvidence";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The resolved symbol details returned for a set of handles.
|
|
6
|
+
*
|
|
7
|
+
* The default payload is source-free: signatures, member outlines, and direct
|
|
8
|
+
* graph summaries with sourceSpan anchors.
|
|
9
|
+
*/
|
|
10
|
+
export interface ITtscGraphDetails {
|
|
11
|
+
/** Discriminator for selected symbol inspection. */
|
|
12
|
+
type: "details";
|
|
13
|
+
|
|
14
|
+
nodes: ITtscGraphDetails.INode[];
|
|
15
|
+
|
|
16
|
+
/** Handles that resolved to no node, or that were ambiguous. */
|
|
17
|
+
unknown: string[];
|
|
18
|
+
}
|
|
19
|
+
export namespace ITtscGraphDetails {
|
|
20
|
+
/** Which handles to inspect, and how much of each to return. */
|
|
21
|
+
export interface IRequest {
|
|
22
|
+
/** Discriminator for selected symbol inspection. */
|
|
23
|
+
type: "details";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Node ids from another tool, or dotted symbol handles such as
|
|
27
|
+
* `OrderService.create`. Pass the few handles you need for source-free
|
|
28
|
+
* details; use `trace` when you need a path instead of widening this call.
|
|
29
|
+
*/
|
|
30
|
+
handles: string[];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Also list each node's direct dependencies and dependents (the symbols it
|
|
34
|
+
* uses and the symbols that use it). The list is capped; raise
|
|
35
|
+
* `neighborLimit` only when the first slice is not enough.
|
|
36
|
+
*
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
neighbors?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maximum dependencies and dependents to return per side when
|
|
43
|
+
* `neighbors:true`.
|
|
44
|
+
*
|
|
45
|
+
* @default 2
|
|
46
|
+
*/
|
|
47
|
+
neighborLimit?: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Maximum owned members to return for a container or object literal. Raise
|
|
51
|
+
* only when the first outline is truncated.
|
|
52
|
+
*
|
|
53
|
+
* @default 6
|
|
54
|
+
*/
|
|
55
|
+
memberLimit?: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Maximum direct execution and type references to return per group. Raise
|
|
59
|
+
* only when the first dependency slice is not enough.
|
|
60
|
+
*
|
|
61
|
+
* @default 1
|
|
62
|
+
*/
|
|
63
|
+
dependencyLimit?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One inspected node: its declared shape and graph coordinates. */
|
|
67
|
+
export interface INode {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
kind: string;
|
|
71
|
+
file: string;
|
|
72
|
+
/** 1-based declaration line, when known. */
|
|
73
|
+
line?: number;
|
|
74
|
+
/** The declaration signature: its first line(s) up to the body. */
|
|
75
|
+
signature?: string;
|
|
76
|
+
/** Decorators written on this declaration, when any. */
|
|
77
|
+
decorators?: ITtscGraphDecorator[];
|
|
78
|
+
/** Assigned implementation span, when source comes from one. */
|
|
79
|
+
implementation?: ITtscGraphEvidence;
|
|
80
|
+
/** Direct execution dependencies in source order, with edge evidence. */
|
|
81
|
+
calls?: IReference[];
|
|
82
|
+
/** Direct type dependencies in source order, with edge evidence. */
|
|
83
|
+
types?: IReference[];
|
|
84
|
+
/** String literal values from the signature. */
|
|
85
|
+
literals?: string[];
|
|
86
|
+
/**
|
|
87
|
+
* For a container or object-literal variable: the owned symbol or top-level
|
|
88
|
+
* property outline a consumer reaches for, without bodies.
|
|
89
|
+
*/
|
|
90
|
+
members?: IMember[];
|
|
91
|
+
/** The declaration or implementation file and line range, when known. */
|
|
92
|
+
sourceSpan?: Pick<ITtscGraphEvidence, "file" | "startLine" | "endLine">;
|
|
93
|
+
/** Symbols this node uses (outgoing dependency edges). */
|
|
94
|
+
dependsOn?: IReference[];
|
|
95
|
+
/** Symbols that use this node (incoming dependency edges). */
|
|
96
|
+
dependedOnBy?: IReference[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One member of a container node, with its signature but not its body. */
|
|
100
|
+
export interface IMember {
|
|
101
|
+
name: string;
|
|
102
|
+
kind: string;
|
|
103
|
+
/** 1-based declaration line, when known. */
|
|
104
|
+
line?: number;
|
|
105
|
+
/** The member's declaration signature. */
|
|
106
|
+
signature?: string;
|
|
107
|
+
/** Decorators written on this member, when any. */
|
|
108
|
+
decorators?: ITtscGraphDecorator[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A dependency neighbor of an inspected node and the edge that links them. */
|
|
112
|
+
export interface IReference {
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
kind: string;
|
|
116
|
+
file: string;
|
|
117
|
+
/** 1-based declaration line, when known. */
|
|
118
|
+
line?: number;
|
|
119
|
+
/** The edge kind connecting the two (`calls`, `type_ref`, ...). */
|
|
120
|
+
relation: string;
|
|
121
|
+
/**
|
|
122
|
+
* Source span for the expression that produced this relationship. It lets
|
|
123
|
+
* an agent see why the edge exists without opening the file.
|
|
124
|
+
*/
|
|
125
|
+
evidence?: ITtscGraphEvidence;
|
|
126
|
+
/**
|
|
127
|
+
* Stable access-path aliases derived from edge evidence. For example, an
|
|
128
|
+
* edge to `Owner.member` through `obj.slot.member` may expose
|
|
129
|
+
* `Owner.slot.member` so answers can preserve both the resolved symbol and
|
|
130
|
+
* the source access path.
|
|
131
|
+
*/
|
|
132
|
+
aliases?: string[];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A compiler or plugin diagnostic, fused onto the graph so an edit-triage query
|
|
3
|
+
* can name the owning symbol of an error.
|
|
4
|
+
*
|
|
5
|
+
* The TypeScript semantic pass contributes numeric-coded `tsc` diagnostics;
|
|
6
|
+
* `@ttsc/lint` rules and transform plugins (typia, nestia, …) contribute
|
|
7
|
+
* `plugin`/`lint` findings whose `code` is a string. `node` is set when the
|
|
8
|
+
* finding's position was attributed to a graph node.
|
|
9
|
+
*/
|
|
10
|
+
export interface ITtscGraphDiagnostic {
|
|
11
|
+
/** Project-relative path of the file the diagnostic is reported in. */
|
|
12
|
+
file: string;
|
|
13
|
+
|
|
14
|
+
/** 1-based line of the diagnostic. */
|
|
15
|
+
line: number;
|
|
16
|
+
|
|
17
|
+
/** 1-based column of the diagnostic, when known. */
|
|
18
|
+
column?: number;
|
|
19
|
+
|
|
20
|
+
/** Numeric `tsc` code, or string rule id for a lint/plugin finding. */
|
|
21
|
+
code: number | string;
|
|
22
|
+
|
|
23
|
+
/** The human-readable diagnostic message. */
|
|
24
|
+
message: string;
|
|
25
|
+
|
|
26
|
+
/** Severity, when the producer distinguishes it. */
|
|
27
|
+
severity?: "error" | "warning" | "info" | "hint";
|
|
28
|
+
|
|
29
|
+
/** Which lane produced the diagnostic. */
|
|
30
|
+
origin?: "tsc" | "plugin" | "lint";
|
|
31
|
+
|
|
32
|
+
/** Node id the diagnostic was fused onto, when resolved. */
|
|
33
|
+
node?: string;
|
|
34
|
+
}
|