@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.
Files changed (125) hide show
  1. package/README.md +231 -36
  2. package/lib/TtscGraphApplication.d.ts +4 -3
  3. package/lib/TtscGraphApplication.js +54 -19
  4. package/lib/TtscGraphApplication.js.map +1 -1
  5. package/lib/index.d.ts +9 -4
  6. package/lib/index.js +24 -5
  7. package/lib/index.js.map +1 -1
  8. package/lib/model/TtscGraphMemory.js +80 -22
  9. package/lib/model/TtscGraphMemory.js.map +1 -1
  10. package/lib/model/TtscGraphSession.d.ts +36 -0
  11. package/lib/model/TtscGraphSession.js +457 -0
  12. package/lib/model/TtscGraphSession.js.map +1 -0
  13. package/lib/model/loadGraph.d.ts +3 -3
  14. package/lib/model/loadGraph.js +94 -141
  15. package/lib/model/loadGraph.js.map +1 -1
  16. package/lib/resolveGraphBinary.d.ts +4 -2
  17. package/lib/resolveGraphBinary.js +7 -3
  18. package/lib/resolveGraphBinary.js.map +1 -1
  19. package/lib/server/createServer.d.ts +16 -9
  20. package/lib/server/createServer.js +286 -190
  21. package/lib/server/createServer.js.map +1 -1
  22. package/lib/server/exportSurface.d.ts +26 -0
  23. package/lib/server/exportSurface.js +44 -0
  24. package/lib/server/exportSurface.js.map +1 -0
  25. package/lib/server/resolveHandle.d.ts +26 -1
  26. package/lib/server/resolveHandle.js +124 -4
  27. package/lib/server/resolveHandle.js.map +1 -1
  28. package/lib/server/resultAudit.d.ts +74 -0
  29. package/lib/server/resultAudit.js +110 -0
  30. package/lib/server/resultAudit.js.map +1 -0
  31. package/lib/server/resultNext.d.ts +9 -0
  32. package/lib/server/resultNext.js +11 -0
  33. package/lib/server/resultNext.js.map +1 -0
  34. package/lib/server/runDetails.d.ts +15 -3
  35. package/lib/server/runDetails.js +105 -23
  36. package/lib/server/runDetails.js.map +1 -1
  37. package/lib/server/runEntrypoints.d.ts +2 -1
  38. package/lib/server/runEntrypoints.js +17 -10
  39. package/lib/server/runEntrypoints.js.map +1 -1
  40. package/lib/server/runLookup.d.ts +2 -1
  41. package/lib/server/runLookup.js +21 -9
  42. package/lib/server/runLookup.js.map +1 -1
  43. package/lib/server/runOverview.d.ts +2 -1
  44. package/lib/server/runOverview.js +5 -4
  45. package/lib/server/runOverview.js.map +1 -1
  46. package/lib/server/runTour.d.ts +2 -1
  47. package/lib/server/runTour.js +707 -331
  48. package/lib/server/runTour.js.map +1 -1
  49. package/lib/server/runTrace.d.ts +2 -1
  50. package/lib/server/runTrace.js +234 -54
  51. package/lib/server/runTrace.js.map +1 -1
  52. package/lib/server/startServer.d.ts +3 -2
  53. package/lib/server/startServer.js +13 -7
  54. package/lib/server/startServer.js.map +1 -1
  55. package/lib/structures/ITtscGraphApplication.d.ts +122 -114
  56. package/lib/structures/ITtscGraphDecorator.d.ts +4 -7
  57. package/lib/structures/ITtscGraphDetails.d.ts +51 -43
  58. package/lib/structures/ITtscGraphDump.d.ts +36 -11
  59. package/lib/structures/ITtscGraphEdge.d.ts +4 -6
  60. package/lib/structures/ITtscGraphEntrypoints.d.ts +10 -27
  61. package/lib/structures/ITtscGraphEscape.d.ts +6 -16
  62. package/lib/structures/ITtscGraphEvidence.d.ts +5 -8
  63. package/lib/structures/ITtscGraphLookup.d.ts +9 -20
  64. package/lib/structures/ITtscGraphNext.d.ts +10 -7
  65. package/lib/structures/ITtscGraphNode.d.ts +17 -8
  66. package/lib/structures/ITtscGraphOverview.d.ts +10 -14
  67. package/lib/structures/ITtscGraphSpan.d.ts +29 -0
  68. package/lib/structures/{ITtscGraphDiagnostic.js → ITtscGraphSpan.js} +1 -1
  69. package/lib/structures/ITtscGraphSpan.js.map +1 -0
  70. package/lib/structures/ITtscGraphTour.d.ts +65 -23
  71. package/lib/structures/ITtscGraphTrace.d.ts +72 -49
  72. package/lib/structures/TtscGraphEdgeKind.d.ts +7 -1
  73. package/lib/structures/index.d.ts +1 -1
  74. package/lib/structures/index.js +1 -1
  75. package/lib/structures/index.js.map +1 -1
  76. package/lib/view.js +3 -1
  77. package/lib/view.js.map +1 -1
  78. package/package.json +4 -4
  79. package/src/TtscGraphApplication.ts +66 -31
  80. package/src/index.ts +23 -5
  81. package/src/model/TtscGraphMemory.ts +86 -22
  82. package/src/model/TtscGraphSession.ts +204 -0
  83. package/src/model/loadGraph.ts +6 -4
  84. package/src/resolveGraphBinary.ts +7 -3
  85. package/src/server/createServer.ts +24 -17
  86. package/src/server/exportSurface.ts +40 -0
  87. package/src/server/resolveHandle.ts +141 -4
  88. package/src/server/resultAudit.ts +109 -0
  89. package/src/server/resultNext.ts +22 -0
  90. package/src/server/runDetails.ts +112 -28
  91. package/src/server/runEntrypoints.ts +26 -17
  92. package/src/server/runLookup.ts +29 -18
  93. package/src/server/runOverview.ts +9 -10
  94. package/src/server/runTour.ts +764 -363
  95. package/src/server/runTrace.ts +265 -71
  96. package/src/server/startServer.ts +13 -8
  97. package/src/structures/ITtscGraphApplication.ts +124 -114
  98. package/src/structures/ITtscGraphDecorator.ts +4 -7
  99. package/src/structures/ITtscGraphDetails.ts +60 -46
  100. package/src/structures/ITtscGraphDump.ts +41 -11
  101. package/src/structures/ITtscGraphEdge.ts +4 -6
  102. package/src/structures/ITtscGraphEntrypoints.ts +10 -30
  103. package/src/structures/ITtscGraphEscape.ts +6 -19
  104. package/src/structures/ITtscGraphEvidence.ts +5 -8
  105. package/src/structures/ITtscGraphLookup.ts +9 -22
  106. package/src/structures/ITtscGraphNext.ts +10 -7
  107. package/src/structures/ITtscGraphNode.ts +18 -8
  108. package/src/structures/ITtscGraphOverview.ts +10 -17
  109. package/src/structures/ITtscGraphSpan.ts +33 -0
  110. package/src/structures/ITtscGraphTour.ts +70 -28
  111. package/src/structures/ITtscGraphTrace.ts +82 -51
  112. package/src/structures/TtscGraphEdgeKind.ts +7 -0
  113. package/src/structures/index.ts +1 -1
  114. package/src/view.ts +3 -1
  115. package/lib/server/accessAliases.d.ts +0 -8
  116. package/lib/server/accessAliases.js +0 -53
  117. package/lib/server/accessAliases.js.map +0 -1
  118. package/lib/server/resultGuide.d.ts +0 -3
  119. package/lib/server/resultGuide.js +0 -16
  120. package/lib/server/resultGuide.js.map +0 -1
  121. package/lib/structures/ITtscGraphDiagnostic.d.ts +0 -27
  122. package/lib/structures/ITtscGraphDiagnostic.js.map +0 -1
  123. package/src/server/accessAliases.ts +0 -55
  124. package/src/server/resultGuide.ts +0 -20
  125. 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
