@ttsc/graph 0.18.3 → 0.19.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/README.md +231 -36
- package/lib/TtscGraphApplication.d.ts +4 -3
- package/lib/TtscGraphApplication.js +54 -19
- package/lib/TtscGraphApplication.js.map +1 -1
- package/lib/index.d.ts +9 -4
- package/lib/index.js +24 -5
- 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.d.ts +36 -0
- package/lib/model/TtscGraphSession.js +457 -0
- package/lib/model/TtscGraphSession.js.map +1 -0
- package/lib/model/loadGraph.d.ts +3 -3
- package/lib/model/loadGraph.js +94 -141
- 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/server/startServer.d.ts +3 -2
- package/lib/server/startServer.js +13 -7
- package/lib/server/startServer.js.map +1 -1
- package/lib/structures/ITtscGraphApplication.d.ts +122 -114
- package/lib/structures/ITtscGraphDecorator.d.ts +4 -7
- package/lib/structures/ITtscGraphDetails.d.ts +51 -43
- package/lib/structures/ITtscGraphDump.d.ts +36 -11
- 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 +66 -31
- package/src/index.ts +23 -5
- package/src/model/TtscGraphMemory.ts +86 -22
- package/src/model/TtscGraphSession.ts +204 -0
- package/src/model/loadGraph.ts +6 -4
- 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/server/startServer.ts +13 -8
- package/src/structures/ITtscGraphApplication.ts +124 -114
- package/src/structures/ITtscGraphDecorator.ts +4 -7
- package/src/structures/ITtscGraphDetails.ts +60 -46
- package/src/structures/ITtscGraphDump.ts +41 -11
- 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/lib/server/runTour.js
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runTour = runTour;
|
|
4
|
+
const exportSurface_1 = require("./exportSurface");
|
|
4
5
|
const pathPolicy_1 = require("./pathPolicy");
|
|
5
|
-
const
|
|
6
|
+
const resolveHandle_1 = require("./resolveHandle");
|
|
7
|
+
const resultNext_1 = require("./resultNext");
|
|
6
8
|
const runDetails_1 = require("./runDetails");
|
|
7
9
|
const runEntrypoints_1 = require("./runEntrypoints");
|
|
8
10
|
const runTrace_1 = require("./runTrace");
|
|
9
11
|
const DEFAULT_LIMIT = 5;
|
|
10
12
|
const MAX_LIMIT = 5;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The share of a tour's seeds the symbols the caller named may take.
|
|
15
|
+
*
|
|
16
|
+
* Two authorities pick a tour, and neither is allowed to be the only one. Half
|
|
17
|
+
* the seeds go to what the caller says the answer is made of — it has read the
|
|
18
|
+
* question and the codebase's docs, and the tour has read neither. The other
|
|
19
|
+
* half goes to what the graph says is central to the question, and the names do
|
|
20
|
+
* not touch that ranking, because a caller cannot name what it does not know is
|
|
21
|
+
* there: Opus named ten symbols along RxJS's subscribe path, and `operate` —
|
|
22
|
+
* the head of the operator chain the question asked about, and the second seed
|
|
23
|
+
* of the same tour with no names at all — fell out of the tour it did not
|
|
24
|
+
* name.
|
|
25
|
+
*/
|
|
26
|
+
const NAMED_SHARE = 0.5;
|
|
27
|
+
const FLOW_SEEDS = 4;
|
|
28
|
+
/** How many ranked seeds deep to look for flows that actually move. */
|
|
29
|
+
const FLOW_SEED_CANDIDATES = 4;
|
|
13
30
|
const TEST_SEEDS = 3;
|
|
14
31
|
const MAX_FLOW_ANCHORS = 8;
|
|
15
32
|
const MAX_NEARBY = 10;
|
|
16
33
|
const MAX_TESTS = 8;
|
|
17
34
|
const MAX_READ_NEXT = 14;
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
// A public entry stands several hops above the code that does the work: an app
|
|
36
|
+
// factory calls a mount, which calls a renderer, which calls the patch. Three
|
|
37
|
+
// hops stopped at that boundary, and the model finished the chain by hand —
|
|
38
|
+
// "the tour stopped short of the actual patch engine", then four more calls.
|
|
39
|
+
// The flow reaches the work now.
|
|
40
|
+
// Two flows that land in the same places are one flow. Above this share of a
|
|
41
|
+
// candidate's reached set already told, it is a synonym of a flow the tour has.
|
|
42
|
+
const FLOW_OVERLAP = 0.6;
|
|
43
|
+
const TOUR_TRACE_MAX_DEPTH = 6;
|
|
44
|
+
const TOUR_TRACE_MAX_NODES = 18;
|
|
20
45
|
const STRUCTURAL_KINDS = new Set(["contains", "exports", "imports"]);
|
|
21
46
|
const EXECUTION_KINDS = new Set([
|
|
22
47
|
"calls",
|
|
@@ -34,89 +59,47 @@ const TOUR_SEED_KINDS = new Set([
|
|
|
34
59
|
"namespace",
|
|
35
60
|
"enum",
|
|
36
61
|
]);
|
|
37
|
-
const QUERY_STOP_WORDS = new Set([
|
|
38
|
-
"about",
|
|
39
|
-
"after",
|
|
40
|
-
"and",
|
|
41
|
-
"are",
|
|
42
|
-
"api",
|
|
43
|
-
"architecture",
|
|
44
|
-
"around",
|
|
45
|
-
"before",
|
|
46
|
-
"based",
|
|
47
|
-
"behavior",
|
|
48
|
-
"between",
|
|
49
|
-
"but",
|
|
50
|
-
"can",
|
|
51
|
-
"central",
|
|
52
|
-
"change",
|
|
53
|
-
"changes",
|
|
54
|
-
"code",
|
|
55
|
-
"does",
|
|
56
|
-
"for",
|
|
57
|
-
"first",
|
|
58
|
-
"find",
|
|
59
|
-
"flow",
|
|
60
|
-
"from",
|
|
61
|
-
"has",
|
|
62
|
-
"have",
|
|
63
|
-
"how",
|
|
64
|
-
"include",
|
|
65
|
-
"including",
|
|
66
|
-
"implementation",
|
|
67
|
-
"into",
|
|
68
|
-
"its",
|
|
69
|
-
"nearby",
|
|
70
|
-
"need",
|
|
71
|
-
"needs",
|
|
72
|
-
"new",
|
|
73
|
-
"next",
|
|
74
|
-
"path",
|
|
75
|
-
"paths",
|
|
76
|
-
"project",
|
|
77
|
-
"public",
|
|
78
|
-
"read",
|
|
79
|
-
"real",
|
|
80
|
-
"runtime",
|
|
81
|
-
"should",
|
|
82
|
-
"show",
|
|
83
|
-
"that",
|
|
84
|
-
"the",
|
|
85
|
-
"this",
|
|
86
|
-
"test",
|
|
87
|
-
"tests",
|
|
88
|
-
"trace",
|
|
89
|
-
"through",
|
|
90
|
-
"with",
|
|
91
|
-
"without",
|
|
92
|
-
"tour",
|
|
93
|
-
"typescript",
|
|
94
|
-
"user",
|
|
95
|
-
"what",
|
|
96
|
-
"where",
|
|
97
|
-
"which",
|
|
98
|
-
"work",
|
|
99
|
-
]);
|
|
100
62
|
/**
|
|
101
63
|
* Compose a repository-orientation/code-tour answer surface from existing graph
|
|
102
64
|
* operations. It returns selected symbols, flows, nearby edges, test anchors,
|
|
103
65
|
* and answer anchors without reading or embedding source bodies.
|
|
104
66
|
*/
|
|
105
|
-
function runTour(graph, props) {
|
|
106
|
-
const query =
|
|
67
|
+
function runTour(graph, props, question) {
|
|
68
|
+
const query = question.trim();
|
|
69
|
+
const named = namedNodesOf(graph, props.reinterpretations);
|
|
70
|
+
// The words that rank the tour are the caller's names, never the question's
|
|
71
|
+
// prose. Which words in a question are its keywords is the caller's judgement
|
|
72
|
+
// to make -- it read the question -- and the server used to make it instead,
|
|
73
|
+
// with a list of sixty-eight words it happened to think were filler.
|
|
74
|
+
const terms = queryTermsOf(graph, props.reinterpretations);
|
|
107
75
|
const limit = bound(props.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
108
76
|
const entry = (0, runEntrypoints_1.runEntrypoints)(graph, {
|
|
109
77
|
type: "entrypoints",
|
|
110
78
|
query,
|
|
111
79
|
limit,
|
|
112
80
|
neighbors: 1,
|
|
113
|
-
});
|
|
114
|
-
const seeds = tourSeedsOf(graph, entry, query, limit);
|
|
81
|
+
}).result;
|
|
82
|
+
const seeds = tourSeedsOf(graph, entry, query, limit, named, terms);
|
|
115
83
|
const seedIds = seeds.map((node) => node.id);
|
|
116
|
-
const flowSeedIds = flowSeedIdsOf(seeds);
|
|
117
84
|
const entrypoints = seeds.map((node) => graphNodeOf(graph, node));
|
|
85
|
+
// A flow that goes nowhere is a wasted slot: a seed can match the question by
|
|
86
|
+
// name and still drive nothing (a decorator factory, a metadata helper), and
|
|
87
|
+
// tracing the top five seeds blind spent the tour's flows on them — the model
|
|
88
|
+
// read "the tour didn't surface the request pipeline" and went to the files.
|
|
89
|
+
// Walk the ranked seeds instead, keeping the ones whose trace actually moves,
|
|
90
|
+
// until the tour has its flows.
|
|
91
|
+
// A flow the tour already told is not a second flow. zod's four public parse
|
|
92
|
+
// entries — parse, parseAsync, safeParse, safeParseAsync — run the same chain
|
|
93
|
+
// into the same internals, and the tour spent all four of its slots saying it
|
|
94
|
+
// four times: 18 KB of payload, three quarters of it a repeat, and the rest of
|
|
95
|
+
// the library (schema construction, checks, error formatting) unmentioned. A
|
|
96
|
+
// candidate whose trace lands where a kept flow already landed is a synonym,
|
|
97
|
+
// so keep the first and walk on to one that tells something else.
|
|
118
98
|
const primaryFlow = [];
|
|
119
|
-
|
|
99
|
+
const told = [];
|
|
100
|
+
for (const id of flowSeedIdsOf(tourSeedsOf(graph, entry, query, limit * FLOW_SEED_CANDIDATES, named, terms))) {
|
|
101
|
+
if (primaryFlow.length >= FLOW_SEEDS)
|
|
102
|
+
break;
|
|
120
103
|
const trace = (0, runTrace_1.runTrace)(graph, {
|
|
121
104
|
type: "trace",
|
|
122
105
|
from: id,
|
|
@@ -124,19 +107,34 @@ function runTour(graph, props) {
|
|
|
124
107
|
focus: "execution",
|
|
125
108
|
maxDepth: TOUR_TRACE_MAX_DEPTH,
|
|
126
109
|
maxNodes: TOUR_TRACE_MAX_NODES,
|
|
127
|
-
});
|
|
110
|
+
}).result;
|
|
128
111
|
const start = trace.start;
|
|
129
112
|
if (start === undefined)
|
|
130
113
|
continue;
|
|
131
114
|
const hops = trace.hops.filter((hop) => isTourHop(graph, hop));
|
|
132
|
-
|
|
115
|
+
if (hops.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
const reached = trace.reached.filter((node) => isTourTraceNode(graph, node));
|
|
118
|
+
const landed = new Set(reached.map((node) => node.id));
|
|
119
|
+
if (told.some((earlier) => overlaps(landed, earlier)))
|
|
120
|
+
continue;
|
|
121
|
+
told.push(landed);
|
|
122
|
+
const steps = hops
|
|
123
|
+
.slice(0, MAX_FLOW_ANCHORS)
|
|
124
|
+
.map((hop) => flowStepOf(graph, hop));
|
|
125
|
+
// Every node the flow reached is listed, including the ones its steps name.
|
|
126
|
+
// A step is prose — `App.render -[calls at App.tsx:2093]-> renderScene` — and
|
|
127
|
+
// it carries the name and the citation but not the *handle*, and the handle
|
|
128
|
+
// is what a second call needs. Holding back the nodes the steps had named
|
|
129
|
+
// took their ids away with them: Sonnet traced `mutateElement` by name, got
|
|
130
|
+
// the several nodes that name, and re-traced it by id — two calls for one
|
|
131
|
+
// symbol, four times over in a single Excalidraw tour, which went from five
|
|
132
|
+
// graph calls to fifteen. What `reached` is for is not the story, which the
|
|
133
|
+
// steps tell; it is the handles to go on with.
|
|
133
134
|
primaryFlow.push({
|
|
134
|
-
start:
|
|
135
|
-
steps
|
|
136
|
-
.slice(0, MAX_FLOW_ANCHORS)
|
|
137
|
-
.map((hop) => flowStepOf(graph, hop)),
|
|
135
|
+
start: flowStartOf(start),
|
|
136
|
+
steps,
|
|
138
137
|
reached: reached.map(traceNodeOf),
|
|
139
|
-
anchors: flowAnchorsOf(trace, hops, reached).slice(0, MAX_FLOW_ANCHORS),
|
|
140
138
|
...(trace.truncated ? { truncated: true } : {}),
|
|
141
139
|
});
|
|
142
140
|
}
|
|
@@ -144,12 +142,12 @@ function runTour(graph, props) {
|
|
|
144
142
|
? undefined
|
|
145
143
|
: (0, runDetails_1.runDetails)(graph, {
|
|
146
144
|
type: "details",
|
|
147
|
-
handles: seedIds
|
|
145
|
+
handles: seedIds,
|
|
148
146
|
neighbors: true,
|
|
149
147
|
memberLimit: 4,
|
|
150
148
|
dependencyLimit: 2,
|
|
151
149
|
neighborLimit: 2,
|
|
152
|
-
});
|
|
150
|
+
}).result;
|
|
153
151
|
const nearby = details === undefined ? [] : nearbyAnchorsOf(details);
|
|
154
152
|
const tests = props.includeTests === false
|
|
155
153
|
? []
|
|
@@ -159,29 +157,102 @@ function runTour(graph, props) {
|
|
|
159
157
|
]));
|
|
160
158
|
const answerAnchors = uniqueAnchors([
|
|
161
159
|
...entrypoints.flatMap((node) => anchorFromNode("central entrypoint", node)),
|
|
162
|
-
...primaryFlow.flatMap((flow) => flow.anchors),
|
|
163
160
|
...nearby,
|
|
164
161
|
...tests,
|
|
165
162
|
]).slice(0, MAX_READ_NEXT);
|
|
166
163
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
164
|
+
result: {
|
|
165
|
+
type: "tour",
|
|
166
|
+
entrypoints,
|
|
167
|
+
primaryFlow,
|
|
168
|
+
nearby: nearby.slice(0, MAX_NEARBY),
|
|
169
|
+
tests: tests.slice(0, MAX_TESTS),
|
|
170
|
+
answerAnchors,
|
|
171
|
+
...(entry.truncated ||
|
|
172
|
+
primaryFlow.some((flow) => flow.truncated === true) ||
|
|
173
|
+
nearby.length > MAX_NEARBY ||
|
|
174
|
+
tests.length > MAX_TESTS
|
|
175
|
+
? { truncated: true }
|
|
176
|
+
: {}),
|
|
177
|
+
},
|
|
178
|
+
next: tourNext(),
|
|
182
179
|
};
|
|
183
180
|
}
|
|
184
|
-
|
|
181
|
+
/**
|
|
182
|
+
* What the tour says it is.
|
|
183
|
+
*
|
|
184
|
+
* It used to say what it had _covered of the question_, and it could not know
|
|
185
|
+
* that. The claim was decided by matching the question's words against
|
|
186
|
+
* identifier names: a word the tour's own symbols did not spell, and for which
|
|
187
|
+
* some symbol somewhere in the graph did, came back as a stage the tour owed
|
|
188
|
+
* the reader — `inspect`, with the tool description telling the model to make
|
|
189
|
+
* exactly the one request it names.
|
|
190
|
+
*
|
|
191
|
+
* What that string match actually found, on the corpus: shopping-backend's
|
|
192
|
+
* question says "from request handling through auth, provider logic, Prisma",
|
|
193
|
+
* and the tour reported "handling" missing on the strength of `handlePayment`
|
|
194
|
+
* and `handleCancel`, two deposit and order helpers with nothing to do with
|
|
195
|
+
* handling a request. Vue's says "a component/template read", and "template"
|
|
196
|
+
* came back missing because the compiler's AST has a `TemplateLiteral`. Zod's
|
|
197
|
+
* ends "the returned result", and `ParseResult.data` made "result" a stage. In
|
|
198
|
+
* five of the eight project-specific tours the server reported a hole it did
|
|
199
|
+
* not have and spent the model a call on it — and the drill-down started there:
|
|
200
|
+
* shopping-backend's eight follow-ups open with the `lookup` this `next` asked
|
|
201
|
+
* for.
|
|
202
|
+
*
|
|
203
|
+
* The failure is not a threshold. A question names concepts and a graph holds
|
|
204
|
+
* identifiers, and no lexical rule bridges the two: "request handling" is not
|
|
205
|
+
* `handlePayment` and never will be. The server cannot know what the question
|
|
206
|
+
* means, so it cannot know whether it answered it — and the selection audit
|
|
207
|
+
* riding in the same payload says exactly that: each fact is compiler-verified,
|
|
208
|
+
* but the shortlist was ranked against the question and whether it covers the
|
|
209
|
+
* question is the caller's to judge.
|
|
210
|
+
*
|
|
211
|
+
* So the tour states what it returned, which is a fact, and leaves what to do
|
|
212
|
+
* with it to the reader, which was never the server's to decide. A tour that
|
|
213
|
+
* misses what the reader needs is a tour the reader keeps asking past — and the
|
|
214
|
+
* models do exactly that, without being told to.
|
|
215
|
+
*/
|
|
216
|
+
function tourNext() {
|
|
217
|
+
return (0, resultNext_1.resultNext)("answer", "This is what the graph holds for the query: the entrypoints it ranked, the flows they run, the paths and tests around them, and the anchors to cite. Nothing in it needs verifying, and anything past it is another request.");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The symbols the caller named, as the graph knows them.
|
|
221
|
+
*
|
|
222
|
+
* A name is not a word. Ranked as text, the reading `setupRenderEffect` — the
|
|
223
|
+
* render effect Opus was asking about, and had written down — is shredded into
|
|
224
|
+
* "setup", "render", "effect", and the tour opens on `queuePostRenderEffect`
|
|
225
|
+
* instead. The model then traced `setupRenderEffect` by hand, which is the call
|
|
226
|
+
* the tour exists to save.
|
|
227
|
+
*
|
|
228
|
+
* So each entry is resolved the way a handle is: the graph either holds that
|
|
229
|
+
* symbol or it does not, and what it does not hold is dropped without ceremony.
|
|
230
|
+
* A phrase is not a name and resolves to nothing, so prose costs the tour
|
|
231
|
+
* nothing — which is what makes a guess free, and a repository the caller has
|
|
232
|
+
* never seen safe to guess about.
|
|
233
|
+
*/
|
|
234
|
+
function namedNodesOf(graph, names) {
|
|
235
|
+
const out = new Set();
|
|
236
|
+
for (const raw of names) {
|
|
237
|
+
const name = raw.trim();
|
|
238
|
+
if (name.length === 0 || /\s/.test(name))
|
|
239
|
+
continue;
|
|
240
|
+
// An ambiguous guess is not evidence. A name the project declares once is a
|
|
241
|
+
// symbol the caller named; a name it declares many times is a word, and the
|
|
242
|
+
// graph does not get to decide which one was meant. Resolving to the first
|
|
243
|
+
// candidate decides anyway: Sonnet asked for `handlePointerDown`, Excalidraw
|
|
244
|
+
// declares it on several classes, and the tour of a drawing app opened on
|
|
245
|
+
// its line editor and cost eight calls. Boosting all of them instead only
|
|
246
|
+
// spreads the same guess wider. Both are the graph inventing a belief the
|
|
247
|
+
// caller did not have, so an ambiguous name is dropped, exactly like a name
|
|
248
|
+
// the graph has never heard of.
|
|
249
|
+
const resolved = (0, resolveHandle_1.resolveGraphHandle)(graph, name);
|
|
250
|
+
if (resolved.node !== undefined)
|
|
251
|
+
out.add(resolved.node.id);
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
function tourSeedsOf(graph, entry, query, limit, named, terms) {
|
|
185
256
|
const out = [];
|
|
186
257
|
const seen = new Set();
|
|
187
258
|
const add = (node) => {
|
|
@@ -190,14 +261,43 @@ function tourSeedsOf(graph, entry, query, limit) {
|
|
|
190
261
|
seen.add(node.id);
|
|
191
262
|
out.push(node);
|
|
192
263
|
};
|
|
264
|
+
// A symbol the question names is an entrypoint of the tour, and a name the
|
|
265
|
+
// project declares more than once is not a name the project does not declare.
|
|
266
|
+
// Zod's question says `schema.parse`; the graph holds three `parse`s, so the
|
|
267
|
+
// mention came back as candidates rather than a node, and the tour dropped it
|
|
268
|
+
// and opened on `fromJSONSchema` — the model's first move was to go and trace
|
|
269
|
+
// `ZodType.parse` itself. The candidates arrive ranked by what the package
|
|
270
|
+
// publishes, and a tour is a ranked product: take the reading the ranking put
|
|
271
|
+
// first, which is the one a reader means.
|
|
193
272
|
for (const mention of entry.mentions) {
|
|
194
|
-
|
|
273
|
+
const mentioned = mention.node ?? mention.candidates?.[0];
|
|
274
|
+
add(mentioned === undefined ? undefined : graph.node(mentioned.id));
|
|
195
275
|
}
|
|
196
276
|
if (hasExplicitSymbolHandle(query)) {
|
|
197
277
|
for (const hit of entry.hits)
|
|
198
278
|
add(graph.node(hit.id));
|
|
199
279
|
}
|
|
200
|
-
|
|
280
|
+
// The symbols the caller named get seats, and the graph's own centre keeps the
|
|
281
|
+
// rest. A name the *user* wrote is seeded outright, above — it is the symbol
|
|
282
|
+
// the question is about. A name the *caller* wrote is a belief about where the
|
|
283
|
+
// answer lives, which is worth a seat and is not worth the tour: the beliefs
|
|
284
|
+
// are ranked among themselves and take half the seeds, and centrality fills
|
|
285
|
+
// what is left. A caller that names nine symbols does not get nine names and
|
|
286
|
+
// no centre, and one that names the wrong symbol still gets a tour.
|
|
287
|
+
const share = Math.floor(limit * NAMED_SHARE);
|
|
288
|
+
if (share > 0 && named.size !== 0) {
|
|
289
|
+
for (const node of rankedTourSeeds(graph, terms, share, named))
|
|
290
|
+
add(node);
|
|
291
|
+
}
|
|
292
|
+
// The other half is the graph's, and the names do not reach it. Weighting the
|
|
293
|
+
// named symbols in this ranking too let them take both halves: Opus named ten
|
|
294
|
+
// symbols along RxJS's subscribe path, they filled the seeds, and `operate` —
|
|
295
|
+
// the second seed of the same tour without any names, and the head of the
|
|
296
|
+
// operator chain the question asked about — fell out of it. The model fetched
|
|
297
|
+
// it by hand and said so. A caller's belief about where the answer lives is
|
|
298
|
+
// worth half a tour; the other half is what the codebase says is central,
|
|
299
|
+
// including the symbol the caller did not think to name.
|
|
300
|
+
for (const node of rankedTourSeeds(graph, terms, limit))
|
|
201
301
|
add(node);
|
|
202
302
|
if (out.length === 0) {
|
|
203
303
|
for (const hit of entry.hits)
|
|
@@ -208,6 +308,7 @@ function tourSeedsOf(graph, entry, query, limit) {
|
|
|
208
308
|
function graphNodeOf(graph, node) {
|
|
209
309
|
const span = node.implementation ?? node.evidence;
|
|
210
310
|
const signature = (0, runDetails_1.signatureOf)(graph.project, node);
|
|
311
|
+
const doc = (0, runDetails_1.docOf)(graph.project, node);
|
|
211
312
|
const decorators = (0, runDetails_1.decoratorsOf)(node);
|
|
212
313
|
return {
|
|
213
314
|
id: node.id,
|
|
@@ -227,10 +328,31 @@ function graphNodeOf(graph, node) {
|
|
|
227
328
|
}
|
|
228
329
|
: {}),
|
|
229
330
|
...(signature !== undefined ? { signature } : {}),
|
|
331
|
+
...(doc !== undefined ? { doc } : {}),
|
|
230
332
|
...(decorators !== undefined ? { decorators } : {}),
|
|
231
333
|
};
|
|
232
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* A node a flow reached, as its handle and its line. Its span and signature are
|
|
337
|
+
* already in the flow's `steps` and `anchors`, and carrying them a second time
|
|
338
|
+
* cost half the tour's payload — enough that a client which caps a tool result
|
|
339
|
+
* spilled the whole thing to a file, and the model shelled out to read back its
|
|
340
|
+
* own answer.
|
|
341
|
+
*
|
|
342
|
+
* The file and the kind went the same way, for the same reason: a node id _is_
|
|
343
|
+
* `path/to/file.ts#Owner.member:kind`, so a reached node that also carried them
|
|
344
|
+
* bought one fact three times, and the flows are two thirds of a tour that is
|
|
345
|
+
* re-sent whole on every turn of the conversation it opened. The flow's start
|
|
346
|
+
* keeps the full node; the chain behind it does not need one.
|
|
347
|
+
*/
|
|
233
348
|
function traceNodeOf(node) {
|
|
349
|
+
return {
|
|
350
|
+
id: node.id,
|
|
351
|
+
name: node.name,
|
|
352
|
+
...(node.line !== undefined ? { line: node.line } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function flowStartOf(node) {
|
|
234
356
|
return {
|
|
235
357
|
id: node.id,
|
|
236
358
|
name: node.name,
|
|
@@ -258,11 +380,31 @@ function flowAnchorsOf(trace, hops, reached) {
|
|
|
258
380
|
...hops.flatMap((hop) => anchorFromEvidence("flow edge", `${hop.from} -> ${hop.to}`, hop.evidence)),
|
|
259
381
|
]);
|
|
260
382
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
383
|
+
/**
|
|
384
|
+
* The words of the question that could name code: the ranking's own terms.
|
|
385
|
+
*
|
|
386
|
+
* A question about TypeORM says "TypeORM", which splits into `type` and `orm`,
|
|
387
|
+
* and both are inside the project's own name — they say nothing about which
|
|
388
|
+
* part of the project is being asked about, while matching a hundred
|
|
389
|
+
* identifiers. Whole-word equality is not enough to drop them, so a term the
|
|
390
|
+
* project name contains goes too.
|
|
391
|
+
*/
|
|
392
|
+
function queryTermsOf(graph, names) {
|
|
393
|
+
const project = graph.project.toLowerCase();
|
|
394
|
+
const out = [];
|
|
395
|
+
for (const name of names) {
|
|
396
|
+
for (const term of subwords(name)) {
|
|
397
|
+
if (term.length <= 2 || project.includes(term))
|
|
398
|
+
continue;
|
|
399
|
+
if (!out.includes(term))
|
|
400
|
+
out.push(term);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
function rankedTourSeeds(graph, terms, count, only) {
|
|
264
406
|
const items = graph.nodes
|
|
265
|
-
.filter((node) => isTourSeed(graph, node))
|
|
407
|
+
.filter((node) => isTourSeed(graph, node) && (only === undefined || only.has(node.id)))
|
|
266
408
|
.map((node) => ({
|
|
267
409
|
node,
|
|
268
410
|
score: tourSeedScore(graph, node, terms),
|
|
@@ -270,31 +412,185 @@ function rankedTourSeeds(graph, query) {
|
|
|
270
412
|
}))
|
|
271
413
|
.filter((item) => item.score > 0)
|
|
272
414
|
.sort((a, b) => b.score - a.score);
|
|
273
|
-
return diverseTourSeeds(items, terms).map((item) => item.node);
|
|
415
|
+
return diverseTourSeeds(items, terms, count).map((item) => item.node);
|
|
274
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* How central a symbol is to running this codebase, as one standard algorithm
|
|
419
|
+
* instead of a ledger of hand-tuned bonuses.
|
|
420
|
+
*
|
|
421
|
+
* This score used to be a sum: so many points for the symbol's kind, so many
|
|
422
|
+
* per log of each degree, a capped term for its reach, another for what its
|
|
423
|
+
* package exports, another for what the tests call — nine signals, each with a
|
|
424
|
+
* multiplier and a cap that someone picked while watching a benchmark. Word
|
|
425
|
+
* lists in numeric form. All of it was approximating one question the graph can
|
|
426
|
+
* answer exactly: _if you use what this package publishes, what runs?_
|
|
427
|
+
*
|
|
428
|
+
* Personalized PageRank answers it. The walker starts on the export surface —
|
|
429
|
+
* the symbols the package puts on the wire, members included — and follows the
|
|
430
|
+
* execution edges, crossing from an abstract declaration to its implementations
|
|
431
|
+
* the way `runTrace` does. Public entries hold mass because the walk starts on
|
|
432
|
+
* them; the spine holds mass because every path runs through it. One damping
|
|
433
|
+
* constant, 0.85, from the literature — the same algorithm aider's repo map
|
|
434
|
+
* ranks symbols with.
|
|
435
|
+
*/
|
|
275
436
|
function tourSeedScore(graph, node, terms) {
|
|
276
|
-
const degree = realDegree(graph, node.id);
|
|
277
|
-
const execution = executionDegree(graph, node.id);
|
|
278
437
|
const queryWords = new Set(terms);
|
|
279
438
|
const matchScore = queryMatchScore(node, terms);
|
|
280
|
-
let score =
|
|
281
|
-
const surface = entrySurfaceScore(node);
|
|
282
|
-
score += surface;
|
|
283
|
-
score += runtimeEntryScore(node, surface);
|
|
284
|
-
score += Math.min(14, Math.log2(1 + degree.in) * 4);
|
|
285
|
-
score += Math.min(30, Math.log2(1 + degree.out) * 9);
|
|
286
|
-
score += Math.min(28, Math.log2(1 + execution.out) * 10);
|
|
287
|
-
if (node.exported)
|
|
288
|
-
score += 14;
|
|
289
|
-
if (node.decorators !== undefined && node.decorators.length > 0)
|
|
290
|
-
score += 10;
|
|
291
|
-
score += matchScore;
|
|
439
|
+
let score = centralityOf(graph, node.id) + matchScore;
|
|
292
440
|
score *= queryAlignmentFactor(matchScore, queryWords);
|
|
293
|
-
score *= broadTourDamping(node
|
|
441
|
+
score *= broadTourDamping(node);
|
|
294
442
|
return score;
|
|
295
443
|
}
|
|
444
|
+
/** Centrality is reported on a 0..100 scale: 100 is this graph's most central. */
|
|
445
|
+
const CENTRALITY_SCALE = 100;
|
|
446
|
+
/** Execution reach is walked this deep and no further. */
|
|
447
|
+
const REACH_DEPTH = 4;
|
|
448
|
+
const REACH_NODE_BUDGET = 400;
|
|
449
|
+
/** Invocation: the edges that mean "this makes that run". */
|
|
450
|
+
const INVOKE_KINDS = new Set(["calls", "instantiates", "renders"]);
|
|
451
|
+
const DISPATCH_KINDS = new Set(["overrides", "implements"]);
|
|
452
|
+
const centralityCache = new WeakMap();
|
|
453
|
+
function centralityOf(graph, id) {
|
|
454
|
+
let ranks = centralityCache.get(graph);
|
|
455
|
+
if (ranks === undefined) {
|
|
456
|
+
ranks = computeCentrality(graph);
|
|
457
|
+
centralityCache.set(graph, ranks);
|
|
458
|
+
}
|
|
459
|
+
return ranks.get(id) ?? 0;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Three facts, one product: `log2(1 + published) * max(reach, fan-in)`.
|
|
463
|
+
*
|
|
464
|
+
* - _Published_: how many modules put the symbol on the wire, counting the class
|
|
465
|
+
* that owns it — a member is published by publishing its owner.
|
|
466
|
+
* - _Reach_: how many production files its forward invocation chain touches,
|
|
467
|
+
* crossing from an abstract declaration to the implementations, the way
|
|
468
|
+
* execution does. This is what separates an entry that drives half the
|
|
469
|
+
* framework from a fat method whose fifty calls stay in its own file.
|
|
470
|
+
* - _Fan-in_: how many production call sites invoke it. This is what separates
|
|
471
|
+
* the spine everything runs through from a leaf beside it.
|
|
472
|
+
*
|
|
473
|
+
* The product means a symbol must be public _and_ load-bearing: a utility is
|
|
474
|
+
* published everywhere but drives nothing and dies on the max term's
|
|
475
|
+
* normalization; an internal engine drives everything but is published nowhere
|
|
476
|
+
* and dies on the surface term. Tests and generated support files are not part
|
|
477
|
+
* of the structure being toured and stay out of all three.
|
|
478
|
+
*/
|
|
479
|
+
function computeCentrality(graph) {
|
|
480
|
+
const production = (node) => !node.external &&
|
|
481
|
+
!node.ignored &&
|
|
482
|
+
!(0, pathPolicy_1.isTestPath)(node.file) &&
|
|
483
|
+
!isNoisePath(node.file);
|
|
484
|
+
const invoked = (id) => {
|
|
485
|
+
const out = [];
|
|
486
|
+
let hasBody = false;
|
|
487
|
+
for (const edge of graph.outgoing(id)) {
|
|
488
|
+
if (!INVOKE_KINDS.has(edge.kind))
|
|
489
|
+
continue;
|
|
490
|
+
hasBody = true;
|
|
491
|
+
out.push(edge.to);
|
|
492
|
+
}
|
|
493
|
+
if (!hasBody) {
|
|
494
|
+
for (const edge of graph.incoming(id)) {
|
|
495
|
+
if (DISPATCH_KINDS.has(edge.kind))
|
|
496
|
+
out.push(edge.from);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
};
|
|
501
|
+
const reachOf = (id) => {
|
|
502
|
+
const seen = new Set([id]);
|
|
503
|
+
const files = new Set();
|
|
504
|
+
let frontier = [id];
|
|
505
|
+
for (let depth = 0; depth < REACH_DEPTH && seen.size < REACH_NODE_BUDGET; depth++) {
|
|
506
|
+
const next = [];
|
|
507
|
+
for (const current of frontier) {
|
|
508
|
+
for (const to of invoked(current)) {
|
|
509
|
+
if (seen.has(to))
|
|
510
|
+
continue;
|
|
511
|
+
const target = graph.node(to);
|
|
512
|
+
if (target === undefined || !production(target))
|
|
513
|
+
continue;
|
|
514
|
+
seen.add(to);
|
|
515
|
+
files.add(target.file);
|
|
516
|
+
next.push(to);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (next.length === 0)
|
|
520
|
+
break;
|
|
521
|
+
frontier = next;
|
|
522
|
+
}
|
|
523
|
+
return files.size;
|
|
524
|
+
};
|
|
525
|
+
const out = new Map();
|
|
526
|
+
const candidates = [];
|
|
527
|
+
for (const node of graph.nodes) {
|
|
528
|
+
if (!production(node))
|
|
529
|
+
continue;
|
|
530
|
+
const surface = publicFanIn(graph, node.id);
|
|
531
|
+
if (surface <= 0)
|
|
532
|
+
continue;
|
|
533
|
+
let fanIn = 0;
|
|
534
|
+
for (const edge of graph.incoming(node.id)) {
|
|
535
|
+
if (!INVOKE_KINDS.has(edge.kind))
|
|
536
|
+
continue;
|
|
537
|
+
const caller = graph.node(edge.from);
|
|
538
|
+
if (caller !== undefined && production(caller))
|
|
539
|
+
fanIn++;
|
|
540
|
+
}
|
|
541
|
+
candidates.push({ id: node.id, surface, fanIn });
|
|
542
|
+
}
|
|
543
|
+
let reachMax = 1;
|
|
544
|
+
let fanInMax = 1;
|
|
545
|
+
const reaches = new Map();
|
|
546
|
+
for (const candidate of candidates) {
|
|
547
|
+
const reach = reachOf(candidate.id);
|
|
548
|
+
reaches.set(candidate.id, reach);
|
|
549
|
+
if (reach > reachMax)
|
|
550
|
+
reachMax = reach;
|
|
551
|
+
if (candidate.fanIn > fanInMax)
|
|
552
|
+
fanInMax = candidate.fanIn;
|
|
553
|
+
}
|
|
554
|
+
let max = 0;
|
|
555
|
+
const raw = new Map();
|
|
556
|
+
for (const candidate of candidates) {
|
|
557
|
+
const load = Math.max((reaches.get(candidate.id) ?? 0) / reachMax, candidate.fanIn / fanInMax);
|
|
558
|
+
const score = Math.log2(1 + candidate.surface) * load;
|
|
559
|
+
raw.set(candidate.id, score);
|
|
560
|
+
if (score > max)
|
|
561
|
+
max = score;
|
|
562
|
+
}
|
|
563
|
+
if (max > 0) {
|
|
564
|
+
for (const [id, score] of raw)
|
|
565
|
+
out.set(id, (score / max) * CENTRALITY_SCALE);
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* A tour ranks and walks the project's surface.
|
|
571
|
+
*
|
|
572
|
+
* Not because a closure is beneath an index — a trace, a lookup, or a details
|
|
573
|
+
* request answers with one, and that is what the specific-flow lane needed. It
|
|
574
|
+
* is because the seed score leans on reach, and reach breaks when it counts
|
|
575
|
+
* them. Reach stands in for "gets to the code that does the work", and a method
|
|
576
|
+
* whose body is full of callbacks lands in more files than one that calls three
|
|
577
|
+
* things and means them: TypeORM's `SelectQueryBuilder` outranked its insert
|
|
578
|
+
* path on breadth alone, and the tour it led came back a walk through the query
|
|
579
|
+
* builder's fluent API — escape, clone, addSelect, limit, offset — while the
|
|
580
|
+
* insert flow that reaches the broadcaster, the metadata, and the driver fell
|
|
581
|
+
* out of the tour entirely. Wide and shallow beat deep and few, and the model
|
|
582
|
+
* went back to the files.
|
|
583
|
+
*
|
|
584
|
+
* So the surface is scored by the surface, and the body is answered when it is
|
|
585
|
+
* asked for. The specific-flow lane wants the closures and gets them; the
|
|
586
|
+
* orientation lane wants the surface and gets that. Judged by the answer rather
|
|
587
|
+
* than the token count, the gated tour is the better one: it reaches the
|
|
588
|
+
* broadcaster and the driver, where the ungated tour reached `limit` and
|
|
589
|
+
* `offset`.
|
|
590
|
+
*/
|
|
296
591
|
function isTourSeed(graph, node) {
|
|
297
|
-
return (
|
|
592
|
+
return (node.closure !== true &&
|
|
593
|
+
TOUR_SEED_KINDS.has(node.kind) &&
|
|
298
594
|
(node.kind !== "property" || executionDegree(graph, node.id).out > 0) &&
|
|
299
595
|
!node.external &&
|
|
300
596
|
!node.ignored &&
|
|
@@ -306,17 +602,54 @@ function flowSeedIdsOf(seeds) {
|
|
|
306
602
|
const source = executable.length === 0 ? seeds : executable;
|
|
307
603
|
return source.map((node) => node.id);
|
|
308
604
|
}
|
|
309
|
-
|
|
310
|
-
|
|
605
|
+
/**
|
|
606
|
+
* True when two flows land in mostly the same places — the same story told
|
|
607
|
+
* twice. Overlap is measured against the smaller flow, so a short chain fully
|
|
608
|
+
* contained in a longer one counts as told, which is what a sibling entry
|
|
609
|
+
* (`parse` beside `safeParse`) actually is.
|
|
610
|
+
*/
|
|
611
|
+
function overlaps(candidate, told) {
|
|
612
|
+
const smaller = candidate.size <= told.size ? candidate : told;
|
|
613
|
+
const larger = smaller === candidate ? told : candidate;
|
|
614
|
+
if (smaller.size === 0)
|
|
615
|
+
return true;
|
|
616
|
+
let shared = 0;
|
|
617
|
+
for (const id of smaller)
|
|
618
|
+
if (larger.has(id))
|
|
619
|
+
shared++;
|
|
620
|
+
return shared / smaller.size >= FLOW_OVERLAP;
|
|
621
|
+
}
|
|
622
|
+
function isTourTraceNode(graph, node) {
|
|
623
|
+
return (graph.node(node.id)?.closure !== true &&
|
|
624
|
+
!isNoisePath(node.file) &&
|
|
625
|
+
!isSharedUtility(graph, node.id));
|
|
311
626
|
}
|
|
312
627
|
function isTourHop(graph, hop) {
|
|
313
628
|
const from = graph.node(hop.from);
|
|
314
629
|
const to = graph.node(hop.to);
|
|
315
630
|
return (from !== undefined &&
|
|
316
631
|
to !== undefined &&
|
|
632
|
+
from.closure !== true &&
|
|
633
|
+
to.closure !== true &&
|
|
317
634
|
!STRUCTURAL_KINDS.has(hop.kind) &&
|
|
318
635
|
!isNoisePath(from.file) &&
|
|
319
|
-
!isNoisePath(to.file)
|
|
636
|
+
!isNoisePath(to.file) &&
|
|
637
|
+
!isSharedUtility(graph, hop.to));
|
|
638
|
+
}
|
|
639
|
+
// A fan-in hub that drives no further execution: reached from a dozen-plus
|
|
640
|
+
// sites yet calling nothing onward (a shared type, guard, or leaf helper). It is
|
|
641
|
+
// a terminus, not a step in the runtime call chain, so the tour drops it from
|
|
642
|
+
// the flow to keep the meaningful chain legible — it still surfaces as a seed,
|
|
643
|
+
// nearby node, or detail when it is itself the subject.
|
|
644
|
+
//
|
|
645
|
+
// The `in >= 12` cut is not a fixture-tuned constant: because real-in-degree is
|
|
646
|
+
// heavy-tailed, this fixed count lands at the ~93rd percentile of fan-in across
|
|
647
|
+
// every benchmark project (900–16k nodes, 92.4–95.5%), so it selects the same
|
|
648
|
+
// "top few percent of hubs" band regardless of project size, while the absolute
|
|
649
|
+
// floor makes it a no-op on small graphs that have no genuine hub. The `out <= 1`
|
|
650
|
+
// guard keeps thin pass-throughs out but never prunes a real branching step.
|
|
651
|
+
function isSharedUtility(graph, id) {
|
|
652
|
+
return realDegree(graph, id).in >= 12 && executionDegree(graph, id).out <= 1;
|
|
320
653
|
}
|
|
321
654
|
function flowStepOf(graph, hop) {
|
|
322
655
|
const from = graph.node(hop.from);
|
|
@@ -327,14 +660,26 @@ function flowStepOf(graph, hop) {
|
|
|
327
660
|
const at = evidence === undefined ? "" : ` at ${evidence.file}:${evidence.startLine}`;
|
|
328
661
|
return `${lhs} -[${hop.kind}${at}]-> ${rhs}`;
|
|
329
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* True when the node at the other end of an edge is a closure.
|
|
665
|
+
*
|
|
666
|
+
* A tour scores the surface, and a closure is not on it — but a closure's edges
|
|
667
|
+
* still land on surface nodes, and counted there they move the score of the
|
|
668
|
+
* very declarations a tour ranks. Keeping closures out of the seed list was not
|
|
669
|
+
* enough: TypeORM's tour still traded its insert flow for a walk through the
|
|
670
|
+
* query builder's fluent API. The surface is scored by the surface.
|
|
671
|
+
*/
|
|
672
|
+
function touchesClosure(graph, id) {
|
|
673
|
+
return graph.node(id)?.closure === true;
|
|
674
|
+
}
|
|
330
675
|
function realDegree(graph, id) {
|
|
331
676
|
let incoming = 0;
|
|
332
677
|
let outgoing = 0;
|
|
333
678
|
for (const edge of graph.outgoing(id))
|
|
334
|
-
if (!STRUCTURAL_KINDS.has(edge.kind))
|
|
679
|
+
if (!STRUCTURAL_KINDS.has(edge.kind) && !touchesClosure(graph, edge.to))
|
|
335
680
|
outgoing++;
|
|
336
681
|
for (const edge of graph.incoming(id))
|
|
337
|
-
if (!STRUCTURAL_KINDS.has(edge.kind))
|
|
682
|
+
if (!STRUCTURAL_KINDS.has(edge.kind) && !touchesClosure(graph, edge.from))
|
|
338
683
|
incoming++;
|
|
339
684
|
return { in: incoming, out: outgoing };
|
|
340
685
|
}
|
|
@@ -342,119 +687,73 @@ function executionDegree(graph, id) {
|
|
|
342
687
|
let incoming = 0;
|
|
343
688
|
let outgoing = 0;
|
|
344
689
|
for (const edge of graph.outgoing(id))
|
|
345
|
-
if (EXECUTION_KINDS.has(edge.kind))
|
|
690
|
+
if (EXECUTION_KINDS.has(edge.kind) && !touchesClosure(graph, edge.to))
|
|
346
691
|
outgoing++;
|
|
347
692
|
for (const edge of graph.incoming(id))
|
|
348
|
-
if (EXECUTION_KINDS.has(edge.kind))
|
|
693
|
+
if (EXECUTION_KINDS.has(edge.kind) && !touchesClosure(graph, edge.from))
|
|
349
694
|
incoming++;
|
|
350
695
|
return { in: incoming, out: outgoing };
|
|
351
696
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
else if (depth === 1)
|
|
381
|
-
score += 32;
|
|
382
|
-
else if (depth === 2)
|
|
383
|
-
score += 12;
|
|
384
|
-
}
|
|
385
|
-
else if (stem === "main" || stem === "server" || stem === "bootstrap")
|
|
386
|
-
score += 42;
|
|
387
|
-
else if (stem === "app" || stem === "application")
|
|
388
|
-
score += 28;
|
|
389
|
-
if (depth <= 1)
|
|
390
|
-
score += 22;
|
|
391
|
-
else if (depth === 2)
|
|
392
|
-
score += 12;
|
|
393
|
-
else if (depth === 3)
|
|
394
|
-
score += 5;
|
|
395
|
-
if (node.exported && score > 0)
|
|
396
|
-
score += 12;
|
|
397
|
-
return score;
|
|
697
|
+
/**
|
|
698
|
+
* What the declaration is, scored by what it does rather than how it was
|
|
699
|
+
* written. `export const parse = (input) => ...` is a function that happens to
|
|
700
|
+
* be bound to a name, and the checker sees it call things; scoring it eight
|
|
701
|
+
* points against a method's twenty-eight is a bias toward one syntax, and it
|
|
702
|
+
* cost zod its own public API — `parse` and `safeParse`, both const arrows,
|
|
703
|
+
* lost their tour seats to `ZodType.safeParse`, a method of the previous
|
|
704
|
+
* major.
|
|
705
|
+
*/
|
|
706
|
+
/**
|
|
707
|
+
* How many modules put a symbol on the wire — counting the one that owns it.
|
|
708
|
+
*
|
|
709
|
+
* A method is not exported; its class is. `Observable.subscribe` and
|
|
710
|
+
* `ZodType.parse` carry an export fan-in of zero, because nothing re-exports a
|
|
711
|
+
* member: what a package publishes is `Observable`, and calling `subscribe` on
|
|
712
|
+
* it is what publishing `Observable` was for. Scored on its own fan-in, the
|
|
713
|
+
* public method a whole library exists to be called through ranks below every
|
|
714
|
+
* loose function beside it, and a list of English verbs — `parse`, `subscribe`,
|
|
715
|
+
* `render` — was what used to put it back.
|
|
716
|
+
*
|
|
717
|
+
* A member inherits the surface of what contains it. That is an edge the graph
|
|
718
|
+
* already draws, and it says the same thing in a codebase whose classes are
|
|
719
|
+
* named in Japanese.
|
|
720
|
+
*/
|
|
721
|
+
function publicFanIn(graph, id) {
|
|
722
|
+
const own = (0, exportSurface_1.exportFanIn)(graph, id);
|
|
723
|
+
const owner = ownerOf(graph, id);
|
|
724
|
+
return owner === undefined ? own : Math.max(own, (0, exportSurface_1.exportFanIn)(graph, owner));
|
|
398
725
|
}
|
|
399
|
-
function
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"execute",
|
|
410
|
-
"handle",
|
|
411
|
-
"init",
|
|
412
|
-
"initialize",
|
|
413
|
-
"listen",
|
|
414
|
-
"mount",
|
|
415
|
-
"open",
|
|
416
|
-
"parse",
|
|
417
|
-
"render",
|
|
418
|
-
"run",
|
|
419
|
-
"safe",
|
|
420
|
-
"safeparse",
|
|
421
|
-
"start",
|
|
422
|
-
"startup",
|
|
423
|
-
"subscribe",
|
|
424
|
-
"update",
|
|
425
|
-
]);
|
|
426
|
-
if (node.kind === "method" && hasVerb)
|
|
427
|
-
return 90;
|
|
428
|
-
if ((node.kind === "function" ||
|
|
429
|
-
node.kind === "property" ||
|
|
430
|
-
node.kind === "variable") &&
|
|
431
|
-
surface > 0 &&
|
|
432
|
-
hasVerb) {
|
|
433
|
-
return 70;
|
|
434
|
-
}
|
|
435
|
-
if (node.kind === "class" &&
|
|
436
|
-
hasAny(words, [
|
|
437
|
-
"application",
|
|
438
|
-
"app",
|
|
439
|
-
"backend",
|
|
440
|
-
"client",
|
|
441
|
-
"datasource",
|
|
442
|
-
"factory",
|
|
443
|
-
"server",
|
|
444
|
-
])) {
|
|
445
|
-
return 45;
|
|
446
|
-
}
|
|
447
|
-
return 0;
|
|
448
|
-
}
|
|
449
|
-
function sourceDepth(file) {
|
|
450
|
-
const parts = file.split("/").filter(Boolean);
|
|
451
|
-
if (parts[0] === "src")
|
|
452
|
-
return Math.max(0, parts.length - 2);
|
|
453
|
-
if (parts[0] === "packages" && parts.length >= 3) {
|
|
454
|
-
return Math.max(0, parts.length - 3);
|
|
726
|
+
function ownerOf(graph, id) {
|
|
727
|
+
for (const edge of graph.incoming(id)) {
|
|
728
|
+
if (edge.kind !== "contains")
|
|
729
|
+
continue;
|
|
730
|
+
const owner = graph.node(edge.from);
|
|
731
|
+
if (owner !== undefined &&
|
|
732
|
+
owner.kind !== "file" &&
|
|
733
|
+
owner.kind !== "module") {
|
|
734
|
+
return owner.id;
|
|
735
|
+
}
|
|
455
736
|
}
|
|
456
|
-
return
|
|
737
|
+
return undefined;
|
|
457
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* What a user of this package can call, as the graph knows it.
|
|
741
|
+
*
|
|
742
|
+
* This used to pay for an English verb anywhere in the name — `create`,
|
|
743
|
+
* `parse`, `render`, `subscribe` — and for a class whose name contained `app`,
|
|
744
|
+
* `server` or `factory`. Neither is a fact about the code. They are a guess
|
|
745
|
+
* about the language its authors happened to write in, and a codebase that
|
|
746
|
+
* names its entry `起動` or `mk` or `boot` is one the guess is simply wrong
|
|
747
|
+
* about. It was also wrong in English: `onRenderTracked` is a devtools hook and
|
|
748
|
+
* it took the bonus for the "render" inside it, outranking `track`, the
|
|
749
|
+
* function it is named after.
|
|
750
|
+
*
|
|
751
|
+
* Two facts say the same thing without reading a word of the name. The package
|
|
752
|
+
* _publishes_ this symbol — that is its export surface, counted in
|
|
753
|
+
* {@link exportFanIn} — and it is a _callable_, so publishing it is publishing
|
|
754
|
+
* something to run. A user calls what a package exports and what a package
|
|
755
|
+
* exports to be called.
|
|
756
|
+
*/
|
|
458
757
|
function queryMatchScore(node, terms) {
|
|
459
758
|
return (matchedQueryTerms(node, terms).size * 8 +
|
|
460
759
|
matchedFileTerms(node, terms).size * 2);
|
|
@@ -479,17 +778,30 @@ function matchedTerms(words, terms) {
|
|
|
479
778
|
}
|
|
480
779
|
return matched;
|
|
481
780
|
}
|
|
482
|
-
|
|
781
|
+
/**
|
|
782
|
+
* Greedy set cover over the query's terms: each pick is the highest-scoring
|
|
783
|
+
* candidate that still covers a term no pick covers yet, so the seeds spread
|
|
784
|
+
* across the question instead of crowding onto its loudest word.
|
|
785
|
+
*
|
|
786
|
+
* It picks `count` of them, not all of them. Ordering every candidate cost
|
|
787
|
+
* O(n²) — on VS Code, where tens of thousands of symbols score above zero, one
|
|
788
|
+
* tour spent six minutes ranking seeds it then threw away, because the caller
|
|
789
|
+
* keeps only the first few. Stopping at `count` makes the cover O(count · n),
|
|
790
|
+
* and the picks it does make are the same ones.
|
|
791
|
+
*/
|
|
792
|
+
function diverseTourSeeds(items, terms, count) {
|
|
483
793
|
if (items.length <= 1 || terms.length === 0)
|
|
484
|
-
return items;
|
|
794
|
+
return items.slice(0, count);
|
|
485
795
|
const out = [];
|
|
486
796
|
const remaining = [...items];
|
|
487
797
|
const uncovered = new Set(terms);
|
|
488
|
-
while (remaining.length > 0) {
|
|
489
|
-
let bestIndex =
|
|
798
|
+
while (remaining.length > 0 && out.length < count) {
|
|
799
|
+
let bestIndex = -1;
|
|
490
800
|
let bestScore = Number.NEGATIVE_INFINITY;
|
|
491
801
|
for (let i = 0; i < remaining.length; i++) {
|
|
492
802
|
const item = remaining[i];
|
|
803
|
+
if (out.some((picked) => restates(picked.node, item.node)))
|
|
804
|
+
continue;
|
|
493
805
|
let coverage = 0;
|
|
494
806
|
for (const term of item.matchedTerms)
|
|
495
807
|
if (uncovered.has(term))
|
|
@@ -500,6 +812,8 @@ function diverseTourSeeds(items, terms) {
|
|
|
500
812
|
bestIndex = i;
|
|
501
813
|
}
|
|
502
814
|
}
|
|
815
|
+
if (bestIndex === -1)
|
|
816
|
+
break;
|
|
503
817
|
const [picked] = remaining.splice(bestIndex, 1);
|
|
504
818
|
out.push(picked);
|
|
505
819
|
for (const term of picked.matchedTerms)
|
|
@@ -507,6 +821,27 @@ function diverseTourSeeds(items, terms) {
|
|
|
507
821
|
}
|
|
508
822
|
return out;
|
|
509
823
|
}
|
|
824
|
+
/**
|
|
825
|
+
* Whether a candidate seed would only say again what a chosen seed says: the
|
|
826
|
+
* same file, and a name the chosen one already contains word for word.
|
|
827
|
+
*
|
|
828
|
+
* `LinearElementEditor.handlePointerMove` and its `...InEditMode` sibling, and
|
|
829
|
+
* `renderNewElementScene` beside its own throttled twin, took four of the five
|
|
830
|
+
* seeds on Excalidraw's edit-pipeline tour. The mutation and history layers the
|
|
831
|
+
* question named took none, and Sonnet spent twenty-two graph calls finding
|
|
832
|
+
* them. A seed that restates a chosen one is a slot spent on a fact the tour
|
|
833
|
+
* already has.
|
|
834
|
+
*/
|
|
835
|
+
function restates(chosen, candidate) {
|
|
836
|
+
if (chosen.file !== candidate.file)
|
|
837
|
+
return false;
|
|
838
|
+
const chosenWords = subwords(chosen.name).map(stemWord);
|
|
839
|
+
const candidateWords = subwords(candidate.name).map(stemWord);
|
|
840
|
+
const [shorter, longer] = chosenWords.length <= candidateWords.length
|
|
841
|
+
? [chosenWords, candidateWords]
|
|
842
|
+
: [candidateWords, chosenWords];
|
|
843
|
+
return (shorter.length > 0 && shorter.every((word, index) => longer[index] === word));
|
|
844
|
+
}
|
|
510
845
|
function queryAlignmentFactor(matchScore, queryWords) {
|
|
511
846
|
if (queryWords.size === 0)
|
|
512
847
|
return 1;
|
|
@@ -516,73 +851,48 @@ function queryAlignmentFactor(matchScore, queryWords) {
|
|
|
516
851
|
return 1.15;
|
|
517
852
|
return 0.45;
|
|
518
853
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
"env",
|
|
538
|
-
"environment",
|
|
539
|
-
"option",
|
|
540
|
-
"options",
|
|
541
|
-
"port",
|
|
542
|
-
]) &&
|
|
543
|
-
(node.kind === "variable" || node.kind === "property") &&
|
|
544
|
-
hasAny(words, [
|
|
545
|
-
"config",
|
|
546
|
-
"configuration",
|
|
547
|
-
"env",
|
|
548
|
-
"environment",
|
|
549
|
-
"option",
|
|
550
|
-
"options",
|
|
551
|
-
"port",
|
|
552
|
-
])) {
|
|
553
|
-
factor *= 0.35;
|
|
554
|
-
}
|
|
555
|
-
if (!hasAny(queryWords, [
|
|
556
|
-
"deserialize",
|
|
557
|
-
"deserializer",
|
|
558
|
-
"serializer",
|
|
559
|
-
"serialize",
|
|
560
|
-
"serialization",
|
|
561
|
-
]) &&
|
|
562
|
-
hasAny(words, [
|
|
563
|
-
"deserialize",
|
|
564
|
-
"deserializer",
|
|
565
|
-
"serializer",
|
|
566
|
-
"serialize",
|
|
567
|
-
"serialization",
|
|
568
|
-
])) {
|
|
569
|
-
factor *= 0.25;
|
|
570
|
-
}
|
|
571
|
-
return factor;
|
|
572
|
-
}
|
|
573
|
-
function hasAny(words, candidates) {
|
|
574
|
-
return candidates.some((word) => words.has(word));
|
|
854
|
+
/**
|
|
855
|
+
* What a tour demotes, without reading a word of the name.
|
|
856
|
+
*
|
|
857
|
+
* This used to hold four lists of English words -- error, exception, config,
|
|
858
|
+
* env, option, port, serialize, deserialize, internal, private -- and it
|
|
859
|
+
* quartered the score of any symbol whose name contained one, unless the
|
|
860
|
+
* question contained it too. A codebase that names its errors `エラー` was never
|
|
861
|
+
* damped, and a question asked in Japanese never lifted the damping. The lists
|
|
862
|
+
* were a guess about the language, not a fact about the code.
|
|
863
|
+
*
|
|
864
|
+
* A leading underscore stays: it is punctuation, and it means the same thing in
|
|
865
|
+
* every language a symbol can be named in. Everything else a tour used to
|
|
866
|
+
* demote by vocabulary -- a config bag, an error type, a serializer -- the
|
|
867
|
+
* score already demotes by structure: they run nothing, so they carry no
|
|
868
|
+
* execution degree and no execution reach.
|
|
869
|
+
*/
|
|
870
|
+
function broadTourDamping(node) {
|
|
871
|
+
return isPrivateLike(node) ? 0.25 : 1;
|
|
575
872
|
}
|
|
576
|
-
function isPrivateLike(node
|
|
873
|
+
function isPrivateLike(node) {
|
|
874
|
+
// The leading underscore is punctuation, not vocabulary: it means the same
|
|
875
|
+
// in a codebase whose symbols are named in Japanese as in one named in
|
|
876
|
+
// English. The words that used to sit here -- "inner", "internal",
|
|
877
|
+
// "private" -- did not.
|
|
577
878
|
const name = node.qualifiedName ?? node.name;
|
|
578
|
-
return
|
|
579
|
-
name.includes("._") ||
|
|
580
|
-
hasAny(words, ["inner", "internal", "private"]));
|
|
879
|
+
return name.startsWith("_") || name.includes("._");
|
|
581
880
|
}
|
|
582
881
|
function hasExplicitSymbolHandle(query) {
|
|
583
882
|
return (/`[^`]+`/.test(query) ||
|
|
584
883
|
/\b[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*\b/.test(query));
|
|
585
884
|
}
|
|
885
|
+
/** How many leading path segments two files share. */
|
|
886
|
+
function sharedPathDepth(a, b) {
|
|
887
|
+
const left = a.split("/");
|
|
888
|
+
const right = b.split("/");
|
|
889
|
+
let shared = 0;
|
|
890
|
+
while (shared < left.length - 1 &&
|
|
891
|
+
shared < right.length - 1 &&
|
|
892
|
+
left[shared] === right[shared])
|
|
893
|
+
shared++;
|
|
894
|
+
return shared;
|
|
895
|
+
}
|
|
586
896
|
function isNoisePath(file) {
|
|
587
897
|
return (0, pathPolicy_1.isSupportPath)(file);
|
|
588
898
|
}
|
|
@@ -594,13 +904,39 @@ function subwords(text) {
|
|
|
594
904
|
.filter((word) => word.length > 0)
|
|
595
905
|
.map((word) => word.toLowerCase());
|
|
596
906
|
}
|
|
907
|
+
/**
|
|
908
|
+
* A question and the code it asks about name the same thing in different parts
|
|
909
|
+
* of speech: the asker writes "scene mutation", the symbol is `mutateElement`.
|
|
910
|
+
* Inflection alone does not bridge that — "mutation" and "mutate" share five
|
|
911
|
+
* characters and the prefix rule wants six — so a tour of Excalidraw's edit
|
|
912
|
+
* pipeline seeded four renderers, never surfaced the mutation layer the
|
|
913
|
+
* question named, and Sonnet went and found it itself in twenty-one further
|
|
914
|
+
* graph calls.
|
|
915
|
+
*
|
|
916
|
+
* Stripping the derivational suffixes as well collapses both spellings onto the
|
|
917
|
+
* same stem, so the noun in the question reaches the verb in the code.
|
|
918
|
+
*/
|
|
597
919
|
function stemWord(word) {
|
|
598
|
-
for (const suffix of [
|
|
920
|
+
for (const suffix of [
|
|
921
|
+
"ation",
|
|
922
|
+
"ing",
|
|
923
|
+
"ment",
|
|
924
|
+
"ence",
|
|
925
|
+
"ance",
|
|
926
|
+
"ion",
|
|
927
|
+
"ity",
|
|
928
|
+
"ed",
|
|
929
|
+
"es",
|
|
930
|
+
"s",
|
|
931
|
+
]) {
|
|
599
932
|
if (word.length > suffix.length + 3 && word.endsWith(suffix)) {
|
|
600
|
-
return word.slice(0, -suffix.length);
|
|
933
|
+
return trimTrailingE(word.slice(0, -suffix.length));
|
|
601
934
|
}
|
|
602
935
|
}
|
|
603
|
-
return word;
|
|
936
|
+
return trimTrailingE(word);
|
|
937
|
+
}
|
|
938
|
+
function trimTrailingE(word) {
|
|
939
|
+
return word.length > 4 && word.endsWith("e") ? word.slice(0, -1) : word;
|
|
604
940
|
}
|
|
605
941
|
function commonPrefixLength(a, b) {
|
|
606
942
|
const n = Math.min(a.length, b.length);
|
|
@@ -609,61 +945,101 @@ function commonPrefixLength(a, b) {
|
|
|
609
945
|
i++;
|
|
610
946
|
return i;
|
|
611
947
|
}
|
|
948
|
+
/**
|
|
949
|
+
* The code paths around each selected symbol: what runs it, what it runs, and
|
|
950
|
+
* what it is declared against — in that order, once each.
|
|
951
|
+
*
|
|
952
|
+
* `dependsOn` is the union of what a symbol calls and what it names in a type
|
|
953
|
+
* position, so walking `calls`, then `types`, then `dependsOn` listed the same
|
|
954
|
+
* neighbour under three labels, and the ten nearby slots of Excalidraw's edit
|
|
955
|
+
* tour went: `_renderInteractiveScene` as a call, `_renderInteractiveScene` as
|
|
956
|
+
* a type, `InteractiveSceneRenderConfig` as a type, and the same again for the
|
|
957
|
+
* next symbol. Two of five stages consumed the whole list, and the stage the
|
|
958
|
+
* reader would have to look up next — who calls the mutation — was not in it.
|
|
959
|
+
* Sonnet then asked the graph "who calls this" thirteen times.
|
|
960
|
+
*
|
|
961
|
+
* So a neighbour is named once, and the callers come first. A tour follows what
|
|
962
|
+
* runs; a type reference is the weakest thing a symbol can say about itself,
|
|
963
|
+
* and it goes last, where the cap can drop it without dropping a call path.
|
|
964
|
+
*/
|
|
612
965
|
function nearbyAnchorsOf(details) {
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
966
|
+
const perNode = details.nodes.map((node) => {
|
|
967
|
+
// The selected symbol is not near itself. It is an entrypoint, with its
|
|
968
|
+
// span, its signature and its doc, and it is an answer anchor under that
|
|
969
|
+
// name — a third copy here spent half the nearby list saying what the top
|
|
970
|
+
// of the tour already said.
|
|
971
|
+
const anchors = [];
|
|
972
|
+
const named = new Set([node.name]);
|
|
616
973
|
for (const ref of [
|
|
974
|
+
...(node.dependedOnBy ?? []),
|
|
617
975
|
...(node.calls ?? []),
|
|
618
|
-
...(node.types ?? []),
|
|
619
976
|
...(node.implementedBy ?? []),
|
|
620
977
|
...(node.dependsOn ?? []),
|
|
621
|
-
...(node.
|
|
978
|
+
...(node.types ?? []),
|
|
622
979
|
]) {
|
|
980
|
+
if (named.has(ref.name))
|
|
981
|
+
continue;
|
|
982
|
+
named.add(ref.name);
|
|
623
983
|
anchors.push(...anchorFromEvidence(`${ref.relation} ${ref.name}`, ref.name, ref.evidence));
|
|
624
984
|
}
|
|
625
|
-
|
|
985
|
+
return anchors;
|
|
986
|
+
});
|
|
987
|
+
// A stage at a time, not a symbol at a time. The list is capped, and taken
|
|
988
|
+
// symbol by symbol the first one's neighbourhood filled it: Excalidraw's
|
|
989
|
+
// renderer spent six of the ten slots on its own callees and types, and the
|
|
990
|
+
// mutation, the history and the collaboration the question named got none.
|
|
991
|
+
const anchors = [];
|
|
992
|
+
const told = new Set();
|
|
993
|
+
const depth = Math.max(0, ...perNode.map((list) => list.length));
|
|
994
|
+
for (let index = 0; index < depth; index++)
|
|
995
|
+
for (const list of perNode) {
|
|
996
|
+
const anchor = list[index];
|
|
997
|
+
if (anchor === undefined || told.has(anchor.name))
|
|
998
|
+
continue;
|
|
999
|
+
told.add(anchor.name);
|
|
1000
|
+
anchors.push(anchor);
|
|
1001
|
+
}
|
|
626
1002
|
return uniqueAnchors(anchors);
|
|
627
1003
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
...(node.sourceSpan.endLine !== undefined
|
|
641
|
-
? { endLine: node.sourceSpan.endLine }
|
|
642
|
-
: {}),
|
|
643
|
-
},
|
|
644
|
-
}
|
|
645
|
-
: {}),
|
|
646
|
-
...(node.signature !== undefined ? { signature: node.signature } : {}),
|
|
647
|
-
...(node.decorators !== undefined ? { decorators: node.decorators } : {}),
|
|
648
|
-
};
|
|
649
|
-
}
|
|
1004
|
+
/**
|
|
1005
|
+
* The tests that exercise the tour's symbols, nearest first.
|
|
1006
|
+
*
|
|
1007
|
+
* A subject is covered by more than one suite: NestJS's
|
|
1008
|
+
* `NestFactoryStatic.create` is called by three GraphQL end-to-end specs under
|
|
1009
|
+
* integration/ and by the unit spec that sits beside the code, and the tour's
|
|
1010
|
+
* slots went to whichever the edge list happened to hold first — the e2e ones.
|
|
1011
|
+
* So the model globbed the disk for `packages/core/test/nest-factory.spec.ts`,
|
|
1012
|
+
* which the graph had all along. A test that lives next to its subject is the
|
|
1013
|
+
* one a newcomer reads, so the anchors come back ordered by how much of the
|
|
1014
|
+
* subject's path the test shares.
|
|
1015
|
+
*/
|
|
650
1016
|
function testAnchorsOf(graph, seedIds) {
|
|
651
1017
|
const anchors = [];
|
|
652
1018
|
for (const id of seedIds) {
|
|
1019
|
+
const subject = graph.node(id);
|
|
1020
|
+
const near = [];
|
|
653
1021
|
for (const edge of graph.incoming(id)) {
|
|
654
1022
|
const node = graph.node(edge.from);
|
|
655
1023
|
if (node === undefined || !(0, pathPolicy_1.isTestPath)(node.file))
|
|
656
1024
|
continue;
|
|
657
|
-
|
|
658
|
-
|
|
1025
|
+
near.push({
|
|
1026
|
+
proximity: sharedPathDepth(subject?.file ?? "", node.file),
|
|
1027
|
+
anchors: [
|
|
1028
|
+
...anchorFromNode("test coverage", graphNodeOf(graph, node)),
|
|
1029
|
+
...anchorFromEvidence(`${edge.kind} ${node.qualifiedName ?? node.name}`, node.qualifiedName ?? node.name, edge.evidence),
|
|
1030
|
+
],
|
|
1031
|
+
});
|
|
659
1032
|
}
|
|
1033
|
+
near.sort((a, b) => b.proximity - a.proximity);
|
|
1034
|
+
for (const item of near)
|
|
1035
|
+
anchors.push(...item.anchors);
|
|
660
1036
|
const impact = (0, runTrace_1.runTrace)(graph, {
|
|
661
1037
|
type: "trace",
|
|
662
1038
|
from: id,
|
|
663
1039
|
direction: "impact",
|
|
664
1040
|
maxDepth: 4,
|
|
665
1041
|
maxNodes: 16,
|
|
666
|
-
});
|
|
1042
|
+
}).result;
|
|
667
1043
|
for (const node of impact.reached) {
|
|
668
1044
|
if (node.roles?.includes("test")) {
|
|
669
1045
|
anchors.push(...anchorFromNode("test coverage", node));
|