@ttsc/graph 0.23.0 → 0.24.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 (62) 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 -0
  8. package/lib/model/TtscGraphSession.js +414 -220
  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 +48 -54
  14. package/lib/model/loadGraph.js.map +1 -1
  15. package/lib/server/runDetails.js +4 -9
  16. package/lib/server/runDetails.js.map +1 -1
  17. package/lib/server/runEntrypoints.js +4 -7
  18. package/lib/server/runEntrypoints.js.map +1 -1
  19. package/lib/server/runLookup.js +1 -1
  20. package/lib/server/runLookup.js.map +1 -1
  21. package/lib/server/runOverview.js +3 -3
  22. package/lib/server/runOverview.js.map +1 -1
  23. package/lib/server/runTour.js +7 -11
  24. package/lib/server/runTour.js.map +1 -1
  25. package/lib/server/runTrace.d.ts +4 -5
  26. package/lib/server/runTrace.js +10 -19
  27. package/lib/server/runTrace.js.map +1 -1
  28. package/lib/structures/ITtscGraphDump.d.ts +8 -2
  29. package/lib/structures/ITtscGraphNode.d.ts +1 -2
  30. package/lib/structures/ITtscGraphSnapshot.d.ts +37 -1
  31. package/lib/structures/TtscGraphDumpEdgeKind.d.ts +2 -0
  32. package/lib/structures/TtscGraphDumpEdgeKind.js +3 -0
  33. package/lib/structures/TtscGraphDumpEdgeKind.js.map +1 -0
  34. package/lib/structures/TtscGraphDumpNodeKind.d.ts +2 -0
  35. package/lib/structures/TtscGraphDumpNodeKind.js +3 -0
  36. package/lib/structures/TtscGraphDumpNodeKind.js.map +1 -0
  37. package/lib/structures/TtscGraphEdgeKind.d.ts +8 -6
  38. package/lib/structures/TtscGraphNodeKind.d.ts +5 -6
  39. package/lib/structures/TtscGraphNodeModifier.d.ts +1 -1
  40. package/lib/structures/index.d.ts +2 -0
  41. package/lib/structures/index.js +2 -0
  42. package/lib/structures/index.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/index.ts +40 -0
  45. package/src/model/TtscGraphMemory.ts +10 -8
  46. package/src/model/TtscGraphSession.ts +64 -16
  47. package/src/model/TtscGraphShardStore.ts +371 -0
  48. package/src/server/runDetails.ts +5 -10
  49. package/src/server/runEntrypoints.ts +4 -7
  50. package/src/server/runLookup.ts +1 -1
  51. package/src/server/runOverview.ts +3 -3
  52. package/src/server/runTour.ts +8 -14
  53. package/src/server/runTrace.ts +10 -19
  54. package/src/structures/ITtscGraphDump.ts +10 -2
  55. package/src/structures/ITtscGraphNode.ts +1 -2
  56. package/src/structures/ITtscGraphSnapshot.ts +42 -1
  57. package/src/structures/TtscGraphDumpEdgeKind.ts +11 -0
  58. package/src/structures/TtscGraphDumpNodeKind.ts +10 -0
  59. package/src/structures/TtscGraphEdgeKind.ts +8 -9
  60. package/src/structures/TtscGraphNodeKind.ts +5 -11
  61. package/src/structures/TtscGraphNodeModifier.ts +1 -2
  62. package/src/structures/index.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/graph",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Checker-resolved architecture graph over MCP for coding agents, backed by ttsc's in-process TypeScript-Go compiler.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  "typia": "13.2.0"
21
21
  },
22
22
  "peerDependencies": {
23
- "ttsc": "^0.23.0"
23
+ "ttsc": "^0.24.0"
24
24
  },
25
25
  "keywords": [
26
26
  "ttsc",
@@ -42,7 +42,7 @@
42
42
  "three": "^0.184.0",
43
43
  "three-forcegraph": "^1.43.4",
44
44
  "typescript": "^7.0.2",
45
- "ttsc": "0.23.0"
45
+ "ttsc": "0.24.0"
46
46
  },
