ttsc 0.24.0 → 0.26.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 (63) hide show
  1. package/README.md +70 -63
  2. package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
  3. package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
  4. package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
  5. package/internal/graph/edges.go +15 -1
  6. package/internal/lspserver/lsp_native_plugin_source.go +5 -6
  7. package/internal/lspserver/lsp_native_resident.go +17 -24
  8. package/lib/TtscService.d.ts +1 -6
  9. package/lib/TtscService.js +2 -2
  10. package/lib/TtscService.js.map +1 -1
  11. package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
  12. package/lib/compiler/internal/captureProcessOutput.js +92 -0
  13. package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
  14. package/lib/compiler/internal/compileProjectInMemory.js +4 -7
  15. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  16. package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
  17. package/lib/compiler/internal/residentCheckProcess.js +1 -15
  18. package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
  19. package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
  20. package/lib/compiler/internal/residentTransformProcess.js +0 -25
  21. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  22. package/lib/compiler/internal/spawnNative.d.ts +14 -6
  23. package/lib/compiler/internal/spawnNative.js +32 -11
  24. package/lib/compiler/internal/spawnNative.js.map +1 -1
  25. package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
  26. package/lib/compiler/internal/startResidentTransform.js +1 -3
  27. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  28. package/lib/launcher/internal/prepareExecution.d.ts +2 -1
  29. package/lib/launcher/internal/prepareExecution.js +269 -23
  30. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  31. package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
  32. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
  33. package/lib/launcher/internal/runTtscserver.js +4 -9
  34. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  35. package/lib/launcher/internal/runTtsx.js +2 -55
  36. package/lib/launcher/internal/runTtsx.js.map +1 -1
  37. package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
  38. package/lib/launcher/internal/runtimeHooks.js +344 -69
  39. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  40. package/lib/plugin/internal/buildSourcePlugin.js +31 -3
  41. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  42. package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
  43. package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
  44. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
  45. package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
  46. package/lib/plugin/internal/loadProjectPlugins.js +77 -28
  47. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  48. package/package.json +8 -8
  49. package/src/TtscService.ts +8 -20
  50. package/src/compiler/internal/captureProcessOutput.ts +96 -0
  51. package/src/compiler/internal/compileProjectInMemory.ts +7 -7
  52. package/src/compiler/internal/residentCheckProcess.ts +1 -21
  53. package/src/compiler/internal/residentTransformProcess.ts +0 -42
  54. package/src/compiler/internal/spawnNative.ts +39 -18
  55. package/src/compiler/internal/startResidentTransform.ts +1 -12
  56. package/src/launcher/internal/prepareExecution.ts +297 -26
  57. package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
  58. package/src/launcher/internal/runTtscserver.ts +4 -9
  59. package/src/launcher/internal/runTtsx.ts +2 -78
  60. package/src/launcher/internal/runtimeHooks.ts +385 -82
  61. package/src/plugin/internal/buildSourcePlugin.ts +36 -3
  62. package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
  63. package/src/plugin/internal/loadProjectPlugins.ts +78 -39
package/README.md CHANGED
@@ -8,8 +8,9 @@ A `typescript-go` toolchain for compiler-powered plugins and type-safe execution
8
8
 
9
9
  - **`ttsc`**: build, check, and transform.
10
10
  - **`ttsx`**: execute TypeScript with type checking.
