@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.
Files changed (92) hide show
  1. package/lib/TtscGraphApplication.js +5 -1
  2. package/lib/TtscGraphApplication.js.map +1 -1
  3. package/lib/bin.js +15 -3
  4. package/lib/bin.js.map +1 -1
  5. package/lib/index.d.ts +13 -2
  6. package/lib/index.js +25 -30
  7. package/lib/index.js.map +1 -1
  8. package/lib/launcherArgs.d.ts +24 -0
  9. package/lib/launcherArgs.js +136 -0
  10. package/lib/launcherArgs.js.map +1 -0
  11. package/lib/model/TtscGraphMemory.d.ts +9 -5
  12. package/lib/model/TtscGraphMemory.js +35 -58
  13. package/lib/model/TtscGraphMemory.js.map +1 -1
  14. package/lib/model/TtscGraphNodeId.d.ts +17 -0
  15. package/lib/model/TtscGraphNodeId.js +69 -0
  16. package/lib/model/TtscGraphNodeId.js.map +1 -0
  17. package/lib/model/TtscGraphSession.d.ts +23 -7
  18. package/lib/model/TtscGraphSession.js +423 -171
  19. package/lib/model/TtscGraphSession.js.map +1 -1
  20. package/lib/model/TtscGraphSourceReader.d.ts +27 -0
  21. package/lib/model/TtscGraphSourceReader.js +97 -0
  22. package/lib/model/TtscGraphSourceReader.js.map +1 -0
  23. package/lib/model/loadGraph.d.ts +14 -0
  24. package/lib/model/loadGraph.js +256 -143
  25. package/lib/model/loadGraph.js.map +1 -1
  26. package/lib/reduce.d.ts +4 -1
  27. package/lib/reduce.js +112 -29
  28. package/lib/reduce.js.map +1 -1
  29. package/lib/resolveGraphBinary.d.ts +9 -3
  30. package/lib/resolveGraphBinary.js +9 -3
  31. package/lib/resolveGraphBinary.js.map +1 -1
  32. package/lib/server/createServer.js +22 -11
  33. package/lib/server/createServer.js.map +1 -1
  34. package/lib/server/pathPolicy.d.ts +6 -0
  35. package/lib/server/pathPolicy.js +10 -1
  36. package/lib/server/pathPolicy.js.map +1 -1
  37. package/lib/server/resolveHandle.js +22 -18
  38. package/lib/server/resolveHandle.js.map +1 -1
  39. package/lib/server/resultAudit.d.ts +56 -14
  40. package/lib/server/resultAudit.js +90 -24
  41. package/lib/server/resultAudit.js.map +1 -1
  42. package/lib/server/resultNext.d.ts +5 -0
  43. package/lib/server/resultNext.js.map +1 -1
  44. package/lib/server/runDetails.d.ts +9 -2
  45. package/lib/server/runDetails.js +132 -157
  46. package/lib/server/runDetails.js.map +1 -1
  47. package/lib/server/runEntrypoints.js +1 -1
  48. package/lib/server/runEntrypoints.js.map +1 -1
  49. package/lib/server/runLookup.js +1 -1
  50. package/lib/server/runLookup.js.map +1 -1
  51. package/lib/server/runTour.js +113 -30
  52. package/lib/server/runTour.js.map +1 -1
  53. package/lib/server/runTrace.d.ts +22 -0
  54. package/lib/server/runTrace.js +293 -63
  55. package/lib/server/runTrace.js.map +1 -1
  56. package/lib/structures/ITtscGraphApplication.d.ts +8 -7
  57. package/lib/structures/ITtscGraphDetails.d.ts +18 -8
  58. package/lib/structures/ITtscGraphDump.d.ts +13 -13
  59. package/lib/structures/ITtscGraphNode.d.ts +71 -0
  60. package/lib/structures/ITtscGraphTour.d.ts +1 -1
  61. package/lib/structures/ITtscGraphTrace.d.ts +1 -1
  62. package/lib/view.js +18 -28
  63. package/lib/view.js.map +1 -1
  64. package/package.json +7 -4
  65. package/src/TtscGraphApplication.ts +8 -1
  66. package/src/bin.ts +13 -3
  67. package/src/index.ts +35 -27
  68. package/src/launcherArgs.ts +168 -0
  69. package/src/model/TtscGraphMemory.ts +34 -60
  70. package/src/model/TtscGraphNodeId.ts +77 -0
  71. package/src/model/TtscGraphSession.ts +253 -53
  72. package/src/model/TtscGraphSourceReader.ts +117 -0
  73. package/src/model/loadGraph.ts +6 -1
  74. package/src/reduce.ts +136 -31
  75. package/src/resolveGraphBinary.ts +9 -3
  76. package/src/server/createServer.ts +12 -2
  77. package/src/server/pathPolicy.ts +10 -1
  78. package/src/server/resolveHandle.ts +21 -22
  79. package/src/server/resultAudit.ts +93 -23
  80. package/src/server/resultNext.ts +6 -0
  81. package/src/server/runDetails.ts +134 -165
  82. package/src/server/runEntrypoints.ts +1 -1
  83. package/src/server/runLookup.ts +1 -1
  84. package/src/server/runTour.ts +133 -39
  85. package/src/server/runTrace.ts +401 -65
  86. package/src/structures/ITtscGraphApplication.ts +8 -7
  87. package/src/structures/ITtscGraphDetails.ts +18 -8
  88. package/src/structures/ITtscGraphDump.ts +13 -13
  89. package/src/structures/ITtscGraphNode.ts +80 -0
  90. package/src/structures/ITtscGraphTour.ts +1 -1
  91. package/src/structures/ITtscGraphTrace.ts +1 -1
  92. 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
