@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.
- package/README.md +1 -1
- package/lib/index.js +36 -0
- package/lib/index.js.map +1 -1
- package/lib/model/TtscGraphMemory.d.ts +1 -1
- package/lib/model/TtscGraphMemory.js +16 -13
- package/lib/model/TtscGraphMemory.js.map +1 -1
- package/lib/model/TtscGraphSession.d.ts +2 -6
- package/lib/model/TtscGraphSession.js +414 -235
- package/lib/model/TtscGraphSession.js.map +1 -1
- package/lib/model/TtscGraphShardStore.d.ts +16 -0
- package/lib/model/TtscGraphShardStore.js +272 -0
- package/lib/model/TtscGraphShardStore.js.map +1 -0
- package/lib/model/loadGraph.js +68 -61
- package/lib/model/loadGraph.js.map +1 -1
- package/lib/nativeExecutable.d.ts +22 -0
- package/lib/nativeExecutable.js +76 -0
- package/lib/nativeExecutable.js.map +1 -1
- package/lib/server/runDetails.js +4 -9
- package/lib/server/runDetails.js.map +1 -1
- package/lib/server/runEntrypoints.js +4 -7
- package/lib/server/runEntrypoints.js.map +1 -1
- package/lib/server/runLookup.js +1 -1
- package/lib/server/runLookup.js.map +1 -1
- package/lib/server/runOverview.js +3 -3
- package/lib/server/runOverview.js.map +1 -1
- package/lib/server/runTour.js +7 -11
- package/lib/server/runTour.js.map +1 -1
- package/lib/server/runTrace.d.ts +4 -5
- package/lib/server/runTrace.js +10 -19
- package/lib/server/runTrace.js.map +1 -1
- package/lib/structures/ITtscGraphDump.d.ts +8 -2
- package/lib/structures/ITtscGraphNode.d.ts +1 -2
- package/lib/structures/ITtscGraphSnapshot.d.ts +37 -1
- package/lib/structures/TtscGraphDumpEdgeKind.d.ts +2 -0
- package/lib/structures/TtscGraphDumpEdgeKind.js +3 -0
- package/lib/structures/TtscGraphDumpEdgeKind.js.map +1 -0
- package/lib/structures/TtscGraphDumpNodeKind.d.ts +2 -0
- package/lib/structures/TtscGraphDumpNodeKind.js +3 -0
- package/lib/structures/TtscGraphDumpNodeKind.js.map +1 -0
- package/lib/structures/TtscGraphEdgeKind.d.ts +8 -6
- package/lib/structures/TtscGraphNodeKind.d.ts +5 -6
- package/lib/structures/TtscGraphNodeModifier.d.ts +1 -1
- package/lib/structures/index.d.ts +2 -0
- package/lib/structures/index.js +2 -0
- package/lib/structures/index.js.map +1 -1
- package/lib/view.js +17 -2
- package/lib/view.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +40 -0
- package/src/model/TtscGraphMemory.ts +10 -8
- package/src/model/TtscGraphSession.ts +64 -47
- package/src/model/TtscGraphShardStore.ts +371 -0
- package/src/model/loadGraph.ts +20 -13
- package/src/nativeExecutable.ts +83 -0
- package/src/server/runDetails.ts +5 -10
- package/src/server/runEntrypoints.ts +4 -7
- package/src/server/runLookup.ts +1 -1
- package/src/server/runOverview.ts +3 -3
- package/src/server/runTour.ts +8 -14
- package/src/server/runTrace.ts +10 -19
- package/src/structures/ITtscGraphDump.ts +10 -2
- package/src/structures/ITtscGraphNode.ts +1 -2
- package/src/structures/ITtscGraphSnapshot.ts +42 -1
- package/src/structures/TtscGraphDumpEdgeKind.ts +11 -0
- package/src/structures/TtscGraphDumpNodeKind.ts +10 -0
- package/src/structures/TtscGraphEdgeKind.ts +8 -9
- package/src/structures/TtscGraphNodeKind.ts +5 -11
- package/src/structures/TtscGraphNodeModifier.ts +1 -2
- package/src/structures/index.ts +2 -0
- 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
|
+
}
|
package/src/model/loadGraph.ts
CHANGED
|
@@ -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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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}: ${
|
|
86
|
+
`@ttsc/graph: ttscgraph dump exited with ${result.status}: ${stderr.trim()}`,
|
|
80
87
|
);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
return TtscGraphMemory.from(parseDump(
|
|
90
|
+
return TtscGraphMemory.from(parseDump(stdout));
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
/**
|
package/src/nativeExecutable.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/server/runDetails.ts
CHANGED
|
@@ -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"
|
|
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<
|
|
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
|
-
|
|
471
|
-
return 4;
|
|
466
|
+
return 3;
|
|
472
467
|
case "extends":
|
|
473
468
|
case "implements":
|
|
474
|
-
return
|
|
469
|
+
return 4;
|
|
475
470
|
case "type_ref":
|
|
476
|
-
return
|
|
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"
|
|
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
|
-
|
|
205
|
-
return 4;
|
|
202
|
+
return 3;
|
|
206
203
|
case "extends":
|
|
207
204
|
case "implements":
|
|
208
|
-
return
|
|
205
|
+
return 4;
|
|
209
206
|
case "type_ref":
|
|
210
|
-
return
|
|
207
|
+
return 5;
|
|
211
208
|
default:
|
|
212
209
|
return 10;
|
|
213
210
|
}
|
package/src/server/runLookup.ts
CHANGED
|
@@ -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"
|
|
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"
|
|
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
|
|
87
|
-
*
|
|
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 => {
|
package/src/server/runTour.ts
CHANGED
|
@@ -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"
|
|
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<
|
|
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
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
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
|
}
|