@ttsc/graph 0.16.4 → 0.16.5

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 (109) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +24 -53
  3. package/lib/TtscGraphApplication.js +46 -0
  4. package/lib/TtscGraphApplication.js.map +1 -0
  5. package/lib/index.js +31 -108
  6. package/lib/index.js.map +1 -1
  7. package/lib/model/TtscGraphMemory.js +185 -0
  8. package/lib/model/TtscGraphMemory.js.map +1 -0
  9. package/lib/model/loadGraph.js +418 -0
  10. package/lib/model/loadGraph.js.map +1 -0
  11. package/lib/reduce.js +101 -0
  12. package/lib/reduce.js.map +1 -0
  13. package/lib/resolveGraphBinary.js +42 -0
  14. package/lib/resolveGraphBinary.js.map +1 -0
  15. package/lib/server/createServer.js +1227 -0
  16. package/lib/server/createServer.js.map +1 -0
  17. package/lib/server/instructions.js +32 -0
  18. package/lib/server/instructions.js.map +1 -0
  19. package/lib/server/resolveHandle.js +25 -0
  20. package/lib/server/resolveHandle.js.map +1 -0
  21. package/lib/server/runExpand.js +177 -0
  22. package/lib/server/runExpand.js.map +1 -0
  23. package/lib/server/runIndex.js +146 -0
  24. package/lib/server/runIndex.js.map +1 -0
  25. package/lib/server/runOverview.js +150 -0
  26. package/lib/server/runOverview.js.map +1 -0
  27. package/lib/server/runQuery.js +147 -0
  28. package/lib/server/runQuery.js.map +1 -0
  29. package/lib/server/runTrace.js +219 -0
  30. package/lib/server/runTrace.js.map +1 -0
  31. package/lib/server/startServer.js +21 -0
  32. package/lib/server/startServer.js.map +1 -0
  33. package/lib/structures/ITtscGraphApplication.js +3 -0
  34. package/lib/structures/ITtscGraphApplication.js.map +1 -0
  35. package/lib/structures/ITtscGraphDecorator.js +3 -0
  36. package/lib/structures/ITtscGraphDecorator.js.map +1 -0
  37. package/lib/structures/ITtscGraphDiagnostic.js +3 -0
  38. package/lib/structures/ITtscGraphDiagnostic.js.map +1 -0
  39. package/lib/structures/ITtscGraphDump.js +3 -0
  40. package/lib/structures/ITtscGraphDump.js.map +1 -0
  41. package/lib/structures/ITtscGraphEdge.js +3 -0
  42. package/lib/structures/ITtscGraphEdge.js.map +1 -0
  43. package/lib/structures/ITtscGraphEvidence.js +3 -0
  44. package/lib/structures/ITtscGraphEvidence.js.map +1 -0
  45. package/lib/structures/ITtscGraphExpand.js +3 -0
  46. package/lib/structures/ITtscGraphExpand.js.map +1 -0
  47. package/lib/structures/ITtscGraphIndex.js +3 -0
  48. package/lib/structures/ITtscGraphIndex.js.map +1 -0
  49. package/lib/structures/ITtscGraphNode.js +3 -0
  50. package/lib/structures/ITtscGraphNode.js.map +1 -0
  51. package/lib/structures/ITtscGraphOverview.js +3 -0
  52. package/lib/structures/ITtscGraphOverview.js.map +1 -0
  53. package/lib/structures/ITtscGraphQuery.js +3 -0
  54. package/lib/structures/ITtscGraphQuery.js.map +1 -0
  55. package/lib/structures/ITtscGraphTrace.js +3 -0
  56. package/lib/structures/ITtscGraphTrace.js.map +1 -0
  57. package/lib/structures/TtscGraphEdgeKind.js +3 -0
  58. package/lib/structures/TtscGraphEdgeKind.js.map +1 -0
  59. package/lib/structures/TtscGraphNodeKind.js +3 -0
  60. package/lib/structures/TtscGraphNodeKind.js.map +1 -0
  61. package/lib/structures/TtscGraphNodeModifier.js +3 -0
  62. package/lib/structures/TtscGraphNodeModifier.js.map +1 -0
  63. package/lib/structures/index.js +37 -0
  64. package/lib/structures/index.js.map +1 -0
  65. package/lib/view.js +140 -0
  66. package/lib/view.js.map +1 -0
  67. package/lib/viewer/index.html +123 -0
  68. package/lib/viewer/viewer.js +4610 -0
  69. package/package.json +17 -4
  70. package/src/TtscGraphApplication.ts +57 -0
  71. package/src/index.ts +40 -126
  72. package/src/model/TtscGraphMemory.ts +204 -0
  73. package/src/model/loadGraph.ts +85 -0
  74. package/src/reduce.ts +165 -0
  75. package/src/resolveGraphBinary.ts +40 -0
  76. package/src/server/createServer.ts +104 -0
  77. package/src/server/instructions.ts +28 -0
  78. package/src/server/resolveHandle.ts +34 -0
  79. package/src/server/runExpand.ts +186 -0
  80. package/src/server/runIndex.ts +181 -0
  81. package/src/server/runOverview.ts +154 -0
  82. package/src/server/runQuery.ts +150 -0
  83. package/src/server/runTrace.ts +233 -0
  84. package/src/server/startServer.ts +25 -0
  85. package/src/structures/ITtscGraphApplication.ts +68 -0
  86. package/src/structures/ITtscGraphDecorator.ts +33 -0
  87. package/src/structures/ITtscGraphDiagnostic.ts +34 -0
  88. package/src/structures/ITtscGraphDump.ts +35 -0
  89. package/src/structures/ITtscGraphEdge.ts +24 -0
  90. package/src/structures/ITtscGraphEvidence.ts +29 -0
  91. package/src/structures/ITtscGraphExpand.ts +85 -0
  92. package/src/structures/ITtscGraphIndex.ts +100 -0
  93. package/src/structures/ITtscGraphNode.ts +64 -0
  94. package/src/structures/ITtscGraphOverview.ts +79 -0
  95. package/src/structures/ITtscGraphQuery.ts +49 -0
  96. package/src/structures/ITtscGraphTrace.ts +97 -0
  97. package/src/structures/TtscGraphEdgeKind.ts +23 -0
  98. package/src/structures/TtscGraphNodeKind.ts +26 -0
  99. package/src/structures/TtscGraphNodeModifier.ts +19 -0
  100. package/src/structures/index.ts +21 -0
  101. package/src/view.ts +155 -0
  102. package/src/viewer/index.html +123 -0
  103. package/src/viewer/main.ts +209 -0
  104. package/lib/bin.d.ts +0 -2
  105. package/lib/diagnostics.d.ts +0 -23
  106. package/lib/diagnostics.js +0 -84
  107. package/lib/diagnostics.js.map +0 -1
  108. package/lib/index.d.ts +0 -29
  109. package/src/diagnostics.ts +0 -93
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ttsc/graph",
3
- "version": "0.16.4",
4
- "description": "Checker-resolved code graph and diagnostics over MCP for coding agents, backed by ttsc's in-process TypeScript-Go compiler.",
3
+ "version": "0.16.5",
4
+ "description": "Checker-resolved architecture graph over MCP for coding agents, backed by ttsc's in-process TypeScript-Go compiler.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "bin": {
@@ -14,6 +14,15 @@
14
14
  },
