ttsc 0.19.0 → 0.19.2

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 (37) hide show
  1. package/README.md +68 -186
  2. package/cmd/graphdump/main.go +21 -1
  3. package/cmd/ttsc/api_compile.go +3 -13
  4. package/cmd/ttsc/api_transform.go +13 -0
  5. package/cmd/ttscgraph/dump.go +40 -1
  6. package/cmd/ttscgraph/serve.go +194 -25
  7. package/cmd/ttscgraph/serve_error_response_still_names_its_mode_test.go +73 -0
  8. package/cmd/ttscgraph/serve_protocol_fails_the_stream_on_an_unaddressable_request_line_test.go +43 -0
  9. package/cmd/ttscgraph/serve_protocol_version_matches_the_typescript_client_test.go +48 -0
  10. package/cmd/ttscgraph/serve_snapshot_proves_its_program_without_a_second_read_test.go +109 -0
  11. package/driver/graph.go +133 -0
  12. package/internal/graph/diagnostics.go +6 -5
  13. package/internal/graph/dump.go +69 -17
  14. package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +79 -0
  15. package/internal/graph/dump_evidence_skips_leading_trivia_test.go +1 -1
  16. package/internal/graph/dump_is_a_function_of_the_source_test.go +1 -1
  17. package/internal/graph/dump_never_serializes_a_list_as_null_test.go +76 -0
  18. package/internal/graph/dump_schema_version_matches_the_typescript_client_test.go +48 -0
  19. package/internal/graph/dump_serializes_the_full_graph_test.go +21 -7
  20. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  21. package/internal/graph/provenance.go +312 -0
  22. package/lib/TtscCompiler.js +9 -4
  23. package/lib/TtscCompiler.js.map +1 -1
  24. package/lib/compiler/internal/transformProjectInMemory.d.ts +4 -0
  25. package/lib/compiler/internal/transformProjectInMemory.js +67 -12
  26. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  27. package/lib/structures/ITtscCompilerTransformation.d.ts +98 -0
  28. package/package.json +8 -8
  29. package/shim/compiler/incremental.go +58 -0
  30. package/shim/core/shim.go +5 -0
  31. package/src/TtscCompiler.ts +19 -4
  32. package/src/compiler/internal/transformProjectInMemory.ts +106 -12
  33. package/src/structures/ITtscCompilerTransformation.ts +107 -0
  34. package/tools/shim_audit/baseline.json +2 -1
  35. package/tools/shim_audit/main.go +17 -0
  36. package/utility/host.go +17 -15
  37. package/cmd/ttscgraph/serve_protocol_rejects_invalid_request_line_test.go +0 -43
