@ttsc/graph 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/TtscGraphApplication.js +5 -1
- package/lib/TtscGraphApplication.js.map +1 -1
- package/lib/bin.js +15 -3
- package/lib/bin.js.map +1 -1
- package/lib/index.d.ts +13 -2
- package/lib/index.js +25 -30
- package/lib/index.js.map +1 -1
- package/lib/launcherArgs.d.ts +24 -0
- package/lib/launcherArgs.js +136 -0
- package/lib/launcherArgs.js.map +1 -0
- package/lib/model/TtscGraphMemory.d.ts +9 -5
- package/lib/model/TtscGraphMemory.js +35 -58
- package/lib/model/TtscGraphMemory.js.map +1 -1
- package/lib/model/TtscGraphNodeId.d.ts +17 -0
- package/lib/model/TtscGraphNodeId.js +69 -0
- package/lib/model/TtscGraphNodeId.js.map +1 -0
- package/lib/model/TtscGraphSession.d.ts +23 -7
- package/lib/model/TtscGraphSession.js +423 -171
- package/lib/model/TtscGraphSession.js.map +1 -1
- package/lib/model/TtscGraphSourceReader.d.ts +27 -0
- package/lib/model/TtscGraphSourceReader.js +97 -0
- package/lib/model/TtscGraphSourceReader.js.map +1 -0
- package/lib/model/loadGraph.d.ts +14 -0
- package/lib/model/loadGraph.js +256 -143
- package/lib/model/loadGraph.js.map +1 -1
- package/lib/reduce.d.ts +4 -1
- package/lib/reduce.js +112 -29
- package/lib/reduce.js.map +1 -1
- package/lib/resolveGraphBinary.d.ts +9 -3
- package/lib/resolveGraphBinary.js +9 -3
- package/lib/resolveGraphBinary.js.map +1 -1
- package/lib/server/createServer.js +22 -11
- package/lib/server/createServer.js.map +1 -1
- package/lib/server/pathPolicy.d.ts +6 -0
- package/lib/server/pathPolicy.js +10 -1
- package/lib/server/pathPolicy.js.map +1 -1
- package/lib/server/resolveHandle.js +22 -18
- package/lib/server/resolveHandle.js.map +1 -1
- package/lib/server/resultAudit.d.ts +56 -14
- package/lib/server/resultAudit.js +90 -24
- package/lib/server/resultAudit.js.map +1 -1
- package/lib/server/resultNext.d.ts +5 -0
- package/lib/server/resultNext.js.map +1 -1
- package/lib/server/runDetails.d.ts +9 -2
- package/lib/server/runDetails.js +132 -157
- package/lib/server/runDetails.js.map +1 -1
- package/lib/server/runEntrypoints.js +1 -1
- package/lib/server/runEntrypoints.js.map +1 -1
- package/lib/server/runLookup.js +1 -1
- package/lib/server/runLookup.js.map +1 -1
- package/lib/server/runTour.js +113 -30
- package/lib/server/runTour.js.map +1 -1
- package/lib/server/runTrace.d.ts +22 -0
- package/lib/server/runTrace.js +293 -63
- package/lib/server/runTrace.js.map +1 -1
- package/lib/structures/ITtscGraphApplication.d.ts +8 -7
- package/lib/structures/ITtscGraphDetails.d.ts +18 -8
- package/lib/structures/ITtscGraphDump.d.ts +13 -13
- package/lib/structures/ITtscGraphNode.d.ts +71 -0
- package/lib/structures/ITtscGraphTour.d.ts +1 -1
- package/lib/structures/ITtscGraphTrace.d.ts +1 -1
- package/lib/view.js +18 -28
- package/lib/view.js.map +1 -1
- package/package.json +7 -4
- package/src/TtscGraphApplication.ts +8 -1
- package/src/bin.ts +13 -3
- package/src/index.ts +35 -27
- package/src/launcherArgs.ts +168 -0
- package/src/model/TtscGraphMemory.ts +34 -60
- package/src/model/TtscGraphNodeId.ts +77 -0
- package/src/model/TtscGraphSession.ts +253 -53
- package/src/model/TtscGraphSourceReader.ts +117 -0
- package/src/model/loadGraph.ts +6 -1
- package/src/reduce.ts +136 -31
- package/src/resolveGraphBinary.ts +9 -3
- package/src/server/createServer.ts +12 -2
- package/src/server/pathPolicy.ts +10 -1
- package/src/server/resolveHandle.ts +21 -22
- package/src/server/resultAudit.ts +93 -23
- package/src/server/resultNext.ts +6 -0
- package/src/server/runDetails.ts +134 -165
- package/src/server/runEntrypoints.ts +1 -1
- package/src/server/runLookup.ts +1 -1
- package/src/server/runTour.ts +133 -39
- package/src/server/runTrace.ts +401 -65
- package/src/structures/ITtscGraphApplication.ts +8 -7
- package/src/structures/ITtscGraphDetails.ts +18 -8
- package/src/structures/ITtscGraphDump.ts +13 -13
- package/src/structures/ITtscGraphNode.ts +80 -0
- package/src/structures/ITtscGraphTour.ts +1 -1
- package/src/structures/ITtscGraphTrace.ts +1 -1
- package/src/view.ts +25 -23
|
@@ -4,16 +4,19 @@ import { ITtscGraphEvidence } from "../structures/ITtscGraphEvidence";
|
|
|
4
4
|
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
5
5
|
import { ITtscGraphSpan } from "../structures/ITtscGraphSpan";
|
|
6
6
|
import { TtscGraphEdgeKind } from "../structures/TtscGraphEdgeKind";
|
|
7
|
+
import { ttscGraphNodeIdPath } from "./TtscGraphNodeId";
|
|
8
|
+
import { TtscGraphSourceReader } from "./TtscGraphSourceReader";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* The in-memory resident graph the MCP tools answer from.
|
|
10
12
|
*
|
|
11
13
|
* It loads one `ttscgraph dump` — the checker-resolved fact graph — then
|
|
12
14
|
* synthesizes the structural relationships the dump deliberately leaves to this
|
|
13
|
-
* layer: `file` container nodes
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* layer: `file` container nodes and the `contains` ownership tree, plus the
|
|
16
|
+
* refinement of a class-member `variable` to a `property`. Export and member
|
|
17
|
+
* implementation relationships are checker facts already present in the dump.
|
|
18
|
+
* Every tool call is then a lookup or traversal over the indexes built here;
|
|
19
|
+
* nothing recompiles.
|
|
17
20
|
*/
|
|
18
21
|
export class TtscGraphMemory {
|
|
19
22
|
private readonly byId: Map<string, ITtscGraphNode>;
|
|
@@ -26,17 +29,21 @@ export class TtscGraphMemory {
|
|
|
26
29
|
readonly project: string;
|
|
27
30
|
/** Every node, raw plus synthesized (file containers). */
|
|
28
31
|
readonly nodes: readonly ITtscGraphNode[];
|
|
29
|
-
/** Every edge, raw plus synthesized
|
|
32
|
+
/** Every edge, raw plus synthesized containment. */
|
|
30
33
|
readonly edges: readonly ITtscGraphEdge[];
|
|
34
|
+
/** Provenance-gated source display facts cached for this exact snapshot. */
|
|
35
|
+
readonly source: TtscGraphSourceReader;
|
|
31
36
|
|
|
32
37
|
private constructor(
|
|
33
38
|
project: string,
|
|
34
39
|
nodes: ITtscGraphNode[],
|
|
35
40
|
edges: ITtscGraphEdge[],
|
|
41
|
+
provenance: ITtscGraphDump.IProvenance,
|
|
36
42
|
) {
|
|
37
43
|
this.project = project;
|
|
38
44
|
this.nodes = nodes;
|
|
39
45
|
this.edges = edges;
|
|
46
|
+
this.source = new TtscGraphSourceReader(project, provenance);
|
|
40
47
|
|
|
41
48
|
this.byId = new Map(nodes.map((n) => [n.id, n]));
|
|
42
49
|
this.byNameIndex = new Map();
|
|
@@ -63,7 +70,7 @@ export class TtscGraphMemory {
|
|
|
63
70
|
/** Build a model from a parsed dump, synthesizing structural relationships. */
|
|
64
71
|
static from(dump: ITtscGraphDump): TtscGraphMemory {
|
|
65
72
|
const { nodes, edges } = synthesize(dump);
|
|
66
|
-
return new TtscGraphMemory(dump.project, nodes, edges);
|
|
73
|
+
return new TtscGraphMemory(dump.project, nodes, edges, dump.provenance);
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/** The node with this id, or undefined. */
|
|
@@ -113,13 +120,23 @@ function keyOf(node: ITtscGraphNode): string {
|
|
|
113
120
|
return node.qualifiedName ?? node.name;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
/**
|
|
124
|
+
* The owner key derived from facts the producer serialized separately.
|
|
125
|
+
*
|
|
126
|
+
* A quoted member named `"a.b"` has Name `a.b` and QualifiedName `Box.a.b`.
|
|
127
|
+
* Cutting the qualified name at its last dot invents owner `Box.a`; removing
|
|
128
|
+
* the exact `.${name}` suffix instead preserves the producer's real boundary.
|
|
129
|
+
*/
|
|
130
|
+
function ownerKey(node: ITtscGraphNode): string | undefined {
|
|
131
|
+
if (node.qualifiedName === undefined || node.qualifiedName === node.name)
|
|
132
|
+
return undefined;
|
|
133
|
+
const suffix = `.${node.name}`;
|
|
134
|
+
if (!node.qualifiedName.endsWith(suffix)) return undefined;
|
|
135
|
+
const owner = node.qualifiedName.slice(0, -suffix.length);
|
|
136
|
+
return owner === "" ? undefined : owner;
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
/** A file's id and node name from its
|
|
139
|
+
/** A file's id and node name from its schema-v6 path coordinate. */
|
|
123
140
|
function fileNodeId(file: string): string {
|
|
124
141
|
return file;
|
|
125
142
|
}
|
|
@@ -138,8 +155,7 @@ function spanIn(span: ITtscGraphSpan, file: string): ITtscGraphEvidence {
|
|
|
138
155
|
* file node's id is the path itself.
|
|
139
156
|
*/
|
|
140
157
|
function fileOfNodeId(id: string): string {
|
|
141
|
-
|
|
142
|
-
return hash === -1 ? id : id.slice(0, hash);
|
|
158
|
+
return ttscGraphNodeIdPath(id) ?? id;
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
function basename(file: string): string {
|
|
@@ -149,8 +165,8 @@ function basename(file: string): string {
|
|
|
149
165
|
|
|
150
166
|
/**
|
|
151
167
|
* Derive the structural layer from a dump's faithful facts: refine class-member
|
|
152
|
-
* variables to properties, add a `file` node per workspace source,
|
|
153
|
-
*
|
|
168
|
+
* variables to properties, add a `file` node per workspace source, connect the
|
|
169
|
+
* `contains` ownership tree, and re-anchor compiler-owned `exports` edges.
|
|
154
170
|
*/
|
|
155
171
|
function synthesize(dump: ITtscGraphDump): {
|
|
156
172
|
nodes: ITtscGraphNode[];
|
|
@@ -207,8 +223,8 @@ function synthesize(dump: ITtscGraphDump): {
|
|
|
207
223
|
if (!node.external) byFileKey.set(node.file + "\0" + keyOf(node), node);
|
|
208
224
|
}
|
|
209
225
|
const owner = (node: ITtscGraphNode): ITtscGraphNode | undefined => {
|
|
210
|
-
const parent = ownerKey(
|
|
211
|
-
if (parent ===
|
|
226
|
+
const parent = ownerKey(node);
|
|
227
|
+
if (parent === undefined) return undefined;
|
|
212
228
|
return byFileKey.get(node.file + "\0" + parent);
|
|
213
229
|
};
|
|
214
230
|
|
|
@@ -250,11 +266,9 @@ function synthesize(dump: ITtscGraphDump): {
|
|
|
250
266
|
// public, which is the fact that cannot tell a package's front door from its
|
|
251
267
|
// legacy subpath.
|
|
252
268
|
const structural: ITtscGraphEdge[] = [];
|
|
253
|
-
const membersByOwner = new Map<string, ITtscGraphNode[]>();
|
|
254
269
|
for (const node of nodes) {
|
|
255
270
|
if (node.external || node.file === "") continue;
|
|
256
271
|
const parent = owner(node);
|
|
257
|
-
if (parent !== undefined) push(membersByOwner, parent.id, node);
|
|
258
272
|
const container = parent ? parent.id : fileNodeId(node.file);
|
|
259
273
|
structural.push({
|
|
260
274
|
from: container,
|
|
@@ -263,48 +277,8 @@ function synthesize(dump: ITtscGraphDump): {
|
|
|
263
277
|
});
|
|
264
278
|
}
|
|
265
279
|
|
|
266
|
-
const synthesized = [...edges, ...structural];
|
|
267
|
-
const edgeKeys = new Set(
|
|
268
|
-
synthesized.map((edge) => `${edge.kind}\0${edge.from}\0${edge.to}`),
|
|
269
|
-
);
|
|
270
|
-
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
271
|
-
for (const edge of edges) {
|
|
272
|
-
const kind: TtscGraphEdgeKind | undefined =
|
|
273
|
-
edge.kind === "implements"
|
|
274
|
-
? "implements"
|
|
275
|
-
: edge.kind === "extends"
|
|
276
|
-
? "overrides"
|
|
277
|
-
: undefined;
|
|
278
|
-
if (kind === undefined) continue;
|
|
279
|
-
const derived = byId.get(edge.from);
|
|
280
|
-
const base = byId.get(edge.to);
|
|
281
|
-
if (derived === undefined || base === undefined) continue;
|
|
282
|
-
const derivedMembers = membersByOwner.get(derived.id) ?? [];
|
|
283
|
-
const baseMembers = membersByOwner.get(base.id) ?? [];
|
|
284
|
-
for (const baseMember of baseMembers) {
|
|
285
|
-
const derivedMember = derivedMembers.find(
|
|
286
|
-
(member) =>
|
|
287
|
-
member.name === baseMember.name &&
|
|
288
|
-
implementationMemberKinds.has(member.kind) &&
|
|
289
|
-
implementationMemberKinds.has(baseMember.kind),
|
|
290
|
-
);
|
|
291
|
-
if (derivedMember === undefined) continue;
|
|
292
|
-
const key = `${kind}\0${derivedMember.id}\0${baseMember.id}`;
|
|
293
|
-
if (edgeKeys.has(key)) continue;
|
|
294
|
-
edgeKeys.add(key);
|
|
295
|
-
synthesized.push({
|
|
296
|
-
from: derivedMember.id,
|
|
297
|
-
to: baseMember.id,
|
|
298
|
-
kind,
|
|
299
|
-
evidence: derivedMember.implementation ?? derivedMember.evidence,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
280
|
return {
|
|
305
281
|
nodes: [...nodes, ...fileNodes.values()],
|
|
306
|
-
edges:
|
|
282
|
+
edges: [...edges, ...structural],
|
|
307
283
|
};
|
|
308
284
|
}
|
|
309
|
-
|
|
310
|
-
const implementationMemberKinds = new Set(["method", "property"]);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** The structured components carried by a graph symbol id. */
|
|
2
|
+
export interface ITtscGraphNodeId {
|
|
3
|
+
path: string;
|
|
4
|
+
name: string;
|
|
5
|
+
kind?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Decode the graph's position-invariant `path#name:kind` identity.
|
|
10
|
+
*
|
|
11
|
+
* The producer quotes `#` and `\\` inside path and name. This reader also
|
|
12
|
+
* accepts pre-codec ordinary ids, so a current package can read an older dump.
|
|
13
|
+
*/
|
|
14
|
+
export function parseTtscGraphNodeId(id: string): ITtscGraphNodeId | undefined {
|
|
15
|
+
const hash = graphNodeIdHash(id);
|
|
16
|
+
if (hash < 0) return undefined;
|
|
17
|
+
const tail = id.slice(hash + 1);
|
|
18
|
+
if (tail === "") return undefined;
|
|
19
|
+
const colon = tail.lastIndexOf(":");
|
|
20
|
+
if (colon === 0 || colon === tail.length - 1) return undefined;
|
|
21
|
+
return {
|
|
22
|
+
path: unescapeGraphNodeIdPart(id.slice(0, hash)),
|
|
23
|
+
name: unescapeGraphNodeIdPart(colon < 0 ? tail : tail.slice(0, colon)),
|
|
24
|
+
...(colon < 0 ? {} : { kind: tail.slice(colon + 1) }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Encode a symbol identity without making its component boundaries ambiguous. */
|
|
29
|
+
export function writeTtscGraphNodeId(
|
|
30
|
+
path: string,
|
|
31
|
+
name: string,
|
|
32
|
+
kind: string,
|
|
33
|
+
): string {
|
|
34
|
+
return `${escapeGraphNodeIdPart(path)}#${escapeGraphNodeIdPart(name)}:${kind}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Return the raw path component when id is a symbol identity. */
|
|
38
|
+
export function ttscGraphNodeIdPath(id: string): string | undefined {
|
|
39
|
+
return parseTtscGraphNodeId(id)?.path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeGraphNodeIdPart(value: string): string {
|
|
43
|
+
return value.replaceAll("\\", "\\\\").replaceAll("#", "\\#");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function unescapeGraphNodeIdPart(value: string): string {
|
|
47
|
+
let result = "";
|
|
48
|
+
for (let index = 0; index < value.length; index++) {
|
|
49
|
+
const next = value[index + 1];
|
|
50
|
+
if (value[index] === "\\" && next !== undefined) {
|
|
51
|
+
if (next === "#" || (next === "\\" && !legacyUNCStart(value, index))) {
|
|
52
|
+
result += next;
|
|
53
|
+
index++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
result += value[index];
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function legacyUNCStart(value: string, index: number): boolean {
|
|
63
|
+
return (
|
|
64
|
+
index === 0 && value.length > 2 && value[2] !== "\\" && value[2] !== "#"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function graphNodeIdHash(id: string): number {
|
|
69
|
+
for (let index = 0; index < id.length; index++) {
|
|
70
|
+
if (id[index] !== "#") continue;
|
|
71
|
+
let slashes = 0;
|
|
72
|
+
for (let slash = index - 1; slash >= 0 && id[slash] === "\\"; slash--)
|
|
73
|
+
slashes++;
|
|
74
|
+
if (slashes % 2 === 0) return index;
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
@@ -6,6 +6,7 @@ import { ensureExecutable } from "../nativeExecutable";
|
|
|
6
6
|
import { resolveGraphBinary } from "../resolveGraphBinary";
|
|
7
7
|
import { ITtscGraphSnapshot } from "../structures/ITtscGraphSnapshot";
|
|
8
8
|
import { TtscGraphMemory } from "./TtscGraphMemory";
|
|
9
|
+
import { DUMP_SCHEMA_VERSION } from "./loadGraph";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The serve protocol version this client speaks.
|
|
@@ -16,10 +17,44 @@ import { TtscGraphMemory } from "./TtscGraphMemory";
|
|
|
16
17
|
* constant out of this file and fails if the pair drifts.
|
|
17
18
|
*/
|
|
18
19
|
const PROTOCOL_VERSION = 1;
|
|
20
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;
|
|
21
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
22
|
+
const TERMINATION_GRACE_MS = 1_000;
|
|
19
23
|
|
|
20
24
|
interface Pending {
|
|
25
|
+
child: NativeChild;
|
|
21
26
|
resolve: (response: ITtscGraphSnapshot) => void;
|
|
22
27
|
reject: (error: Error) => void;
|
|
28
|
+
timer: NodeJS.Timeout;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
abort?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface NativeChild {
|
|
34
|
+
process: ChildProcessWithoutNullStreams;
|
|
35
|
+
lines: readline.Interface;
|
|
36
|
+
stderr: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Construction options for a resident native graph session. */
|
|
40
|
+
export interface TtscGraphSessionOptions {
|
|
41
|
+
/** Project root passed to `ttscgraph serve`. */
|
|
42
|
+
cwd: string;
|
|
43
|
+
/** Project tsconfig passed to `ttscgraph serve`. */
|
|
44
|
+
tsconfig: string;
|
|
45
|
+
/** Absolute native binary path, resolved from `cwd` when omitted. */
|
|
46
|
+
binary?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Maximum time for one native snapshot response. Defaults to five minutes,
|
|
49
|
+
* which is more than ten times the published 28.7-second VS Code cold index.
|
|
50
|
+
*/
|
|
51
|
+
requestTimeoutMs?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Per-call controls for a native graph refresh. */
|
|
55
|
+
export interface TtscGraphRequestOptions {
|
|
56
|
+
/** Cancel this refresh and retire its native session. */
|
|
57
|
+
signal?: AbortSignal;
|
|
23
58
|
}
|
|
24
59
|
|
|
25
60
|
/**
|
|
@@ -34,19 +69,26 @@ export class TtscGraphSession {
|
|
|
34
69
|
private readonly cwd: string;
|
|
35
70
|
private readonly tsconfig: string;
|
|
36
71
|
private readonly binary: string;
|
|
37
|
-
private
|
|
38
|
-
private
|
|
72
|
+
private readonly requestTimeoutMs: number;
|
|
73
|
+
private child: NativeChild | undefined;
|
|
39
74
|
private nextId = 0;
|
|
40
75
|
private readonly pending = new Map<number, Pending>();
|
|
41
76
|
private queue: Promise<void> = Promise.resolve();
|
|
42
77
|
private current: TtscGraphMemory | undefined;
|
|
43
78
|
private closed = false;
|
|
44
79
|
|
|
45
|
-
public constructor(options: {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
public constructor(options: TtscGraphSessionOptions) {
|
|
81
|
+
const requestTimeoutMs =
|
|
82
|
+
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
83
|
+
if (
|
|
84
|
+
!Number.isSafeInteger(requestTimeoutMs) ||
|
|
85
|
+
requestTimeoutMs <= 0 ||
|
|
86
|
+
requestTimeoutMs > MAX_TIMER_MS
|
|
87
|
+
) {
|
|
88
|
+
throw new TypeError(
|
|
89
|
+
`@ttsc/graph: requestTimeoutMs must be an integer between 1 and ${String(MAX_TIMER_MS)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
50
92
|
// Resolve the platform binary from the project this session serves, so the
|
|
51
93
|
// MCP server started from an unrelated directory still finds the target's
|
|
52
94
|
// installed `ttsc`.
|
|
@@ -63,21 +105,48 @@ export class TtscGraphSession {
|
|
|
63
105
|
this.cwd = options.cwd;
|
|
64
106
|
this.tsconfig = options.tsconfig;
|
|
65
107
|
this.binary = binary;
|
|
108
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
66
109
|
}
|
|
67
110
|
|
|
68
111
|
/** Return a graph for the current disk snapshot, serialized per tool call. */
|
|
69
|
-
public graph(
|
|
112
|
+
public graph(
|
|
113
|
+
options: TtscGraphRequestOptions = {},
|
|
114
|
+
): Promise<TtscGraphMemory> {
|
|
115
|
+
if (this.closed) {
|
|
116
|
+
return Promise.reject(new Error("@ttsc/graph: native session is closed"));
|
|
117
|
+
}
|
|
70
118
|
let resolve!: (graph: TtscGraphMemory) => void;
|
|
71
119
|
let reject!: (error: Error) => void;
|
|
120
|
+
let started = false;
|
|
121
|
+
let settled = false;
|
|
72
122
|
const result = new Promise<TtscGraphMemory>((res, rej) => {
|
|
73
|
-
resolve =
|
|
74
|
-
|
|
123
|
+
resolve = (graph) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
126
|
+
res(graph);
|
|
127
|
+
};
|
|
128
|
+
reject = (error) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
rej(error);
|
|
132
|
+
};
|
|
75
133
|
});
|
|
134
|
+
const cancelQueued = () => {
|
|
135
|
+
if (!started) reject(cancelledError(options.signal));
|
|
136
|
+
};
|
|
137
|
+
if (options.signal?.aborted) {
|
|
138
|
+
reject(cancelledError(options.signal));
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
options.signal?.addEventListener("abort", cancelQueued, { once: true });
|
|
76
142
|
this.queue = this.queue
|
|
77
143
|
.catch(() => undefined)
|
|
78
144
|
.then(async () => {
|
|
145
|
+
started = true;
|
|
146
|
+
options.signal?.removeEventListener("abort", cancelQueued);
|
|
147
|
+
if (settled) return;
|
|
79
148
|
try {
|
|
80
|
-
resolve(await this.refresh());
|
|
149
|
+
resolve(await this.refresh(options.signal));
|
|
81
150
|
} catch (error) {
|
|
82
151
|
reject(asError(error));
|
|
83
152
|
}
|
|
@@ -87,17 +156,17 @@ export class TtscGraphSession {
|
|
|
87
156
|
|
|
88
157
|
/** Close the native session. Safe to call more than once. */
|
|
89
158
|
public close(): void {
|
|
159
|
+
if (this.closed) return;
|
|
90
160
|
this.closed = true;
|
|
91
|
-
const
|
|
92
|
-
this.child
|
|
93
|
-
|
|
94
|
-
this.failPending(new Error("@ttsc/graph: native session closed"));
|
|
161
|
+
const error = new Error("@ttsc/graph: native session closed");
|
|
162
|
+
if (this.child !== undefined) this.failChild(this.child, error);
|
|
163
|
+
else this.failPending(error);
|
|
95
164
|
}
|
|
96
165
|
|
|
97
|
-
private async refresh(): Promise<TtscGraphMemory> {
|
|
166
|
+
private async refresh(signal?: AbortSignal): Promise<TtscGraphMemory> {
|
|
98
167
|
// The protocol version and the envelope shape were both settled in onLine,
|
|
99
168
|
// before this frame was ever routed here.
|
|
100
|
-
const response = await this.request();
|
|
169
|
+
const response = await this.request(signal);
|
|
101
170
|
if (response.error !== undefined) {
|
|
102
171
|
throw new Error(`@ttsc/graph: ${response.error}`);
|
|
103
172
|
}
|
|
@@ -117,15 +186,41 @@ export class TtscGraphSession {
|
|
|
117
186
|
return this.current;
|
|
118
187
|
}
|
|
119
188
|
|
|
120
|
-
private request(): Promise<ITtscGraphSnapshot> {
|
|
189
|
+
private request(signal?: AbortSignal): Promise<ITtscGraphSnapshot> {
|
|
190
|
+
if (signal?.aborted) throw cancelledError(signal);
|
|
121
191
|
const child = this.ensureChild();
|
|
122
192
|
const id = ++this.nextId;
|
|
123
193
|
return new Promise<ITtscGraphSnapshot>((resolve, reject) => {
|
|
124
|
-
|
|
125
|
-
|
|
194
|
+
const pending: Pending = {
|
|
195
|
+
child,
|
|
196
|
+
resolve,
|
|
197
|
+
reject,
|
|
198
|
+
timer: setTimeout(() => {
|
|
199
|
+
this.failChild(
|
|
200
|
+
child,
|
|
201
|
+
new Error(
|
|
202
|
+
`@ttsc/graph: native snapshot request timed out after ${String(this.requestTimeoutMs)} ms${stderrSuffix(child)}`,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}, this.requestTimeoutMs),
|
|
206
|
+
signal,
|
|
207
|
+
};
|
|
208
|
+
pending.timer.unref();
|
|
209
|
+
if (signal !== undefined) {
|
|
210
|
+
pending.abort = () =>
|
|
211
|
+
this.failChild(child, cancelledError(signal, child));
|
|
212
|
+
signal.addEventListener("abort", pending.abort, { once: true });
|
|
213
|
+
}
|
|
214
|
+
this.pending.set(id, pending);
|
|
215
|
+
if (signal?.aborted) {
|
|
216
|
+
pending.abort!();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
child.process.stdin.write(`${JSON.stringify({ id })}\n`, (error) => {
|
|
126
220
|
if (error === null || error === undefined) return;
|
|
127
|
-
this.pending.
|
|
128
|
-
|
|
221
|
+
if (this.pending.get(id) !== pending) return;
|
|
222
|
+
this.failChild(
|
|
223
|
+
child,
|
|
129
224
|
new Error(
|
|
130
225
|
`@ttsc/graph: could not request native snapshot: ${error.message}`,
|
|
131
226
|
),
|
|
@@ -134,49 +229,61 @@ export class TtscGraphSession {
|
|
|
134
229
|
});
|
|
135
230
|
}
|
|
136
231
|
|
|
137
|
-
private ensureChild():
|
|
232
|
+
private ensureChild(): NativeChild {
|
|
138
233
|
if (this.closed) {
|
|
139
234
|
// A request queued behind the close must not respawn the native
|
|
140
235
|
// process; an orphaned resident compiler would outlive the MCP server.
|
|
141
236
|
throw new Error("@ttsc/graph: native session is closed");
|
|
142
237
|
}
|
|
143
|
-
if (
|
|
238
|
+
if (
|
|
239
|
+
this.child !== undefined &&
|
|
240
|
+
this.child.process.exitCode === null &&
|
|
241
|
+
this.child.process.signalCode === null
|
|
242
|
+
) {
|
|
144
243
|
return this.child;
|
|
145
244
|
}
|
|
146
|
-
|
|
147
|
-
const child = spawn(
|
|
245
|
+
const process = spawn(
|
|
148
246
|
this.binary,
|
|
149
247
|
["serve", "--cwd", this.cwd, "--tsconfig", this.tsconfig],
|
|
150
248
|
{ stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
151
249
|
);
|
|
250
|
+
const lines = readline.createInterface({ input: process.stdout });
|
|
251
|
+
const child: NativeChild = { process, lines, stderr: "" };
|
|
152
252
|
this.child = child;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
253
|
+
process.stderr.setEncoding("utf8");
|
|
254
|
+
process.stderr.on("data", (chunk: string) => {
|
|
255
|
+
child.stderr = (child.stderr + chunk).slice(-64 * 1024);
|
|
156
256
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
257
|
+
lines.on("line", (line) => this.onLine(child, line));
|
|
258
|
+
process.on("error", (error) =>
|
|
259
|
+
this.failChild(
|
|
260
|
+
child,
|
|
261
|
+
new Error(`@ttsc/graph: native session failed: ${error.message}`),
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
process.on("exit", (code, signal) => {
|
|
161
265
|
if (this.child !== child) return;
|
|
162
|
-
this.
|
|
163
|
-
|
|
266
|
+
this.failChild(
|
|
267
|
+
child,
|
|
164
268
|
new Error(
|
|
165
|
-
`@ttsc/graph: native session exited (code=${String(code)}, signal=${String(signal)})${
|
|
166
|
-
|
|
167
|
-
}`,
|
|
269
|
+
`@ttsc/graph: native session exited (code=${String(code)}, signal=${String(signal)})${stderrSuffix(
|
|
270
|
+
child,
|
|
271
|
+
)}`,
|
|
168
272
|
),
|
|
273
|
+
false,
|
|
169
274
|
);
|
|
170
275
|
});
|
|
171
276
|
return child;
|
|
172
277
|
}
|
|
173
278
|
|
|
174
|
-
private onLine(line: string): void {
|
|
279
|
+
private onLine(child: NativeChild, line: string): void {
|
|
280
|
+
if (this.child !== child) return;
|
|
175
281
|
let parsed: unknown;
|
|
176
282
|
try {
|
|
177
283
|
parsed = JSON.parse(line);
|
|
178
284
|
} catch (error) {
|
|
179
|
-
this.
|
|
285
|
+
this.failChild(
|
|
286
|
+
child,
|
|
180
287
|
new Error(
|
|
181
288
|
`@ttsc/graph: native session returned invalid JSON: ${asError(error).message}`,
|
|
182
289
|
),
|
|
@@ -198,7 +305,8 @@ export class TtscGraphSession {
|
|
|
198
305
|
if (version !== PROTOCOL_VERSION) {
|
|
199
306
|
// Session-wide: a version mismatch is not one bad frame, it is the wrong
|
|
200
307
|
// binary, and every request against it is equally doomed.
|
|
201
|
-
this.
|
|
308
|
+
this.failChild(
|
|
309
|
+
child,
|
|
202
310
|
new Error(
|
|
203
311
|
`@ttsc/graph: ttscgraph speaks serve protocol ${
|
|
204
312
|
version === undefined ? "an unknown version" : `v${String(version)}`
|
|
@@ -219,30 +327,122 @@ export class TtscGraphSession {
|
|
|
219
327
|
// envelope belongs on this side of that line.
|
|
220
328
|
response = typia.assert<ITtscGraphSnapshot>(parsed);
|
|
221
329
|
} catch (error) {
|
|
222
|
-
this.
|
|
330
|
+
this.failChild(
|
|
331
|
+
child,
|
|
223
332
|
new Error(
|
|
224
333
|
`@ttsc/graph: native session returned an unreadable response: ${asError(error).message}`,
|
|
225
334
|
),
|
|
226
335
|
);
|
|
227
336
|
return;
|
|
228
337
|
}
|
|
338
|
+
// The envelope's version is not the body's, and only the envelope has been
|
|
339
|
+
// held to one so far. A producer can speak this protocol and still carry a
|
|
340
|
+
// dump from another schema — the two move apart the moment a node field is
|
|
341
|
+
// added without the frame around it changing — and then the facts that field
|
|
342
|
+
// holds are silently absent rather than refused. `literals` is exactly that
|
|
343
|
+
// shape: an older producer resolves no value set, so a union comes back
|
|
344
|
+
// looking like a type with no members. Hold the body to its own number too,
|
|
345
|
+
// once the frame is understood.
|
|
346
|
+
if (
|
|
347
|
+
response.dump !== undefined &&
|
|
348
|
+
response.dump.provenance.schemaVersion !== DUMP_SCHEMA_VERSION
|
|
349
|
+
) {
|
|
350
|
+
// Session-wide, for the same reason the protocol mismatch above is: it is
|
|
351
|
+
// the wrong binary, not one bad frame.
|
|
352
|
+
this.failChild(
|
|
353
|
+
child,
|
|
354
|
+
new Error(
|
|
355
|
+
`@ttsc/graph: ttscgraph sends dump schema v${String(
|
|
356
|
+
response.dump.provenance.schemaVersion,
|
|
357
|
+
)}, this client reads v${String(DUMP_SCHEMA_VERSION)}. ` +
|
|
358
|
+
"Install a matching `ttsc` (the binary resolves from the target " +
|
|
359
|
+
"project, or from TTSC_GRAPH_BINARY).",
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
229
365
|
const pending = this.pending.get(response.id);
|
|
230
|
-
if (pending === undefined) return;
|
|
231
|
-
this.
|
|
232
|
-
pending.resolve(response);
|
|
366
|
+
if (pending === undefined || pending.child !== child) return;
|
|
367
|
+
this.settlePending(response.id, pending, response);
|
|
233
368
|
}
|
|
234
369
|
|
|
235
|
-
private failChild(child:
|
|
236
|
-
if (this.child
|
|
237
|
-
this.
|
|
238
|
-
|
|
239
|
-
);
|
|
370
|
+
private failChild(child: NativeChild, error: Error, terminate = true): void {
|
|
371
|
+
if (this.child !== child) return;
|
|
372
|
+
this.child = undefined;
|
|
373
|
+
this.current = undefined;
|
|
374
|
+
child.lines.close();
|
|
375
|
+
this.failPending(error, child);
|
|
376
|
+
if (terminate) terminateChild(child.process);
|
|
240
377
|
}
|
|
241
378
|
|
|
242
|
-
private failPending(error: Error): void {
|
|
243
|
-
for (const pending of this.pending
|
|
244
|
-
|
|
379
|
+
private failPending(error: Error, child?: NativeChild): void {
|
|
380
|
+
for (const [id, pending] of this.pending) {
|
|
381
|
+
if (child === undefined || pending.child === child) {
|
|
382
|
+
this.settlePending(id, pending, error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
245
385
|
}
|
|
386
|
+
|
|
387
|
+
private settlePending(
|
|
388
|
+
id: number,
|
|
389
|
+
pending: Pending,
|
|
390
|
+
result: ITtscGraphSnapshot | Error,
|
|
391
|
+
): void {
|
|
392
|
+
if (this.pending.get(id) !== pending) return;
|
|
393
|
+
this.pending.delete(id);
|
|
394
|
+
clearTimeout(pending.timer);
|
|
395
|
+
if (pending.signal !== undefined && pending.abort !== undefined) {
|
|
396
|
+
pending.signal.removeEventListener("abort", pending.abort);
|
|
397
|
+
}
|
|
398
|
+
if (result instanceof Error) pending.reject(result);
|
|
399
|
+
else pending.resolve(result);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function terminateChild(child: ChildProcessWithoutNullStreams): void {
|
|
404
|
+
if (!child.stdin.destroyed) child.stdin.destroy();
|
|
405
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
406
|
+
try {
|
|
407
|
+
child.kill();
|
|
408
|
+
} catch {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const force = setTimeout(() => {
|
|
412
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
413
|
+
try {
|
|
414
|
+
child.kill("SIGKILL");
|
|
415
|
+
} catch {
|
|
416
|
+
// The process exited between the liveness check and the signal.
|
|
417
|
+
}
|
|
418
|
+
}, TERMINATION_GRACE_MS);
|
|
419
|
+
force.unref();
|
|
420
|
+
child.once("exit", () => clearTimeout(force));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function cancelledError(signal?: AbortSignal, child?: NativeChild): Error {
|
|
424
|
+
const error = new Error(
|
|
425
|
+
`@ttsc/graph: native snapshot request cancelled${abortDetail(signal)}${
|
|
426
|
+
child === undefined ? "" : stderrSuffix(child)
|
|
427
|
+
}`,
|
|
428
|
+
);
|
|
429
|
+
error.name = "AbortError";
|
|
430
|
+
return error;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function abortDetail(signal?: AbortSignal): string {
|
|
434
|
+
const reason = signal?.reason;
|
|
435
|
+
if (reason === undefined) return "";
|
|
436
|
+
try {
|
|
437
|
+
return `: ${reason instanceof Error ? reason.message : String(reason)}`;
|
|
438
|
+
} catch {
|
|
439
|
+
return "";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function stderrSuffix(child: NativeChild): string {
|
|
444
|
+
const stderr = child.stderr.trim();
|
|
445
|
+
return stderr === "" ? "" : `: ${stderr}`;
|
|
246
446
|
}
|
|
247
447
|
|
|
248
448
|
function asError(error: unknown): Error {
|