@ttsc/graph 0.18.4 → 0.19.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.
Files changed (119) hide show
  1. package/README.md +231 -37
  2. package/lib/TtscGraphApplication.d.ts +1 -1
  3. package/lib/TtscGraphApplication.js +50 -17
  4. package/lib/TtscGraphApplication.js.map +1 -1
  5. package/lib/index.d.ts +5 -0
  6. package/lib/index.js +20 -1
  7. package/lib/index.js.map +1 -1
  8. package/lib/model/TtscGraphMemory.js +80 -22
  9. package/lib/model/TtscGraphMemory.js.map +1 -1
  10. package/lib/model/TtscGraphSession.js +92 -138
  11. package/lib/model/TtscGraphSession.js.map +1 -1
  12. package/lib/model/loadGraph.js +91 -138
  13. package/lib/model/loadGraph.js.map +1 -1
  14. package/lib/resolveGraphBinary.d.ts +4 -2
  15. package/lib/resolveGraphBinary.js +7 -3
  16. package/lib/resolveGraphBinary.js.map +1 -1
  17. package/lib/server/createServer.d.ts +16 -9
  18. package/lib/server/createServer.js +286 -190
  19. package/lib/server/createServer.js.map +1 -1
  20. package/lib/server/exportSurface.d.ts +26 -0
  21. package/lib/server/exportSurface.js +44 -0
  22. package/lib/server/exportSurface.js.map +1 -0
  23. package/lib/server/resolveHandle.d.ts +26 -1
  24. package/lib/server/resolveHandle.js +124 -4
  25. package/lib/server/resolveHandle.js.map +1 -1
  26. package/lib/server/resultAudit.d.ts +74 -0
  27. package/lib/server/resultAudit.js +110 -0
  28. package/lib/server/resultAudit.js.map +1 -0
  29. package/lib/server/resultNext.d.ts +9 -0
  30. package/lib/server/resultNext.js +11 -0
  31. package/lib/server/resultNext.js.map +1 -0
  32. package/lib/server/runDetails.d.ts +15 -3
  33. package/lib/server/runDetails.js +105 -23
  34. package/lib/server/runDetails.js.map +1 -1
  35. package/lib/server/runEntrypoints.d.ts +2 -1
  36. package/lib/server/runEntrypoints.js +17 -10
  37. package/lib/server/runEntrypoints.js.map +1 -1
  38. package/lib/server/runLookup.d.ts +2 -1
  39. package/lib/server/runLookup.js +21 -9
  40. package/lib/server/runLookup.js.map +1 -1
  41. package/lib/server/runOverview.d.ts +2 -1
  42. package/lib/server/runOverview.js +5 -4
  43. package/lib/server/runOverview.js.map +1 -1
  44. package/lib/server/runTour.d.ts +2 -1
  45. package/lib/server/runTour.js +707 -331
  46. package/lib/server/runTour.js.map +1 -1
  47. package/lib/server/runTrace.d.ts +2 -1
  48. package/lib/server/runTrace.js +234 -54
  49. package/lib/server/runTrace.js.map +1 -1
  50. package/lib/structures/ITtscGraphApplication.d.ts +122 -118
  51. package/lib/structures/ITtscGraphDecorator.d.ts +4 -7
  52. package/lib/structures/ITtscGraphDetails.d.ts +51 -43
  53. package/lib/structures/ITtscGraphDump.d.ts +32 -8
  54. package/lib/structures/ITtscGraphEdge.d.ts +4 -6
  55. package/lib/structures/ITtscGraphEntrypoints.d.ts +10 -27
  56. package/lib/structures/ITtscGraphEscape.d.ts +6 -16
  57. package/lib/structures/ITtscGraphEvidence.d.ts +5 -8
  58. package/lib/structures/ITtscGraphLookup.d.ts +9 -20
  59. package/lib/structures/ITtscGraphNext.d.ts +10 -7
  60. package/lib/structures/ITtscGraphNode.d.ts +17 -8
  61. package/lib/structures/ITtscGraphOverview.d.ts +10 -14
  62. package/lib/structures/ITtscGraphSpan.d.ts +29 -0
  63. package/lib/structures/{ITtscGraphDiagnostic.js → ITtscGraphSpan.js} +1 -1
  64. package/lib/structures/ITtscGraphSpan.js.map +1 -0
  65. package/lib/structures/ITtscGraphTour.d.ts +65 -23
  66. package/lib/structures/ITtscGraphTrace.d.ts +72 -49
  67. package/lib/structures/TtscGraphEdgeKind.d.ts +7 -1
  68. package/lib/structures/index.d.ts +1 -1
  69. package/lib/structures/index.js +1 -1
  70. package/lib/structures/index.js.map +1 -1
  71. package/lib/view.js +3 -1
  72. package/lib/view.js.map +1 -1
  73. package/package.json +4 -4
  74. package/src/TtscGraphApplication.ts +58 -27
  75. package/src/index.ts +19 -1
  76. package/src/model/TtscGraphMemory.ts +86 -22
  77. package/src/model/TtscGraphSession.ts +5 -1
  78. package/src/model/loadGraph.ts +3 -1
  79. package/src/resolveGraphBinary.ts +7 -3
  80. package/src/server/createServer.ts +24 -17
  81. package/src/server/exportSurface.ts +40 -0
  82. package/src/server/resolveHandle.ts +141 -4
  83. package/src/server/resultAudit.ts +109 -0
  84. package/src/server/resultNext.ts +22 -0
  85. package/src/server/runDetails.ts +112 -28
  86. package/src/server/runEntrypoints.ts +26 -17
  87. package/src/server/runLookup.ts +29 -18
  88. package/src/server/runOverview.ts +9 -10
  89. package/src/server/runTour.ts +764 -363
  90. package/src/server/runTrace.ts +265 -71
  91. package/src/structures/ITtscGraphApplication.ts +124 -118
  92. package/src/structures/ITtscGraphDecorator.ts +4 -7
  93. package/src/structures/ITtscGraphDetails.ts +60 -46
  94. package/src/structures/ITtscGraphDump.ts +37 -8
  95. package/src/structures/ITtscGraphEdge.ts +4 -6
  96. package/src/structures/ITtscGraphEntrypoints.ts +10 -30
  97. package/src/structures/ITtscGraphEscape.ts +6 -19
  98. package/src/structures/ITtscGraphEvidence.ts +5 -8
  99. package/src/structures/ITtscGraphLookup.ts +9 -22
  100. package/src/structures/ITtscGraphNext.ts +10 -7
  101. package/src/structures/ITtscGraphNode.ts +18 -8
  102. package/src/structures/ITtscGraphOverview.ts +10 -17
  103. package/src/structures/ITtscGraphSpan.ts +33 -0
  104. package/src/structures/ITtscGraphTour.ts +70 -28
  105. package/src/structures/ITtscGraphTrace.ts +82 -51
  106. package/src/structures/TtscGraphEdgeKind.ts +7 -0
  107. package/src/structures/index.ts +1 -1
  108. package/src/view.ts +3 -1
  109. package/lib/server/accessAliases.d.ts +0 -8
  110. package/lib/server/accessAliases.js +0 -53
  111. package/lib/server/accessAliases.js.map +0 -1
  112. package/lib/server/resultGuide.d.ts +0 -3
  113. package/lib/server/resultGuide.js +0 -16
  114. package/lib/server/resultGuide.js.map +0 -1
  115. package/lib/structures/ITtscGraphDiagnostic.d.ts +0 -27
  116. package/lib/structures/ITtscGraphDiagnostic.js.map +0 -1
  117. package/src/server/accessAliases.ts +0 -55
  118. package/src/server/resultGuide.ts +0 -20
  119. package/src/structures/ITtscGraphDiagnostic.ts +0 -34
