@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.
- package/README.md +231 -37
- package/lib/TtscGraphApplication.d.ts +1 -1
- package/lib/TtscGraphApplication.js +50 -17
- package/lib/TtscGraphApplication.js.map +1 -1
- package/lib/index.d.ts +5 -0
- package/lib/index.js +20 -1
- package/lib/index.js.map +1 -1
- package/lib/model/TtscGraphMemory.js +80 -22
- package/lib/model/TtscGraphMemory.js.map +1 -1
- package/lib/model/TtscGraphSession.js +92 -138
- package/lib/model/TtscGraphSession.js.map +1 -1
- package/lib/model/loadGraph.js +91 -138
- package/lib/model/loadGraph.js.map +1 -1
- package/lib/resolveGraphBinary.d.ts +4 -2
- package/lib/resolveGraphBinary.js +7 -3
- package/lib/resolveGraphBinary.js.map +1 -1
- package/lib/server/createServer.d.ts +16 -9
- package/lib/server/createServer.js +286 -190
- package/lib/server/createServer.js.map +1 -1
- package/lib/server/exportSurface.d.ts +26 -0
- package/lib/server/exportSurface.js +44 -0
- package/lib/server/exportSurface.js.map +1 -0
- package/lib/server/resolveHandle.d.ts +26 -1
- package/lib/server/resolveHandle.js +124 -4
- package/lib/server/resolveHandle.js.map +1 -1
- package/lib/server/resultAudit.d.ts +74 -0
- package/lib/server/resultAudit.js +110 -0
- package/lib/server/resultAudit.js.map +1 -0
- package/lib/server/resultNext.d.ts +9 -0
- package/lib/server/resultNext.js +11 -0
- package/lib/server/resultNext.js.map +1 -0
- package/lib/server/runDetails.d.ts +15 -3
- package/lib/server/runDetails.js +105 -23
- package/lib/server/runDetails.js.map +1 -1
- package/lib/server/runEntrypoints.d.ts +2 -1
- package/lib/server/runEntrypoints.js +17 -10
- package/lib/server/runEntrypoints.js.map +1 -1
- package/lib/server/runLookup.d.ts +2 -1
- package/lib/server/runLookup.js +21 -9
- package/lib/server/runLookup.js.map +1 -1
- package/lib/server/runOverview.d.ts +2 -1
- package/lib/server/runOverview.js +5 -4
- package/lib/server/runOverview.js.map +1 -1
- package/lib/server/runTour.d.ts +2 -1
- package/lib/server/runTour.js +707 -331
- package/lib/server/runTour.js.map +1 -1
- package/lib/server/runTrace.d.ts +2 -1
- package/lib/server/runTrace.js +234 -54
- package/lib/server/runTrace.js.map +1 -1
- package/lib/structures/ITtscGraphApplication.d.ts +122 -118
- package/lib/structures/ITtscGraphDecorator.d.ts +4 -7
- package/lib/structures/ITtscGraphDetails.d.ts +51 -43
- package/lib/structures/ITtscGraphDump.d.ts +32 -8
- package/lib/structures/ITtscGraphEdge.d.ts +4 -6
- package/lib/structures/ITtscGraphEntrypoints.d.ts +10 -27
- package/lib/structures/ITtscGraphEscape.d.ts +6 -16
- package/lib/structures/ITtscGraphEvidence.d.ts +5 -8
- package/lib/structures/ITtscGraphLookup.d.ts +9 -20
- package/lib/structures/ITtscGraphNext.d.ts +10 -7
- package/lib/structures/ITtscGraphNode.d.ts +17 -8
- package/lib/structures/ITtscGraphOverview.d.ts +10 -14
- package/lib/structures/ITtscGraphSpan.d.ts +29 -0
- package/lib/structures/{ITtscGraphDiagnostic.js → ITtscGraphSpan.js} +1 -1
- package/lib/structures/ITtscGraphSpan.js.map +1 -0
- package/lib/structures/ITtscGraphTour.d.ts +65 -23
- package/lib/structures/ITtscGraphTrace.d.ts +72 -49
- package/lib/structures/TtscGraphEdgeKind.d.ts +7 -1
- package/lib/structures/index.d.ts +1 -1
- package/lib/structures/index.js +1 -1
- package/lib/structures/index.js.map +1 -1
- package/lib/view.js +3 -1
- package/lib/view.js.map +1 -1
- package/package.json +4 -4
- package/src/TtscGraphApplication.ts +58 -27
- package/src/index.ts +19 -1
- package/src/model/TtscGraphMemory.ts +86 -22
- package/src/model/TtscGraphSession.ts +5 -1
- package/src/model/loadGraph.ts +3 -1
- package/src/resolveGraphBinary.ts +7 -3
- package/src/server/createServer.ts +24 -17
- package/src/server/exportSurface.ts +40 -0
- package/src/server/resolveHandle.ts +141 -4
- package/src/server/resultAudit.ts +109 -0
- package/src/server/resultNext.ts +22 -0
- package/src/server/runDetails.ts +112 -28
- package/src/server/runEntrypoints.ts +26 -17
- package/src/server/runLookup.ts +29 -18
- package/src/server/runOverview.ts +9 -10
- package/src/server/runTour.ts +764 -363
- package/src/server/runTrace.ts +265 -71
- package/src/structures/ITtscGraphApplication.ts +124 -118
- package/src/structures/ITtscGraphDecorator.ts +4 -7
- package/src/structures/ITtscGraphDetails.ts +60 -46
- package/src/structures/ITtscGraphDump.ts +37 -8
- package/src/structures/ITtscGraphEdge.ts +4 -6
- package/src/structures/ITtscGraphEntrypoints.ts +10 -30
- package/src/structures/ITtscGraphEscape.ts +6 -19
- package/src/structures/ITtscGraphEvidence.ts +5 -8
- package/src/structures/ITtscGraphLookup.ts +9 -22
- package/src/structures/ITtscGraphNext.ts +10 -7
- package/src/structures/ITtscGraphNode.ts +18 -8
- package/src/structures/ITtscGraphOverview.ts +10 -17
- package/src/structures/ITtscGraphSpan.ts +33 -0
- package/src/structures/ITtscGraphTour.ts +70 -28
- package/src/structures/ITtscGraphTrace.ts +82 -51
- package/src/structures/TtscGraphEdgeKind.ts +7 -0
- package/src/structures/index.ts +1 -1
- package/src/view.ts +3 -1
- package/lib/server/accessAliases.d.ts +0 -8
- package/lib/server/accessAliases.js +0 -53
- package/lib/server/accessAliases.js.map +0 -1
- package/lib/server/resultGuide.d.ts +0 -3
- package/lib/server/resultGuide.js +0 -16
- package/lib/server/resultGuide.js.map +0 -1
- package/lib/structures/ITtscGraphDiagnostic.d.ts +0 -27
- package/lib/structures/ITtscGraphDiagnostic.js.map +0 -1
- package/src/server/accessAliases.ts +0 -55
- package/src/server/resultGuide.ts +0 -20
- package/src/structures/ITtscGraphDiagnostic.ts +0 -34
package/src/server/runTrace.ts
CHANGED
|
@@ -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 {
|
|
10
|
-
import { edgeEvidenceOf,
|
|
8
|
+
import { IRunnerOutput, resultNext } from "./resultNext";
|
|
9
|
+
import { edgeEvidenceOf, signatureOf } from "./runDetails";
|
|
11
10
|
|
|
12
|
-
const DEFAULT_DEPTH =
|
|
13
|
-
const DEFAULT_MAX_NODES =
|
|
14
|
-
|
|
15
|
-
|
|
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 =
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
result: {
|
|
95
|
+
type: "trace",
|
|
96
|
+
direction,
|
|
97
|
+
hops: [],
|
|
98
|
+
reached: [],
|
|
99
|
+
truncated: false,
|
|
100
|
+
},
|
|
79
101
|
next: resultNext(
|
|
80
|
-
"
|
|
81
|
-
"The start handle did not resolve in the
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 {
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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":
|