@ttsc/graph 0.16.5 → 0.16.7
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 +22 -0
- package/lib/TtscGraphApplication.js +58 -22
- package/lib/TtscGraphApplication.js.map +1 -1
- package/lib/model/TtscGraphMemory.js +43 -4
- package/lib/model/TtscGraphMemory.js.map +1 -1
- package/lib/model/loadGraph.js +17 -17
- package/lib/server/accessAliases.js +53 -0
- package/lib/server/accessAliases.js.map +1 -0
- package/lib/server/createServer.js +1490 -657
- package/lib/server/createServer.js.map +1 -1
- package/lib/server/instructions.js +81 -21
- package/lib/server/instructions.js.map +1 -1
- package/lib/server/pathPolicy.js +35 -0
- package/lib/server/pathPolicy.js.map +1 -0
- package/lib/server/resultGuide.js +16 -0
- package/lib/server/resultGuide.js.map +1 -0
- package/lib/server/runDetails.js +474 -0
- package/lib/server/runDetails.js.map +1 -0
- package/lib/server/{runIndex.js → runEntrypoints.js} +92 -28
- package/lib/server/runEntrypoints.js.map +1 -0
- package/lib/server/runLookup.js +246 -0
- package/lib/server/runLookup.js.map +1 -0
- package/lib/server/runOverview.js +44 -27
- package/lib/server/runOverview.js.map +1 -1
- package/lib/server/runTour.js +737 -0
- package/lib/server/runTour.js.map +1 -0
- package/lib/server/runTrace.js +224 -43
- package/lib/server/runTrace.js.map +1 -1
- package/lib/structures/ITtscGraphDetails.js +3 -0
- package/lib/structures/ITtscGraphDetails.js.map +1 -0
- package/lib/structures/ITtscGraphEntrypoints.js +3 -0
- package/lib/structures/ITtscGraphEntrypoints.js.map +1 -0
- package/lib/structures/{ITtscGraphExpand.js → ITtscGraphEscape.js} +1 -1
- package/lib/structures/ITtscGraphEscape.js.map +1 -0
- package/lib/structures/ITtscGraphLookup.js +3 -0
- package/lib/structures/ITtscGraphLookup.js.map +1 -0
- package/lib/structures/{ITtscGraphIndex.js → ITtscGraphNext.js} +1 -1
- package/lib/structures/ITtscGraphNext.js.map +1 -0
- package/lib/structures/{ITtscGraphQuery.js → ITtscGraphTour.js} +1 -1
- package/lib/structures/ITtscGraphTour.js.map +1 -0
- package/lib/structures/index.js +6 -3
- package/lib/structures/index.js.map +1 -1
- package/package.json +2 -2
- package/src/TtscGraphApplication.ts +70 -30
- package/src/model/TtscGraphMemory.ts +46 -4
- package/src/server/accessAliases.ts +55 -0
- package/src/server/createServer.ts +4 -7
- package/src/server/instructions.ts +81 -21
- package/src/server/pathPolicy.ts +43 -0
- package/src/server/resultGuide.ts +20 -0
- package/src/server/runDetails.ts +553 -0
- package/src/server/{runIndex.ts → runEntrypoints.ts} +116 -36
- package/src/server/runLookup.ts +284 -0
- package/src/server/runOverview.ts +59 -31
- package/src/server/runTour.ts +881 -0
- package/src/server/runTrace.ts +299 -45
- package/src/structures/ITtscGraphApplication.ts +75 -56
- package/src/structures/ITtscGraphDecorator.ts +12 -14
- package/src/structures/ITtscGraphDetails.ts +197 -0
- package/src/structures/ITtscGraphEntrypoints.ts +149 -0
- package/src/structures/ITtscGraphEscape.ts +49 -0
- package/src/structures/ITtscGraphEvidence.ts +5 -8
- package/src/structures/ITtscGraphLookup.ts +82 -0
- package/src/structures/ITtscGraphNext.ts +23 -0
- package/src/structures/ITtscGraphNode.ts +10 -4
- package/src/structures/ITtscGraphOverview.ts +41 -18
- package/src/structures/ITtscGraphTour.ts +150 -0
- package/src/structures/ITtscGraphTrace.ts +93 -13
- package/src/structures/TtscGraphNodeKind.ts +2 -2
- package/src/structures/index.ts +6 -3
- package/lib/server/runExpand.js +0 -177
- package/lib/server/runExpand.js.map +0 -1
- package/lib/server/runIndex.js.map +0 -1
- package/lib/server/runQuery.js +0 -147
- package/lib/server/runQuery.js.map +0 -1
- package/lib/structures/ITtscGraphExpand.js.map +0 -1
- package/lib/structures/ITtscGraphIndex.js.map +0 -1
- package/lib/structures/ITtscGraphQuery.js.map +0 -1
- package/src/server/runExpand.ts +0 -186
- package/src/server/runQuery.ts +0 -150
- package/src/structures/ITtscGraphExpand.ts +0 -85
- package/src/structures/ITtscGraphIndex.ts +0 -100
- package/src/structures/ITtscGraphQuery.ts +0 -49
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
2
|
import { ITtscGraphEdge } from "../structures/ITtscGraphEdge";
|
|
3
|
-
import {
|
|
3
|
+
import { ITtscGraphEntrypoints } from "../structures/ITtscGraphEntrypoints";
|
|
4
4
|
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
5
5
|
import { resolveGraphHandle } from "./resolveHandle";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
6
|
+
import { resultGuide, resultNext } from "./resultGuide";
|
|
7
|
+
import { decoratorsOf, edgeEvidenceOf, signatureOf } from "./runDetails";
|
|
8
|
+
import { runLookup } from "./runLookup";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_LIMIT = 4;
|
|
11
|
+
const MAX_LIMIT = 8;
|
|
12
|
+
const DEFAULT_NEIGHBORS = 0;
|
|
13
|
+
const MAX_NEIGHBORS = 2;
|
|
14
|
+
const MAX_SEEDS = 3;
|
|
14
15
|
const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Build the first source-free
|
|
18
|
-
* model stable handles, declaration signatures, and direct graph
|
|
19
|
-
* deliberately not a source reader;
|
|
20
|
-
*
|
|
18
|
+
* Build the first source-free entrypoints list for a code question. The result
|
|
19
|
+
* gives the model stable handles, declaration signatures, and direct graph
|
|
20
|
+
* context. It is deliberately not a source reader; details adds selected symbol
|
|
21
|
+
* shape and ranges, not implementation text.
|
|
21
22
|
*/
|
|
22
|
-
export function
|
|
23
|
+
export function runEntrypoints(
|
|
23
24
|
graph: TtscGraphMemory,
|
|
24
|
-
props:
|
|
25
|
-
):
|
|
25
|
+
props: ITtscGraphEntrypoints.IRequest,
|
|
26
|
+
): ITtscGraphEntrypoints {
|
|
26
27
|
const query = props.query.trim();
|
|
27
28
|
const limit = bound(props.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
28
29
|
const neighborLimit = bound(
|
|
@@ -32,12 +33,12 @@ export function runIndex(
|
|
|
32
33
|
MAX_NEIGHBORS,
|
|
33
34
|
);
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
-
const hits =
|
|
36
|
+
const lookupResult = runLookup(graph, { type: "lookup", query, limit });
|
|
37
|
+
const hits = lookupResult.hits.map((hit) => ({ ...hit }));
|
|
37
38
|
|
|
38
|
-
const mentions = directMentions(query).map((handle) => {
|
|
39
|
+
const mentions = directMentions(graph, query).map((handle) => {
|
|
39
40
|
const resolved = resolveGraphHandle(graph, handle, 6);
|
|
40
|
-
const mention:
|
|
41
|
+
const mention: ITtscGraphEntrypoints.IMention = { handle };
|
|
41
42
|
if (resolved.node !== undefined)
|
|
42
43
|
mention.node = nodeOf(graph, resolved.node);
|
|
43
44
|
if (resolved.candidates !== undefined) {
|
|
@@ -61,7 +62,7 @@ export function runIndex(
|
|
|
61
62
|
for (const hit of hits) addSeed(graph.node(hit.id));
|
|
62
63
|
|
|
63
64
|
let truncated = seeds.length > MAX_SEEDS;
|
|
64
|
-
const neighborhood:
|
|
65
|
+
const neighborhood: ITtscGraphEntrypoints.INeighborhood[] = [];
|
|
65
66
|
for (const seed of seeds.slice(0, MAX_SEEDS)) {
|
|
66
67
|
const outgoing = refs(graph, graph.outgoing(seed.id), "to", neighborLimit);
|
|
67
68
|
const incoming = refs(
|
|
@@ -79,16 +80,19 @@ export function runIndex(
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
return {
|
|
83
|
+
type: "entrypoints",
|
|
82
84
|
query,
|
|
83
85
|
hits,
|
|
84
86
|
mentions,
|
|
85
87
|
neighborhood,
|
|
86
|
-
next:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
next: resultNext(
|
|
89
|
+
"inspect",
|
|
90
|
+
"Use one returned handle for trace/details when the answer needs flow or selected shape.",
|
|
91
|
+
"trace",
|
|
92
|
+
),
|
|
93
|
+
guide: resultGuide(
|
|
94
|
+
"Use hits, mentions, and neighborhood as the code index. If they identify the relevant files and symbols, answer or make one focused trace/details call; do not search the repository to verify them.",
|
|
95
|
+
),
|
|
92
96
|
...(truncated ? { truncated: true } : {}),
|
|
93
97
|
};
|
|
94
98
|
}
|
|
@@ -96,8 +100,8 @@ export function runIndex(
|
|
|
96
100
|
function nodeOf(
|
|
97
101
|
graph: TtscGraphMemory,
|
|
98
102
|
node: ITtscGraphNode,
|
|
99
|
-
):
|
|
100
|
-
const out:
|
|
103
|
+
): ITtscGraphEntrypoints.INode {
|
|
104
|
+
const out: ITtscGraphEntrypoints.INode = {
|
|
101
105
|
id: node.id,
|
|
102
106
|
name: node.qualifiedName ?? node.name,
|
|
103
107
|
kind: node.kind,
|
|
@@ -107,14 +111,16 @@ function nodeOf(
|
|
|
107
111
|
out.line = node.evidence.startLine;
|
|
108
112
|
const signature = signatureOf(graph.project, node);
|
|
109
113
|
if (signature !== undefined) out.signature = signature;
|
|
114
|
+
const decorators = decoratorsOf(node);
|
|
115
|
+
if (decorators !== undefined) out.decorators = decorators;
|
|
110
116
|
return out;
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
function refOf(
|
|
114
120
|
node: ITtscGraphNode,
|
|
115
121
|
edge: ITtscGraphEdge,
|
|
116
|
-
):
|
|
117
|
-
const out:
|
|
122
|
+
): ITtscGraphEntrypoints.IReference {
|
|
123
|
+
const out: ITtscGraphEntrypoints.IReference = {
|
|
118
124
|
id: node.id,
|
|
119
125
|
name: node.qualifiedName ?? node.name,
|
|
120
126
|
kind: node.kind,
|
|
@@ -123,6 +129,8 @@ function refOf(
|
|
|
123
129
|
};
|
|
124
130
|
if (node.evidence?.startLine !== undefined)
|
|
125
131
|
out.line = node.evidence.startLine;
|
|
132
|
+
const evidence = edgeEvidenceOf(edge);
|
|
133
|
+
if (evidence !== undefined) out.evidence = evidence;
|
|
126
134
|
return out;
|
|
127
135
|
}
|
|
128
136
|
|
|
@@ -131,8 +139,9 @@ function refs(
|
|
|
131
139
|
edges: readonly ITtscGraphEdge[],
|
|
132
140
|
end: "to" | "from",
|
|
133
141
|
limit: number,
|
|
134
|
-
): { items:
|
|
135
|
-
const
|
|
142
|
+
): { items: ITtscGraphEntrypoints.IReference[]; truncated: boolean } {
|
|
143
|
+
const ranked: Array<{ ref: ITtscGraphEntrypoints.IReference; rank: number }> =
|
|
144
|
+
[];
|
|
136
145
|
const seen = new Set<string>();
|
|
137
146
|
let available = 0;
|
|
138
147
|
for (const edge of edges) {
|
|
@@ -143,15 +152,74 @@ function refs(
|
|
|
143
152
|
if (seen.has(key)) continue;
|
|
144
153
|
seen.add(key);
|
|
145
154
|
available++;
|
|
146
|
-
|
|
155
|
+
const ref = refOf(other, edge);
|
|
156
|
+
ranked.push({ ref, rank: refRank(ref, edge) });
|
|
157
|
+
}
|
|
158
|
+
ranked.sort((a, b) => a.rank - b.rank);
|
|
159
|
+
const items: ITtscGraphEntrypoints.IReference[] = [];
|
|
160
|
+
for (const item of ranked) {
|
|
161
|
+
if (items.length < limit) items.push(item.ref);
|
|
147
162
|
}
|
|
148
163
|
return { items, truncated: available > items.length };
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
function
|
|
166
|
+
function refRank(
|
|
167
|
+
ref: ITtscGraphEntrypoints.IReference,
|
|
168
|
+
edge: ITtscGraphEdge,
|
|
169
|
+
): number {
|
|
170
|
+
return (
|
|
171
|
+
edgeKindRank(edge.kind) * 100_000 +
|
|
172
|
+
evidenceRank(edge) +
|
|
173
|
+
(ref.file.startsWith("bundled://") ? 20_000 : 0)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function evidenceRank(edge: ITtscGraphEdge): number {
|
|
178
|
+
const line = edge.evidence?.startLine ?? 9_999;
|
|
179
|
+
const col = edge.evidence?.startCol ?? 999;
|
|
180
|
+
return line * 100 + col;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function edgeKindRank(kind: string): number {
|
|
184
|
+
switch (kind) {
|
|
185
|
+
case "calls":
|
|
186
|
+
return 0;
|
|
187
|
+
case "instantiates":
|
|
188
|
+
return 1;
|
|
189
|
+
case "accesses":
|
|
190
|
+
case "renders":
|
|
191
|
+
return 2;
|
|
192
|
+
case "tests":
|
|
193
|
+
return 3;
|
|
194
|
+
case "overrides":
|
|
195
|
+
case "decorates":
|
|
196
|
+
return 4;
|
|
197
|
+
case "extends":
|
|
198
|
+
case "implements":
|
|
199
|
+
return 5;
|
|
200
|
+
case "type_ref":
|
|
201
|
+
return 6;
|
|
202
|
+
default:
|
|
203
|
+
return 10;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function directMentions(graph: TtscGraphMemory, query: string): string[] {
|
|
152
208
|
const handles = new Set<string>();
|
|
209
|
+
for (const token of query.split(/\s+/)) {
|
|
210
|
+
const handle = normalizeNodeIdToken(token);
|
|
211
|
+
if (handle !== undefined && graph.node(handle) !== undefined) {
|
|
212
|
+
handles.add(handle);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
153
215
|
for (const match of query.matchAll(/`([^`]+)`/g)) {
|
|
154
|
-
const
|
|
216
|
+
const raw = match[1] ?? "";
|
|
217
|
+
const id = normalizeNodeIdToken(raw);
|
|
218
|
+
if (id !== undefined && graph.node(id) !== undefined) {
|
|
219
|
+
handles.add(id);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const handle = normalizeHandle(raw);
|
|
155
223
|
if (handle !== undefined) handles.add(handle);
|
|
156
224
|
}
|
|
157
225
|
for (const match of query.matchAll(
|
|
@@ -163,6 +231,18 @@ function directMentions(query: string): string[] {
|
|
|
163
231
|
return [...handles];
|
|
164
232
|
}
|
|
165
233
|
|
|
234
|
+
function normalizeNodeIdToken(raw: string): string | undefined {
|
|
235
|
+
const value = raw
|
|
236
|
+
.trim()
|
|
237
|
+
.replace(/^[`"'([{]+/, "")
|
|
238
|
+
.replace(/[`"',.;:)\]}]+$/, "");
|
|
239
|
+
return /^[^\s#]+#[^\s#]+:(class|interface|type|enum|function|method|variable|property)$/.test(
|
|
240
|
+
value,
|
|
241
|
+
)
|
|
242
|
+
? value
|
|
243
|
+
: undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
166
246
|
function normalizeHandle(raw: string): string | undefined {
|
|
167
247
|
const value = raw.trim();
|
|
168
248
|
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(value)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
|
+
import { ITtscGraphLookup } from "../structures/ITtscGraphLookup";
|
|
3
|
+
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
4
|
+
import { isExternalNode, isSupportPath } from "./pathPolicy";
|
|
5
|
+
import { resultGuide, resultNext } from "./resultGuide";
|
|
6
|
+
import { decoratorsOf, signatureOf } from "./runDetails";
|
|
7
|
+
|
|
8
|
+
// One file should not crowd out the rest of the ranking, so cap hits per file.
|
|
9
|
+
const PER_FILE = 3;
|
|
10
|
+
const DEFAULT_LIMIT = 5;
|
|
11
|
+
const MAX_LIMIT = 6;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Rank the graph's symbols against a natural query. Scoring blends exact and
|
|
15
|
+
* dotted-name matches, CamelCase/subword coverage, file-path terms, a prefix
|
|
16
|
+
* bonus, and dependency centrality, then dampens external, generated, and test
|
|
17
|
+
* nodes and caps per file so the result is a diverse, relevant shortlist rather
|
|
18
|
+
* than one file's roster.
|
|
19
|
+
*/
|
|
20
|
+
export function runLookup(
|
|
21
|
+
graph: TtscGraphMemory,
|
|
22
|
+
props: ITtscGraphLookup.IRequest,
|
|
23
|
+
): ITtscGraphLookup {
|
|
24
|
+
const terms = subwords(props.query);
|
|
25
|
+
const codeTerms = exactCodeTerms(props.query);
|
|
26
|
+
const requestedKinds = requestedSymbolKinds(props.query);
|
|
27
|
+
const queryLc = props.query.trim().toLowerCase();
|
|
28
|
+
const wantsInternal = wantsInternalSymbol(queryLc, codeTerms);
|
|
29
|
+
const wantsSupport = wantsSupportSymbol(queryLc);
|
|
30
|
+
const includeExternal = props.includeExternal === true;
|
|
31
|
+
if (terms.length === 0)
|
|
32
|
+
return {
|
|
33
|
+
type: "lookup",
|
|
34
|
+
hits: [],
|
|
35
|
+
next: resultNext(
|
|
36
|
+
"clarify",
|
|
37
|
+
"No symbol matched; ask for a concrete symbol or scope.",
|
|
38
|
+
),
|
|
39
|
+
guide: resultGuide(
|
|
40
|
+
"No symbol matched; answer that the graph did not resolve this name or ask for a more concrete symbol.",
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const scored: ITtscGraphLookup.IHit[] = [];
|
|
45
|
+
for (const node of graph.nodes) {
|
|
46
|
+
if (node.kind === "file") continue;
|
|
47
|
+
if (!includeExternal && isExternalNode(node)) continue;
|
|
48
|
+
const score = scoreNode(
|
|
49
|
+
graph,
|
|
50
|
+
node,
|
|
51
|
+
queryLc,
|
|
52
|
+
terms,
|
|
53
|
+
codeTerms,
|
|
54
|
+
requestedKinds,
|
|
55
|
+
wantsInternal,
|
|
56
|
+
wantsSupport,
|
|
57
|
+
);
|
|
58
|
+
if (score <= 0) continue;
|
|
59
|
+
const hit: ITtscGraphLookup.IHit = {
|
|
60
|
+
id: node.id,
|
|
61
|
+
name: node.qualifiedName ?? node.name,
|
|
62
|
+
kind: node.kind,
|
|
63
|
+
file: node.file,
|
|
64
|
+
line: node.evidence?.startLine,
|
|
65
|
+
score: Math.round(score),
|
|
66
|
+
};
|
|
67
|
+
const decorators = decoratorsOf(node);
|
|
68
|
+
if (decorators !== undefined) hit.decorators = decorators;
|
|
69
|
+
scored.push(hit);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
scored.sort((a, b) => b.score - a.score);
|
|
73
|
+
|
|
74
|
+
// Diversity: keep at most PER_FILE hits per file while filling up to the limit.
|
|
75
|
+
const limit = bound(props.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
76
|
+
const perFile = new Map<string, number>();
|
|
77
|
+
const hits: ITtscGraphLookup.IHit[] = [];
|
|
78
|
+
for (const hit of scored) {
|
|
79
|
+
const used = perFile.get(hit.file) ?? 0;
|
|
80
|
+
if (used >= PER_FILE) continue;
|
|
81
|
+
perFile.set(hit.file, used + 1);
|
|
82
|
+
hits.push(hit);
|
|
83
|
+
if (hits.length >= limit) break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Attach each kept hit's signature only for the shortlist, so the model can
|
|
87
|
+
// often answer from lookup alone without a details call.
|
|
88
|
+
for (const hit of hits) {
|
|
89
|
+
const node = graph.node(hit.id);
|
|
90
|
+
if (node === undefined) continue;
|
|
91
|
+
const sig = signatureOf(graph.project, node);
|
|
92
|
+
if (sig !== undefined) hit.signature = sig;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
type: "lookup",
|
|
96
|
+
hits,
|
|
97
|
+
next: resultNext(
|
|
98
|
+
"inspect",
|
|
99
|
+
"Use one returned id for trace/details when the answer needs flow or shape.",
|
|
100
|
+
"details",
|
|
101
|
+
),
|
|
102
|
+
guide: resultGuide(
|
|
103
|
+
"Use ranked hits and signatures as symbol evidence. If the target is clear, answer or make one focused trace/details call; do not search files to verify the match.",
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Score one node against the query; 0 means no match. */
|
|
109
|
+
function scoreNode(
|
|
110
|
+
graph: TtscGraphMemory,
|
|
111
|
+
node: ITtscGraphNode,
|
|
112
|
+
queryLc: string,
|
|
113
|
+
terms: string[],
|
|
114
|
+
codeTerms: string[],
|
|
115
|
+
requestedKinds: Set<string>,
|
|
116
|
+
wantsInternal: boolean,
|
|
117
|
+
wantsSupport: boolean,
|
|
118
|
+
): number {
|
|
119
|
+
const name = node.name.toLowerCase();
|
|
120
|
+
const qualified = (node.qualifiedName ?? node.name).toLowerCase();
|
|
121
|
+
const nameSubs = subwords(node.name);
|
|
122
|
+
const qualifiedSubs = subwords(node.qualifiedName ?? node.name);
|
|
123
|
+
const pathSubs = subwords(node.file);
|
|
124
|
+
|
|
125
|
+
let score = 0;
|
|
126
|
+
if (queryLc === name || queryLc === qualified) {
|
|
127
|
+
score += 100;
|
|
128
|
+
} else if (qualified.includes(".") && queryLc.includes(qualified)) {
|
|
129
|
+
score += 85;
|
|
130
|
+
} else if (queryLc.includes(".") && qualified.includes(queryLc)) {
|
|
131
|
+
score += 60;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const codeTerm of codeTerms) {
|
|
135
|
+
if (name === codeTerm || qualified === codeTerm) {
|
|
136
|
+
score += 110;
|
|
137
|
+
} else if (qualified.endsWith(`.${codeTerm}`)) {
|
|
138
|
+
score += 95;
|
|
139
|
+
} else if (codeTerm.includes(".") && qualified.endsWith(codeTerm)) {
|
|
140
|
+
score += 85;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (requestedKinds.has(node.kind)) score += 16;
|
|
145
|
+
|
|
146
|
+
let covered = 0;
|
|
147
|
+
for (const term of terms) {
|
|
148
|
+
if (nameSubs.includes(term)) {
|
|
149
|
+
score += 12;
|
|
150
|
+
covered++;
|
|
151
|
+
} else if (qualifiedSubs.includes(term)) {
|
|
152
|
+
score += 8;
|
|
153
|
+
covered++;
|
|
154
|
+
} else if (name.includes(term)) {
|
|
155
|
+
score += 5;
|
|
156
|
+
covered++;
|
|
157
|
+
} else if (pathSubs.includes(term)) {
|
|
158
|
+
score += 3;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Every query term landed somewhere in the name: a strong whole-query match.
|
|
162
|
+
if (covered === terms.length) score += 10;
|
|
163
|
+
if (name.startsWith(terms[0]!)) score += 4;
|
|
164
|
+
|
|
165
|
+
if (score <= 0) return 0;
|
|
166
|
+
|
|
167
|
+
// Centrality: a symbol the codebase leans on is a likelier target.
|
|
168
|
+
const fan = degree(graph, node.id);
|
|
169
|
+
score += Math.min(8, Math.log2(1 + fan) * 2);
|
|
170
|
+
|
|
171
|
+
// Dampen what is rarely the intended target.
|
|
172
|
+
if (node.ignored) score *= 0.3;
|
|
173
|
+
if (isTestFile(node.file)) score *= 0.7;
|
|
174
|
+
if (!wantsSupport && isSupportPath(node.file)) score *= 0.35;
|
|
175
|
+
if (!wantsInternal && isInternalish(node)) score *= 0.82;
|
|
176
|
+
return score;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function wantsSupportSymbol(queryLc: string): boolean {
|
|
180
|
+
return /\b(test|tests|spec|fixture|fixtures|sample|samples|example|examples|generated|build|dist)\b/.test(
|
|
181
|
+
queryLc,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function wantsInternalSymbol(queryLc: string, codeTerms: string[]): boolean {
|
|
186
|
+
return (
|
|
187
|
+
/\b(internal|private|implementation|impl)\b/.test(queryLc) ||
|
|
188
|
+
codeTerms.some((term) => term.startsWith("_") || term.includes("internal"))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isInternalish(node: ITtscGraphNode): boolean {
|
|
193
|
+
const name = node.qualifiedName ?? node.name;
|
|
194
|
+
return (
|
|
195
|
+
name.startsWith("_") ||
|
|
196
|
+
name.includes("._") ||
|
|
197
|
+
subwords(name).some((word) => word === "internal" || word === "internals")
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function exactCodeTerms(query: string): string[] {
|
|
202
|
+
const out = new Set<string>();
|
|
203
|
+
for (const match of query.matchAll(/`([^`]+)`/g)) {
|
|
204
|
+
const normalized = normalizeCodeTerm(match[1] ?? "");
|
|
205
|
+
if (normalized !== undefined) out.add(normalized);
|
|
206
|
+
}
|
|
207
|
+
for (const match of query.matchAll(
|
|
208
|
+
/\b([A-Za-z_$][\w$]*)\s+(method|function|class|interface|type|variable)\b/gi,
|
|
209
|
+
)) {
|
|
210
|
+
const normalized = normalizeCodeTerm(match[1] ?? "");
|
|
211
|
+
if (normalized !== undefined) out.add(normalized);
|
|
212
|
+
}
|
|
213
|
+
for (const match of query.matchAll(
|
|
214
|
+
/\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b/g,
|
|
215
|
+
)) {
|
|
216
|
+
const normalized = normalizeCodeTerm(match[0]);
|
|
217
|
+
if (normalized !== undefined) out.add(normalized);
|
|
218
|
+
}
|
|
219
|
+
return [...out];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function requestedSymbolKinds(query: string): Set<string> {
|
|
223
|
+
const kinds = new Set<string>();
|
|
224
|
+
const lc = query.toLowerCase();
|
|
225
|
+
if (/\bmethods?\b/.test(lc)) kinds.add("method");
|
|
226
|
+
if (/\bfunctions?\b/.test(lc)) {
|
|
227
|
+
kinds.add("function");
|
|
228
|
+
kinds.add("method");
|
|
229
|
+
kinds.add("variable");
|
|
230
|
+
}
|
|
231
|
+
if (/\bclasses?\b/.test(lc)) kinds.add("class");
|
|
232
|
+
if (/\binterfaces?\b/.test(lc)) kinds.add("interface");
|
|
233
|
+
if (/\btypes?\b/.test(lc)) kinds.add("type");
|
|
234
|
+
if (/\bvariables?\b|\bconst\b|\blet\b/.test(lc)) kinds.add("variable");
|
|
235
|
+
return kinds;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeCodeTerm(raw: string): string | undefined {
|
|
239
|
+
const value = raw.trim().toLowerCase();
|
|
240
|
+
return /^[a-z_$][\w$]*(?:\.[a-z_$][\w$]*)*$/.test(value) ? value : undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Non-structural in+out degree (code dependency, not nesting). */
|
|
244
|
+
function degree(graph: TtscGraphMemory, id: string): number {
|
|
245
|
+
let n = 0;
|
|
246
|
+
for (const edge of graph.outgoing(id)) if (!isStructural(edge.kind)) n++;
|
|
247
|
+
for (const edge of graph.incoming(id)) if (!isStructural(edge.kind)) n++;
|
|
248
|
+
return n;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isStructural(kind: string): boolean {
|
|
252
|
+
return kind === "contains" || kind === "exports" || kind === "imports";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isTestFile(file: string): boolean {
|
|
256
|
+
return (
|
|
257
|
+
/(^|\/)(test|tests|__tests__|spec)\//.test(file) ||
|
|
258
|
+
/\.(test|spec)\.[cm]?tsx?$/.test(file)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function bound(
|
|
263
|
+
value: number | undefined,
|
|
264
|
+
fallback: number,
|
|
265
|
+
min: number,
|
|
266
|
+
max: number,
|
|
267
|
+
): number {
|
|
268
|
+
const n = value === undefined || !Number.isFinite(value) ? fallback : value;
|
|
269
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Split an identifier or phrase into lowercase subword tokens: CamelCase,
|
|
274
|
+
* snake, dotted, and space boundaries all break, so `getHTTPResponse`,
|
|
275
|
+
* `find_by_id`, and `OrderService.create` tokenize the way a query would.
|
|
276
|
+
*/
|
|
277
|
+
function subwords(text: string): string[] {
|
|
278
|
+
return text
|
|
279
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
280
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
281
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
282
|
+
.filter((w) => w.length > 0)
|
|
283
|
+
.map((w) => w.toLowerCase());
|
|
284
|
+
}
|