15
15
  "./package.json": "./package.json"
16
16
  },
17
+ "ttsc": {
18
+ "plugin": {
19
+ "transform": "typia/lib/transform"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.26.0",
24
+ "typia": "13.0.0-dev.20260605.1"
25
+ },
17
26
  "keywords": [
18
27
  "ttsc",
19
28
  "mcp",
@@ -28,9 +37,13 @@
28
37
  ],
29
38
  "devDependencies": {
30
39
  "@types/node": "^25.3.0",
40
+ "@types/three": "^0.184.1",
41
+ "esbuild": "^0.25.12",
31
42
  "rimraf": "^6.1.2",
43
+ "three": "^0.184.0",
44
+ "three-forcegraph": "^1.43.4",
32
45
  "typescript": "7.0.1-rc",
33
- "ttsc": "0.16.4"
46
+ "ttsc": "0.16.5"
34
47
  },
35
48
  "repository": {
36
49
  "type": "git",
@@ -43,6 +56,6 @@
43
56
  },
44
57
  "homepage": "https://ttsc.dev",
45
58
  "scripts": {
46
- "build": "rimraf lib && tsc"
59
+ "build": "rimraf lib && node node_modules/ttsc/lib/launcher/ttsc.js && node build/bundle-viewer.mjs"
47
60
  }
48
61
  }
