@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.
Files changed (61) hide show
  1. package/lib/TtscGraphApplication.js +36 -23
  2. package/lib/TtscGraphApplication.js.map +1 -1
  3. package/lib/model/loadGraph.js +17 -17
  4. package/lib/server/accessAliases.js +53 -0
  5. package/lib/server/accessAliases.js.map +1 -0
  6. package/lib/server/createServer.js +1133 -634
  7. package/lib/server/createServer.js.map +1 -1
  8. package/lib/server/instructions.js +69 -21
  9. package/lib/server/instructions.js.map +1 -1
  10. package/lib/server/runDetails.js +413 -0
  11. package/lib/server/runDetails.js.map +1 -0
  12. package/lib/server/{runIndex.js → runEntrypoints.js} +91 -26
  13. package/lib/server/runEntrypoints.js.map +1 -0
  14. package/lib/server/{runQuery.js → runLookup.js} +102 -15
  15. package/lib/server/runLookup.js.map +1 -0
  16. package/lib/server/runOverview.js +31 -21
  17. package/lib/server/runOverview.js.map +1 -1
  18. package/lib/server/runTrace.js +133 -38
  19. package/lib/server/runTrace.js.map +1 -1
  20. package/lib/structures/{ITtscGraphQuery.js → ITtscGraphDetails.js} +1 -1
  21. package/lib/structures/ITtscGraphDetails.js.map +1 -0
  22. package/lib/structures/ITtscGraphEntrypoints.js +3 -0
  23. package/lib/structures/ITtscGraphEntrypoints.js.map +1 -0
  24. package/lib/structures/{ITtscGraphExpand.js → ITtscGraphEscape.js} +1 -1
  25. package/lib/structures/ITtscGraphEscape.js.map +1 -0
  26. package/lib/structures/{ITtscGraphIndex.js → ITtscGraphLookup.js} +1 -1
  27. package/lib/structures/ITtscGraphLookup.js.map +1 -0
  28. package/lib/structures/index.js +4 -3
  29. package/lib/structures/index.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/TtscGraphApplication.ts +39 -32
  32. package/src/server/accessAliases.ts +55 -0
  33. package/src/server/createServer.ts +4 -7
  34. package/src/server/instructions.ts +69 -21
  35. package/src/server/runDetails.ts +469 -0
  36. package/src/server/{runIndex.ts → runEntrypoints.ts} +109 -34
  37. package/src/server/{runQuery.ts → runLookup.ts} +123 -16
  38. package/src/server/runOverview.ts +35 -23
  39. package/src/server/runTrace.ts +164 -38
  40. package/src/structures/ITtscGraphApplication.ts +100 -52
  41. package/src/structures/ITtscGraphDecorator.ts +12 -14
  42. package/src/structures/ITtscGraphDetails.ts +134 -0
  43. package/src/structures/{ITtscGraphIndex.ts → ITtscGraphEntrypoints.ts} +34 -16
  44. package/src/structures/ITtscGraphEscape.ts +39 -0
  45. package/src/structures/ITtscGraphEvidence.ts +5 -8
  46. package/src/structures/ITtscGraphLookup.ts +61 -0
  47. package/src/structures/ITtscGraphNode.ts +10 -4
  48. package/src/structures/ITtscGraphOverview.ts +24 -16
  49. package/src/structures/ITtscGraphTrace.ts +54 -11
  50. package/src/structures/TtscGraphNodeKind.ts +2 -2
  51. package/src/structures/index.ts +4 -3
  52. package/lib/server/runExpand.js +0 -177
  53. package/lib/server/runExpand.js.map +0 -1
  54. package/lib/server/runIndex.js.map +0 -1
  55. package/lib/server/runQuery.js.map +0 -1
  56. package/lib/structures/ITtscGraphExpand.js.map +0 -1
  57. package/lib/structures/ITtscGraphIndex.js.map +0 -1
  58. package/lib/structures/ITtscGraphQuery.js.map +0 -1
  59. package/src/server/runExpand.ts +0 -186
  60. package/src/structures/ITtscGraphExpand.ts +0 -85
  61. 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 { ITtscGraphIndex } from "../structures/ITtscGraphIndex";
