@ttsc/graph 0.19.3 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/lib/TtscGraphApplication.js +3 -1
  2. package/lib/TtscGraphApplication.js.map +1 -1
  3. package/lib/bin.js +15 -3
  4. package/lib/bin.js.map +1 -1
  5. package/lib/index.d.ts +13 -2
  6. package/lib/index.js +24 -30
  7. package/lib/index.js.map +1 -1
  8. package/lib/launcherArgs.d.ts +24 -0
  9. package/lib/launcherArgs.js +136 -0
  10. package/lib/launcherArgs.js.map +1 -0
  11. package/lib/model/TtscGraphMemory.d.ts +9 -5
  12. package/lib/model/TtscGraphMemory.js +35 -58
  13. package/lib/model/TtscGraphMemory.js.map +1 -1
  14. package/lib/model/TtscGraphNodeId.d.ts +17 -0
  15. package/lib/model/TtscGraphNodeId.js +69 -0
  16. package/lib/model/TtscGraphNodeId.js.map +1 -0
  17. package/lib/model/TtscGraphSession.d.ts +23 -7
  18. package/lib/model/TtscGraphSession.js +402 -210
  19. package/lib/model/TtscGraphSession.js.map +1 -1
  20. package/lib/model/TtscGraphSourceReader.d.ts +27 -0
  21. package/lib/model/TtscGraphSourceReader.js +97 -0
  22. package/lib/model/TtscGraphSourceReader.js.map +1 -0
  23. package/lib/model/loadGraph.d.ts +4 -4
  24. package/lib/model/loadGraph.js +233 -168
  25. package/lib/model/loadGraph.js.map +1 -1
  26. package/lib/reduce.d.ts +4 -1
  27. package/lib/reduce.js +112 -29
  28. package/lib/reduce.js.map +1 -1
  29. package/lib/server/createServer.js +17 -6
  30. package/lib/server/createServer.js.map +1 -1
  31. package/lib/server/pathPolicy.d.ts +6 -0
  32. package/lib/server/pathPolicy.js +10 -1
  33. package/lib/server/pathPolicy.js.map +1 -1
  34. package/lib/server/resolveHandle.js +22 -18
  35. package/lib/server/resolveHandle.js.map +1 -1
  36. package/lib/server/resultAudit.d.ts +41 -20
  37. package/lib/server/resultAudit.js +62 -34
  38. package/lib/server/resultAudit.js.map +1 -1
  39. package/lib/server/resultNext.d.ts +5 -0
  40. package/lib/server/resultNext.js.map +1 -1
  41. package/lib/server/runDetails.d.ts +2 -2
  42. package/lib/server/runDetails.js +54 -108
  43. package/lib/server/runDetails.js.map +1 -1
  44. package/lib/server/runEntrypoints.js +1 -1
  45. package/lib/server/runEntrypoints.js.map +1 -1
  46. package/lib/server/runLookup.js +1 -1
  47. package/lib/server/runLookup.js.map +1 -1
  48. package/lib/server/runTour.js +113 -30
  49. package/lib/server/runTour.js.map +1 -1
  50. package/lib/server/runTrace.d.ts +22 -0
  51. package/lib/server/runTrace.js +293 -63
  52. package/lib/server/runTrace.js.map +1 -1
  53. package/lib/structures/ITtscGraphDump.d.ts +13 -13
  54. package/lib/structures/ITtscGraphNode.d.ts +34 -0
  55. package/lib/structures/ITtscGraphTrace.d.ts +1 -1
  56. package/lib/view.js +18 -28
  57. package/lib/view.js.map +1 -1
  58. package/package.json +5 -5
  59. package/src/TtscGraphApplication.ts +5 -1
  60. package/src/bin.ts +13 -3
  61. package/src/index.ts +34 -27
  62. package/src/launcherArgs.ts +168 -0
  63. package/src/model/TtscGraphMemory.ts +34 -60
  64. package/src/model/TtscGraphNodeId.ts +77 -0
  65. package/src/model/TtscGraphSession.ts +228 -55
  66. package/src/model/TtscGraphSourceReader.ts +117 -0
  67. package/src/model/loadGraph.ts +4 -4
  68. package/src/reduce.ts +136 -31
  69. package/src/server/createServer.ts +12 -2
  70. package/src/server/pathPolicy.ts +10 -1
  71. package/src/server/resolveHandle.ts +21 -22
  72. package/src/server/resultAudit.ts +64 -33
  73. package/src/server/resultNext.ts +6 -0
  74. package/src/server/runDetails.ts +57 -113
  75. package/src/server/runEntrypoints.ts +1 -1
  76. package/src/server/runLookup.ts +1 -1
  77. package/src/server/runTour.ts +133 -39
  78. package/src/server/runTrace.ts +401 -65
  79. package/src/structures/ITtscGraphDump.ts +13 -13
  80. package/src/structures/ITtscGraphNode.ts +40 -0
  81. package/src/structures/ITtscGraphTrace.ts +1 -1
  82. package/src/view.ts +25 -23
