@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
@@ -4,25 +4,73 @@
4
4
  * files. Keep it short; the per-tool descriptions carry the detail.
5
5
  */
6
6
  export const instructions = `
7
- This TypeScript project is indexed by the compiler. Use the graph as the first
8
- source of codebase coordinates: it has already resolved symbols and the
9
- relationships between them.
10
-
11
- - graph_index: first call for source questions. It returns ranked symbols,
12
- declaration signatures, directly mentioned code handles, and nearby dependency
13
- context without source bodies.
14
- - graph_overview: the architecture, layers, hotspots, and public API.
15
- - graph_query: find any symbol by name or description; each hit carries its
16
- signature, usually enough to answer without expanding. Use next.expand when
17
- you need bodies.
18
- - graph_trace: follow a flow forward, reverse, or to its impact; or give dotted
19
- from/to names for the path between two symbols, how A reaches B, in one call.
20
- - graph_expand: a symbol's declared shape, its signature, and a container's
21
- members; source:true to read a specific TypeScript body after graph_index,
22
- graph_query, or graph_trace has located it.
23
-
24
- Answer in as few calls as you can. For TypeScript declarations, read source with
25
- graph_expand(source:true) on the resolved handles. Use shell or file reads only
26
- for a non-TypeScript file, generated output, or an exact literal text search that
27
- is not represented as symbols or edges.
7
+ Before answering a TypeScript codebase question, call query. It is the
8
+ code-evidence path:
9
+ symbols, dependency paths, edge evidence ranges, and sourceSpan line anchors
10
+ from the resident project graph. Do not answer from assumptions, ls, rg, cat,
11
+ or Get-Content when graph evidence can answer the code question.
12
+
13
+ The graph is a resident TypeScript fact map, not an answer writer. Fill
14
+ arguments in order: question, graphNeed, draft, review, request. Write
15
+ draft.reason before draft.type, review it for overfetch and non-graph fallback,
16
+ then choose one final request.type:
17
+ entrypoints, lookup, trace, details, overview, or escape. If the review shows the
18
+ question is about scripts, config, generated output, prose docs, or evidence
19
+ already in hand, choose escape instead of spending a graph
20
+ operation. If more TypeScript evidence is needed, make another graph request
21
+ instead of switching to shell search.
22
+
23
+ Budget graph calls before the first request. Most answers need 1-3 calls, and
24
+ four calls is the hard stop for one answer. A fifth graph call means the tool is
25
+ being used as a source reader; answer from returned handles/ranges or choose
26
+ escape and report the missing span.
27
+
28
+ For behavior, lifecycle, request-flow, rendering-flow, or validation-flow
29
+ questions, start with one default entrypoints call, then one trace from the best
30
+ handle, then answer. Use overview only for broad architecture or public API
31
+ orientation.
32
+
33
+ The graph already knows resolved symbols, dependency edges, evidence spans,
34
+ decorators, stable handles, and sourceSpan line anchors. Use returned ranges and
35
+ handles as the evidence. If implementation text is required to decide a detail,
36
+ report the gap and the smallest sourceSpan instead of opening files during the
37
+ graph answer.
38
+
39
+ For caller or call-site questions, do not use rg. Use trace with direction:
40
+ "reverse" or details with neighbors: true; both return edge evidence and line
41
+ anchors for the call expression.
42
+
43
+ Request types:
44
+
45
+ - entrypoints: compact shortlist for behavior-specific code questions. It returns
46
+ ranked symbols, direct mentions, and small dependency orientation without
47
+ implementation text. Do not use it as the first broad public API map.
48
+ - lookup: targeted symbol search for a class, method, function, property, or
49
+ type when you do not already have its handle.
50
+ - trace: call/type/dependency flow for "how A reaches B", lifecycle,
51
+ request-flow, rendering-flow, validation-flow, and impact questions.
52
+ - details: signatures, members, direct calls, direct types, dependency
53
+ neighbors, and sourceSpan anchors for selected handles.
54
+ - overview: source-free architecture map for layers, hotspots, counts, and
55
+ public API. Use it to choose a central exported TypeScript API or entry point
56
+ without reading package scripts.
57
+ - escape: no-op route when the review decides this tool was the wrong evidence
58
+ source or the previous graph result is enough.
59
+
60
+ For a flow question, use entrypoints once, then trace before details. Keep
61
+ dependency maps compact: default limits first, one to three handles in details,
62
+ and no larger limits unless the previous result was truncated and the missing
63
+ piece is named. Do not spend graph calls only to find tests; mention tests only
64
+ when the returned graph slice already exposes them. Stop once file/symbol/range
65
+ evidence is enough to answer.
66
+
67
+ Copy exact names from returned nodes, references, aliases, evidence ranges,
68
+ sourceSpan anchors, and trace steps. Do not use shell to recover TypeScript line
69
+ numbers, call targets, or branch details not already returned by graph evidence;
70
+ name the missing detail and give the returned range.
71
+
72
+ Package scripts, config files, generated output, prose documentation, and exact
73
+ text searches are separate evidence sources. Use them only when the user asks
74
+ about those sources directly; do not use them to answer a TypeScript API or
75
+ call-path question.
28
76
  `.trim();
