@ttsc/graph 0.23.0 → 0.25.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 (70) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +36 -0
  3. package/lib/index.js.map +1 -1
  4. package/lib/model/TtscGraphMemory.d.ts +1 -1
  5. package/lib/model/TtscGraphMemory.js +16 -13
  6. package/lib/model/TtscGraphMemory.js.map +1 -1
  7. package/lib/model/TtscGraphSession.d.ts +2 -6
  8. package/lib/model/TtscGraphSession.js +414 -235
  9. package/lib/model/TtscGraphSession.js.map +1 -1
  10. package/lib/model/TtscGraphShardStore.d.ts +16 -0
  11. package/lib/model/TtscGraphShardStore.js +272 -0
  12. package/lib/model/TtscGraphShardStore.js.map +1 -0
  13. package/lib/model/loadGraph.js +68 -61
  14. package/lib/model/loadGraph.js.map +1 -1
  15. package/lib/nativeExecutable.d.ts +22 -0
  16. package/lib/nativeExecutable.js +76 -0
  17. package/lib/nativeExecutable.js.map +1 -1
  18. package/lib/server/runDetails.js +4 -9
  19. package/lib/server/runDetails.js.map +1 -1
  20. package/lib/server/runEntrypoints.js +4 -7
  21. package/lib/server/runEntrypoints.js.map +1 -1
  22. package/lib/server/runLookup.js +1 -1
  23. package/lib/server/runLookup.js.map +1 -1
  24. package/lib/server/runOverview.js +3 -3
  25. package/lib/server/runOverview.js.map +1 -1
  26. package/lib/server/runTour.js +7 -11
  27. package/lib/server/runTour.js.map +1 -1
  28. package/lib/server/runTrace.d.ts +4 -5
  29. package/lib/server/runTrace.js +10 -19
  30. package/lib/server/runTrace.js.map +1 -1
  31. package/lib/structures/ITtscGraphDump.d.ts +8 -2
  32. package/lib/structures/ITtscGraphNode.d.ts +1 -2
  33. package/lib/structures/ITtscGraphSnapshot.d.ts +37 -1
  34. package/lib/structures/TtscGraphDumpEdgeKind.d.ts +2 -0
  35. package/lib/structures/TtscGraphDumpEdgeKind.js +3 -0
  36. package/lib/structures/TtscGraphDumpEdgeKind.js.map +1 -0
  37. package/lib/structures/TtscGraphDumpNodeKind.d.ts +2 -0
  38. package/lib/structures/TtscGraphDumpNodeKind.js +3 -0
  39. package/lib/structures/TtscGraphDumpNodeKind.js.map +1 -0
  40. package/lib/structures/TtscGraphEdgeKind.d.ts +8 -6
  41. package/lib/structures/TtscGraphNodeKind.d.ts +5 -6
  42. package/lib/structures/TtscGraphNodeModifier.d.ts +1 -1
  43. package/lib/structures/index.d.ts +2 -0
  44. package/lib/structures/index.js +2 -0
  45. package/lib/structures/index.js.map +1 -1
  46. package/lib/view.js +17 -2
  47. package/lib/view.js.map +1 -1
  48. package/package.json +3 -3
  49. package/src/index.ts +40 -0
  50. package/src/model/TtscGraphMemory.ts +10 -8
  51. package/src/model/TtscGraphSession.ts +64 -47
  52. package/src/model/TtscGraphShardStore.ts +371 -0
  53. package/src/model/loadGraph.ts +20 -13
  54. package/src/nativeExecutable.ts +83 -0
  55. package/src/server/runDetails.ts +5 -10
  56. package/src/server/runEntrypoints.ts +4 -7
  57. package/src/server/runLookup.ts +1 -1
  58. package/src/server/runOverview.ts +3 -3
  59. package/src/server/runTour.ts +8 -14
  60. package/src/server/runTrace.ts +10 -19
  61. package/src/structures/ITtscGraphDump.ts +10 -2
  62. package/src/structures/ITtscGraphNode.ts +1 -2
  63. package/src/structures/ITtscGraphSnapshot.ts +42 -1
  64. package/src/structures/TtscGraphDumpEdgeKind.ts +11 -0
  65. package/src/structures/TtscGraphDumpNodeKind.ts +10 -0
  66. package/src/structures/TtscGraphEdgeKind.ts +8 -9
  67. package/src/structures/TtscGraphNodeKind.ts +5 -11
  68. package/src/structures/TtscGraphNodeModifier.ts +1 -2
  69. package/src/structures/index.ts +2 -0
  70. package/src/view.ts +21 -7