@@ -0,0 +1,133 @@
1
+ package driver
2
+
3
+ import (
4
+ "path/filepath"
5
+ "sort"
6
+ "strings"
7
+
8
+ "github.com/microsoft/typescript-go/shim/ast"
9
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
10
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
11
+ )
12
+
13
+ // bundledScheme prefixes the virtual paths of the TypeScript-Go standard
14
+ // library files embedded in the binary. They are not filesystem inputs — they
15
+ // change only with the compiler itself — so the reference graph excludes them.
16
+ const bundledScheme = "bundled:///"
17
+
18
+ // TransformGraph is the host-owned reference-graph section of a transform
19
+ // envelope (`graph` in the stdout JSON). It carries the language-semantic
20
+ // input set of a transform under `tsc --incremental` semantics, so cache
21
+ // layers (bundler filesystem caches, watch graphs) can register every file
22
+ // whose content can influence a transformed module's output:
23
+ //
24
+ // - Edges maps each file to its direct resolved references — imports,
25
+ // re-exports, `/// <reference>` targets, type reference directives, and
26
+ // ambient-module declaration files, type-only edges included. Direct
27
+ // edges are the minimal sufficient statistic; consumers that need a flat
28
+ // per-file list compute the reachability closure themselves.
29
+ // - Globals lists the files that contribute to the global scope (ambient
30
+ // declaration files, script files, global augmentations, `typeRoots`
31
+ // entries). A change to any of them can affect every file.
32
+ // - Configs lists the project tsconfig followed by its `extends` ancestry.
33
+ //
34
+ // Keys and values use the same convention as the envelope's `typescript`
35
+ // map: project-relative slash paths, falling back to slash-normalized
36
+ // absolute paths outside the project root (see TransformOutputKey).
37
+ type TransformGraph struct {
38
+ Edges map[string][]string `json:"edges"`
39
+ Globals []string `json:"globals"`
40
+ Configs []string `json:"configs"`
41
+ }
42
+
43
+ // NewTransformGraph computes the reference graph of a loaded program, keyed
44
+ // relative to cwd exactly like the transform envelope's `typescript` map.
45
+ // Hosts stamp the result into their stdout envelope's `graph` field;
46
+ // `cmd/ttsc api-transform` and the linked-plugin utility host both do.
47
+ // Returns nil only for a nil or unloaded program.
48
+ func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
49
+ if prog == nil || prog.TSProgram == nil {
50
+ return nil
51
+ }
52
+ graph := &TransformGraph{
53
+ Edges: map[string][]string{},
54
+ Globals: []string{},
55
+ Configs: []string{},
56
+ }
57
+ for _, file := range prog.TSProgram.SourceFiles() {
58
+ fileName := file.FileName()
59
+ if strings.HasPrefix(fileName, bundledScheme) {
60
+ continue
61
+ }
62
+ key := TransformOutputKey(cwd, fileName)
63
+ if shimcompiler.FileAffectsGlobalScope(file) {
64
+ graph.Globals = append(graph.Globals, key)
65
+ }
66
+ targets := referenceTargets(prog, cwd, file)
67
+ if len(targets) != 0 {
68
+ graph.Edges[key] = targets
69
+ }
70
+ }
71
+ sort.Strings(graph.Globals)
72
+ graph.Configs = configChain(prog, cwd)
73
+ return graph
74
+ }
75
+
76
+ // referenceTargets resolves one file's direct reference set to sorted envelope
77
+ // keys, dropping bundled library files and the file itself.
78
+ func referenceTargets(prog *Program, cwd string, file *ast.SourceFile) []string {
79
+ paths := shimcompiler.GetReferencedFilePaths(prog.TSProgram, file)
80
+ targets := make([]string, 0, len(paths))
81
+ for _, path := range paths {
82
+ fileName := path
83
+ // Referenced paths are case-canonicalized tspath.Path values; recover the
84
+ // real spelling from the program so consumers can compare them against
85
+ // filesystem paths byte-for-byte.
86
+ if resolved := prog.TSProgram.GetSourceFileByPath(shimtspath.Path(path)); resolved != nil {
87
+ fileName = resolved.FileName()
88
+ }
89
+ if fileName == file.FileName() || strings.HasPrefix(fileName, bundledScheme) {
90
+ continue
91
+ }
92
+ targets = append(targets, TransformOutputKey(cwd, fileName))
93
+ }
94
+ sort.Strings(targets)
95
+ return targets
96
+ }
97
+
98
+ // configChain returns the project tsconfig followed by its `extends` ancestry
99
+ // as envelope keys. An inferred (config-less) program yields an empty list.
100
+ func configChain(prog *Program, cwd string) []string {
101
+ configs := []string{}
102
+ parsed := prog.ParsedConfig
103
+ if parsed == nil || parsed.ConfigFile == nil {
104
+ return configs
105
+ }
106
+ if source := parsed.ConfigFile.SourceFile; source != nil {
107
+ configs = append(configs, TransformOutputKey(cwd, source.FileName()))
108
+ }
109
+ for _, extended := range parsed.ExtendedSourceFiles() {
110
+ configs = append(configs, TransformOutputKey(cwd, extended))
111
+ }
112
+ return configs
113
+ }
114
+
115
+ // TransformOutputKey converts an absolute fileName to the key used by the
116
+ // transform and compile envelopes: a slash-separated path relative to cwd,
117
+ // falling back to the slash-normalized absolute path when the file lives
118
+ // outside the project root. Every envelope section (`typescript`, `graph`,
119
+ // `dependencies` producers) must share this one implementation so a consumer
120
+ // can join sections by key.
121
+ func TransformOutputKey(cwd, fileName string) string {
122
+ rel, err := filepath.Rel(cwd, fileName)
123
+ if err != nil || isOutsideRelativePath(rel) {
124
+ return filepath.ToSlash(fileName)
125
+ }
126
+ return filepath.ToSlash(rel)
127
+ }
128
+
129
+ // isOutsideRelativePath reports whether rel escapes the project root (starts
130
+ // with ".." or is exactly "..").
131
+ func isOutsideRelativePath(rel string) bool {
132
+ return rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator))
133
+ }
@@ -8,11 +8,12 @@ import "github.com/samchon/ttsc/packages/ttsc/driver"
8
8
  // over the warm checker, not a second compile.
