@ttsc/graph 0.19.2 → 0.19.3

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 (36) hide show
  1. package/lib/TtscGraphApplication.js +3 -1
  2. package/lib/TtscGraphApplication.js.map +1 -1
  3. package/lib/index.d.ts +1 -1
  4. package/lib/index.js +2 -1
  5. package/lib/index.js.map +1 -1
  6. package/lib/model/TtscGraphSession.js +109 -49
  7. package/lib/model/TtscGraphSession.js.map +1 -1
  8. package/lib/model/loadGraph.d.ts +14 -0
  9. package/lib/model/loadGraph.js +114 -66
  10. package/lib/model/loadGraph.js.map +1 -1
  11. package/lib/resolveGraphBinary.d.ts +9 -3
  12. package/lib/resolveGraphBinary.js +9 -3
  13. package/lib/resolveGraphBinary.js.map +1 -1
  14. package/lib/server/createServer.js +5 -5
  15. package/lib/server/resultAudit.d.ts +29 -8
  16. package/lib/server/resultAudit.js +47 -9
  17. package/lib/server/resultAudit.js.map +1 -1
  18. package/lib/server/runDetails.d.ts +7 -0
  19. package/lib/server/runDetails.js +87 -58
  20. package/lib/server/runDetails.js.map +1 -1
  21. package/lib/structures/ITtscGraphApplication.d.ts +8 -7
  22. package/lib/structures/ITtscGraphDetails.d.ts +18 -8
  23. package/lib/structures/ITtscGraphNode.d.ts +37 -0
  24. package/lib/structures/ITtscGraphTour.d.ts +1 -1
  25. package/package.json +5 -2
  26. package/src/TtscGraphApplication.ts +4 -1
  27. package/src/index.ts +1 -0
  28. package/src/model/TtscGraphSession.ts +27 -0
  29. package/src/model/loadGraph.ts +6 -1
  30. package/src/resolveGraphBinary.ts +9 -3
  31. package/src/server/resultAudit.ts +47 -8
  32. package/src/server/runDetails.ts +95 -70
  33. package/src/structures/ITtscGraphApplication.ts +8 -7
  34. package/src/structures/ITtscGraphDetails.ts +18 -8
  35. package/src/structures/ITtscGraphNode.ts +40 -0
  36. package/src/structures/ITtscGraphTour.ts +1 -1
@@ -47,14 +47,18 @@
47
47
  * payload, and told it to stop where the shortlist's coverage was exactly what
48
48
  * needed judging.
49
49
  *
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.
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.
58
62
  *
59
63
  * Keep both narrow, and keep the stop-or-continue decision with the result's
60
64
  * `next`, so neither contradicts a partial result that legitimately asks for
@@ -104,6 +108,41 @@ span is a sound next step, not a failure to trust the result.
104
108
  Follow \`next\` for where that leaves the question.
105
109
  `.trim();
106
110
 
111
+ /**
112
+ * The audit for `details`, whose contract is not the other exact operations'.
113
+ *
114
+ * `trace` walks and marks where the walk was cut with `truncated`; `details`
115
+ * 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
117
+ * declaration and returned whole, so the old "trust it, do not open the file"
118
+ * 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
120
+ * implementers, its dependents — is bounded by how widely it is used, not by
121
+ * the symbol, so returning it whole is a `trace`/impact answer of a thousand
122
+ * refs in a "what is this" call. That half is a short orientation slice, and
123
+ * the audit has to say which half is which so the reader trusts the complete
124
+ * one and reaches for `trace` on the other, instead of reading the file for a
125
+ * member list that is already here.
126
+ */
127
+ export const RESULT_AUDIT_DETAILS: string = `
128
+ AUDITED BEFORE RETURNING. READ FIRST.
129
+
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.
134
+
135
+ This is the structure the graph holds for the handles you named. What a symbol is — its
136
+ members, its values, its signature — is complete: trust it and do not open the file to read
137
+ what is already here. What a symbol reaches or is reached by — its calls, its type
138
+ references, its implementers, and under \`neighbors\` its dependents — is a short orientation
139
+ slice, not the whole set, because that grows with how widely a symbol is used; \`trace\`
140
+ follows it in full.
141
+
142
+ Follow \`next\`: answer from this result, and re-call the graph only when it says inspect, or
143
+ after you edit the source.
144
+ `.trim();
145
+
107
146
  /** The escape branch carries no graph facts, so it claims none. */