@@ -0,0 +1,371 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHash } from "node:crypto";
3
+
4
+ import { ITtscGraphDump } from "../structures/ITtscGraphDump";
5
+ import { ITtscGraphSnapshot } from "../structures/ITtscGraphSnapshot";
6
+ import { DUMP_SCHEMA_VERSION } from "./loadGraph";
7
+
8
+ /** Atomic validator and assembler for native `ttscgraph` shard transactions. */
9
+ export class TtscGraphShardStore {
10
+ static readonly PROTOCOL_VERSION = 1;
11
+
12
+ private sequence: number | undefined;
13
+ private generation: string | undefined;
14
+ private project: string | undefined;
15
+ private tsconfig: string | undefined;
16
+ private shards = new Map<
17
+ string,
18
+ { digest: string; shard: ITtscGraphSnapshot.IShard }
19
+ >();
20
+
21
+ /** Validate and atomically commit one complete or base-generation delta. */
22
+ apply(transaction: ITtscGraphSnapshot.ITransaction): ITtscGraphDump {
23
+ this.assertCoordinates(transaction);
24
+ const next = new Map(this.shards);
25
+ const touched = new Set<string>();
26
+ for (const key of transaction.deletes) {
27
+ assertShardKey(key);
28
+ if (touched.has(key)) {
29
+ throw new Error(`@ttsc/graph: native transaction repeats shard ${key}`);
30
+ }
31
+ touched.add(key);
32
+ if (!next.delete(key)) {
33
+ throw new Error(
34
+ `@ttsc/graph: native transaction deletes unknown shard ${key}`,
35
+ );
36
+ }
37
+ }
38
+ for (const upsert of transaction.upserts) {
39
+ assertShardKey(upsert.shard.key);
40
+ if (touched.has(upsert.shard.key)) {
41
+ throw new Error(
42
+ `@ttsc/graph: native transaction touches shard ${upsert.shard.key} more than once`,
43
+ );
44
+ }
45
+ touched.add(upsert.shard.key);
46
+ const digest = TtscGraphShardStore.shardDigest(upsert.shard);
47
+ if (digest !== upsert.digest) {
48
+ throw new Error(
49
+ `@ttsc/graph: native shard ${upsert.shard.key} digest ${upsert.digest} does not match ${digest}`,
50
+ );
51
+ }
52
+ next.set(upsert.shard.key, { digest, shard: upsert.shard });
53
+ }
54
+
55
+ const manifest = [...transaction.manifest];
56
+ for (let index = 0; index < manifest.length; index++) {
57
+ const reference = manifest[index]!;
58
+ if (
59
+ index !== 0 &&
60
+ compareText(manifest[index - 1]!.key, reference.key) >= 0
61
+ ) {
62
+ throw new Error(
63
+ "@ttsc/graph: native shard manifest must be strictly key-sorted",
64
+ );
65
+ }
66
+ }
67
+ if (manifest.length !== next.size) {
68
+ throw new Error(
69
+ "@ttsc/graph: native shard manifest does not describe the reconstructed generation",
70
+ );
71
+ }
72
+ for (const reference of manifest) {
73
+ assertShardKey(reference.key);
74
+ const committed = next.get(reference.key);
75
+ if (committed === undefined || committed.digest !== reference.digest) {
76
+ throw new Error(
77
+ `@ttsc/graph: native shard manifest disagrees at ${reference.key}`,
78
+ );
79
+ }
80
+ }
81
+ const generation = digest({
82
+ tsconfig: transaction.tsconfig,
83
+ producer: transaction.producer,
84
+ capabilities: transaction.capabilities,
85
+ universe: transaction.universe,
86
+ manifest,
87
+ });
88
+ if (generation !== transaction.generation) {
89
+ throw new Error(
90
+ `@ttsc/graph: native generation ${transaction.generation} does not match ${generation}`,
91
+ );
92
+ }
93
+
94
+ const dump = assemble(transaction, next);
95
+ this.sequence = transaction.sequence;
96
+ this.generation = transaction.generation;
97
+ this.project = transaction.project;
98
+ this.tsconfig = transaction.tsconfig;
99
+ this.shards = next;
100
+ return dump;
101
+ }
102
+
103
+ /** SHA-256 over the producer's deterministic Go JSON encoding. */
104
+ static shardDigest(shard: ITtscGraphSnapshot.IShard): string {
105
+ return digest(shard);
106
+ }
107
+
108
+ private assertCoordinates(
109
+ transaction: ITtscGraphSnapshot.ITransaction,
110
+ ): void {
111
+ if (transaction.protocolVersion !== TtscGraphShardStore.PROTOCOL_VERSION) {
112
+ throw new Error(
113
+ `@ttsc/graph: ttscgraph sends graph snapshot protocol v${String(transaction.protocolVersion)}, this client reads v${String(TtscGraphShardStore.PROTOCOL_VERSION)}`,
114
+ );
115
+ }
116
+ if (transaction.schemaVersion !== DUMP_SCHEMA_VERSION) {
117
+ throw new Error(
118
+ `@ttsc/graph: ttscgraph sends dump schema v${String(transaction.schemaVersion)}, this client reads v${String(DUMP_SCHEMA_VERSION)}`,
119
+ );
120
+ }
121
+ if (
122
+ !Number.isSafeInteger(transaction.sequence) ||
123
+ transaction.sequence < 1
124
+ ) {
125
+ throw new Error("@ttsc/graph: native transaction sequence is invalid");
126
+ }
127
+ if (transaction.generation === "") {
128
+ throw new Error("@ttsc/graph: native transaction generation is empty");
129
+ }
130
+ if (this.sequence === undefined || this.generation === undefined) {
131
+ if (
132
+ transaction.sequence !== 1 ||
133
+ transaction.baseSequence !== undefined ||
134
+ transaction.baseGeneration !== undefined ||
135
+ transaction.deletes.length !== 0
136
+ ) {
137
+ throw new Error(
138
+ "@ttsc/graph: initial native transaction is not a complete generation",
139
+ );
140
+ }
141
+ return;
142
+ }
143
+ if (
144
+ transaction.sequence !== this.sequence + 1 ||
145
+ transaction.baseSequence !== this.sequence ||
146
+ transaction.baseGeneration !== this.generation
147
+ ) {
148
+ throw new Error(
149
+ `@ttsc/graph: native transaction has stale base ${String(transaction.baseSequence)}/${String(transaction.baseGeneration)}`,
150
+ );
151
+ }
152
+ if (
153
+ transaction.project !== this.project ||
154
+ transaction.tsconfig !== this.tsconfig
155
+ ) {
156
+ throw new Error(
157
+ "@ttsc/graph: native transaction changed its resident project coordinates",
158
+ );
159
+ }
160
+ }
161
+ }
162
+
163
+ function assemble(
164
+ transaction: ITtscGraphSnapshot.ITransaction,
165
+ committed: ReadonlyMap<
166
+ string,
167
+ { digest: string; shard: ITtscGraphSnapshot.IShard }
168
+ >,
169
+ ): ITtscGraphDump {
170
+ const nodes: ITtscGraphDump.INode[] = [];
171
+ const edges: ITtscGraphDump.IEdge[] = [];
172
+ const diagnostics: ITtscGraphDump.IDiagnostic[] = [];
173
+ const sources: ITtscGraphDump.ISourceDigest[] = [];
174
+ const nodeOwners = new Map<string, string>();
175
+ const sourceFiles = new Set<string>();
176
+ const configInputs = new Map<string, string>();
177
+ for (const [key, value] of committed) {
178
+ const shard = value.shard;
179
+ if (shard.key !== key) {
180
+ throw new Error(`@ttsc/graph: native shard key disagrees at ${key}`);
181
+ }
182
+ if (shard.source !== undefined && shard.config !== undefined) {
183
+ throw new Error(
184
+ `@ttsc/graph: native shard ${key} claims both source and config input`,
185
+ );
186
+ }
187
+ if (
188
+ shard.config !== undefined &&
189
+ (shard.nodes.length !== 0 || shard.edges.length !== 0)
190
+ ) {
191
+ throw new Error(
192
+ `@ttsc/graph: native config shard ${key} unexpectedly owns facts`,
193
+ );
194
+ }
195
+ if (shard.config !== undefined) {
196
+ if (configInputs.has(shard.config.file)) {
197
+ throw new Error(
198
+ `@ttsc/graph: native config ${shard.config.file} has more than one shard`,
199
+ );
200
+ }
201
+ configInputs.set(shard.config.file, shard.config.digest);
202
+ }
203
+ if (shard.source !== undefined) {
204
+ if (sourceFiles.has(shard.source.file)) {
205
+ throw new Error(
206
+ `@ttsc/graph: native source ${shard.source.file} has more than one shard`,
207
+ );
208
+ }
209
+ sourceFiles.add(shard.source.file);
210
+ sources.push({ ...shard.source });
211
+ }
212
+ assertShardContents(key, shard);
213
+ for (const node of shard.nodes) {
214
+ const owner = nodeOwners.get(node.id);
215
+ if (owner !== undefined) {
216
+ throw new Error(
217
+ `@ttsc/graph: native node ${node.id} is owned by both ${owner} and ${key}`,
218
+ );
219
+ }
220
+ nodeOwners.set(node.id, key);
221
+ nodes.push(node);
222
+ }
223
+ edges.push(...shard.edges);
224
+ diagnostics.push(...shard.diagnostics);
225
+ }
226
+ for (const [key, value] of committed) {
227
+ for (const edge of value.shard.edges) {
228
+ if (nodeOwners.get(edge.from) !== key) {
229
+ throw new Error(
230
+ `@ttsc/graph: native shard ${key} does not own edge source ${edge.from}`,
231
+ );
232
+ }
233
+ if (!nodeOwners.has(edge.to)) {
234
+ throw new Error(
235
+ `@ttsc/graph: native edge target is absent from the generation: ${edge.to}`,
236
+ );
237
+ }
238
+ }
239
+ }
240
+ const unmatchedConfigs = new Map(configInputs);
241
+ for (const config of transaction.universe.configs) {
242
+ if (
243
+ unmatchedConfigs.get(config.file) !== config.digest ||
244
+ !unmatchedConfigs.delete(config.file)
245
+ ) {
246
+ throw new Error(
247
+ `@ttsc/graph: native config shard disagrees with universe input ${config.file}`,
248
+ );
249
+ }
250
+ }
251
+ if (unmatchedConfigs.size !== 0) {
252
+ throw new Error(
253
+ "@ttsc/graph: native config shards do not cover the build universe",
254
+ );
255
+ }
256
+ nodes.sort((left, right) => compareText(left.id, right.id));
257
+ edges.sort(
258
+ (left, right) =>
259
+ compareText(left.from, right.from) ||
260
+ compareText(left.to, right.to) ||
261
+ compareText(left.kind, right.kind),
262
+ );
263
+ diagnostics.sort(
264
+ (left, right) =>
265
+ compareText(left.file, right.file) ||
266
+ left.line - right.line ||
267
+ left.column - right.column ||
268
+ left.code - right.code,
269
+ );
270
+ sources.sort((left, right) => compareText(left.file, right.file));
271
+ return {
272
+ project: transaction.project,
273
+ tsconfig: transaction.tsconfig,
274
+ provenance: {
275
+ schemaVersion: transaction.schemaVersion,
276
+ capabilities: [...transaction.capabilities],
277
+ producer: { ...transaction.producer },
278
+ universe: {
279
+ configs: transaction.universe.configs.map((config) => ({ ...config })),
280
+ roots: transaction.universe.roots.map((root) => ({ ...root })),
281
+ },
282
+ sources,
283
+ },
284
+ diagnostics,
285
+ nodes,
286
+ edges,
287
+ };
288
+ }
289
+
290
+ function assertShardContents(
291
+ key: string,
292
+ shard: ITtscGraphSnapshot.IShard,
293
+ ): void {
294
+ if (shard.source !== undefined) {
295
+ for (const node of shard.nodes) {
296
+ if (node.external || node.file !== shard.source.file) {
297
+ throw new Error(
298
+ `@ttsc/graph: native source shard ${key} owns node ${node.id} from ${node.file}`,
299
+ );
300
+ }
301
+ }
302
+ for (const diagnostic of shard.diagnostics) {
303
+ if (diagnostic.file !== shard.source.file) {
304
+ throw new Error(
305
+ `@ttsc/graph: native source shard ${key} owns diagnostic from ${diagnostic.file}`,
306
+ );
307
+ }
308
+ }
309
+ return;
310
+ }
311
+ if (shard.edges.length !== 0) {
312
+ throw new Error(
313
+ `@ttsc/graph: native non-source shard ${key} unexpectedly owns edges`,
314
+ );
315
+ }
316
+ if (shard.config !== undefined) {
317
+ for (const diagnostic of shard.diagnostics) {
318
+ if (diagnostic.file !== shard.config.file) {
319
+ throw new Error(
320
+ `@ttsc/graph: native config shard ${key} owns diagnostic from ${diagnostic.file}`,
321
+ );
322
+ }
323
+ }
324
+ return;
325
+ }
326
+ for (const node of shard.nodes) {
327
+ if (!node.external) {
328
+ throw new Error(
329
+ `@ttsc/graph: native metadata shard ${key} owns authored node ${node.id}`,
330
+ );
331
+ }
332
+ }
333
+ for (const diagnostic of shard.diagnostics) {
334
+ if (diagnostic.file !== "") {
335
+ throw new Error(
336
+ `@ttsc/graph: native metadata shard ${key} owns diagnostic from ${diagnostic.file}`,
337
+ );
338
+ }
339
+ }
340
+ }
341
+
342
+ function assertShardKey(key: string): void {
343
+ if (key === "" || key.includes("\0")) {
344
+ throw new Error(`@ttsc/graph: native shard key is invalid: ${key}`);
345
+ }
346
+ }
347
+
348
+ function compareText(left: string, right: string): number {
349
+ return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
350
+ }
351
+
352
+ function goJSON(value: unknown): string {
353
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/gu, (character) => {
354
+ switch (character) {
355
+ case "<":
356
+ return "\\u003c";
357
+ case ">":
358
+ return "\\u003e";
359
+ case "&":
360
+ return "\\u0026";
361
+ case "\u2028":
362
+ return "\\u2028";
363
+ default:
364
+ return "\\u2029";
365
+ }
366
+ });
367
+ }
368
+
369
+ function digest(value: unknown): string {
370
+ return createHash("sha256").update(goJSON(value)).digest("hex");
371
+ }
@@ -1,16 +1,11 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import typia from "typia";
3
3
 