11
- - [**`@ttsc/graph`**](https://github.com/samchon/ttsc/tree/master/packages/graph): MCP code graph that reduces agent token usage.
12
11
  - [**`@ttsc/lint`**](https://github.com/samchon/ttsc/tree/master/packages/lint): lint violations as compiler errors.
12
+ - [**`@ttsc/evidence`**](https://github.com/samchon/ttsc/tree/master/packages/evidence): 100% requirement coverage, or the build fails.
13
+ - [**`@ttsc/graph`**](https://github.com/samchon/ttsc/tree/master/packages/graph): compiler knowledge graph that cuts agent tokens by 92%.
13
14
  - **plugin support**: compiler-powered libraries, such as `typia`.
14
15
 
15
16
  ## Setup
@@ -35,94 +36,99 @@ That covers the CLI. The integrations each have a short guide:
35
36
  - [`@ttsc/metro`](https://ttsc.dev/docs/setup/metro): React Native and Expo.
36
37
  - [`@ttsc/vscode`](https://ttsc.dev/docs/setup/vscode): live editor diagnostics.
37
38
 
38
- ## Graph
39
-
40
- Your coding agent answers from the compiler, spending roughly 90% fewer tokens.
39
+ ## Lint
41
40
 
42
- `@ttsc/graph` is an MCP server that gives the agent a compiler-resolved graph of your project: what calls what, what a change would touch, and where to start reading. The agent asks the type checker instead of grepping and re-reading files.
41
+ Lint and format inside the type-check you already run. 720+ rules across 21 families, plus a formatter that reproduces Prettier's output.
43
42
 
44
- Register it with your MCP client. For Claude Code, a `.mcp.json` in the project root:
43
+ ```ts
44
+ // src/index.ts
45
+ var x: number = 3;
46
+ let y: number = 4;
47
+ const z: string = 5;
48
+ ```
45
49
 
46
50
  ```bash
47
- npm install -D ttsc @ttsc/graph typescript
48
- ```
51
+ $ npx ttsc --noEmit
52
+ src/index.ts:3:7 - error TS2322: Type 'number' is not assignable to type 'string'.
49
53
 
50
- ```json
51
- {
52
- "mcpServers": {
53
- "ttsc-graph": {
54
- "command": "npx",
55
- "args": ["-y", "@ttsc/graph"]
56
- }
57
- }
58
- }
59
- ```
54
+ 3 const z: string = 5;
55
+ ~
60
56
 
61
- On the agent-cost benchmark, Claude agents answer reading zero files, cutting tokens by roughly 90% and tool calls by 93% to 96%. The design and per-repository numbers are in the [Code Graph guide](https://ttsc.dev/docs/graph) and the [benchmark](https://ttsc.dev/docs/benchmark/graph).
57
+ src/index.ts:2:5 - error TS17397: [prefer-const] Use const instead of let.
62
58
 
63
- ![Median tokens on the shared onboarding question, lower is better](https://ttsc.dev/benchmark/svg/graph-common-codex-gpt-5.6-sol.svg)
59
+ 2 let y: number = 4;
60
+ ~~~~~~~~~~~~~
64
61
 
65
- ## Lint
62
+ src/index.ts:1:1 - error TS11966: [no-var] Unexpected var, use let or const instead.
66
63
 
67
- Lint and format at compiler speed: up to 800x faster than ESLint.
64
+ 1 var x: number = 3;
65
+ ~~~~~~~~~~~~~~~~~~
68
66
 
69
- `@ttsc/lint` replaces ESLint and Prettier with rules that run inside the type-check. It shares one AST pass with the compiler, so linting and formatting add almost nothing to the build you already run.
67
+ Found 3 errors in the same file, starting at: src/index.ts:3
68
+ ```
70
69
 
71
- Configuration is a single file. Each rule takes `"error"`, `"warning"`, or `"off"`, and the `format` block mirrors `.prettierrc`.
70
+ Type errors and lint violations arrive in one stream, so the CI step that already runs `ttsc --noEmit` gates lint with no second job and no second parse. On vscode's 6,093 files the rules take 73 ms inside that check, where ESLint spends 66.7 s as its own command.
72
71
 
73
- ```bash
74
- npm install -D @ttsc/lint
75
- ```
72
+ `npx ttsc fix` applies autofixes and formatting; `npx ttsc format` only formats. Rules and every `format` key are in the [Lint and Format guide](https://ttsc.dev/docs/lint).
76
73
 
77
- ```ts
78
- // lint.config.ts
79
- import type { ITtscLintConfig } from "@ttsc/lint";
80
-
81
- export default {
82
- rules: {
83
- "no-var": "error",
84
- "prefer-const": "error",
85
- "typescript/no-explicit-any": "warning",
86
- },
87
- format: {
88
- printWidth: 100,
89
- singleQuote: true,
90
- trailingComma: "all",
91
- },
92
- } satisfies ITtscLintConfig;
74
+ ## Evidence Graph
75
+
76
+ Your spec becomes a compile error, so requirement coverage is 100% or the build does not pass. An agent can still lie, but it cannot lie by omission.
77
+
78
+ ```tsx
79
+ /**
80
+ * @evidence docs/discount.md#coupon-stacking
81
+ * States the per-issuer stacking limit
82
+ * this section defines, in the buyer's words.
83
+ * @evidence POST:/orders/{orderId}/coupons
84
+ * Explains the rejection this endpoint returns
85
+ * for an over-stacked coupon set.
86
+ * @evidence {@link hooks.useCouponStacking} Renders the limit this hook resolves.
87
+ */
88
+ export function CouponStackingNotice(props: IProps): JSX.Element;
93
89
  ```
94
90
 
95
- Violations surface as compiler diagnostics, in the same stream as type errors, so the CI step that already runs `ttsc --noEmit` gates lint without a second job:
91
+ `@evidence <target> <reason>` names one unit of the spec and why this declaration answers for it. A target is a document section, an API operation, a database schema model, or a TypeScript symbol as an inline link.
96
92
 
97
- ```ts
98
- // src/index.ts
99
- var count = 3;
100
- let total = count;
93
+ ```bash
94
+ $ npx ttsc
95
+ error TS16411: [evidence/graph] Missing acknowledgement for 'docs/discount.md#coupon-stacking'
96
+ (Markdown H2 'Coupon Stacking' at docs/discount.md:3)
97
+ in Claim 1 reference 1 (markdown, symbols: h2, h3).
98
+
99
+ Cite the artifact that answers for this unit with @evidence on a selected
100
+ typescript host, building that artifact first when none does, or write
101
+ @evidenceExclude on an eligible carrier when nothing here owes it. Never
102
+ leave an untrue tag standing just to pass this check; it removes the error,
103
+ not the problem.
104
+
105
+ Found 3 errors.
101
106
  ```
102
107
 
103
- ```text
104
- $ npx ttsc --noEmit
105
- src/index.ts:1:1 - error TS11966: [no-var] Unexpected var, use let or const instead.
108
+ Without those tags, the build fails once per obligation, because one reference never covers another. An AI coding agent has to clear them to finish, and clearing them means citing each target and writing down why its code answers for it.
106
109
 
107
- 1 var count = 3;
108
- ~~~
110
+ ![Coverage and token spend, Plain against Evidence](https://raw.githubusercontent.com/samchon/ttsc/gh-pages/benchmark/png/evidence-summary.png)
109
111
 
110
- src/index.ts:2:5 - error TS17397: [prefer-const] Use const instead of let.
112
+ ## Compiler Knowledge Graph
111
113
 
112
- 2 let total = count;
113
- ~~~~~
114
+ Your coding agent answers from the compiler instead of grepping and re-reading files.
114
115
 
115
- Found 2 errors in the same file, starting at: src/index.ts:1
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "ttsc-graph": {
120
+ "command": "npx",
121
+ "args": ["-y", "@ttsc/graph"]
122
+ }
123
+ }
124
+ }
116
125
  ```
117
126
 
118
- Clean the project in place:
127
+ One typed MCP tool over a graph the type checker resolved: what calls what, what a change would touch, where to start reading. Answers carry names, signatures, edges, and spans, never file bodies, so a large repository cannot inflate the response.
119
128
 
120
- ```bash
121
- npx ttsc fix # lint autofixes + format edits
122
- npx ttsc format # format edits only, never changes behavior
123
- ```
129
+ Across 64 measured question and model pairs, the median answer costs 92% fewer tokens and 95% fewer tool calls than the same agent with no MCP. The design and the comparators are in [`@ttsc/graph`](https://github.com/samchon/ttsc/tree/master/packages/graph).
124
130
 
125
- The rule catalog and every `format` key are in the [Lint & Format guide](https://ttsc.dev/docs/lint).
131
+ ![Median tokens on the shared onboarding question, lower is better](https://raw.githubusercontent.com/samchon/ttsc/gh-pages/benchmark/png/graph-common-codex-gpt-5.6-sol.png)
126
132
 
127
133
  ## Plugins
128
134
 
@@ -148,6 +154,7 @@ export const isStringArray = (() => {
148
154
  Utility plugins shipped in this repository:
149
155
 
150
156
  - [`@ttsc/banner`](https://github.com/samchon/ttsc/tree/master/packages/banner): adds `@packageDocumentation` JSDoc banners.
157
+ - [`@ttsc/evidence`](https://github.com/samchon/ttsc/tree/master/packages/evidence): turns a requirement into a compile error until code, tests, or docs acknowledge it by name.
151
158
  - [`@ttsc/graph`](https://github.com/samchon/ttsc/tree/master/packages/graph): MCP server exposing a checker-resolved code graph to coding agents.
152
159
  - [`@ttsc/lint`](https://github.com/samchon/ttsc/tree/master/packages/lint): lints and formats TypeScript source.
153
160
  - [`@ttsc/paths`](https://github.com/samchon/ttsc/tree/master/packages/paths): rewrites source path aliases so JS and declaration emit receive relative imports.
@@ -0,0 +1,69 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "path/filepath"
7
+ "strings"
8
+ "testing"
9
+ )
10
+
11
+ // TestServeAnswersAProjectThatImplementsItsOwnAmbientDeclaration verifies shard
12
+ // assembly succeeds for a project that declares an ambient global in its own
13
+ // `.d.ts` and assigns the implementation elsewhere.
14
+ //
15
+ // Shard partition indexes edge sources by non-external node, and external nodes
16
+ // live in a non-source shard that may own no edges. An edge leaving one is
17
+ // therefore not a wrong fact but an unassemblable snapshot: the session refused
18
+ // the whole transaction with "edge source node is absent from shard facts", so
19
+ // every request failed before it ran while `dump`, which applies no such check,
20
+ // still emitted the graph.
21
+ //
22
+ // 1. Serve a project whose `src/globals.d.ts` declares `var patched` and whose
23
+ // `src/index.ts` assigns an arrow function to it.
24
+ // 2. Request one negotiated shard snapshot.
25
+ // 3. Assert the response carries a complete transaction and no error.
26
+ func TestServeAnswersAProjectThatImplementsItsOwnAmbientDeclaration(t *testing.T) {
27
+ root := t.TempDir()
28
+ writeGraphFile(t, filepath.Join(root, "tsconfig.json"), `{
29
+ "compilerOptions": { "target": "ES2022", "module": "commonjs", "strict": true },
30
+ "include": ["src"]
31
+ }`)
32
+ writeGraphFile(t, filepath.Join(root, "src", "globals.d.ts"), `declare global {
33
+ var patched: (message: string) => void;
34
+ }
35
+ export {};
36
+ `)
37
+ writeGraphFile(t, filepath.Join(root, "src", "index.ts"), `export function helper(): void {}
38
+ patched = (message: string): void => {
39
+ helper();
40
+ };
41
+ `)
42
+
43
+ input := strings.NewReader("{\"id\":1,\"graphSnapshotVersion\":1}\n")
44
+ var output bytes.Buffer
45
+ if code := serveSnapshots(input, &output, root, "tsconfig.json"); code != 0 {
46
+ t.Fatalf("serveSnapshots exited %d: %s", code, output.String())
47
+ }
48
+
49
+ var initial serveResponse
50
+ if err := json.NewDecoder(&output).Decode(&initial); err != nil {
51
+ t.Fatal(err)
52
+ }
53
+ if initial.Error != "" {
54
+ t.Fatalf("shard assembly refused a project that implements its own ambient declaration: %s", initial.Error)
55
+ }
56
+ if initial.Mode != serveModeInitial || !initial.Changed || initial.Dump != nil || initial.Snapshot == nil {
57
+ t.Fatalf("negotiated initial response: %#v", initial)
58
+ }
59
+ if len(initial.Snapshot.Manifest) == 0 ||
60
+ len(initial.Snapshot.Upserts) != len(initial.Snapshot.Manifest) ||
61
+ len(initial.Snapshot.Deletes) != 0 {
62
+ t.Fatalf(
63
+ "initial transaction is not complete: manifest=%d upserts=%d deletes=%d",
64
+ len(initial.Snapshot.Manifest),
65
+ len(initial.Snapshot.Upserts),
66
+ len(initial.Snapshot.Deletes),
67
+ )
68
+ }
69
+ }
@@ -0,0 +1,84 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // ambientGlobalFixtureTSConfig compiles a project that declares an ambient
11
+ // global in its own `.d.ts` and assigns the implementation in a sibling source.
12
+ // Both files are named because a global augmentation only reaches the program
13
+ // when its declaration file is an input.
14
+ const ambientGlobalFixtureTSConfig = `{
15
+ "compilerOptions": {
16
+ "target": "ES2022",
17
+ "module": "commonjs",
18
+ "strict": true,
19
+ "rootDir": "src",
20
+ "outDir": "dist"
21
+ },
22
+ "files": ["src/globals.d.ts", "src/main.ts"]
23
+ }
24
+ `
25
+
26
+ // TestAssignedImplementationsOfDeclarationFileSymbolsLeaveNoEdgeSource verifies
27
+ // that assigning a function to a symbol declared in a declaration file records
28
+ // no edge leaving the external boundary leaf that symbol enters the graph as.
29
+ //
30
+ // An external leaf has no outgoing side: the graph never walks a dependency's
31
+ // internals, and no shard may hold an edge whose source is one — an external
32
+ // node is owned by the non-source external shard, which is forbidden to own
33
+ // edges. Attributing the assigned body to it therefore did not merely misplace
34
+ // facts (with evidence offsets from the implementation read against the
35
+ // declaration file); it produced a snapshot `ttscgraph serve` could not
36
+ // assemble, so every request against such a project failed. The calls the body
37
+ // makes are not lost by refusing it: the module that runs the assignment owns
38
+ // them.
39
+ //
40
+ // 1. Compile a fixture whose `src/globals.d.ts` declares `var patched` in a
41
+ // global augmentation and whose `src/main.ts` assigns an arrow function to
42
+ // it that calls a local `helper`.
43
+ // 2. Build the graph.
44
+ // 3. Assert no edge leaves an external node, and that `main.ts`'s module node
45
+ // still owns the value-call edge to `helper`.
46
+ func TestAssignedImplementationsOfDeclarationFileSymbolsLeaveNoEdgeSource(t *testing.T) {
47
+ root := t.TempDir()
48
+ writeFile(t, filepath.Join(root, "tsconfig.json"), ambientGlobalFixtureTSConfig)
49
+ writeFile(t, filepath.Join(root, "src", "globals.d.ts"), `declare global {
50
+ var patched: (message: string) => void;
51
+ }
52
+ export {};
53
+ `)
54
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export function helper(): void {}
55
+ patched = (message: string): void => {
56
+ helper();
57
+ };
58
+ `)
59
+
60
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
61
+ if err != nil {
62
+ t.Fatal(err)
63
+ }
64
+ if len(diags) != 0 {
65
+ t.Fatalf("unexpected diagnostics: %v", diags)
66
+ }
67
+ defer func() { _ = prog.Close() }()
68
+
69
+ graph := Build(prog)
70
+ mainPath := sourceFile(t, prog, "main.ts").FileName()
71
+
72
+ for _, edge := range graph.Edges {
73
+ node, ok := graph.Nodes[edge.From]
74
+ if ok && node.External {
75
+ t.Fatalf("edge leaves the external boundary: %s -> %s (%s), source declared in %s", edge.From, edge.To, edge.Kind, node.File)
76
+ }
77
+ }
78
+
79
+ module := moduleID(mainPath)
80
+ helper := nodeID(mainPath, "helper", NodeFunction)
81
+ if !hasEdge(graph, module, helper, EdgeValueCall) {
82
+ t.Fatalf("the assigned body's call to helper is not owned by the running module; edges: %v", graph.Edges)
83
+ }
84
+ }
@@ -0,0 +1,85 @@
1
+ package graph
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+
7
+ "github.com/samchon/ttsc/packages/ttsc/driver"
8
+ )
9
+
10
+ // nodeModulesGlobalFixtureTSConfig compiles a project whose ambient global is
11
+ // declared by a dependency's raw `.ts` source. Both inputs are named because
12
+ // the declaring file is reached by neither an import nor a glob, and no
13
+ // `rootDir` or `outDir` is set because a `node_modules` input under one would
14
+ // be an input outside the root.
15
+ const nodeModulesGlobalFixtureTSConfig = `{
16
+ "compilerOptions": {
17
+ "target": "ES2022",
18
+ "module": "commonjs",
19
+ "strict": true
20
+ },
21
+ "files": ["node_modules/dep/globals.ts", "src/main.ts"]
22
+ }
23
+ `
24
+
25
+ // TestAssignedImplementationsOfNodeModulesSymbolsLeaveNoEdgeSource verifies the
26
+ // other half of the external boundary: a symbol declared under `node_modules`
27
+ // gains no outgoing edge from an assignment either.
28
+ //
29
+ // `IsWorkspaceSourceFile` refuses a file for two disjoint reasons, and only one
30
+ // of them is being a declaration file. A dependency that ships raw TypeScript
31
+ // is not a declaration file and still owns no facts in this graph, so an
32
+ // assignment to something it declares must be refused by the same rule. Without
33
+ // this case the fix is pinned on one branch of the classifier and a change to
34
+ // the other would pass.
35
+ //
36
+ // 1. Compile a fixture whose `node_modules/dep/globals.ts` declares `var
37
+ // patched` in a global augmentation and whose `src/main.ts` assigns an arrow
38
+ // function to it that calls a local `helper`.
39
+ // 2. Build the graph.
40
+ // 3. Assert no edge leaves an external node, and that `main.ts`'s module node
41
+ // still owns the value-call edge to `helper`.
42
+ func TestAssignedImplementationsOfNodeModulesSymbolsLeaveNoEdgeSource(t *testing.T) {
43
+ root := t.TempDir()
44
+ writeFile(t, filepath.Join(root, "tsconfig.json"), nodeModulesGlobalFixtureTSConfig)
45
+ writeFile(t, filepath.Join(root, "node_modules", "dep", "package.json"), `{
46
+ "name": "dep",
47
+ "version": "1.0.0"
48
+ }
49
+ `)
50
+ writeFile(t, filepath.Join(root, "node_modules", "dep", "globals.ts"), `declare global {
51
+ var patched: (message: string) => void;
52
+ }
53
+ export {};
54
+ `)
55
+ writeFile(t, filepath.Join(root, "src", "main.ts"), `export function helper(): void {}
56
+ patched = (message: string): void => {
57
+ helper();
58
+ };
59
+ `)
60
+
61
+ prog, diags, err := driver.LoadProgram(root, "tsconfig.json", driver.LoadProgramOptions{})
62
+ if err != nil {
63
+ t.Fatal(err)
64
+ }
65
+ if len(diags) != 0 {
66
+ t.Fatalf("unexpected diagnostics: %v", diags)
67
+ }
68
+ defer func() { _ = prog.Close() }()
69
+
70
+ graph := Build(prog)
71
+ mainPath := sourceFile(t, prog, "main.ts").FileName()
72
+
73
+ for _, edge := range graph.Edges {
74
+ node, ok := graph.Nodes[edge.From]
75
+ if ok && node.External {
76
+ t.Fatalf("edge leaves the external boundary: %s -> %s (%s), source declared in %s", edge.From, edge.To, edge.Kind, node.File)
77
+ }
78
+ }
79
+
80
+ module := moduleID(mainPath)
81
+ helper := nodeID(mainPath, "helper", NodeFunction)
82
+ if !hasEdge(graph, module, helper, EdgeValueCall) {
83
+ t.Fatalf("the assigned body's call to helper is not owned by the running module; edges: %v", graph.Edges)
84
+ }
85
+ }
@@ -604,6 +604,20 @@ func (g *Graph) callsWithin(checker *shimchecker.Checker, from string, node *shi
604
604
  })
605
605
  }
606
606
 
607
+ // assignedFunctionTarget returns the graph node an `x.y = function` assignment
608
+ // implements, or "" when the assignment implements no node this graph owns.
609
+ //
610
+ // A declaration outside the workspace is not such a node. `globalThis.assert =
611
+ // …` against a `declare global { function assert(…) }`, or a patched member of
612
+ // a dependency's `@types`, resolves to a symbol whose only declaration is in a
613
+ // declaration file — which has no body, holds no facts, and enters the graph as
614
+ // an external boundary leaf. Re-attributing the assigned body to it moved the
615
+ // implementation's calls and type references onto a node that never ran them,
616
+ // under offsets read against the wrong file: `recordImplementation` already
617
+ // refuses an external node, so the evidence file stayed unset and the spans
618
+ // pointed at neither the declaration nor the implementation. And the edge
619
+ // itself was unassemblable, so every request against such a project failed at
620
+ // shard partition.
607
621
  func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string, node *shimast.Node) string {
608
622
  binary := node.AsBinaryExpression()
609
623
  if binary == nil ||
@@ -614,7 +628,7 @@ func (g *Graph) assignedFunctionTarget(checker *shimchecker.Checker, from string
614
628
  return ""
615
629
  }
616
630
  target := g.resolve(checker, binary.Left)
617
- if target == nil || target.Symbol == nil {
631
+ if target == nil || target.Symbol == nil || target.External {
618
632
  return ""
619
633
  }
620
634
  to := g.ensureTargetNode(target)
@@ -11,10 +11,8 @@ import (
11
11
  "strings"
12
12
  "sync"
13
13
  "sync/atomic"
14
- "time"
15
14
  )
16
15
 
17
- const nativePluginCommandTimeout = 30 * time.Second
18
16
  const nativePluginCommandStdoutLimit = 4 * 1024 * 1024
19
17
  const nativePluginCommandStderrLimit = 1024 * 1024
20
18
 
@@ -825,7 +823,11 @@ func (s *NativePluginSource) runWithStdin(plugin NativeLSPPluginEntry, command s
825
823
  if strings.TrimSpace(plugin.Binary) == "" {
826
824
  return nil, fmt.Errorf("ttscserver: %s has no binary", pluginLabel(plugin))
827
825
  }
828
- ctx, cancel := context.WithTimeout(context.Background(), nativePluginCommandTimeout)
826
+ // No deadline: the command is running the user's own rules, and how long
827
+ // that takes is not this process's call. The context stays so that a future
828
+ // early return cannot leave the child running — cancelling it kills the
829
+ // process rather than abandoning it.
830
+ ctx, cancel := context.WithCancel(context.Background())
829
831
  defer cancel()
830
832
  allArgs := []string{
831
833
  command,
@@ -848,9 +850,6 @@ func (s *NativePluginSource) runWithStdin(plugin NativeLSPPluginEntry, command s
848
850
  cmd.Stdout = &stdout
849
851
  cmd.Stderr = &stderr
850
852
  err := cmd.Run()
851
- if ctx.Err() != nil {
852
- return nil, fmt.Errorf("ttscserver: %s %s timed out", pluginLabel(plugin), command)
853
- }
854
853
  if err != nil {
855
854
  msg := strings.TrimSpace(stderr.String())
856
855
  if msg == "" {
@@ -9,15 +9,8 @@ import (
9
9
  "os/exec"
10
10
  "strings"
11
11
  "sync"
12
- "time"
13
12
  )
14
13
 
15
- // residentRequestTimeout bounds one resident verb round trip, mirroring the
16
- // spawn-per-verb path's nativePluginCommandTimeout. A rule that hangs must not
17
- // wedge the daemon's request mutex forever; on timeout the sidecar is killed and
18
- // the caller falls back to a fresh spawn.
19
- const residentRequestTimeout = nativePluginCommandTimeout
20
-
21
14
  // The serve verbs below are routed to the resident daemon because they load a
22
15
  // Program.
23
16
  // lsp-command-ids / lsp-code-action-kinds run once at startup and never load one
@@ -121,8 +114,10 @@ func (s *NativePluginSource) serveRun(plugin NativeLSPPluginEntry, verb string,
121
114
  }
122
115
 
123
116
  // call sends one request to the daemon and reads its reply, serializing access
124
- // to the single pipe. It spawns the child on first use or after a death, and
125
- // enforces a per-request timeout so a hung rule cannot wedge the mutex.
117
+ // to the single pipe. It spawns the child on first use or after a death. The
118
+ // read is unbounded — the rule is the user's own code — and the mutex is held
119
+ // only as long as the sidecar is alive, because its death closes stdout and
120
+ // ends the read.
126
121
  func (sc *residentSidecar) call(s *NativePluginSource, plugin NativeLSPPluginEntry, req serveClientRequest) ([]byte, int, error) {
127
122
  sc.mu.Lock()
128
123
  defer sc.mu.Unlock()
@@ -162,23 +157,21 @@ func (sc *residentSidecar) call(s *NativePluginSource, plugin NativeLSPPluginEnt
162
157
  raw, err := sc.stdout.ReadBytes('\n')
163
158
  done <- readResult{line: raw, err: err}
164
159
  }()
165
- select {
166
- case <-time.After(residentRequestTimeout):
160
+ // No deadline: a rule that takes a long time is the user's own code running,
161
+ // and the read still ends the moment the sidecar dies, because closing its
162
+ // stdout surfaces here as a read error.
163
+ res := <-done
164
+ if res.err != nil {
167
165
  sc.kill()
168
- return nil, 0, fmt.Errorf("ttscserver: %s %s (resident) timed out", pluginLabel(plugin), req.Verb)
169
- case res := <-done:
170
- if res.err != nil {
171
- sc.kill()
172
- return nil, 0, res.err
173
- }
174
- var resp serveClientResponse
175
- if err := json.Unmarshal(res.line, &resp); err != nil {
176
- sc.kill()
177
- return nil, 0, err
178
- }
179
- sc.everServed = true
180
- return resp.Result, resp.Code, nil
166
+ return nil, 0, res.err
167
+ }
168
+ var resp serveClientResponse
169
+ if err := json.Unmarshal(res.line, &resp); err != nil {
170
+ sc.kill()
171
+ return nil, 0, err
181
172
  }
173
+ sc.everServed = true
174
+ return resp.Result, resp.Code, nil
182
175
  }
183
176
 
184
177
  // spawn starts the resident child with the base project args and the lsp-serve
@@ -1,9 +1,4 @@
1
1
  import type { ITtscCompilerContext } from "./structures/ITtscCompilerContext";
2
- /** Construction controls for the resident transform request lifecycle. */
3
- export interface TtscServiceOptions {
4
- /** Maximum time one transform or update request may await the host's reply. */
5
- requestTimeoutMs?: number;
6
- }
7
2
  /** Per-call controls for a resident transform or update request. */
8
3
  export interface TtscServiceRequestOptions {
9
4
  /** Abort this call before it receives a reply. */
@@ -41,7 +36,7 @@ export declare class TtscService {
41
36
  * host. The context is the same shape {@link TtscCompiler} accepts; it is not
42
37
  * replaceable per call.
43
38
  */
44
- constructor(context?: ITtscCompilerContext, options?: TtscServiceOptions);
39
+ constructor(context?: ITtscCompilerContext);
45
40
  /**
46
41
  * Return the transformed TypeScript source for one file, or `undefined` when
47
42
  * the resident program does not contain it (for example a file excluded from
@@ -38,14 +38,14 @@ class TtscService {
38
38
  * host. The context is the same shape {@link TtscCompiler} accepts; it is not
39
39
  * replaceable per call.
40
40
  */
41
- constructor(context = {}, options = {}) {
41
+ constructor(context = {}) {
42
42
  const started = (0, startResidentTransform_1.startResidentTransform)({
43
43
  ...context,
44
44
  env: context.env ? { ...context.env } : undefined,
45
45
  plugins: Array.isArray(context.plugins)
46
46
  ? [...context.plugins]
47
47
  : context.plugins,
48
- }, options);
48
+ });
49
49
  this.resident = started.process;
50
50
  this.projectRoot = started.projectRoot;
51
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TtscService.js","sourceRoot":"","sources":["../src/TtscService.ts"],"names":[],"mappings":";;;;;;AAAA,0DAA6B;AAG7B,uFAAoF;AAepF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;IACmB,QAAQ,CAA2B;IACnC,WAAW,CAAS;IAErC;;;;OAIG;IACH,YACE,OAAO,GAAyB,EAAE,EAClC,OAAO,GAAuB,EAAE;QAEhC,MAAM,OAAO,GAAG,IAAA,+CAAsB,EACpC;YACE,GAAG,OAAO;YACV,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS;YACjD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;gBACrC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;gBACtB,CAAC,CAAC,OAAO,CAAC,OAAO;SACpB,EACD,OAAO,CACR,CAAC;QACF,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IACzC,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,aAAa,CACxB,QAAgB,EAChB,OAAO,GAA8B,EAAE;QAEvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CACvC,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,EACrC,WAAW,EACX,OAAO,CACR,CAAC;QACF,0EAA0E;QAC1E,uEAAuE;QACvE,wEAAwE;QACxE,uEAAuE;QACvE,OAAO,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;YACjE,CAAC,CAAC,KAAK,CAAC,UAAU;YAClB,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,UAAU,CACrB,QAAgB,EAChB,OAAe,EACf,OAAO,GAA8B,EAAE;QAEvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CACvC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,EAChD,QAAQ,EACR,OAAO,CACR,CAAC;QACF,4EAA4E;QAC5E,4EAA4E;QAC5E,yDAAyD;QACzD,OAAO,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC;IAChC,CAAC;IAED,qEAAqE;IAC9D,OAAO;QACZ,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC1B,CAAC;IAEO,YAAY,CAAC,QAAgB;QACnC,OAAO,mBAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAC9B,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,mBAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC/C,CAAC;CACF"}
1
+ {"version":3,"file":"TtscService.js","sourceRoot":"","sources":["../src/TtscService.ts"],"names":[],"mappings":";;;;;;AAAA,0DAA6B;AAG7B,uFAAoF;AASpF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;IACmB,QAAQ,CAA2B;IACnC,WAAW,CAAS;IAErC;;;;OAIG;IACH,YAAmB,OAAO,GAAyB,EAAE;QACnD,MAAM,OAAO,GAAG,IAAA,+CAAsB,EAAC;YACrC,GAAG,OAAO;YACV,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS;YACjD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;gBACrC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;gBACtB,CAAC,CAAC,OAAO,CAAC,OAAO;SACpB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IACzC,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,aAAa,CACxB,QAAgB,EAChB,OAAO,GAA8B,EAAE;QAEvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CACvC,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,EACrC,WAAW,EACX,OAAO,CACR,CAAC;QACF,0EAA0E;QAC1E,uEAAuE;QACvE,wEAAwE;QACxE,uEAAuE;QACvE,OAAO,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;YACjE,CAAC,CAAC,KAAK,CAAC,UAAU;YAClB,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,UAAU,CACrB,QAAgB,EAChB,OAAe,EACf,OAAO,GAA8B,EAAE;QAEvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CACvC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,EAChD,QAAQ,EACR,OAAO,CACR,CAAC;QACF,4EAA4E;QAC5E,4EAA4E;QAC5E,yDAAyD;QACzD,OAAO,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC;IAChC,CAAC;IAED,qEAAqE;IAC9D,OAAO;QACZ,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC1B,CAAC;IAEO,YAAY,CAAC,QAAgB;QACnC,OAAO,mBAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAC9B,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,mBAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC/C,CAAC;CACF"}
@@ -0,0 +1,26 @@
1
+ export interface CapturedProcessOutput {
2
+ /** Close the descriptors and remove the backing files. */
3
+ dispose(): void;
4
+ /** Read one stream's bytes, decoded unless `"buffer"` is asked for. */
5
+ read(stream: "stdout" | "stderr", encoding: BufferEncoding | "buffer" | undefined): string | Buffer;
6
+ stderrFd: number;
7
+ stdoutFd: number;
8
+ }
9
+ /**
10
+ * A pair of temporary files standing in for a child process's pipes.
11
+ *
12
+ * `spawnSync` holds a _piped_ stream in this process's memory and refuses to
13
+ * keep more than `maxBuffer` bytes, so any piped capture has to name a ceiling
14
+ * — and a ceiling is a number nobody chose for this machine, deciding on the
15
+ * user's behalf that a large but legitimate build said too much. Handing the
16
+ * child a file descriptor instead means the bytes never pass through this heap
17
+ * on their way out of the child: how much a process may write is the
18
+ * filesystem's business, and that is the same answer on every machine. Reading
19
+ * the result back still materializes a string, so V8's own maximum string
20
+ * length remains the outer bound — but that is a property of the runtime rather
21
+ * than a budget chosen here, and it is identical everywhere this runs.
22
+ *
23
+ * The directory is per-call, so two concurrent spawns cannot read each other's
24
+ * bytes.
25
+ */
26
+ export declare function captureProcessOutput(): CapturedProcessOutput;