- // Clone nodes so property refinement does not mutate the caller's dump.
140
- const nodes: ITtscGraphNode[] = dump.nodes.map((n) => ({ ...n }));
141
- const edges: ITtscGraphEdge[] = dump.edges.slice();
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 || node.file === "") continue;
168
- if (!fileNodes.has(node.file)) {
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: a file exports each of its public nodes.
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
+ }
@@ -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 cold path behind the MCP tool
17
- * calls: one type-check in Go produces the checker-resolved fact graph, then
18
- * every later tool call is answered from the in-memory model.
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
- const binary = options.binary ?? resolveGraphBinary();
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 `process.cwd()` (the project where the agent ran the
19
- * server).
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, { type ILlmController } from "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. `typia.llm.application` reflects
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
- * We assemble the `ILlmController` (`{ protocol, name, application, execute }`)
15
- * directly rather than via `typia.llm.controller` so the server is named
16
- * "ttsc-graph" on our terms, not coupled to a reflected class name. Handshake
17
- * instructions come from the class JSDoc; the single tool is named from its
18
- * method, `inspect_typescript_graph`.
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
- const controller: ILlmController<ITtscGraphApplication> = {
25
- protocol: "class",
26
- name: "ttsc-graph",
27
- application: typia.llm.application<ITtscGraphApplication>(),
28
- execute: new TtscGraphApplication(graph),
29
- };
30
- return createMcpServer(controller, version);
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
- /** Resolve a tool handle as an id, exact symbol name, or dotted suffix. */
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 exact = graph.symbols(handle);
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 (handle.includes(".")) {
23
- const suffix = `.${handle}`;
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
+ }