@@ -0,0 +1,57 @@
1
+ import { TtscGraphMemory } from "./model/TtscGraphMemory";
2
+ import { runExpand } from "./server/runExpand";
3
+ import { runIndex } from "./server/runIndex";
4
+ import { runOverview } from "./server/runOverview";
5
+ import { runQuery } from "./server/runQuery";
6
+ import { runTrace } from "./server/runTrace";
7
+ import { ITtscGraphApplication } from "./structures/ITtscGraphApplication";
8
+ import { ITtscGraphExpand } from "./structures/ITtscGraphExpand";
9
+ import { ITtscGraphIndex } from "./structures/ITtscGraphIndex";
10
+ import { ITtscGraphOverview } from "./structures/ITtscGraphOverview";
11
+ import { ITtscGraphQuery } from "./structures/ITtscGraphQuery";
12
+ import { ITtscGraphTrace } from "./structures/ITtscGraphTrace";
13
+
14
+ export type TtscGraphSource = TtscGraphMemory | (() => TtscGraphMemory);
15
+
16
+ /**
17
+ * The MCP tool surface as a plain class over the resident
18
+ * {@link TtscGraphMemory}.
19
+ *
20
+ * Each public method is one MCP tool — `typia.llm.controller` reflects
21
+ * {@link ITtscGraphApplication} to generate every tool's JSON schema and
22
+ * argument validator from these signatures and their JSDoc, with no
23
+ * hand-written schema. The methods delegate to the pure tool functions in
24
+ * `./server`, which are unit-testable without a transport; this class only
25
+ * binds them to the graph.
26
+ *
27
+ * Every method answers from the resident graph; none recompiles. Output is kept
28
+ * compact and bounded so a model can read structure without a file read, which
29
+ * is the token win the redesign exists for.
30
+ */
31
+ export class TtscGraphApplication implements ITtscGraphApplication {
32
+ private readonly graph: () => TtscGraphMemory;
33
+
34
+ public constructor(source: TtscGraphSource) {
35
+ this.graph = typeof source === "function" ? source : () => source;
36
+ }
37
+
38
+ public graph_index(props: ITtscGraphIndex.IProps): ITtscGraphIndex {
39
+ return runIndex(this.graph(), props);
40
+ }
41
+
42
+ public graph_overview(props: ITtscGraphOverview.IProps): ITtscGraphOverview {
43
+ return runOverview(this.graph(), props);
44
+ }
45
+
46
+ public graph_expand(props: ITtscGraphExpand.IProps): ITtscGraphExpand {
47
+ return runExpand(this.graph(), props);
48
+ }
49
+
50
+ public graph_query(props: ITtscGraphQuery.IProps): ITtscGraphQuery {
51
+ return runQuery(this.graph(), props);
52
+ }
53
+
54
+ public graph_trace(props: ITtscGraphTrace.IProps): ITtscGraphTrace {
55
+ return runTrace(this.graph(), props);
56
+ }
57
+ }
package/src/index.ts CHANGED
@@ -1,50 +1,19 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import { createRequire } from "node:module";
4
- import os from "node:os";
1
+ import { spawnSync } from "node:child_process";
5
2
  import path from "node:path";
6
3
 
