@ttsc/graph 0.19.2 → 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 (92) hide show
  1. package/lib/TtscGraphApplication.js +5 -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 +25 -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 +423 -171
  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 +14 -0
  24. package/lib/model/loadGraph.js +256 -143
  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/resolveGraphBinary.d.ts +9 -3
  30. package/lib/resolveGraphBinary.js +9 -3
  31. package/lib/resolveGraphBinary.js.map +1 -1
  32. package/lib/server/createServer.js +22 -11
  33. package/lib/server/createServer.js.map +1 -1
  34. package/lib/server/pathPolicy.d.ts +6 -0
  35. package/lib/server/pathPolicy.js +10 -1
  36. package/lib/server/pathPolicy.js.map +1 -1
  37. package/lib/server/resolveHandle.js +22 -18
  38. package/lib/server/resolveHandle.js.map +1 -1
  39. package/lib/server/resultAudit.d.ts +56 -14
  40. package/lib/server/resultAudit.js +90 -24
  41. package/lib/server/resultAudit.js.map +1 -1
  42. package/lib/server/resultNext.d.ts +5 -0
  43. package/lib/server/resultNext.js.map +1 -1
  44. package/lib/server/runDetails.d.ts +9 -2
  45. package/lib/server/runDetails.js +132 -157
  46. package/lib/server/runDetails.js.map +1 -1
  47. package/lib/server/runEntrypoints.js +1 -1
  48. package/lib/server/runEntrypoints.js.map +1 -1
  49. package/lib/server/runLookup.js +1 -1
  50. package/lib/server/runLookup.js.map +1 -1
  51. package/lib/server/runTour.js +113 -30
  52. package/lib/server/runTour.js.map +1 -1
  53. package/lib/server/runTrace.d.ts +22 -0
  54. package/lib/server/runTrace.js +293 -63
  55. package/lib/server/runTrace.js.map +1 -1
  56. package/lib/structures/ITtscGraphApplication.d.ts +8 -7
  57. package/lib/structures/ITtscGraphDetails.d.ts +18 -8
  58. package/lib/structures/ITtscGraphDump.d.ts +13 -13
  59. package/lib/structures/ITtscGraphNode.d.ts +71 -0
  60. package/lib/structures/ITtscGraphTour.d.ts +1 -1
  61. package/lib/structures/ITtscGraphTrace.d.ts +1 -1
  62. package/lib/view.js +18 -28
  63. package/lib/view.js.map +1 -1
  64. package/package.json +7 -4
  65. package/src/TtscGraphApplication.ts +8 -1
  66. package/src/bin.ts +13 -3
  67. package/src/index.ts +35 -27
  68. package/src/launcherArgs.ts +168 -0
  69. package/src/model/TtscGraphMemory.ts +34 -60
  70. package/src/model/TtscGraphNodeId.ts +77 -0
  71. package/src/model/TtscGraphSession.ts +253 -53
  72. package/src/model/TtscGraphSourceReader.ts +117 -0
  73. package/src/model/loadGraph.ts +6 -1
  74. package/src/reduce.ts +136 -31
  75. package/src/resolveGraphBinary.ts +9 -3
  76. package/src/server/createServer.ts +12 -2
  77. package/src/server/pathPolicy.ts +10 -1
  78. package/src/server/resolveHandle.ts +21 -22
  79. package/src/server/resultAudit.ts +93 -23
  80. package/src/server/resultNext.ts +6 -0
  81. package/src/server/runDetails.ts +134 -165
  82. package/src/server/runEntrypoints.ts +1 -1
  83. package/src/server/runLookup.ts +1 -1
  84. package/src/server/runTour.ts +133 -39
  85. package/src/server/runTrace.ts +401 -65
  86. package/src/structures/ITtscGraphApplication.ts +8 -7
  87. package/src/structures/ITtscGraphDetails.ts +18 -8
  88. package/src/structures/ITtscGraphDump.ts +13 -13
  89. package/src/structures/ITtscGraphNode.ts +80 -0
  90. package/src/structures/ITtscGraphTour.ts +1 -1
  91. package/src/structures/ITtscGraphTrace.ts +1 -1
  92. 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,14 +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 there are two audits. {@link RESULT_AUDIT} covers the operations that walk