@@ -0,0 +1,469 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { TtscGraphMemory } from "../model/TtscGraphMemory";
5
+ import { ITtscGraphDecorator } from "../structures/ITtscGraphDecorator";
6
+ import { ITtscGraphDetails } from "../structures/ITtscGraphDetails";
7
+ import { ITtscGraphEdge } from "../structures/ITtscGraphEdge";
8
+ import { ITtscGraphEvidence } from "../structures/ITtscGraphEvidence";
9
+ import { ITtscGraphNode } from "../structures/ITtscGraphNode";
10
+ import { accessAliasesFor } from "./accessAliases";
11
+ import { resolveGraphHandle } from "./resolveHandle";
12
+
13
+ // A signature is the declaration head up to the body brace: a handful of lines.
14
+ const MAX_SIGNATURE_LINES = 4;
15
+ // Neighbor lists are a map, not a dump; keep them scannable.
16
+ const DEFAULT_NEIGHBORS = 2;
17
+ const MAX_NEIGHBORS = 3;
18
+ // A container outline can be long; default to a scannable first page.
19
+ const DEFAULT_MEMBERS = 6;
20
+ const MAX_MEMBERS = 8;
21
+ // Direct dependency groups are orientation slices, not full fan-out dumps.
22
+ const DEFAULT_DEPENDENCIES = 1;
23
+ const MAX_DEPENDENCIES = 2;
24
+ // Object literal outlines are navigation aids, not source excerpts.
25
+ const MAX_OBJECT_MEMBER_LINES = 300;
26
+ // Structural relationships are navigation, not the dependency picture details is for.
27
+ const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
28
+ // Kinds whose value is their member outline, not implementation text.
29
+ const CONTAINER_KINDS = new Set<string>([
30
+ "class",
31
+ "interface",
32
+ "namespace",
33
+ "module",
34
+ "enum",
35
+ "file",
36
+ ]);
37
+
38
+ /**
39
+ * Resolve each handle to its declared shape: sourceSpan anchors, signature,
40
+ * direct dependencies, and for containers, member outlines. It answers from the
41
+ * graph's resolved structure instead of inlining implementation bodies.
42
+ */
43
+ export function runDetails(
44
+ graph: TtscGraphMemory,
45
+ props: ITtscGraphDetails.IRequest,
46
+ ): ITtscGraphDetails {
47
+ const neighborLimit = bound(
48
+ props.neighborLimit,
49
+ DEFAULT_NEIGHBORS,
50
+ 1,
51
+ MAX_NEIGHBORS,
52
+ );
53
+ const memberLimit = bound(props.memberLimit, DEFAULT_MEMBERS, 1, MAX_MEMBERS);
54
+ const dependencyLimit = bound(
55
+ props.dependencyLimit,
56
+ DEFAULT_DEPENDENCIES,
57
+ 1,
58
+ MAX_DEPENDENCIES,
59
+ );
60
+ const wantNeighbors = props.neighbors === true;
61
+ const nodes: ITtscGraphDetails.INode[] = [];
62
+ const unknown: string[] = [];
63
+ for (const handle of props.handles) {
64
+ const resolved = resolveGraphHandle(graph, handle);
65
+ if (resolved.node === undefined) {
66
+ unknown.push(handle);
67
+ continue;
68
+ }
69
+ const node = resolved.node;
70
+ const detail: ITtscGraphDetails.INode = {
71
+ id: node.id,
72
+ name: node.qualifiedName ?? node.name,
73
+ kind: node.kind,
74
+ file: node.file,
75
+ };
76
+ if (node.evidence?.startLine) detail.line = node.evidence.startLine;
77
+ const sig = signatureOf(graph.project, node);
78
+ if (sig !== undefined) detail.signature = sig;
79
+ const signatureLiterals = literalSummaries(sig);
80
+ const decorators = decoratorsOf(node);
81
+ if (decorators !== undefined) detail.decorators = decorators;
82
+ const implementation = evidenceCoordinatesOf(node.implementation);
83
+ if (implementation !== undefined) detail.implementation = implementation;
84
+ const span = implementation ?? evidenceCoordinatesOf(node.evidence);
85
+ if (span !== undefined) {
86
+ detail.sourceSpan = {
87
+ file: span.file,
88
+ startLine: span.startLine,
89
+ endLine: span.endLine,
90
+ };
91
+ }
92
+ const calls = dependencyRefs(graph, node, executionKinds, dependencyLimit);
93
+ if (calls.length > 0) detail.calls = calls;
94
+ const types = dependencyRefs(graph, node, typeKinds, dependencyLimit);
95
+ if (types.length > 0) detail.types = types;
96
+ if (CONTAINER_KINDS.has(node.kind)) {
97
+ const list = members(graph, node, memberLimit);
98
+ if (list.length > 0) detail.members = list;
99
+ }
100
+ if (node.kind === "variable" && detail.sourceSpan !== undefined) {
101
+ const list = objectLiteralMembers(
102
+ graph.project,
103
+ detail.sourceSpan,
104
+ memberLimit,
105
+ );
106
+ if (list.length > 0) detail.members = list;
107
+ }
108
+ if (signatureLiterals.length > 0)
109
+ detail.literals = signatureLiterals.slice(0, 6);
110
+ if (wantNeighbors) {
111
+ detail.dependsOn = refs(
112
+ graph,
113
+ graph.outgoing(node.id),
114
+ "to",
115
+ neighborLimit,
116
+ );
117
+ detail.dependedOnBy = refs(
118
+ graph,
119
+ graph.incoming(node.id),
120
+ "from",
121
+ neighborLimit,
122
+ );
123
+ }
124
+ nodes.push(detail);
125
+ }
126
+ return { type: "details", nodes, unknown };
127
+ }
128
+
129
+ /** The members a container owns (via `contains`), each with its own signature. */
130
+ function members(
131
+ graph: TtscGraphMemory,
132
+ node: ITtscGraphNode,
133
+ limit: number,
134
+ ): ITtscGraphDetails.IMember[] {
135
+ const out: ITtscGraphDetails.IMember[] = [];
136
+ for (const edge of graph.outgoing(node.id)) {
137
+ if (edge.kind !== "contains") continue;
138
+ const member = graph.node(edge.to);
139
+ if (member === undefined) continue;
140
+ const m: ITtscGraphDetails.IMember = {
141
+ name: member.qualifiedName ?? member.name,
142
+ kind: member.kind,
143
+ };
144
+ if (member.evidence?.startLine) m.line = member.evidence.startLine;
145
+ const sig = signatureOf(graph.project, member);
146
+ if (sig !== undefined) m.signature = sig;
147
+ const decorators = decoratorsOf(member);
148
+ if (decorators !== undefined) m.decorators = decorators;
149
+ out.push(m);
150
+ if (out.length >= limit) break;
151
+ }
152
+ return out;
153
+ }
154
+
155
+ function objectLiteralMembers(
156
+ project: string,
157
+ span: Pick<ITtscGraphEvidence, "file" | "startLine" | "endLine">,
158
+ limit: number,
159
+ ): ITtscGraphDetails.IMember[] {
160
+ if (span.endLine === undefined) return [];
161
+ if (span.endLine - span.startLine > MAX_OBJECT_MEMBER_LINES) return [];
162
+ const lines = fileLines(project, span.file);
163
+ if (lines === undefined) return [];
164
+ const start = Math.max(0, span.startLine - 1);
165
+ const end = Math.min(lines.length - 1, span.endLine - 1);
166
+ const members: ITtscGraphDetails.IMember[] = [];
167
+ let depth = 0;
168
+ let entered = false;
169
+ for (let i = start; i <= end; i++) {
170
+ const raw = lines[i] ?? "";
171
+ const text = stripStrings(raw);
172
+ const before = depth;
173
+ if (entered && before === 1) {
174
+ const member = objectMemberOf(raw, i + 1);
175
+ if (member !== undefined) {
176
+ members.push(member);
177
+ if (members.length >= limit) break;
178
+ }
179
+ }
180
+ for (const char of text) {
181
+ if (char === "{") {
182
+ depth++;
183
+ entered = true;
184
+ } else if (char === "}") {
185
+ depth = Math.max(0, depth - 1);
186
+ }
187
+ }
188
+ }
189
+ return members;
190
+ }
191
+
192
+ function objectMemberOf(
193
+ line: string,
194
+ lineNumber: number,
195
+ ): ITtscGraphDetails.IMember | undefined {
196
+ const text = line.trim();
197
+ if (
198
+ text === "" ||
199
+ text.startsWith("//") ||
200
+ text.startsWith("/*") ||
201
+ text.startsWith("*")
202
+ ) {
203
+ return undefined;
204
+ }
205
+ const property = /^(['"]?)([A-Za-z_$][\w$-]*)\1\s*\??\s*:/.exec(text);
206
+ if (property !== null) {
207
+ return {
208
+ name: property[2]!,
209
+ kind: "property",
210
+ line: lineNumber,
211
+ signature: signatureLine(text),
212
+ };
213
+ }
214
+ const method =
215
+ /^(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$-]*)\s*\(/.exec(text);
216
+ if (method !== null) {
217
+ return {
218
+ name: method[1]!,
219
+ kind: "method",
220
+ line: lineNumber,
221
+ signature: signatureLine(text),
222
+ };
223
+ }
224
+ return undefined;
225
+ }
226
+
227
+ function signatureLine(text: string): string {
228
+ return text.replace(/\s+/g, " ").replace(/,$/, "");
229
+ }
230
+
231
+ function stripStrings(line: string): string {
232
+ return line.replace(/\/\/.*$/, "").replace(/(['"`])(?:\\.|(?!\1).)*\1/g, "");
233
+ }
234
+
235
+ /** Map dependency edges to references on their far endpoint, dropping structure. */
236
+ function refs(
237
+ graph: TtscGraphMemory,
238
+ edges: readonly ITtscGraphEdge[],
239
+ end: "to" | "from",
240
+ limit: number,
241
+ ): ITtscGraphDetails.IReference[] {
242
+ const ranked: Array<{ ref: ITtscGraphDetails.IReference; rank: number }> = [];
243
+ for (const edge of edges) {
244
+ if (STRUCTURAL_KINDS.has(edge.kind)) continue;
245
+ const other = graph.node(end === "to" ? edge.to : edge.from);
246
+ if (other === undefined) continue;
247
+ const ref: ITtscGraphDetails.IReference = {
248
+ id: other.id,
249
+ name: other.qualifiedName ?? other.name,
250
+ kind: other.kind,
251
+ file: other.file,
252
+ relation: edge.kind,
253
+ };
254
+ if (other.evidence?.startLine) ref.line = other.evidence.startLine;
255
+ const evidence = edgeEvidenceOf(edge);
256
+ if (evidence !== undefined) ref.evidence = evidence;
257
+ const aliases = accessAliasesFor(other, edgeEvidenceTextOf(edge));
258
+ if (aliases !== undefined) ref.aliases = aliases;
259
+ ranked.push({ ref, rank: refRank(ref, edge) });
260
+ }
261
+ ranked.sort((a, b) => a.rank - b.rank);
262
+ const out: ITtscGraphDetails.IReference[] = [];
263
+ for (const item of ranked) {
264
+ out.push(item.ref);
265
+ if (out.length >= limit) break;
266
+ }
267
+ return out;
268
+ }
269
+
270
+ const executionKinds = new Set([
271
+ "calls",
272
+ "instantiates",
273
+ "accesses",
274
+ "renders",
275
+ ]);
276
+ const typeKinds = new Set(["type_ref", "extends", "implements", "overrides"]);
277
+
278
+ function dependencyRefs(
279
+ graph: TtscGraphMemory,
280
+ node: ITtscGraphNode,
281
+ kinds: ReadonlySet<string>,
282
+ limit: number,
283
+ ): ITtscGraphDetails.IReference[] {
284
+ const ranked: Array<{ ref: ITtscGraphDetails.IReference; rank: number }> = [];
285
+ for (const edge of graph.outgoing(node.id)) {
286
+ if (!kinds.has(edge.kind)) continue;
287
+ const other = graph.node(edge.to);
288
+ if (other === undefined || other.kind === "file") continue;
289
+ const name = other.qualifiedName ?? other.name;
290
+ const ref: ITtscGraphDetails.IReference = {
291
+ id: other.id,
292
+ name,
293
+ kind: other.kind,
294
+ file: other.file,
295
+ relation: edge.kind,
296
+ };
297
+ if (other.evidence?.startLine) ref.line = other.evidence.startLine;
298
+ const evidence = edgeEvidenceOf(edge);
299
+ if (evidence !== undefined) ref.evidence = evidence;
300
+ const aliases = accessAliasesFor(other, edgeEvidenceTextOf(edge));
301
+ if (aliases !== undefined) ref.aliases = aliases;
302
+ ranked.push({
303
+ ref,
304
+ rank: refRank(ref, edge),
305
+ });
306
+ }
307
+ ranked.sort((a, b) => a.rank - b.rank);
308
+ const out: ITtscGraphDetails.IReference[] = [];
309
+ const seen = new Set<string>();
310
+ for (const item of ranked) {
311
+ const key = `${item.ref.relation}:${item.ref.id}`;
312
+ if (seen.has(key)) continue;
313
+ seen.add(key);
314
+ out.push(item.ref);
315
+ if (out.length >= limit) break;
316
+ }
317
+ return out;
318
+ }
319
+
320
+ function literalSummaries(text: string | undefined): string[] {
321
+ if (text === undefined) return [];
322
+ const out: string[] = [];
323
+ for (const match of text.matchAll(/(["'`])((?:\\.|(?!\1).){1,80})\1/g)) {
324
+ const value = cleanLiteral(match[2]);
325
+ if (value !== undefined && !out.includes(value)) out.push(value);
326
+ if (out.length >= 20) break;
327
+ }
328
+ return out;
329
+ }
330
+
331
+ function cleanLiteral(value: string | undefined): string | undefined {
332
+ const text = value?.replace(/\s+/g, " ").trim();
333
+ if (
334
+ text === undefined ||
335
+ text === "" ||
336
+ text.length > 40 ||
337
+ /^[{}()[\],.:;]+$/.test(text)
338
+ ) {
339
+ return undefined;
340
+ }
341
+ return text;
342
+ }
343
+
344
+ function bound(
345
+ value: number | undefined,
346
+ fallback: number,
347
+ min: number,
348
+ max: number,
349
+ ): number {
350
+ const n = value === undefined || !Number.isFinite(value) ? fallback : value;
351
+ return Math.max(min, Math.min(max, Math.floor(n)));
352
+ }
353
+
354
+ function refRank(
355
+ ref: ITtscGraphDetails.IReference,
356
+ edge: ITtscGraphEdge,
357
+ ): number {
358
+ return (
359
+ edgeKindRank(edge.kind) * 100_000 +
360
+ evidenceRank(edge) +
361
+ (ref.file.startsWith("bundled://") ? 20_000 : 0)
362
+ );
363
+ }
364
+
365
+ function evidenceRank(edge: ITtscGraphEdge): number {
366
+ const line = edge.evidence?.startLine ?? 9_999;
367
+ const col = edge.evidence?.startCol ?? 999;
368
+ return line * 100 + col;
369
+ }
370
+
371
+ function edgeKindRank(kind: string): number {
372
+ switch (kind) {
373
+ case "calls":
374
+ return 0;
375
+ case "instantiates":
376
+ return 1;
377
+ case "accesses":
378
+ case "renders":
379
+ return 2;
380
+ case "tests":
381
+ return 3;
382
+ case "overrides":
383
+ case "decorates":
384
+ return 4;
385
+ case "extends":
386
+ case "implements":
387
+ return 5;
388
+ case "type_ref":
389
+ return 6;
390
+ default:
391
+ return 10;
392
+ }
393
+ }
394
+
395
+ /** Decorator facts already captured on a node, omitted when absent. */
396
+ export function decoratorsOf(
397
+ node: ITtscGraphNode,
398
+ ): ITtscGraphDecorator[] | undefined {
399
+ return node.decorators !== undefined && node.decorators.length > 0
400
+ ? node.decorators
401
+ : undefined;
402
+ }
403
+
404
+ /** Relationship evidence as public coordinates, omitted when absent. */
405
+ export function edgeEvidenceOf(
406
+ edge: ITtscGraphEdge,
407
+ ): ITtscGraphEvidence | undefined {
408
+ return evidenceCoordinatesOf(edge.evidence);
409
+ }
410
+
411
+ function evidenceCoordinatesOf(
412
+ evidence: ITtscGraphEvidence | undefined,
413
+ ): ITtscGraphEvidence | undefined {
414
+ if (evidence === undefined) return undefined;
415
+ return {
416
+ file: evidence.file,
417
+ startLine: evidence.startLine,
418
+ ...(evidence.startCol !== undefined ? { startCol: evidence.startCol } : {}),
419
+ ...(evidence.endLine !== undefined ? { endLine: evidence.endLine } : {}),
420
+ ...(evidence.endCol !== undefined ? { endCol: evidence.endCol } : {}),
421
+ };
422
+ }
423
+
424
+ /** Source text is an internal alias hint, not part of the MCP evidence object. */
425
+ export function edgeEvidenceTextOf(edge: ITtscGraphEdge): string | undefined {
426
+ const text = (
427
+ edge.evidence as (ITtscGraphEvidence & { text?: string }) | undefined
428
+ )?.text;
429
+ return typeof text === "string" && text.length > 0 ? text : undefined;
430
+ }
431
+
432
+ /** Read a file's lines once, or undefined when it cannot be read. */
433
+ function fileLines(project: string, file: string): string[] | undefined {
434
+ if (file === "") return undefined;
435
+ try {
436
+ return fs.readFileSync(path.join(project, file), "utf8").split(/\r?\n/);
437
+ } catch {
438
+ return undefined;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * The declaration signature: the head of the declaration up to and including
444
+ * the line that opens its body (`{`), or the single declaration line when there
445
+ * is no brace, capped so a wrapped signature cannot run away.
446
+ */
447
+ export function signatureOf(
448
+ project: string,
449
+ node: ITtscGraphNode,
450
+ ): string | undefined {
451
+ const evidence = node.evidence;
452
+ const lines =
453
+ evidence === undefined ? undefined : fileLines(project, evidence.file);
454
+ if (lines === undefined || evidence === undefined) return undefined;
455
+ const start = Math.max(0, evidence.startLine - 1);
456
+ const out: string[] = [];
457
+ for (
458
+ let i = start;
459
+ i < lines.length && out.length < MAX_SIGNATURE_LINES;
460
+ i++
461
+ ) {
462
+ const line = lines[i];
463
+ if (line === undefined) break;
464
+ out.push(line);
465
+ if (line.includes("{")) break;
466
+ }
467
+ const text = out.join("\n").trim();
468
+ return text === "" ? undefined : text;
469
+ }