@ttsc/graph 0.19.1 → 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 (46) 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 +509 -162
  7. package/lib/model/TtscGraphSession.js.map +1 -1
  8. package/lib/model/loadGraph.d.ts +14 -0
  9. package/lib/model/loadGraph.js +382 -101
  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/ITtscGraphDump.d.ts +169 -6
  24. package/lib/structures/ITtscGraphNode.d.ts +37 -0
  25. package/lib/structures/ITtscGraphSnapshot.d.ts +55 -0
  26. package/lib/structures/ITtscGraphSnapshot.js +3 -0
  27. package/lib/structures/ITtscGraphSnapshot.js.map +1 -0
  28. package/lib/structures/ITtscGraphTour.d.ts +1 -1
  29. package/lib/structures/index.d.ts +1 -0
  30. package/lib/structures/index.js +1 -0
  31. package/lib/structures/index.js.map +1 -1
  32. package/package.json +5 -2
  33. package/src/TtscGraphApplication.ts +4 -1
  34. package/src/index.ts +1 -0
  35. package/src/model/TtscGraphSession.ts +89 -16
  36. package/src/model/loadGraph.ts +36 -0
  37. package/src/resolveGraphBinary.ts +9 -3
  38. package/src/server/resultAudit.ts +47 -8
  39. package/src/server/runDetails.ts +95 -70
  40. package/src/structures/ITtscGraphApplication.ts +8 -7
  41. package/src/structures/ITtscGraphDetails.ts +18 -8
  42. package/src/structures/ITtscGraphDump.ts +194 -6
  43. package/src/structures/ITtscGraphNode.ts +40 -0
  44. package/src/structures/ITtscGraphSnapshot.ts +69 -0
  45. package/src/structures/ITtscGraphTour.ts +1 -1
  46. package/src/structures/index.ts +1 -0
