@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/src/reduce.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Reduce a raw `ttscgraph dump` to the payload the bundled 3D viewer renders.
|
|
2
|
+
// This mirrors website/src/components/graph/graphReduce.ts (the same pure
|
|
3
|
+
// transform); keep the two in sync. The CLI reduces in Node before serving, so
|
|
4
|
+
// the browser viewer only ever renders a ready `{ nodes, links }`.
|
|
5
|
+
|
|
6
|
+
export interface RawNode {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
file: string;
|
|
11
|
+
external?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RawEdge {
|
|
15
|
+
from: string;
|
|
16
|
+
to: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RawDump {
|
|
21
|
+
project?: string;
|
|
22
|
+
nodes: RawNode[];
|
|
23
|
+
edges: RawEdge[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ViewerNode {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
file: string;
|
|
31
|
+
degree: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ViewerLink {
|
|
35
|
+
source: string;
|
|
36
|
+
target: string;
|
|
37
|
+
kind: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ViewerPayload {
|
|
41
|
+
project: string;
|
|
42
|
+
counts: {
|
|
43
|
+
rawNodes: number;
|
|
44
|
+
rawEdges: number;
|
|
45
|
+
nodes: number;
|
|
46
|
+
links: number;
|
|
47
|
+
droppedExternal: number;
|
|
48
|
+
droppedByCap: number;
|
|
49
|
+
};
|
|
50
|
+
nodes: ViewerNode[];
|
|
51
|
+
links: ViewerLink[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function posix(p: string): string {
|
|
55
|
+
return p.replace(/\\/g, "/");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function commonRoot(files: string[]): string {
|
|
59
|
+
if (files.length === 0) return "";
|
|
60
|
+
let parts = posix(files[0]!).split("/");
|
|
61
|
+
for (const file of files.slice(1)) {
|
|
62
|
+
const other = posix(file).split("/");
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < parts.length && i < other.length && parts[i] === other[i]) i++;
|
|
65
|
+
parts = parts.slice(0, i);
|
|
66
|
+
if (parts.length === 0) break;
|
|
67
|
+
}
|
|
68
|
+
return parts.join("/");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function relativize(abs: string, root: string): string {
|
|
72
|
+
const a = posix(abs);
|
|
73
|
+
const r = posix(root).replace(/\/+$/, "");
|
|
74
|
+
if (r && (a === r || a.startsWith(r + "/")))
|
|
75
|
+
return a.slice(r.length).replace(/^\/+/, "");
|
|
76
|
+
const nm = a.lastIndexOf("node_modules/");
|
|
77
|
+
if (nm >= 0) return a.slice(nm);
|
|
78
|
+
const slash = a.lastIndexOf("/");
|
|
79
|
+
return slash >= 0 ? a.slice(slash + 1) : a;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function rewriteId(id: string, root: string): string {
|
|
83
|
+
const hash = id.indexOf("#");
|
|
84
|
+
if (hash < 0) return id;
|
|
85
|
+
return relativize(id.slice(0, hash), root) + id.slice(hash);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function degreeOf(
|
|
89
|
+
nodes: { id: string }[],
|
|
90
|
+
edges: { from: string; to: string }[],
|
|
91
|
+
): Map<string, number> {
|
|
92
|
+
const degree = new Map<string, number>(nodes.map((n) => [n.id, 0]));
|
|
93
|
+
for (const e of edges) {
|
|
94
|
+
if (degree.has(e.from)) degree.set(e.from, degree.get(e.from)! + 1);
|
|
95
|
+
if (degree.has(e.to)) degree.set(e.to, degree.get(e.to)! + 1);
|
|
96
|
+
}
|
|
97
|
+
return degree;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function reduce(
|
|
101
|
+
raw: RawDump,
|
|
102
|
+
{
|
|
103
|
+
maxNodes = 1200,
|
|
104
|
+
keepExternal = false,
|
|
105
|
+
}: { maxNodes?: number; keepExternal?: boolean } = {},
|
|
106
|
+
): ViewerPayload {
|
|
107
|
+
const keptByExternal = raw.nodes.filter((n) => keepExternal || !n.external);
|
|
108
|
+
const root = commonRoot(
|
|
109
|
+
raw.nodes.filter((n) => !n.external).map((n) => n.file),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const liveIds = new Set(keptByExternal.map((n) => n.id));
|
|
113
|
+
const liveEdges = raw.edges.filter(
|
|
114
|
+
(e) => liveIds.has(e.from) && liveIds.has(e.to),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const degree = degreeOf(keptByExternal, liveEdges);
|
|
118
|
+
let kept = keptByExternal;
|
|
119
|
+
let droppedByCap = 0;
|
|
120
|
+
if (kept.length > maxNodes) {
|
|
121
|
+
kept = [...kept]
|
|
122
|
+
.sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0))
|
|
123
|
+
.slice(0, maxNodes);
|
|
124
|
+
droppedByCap = keptByExternal.length - kept.length;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const keptIds = new Set(kept.map((n) => n.id));
|
|
128
|
+
const edges = liveEdges.filter(
|
|
129
|
+
(e) => keptIds.has(e.from) && keptIds.has(e.to),
|
|
130
|
+
);
|
|
131
|
+
const finalDegree = degreeOf(kept, edges);
|
|
132
|
+
|
|
133
|
+
const nodes: ViewerNode[] = kept
|
|
134
|
+
.filter((n) => (finalDegree.get(n.id) ?? 0) > 0)
|
|
135
|
+
.map((n) => ({
|
|
136
|
+
id: rewriteId(n.id, root),
|
|
137
|
+
name: n.name,
|
|
138
|
+
kind: n.kind,
|
|
139
|
+
file: relativize(n.file, root),
|
|
140
|
+
degree: finalDegree.get(n.id) ?? 0,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
144
|
+
const links: ViewerLink[] = edges
|
|
145
|
+
.map((e) => ({
|
|
146
|
+
source: rewriteId(e.from, root),
|
|
147
|
+
target: rewriteId(e.to, root),
|
|
148
|
+
kind: e.kind,
|
|
149
|
+
}))
|
|
150
|
+
.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
project: raw.project ?? "",
|
|
154
|
+
counts: {
|
|
155
|
+
rawNodes: raw.nodes.length,
|
|
156
|
+
rawEdges: raw.edges.length,
|
|
157
|
+
nodes: nodes.length,
|
|
158
|
+
links: links.length,
|
|
159
|
+
droppedExternal: raw.nodes.length - keptByExternal.length,
|
|
160
|
+
droppedByCap,
|
|
161
|
+
},
|
|
162
|
+
nodes,
|
|
163
|
+
links,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the per-platform `ttscgraph` binary, or `null` when it cannot be
|
|
6
|
+
* located.
|
|
7
|
+
*
|
|
8
|
+
* `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
|
|
9
|
+
* of this launcher), so resolution starts from the user's project, not from
|
|
10
|
+
* this package's own tree.
|
|
11
|
+
*
|
|
12
|
+
* Resolution order:
|
|
13
|
+
*
|
|
14
|
+
* 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
|
|
15
|
+
* 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
|
|
16
|
+
* That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
|
|
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).
|
|
20
|
+
*/
|
|
21
|
+
export function resolveGraphBinary(
|
|
22
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
23
|
+
cwd: string = process.cwd(),
|
|
24
|
+
): string | null {
|
|
25
|
+
if (env.TTSC_GRAPH_BINARY && path.isAbsolute(env.TTSC_GRAPH_BINARY)) {
|
|
26
|
+
return env.TTSC_GRAPH_BINARY;
|
|
27
|
+
}
|
|
28
|
+
const exe = process.platform === "win32" ? "ttscgraph.exe" : "ttscgraph";
|
|
29
|
+
try {
|
|
30
|
+
const ttscPackageJson = require.resolve("ttsc/package.json", {
|
|
31
|
+
paths: [cwd],
|
|
32
|
+
});
|
|
33
|
+
const fromTtsc = createRequire(ttscPackageJson);
|
|
34
|
+
return fromTtsc.resolve(
|
|
35
|
+
`@ttsc/${process.platform}-${process.arch}/bin/${exe}`,
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import typia from "typia";
|
|
7
|
+
|
|
8
|
+
import { TtscGraphApplication, TtscGraphSource } from "../TtscGraphApplication";
|
|
9
|
+
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
10
|
+
import { ITtscGraphApplication } from "../structures/ITtscGraphApplication";
|
|
11
|
+
import { instructions } from "./instructions";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the MCP server for a graph. `typia.llm.controller` reflects
|
|
15
|
+
* {@link ITtscGraphApplication} into a validated tool application — every tool's
|
|
16
|
+
* JSON schema and argument validator is generated from the method's TypeScript
|
|
17
|
+
* types and JSDoc, so there is no hand-written schema. The list/call handlers
|
|
18
|
+
* below are the minimal standalone registration: list the generated functions,
|
|
19
|
+
* and on a call validate the arguments (returning typia's errors for the model
|
|
20
|
+
* to self-correct) before invoking the method.
|
|
21
|
+
*
|
|
22
|
+
* Registration is inlined rather than pulled from `@typia/mcp` to keep the
|
|
23
|
+
* dependency surface to `typia` plus the MCP SDK and avoid version-pinning the
|
|
24
|
+
* wider typia ecosystem; the shape it relies on is `typia.llm.controller`'s
|
|
25
|
+
* public output.
|
|
26
|
+
*/
|
|
27
|
+
export function createServer(
|
|
28
|
+
graph: TtscGraphSource,
|
|
29
|
+
version: string,
|
|
30
|
+
): McpServer {
|
|
31
|
+
const controller = typia.llm.controller<ITtscGraphApplication>(
|
|
32
|
+
"graph",
|
|
33
|
+
new TtscGraphApplication(graph),
|
|
34
|
+
);
|
|
35
|
+
const functions = controller.application.functions;
|
|
36
|
+
const execute = controller.execute as unknown as Record<
|
|
37
|
+
string,
|
|
38
|
+
(input: unknown) => unknown
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
const server = new McpServer(
|
|
42
|
+
{ name: "ttsc-graph", version },
|
|
43
|
+
{ capabilities: { tools: {} }, instructions },
|
|
44
|
+
);
|
|
45
|
+
const raw = server.server;
|
|
46
|
+
|
|
47
|
+
raw.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
48
|
+
tools: functions.map((func) => ({
|
|
49
|
+
name: func.name,
|
|
50
|
+
description: func.description,
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object" as const,
|
|
53
|
+
properties: func.parameters.properties,
|
|
54
|
+
required: func.parameters.required,
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
$defs: func.parameters.$defs,
|
|
57
|
+
},
|
|
58
|
+
})),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
raw.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
62
|
+
const func = functions.find((f) => f.name === request.params.name);
|
|
63
|
+
const method = execute[request.params.name];
|
|
64
|
+
if (func === undefined || method === undefined) {
|
|
65
|
+
return error(`Unknown tool: ${request.params.name}`);
|
|
66
|
+
}
|
|
67
|
+
// A client may omit `arguments` for an all-optional tool (e.g. graph_overview);
|
|
68
|
+
// validate an empty object so that call succeeds instead of failing on "expected object".
|
|
69
|
+
const validation = func.validate(request.params.arguments ?? {});
|
|
70
|
+
if (!validation.success) {
|
|
71
|
+
// Hand typia's validation errors back so the model can correct its call.
|
|
72
|
+
return error(JSON.stringify(validation.errors, null, 2));
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const result = await method.call(execute, validation.data);
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text" as const,
|
|
80
|
+
text:
|
|
81
|
+
result === undefined
|
|
82
|
+
? "Success"
|
|
83
|
+
: JSON.stringify(result, null, 2),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
} catch (exception) {
|
|
88
|
+
return error(
|
|
89
|
+
exception instanceof Error
|
|
90
|
+
? `${exception.name}: ${exception.message}`
|
|
91
|
+
: String(exception),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return server;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function error(text: string): {
|
|
100
|
+
isError: true;
|
|
101
|
+
content: { type: "text"; text: string }[];
|
|
102
|
+
} {
|
|
103
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
104
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guidance delivered in the MCP initialize response. It is the only place
|
|
3
|
+
* the agent is told how to use the graph; nothing is written to its config
|
|
4
|
+
* files. Keep it short; the per-tool descriptions carry the detail.
|
|
5
|
+
*/
|
|
6
|
+
export const instructions = `
|
|
7
|
+
This TypeScript project is indexed by the compiler. Use the graph as the first
|
|
8
|
+
source of codebase coordinates: it has already resolved symbols and the
|
|
9
|
+
relationships between them.
|
|
10
|
+
|
|
11
|
+
- graph_index: first call for source questions. It returns ranked symbols,
|
|
12
|
+
declaration signatures, directly mentioned code handles, and nearby dependency
|
|
13
|
+
context without source bodies.
|
|
14
|
+
- graph_overview: the architecture, layers, hotspots, and public API.
|
|
15
|
+
- graph_query: find any symbol by name or description; each hit carries its
|
|
16
|
+
signature, usually enough to answer without expanding. Use next.expand when
|
|
17
|
+
you need bodies.
|
|
18
|
+
- graph_trace: follow a flow forward, reverse, or to its impact; or give dotted
|
|
19
|
+
from/to names for the path between two symbols, how A reaches B, in one call.
|
|
20
|
+
- graph_expand: a symbol's declared shape, its signature, and a container's
|
|
21
|
+
members; source:true to read a specific TypeScript body after graph_index,
|
|
22
|
+
graph_query, or graph_trace has located it.
|
|
23
|
+
|
|
24
|
+
Answer in as few calls as you can. For TypeScript declarations, read source with
|
|
25
|
+
graph_expand(source:true) on the resolved handles. Use shell or file reads only
|
|
26
|
+
for a non-TypeScript file, generated output, or an exact literal text search that
|
|
27
|
+
is not represented as symbols or edges.
|
|
28
|
+
`.trim();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
|
+
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
3
|
+
|
|
4
|
+
export interface IResolvedGraphHandle {
|
|
5
|
+
node?: ITtscGraphNode;
|
|
6
|
+
candidates?: ITtscGraphNode[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Resolve a tool handle as an id, exact symbol name, or dotted suffix. */
|
|
10
|
+
export function resolveGraphHandle(
|
|
11
|
+
graph: TtscGraphMemory,
|
|
12
|
+
handle: string,
|
|
13
|
+
candidateLimit = 12,
|
|
14
|
+
): IResolvedGraphHandle {
|
|
15
|
+
const byId = graph.node(handle);
|
|
16
|
+
if (byId !== undefined) return { node: byId };
|
|
17
|
+
|
|
18
|
+
const exact = graph.symbols(handle);
|
|
19
|
+
if (exact.length === 1) return { node: exact[0] };
|
|
20
|
+
if (exact.length > 1) return { candidates: exact.slice(0, candidateLimit) };
|
|
21
|
+
|
|
22
|
+
if (handle.includes(".")) {
|
|
23
|
+
const suffix = `.${handle}`;
|
|
24
|
+
const suffixMatches = graph.nodes.filter(
|
|
25
|
+
(node) =>
|
|
26
|
+
node.kind !== "file" && node.qualifiedName?.endsWith(suffix) === true,
|
|
27
|
+
);
|
|
28
|
+
if (suffixMatches.length === 1) return { node: suffixMatches[0] };
|
|
29
|
+
if (suffixMatches.length > 1) {
|
|
30
|
+
return { candidates: suffixMatches.slice(0, candidateLimit) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
5
|
+
import { ITtscGraphEdge } from "../structures/ITtscGraphEdge";
|
|
6
|
+
import { ITtscGraphExpand } from "../structures/ITtscGraphExpand";
|
|
7
|
+
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
8
|
+
import { resolveGraphHandle } from "./resolveHandle";
|
|
9
|
+
|
|
10
|
+
// A whole declaration body can be large, so the full source is opt-in and capped
|
|
11
|
+
// when asked for; the default response carries only the declared shape.
|
|
12
|
+
const MAX_SOURCE_LINES = 200;
|
|
13
|
+
// A signature is the declaration head up to the body brace — a handful of lines.
|
|
14
|
+
const MAX_SIGNATURE_LINES = 6;
|
|
15
|
+
// Neighbor lists are a map, not a dump; keep them scannable.
|
|
16
|
+
const MAX_NEIGHBORS = 40;
|
|
17
|
+
// A container's outline can be long (a big class); keep it bounded.
|
|
18
|
+
const MAX_MEMBERS = 80;
|
|
19
|
+
// Structural relationships are navigation, not the dependency picture expand is for.
|
|
20
|
+
const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
|
|
21
|
+
// Kinds whose value is their member outline, not a source body.
|
|
22
|
+
const CONTAINER_KINDS = new Set<string>([
|
|
23
|
+
"class",
|
|
24
|
+
"interface",
|
|
25
|
+
"namespace",
|
|
26
|
+
"module",
|
|
27
|
+
"enum",
|
|
28
|
+
"file",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve each handle to its declared shape — its signature, and for a
|
|
33
|
+
* container the outline of its members — and, only when asked, its full source
|
|
34
|
+
* body. This is the graph's edge over a plain file read: it answers from the
|
|
35
|
+
* resolved structure it already holds, so the agent reads compact shape, not
|
|
36
|
+
* inlined code, unless it explicitly needs a body's logic.
|
|
37
|
+
*/
|
|
38
|
+
export function runExpand(
|
|
39
|
+
graph: TtscGraphMemory,
|
|
40
|
+
props: ITtscGraphExpand.IProps,
|
|
41
|
+
): ITtscGraphExpand {
|
|
42
|
+
const wantSource = props.source === true;
|
|
43
|
+
const nodes: ITtscGraphExpand.INode[] = [];
|
|
44
|
+
const unknown: string[] = [];
|
|
45
|
+
for (const handle of props.handles) {
|
|
46
|
+
const resolved = resolveGraphHandle(graph, handle);
|
|
47
|
+
if (resolved.node === undefined) {
|
|
48
|
+
unknown.push(handle);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const node = resolved.node;
|
|
52
|
+
const expanded: ITtscGraphExpand.INode = {
|
|
53
|
+
id: node.id,
|
|
54
|
+
name: node.qualifiedName ?? node.name,
|
|
55
|
+
kind: node.kind,
|
|
56
|
+
file: node.file,
|
|
57
|
+
};
|
|
58
|
+
if (node.evidence?.startLine) expanded.line = node.evidence.startLine;
|
|
59
|
+
const sig = signatureOf(graph.project, node);
|
|
60
|
+
if (sig !== undefined) expanded.signature = sig;
|
|
61
|
+
if (CONTAINER_KINDS.has(node.kind)) {
|
|
62
|
+
const list = members(graph, node);
|
|
63
|
+
if (list.length > 0) expanded.members = list;
|
|
64
|
+
}
|
|
65
|
+
if (wantSource) {
|
|
66
|
+
const source = readSource(graph.project, node);
|
|
67
|
+
if (source !== undefined) {
|
|
68
|
+
expanded.source = source.text;
|
|
69
|
+
if (source.truncated) expanded.truncated = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (props.neighbors === true) {
|
|
73
|
+
expanded.dependsOn = refs(graph, graph.outgoing(node.id), "to");
|
|
74
|
+
expanded.dependedOnBy = refs(graph, graph.incoming(node.id), "from");
|
|
75
|
+
}
|
|
76
|
+
nodes.push(expanded);
|
|
77
|
+
}
|
|
78
|
+
return { nodes, unknown };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The members a container owns (via `contains`), each with its own signature. */
|
|
82
|
+
function members(
|
|
83
|
+
graph: TtscGraphMemory,
|
|
84
|
+
node: ITtscGraphNode,
|
|
85
|
+
): ITtscGraphExpand.IMember[] {
|
|
86
|
+
const out: ITtscGraphExpand.IMember[] = [];
|
|
87
|
+
for (const edge of graph.outgoing(node.id)) {
|
|
88
|
+
if (edge.kind !== "contains") continue;
|
|
89
|
+
const member = graph.node(edge.to);
|
|
90
|
+
if (member === undefined) continue;
|
|
91
|
+
const m: ITtscGraphExpand.IMember = {
|
|
92
|
+
name: member.qualifiedName ?? member.name,
|
|
93
|
+
kind: member.kind,
|
|
94
|
+
};
|
|
95
|
+
if (member.evidence?.startLine) m.line = member.evidence.startLine;
|
|
96
|
+
const sig = signatureOf(graph.project, member);
|
|
97
|
+
if (sig !== undefined) m.signature = sig;
|
|
98
|
+
out.push(m);
|
|
99
|
+
if (out.length >= MAX_MEMBERS) break;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Map dependency edges to references on their far endpoint, dropping structure. */
|
|
105
|
+
function refs(
|
|
106
|
+
graph: TtscGraphMemory,
|
|
107
|
+
edges: readonly ITtscGraphEdge[],
|
|
108
|
+
end: "to" | "from",
|
|
109
|
+
): ITtscGraphExpand.IReference[] {
|
|
110
|
+
const out: ITtscGraphExpand.IReference[] = [];
|
|
111
|
+
for (const edge of edges) {
|
|
112
|
+
if (STRUCTURAL_KINDS.has(edge.kind)) continue;
|
|
113
|
+
const other = graph.node(end === "to" ? edge.to : edge.from);
|
|
114
|
+
if (other === undefined) continue;
|
|
115
|
+
out.push({
|
|
116
|
+
id: other.id,
|
|
117
|
+
name: other.qualifiedName ?? other.name,
|
|
118
|
+
kind: other.kind,
|
|
119
|
+
relation: edge.kind,
|
|
120
|
+
});
|
|
121
|
+
if (out.length >= MAX_NEIGHBORS) break;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Read a file's lines once, or undefined when it cannot be read. */
|
|
127
|
+
function fileLines(
|
|
128
|
+
project: string,
|
|
129
|
+
node: ITtscGraphNode,
|
|
130
|
+
): string[] | undefined {
|
|
131
|
+
if (node.evidence === undefined || node.file === "") return undefined;
|
|
132
|
+
try {
|
|
133
|
+
return fs
|
|
134
|
+
.readFileSync(path.join(project, node.file), "utf8")
|
|
135
|
+
.split(/\r?\n/);
|
|
136
|
+
} catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The declaration signature: the head of the declaration up to and including
|
|
143
|
+
* the line that opens its body (`{`), or the single declaration line when there
|
|
144
|
+
* is no brace, capped so a wrapped signature cannot run away.
|
|
145
|
+
*/
|
|
146
|
+
export function signatureOf(
|
|
147
|
+
project: string,
|
|
148
|
+
node: ITtscGraphNode,
|
|
149
|
+
): string | undefined {
|
|
150
|
+
const lines = fileLines(project, node);
|
|
151
|
+
const evidence = node.evidence;
|
|
152
|
+
if (lines === undefined || evidence === undefined) return undefined;
|
|
153
|
+
const start = Math.max(0, evidence.startLine - 1);
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
for (
|
|
156
|
+
let i = start;
|
|
157
|
+
i < lines.length && out.length < MAX_SIGNATURE_LINES;
|
|
158
|
+
i++
|
|
159
|
+
) {
|
|
160
|
+
const line = lines[i];
|
|
161
|
+
if (line === undefined) break;
|
|
162
|
+
out.push(line);
|
|
163
|
+
if (line.includes("{")) break;
|
|
164
|
+
}
|
|
165
|
+
const text = out.join("\n").trim();
|
|
166
|
+
return text === "" ? undefined : text;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Slice a node's full declaration source from disk, capped at MAX_SOURCE_LINES. */
|
|
170
|
+
function readSource(
|
|
171
|
+
project: string,
|
|
172
|
+
node: ITtscGraphNode,
|
|
173
|
+
): { text: string; truncated: boolean } | undefined {
|
|
174
|
+
const lines = fileLines(project, node);
|
|
175
|
+
const evidence = node.evidence;
|
|
176
|
+
if (lines === undefined || evidence === undefined) return undefined;
|
|
177
|
+
const start = Math.max(0, evidence.startLine - 1);
|
|
178
|
+
const end = Math.min(evidence.endLine ?? evidence.startLine, lines.length);
|
|
179
|
+
let slice = lines.slice(start, Math.max(start + 1, end));
|
|
180
|
+
let truncated = false;
|
|
181
|
+
if (slice.length > MAX_SOURCE_LINES) {
|
|
182
|
+
slice = slice.slice(0, MAX_SOURCE_LINES);
|
|
183
|
+
truncated = true;
|
|
184
|
+
}
|
|
185
|
+
return { text: slice.join("\n"), truncated };
|
|
186
|
+
}
|