@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.
- package/lib/TtscGraphApplication.js +5 -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 +25 -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 +423 -171
- 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 +14 -0
- package/lib/model/loadGraph.js +256 -143
- 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/resolveGraphBinary.d.ts +9 -3
- package/lib/resolveGraphBinary.js +9 -3
- package/lib/resolveGraphBinary.js.map +1 -1
- package/lib/server/createServer.js +22 -11
- 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 +56 -14
- package/lib/server/resultAudit.js +90 -24
- 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 +9 -2
- package/lib/server/runDetails.js +132 -157
- 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/ITtscGraphApplication.d.ts +8 -7
- package/lib/structures/ITtscGraphDetails.d.ts +18 -8
- package/lib/structures/ITtscGraphDump.d.ts +13 -13
- package/lib/structures/ITtscGraphNode.d.ts +71 -0
- package/lib/structures/ITtscGraphTour.d.ts +1 -1
- package/lib/structures/ITtscGraphTrace.d.ts +1 -1
- package/lib/view.js +18 -28
- package/lib/view.js.map +1 -1
- package/package.json +7 -4
- package/src/TtscGraphApplication.ts +8 -1
- package/src/bin.ts +13 -3
- package/src/index.ts +35 -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 +253 -53
- package/src/model/TtscGraphSourceReader.ts +117 -0
- package/src/model/loadGraph.ts +6 -1
- package/src/reduce.ts +136 -31
- package/src/resolveGraphBinary.ts +9 -3
- 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 +93 -23
- package/src/server/resultNext.ts +6 -0
- package/src/server/runDetails.ts +134 -165
- 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/ITtscGraphApplication.ts +8 -7
- package/src/structures/ITtscGraphDetails.ts +18 -8
- package/src/structures/ITtscGraphDump.ts +13 -13
- package/src/structures/ITtscGraphNode.ts +80 -0
- package/src/structures/ITtscGraphTour.ts +1 -1
- package/src/structures/ITtscGraphTrace.ts +1 -1
- package/src/view.ts +25 -23
package/src/server/runTrace.ts
CHANGED
|
@@ -3,7 +3,7 @@ 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 { isExternalNode, isTestPath } from "./pathPolicy";
|
|
6
|
+
import { isDeclarationFile, isExternalNode, isTestPath } from "./pathPolicy";
|
|
7
7
|
import { resolveGraphHandle } from "./resolveHandle";
|
|
8
8
|
import { IRunnerOutput, resultNext } from "./resultNext";
|
|
9
9
|
import { edgeEvidenceOf, signatureOf } from "./runDetails";
|
|
@@ -20,15 +20,23 @@ const MAX_OPEN_DEPTH = 8;
|
|
|
20
20
|
const MAX_OPEN_NODES = 32;
|
|
21
21
|
const MAX_IMPACT_DEPTH = 4;
|
|
22
22
|
const MAX_IMPACT_NODES = 16;
|
|
23
|
+
// Path mode walks further than an open trace because it is looking for one
|
|
24
|
+
// named end, not building a picture. The cap is the one the request contract
|
|
25
|
+
// publishes for path mode.
|
|
26
|
+
const MAX_PATH_DEPTH = 12;
|
|
23
27
|
const MAX_HOPS_PER_NODE = 2;
|
|
24
28
|
const MAX_STEPS = 12;
|
|
25
|
-
const EXECUTION_KINDS = new Set<string>([
|
|
26
|
-
"calls",
|
|
27
|
-
"instantiates",
|
|
28
|
-
"accesses",
|
|
29
|
-
"renders",
|
|
30
|
-
]);
|
|
31
29
|
const DISPATCH_KINDS = new Set<string>(["overrides", "implements"]);
|
|
30
|
+
// A declaration whose kind is a type surface never carries a body, and an
|
|
31
|
+
// external leaf carries one the graph deliberately does not hold.
|
|
32
|
+
const BODYLESS_KINDS = new Set<string>([
|
|
33
|
+
"interface",
|
|
34
|
+
"type",
|
|
35
|
+
"external_symbol",
|
|
36
|
+
]);
|
|
37
|
+
// `abstract` and `declare` are the two keywords that take the body away from a
|
|
38
|
+
// declaration that would otherwise have to have one.
|
|
39
|
+
const BODYLESS_MODIFIERS = new Set<string>(["abstract", "declare"]);
|
|
32
40
|
// An interface the codebase implements everywhere — a disposable, a listener, a
|
|
33
41
|
// lifecycle hook — is not a step in one flow, and naming its implementors is a
|
|
34
42
|
// dump of the codebase rather than an answer. Past this many, the declaration
|
|
@@ -143,18 +151,24 @@ export function runTrace(
|
|
|
143
151
|
),
|
|
144
152
|
};
|
|
145
153
|
}
|
|
146
|
-
const
|
|
154
|
+
const pathDepth = bound(props.maxDepth, MAX_PATH_DEPTH, 1, MAX_PATH_DEPTH);
|
|
155
|
+
const search = findPath(
|
|
147
156
|
graph,
|
|
148
157
|
start.node.id,
|
|
149
158
|
target.node.id,
|
|
150
|
-
|
|
159
|
+
pathDepth,
|
|
151
160
|
focus,
|
|
152
161
|
includeExternal,
|
|
153
162
|
);
|
|
154
|
-
const
|
|
155
|
-
const
|
|
163
|
+
const hasPath = search.found !== undefined;
|
|
164
|
+
const path = search.found?.path ?? [];
|
|
165
|
+
const hops = search.found?.hops ?? [];
|
|
166
|
+
// Junctions explain an absence, so they are only computed once absence is
|
|
167
|
+
// established: a walk the depth bound stopped has not established one, and
|
|
168
|
+
// presenting a shared symbol as "the seam" when a direct call path may run
|
|
169
|
+
// past the bound sends the caller to a seam that is not there.
|
|
156
170
|
const junctions =
|
|
157
|
-
|
|
171
|
+
hasPath || search.bounded
|
|
158
172
|
? []
|
|
159
173
|
: junctionsBetween(graph, start.node.id, target.node.id, focus);
|
|
160
174
|
return {
|
|
@@ -167,18 +181,23 @@ export function runTrace(
|
|
|
167
181
|
steps: traceSteps(graph, hops),
|
|
168
182
|
...(junctions.length > 0 ? { junctions } : {}),
|
|
169
183
|
},
|
|
170
|
-
//
|
|
184
|
+
// A missing path is a fact, not an answer, and the old message called it
|
|
171
185
|
// one: "its path nodes and evidence ranges are what the graph holds
|
|
172
|
-
// between the two ends" — of a result that held nothing.
|
|
173
|
-
//
|
|
174
|
-
//
|
|
186
|
+
// between the two ends" — of a result that held nothing. `findPath` says
|
|
187
|
+
// which fact it is; zero hops is instead the valid path from a node to
|
|
188
|
+
// itself. A walk the depth bound stopped establishes nothing at all, so
|
|
189
|
+
// it reports the bound. A walk that ran out of eligible graph did
|
|
190
|
+
// establish an absence, and distinct nodes without a path do not call
|
|
191
|
+
// each other, which in an event-driven codebase is the common case: a
|
|
192
|
+
// pointer handler emits, an emitter's `emit()` runs listeners a
|
|
175
193
|
// registration put in an array, and no call edge crosses that array. The
|
|
176
194
|
// callers of the target are the way across, and the graph has them, so
|
|
177
195
|
// say which call to make instead of handing back an empty result dressed
|
|
178
196
|
// as the answer. Excalidraw's tour spent eleven calls finding this out.
|
|
179
|
-
next:
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
next: hasPath
|
|
198
|
+
? pathNext
|
|
199
|
+
: search.bounded
|
|
200
|
+
? resultNext("inspect", boundedPathReason(pathDepth), "trace")
|
|
182
201
|
: junctions.length > 0
|
|
183
202
|
? resultNext(
|
|
184
203
|
"inspect",
|
|
@@ -203,24 +222,58 @@ export function runTrace(
|
|
|
203
222
|
while (queue.length > 0) {
|
|
204
223
|
const next: Array<{ id: string; depth: number }> = [];
|
|
205
224
|
for (const { id, depth } of queue) {
|
|
206
|
-
|
|
207
|
-
truncated = true;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
const edges = orderedEdges(
|
|
225
|
+
const { edges: candidates, omitted } = traceEdges(
|
|
211
226
|
graph,
|
|
212
|
-
|
|
213
|
-
? graph.incoming(id)
|
|
214
|
-
: [...graph.outgoing(id), ...dispatchEdges(graph, id, focus)],
|
|
215
|
-
direction,
|
|
227
|
+
id,
|
|
216
228
|
reverse,
|
|
229
|
+
focus,
|
|
217
230
|
);
|
|
231
|
+
// A hop the hub bound withheld is omitted whatever the depth budget does
|
|
232
|
+
// next, so the flag is set before the boundary check consumes the step.
|
|
233
|
+
if (
|
|
234
|
+
!truncated &&
|
|
235
|
+
omitted.some(
|
|
236
|
+
(edge) =>
|
|
237
|
+
eligibleTraceEndpoint(
|
|
238
|
+
graph,
|
|
239
|
+
edge,
|
|
240
|
+
reverse,
|
|
241
|
+
focus,
|
|
242
|
+
includeExternal,
|
|
243
|
+
) !== undefined,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
truncated = true;
|
|
247
|
+
if (depth >= maxDepth) {
|
|
248
|
+
// Reaching the configured boundary does not itself omit data. The
|
|
249
|
+
// response is truncated only when the selected walk has another hop
|
|
250
|
+
// (and, for an unseen endpoint, another node) beyond the boundary.
|
|
251
|
+
if (
|
|
252
|
+
candidates.some(
|
|
253
|
+
(edge) =>
|
|
254
|
+
eligibleTraceEndpoint(
|
|
255
|
+
graph,
|
|
256
|
+
edge,
|
|
257
|
+
reverse,
|
|
258
|
+
focus,
|
|
259
|
+
includeExternal,
|
|
260
|
+
) !== undefined,
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
truncated = true;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const edges = orderedEdges(graph, candidates, direction, reverse);
|
|
218
267
|
for (const edge of edges) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
268
|
+
const endpoint = eligibleTraceEndpoint(
|
|
269
|
+
graph,
|
|
270
|
+
edge,
|
|
271
|
+
reverse,
|
|
272
|
+
focus,
|
|
273
|
+
includeExternal,
|
|
274
|
+
);
|
|
275
|
+
if (endpoint === undefined) continue;
|
|
276
|
+
const { otherId, other } = endpoint;
|
|
224
277
|
const hop: ITtscGraphTrace.IHop = {
|
|
225
278
|
from: edge.from,
|
|
226
279
|
to: edge.to,
|
|
@@ -272,6 +325,72 @@ export function runTrace(
|
|
|
272
325
|
};
|
|
273
326
|
}
|
|
274
327
|
|
|
328
|
+
interface ITraceEndpoint {
|
|
329
|
+
otherId: string;
|
|
330
|
+
other: ITtscGraphNode;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Candidate edges in the selected direction before focus and node policy,
|
|
335
|
+
* together with the dispatch hops a bound withheld.
|
|
336
|
+
*
|
|
337
|
+
* The two have to travel together. A hub-suppressed fanout used to arrive as an
|
|
338
|
+
* empty dispatch list, which reads exactly like "this declaration dispatches to
|
|
339
|
+
* nothing", and the caller then reported a complete result while eligible hops
|
|
340
|
+
* had been dropped.
|
|
341
|
+
*/
|
|
342
|
+
function traceEdges(
|
|
343
|
+
graph: TtscGraphMemory,
|
|
344
|
+
id: string,
|
|
345
|
+
reverse: boolean,
|
|
346
|
+
focus: ITtscGraphTrace.IRequest["focus"],
|
|
347
|
+
): { edges: readonly ITtscGraphEdge[]; omitted: readonly ITtscGraphEdge[] } {
|
|
348
|
+
const edges = reverse ? graph.incoming(id) : graph.outgoing(id);
|
|
349
|
+
const dispatched = reverse
|
|
350
|
+
? reverseDispatchEdges(graph, id, focus)
|
|
351
|
+
: dispatchEdges(graph, id, focus);
|
|
352
|
+
// Nothing to add is the common case, and a walk visits the nodes with the
|
|
353
|
+
// largest edge lists, so hand back the stored list rather than a copy of it.
|
|
354
|
+
return {
|
|
355
|
+
edges:
|
|
356
|
+
dispatched.selected.length === 0
|
|
357
|
+
? edges
|
|
358
|
+
: [...edges, ...dispatched.selected],
|
|
359
|
+
omitted: dispatched.omitted,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** A dispatch fanout split into what the walk follows and what a bound withheld. */
|
|
364
|
+
interface IDispatchSelection {
|
|
365
|
+
readonly selected: readonly ITtscGraphEdge[];
|
|
366
|
+
readonly omitted: readonly ITtscGraphEdge[];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Returned by every no-dispatch path rather than allocated per call, which is
|
|
370
|
+
// why the lists above are readonly: one shared value that anything could push
|
|
371
|
+
// onto would leak a hop from one node's walk into another's.
|
|
372
|
+
const NO_DISPATCH: IDispatchSelection = { selected: [], omitted: [] };
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The endpoint the selected open trace would represent if no result bound
|
|
376
|
+
* stopped it. The depth probe and traversal share this decision so focus,
|
|
377
|
+
* external nodes, file nodes, and direction cannot disagree about omission.
|
|
378
|
+
*/
|
|
379
|
+
function eligibleTraceEndpoint(
|
|
380
|
+
graph: TtscGraphMemory,
|
|
381
|
+
edge: ITtscGraphEdge,
|
|
382
|
+
reverse: boolean,
|
|
383
|
+
focus: ITtscGraphTrace.IRequest["focus"],
|
|
384
|
+
includeExternal: boolean,
|
|
385
|
+
): ITraceEndpoint | undefined {
|
|
386
|
+
if (!traversable(edge.kind, focus)) return undefined;
|
|
387
|
+
const otherId = reverse ? edge.from : edge.to;
|
|
388
|
+
const other = graph.node(otherId);
|
|
389
|
+
if (other === undefined || other.kind === "file") return undefined;
|
|
390
|
+
if (!includeExternal && isExternalNode(other)) return undefined;
|
|
391
|
+
return { otherId, other };
|
|
392
|
+
}
|
|
393
|
+
|
|
275
394
|
function traceSteps(
|
|
276
395
|
graph: TtscGraphMemory,
|
|
277
396
|
hops: ITtscGraphTrace.IHop[],
|
|
@@ -289,11 +408,6 @@ function traceSteps(
|
|
|
289
408
|
});
|
|
290
409
|
}
|
|
291
410
|
|
|
292
|
-
/**
|
|
293
|
-
* The shortest dependency path from `startId` to `targetId` over real (non-
|
|
294
|
-
* structural) forward edges, breadth-first, or null when `targetId` is not
|
|
295
|
-
* reachable within maxDepth. Returns the nodes in order and the hops between.
|
|
296
|
-
*/
|
|
297
411
|
/**
|
|
298
412
|
* The symbols both ends of an unreachable path touch.
|
|
299
413
|
*
|
|
@@ -382,6 +496,57 @@ function touchedBy(
|
|
|
382
496
|
return touched;
|
|
383
497
|
}
|
|
384
498
|
|
|
499
|
+
/**
|
|
500
|
+
* What to say when the depth bound, not the graph, ended a path search.
|
|
501
|
+
*
|
|
502
|
+
* The caller asked a bounded question and the old answer returned a claim about
|
|
503
|
+
* the whole graph — "they touch nothing in common, so the graph holds no
|
|
504
|
+
* connection between them" — which is the worst thing an index can say wrongly:
|
|
505
|
+
* the caller stops asking and either reads files or concludes the dependency is
|
|
506
|
+
* not there. So report the boundary, and make the continuation one the caller
|
|
507
|
+
* can actually take. At the 12-hop ceiling there is no larger `maxDepth` to
|
|
508
|
+
* retry with, and a message that only invites one would be a dead end of its
|
|
509
|
+
* own; two bounded walks from opposite ends cover twice the distance and are
|
|
510
|
+
* requests the tool already answers.
|
|
511
|
+
*/
|
|
512
|
+
function boundedPathReason(depth: number): string {
|
|
513
|
+
return (
|
|
514
|
+
`No path was found within the requested depth of ${depth}, but the walk stopped on that bound with eligible graph still ahead of it. ` +
|
|
515
|
+
`This is a boundary, not an absence: the two ends may be connected further out, and nothing here says they are not. ` +
|
|
516
|
+
(depth < MAX_PATH_DEPTH
|
|
517
|
+
? `Re-run the same path request with a larger \`maxDepth\` (up to ${MAX_PATH_DEPTH}).`
|
|
518
|
+
: `\`maxDepth\` is already at its ${MAX_PATH_DEPTH}-hop maximum, so close the gap from both ends: trace forward from the start, trace the target with \`direction: "reverse"\`, then request the path between a symbol both results name.`)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* What a bounded shortest-path walk learned: the path when it found one, and
|
|
524
|
+
* otherwise whether the walk was stopped by the caller's depth bound or ran the
|
|
525
|
+
* eligible graph out. The two are not the same answer and the caller must not
|
|
526
|
+
* be told the second when only the first happened.
|
|
527
|
+
*/
|
|
528
|
+
interface IPathSearch {
|
|
529
|
+
/** The shortest eligible path and its hops, when one was found. */
|
|
530
|
+
found?: { path: ITtscGraphNode[]; hops: ITtscGraphTrace.IHop[] };
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* True when the walk stopped at `maxDepth` with an eligible, unvisited node
|
|
534
|
+
* still ahead of it, so nothing was proven about the two ends.
|
|
535
|
+
*/
|
|
536
|
+
bounded: boolean;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* The shortest dependency path from `startId` to `targetId` over real (non-
|
|
541
|
+
* structural) forward edges, breadth-first, within `maxDepth` hops.
|
|
542
|
+
*
|
|
543
|
+
* When no path is found, the walk reports whether the bound stopped it. A
|
|
544
|
+
* boundary is a fact about the request; absence is a fact about the graph, and
|
|
545
|
+
* a search that never reached the far side of its own bound has not established
|
|
546
|
+
* one. Eligibility is the open trace's, so a frontier made only of nodes the
|
|
547
|
+
* selected focus, the external-node policy, a file node, or an earlier visit
|
|
548
|
+
* already excluded is not a frontier and the walk is exhausted.
|
|
549
|
+
*/
|
|
385
550
|
function findPath(
|
|
386
551
|
graph: TtscGraphMemory,
|
|
387
552
|
startId: string,
|
|
@@ -389,10 +554,11 @@ function findPath(
|
|
|
389
554
|
maxDepth: number,
|
|
390
555
|
focus: ITtscGraphTrace.IRequest["focus"],
|
|
391
556
|
includeExternal: boolean,
|
|
392
|
-
):
|
|
557
|
+
): IPathSearch {
|
|
393
558
|
const startNode = graph.node(startId);
|
|
394
|
-
if (startNode === undefined) return
|
|
395
|
-
if (startId === targetId)
|
|
559
|
+
if (startNode === undefined) return { bounded: false };
|
|
560
|
+
if (startId === targetId)
|
|
561
|
+
return { found: { path: [startNode], hops: [] }, bounded: false };
|
|
396
562
|
const parent = new Map<
|
|
397
563
|
string,
|
|
398
564
|
{
|
|
@@ -402,21 +568,35 @@ function findPath(
|
|
|
402
568
|
}
|
|
403
569
|
>();
|
|
404
570
|
const visited = new Set<string>([startId]);
|
|
571
|
+
let bounded = false;
|
|
405
572
|
let queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
|
|
406
573
|
while (queue.length > 0) {
|
|
407
574
|
const next: Array<{ id: string; depth: number }> = [];
|
|
408
575
|
for (const { id, depth } of queue) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
576
|
+
// The forward step the open trace would take, built in one place so the
|
|
577
|
+
// two walks cannot disagree about what a step follows.
|
|
578
|
+
const { edges: candidates } = traceEdges(graph, id, false, focus);
|
|
579
|
+
if (depth >= maxDepth) {
|
|
580
|
+
if (
|
|
581
|
+
candidates.some(
|
|
582
|
+
(edge) =>
|
|
583
|
+
pathEndpoint(graph, edge, focus, includeExternal, visited) !==
|
|
584
|
+
undefined,
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
bounded = true;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
for (const edge of candidates) {
|
|
591
|
+
const endpoint = pathEndpoint(
|
|
592
|
+
graph,
|
|
593
|
+
edge,
|
|
594
|
+
focus,
|
|
595
|
+
includeExternal,
|
|
596
|
+
visited,
|
|
597
|
+
);
|
|
598
|
+
if (endpoint === undefined) continue;
|
|
599
|
+
const otherId = endpoint.otherId;
|
|
420
600
|
visited.add(otherId);
|
|
421
601
|
const evidence = edgeEvidenceOf(edge);
|
|
422
602
|
parent.set(otherId, {
|
|
@@ -452,14 +632,42 @@ function findPath(
|
|
|
452
632
|
hop.evidence = parentEdge.evidence;
|
|
453
633
|
hops.push(hop);
|
|
454
634
|
}
|
|
455
|
-
return { path, hops };
|
|
635
|
+
return { found: { path, hops }, bounded };
|
|
456
636
|
}
|
|
457
637
|
next.push({ id: otherId, depth: depth + 1 });
|
|
458
638
|
}
|
|
459
639
|
}
|
|
460
640
|
queue = next;
|
|
461
641
|
}
|
|
462
|
-
return
|
|
642
|
+
return { bounded };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* The node a path expansion would represent, or undefined when the selected
|
|
647
|
+
* policy or an earlier visit excludes it. The expansion and the boundary probe
|
|
648
|
+
* share this decision, so what the walk would have followed and what counts as
|
|
649
|
+
* unexplored graph beyond the bound cannot disagree.
|
|
650
|
+
*
|
|
651
|
+
* A path walk is always forward and, unlike the open trace, a node it already
|
|
652
|
+
* reached is not a continuation: the shortest path to it is already known, so a
|
|
653
|
+
* second arrival adds nothing to explore.
|
|
654
|
+
*/
|
|
655
|
+
function pathEndpoint(
|
|
656
|
+
graph: TtscGraphMemory,
|
|
657
|
+
edge: ITtscGraphEdge,
|
|
658
|
+
focus: ITtscGraphTrace.IRequest["focus"],
|
|
659
|
+
includeExternal: boolean,
|
|
660
|
+
visited: ReadonlySet<string>,
|
|
661
|
+
): ITraceEndpoint | undefined {
|
|
662
|
+
const endpoint = eligibleTraceEndpoint(
|
|
663
|
+
graph,
|
|
664
|
+
edge,
|
|
665
|
+
false,
|
|
666
|
+
focus,
|
|
667
|
+
includeExternal,
|
|
668
|
+
);
|
|
669
|
+
if (endpoint === undefined || visited.has(endpoint.otherId)) return undefined;
|
|
670
|
+
return endpoint;
|
|
463
671
|
}
|
|
464
672
|
|
|
465
673
|
function orderedEdges(
|
|
@@ -544,7 +752,7 @@ function summary(
|
|
|
544
752
|
}
|
|
545
753
|
if (depth !== undefined) out.depth = depth;
|
|
546
754
|
if (withSignature) {
|
|
547
|
-
const sig = signatureOf(graph
|
|
755
|
+
const sig = signatureOf(graph, node);
|
|
548
756
|
if (sig !== undefined) out.signature = sig;
|
|
549
757
|
}
|
|
550
758
|
if (withRoles) {
|
|
@@ -580,14 +788,36 @@ function dispatchEdges(
|
|
|
580
788
|
graph: TtscGraphMemory,
|
|
581
789
|
id: string,
|
|
582
790
|
focus: ITtscGraphTrace.IRequest["focus"],
|
|
583
|
-
):
|
|
584
|
-
if (focus === "types"
|
|
585
|
-
|
|
791
|
+
): IDispatchSelection {
|
|
792
|
+
if (focus === "types") return NO_DISPATCH;
|
|
793
|
+
// The checker relations first: almost no node has one, and reading a
|
|
794
|
+
// declaration's own facts walks its ownership chain, which is work worth
|
|
795
|
+
// doing only where there is something to dispatch to.
|
|
796
|
+
let relations: ITtscGraphEdge[] | undefined;
|
|
586
797
|
for (const edge of graph.incoming(id)) {
|
|
587
|
-
if (
|
|
798
|
+
if (DISPATCH_KINDS.has(edge.kind)) (relations ??= []).push(edge);
|
|
799
|
+
}
|
|
800
|
+
if (relations === undefined) return NO_DISPATCH;
|
|
801
|
+
const declaration = graph.node(id);
|
|
802
|
+
if (declaration === undefined || hasDeclarationBody(graph, declaration))
|
|
803
|
+
return NO_DISPATCH;
|
|
804
|
+
const out: ITtscGraphEdge[] = [];
|
|
805
|
+
// Per implementation, not per relation. A class may name one base in two
|
|
806
|
+
// heritage clauses — `class Impl extends Base implements Base` is legal — and
|
|
807
|
+
// the producer records the member pair once per clause, as `overrides` and as
|
|
808
|
+
// `implements`. That is one implementation and one crossing: emitting it
|
|
809
|
+
// twice would repeat the hop and count the same class twice against the hub
|
|
810
|
+
// cut.
|
|
811
|
+
const dispatched = new Set<string>();
|
|
812
|
+
for (const edge of relations) {
|
|
813
|
+
if (dispatched.has(edge.from)) continue;
|
|
588
814
|
const implementation = graph.node(edge.from);
|
|
589
|
-
if (
|
|
815
|
+
if (
|
|
816
|
+
implementation === undefined ||
|
|
817
|
+
!hasDeclarationBody(graph, implementation)
|
|
818
|
+
)
|
|
590
819
|
continue;
|
|
820
|
+
dispatched.add(edge.from);
|
|
591
821
|
out.push({
|
|
592
822
|
from: id,
|
|
593
823
|
to: edge.from,
|
|
@@ -595,16 +825,122 @@ function dispatchEdges(
|
|
|
595
825
|
...(edge.evidence !== undefined ? { evidence: edge.evidence } : {}),
|
|
596
826
|
});
|
|
597
827
|
}
|
|
598
|
-
|
|
828
|
+
// Above the hub cut the fanout stops being a trace and starts being a
|
|
829
|
+
// listing, so the walk does not follow it — but the hops are real and their
|
|
830
|
+
// absence is an omission the caller has to be able to report.
|
|
831
|
+
return out.length >= DISPATCH_HUB
|
|
832
|
+
? { selected: [], omitted: out }
|
|
833
|
+
: { selected: out, omitted: [] };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The declaration a change to this implementation is a change to.
|
|
838
|
+
*
|
|
839
|
+
* `dispatches` is one fact and both directions have to see it. Forward, a call
|
|
840
|
+
* that lands on a bodyless declaration continues in the implementation that
|
|
841
|
+
* runs; reverse, a change to that implementation is a change every caller of
|
|
842
|
+
* the declaration feels — and `impact` is the query an agent runs _before_
|
|
843
|
+
* editing a method. The checker relation is oriented implementation-to-base, so
|
|
844
|
+
* from the implementation it is an outgoing edge and a reverse walk, which only
|
|
845
|
+
* reads incoming edges, never sees it. Both halves of the path are one step
|
|
846
|
+
* away in a direction the traversal does not take.
|
|
847
|
+
*
|
|
848
|
+
* The synthetic edge is the forward one, unchanged, so eligibility, checker
|
|
849
|
+
* validity, and the hub cut cannot drift apart between the two directions: ask
|
|
850
|
+
* the base what it dispatches to, and keep the edges that land here.
|
|
851
|
+
*/
|
|
852
|
+
function reverseDispatchEdges(
|
|
853
|
+
graph: TtscGraphMemory,
|
|
854
|
+
id: string,
|
|
855
|
+
focus: ITtscGraphTrace.IRequest["focus"],
|
|
856
|
+
): IDispatchSelection {
|
|
857
|
+
if (focus === "types") return NO_DISPATCH;
|
|
858
|
+
const selected: ITtscGraphEdge[] = [];
|
|
859
|
+
const omitted: ITtscGraphEdge[] = [];
|
|
860
|
+
const bases = new Set<string>();
|
|
861
|
+
for (const edge of graph.outgoing(id)) {
|
|
862
|
+
if (!DISPATCH_KINDS.has(edge.kind) || bases.has(edge.to)) continue;
|
|
863
|
+
bases.add(edge.to);
|
|
864
|
+
const fanout = dispatchEdges(graph, edge.to, focus);
|
|
865
|
+
for (const dispatch of fanout.selected)
|
|
866
|
+
if (dispatch.to === id) selected.push(dispatch);
|
|
867
|
+
// The base's fanout is bounded as a whole, so a reverse walk that lands on
|
|
868
|
+
// a suppressed sibling has the same omission to report as the forward one.
|
|
869
|
+
for (const dispatch of fanout.omitted)
|
|
870
|
+
if (dispatch.to === id) omitted.push(dispatch);
|
|
871
|
+
}
|
|
872
|
+
return { selected, omitted };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Whether the declaration this node stands for has a body of its own.
|
|
877
|
+
*
|
|
878
|
+
* Having a body and naming a modeled dependency are different facts. Counting
|
|
879
|
+
* outgoing `calls`/`accesses`/`instantiates`/`renders` edges measures the
|
|
880
|
+
* second and answers as though it were the first, and it is wrong in both
|
|
881
|
+
* directions: an implementation whose body returns a literal, throws, or only
|
|
882
|
+
* moves locals around has degree zero and was refused as a dispatch target,
|
|
883
|
+
* while a concrete base method was promoted through its own override the moment
|
|
884
|
+
* its body stopped naming anything the graph models. Two graphs identical in
|
|
885
|
+
* every declaration fact then answered differently because of one statement
|
|
886
|
+
* inside a body.
|
|
887
|
+
*
|
|
888
|
+
* So read the declaration instead. A type surface has no body; `abstract` and
|
|
889
|
+
* `declare` take it away; an interface member and anything inside an ambient
|
|
890
|
+
* container never had one; a `.d.ts` declaration is ambient whether or not the
|
|
891
|
+
* keyword is written; and an external leaf has a body the graph deliberately
|
|
892
|
+
* does not hold. Everything else is a concrete declaration, which is a real
|
|
893
|
+
* destination and is never promoted through an override, whatever it calls.
|
|
894
|
+
*/
|
|
895
|
+
export function hasDeclarationBody(
|
|
896
|
+
graph: TtscGraphMemory,
|
|
897
|
+
node: ITtscGraphNode,
|
|
898
|
+
): boolean {
|
|
899
|
+
if (BODYLESS_KINDS.has(node.kind)) return false;
|
|
900
|
+
if (isExternalNode(node)) return false;
|
|
901
|
+
if (isDeclarationFile(node.file)) return false;
|
|
902
|
+
if (hasBodylessModifier(node)) return false;
|
|
903
|
+
return !inBodylessContainer(graph, node);
|
|
599
904
|
}
|
|
600
905
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
906
|
+
function hasBodylessModifier(node: ITtscGraphNode): boolean {
|
|
907
|
+
return node.modifiers?.some((m) => BODYLESS_MODIFIERS.has(m)) === true;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Whether an owner up the `contains` tree makes this declaration bodyless: an
|
|
912
|
+
* interface, or an ambient container. A member writes no keyword of its own —
|
|
913
|
+
* `declare` on a class or namespace is not repeated on what it holds — so the
|
|
914
|
+
* fact lives on the owner and the walk has to go get it.
|
|
915
|
+
*/
|
|
916
|
+
function inBodylessContainer(
|
|
917
|
+
graph: TtscGraphMemory,
|
|
918
|
+
node: ITtscGraphNode,
|
|
919
|
+
): boolean {
|
|
920
|
+
const seen = new Set<string>([node.id]);
|
|
921
|
+
let current: ITtscGraphNode | undefined = node;
|
|
922
|
+
while (current !== undefined) {
|
|
923
|
+
const container: ITtscGraphNode | undefined = containerOf(graph, current);
|
|
924
|
+
if (container === undefined || container.kind === "file") return false;
|
|
925
|
+
if (seen.has(container.id)) return false;
|
|
926
|
+
seen.add(container.id);
|
|
927
|
+
if (container.kind === "interface" || hasBodylessModifier(container))
|
|
928
|
+
return true;
|
|
929
|
+
current = container;
|
|
930
|
+
}
|
|
605
931
|
return false;
|
|
606
932
|
}
|
|
607
933
|
|
|
934
|
+
/** The declaration that owns this one, through the synthesized ownership tree. */
|
|
935
|
+
function containerOf(
|
|
936
|
+
graph: TtscGraphMemory,
|
|
937
|
+
node: ITtscGraphNode,
|
|
938
|
+
): ITtscGraphNode | undefined {
|
|
939
|
+
for (const edge of graph.incoming(node.id))
|
|
940
|
+
if (edge.kind === "contains") return graph.node(edge.from);
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
|
|
608
944
|
/** An edge the trace should follow: a real dependency, not a structural edge. */
|
|
609
945
|
function traversable(
|
|
610
946
|
kind: string,
|
|
@@ -169,13 +169,14 @@ export namespace ITtscGraphApplication {
|
|
|
169
169
|
* resolves to the type-checked program for the snapshot the call synced to,
|
|
170
170
|
* so opening a file it cites only returns a fact already in it.
|
|
171
171
|
*
|
|
172
|
-
* The audit is operation-aware. For the
|
|
173
|
-
* `
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
172
|
+
* The audit is operation-aware. For the walks from a named handle (`trace`,
|
|
173
|
+
* `overview`) it reports the result as the structure the graph holds,
|
|
174
|
+
* bounded where `truncated` says. For `details` it reports the two halves of
|
|
175
|
+
* a resolved symbol: its own shape returned whole, its fan-out returned as a
|
|
176
|
+
* slice with `trace` for the rest. For the ranked operations (`lookup`,
|
|
177
|
+
* `entrypoints`, `tour`) it adds that the selection is heuristic — matched,
|
|
178
|
+
* scored, ranked, and limited against the question — so the facts are
|
|
179
|
+
* verified but the shortlist's coverage is the caller's to judge.
|
|
179
180
|
*/
|
|
180
181
|
audit: string;
|
|
181
182
|
|
|
@@ -49,24 +49,26 @@ export namespace ITtscGraphDetails {
|
|
|
49
49
|
neighbors?: boolean;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Dependencies and dependents per side when `neighbors:true`. A small
|
|
53
|
+
* orientation slice by default; what uses a symbol grows with its
|
|
54
|
+
* popularity, so `trace` answers the whole "who uses this".
|
|
54
55
|
*
|
|
55
56
|
* @default 2
|
|
56
57
|
*/
|
|
57
58
|
neighborLimit?: number;
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* Owned members for a container or object literal. The complete outline by
|
|
62
|
+
* default — a class's members and an enum's are the symbol itself, so they
|
|
63
|
+
* are not sampled. Pass a number to cap.
|
|
63
64
|
*/
|
|
64
65
|
memberLimit?: number;
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
|
-
*
|
|
68
|
+
* Direct execution and type references per group. A small orientation slice
|
|
69
|
+
* by default; `trace` follows the whole fan-out.
|
|
68
70
|
*
|
|
69
|
-
* @default
|
|
71
|
+
* @default 2
|
|
70
72
|
*/
|
|
71
73
|
dependencyLimit?: number;
|
|
72
74
|
|
|
@@ -145,7 +147,15 @@ export namespace ITtscGraphDetails {
|
|
|
145
147
|
/** Concrete nodes that implement or override this interface/base member. */
|
|
146
148
|
implementedBy?: IReference[];
|
|
147
149
|
|
|
148
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* The complete value set a type alias or enum admits, in TypeScript source
|
|
152
|
+
* form (`"a"`, `1`, `true`, `null`) — the checker's resolved union members,
|
|
153
|
+
* not the quoted tokens that happened to fit in `signature`.
|
|
154
|
+
*
|
|
155
|
+
* Absent when the type has no enumerable value set. A `signature` is capped
|
|
156
|
+
* at the declaration head, so for a union or enum written across several
|
|
157
|
+
* lines this is the field that carries the members.
|
|
158
|
+
*/
|
|
149
159
|
literals?: string[];
|
|
150
160
|
|
|
151
161
|
/**
|