ttsc 0.27.0 → 0.28.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 (73) hide show
  1. package/cmd/graphbench/main.go +26 -16
  2. package/cmd/graphdump/main.go +37 -12
  3. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  4. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  5. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  6. package/cmd/ttsc/api_transform.go +17 -6
  7. package/cmd/ttscgraph/dump.go +43 -11
  8. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  9. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  10. package/cmd/ttscgraph/serve.go +253 -25
  11. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  12. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  13. package/cmd/ttscgraph/serve_shards.go +31 -9
  14. package/driver/config_discovery.go +121 -0
  15. package/driver/emit_plugin.go +57 -8
  16. package/driver/plugins.go +125 -14
  17. package/driver/transform_dependencies.go +283 -0
  18. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  19. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  20. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  21. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  22. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  23. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  24. package/internal/graph/artifacts.go +230 -0
  25. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  26. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  27. package/internal/graph/build.go +35 -0
  28. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  29. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  30. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  31. package/internal/graph/doc_tags.go +416 -0
  32. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  33. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  34. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  35. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  36. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  37. package/internal/graph/dump.go +60 -13
  38. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  39. package/internal/graph/dump_paths.go +2 -2
  40. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  41. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  42. package/internal/graph/edges.go +1 -0
  43. package/internal/graph/graph.go +79 -4
  44. package/internal/graph/provenance.go +29 -2
  45. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  46. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  47. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  48. package/lib/index.d.ts +1 -0
  49. package/lib/index.js +1 -0
  50. package/lib/index.js.map +1 -1
  51. package/lib/launcher/internal/runtimeHooks.d.ts +10 -2
  52. package/lib/launcher/internal/runtimeHooks.js +93 -2
  53. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  54. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  55. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  56. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  57. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  58. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  59. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  60. package/lib/structures/ITtscCompilerTransformation.d.ts +7 -0
  61. package/package.json +8 -8
  62. package/shim/ast/parent.go +16 -3
  63. package/shim/checker/enums_gen.go +11 -0
  64. package/shim/checker/shim.go +3 -0
  65. package/shim/compiler/emithook.go +10 -0
  66. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  67. package/src/index.ts +1 -0
  68. package/src/launcher/internal/runtimeHooks.ts +111 -2
  69. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  70. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  71. package/src/structures/ITtscCompilerTransformation.ts +7 -0
  72. package/tools/shim_audit/baseline.json +0 -1
  73. package/utility/host.go +15 -6
@@ -29,10 +29,16 @@ type metrics struct {
29
29
  Nodes int `json:"nodes"`
30
30
  ExternalNodes int `json:"externalNodes"`
31
31
  Edges map[string]int `json:"edges"`
32
- TotalEdges int `json:"totalEdges"`
33
- SymbolFiles int `json:"symbolFiles"`
34
- CoveredFiles int `json:"coveredFiles"`
35
- Coverage float64 `json:"coverage"`
32
+ // EdgeVocabulary names which of the three edge-kind vocabularies Edges is
33
+ // keyed by, so a reader comparing these numbers against a dump does not
34
+ // compare the wrong things. Build produces *internal* kinds; a dump carries
35
+ // finer *wire* kinds (value-call splits into calls/instantiates/renders);
36
+ // the viewer folds wire kinds onto *display* families.
37
+ EdgeVocabulary string `json:"edgeVocabulary"`
38
+ TotalEdges int `json:"totalEdges"`
39
+ SymbolFiles int `json:"symbolFiles"`
40
+ CoveredFiles int `json:"coveredFiles"`
41
+ Coverage float64 `json:"coverage"`
36
42
  }
37
43
 