@@ -3,20 +3,41 @@ import { ITtscGraphEdge } from "../structures/ITtscGraphEdge";
3
3
  import { ITtscGraphEvidence } from "../structures/ITtscGraphEvidence";
4
4
  import { ITtscGraphNode } from "../structures/ITtscGraphNode";
5
5
  import { ITtscGraphTrace } from "../structures/ITtscGraphTrace";
6
- import { accessAliasesFor } from "./accessAliases";
7
6
  import { isExternalNode, isTestPath } from "./pathPolicy";
8
7
  import { resolveGraphHandle } from "./resolveHandle";
9
- import { resultGuide, resultNext } from "./resultGuide";
10
- import { edgeEvidenceOf, edgeEvidenceTextOf, signatureOf } from "./runDetails";
8
+ import { IRunnerOutput, resultNext } from "./resultNext";
9
+ import { edgeEvidenceOf, signatureOf } from "./runDetails";
11
10
 
12
- const DEFAULT_DEPTH = 2;
13
- const DEFAULT_MAX_NODES = 6;
14
- const MAX_OPEN_DEPTH = 2;
15
- const MAX_OPEN_NODES = 8;
11
+ const DEFAULT_DEPTH = 3;
12
+ const DEFAULT_MAX_NODES = 12;
13
+ // An open trace used to stop at two hops and eight nodes, whatever depth the
14
+ // caller asked for. A question that spans a runtime chain — a state change
15
+ // through tracking, scheduling, rendering, then patching — is that chain hop by
16
+ // hop, so the model re-issued a trace per hop and paid a round trip for each: a
17
+ // single vue question spent twenty-six calls walking a flow the graph could have
18
+ // walked once. The cap now follows the chain instead of cutting it.
19
+ const MAX_OPEN_DEPTH = 8;
20
+ const MAX_OPEN_NODES = 32;
16
21
  const MAX_IMPACT_DEPTH = 4;