47
47
  "repository": {
48
48
  "type": "git",
package/src/index.ts CHANGED
@@ -83,6 +83,37 @@ export function runGraph(
83
83
  );
84
84
  }
85
85
 
86
+ /** The help spellings `ttscgraph` itself accepts, mirrored for the fallback. */
87
+ const DUMP_HELP_FLAGS = new Set(["--help", "-help", "-h"]);
88
+
89
+ /**
90
+ * Print a `dump` usage summary when the native binary cannot be resolved.
91
+ *
92
+ * This is a fallback, not a second contract: `cmd/ttscgraph/main.go` owns these
93
+ * flags and answers whenever it is installed. Keep this short and point at that
94
+ * authority, so a drift degrades to a stale summary rather than a wrong
95
+ * answer.
96
+ */
97
+ function printDumpHelp(): void {
98
+ process.stdout.write(
99
+ [
100
+ "Usage: ttsc-graph dump [options]",
101
+ "",
102
+ "Write the whole compiler graph as JSON to stdout: every node and edge,",
103
+ "none of the MCP response caps.",
104
+ "",
105
+ "Options:",
106
+ " --cwd <dir> Project root (default: current directory).",
107
+ " --tsconfig <path> Project tsconfig path (default: tsconfig.json).",
108
+ " --pretty Indent the JSON output.",
109
+ "",
110
+ "The native `ttscgraph` binary owns these flags and is not installed here,",
111
+ "so this summary may lag it. Install `ttsc` and rerun for the exact list.",
112
+ "",
113
+ ].join("\n"),
114
+ );
115
+ }
116
+
86
117
  /**
87
118
  * Pass `dump` through to the native binary, inheriting stdio so the JSON lands
88
119
  * on this process's stdout. Returns the child's exit code.
@@ -93,6 +124,15 @@ function runDump(argv: readonly string[]): number {
93
124
  const { cwd } = projectOptions(parseLauncherOptions(argv, DUMP_OPTIONS));
94
125
  const binary = resolveGraphBinary(process.env, cwd);
95
126
  if (binary === null) {
127
+ // `ttscgraph` owns the flag contract, so a resolvable binary always answers
128
+ // `--help` itself and stays the single source of truth. It cannot answer
129
+ // when it is absent, and that is exactly when a caller is most likely to be
130
+ // asking what the command needs — so fall back to a summary that names the
131
+ // authority rather than making usage unreachable behind an install error.
132
+ if (argv.some((argument) => DUMP_HELP_FLAGS.has(argument))) {
133
+ printDumpHelp();
134
+ return 0;
135
+ }
96
136
  process.stderr.write(
97
137
  "@ttsc/graph: could not resolve the ttscgraph binary. " +
98
138
  "Install `ttsc` so its platform package is present, " +
@@ -27,7 +27,7 @@ export class TtscGraphMemory {
27
27
 
28
28
  /** The absolute project root the dump was built for. */
29
29
  readonly project: string;
30
- /** Every node, raw plus synthesized (file containers). */
30
+ /** Every post-fold node, including refined properties and file containers. */
31
31
  readonly nodes: readonly ITtscGraphNode[];
32
32
  /** Every edge, raw plus synthesized containment. */
33
33
  readonly edges: readonly ITtscGraphEdge[];