108
147
  export const RESULT_AUDIT_ESCAPE: string =
109
148
  "This escape carries no graph facts to audit.";
@@ -15,17 +15,20 @@ import { IRunnerOutput, resultNext } from "./resultNext";
15
15
  const MAX_SIGNATURE_LINES = 4;
16
16
  // A doc summary is one sentence; the rest of the comment is the file's to keep.
17
17
  const MAX_DOC_CHARS = 200;
18
- // Neighbor lists are a map, not a dump; keep them scannable.
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
+ // A symbol's fan-out — what it calls, what names it in a type, what depends on
22
+ // it — scales with how popular it is, not with the symbol: a central type is
23
+ // named in a thousand places, and returning all of them is a hundred thousand
24
+ // tokens of "who uses this", which is a trace/impact question, not "what is
25
+ // this". So fan-out is a small default slice; identity (members, literals) is
26
+ // not, because a class's members and a union's values are the symbol itself and
27
+ // are bounded by the declaration.
19
28
  const DEFAULT_NEIGHBORS = 2;
20
29
  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
30
  const DEFAULT_DEPENDENCIES = 2;
26
31
  const MAX_DEPENDENCIES = 4;
27
- // Object literal outlines are navigation aids, not source excerpts.
28
- const MAX_OBJECT_MEMBER_LINES = 300;
29
32
  // Structural relationships are navigation, not the dependency picture details is for.
30
33
  const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
31
34
  // Kinds whose value is their member outline, not implementation text.