4
+ import { resolveGraphBinary } from "./resolveGraphBinary";
5
+ import { startServer } from "./server/startServer";
6
+ import { runView } from "./view";
7
+
8
+ // The server version reported in the MCP handshake; read from this package.
9
+ const VERSION: string = (require("../package.json") as { version: string })
10
+ .version;
11
+
7
12
  /**
8
- * Resolve the per-platform `ttscgraph` MCP server binary, or `null` when it
9
- * cannot be located.
10
- *
11
- * `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
12
- * of this launcher), so resolution starts from the user's project, not from
13
- * this package's own tree.
14
- *
15
- * Resolution order:
16
- *
17
- * 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
18
- * 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
19
- * That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
20
- * an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
21
- * location — found from `process.cwd()` (the project where the agent ran the
22
- * server).
13
+ * The project root and tsconfig to build the graph for, from the `--cwd` /
14
+ * `--tsconfig` flags (the same ones `ttscgraph dump` accepts). Defaults are the
15
+ * process working directory and `tsconfig.json`.
23
16
  */
24
- export function resolveGraphBinary(
25
- env: NodeJS.ProcessEnv = process.env,
26
- cwd: string = process.cwd(),
27
- ): string | null {
28
- if (env.TTSC_GRAPH_BINARY && path.isAbsolute(env.TTSC_GRAPH_BINARY)) {
29
- return env.TTSC_GRAPH_BINARY;
30
- }
31
- const exe = process.platform === "win32" ? "ttscgraph.exe" : "ttscgraph";
32
- try {
33
- const ttscPackageJson = require.resolve("ttsc/package.json", {
34
- paths: [cwd],
35
- });
36
- const fromTtsc = createRequire(ttscPackageJson);
37
- return fromTtsc.resolve(
38
- `@ttsc/${process.platform}-${process.arch}/bin/${exe}`,
39
- );
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- /** The project root and tsconfig the server was pointed at, mirroring the
46
- * `--cwd` / `--tsconfig` flags ttscgraph itself parses, so the background
47
- * diagnostics worker checks the same project. */
48
17
  function parseProjectArgs(argv: readonly string[]): {
49
18
  cwd: string;
50
19
  tsconfig: string;
@@ -56,62 +25,45 @@ function parseProjectArgs(argv: readonly string[]): {
56
25
  if (arg === "--cwd" && i + 1 < argv.length) cwd = argv[++i]!;
57
26
  else if (arg.startsWith("--cwd=")) cwd = arg.slice("--cwd=".length);
58
27
  else if (arg === "--tsconfig" && i + 1 < argv.length) tsconfig = argv[++i]!;
59
- else if (arg.startsWith("--tsconfig=")) tsconfig = arg.slice("--tsconfig=".length);
28
+ else if (arg.startsWith("--tsconfig="))
29
+ tsconfig = arg.slice("--tsconfig=".length);
60
30
  }
61
31
  return { cwd: path.resolve(cwd), tsconfig };
62
32
  }
63
33
 
64
- /** A `--connect` proxy pipes stdio to a running daemon and serves no graph of
65
- * its own, so it needs no diagnostics computed locally. */
66
- function isConnectProxy(argv: readonly string[]): boolean {
67
- return argv.some((a) => a === "--connect" || a.startsWith("--connect="));
68
- }
69
-
70
34
  /**
71
- * Start the background worker that computes the project's plugin diagnostics
72
- * (`@ttsc/lint` and transform-plugin findings) and writes them to
73
- * `diagnosticsFile`, where the server picks them up. Returns the child handle
74
- * for cleanup, or `null` when it could not be spawned.
35
+ * Run the `@ttsc/graph` launcher.
75
36
  *
76
- * It runs in parallel with the server so the MCP handshake is never blocked on
77
- * a compile; the server shows its tsc diagnostics immediately and fuses the
78
- * plugin findings once the file lands. Any failure is non-fatal — the graph
79
- * simply stays tsc-only.
37
+ * - `view`: JS-orchestrated 3D viewer (dump -> reduce -> serve -> open).
38
+ * - `dump`: pass through to the native `ttscgraph dump`, which prints the whole
39
+ * graph as JSON for piping or the viewer.
40
+ * - Default: serve the MCP graph over stdio. The TypeScript server runs
41
+ * `ttscgraph dump` once to build the resident graph, then answers tool calls
42
+ * from memory; the agent's MCP client speaks JSON-RPC over this process's
43
+ * stdin/stdout. The process stays alive on the stdio transport.
80
44
  */
81
- function startDiagnosticsWorker(
82
- argv: readonly string[],
83
- diagnosticsFile: string,
84
- ): ReturnType<typeof spawn> | null {
85
- try {
86
- const { cwd, tsconfig } = parseProjectArgs(argv);
87
- const worker = spawn(
88
- process.execPath,
89
- [require.resolve("./diagnostics.js"), cwd, tsconfig, diagnosticsFile],
90
- { stdio: "ignore", windowsHide: true },
91
- );
92
- worker.on("error", () => {
93
- /* resilient: a worker that cannot start leaves no file */
94
- });
95
- worker.unref();
96
- return worker;
97
- } catch {
98
- return null;
99
- }
45
+ export function runGraph(
46
+ argv: readonly string[] = process.argv.slice(2),
47
+ ): number | void {
48
+ if (argv[0] === "view") return runView(argv.slice(1));
49
+ if (argv[0] === "dump") return runDump(argv);
50
+
51
+ const { cwd, tsconfig } = parseProjectArgs(argv);
52
+ void startServer({ cwd, tsconfig, version: VERSION }).catch(
53
+ (error: unknown) => {
54
+ process.stderr.write(
55
+ `@ttsc/graph: ${error instanceof Error ? error.message : String(error)}\n`,
56
+ );
57
+ process.exit(1);
58
+ },
59
+ );
100
60
  }
101
61
 
102
62
  /**
103
- * Spawn the resident MCP server, inheriting stdio so the agent's MCP client
104
- * speaks JSON-RPC to it directly over this process's stdin/stdout. Returns the
105
- * child's exit code.
106
- *
107
- * Before spawning, it kicks off the background diagnostics worker (except in
108
- * `--connect` proxy mode) and points the server at its output file, so a
109
- * plugin-using project's lint and transform-plugin diagnostics fuse onto the
110
- * graph without blocking startup.
63
+ * Pass `dump` through to the native binary, inheriting stdio so the JSON lands
64
+ * on this process's stdout. Returns the child's exit code.
111
65
  */
112
- export function runGraph(
113
- argv: readonly string[] = process.argv.slice(2),
114
- ): number {
66
+ function runDump(argv: readonly string[]): number {
115
67
  const binary = resolveGraphBinary();
116
68
  if (binary === null) {
117
69
  process.stderr.write(
@@ -121,48 +73,10 @@ export function runGraph(
121
73
  );
122
74
  return 1;
123
75
  }
124
-
125
- const env: NodeJS.ProcessEnv = { ...process.env };
126
- let diagnosticsFile: string | null = null;
127
- let worker: ReturnType<typeof spawn> | null = null;
128
- if (!isConnectProxy(argv)) {
129
- diagnosticsFile = path.join(
130
- os.tmpdir(),
131
- `ttsc-graph-diagnostics-${process.pid}.json`,
132
- );
133
- env.TTSC_GRAPH_DIAGNOSTICS_FILE = diagnosticsFile;
134
- worker = startDiagnosticsWorker(argv, diagnosticsFile);
135
- }
136
-
137
76
  const result = spawnSync(binary, [...argv], {
138
77
  stdio: "inherit",
139
- env,
140
78
  windowsHide: true,
141
79
  });
142
-
143
- const workerPid = worker?.pid;
144
- try {
145
- worker?.kill();
146
- } catch {
147
- /* ignore */
148
- }
149
- if (diagnosticsFile) {
150
- try {
151
- fs.rmSync(diagnosticsFile, { force: true });
152
- } catch {
153
- /* ignore */
154
- }
155
- // The worker writes atomically through `<file>.<pid>.tmp`; remove a leftover
156
- // if it was killed mid-write, so a churning daemon does not litter tmpdir.
157
- if (workerPid !== undefined) {
158
- try {
159
- fs.rmSync(`${diagnosticsFile}.${workerPid}.tmp`, { force: true });
160
- } catch {
161
- /* ignore */
162
- }
163
- }
164
- }
165
-
166
80
  if (result.error) {
167
81
  process.stderr.write(`@ttsc/graph: ${result.error.message}\n`);
168
82
  return 1;
@@ -0,0 +1,204 @@
1
+ import { ITtscGraphDump } from "../structures/ITtscGraphDump";
2
+ import { ITtscGraphEdge } from "../structures/ITtscGraphEdge";
3
+ import { ITtscGraphNode } from "../structures/ITtscGraphNode";
4
+ import { TtscGraphEdgeKind } from "../structures/TtscGraphEdgeKind";
5
+
6
+ /**
7
+ * The in-memory resident graph the MCP tools answer from.
8
+ *
9
+ * It loads one `ttscgraph dump` — the checker-resolved fact graph — then
10
+ * synthesizes the structural relationships the dump deliberately leaves to this
11
+ * layer: `file` container nodes, the `contains` ownership tree, and `exports`
12
+ * edges, plus the refinement of a class-member `variable` to a `property`.
13
+ * Every tool call is then a lookup or a traversal over the indexes built here;
14
+ * nothing recompiles.
15
+ */
16
+ export class TtscGraphMemory {
17
+ private readonly byId: Map<string, ITtscGraphNode>;
18
+ private readonly outEdges: Map<string, ITtscGraphEdge[]>;
19
+ private readonly inEdges: Map<string, ITtscGraphEdge[]>;
20
+ private readonly byNameIndex: Map<string, ITtscGraphNode[]>;
21
+ private readonly bySymbolIndex: Map<string, ITtscGraphNode[]>;
22
+
23
+ /** The absolute project root the dump was built for. */
24
+ readonly project: string;
25
+ /** Every node, raw plus synthesized (file containers). */
26
+ readonly nodes: readonly ITtscGraphNode[];
27
+ /** Every edge, raw plus synthesized (contains, exports). */
28
+ readonly edges: readonly ITtscGraphEdge[];
29
+
30
+ private constructor(
31
+ project: string,
32
+ nodes: ITtscGraphNode[],
33
+ edges: ITtscGraphEdge[],
34
+ ) {
35
+ this.project = project;
36
+ this.nodes = nodes;
37
+ this.edges = edges;
38
+
39
+ this.byId = new Map(nodes.map((n) => [n.id, n]));
40
+ this.byNameIndex = new Map();
41
+ this.bySymbolIndex = new Map();
42
+ for (const node of nodes) {
43
+ const bucket = this.byNameIndex.get(node.name);
44
+ if (bucket) bucket.push(node);
45
+ else this.byNameIndex.set(node.name, [node]);
46
+ if (node.kind !== "file") {
47
+ push(this.bySymbolIndex, node.name, node);
48
+ if (node.qualifiedName !== undefined) {
49
+ push(this.bySymbolIndex, node.qualifiedName, node);
50
+ }
51
+ }
52
+ }
53
+ this.outEdges = new Map();
54
+ this.inEdges = new Map();
55
+ for (const edge of edges) {
56
+ push(this.outEdges, edge.from, edge);
57
+ push(this.inEdges, edge.to, edge);
58
+ }
59
+ }
60
+
61
+ /** Build a model from a parsed dump, synthesizing structural relationships. */
62
+ static from(dump: ITtscGraphDump): TtscGraphMemory {
63
+ const { nodes, edges } = synthesize(dump);
64
+ return new TtscGraphMemory(dump.project, nodes, edges);
65
+ }
66
+
67
+ /** The node with this id, or undefined. */
68
+ node(id: string): ITtscGraphNode | undefined {
69
+ return this.byId.get(id);
70
+ }
71
+
72
+ /** Edges leaving a node (the node is the `from`). */
73
+ outgoing(id: string): readonly ITtscGraphEdge[] {
74
+ return this.outEdges.get(id) ?? [];
75
+ }
76
+
77
+ /** Edges entering a node (the node is the `to`). */
78
+ incoming(id: string): readonly ITtscGraphEdge[] {
79
+ return this.inEdges.get(id) ?? [];
80
+ }
81
+
82
+ /** Every node whose simple name equals `name`. */
83
+ named(name: string): readonly ITtscGraphNode[] {
84
+ return this.byNameIndex.get(name) ?? [];
85
+ }
86
+
87
+ /** Every non-file node whose simple or owner-qualified symbol handle matches. */
88
+ symbols(handle: string): readonly ITtscGraphNode[] {
89
+ return this.bySymbolIndex.get(handle) ?? [];
90
+ }
91
+
92
+ /** Every workspace node on its module's export surface. */
93
+ exported(): ITtscGraphNode[] {
94
+ return this.nodes.filter((n) => n.exported && !n.external);
95
+ }
96
+ }
97
+
98
+ /** Append value to the slice stored at key, creating the slice on first use. */
99
+ function push<K, V>(map: Map<K, V[]>, key: K, value: V): void {
100
+ const bucket = map.get(key);
101
+ if (bucket) bucket.push(value);
102
+ else map.set(key, [value]);
103
+ }
104
+
105
+ /**
106
+ * The within-file identity of a node: its owner-qualified name when it has one
107
+ * (`Class.method`), else its simple name. Two nodes in one file never share a
108
+ * key, so it is the handle the ownership synthesis looks owners up by.
109
+ */
110
+ function keyOf(node: ITtscGraphNode): string {
111
+ return node.qualifiedName ?? node.name;
112
+ }
113
+
114
+ /** The owner key of a dotted key (`A.B.c` -> `A.B`), or "" for a top-level key. */
115
+ function ownerKey(key: string): string {
116
+ const dot = key.lastIndexOf(".");
117
+ return dot >= 0 ? key.slice(0, dot) : "";
118
+ }
119
+
120
+ /** A file's id and node name from its project-relative path. */
121
+ function fileNodeId(file: string): string {
122
+ return file;
123
+ }
124
+
125
+ function basename(file: string): string {
126
+ const slash = file.lastIndexOf("/");
127
+ return slash >= 0 ? file.slice(slash + 1) : file;
128
+ }
129
+
130
+ /**
131
+ * Derive the structural layer from a dump's faithful facts: refine class-member
132
+ * variables to properties, add a `file` node per workspace source, and connect
133
+ * the `contains` ownership tree and `exports` surface.
134
+ */
135
+ function synthesize(dump: ITtscGraphDump): {
136
+ nodes: ITtscGraphNode[];
137
+ edges: ITtscGraphEdge[];
138
+ } {
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();
142
+
143
+ // Index workspace nodes by (file, within-file key) so ownership can resolve a
144
+ // member to its declaring class/namespace.
145
+ const byFileKey = new Map<string, ITtscGraphNode>();
146
+ for (const node of nodes) {
147
+ if (!node.external) byFileKey.set(node.file + "\0" + keyOf(node), node);
148
+ }
149
+ const owner = (node: ITtscGraphNode): ITtscGraphNode | undefined => {
150
+ const parent = ownerKey(keyOf(node));
151
+ if (parent === "") return undefined;
152
+ return byFileKey.get(node.file + "\0" + parent);
153
+ };
154
+
155
+ // Refine: a `variable` whose owner is a class or interface is a property.
156
+ for (const node of nodes) {
157
+ if (node.kind !== "variable" || node.external) continue;
158
+ const parent = owner(node);
159
+ if (parent && (parent.kind === "class" || parent.kind === "interface")) {
160
+ node.kind = "property";
161
+ }
162
+ }
163
+
164
+ // One file container node per distinct workspace source file.
165
+ const fileNodes = new Map<string, ITtscGraphNode>();
166
+ 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
+ }
177
+ }
178
+
179
+ // 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.
181
+ const structural: ITtscGraphEdge[] = [];
182
+ for (const node of nodes) {
183
+ if (node.external || node.file === "") continue;
184
+ const parent = owner(node);
185
+ const container = parent ? parent.id : fileNodeId(node.file);
186
+ structural.push({
187
+ from: container,
188
+ to: node.id,
189
+ kind: "contains",
190
+ });
191
+ if (node.exported) {
192
+ structural.push({
193
+ from: fileNodeId(node.file),
194
+ to: node.id,
195
+ kind: "exports" satisfies TtscGraphEdgeKind,
196
+ });
197
+ }
198
+ }
199
+
200
+ return {
201
+ nodes: [...nodes, ...fileNodes.values()],
202
+ edges: [...edges, ...structural],
203
+ };
204
+ }
@@ -0,0 +1,85 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import typia from "typia";
3
+
4
+ import { resolveGraphBinary } from "../resolveGraphBinary";
5
+ import { ITtscGraphDump } from "../structures/ITtscGraphDump";
6
+ import { TtscGraphMemory } from "./TtscGraphMemory";
7
+
8
+ // A full-project dump is the whole fact graph as one JSON document; a large
9
+ // monorepo runs to many megabytes, well past spawnSync's 1 MiB default, so the
10
+ // buffer is raised to a ceiling no real graph reaches.
11
+ const MAX_DUMP_BYTES = 1024 * 1024 * 1024;
12
+
13
+ /**
14
+ * Build the resident {@link TtscGraphMemory} for a project by running `ttscgraph
15
+ * dump` once and loading its JSON. This is the cold path behind the MCP tool
16
+ * calls: one type-check in Go produces the checker-resolved fact graph, then
17
+ * every later tool call is answered from the in-memory model.
18
+ *
19
+ * Throws when the binary cannot be resolved, the dump command fails, or its
20
+ * output is not a readable graph — the server surfaces the failure rather than
21
+ * answering from an empty graph.
22
+ */
23
+ export function loadGraph(
24
+ options: {
25
+ /** Project root the graph is built for (default: `process.cwd()`). */
26
+ cwd?: string;
27
+ /** Project tsconfig, relative to `cwd` (default: `tsconfig.json`). */
28
+ tsconfig?: string;
29
+ /**
30
+ * Absolute path to the `ttscgraph` binary. Defaults to the per-platform
31
+ * binary resolved from the project's installed `ttsc`; pass it explicitly
32
+ * to point at a custom build.
33
+ */
34
+ binary?: string;
35
+ } = {},
36
+ ): TtscGraphMemory {
37
+ const cwd = options.cwd ?? process.cwd();
38
+ const tsconfig = options.tsconfig ?? "tsconfig.json";
39
+ const binary = options.binary ?? resolveGraphBinary();
40
+ if (binary === null) {
41
+ throw new Error(
42
+ "@ttsc/graph: could not resolve the ttscgraph binary. " +
43
+ "Install `ttsc` so its platform package is present, " +
44
+ "or set TTSC_GRAPH_BINARY to an absolute path.",
45
+ );
46
+ }
47
+
48
+ const result = spawnSync(
49
+ binary,
50
+ ["dump", "--cwd", cwd, "--tsconfig", tsconfig],
51
+ { encoding: "utf8", maxBuffer: MAX_DUMP_BYTES, windowsHide: true },
52
+ );
53
+ if (result.error) {
54
+ throw new Error(
55
+ `@ttsc/graph: ttscgraph dump failed: ${result.error.message}`,
56
+ );
57
+ }
58
+ if (result.status !== 0) {
59
+ throw new Error(
60
+ `@ttsc/graph: ttscgraph dump exited with ${result.status}: ${(result.stderr ?? "").trim()}`,
61
+ );
62
+ }
63
+
64
+ return TtscGraphMemory.from(parseDump(result.stdout));
65
+ }
66
+
67
+ /**
68
+ * Parse and validate `ttscgraph dump` output. typia asserts the full
69
+ * {@link ITtscGraphDump} shape so a malformed or stale dump fails loudly here
70
+ * rather than producing wrong answers downstream, and the schema version is
71
+ * checked so an incompatible producer is refused.
72
+ */
73
+ function parseDump(json: string): ITtscGraphDump {
74
+ let value: unknown;
75
+ try {
76
+ value = JSON.parse(json);
77
+ } catch (error) {
78
+ throw new Error(
79
+ `@ttsc/graph: dump output is not valid JSON: ${
80
+ error instanceof Error ? error.message : String(error)
81
+ }`,
82
+ );
83
+ }
84
+ return typia.assert<ITtscGraphDump>(value);
85
+ }