@@ -42,22 +42,24 @@ export declare namespace ITtscGraphDetails {
42
42
  */
43
43
  neighbors?: boolean;
44
44
  /**
45
- * Maximum dependencies and dependents per side when `neighbors:true`. Above
46
- * a few is usually overfetch; call `trace` for flow instead.
45
+ * Dependencies and dependents per side when `neighbors:true`. A small
46
+ * orientation slice by default; what uses a symbol grows with its
47
+ * popularity, so `trace` answers the whole "who uses this".
47
48
  *
48
49
  * @default 2
49
50
  */
50
51
  neighborLimit?: number;
51
52
  /**
52
- * Maximum owned members for a container or object literal.
53
- *
54
- * @default 6
53
+ * Owned members for a container or object literal. The complete outline by
54
+ * default — a class's members and an enum's are the symbol itself, so they
55
+ * are not sampled. Pass a number to cap.
55
56
  */
56
57
  memberLimit?: number;
57
58
  /**
58
- * Maximum direct execution and type references per group.
59
+ * Direct execution and type references per group. A small orientation slice
60
+ * by default; `trace` follows the whole fan-out.
59
61
  *
60
- * @default 1
62
+ * @default 2
61
63
  */
62
64
  dependencyLimit?: number;
63
65
  /**
@@ -115,7 +117,15 @@ export declare namespace ITtscGraphDetails {
115
117
  types?: IReference[];
116
118
  /** Concrete nodes that implement or override this interface/base member. */
117
119
  implementedBy?: IReference[];
118
- /** String literal values from the signature. */
120
+ /**
121
+ * The complete value set a type alias or enum admits, in TypeScript source
122
+ * form (`"a"`, `1`, `true`, `null`) — the checker's resolved union members,
123
+ * not the quoted tokens that happened to fit in `signature`.
124
+ *
125
+ * Absent when the type has no enumerable value set. A `signature` is capped
126
+ * at the declaration head, so for a union or enum written across several
127
+ * lines this is the field that carries the members.
128
+ */
119
129
  literals?: string[];
120
130
  /**
121
131
  * Owned symbol or top-level property outline a consumer reaches for on a
@@ -6,25 +6,188 @@ import { ITtscGraphSpan } from "./ITtscGraphSpan";
6
6
  * wire contract between the Go fact-builder and the TypeScript graph engine.
7
7
  *
8
8
  * It is the complete graph with none of the per-response caps the MCP tools
9
- * apply: every node and edge the build resolved. The server parses each changed
10
- * native snapshot (typia-validated) into an in-memory resident graph and reuses
11
- * that warm model while project inputs stay unchanged; the bundled 3D viewer
12
- * reduces the same dump.
9
+ * apply: every node and edge the build resolved, plus the `provenance` that
10
+ * says which program resolved them. The server parses each changed native
11
+ * snapshot (typia-validated) into an in-memory resident graph and reuses that
12
+ * warm model while project inputs stay unchanged; the bundled 3D viewer reduces
13
+ * the same dump.
13
14
  *
14
- * Paths in `project` and `tsconfig` are absolute; `file` fields on nodes and
15
- * edges are project-relative.
15
+ * `project` is absolute. Every other path is relative to it — `tsconfig`, and
16
+ * the `file` fields on nodes, edges, diagnostics, and the provenance manifest.
17
+ *
18
+ * Two kinds of path fall outside the project and so cannot be relative to it: a
19
+ * dependency keeps its `node_modules/`-relative tail, which is what makes a
20
+ * dependency leaf readable, and anything else the compiler loaded keeps the
21
+ * identity the compiler gave it — a virtual lib stays `bundled:///…`.
16
22
  */
17
23
  export interface ITtscGraphDump {
18
24
  /** Absolute path of the project root the graph was built for. */
19
25
  project: string;
20
26
  /** The tsconfig the program was loaded from, relative to `project`. */
21
27
  tsconfig: string;
28
+ /** Evidence about the one program that produced everything below. */
29
+ provenance: ITtscGraphDump.IProvenance;
30
+ /**
31
+ * The compiler's findings for the same generation that produced the facts.
32
+ *
33
+ * Empty means the program reported none. It does not mean they were not
34
+ * collected — `provenance.capabilities` is what says whether they were.
35
+ */
36
+ diagnostics: ITtscGraphDump.IDiagnostic[];
22
37
  /** Every node the build recorded. */
23
38
  nodes: ITtscGraphDump.INode[];
24
39
  /** Every edge the build resolved. */
25
40
  edges: ITtscGraphDump.IEdge[];
26
41
  }
27
42
  export declare namespace ITtscGraphDump {
43
+ /**
44
+ * What a snapshot knows about its own origin.
45
+ *
46
+ * The graph's claim is that its nodes, edges, spans, and diagnostics all came
47
+ * from one `Program`. Without this the claim is unprovable from the response:
48
+ * a consumer could only re-read the disk afterwards and hope nothing moved,
49
+ * which is not sound — a write that lands and reverts in between is invisible
50
+ * to it, and a re-read proves what the disk says now, never what the checker
51
+ * resolved against.
52
+ *
53
+ * This carries no source text. A digest is the opposite of inlining: it is
54
+ * what lets a consumer prove byte-identity against text it read itself,
55
+ * without the graph ever shipping that text.
56
+ */
57
+ interface IProvenance {
58
+ /**
59
+ * The dump body's schema version, moved when a field is added, removed, or
60
+ * redefined. Independent of the serve protocol's version: a dump written to
61
+ * a file has a schema but never rode the protocol.
62
+ */
63
+ schemaVersion: number;
64
+ /**
65
+ * What this snapshot proves. A consumer degrades against this rather than
66
+ * guessing from a field's emptiness, because an empty list and an
67
+ * uncollected one look identical on the wire.
68
+ *
69
+ * The known members are `universe`, `sourceDigests`, `diskDigests`, and
70
+ * `diagnostics`. The type stays `string[]` rather than a union of those on
71
+ * purpose: a union would make `typia.assert` reject a newer producer for
72
+ * naming a capability this client has not heard of, turning "proves more
73
+ * than you know about" into a hard failure. An unknown capability is
74
+ * exactly the case a consumer should ignore.
75
+ */
76
+ capabilities: string[];
77
+ /** What built the snapshot. */
78
+ producer: IProducer;
79
+ /** The inputs that decide which files are in the program at all. */
80
+ universe: IUniverse;
81
+ /** One entry per file the program loaded, ordered by file. */
82
+ sources: ISourceDigest[];
83
+ }
84
+ /**
85
+ * Identifies the binary and the checker behind the facts.
86
+ *
87
+ * `tool` and `version` are separate because more than one binary can produce
88
+ * a dump and they do not share a version line — the shipped `ttscgraph` is
89
+ * stamped at release, the internal viewer tool is not versioned at all — so
90
+ * folding the name in would hand a consumer that parses a version a tool
91
+ * name.
92
+ */
93
+ interface IProducer {
94
+ /** The producing binary's name, such as `ttscgraph`. */
95
+ tool: string;
96
+ /**
97
+ * The producing binary's build version, as its `--version` prints it. A
98
+ * local build reports the dev placeholder; a tool that carries no version
99
+ * reports `""`.
100
+ */
101
+ version: string;
102
+ /** The TypeScript version typescript-go implements. */
103
+ typescript: string;
104
+ }
105
+ /**
106
+ * The build universe: the inputs that decide which files the program
107
+ * contains, as opposed to what is inside them. A change to any of them can
108
+ * add or drop whole files, so a consumer reusing facts across snapshots must
109
+ * treat a universe change as invalidating everything.
110
+ */
111
+ interface IUniverse {
112
+ /**
113
+ * The tsconfig chain — the project's config and everything it extends.
114
+ *
115
+ * It stays an input regardless of what any source contains: compiler
116
+ * options change the meaning of code the checker resolves without any
117
+ * source file changing.
118
+ */
119
+ configs: IFileDigest[];
120
+ /**
121
+ * The resolved root file set, one entry per (config, file) pair. A root a
122
+ * config names but that does not exist is still listed: its absence is part
123
+ * of the fingerprint, and creating it later changes the program.
124
+ */
125
+ roots: IRootFile[];
126
+ }
127
+ /** A root file attributed to the config that named it. */
128
+ interface IRootFile {
129
+ /** The tsconfig that named this root, project-relative. */
130
+ config: string;
131
+ /** The root file, project-relative. */
132
+ file: string;
133
+ }
134
+ /** A file and the hex-encoded SHA-256 of its on-disk bytes. */
135
+ interface IFileDigest {
136
+ /** Project-relative. */
137
+ file: string;
138
+ /** Hex-encoded SHA-256. */
139
+ digest: string;
140
+ }
141
+ /**
142
+ * The manifest entry for one source file the program loaded.
143
+ *
144
+ * Two digests, because "the bytes the checker read" and "the bytes on disk"
145
+ * are not always the same string and a consumer needs to know which one it
146
+ * compares against. They diverge when a source-preamble plugin injects text
147
+ * ahead of the file before tsgo parses it, which a real plugin project does
148
+ * on every build.
149
+ */
150
+ interface ISourceDigest {
151
+ /** Project-relative. */
152
+ file: string;
153
+ /**
154
+ * Hex-encoded SHA-256 of the text the checker resolved against — the ground
155
+ * truth for the facts. Every node, edge, and span attributed to this file
156
+ * was computed from these bytes.
157
+ */
158
+ checkerDigest: string;
159
+ /**
160
+ * Hex-encoded SHA-256 of the file's on-disk bytes at snapshot time, or `""`
161
+ * when it could not be read: it vanished mid-load, or it is a virtual
162
+ * source with no on-disk identity.
163
+ *
164
+ * This is the one a consumer that opens the file itself can reproduce. When
165
+ * it equals `checkerDigest`, a matching read proves byte-identity with the
166
+ * facts. When it does not, the checker saw augmented text and that proof is
167
+ * simply not available for this file — which is a thing to report, not to
168
+ * paper over.
169
+ *
170
+ * Read it only when `provenance.capabilities` lists `diskDigests`. Without
171
+ * that claim every one of these is empty because the producer never hashed
172
+ * the disk, which is a different fact from a file that could not be read.
173
+ */
174
+ diskDigest: string;
175
+ }
176
+ /** One compiler diagnostic from the generation that produced the facts. */
177
+ interface IDiagnostic {
178
+ /** Project-relative. */
179
+ file: string;
180
+ /** 1-based line. */
181
+ line: number;
182
+ /** 1-based column. */
183
+ column: number;
184
+ /** The TypeScript diagnostic code, such as 2322. */
185
+ code: number;
186
+ /** Whether the finding fails a build. */
187
+ category: "error" | "warning";
188
+ /** The diagnostic text, without the code prefix. */
189
+ message: string;
190
+ }
28
191
  /**
29
192
  * A node as the builder sends it: the graph node, minus the file paths inside
30
193
  * its spans, which the loader puts back from the node's own `file`.
@@ -52,6 +52,30 @@ export interface ITtscGraphNode {
52
52
  closure?: boolean;
53
53
  /** Declaration modifiers, when the declaration pass recorded any. */
54
54
  modifiers?: TtscGraphNodeModifier[];
55
+ /**
56
+ * The complete value set of a type alias or enum whose declared type the
57
+ * checker resolved to literals, each in TypeScript source form (`"a"`, `1`,
58
+ * `true`, `null`).
59
+ *
60
+ * Present only when every constituent is enumerable, so the list is the whole
61
+ * type and never a sample of it: `type T = Kind | string` admits values no
62
+ * list can name and carries none. It is resolved from the type, not read off
63
+ * the declaration, so indirection (`type I = Kind | 'f'`) is followed and the
64
+ * answer does not depend on how the declaration is wrapped.
65
+ */
66
+ literals?: string[];
67
+ /**
68
+ * What an enum declares, in checker order: the name a caller writes and the
69
+ * value it carries. Absent on every other kind.
70
+ *
71
+ * `literals` says which values the enum admits, which is what a serializer
72
+ * asks. The code says `Colors.Red`, so the names are the other half, and
73
+ * without them a caller that had already named the enum still had to open the
74
+ * file to learn what to type. The members are not nodes — `Colors.Red` is a
75
+ * string a grep finds exactly — so this fills in the node the graph already
76
+ * holds instead of minting one per member.
77
+ */
78
+ enumMembers?: ITtscGraphNode.IEnumMember[];
55
79
  /**
56
80
  * Decorators written on this declaration, in source order: raw facts
57
81
  * (`@Controller`, `@Get`) a consumer interprets without re-parsing source.
@@ -65,3 +89,16 @@ export interface ITtscGraphNode {
65
89
  */
66
90
  implementation?: ITtscGraphEvidence;
67
91
  }
92
+ export declare namespace ITtscGraphNode {
93
+ /** One member of an enum: the name a caller writes and the value it carries. */
94
+ interface IEnumMember {
95
+ /** The member's own name, unqualified (`Red` on `Colors.Red`). */
96
+ name: string;
97
+ /**
98
+ * The value it carries, in TypeScript source form (`"red"`, `1`). Absent
99
+ * for a computed member the checker could not fold to a constant; the name
100
+ * still stands.
101
+ */
102
+ value?: string;
103
+ }
104
+ }
@@ -0,0 +1,55 @@
1
+ import { ITtscGraphDump } from "./ITtscGraphDump";
2
+ /**
3
+ * One response frame of the `ttscgraph serve` protocol.
4
+ *
5
+ * This is the envelope around a snapshot, mirrored by hand from `serveResponse`
6
+ * in `packages/ttsc/cmd/ttscgraph/serve.go`. There is no generator between the
7
+ * Go struct and this interface, so the two drift silently unless something
8
+ * checks them; `TtscGraphSession` validates every frame against this shape
9
+ * rather than casting it, so a drift surfaces as a precise error on the first
10
+ * frame instead of an `undefined` several layers downstream.
11
+ */
12
+ export interface ITtscGraphSnapshot {
13
+ /** Echoes the request's id, so a response finds its caller. */
14
+ id: number;
15
+ /**
16
+ * The protocol version the server speaks.
17
+ *
18
+ * It rides every frame rather than a handshake. The binary and this package
19
+ * version independently — the session runs whichever `ttscgraph` the target
20
+ * project installed, or whatever `TTSC_GRAPH_BINARY` points at — so a
21
+ * mismatched pair is reachable, and before this field nothing detected it:
22
+ * the first symptom was a misparsed dump or a silently absent value.
23
+ */
24
+ protocolVersion: number;
25
+ /**
26
+ * What the producer did to answer this request.
27
+ *
28
+ * Required, and never absent — including on the error path, where it is
29
+ * `"error"`. A consumer can report `rebuild` versus `incremental` honestly
30
+ * because the compiler said so; no generation counter can distinguish a reuse
31
+ * from a full rebuild after the fact.
32
+ */
33
+ mode: ITtscGraphSnapshot.Mode;
34
+ /** What this server can prove about the snapshots it publishes. */
35
+ capabilities: string[];
36
+ /** Whether the graph moved since the last snapshot. */
37
+ changed: boolean;
38
+ /** The snapshot, present exactly when `changed` is true. */
39
+ dump?: ITtscGraphDump;
40
+ /** Set when the request produced no snapshot; `mode` is then `"error"`. */
41
+ error?: string;
42
+ }
43
+ export declare namespace ITtscGraphSnapshot {
44
+ /**
45
+ * The computation modes the producer reports, plus the transport's `error`.
46
+ *
47
+ * - `initial`: the session's first snapshot.
48
+ * - `reload`: the build universe moved, so the program was reloaded whole.
49
+ * - `unchanged`: nothing moved; no dump rides it and the last one still holds.
50
+ * - `incremental`: edits applied onto the reused resident program.
51
+ * - `rebuild`: edits applied, but the program could not be reused.
52
+ * - `error`: no snapshot was produced.
53
+ */
54
+ type Mode = "initial" | "reload" | "unchanged" | "incremental" | "rebuild" | "error";
55
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=ITtscGraphSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ITtscGraphSnapshot.js","sourceRoot":"","sources":["../../src/structures/ITtscGraphSnapshot.ts"],"names":[],"mappings":""}
@@ -56,7 +56,7 @@ export declare namespace ITtscGraphTour {
56
56
  * Central entrypoints to seed the tour. Raise only when the question names
57
57
  * several public paths that must all appear in one answer.
58
58
  *
59
- * @default 4
59
+ * @default 5
60
60
  */
61
61
  limit?: number;
62
62
  /**
@@ -11,6 +11,7 @@ export * from "./ITtscGraphOverview";
11
11
  export * from "./ITtscGraphLookup";
12
12
  export * from "./ITtscGraphNext";
13
13
  export * from "./ITtscGraphTrace";
14
+ export * from "./ITtscGraphSnapshot";
14
15
  export * from "./ITtscGraphSpan";
15
16
  export * from "./ITtscGraphTour";
16
17
  export * from "./TtscGraphEdgeKind";
@@ -32,6 +32,7 @@ __exportStar(require("./ITtscGraphOverview"), exports);
32
32
  __exportStar(require("./ITtscGraphLookup"), exports);
33
33
  __exportStar(require("./ITtscGraphNext"), exports);
34
34
  __exportStar(require("./ITtscGraphTrace"), exports);
35
+ __exportStar(require("./ITtscGraphSnapshot"), exports);
35
36
  __exportStar(require("./ITtscGraphSpan"), exports);
36
37
  __exportStar(require("./ITtscGraphTour"), exports);
37
38
  __exportStar(require("./TtscGraphEdgeKind"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/structures/index.ts"],"names":[],"mappings":";AAAA,uEAAuE;AACvE,0EAA0E;AAC1E,8EAA8E;AAC9E,gFAAgF;AAChF,6BAA6B;;;;;;;;;;;;;;;;AAE7B,0DAAwC;AACxC,wDAAsC;AACtC,mDAAiC;AACjC,mDAAiC;AACjC,uDAAqC;AACrC,qDAAmC;AACnC,sDAAoC;AACpC,0DAAwC;AACxC,mDAAiC;AACjC,uDAAqC;AACrC,qDAAmC;AACnC,mDAAiC;AACjC,oDAAkC;AAClC,mDAAiC;AACjC,mDAAiC;AACjC,sDAAoC;AACpC,sDAAoC;AACpC,0DAAwC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/structures/index.ts"],"names":[],"mappings":";AAAA,uEAAuE;AACvE,0EAA0E;AAC1E,8EAA8E;AAC9E,gFAAgF;AAChF,6BAA6B;;;;;;;;;;;;;;;;AAE7B,0DAAwC;AACxC,wDAAsC;AACtC,mDAAiC;AACjC,mDAAiC;AACjC,uDAAqC;AACrC,qDAAmC;AACnC,sDAAoC;AACpC,0DAAwC;AACxC,mDAAiC;AACjC,uDAAqC;AACrC,qDAAmC;AACnC,mDAAiC;AACjC,oDAAkC;AAClC,uDAAqC;AACrC,mDAAiC;AACjC,mDAAiC;AACjC,sDAAoC;AACpC,sDAAoC;AACpC,0DAAwC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/graph",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
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",
@@ -19,6 +19,9 @@
19
19
  "@typia/mcp": "13.1.0",
20
20
  "typia": "13.1.0"
21
21
  },
22
+ "peerDependencies": {
23
+ "ttsc": "0.19.3"
24
+ },
22
25
  "keywords": [
23
26
  "ttsc",
24
27
  "mcp",
@@ -39,7 +42,7 @@
39
42
  "three": "^0.184.0",
40
43
  "three-forcegraph": "^1.43.4",
41
44
  "typescript": "^7.0.2",
42
- "ttsc": "0.19.1"
45
+ "ttsc": "0.19.3"
43
46
  },
44
47
  "repository": {
45
48
  "type": "git",
@@ -1,6 +1,7 @@
1
1
  import { TtscGraphMemory } from "./model/TtscGraphMemory";
2
2
  import {
3
3
  RESULT_AUDIT,
4
+ RESULT_AUDIT_DETAILS,
4
5
  RESULT_AUDIT_ESCAPE,
5
6
  RESULT_AUDIT_SELECTION,
6
7
  } from "./server/resultAudit";
@@ -90,9 +91,11 @@ export class TtscGraphApplication implements ITtscGraphApplication {
90
91
  };
91
92
  }
92
93
  case "details": {
94
+ // details' identity is complete and its fan-out is a slice, which is
95
+ // not the walk-bounded-and-marked shape RESULT_AUDIT states.
93
96
  const r = runDetails(graph, props.request);
94
97
  return {
95
- audit: RESULT_AUDIT,
98
+ audit: RESULT_AUDIT_DETAILS,
96
99
  next: r.next,
97
100
  result: r.result,
98
101
  };
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { runView } from "./view";
9
9
  /** What a graph result says about where its facts came from. */
10
10
  export {
11
11
  RESULT_AUDIT,
12
+ RESULT_AUDIT_DETAILS,
12
13
  RESULT_AUDIT_ESCAPE,
13
14
  RESULT_AUDIT_SELECTION,
14
15
  } from "./server/resultAudit";
@@ -4,19 +4,22 @@ import typia from "typia";
4
4
 
5
5
  import { ensureExecutable } from "../nativeExecutable";
6
6
  import { resolveGraphBinary } from "../resolveGraphBinary";
7
- import { ITtscGraphDump } from "../structures/ITtscGraphDump";
7
+ import { ITtscGraphSnapshot } from "../structures/ITtscGraphSnapshot";
8
+ import { DUMP_SCHEMA_VERSION } from "./loadGraph";
8
9
  import { TtscGraphMemory } from "./TtscGraphMemory";
9
10
 
10
- interface SessionResponse {
11
- id: number;
12
- changed: boolean;
13
- mode?: "initial" | "unchanged" | "incremental" | "rebuild" | "reload";
14
- dump?: unknown;
15
- error?: string;
16
- }
11
+ /**
12
+ * The serve protocol version this client speaks.
13
+ *
14
+ * Keep it equal to `serveProtocolVersion` in
15
+ * `packages/ttsc/cmd/ttscgraph/serve.go`. The two are hand-synchronized, and
16
+ * `serve_protocol_version_matches_the_typescript_client_test.go` reads this
17
+ * constant out of this file and fails if the pair drifts.
18
+ */
19
+ const PROTOCOL_VERSION = 1;
17
20
 
18
21
  interface Pending {
19
- resolve: (response: SessionResponse) => void;
22
+ resolve: (response: ITtscGraphSnapshot) => void;
20
23
  reject: (error: Error) => void;
21
24
  }
22
25
 
@@ -93,6 +96,8 @@ export class TtscGraphSession {
93
96
  }
94
97
 
95
98
  private async refresh(): Promise<TtscGraphMemory> {
99
+ // The protocol version and the envelope shape were both settled in onLine,
100
+ // before this frame was ever routed here.
96
101
  const response = await this.request();
97
102
  if (response.error !== undefined) {
98
103
  throw new Error(`@ttsc/graph: ${response.error}`);
@@ -100,11 +105,10 @@ export class TtscGraphSession {
100
105
  if (response.changed) {
101
106
  if (response.dump === undefined) {
102
107
  throw new Error(
103
- `@ttsc/graph: native ${response.mode ?? "changed"} response omitted its dump`,
108
+ `@ttsc/graph: native ${response.mode} response omitted its dump`,
104
109
  );
105
110
  }
106
- const dump = typia.assert<ITtscGraphDump>(response.dump);
107
- this.current = TtscGraphMemory.from(dump);
111
+ this.current = TtscGraphMemory.from(response.dump);
108
112
  }
109
113
  if (this.current === undefined) {
110
114
  throw new Error(
@@ -114,10 +118,10 @@ export class TtscGraphSession {
114
118
  return this.current;
115
119
  }
116
120
 
117
- private request(): Promise<SessionResponse> {
121
+ private request(): Promise<ITtscGraphSnapshot> {
118
122
  const child = this.ensureChild();
119
123
  const id = ++this.nextId;
120
- return new Promise<SessionResponse>((resolve, reject) => {
124
+ return new Promise<ITtscGraphSnapshot>((resolve, reject) => {
121
125
  this.pending.set(id, { resolve, reject });
122
126
  child.stdin.write(`${JSON.stringify({ id })}\n`, (error) => {
123
127
  if (error === null || error === undefined) return;
@@ -169,9 +173,9 @@ export class TtscGraphSession {
169
173
  }
170
174
 
171
175
  private onLine(line: string): void {
172
- let response: SessionResponse;
176
+ let parsed: unknown;
173
177
  try {
174
- response = JSON.parse(line) as SessionResponse;
178
+ parsed = JSON.parse(line);
175
179
  } catch (error) {
176
180
  this.failPending(
177
181
  new Error(
@@ -180,6 +184,75 @@ export class TtscGraphSession {
180
184
  );
181
185
  return;
182
186
  }
187
+
188
+ // Read the version before the shape, because a server speaking another
189
+ // version is entitled to a different shape. Asserting first would report
190
+ // that mismatch as a field complaint — "expected string at $input.mode" —
191
+ // about a contract the other side never agreed to, which is the misparse
192
+ // this field exists to prevent. Ask what protocol it is first, then hold it
193
+ // to that protocol.
194
+ const version: number | undefined = typia.is<{ protocolVersion: number }>(
195
+ parsed,
196
+ )
197
+ ? parsed.protocolVersion
198
+ : undefined;
199
+ if (version !== PROTOCOL_VERSION) {
200
+ // Session-wide: a version mismatch is not one bad frame, it is the wrong
201
+ // binary, and every request against it is equally doomed.
202
+ this.failPending(
203
+ new Error(
204
+ `@ttsc/graph: ttscgraph speaks serve protocol ${
205
+ version === undefined ? "an unknown version" : `v${String(version)}`
206
+ }, this client speaks v${String(PROTOCOL_VERSION)}. ` +
207
+ "Install a matching `ttsc` (the binary resolves from the target " +
208
+ "project, or from TTSC_GRAPH_BINARY).",
209
+ ),
210
+ );
211
+ return;
212
+ }
213
+
214
+ let response: ITtscGraphSnapshot;
215
+ try {
216
+ // Validate the envelope, not just the dump it carries. The dump was
217
+ // typia-asserted while the envelope around it was a bare cast, so the
218
+ // fields the client actually branches on — the mode, and the id that
219
+ // routes the frame — were the unchecked ones. Anything added to the
220
+ // envelope belongs on this side of that line.
221
+ response = typia.assert<ITtscGraphSnapshot>(parsed);
222
+ } catch (error) {
223
+ this.failPending(
224
+ new Error(
225
+ `@ttsc/graph: native session returned an unreadable response: ${asError(error).message}`,
226
+ ),
227
+ );
228
+ return;
229
+ }
230
+ // The envelope's version is not the body's, and only the envelope has been
231
+ // held to one so far. A producer can speak this protocol and still carry a
232
+ // dump from another schema — the two move apart the moment a node field is
233
+ // added without the frame around it changing — and then the facts that field
234
+ // holds are silently absent rather than refused. `literals` is exactly that
235
+ // shape: an older producer resolves no value set, so a union comes back
236
+ // looking like a type with no members. Hold the body to its own number too,
237
+ // once the frame is understood.
238
+ if (
239
+ response.dump !== undefined &&
240
+ response.dump.provenance.schemaVersion !== DUMP_SCHEMA_VERSION
241
+ ) {
242
+ // Session-wide, for the same reason the protocol mismatch above is: it is
243
+ // the wrong binary, not one bad frame.
244
+ this.failPending(
245
+ new Error(
246
+ `@ttsc/graph: ttscgraph sends dump schema v${String(
247
+ response.dump.provenance.schemaVersion,
248
+ )}, this client reads v${String(DUMP_SCHEMA_VERSION)}. ` +
249
+ "Install a matching `ttsc` (the binary resolves from the target " +
250
+ "project, or from TTSC_GRAPH_BINARY).",
251
+ ),
252
+ );
253
+ return;
254
+ }
255
+
183
256
  const pending = this.pending.get(response.id);
184
257
  if (pending === undefined) return;
185
258
  this.pending.delete(response.id);
@@ -11,6 +11,21 @@ import { TtscGraphMemory } from "./TtscGraphMemory";
11
11
  // buffer is raised to a ceiling no real graph reaches.
12
12
  const MAX_DUMP_BYTES = 1024 * 1024 * 1024;
13
13
 
14
+ /**
15
+ * The dump schema version this client reads.
16
+ *
17
+ * Keep it equal to `DumpSchemaVersion` in
18
+ * `packages/ttsc/internal/graph/provenance.go`. The two are hand-synchronized,
19
+ * and `dump_schema_version_matches_the_typescript_client_test.go` reads this
20
+ * constant out of this file and fails if the pair drifts.
21
+ *
22
+ * Exported because the resident session reads the same dump body over the serve
23
+ * protocol and has to hold it to the same number: the envelope's version and the
24
+ * body's are independent, so a producer can speak this protocol and still send a
25
+ * body from another schema.
26
+ */
27
+ export const DUMP_SCHEMA_VERSION = 3;
28
+
14
29
  /**
15
30
  * Build the resident {@link TtscGraphMemory} for a project by running `ttscgraph
16
31
  * dump` once and loading its JSON. This is the one-shot path for direct callers
@@ -73,6 +88,13 @@ export function loadGraph(
73
88
  * {@link ITtscGraphDump} shape so a malformed or stale dump fails loudly here
74
89
  * rather than producing wrong answers downstream, and the schema version is
75
90
  * checked so an incompatible producer is refused.
91
+ *
92
+ * The version is read before the shape: a dump from another schema is entitled
93
+ * to another shape, so asserting first would report the mismatch as a field
94
+ * complaint about a contract that producer never agreed to. A dump also
95
+ * outlives the process that wrote it — this is the one-shot path, and the JSON
96
+ * on disk may be from any build — so the version is the first question to ask
97
+ * of it.
76
98
  */
77
99
  function parseDump(json: string): ITtscGraphDump {
78
100
  let value: unknown;
@@ -85,5 +107,19 @@ function parseDump(json: string): ITtscGraphDump {
85
107
  }`,
86
108
  );
87
109
  }
110
+ const version: number | undefined = typia.is<{
111
+ provenance: { schemaVersion: number };
112
+ }>(value)
113
+ ? value.provenance.schemaVersion
114
+ : undefined;
115
+ if (version !== DUMP_SCHEMA_VERSION) {
116
+ throw new Error(
117
+ `@ttsc/graph: ttscgraph dump is schema ${
118
+ version === undefined ? "unknown" : `v${String(version)}`
119
+ }, this client reads v${String(DUMP_SCHEMA_VERSION)}. ` +
120
+ "Install a matching `ttsc` (the binary resolves from the target " +
121
+ "project, or from TTSC_GRAPH_BINARY).",
122
+ );
123
+ }
88
124
  return typia.assert<ITtscGraphDump>(value);
89
125
  }