9
9
  //
10
10
  // This is the compiler-only slice: it carries the TypeScript semantic
11
- // diagnostics, not @ttsc/lint or transform-plugin findings. Nothing in the
12
- // graph pipeline calls it yet; the dump and MCP results carry no diagnostics.
13
- // This helper and its tests pin the compiler-diagnostic conversion (code,
14
- // column, message) so a future diagnostics integration starts from a proven
15
- // producer.
11
+ // diagnostics, not @ttsc/lint or transform-plugin findings.
12
+ //
13
+ // This is the per-file view. The dump publishes the whole-program view through
14
+ // NewDiagnostics, which groups and relativizes the same driver.Diagnostic values
15
+ // in one pass; prefer it when every file is wanted, because filtering per file
16
+ // here rescans the program's diagnostics once per call.
16
17
  func FileDiagnostics(prog *driver.Program, path string) []driver.Diagnostic {
17
18
  out := make([]driver.Diagnostic, 0)
18
19
  for _, diagnostic := range prog.Diagnostics() {
@@ -17,7 +17,6 @@ import (
17
17
  // - internal node kinds map straight through (NodeTypeAlias is already "type");
18
18
  // - one EdgeValueCall splits by Origin into "calls" / "instantiates" /
19
19
  // "renders", and EdgeHeritage into "extends" / "implements";
20
- // - every edge is tagged checker-resolved / high (the graph's whole contract);
21
20
  // - byte spans become 1-based line/col Evidence ranges;
22
21
  // - decorator facts ride on their target node;
23
22
  // - file paths are project-relative and the output is sorted, so the dump is
@@ -84,21 +83,46 @@ type DumpEdge struct {
84
83
  Evidence *DumpEvidence `json:"evidence,omitempty"`
85
84
  }
86
85
 
87
- // Dump is the IGraphDump envelope: the project it was built for and the full
88
- // node and edge sets with none of the MCP response caps.
86
+ // Dump is the IGraphDump envelope: the project it was built for, the evidence
87
+ // about the program that produced it, and the full node and edge sets with none
88
+ // of the MCP response caps.
89
89
  type Dump struct {
90
- Project string `json:"project"`
91
- Tsconfig string `json:"tsconfig"`
92
- Nodes []DumpNode `json:"nodes"`
93
- Edges []DumpEdge `json:"edges"`
90
+ Project string `json:"project"`
91
+ Tsconfig string `json:"tsconfig"`
92
+ // Provenance proves the rest of this dump came from one Program. It rides the
93
+ // body rather than the serve envelope so a dump written to a file by the
94
+ // one-shot command keeps its evidence, and so a consumer holding only the
95
+ // parsed dump never has to ask where it came from.
96
+ Provenance Provenance `json:"provenance"`
97
+ // Diagnostics are the compiler's findings for the same program generation
98
+ // that produced Nodes and Edges. Empty means the program had none, not that
99
+ // they were not collected; the producer states that in its capabilities.
100
+ Diagnostics []Diagnostic `json:"diagnostics"`
101
+ Nodes []DumpNode `json:"nodes"`
102
+ Edges []DumpEdge `json:"edges"`
103
+ }
104
+
105
+ // DumpOrigin is the snapshot evidence a caller attaches to a dump: who built it
106
+ // and what the same program generation had to say about the code. It is a
107
+ // separate struct because only the commands that own a compiler session can
108
+ // produce it, while the graph projection below is pure.
109
+ type DumpOrigin struct {
110
+ // Provenance identifies the producing program. NewDump always stamps the
111
+ // schema version itself, so a caller cannot publish a wrong one.
112
+ Provenance Provenance
113
+
114
+ // Diagnostics are the compiler findings for the producing generation, or nil
115
+ // when the caller did not collect them.
116
+ Diagnostics []Diagnostic
94
117
  }
95
118
 
96
119
  // NewDump projects a built graph onto the export shape. project is the absolute
97
120
  // project root used to relativize file paths and node ids; ignored is the
98
121
  // git-ignored source set (nil for a non-git project); sources maps a source
99
122
  // file's path to its text so byte spans become line/col evidence (nil omits
100
- // evidence).
101
- func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string) Dump {
123
+ // evidence); origin is the snapshot evidence that proves where the facts came
124
+ // from.
125
+ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin) Dump {
102
126
  ctx := newDumpContext(project, sources)
103
127
 
104
128
  // Decorators ride on their target node; group by the internal node id before
@@ -160,18 +184,46 @@ func NewDump(g *Graph, project, tsconfig string, ignored map[string]bool, source
160
184
  return edges[i].Kind < edges[j].Kind
161
185
  })
162
186
 
187
+ // The schema version describes this code, not the caller's belief about it,
188
+ // so stamp it here rather than trusting what came in.
189
+ provenance := origin.Provenance
190
+ provenance.SchemaVersion = DumpSchemaVersion
191
+
192
+ // A nil slice encodes as JSON null, and null is not an empty list: a reader
193
+ // validating `string[]` rejects it, and a reader that does not would have to
194
+ // guess which of the two the producer meant. Every list on the wire is a list.
195
+ if provenance.Capabilities == nil {
196
+ provenance.Capabilities = []string{}
197
+ }
198
+ if provenance.Sources == nil {
199
+ provenance.Sources = []SourceDigest{}
200
+ }
201
+ if provenance.Universe.Configs == nil {
202
+ provenance.Universe.Configs = []FileDigest{}
203
+ }
204
+ if provenance.Universe.Roots == nil {
205
+ provenance.Universe.Roots = []RootFile{}
206
+ }
207
+
208
+ diagnostics := origin.Diagnostics
209
+ if diagnostics == nil {
210
+ diagnostics = []Diagnostic{}
211
+ }
212
+
163
213
  return Dump{
164
- Project: project,
165
- Tsconfig: tsconfig,
166
- Nodes: nodes,
167
- Edges: edges,
214
+ Project: project,
215
+ Tsconfig: tsconfig,
216
+ Provenance: provenance,
217
+ Diagnostics: diagnostics,
218
+ Nodes: nodes,
219
+ Edges: edges,
168
220
  }
169
221
  }
170
222
 
171
223
  // MarshalDump serializes a built graph to the export JSON, indented when pretty.
172
224
  // See NewDump for the parameters.
173
- func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, pretty bool) ([]byte, error) {
174
- d := NewDump(g, project, tsconfig, ignored, sources)
225
+ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) ([]byte, error) {
226
+ d := NewDump(g, project, tsconfig, ignored, sources, origin)
175
227
  if pretty {
176
228
  return json.MarshalIndent(d, "", " ")
177
229
  }
@@ -186,13 +238,13 @@ func MarshalDump(g *Graph, project, tsconfig string, ignored map[string]bool, so
186
238
  // string. On VS Code the document is 323 MB, so the conversion was a second full
187
239
  // copy of it held live beside the first — half a gigabyte of peak heap that
188
240
  // bought nothing, because the bytes were already exactly what stdout wanted.
189
- func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, pretty bool) error {
241
+ func EncodeDump(w io.Writer, g *Graph, project, tsconfig string, ignored map[string]bool, sources map[string]string, origin DumpOrigin, pretty bool) error {
190
242
  buffered := bufio.NewWriterSize(w, 1<<20)
191
243
  encoder := json.NewEncoder(buffered)
192
244
  if pretty {
193
245
  encoder.SetIndent("", " ")
194
246
  }
195
- if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources)); err != nil {
247
+ if err := encoder.Encode(NewDump(g, project, tsconfig, ignored, sources, origin)); err != nil {
196
248
  return err
197
249
  }
198
250
  return buffered.Flush()
@@ -0,0 +1,79 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "strings"
6
+ "testing"
7
+
8
+ "github.com/samchon/ttsc/packages/ttsc/driver"
9
+ )
10
+
11
+ // TestDumpDiagnosticsRideTheGenerationThatBuiltIt verifies the dump carries the
12
+ // compiler's findings for the same program that produced its nodes and edges,
13
+ // relativized like every other path on the wire.
14
+ //
15
+ // FileDiagnostics was written, tested, and then called by nothing: its own
16
+ // comment said "the dump and MCP results carry no diagnostics", so a consumer
17
+ // that wanted to know whether the code it was reading even compiles had to run a
18
+ // second compile to find out — against a program that, by then, was not this one.
19
+ // The value of riding along is precisely that the answer belongs to this
20
+ // snapshot; a diagnostics list assembled separately would be a different
21
+ // program's opinion.
22
+ //
23
+ // 1. Build a fixture whose only file assigns a string to a number binding.
24
+ // 2. Dump it.
25
+ // 3. Assert the TS2322 sits in the dump, at the location tsgo reports, against
26
+ // a project-relative path.
27
+ func TestDumpDiagnosticsRideTheGenerationThatBuiltIt(t *testing.T) {
28
+ root := t.TempDir()
29
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
30
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export const broken: number = "nope";
31
+ `)
32
+
33
+ prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
34
+ if err != nil {
35
+ t.Fatal(err)
36
+ }
37
+ defer func() { _ = prog.Close() }()
38
+
39
+ dump := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{
40
+ Diagnostics: NewDiagnostics(prog, root),
41
+ })
42
+
43
+ var match *Diagnostic
44
+ for i := range dump.Diagnostics {
45
+ if dump.Diagnostics[i].Code == 2322 {
46
+ match = &dump.Diagnostics[i]
47
+ break
48
+ }
49
+ }
50
+ if match == nil {
51
+ t.Fatalf("the dump carried no TS2322 for the bad assignment: %v", dump.Diagnostics)
52
+ }
53
+ // The same oracle the FileDiagnostics probe uses: tsgo attributes the
54
+ // assignability error to the declared binding, 1-based column 14.
55
+ if match.Line != 1 || match.Column != 14 {
56
+ t.Fatalf("TS2322 at line %d column %d, expected line 1 column 14", match.Line, match.Column)
57
+ }
58
+ if !strings.Contains(match.Message, "not assignable") {
59
+ t.Fatalf("TS2322 message %q does not mention 'not assignable'", match.Message)
60
+ }
61
+ if match.Category != "error" {
62
+ t.Fatalf("TS2322 category %q, want error", match.Category)
63
+ }
64
+ // Paths on the wire are project-relative; an absolute one would leak the
65
+ // producer's disk layout into a document a consumer reads elsewhere.
66
+ if match.File != "src/main.ts" {
67
+ t.Fatalf("diagnostic file %q, want the project-relative src/main.ts", match.File)
68
+ }
69
+
70
+ // The negative twin: a caller that does not collect diagnostics publishes an
71
+ // empty list, never a nil that would encode as JSON null.
72
+ quiet := NewDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{})
73
+ if quiet.Diagnostics == nil {
74
+ t.Fatal("an uncollected diagnostics list is nil, so it serializes as null rather than []")
75
+ }
76
+ if len(quiet.Diagnostics) != 0 {
77
+ t.Fatalf("an uncollected diagnostics list invented %d entries", len(quiet.Diagnostics))
78
+ }
79
+ }
@@ -46,7 +46,7 @@ export function main(): void {
46
46
  defer func() { _ = prog.Close() }()
47
47
 
48
48
  g := Build(prog)
49
- data, err := MarshalDump(g, root, "tsconfig.json", nil, SourceTexts(prog), false)
49
+ data, err := MarshalDump(g, root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{}, false)
50
50
  if err != nil {
51
51
  t.Fatalf("MarshalDump: %v", err)
52
52
  }
@@ -63,7 +63,7 @@ func dumpBytes(t *testing.T, root string) []byte {
63
63
  }
64
64
  defer func() { _ = prog.Close() }()
65
65
 
66
- data, err := MarshalDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), false)
66
+ data, err := MarshalDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{}, false)
67
67
  if err != nil {
68
68
  t.Fatalf("MarshalDump: %v", err)
69
69
  }
@@ -0,0 +1,76 @@
1
+ package graph
2
+
3
+ import (
4
+ "encoding/json"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/samchon/ttsc/packages/ttsc/driver"
10
+ )
11
+
12
+ // TestDumpNeverSerializesAListAsNull verifies every list the provenance contract
13
+ // declares reaches the wire as a list, even when the producer filled none of it.
14
+ //
15
+ // A nil Go slice encodes as `null`, and the TypeScript mirror types these as
16
+ // `string[]` / `T[]`, so a nil would fail validation at the consumer rather than
17
+ // here — and a consumer that did not validate would have to guess whether `null`
18
+ // meant "empty" or "absent". The distinction the contract actually uses for
19
+ // absence is `capabilities`, not the difference between `null` and `[]`, so the
20
+ // wire must never offer the second question.
21
+ //
22
+ // 1. Build a dump whose origin declares nothing at all.
23
+ // 2. Assert it still parses into lists, not nulls.
24
+ func TestDumpNeverSerializesAListAsNull(t *testing.T) {
25
+ root := t.TempDir()
26
+ writeFile(t, filepath.Join(root, "tsconfig.json"), fixtureTSConfig)
27
+ writeFile(t, filepath.Join(root, "src", "main.ts"), "export const value = 1;\n")
28
+
29
+ prog, _, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
30
+ if err != nil {
31
+ t.Fatal(err)
32
+ }
33
+ defer func() { _ = prog.Close() }()
34
+
35
+ // The emptiest origin a caller can pass: every list below is nil in Go.
36
+ data, err := MarshalDump(Build(prog), root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{}, false)
37
+ if err != nil {
38
+ t.Fatal(err)
39
+ }
40
+ if strings.Contains(string(data), ":null") {
41
+ t.Fatalf("dump serialized a null where a list belongs:\n%s", data)
42
+ }
43
+
44
+ var parsed struct {
45
+ Provenance struct {
46
+ Capabilities []string `json:"capabilities"`
47
+ Sources []any `json:"sources"`
48
+ Universe struct {
49
+ Configs []any `json:"configs"`
50
+ Roots []any `json:"roots"`
51
+ } `json:"universe"`
52
+ } `json:"provenance"`
53
+ Diagnostics []any `json:"diagnostics"`
54
+ }
55
+ if err := json.Unmarshal(data, &parsed); err != nil {
56
+ t.Fatal(err)
57
+ }
58
+ for name, list := range map[string]any{
59
+ "provenance.capabilities": parsed.Provenance.Capabilities,
60
+ "provenance.sources": parsed.Provenance.Sources,
61
+ "provenance.universe.configs": parsed.Provenance.Universe.Configs,
62
+ "provenance.universe.roots": parsed.Provenance.Universe.Roots,
63
+ "diagnostics": parsed.Diagnostics,
64
+ } {
65
+ switch typed := list.(type) {
66
+ case []string:
67
+ if typed == nil {
68
+ t.Fatalf("%s came back nil, so it rode the wire as null", name)
69
+ }
70
+ case []any:
71
+ if typed == nil {
72
+ t.Fatalf("%s came back nil, so it rode the wire as null", name)
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,48 @@
1
+ package graph
2
+
3
+ import (
4
+ "bytes"
5
+ "os"
6
+ "path/filepath"
7
+ "regexp"
8
+ "strconv"
9
+ "testing"
10
+ )
11
+
12
+ // TestDumpSchemaVersionMatchesTheTypescriptClient verifies the Go producer and
13
+ // the TypeScript reader agree on the dump schema version.
14
+ //
15
+ // The dump body is mirrored by hand: Dump here, ITtscGraphDump in @ttsc/graph.
16
+ // A dump also outlives the process that wrote it — the one-shot command writes
17
+ // JSON to a file that any later build may read — so the version is what tells a
18
+ // reader whether the document in front of it is the document it understands. It
19
+ // can only do that while both sides mean the same number by it.
20
+ //
21
+ // 1. Read DUMP_SCHEMA_VERSION out of the TypeScript reader's source.
22
+ // 2. Compare it to DumpSchemaVersion.
23
+ func TestDumpSchemaVersionMatchesTheTypescriptClient(t *testing.T) {
24
+ reader := filepath.Join("..", "..", "..", "graph", "src", "model", "loadGraph.ts")
25
+ source, err := os.ReadFile(reader)
26
+ if err != nil {
27
+ t.Fatalf("read the TypeScript reader: %v", err)
28
+ }
29
+ // The checkout is CRLF on Windows, and a line-anchored match would never
30
+ // see the end of a line that ends in a carriage return.
31
+ source = bytes.ReplaceAll(source, []byte("\r\n"), []byte("\n"))
32
+ match := regexp.MustCompile(`(?m)^const DUMP_SCHEMA_VERSION = (\d+);$`).FindSubmatch(source)
33
+ if match == nil {
34
+ t.Fatalf("no `const DUMP_SCHEMA_VERSION = <n>;` in %s; if it moved, this gate must follow it", reader)
35
+ }
36
+ declared, err := strconv.Atoi(string(match[1]))
37
+ if err != nil {
38
+ t.Fatalf("DUMP_SCHEMA_VERSION is not a number: %v", err)
39
+ }
40
+ if declared != DumpSchemaVersion {
41
+ t.Fatalf(
42
+ "dump schema drifted: Go produces v%d, %s reads v%d",
43
+ DumpSchemaVersion,
44
+ reader,
45
+ declared,
46
+ )
47
+ }
48
+ }
@@ -46,7 +46,7 @@ export function main(): void {
46
46
  g := Build(prog)
47
47
  sources := SourceTexts(prog)
48
48
 
49
- data, err := MarshalDump(g, root, "tsconfig.json", nil, sources, false)
49
+ data, err := MarshalDump(g, root, "tsconfig.json", nil, sources, DumpOrigin{}, false)
50
50
  if err != nil {
51
51
  t.Fatalf("MarshalDump: %v", err)
52
52
  }
@@ -138,17 +138,31 @@ export function main(): void {
138
138
  t.Fatalf("dump leaked Go field name %s:\n%s", leaked, s)
139
139
  }
140
140
  }
141
- // The dump carries no ceremony keys: it is wholly checker-resolved, so it has
142
- // no per-edge trust flags and no schema version to negotiate. It also carries
143
- // no keys the schema stopped promising: no inlined evidence text and no
144
- // diagnostics array.
145
- for _, gone := range []string{`"schemaVersion"`, `"provenance":`, `"confidence":`, `"text":`, `"diagnostics":`} {
141
+ // Every edge is checker-resolved by construction, so no record carries a
142
+ // per-edge trust flag saying so — that is the `provenance`/`confidence` pair
143
+ // 6d74a88c3 removed, and it stays removed. The dump's own `provenance` is a
144
+ // different thing at a different level: it describes the one program that
145
+ // produced every record, which no record can state about itself.
146
+ //
147
+ // No source body text rides the wire either. The manifest names its fields
148
+ // `checkerDigest` and `diskDigest` precisely so a digest can never be read as
149
+ // the text it stands for.
150
+ for _, gone := range []string{`"confidence":`, `"text":`} {
146
151
  if strings.Contains(s, gone) {
147
152
  t.Fatalf("dump still emits removed key %s:\n%s", gone, s)
148
153
  }
149
154
  }
155
+ for _, edge := range dump.Edges {
156
+ encoded, err := json.Marshal(edge)
157
+ if err != nil {
158
+ t.Fatalf("marshal edge %s->%s: %v", edge.From, edge.To, err)
159
+ }
160
+ if strings.Contains(string(encoded), `"provenance"`) {
161
+ t.Fatalf("edge regained a per-edge trust flag:\n%s", encoded)
162
+ }
163
+ }
150
164
 
151
- pretty, err := MarshalDump(g, root, "tsconfig.json", nil, sources, true)
165
+ pretty, err := MarshalDump(g, root, "tsconfig.json", nil, sources, DumpOrigin{}, true)
152
166
  if err != nil {
153
167
  t.Fatalf("MarshalDump pretty: %v", err)
154
168
  }
@@ -47,7 +47,7 @@ export const enum Mode {
47
47
  defer func() { _ = prog.Close() }()
48
48
 
49
49
  g := Build(prog)
50
- dump := NewDump(g, root, "tsconfig.json", nil, SourceTexts(prog))
50
+ dump := NewDump(g, root, "tsconfig.json", nil, SourceTexts(prog), DumpOrigin{})
51
51
 
52
52
  byID := make(map[string]DumpNode, len(dump.Nodes))
53
53
  for _, n := range dump.Nodes {