@@ -190,20 +190,22 @@ function synthesize(dump: ITtscGraphDump): {
190
190
  // small — the two copies are 17% of the document, 55 MB of VS Code's 323 MB,
191
191
  // paid again in the encode, the pipe, the parse and the validation. Nothing
192
192
  // downstream of this line sees a span without its file.
193
- const nodes: ITtscGraphNode[] = dump.nodes
194
- .filter((n) => n.kind !== "module")
195
- .map((n) => {
196
- const { evidence, implementation, ...rest } = n;
197
- return {
193
+ const nodes: ITtscGraphNode[] = dump.nodes.flatMap((n): ITtscGraphNode[] => {
194
+ if (n.kind === "module") return [];
195
+ const { evidence, implementation, ...rest } = n;
196
+ return [
197
+ {
198
198
  ...rest,
199
+ kind: n.kind,
199
200
  ...(evidence !== undefined
200
201
  ? { evidence: spanIn(evidence, n.file) }
201
202
  : {}),
202
203
  ...(implementation !== undefined
203
204
  ? { implementation: spanIn(implementation, n.file) }
204
205
  : {}),
205
- };
206
- });
206
+ },
207
+ ];
208
+ });
207
209
  const edges: ITtscGraphEdge[] = dump.edges.map((edge) => {
208
210
  const { evidence, ...rest } = edge;
209
211
  const from = moduleIds.get(edge.from);
@@ -6,6 +6,7 @@ import { ensureExecutable } from "../nativeExecutable";
6
6
  import { resolveGraphBinary } from "../resolveGraphBinary";
7
7
  import { ITtscGraphSnapshot } from "../structures/ITtscGraphSnapshot";
8
8
  import { TtscGraphMemory } from "./TtscGraphMemory";
9
+ import { TtscGraphShardStore } from "./TtscGraphShardStore";
9
10
  import { DUMP_SCHEMA_VERSION } from "./loadGraph";
10
11
 
11
12
  /**
@@ -17,6 +18,7 @@ import { DUMP_SCHEMA_VERSION } from "./loadGraph";
17
18
  * constant out of this file and fails if the pair drifts.
18
19
  */
19
20
  const PROTOCOL_VERSION = 1;
21
+ const GRAPH_SNAPSHOT_PROTOCOL_VERSION = 1;
20
22
  const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;
21
23
  const MAX_TIMER_MS = 2_147_483_647;
22
24
  const TERMINATION_GRACE_MS = 1_000;
@@ -75,6 +77,7 @@ export class TtscGraphSession {
75
77
  private readonly pending = new Map<number, Pending>();
76
78
  private queue: Promise<void> = Promise.resolve();
77
79
  private current: TtscGraphMemory | undefined;
80
+ private shardStore = new TtscGraphShardStore();
78
81
  private closed = false;
79
82
 
80
83
  public constructor(options: TtscGraphSessionOptions) {
@@ -167,16 +170,30 @@ export class TtscGraphSession {
167
170
  // The protocol version and the envelope shape were both settled in onLine,
168
171
  // before this frame was ever routed here.
169
172
  const response = await this.request(signal);
173
+ this.assertResponseSemantics(response);
170
174
  if (response.error !== undefined) {
171
175
  throw new Error(`@ttsc/graph: ${response.error}`);
172
176
  }
173
177
  if (response.changed) {
174
- if (response.dump === undefined) {
178
+ if (response.snapshot !== undefined) {
179
+ try {
180
+ this.current = TtscGraphMemory.from(
181
+ this.shardStore.apply(response.snapshot),
182
+ );
183
+ } catch (error) {
184
+ const failure = asError(error);
185
+ if (this.child !== undefined) this.failChild(this.child, failure);
186
+ throw failure;
187
+ }
188
+ } else if (response.dump !== undefined) {
189
+ // A serve-protocol-v1 binary predating graph shard negotiation ignores
190
+ // the optional request field and retains the complete-dump response.
191
+ this.current = TtscGraphMemory.from(response.dump);
192
+ } else {
175
193
  throw new Error(
176
- `@ttsc/graph: native ${response.mode} response omitted its dump`,
194
+ `@ttsc/graph: native ${response.mode} response omitted both graph snapshot protocol v${String(GRAPH_SNAPSHOT_PROTOCOL_VERSION)} data and a compatible dump`,
177
195
  );
178
196
  }
179
- this.current = TtscGraphMemory.from(response.dump);
180
197
  }
181
198
  if (this.current === undefined) {
182
199
  throw new Error(
@@ -186,6 +203,30 @@ export class TtscGraphSession {
186
203
  return this.current;
187
204
  }
188
205
 
206
+ private assertResponseSemantics(response: ITtscGraphSnapshot): void {
207
+ const bodies =
208
+ Number(response.dump !== undefined) +
209
+ Number(response.snapshot !== undefined);
210
+ let problem: string | undefined;
211
+ if (response.error !== undefined) {
212
+ if (response.mode !== "error" || response.changed || bodies !== 0) {
213
+ problem = "an error response carried snapshot state";
214
+ }
215
+ } else if (response.mode === "error") {
216
+ problem = "an error-mode response omitted its error";
217
+ } else if (response.changed) {
218
+ if (response.mode === "unchanged" || bodies !== 1) {
219
+ problem = "a changed response did not carry exactly one snapshot body";
220
+ }
221
+ } else if (response.mode !== "unchanged" || bodies !== 0) {
222
+ problem = "an unchanged response carried changed mode or snapshot state";
223
+ }
224
+ if (problem === undefined) return;
225
+ const error = new Error(`@ttsc/graph: native session returned ${problem}`);
226
+ if (this.child !== undefined) this.failChild(this.child, error);
227
+ throw error;
228
+ }
229
+
189
230
  private request(signal?: AbortSignal): Promise<ITtscGraphSnapshot> {
190
231
  if (signal?.aborted) throw cancelledError(signal);
191
232
  const child = this.ensureChild();
@@ -216,16 +257,19 @@ export class TtscGraphSession {
216
257
  pending.abort!();
217
258
  return;
218
259
  }
219
- child.process.stdin.write(`${JSON.stringify({ id })}\n`, (error) => {
220
- if (error === null || error === undefined) return;
221
- if (this.pending.get(id) !== pending) return;
222
- this.failChild(
223
- child,
224
- new Error(
225
- `@ttsc/graph: could not request native snapshot: ${error.message}`,
226
- ),
227
- );
228
- });
260
+ child.process.stdin.write(
261
+ `${JSON.stringify({ id, graphSnapshotVersion: GRAPH_SNAPSHOT_PROTOCOL_VERSION })}\n`,
262
+ (error) => {
263
+ if (error === null || error === undefined) return;
264
+ if (this.pending.get(id) !== pending) return;
265
+ this.failChild(
266
+ child,
267
+ new Error(
268
+ `@ttsc/graph: could not request native snapshot: ${error.message}`,
269
+ ),
270
+ );
271
+ },
272
+ );
229
273
  });
230
274
  }
231
275
 
@@ -344,8 +388,10 @@ export class TtscGraphSession {
344
388
  // looking like a type with no members. Hold the body to its own number too,
345
389
  // once the frame is understood.
346
390
  if (
347
- response.dump !== undefined &&
348
- response.dump.provenance.schemaVersion !== DUMP_SCHEMA_VERSION
391
+ (response.dump !== undefined &&
392
+ response.dump.provenance.schemaVersion !== DUMP_SCHEMA_VERSION) ||
393
+ (response.snapshot !== undefined &&
394
+ response.snapshot.schemaVersion !== DUMP_SCHEMA_VERSION)
349
395
  ) {
350
396
  // Session-wide, for the same reason the protocol mismatch above is: it is
351
397
  // the wrong binary, not one bad frame.
@@ -353,7 +399,8 @@ export class TtscGraphSession {
353
399
  child,
354
400
  new Error(
355
401
  `@ttsc/graph: ttscgraph sends dump schema v${String(
356
- response.dump.provenance.schemaVersion,
402
+ response.dump?.provenance.schemaVersion ??
403
+ response.snapshot?.schemaVersion,
357
404
  )}, this client reads v${String(DUMP_SCHEMA_VERSION)}. ` +
358
405
  "Install a matching `ttsc` (the binary resolves from the target " +
359
406
  "project, or from TTSC_GRAPH_BINARY).",
@@ -371,6 +418,7 @@ export class TtscGraphSession {
371
418
  if (this.child !== child) return;
372
419
  this.child = undefined;
373
420
  this.current = undefined;
421
+ this.shardStore = new TtscGraphShardStore();
374
422
  child.lines.close();
375
423
  this.failPending(error, child);
376
424
  if (terminate) terminateChild(child.process);
@@ -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
+ }
@@ -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
  }