@@ -47,17 +50,17 @@ export function runDetails(
47
50
  graph: TtscGraphMemory,
48
51
  props: ITtscGraphDetails.IRequest,
49
52
  ): IRunnerOutput<ITtscGraphDetails> {
50
- const neighborLimit = bound(
51
- props.neighborLimit,
52
- DEFAULT_NEIGHBORS,
53
- 1,
54
- MAX_NEIGHBORS,
55
- );
56
- const memberLimit = bound(props.memberLimit, DEFAULT_MEMBERS, 1, MAX_MEMBERS);
57
- const dependencyLimit = bound(
53
+ // Identity is the whole answer. The caller named this handle to learn what it
54
+ // is, and a class's members or a union's values are the symbol itself — cut
55
+ // them and the model reads the file for the rest, the read this index exists
56
+ // to remove. So `memberLimit` and `literals` default to unlimited. Fan-out
57
+ // does not: what names or uses a symbol is bounded by its popularity, not by
58
+ // it, so those stay a small slice with `trace` for the rest.
59
+ const memberLimit = limitOf(props.memberLimit);
60
+ const neighborLimit = capOf(props.neighborLimit, DEFAULT_NEIGHBORS, MAX_NEIGHBORS);
61
+ const dependencyLimit = capOf(
58
62
  props.dependencyLimit,
59
63
  DEFAULT_DEPENDENCIES,
60
- 1,
61
64
  MAX_DEPENDENCIES,
62
65
  );
63
66
  const wantNeighbors = props.neighbors === true;
@@ -101,7 +104,6 @@ export function runDetails(
101
104
  if (sig !== undefined) detail.signature = sig;
102
105
  const doc = docOf(graph.project, node);
103
106
  if (doc !== undefined) detail.doc = doc;
104
- const signatureLiterals = literalSummaries(sig);
105
107
  const decorators = decoratorsOf(node);
106
108
  if (decorators !== undefined) detail.decorators = decorators;
107
109
  const implementation = evidenceCoordinatesOf(node.implementation);
@@ -150,8 +152,18 @@ export function runDetails(
150
152
  );
151
153
  if (list.length > 0) detail.members = list;
152
154
  }
153
- if (signatureLiterals.length > 0)
154
- detail.literals = signatureLiterals.slice(0, 6);
155
+ // An enum's members ride on its own node rather than on `contains` edges,
156
+ // because they are not nodes: the outline above finds nothing for an enum
157
+ // and always did. Its signature stops at the `{`, so without this the one
158
+ // kind whose entire content is its member list answered with none of it.
159
+ // Uncapped like every other identity list — the members are the enum.
160
+ if (node.kind === "enum") {
161
+ const list = enumMembers(node, memberLimit);
162
+ if (list.length > 0) detail.members = list;
163
+ }
164
+ if (node.literals !== undefined && node.literals.length > 0) {
165
+ detail.literals = node.literals;
166
+ }
155
167
  if (wantNeighbors) {
156
168
  detail.dependsOn = refs(
157
169
  graph,
@@ -222,6 +234,28 @@ function members(
222
234
  return out;
223
235
  }
224
236
 
237
+ /**
238
+ * An enum's members, owner-qualified so the name reads the way the code writes
239
+ * it, with the value each carries as its signature.
240
+ *
241
+ * The name is why this exists. `literals` answers what values the enum admits,
242
+ * but a caller writes `Colors.Red` and never `"red"`, so an enum the graph
243
+ * already held sent a caller that had named it to the file for the one fact it
244
+ * came for (#738).
245
+ */
246
+ function enumMembers(
247
+ node: ITtscGraphNode,
248
+ limit: number,
249
+ ): ITtscGraphDetails.IMember[] {
250
+ return (node.enumMembers ?? []).slice(0, limit).map((member) => ({
251
+ name: `${node.qualifiedName ?? node.name}.${member.name}`,
252
+ kind: "property",
253
+ ...(member.value !== undefined
254
+ ? { signature: `${member.name} = ${member.value}` }
255
+ : {}),
256
+ }));
257
+ }
258
+
225
259
  function objectLiteralMembers(
226
260
  project: string,
227
261
  span: Pick<ITtscGraphEvidence, "file" | "startLine" | "endLine">,
@@ -328,13 +362,11 @@ function refs(
328
362
  if (evidence !== undefined) ref.evidence = evidence;
329
363
  ranked.push({ ref, rank: refRank(ref, edge) });
330
364
  }
365
+ // Ranked so a caller that does throttle (the tour) keeps the strongest refs,
366
+ // not the ones nearest the top of a file. Uncapped, `limit` is Infinity and
367
+ // the sort is just a stable order.
331
368
  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;
369
+ return ranked.map((item) => item.ref).slice(0, limit);
338
370
  }
339
371
 
340
372
  const executionKinds = new Set([
@@ -375,17 +407,7 @@ function dependencyRefs(
375
407
  rank: refRank(ref, edge),
376
408
  });
377
409
  }
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;
410
+ return rankedRefs(ranked, limit);
389
411
  }
390
412
 
391
413
  function incomingDependencyRefs(
@@ -416,6 +438,14 @@ function incomingDependencyRefs(
416
438
  rank: refRank(ref, edge),
417
439
  });
418
440
  }
441
+ return rankedRefs(ranked, limit);
442
+ }
443
+
444
+ /** Sort by rank, drop duplicate (relation, id) pairs, and cut to `limit`. */
445
+ function rankedRefs(
446
+ ranked: Array<{ ref: ITtscGraphDetails.IReference; rank: number }>,
447
+ limit: number,
448
+ ): ITtscGraphDetails.IReference[] {
419
449
  ranked.sort((a, b) => a.rank - b.rank);
420
450
  const out: ITtscGraphDetails.IReference[] = [];
421
451
  const seen = new Set<string>();
@@ -429,38 +459,26 @@ function incomingDependencyRefs(
429
459
  return out;
430
460
  }
431
461
 
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;
462
+ /**
463
+ * 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
466
+ * compact slice of its own.
467
+ */
468
+ function limitOf(value: number | undefined): number {
469
+ return value === undefined || !Number.isFinite(value)
470
+ ? Infinity
471
+ : Math.max(1, Math.floor(value));
454
472
  }
455
473
 
456
- function bound(
457
- value: number | undefined,
458
- fallback: number,
459
- min: number,
460
- max: number,
461
- ): number {
474
+ /**
475
+ * A fan-out list's cap: a small default, clamped to a ceiling. What names or
476
+ * uses a symbol grows with its popularity, not with the symbol, so the whole
477
+ * list is a trace/impact answer and details returns an orientation slice.
478
+ */
479
+ function capOf(value: number | undefined, fallback: number, max: number): number {
462
480
  const n = value === undefined || !Number.isFinite(value) ? fallback : value;
463
- return Math.max(min, Math.min(max, Math.floor(n)));
481
+ return Math.max(1, Math.min(max, Math.floor(n)));
464
482
  }
465
483
 
466
484
  /**
@@ -611,6 +629,13 @@ export function docOf(
611
629
  * The declaration signature: the head of the declaration up to and including
612
630
  * the line that opens its body (`{`), or the single declaration line when there
613
631
  * is no brace, capped so a wrapped signature cannot run away.
632
+ *
633
+ * It never runs past the declaration's own span. The stop used to be the brace,
634
+ * the trailing semicolon, or the line cap, and a declaration ending in none of
635
+ * them read its neighbors instead: an enum member ends in a comma, so `VIEW`
636
+ * came back as itself plus the two members after it and the closing brace. The
637
+ * span is the fact that says where the declaration ends, and it was already on
638
+ * the node.
614
639
  */
615
640
  export function signatureOf(
616
641
  project: string,
@@ -621,12 +646,12 @@ export function signatureOf(
621
646
  evidence === undefined ? undefined : fileLines(project, evidence.file);
622
647
  if (lines === undefined || evidence === undefined) return undefined;
623
648
  const start = Math.max(0, evidence.startLine - 1);
649
+ const last =
650
+ evidence.endLine === undefined
651
+ ? lines.length - 1
652
+ : Math.min(lines.length - 1, evidence.endLine - 1);
624
653
  const out: string[] = [];
625
- for (
626
- let i = start;
627
- i < lines.length && out.length < MAX_SIGNATURE_LINES;
628
- i++
629
- ) {
654
+ for (let i = start; i <= last && out.length < MAX_SIGNATURE_LINES; i++) {
630
655
  const line = lines[i];
631
656
  if (line === undefined) break;
632
657
  out.push(line);
@@ -169,13 +169,14 @@ export namespace ITtscGraphApplication {
169
169
  * resolves to the type-checked program for the snapshot the call synced to,
170
170
  * so opening a file it cites only returns a fact already in it.
171
171
  *
172
- * The audit is operation-aware. For the exact operations (`trace`,
173
- * `details`, `overview`) it reports the result as the structure the graph
174
- * holds for the handles named, bounded only where `truncated` says. For the
175
- * ranked operations (`lookup`, `entrypoints`, `tour`) it adds that the
176
- * selection is heuristic matched, scored, ranked, and limited against the
177
- * question so the facts are verified but the shortlist's coverage is the
178
- * caller's to judge.
172
+ * The audit is operation-aware. For the walks from a named handle (`trace`,
173
+ * `overview`) it reports the result as the structure the graph holds,
174
+ * bounded where `truncated` says. For `details` it reports the two halves of
175
+ * a resolved symbol: its own shape returned whole, its fan-out returned as a
176
+ * slice with `trace` for the rest. For the ranked operations (`lookup`,
177
+ * `entrypoints`, `tour`) it adds that the selection is heuristic matched,
178
+ * scored, ranked, and limited against the question — so the facts are
179
+ * verified but the shortlist's coverage is the caller's to judge.
179
180
  */
180
181
  audit: string;
181
182
 
@@ -49,24 +49,26 @@ export namespace ITtscGraphDetails {
49
49
  neighbors?: boolean;
50
50
 
51
51
  /**
52
- * Maximum dependencies and dependents per side when `neighbors:true`. Above
53
- * a few is usually overfetch; call `trace` for flow instead.
52
+ * Dependencies and dependents per side when `neighbors:true`. A small
53
+ * orientation slice by default; what uses a symbol grows with its
54
+ * popularity, so `trace` answers the whole "who uses this".
54
55
  *
55
56
  * @default 2
56
57
  */
57
58
  neighborLimit?: number;
58
59
 
59
60
  /**
60
- * Maximum owned members for a container or object literal.
61
- *
62
- * @default 6
61
+ * Owned members for a container or object literal. The complete outline by
62
+ * default — a class's members and an enum's are the symbol itself, so they
63
+ * are not sampled. Pass a number to cap.
63
64
  */
64
65
  memberLimit?: number;
65
66
 
66
67
  /**
67
- * Maximum direct execution and type references per group.
68
+ * Direct execution and type references per group. A small orientation slice
69
+ * by default; `trace` follows the whole fan-out.
68
70
  *
69
- * @default 1
71
+ * @default 2
70
72
  */
71
73
  dependencyLimit?: number;
72
74
 
@@ -145,7 +147,15 @@ export namespace ITtscGraphDetails {
145
147
  /** Concrete nodes that implement or override this interface/base member. */
146
148
  implementedBy?: IReference[];
147
149
 
148
- /** String literal values from the signature. */
150
+ /**
151
+ * The complete value set a type alias or enum admits, in TypeScript source
152
+ * form (`"a"`, `1`, `true`, `null`) — the checker's resolved union members,
153
+ * not the quoted tokens that happened to fit in `signature`.
154
+ *
155
+ * Absent when the type has no enumerable value set. A `signature` is capped
156
+ * at the declaration head, so for a union or enum written across several
157
+ * lines this is the field that carries the members.
158
+ */
149
159
  literals?: string[];
150
160
 
151
161
  /**
@@ -63,6 +63,32 @@ export interface ITtscGraphNode {
63
63
  /** Declaration modifiers, when the declaration pass recorded any. */
64
64
  modifiers?: TtscGraphNodeModifier[];
65
65
 
66
+ /**
67
+ * The complete value set of a type alias or enum whose declared type the
68
+ * checker resolved to literals, each in TypeScript source form (`"a"`, `1`,
69
+ * `true`, `null`).
70
+ *
71
+ * Present only when every constituent is enumerable, so the list is the whole
72
+ * type and never a sample of it: `type T = Kind | string` admits values no
73
+ * list can name and carries none. It is resolved from the type, not read off
74
+ * the declaration, so indirection (`type I = Kind | 'f'`) is followed and the
75
+ * answer does not depend on how the declaration is wrapped.
76
+ */
77
+ literals?: string[];
78
+
79
+ /**
80
+ * What an enum declares, in checker order: the name a caller writes and the
81
+ * value it carries. Absent on every other kind.
82
+ *
83
+ * `literals` says which values the enum admits, which is what a serializer
84
+ * asks. The code says `Colors.Red`, so the names are the other half, and
85
+ * without them a caller that had already named the enum still had to open the
86
+ * file to learn what to type. The members are not nodes — `Colors.Red` is a
87
+ * string a grep finds exactly — so this fills in the node the graph already
88
+ * holds instead of minting one per member.
89
+ */
90
+ enumMembers?: ITtscGraphNode.IEnumMember[];
91
+
66
92
  /**
67
93
  * Decorators written on this declaration, in source order: raw facts
68
94
  * (`@Controller`, `@Get`) a consumer interprets without re-parsing source.
@@ -78,3 +104,17 @@ export interface ITtscGraphNode {
78
104
  */
79
105
  implementation?: ITtscGraphEvidence;
80
106
  }
107
+ export namespace ITtscGraphNode {
108
+ /** One member of an enum: the name a caller writes and the value it carries. */
109
+ export interface IEnumMember {
110
+ /** The member's own name, unqualified (`Red` on `Colors.Red`). */
111
+ name: string;
112
+
113
+ /**
114
+ * The value it carries, in TypeScript source form (`"red"`, `1`). Absent
115
+ * for a computed member the checker could not fold to a constant; the name
116
+ * still stands.
117
+ */
118
+ value?: string;
119
+ }
120
+ }
@@ -66,7 +66,7 @@ export namespace ITtscGraphTour {
66
66
  * Central entrypoints to seed the tour. Raise only when the question names
67
67
  * several public paths that must all appear in one answer.
68
68
  *
69
- * @default 4
69
+ * @default 5
70
70
  */
71
71
  limit?: number;
72
72