3
+ import { ITtscGraphEntrypoints } from "../structures/ITtscGraphEntrypoints";
4
4
  import { ITtscGraphNode } from "../structures/ITtscGraphNode";
5
5
  import { resolveGraphHandle } from "./resolveHandle";
6
- import { signatureOf } from "./runExpand";
7
- import { runQuery } from "./runQuery";
8
-
9
- const DEFAULT_LIMIT = 8;
10
- const MAX_LIMIT = 20;
11
- const DEFAULT_NEIGHBORS = 4;
12
- const MAX_NEIGHBORS = 8;
13
- const MAX_SEEDS = 6;
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 index for a code question. The result gives the
18
- * model stable handles, declaration signatures, and direct graph context. It is
19
- * deliberately not a source reader; source remains opt-in through
20
- * graph_expand.
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 runIndex(
22
+ export function runEntrypoints(
23
23
  graph: TtscGraphMemory,
24
- props: ITtscGraphIndex.IProps,
25
- ): ITtscGraphIndex {
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 queryResult = runQuery(graph, { query, limit });
36
- const hits = queryResult.hits.map((hit) => ({ ...hit }));
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: ITtscGraphIndex.IMention = { handle };
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: ITtscGraphIndex.INeighborhood[] = [];
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
- expand: seeds.slice(0, MAX_SEEDS).map((node) => node.id),
88
- traceFrom: mentions
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
- ): ITtscGraphIndex.INode {
100
- const out: ITtscGraphIndex.INode = {
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
- ): ITtscGraphIndex.IReference {
117
- const out: ITtscGraphIndex.IReference = {
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: ITtscGraphIndex.IReference[]; truncated: boolean } {
135
- const items: ITtscGraphIndex.IReference[] = [];
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
- if (items.length < limit) items.push(refOf(other, edge));
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 directMentions(query: string): string[] {
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 handle = normalizeHandle(match[1] ?? "");
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 { ITtscGraphQuery } from "../structures/ITtscGraphQuery";
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 = 12;
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 runQuery(
18
+ export function runLookup(
18
19
  graph: TtscGraphMemory,
19
- props: ITtscGraphQuery.IProps,
20
- ): ITtscGraphQuery {
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
- if (terms.length === 0) return { hits: [], next: { expand: [] } };
26
+ const wantsInternal = wantsInternalSymbol(queryLc, codeTerms);
27
+ if (terms.length === 0)
28
+ return { type: "lookup", hits: [], next: { details: [], traceFrom: [] } };
24
29
 
25
- const scored: ITtscGraphQuery.IHit[] = [];
30
+ const scored: ITtscGraphLookup.IHit[] = [];
26
31
  for (const node of graph.nodes) {
27
32
  if (node.kind === "file") continue;
28
- const score = scoreNode(graph, node, queryLc, terms);
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
- scored.push({
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 = Math.max(1, props.limit ?? DEFAULT_LIMIT);
59
+ const limit = bound(props.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
44
60
  const perFile = new Map<string, number>();
45
- const hits: ITtscGraphQuery.IHit[] = [];
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 only the shortlist, so the read cost is
55
- // bounded — so the model can often answer from the query alone, no expand.
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 { hits, next: { expand: hits.map((hit) => hit.id) } };
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.IProps,
17
+ props: ITtscGraphOverview.IRequest,
17
18
  ): ITtscGraphOverview {
18
19
  const aspect = props.aspect ?? "all";
19
- const want = (a: ITtscGraphOverview.IProps["aspect"]): boolean =>
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, 16);
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 structural `contains`/`exports`/`imports` edges are excluded so the
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
- id: node.id,
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, 15);
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 would
106
- * use, ranked by how depended-on each is (real fan-in/out, structural edges
107
- * excluded). Ranking by dependency rather than by which file declares the most
108
- * exports surfaces the load-bearing types (a DataSource, a SelectQueryBuilder)
109
- * instead of whichever file bundles the most type aliases; test, typings, and
110
- * generated files are dropped so they cannot crowd the real surface out.
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
- name: node.qualifiedName ?? node.name,
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, 30)
132
- .map(({ name, kind, file }) => ({ name, kind, file }));
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 are
138
- * universal (a `test`/`spec` path, a `typings` file), so excluding them is not
139
- * framework-specific it keeps the API surface to authored, used code.
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
+ }