17
22
  const MAX_IMPACT_NODES = 16;
18
23
  const MAX_HOPS_PER_NODE = 2;
19
- const MAX_STEPS = 6;
24
+ const MAX_STEPS = 12;
25
+ const EXECUTION_KINDS = new Set<string>([
26
+ "calls",
27
+ "instantiates",
28
+ "accesses",
29
+ "renders",
30
+ ]);
31
+ const DISPATCH_KINDS = new Set<string>(["overrides", "implements"]);
32
+ // An interface the codebase implements everywhere — a disposable, a listener, a
33
+ // lifecycle hook — is not a step in one flow, and naming its implementors is a
34
+ // dump of the codebase rather than an answer. Past this many, the declaration
35
+ // stays a leaf and `details` answers `implementedBy` for a caller that wants the
36
+ // list. The cut is the graph's existing definition of a hub (see
37
+ // `isSharedUtility`): across the benchmark corpus it follows 84–100% of dispatch
38
+ // sites per project and refuses only the genuinely polymorphic ones (zod's
39
+ // 36-way schema interface, VS Code's 533-way disposable).
40
+ const DISPATCH_HUB = 12;
20
41
 
21
42
  /**
22
43
  * Breadth-first trace along the dependency graph. Structural
@@ -28,7 +49,7 @@ const MAX_STEPS = 6;
28
49
  export function runTrace(
29
50
  graph: TtscGraphMemory,
30
51
  props: ITtscGraphTrace.IRequest,
31
- ): ITtscGraphTrace {
52
+ ): IRunnerOutput<ITtscGraphTrace> {
32
53
  const direction = props.direction ?? "forward";
33
54
  const focus = props.focus ?? "all";
34
55
  const impact = direction === "impact";
@@ -54,34 +75,32 @@ export function runTrace(
54
75
  const start = resolveGraphHandle(graph, props.from);
55
76
  if (start.candidates) {
56
77
  return {
57
- type: "trace",
58
- direction,
59
- hops: [],
60
- reached: [],
61
- truncated: false,
78
+ result: {
79
+ type: "trace",
80
+ direction,
81
+ hops: [],
82
+ reached: [],
83
+ truncated: false,
84
+ candidates: start.candidates.map((n) => summary(graph, n)),
85
+ },
62
86
  next: resultNext(
63
87
  "clarify",
64
- "The start handle is ambiguous; choose one returned candidate.",
88
+ "The start handle is ambiguous: it matched several candidates, one of which names the trace.",
65
89
  ),
66
- guide: resultGuide(
67
- "Disambiguate with the returned candidates, or ask the user for the intended symbol.",
68
- ),
69
- candidates: start.candidates.map((n) => summary(graph, n)),
70
90
  };
71
91
  }
72
92
  if (start.node === undefined) {
73
93
  return {
74
- type: "trace",
75
- direction,
76
- hops: [],
77
- reached: [],
78
- truncated: false,
94
+ result: {
95
+ type: "trace",
96
+ direction,
97
+ hops: [],
98
+ reached: [],
99
+ truncated: false,
100
+ },
79
101
  next: resultNext(
80
- "clarify",
81
- "The start handle did not resolve in the compiler graph.",
82
- ),
83
- guide: resultGuide(
84
- "The start symbol was not resolved; answer that the graph has no trace from this handle.",
102
+ "outside",
103
+ "The start handle did not resolve in the graph, so it holds no trace from this handle.",
85
104
  ),
86
105
  };
87
106
  }
@@ -95,24 +114,34 @@ export function runTrace(
95
114
  hops: [],
96
115
  reached: [],
97
116
  truncated: false,
98
- next: resultNext(
99
- "answer",
100
- "The path result is the structural flow answer; cite path nodes and evidence ranges.",
101
- ),
102
- guide: resultGuide(
103
- "Use the returned path, hops, and evidence ranges as the flow answer.",
104
- ),
105
117
  };
118
+ const pathNext = resultNext(
119
+ "answer",
120
+ "The path result is the structural flow: its path nodes and evidence ranges are what the graph holds between the two ends.",
121
+ );
106
122
  const target = resolveGraphHandle(graph, props.to);
107
123
  if (target.candidates) {
108
124
  return {
109
- ...base,
110
- start: summary(graph, start.node),
111
- candidates: target.candidates.map((n) => summary(graph, n)),
125
+ result: {
126
+ ...base,
127
+ start: summary(graph, start.node),
128
+ candidates: target.candidates.map((n) => summary(graph, n)),
129
+ },
130
+ next: resultNext(
131
+ "inspect",
132
+ "The target names several nodes; re-trace with the id of the one the question means.",
133
+ "trace",
134
+ ),
112
135
  };
113
136
  }
114
137
  if (target.node === undefined) {
115
- return { ...base, start: summary(graph, start.node) };
138
+ return {
139
+ result: { ...base, start: summary(graph, start.node) },
140
+ next: resultNext(
141
+ "outside",
142
+ "The target resolved to no node, so the graph holds no path to it.",
143
+ ),
144
+ };
116
145
  }
117
146
  const found = findPath(
118
147
  graph,
@@ -124,13 +153,42 @@ export function runTrace(
124
153
  );
125
154
  const path = found?.path ?? [];
126
155
  const hops = found?.hops ?? [];
156
+ const junctions =
157
+ hops.length > 0
158
+ ? []
159
+ : junctionsBetween(graph, start.node.id, target.node.id, focus);
127
160
  return {
128
- ...base,
129
- start: summary(graph, start.node),
130
- target: summary(graph, target.node),
131
- hops,
132
- path: path.map((node, i) => summary(graph, node, i, false, true)),
133
- steps: traceSteps(graph, hops),
161
+ result: {
162
+ ...base,
163
+ start: summary(graph, start.node),
164
+ target: summary(graph, target.node),
165
+ hops,
166
+ path: path.map((node, i) => summary(graph, node, i, false, true)),
167
+ steps: traceSteps(graph, hops),
168
+ ...(junctions.length > 0 ? { junctions } : {}),
169
+ },
170
+ // An empty path is a fact, not an answer, and the old message called it
171
+ // one: "its path nodes and evidence ranges are what the graph holds
172
+ // between the two ends" — of a result that held nothing. The two ends do
173
+ // not call each other, which in an event-driven codebase is the common
174
+ // case: a pointer handler emits, an emitter's `emit()` runs listeners a
175
+ // registration put in an array, and no call edge crosses that array. The
176
+ // callers of the target are the way across, and the graph has them, so
177
+ // say which call to make instead of handing back an empty result dressed
178
+ // as the answer. Excalidraw's tour spent eleven calls finding this out.
179
+ next:
180
+ hops.length > 0
181
+ ? pathNext
182
+ : junctions.length > 0
183
+ ? resultNext(
184
+ "inspect",
185
+ "No call path runs between the two ends — a callback stands between them (an event emitter, a subscription, a lifecycle hook), and no call edge crosses one. `junctions` names the symbols both ends touch, which is the seam: trace the junction to see who registers on it and who fires it.",
186
+ "trace",
187
+ )
188
+ : resultNext(
189
+ "outside",
190
+ "No call path runs from the start to the target and they touch nothing in common, so the graph holds no connection between them.",
191
+ ),
134
192
  };
135
193
  }
136
194
 
@@ -151,7 +209,9 @@ export function runTrace(
151
209
  }
152
210
  const edges = orderedEdges(
153
211
  graph,
154
- reverse ? graph.incoming(id) : graph.outgoing(id),
212
+ reverse
213
+ ? graph.incoming(id)
214
+ : [...graph.outgoing(id), ...dispatchEdges(graph, id, focus)],
155
215
  direction,
156
216
  reverse,
157
217
  );
@@ -169,11 +229,6 @@ export function runTrace(
169
229
  };
170
230
  const evidence = edgeEvidenceOf(edge);
171
231
  if (evidence !== undefined) hop.evidence = evidence;
172
- const aliases = accessAliasesFor(
173
- graph.node(edge.to),
174
- edgeEvidenceTextOf(edge),
175
- );
176
- if (aliases !== undefined) hop.aliases = aliases;
177
232
  // A back-edge to the start or an already-reached node: record the hop;
178
233
  // its endpoints are already represented.
179
234
  if (visited.has(otherId)) {
@@ -201,20 +256,19 @@ export function runTrace(
201
256
  }
202
257
 
203
258
  return {
204
- type: "trace",
205
- start: summary(graph, start.node),
206
- direction,
207
- hops,
208
- reached: [...reached.values()],
209
- steps: traceSteps(graph, hops),
259
+ result: {
260
+ type: "trace",
261
+ start: summary(graph, start.node),
262
+ direction,
263
+ hops,
264
+ reached: [...reached.values()],
265
+ steps: traceSteps(graph, hops),
266
+ truncated,
267
+ },
210
268
  next: resultNext(
211
269
  "answer",
212
- "Steps, hops, reached nodes, and evidence ranges are the flow answer surface.",
270
+ "Steps, hops, reached nodes, and evidence ranges are the flow the graph holds from this start.",
213
271
  ),
214
- guide: resultGuide(
215
- "Use steps, hops, reached nodes, and evidence ranges as the flow answer or reading-list anchor.",
216
- ),
217
- truncated,
218
272
  };
219
273
  }
220
274
 
@@ -231,9 +285,7 @@ function traceSteps(
231
285
  hop.evidence === undefined
232
286
  ? ""
233
287
  : ` at ${hop.evidence.file}:${hop.evidence.startLine}`;
234
- const aliases =
235
- hop.aliases === undefined ? "" : ` aliases ${hop.aliases.join(", ")}`;
236
- return `${lhs} -[${hop.kind}${evidence}${aliases}]-> ${rhs}`;
288
+ return `${lhs} -[${hop.kind}${evidence}]-> ${rhs}`;
237
289
  });
238
290
  }
239
291
 
@@ -242,6 +294,94 @@ function traceSteps(
242
294
  * structural) forward edges, breadth-first, or null when `targetId` is not
243
295
  * reachable within maxDepth. Returns the nodes in order and the hops between.
244
296
  */