51
- * from an explicit handle or structure (`trace`, `details`, `overview`): the
52
- * result is what the graph holds for what you named, bounded only where
53
- * `truncated` says. {@link RESULT_AUDIT_SELECTION} covers the ranked shortlists
54
- * (`lookup`, `entrypoints`, `tour`): it keeps the same fact-verification and
55
- * adds that the _selection_ is heuristic, so whether the shortlist covers the
56
- * question is the caller's to judge and a follow-up request or a read of a
57
- * cited span is a sound next 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.
58
72
  *
59
73
  * Keep both narrow, and keep the stop-or-continue decision with the result's
60
74
  * `next`, so neither contradicts a partial result that legitimately asks for
@@ -65,10 +79,10 @@
65
79
  export const RESULT_AUDIT: string = `
66
80
  AUDITED BEFORE RETURNING. READ FIRST.
67
81
 
68
- The server assembled this \`result\`, then checked it: every fact in iteach name, span,
69
- edge, signature, and step — was taken back to the type-checked program for the snapshot this
70
- call synced to, and resolved there. The compiler resolved these facts and the server
71
- 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.
72
86
 
73
87
  This is the structure the graph holds for the handles you named, not a shortlist matched
74
88
  against a natural-language question. Trust every fact it gives and re-verify none: a returned
@@ -88,11 +102,11 @@ after you edit the source.
88
102
  export const RESULT_AUDIT_SELECTION: string = `
89
103
  AUDITED BEFORE RETURNING. READ FIRST.
90
104
 
91
- Each fact in this \`result\` — every name, span, edge, and signature — was taken back to the
92
- type-checked program for the snapshot this call synced to, and resolved there. The compiler
93
- resolved these facts and the server verified them again on the way out, so for this snapshot
94
- a fact here cannot be wrong: a returned span is the citation, not a cue to open the file to
95
- 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.
96
110
 
97
111
  What was selected is heuristic, not exhaustive. This result was matched against your
98
112
  natural-language question, scored and ranked, held to a few hits per file, and cut to a
@@ -104,6 +118,62 @@ span is a sound next step, not a failure to trust the result.
104
118
  Follow \`next\` for where that leaves the question.
105
119
  `.trim();
106
120
 
121
+ /**
122
+ * The audit for `details`, whose contract is not the other exact operations'.
123
+ *
124
+ * `trace` walks and marks where the walk was cut with `truncated`; `details`
125
+ * does not walk. It resolves a named handle, and its result splits in two. What
126
+ * a symbol _is_ — its members, its values, its signature — is bounded by the
127
+ * declaration and returned whole, so the old "trust it, do not open the file"
128
+ * is finally true of it rather than contradicted by a capped member list. What
129
+ * a symbol _reaches or is reached by_ — its calls, its type references, its
130
+ * implementers, its dependents — is bounded by how widely it is used, not by
131
+ * the symbol, so returning it whole is a `trace`/impact answer of a thousand
132
+ * refs in a "what is this" call. That half is a short orientation slice, and
133
+ * the audit has to say which half is which so the reader trusts the complete
134
+ * one and reaches for `trace` on the other, instead of reading the file for a
135
+ * member list that is already here.
136
+ */
137
+ export const RESULT_AUDIT_DETAILS: string = `
138
+ AUDITED BEFORE RETURNING. READ FIRST.
139
+
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.
144
+
145
+ This is the structure the graph holds for the handles you named. What a symbol is — its
146
+ members, its values, its signature — is complete: trust it and do not open the file to read
147
+ what is already here. What a symbol reaches or is reached by — its calls, its type
148
+ references, its implementers, and under \`neighbors\` its dependents — is a short orientation
149
+ slice, not the whole set, because that grows with how widely a symbol is used; \`trace\`
150
+ follows it in full.
151
+
152
+ Follow \`next\`: answer from this result, and re-call the graph only when it says inspect, or
153
+ after you edit the source.
154
+ `.trim();
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
+
107
177
  /** The escape branch carries no graph facts, so it claims none. */
108
178
  export const RESULT_AUDIT_ESCAPE: string =
