@ttsc/graph 0.16.5 → 0.16.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/TtscGraphApplication.js +36 -23
- package/lib/TtscGraphApplication.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 +1133 -634
- package/lib/server/createServer.js.map +1 -1
- package/lib/server/instructions.js +69 -21
- package/lib/server/instructions.js.map +1 -1
- package/lib/server/runDetails.js +413 -0
- package/lib/server/runDetails.js.map +1 -0
- package/lib/server/{runIndex.js → runEntrypoints.js} +91 -26
- package/lib/server/runEntrypoints.js.map +1 -0
- package/lib/server/{runQuery.js → runLookup.js} +102 -15
- package/lib/server/runLookup.js.map +1 -0
- package/lib/server/runOverview.js +31 -21
- package/lib/server/runOverview.js.map +1 -1
- package/lib/server/runTrace.js +133 -38
- package/lib/server/runTrace.js.map +1 -1
- package/lib/structures/{ITtscGraphQuery.js → ITtscGraphDetails.js} +1 -1
- 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/{ITtscGraphIndex.js → ITtscGraphLookup.js} +1 -1
- package/lib/structures/ITtscGraphLookup.js.map +1 -0
- package/lib/structures/index.js +4 -3
- package/lib/structures/index.js.map +1 -1
- package/package.json +2 -2
- package/src/TtscGraphApplication.ts +39 -32
- package/src/server/accessAliases.ts +55 -0
- package/src/server/createServer.ts +4 -7
- package/src/server/instructions.ts +69 -21
- package/src/server/runDetails.ts +469 -0
- package/src/server/{runIndex.ts → runEntrypoints.ts} +109 -34
- package/src/server/{runQuery.ts → runLookup.ts} +123 -16
- package/src/server/runOverview.ts +35 -23
- package/src/server/runTrace.ts +164 -38
- package/src/structures/ITtscGraphApplication.ts +100 -52
- package/src/structures/ITtscGraphDecorator.ts +12 -14
- package/src/structures/ITtscGraphDetails.ts +134 -0
- package/src/structures/{ITtscGraphIndex.ts → ITtscGraphEntrypoints.ts} +34 -16
- package/src/structures/ITtscGraphEscape.ts +39 -0
- package/src/structures/ITtscGraphEvidence.ts +5 -8
- package/src/structures/ITtscGraphLookup.ts +61 -0
- package/src/structures/ITtscGraphNode.ts +10 -4
- package/src/structures/ITtscGraphOverview.ts +24 -16
- package/src/structures/ITtscGraphTrace.ts +54 -11
- package/src/structures/TtscGraphNodeKind.ts +2 -2
- package/src/structures/index.ts +4 -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.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/structures/ITtscGraphExpand.ts +0 -85
- package/src/structures/ITtscGraphQuery.ts +0 -49
|
@@ -1,28 +1,28 @@
|
|
|
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 { signatureOf } from "./
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
const DEFAULT_LIMIT =
|
|
10
|
-
const MAX_LIMIT =
|
|
11
|
-
const DEFAULT_NEIGHBORS =
|
|
12
|
-
const MAX_NEIGHBORS =
|
|
13
|
-
const MAX_SEEDS =
|
|
6
|
+
import { decoratorsOf, edgeEvidenceOf, signatureOf } from "./runDetails";
|
|
7
|
+
import { runLookup } from "./runLookup";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_LIMIT = 4;
|
|
10
|
+
const MAX_LIMIT = 8;
|
|
11
|
+
const DEFAULT_NEIGHBORS = 0;
|
|
12
|
+
const MAX_NEIGHBORS = 2;
|
|
13
|
+
const MAX_SEEDS = 3;
|
|
14
14
|
const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Build the first source-free
|
|
18
|
-
* model stable handles, declaration signatures, and direct graph
|
|
19
|
-
* deliberately not a source reader;
|
|
20
|
-
*
|
|
17
|
+
* Build the first source-free entrypoints list for a code question. The result
|
|
18
|
+
* gives the model stable handles, declaration signatures, and direct graph
|
|
19
|
+
* context. It is deliberately not a source reader; details adds selected symbol
|
|
20
|
+
* shape and ranges, not implementation text.
|
|
21
21
|
*/
|
|
22
|
-
export function
|
|
22
|
+
export function runEntrypoints(
|
|
23
23
|
graph: TtscGraphMemory,
|
|
24
|
-
props:
|
|
25
|
-
):
|
|
24
|
+
props: ITtscGraphEntrypoints.IRequest,
|
|
25
|
+
): ITtscGraphEntrypoints {
|
|
26
26
|
const query = props.query.trim();
|
|
27
27
|
const limit = bound(props.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
28
28
|
const neighborLimit = bound(
|
|
@@ -32,12 +32,12 @@ export function runIndex(
|
|
|
32
32
|
MAX_NEIGHBORS,
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
const
|
|
36
|
-
const hits =
|
|
35
|
+
const lookupResult = runLookup(graph, { type: "lookup", query, limit });
|
|
36
|
+
const hits = lookupResult.hits.map((hit) => ({ ...hit }));
|
|
37
37
|
|
|
38
|
-
const mentions = directMentions(query).map((handle) => {
|
|
38
|
+
const mentions = directMentions(graph, query).map((handle) => {
|
|
39
39
|
const resolved = resolveGraphHandle(graph, handle, 6);
|
|
40
|
-
const mention:
|
|
40
|
+
const mention: ITtscGraphEntrypoints.IMention = { handle };
|
|
41
41
|
if (resolved.node !== undefined)
|
|
42
42
|
mention.node = nodeOf(graph, resolved.node);
|
|
43
43
|
if (resolved.candidates !== undefined) {
|
|
@@ -61,7 +61,7 @@ export function runIndex(
|
|
|
61
61
|
for (const hit of hits) addSeed(graph.node(hit.id));
|
|
62
62
|
|
|
63
63
|
let truncated = seeds.length > MAX_SEEDS;
|
|
64
|
-
const neighborhood:
|
|
64
|
+
const neighborhood: ITtscGraphEntrypoints.INeighborhood[] = [];
|
|
65
65
|
for (const seed of seeds.slice(0, MAX_SEEDS)) {
|
|
66
66
|
const outgoing = refs(graph, graph.outgoing(seed.id), "to", neighborLimit);
|
|
67
67
|
const incoming = refs(
|
|
@@ -79,15 +79,14 @@ export function runIndex(
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
return {
|
|
82
|
+
type: "entrypoints",
|
|
82
83
|
query,
|
|
83
84
|
hits,
|
|
84
85
|
mentions,
|
|
85
86
|
neighborhood,
|
|
86
87
|
next: {
|
|
87
|
-
|
|
88
|
-
traceFrom:
|
|
89
|
-
.map((mention) => mention.node?.id)
|
|
90
|
-
.filter((id): id is string => id !== undefined),
|
|
88
|
+
details: seeds.slice(0, MAX_SEEDS).map((node) => node.id),
|
|
89
|
+
traceFrom: seeds.slice(0, MAX_SEEDS).map((node) => node.id),
|
|
91
90
|
},
|
|
92
91
|
...(truncated ? { truncated: true } : {}),
|
|
93
92
|
};
|
|
@@ -96,8 +95,8 @@ export function runIndex(
|
|
|
96
95
|
function nodeOf(
|
|
97
96
|
graph: TtscGraphMemory,
|
|
98
97
|
node: ITtscGraphNode,
|
|
99
|
-
):
|
|
100
|
-
const out:
|
|
98
|
+
): ITtscGraphEntrypoints.INode {
|
|
99
|
+
const out: ITtscGraphEntrypoints.INode = {
|
|
101
100
|
id: node.id,
|
|
102
101
|
name: node.qualifiedName ?? node.name,
|
|
103
102
|
kind: node.kind,
|
|
@@ -107,14 +106,16 @@ function nodeOf(
|
|
|
107
106
|
out.line = node.evidence.startLine;
|
|
108
107
|
const signature = signatureOf(graph.project, node);
|
|
109
108
|
if (signature !== undefined) out.signature = signature;
|
|
109
|
+
const decorators = decoratorsOf(node);
|
|
110
|
+
if (decorators !== undefined) out.decorators = decorators;
|
|
110
111
|
return out;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
function refOf(
|
|
114
115
|
node: ITtscGraphNode,
|
|
115
116
|
edge: ITtscGraphEdge,
|
|
116
|
-
):
|
|
117
|
-
const out:
|
|
117
|
+
): ITtscGraphEntrypoints.IReference {
|
|
118
|
+
const out: ITtscGraphEntrypoints.IReference = {
|
|
118
119
|
id: node.id,
|
|
119
120
|
name: node.qualifiedName ?? node.name,
|
|
120
121
|
kind: node.kind,
|
|
@@ -123,6 +124,8 @@ function refOf(
|
|
|
123
124
|
};
|
|
124
125
|
if (node.evidence?.startLine !== undefined)
|
|
125
126
|
out.line = node.evidence.startLine;
|
|
127
|
+
const evidence = edgeEvidenceOf(edge);
|
|
128
|
+
if (evidence !== undefined) out.evidence = evidence;
|
|
126
129
|
return out;
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -131,8 +134,9 @@ function refs(
|
|
|
131
134
|
edges: readonly ITtscGraphEdge[],
|
|
132
135
|
end: "to" | "from",
|
|
133
136
|
limit: number,
|
|
134
|
-
): { items:
|
|
135
|
-
const
|
|
137
|
+
): { items: ITtscGraphEntrypoints.IReference[]; truncated: boolean } {
|
|
138
|
+
const ranked: Array<{ ref: ITtscGraphEntrypoints.IReference; rank: number }> =
|
|
139
|
+
[];
|
|
136
140
|
const seen = new Set<string>();
|
|
137
141
|
let available = 0;
|
|
138
142
|
for (const edge of edges) {
|
|
@@ -143,15 +147,74 @@ function refs(
|
|
|
143
147
|
if (seen.has(key)) continue;
|
|
144
148
|
seen.add(key);
|
|
145
149
|
available++;
|
|
146
|
-
|
|
150
|
+
const ref = refOf(other, edge);
|
|
151
|
+
ranked.push({ ref, rank: refRank(ref, edge) });
|
|
152
|
+
}
|
|
153
|
+
ranked.sort((a, b) => a.rank - b.rank);
|
|
154
|
+
const items: ITtscGraphEntrypoints.IReference[] = [];
|
|
155
|
+
for (const item of ranked) {
|
|
156
|
+
if (items.length < limit) items.push(item.ref);
|
|
147
157
|
}
|
|
148
158
|
return { items, truncated: available > items.length };
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
function
|
|
161
|
+
function refRank(
|
|
162
|
+
ref: ITtscGraphEntrypoints.IReference,
|
|
163
|
+
edge: ITtscGraphEdge,
|
|
164
|
+
): number {
|
|
165
|
+
return (
|
|
166
|
+
edgeKindRank(edge.kind) * 100_000 +
|
|
167
|
+
evidenceRank(edge) +
|
|
168
|
+
(ref.file.startsWith("bundled://") ? 20_000 : 0)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function evidenceRank(edge: ITtscGraphEdge): number {
|
|
173
|
+
const line = edge.evidence?.startLine ?? 9_999;
|
|
174
|
+
const col = edge.evidence?.startCol ?? 999;
|
|
175
|
+
return line * 100 + col;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function edgeKindRank(kind: string): number {
|
|
179
|
+
switch (kind) {
|
|
180
|
+
case "calls":
|
|
181
|
+
return 0;
|
|
182
|
+
case "instantiates":
|
|
183
|
+
return 1;
|
|
184
|
+
case "accesses":
|
|
185
|
+
case "renders":
|
|
186
|
+
return 2;
|
|
187
|
+
case "tests":
|
|
188
|
+
return 3;
|
|
189
|
+
case "overrides":
|
|
190
|
+
case "decorates":
|
|
191
|
+
return 4;
|
|
192
|
+
case "extends":
|
|
193
|
+
case "implements":
|
|
194
|
+
return 5;
|
|
195
|
+
case "type_ref":
|
|
196
|
+
return 6;
|
|
197
|
+
default:
|
|
198
|
+
return 10;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function directMentions(graph: TtscGraphMemory, query: string): string[] {
|
|
152
203
|
const handles = new Set<string>();
|
|
204
|
+
for (const token of query.split(/\s+/)) {
|
|
205
|
+
const handle = normalizeNodeIdToken(token);
|
|
206
|
+
if (handle !== undefined && graph.node(handle) !== undefined) {
|
|
207
|
+
handles.add(handle);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
153
210
|
for (const match of query.matchAll(/`([^`]+)`/g)) {
|
|
154
|
-
const
|
|
211
|
+
const raw = match[1] ?? "";
|
|
212
|
+
const id = normalizeNodeIdToken(raw);
|
|
213
|
+
if (id !== undefined && graph.node(id) !== undefined) {
|
|
214
|
+
handles.add(id);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const handle = normalizeHandle(raw);
|
|
155
218
|
if (handle !== undefined) handles.add(handle);
|
|
156
219
|
}
|
|
157
220
|
for (const match of query.matchAll(
|
|
@@ -163,6 +226,18 @@ function directMentions(query: string): string[] {
|
|
|
163
226
|
return [...handles];
|
|
164
227
|
}
|
|
165
228
|
|
|
229
|
+
function normalizeNodeIdToken(raw: string): string | undefined {
|
|
230
|
+
const value = raw
|
|
231
|
+
.trim()
|
|
232
|
+
.replace(/^[`"'([{]+/, "")
|
|
233
|
+
.replace(/[`"',.;:)\]}]+$/, "");
|
|
234
|
+
return /^[^\s#]+#[^\s#]+:(class|interface|type|enum|function|method|variable|property)$/.test(
|
|
235
|
+
value,
|
|
236
|
+
)
|
|
237
|
+
? value
|
|
238
|
+
: undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
166
241
|
function normalizeHandle(raw: string): string | undefined {
|
|
167
242
|
const value = raw.trim();
|
|
168
243
|
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(value)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
|
+
import { ITtscGraphLookup } from "../structures/ITtscGraphLookup";
|
|
2
3
|
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
3
|
-
import {
|
|
4
|
-
import { signatureOf } from "./runExpand";
|
|
4
|
+
import { decoratorsOf, signatureOf } from "./runDetails";
|
|
5
5
|
|
|
6
6
|
// One file should not crowd out the rest of the ranking, so cap hits per file.
|
|
7
7
|
const PER_FILE = 3;
|
|
8
|
-
const DEFAULT_LIMIT =
|
|
8
|
+
const DEFAULT_LIMIT = 5;
|
|
9
|
+
const MAX_LIMIT = 6;
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Rank the graph's symbols against a natural query. Scoring blends exact and
|
|
@@ -14,35 +15,50 @@ const DEFAULT_LIMIT = 12;
|
|
|
14
15
|
* nodes and caps per file so the result is a diverse, relevant shortlist rather
|
|
15
16
|
* than one file's roster.
|
|
16
17
|
*/
|
|
17
|
-
export function
|
|
18
|
+
export function runLookup(
|
|
18
19
|
graph: TtscGraphMemory,
|
|
19
|
-
props:
|
|
20
|
-
):
|
|
20
|
+
props: ITtscGraphLookup.IRequest,
|
|
21
|
+
): ITtscGraphLookup {
|
|
21
22
|
const terms = subwords(props.query);
|
|
23
|
+
const codeTerms = exactCodeTerms(props.query);
|
|
24
|
+
const requestedKinds = requestedSymbolKinds(props.query);
|
|
22
25
|
const queryLc = props.query.trim().toLowerCase();
|
|
23
|
-
|
|
26
|
+
const wantsInternal = wantsInternalSymbol(queryLc, codeTerms);
|
|
27
|
+
if (terms.length === 0)
|
|
28
|
+
return { type: "lookup", hits: [], next: { details: [], traceFrom: [] } };
|
|
24
29
|
|
|
25
|
-
const scored:
|
|
30
|
+
const scored: ITtscGraphLookup.IHit[] = [];
|
|
26
31
|
for (const node of graph.nodes) {
|
|
27
32
|
if (node.kind === "file") continue;
|
|
28
|
-
const score = scoreNode(
|
|
33
|
+
const score = scoreNode(
|
|
34
|
+
graph,
|
|
35
|
+
node,
|
|
36
|
+
queryLc,
|
|
37
|
+
terms,
|
|
38
|
+
codeTerms,
|
|
39
|
+
requestedKinds,
|
|
40
|
+
wantsInternal,
|
|
41
|
+
);
|
|
29
42
|
if (score <= 0) continue;
|
|
30
|
-
|
|
43
|
+
const hit: ITtscGraphLookup.IHit = {
|
|
31
44
|
id: node.id,
|
|
32
45
|
name: node.qualifiedName ?? node.name,
|
|
33
46
|
kind: node.kind,
|
|
34
47
|
file: node.file,
|
|
35
48
|
line: node.evidence?.startLine,
|
|
36
49
|
score: Math.round(score),
|
|
37
|
-
}
|
|
50
|
+
};
|
|
51
|
+
const decorators = decoratorsOf(node);
|
|
52
|
+
if (decorators !== undefined) hit.decorators = decorators;
|
|
53
|
+
scored.push(hit);
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
scored.sort((a, b) => b.score - a.score);
|
|
41
57
|
|
|
42
58
|
// Diversity: keep at most PER_FILE hits per file while filling up to the limit.
|
|
43
|
-
const limit =
|
|
59
|
+
const limit = bound(props.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
44
60
|
const perFile = new Map<string, number>();
|
|
45
|
-
const hits:
|
|
61
|
+
const hits: ITtscGraphLookup.IHit[] = [];
|
|
46
62
|
for (const hit of scored) {
|
|
47
63
|
const used = perFile.get(hit.file) ?? 0;
|
|
48
64
|
if (used >= PER_FILE) continue;
|
|
@@ -51,15 +67,22 @@ export function runQuery(
|
|
|
51
67
|
if (hits.length >= limit) break;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
|
-
// Attach each kept hit's signature
|
|
55
|
-
//
|
|
70
|
+
// Attach each kept hit's signature only for the shortlist, so the model can
|
|
71
|
+
// often answer from lookup alone without a details call.
|
|
56
72
|
for (const hit of hits) {
|
|
57
73
|
const node = graph.node(hit.id);
|
|
58
74
|
if (node === undefined) continue;
|
|
59
75
|
const sig = signatureOf(graph.project, node);
|
|
60
76
|
if (sig !== undefined) hit.signature = sig;
|
|
61
77
|
}
|
|
62
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
type: "lookup",
|
|
80
|
+
hits,
|
|
81
|
+
next: {
|
|
82
|
+
details: hits.map((hit) => hit.id),
|
|
83
|
+
traceFrom: hits.map((hit) => hit.id),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
63
86
|
}
|
|
64
87
|
|
|
65
88
|
/** Score one node against the query; 0 means no match. */
|
|
@@ -68,6 +91,9 @@ function scoreNode(
|
|
|
68
91
|
node: ITtscGraphNode,
|
|
69
92
|
queryLc: string,
|
|
70
93
|
terms: string[],
|
|
94
|
+
codeTerms: string[],
|
|
95
|
+
requestedKinds: Set<string>,
|
|
96
|
+
wantsInternal: boolean,
|
|
71
97
|
): number {
|
|
72
98
|
const name = node.name.toLowerCase();
|
|
73
99
|
const qualified = (node.qualifiedName ?? node.name).toLowerCase();
|
|
@@ -84,6 +110,18 @@ function scoreNode(
|
|
|
84
110
|
score += 60;
|
|
85
111
|
}
|
|
86
112
|
|
|
113
|
+
for (const codeTerm of codeTerms) {
|
|
114
|
+
if (name === codeTerm || qualified === codeTerm) {
|
|
115
|
+
score += 110;
|
|
116
|
+
} else if (qualified.endsWith(`.${codeTerm}`)) {
|
|
117
|
+
score += 95;
|
|
118
|
+
} else if (codeTerm.includes(".") && qualified.endsWith(codeTerm)) {
|
|
119
|
+
score += 85;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (requestedKinds.has(node.kind)) score += 16;
|
|
124
|
+
|
|
87
125
|
let covered = 0;
|
|
88
126
|
for (const term of terms) {
|
|
89
127
|
if (nameSubs.includes(term)) {
|
|
@@ -113,9 +151,68 @@ function scoreNode(
|
|
|
113
151
|
if (node.external) score *= 0.5;
|
|
114
152
|
if (node.ignored) score *= 0.3;
|
|
115
153
|
if (isTestFile(node.file)) score *= 0.7;
|
|
154
|
+
if (!wantsInternal && isInternalish(node)) score *= 0.82;
|
|
116
155
|
return score;
|
|
117
156
|
}
|
|
118
157
|
|
|
158
|
+
function wantsInternalSymbol(queryLc: string, codeTerms: string[]): boolean {
|
|
159
|
+
return (
|
|
160
|
+
/\b(internal|private|implementation|impl)\b/.test(queryLc) ||
|
|
161
|
+
codeTerms.some((term) => term.startsWith("_") || term.includes("internal"))
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isInternalish(node: ITtscGraphNode): boolean {
|
|
166
|
+
const name = node.qualifiedName ?? node.name;
|
|
167
|
+
return (
|
|
168
|
+
name.startsWith("_") ||
|
|
169
|
+
name.includes("._") ||
|
|
170
|
+
subwords(name).some((word) => word === "internal" || word === "internals")
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function exactCodeTerms(query: string): string[] {
|
|
175
|
+
const out = new Set<string>();
|
|
176
|
+
for (const match of query.matchAll(/`([^`]+)`/g)) {
|
|
177
|
+
const normalized = normalizeCodeTerm(match[1] ?? "");
|
|
178
|
+
if (normalized !== undefined) out.add(normalized);
|
|
179
|
+
}
|
|
180
|
+
for (const match of query.matchAll(
|
|
181
|
+
/\b([A-Za-z_$][\w$]*)\s+(method|function|class|interface|type|variable)\b/gi,
|
|
182
|
+
)) {
|
|
183
|
+
const normalized = normalizeCodeTerm(match[1] ?? "");
|
|
184
|
+
if (normalized !== undefined) out.add(normalized);
|
|
185
|
+
}
|
|
186
|
+
for (const match of query.matchAll(
|
|
187
|
+
/\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b/g,
|
|
188
|
+
)) {
|
|
189
|
+
const normalized = normalizeCodeTerm(match[0]);
|
|
190
|
+
if (normalized !== undefined) out.add(normalized);
|
|
191
|
+
}
|
|
192
|
+
return [...out];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function requestedSymbolKinds(query: string): Set<string> {
|
|
196
|
+
const kinds = new Set<string>();
|
|
197
|
+
const lc = query.toLowerCase();
|
|
198
|
+
if (/\bmethods?\b/.test(lc)) kinds.add("method");
|
|
199
|
+
if (/\bfunctions?\b/.test(lc)) {
|
|
200
|
+
kinds.add("function");
|
|
201
|
+
kinds.add("method");
|
|
202
|
+
kinds.add("variable");
|
|
203
|
+
}
|
|
204
|
+
if (/\bclasses?\b/.test(lc)) kinds.add("class");
|
|
205
|
+
if (/\binterfaces?\b/.test(lc)) kinds.add("interface");
|
|
206
|
+
if (/\btypes?\b/.test(lc)) kinds.add("type");
|
|
207
|
+
if (/\bvariables?\b|\bconst\b|\blet\b/.test(lc)) kinds.add("variable");
|
|
208
|
+
return kinds;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeCodeTerm(raw: string): string | undefined {
|
|
212
|
+
const value = raw.trim().toLowerCase();
|
|
213
|
+
return /^[a-z_$][\w$]*(?:\.[a-z_$][\w$]*)*$/.test(value) ? value : undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
119
216
|
/** Non-structural in+out degree (code dependency, not nesting). */
|
|
120
217
|
function degree(graph: TtscGraphMemory, id: string): number {
|
|
121
218
|
let n = 0;
|
|
@@ -135,6 +232,16 @@ function isTestFile(file: string): boolean {
|
|
|
135
232
|
);
|
|
136
233
|
}
|
|
137
234
|
|
|
235
|
+
function bound(
|
|
236
|
+
value: number | undefined,
|
|
237
|
+
fallback: number,
|
|
238
|
+
min: number,
|
|
239
|
+
max: number,
|
|
240
|
+
): number {
|
|
241
|
+
const n = value === undefined || !Number.isFinite(value) ? fallback : value;
|
|
242
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
243
|
+
}
|
|
244
|
+
|
|
138
245
|
/**
|
|
139
246
|
* Split an identifier or phrase into lowercase subword tokens: CamelCase,
|
|
140
247
|
* snake, dotted, and space boundaries all break, so `getHTTPResponse`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TtscGraphMemory } from "../model/TtscGraphMemory";
|
|
2
|
+
import { ITtscGraphNode } from "../structures/ITtscGraphNode";
|
|
2
3
|
import { ITtscGraphOverview } from "../structures/ITtscGraphOverview";
|
|
3
4
|
|
|
4
5
|
/** Edges that express nesting/packaging, not code dependency. */
|
|
@@ -13,10 +14,10 @@ const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
|
|
|
13
14
|
*/
|
|
14
15
|
export function runOverview(
|
|
15
16
|
graph: TtscGraphMemory,
|
|
16
|
-
props: ITtscGraphOverview.
|
|
17
|
+
props: ITtscGraphOverview.IRequest,
|
|
17
18
|
): ITtscGraphOverview {
|
|
18
19
|
const aspect = props.aspect ?? "all";
|
|
19
|
-
const want = (a: ITtscGraphOverview.
|
|
20
|
+
const want = (a: ITtscGraphOverview.IRequest["aspect"]): boolean =>
|
|
20
21
|
aspect === "all" || aspect === a;
|
|
21
22
|
|
|
22
23
|
const byKind: Record<string, number> = {};
|
|
@@ -27,6 +28,7 @@ export function runOverview(
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const result: ITtscGraphOverview = {
|
|
31
|
+
type: "overview",
|
|
30
32
|
project: graph.project,
|
|
31
33
|
counts: {
|
|
32
34
|
files,
|
|
@@ -62,12 +64,12 @@ function layers(graph: TtscGraphMemory): ITtscGraphOverview.ILayer[] {
|
|
|
62
64
|
exported: entry.exported,
|
|
63
65
|
}))
|
|
64
66
|
.sort((a, b) => b.files - a.files)
|
|
65
|
-
.slice(0,
|
|
67
|
+
.slice(0, 10);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* The symbols at the center of the dependency graph, ranked by real fan-in and
|
|
70
|
-
* fan-out
|
|
72
|
+
* fan-out. Structural `contains`/`exports`/`imports` edges are excluded so the
|
|
71
73
|
* ranking reflects code dependency, not nesting.
|
|
72
74
|
*/
|
|
73
75
|
function hotspots(graph: TtscGraphMemory): ITtscGraphOverview.IHotspot[] {
|
|
@@ -80,16 +82,13 @@ function hotspots(graph: TtscGraphMemory): ITtscGraphOverview.IHotspot[] {
|
|
|
80
82
|
return graph.nodes
|
|
81
83
|
.filter((node) => !node.external && node.kind !== "file")
|
|
82
84
|
.map((node) => ({
|
|
83
|
-
|
|
84
|
-
name: node.qualifiedName ?? node.name,
|
|
85
|
-
kind: node.kind,
|
|
86
|
-
file: node.file,
|
|
85
|
+
...nodeOf(node),
|
|
87
86
|
fanIn: real(node.id, "in"),
|
|
88
87
|
fanOut: real(node.id, "out"),
|
|
89
88
|
}))
|
|
90
89
|
.filter((h) => h.fanIn + h.fanOut > 0)
|
|
91
90
|
.sort((a, b) => b.fanIn + b.fanOut - (a.fanIn + a.fanOut))
|
|
92
|
-
.slice(0,
|
|
91
|
+
.slice(0, 10);
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
/** Declaration kinds that make up a meaningful public API surface. */
|
|
@@ -102,12 +101,13 @@ const API_KINDS = new Set<string>([
|
|
|
102
101
|
]);
|
|
103
102
|
|
|
104
103
|
/**
|
|
105
|
-
* The exported API surface: the exported symbols a consumer of the project
|
|
106
|
-
* use, ranked by how depended-on each is (real fan-in/out, structural
|
|
107
|
-
* excluded). Ranking by dependency rather than by which file declares the
|
|
108
|
-
* exports surfaces the load-bearing types (a DataSource, a
|
|
109
|
-
* instead of whichever file bundles the most type aliases;
|
|
110
|
-
* generated files are dropped so they cannot crowd the real
|
|
104
|
+
* The exported API surface: the exported symbols a consumer of the project
|
|
105
|
+
* would use, ranked by how depended-on each is (real fan-in/out, structural
|
|
106
|
+
* edges excluded). Ranking by dependency rather than by which file declares the
|
|
107
|
+
* most exports surfaces the load-bearing types (a DataSource, a
|
|
108
|
+
* SelectQueryBuilder) instead of whichever file bundles the most type aliases;
|
|
109
|
+
* test, typings, and generated files are dropped so they cannot crowd the real
|
|
110
|
+
* surface out.
|
|
111
111
|
*/
|
|
112
112
|
function publicApi(graph: TtscGraphMemory): ITtscGraphOverview.IPublicApi[] {
|
|
113
113
|
const degree = (id: string): number => {
|
|
@@ -122,21 +122,19 @@ function publicApi(graph: TtscGraphMemory): ITtscGraphOverview.IPublicApi[] {
|
|
|
122
122
|
.exported()
|
|
123
123
|
.filter((node) => API_KINDS.has(node.kind) && !isNoiseFile(node.file))
|
|
124
124
|
.map((node) => ({
|
|
125
|
-
|
|
126
|
-
kind: node.kind,
|
|
127
|
-
file: node.file,
|
|
125
|
+
node: nodeOf(node),
|
|
128
126
|
degree: degree(node.id),
|
|
129
127
|
}))
|
|
130
128
|
.sort((a, b) => b.degree - a.degree)
|
|
131
|
-
.slice(0,
|
|
132
|
-
.map((
|
|
129
|
+
.slice(0, 15)
|
|
130
|
+
.map((ranked) => ranked.node);
|
|
133
131
|
}
|
|
134
132
|
|
|
135
133
|
/**
|
|
136
134
|
* A file whose exports are noise for an architecture overview: a test, a
|
|
137
|
-
* dependency's bundled `.d.ts`/typings, or generated output. The conventions
|
|
138
|
-
* universal (a `test`/`spec` path, a `typings` file), so excluding them is
|
|
139
|
-
* framework-specific
|
|
135
|
+
* dependency's bundled `.d.ts`/typings, or generated output. The conventions
|
|
136
|
+
* are universal (a `test`/`spec` path, a `typings` file), so excluding them is
|
|
137
|
+
* not framework-specific; it keeps the API surface to authored, used code.
|
|
140
138
|
*/
|
|
141
139
|
function isNoiseFile(file: string): boolean {
|
|
142
140
|
return (
|
|
@@ -152,3 +150,17 @@ function dirname(file: string): string {
|
|
|
152
150
|
const slash = file.lastIndexOf("/");
|
|
153
151
|
return slash >= 0 ? file.slice(0, slash) : ".";
|
|
154
152
|
}
|
|
153
|
+
|
|
154
|
+
/** Stable symbol coordinate shared by overview facets. */
|
|
155
|
+
function nodeOf(node: ITtscGraphNode): ITtscGraphOverview.INode {
|
|
156
|
+
const out: ITtscGraphOverview.INode = {
|
|
157
|
+
id: node.id,
|
|
158
|
+
name: node.qualifiedName ?? node.name,
|
|
159
|
+
kind: node.kind,
|
|
160
|
+
file: node.file,
|
|
161
|
+
};
|
|
162
|
+
if (node.evidence?.startLine !== undefined) {
|
|
163
|
+
out.line = node.evidence.startLine;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|