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
@@ -0,0 +1,96 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "path/filepath"
7
+ "slices"
8
+ "strings"
9
+ "testing"
10
+
11
+ "github.com/samchon/ttsc/packages/ttsc/internal/graph"
12
+ )
13
+
14
+ // TestRunDumpDeclaresTheCapabilityItsOutputCarries verifies that the shipped
15
+ // `dump` command's own output declares the documentation-tag capability, and
16
+ // that a project carrying no tag emits the field nowhere.
17
+ //
18
+ // The claim is the contract, and it can only be tested where it is made. A test
19
+ // that hands a capability list to the marshaller and reads it back proves the
20
+ // slice was copied; it leaves the production list free to lose the member with
21
+ // nothing failing. That matters because the two states a consumer must
22
+ // distinguish — "this declaration cites nothing" and "this producer never
23
+ // looked" — are the same absent field, so a dump that carries tags and forgets
24
+ // to say so is read as a repository where nothing cites anything.
25
+ //
26
+ // 1. Run the `dump` subcommand over a project with a tag, capturing stdout.
27
+ // 2. Assert the capability is declared and the tag is present in the output.
28
+ // 3. Run it over a project with no tag and assert the field appears nowhere,
29
+ // while the claim still does.
30
+ func TestRunDumpDeclaresTheCapabilityItsOutputCarries(t *testing.T) {
31
+ tagged := runDumpForCapabilityProbe(t, `/** @evidence docs/a.md#x Cited. */
32
+ export function subject(): void {}
33
+ `)
34
+ untagged := runDumpForCapabilityProbe(t, `/** Ordinary documentation. */
35
+ export function subject(): void {}
36
+ `)
37
+
38
+ for name, raw := range map[string]string{"tagged": tagged, "untagged": untagged} {
39
+ var parsed struct {
40
+ Provenance struct {
41
+ Capabilities []string `json:"capabilities"`
42
+ } `json:"provenance"`
43
+ }
44
+ if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
45
+ t.Fatalf("%s dump is not JSON: %v", name, err)
46
+ }
47
+ if !slices.Contains(parsed.Provenance.Capabilities, graph.CapabilityDocTags) {
48
+ t.Fatalf("%s dump declares %v, missing %q; a consumer would then read a "+
49
+ "tagged declaration as citing nothing",
50
+ name, parsed.Provenance.Capabilities, graph.CapabilityDocTags)
51
+ }
52
+ }
53
+
54
+ if !strings.Contains(tagged, `"docs/a.md#x Cited."`) {
55
+ t.Fatalf("the tagged dump carried no tag text")
56
+ }
57
+ // Read the raw document rather than a decoded slice: an absent field and an
58
+ // empty one decode identically, and absent is what the wire contract says.
59
+ // The key is matched with its colon, because the capability shares the name
60
+ // and rides the same document as a bare string.
61
+ if strings.Contains(untagged, `"docTags":`) {
62
+ t.Fatalf("the untagged dump carried a docTags field; it must be absent")
63
+ }
64
+ if !strings.Contains(tagged, `"docTags":`) {
65
+ t.Fatalf("the tagged dump carried no docTags field, so the check above proves nothing")
66
+ }
67
+ }
68
+
69
+ // runDumpForCapabilityProbe runs the dump subcommand over a one-file project and
70
+ // returns its stdout.
71
+ func runDumpForCapabilityProbe(t *testing.T, source string) string {
72
+ t.Helper()
73
+ root := t.TempDir()
74
+ writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
75
+ "compilerOptions": {
76
+ "target": "ES2022",
77
+ "module": "commonjs",
78
+ "strict": true,
79
+ "rootDir": "src",
80
+ "outDir": "dist"
81
+ },
82
+ "files": ["src/main.ts"]
83
+ }
84
+ `)
85
+ writeGraphFile(t, filepath.Join(root, "src", "main.ts"), source)
86
+
87
+ var out, errOut bytes.Buffer
88
+ restoreStdout, restoreStderr := stdout, stderr
89
+ stdout, stderr = &out, &errOut
90
+ defer func() { stdout, stderr = restoreStdout, restoreStderr }()
91
+
92
+ if code := run([]string{"dump", "--cwd", root, "--tsconfig", "tsconfig.json"}); code != 0 {
93
+ t.Fatalf("dump exited %d: %s", code, errOut.String())
94
+ }
95
+ return out.String()
96
+ }
@@ -57,27 +57,82 @@ const (
57
57
  )
58
58
 
59
59
  // fullSnapshotCapabilities is what a snapshot from a resident compiler session
60
- // proves. Both commands that own a real Program declare all three; the constant
61
- // exists so the envelope's claim and the dump's claim cannot drift apart.
60
+ // proves. Both commands that own a real Program declare every one of them; the
61
+ // constant exists so the envelope's claim and the dump's claim cannot drift
62
+ // apart.
62
63
  var fullSnapshotCapabilities = []string{
63
64
  graph.CapabilityUniverse,
64
65
  graph.CapabilitySourceDigests,
65
66
  graph.CapabilityDiskDigests,
66
67
  graph.CapabilityDiagnostics,
68
+ graph.CapabilityDocTags,
67
69
  }
68
70
 
69
- // serveCapabilities is what this server can prove, answered before a consumer
70
- // has a dump to inspect — an `unchanged` response carries no dump, and a client
71
- // negotiating on the first frame has not parsed one yet. It mirrors what every
72
- // dump this server publishes declares for itself.
71
+ // serveCapabilities is what this server can prove before a session exists —
72
+ // the frame a client reads when the project could not even be loaded. Once a
73
+ // session exists its own capabilities answer, because those depend on what a
74
+ // plugin published and this list cannot know it.
73
75
  var serveCapabilities = fullSnapshotCapabilities
74
76
 
77
+ // capabilities is what THIS session proves, which is the shared list plus the
78
+ // artifact claim when a plugin published one.
79
+ //
80
+ // The claim has to be conditional for the same reason it is on the dump command:
81
+ // a project that publishes no artifacts and a session that was never handed any
82
+ // are the same absent nodes, and only the claim separates them.
83
+ func (s *graphSession) capabilities() []string {
84
+ if len(s.artifacts) == 0 {
85
+ return fullSnapshotCapabilities
86
+ }
87
+ return append(
88
+ append([]string{}, fullSnapshotCapabilities...),
89
+ graph.CapabilityArtifactNodes,
90
+ )
91
+ }
92
+
93
+ // artifactProducer names the second producer behind this session's artifact
94
+ // nodes, and is nil when it has none.
95
+ // provenance is this session's published origin for one generation.
96
+ func (s *graphSession) provenance(texts map[string]string) graph.Provenance {
97
+ built := graph.NewProvenance(
98
+ serveProducer(),
99
+ s.capabilities(),
100
+ s.configDigests,
101
+ s.roots,
102
+ texts,
103
+ s.diskDigests,
104
+ )
105
+ built.ArtifactProducer = s.artifactProducer()
106
+ return built
107
+ }
108
+
109
+ func (s *graphSession) artifactProducer() *graph.Producer {
110
+ if len(s.artifacts) == 0 {
111
+ return nil
112
+ }
113
+ return &graph.Producer{Tool: "@ttsc/lint graph-nodes"}
114
+ }
115
+
75
116
  type serveRequest struct {
76
117
  ID int `json:"id"`
77
118
  // GraphSnapshotVersion opts into the incremental shard protocol. Omitted
78
- // requests retain the schema-v6 full-dump response for existing
79
- // @ttsc/graph clients.
119
+ // requests retain the full-dump response for existing @ttsc/graph clients.
80
120
  GraphSnapshotVersion int `json:"graphSnapshotVersion,omitempty"`
121
+ // Artifacts is the path to the set a plugin published for this request, which
122
+ // the client re-derives when the documents or lint configuration behind it
123
+ // moved. It rides every request rather than only the changed ones: the client
124
+ // is the only side that can see those inputs, and the server is the only side
125
+ // that knows what it last applied, so the honest exchange is the client
126
+ // stating what it currently has and the server comparing it with its own.
127
+ //
128
+ // Three states, which is why it is a pointer. Absent is a client with no
129
+ // opinion — one predating this field, or one driving a session through the
130
+ // startup flag — and the set already applied stands. Empty is a client
131
+ // stating it now has none, which withdraws that set: a project whose
132
+ // publisher was removed must stop being answered with its artifacts, and
133
+ // without a way to say so it would be answered with them until the editor
134
+ // restarted. A path is the set to apply.
135
+ Artifacts *string `json:"artifacts,omitempty"`
81
136
  }
82
137
 
83
138
  type serveResponse struct {
@@ -119,8 +174,30 @@ func errorResponse(id int, message string) serveResponse {
119
174
  }
120
175
 
121
176
  type graphSession struct {
122
- cwd string
123
- tsconfig string
177
+ cwd string
178
+ tsconfig string
179
+ // artifacts is the set a plugin published: a second producer's facts about
180
+ // documents this Program never read. Refreshing it is therefore not a refresh
181
+ // of the graph — editing a Markdown heading moves the artifact and not one
182
+ // compiler fact — so the two invalidations stay separate questions, and this
183
+ // one is answered by adoptArtifacts rather than by the build universe.
184
+ artifacts []graph.Artifact
185
+ // artifactsDigest is the content of the set currently applied. Content, and
186
+ // not the path or its modification time: the client overwrites one file per
187
+ // process and project, so the path never moves for a given session, and a
188
+ // republish triggered by a document whose headings did not actually change
189
+ // writes the same bytes and must therefore cost nothing.
190
+ artifactsDigest [sha256.Size]byte
191
+ // artifactsFile and artifactsStat are what the digest was taken from, kept so
192
+ // an unchanged file need not be read at all. The set is one entry per
193
+ // document section, model field, and operation, so it is bounded by the
194
+ // project's documentation rather than by anything small — and this comparison
195
+ // runs before every graph request, where reading and hashing that whole file
196
+ // to learn it did not move is the kind of cost a resident session pays
197
+ // forever.
198
+ artifactsFile string
199
+ artifactsStat artifactsFileState
200
+
124
201
  compiler *driver.Session
125
202
  configHashes map[string][sha256.Size]byte
126
203
  auxStates map[string]diskState
@@ -140,21 +217,134 @@ type graphSession struct {
140
217
  graphStore *serveGraphStore
141
218
  requestProtocol int
142
219
  requestProtocolSelected bool
143
- // pending remembers a generation whose state was captured but whose selected
144
- // projection failed. Until an input change lets the session rebuild, an
145
- // unchanged request retries the same full or partial invalidation instead of
146
- // falsely confirming the older client graph.
220
+ // pending remembers work the next request owes regardless of what the build
221
+ // universe says: a generation whose state was captured but whose selected
222
+ // projection failed, or an artifact set adopted after the graph carrying the
223
+ // previous one was built. Until it is discharged, an otherwise unchanged
224
+ // request retries the recorded invalidation instead of falsely confirming the
225
+ // older client graph.
147
226
  pending *graphChange
148
227
  }
149
228
 
150
229
  func newGraphSession(cwd, tsconfig string) (*graphSession, error) {
151
- session := &graphSession{cwd: cwd, tsconfig: tsconfig}
230
+ return newGraphSessionWithArtifacts(cwd, tsconfig, nil)
231
+ }
232
+
233
+ // newGraphSessionWithArtifacts is the constructor the serve command uses, and
234
+ // the plain one above is it with nothing published. The artifacts are read once
235
+ // and held for the session: they are a second producer's facts about documents
236
+ // this Program never read, so refreshing them is a different question from
237
+ // refreshing the graph, and only the first answer is wired today.
238
+ func newGraphSessionWithArtifacts(
239
+ cwd, tsconfig string,
240
+ artifacts []graph.Artifact,
241
+ ) (*graphSession, error) {
242
+ session := &graphSession{cwd: cwd, tsconfig: tsconfig, artifacts: artifacts}
152
243
  if err := session.reload(); err != nil {
153
244
  return nil, err
154
245
  }
155
246
  return session, nil
156
247
  }
157
248
 
249
+ // adoptArtifacts makes what the request states the set this session applies,
250
+ // and reports whether that replaced a set some already-built graph carries.
251
+ //
252
+ // A nil statement is a client with no opinion, and the set already applied
253
+ // stands: dropping it on the say-so of a client that never mentioned artifacts
254
+ // would delete them from the graph of every session driven through the startup
255
+ // flag alone.
256
+ //
257
+ // A read failure is returned rather than swallowed. The client wrote this file
258
+ // moments ago and named it in the same request, so a file that cannot be read
259
+ // is a broken exchange, not a project without artifacts — and answering with a
260
+ // silently artifact-free graph would look exactly like a correct answer.
261
+ func (s *graphSession) adoptArtifacts(named *string) (bool, error) {
262
+ if named == nil {
263
+ return false, nil
264
+ }
265
+ path := strings.TrimSpace(*named)
266
+ if path == "" {
267
+ return s.applyArtifacts(nil, withdrawnArtifactsDigest), nil
268
+ }
269
+ stat, err := os.Stat(path)
270
+ if err != nil {
271
+ return false, err
272
+ }
273
+ state := artifactsFileState{size: stat.Size(), modTime: stat.ModTime().UnixNano()}
274
+ if path == s.artifactsFile && state == s.artifactsStat {
275
+ return false, nil
276
+ }
277
+ // One read, hashed and decoded from the same bytes. Reading twice would let
278
+ // an overwrite land between them and leave this session recording an identity
279
+ // for a set it is not holding — after which a later request naming the file
280
+ // it recorded would be answered as unchanged.
281
+ contents, err := os.ReadFile(path)
282
+ if err != nil {
283
+ return false, err
284
+ }
285
+ next, err := graph.ParseArtifacts(contents)
286
+ if err != nil {
287
+ return false, err
288
+ }
289
+ s.artifactsFile = path
290
+ s.artifactsStat = state
291
+ return s.applyArtifacts(next, sha256.Sum256(contents)), nil
292
+ }
293
+
294
+ // artifactsFileState is what an unchanged published file looks like from the
295
+ // outside: size and modification time, never contents. It exists only to decide
296
+ // whether the file is worth reading, and the digest taken from those contents is
297
+ // still what decides whether the set is worth applying.
298
+ type artifactsFileState struct {
299
+ size int64
300
+ // modTime is nanoseconds since the epoch rather than a time.Time, so the
301
+ // struct compares with == and means it. A time.Time carries a location
302
+ // pointer and an optional monotonic reading, and comparing two of those with
303
+ // == is a well-known way to get a false inequality that reads as a changed
304
+ // file forever.
305
+ modTime int64
306
+ }
307
+
308
+ // applyArtifacts installs a set and reports whether it differs from the applied
309
+ // one. The first application of a session reports a change like any other and
310
+ // costs nothing: no graph has been projected yet, so the invalidation it
311
+ // records is discharged by the initial projection that was going to happen
312
+ // regardless.
313
+ func (s *graphSession) applyArtifacts(next []graph.Artifact, digest [sha256.Size]byte) bool {
314
+ if digest == s.artifactsDigest {
315
+ return false
316
+ }
317
+ s.artifacts = next
318
+ s.artifactsDigest = digest
319
+ return true
320
+ }
321
+
322
+ // withdrawnArtifactsDigest stands for "the client states it has none".
323
+ //
324
+ // A constant rather than the hash of an empty file, so that the withdrawn state
325
+ // and a file whose contents happen to hash to the same value cannot be confused
326
+ // — and domain-separated so it is not the digest of anything a producer writes.
327
+ var withdrawnArtifactsDigest = sha256.Sum256([]byte("ttscgraph:artifacts:none"))
328
+
329
+ // invalidateArtifacts records that the next projection must be a full one.
330
+ //
331
+ // Full, because replacing the published set is not a per-file delta: it moves
332
+ // the artifact nodes and every citation edge into them at once, and the
333
+ // incremental path is built to answer a different question — which sources
334
+ // changed. It happens to reach the same answer here, through the closure
335
+ // expansion that pulls a citing source back in, but that is a property of
336
+ // machinery aimed elsewhere and not a guarantee this depends on.
337
+ //
338
+ // Full, but not a reload: the Program is reused untouched, because no compiler
339
+ // input moved. That is the whole point of keeping documents out of the build
340
+ // universe — a Markdown edit costs one projection, never one typecheck.
341
+ func (s *graphSession) invalidateArtifacts() {
342
+ if s.pending != nil && s.pending.full {
343
+ return
344
+ }
345
+ s.pending = &graphChange{mode: serveModeRebuild, full: true}
346
+ }
347
+
158
348
  func (s *graphSession) Close() error {
159
349
  if s.compiler == nil {
160
350
  return nil
@@ -441,6 +631,7 @@ func missingRootInputs(configs []*shimtsoptions.ParsedCommandLine, sourceHashes
441
631
  func (s *graphSession) buildDump() (graph.Dump, error) {
442
632
  program := s.compiler.Program()
443
633
  built := graph.Build(program)
634
+ graph.ApplyArtifacts(built, s.artifacts)
444
635
  // One texts map feeds both the spans and the manifest digests, so the bytes a
445
636
  // span points into are provably the bytes the manifest attests to.
446
637
  texts := graph.SourceTexts(program)
@@ -451,14 +642,7 @@ func (s *graphSession) buildDump() (graph.Dump, error) {
451
642
  graph.GitIgnoredFiles(s.cwd, built),
452
643
  texts,
453
644
  graph.DumpOrigin{
454
- Provenance: graph.NewProvenance(
455
- serveProducer(),
456
- fullSnapshotCapabilities,
457
- s.configDigests,
458
- s.roots,
459
- texts,
460
- s.diskDigests,
461
- ),
645
+ Provenance: s.provenance(texts),
462
646
  Diagnostics: graph.NewDiagnostics(program),
463
647
  },
464
648
  )
@@ -861,9 +1045,23 @@ func runServe(args []string) int {
861
1045
  fs.SetOutput(stderr)
862
1046
  cwdFlag := fs.String("cwd", "", "project root (defaults to process cwd)")
863
1047
  tsconfigFlag := fs.String("tsconfig", "tsconfig.json", "project tsconfig path")
1048
+ artifactsFlag := fs.String(
1049
+ "artifacts",
1050
+ "",
1051
+ "path to the artifacts a plugin published (JSON); absent or missing means none",
1052
+ )
864
1053
  if err := fs.Parse(args); err != nil {
865
1054
  return 2
866
1055
  }
1056
+ artifacts, artifactsErr := graph.LoadArtifacts(strings.TrimSpace(*artifactsFlag))
1057
+ if artifactsErr != nil {
1058
+ fmt.Fprintf(
1059
+ stderr,
1060
+ "ttscgraph: could not read the published artifacts: %v\n",
1061
+ artifactsErr,
1062
+ )
1063
+ return 1
1064
+ }
867
1065
 
868
1066
  cwd := strings.TrimSpace(*cwdFlag)
869
1067
  if cwd == "" {
@@ -880,10 +1078,22 @@ func runServe(args []string) int {
880
1078
  cwd = shimtspath.ResolvePath(cwd)
881
1079
  tsconfig := strings.TrimSpace(*tsconfigFlag)
882
1080
 
883
- return serveSnapshots(os.Stdin, stdout, cwd, tsconfig)
1081
+ return serveSnapshotsWithArtifacts(os.Stdin, stdout, cwd, tsconfig, artifacts)
884
1082
  }
885
1083
 
886
1084
  func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int {
1085
+ return serveSnapshotsWithArtifacts(input, output, cwd, tsconfig, nil)
1086
+ }
1087
+
1088
+ // serveSnapshotsWithArtifacts is the loop the serve command runs, and
1089
+ // serveSnapshots is it with nothing published — the shape every existing case
1090
+ // drives, unchanged.
1091
+ func serveSnapshotsWithArtifacts(
1092
+ input io.Reader,
1093
+ output io.Writer,
1094
+ cwd, tsconfig string,
1095
+ artifacts []graph.Artifact,
1096
+ ) int {
887
1097
  scanner := bufio.NewScanner(input)
888
1098
  scanner.Buffer(make([]byte, 64*1024), 1024*1024)
889
1099
  encoder := json.NewEncoder(output)
@@ -932,7 +1142,7 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
932
1142
  var loadDuration time.Duration
933
1143
  if session == nil {
934
1144
  loadStarted := time.Now()
935
- created, err := newGraphSession(cwd, tsconfig)
1145
+ created, err := newGraphSessionWithArtifacts(cwd, tsconfig, artifacts)
936
1146
  loadDuration = time.Since(loadStarted)
937
1147
  if err != nil {
938
1148
  if err := encodeServeResponseWithTrace(
@@ -962,6 +1172,18 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
962
1172
  }
963
1173
  session.requestProtocol = request.GraphSnapshotVersion
964
1174
  session.requestProtocolSelected = true
1175
+ if replaced, err := session.adoptArtifacts(request.Artifacts); err != nil {
1176
+ if err := encodeServeResponseWithTrace(encoder, errorResponse(
1177
+ request.ID,
1178
+ fmt.Sprintf("ttscgraph: could not read the published artifacts: %v", err),
1179
+ ), requestStarted, loadDuration, 0, 0); err != nil {
1180
+ fmt.Fprintf(stderr, "ttscgraph: write serve response: %v\n", err)
1181
+ return 1
1182
+ }
1183
+ continue
1184
+ } else if replaced {
1185
+ session.invalidateArtifacts()
1186
+ }
965
1187
  var dump *graph.Dump
966
1188
  var snapshot *serveGraphSnapshot
967
1189
  var mode string
@@ -975,6 +1197,12 @@ func serveSnapshots(input io.Reader, output io.Writer, cwd, tsconfig string) int
975
1197
  dump, mode, changed, err = session.Snapshot()
976
1198
  }
977
1199
  response := newServeResponse(request.ID)
1200
+ // The envelope answers for THIS session, not for the command in general. An
1201
+ // `unchanged` response carries no dump, so the envelope is the only place a
1202
+ // client learns what this server proves — and a session holding artifacts
1203
+ // publishes a dump that claims them. Leaving the shared list here made the
1204
+ // envelope and the dump disagree on the one frame that has no dump to check.
1205
+ response.Capabilities = session.capabilities()
978
1206
  response.Dump = dump
979
1207
  response.Snapshot = snapshot
980
1208
  response.Mode = mode