109
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,17 +12,17 @@ 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
- // Neighbor lists are a map, not a dump; keep them scannable.
15
+ // A symbol's fan-out what it calls, what names it in a type, what depends on
16
+ // it — scales with how popular it is, not with the symbol: a central type is
17
+ // named in a thousand places, and returning all of them is a hundred thousand
18
+ // tokens of "who uses this", which is a trace/impact question, not "what is
19
+ // this". So fan-out is a small default slice; identity (members, literals) is
20
+ // not, because a class's members and a union's values are the symbol itself and
21
+ // are bounded by the declaration.
19
22
  const DEFAULT_NEIGHBORS = 2;
20
23
  const MAX_NEIGHBORS = 3;
21
- // A container outline can be long; default to a scannable first page.
22
- const DEFAULT_MEMBERS = 6;
23
- const MAX_MEMBERS = 8;
24
- // Direct dependency groups are orientation slices, not full fan-out dumps.
25
24
  const DEFAULT_DEPENDENCIES = 2;
26
25
  const MAX_DEPENDENCIES = 4;
27
- // Object literal outlines are navigation aids, not source excerpts.
28
- const MAX_OBJECT_MEMBER_LINES = 300;
29
26
  // Structural relationships are navigation, not the dependency picture details is for.
30
27
  const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
31
28
  // Kinds whose value is their member outline, not implementation text.
