@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.
- package/LICENSE +21 -21
- package/README.md +24 -53
- package/lib/TtscGraphApplication.js +46 -0
- package/lib/TtscGraphApplication.js.map +1 -0
- package/lib/index.js +31 -108
- package/lib/index.js.map +1 -1
- package/lib/model/TtscGraphMemory.js +185 -0
- package/lib/model/TtscGraphMemory.js.map +1 -0
- package/lib/model/loadGraph.js +418 -0
- package/lib/model/loadGraph.js.map +1 -0
- package/lib/reduce.js +101 -0
- package/lib/reduce.js.map +1 -0
- package/lib/resolveGraphBinary.js +42 -0
- package/lib/resolveGraphBinary.js.map +1 -0
- package/lib/server/createServer.js +1227 -0
- package/lib/server/createServer.js.map +1 -0
- package/lib/server/instructions.js +32 -0
- package/lib/server/instructions.js.map +1 -0
- package/lib/server/resolveHandle.js +25 -0
- package/lib/server/resolveHandle.js.map +1 -0
- package/lib/server/runExpand.js +177 -0
- package/lib/server/runExpand.js.map +1 -0
- package/lib/server/runIndex.js +146 -0
- package/lib/server/runIndex.js.map +1 -0
- package/lib/server/runOverview.js +150 -0
- package/lib/server/runOverview.js.map +1 -0
- package/lib/server/runQuery.js +147 -0
- package/lib/server/runQuery.js.map +1 -0
- package/lib/server/runTrace.js +219 -0
- package/lib/server/runTrace.js.map +1 -0
- package/lib/server/startServer.js +21 -0
- package/lib/server/startServer.js.map +1 -0
- package/lib/structures/ITtscGraphApplication.js +3 -0
- package/lib/structures/ITtscGraphApplication.js.map +1 -0
- package/lib/structures/ITtscGraphDecorator.js +3 -0
- package/lib/structures/ITtscGraphDecorator.js.map +1 -0
- package/lib/structures/ITtscGraphDiagnostic.js +3 -0
- package/lib/structures/ITtscGraphDiagnostic.js.map +1 -0
- package/lib/structures/ITtscGraphDump.js +3 -0
- package/lib/structures/ITtscGraphDump.js.map +1 -0
- package/lib/structures/ITtscGraphEdge.js +3 -0
- package/lib/structures/ITtscGraphEdge.js.map +1 -0
- package/lib/structures/ITtscGraphEvidence.js +3 -0
- package/lib/structures/ITtscGraphEvidence.js.map +1 -0
- package/lib/structures/ITtscGraphExpand.js +3 -0
- package/lib/structures/ITtscGraphExpand.js.map +1 -0
- package/lib/structures/ITtscGraphIndex.js +3 -0
- package/lib/structures/ITtscGraphIndex.js.map +1 -0
- package/lib/structures/ITtscGraphNode.js +3 -0
- package/lib/structures/ITtscGraphNode.js.map +1 -0
- package/lib/structures/ITtscGraphOverview.js +3 -0
- package/lib/structures/ITtscGraphOverview.js.map +1 -0
- package/lib/structures/ITtscGraphQuery.js +3 -0
- package/lib/structures/ITtscGraphQuery.js.map +1 -0
- package/lib/structures/ITtscGraphTrace.js +3 -0
- package/lib/structures/ITtscGraphTrace.js.map +1 -0
- package/lib/structures/TtscGraphEdgeKind.js +3 -0
- package/lib/structures/TtscGraphEdgeKind.js.map +1 -0
- package/lib/structures/TtscGraphNodeKind.js +3 -0
- package/lib/structures/TtscGraphNodeKind.js.map +1 -0
- package/lib/structures/TtscGraphNodeModifier.js +3 -0
- package/lib/structures/TtscGraphNodeModifier.js.map +1 -0
- package/lib/structures/index.js +37 -0
- package/lib/structures/index.js.map +1 -0
- package/lib/view.js +140 -0
- package/lib/view.js.map +1 -0
- package/lib/viewer/index.html +123 -0
- package/lib/viewer/viewer.js +4610 -0
- package/package.json +17 -4
- package/src/TtscGraphApplication.ts +57 -0
- package/src/index.ts +40 -126
- package/src/model/TtscGraphMemory.ts +204 -0
- package/src/model/loadGraph.ts +85 -0
- package/src/reduce.ts +165 -0
- package/src/resolveGraphBinary.ts +40 -0
- package/src/server/createServer.ts +104 -0
- package/src/server/instructions.ts +28 -0
- package/src/server/resolveHandle.ts +34 -0
- package/src/server/runExpand.ts +186 -0
- package/src/server/runIndex.ts +181 -0
- package/src/server/runOverview.ts +154 -0
- package/src/server/runQuery.ts +150 -0
- package/src/server/runTrace.ts +233 -0
- package/src/server/startServer.ts +25 -0
- package/src/structures/ITtscGraphApplication.ts +68 -0
- package/src/structures/ITtscGraphDecorator.ts +33 -0
- package/src/structures/ITtscGraphDiagnostic.ts +34 -0
- package/src/structures/ITtscGraphDump.ts +35 -0
- package/src/structures/ITtscGraphEdge.ts +24 -0
- package/src/structures/ITtscGraphEvidence.ts +29 -0
- package/src/structures/ITtscGraphExpand.ts +85 -0
- package/src/structures/ITtscGraphIndex.ts +100 -0
- package/src/structures/ITtscGraphNode.ts +64 -0
- package/src/structures/ITtscGraphOverview.ts +79 -0
- package/src/structures/ITtscGraphQuery.ts +49 -0
- package/src/structures/ITtscGraphTrace.ts +97 -0
- package/src/structures/TtscGraphEdgeKind.ts +23 -0
- package/src/structures/TtscGraphNodeKind.ts +26 -0
- package/src/structures/TtscGraphNodeModifier.ts +19 -0
- package/src/structures/index.ts +21 -0
- package/src/view.ts +155 -0
- package/src/viewer/index.html +123 -0
- package/src/viewer/main.ts +209 -0
- package/lib/bin.d.ts +0 -2
- package/lib/diagnostics.d.ts +0 -23
- package/lib/diagnostics.js +0 -84
- package/lib/diagnostics.js.map +0 -1
- package/lib/index.d.ts +0 -29
- package/src/diagnostics.ts +0 -93
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/graph",
|
|
3
|
-
"version": "0.16.
|
|
4
|
-
"description": "Checker-resolved
|
|
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.
|
|
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 &&
|
|
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 {
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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="))
|
|
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
|
-
*
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
|
82
|
-
argv: readonly string[],
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
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
|
-
*
|
|
104
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|