+ }
@@ -18,8 +18,13 @@ const MAX_DUMP_BYTES = 1024 * 1024 * 1024;
18
18
  * `packages/ttsc/internal/graph/provenance.go`. The two are hand-synchronized,
19
19
  * and `dump_schema_version_matches_the_typescript_client_test.go` reads this
20
20
  * constant out of this file and fails if the pair drifts.
21
+ *
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
+ * the body's are independent, so a producer can speak this protocol and still
25
+ * send a body from another schema.
21
26
  */
22
- const DUMP_SCHEMA_VERSION = 1;
27
+ export const DUMP_SCHEMA_VERSION = 6;
23
28
 
24
29
  /**
25
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
- /** An absolute path (POSIX or Windows drive); relative dumps skip rerooting. */
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 commonRoot(files: string[]): string {
64
- if (files.length === 0) return "";
65
- let parts = posix(files[0]!).split("/");
66
- for (const file of files.slice(1)) {
67
- const other = posix(file).split("/");
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 (i < parts.length && i < other.length && parts[i] === other[i]) i++;
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
- // An empty root means the dump's paths are already project-relative (the
77
- // current `ttscgraph dump` contract); they pass through structure-intact.
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
- const r = posix(root).replace(/\/+$/, "");
81
- if (!r) return a;
82
- if (a === r || a.startsWith(r + "/"))
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
- const slash = a.lastIndexOf("/");
87
- return slash >= 0 ? a.slice(slash + 1) : a;
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.indexOf("#");
127
+ function rewriteId(id: string, root: string | null): string {
128
+ const hash = graphNodeIdHash(id);
92
129
  if (hash < 0) return id;
93
- return relativize(id.slice(0, hash), root) + id.slice(hash);
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) into the
111
- * three display families the viewer colors and its legend name. An unknown kind
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
- }: { maxNodes?: number; keepExternal?: boolean } = {},
212
+ keepIgnored = false,
213
+ }: {
214
+ maxNodes?: number;
215
+ keepExternal?: boolean;
216
+ keepIgnored?: boolean;
217
+ } = {},
134
218
  ): ViewerPayload {
135
- const keptByExternal = raw.nodes.filter((n) => keepExternal || !n.external);
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.filter((n) => !n.external).map((n) => n.file);
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
- ? commonRoot(projectFiles)
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(keptByExternal.map((n) => n.id));
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(keptByExternal, liveEdges);
150
- let kept = keptByExternal;
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 = keptByExternal.length - kept.length;
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
- droppedExternal: raw.nodes.length - keptByExternal.length,
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,
@@ -5,9 +5,15 @@ import path from "node:path";
5
5
  * Resolve the per-platform `ttscgraph` binary, or `null` when it cannot be
6
6
  * located.
7
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.
8
+ * `ttsc` is a peer dependency pinned to this package's own version, installed
9
+ * alongside `@ttsc/graph` in the user's project rather than nested under this
10
+ * launcher, so resolution starts from the user's project, not from this
11
+ * package's own tree.
12
+ *
13
+ * The pin is what keeps the two halves of the graph in step. `literals` and the
14
+ * rest of the node facts are resolved by the Go builder and ride the dump, so a
15
+ * newer `@ttsc/graph` reading an older `ttscgraph` would find those fields
16
+ * simply absent and answer from a graph missing facts it believes are there.
11
17
  *
12
18
  * Resolution order:
13
19
  *
@@ -28,11 +28,21 @@ export function createServer(
28
28
  graph: TtscGraphSource,
29
29
  version: string,
30
30
  ): McpServer {
31
- void version;
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
  }
@@ -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
- /\.d\.[cm]?ts$/.test(file) ||
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, candidateLimit);
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, candidateLimit);
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
- const hash = handle.lastIndexOf("#");
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.slice(0, candidateLimit) };
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.slice(0, candidateLimit) };
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 = [...resolved.candidates]
157
- .sort((a, b) => candidateScore(graph, b) - candidateScore(graph, a))
158
- .slice(0, candidateLimit);
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;