38
44
  func main() {
@@ -82,10 +88,13 @@ func run() int {
82
88
  // counts, edges per kind, and fair coverage (workspace files holding at least one
83
89
  // node that an edge connects to a node in another file).
84
90
  func summarize(g *graph.Graph, sourceFiles int) metrics {
85
- edges := map[string]int{
86
- string(graph.EdgeHeritage): 0,
87
- string(graph.EdgeValueCall): 0,
88
- string(graph.EdgeTypeRef): 0,
91
+ // Seeded with every kind Build can produce, so a family that happened not to
92
+ // occur reports 0 rather than vanishing from the object. Three of the seven
93
+ // used to be seeded and the map grew the rest on first sight, which meant the
94
+ // reported shape depended on the project measured.
95
+ edges := map[string]int{}
96
+ for _, kind := range graph.EdgeKinds() {
97
+ edges[string(kind)] = 0
89
98
  }
90
99
  for _, edge := range g.Edges {
91
100
  edges[string(edge.Kind)]++
@@ -123,13 +132,14 @@ func summarize(g *graph.Graph, sourceFiles int) metrics {
123
132
  }
124
133
 
125
134
  return metrics{
126
- SourceFiles: sourceFiles,
127
- Nodes: len(g.Nodes),
128
- ExternalNodes: external,
129
- Edges: edges,
130
- TotalEdges: len(g.Edges),
131
- SymbolFiles: len(symbolFiles),
132
- CoveredFiles: len(coveredFiles),
133
- Coverage: coverage,
135
+ SourceFiles: sourceFiles,
136
+ Nodes: len(g.Nodes),
137
+ ExternalNodes: external,
138
+ Edges: edges,
139
+ EdgeVocabulary: "internal",
140
+ TotalEdges: len(g.Edges),
141
+ SymbolFiles: len(symbolFiles),
142
+ CoveredFiles: len(coveredFiles),
143
+ Coverage: coverage,
134
144
  }
135
145
  }
@@ -6,8 +6,10 @@
6
6
  package main
7
7
 
8
8
  import (
9
+ "errors"
9
10
  "flag"
10
11
  "fmt"
12
+ "io"
11
13
  "os"
12
14
  "path/filepath"
13
15
 
@@ -17,18 +19,37 @@ import (
17
19
  "github.com/samchon/ttsc/packages/ttsc/internal/graph"
18
20
  )
19
21
 
22
+ // Package-level streams so command tests can capture I/O without patching the
23
+ // os globals, and argv arrives as a parameter for the same reason. Both mirror
24
+ // the shipped sibling in cmd/ttscgraph, whose capability claim is tested the
25
+ // same way; this command's claim went undefended because it had neither seam.
26
+ var (
27
+ stdout io.Writer = os.Stdout
28
+ stderr io.Writer = os.Stderr
29
+ )
30
+
20
31
  func main() {
21
- os.Exit(run())
32
+ os.Exit(run(os.Args[1:]))
22
33
  }
23
34
 
24
- func run() int {
25
- cwd := flag.String("cwd", ".", "project root")
26
- tsconfig := flag.String("tsconfig", "tsconfig.json", "tsconfig path, relative to cwd")
27
- pretty := flag.Bool("pretty", false, "indent the JSON output")
28
- flag.Parse()
35
+ func run(args []string) int {
36
+ fs := flag.NewFlagSet("graphdump", flag.ContinueOnError)
37
+ fs.SetOutput(stderr)
38
+ cwd := fs.String("cwd", ".", "project root")
39
+ tsconfig := fs.String("tsconfig", "tsconfig.json", "tsconfig path, relative to cwd")
40
+ pretty := fs.Bool("pretty", false, "indent the JSON output")
41
+ if err := fs.Parse(args); err != nil {
42
+ // `-h` asks for the usage this just printed, so it is a request that
43
+ // succeeded rather than an argument that failed. Under the global flag set
44
+ // this command used to read, `ExitOnError` already exited 0 for it.
45
+ if errors.Is(err, flag.ErrHelp) {
46
+ return 0
47
+ }
48
+ return 2
49
+ }
29
50
 
30
51
  // Resolve the project root the same way LoadProgram does (absolute, then
31
- // tsgo-normalized) so the schema-v6 mapper receives the same canonical root
52
+ // tsgo-normalized) so the dump path mapper receives the same canonical root
32
53
  // grammar and drive-letter case as the compiler's source paths.
33
54
  root := *cwd
34
55
  if abs, err := filepath.Abs(root); err == nil {
@@ -38,11 +59,11 @@ func run() int {
38
59
 
39
60
  prog, _, err := driver.LoadProgram(root, *tsconfig, driver.LoadProgramOptions{})
40
61
  if err != nil {
41
- fmt.Fprintf(os.Stderr, "graphdump: could not load %s/%s: %v\n", root, *tsconfig, err)
62
+ fmt.Fprintf(stderr, "graphdump: could not load %s/%s: %v\n", root, *tsconfig, err)
42
63
  return 1
43
64
  }
44
65
  if prog == nil {
45
- fmt.Fprintf(os.Stderr, "graphdump: could not load %s/%s\n", root, *tsconfig)
66
+ fmt.Fprintf(stderr, "graphdump: could not load %s/%s\n", root, *tsconfig)
46
67
  return 1
47
68
  }
48
69
  defer func() { _ = prog.Close() }()
@@ -62,7 +83,11 @@ func run() int {
62
83
  // No version: this tool is built from the tree on demand and never
63
84
  // stamped, and an invented one would be worse than an absent one.
64
85
  graph.Producer{Tool: "graphdump", Typescript: graph.TypescriptVersion()},
65
- []string{graph.CapabilitySourceDigests},
86
+ // The declaration walk is the shipped one, so this dump carries every
87
+ // documentation tag and must say so: under the capability contract an
88
+ // absent claim means the producer never looked, which would make a
89
+ // consumer read a tagged declaration as citing nothing.
90
+ []string{graph.CapabilitySourceDigests, graph.CapabilityDocTags},
66
91
  nil,
67
92
  nil,
68
93
  texts,
@@ -70,9 +95,9 @@ func run() int {
70
95
  ),
71
96
  }, *pretty)
72
97
  if err != nil {
73
- fmt.Fprintf(os.Stderr, "graphdump: %v\n", err)
98
+ fmt.Fprintf(stderr, "graphdump: %v\n", err)
74
99
  return 1
75
100
  }
76
- fmt.Println(string(data))
101
+ fmt.Fprintln(stdout, string(data))
77
102
  return 0
78
103
  }
@@ -0,0 +1,137 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "os"
7
+ "path/filepath"
8
+ "slices"
9
+ "strings"
10
+ "testing"
11
+
12
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
13
+ )
14
+
15
+ // TestRunDeclaresTheCapabilityItsOutputCarries verifies that every capability
16
+ // this command declares is paired with the output that backs it, and that a
17
+ // project carrying no documentation tag emits that field nowhere.
18
+ //
19
+ // The claim is a separate literal from the shipped `ttscgraph dump`'s, so the
20
+ // test that guards that one guards nothing here: deleting graph.CapabilityDocTags
21
+ // from this command's list used to break nothing at all, in a package that had
22
+ // never had a test file. The claim matters because the two states a consumer must
23
+ // distinguish — "this declaration cites nothing" and "this producer never
24
+ // looked" — are the same absent field, so a dump that carries tags and forgets to
25
+ // say so is read as a repository where nothing cites anything.
26
+ //
27
+ // The declared set is compared whole rather than member by member, so a
28
+ // capability added to the list arrives with the evidence for it or fails here.
29
+ //
30
+ // 1. Run the command over a project with a tag, capturing stdout.
31
+ // 2. Assert the declared set is exactly what this command claims, and that each
32
+ // claim is backed: a tag in the output, and a digested source.
33
+ // 3. Run it over a project with no tag and assert the field appears nowhere,
34
+ // while both claims still do.
35
+ func TestRunDeclaresTheCapabilityItsOutputCarries(t *testing.T) {
36
+ tagged := runGraphdump(t, `/** @evidence docs/a.md#x Cited. */
37
+ export function subject(): void {}
38
+ `)
39
+ untagged := runGraphdump(t, `/** Ordinary documentation. */
40
+ export function subject(): void {}
41
+ `)
42
+
43
+ declared := []string{graph.CapabilityDocTags, graph.CapabilitySourceDigests}
44
+ slices.Sort(declared)
45
+ for name, raw := range map[string]string{"tagged": tagged, "untagged": untagged} {
46
+ var parsed struct {
47
+ Provenance struct {
48
+ Capabilities []string `json:"capabilities"`
49
+ Sources []struct {
50
+ File string `json:"file"`
51
+ CheckerDigest string `json:"checkerDigest"`
52
+ } `json:"sources"`
53
+ } `json:"provenance"`
54
+ }
55
+ if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
56
+ t.Fatalf("%s dump is not JSON: %v", name, err)
57
+ }
58
+ got := slices.Clone(parsed.Provenance.Capabilities)
59
+ slices.Sort(got)
60
+ if !slices.Equal(got, declared) {
61
+ t.Fatalf("%s dump declares %v, want %v; an absent claim means the producer "+
62
+ "never looked, so a tagged declaration reads as citing nothing and a "+
63
+ "digested source reads as unread",
64
+ name, got, declared)
65
+ }
66
+ // The sourceDigests claim, backed the same way the docTags claim is: a
67
+ // declared capability whose output carries nothing is the tautology this
68
+ // test exists to avoid.
69
+ digested := 0
70
+ for _, source := range parsed.Provenance.Sources {
71
+ if source.CheckerDigest != "" {
72
+ digested++
73
+ }
74
+ }
75
+ if digested == 0 {
76
+ t.Fatalf("%s dump declares %q over %d sources, none of which carries a checker digest",
77
+ name, graph.CapabilitySourceDigests, len(parsed.Provenance.Sources))
78
+ }
79
+ }
80
+
81
+ if !strings.Contains(tagged, `"docs/a.md#x Cited."`) {
82
+ t.Fatalf("the tagged dump carried no tag text")
83
+ }
84
+ // Read the raw document rather than a decoded slice: an absent field and an
85
+ // empty one decode identically, and absent is what the wire contract says. The
86
+ // key is matched with its colon, because the capability shares the name and
87
+ // rides the same document as a bare string.
88
+ if strings.Contains(untagged, `"docTags":`) {
89
+ t.Fatalf("the untagged dump carried a docTags field; it must be absent")
90
+ }
91
+ if !strings.Contains(tagged, `"docTags":`) {
92
+ t.Fatalf("the tagged dump carried no docTags field, so the check above proves nothing")
93
+ }
94
+ }
95
+
96
+ // runGraphdump runs the command over a one-file project and returns its stdout.
97
+ //
98
+ // It goes through run(args) and the package streams rather than through os.Args,
99
+ // flag.CommandLine, and an os.Stdout pipe. Those seams exist on the shipped
100
+ // sibling for this reason, and this command grew them so its own claim could be
101
+ // asserted against its own output without patching process globals.
102
+ func runGraphdump(t *testing.T, source string) string {
103
+ t.Helper()
104
+ root := t.TempDir()
105
+ writeGraphdumpFile(t, filepath.Join(root, "tsconfig.json"), `{
106
+ "compilerOptions": {
107
+ "target": "ES2022",
108
+ "module": "commonjs",
109
+ "strict": true,
110
+ "rootDir": "src",
111
+ "outDir": "dist"
112
+ },
113
+ "files": ["src/main.ts"]
114
+ }
115
+ `)
116
+ writeGraphdumpFile(t, filepath.Join(root, "src", "main.ts"), source)
117
+
118
+ var out, errOut bytes.Buffer
119
+ restoreStdout, restoreStderr := stdout, stderr
120
+ stdout, stderr = &out, &errOut
121
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
122
+
123
+ if code := run([]string{"--cwd", root, "--tsconfig", "tsconfig.json"}); code != 0 {
124
+ t.Fatalf("graphdump exited %d: %s", code, errOut.String())
125
+ }
126
+ return out.String()
127
+ }
128
+
129
+ func writeGraphdumpFile(t *testing.T, path, content string) {
130
+ t.Helper()
131
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
132
+ t.Fatal(err)
133
+ }
134
+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
135
+ t.Fatal(err)
136
+ }
137
+ }
@@ -0,0 +1,56 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ // TestRunReportsAnUnknownFlagWithoutExitingTheProcess verifies that a bad
10
+ // argument returns an exit code and writes usage to the command's own stream,
11
+ // instead of terminating the process.
12
+ //
13
+ // This is the negative twin of the capability case and the reason the seams
14
+ // exist. The command used to parse flag.CommandLine, whose default error
15
+ // handling is flag.ExitOnError: a malformed argument called os.Exit(2) from
16
+ // inside run, which no test can observe and which would take a test binary down
17
+ // with it. Nothing else about the command is reachable while that is true.
18
+ //
19
+ // 1. Run the command with an unknown flag, capturing both streams.
20
+ // 2. Assert it returns 2 rather than exiting, and that nothing reached stdout.
21
+ // 3. Assert the usage text went to the command's stderr stream.
22
+ // 4. Assert `-h` still succeeds, because asking for usage is not an error.
23
+ func TestRunReportsAnUnknownFlagWithoutExitingTheProcess(t *testing.T) {
24
+ var out, errOut bytes.Buffer
25
+ restoreStdout, restoreStderr := stdout, stderr
26
+ stdout, stderr = &out, &errOut
27
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
28
+
29
+ code := run([]string{"--not-a-flag"})
30
+
31
+ if code != 2 {
32
+ t.Fatalf("graphdump exited %d for an unknown flag, want 2", code)
33
+ }
34
+ if out.Len() != 0 {
35
+ t.Fatalf("a rejected invocation wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
36
+ }
37
+ if !strings.Contains(errOut.String(), "not-a-flag") {
38
+ t.Fatalf("stderr does not name the rejected flag: %q", errOut.String())
39
+ }
40
+ if !strings.Contains(errOut.String(), "-tsconfig") {
41
+ t.Fatalf("stderr carried no usage text: %q", errOut.String())
42
+ }
43
+
44
+ // The negative twin of the rejection: `flag.ContinueOnError` reports `-h` as
45
+ // an error too, and mapping every parse failure to 2 would turn the help flag
46
+ // into a failed invocation. The global flag set this command used to read
47
+ // exited 0 for it.
48
+ out.Reset()
49
+ errOut.Reset()
50
+ if code := run([]string{"-h"}); code != 0 {
51
+ t.Fatalf("graphdump exited %d for -h, want 0", code)
52
+ }
53
+ if !strings.Contains(errOut.String(), "-tsconfig") {
54
+ t.Fatalf("-h printed no usage text: %q", errOut.String())
55
+ }
56
+ }
@@ -0,0 +1,46 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ // TestRunReportsAnUnloadableProjectWithoutPrintingADump verifies that a project
10
+ // the command cannot load exits non-zero, prints nothing to stdout, and names
11
+ // what it could not load.
12
+ //
13
+ // The benchmark viewer pipeline runs this command through `execFileSync`, which
14
+ // raises only on a non-zero exit and otherwise parses stdout as JSON. Every
15
+ // failure path here therefore has to be a non-zero exit: turning one into 0
16
+ // hands the pipeline an empty string, and the caller dies inside its JSON parse
17
+ // with an opaque error instead of reading the diagnostic this command already
18
+ // wrote. All three failure paths used to be unpinned, so replacing every
19
+ // `return 1` with `return 0` broke no test.
20
+ //
21
+ // 1. Run the command against a tsconfig that does not exist.
22
+ // 2. Assert it returns 1 and wrote nothing to stdout.
23
+ // 3. Assert stderr names the command and the path it could not load.
24
+ func TestRunReportsAnUnloadableProjectWithoutPrintingADump(t *testing.T) {
25
+ root := t.TempDir()
26
+
27
+ var out, errOut bytes.Buffer
28
+ restoreStdout, restoreStderr := stdout, stderr
29
+ stdout, stderr = &out, &errOut
30
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
31
+
32
+ code := run([]string{"--cwd", root, "--tsconfig", "absent.json"})
33
+
34
+ if code != 1 {
35
+ t.Fatalf("graphdump exited %d for an unloadable project, want 1", code)
36
+ }
37
+ if out.Len() != 0 {
38
+ t.Fatalf("a failed run wrote %q to stdout; the viewer pipeline parses that stream as JSON", out.String())
39
+ }
40
+ if !strings.Contains(errOut.String(), "graphdump:") {
41
+ t.Fatalf("stderr does not identify the command: %q", errOut.String())
42
+ }
43
+ if !strings.Contains(errOut.String(), "absent.json") {
44
+ t.Fatalf("stderr does not name the tsconfig it could not load: %q", errOut.String())
45
+ }
46
+ }
@@ -26,6 +26,13 @@ type apiTransformResult struct {
26
26
  // content can influence a transformed module, so bundler caches invalidate
27
27
  // soundly without per-plugin reporting.
28
28
  Graph *driver.TransformGraph `json:"graph,omitempty"`
29
+ // Dependencies and DependenciesComplete are the envelope's dependency side
30
+ // channel. This lane runs no plugin transform at all — each output is the
31
+ // file's own parsed text — so every file is complete with an empty list
32
+ // unless a linked plugin is active and declares otherwise; see
33
+ // driver.Program.TransformDependenciesFor.
34
+ Dependencies map[string][]string `json:"dependencies,omitempty"`
35
+ DependenciesComplete []string `json:"dependenciesComplete,omitempty"`
29
36
  // HostInputs are absolute native plugin config files evaluated while this
30
37
  // generation loaded. JavaScript hosts merge them with descriptor inputs.
31
38
  HostInputs []string `json:"hostInputs,omitempty"`
@@ -74,6 +81,7 @@ func runAPITransform(args []string) int {
74
81
  return 2
75
82
  }
76
83
  typescript := map[string]string{}
84
+ var dependencies driver.TransformDependencies
77
85
  var graph *driver.TransformGraph
78
86
  var hostInputs []string
79
87
  var hostInputHashes map[string]*string
@@ -89,18 +97,21 @@ func runAPITransform(args []string) int {
89
97
  typescript[apiOutputKey(cwd, file.FileName())] = file.Text()
90
98
  }
91
99
  diags = append(diags, prog.Diagnostics()...)
100
+ dependencies = prog.TransformDependenciesFor(cwd)
92
101
  hostInputs = prog.PluginHostInputs()
93
102
  hostInputHashes = prog.PluginHostInputHashes()
94
103
  hostInputRealpaths = prog.PluginHostInputRealpaths()
95
104
  }
96
105
 
97
106
  result := apiTransformResult{
98
- Diagnostics: make([]apiCompileDiagnostic, 0, len(diags)),
99
- Graph: graph,
100
- HostInputs: hostInputs,
101
- HostInputHashes: hostInputHashes,
102
- HostInputRealpaths: hostInputRealpaths,
103
- TypeScript: typescript,
107
+ Dependencies: dependencies.Dependencies,
108
+ DependenciesComplete: dependencies.Complete,
109
+ Diagnostics: make([]apiCompileDiagnostic, 0, len(diags)),
110
+ Graph: graph,
111
+ HostInputs: hostInputs,
112
+ HostInputHashes: hostInputHashes,
113
+ HostInputRealpaths: hostInputRealpaths,
114
+ TypeScript: typescript,
104
115
  }
105
116
  for _, diag := range diags {
106
117
  result.Diagnostics = append(result.Diagnostics, toAPICompileDiagnostic(diag))
@@ -23,6 +23,11 @@ func runDump(args []string) int {
23
23
  cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
24
24
  tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
25
25
  prettyFlag := fs.Bool("pretty", false, "indent the JSON output")
26
+ artifactsFlag := fs.String(
27
+ "artifacts",
28
+ "",
29
+ "path to the artifacts a plugin published (JSON); absent or missing means none",
30
+ )
26
31
  if err := fs.Parse(args); err != nil {
27
32
  return 2
28
33
  }
@@ -37,7 +42,7 @@ func runDump(args []string) int {
37
42
  cwd = resolved
38
43
  }
39
44
  // Resolve the project root the same way LoadProgram does (absolute, then
40
- // tsgo-normalized) so the schema-v6 mapper receives the same canonical root
45
+ // tsgo-normalized) so the dump path mapper receives the same canonical root
41
46
  // grammar and drive-letter case as the compiler's source paths.
42
47
  if abs, err := filepath.Abs(cwd); err == nil {
43
48
  cwd = abs
@@ -57,9 +62,19 @@ func runDump(args []string) int {
57
62
  defer func() { _ = prog.Close() }()
58
63
 
59
64
  g := graph.Build(prog)
65
+ // The artifacts arrive from a plugin that parsed documents this Program never
66
+ // read, so they are a second producer's facts and the origin says so. Applying
67
+ // them before the dump is what turns a citation of a document section from a
68
+ // token into a relation.
69
+ artifacts, err := graph.LoadArtifacts(strings.TrimSpace(*artifactsFlag))
70
+ if err != nil {
71
+ fmt.Fprintf(stderr, "ttscgraph: could not read the published artifacts: %v\n", err)
72
+ return 1
73
+ }
74
+ graph.ApplyArtifacts(g, artifacts)
60
75
  ignored := graph.GitIgnoredFiles(cwd, g)
61
76
  texts := graph.SourceTexts(prog)
62
- origin, err := dumpOrigin(prog, texts)
77
+ origin, err := dumpOrigin(prog, texts, strings.TrimSpace(*artifactsFlag) != "")
63
78
  if err != nil {
64
79
  fmt.Fprintf(stderr, "ttscgraph: %v\n", err)
65
80
  return 1
@@ -89,7 +104,16 @@ func runDump(args []string) int {
89
104
  // saw the project, so it carries the same provenance a served snapshot does:
90
105
  // without it the file is a pile of facts with no way to tell which program, or
91
106
  // which day, they describe.
92
- func dumpOrigin(prog *driver.Program, texts map[string]string) (graph.DumpOrigin, error) {
107
+ // dumpOrigin builds the provenance for one dump. askedForArtifacts is what
108
+ // turns the artifact capability from a guess into a claim: a producer that was
109
+ // never pointed at a publisher did not look, and saying it did would make a
110
+ // project with no artifacts indistinguishable from one whose artifacts were
111
+ // never requested.
112
+ func dumpOrigin(
113
+ prog *driver.Program,
114
+ texts map[string]string,
115
+ askedForArtifacts bool,
116
+ ) (graph.DumpOrigin, error) {
93
117
  configs, err := parsedConfigs(prog)
94
118
  if err != nil {
95
119
  return graph.DumpOrigin{}, err
@@ -102,15 +126,23 @@ func dumpOrigin(prog *driver.Program, texts map[string]string) (graph.DumpOrigin
102
126
  if err != nil {
103
127
  return graph.DumpOrigin{}, err
104
128
  }
129
+ capabilities := fullSnapshotCapabilities
130
+ if askedForArtifacts {
131
+ capabilities = append(append([]string{}, capabilities...), graph.CapabilityArtifactNodes)
132
+ }
133
+ provenance := graph.NewProvenance(
134
+ serveProducer(),
135
+ capabilities,
136
+ fileDigests(configHashes),
137
+ rootFileEntries(projectRootFilesFromConfigs(configs, false)),
138
+ texts,
139
+ diskDigests,
140
+ )
141
+ if askedForArtifacts {
142
+ provenance.ArtifactProducer = &graph.Producer{Tool: "@ttsc/lint graph-nodes"}
143
+ }
105
144
  return graph.DumpOrigin{
106
- Provenance: graph.NewProvenance(
107
- serveProducer(),
108
- fullSnapshotCapabilities,
109
- fileDigests(configHashes),
110
- rootFileEntries(projectRootFilesFromConfigs(configs, false)),
111
- texts,
112
- diskDigests,
113
- ),
145
+ Provenance: provenance,
114
146
  Diagnostics: graph.NewDiagnostics(prog),
115
147
  }, nil
116
148
  }
@@ -0,0 +1,135 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "os"
7
+ "path/filepath"
8
+ "slices"
9
+ "strings"
10
+ "testing"
11
+
12
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
13
+ )
14
+
15
+ // TestRunDumpClaimsArtifactsOnlyWhenItAsked verifies that the artifact
16
+ // capability is a statement about what this producer did, not about what it
17
+ // found.
18
+ //
19
+ // The two states a consumer must distinguish are "this project publishes no
20
+ // artifacts" and "this producer was never pointed at a publisher". Both are the
21
+ // same absent nodes, so only the claim separates them — and a producer that
22
+ // claimed unconditionally would make a project with no lint install look like a
23
+ // project whose documents cite nothing. The second producer is named for the
24
+ // same reason: these facts did not come from this Program, and the dump's
25
+ // one-generation contract stays honest by saying so rather than by hiding it.
26
+ //
27
+ // 1. Dump a project with no `--artifacts`, and assert neither the claim nor the
28
+ // second producer appears, and that the citation stays a token.
29
+ // 2. Dump the same project with a published set, and assert both appear and the
30
+ // citation resolved to a node carrying its heading and its line.
31
+ func TestRunDumpClaimsArtifactsOnlyWhenItAsked(t *testing.T) {
32
+ root := t.TempDir()
33
+ writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
34
+ "compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
35
+ "files": ["src/main.ts"]
36
+ }
37
+ `)
38
+ writeGraphFile(t, filepath.Join(root, "src", "main.ts"), `/** @evidence docs/sale.md#pricing States the rule. */
39
+ export function priced(): void {}
40
+ `)
41
+
42
+ plain := decodeArtifactDump(t, runDumpForArtifacts(t, root, ""))
43
+ if slices.Contains(plain.Provenance.Capabilities, graph.CapabilityArtifactNodes) {
44
+ t.Fatalf("a dump that was never pointed at a publisher claimed %q", graph.CapabilityArtifactNodes)
45
+ }
46
+ if plain.Provenance.ArtifactProducer != nil {
47
+ t.Fatal("a dump with no artifacts named a second producer")
48
+ }
49
+ for _, node := range plain.Nodes {
50
+ if node.ID == "docs/sale.md#pricing" {
51
+ t.Fatal("an unrequested dump fabricated an artifact node")
52
+ }
53
+ }
54
+
55
+ published := filepath.Join(root, "artifacts.json")
56
+ if err := os.WriteFile(published, []byte(`[
57
+ {"address":"docs/sale.md","kind":"markdown_document","readable":"Sale","file":"docs/sale.md","line":1},
58
+ {"address":"docs/sale.md#pricing","kind":"markdown_section","readable":"Pricing","parent":"docs/sale.md","file":"docs/sale.md","line":7}
59
+ ]`), 0o644); err != nil {
60
+ t.Fatal(err)
61
+ }
62
+
63
+ indexed := decodeArtifactDump(t, runDumpForArtifacts(t, root, published))
64
+ if !slices.Contains(indexed.Provenance.Capabilities, graph.CapabilityArtifactNodes) {
65
+ t.Fatalf("a dump that asked declares %v, missing %q",
66
+ indexed.Provenance.Capabilities, graph.CapabilityArtifactNodes)
67
+ }
68
+ if indexed.Provenance.ArtifactProducer == nil {
69
+ t.Fatal("the artifact nodes came from another Program and the dump did not say so")
70
+ }
71
+
72
+ var section *graph.DumpNode
73
+ for index := range indexed.Nodes {
74
+ if indexed.Nodes[index].ID == "docs/sale.md#pricing" {
75
+ section = &indexed.Nodes[index]
76
+ }
77
+ }
78
+ if section == nil {
79
+ t.Fatal("the cited section is not a node")
80
+ }
81
+ if section.Name != "Pricing" {
82
+ t.Fatalf("the section is named %q, not the heading text an index exists to hold", section.Name)
83
+ }
84
+ if section.Parent != "docs/sale.md" {
85
+ t.Fatalf("the section's parent is %q, want its document", section.Parent)
86
+ }
87
+ if section.Evidence == nil || section.Evidence.StartLine != 7 {
88
+ t.Fatalf("the section carries no span at its heading: %+v", section.Evidence)
89
+ }
90
+ cited := false
91
+ for _, edge := range indexed.Edges {
92
+ if edge.Kind == "doc_ref" && edge.To == "docs/sale.md#pricing" {
93
+ cited = true
94
+ }
95
+ }
96
+ if !cited {
97
+ t.Fatal("the citation stayed a token; it has to resolve to the node now that one exists")
98
+ }
99
+ }
100
+
101
+ // runDumpForArtifacts runs the dump subcommand and returns its stdout.
102
+ func runDumpForArtifacts(t *testing.T, root, artifacts string) string {
103
+ t.Helper()
104
+ args := []string{"dump", "--cwd", root, "--tsconfig", "tsconfig.json"}
105
+ if artifacts != "" {
106
+ args = append(args, "--artifacts", artifacts)
107
+ }
108
+ var out, errOut bytes.Buffer
109
+ restoreStdout, restoreStderr := stdout, stderr
110
+ stdout, stderr = &out, &errOut
111
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
112
+
113
+ if code := run(args); code != 0 {
114
+ t.Fatalf("dump exited %d: %s", code, errOut.String())
115
+ }
116
+ return out.String()
117
+ }
118
+
119
+ type artifactDump struct {
120
+ Provenance struct {
121
+ Capabilities []string `json:"capabilities"`
122
+ ArtifactProducer *graph.Producer `json:"artifactProducer"`
123
+ } `json:"provenance"`
124
+ Nodes []graph.DumpNode `json:"nodes"`
125
+ Edges []graph.DumpEdge `json:"edges"`
126
+ }
127
+
128
+ func decodeArtifactDump(t *testing.T, raw string) artifactDump {
129
+ t.Helper()
130
+ var parsed artifactDump
131
+ if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &parsed); err != nil {
132
+ t.Fatalf("dump is not JSON: %v", err)
133
+ }
134
+ return parsed
135
+ }