@ttsc/graph 0.18.3 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -36
- package/lib/TtscGraphApplication.d.ts +4 -3
- package/lib/TtscGraphApplication.js +54 -19
- package/lib/TtscGraphApplication.js.map +1 -1
- package/lib/index.d.ts +9 -4
- package/lib/index.js +24 -5
- package/lib/index.js.map +1 -1
- package/lib/model/TtscGraphMemory.js +80 -22
- package/lib/model/TtscGraphMemory.js.map +1 -1
- package/lib/model/TtscGraphSession.d.ts +36 -0
- package/lib/model/TtscGraphSession.js +457 -0
- package/lib/model/TtscGraphSession.js.map +1 -0
- package/lib/model/loadGraph.d.ts +3 -3
- package/lib/model/loadGraph.js +94 -141
- package/lib/model/loadGraph.js.map +1 -1
- package/lib/resolveGraphBinary.d.ts +4 -2
- package/lib/resolveGraphBinary.js +7 -3
- package/lib/resolveGraphBinary.js.map +1 -1
- package/lib/server/createServer.d.ts +16 -9
- package/lib/server/createServer.js +286 -190
- package/lib/server/createServer.js.map +1 -1
- package/lib/server/exportSurface.d.ts +26 -0
- package/lib/server/exportSurface.js +44 -0
- package/lib/server/exportSurface.js.map +1 -0
- package/lib/server/resolveHandle.d.ts +26 -1
- package/lib/server/resolveHandle.js +124 -4
- package/lib/server/resolveHandle.js.map +1 -1
- package/lib/server/resultAudit.d.ts +74 -0
- package/lib/server/resultAudit.js +110 -0
- package/lib/server/resultAudit.js.map +1 -0
- package/lib/server/resultNext.d.ts +9 -0
- package/lib/server/resultNext.js +11 -0
- package/lib/server/resultNext.js.map +1 -0
- package/lib/server/runDetails.d.ts +15 -3
- package/lib/server/runDetails.js +105 -23
- package/lib/server/runDetails.js.map +1 -1
- package/lib/server/runEntrypoints.d.ts +2 -1
- package/lib/server/runEntrypoints.js +17 -10
- package/lib/server/runEntrypoints.js.map +1 -1
- package/lib/server/runLookup.d.ts +2 -1
- package/lib/server/runLookup.js +21 -9
- package/lib/server/runLookup.js.map +1 -1
- package/lib/server/runOverview.d.ts +2 -1
- package/lib/server/runOverview.js +5 -4
- package/lib/server/runOverview.js.map +1 -1
- package/lib/server/runTour.d.ts +2 -1
- package/lib/server/runTour.js +707 -331
- package/lib/server/runTour.js.map +1 -1
- package/lib/server/runTrace.d.ts +2 -1
- package/lib/server/runTrace.js +234 -54
- package/lib/server/runTrace.js.map +1 -1
- package/lib/server/startServer.d.ts +3 -2
- package/lib/server/startServer.js +13 -7
- package/lib/server/startServer.js.map +1 -1
- package/lib/structures/ITtscGraphApplication.d.ts +122 -114
- package/lib/structures/ITtscGraphDecorator.d.ts +4 -7
- package/lib/structures/ITtscGraphDetails.d.ts +51 -43
- package/lib/structures/ITtscGraphDump.d.ts +36 -11
- package/lib/structures/ITtscGraphEdge.d.ts +4 -6
- package/lib/structures/ITtscGraphEntrypoints.d.ts +10 -27
- package/lib/structures/ITtscGraphEscape.d.ts +6 -16
- package/lib/structures/ITtscGraphEvidence.d.ts +5 -8
- package/lib/structures/ITtscGraphLookup.d.ts +9 -20
- package/lib/structures/ITtscGraphNext.d.ts +10 -7
- package/lib/structures/ITtscGraphNode.d.ts +17 -8
- package/lib/structures/ITtscGraphOverview.d.ts +10 -14
- package/lib/structures/ITtscGraphSpan.d.ts +29 -0
- package/lib/structures/{ITtscGraphDiagnostic.js → ITtscGraphSpan.js} +1 -1
- package/lib/structures/ITtscGraphSpan.js.map +1 -0
- package/lib/structures/ITtscGraphTour.d.ts +65 -23
- package/lib/structures/ITtscGraphTrace.d.ts +72 -49
- package/lib/structures/TtscGraphEdgeKind.d.ts +7 -1
- package/lib/structures/index.d.ts +1 -1
- package/lib/structures/index.js +1 -1
- package/lib/structures/index.js.map +1 -1
- package/lib/view.js +3 -1
- package/lib/view.js.map +1 -1
- package/package.json +4 -4
- package/src/TtscGraphApplication.ts +66 -31
- package/src/index.ts +23 -5
- package/src/model/TtscGraphMemory.ts +86 -22
- package/src/model/TtscGraphSession.ts +204 -0
- package/src/model/loadGraph.ts +6 -4
- package/src/resolveGraphBinary.ts +7 -3
- package/src/server/createServer.ts +24 -17
- package/src/server/exportSurface.ts +40 -0
- package/src/server/resolveHandle.ts +141 -4
- package/src/server/resultAudit.ts +109 -0
- package/src/server/resultNext.ts +22 -0
- package/src/server/runDetails.ts +112 -28
- package/src/server/runEntrypoints.ts +26 -17
- package/src/server/runLookup.ts +29 -18
- package/src/server/runOverview.ts +9 -10
- package/src/server/runTour.ts +764 -363
- package/src/server/runTrace.ts +265 -71
- package/src/server/startServer.ts +13 -8
- package/src/structures/ITtscGraphApplication.ts +124 -114
- package/src/structures/ITtscGraphDecorator.ts +4 -7
- package/src/structures/ITtscGraphDetails.ts +60 -46
- package/src/structures/ITtscGraphDump.ts +41 -11
- package/src/structures/ITtscGraphEdge.ts +4 -6
- package/src/structures/ITtscGraphEntrypoints.ts +10 -30
- package/src/structures/ITtscGraphEscape.ts +6 -19
- package/src/structures/ITtscGraphEvidence.ts +5 -8
- package/src/structures/ITtscGraphLookup.ts +9 -22
- package/src/structures/ITtscGraphNext.ts +10 -7
- package/src/structures/ITtscGraphNode.ts +18 -8
- package/src/structures/ITtscGraphOverview.ts +10 -17
- package/src/structures/ITtscGraphSpan.ts +33 -0
- package/src/structures/ITtscGraphTour.ts +70 -28
- package/src/structures/ITtscGraphTrace.ts +82 -51
- package/src/structures/TtscGraphEdgeKind.ts +7 -0
- package/src/structures/index.ts +1 -1
- package/src/view.ts +3 -1
- package/lib/server/accessAliases.d.ts +0 -8
- package/lib/server/accessAliases.js +0 -53
- package/lib/server/accessAliases.js.map +0 -1
- package/lib/server/resultGuide.d.ts +0 -3
- package/lib/server/resultGuide.js +0 -16
- package/lib/server/resultGuide.js.map +0 -1
- package/lib/structures/ITtscGraphDiagnostic.d.ts +0 -27
- package/lib/structures/ITtscGraphDiagnostic.js.map +0 -1
- package/src/server/accessAliases.ts +0 -55
- package/src/server/resultGuide.ts +0 -20
- package/src/structures/ITtscGraphDiagnostic.ts +0 -34
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ITtscGraphDump } from "../structures/ITtscGraphDump";
|
|
2
2
|
import { ITtscGraphEdge } from "../structures/ITtscGraphEdge";
|
|
3
|
+
import { ITtscGraphEvidence } from "../structures/ITtscGraphEvidence";
|
|
3
4
|
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
5
|
+
import { ITtscGraphSpan } from "../structures/ITtscGraphSpan";
|
|
4
6
|
import { TtscGraphEdgeKind } from "../structures/TtscGraphEdgeKind";
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -122,6 +124,24 @@ function fileNodeId(file: string): string {
|
|
|
122
124
|
return file;
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
/**
|
|
128
|
+
* A wire span with its file put back: the one the builder left out because the
|
|
129
|
+
* reader has it, or the one it kept because it could not be derived (an
|
|
130
|
+
* implementation in another file).
|
|
131
|
+
*/
|
|
132
|
+
function spanIn(span: ITtscGraphSpan, file: string): ITtscGraphEvidence {
|
|
133
|
+
return { ...span, file: span.file ?? file };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The source file a node id names. An id is `path#Qualified.Name:kind`, and a
|
|
138
|
+
* file node's id is the path itself.
|
|
139
|
+
*/
|
|
140
|
+
function fileOfNodeId(id: string): string {
|
|
141
|
+
const hash = id.indexOf("#");
|
|
142
|
+
return hash === -1 ? id : id.slice(0, hash);
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
function basename(file: string): string {
|
|
126
146
|
const slash = file.lastIndexOf("/");
|
|
127
147
|
return slash >= 0 ? file.slice(slash + 1) : file;
|
|
@@ -136,9 +156,49 @@ function synthesize(dump: ITtscGraphDump): {
|
|
|
136
156
|
nodes: ITtscGraphNode[];
|
|
137
157
|
edges: ITtscGraphEdge[];
|
|
138
158
|
} {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
// A module node is the dump's name for a source file's export surface, and a
|
|
160
|
+
// file node is this layer's name for the same file. Fold the two: the module
|
|
161
|
+
// keeps its file present here even when the file declares nothing (a barrel),
|
|
162
|
+
// and its `exports` edges are re-anchored on the file id every other tool
|
|
163
|
+
// already traverses. What the module carried, the file now carries.
|
|
164
|
+
const moduleFiles = new Set(
|
|
165
|
+
dump.nodes.filter((n) => n.kind === "module").map((n) => n.file),
|
|
166
|
+
);
|
|
167
|
+
const moduleIds = new Map(
|
|
168
|
+
dump.nodes.filter((n) => n.kind === "module").map((n) => [n.id, n.file]),
|
|
169
|
+
);
|
|
170
|
+
// Clone nodes so property refinement does not mutate the caller's dump, and
|
|
171
|
+
// put back the file the builder left out of every span: a node's span is in
|
|
172
|
+
// the node's file, an edge's span is in the file its `from` id names. The
|
|
173
|
+
// builder omits both because they are exactly reconstructible and they are not
|
|
174
|
+
// small — the two copies are 17% of the document, 55 MB of VS Code's 323 MB,
|
|
175
|
+
// paid again in the encode, the pipe, the parse and the validation. Nothing
|
|
176
|
+
// downstream of this line sees a span without its file.
|
|
177
|
+
const nodes: ITtscGraphNode[] = dump.nodes
|
|
178
|
+
.filter((n) => n.kind !== "module")
|
|
179
|
+
.map((n) => {
|
|
180
|
+
const { evidence, implementation, ...rest } = n;
|
|
181
|
+
return {
|
|
182
|
+
...rest,
|
|
183
|
+
...(evidence !== undefined
|
|
184
|
+
? { evidence: spanIn(evidence, n.file) }
|
|
185
|
+
: {}),
|
|
186
|
+
...(implementation !== undefined
|
|
187
|
+
? { implementation: spanIn(implementation, n.file) }
|
|
188
|
+
: {}),
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
const edges: ITtscGraphEdge[] = dump.edges.map((edge) => {
|
|
192
|
+
const { evidence, ...rest } = edge;
|
|
193
|
+
const from = moduleIds.get(edge.from);
|
|
194
|
+
return {
|
|
195
|
+
...rest,
|
|
196
|
+
...(from !== undefined ? { from: fileNodeId(from) } : {}),
|
|
197
|
+
...(evidence !== undefined
|
|
198
|
+
? { evidence: spanIn(evidence, fileOfNodeId(edge.from)) }
|
|
199
|
+
: {}),
|
|
200
|
+
};
|
|
201
|
+
});
|
|
142
202
|
|
|
143
203
|
// Index workspace nodes by (file, within-file key) so ownership can resolve a
|
|
144
204
|
// member to its declaring class/namespace.
|
|
@@ -161,23 +221,34 @@ function synthesize(dump: ITtscGraphDump): {
|
|
|
161
221
|
}
|
|
162
222
|
}
|
|
163
223
|
|
|
164
|
-
// One file container node per distinct workspace source file
|
|
224
|
+
// One file container node per distinct workspace source file, plus every file
|
|
225
|
+
// the dump saw an export surface on — a barrel declares nothing, so its only
|
|
226
|
+
// trace in the dump is its module node, and it is exactly the file a consumer
|
|
227
|
+
// imports the package from.
|
|
165
228
|
const fileNodes = new Map<string, ITtscGraphNode>();
|
|
229
|
+
const addFileNode = (file: string): void => {
|
|
230
|
+
if (file === "" || fileNodes.has(file)) return;
|
|
231
|
+
fileNodes.set(file, {
|
|
232
|
+
id: fileNodeId(file),
|
|
233
|
+
kind: "file",
|
|
234
|
+
name: basename(file),
|
|
235
|
+
file,
|
|
236
|
+
external: false,
|
|
237
|
+
});
|
|
238
|
+
};
|
|
166
239
|
for (const node of nodes) {
|
|
167
|
-
if (node.external
|
|
168
|
-
|
|
169
|
-
fileNodes.set(node.file, {
|
|
170
|
-
id: fileNodeId(node.file),
|
|
171
|
-
kind: "file",
|
|
172
|
-
name: basename(node.file),
|
|
173
|
-
file: node.file,
|
|
174
|
-
external: false,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
240
|
+
if (node.external) continue;
|
|
241
|
+
addFileNode(node.file);
|
|
177
242
|
}
|
|
243
|
+
for (const file of moduleFiles) addFileNode(file);
|
|
178
244
|
|
|
179
245
|
// Ownership: a member is contained by its owner; a top-level declaration by
|
|
180
|
-
// its file. Exports
|
|
246
|
+
// its file. Exports are not synthesized here: the dump's `exports` edges come
|
|
247
|
+
// from the checker's export table, which follows re-exports and barrels, so
|
|
248
|
+
// they say which module puts a symbol on the wire. Deriving them from the
|
|
249
|
+
// `exported` flag instead would say only that the declaring file made it
|
|
250
|
+
// public, which is the fact that cannot tell a package's front door from its
|
|
251
|
+
// legacy subpath.
|
|
181
252
|
const structural: ITtscGraphEdge[] = [];
|
|
182
253
|
const membersByOwner = new Map<string, ITtscGraphNode[]>();
|
|
183
254
|
for (const node of nodes) {
|
|
@@ -190,13 +261,6 @@ function synthesize(dump: ITtscGraphDump): {
|
|
|
190
261
|
to: node.id,
|
|
191
262
|
kind: "contains",
|
|
192
263
|
});
|
|
193
|
-
if (node.exported) {
|
|
194
|
-
structural.push({
|
|
195
|
-
from: fileNodeId(node.file),
|
|
196
|
-
to: node.id,
|
|
197
|
-
kind: "exports" satisfies TtscGraphEdgeKind,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
264
|
}
|
|
201
265
|
|
|
202
266
|
const synthesized = [...edges, ...structural];
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import typia from "typia";
|
|
4
|
+
|
|
5
|
+
import { ensureExecutable } from "../nativeExecutable";
|
|
6
|
+
import { resolveGraphBinary } from "../resolveGraphBinary";
|
|
7
|
+
import { ITtscGraphDump } from "../structures/ITtscGraphDump";
|
|
8
|
+
import { TtscGraphMemory } from "./TtscGraphMemory";
|
|
9
|
+
|
|
10
|
+
interface SessionResponse {
|
|
11
|
+
id: number;
|
|
12
|
+
changed: boolean;
|
|
13
|
+
mode?: "initial" | "unchanged" | "incremental" | "rebuild" | "reload";
|
|
14
|
+
dump?: unknown;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Pending {
|
|
19
|
+
resolve: (response: SessionResponse) => void;
|
|
20
|
+
reject: (error: Error) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resident bridge to `ttscgraph serve`.
|
|
25
|
+
*
|
|
26
|
+
* Every graph request first asks the native session for the current disk
|
|
27
|
+
* snapshot. Unchanged requests reuse the existing {@link TtscGraphMemory}; an
|
|
28
|
+
* edited source reuses tsgo's resident Program through `driver.Session`, while
|
|
29
|
+
* config and root-file-set changes force a safe full reload.
|
|
30
|
+
*/
|
|
31
|
+
export class TtscGraphSession {
|
|
32
|
+
private readonly cwd: string;
|
|
33
|
+
private readonly tsconfig: string;
|
|
34
|
+
private readonly binary: string;
|
|
35
|
+
private child: ChildProcessWithoutNullStreams | undefined;
|
|
36
|
+
private stderr = "";
|
|
37
|
+
private nextId = 0;
|
|
38
|
+
private readonly pending = new Map<number, Pending>();
|
|
39
|
+
private queue: Promise<void> = Promise.resolve();
|
|
40
|
+
private current: TtscGraphMemory | undefined;
|
|
41
|
+
private closed = false;
|
|
42
|
+
|
|
43
|
+
public constructor(options: {
|
|
44
|
+
cwd: string;
|
|
45
|
+
tsconfig: string;
|
|
46
|
+
binary?: string;
|
|
47
|
+
}) {
|
|
48
|
+
// Resolve the platform binary from the project this session serves, so the
|
|
49
|
+
// MCP server started from an unrelated directory still finds the target's
|
|
50
|
+
// installed `ttsc`.
|
|
51
|
+
const binary =
|
|
52
|
+
options.binary ?? resolveGraphBinary(process.env, options.cwd);
|
|
53
|
+
if (binary === null) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"@ttsc/graph: could not resolve the ttscgraph binary. " +
|
|
56
|
+
"Install `ttsc` so its platform package is present, " +
|
|
57
|
+
"or set TTSC_GRAPH_BINARY to an absolute path.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
ensureExecutable(binary);
|
|
61
|
+
this.cwd = options.cwd;
|
|
62
|
+
this.tsconfig = options.tsconfig;
|
|
63
|
+
this.binary = binary;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Return a graph for the current disk snapshot, serialized per tool call. */
|
|
67
|
+
public graph(): Promise<TtscGraphMemory> {
|
|
68
|
+
let resolve!: (graph: TtscGraphMemory) => void;
|
|
69
|
+
let reject!: (error: Error) => void;
|
|
70
|
+
const result = new Promise<TtscGraphMemory>((res, rej) => {
|
|
71
|
+
resolve = res;
|
|
72
|
+
reject = rej;
|
|
73
|
+
});
|
|
74
|
+
this.queue = this.queue
|
|
75
|
+
.catch(() => undefined)
|
|
76
|
+
.then(async () => {
|
|
77
|
+
try {
|
|
78
|
+
resolve(await this.refresh());
|
|
79
|
+
} catch (error) {
|
|
80
|
+
reject(asError(error));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Close the native session. Safe to call more than once. */
|
|
87
|
+
public close(): void {
|
|
88
|
+
this.closed = true;
|
|
89
|
+
const child = this.child;
|
|
90
|
+
this.child = undefined;
|
|
91
|
+
if (child !== undefined && !child.killed) child.stdin.end();
|
|
92
|
+
this.failPending(new Error("@ttsc/graph: native session closed"));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async refresh(): Promise<TtscGraphMemory> {
|
|
96
|
+
const response = await this.request();
|
|
97
|
+
if (response.error !== undefined) {
|
|
98
|
+
throw new Error(`@ttsc/graph: ${response.error}`);
|
|
99
|
+
}
|
|
100
|
+
if (response.changed) {
|
|
101
|
+
if (response.dump === undefined) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`@ttsc/graph: native ${response.mode ?? "changed"} response omitted its dump`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const dump = typia.assert<ITtscGraphDump>(response.dump);
|
|
107
|
+
this.current = TtscGraphMemory.from(dump);
|
|
108
|
+
}
|
|
109
|
+
if (this.current === undefined) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"@ttsc/graph: native session returned no initial graph snapshot",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return this.current;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private request(): Promise<SessionResponse> {
|
|
118
|
+
const child = this.ensureChild();
|
|
119
|
+
const id = ++this.nextId;
|
|
120
|
+
return new Promise<SessionResponse>((resolve, reject) => {
|
|
121
|
+
this.pending.set(id, { resolve, reject });
|
|
122
|
+
child.stdin.write(`${JSON.stringify({ id })}\n`, (error) => {
|
|
123
|
+
if (error === null || error === undefined) return;
|
|
124
|
+
this.pending.delete(id);
|
|
125
|
+
reject(
|
|
126
|
+
new Error(
|
|
127
|
+
`@ttsc/graph: could not request native snapshot: ${error.message}`,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private ensureChild(): ChildProcessWithoutNullStreams {
|
|
135
|
+
if (this.closed) {
|
|
136
|
+
// A request queued behind the close must not respawn the native
|
|
137
|
+
// process; an orphaned resident compiler would outlive the MCP server.
|
|
138
|
+
throw new Error("@ttsc/graph: native session is closed");
|
|
139
|
+
}
|
|
140
|
+
if (this.child !== undefined && this.child.exitCode === null) {
|
|
141
|
+
return this.child;
|
|
142
|
+
}
|
|
143
|
+
this.stderr = "";
|
|
144
|
+
const child = spawn(
|
|
145
|
+
this.binary,
|
|
146
|
+
["serve", "--cwd", this.cwd, "--tsconfig", this.tsconfig],
|
|
147
|
+
{ stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
148
|
+
);
|
|
149
|
+
this.child = child;
|
|
150
|
+
child.stderr.setEncoding("utf8");
|
|
151
|
+
child.stderr.on("data", (chunk: string) => {
|
|
152
|
+
this.stderr = (this.stderr + chunk).slice(-64 * 1024);
|
|
153
|
+
});
|
|
154
|
+
const lines = readline.createInterface({ input: child.stdout });
|
|
155
|
+
lines.on("line", (line) => this.onLine(line));
|
|
156
|
+
child.on("error", (error) => this.failChild(child, error));
|
|
157
|
+
child.on("exit", (code, signal) => {
|
|
158
|
+
if (this.child !== child) return;
|
|
159
|
+
this.child = undefined;
|
|
160
|
+
this.failPending(
|
|
161
|
+
new Error(
|
|
162
|
+
`@ttsc/graph: native session exited (code=${String(code)}, signal=${String(signal)})${
|
|
163
|
+
this.stderr.trim() === "" ? "" : `: ${this.stderr.trim()}`
|
|
164
|
+
}`,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
return child;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private onLine(line: string): void {
|
|
172
|
+
let response: SessionResponse;
|
|
173
|
+
try {
|
|
174
|
+
response = JSON.parse(line) as SessionResponse;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.failPending(
|
|
177
|
+
new Error(
|
|
178
|
+
`@ttsc/graph: native session returned invalid JSON: ${asError(error).message}`,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const pending = this.pending.get(response.id);
|
|
184
|
+
if (pending === undefined) return;
|
|
185
|
+
this.pending.delete(response.id);
|
|
186
|
+
pending.resolve(response);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private failChild(child: ChildProcessWithoutNullStreams, error: Error): void {
|
|
190
|
+
if (this.child === child) this.child = undefined;
|
|
191
|
+
this.failPending(
|
|
192
|
+
new Error(`@ttsc/graph: native session failed: ${error.message}`),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private failPending(error: Error): void {
|
|
197
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
198
|
+
this.pending.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function asError(error: unknown): Error {
|
|
203
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
204
|
+
}
|
package/src/model/loadGraph.ts
CHANGED
|
@@ -13,9 +13,9 @@ const MAX_DUMP_BYTES = 1024 * 1024 * 1024;
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Build the resident {@link TtscGraphMemory} for a project by running `ttscgraph
|
|
16
|
-
* dump` once and loading its JSON. This is the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* dump` once and loading its JSON. This is the one-shot path for direct callers
|
|
17
|
+
* and the viewer. The MCP server uses `TtscGraphSession` instead so source
|
|
18
|
+
* edits refresh a resident compiler session.
|
|
19
19
|
*
|
|
20
20
|
* Throws when the binary cannot be resolved, the dump command fails, or its
|
|
21
21
|
* output is not a readable graph — the server surfaces the failure rather than
|
|
@@ -37,7 +37,9 @@ export function loadGraph(
|
|
|
37
37
|
): TtscGraphMemory {
|
|
38
38
|
const cwd = options.cwd ?? process.cwd();
|
|
39
39
|
const tsconfig = options.tsconfig ?? "tsconfig.json";
|
|
40
|
-
|
|
40
|
+
// Resolve the platform binary from the selected project cwd, not the caller's
|
|
41
|
+
// process directory, so a one-shot load names its own installation.
|
|
42
|
+
const binary = options.binary ?? resolveGraphBinary(process.env, cwd);
|
|
41
43
|
if (binary === null) {
|
|
42
44
|
throw new Error(
|
|
43
45
|
"@ttsc/graph: could not resolve the ttscgraph binary. " +
|
|
@@ -15,8 +15,10 @@ import path from "node:path";
|
|
|
15
15
|
* 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
|
|
16
16
|
* That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
|
|
17
17
|
* an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
|
|
18
|
-
* location — found from `
|
|
19
|
-
*
|
|
18
|
+
* location — found from `cwd`, the project the caller selected (via `--cwd`
|
|
19
|
+
* or an API `cwd` option), defaulting to `process.cwd()` when no project was
|
|
20
|
+
* named. A launcher started from an unrelated directory still resolves the
|
|
21
|
+
* `ttsc` installed under the target project it was asked to graph.
|
|
20
22
|
*/
|
|
21
23
|
export function resolveGraphBinary(
|
|
22
24
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -27,8 +29,10 @@ export function resolveGraphBinary(
|
|
|
27
29
|
}
|
|
28
30
|
const exe = process.platform === "win32" ? "ttscgraph.exe" : "ttscgraph";
|
|
29
31
|
try {
|
|
32
|
+
// Anchor package lookup at the absolute project root so a relative `--cwd`
|
|
33
|
+
// resolves the same way the native process interprets it.
|
|
30
34
|
const ttscPackageJson = require.resolve("ttsc/package.json", {
|
|
31
|
-
paths: [cwd],
|
|
35
|
+
paths: [path.resolve(cwd)],
|
|
32
36
|
});
|
|
33
37
|
const fromTtsc = createRequire(ttscPackageJson);
|
|
34
38
|
return fromTtsc.resolve(
|
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { createMcpServer } from "@typia/mcp";
|
|
3
|
-
import typia
|
|
3
|
+
import typia from "typia";
|
|
4
4
|
|
|
5
5
|
import { TtscGraphApplication, TtscGraphSource } from "../TtscGraphApplication";
|
|
6
6
|
import { ITtscGraphApplication } from "../structures/ITtscGraphApplication";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Build the MCP server for a graph.
|
|
10
|
-
* {@link ITtscGraphApplication} into the tool schema and validator (no
|
|
11
|
-
* hand-written schema), and `createMcpServer` from `@typia/mcp` handles the
|
|
12
|
-
* list/call registration, argument validation, and structured output.
|
|
9
|
+
* Build the MCP server for a graph.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* `typia.llm.controller` reflects {@link ITtscGraphApplication} into the tool's
|
|
12
|
+
* input and output schemas and its argument validator, with no hand-written
|
|
13
|
+
* schema: the interface's JSDoc becomes the handshake instructions, the
|
|
14
|
+
* method's becomes the tool description, and every property's becomes the
|
|
15
|
+
* description of that field — including `audit`, whose JSDoc is how a caller
|
|
16
|
+
* learns what the server checked before it answered.
|
|
17
|
+
*
|
|
18
|
+
* The registration was hand-written here for a while, because a tool that
|
|
19
|
+
* declares an output schema must answer with `structuredContent` and the helper
|
|
20
|
+
* also serialized the same JSON into a text block: the payload crossed the wire
|
|
21
|
+
* twice, a client counted both copies against its tool-result cap, and a 30 KB
|
|
22
|
+
* tour arrived as 60 KB, blew the cap, and was spilled to a file the model then
|
|
23
|
+
* shelled out to read back. `@typia/mcp` 13.1.0 ships the structured result
|
|
24
|
+
* once (samchon/typia#2020), so the hand-written server had nothing left to fix
|
|
25
|
+
* and the library owns the registration again.
|
|
19
26
|
*/
|
|
20
27
|
export function createServer(
|
|
21
28
|
graph: TtscGraphSource,
|
|
22
29
|
version: string,
|
|
23
30
|
): McpServer {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
void version;
|
|
32
|
+
return createMcpServer(
|
|
33
|
+
typia.llm.controller<ITtscGraphApplication>(
|
|
34
|
+
"ttsc-graph",
|
|
35
|
+
new TtscGraphApplication(graph),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
31
38
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How public a symbol is, counted from the graph and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* A module's `exports` edges are the checker's export table, resolved through
|
|
7
|
+
* every re-export and barrel it passes. So a symbol carries one edge per module
|
|
8
|
+
* that puts it on the wire, and that count is the project's own answer to how
|
|
9
|
+
* far forward the symbol stands: an internal helper is exported by the file
|
|
10
|
+
* that declares it or by nothing at all, while the name a consumer imports from
|
|
11
|
+
* the package has been re-exported up a chain of barrels and carries an edge
|
|
12
|
+
* from each one.
|
|
13
|
+
*
|
|
14
|
+
* On zod the count is the whole difference between the current API and the
|
|
15
|
+
* previous major it still ships: `parse` and `safeParse` in v4's classic
|
|
16
|
+
* surface carry five, v3's `ZodString` carries three, and v3's
|
|
17
|
+
* `ZodType.safeParse` — a class method, which no export table ever names —
|
|
18
|
+
* carries none. A ranker that knew only the `exported` flag saw all of these as
|
|
19
|
+
* equally public, picked the one whose name matched the question best, and
|
|
20
|
+
* opened zod's tour on the legacy implementation.
|
|
21
|
+
*
|
|
22
|
+
* The count is a fact the compiler resolved. It reads no package.json, guesses
|
|
23
|
+
* from no filename, and holds for a project that has neither.
|
|
24
|
+
*/
|
|
25
|
+
export function exportFanIn(graph: TtscGraphMemory, id: string): number {
|
|
26
|
+
let count = 0;
|
|
27
|
+
for (const edge of graph.incoming(id)) if (edge.kind === "exports") count++;
|
|
28
|
+
return count;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the dump carries an export surface at all. */
|
|
32
|
+
export function hasExportSurface(graph: TtscGraphMemory): boolean {
|
|
33
|
+
const known = cache.get(graph);
|
|
34
|
+
if (known !== undefined) return known;
|
|
35
|
+
const found = graph.edges.some((edge) => edge.kind === "exports");
|
|
36
|
+
cache.set(graph, found);
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cache = new WeakMap<TtscGraphMemory, boolean>();
|
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
2
|
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
3
|
+
import { exportFanIn } from "./exportSurface";
|
|
4
|
+
import { isSupportPath } from "./pathPolicy";
|
|
3
5
|
|
|
4
6
|
export interface IResolvedGraphHandle {
|
|
5
7
|
node?: ITtscGraphNode;
|
|
6
8
|
candidates?: ITtscGraphNode[];
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a tool handle as an id, an exact symbol name, a dotted suffix, or a
|
|
13
|
+
* file-qualified name.
|
|
14
|
+
*
|
|
15
|
+
* A model writes handles from memory of an earlier result, and it writes them
|
|
16
|
+
* the way the result read: a symbol with the file it came from. Three forms all
|
|
17
|
+
* mean one node and all used to miss.
|
|
18
|
+
*
|
|
19
|
+
* - A `file#symbol` id whose file is one refactor stale (`effect.ts#track` for
|
|
20
|
+
* what now lives in `dep.ts`). The graph knows the symbol, so it answers
|
|
21
|
+
* rather than sending the caller back through a lookup.
|
|
22
|
+
* - `renderer.render` — the file's stem and the symbol it declares. It is not a
|
|
23
|
+
* qualified name, so a suffix match on `.render` finds nothing and the caller
|
|
24
|
+
* gets an empty result for a symbol the graph holds. Vue's tour spent a trace
|
|
25
|
+
* call and four file reads on exactly this.
|
|
26
|
+
* - A name the project declares more than once, which is not a name the project
|
|
27
|
+
* does not declare. The candidates come back ranked by what the package
|
|
28
|
+
* publishes, so the one a caller means is the one it reads first.
|
|
29
|
+
* - `schema.parse` — a call written the way it is written in a program, on a
|
|
30
|
+
* value rather than on the type that declares it. There is no `schema` in the
|
|
31
|
+
* graph, so every exact form misses, and the handle resolves to nothing for a
|
|
32
|
+
* member the graph holds under `ZodType.parse`. It is how people name a
|
|
33
|
+
* method (`db.query`, `app.listen`, `repo.save`), so the member is what it
|
|
34
|
+
* means, and the candidates come back ranked when several classes declare
|
|
35
|
+
* it.
|
|
36
|
+
*/
|
|
10
37
|
export function resolveGraphHandle(
|
|
11
38
|
graph: TtscGraphMemory,
|
|
12
39
|
handle: string,
|
|
@@ -15,12 +42,60 @@ export function resolveGraphHandle(
|
|
|
15
42
|
const byId = graph.node(handle);
|
|
16
43
|
if (byId !== undefined) return { node: byId };
|
|
17
44
|
|
|
18
|
-
const
|
|
45
|
+
const byName = resolveGraphName(graph, handle, candidateLimit);
|
|
46
|
+
if (byName.node !== undefined || byName.candidates !== undefined)
|
|
47
|
+
return rank(graph, byName, candidateLimit);
|
|
48
|
+
|
|
49
|
+
const byFile = resolveFileQualified(graph, handle, candidateLimit);
|
|
50
|
+
if (byFile.node !== undefined || byFile.candidates !== undefined)
|
|
51
|
+
return rank(graph, byFile, candidateLimit);
|
|
52
|
+
|
|
53
|
+
const symbol = symbolPartOf(handle) ?? memberPartOf(handle);
|
|
54
|
+
if (symbol !== undefined)
|
|
55
|
+
return rank(
|
|
56
|
+
graph,
|
|
57
|
+
resolveGraphName(graph, symbol, candidateLimit),
|
|
58
|
+
candidateLimit,
|
|
59
|
+
);
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The member a dotted handle names when its receiver is a value: the last
|
|
65
|
+
* segment of `schema.parse`, of `this.store.commit`, of `db.query`.
|
|
66
|
+
*
|
|
67
|
+
* It is the last thing tried, after the whole handle has failed as an id, as a
|
|
68
|
+
* qualified name, as a `.suffix`, and as a file-qualified name — so a receiver
|
|
69
|
+
* that _is_ a type or a file never reaches here.
|
|
70
|
+
*/
|
|
71
|
+
function memberPartOf(handle: string): string | undefined {
|
|
72
|
+
const dot = handle.lastIndexOf(".");
|
|
73
|
+
if (dot <= 0) return undefined;
|
|
74
|
+
const member = handle.slice(dot + 1);
|
|
75
|
+
return member.length > 0 ? member : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The symbol an id-shaped handle names: `dir/file.ts#Class.method:kind`. */
|
|
79
|
+
function symbolPartOf(handle: string): string | undefined {
|
|
80
|
+
const hash = handle.lastIndexOf("#");
|
|
81
|
+
if (hash < 0) return undefined;
|
|
82
|
+
const symbol = handle.slice(hash + 1);
|
|
83
|
+
const kind = symbol.lastIndexOf(":");
|
|
84
|
+
const name = kind < 0 ? symbol : symbol.slice(0, kind);
|
|
85
|
+
return name.length > 0 ? name : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveGraphName(
|
|
89
|
+
graph: TtscGraphMemory,
|
|
90
|
+
name: string,
|
|
91
|
+
candidateLimit: number,
|
|
92
|
+
): IResolvedGraphHandle {
|
|
93
|
+
const exact = graph.symbols(name);
|
|
19
94
|
if (exact.length === 1) return { node: exact[0] };
|
|
20
95
|
if (exact.length > 1) return { candidates: exact.slice(0, candidateLimit) };
|
|
21
96
|
|
|
22
|
-
if (
|
|
23
|
-
const suffix = `.${
|
|
97
|
+
if (name.includes(".")) {
|
|
98
|
+
const suffix = `.${name}`;
|
|
24
99
|
const suffixMatches = graph.nodes.filter(
|
|
25
100
|
(node) =>
|
|
26
101
|
node.kind !== "file" && node.qualifiedName?.endsWith(suffix) === true,
|
|
@@ -32,3 +107,65 @@ export function resolveGraphHandle(
|
|
|
32
107
|
}
|
|
33
108
|
return {};
|
|
34
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A `file.symbol` handle: the stem of the file a result cited, then the symbol
|
|
113
|
+
* it declared there (`renderer.render`, `parse.safeParse`). It is how a model
|
|
114
|
+
* disambiguates a common name from what the graph just showed it, and it names
|
|
115
|
+
* exactly one node whenever that file declares the symbol.
|
|
116
|
+
*/
|
|
117
|
+
function resolveFileQualified(
|
|
118
|
+
graph: TtscGraphMemory,
|
|
119
|
+
handle: string,
|
|
120
|
+
candidateLimit: number,
|
|
121
|
+
): IResolvedGraphHandle {
|
|
122
|
+
const dot = handle.indexOf(".");
|
|
123
|
+
if (dot <= 0) return {};
|
|
124
|
+
const stem = handle.slice(0, dot).toLowerCase();
|
|
125
|
+
const name = handle.slice(dot + 1);
|
|
126
|
+
if (name === "") return {};
|
|
127
|
+
const matches = graph
|
|
128
|
+
.symbols(name)
|
|
129
|
+
.filter((node) => fileStem(node.file) === stem);
|
|
130
|
+
if (matches.length === 1) return { node: matches[0] };
|
|
131
|
+
if (matches.length > 1)
|
|
132
|
+
return { candidates: matches.slice(0, candidateLimit) };
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `packages/core/src/renderer.ts` -> `renderer`. */
|
|
137
|
+
function fileStem(file: string): string {
|
|
138
|
+
const base = file.slice(file.lastIndexOf("/") + 1);
|
|
139
|
+
return base.replace(/\.[cm]?[tj]sx?$/, "").toLowerCase();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Order candidates by how likely a caller means them: what the package
|
|
144
|
+
* publishes first, then how much of the codebase leans on the node, with test
|
|
145
|
+
* and fixture declarations last. An unranked list hands back whichever
|
|
146
|
+
* declaration the graph happened to visit first — Vue's `render` came back as a
|
|
147
|
+
* template pre-processor's method — and a caller that trusts the order traces
|
|
148
|
+
* the wrong one.
|
|
149
|
+
*/
|
|
150
|
+
function rank(
|
|
151
|
+
graph: TtscGraphMemory,
|
|
152
|
+
resolved: IResolvedGraphHandle,
|
|
153
|
+
candidateLimit: number,
|
|
154
|
+
): IResolvedGraphHandle {
|
|
155
|
+
if (resolved.candidates === undefined) return resolved;
|
|
156
|
+
const ranked = [...resolved.candidates]
|
|
157
|
+
.sort((a, b) => candidateScore(graph, b) - candidateScore(graph, a))
|
|
158
|
+
.slice(0, candidateLimit);
|
|
159
|
+
return { candidates: ranked };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function candidateScore(graph: TtscGraphMemory, node: ITtscGraphNode): number {
|
|
163
|
+
let score = Math.min(48, Math.log2(1 + exportFanIn(graph, node.id)) * 20);
|
|
164
|
+
if (node.exported) score += 12;
|
|
165
|
+
if (node.external) score -= 60;
|
|
166
|
+
if (isSupportPath(node.file)) score -= 30;
|
|
167
|
+
const degree =
|
|
168
|
+
graph.outgoing(node.id).length + graph.incoming(node.id).length;
|
|
169
|
+
score += Math.min(24, Math.log2(1 + degree) * 6);
|
|
170
|
+
return score;
|
|
171
|
+
}
|