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