297
+ /**
298
+ * The symbols both ends of an unreachable path touch.
299
+ *
300
+ * A call graph cannot cross a callback: the registration hands a listener to an
301
+ * emitter, and `emit()` runs whatever the registration put in an array. But the
302
+ * registration and the emit both reference the emitter, and those are edges the
303
+ * checker resolved — Excalidraw's `App.componentDidMount` and its
304
+ * `Store.emitDurableIncrement` both touch `Store.onDurableIncrementEmitter`,
305
+ * which is the exact seam the path walk stops at.
306
+ *
307
+ * So when the path is empty, name what the two ends share. It is not a path and
308
+ * the result says so; it is the symbol to inspect next, with the two edges that
309
+ * make it the seam. Nothing here is matched or inferred: a junction is an edge
310
+ * from each end to the same node.
311
+ */
312
+ function junctionsBetween(
313
+ graph: TtscGraphMemory,
314
+ startId: string,
315
+ targetId: string,
316
+ focus: ITtscGraphTrace.IRequest["focus"],
317
+ ): ITtscGraphTrace.IJunction[] {
318
+ const startTouches = touchedBy(graph, startId, focus);
319
+ const targetTouches = touchedBy(graph, targetId, focus);
320
+ const out: ITtscGraphTrace.IJunction[] = [];
321
+ for (const [id, fromStart] of startTouches) {
322
+ const fromTarget = targetTouches.get(id);
323
+ if (fromTarget === undefined) continue;
324
+ const node = graph.node(id);
325
+ if (node === undefined || node.kind === "file" || isExternalNode(node))
326
+ continue;
327
+ out.push({
328
+ id: node.id,
329
+ name: node.qualifiedName ?? node.name,
330
+ kind: node.kind,
331
+ file: node.file,
332
+ ...(node.evidence?.startLine !== undefined
333
+ ? { line: node.evidence.startLine }
334
+ : {}),
335
+ fromStart,
336
+ fromTarget,
337
+ });
338
+ }
339
+ // A shared leaf helper is noise; a shared emitter, store, or registry is the
340
+ // seam. What both ends hold onto rather than merely call is the thing standing
341
+ // between them, so state comes first.
342
+ out.sort((a, b) => junctionRank(b) - junctionRank(a));
343
+ return out.slice(0, MAX_JUNCTIONS);
344
+ }
345
+
346
+ const MAX_JUNCTIONS = 4;
347
+
348
+ /** How much a shared symbol looks like a seam rather than a shared utility. */
349
+ function junctionRank(junction: ITtscGraphTrace.IJunction): number {
350
+ let rank = 0;
351
+ if (junction.kind === "variable" || junction.kind === "property") rank += 3;
352
+ if (junction.fromStart.kind === "accesses") rank += 2;
353
+ if (junction.fromTarget.kind === "accesses") rank += 2;
354
+ return rank;
355
+ }
356
+
357
+ /** Every node an end touches, with the edge that touches it. */
358
+ function touchedBy(
359
+ graph: TtscGraphMemory,
360
+ id: string,
361
+ focus: ITtscGraphTrace.IRequest["focus"],
362
+ ): Map<string, ITtscGraphTrace.IJunctionEdge> {
363
+ const touched = new Map<string, ITtscGraphTrace.IJunctionEdge>();
364
+ for (const edge of graph.outgoing(id)) {
365
+ if (!traversable(edge.kind, focus)) continue;
366
+ if (!touched.has(edge.to))
367
+ touched.set(edge.to, {
368
+ kind: edge.kind,
369
+ outgoing: true,
370
+ ...(edge.evidence !== undefined ? { evidence: edge.evidence } : {}),
371
+ });
372
+ }
373
+ for (const edge of graph.incoming(id)) {
374
+ if (!traversable(edge.kind, focus)) continue;
375
+ if (!touched.has(edge.from))
376
+ touched.set(edge.from, {
377
+ kind: edge.kind,
378
+ outgoing: false,
379
+ ...(edge.evidence !== undefined ? { evidence: edge.evidence } : {}),
380
+ });
381
+ }
382
+ return touched;
383
+ }
384
+
245
385
  function findPath(
246
386
  graph: TtscGraphMemory,
247
387
  startId: string,
@@ -259,7 +399,6 @@ function findPath(
259
399
  via: string;
260
400
  kind: string;
261
401
  evidence?: ITtscGraphEvidence;
262
- evidenceText?: string;
263
402
  }
264
403
  >();
265
404
  const visited = new Set<string>([startId]);
@@ -268,7 +407,10 @@ function findPath(
268
407
  const next: Array<{ id: string; depth: number }> = [];
269
408
  for (const { id, depth } of queue) {
270
409
  if (depth >= maxDepth) continue;
271
- for (const edge of graph.outgoing(id)) {
410
+ for (const edge of [
411
+ ...graph.outgoing(id),
412
+ ...dispatchEdges(graph, id, focus),
413
+ ]) {
272
414
  if (!traversable(edge.kind, focus)) continue;
273
415
  const otherId = edge.to;
274
416
  if (visited.has(otherId)) continue;
@@ -281,7 +423,6 @@ function findPath(
281
423
  via: id,
282
424
  kind: edge.kind,
283
425
  evidence,
284
- evidenceText: edgeEvidenceTextOf(edge),
285
426
  });
286
427
  if (otherId === targetId) {
287
428
  const ids: string[] = [otherId];
@@ -309,8 +450,6 @@ function findPath(
309
450
  };
310
451
  if (parentEdge?.evidence !== undefined)
311
452
  hop.evidence = parentEdge.evidence;
312
- const aliases = accessAliasesFor(node, parentEdge?.evidenceText);
313
- if (aliases !== undefined) hop.aliases = aliases;
314
453
  hops.push(hop);
315
454
  }
316
455
  return { path, hops };
@@ -417,6 +556,55 @@ function summary(
417
556
  return out;
418
557
  }
419
558
 
559
+ /**
560
+ * The implementations a call that lands here actually runs.
561
+ *
562
+ * A call resolved to an abstract method or an interface member lands on a
563
+ * declaration with no body. A forward walk stops there — and the code that
564
+ * executes is one _incoming_ `overrides`/`implements` edge away, which no
565
+ * forward traversal crosses. NestJS's whole request pipeline sits behind one:
566
+ * `ContextCreator.createContext` calls the abstract `createConcreteContext`,
567
+ * and the guards, pipes and interceptors contexts are its overrides, so the
568
+ * graph said a request reaches an abstract declaration and stops, and the guard
569
+ * it runs was reachable from nothing but its own unit test. Between 1% and 8%
570
+ * of every called symbol in the benchmark projects is such a declaration; every
571
+ * codebase with an abstract base, a strategy, an adapter or a visitor has
572
+ * them.
573
+ *
574
+ * So the walk dispatches: a called declaration with no body continues in the
575
+ * implementations that have one, as a `dispatches` hop cited at the
576
+ * implementation — which is the fact, since the call site named the base and
577
+ * the runtime lands in the override.
578
+ */
579
+ function dispatchEdges(
580
+ graph: TtscGraphMemory,
581
+ id: string,
582
+ focus: ITtscGraphTrace.IRequest["focus"],
583
+ ): ITtscGraphEdge[] {
584
+ if (focus === "types" || hasExecutionBody(graph, id)) return [];
585
+ const out: ITtscGraphEdge[] = [];
586
+ for (const edge of graph.incoming(id)) {
587
+ if (!DISPATCH_KINDS.has(edge.kind)) continue;
588
+ const implementation = graph.node(edge.from);
589
+ if (implementation === undefined || !hasExecutionBody(graph, edge.from))
590
+ continue;
591
+ out.push({
592
+ from: id,
593
+ to: edge.from,
594
+ kind: "dispatches",
595
+ ...(edge.evidence !== undefined ? { evidence: edge.evidence } : {}),
596
+ });
597
+ }
598
+ return out.length >= DISPATCH_HUB ? [] : out;
599
+ }
600
+
601
+ /** Whether the declaration has a body: something it calls, reads, or renders. */
602
+ function hasExecutionBody(graph: TtscGraphMemory, id: string): boolean {
603
+ for (const edge of graph.outgoing(id))
604
+ if (EXECUTION_KINDS.has(edge.kind)) return true;
605
+ return false;
606
+ }
607
+
420
608
  /** An edge the trace should follow: a real dependency, not a structural edge. */
421
609
  function traversable(
422
610
  kind: string,
@@ -425,6 +613,7 @@ function traversable(
425
613
  if (kind === "contains" || kind === "exports" || kind === "imports") {
426
614
  return false;
427
615
  }
616
+ if (kind === "dispatches") return focus !== "types";
428
617
  if (focus === "execution") {
429
618
  return (
430
619
  kind === "calls" ||
@@ -449,6 +638,11 @@ function edgeKindRank(kind: string): number {
449
638
  switch (kind) {
450
639
  case "calls":
451
640
  return 0;
641
+ // Where a call landed on a declaration, the implementation it dispatches to
642
+ // is the continuation of that call, not an afterthought behind the
643
+ // declaration's type references.
644
+ case "dispatches":
645
+ return 1;
452
646
  case "instantiates":
453
647
  return 1;
454
648
  case "renders":