4
- import { ensureExecutable } from "../nativeExecutable";
4
+ import { captureProcessOutput, ensureExecutable } from "../nativeExecutable";
5
5
  import { resolveGraphBinary } from "../resolveGraphBinary";
6
6
  import { ITtscGraphDump } from "../structures/ITtscGraphDump";
7
7
  import { TtscGraphMemory } from "./TtscGraphMemory";
8
8
 
9
- // A full-project dump is the whole fact graph as one JSON document; a large
10
- // monorepo runs to many megabytes, well past spawnSync's 1 MiB default, so the
11
- // buffer is raised to a ceiling no real graph reaches.
12
- const MAX_DUMP_BYTES = 1024 * 1024 * 1024;
13
-
14
9
  /**
15
10
  * The dump schema version this client reads.
16
11
  *
@@ -64,11 +59,23 @@ export function loadGraph(
64
59
  }
65
60
  ensureExecutable(binary);
66
61
 
67
- const result = spawnSync(
68
- binary,
69
- ["dump", "--cwd", cwd, "--tsconfig", tsconfig],
70
- { encoding: "utf8", maxBuffer: MAX_DUMP_BYTES, windowsHide: true },
71
- );
62
+ // The dump goes to a file rather than a pipe, so no output ceiling applies:
63
+ // a graph is as large as the repository is, and any limit named here would be
64
+ // a guess about someone else's monorepo. See `captureProcessOutput`.
65
+ const capture = captureProcessOutput();
66
+ let result;
67
+ let stdout: string;
68
+ let stderr: string;
69
+ try {
70
+ result = spawnSync(binary, ["dump", "--cwd", cwd, "--tsconfig", tsconfig], {
71
+ stdio: ["ignore", capture.stdoutFd, capture.stderrFd],
72
+ windowsHide: true,
73
+ });
74
+ stdout = capture.read("stdout");
75
+ stderr = capture.read("stderr");
76
+ } finally {
77
+ capture.dispose();
78
+ }
72
79
  if (result.error) {
73
80
  throw new Error(
74
81
  `@ttsc/graph: ttscgraph dump failed: ${result.error.message}`,
@@ -76,11 +83,11 @@ export function loadGraph(
76
83
  }
77
84
  if (result.status !== 0) {
78
85
  throw new Error(
79
- `@ttsc/graph: ttscgraph dump exited with ${result.status}: ${(result.stderr ?? "").trim()}`,
86
+ `@ttsc/graph: ttscgraph dump exited with ${result.status}: ${stderr.trim()}`,
80
87
  );
81
88
  }
82
89
 
83
- return TtscGraphMemory.from(parseDump(result.stdout));
90
+ return TtscGraphMemory.from(parseDump(stdout));
84
91
  }
85
92
 
86
93
  /**
@@ -1,4 +1,6 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
2
4
 
3
5
  /**
4
6
  * Ensure a resolved native binary can be executed on POSIX installs.
@@ -22,3 +24,84 @@ export function ensureExecutable(binary: string): void {
22
24
  }
23
25
  }
24
26
  }
27
+
28
+ export interface CapturedProcessOutput {
29
+ /** Close the descriptors and remove the backing files. */
30
+ dispose(): void;
31
+ /** Read one stream's text. */
32
+ read(stream: "stdout" | "stderr"): string;
33
+ stderrFd: number;
34
+ stdoutFd: number;
35
+ }
36
+
37
+ /**
38
+ * A pair of temporary files standing in for a child process's pipes.
39
+ *
40
+ * `spawnSync` holds a _piped_ stream in this process's memory and refuses to
41
+ * keep more than `maxBuffer` bytes, so any piped capture has to name a ceiling
42
+ * — and a ceiling is a number nobody chose for this machine, deciding that a
43
+ * large but legitimate graph said too much. Handing the child a file descriptor
44
+ * instead means the bytes never pass through this heap on their way out of the
45
+ * child: how much a process may write is the filesystem's business, and that is
46
+ * the same answer everywhere. Reading the result back still materializes a
47
+ * string, so V8's own maximum string length remains the outer bound — a
48
+ * property of the runtime rather than a budget chosen here.
49
+ */
50
+ export function captureProcessOutput(): CapturedProcessOutput {
51
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ttscgraph-spawn-"));
52
+ const stdoutPath = path.join(directory, "stdout");
53
+ const stderrPath = path.join(directory, "stderr");
54
+ const stdoutFd = fs.openSync(stdoutPath, "w+");
55
+ let stderrFd: number;
56
+ try {
57
+ stderrFd = fs.openSync(stderrPath, "w+");
58
+ } catch (error) {
59
+ // The first descriptor and the directory are already live, and no caller
60
+ // ever received a handle to dispose of them.
61
+ closeQuietly(stdoutFd);
62
+ removeQuietly(directory);
63
+ throw error;
64
+ }
65
+ return {
66
+ dispose(): void {
67
+ closeQuietly(stdoutFd);
68
+ closeQuietly(stderrFd);
69
+ removeQuietly(directory);
70
+ },
71
+ read(stream): string {
72
+ const location = stream === "stdout" ? stdoutPath : stderrPath;
73
+ try {
74
+ return fs.readFileSync(location, "utf8");
75
+ } catch {
76
+ // A spawn that never launched leaves nothing behind.
77
+ return "";
78
+ }
79
+ },
80
+ stderrFd,
81
+ stdoutFd,
82
+ };
83
+ }
84
+
85
+ /** Close a descriptor, ignoring one that is already closed. */
86
+ function closeQuietly(fd: number): void {
87
+ try {
88
+ fs.closeSync(fd);
89
+ } catch {
90
+ // Already closed; removing the directory is what reclaims the space.
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Remove the capture directory without letting cleanup replace a result.
96
+ *
97
+ * `dispose` runs from a `finally`, so a throw here would surface instead of the
98
+ * spawn's own outcome — and on Windows a grandchild that inherited the handle
99
+ * can hold the file long enough to make removal fail.
100
+ */
101
+ function removeQuietly(directory: string): void {
102
+ try {
103
+ fs.rmSync(directory, { force: true, recursive: true });
104
+ } catch {
105
+ // Best effort.
106
+ }
107
+ }
@@ -24,13 +24,11 @@ const MAX_NEIGHBORS = 3;
24
24
  const DEFAULT_DEPENDENCIES = 2;
25
25
  const MAX_DEPENDENCIES = 4;
26
26
  // Structural relationships are navigation, not the dependency picture details is for.
27
- const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
27
+ const STRUCTURAL_KINDS = new Set<string>(["contains", "exports"]);
28
28
  // Kinds whose value is their member outline, not implementation text.
29
- const CONTAINER_KINDS = new Set<string>([
29
+ const CONTAINER_KINDS = new Set<ITtscGraphNode["kind"]>([
30
30
  "class",
31
31
  "interface",
32
- "namespace",
33
- "module",
34
32
  "enum",
35
33
  "file",
36
34
  ]);
@@ -464,16 +462,13 @@ function edgeKindRank(kind: string): number {
464
462
  case "accesses":
465
463
  case "renders":
466
464
  return 2;
467
- case "tests":
468
- return 3;
469
465
  case "overrides":
470
- case "decorates":
471
- return 4;
466
+ return 3;
472
467
  case "extends":
473
468
  case "implements":
474
- return 5;
469
+ return 4;
475
470
  case "type_ref":
476
- return 6;
471
+ return 5;
477
472
  default:
478
473
  return 10;
479
474
  }
@@ -12,7 +12,7 @@ const MAX_LIMIT = 8;
12
12
  const DEFAULT_NEIGHBORS = 0;
13
13
  const MAX_NEIGHBORS = 2;
14
14
  const MAX_SEEDS = 3;
15
- const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
15
+ const STRUCTURAL_KINDS = new Set<string>(["contains", "exports"]);
16
16
 
17
17
  /**
18
18
  * Build the first source-free entrypoints list for a code question. The result
@@ -198,16 +198,13 @@ function edgeKindRank(kind: string): number {
198
198
  case "accesses":
199
199
  case "renders":
200
200
  return 2;
201
- case "tests":
202
- return 3;
203
201
  case "overrides":
204
- case "decorates":
205
- return 4;
202
+ return 3;
206
203
  case "extends":
207
204
  case "implements":
208
- return 5;
205
+ return 4;
209
206
  case "type_ref":
210
- return 6;
207
+ return 5;
211
208
  default:
212
209
  return 10;
213
210
  }
@@ -260,7 +260,7 @@ function degree(graph: TtscGraphMemory, id: string): number {
260
260
  }
261
261
 
262
262
  function isStructural(kind: string): boolean {
263
- return kind === "contains" || kind === "exports" || kind === "imports";
263
+ return kind === "contains" || kind === "exports";
264
264
  }
265
265
 
266
266
  function isTestFile(file: string): boolean {
@@ -5,7 +5,7 @@ import { isPublicApiNoisePath, isSupportPath } from "./pathPolicy";
5
5
  import { IRunnerOutput, resultNext } from "./resultNext";
6
6
 
7
7
  /** Edges that express nesting/packaging, not code dependency. */
8
- const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
8
+ const STRUCTURAL_KINDS = new Set<string>(["contains", "exports"]);
9
9
 
10
10
  /**
11
11
  * Project a compact, source-read-free architecture map: counts by kind, folder
@@ -83,8 +83,8 @@ function layers(graph: TtscGraphMemory): ITtscGraphOverview.ILayer[] {
83
83
 
84
84
  /**
85
85
  * The symbols at the center of the dependency graph, ranked by real fan-in and
86
- * fan-out. Structural `contains`/`exports`/`imports` edges are excluded so the
87
- * ranking reflects code dependency, not nesting.
86
+ * fan-out. Structural `contains`/`exports` edges are excluded so the ranking
87
+ * reflects code dependency, not nesting.
88
88
  */
89
89
  function hotspots(graph: TtscGraphMemory): ITtscGraphOverview.IHotspot[] {
90
90
  const real = (id: string, side: "in" | "out"): number => {
@@ -49,21 +49,19 @@ const MAX_READ_NEXT = 14;
49
49
  const FLOW_OVERLAP = 0.6;
50
50
  const TOUR_TRACE_MAX_DEPTH = 6;
51
51
  const TOUR_TRACE_MAX_NODES = 18;
52
- const STRUCTURAL_KINDS = new Set<string>(["contains", "exports", "imports"]);
52
+ const STRUCTURAL_KINDS = new Set<string>(["contains", "exports"]);
53
53
  const EXECUTION_KINDS = new Set<string>([
54
54
  "calls",
55
55
  "instantiates",
56
56
  "accesses",
57
57
  "renders",
58
58
  ]);
59
- const TOUR_SEED_KINDS = new Set<string>([
59
+ const TOUR_SEED_KINDS = new Set<ITtscGraphNode["kind"]>([
60
60
  "class",
61
61
  "function",
62
62
  "method",
63
63
  "property",
64
64
  "variable",
65
- "module",
66
- "namespace",
67
65
  "enum",
68
66
  ]);
69
67
 
@@ -845,11 +843,11 @@ function isTourHop(graph: TtscGraphMemory, hop: ITtscGraphTrace.IHop): boolean {
845
843
  // floor makes it a no-op on small graphs that have no genuine hub. The `out <= 1`
846
844
  // guard keeps thin pass-throughs out but never prunes a real branching step.
847
845
  //
848
- // The fan-in counted is EXECUTION fan-in. `realDegree` also counts `type_ref`,
849
- // `extends`, `decorates`, and `tests`, and a widely referenced type or a
850
- // widely decorated symbol is not a call hub: a `Config` class named in twelve
851
- // parameter positions and constructed once was read as a hub and cut out of the
852
- // flow that constructs it. Popularity as a name is not popularity as a call.
846
+ // The fan-in counted is EXECUTION fan-in. `realDegree` also counts `type_ref`
847
+ // and `extends`, and a widely referenced type is not a call hub: a `Config`
848
+ // class named in twelve parameter positions and constructed once was read as a
849
+ // hub and cut out of the flow that constructs it. Popularity as a name is not
850
+ // popularity as a call.
853
851
  function isSharedUtility(graph: TtscGraphMemory, id: string): boolean {
854
852
  const execution = executionDegree(graph, id);
855
853
  return execution.in >= 12 && execution.out <= 1;
@@ -932,11 +930,7 @@ function ownerOf(graph: TtscGraphMemory, id: string): string | undefined {
932
930
  for (const edge of graph.incoming(id)) {
933
931
  if (edge.kind !== "contains") continue;
934
932
  const owner = graph.node(edge.from);
935
- if (
936
- owner !== undefined &&
937
- owner.kind !== "file" &&
938
- owner.kind !== "module"
939
- ) {
933
+ if (owner !== undefined && owner.kind !== "file") {
940
934
  return owner.id;
941
935
  }
942
936
  }