@ttsc/graph 0.19.3 → 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 +3 -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 +24 -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 +402 -210
- 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 +4 -4
- package/lib/model/loadGraph.js +233 -168
- 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/server/createServer.js +17 -6
- 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 +41 -20
- package/lib/server/resultAudit.js +62 -34
- 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 +2 -2
- package/lib/server/runDetails.js +54 -108
- 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/ITtscGraphDump.d.ts +13 -13
- package/lib/structures/ITtscGraphNode.d.ts +34 -0
- package/lib/structures/ITtscGraphTrace.d.ts +1 -1
- package/lib/view.js +18 -28
- package/lib/view.js.map +1 -1
- package/package.json +5 -5
- package/src/TtscGraphApplication.ts +5 -1
- package/src/bin.ts +13 -3
- package/src/index.ts +34 -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 +228 -55
- package/src/model/TtscGraphSourceReader.ts +117 -0
- package/src/model/loadGraph.ts +4 -4
- package/src/reduce.ts +136 -31
- 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 +64 -33
- package/src/server/resultNext.ts +6 -0
- package/src/server/runDetails.ts +57 -113
- 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/ITtscGraphDump.ts +13 -13
- package/src/structures/ITtscGraphNode.ts +40 -0
- 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
|
+
}
|
|
@@ -5,8 +5,8 @@ import typia from "typia";
|
|
|
5
5
|
import { ensureExecutable } from "../nativeExecutable";
|
|
6
6
|
import { resolveGraphBinary } from "../resolveGraphBinary";
|
|
7
7
|
import { ITtscGraphSnapshot } from "../structures/ITtscGraphSnapshot";
|
|
8
|
-
import { DUMP_SCHEMA_VERSION } from "./loadGraph";
|
|
9
8
|
import { TtscGraphMemory } from "./TtscGraphMemory";
|
|
9
|
+
import { DUMP_SCHEMA_VERSION } from "./loadGraph";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The serve protocol version this client speaks.
|
|
@@ -17,10 +17,44 @@ import { TtscGraphMemory } from "./TtscGraphMemory";
|
|
|
17
17
|
* constant out of this file and fails if the pair drifts.
|
|
18
18
|
*/
|
|
19
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;
|
|
20
23
|
|
|
21
24
|
interface Pending {
|
|
25
|
+
child: NativeChild;
|
|
22
26
|
resolve: (response: ITtscGraphSnapshot) => void;
|
|
23
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;
|
|
24
58
|
}
|
|
25
59
|
|
|
26
60
|
/**
|
|
@@ -35,19 +69,26 @@ export class TtscGraphSession {
|
|
|
35
69
|
private readonly cwd: string;
|
|
36
70
|
private readonly tsconfig: string;
|
|
37
71
|
private readonly binary: string;
|
|
38
|
-
private
|
|
39
|
-
private
|
|
72
|
+
private readonly requestTimeoutMs: number;
|
|
73
|
+
private child: NativeChild | undefined;
|
|
40
74
|
private nextId = 0;
|
|
41
75
|
private readonly pending = new Map<number, Pending>();
|
|
42
76
|
private queue: Promise<void> = Promise.resolve();
|
|
43
77
|
private current: TtscGraphMemory | undefined;
|
|
44
78
|
private closed = false;
|
|
45
79
|
|
|
46
|
-
public constructor(options: {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
92
|
// Resolve the platform binary from the project this session serves, so the
|
|
52
93
|
// MCP server started from an unrelated directory still finds the target's
|
|
53
94
|
// installed `ttsc`.
|
|
@@ -64,21 +105,48 @@ export class TtscGraphSession {
|
|
|
64
105
|
this.cwd = options.cwd;
|
|
65
106
|
this.tsconfig = options.tsconfig;
|
|
66
107
|
this.binary = binary;
|
|
108
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
67
109
|
}
|
|
68
110
|
|
|
69
111
|
/** Return a graph for the current disk snapshot, serialized per tool call. */
|
|
70
|
-
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
|
+
}
|
|
71
118
|
let resolve!: (graph: TtscGraphMemory) => void;
|
|
72
119
|
let reject!: (error: Error) => void;
|
|
120
|
+
let started = false;
|
|
121
|
+
let settled = false;
|
|
73
122
|
const result = new Promise<TtscGraphMemory>((res, rej) => {
|
|
74
|
-
resolve =
|
|
75
|
-
|
|
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
|
+
};
|
|
76
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 });
|
|
77
142
|
this.queue = this.queue
|
|
78
143
|
.catch(() => undefined)
|
|
79
144
|
.then(async () => {
|
|
145
|
+
started = true;
|
|
146
|
+
options.signal?.removeEventListener("abort", cancelQueued);
|
|
147
|
+
if (settled) return;
|
|
80
148
|
try {
|
|
81
|
-
resolve(await this.refresh());
|
|
149
|
+
resolve(await this.refresh(options.signal));
|
|
82
150
|
} catch (error) {
|
|
83
151
|
reject(asError(error));
|
|
84
152
|
}
|
|
@@ -88,17 +156,17 @@ export class TtscGraphSession {
|
|
|
88
156
|
|
|
89
157
|
/** Close the native session. Safe to call more than once. */
|
|
90
158
|
public close(): void {
|
|
159
|
+
if (this.closed) return;
|
|
91
160
|
this.closed = true;
|
|
92
|
-
const
|
|
93
|
-
this.child
|
|
94
|
-
|
|
95
|
-
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);
|
|
96
164
|
}
|
|
97
165
|
|
|
98
|
-
private async refresh(): Promise<TtscGraphMemory> {
|
|
166
|
+
private async refresh(signal?: AbortSignal): Promise<TtscGraphMemory> {
|
|
99
167
|
// The protocol version and the envelope shape were both settled in onLine,
|
|
100
168
|
// before this frame was ever routed here.
|
|
101
|
-
const response = await this.request();
|
|
169
|
+
const response = await this.request(signal);
|
|
102
170
|
if (response.error !== undefined) {
|
|
103
171
|
throw new Error(`@ttsc/graph: ${response.error}`);
|
|
104
172
|
}
|
|
@@ -118,15 +186,41 @@ export class TtscGraphSession {
|
|
|
118
186
|
return this.current;
|
|
119
187
|
}
|
|
120
188
|
|
|
121
|
-
private request(): Promise<ITtscGraphSnapshot> {
|
|
189
|
+
private request(signal?: AbortSignal): Promise<ITtscGraphSnapshot> {
|
|
190
|
+
if (signal?.aborted) throw cancelledError(signal);
|
|
122
191
|
const child = this.ensureChild();
|
|
123
192
|
const id = ++this.nextId;
|
|
124
193
|
return new Promise<ITtscGraphSnapshot>((resolve, reject) => {
|
|
125
|
-
|
|
126
|
-
|
|
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) => {
|
|
127
220
|
if (error === null || error === undefined) return;
|
|
128
|
-
this.pending.
|
|
129
|
-
|
|
221
|
+
if (this.pending.get(id) !== pending) return;
|
|
222
|
+
this.failChild(
|
|
223
|
+
child,
|
|
130
224
|
new Error(
|
|
131
225
|
`@ttsc/graph: could not request native snapshot: ${error.message}`,
|
|
132
226
|
),
|
|
@@ -135,49 +229,61 @@ export class TtscGraphSession {
|
|
|
135
229
|
});
|
|
136
230
|
}
|
|
137
231
|
|
|
138
|
-
private ensureChild():
|
|
232
|
+
private ensureChild(): NativeChild {
|
|
139
233
|
if (this.closed) {
|
|
140
234
|
// A request queued behind the close must not respawn the native
|
|
141
235
|
// process; an orphaned resident compiler would outlive the MCP server.
|
|
142
236
|
throw new Error("@ttsc/graph: native session is closed");
|
|
143
237
|
}
|
|
144
|
-
if (
|
|
238
|
+
if (
|
|
239
|
+
this.child !== undefined &&
|
|
240
|
+
this.child.process.exitCode === null &&
|
|
241
|
+
this.child.process.signalCode === null
|
|
242
|
+
) {
|
|
145
243
|
return this.child;
|
|
146
244
|
}
|
|
147
|
-
|
|
148
|
-
const child = spawn(
|
|
245
|
+
const process = spawn(
|
|
149
246
|
this.binary,
|
|
150
247
|
["serve", "--cwd", this.cwd, "--tsconfig", this.tsconfig],
|
|
151
248
|
{ stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
152
249
|
);
|
|
250
|
+
const lines = readline.createInterface({ input: process.stdout });
|
|
251
|
+
const child: NativeChild = { process, lines, stderr: "" };
|
|
153
252
|
this.child = child;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
253
|
+
process.stderr.setEncoding("utf8");
|
|
254
|
+
process.stderr.on("data", (chunk: string) => {
|
|
255
|
+
child.stderr = (child.stderr + chunk).slice(-64 * 1024);
|
|
157
256
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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) => {
|
|
162
265
|
if (this.child !== child) return;
|
|
163
|
-
this.
|
|
164
|
-
|
|
266
|
+
this.failChild(
|
|
267
|
+
child,
|
|
165
268
|
new Error(
|
|
166
|
-
`@ttsc/graph: native session exited (code=${String(code)}, signal=${String(signal)})${
|
|
167
|
-
|
|
168
|
-
}`,
|
|
269
|
+
`@ttsc/graph: native session exited (code=${String(code)}, signal=${String(signal)})${stderrSuffix(
|
|
270
|
+
child,
|
|
271
|
+
)}`,
|
|
169
272
|
),
|
|
273
|
+
false,
|
|
170
274
|
);
|
|
171
275
|
});
|
|
172
276
|
return child;
|
|
173
277
|
}
|
|
174
278
|
|
|
175
|
-
private onLine(line: string): void {
|
|
279
|
+
private onLine(child: NativeChild, line: string): void {
|
|
280
|
+
if (this.child !== child) return;
|
|
176
281
|
let parsed: unknown;
|
|
177
282
|
try {
|
|
178
283
|
parsed = JSON.parse(line);
|
|
179
284
|
} catch (error) {
|
|
180
|
-
this.
|
|
285
|
+
this.failChild(
|
|
286
|
+
child,
|
|
181
287
|
new Error(
|
|
182
288
|
`@ttsc/graph: native session returned invalid JSON: ${asError(error).message}`,
|
|
183
289
|
),
|
|
@@ -199,7 +305,8 @@ export class TtscGraphSession {
|
|
|
199
305
|
if (version !== PROTOCOL_VERSION) {
|
|
200
306
|
// Session-wide: a version mismatch is not one bad frame, it is the wrong
|
|
201
307
|
// binary, and every request against it is equally doomed.
|
|
202
|
-
this.
|
|
308
|
+
this.failChild(
|
|
309
|
+
child,
|
|
203
310
|
new Error(
|
|
204
311
|
`@ttsc/graph: ttscgraph speaks serve protocol ${
|
|
205
312
|
version === undefined ? "an unknown version" : `v${String(version)}`
|
|
@@ -220,7 +327,8 @@ export class TtscGraphSession {
|
|
|
220
327
|
// envelope belongs on this side of that line.
|
|
221
328
|
response = typia.assert<ITtscGraphSnapshot>(parsed);
|
|
222
329
|
} catch (error) {
|
|
223
|
-
this.
|
|
330
|
+
this.failChild(
|
|
331
|
+
child,
|
|
224
332
|
new Error(
|
|
225
333
|
`@ttsc/graph: native session returned an unreadable response: ${asError(error).message}`,
|
|
226
334
|
),
|
|
@@ -241,7 +349,8 @@ export class TtscGraphSession {
|
|
|
241
349
|
) {
|
|
242
350
|
// Session-wide, for the same reason the protocol mismatch above is: it is
|
|
243
351
|
// the wrong binary, not one bad frame.
|
|
244
|
-
this.
|
|
352
|
+
this.failChild(
|
|
353
|
+
child,
|
|
245
354
|
new Error(
|
|
246
355
|
`@ttsc/graph: ttscgraph sends dump schema v${String(
|
|
247
356
|
response.dump.provenance.schemaVersion,
|
|
@@ -254,24 +363,88 @@ export class TtscGraphSession {
|
|
|
254
363
|
}
|
|
255
364
|
|
|
256
365
|
const pending = this.pending.get(response.id);
|
|
257
|
-
if (pending === undefined) return;
|
|
258
|
-
this.
|
|
259
|
-
pending.resolve(response);
|
|
366
|
+
if (pending === undefined || pending.child !== child) return;
|
|
367
|
+
this.settlePending(response.id, pending, response);
|
|
260
368
|
}
|
|
261
369
|
|
|
262
|
-
private failChild(child:
|
|
263
|
-
if (this.child
|
|
264
|
-
this.
|
|
265
|
-
|
|
266
|
-
);
|
|
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);
|
|
377
|
+
}
|
|
378
|
+
|
|
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
|
+
}
|
|
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);
|
|
267
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
|
+
}
|
|
268
432
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 "";
|
|
272
440
|
}
|
|
273
441
|
}
|
|
274
442
|
|
|
443
|
+
function stderrSuffix(child: NativeChild): string {
|
|
444
|
+
const stderr = child.stderr.trim();
|
|
445
|
+
return stderr === "" ? "" : `: ${stderr}`;
|
|
446
|
+
}
|
|
447
|
+
|
|
275
448
|
function asError(error: unknown): Error {
|
|
276
449
|
return error instanceof Error ? error : new Error(String(error));
|
|
277
450
|
}
|