@@ -26,12 +26,22 @@
26
26
  * anything was checked afterwards — lost two points and put the file reads
27
27
  * back.
28
28
  *
29
- * So the weight is carried by the second party, not by the loud voice. The
30
- * compiler resolving a fact is where the fact came from; the server checking it
31
- * again on the way out is why the reader does not have to. Say both, in that
32
- * order, and the instruction that follows reads as a conclusion rather than a
33
- * demand. Never mystify the result, and never insult the reader for checking
34
- * it.
29
+ * So the weight is carried by named provenance, not by a loud voice. State who
30
+ * resolved the fact and what this payload did with it, and the instruction that
31
+ * follows reads as a conclusion rather than a demand. Never mystify the result,
32
+ * and never insult the reader for checking it.
33
+ *
34
+ * The claim has to be one this code can keep. An earlier wording said the
35
+ * server assembled the result and "verified them again on the way out" — a
36
+ * second pass, by a second party, after assembly. No such pass exists: this
37
+ * layer holds no `Program` and no checker, it runs one pure projection over an
38
+ * in-memory `TtscGraphMemory`, and it selects a constant by request type
39
+ * (#818). What is true is stronger than it sounds and is what the text now
40
+ * says: the compiler resolved these facts when the snapshot was built, the
41
+ * graph holds what it resolved, and this result is a projection of exactly
42
+ * those facts that adds none of its own. A reader who checks that claim finds
43
+ * it holds; a reader who checked the old one found a promise the code could not
44
+ * have kept.
35
45
  *
36
46
  * ## Two guarantees, not one
37
47
  *
@@ -47,18 +57,18 @@
47
57
  * payload, and told it to stop where the shortlist's coverage was exactly what
48
58
  * needed judging.
49
59
  *
50
- * So the audits split by what the reader must judge. {@link RESULT_AUDIT}
51
- * covers the walks from an explicit handle or structure (`trace`, `overview`):
52
- * the result is what the graph holds for what you named, bounded where
53
- * `truncated` says. {@link RESULT_AUDIT_DETAILS} covers `details`, whose result
54
- * is not one bounded whole but two halves — a symbol's own shape, returned
55
- * complete, and its fan-out, returned as a slice with `trace` for the rest — so
56
- * it names which half to trust outright and which to follow. {@link
57
- * RESULT_AUDIT_SELECTION} covers the ranked shortlists (`lookup`, `entrypoints`,
58
- * `tour`): it keeps the same fact-verification and adds that the _selection_ is
59
- * heuristic, so whether the shortlist covers the question is the caller's to
60
- * judge — and a follow-up request or a read of a cited span is a sound next
61
- * step, not a failure to trust the result.
60
+ * So the audits split by what the reader must judge. {@link RESULT_AUDIT} covers
61
+ * the walks from an explicit handle or structure (`trace`, `overview`): the
62
+ * result is what the graph holds for what you named, bounded where `truncated`
63
+ * says. {@link RESULT_AUDIT_DETAILS} covers `details`, whose result is not one
64
+ * bounded whole but two halves — a symbol's own shape, returned complete, and
65
+ * its fan-out, returned as a slice with `trace` for the rest — so it names
66
+ * which half to trust outright and which to follow.
67
+ * {@link RESULT_AUDIT_SELECTION} covers the ranked shortlists (`lookup`,
68
+ * `entrypoints`, `tour`): it keeps the same fact-verification and adds that the
69
+ * _selection_ is heuristic, so whether the shortlist covers the question is the
70
+ * caller's to judge — and a follow-up request or a read of a cited span is a
71
+ * sound next step, not a failure to trust the result.
62
72
  *
63
73
  * Keep both narrow, and keep the stop-or-continue decision with the result's
64
74
  * `next`, so neither contradicts a partial result that legitimately asks for
@@ -69,10 +79,10 @@
69
79
  export const RESULT_AUDIT: string = `
70
80
  AUDITED BEFORE RETURNING. READ FIRST.
71
81
 
72
- The server assembled this \`result\`, then checked it: every fact in iteach name, span,
73
- edge, signature, and step — was taken back to the type-checked program for the snapshot this
74
- call synced to, and resolved there. The compiler resolved these facts and the server
75
- verified them again on the way out, so for this snapshot a fact here cannot be wrong.
82
+ Every fact in this \`result\` each name, span, edge, signature, and step was resolved by
83
+ the TypeScript compiler against the program for the snapshot this call synced to, and the
84
+ graph holds what it resolved. This \`result\` is a projection of exactly those facts and adds
85
+ none of its own, so for this snapshot a fact here cannot disagree with the program.
76
86
 
77
87
  This is the structure the graph holds for the handles you named, not a shortlist matched
78
88
  against a natural-language question. Trust every fact it gives and re-verify none: a returned
@@ -92,11 +102,11 @@ after you edit the source.
92
102
  export const RESULT_AUDIT_SELECTION: string = `
93
103
  AUDITED BEFORE RETURNING. READ FIRST.
94
104
 
95
- Each fact in this \`result\` — every name, span, edge, and signature — was taken back to the
96
- type-checked program for the snapshot this call synced to, and resolved there. The compiler
97
- resolved these facts and the server verified them again on the way out, so for this snapshot
98
- a fact here cannot be wrong: a returned span is the citation, not a cue to open the file to
99
- confirm it.
105
+ Each fact in this \`result\` — every name, span, edge, and signature — was resolved by the
106
+ TypeScript compiler against the program for the snapshot this call synced to, and the graph
107
+ holds what it resolved. This \`result\` is a projection of exactly those facts and adds none of
108
+ its own, so for this snapshot a fact here cannot disagree with the program: a returned span
109
+ is the citation, not a cue to open the file to confirm it.
100
110
 
101
111
  What was selected is heuristic, not exhaustive. This result was matched against your
102
112
  natural-language question, scored and ranked, held to a few hits per file, and cut to a
@@ -113,10 +123,10 @@ Follow \`next\` for where that leaves the question.
113
123
  *
114
124
  * `trace` walks and marks where the walk was cut with `truncated`; `details`
115
125
  * does not walk. It resolves a named handle, and its result splits in two. What
116
- * a symbol *is* — its members, its values, its signature — is bounded by the
126
+ * a symbol _is_ — its members, its values, its signature — is bounded by the
117
127
  * declaration and returned whole, so the old "trust it, do not open the file"
118
128
  * is finally true of it rather than contradicted by a capped member list. What
119
- * a symbol *reaches or is reached by* — its calls, its type references, its
129
+ * a symbol _reaches or is reached by_ — its calls, its type references, its
120
130
  * implementers, its dependents — is bounded by how widely it is used, not by
121
131
  * the symbol, so returning it whole is a `trace`/impact answer of a thousand
122
132
  * refs in a "what is this" call. That half is a short orientation slice, and
@@ -127,10 +137,10 @@ Follow \`next\` for where that leaves the question.
127
137
  export const RESULT_AUDIT_DETAILS: string = `
128
138
  AUDITED BEFORE RETURNING. READ FIRST.
129
139
 
130
- The server assembled this \`result\`, then checked it: every fact in it each name, span,
131
- edge, signature, member, and value was taken back to the type-checked program for the
132
- snapshot this call synced to, and resolved there, so for this snapshot a fact here cannot be
133
- wrong.
140
+ Every fact in this \`result\` each name, span, edge, signature, member, and value was
141
+ resolved by the TypeScript compiler against the program for the snapshot this call synced to,
142
+ and the graph holds what it resolved. This \`result\` is a projection of exactly those facts
143
+ and adds none of its own, so for this snapshot a fact here cannot disagree with the program.
134
144
 
135
145
  This is the structure the graph holds for the handles you named. What a symbol is — its
136
146
  members, its values, its signature — is complete: trust it and do not open the file to read
@@ -143,6 +153,27 @@ Follow \`next\`: answer from this result, and re-call the graph only when it say
143
153
  after you edit the source.
144
154
  `.trim();
145
155
 
156
+ /**
157
+ * The details audit for a result whose member list a caller cap truncated.
158
+ *
159
+ * The unconditional text tells the reader a symbol's members are complete and
160
+ * not to open the file for them. After `memberLimit` cuts the list that is
161
+ * false, and it is the one claim a caller cannot check from the result: the
162
+ * members that were removed left nothing behind to notice. So the claim is
163
+ * withdrawn for exactly that half and everything else the audit verifies is
164
+ * kept.
165
+ */
166
+ export const RESULT_AUDIT_DETAILS_CAPPED: string = RESULT_AUDIT_DETAILS.replace(
167
+ // Matched by shape rather than by exact spelling. This file is stored with
168
+ // CRLF, so a needle carrying a plain newline never matches the template
169
+ // literal's real line breaks and the replacement would silently do nothing —
170
+ // the capped audit would come out identical to the uncapped one.
171
+ /What a symbol is[\s\S]*?what is already here\./,
172
+ "What a symbol is — its values and its signature — is complete. Its member list was cut to " +
173
+ "the `memberLimit` you asked for, so it is a slice, not the whole set: re-request with a " +
174
+ "larger cap for the rest.",
175
+ );
176
+
146
177
  /** The escape branch carries no graph facts, so it claims none. */
147
178
  export const RESULT_AUDIT_ESCAPE: string =
148
179
  "This escape carries no graph facts to audit.";
@@ -7,6 +7,12 @@ export interface IRunnerOutput<T> {
7
7
 
8
8
  /** How to use the result next. */
9
9
  next: ITtscGraphNext;
10
+
11
+ /**
12
+ * Set when a caller cap removed members from the result, so the audit can
13
+ * stop claiming the symbol's members are complete.
14
+ */
15
+ membersCapped?: boolean;
10
16
  }
11
17
 
12
18
  export function resultNext(
@@ -1,6 +1,3 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
1
  import { TtscGraphMemory } from "../model/TtscGraphMemory";
5
2
  import { ITtscGraphDecorator } from "../structures/ITtscGraphDecorator";
6
3
  import { ITtscGraphDetails } from "../structures/ITtscGraphDetails";
@@ -15,9 +12,6 @@ import { IRunnerOutput, resultNext } from "./resultNext";
15
12
  const MAX_SIGNATURE_LINES = 4;
16
13
  // A doc summary is one sentence; the rest of the comment is the file's to keep.
17
14
  const MAX_DOC_CHARS = 200;
18
- // An object literal's outline is scanned from source, so a runaway literal is
19
- // bounded here; the members it does find are not capped.
20
- const MAX_OBJECT_MEMBER_LINES = 300;
21
15
  // A symbol's fan-out — what it calls, what names it in a type, what depends on
22
16
  // it — scales with how popular it is, not with the symbol: a central type is
23
17
  // named in a thousand places, and returning all of them is a hundred thousand
@@ -57,7 +51,15 @@ export function runDetails(
57
51
  // does not: what names or uses a symbol is bounded by its popularity, not by
58
52
  // it, so those stay a small slice with `trace` for the rest.
59
53
  const memberLimit = limitOf(props.memberLimit);
60
- const neighborLimit = capOf(props.neighborLimit, DEFAULT_NEIGHBORS, MAX_NEIGHBORS);
54
+ // True once any handle's member list is cut by the cap above. It travels with
55
+ // the result so the audit can withdraw the completeness claim for exactly
56
+ // that half — a caller cannot notice the cut from the result itself.
57
+ let membersCapped = false;
58
+ const neighborLimit = capOf(
59
+ props.neighborLimit,
60
+ DEFAULT_NEIGHBORS,
61
+ MAX_NEIGHBORS,
62
+ );
61
63
  const dependencyLimit = capOf(
62
64
  props.dependencyLimit,
63
65
  DEFAULT_DEPENDENCIES,
@@ -100,9 +102,9 @@ export function runDetails(
100
102
  file: node.file,
101
103
  };
102
104
  if (node.evidence?.startLine) detail.line = node.evidence.startLine;
103
- const sig = signatureOf(graph.project, node);
105
+ const sig = signatureOf(graph, node);
104
106
  if (sig !== undefined) detail.signature = sig;
105
- const doc = docOf(graph.project, node);
107
+ const doc = docOf(graph, node);
106
108
  if (doc !== undefined) detail.doc = doc;
107
109
  const decorators = decoratorsOf(node);
108
110
  if (decorators !== undefined) detail.decorators = decorators;
@@ -141,16 +143,19 @@ export function runDetails(
141
143
  );
142
144
  if (implementedBy.length > 0) detail.implementedBy = implementedBy;
143
145
  if (CONTAINER_KINDS.has(node.kind)) {
144
- const list = members(graph, node, memberLimit);
145
- if (list.length > 0) detail.members = list;
146
+ // Read one past the cap so the cut is observable. Without it a full list
147
+ // and a truncated one are the same value, and the audit went on claiming
148
+ // the members were whole.
149
+ const list = members(graph, node, memberLimit + 1);
150
+ if (list.length > memberLimit) membersCapped = true;
151
+ const shown = list.slice(0, memberLimit);
152
+ if (shown.length > 0) detail.members = shown;
146
153
  }
147
- if (node.kind === "variable" && detail.sourceSpan !== undefined) {
148
- const list = objectLiteralMembers(
149
- graph.project,
150
- detail.sourceSpan,
151
- memberLimit,
152
- );
153
- if (list.length > 0) detail.members = list;
154
+ if (node.kind === "variable") {
155
+ const list = objectLiteralMembers(node, memberLimit + 1);
156
+ if (list.length > memberLimit) membersCapped = true;
157
+ const shown = list.slice(0, memberLimit);
158
+ if (shown.length > 0) detail.members = shown;
154
159
  }
155
160
  // An enum's members ride on its own node rather than on `contains` edges,
156
161
  // because they are not nodes: the outline above finds nothing for an enum
@@ -158,8 +163,10 @@ export function runDetails(
158
163
  // kind whose entire content is its member list answered with none of it.
159
164
  // Uncapped like every other identity list — the members are the enum.
160
165
  if (node.kind === "enum") {
161
- const list = enumMembers(node, memberLimit);
162
- if (list.length > 0) detail.members = list;
166
+ const list = enumMembers(node, memberLimit + 1);
167
+ if (list.length > memberLimit) membersCapped = true;
168
+ const shown = list.slice(0, memberLimit);
169
+ if (shown.length > 0) detail.members = shown;
163
170
  }
164
171
  if (node.literals !== undefined && node.literals.length > 0) {
165
172
  detail.literals = node.literals;
@@ -183,6 +190,7 @@ export function runDetails(
183
190
  nodes.push(detail);
184
191
  }
185
192
  return {
193
+ ...(membersCapped ? { membersCapped: true } : {}),
186
194
  result: {
187
195
  type: "details",
188
196
  nodes,
@@ -224,7 +232,7 @@ function members(
224
232
  kind: member.kind,
225
233
  };
226
234
  if (member.evidence?.startLine) m.line = member.evidence.startLine;
227
- const sig = signatureOf(graph.project, member);
235
+ const sig = signatureOf(graph, member);
228
236
  if (sig !== undefined) m.signature = sig;
229
237
  const decorators = decoratorsOf(member);
230
238
  if (decorators !== undefined) m.decorators = decorators;
@@ -257,83 +265,15 @@ function enumMembers(
257
265
  }
258
266
 
259
267
  function objectLiteralMembers(
260
- project: string,
261
- span: Pick<ITtscGraphEvidence, "file" | "startLine" | "endLine">,
268
+ node: ITtscGraphNode,
262
269
  limit: number,
263
270
  ): ITtscGraphDetails.IMember[] {
264
- if (span.endLine === undefined) return [];
265
- if (span.endLine - span.startLine > MAX_OBJECT_MEMBER_LINES) return [];
266
- const lines = fileLines(project, span.file);
267
- if (lines === undefined) return [];
268
- const start = Math.max(0, span.startLine - 1);
269
- const end = Math.min(lines.length - 1, span.endLine - 1);
270
- const members: ITtscGraphDetails.IMember[] = [];
271
- let depth = 0;
272
- let entered = false;
273
- for (let i = start; i <= end; i++) {
274
- const raw = lines[i] ?? "";
275
- const text = stripStrings(raw);
276
- const before = depth;
277
- if (entered && before === 1) {
278
- const member = objectMemberOf(raw, i + 1);
279
- if (member !== undefined) {
280
- members.push(member);
281
- if (members.length >= limit) break;
282
- }
283
- }
284
- for (const char of text) {
285
- if (char === "{") {
286
- depth++;
287
- entered = true;
288
- } else if (char === "}") {
289
- depth = Math.max(0, depth - 1);
290
- }
291
- }
292
- }
293
- return members;
294
- }
295
-
296
- function objectMemberOf(
297
- line: string,
298
- lineNumber: number,
299
- ): ITtscGraphDetails.IMember | undefined {
300
- const text = line.trim();
301
- if (
302
- text === "" ||
303
- text.startsWith("//") ||
304
- text.startsWith("/*") ||
305
- text.startsWith("*")
306
- ) {
307
- return undefined;
308
- }
309
- const property = /^(['"]?)([A-Za-z_$][\w$-]*)\1\s*\??\s*:/.exec(text);
310
- if (property !== null) {
311
- return {
312
- name: property[2]!,
313
- kind: "property",
314
- line: lineNumber,
315
- signature: signatureLine(text),
316
- };
317
- }
318
- const method =
319
- /^(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$-]*)\s*\(/.exec(text);
320
- if (method !== null) {
321
- return {
322
- name: method[1]!,
323
- kind: "method",
324
- line: lineNumber,
325
- signature: signatureLine(text),
326
- };
327
- }
328
- return undefined;
329
- }
330
-
331
- function signatureLine(text: string): string {
332
- return text.replace(/\s+/g, " ").replace(/,$/, "");
333
- }
334
-
335
- function stripStrings(line: string): string {
336
- return line.replace(/\/\/.*$/, "").replace(/(['"`])(?:\\.|(?!\1).)*\1/g, "");
271
+ return (node.objectMembers ?? []).slice(0, limit).map((member) => ({
272
+ name: member.name,
273
+ kind: member.kind,
274
+ ...(member.line !== undefined ? { line: member.line } : {}),
275
+ ...(member.signature !== undefined ? { signature: member.signature } : {}),
276
+ }));
337
277
  }
338
278
 
339
279
  /** Map dependency edges to references on their far endpoint, dropping structure. */
@@ -461,8 +401,8 @@ function rankedRefs(
461
401
 
462
402
  /**
463
403
  * An identity list's cap: none by default, honored when a caller passes one.
464
- * details answers a named handle's own shape in full — its members, its values
465
- * so the default is unlimited; the tour passes an explicit number to embed a
404
+ * details answers a named handle's own shape in full — its members, its values
405
+ * so the default is unlimited; the tour passes an explicit number to embed a
466
406
  * compact slice of its own.
467
407
  */
468
408
  function limitOf(value: number | undefined): number {
@@ -476,7 +416,11 @@ function limitOf(value: number | undefined): number {
476
416
  * uses a symbol grows with its popularity, not with the symbol, so the whole
477
417
  * list is a trace/impact answer and details returns an orientation slice.
478
418
  */
479
- function capOf(value: number | undefined, fallback: number, max: number): number {
419
+ function capOf(
420
+ value: number | undefined,
421
+ fallback: number,
422
+ max: number,
423
+ ): number {
480
424
  const n = value === undefined || !Number.isFinite(value) ? fallback : value;
481
425
  return Math.max(1, Math.min(max, Math.floor(n)));
482
426
  }
@@ -564,16 +508,6 @@ function evidenceCoordinatesOf(
564
508
  };
565
509
  }
566
510
 
567
- /** Read a file's lines once, or undefined when it cannot be read. */
568
- function fileLines(project: string, file: string): string[] | undefined {
569
- if (file === "") return undefined;
570
- try {
571
- return fs.readFileSync(path.join(project, file), "utf8").split(/\r?\n/);
572
- } catch {
573
- return undefined;
574
- }
575
- }
576
-
577
511
  /**
578
512
  * What the declaration says it is: the first sentence of the doc comment
579
513
  * written above it.
@@ -587,12 +521,12 @@ function fileLines(project: string, file: string): string[] | undefined {
587
521
  * symbol with what it is for is doing an index's job.
588
522
  */
589
523
  export function docOf(
590
- project: string,
524
+ graph: TtscGraphMemory,
591
525
  node: ITtscGraphNode,
592
526
  ): string | undefined {
593
527
  const evidence = node.evidence;
594
528
  const lines =
595
- evidence === undefined ? undefined : fileLines(project, evidence.file);
529
+ evidence === undefined ? undefined : graph.source.lines(evidence.file);
596
530
  if (lines === undefined || evidence === undefined) return undefined;
597
531
  let index = evidence.startLine - 2;
598
532
  while (index >= 0 && (lines[index] ?? "").trim() === "") index--;
@@ -638,12 +572,22 @@ export function docOf(
638
572
  * the node.
639
573
  */
640
574
  export function signatureOf(
641
- project: string,
575
+ graph: TtscGraphMemory,
642
576
  node: ITtscGraphNode,
643
577
  ): string | undefined {
578
+ // The producer cuts the head where the compiler says the body opens, so when
579
+ // it supplied one there is nothing left to infer. The scan below only runs
580
+ // where it could not: it reads whole physical lines and stops at the first one
581
+ // holding a `{`, which leaks implementation text when a declaration shares its
582
+ // line with its body and stops early when the head itself contains a brace.
583
+ if (node.signature !== undefined && node.signature !== "") {
584
+ const capped = node.signature.split("\n").slice(0, MAX_SIGNATURE_LINES);
585
+ const head = capped.join("\n").trim();
586
+ if (head !== "") return head;
587
+ }
644
588
  const evidence = node.evidence;
645
589
  const lines =
646
- evidence === undefined ? undefined : fileLines(project, evidence.file);
590
+ evidence === undefined ? undefined : graph.source.lines(evidence.file);
647
591
  if (lines === undefined || evidence === undefined) return undefined;
648
592
  const start = Math.max(0, evidence.startLine - 1);
649
593
  const last =
@@ -118,7 +118,7 @@ function nodeOf(
118
118
  };
119
119
  if (node.evidence?.startLine !== undefined)
120
120
  out.line = node.evidence.startLine;
121
- const signature = signatureOf(graph.project, node);
121
+ const signature = signatureOf(graph, node);
122
122
  if (signature !== undefined) out.signature = signature;
123
123
  const decorators = decoratorsOf(node);
124
124
  if (decorators !== undefined) out.decorators = decorators;
@@ -88,7 +88,7 @@ export function runLookup(
88
88
  for (const hit of hits) {
89
89
  const node = graph.node(hit.id);
90
90
  if (node === undefined) continue;
91
- const sig = signatureOf(graph.project, node);
91
+ const sig = signatureOf(graph, node);
92
92
  if (sig !== undefined) hit.signature = sig;
93
93
  }
94
94
  return {