@@ -47,17 +44,25 @@ export function runDetails(
47
44
  graph: TtscGraphMemory,
48
45
  props: ITtscGraphDetails.IRequest,
49
46
  ): IRunnerOutput<ITtscGraphDetails> {
50
- const neighborLimit = bound(
47
+ // Identity is the whole answer. The caller named this handle to learn what it
48
+ // is, and a class's members or a union's values are the symbol itself — cut
49
+ // them and the model reads the file for the rest, the read this index exists
50
+ // to remove. So `memberLimit` and `literals` default to unlimited. Fan-out
51
+ // does not: what names or uses a symbol is bounded by its popularity, not by
52
+ // it, so those stay a small slice with `trace` for the rest.
53
+ const memberLimit = limitOf(props.memberLimit);
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(
51
59
  props.neighborLimit,
52
60
  DEFAULT_NEIGHBORS,
53
- 1,
54
61
  MAX_NEIGHBORS,
55
62
  );
56
- const memberLimit = bound(props.memberLimit, DEFAULT_MEMBERS, 1, MAX_MEMBERS);
57
- const dependencyLimit = bound(
63
+ const dependencyLimit = capOf(
58
64
  props.dependencyLimit,
59
65
  DEFAULT_DEPENDENCIES,
60
- 1,
61
66
  MAX_DEPENDENCIES,
62
67
  );
63
68
  const wantNeighbors = props.neighbors === true;
@@ -97,11 +102,10 @@ export function runDetails(
97
102
  file: node.file,
98
103
  };
99
104
  if (node.evidence?.startLine) detail.line = node.evidence.startLine;
100
- const sig = signatureOf(graph.project, node);
105
+ const sig = signatureOf(graph, node);
101
106
  if (sig !== undefined) detail.signature = sig;
102
- const doc = docOf(graph.project, node);
107
+ const doc = docOf(graph, node);
103
108
  if (doc !== undefined) detail.doc = doc;
104
- const signatureLiterals = literalSummaries(sig);
105
109
  const decorators = decoratorsOf(node);
106
110
  if (decorators !== undefined) detail.decorators = decorators;
107
111
  const implementation = evidenceCoordinatesOf(node.implementation);
@@ -139,19 +143,34 @@ export function runDetails(
139
143
  );
140
144
  if (implementedBy.length > 0) detail.implementedBy = implementedBy;
141
145
  if (CONTAINER_KINDS.has(node.kind)) {
142
- const list = members(graph, node, memberLimit);
143
- 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;
144
153
  }
145
- if (node.kind === "variable" && detail.sourceSpan !== undefined) {
146
- const list = objectLiteralMembers(
147
- graph.project,
148
- detail.sourceSpan,
149
- memberLimit,
150
- );
151
- 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;
159
+ }
160
+ // An enum's members ride on its own node rather than on `contains` edges,
161
+ // because they are not nodes: the outline above finds nothing for an enum
162
+ // and always did. Its signature stops at the `{`, so without this the one
163
+ // kind whose entire content is its member list answered with none of it.
164
+ // Uncapped like every other identity list — the members are the enum.
165
+ if (node.kind === "enum") {
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;
170
+ }
171
+ if (node.literals !== undefined && node.literals.length > 0) {
172
+ detail.literals = node.literals;
152
173
  }
153
- if (signatureLiterals.length > 0)
154
- detail.literals = signatureLiterals.slice(0, 6);
155
174
  if (wantNeighbors) {
156
175
  detail.dependsOn = refs(
157
176
  graph,
@@ -171,6 +190,7 @@ export function runDetails(
171
190
  nodes.push(detail);
172
191
  }
173
192
  return {
193
+ ...(membersCapped ? { membersCapped: true } : {}),
174
194
  result: {
175
195
  type: "details",
176
196
  nodes,
@@ -212,7 +232,7 @@ function members(
212
232
  kind: member.kind,
213
233
  };
214
234
  if (member.evidence?.startLine) m.line = member.evidence.startLine;
215
- const sig = signatureOf(graph.project, member);
235
+ const sig = signatureOf(graph, member);
216
236
  if (sig !== undefined) m.signature = sig;
217
237
  const decorators = decoratorsOf(member);
218
238
  if (decorators !== undefined) m.decorators = decorators;
@@ -222,84 +242,38 @@ function members(
222
242
  return out;
223
243
  }
224
244
 
225
- function objectLiteralMembers(
226
- project: string,
227
- span: Pick<ITtscGraphEvidence, "file" | "startLine" | "endLine">,
245
+ /**
246
+ * An enum's members, owner-qualified so the name reads the way the code writes
247
+ * it, with the value each carries as its signature.
248
+ *
249
+ * The name is why this exists. `literals` answers what values the enum admits,
250
+ * but a caller writes `Colors.Red` and never `"red"`, so an enum the graph
251
+ * already held sent a caller that had named it to the file for the one fact it
252
+ * came for (#738).
253
+ */
254
+ function enumMembers(
255
+ node: ITtscGraphNode,
228
256
  limit: number,
229
257
  ): ITtscGraphDetails.IMember[] {
230
- if (span.endLine === undefined) return [];
231
- if (span.endLine - span.startLine > MAX_OBJECT_MEMBER_LINES) return [];
232
- const lines = fileLines(project, span.file);
233
- if (lines === undefined) return [];
234
- const start = Math.max(0, span.startLine - 1);
235
- const end = Math.min(lines.length - 1, span.endLine - 1);
236
- const members: ITtscGraphDetails.IMember[] = [];
237
- let depth = 0;
238
- let entered = false;
239
- for (let i = start; i <= end; i++) {
240
- const raw = lines[i] ?? "";
241
- const text = stripStrings(raw);
242
- const before = depth;
243
- if (entered && before === 1) {
244
- const member = objectMemberOf(raw, i + 1);
245
- if (member !== undefined) {
246
- members.push(member);
247
- if (members.length >= limit) break;
248
- }
249
- }
250
- for (const char of text) {
251
- if (char === "{") {
252
- depth++;
253
- entered = true;
254
- } else if (char === "}") {
255
- depth = Math.max(0, depth - 1);
256
- }
257
- }
258
- }
259
- return members;
260
- }
261
-
262
- function objectMemberOf(
263
- line: string,
264
- lineNumber: number,
265
- ): ITtscGraphDetails.IMember | undefined {
266
- const text = line.trim();
267
- if (
268
- text === "" ||
269
- text.startsWith("//") ||
270
- text.startsWith("/*") ||
271
- text.startsWith("*")
272
- ) {
273
- return undefined;
274
- }
275
- const property = /^(['"]?)([A-Za-z_$][\w$-]*)\1\s*\??\s*:/.exec(text);
276
- if (property !== null) {
277
- return {
278
- name: property[2]!,
279
- kind: "property",
280
- line: lineNumber,
281
- signature: signatureLine(text),
282
- };
283
- }
284
- const method =
285
- /^(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$-]*)\s*\(/.exec(text);
286
- if (method !== null) {
287
- return {
288
- name: method[1]!,
289
- kind: "method",
290
- line: lineNumber,
291
- signature: signatureLine(text),
292
- };
293
- }
294
- return undefined;
258
+ return (node.enumMembers ?? []).slice(0, limit).map((member) => ({
259
+ name: `${node.qualifiedName ?? node.name}.${member.name}`,
260
+ kind: "property",
261
+ ...(member.value !== undefined
262
+ ? { signature: `${member.name} = ${member.value}` }
263
+ : {}),
264
+ }));
295
265
  }
296
266
 
297
- function signatureLine(text: string): string {
298
- return text.replace(/\s+/g, " ").replace(/,$/, "");
299
- }
300
-
301
- function stripStrings(line: string): string {
302
- return line.replace(/\/\/.*$/, "").replace(/(['"`])(?:\\.|(?!\1).)*\1/g, "");
267
+ function objectLiteralMembers(
268
+ node: ITtscGraphNode,
269
+ limit: number,
270
+ ): ITtscGraphDetails.IMember[] {
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
+ }));
303
277
  }
304
278
 
305
279
  /** Map dependency edges to references on their far endpoint, dropping structure. */
@@ -328,13 +302,11 @@ function refs(
328
302
  if (evidence !== undefined) ref.evidence = evidence;
329
303
  ranked.push({ ref, rank: refRank(ref, edge) });
330
304
  }
305
+ // Ranked so a caller that does throttle (the tour) keeps the strongest refs,
306
+ // not the ones nearest the top of a file. Uncapped, `limit` is Infinity and
307
+ // the sort is just a stable order.
331
308
  ranked.sort((a, b) => a.rank - b.rank);
332
- const out: ITtscGraphDetails.IReference[] = [];
333
- for (const item of ranked) {
334
- out.push(item.ref);
335
- if (out.length >= limit) break;
336
- }
337
- return out;
309
+ return ranked.map((item) => item.ref).slice(0, limit);
338
310
  }
339
311
 
340
312
  const executionKinds = new Set([
@@ -375,17 +347,7 @@ function dependencyRefs(
375
347
  rank: refRank(ref, edge),
376
348
  });
377
349
  }
378
- ranked.sort((a, b) => a.rank - b.rank);
379
- const out: ITtscGraphDetails.IReference[] = [];
380
- const seen = new Set<string>();
381
- for (const item of ranked) {
382
- const key = `${item.ref.relation}:${item.ref.id}`;
383
- if (seen.has(key)) continue;
384
- seen.add(key);
385
- out.push(item.ref);
386
- if (out.length >= limit) break;
387
- }
388
- return out;
350
+ return rankedRefs(ranked, limit);
389
351
  }
390
352
 
391
353
  function incomingDependencyRefs(
@@ -416,6 +378,14 @@ function incomingDependencyRefs(
416
378
  rank: refRank(ref, edge),
417
379
  });
418
380
  }
381
+ return rankedRefs(ranked, limit);
382
+ }
383
+
384
+ /** Sort by rank, drop duplicate (relation, id) pairs, and cut to `limit`. */
385
+ function rankedRefs(
386
+ ranked: Array<{ ref: ITtscGraphDetails.IReference; rank: number }>,
387
+ limit: number,
388
+ ): ITtscGraphDetails.IReference[] {
419
389
  ranked.sort((a, b) => a.rank - b.rank);
420
390
  const out: ITtscGraphDetails.IReference[] = [];
421
391
  const seen = new Set<string>();
@@ -429,38 +399,30 @@ function incomingDependencyRefs(
429
399
  return out;
430
400
  }
431
401
 
432
- function literalSummaries(text: string | undefined): string[] {
433
- if (text === undefined) return [];
434
- const out: string[] = [];
435
- for (const match of text.matchAll(/(["'`])((?:\\.|(?!\1).){1,80})\1/g)) {
436
- const value = cleanLiteral(match[2]);
437
- if (value !== undefined && !out.includes(value)) out.push(value);
438
- if (out.length >= 20) break;
439
- }
440
- return out;
441
- }
442
-
443
- function cleanLiteral(value: string | undefined): string | undefined {
444
- const text = value?.replace(/\s+/g, " ").trim();
445
- if (
446
- text === undefined ||
447
- text === "" ||
448
- text.length > 40 ||
449
- /^[{}()[\],.:;]+$/.test(text)
450
- ) {
451
- return undefined;
452
- }
453
- return text;
402
+ /**
403
+ * An identity list's cap: none by default, honored when a caller passes one.
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
406
+ * compact slice of its own.
407
+ */
408
+ function limitOf(value: number | undefined): number {
409
+ return value === undefined || !Number.isFinite(value)
410
+ ? Infinity
411
+ : Math.max(1, Math.floor(value));
454
412
  }
455
413
 
456
- function bound(
414
+ /**
415
+ * A fan-out list's cap: a small default, clamped to a ceiling. What names or
416
+ * uses a symbol grows with its popularity, not with the symbol, so the whole
417
+ * list is a trace/impact answer and details returns an orientation slice.
418
+ */
419
+ function capOf(
457
420
  value: number | undefined,
458
421
  fallback: number,
459
- min: number,
460
422
  max: number,
461
423
  ): number {
462
424
  const n = value === undefined || !Number.isFinite(value) ? fallback : value;
463
- return Math.max(min, Math.min(max, Math.floor(n)));
425
+ return Math.max(1, Math.min(max, Math.floor(n)));
464
426
  }
465
427
 
466
428
  /**
@@ -546,16 +508,6 @@ function evidenceCoordinatesOf(
546
508
  };
547
509
  }
548
510
 
549
- /** Read a file's lines once, or undefined when it cannot be read. */
550
- function fileLines(project: string, file: string): string[] | undefined {
551
- if (file === "") return undefined;
552
- try {
553
- return fs.readFileSync(path.join(project, file), "utf8").split(/\r?\n/);
554
- } catch {
555
- return undefined;
556
- }
557
- }
558
-
559
511
  /**
560
512
  * What the declaration says it is: the first sentence of the doc comment
561
513
  * written above it.
@@ -569,12 +521,12 @@ function fileLines(project: string, file: string): string[] | undefined {
569
521
  * symbol with what it is for is doing an index's job.
570
522
  */
571
523
  export function docOf(
572
- project: string,
524
+ graph: TtscGraphMemory,
573
525
  node: ITtscGraphNode,
574
526
  ): string | undefined {
575
527
  const evidence = node.evidence;
576
528
  const lines =
577
- evidence === undefined ? undefined : fileLines(project, evidence.file);
529
+ evidence === undefined ? undefined : graph.source.lines(evidence.file);
578
530
  if (lines === undefined || evidence === undefined) return undefined;
579
531
  let index = evidence.startLine - 2;
580
532
  while (index >= 0 && (lines[index] ?? "").trim() === "") index--;
@@ -611,22 +563,39 @@ export function docOf(
611
563
  * The declaration signature: the head of the declaration up to and including
612
564
  * the line that opens its body (`{`), or the single declaration line when there
613
565
  * is no brace, capped so a wrapped signature cannot run away.
566
+ *
567
+ * It never runs past the declaration's own span. The stop used to be the brace,
568
+ * the trailing semicolon, or the line cap, and a declaration ending in none of
569
+ * them read its neighbors instead: an enum member ends in a comma, so `VIEW`
570
+ * came back as itself plus the two members after it and the closing brace. The
571
+ * span is the fact that says where the declaration ends, and it was already on
572
+ * the node.
614
573
  */
615
574
  export function signatureOf(
616
- project: string,
575
+ graph: TtscGraphMemory,
617
576
  node: ITtscGraphNode,
618
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
+ }
619
588
  const evidence = node.evidence;
620
589
  const lines =
621
- evidence === undefined ? undefined : fileLines(project, evidence.file);
590
+ evidence === undefined ? undefined : graph.source.lines(evidence.file);
622
591
  if (lines === undefined || evidence === undefined) return undefined;
623
592
  const start = Math.max(0, evidence.startLine - 1);
593
+ const last =
594
+ evidence.endLine === undefined
595
+ ? lines.length - 1
596
+ : Math.min(lines.length - 1, evidence.endLine - 1);
624
597
  const out: string[] = [];
625
- for (
626
- let i = start;
627
- i < lines.length && out.length < MAX_SIGNATURE_LINES;
628
- i++
629
- ) {
598
+ for (let i = start; i <= last && out.length < MAX_SIGNATURE_LINES; i++) {
630
599
  const line = lines[i];
631
600
  if (line === undefined) break;
632
601
  out.push(line);
@@ -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 {