@ttsc/graph 0.19.3 → 0.20.1
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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { type ITtscGraphDump } from "../structures/ITtscGraphDump";
|
|
6
|
+
|
|
7
|
+
type ReadFile = (file: string) => Buffer;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Immutable, provenance-gated source lines owned by one graph snapshot.
|
|
11
|
+
*
|
|
12
|
+
* Signatures and declaration docs are display facts derived from source text,
|
|
13
|
+
* but the live disk is not the snapshot the checker resolved. A file becomes
|
|
14
|
+
* readable here only after its current bytes hash to `diskDigest` and the
|
|
15
|
+
* compiler-decoded text hashes to `checkerDigest`. Both success and failure are
|
|
16
|
+
* cached, so every consumer of a `TtscGraphMemory` sees one adjudication and
|
|
17
|
+
* one immutable line array.
|
|
18
|
+
*/
|
|
19
|
+
export class TtscGraphSourceReader {
|
|
20
|
+
private readonly project: string;
|
|
21
|
+
private readonly digests: ReadonlyMap<
|
|
22
|
+
string,
|
|
23
|
+
{ checker: string; disk: string }
|
|
24
|
+
>;
|
|
25
|
+
private readonly read: ReadFile;
|
|
26
|
+
private readonly cache = new Map<string, readonly string[] | undefined>();
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
project: string,
|
|
30
|
+
provenance:
|
|
31
|
+
| Pick<ITtscGraphDump.IProvenance, "capabilities" | "sources">
|
|
32
|
+
| undefined,
|
|
33
|
+
read: ReadFile = (file) => fs.readFileSync(file),
|
|
34
|
+
) {
|
|
35
|
+
this.project = project;
|
|
36
|
+
this.read = read;
|
|
37
|
+
this.digests = new Map(
|
|
38
|
+
provenance?.capabilities.includes("sourceDigests") === true &&
|
|
39
|
+
provenance.capabilities.includes("diskDigests") === true
|
|
40
|
+
? provenance.sources.map((source) => [
|
|
41
|
+
normalize(source.file),
|
|
42
|
+
{ checker: source.checkerDigest, disk: source.diskDigest },
|
|
43
|
+
])
|
|
44
|
+
: [],
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Return frozen lines only when raw disk bytes and compiler-decoded text both
|
|
50
|
+
* equal the checker snapshot. A missing manifest entry, read failure, or
|
|
51
|
+
* digest mismatch is a cached absence rather than a reason to mix current
|
|
52
|
+
* disk text into old graph facts.
|
|
53
|
+
*/
|
|
54
|
+
lines(file: string): readonly string[] | undefined {
|
|
55
|
+
const key = normalize(file);
|
|
56
|
+
if (this.cache.has(key)) return this.cache.get(key);
|
|
57
|
+
|
|
58
|
+
const expected = this.digests.get(key);
|
|
59
|
+
if (
|
|
60
|
+
expected === undefined ||
|
|
61
|
+
expected.checker === "" ||
|
|
62
|
+
expected.disk === ""
|
|
63
|
+
) {
|
|
64
|
+
this.cache.set(key, undefined);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let bytes: Buffer;
|
|
69
|
+
try {
|
|
70
|
+
bytes = this.read(path.resolve(this.project, file));
|
|
71
|
+
} catch {
|
|
72
|
+
this.cache.set(key, undefined);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (sha256(bytes) !== expected.disk) {
|
|
76
|
+
this.cache.set(key, undefined);
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const text = decodeSource(bytes);
|
|
81
|
+
if (sha256(text) !== expected.checker) {
|
|
82
|
+
this.cache.set(key, undefined);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lines: readonly string[] = Object.freeze(
|
|
87
|
+
text.split(/\r\n|[\n\r\u2028\u2029]/u),
|
|
88
|
+
);
|
|
89
|
+
this.cache.set(key, lines);
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalize(file: string): string {
|
|
95
|
+
return file.replace(/\\/g, "/");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decodeSource(bytes: Buffer): string {
|
|
99
|
+
if (bytes.length >= 2) {
|
|
100
|
+
const body = bytes.subarray(2, bytes.length - ((bytes.length - 2) % 2));
|
|
101
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe) return body.toString("utf16le");
|
|
102
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff)
|
|
103
|
+
return Buffer.from(body).swap16().toString("utf16le");
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
bytes.length >= 3 &&
|
|
107
|
+
bytes[0] === 0xef &&
|
|
108
|
+
bytes[1] === 0xbb &&
|
|
109
|
+
bytes[2] === 0xbf
|
|
110
|
+
)
|
|
111
|
+
return bytes.subarray(3).toString("utf8");
|
|
112
|
+
return bytes.toString("utf8");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sha256(value: string | Buffer): string {
|
|
116
|
+
return createHash("sha256").update(value).digest("hex");
|
|
117
|
+
}
|
package/src/model/loadGraph.ts
CHANGED
|
@@ -20,11 +20,11 @@ const MAX_DUMP_BYTES = 1024 * 1024 * 1024;
|
|
|
20
20
|
* constant out of this file and fails if the pair drifts.
|
|
21
21
|
*
|
|
22
22
|
* Exported because the resident session reads the same dump body over the serve
|
|
23
|
-
* protocol and has to hold it to the same number: the envelope's version and
|
|
24
|
-
* body's are independent, so a producer can speak this protocol and still
|
|
25
|
-
* body from another schema.
|
|
23
|
+
* protocol and has to hold it to the same number: the envelope's version and
|
|
24
|
+
* the body's are independent, so a producer can speak this protocol and still
|
|
25
|
+
* send a body from another schema.
|
|
26
26
|
*/
|
|
27
|
-
export const DUMP_SCHEMA_VERSION =
|
|
27
|
+
export const DUMP_SCHEMA_VERSION = 6;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Build the resident {@link TtscGraphMemory} for a project by running `ttscgraph
|
package/src/reduce.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface RawNode {
|
|
|
9
9
|
kind: string;
|
|
10
10
|
file: string;
|
|
11
11
|
external?: boolean;
|
|
12
|
+
ignored?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface RawEdge {
|
|
@@ -45,6 +46,7 @@ export interface ViewerPayload {
|
|
|
45
46
|
nodes: number;
|
|
46
47
|
links: number;
|
|
47
48
|
droppedExternal: number;
|
|
49
|
+
droppedIgnored: number;
|
|
48
50
|
droppedByCap: number;
|
|
49
51
|
};
|
|
50
52
|
nodes: ViewerNode[];
|
|
@@ -55,42 +57,118 @@ function posix(p: string): string {
|
|
|
55
57
|
return p.replace(/\\/g, "/");
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
/**
|
|
60
|
+
/** Absolute POSIX, Windows drive, or UNC path; relative dumps skip rerooting. */
|
|
59
61
|
function isAbsolute(p: string): boolean {
|
|
60
62
|
return /^(?:[A-Za-z]:)?\//.test(posix(p));
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
function isWindowsPath(p: string): boolean {
|
|
66
|
+
const normalized = posix(p);
|
|
67
|
+
return /^[A-Za-z]:(?:\/|$)/.test(normalized) || normalized.startsWith("//");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function directoryOf(file: string): string {
|
|
71
|
+
const normalized = posix(file).replace(/\/+$/, "");
|
|
72
|
+
const slash = normalized.lastIndexOf("/");
|
|
73
|
+
if (slash < 0) return "";
|
|
74
|
+
return slash === 0 ? "/" : normalized.slice(0, slash);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function commonRoot(directories: string[]): string {
|
|
78
|
+
if (directories.length === 0) return "";
|
|
79
|
+
let parts = posix(directories[0]!).split("/");
|
|
80
|
+
const caseInsensitive = directories.every(isWindowsPath);
|
|
81
|
+
for (const directory of directories.slice(1)) {
|
|
82
|
+
const other = posix(directory).split("/");
|
|
68
83
|
let i = 0;
|
|
69
|
-
while (
|
|
84
|
+
while (
|
|
85
|
+
i < parts.length &&
|
|
86
|
+
i < other.length &&
|
|
87
|
+
(caseInsensitive
|
|
88
|
+
? parts[i]!.toLowerCase() === other[i]!.toLowerCase()
|
|
89
|
+
: parts[i] === other[i])
|
|
90
|
+
)
|
|
91
|
+
i++;
|
|
70
92
|
parts = parts.slice(0, i);
|
|
71
93
|
if (parts.length === 0) break;
|
|
72
94
|
}
|
|
73
95
|
return parts.join("/");
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
function relativize(abs: string, root: string): string {
|
|
98
|
+
// A null root means the dump's paths are already project-relative (the current
|
|
99
|
+
// `ttscgraph dump` contract); they pass through structure-intact.
|
|
100
|
+
function relativize(abs: string, root: string | null): string {
|
|
79
101
|
const a = posix(abs);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
if (root === null) return a;
|
|
103
|
+
const normalizedRoot = posix(root);
|
|
104
|
+
const r = normalizedRoot === "/" ? "/" : normalizedRoot.replace(/\/+$/, "");
|
|
105
|
+
const caseInsensitive = isWindowsPath(a) && isWindowsPath(r);
|
|
106
|
+
const comparedPath = caseInsensitive ? a.toLowerCase() : a;
|
|
107
|
+
const comparedRoot = caseInsensitive ? r.toLowerCase() : r;
|
|
108
|
+
if (
|
|
109
|
+
comparedRoot &&
|
|
110
|
+
(comparedRoot === "/" ||
|
|
111
|
+
comparedPath === comparedRoot ||
|
|
112
|
+
comparedPath.startsWith(comparedRoot + "/"))
|
|
113
|
+
)
|
|
83
114
|
return a.slice(r.length).replace(/^\/+/, "");
|
|
84
115
|
const nm = a.lastIndexOf("node_modules/");
|
|
116
|
+
// A package tail is a deliberate normalization: the same dependency reached
|
|
117
|
+
// through two roots is one thing to look at.
|
|
85
118
|
if (nm >= 0) return a.slice(nm);
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
// Anything else keeps its whole path. Collapsing to the basename made the
|
|
120
|
+
// projection non-injective, and node ids are rewritten with it, so two
|
|
121
|
+
// declarations in two files could become one viewer node and an edge could
|
|
122
|
+
// resolve back to the wrong end. A longer label is a cosmetic cost; a
|
|
123
|
+
// collided id is a wrong picture.
|
|
124
|
+
return a;
|
|
88
125
|
}
|
|
89
126
|
|
|
90
|
-
function rewriteId(id: string, root: string): string {
|
|
91
|
-
const hash = id
|
|
127
|
+
function rewriteId(id: string, root: string | null): string {
|
|
128
|
+
const hash = graphNodeIdHash(id);
|
|
92
129
|
if (hash < 0) return id;
|
|
93
|
-
return
|
|
130
|
+
return (
|
|
131
|
+
escapeGraphNodeIdPart(
|
|
132
|
+
relativize(unescapeGraphNodeIdPart(id.slice(0, hash)), root),
|
|
133
|
+
) + id.slice(hash)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function escapeGraphNodeIdPart(value: string): string {
|
|
138
|
+
return value.replaceAll("\\", "\\\\").replaceAll("#", "\\#");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function unescapeGraphNodeIdPart(value: string): string {
|
|
142
|
+
let result = "";
|
|
143
|
+
for (let index = 0; index < value.length; index++) {
|
|
144
|
+
const next = value[index + 1];
|
|
145
|
+
if (value[index] === "\\" && next !== undefined) {
|
|
146
|
+
if (next === "#" || (next === "\\" && !legacyUNCStart(value, index))) {
|
|
147
|
+
result += next;
|
|
148
|
+
index++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
result += value[index];
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function legacyUNCStart(value: string, index: number): boolean {
|
|
158
|
+
return (
|
|
159
|
+
index === 0 && value.length > 2 && value[2] !== "\\" && value[2] !== "#"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function graphNodeIdHash(id: string): number {
|
|
164
|
+
for (let index = 0; index < id.length; index++) {
|
|
165
|
+
if (id[index] !== "#") continue;
|
|
166
|
+
let slashes = 0;
|
|
167
|
+
for (let slash = index - 1; slash >= 0 && id[slash] === "\\"; slash--)
|
|
168
|
+
slashes++;
|
|
169
|
+
if (slashes % 2 === 0) return index;
|
|
170
|
+
}
|
|
171
|
+
return -1;
|
|
94
172
|
}
|
|
95
173
|
|
|
96
174
|
function degreeOf(
|
|
@@ -107,9 +185,9 @@ function degreeOf(
|
|
|
107
185
|
|
|
108
186
|
/**
|
|
109
187
|
* Collapse the fine-grained wire kinds `ttscgraph dump` emits (calls,
|
|
110
|
-
* instantiates, renders, accesses, type_ref, extends, implements)
|
|
111
|
-
* three display families the viewer colors and its legend name. An
|
|
112
|
-
* passes through and renders with the fallback color.
|
|
188
|
+
* instantiates, renders, accesses, type_ref, extends, implements, overrides)
|
|
189
|
+
* into the three display families the viewer colors and its legend name. An
|
|
190
|
+
* unknown kind passes through and renders with the fallback color.
|
|
113
191
|
*/
|
|
114
192
|
const DISPLAY_KIND: Record<string, string> = {
|
|
115
193
|
calls: "value-call",
|
|
@@ -119,6 +197,7 @@ const DISPLAY_KIND: Record<string, string> = {
|
|
|
119
197
|
type_ref: "type-ref",
|
|
120
198
|
extends: "heritage",
|
|
121
199
|
implements: "heritage",
|
|
200
|
+
overrides: "heritage",
|
|
122
201
|
};
|
|
123
202
|
|
|
124
203
|
function displayKind(kind: string): string {
|
|
@@ -130,30 +209,48 @@ export function reduce(
|
|
|
130
209
|
{
|
|
131
210
|
maxNodes = 1200,
|
|
132
211
|
keepExternal = false,
|
|
133
|
-
|
|
212
|
+
keepIgnored = false,
|
|
213
|
+
}: {
|
|
214
|
+
maxNodes?: number;
|
|
215
|
+
keepExternal?: boolean;
|
|
216
|
+
keepIgnored?: boolean;
|
|
217
|
+
} = {},
|
|
134
218
|
): ViewerPayload {
|
|
135
|
-
|
|
219
|
+
// Drop external boundary leaves and git-ignored generated code (a Prisma
|
|
220
|
+
// client and the like, tagged `ignored` by the dump) so the authored graph is
|
|
221
|
+
// not buried under codegen. The cap is by degree, and generated clients are
|
|
222
|
+
// large and densely connected, so leaving them in does not merely add noise:
|
|
223
|
+
// they outrank authored code for the surviving slots.
|
|
224
|
+
const keep = (n: RawNode) =>
|
|
225
|
+
(keepExternal || !n.external) && (keepIgnored || !n.ignored);
|
|
226
|
+
const keptByBoundary = raw.nodes.filter(keep);
|
|
136
227
|
// Reroot only absolute paths (the legacy dump contract); a current dump's
|
|
137
228
|
// paths are already project-relative and keep their structure as-is.
|
|
138
|
-
const projectFiles = raw.nodes
|
|
229
|
+
const projectFiles = raw.nodes
|
|
230
|
+
.filter((n) => !n.external && !n.ignored)
|
|
231
|
+
.map((n) => n.file);
|
|
139
232
|
const root =
|
|
140
233
|
projectFiles.length > 0 && isAbsolute(projectFiles[0]!)
|
|
141
|
-
?
|
|
142
|
-
|
|
234
|
+
? // A dump may mix path forms from one valid project: in-project sources
|
|
235
|
+
// are relative, package sources carry a node_modules tail, and other
|
|
236
|
+
// out-of-project sources stay absolute. Mixing those into one common
|
|
237
|
+
// root yields the empty string, so only absolute directories vote.
|
|
238
|
+
commonRoot(projectFiles.filter(isAbsolute).map(directoryOf))
|
|
239
|
+
: null;
|
|
143
240
|
|
|
144
|
-
const liveIds = new Set(
|
|
241
|
+
const liveIds = new Set(keptByBoundary.map((n) => n.id));
|
|
145
242
|
const liveEdges = raw.edges.filter(
|
|
146
243
|
(e) => liveIds.has(e.from) && liveIds.has(e.to),
|
|
147
244
|
);
|
|
148
245
|
|
|
149
|
-
const degree = degreeOf(
|
|
150
|
-
let kept =
|
|
246
|
+
const degree = degreeOf(keptByBoundary, liveEdges);
|
|
247
|
+
let kept = keptByBoundary;
|
|
151
248
|
let droppedByCap = 0;
|
|
152
249
|
if (kept.length > maxNodes) {
|
|
153
250
|
kept = [...kept]
|
|
154
251
|
.sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0))
|
|
155
252
|
.slice(0, maxNodes);
|
|
156
|
-
droppedByCap =
|
|
253
|
+
droppedByCap = keptByBoundary.length - kept.length;
|
|
157
254
|
}
|
|
158
255
|
|
|
159
256
|
const keptIds = new Set(kept.map((n) => n.id));
|
|
@@ -188,7 +285,15 @@ export function reduce(
|
|
|
188
285
|
rawEdges: raw.edges.length,
|
|
189
286
|
nodes: nodes.length,
|
|
190
287
|
links: links.length,
|
|
191
|
-
|
|
288
|
+
// Each counter reports what its own filter dropped, and reports zero when
|
|
289
|
+
// that filter was turned off. The two are disjoint: an external node is
|
|
290
|
+
// counted as external whether or not it is also ignored.
|
|
291
|
+
droppedExternal: keepExternal
|
|
292
|
+
? 0
|
|
293
|
+
: raw.nodes.filter((n) => n.external).length,
|
|
294
|
+
droppedIgnored: keepIgnored
|
|
295
|
+
? 0
|
|
296
|
+
: raw.nodes.filter((n) => n.ignored && !n.external).length,
|
|
192
297
|
droppedByCap,
|
|
193
298
|
},
|
|
194
299
|
nodes,
|
|
@@ -28,11 +28,21 @@ export function createServer(
|
|
|
28
28
|
graph: TtscGraphSource,
|
|
29
29
|
version: string,
|
|
30
30
|
): McpServer {
|
|
31
|
-
|
|
32
|
-
return createMcpServer(
|
|
31
|
+
const server = createMcpServer(
|
|
33
32
|
typia.llm.controller<ITtscGraphApplication>(
|
|
34
33
|
"ttsc-graph",
|
|
35
34
|
new TtscGraphApplication(graph),
|
|
36
35
|
),
|
|
37
36
|
);
|
|
37
|
+
// @typia/mcp 13.1.0 exposes no class-controller version option. The MCP SDK
|
|
38
|
+
// keeps the initialize implementation on its public inner Server, so update
|
|
39
|
+
// that already-constructed implementation before any transport connects.
|
|
40
|
+
const inner = server.server as unknown as {
|
|
41
|
+
_serverInfo?: { version: string };
|
|
42
|
+
};
|
|
43
|
+
if (inner._serverInfo === undefined) {
|
|
44
|
+
throw new Error("@ttsc/graph: MCP SDK omitted the server implementation");
|
|
45
|
+
}
|
|
46
|
+
inner._serverInfo.version = version;
|
|
47
|
+
return server;
|
|
38
48
|
}
|
package/src/server/pathPolicy.ts
CHANGED
|
@@ -9,6 +9,15 @@ export function isExternalNode(node: ITtscGraphNode): boolean {
|
|
|
9
9
|
);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* True for a `.d.ts` declaration file. Every declaration such a file holds is
|
|
14
|
+
* ambient by construction, so none of them carries a body the graph can walk
|
|
15
|
+
* into, whether or not the `declare` keyword is written out.
|
|
16
|
+
*/
|
|
17
|
+
export function isDeclarationFile(file: string): boolean {
|
|
18
|
+
return /\.d\.[cm]?ts$/.test(file);
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
/** True for tests, examples, fixtures, generated output, and build artifacts. */
|
|
13
22
|
export function isSupportPath(file: string): boolean {
|
|
14
23
|
return (
|
|
@@ -20,7 +29,7 @@ export function isSupportPath(file: string): boolean {
|
|
|
20
29
|
) ||
|
|
21
30
|
/\.(test|spec)\.[cm]?tsx?$/.test(file) ||
|
|
22
31
|
/(^|\/)typings\.[cm]?ts$/.test(file) ||
|
|
23
|
-
|
|
32
|
+
isDeclarationFile(file) ||
|
|
24
33
|
/(^|\/)(dist|build|coverage|generated|__generated__)\//.test(file)
|
|
25
34
|
);
|
|
26
35
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
|
+
import { parseTtscGraphNodeId } from "../model/TtscGraphNodeId";
|
|
2
3
|
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
3
4
|
import { exportFanIn } from "./exportSurface";
|
|
4
5
|
import { isSupportPath } from "./pathPolicy";
|
|
@@ -42,21 +43,17 @@ export function resolveGraphHandle(
|
|
|
42
43
|
const byId = graph.node(handle);
|
|
43
44
|
if (byId !== undefined) return { node: byId };
|
|
44
45
|
|
|
45
|
-
const byName = resolveGraphName(graph, handle
|
|
46
|
+
const byName = resolveGraphName(graph, handle);
|
|
46
47
|
if (byName.node !== undefined || byName.candidates !== undefined)
|
|
47
48
|
return rank(graph, byName, candidateLimit);
|
|
48
49
|
|
|
49
|
-
const byFile = resolveFileQualified(graph, handle
|
|
50
|
+
const byFile = resolveFileQualified(graph, handle);
|
|
50
51
|
if (byFile.node !== undefined || byFile.candidates !== undefined)
|
|
51
52
|
return rank(graph, byFile, candidateLimit);
|
|
52
53
|
|
|
53
54
|
const symbol = symbolPartOf(handle) ?? memberPartOf(handle);
|
|
54
55
|
if (symbol !== undefined)
|
|
55
|
-
return rank(
|
|
56
|
-
graph,
|
|
57
|
-
resolveGraphName(graph, symbol, candidateLimit),
|
|
58
|
-
candidateLimit,
|
|
59
|
-
);
|
|
56
|
+
return rank(graph, resolveGraphName(graph, symbol), candidateLimit);
|
|
60
57
|
return {};
|
|
61
58
|
}
|
|
62
59
|
|
|
@@ -77,22 +74,16 @@ function memberPartOf(handle: string): string | undefined {
|
|
|
77
74
|
|
|
78
75
|
/** The symbol an id-shaped handle names: `dir/file.ts#Class.method:kind`. */
|
|
79
76
|
function symbolPartOf(handle: string): string | undefined {
|
|
80
|
-
|
|
81
|
-
if (hash < 0) return undefined;
|
|
82
|
-
const symbol = handle.slice(hash + 1);
|
|
83
|
-
const kind = symbol.lastIndexOf(":");
|
|
84
|
-
const name = kind < 0 ? symbol : symbol.slice(0, kind);
|
|
85
|
-
return name.length > 0 ? name : undefined;
|
|
77
|
+
return parseTtscGraphNodeId(handle)?.name;
|
|
86
78
|
}
|
|
87
79
|
|
|
88
80
|
function resolveGraphName(
|
|
89
81
|
graph: TtscGraphMemory,
|
|
90
82
|
name: string,
|
|
91
|
-
candidateLimit: number,
|
|
92
83
|
): IResolvedGraphHandle {
|
|
93
84
|
const exact = graph.symbols(name);
|
|
94
85
|
if (exact.length === 1) return { node: exact[0] };
|
|
95
|
-
if (exact.length > 1) return { candidates: exact
|
|
86
|
+
if (exact.length > 1) return { candidates: [...exact] };
|
|
96
87
|
|
|
97
88
|
if (name.includes(".")) {
|
|
98
89
|
const suffix = `.${name}`;
|
|
@@ -102,7 +93,7 @@ function resolveGraphName(
|
|
|
102
93
|
);
|
|
103
94
|
if (suffixMatches.length === 1) return { node: suffixMatches[0] };
|
|
104
95
|
if (suffixMatches.length > 1) {
|
|
105
|
-
return { candidates: suffixMatches
|
|
96
|
+
return { candidates: suffixMatches };
|
|
106
97
|
}
|
|
107
98
|
}
|
|
108
99
|
return {};
|
|
@@ -117,7 +108,6 @@ function resolveGraphName(
|
|
|
117
108
|
function resolveFileQualified(
|
|
118
109
|
graph: TtscGraphMemory,
|
|
119
110
|
handle: string,
|
|
120
|
-
candidateLimit: number,
|
|
121
111
|
): IResolvedGraphHandle {
|
|
122
112
|
const dot = handle.indexOf(".");
|
|
123
113
|
if (dot <= 0) return {};
|
|
@@ -128,8 +118,7 @@ function resolveFileQualified(
|
|
|
128
118
|
.symbols(name)
|
|
129
119
|
.filter((node) => fileStem(node.file) === stem);
|
|
130
120
|
if (matches.length === 1) return { node: matches[0] };
|
|
131
|
-
if (matches.length > 1)
|
|
132
|
-
return { candidates: matches.slice(0, candidateLimit) };
|
|
121
|
+
if (matches.length > 1) return { candidates: matches };
|
|
133
122
|
return {};
|
|
134
123
|
}
|
|
135
124
|
|
|
@@ -153,12 +142,22 @@ function rank(
|
|
|
153
142
|
candidateLimit: number,
|
|
154
143
|
): IResolvedGraphHandle {
|
|
155
144
|
if (resolved.candidates === undefined) return resolved;
|
|
156
|
-
const ranked =
|
|
157
|
-
.
|
|
158
|
-
.
|
|
145
|
+
const ranked = resolved.candidates
|
|
146
|
+
.map((node) => ({ node, score: candidateScore(graph, node) }))
|
|
147
|
+
.sort((a, b) => {
|
|
148
|
+
const score = b.score - a.score;
|
|
149
|
+
return score !== 0 ? score : compareIdentity(a.node.id, b.node.id);
|
|
150
|
+
})
|
|
151
|
+
.slice(0, candidateLimit)
|
|
152
|
+
.map(({ node }) => node);
|
|
159
153
|
return { candidates: ranked };
|
|
160
154
|
}
|
|
161
155
|
|
|
156
|
+
/** Compare position-invariant ids without locale-dependent collation. */
|
|
157
|
+
function compareIdentity(left: string, right: string): number {
|
|
158
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
162
161
|
function candidateScore(graph: TtscGraphMemory, node: ITtscGraphNode): number {
|
|
163
162
|
let score = Math.min(48, Math.log2(1 + exportFanIn(graph, node.id)) * 20);
|
|
164
163
|
if (node.exported) score += 12;
|