@ttsc/graph 0.30.4 → 0.31.1

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 (292) hide show
  1. package/README.md +122 -30
  2. package/lib/CapturedProcessOutput.d.ts +47 -0
  3. package/lib/CapturedProcessOutput.js +3 -0
  4. package/lib/CapturedProcessOutput.js.map +1 -0
  5. package/lib/ILauncherOption.d.ts +22 -0
  6. package/lib/ILauncherOption.js +3 -0
  7. package/lib/ILauncherOption.js.map +1 -0
  8. package/lib/IProjectOptions.d.ts +18 -0
  9. package/lib/IProjectOptions.js +3 -0
  10. package/lib/IProjectOptions.js.map +1 -0
  11. package/lib/ParsedLauncherOptions.d.ts +13 -0
  12. package/lib/ParsedLauncherOptions.js +3 -0
  13. package/lib/ParsedLauncherOptions.js.map +1 -0
  14. package/lib/TtscGraphApplication.d.ts +38 -1
  15. package/lib/TtscGraphApplication.js +26 -1
  16. package/lib/TtscGraphApplication.js.map +1 -1
  17. package/lib/TtscGraphLauncherArguments.d.ts +115 -0
  18. package/lib/TtscGraphLauncherArguments.js +181 -0
  19. package/lib/TtscGraphLauncherArguments.js.map +1 -0
  20. package/lib/TtscGraphViewSnapshot.d.ts +35 -0
  21. package/lib/TtscGraphViewSnapshot.js +48 -0
  22. package/lib/TtscGraphViewSnapshot.js.map +1 -0
  23. package/lib/index.d.ts +9 -24
  24. package/lib/index.js +6 -128
  25. package/lib/index.js.map +1 -1
  26. package/lib/internal/GraphProcessTrace.d.ts +1 -0
  27. package/lib/internal/GraphProcessTrace.js +235 -0
  28. package/lib/internal/GraphProcessTrace.js.map +1 -0
  29. package/lib/launcherArgs.d.ts +75 -16
  30. package/lib/launcherArgs.js +77 -8
  31. package/lib/launcherArgs.js.map +1 -1
  32. package/lib/model/IArtifactDirectory.d.ts +26 -0
  33. package/lib/model/IArtifactDirectory.js +3 -0
  34. package/lib/model/IArtifactDirectory.js.map +1 -0
  35. package/lib/model/IArtifactInputs.d.ts +19 -0
  36. package/lib/model/IArtifactInputs.js +3 -0
  37. package/lib/model/IArtifactInputs.js.map +1 -0
  38. package/lib/model/IPublishedArtifacts.d.ts +76 -0
  39. package/lib/model/IPublishedArtifacts.js +3 -0
  40. package/lib/model/IPublishedArtifacts.js.map +1 -0
  41. package/lib/model/IPublishedArtifactsResident.d.ts +19 -0
  42. package/lib/model/IPublishedArtifactsResident.js +3 -0
  43. package/lib/model/IPublishedArtifactsResident.js.map +1 -0
  44. package/lib/model/ITtscLintDaemonTarget.d.ts +20 -0
  45. package/lib/model/ITtscLintDaemonTarget.js +3 -0
  46. package/lib/model/ITtscLintDaemonTarget.js.map +1 -0
  47. package/lib/model/TtscGraphLinePeer.d.ts +146 -0
  48. package/lib/model/TtscGraphLinePeer.js +213 -0
  49. package/lib/model/TtscGraphLinePeer.js.map +1 -0
  50. package/lib/model/TtscGraphMemory.d.ts +156 -16
  51. package/lib/model/TtscGraphMemory.js +228 -222
  52. package/lib/model/TtscGraphMemory.js.map +1 -1
  53. package/lib/model/TtscGraphNativeArguments.d.ts +40 -0
  54. package/lib/model/TtscGraphNativeArguments.js +64 -0
  55. package/lib/model/TtscGraphNativeArguments.js.map +1 -0
  56. package/lib/model/TtscGraphNodeId.d.ts +55 -3
  57. package/lib/model/TtscGraphNodeId.js +40 -2
  58. package/lib/model/TtscGraphNodeId.js.map +1 -1
  59. package/lib/model/TtscGraphProjection.d.ts +156 -0
  60. package/lib/model/TtscGraphProjection.js +390 -0
  61. package/lib/model/TtscGraphProjection.js.map +1 -0
  62. package/lib/model/TtscGraphProtocol.d.ts +28 -0
  63. package/lib/model/TtscGraphProtocol.js +928 -0
  64. package/lib/model/TtscGraphProtocol.js.map +1 -0
  65. package/lib/model/TtscGraphReadonly.d.ts +15 -0
  66. package/lib/model/TtscGraphReadonly.js +3 -0
  67. package/lib/model/TtscGraphReadonly.js.map +1 -0
  68. package/lib/model/TtscGraphRequestOptions.d.ts +16 -0
  69. package/lib/model/TtscGraphRequestOptions.js +3 -0
  70. package/lib/model/TtscGraphRequestOptions.js.map +1 -0
  71. package/lib/model/TtscGraphSession.d.ts +82 -36
  72. package/lib/model/TtscGraphSession.js +164 -1238
  73. package/lib/model/TtscGraphSession.js.map +1 -1
  74. package/lib/model/TtscGraphSessionOptions.d.ts +20 -0
  75. package/lib/model/TtscGraphSessionOptions.js +3 -0
  76. package/lib/model/TtscGraphSessionOptions.js.map +1 -0
  77. package/lib/model/TtscGraphSessionState.d.ts +187 -0
  78. package/lib/model/TtscGraphSessionState.js +417 -0
  79. package/lib/model/TtscGraphSessionState.js.map +1 -0
  80. package/lib/model/TtscGraphShardStore.d.ts +64 -3
  81. package/lib/model/TtscGraphShardStore.js +85 -15
  82. package/lib/model/TtscGraphShardStore.js.map +1 -1
  83. package/lib/model/TtscGraphSourceReader.d.ts +31 -8
  84. package/lib/model/TtscGraphSourceReader.js +37 -13
  85. package/lib/model/TtscGraphSourceReader.js.map +1 -1
  86. package/lib/model/TtscLintDaemon.d.ts +35 -17
  87. package/lib/model/TtscLintDaemon.js +39 -117
  88. package/lib/model/TtscLintDaemon.js.map +1 -1
  89. package/lib/model/TtscLintDaemonState.d.ts +69 -0
  90. package/lib/model/TtscLintDaemonState.js +166 -0
  91. package/lib/model/TtscLintDaemonState.js.map +1 -0
  92. package/lib/model/copyGraphRecords.d.ts +14 -0
  93. package/lib/model/copyGraphRecords.js +19 -0
  94. package/lib/model/copyGraphRecords.js.map +1 -0
  95. package/lib/model/copyGraphSnapshot.d.ts +17 -0
  96. package/lib/model/copyGraphSnapshot.js +33 -0
  97. package/lib/model/copyGraphSnapshot.js.map +1 -0
  98. package/lib/model/loadGraph.d.ts +19 -10
  99. package/lib/model/loadGraph.js +21 -12
  100. package/lib/model/loadGraph.js.map +1 -1
  101. package/lib/model/publishedArtifacts.d.ts +78 -97
  102. package/lib/model/publishedArtifacts.js +271 -85
  103. package/lib/model/publishedArtifacts.js.map +1 -1
  104. package/lib/nativeExecutable.d.ts +19 -8
  105. package/lib/nativeExecutable.js +75 -15
  106. package/lib/nativeExecutable.js.map +1 -1
  107. package/lib/reduce.d.ts +19 -45
  108. package/lib/reduce.js +19 -3
  109. package/lib/reduce.js.map +1 -1
  110. package/lib/resolveGraphBinary.d.ts +9 -0
  111. package/lib/resolveGraphBinary.js +11 -3
  112. package/lib/resolveGraphBinary.js.map +1 -1
  113. package/lib/runGraph.d.ts +21 -0
  114. package/lib/runGraph.js +83 -0
  115. package/lib/runGraph.js.map +1 -0
  116. package/lib/server/IResolvedGraphHandle.d.ts +22 -0
  117. package/lib/server/IResolvedGraphHandle.js +3 -0
  118. package/lib/server/IResolvedGraphHandle.js.map +1 -0
  119. package/lib/server/createServer.d.ts +14 -10
  120. package/lib/server/createServer.js +42 -39
  121. package/lib/server/createServer.js.map +1 -1
  122. package/lib/server/exportSurface.d.ts +23 -1
  123. package/lib/server/exportSurface.js +23 -1
  124. package/lib/server/exportSurface.js.map +1 -1
  125. package/lib/server/formatGraphFailure.d.ts +20 -0
  126. package/lib/server/formatGraphFailure.js +35 -0
  127. package/lib/server/formatGraphFailure.js.map +1 -0
  128. package/lib/server/pathPolicy.d.ts +70 -5
  129. package/lib/server/pathPolicy.js +66 -4
  130. package/lib/server/pathPolicy.js.map +1 -1
  131. package/lib/server/resolveHandle.d.ts +10 -5
  132. package/lib/server/resolveHandle.js +39 -7
  133. package/lib/server/resolveHandle.js.map +1 -1
  134. package/lib/server/resultAudit.d.ts +17 -14
  135. package/lib/server/resultAudit.js +56 -27
  136. package/lib/server/resultAudit.js.map +1 -1
  137. package/lib/server/resultNext.d.ts +26 -1
  138. package/lib/server/resultNext.js +14 -0
  139. package/lib/server/resultNext.js.map +1 -1
  140. package/lib/server/runDetails.d.ts +72 -15
  141. package/lib/server/runDetails.js +104 -69
  142. package/lib/server/runDetails.js.map +1 -1
  143. package/lib/server/runEntrypoints.d.ts +9 -0
  144. package/lib/server/runEntrypoints.js +9 -0
  145. package/lib/server/runEntrypoints.js.map +1 -1
  146. package/lib/server/runLookup.d.ts +13 -3
  147. package/lib/server/runLookup.js +19 -6
  148. package/lib/server/runLookup.js.map +1 -1
  149. package/lib/server/runOverview.d.ts +13 -4
  150. package/lib/server/runOverview.js +13 -4
  151. package/lib/server/runOverview.js.map +1 -1
  152. package/lib/server/runTour.d.ts +9 -0
  153. package/lib/server/runTour.js +33 -65
  154. package/lib/server/runTour.js.map +1 -1
  155. package/lib/server/runTrace.d.ts +25 -1
  156. package/lib/server/runTrace.js +133 -88
  157. package/lib/server/runTrace.js.map +1 -1
  158. package/lib/server/startServer.d.ts +16 -0
  159. package/lib/server/startServer.js +24 -1
  160. package/lib/server/startServer.js.map +1 -1
  161. package/lib/structures/ITtscGraphApplication.d.ts +95 -41
  162. package/lib/structures/ITtscGraphDecorator.d.ts +18 -0
  163. package/lib/structures/ITtscGraphDetails.d.ts +81 -6
  164. package/lib/structures/ITtscGraphDocTag.d.ts +17 -8
  165. package/lib/structures/ITtscGraphDump.d.ts +106 -8
  166. package/lib/structures/ITtscGraphEdge.d.ts +14 -2
  167. package/lib/structures/ITtscGraphEntrypoints.d.ts +81 -6
  168. package/lib/structures/ITtscGraphEscape.d.ts +24 -2
  169. package/lib/structures/ITtscGraphEvidence.d.ts +9 -0
  170. package/lib/structures/ITtscGraphLookup.d.ts +42 -9
  171. package/lib/structures/ITtscGraphNext.d.ts +12 -1
  172. package/lib/structures/ITtscGraphNode.d.ts +38 -11
  173. package/lib/structures/ITtscGraphOverview.d.ts +81 -6
  174. package/lib/structures/ITtscGraphSnapshot.d.ts +97 -2
  175. package/lib/structures/ITtscGraphSpan.d.ts +9 -0
  176. package/lib/structures/ITtscGraphTour.d.ts +83 -9
  177. package/lib/structures/ITtscGraphTrace.d.ts +99 -14
  178. package/lib/structures/RawDump.d.ts +22 -0
  179. package/lib/structures/RawDump.js +3 -0
  180. package/lib/structures/RawDump.js.map +1 -0
  181. package/lib/structures/RawEdge.d.ts +20 -0
  182. package/lib/structures/RawEdge.js +3 -0
  183. package/lib/structures/RawEdge.js.map +1 -0
  184. package/lib/structures/RawNode.d.ts +26 -0
  185. package/lib/structures/RawNode.js +3 -0
  186. package/lib/structures/RawNode.js.map +1 -0
  187. package/lib/structures/TtscGraphArtifactNodeKind.d.ts +9 -0
  188. package/lib/structures/TtscGraphArtifactNodeKind.js +9 -0
  189. package/lib/structures/TtscGraphArtifactNodeKind.js.map +1 -1
  190. package/lib/structures/TtscGraphDumpEdgeKind.d.ts +15 -1
  191. package/lib/structures/TtscGraphDumpNodeKind.d.ts +9 -0
  192. package/lib/structures/TtscGraphEdgeKind.d.ts +13 -4
  193. package/lib/structures/TtscGraphNodeKind.d.ts +9 -0
  194. package/lib/structures/TtscGraphNodeModifier.d.ts +9 -0
  195. package/lib/structures/ViewerLink.d.ts +20 -0
  196. package/lib/structures/ViewerLink.js +3 -0
  197. package/lib/structures/ViewerLink.js.map +1 -0
  198. package/lib/structures/ViewerNode.d.ts +24 -0
  199. package/lib/structures/ViewerNode.js +3 -0
  200. package/lib/structures/ViewerNode.js.map +1 -0
  201. package/lib/structures/ViewerPayload.d.ts +42 -0
  202. package/lib/structures/ViewerPayload.js +3 -0
  203. package/lib/structures/ViewerPayload.js.map +1 -0
  204. package/lib/view.d.ts +9 -0
  205. package/lib/view.js +203 -91
  206. package/lib/view.js.map +1 -1
  207. package/lib/viewer/viewer.js +1 -1
  208. package/package.json +8 -6
  209. package/src/CapturedProcessOutput.ts +50 -0
  210. package/src/ILauncherOption.ts +24 -0
  211. package/src/IProjectOptions.ts +19 -0
  212. package/src/ParsedLauncherOptions.ts +13 -0
  213. package/src/TtscGraphApplication.ts +38 -1
  214. package/src/TtscGraphLauncherArguments.ts +207 -0
  215. package/src/TtscGraphViewSnapshot.ts +46 -0
  216. package/src/index.ts +9 -160
  217. package/src/internal/GraphProcessTrace.ts +242 -0
  218. package/src/launcherArgs.ts +89 -23
  219. package/src/model/IArtifactDirectory.ts +27 -0
  220. package/src/model/IArtifactInputs.ts +21 -0
  221. package/src/model/IPublishedArtifacts.ts +81 -0
  222. package/src/model/IPublishedArtifactsResident.ts +24 -0
  223. package/src/model/ITtscLintDaemonTarget.ts +22 -0
  224. package/src/model/TtscGraphLinePeer.ts +318 -0
  225. package/src/model/TtscGraphMemory.ts +284 -246
  226. package/src/model/TtscGraphNativeArguments.ts +67 -0
  227. package/src/model/TtscGraphNodeId.ts +57 -3
  228. package/src/model/TtscGraphProjection.ts +473 -0
  229. package/src/model/TtscGraphProtocol.ts +90 -0
  230. package/src/model/TtscGraphReadonly.ts +17 -0
  231. package/src/model/TtscGraphRequestOptions.ts +16 -0
  232. package/src/model/TtscGraphSession.ts +196 -475
  233. package/src/model/TtscGraphSessionOptions.ts +22 -0
  234. package/src/model/TtscGraphSessionState.ts +558 -0
  235. package/src/model/TtscGraphShardStore.ts +99 -23
  236. package/src/model/TtscGraphSourceReader.ts +37 -15
  237. package/src/model/TtscLintDaemon.ts +55 -136
  238. package/src/model/TtscLintDaemonState.ts +174 -0
  239. package/src/model/copyGraphRecords.ts +17 -0
  240. package/src/model/copyGraphSnapshot.ts +30 -0
  241. package/src/model/loadGraph.ts +23 -12
  242. package/src/model/publishedArtifacts.ts +361 -182
  243. package/src/nativeExecutable.ts +93 -26
  244. package/src/reduce.ts +24 -51
  245. package/src/resolveGraphBinary.ts +13 -3
  246. package/src/runGraph.ts +90 -0
  247. package/src/server/IResolvedGraphHandle.ts +24 -0
  248. package/src/server/createServer.ts +16 -22
  249. package/src/server/exportSurface.ts +23 -1
  250. package/src/server/formatGraphFailure.ts +32 -0
  251. package/src/server/pathPolicy.ts +70 -5
  252. package/src/server/resolveHandle.ts +45 -14
  253. package/src/server/resultAudit.ts +58 -27
  254. package/src/server/resultNext.ts +26 -1
  255. package/src/server/runDetails.ts +138 -73
  256. package/src/server/runEntrypoints.ts +13 -1
  257. package/src/server/runLookup.ts +23 -7
  258. package/src/server/runOverview.ts +17 -5
  259. package/src/server/runTour.ts +44 -82
  260. package/src/server/runTrace.ts +175 -102
  261. package/src/server/startServer.ts +29 -1
  262. package/src/structures/ITtscGraphApplication.ts +95 -41
  263. package/src/structures/ITtscGraphDecorator.ts +18 -0
  264. package/src/structures/ITtscGraphDetails.ts +81 -6
  265. package/src/structures/ITtscGraphDocTag.ts +17 -8
  266. package/src/structures/ITtscGraphDump.ts +106 -8
  267. package/src/structures/ITtscGraphEdge.ts +14 -2
  268. package/src/structures/ITtscGraphEntrypoints.ts +81 -6
  269. package/src/structures/ITtscGraphEscape.ts +24 -2
  270. package/src/structures/ITtscGraphEvidence.ts +9 -0
  271. package/src/structures/ITtscGraphLookup.ts +42 -9
  272. package/src/structures/ITtscGraphNext.ts +12 -1
  273. package/src/structures/ITtscGraphNode.ts +38 -11
  274. package/src/structures/ITtscGraphOverview.ts +88 -6
  275. package/src/structures/ITtscGraphSnapshot.ts +117 -2
  276. package/src/structures/ITtscGraphSpan.ts +9 -0
  277. package/src/structures/ITtscGraphTour.ts +83 -9
  278. package/src/structures/ITtscGraphTrace.ts +100 -14
  279. package/src/structures/RawDump.ts +25 -0
  280. package/src/structures/RawEdge.ts +22 -0
  281. package/src/structures/RawNode.ts +31 -0
  282. package/src/structures/TtscGraphArtifactNodeKind.ts +9 -0
  283. package/src/structures/TtscGraphDumpEdgeKind.ts +15 -1
  284. package/src/structures/TtscGraphDumpNodeKind.ts +9 -0
  285. package/src/structures/TtscGraphEdgeKind.ts +13 -4
  286. package/src/structures/TtscGraphNodeKind.ts +9 -0
  287. package/src/structures/TtscGraphNodeModifier.ts +9 -0
  288. package/src/structures/ViewerLink.ts +22 -0
  289. package/src/structures/ViewerNode.ts +28 -0
  290. package/src/structures/ViewerPayload.ts +52 -0
  291. package/src/view.ts +208 -110
  292. package/src/viewer/legend.ts +119 -27
@@ -1,29 +1,21 @@
1
1
  import { TtscGraphMemory } from "../model/TtscGraphMemory";
2
+ import { TtscGraphReadonly } from "../model/TtscGraphReadonly";
2
3
  import { ITtscGraphEdge } from "../structures/ITtscGraphEdge";
3
4
  import { ITtscGraphEvidence } from "../structures/ITtscGraphEvidence";
4
- import { ITtscGraphNode } from "../structures/ITtscGraphNode";
5
+ import { ITtscGraphNode as NodeShape } from "../structures/ITtscGraphNode";
5
6
  import { ITtscGraphTrace } from "../structures/ITtscGraphTrace";
6
7
  import { isDeclarationFile, isExternalNode, isTestPath } from "./pathPolicy";
7
8
  import { resolveGraphHandle } from "./resolveHandle";
8
9
  import { IRunnerOutput, resultNext } from "./resultNext";
9
10
  import { edgeEvidenceOf, signatureOf } from "./runDetails";
10
11
 
12
+ type ITtscGraphNode = TtscGraphReadonly<NodeShape>;
13
+
11
14
  const DEFAULT_DEPTH = 3;
12
15
  const DEFAULT_MAX_NODES = 12;
13
- // An open trace used to stop at two hops and eight nodes, whatever depth the
14
- // caller asked for. A question that spans a runtime chain — a state change
15
- // through tracking, scheduling, rendering, then patching — is that chain hop by
16
- // hop, so the model re-issued a trace per hop and paid a round trip for each: a
17
- // single vue question spent twenty-six calls walking a flow the graph could have
18
- // walked once. The cap now follows the chain instead of cutting it.
19
- const MAX_OPEN_DEPTH = 8;
20
- const MAX_OPEN_NODES = 32;
21
- const MAX_IMPACT_DEPTH = 4;
22
- const MAX_IMPACT_NODES = 16;
23
- // Path mode walks further than an open trace because it is looking for one
24
- // named end, not building a picture. The cap is the one the request contract
25
- // publishes for path mode.
26
- const MAX_PATH_DEPTH = 12;
16
+ // Exploratory defaults are bounded; explicit budgets have no upper clamp.
17
+ // Path searches default deeper because they seek one named target.
18
+ const DEFAULT_PATH_DEPTH = 12;
27
19
  const MAX_HOPS_PER_NODE = 2;
28
20
  const MAX_STEPS = 12;
29
21
  const DISPATCH_KINDS = new Set<string>(["overrides", "implements"]);
@@ -33,14 +25,8 @@ const BODYLESS_KINDS = new Set<string>(["interface", "type"]);
33
25
  // `abstract` and `declare` are the two keywords that take the body away from a
34
26
  // declaration that would otherwise have to have one.
35
27
  const BODYLESS_MODIFIERS = new Set<string>(["abstract", "declare"]);
36
- // An interface the codebase implements everywhere — a disposable, a listener, a
37
- // lifecycle hook — is not a step in one flow, and naming its implementors is a
38
- // dump of the codebase rather than an answer. Past this many, the declaration
39
- // stays a leaf and `details` answers `implementedBy` for a caller that wants the
40
- // list. The cut is the graph's existing definition of a hub (see
41
- // `isSharedUtility`): across the benchmark corpus it follows 84–100% of dispatch
42
- // sites per project and refuses only the genuinely polymorphic ones (zod's
43
- // 36-way schema interface, VS Code's 533-way disposable).
28
+ // Bounded exploratory requests leave broad polymorphic fanouts unexpanded;
29
+ // complete mode follows every declared concrete implementation.
44
30
  const DISPATCH_HUB = 12;
45
31
 
46
32
  /**
@@ -48,6 +34,18 @@ const DISPATCH_HUB = 12;
48
34
  * edges are excluded so the path is real call/type flow; forward walks callees,
49
35
  * reverse and impact walk callers. Impact additionally tags each reached node's
50
36
  * role so the blast radius on the public surface is legible.
37
+ *
38
+ * Complete requests exhaust the finite admitted graph, or find one shortest
39
+ * requested path, without traversal budgets.
40
+ *
41
+ * @evidence contracts/common.md#principled-implementation Breadth-first traversal preserves shortest reached depth and original edge direction; path search distinguishes found, depth-bounded, dispatch-fanout-withheld and exhausted outcomes before considering shared junctions, so neither bound is reported as an absence.
42
+ * @evidence contracts/common.md#clear-and-simple-design Handle resolution, eligible edges, dispatch, path search and coordinate summaries have helper owners; this function assembles open or requested-path results.
43
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Ambiguity remains candidates and dispatch follows checker implementation relations; shared references are never inserted as guessed execution edges.
44
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains direction, structural exclusion and impact roles; request documentation states bounds and focus meanings.
45
+ * @evidence contracts/performance.md#efficient-algorithms A visited set expands each node once through indexed adjacency. Complete and path requests may inspect the admitted graph; ordering edges costs up to O(E log E), with additional ownership walks for declaration-body facts. Per-base dispatch and target indexes avoid rebuilding or rescanning the same fanout for each reverse arrival. Path reconstruction is linear in path length, and temporary/output space grows with inspected nodes and edges.
46
+ * @evidence contracts/performance.md#reuse-equivalent-work Forward/reverse operations share immutable generation indexes and resolution helpers. A request-local map reuses bodyless dispatch selection for a base across reverse arrivals; its focus and complete policy remain constant for that request. Trace results remain fresh mutable outputs.
47
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Visited/frontier sets and omitted dispatch candidates exist only during the request; defaults bound selected output, not every inspected frontier edge. Complete requests retain up to the admitted graph nodes and edges until return, without a fixed response-size bound; allocation or transport failures propagate instead of yielding partial success.
48
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation walks in-memory edges; no file, path or process.
51
49
  */
52
50
  export function runTrace(
53
51
  graph: TtscGraphMemory,
@@ -55,20 +53,13 @@ export function runTrace(
55
53
  ): IRunnerOutput<ITtscGraphTrace> {
56
54
  const direction = props.direction ?? "forward";
57
55
  const focus = props.focus ?? "all";
58
- const impact = direction === "impact";
59
- const maxDepth = bound(
60
- props.maxDepth,
61
- DEFAULT_DEPTH,
62
- 1,
63
- impact ? MAX_IMPACT_DEPTH : MAX_OPEN_DEPTH,
64
- );
65
- const maxNodes = bound(
66
- props.maxNodes,
67
- DEFAULT_MAX_NODES,
68
- 1,
69
- impact ? MAX_IMPACT_NODES : MAX_OPEN_NODES,
70
- );
71
- const maxHops = maxNodes * MAX_HOPS_PER_NODE;
56
+ const complete = props.complete === true;
57
+ const dispatch = new Map<string, IDispatchSelection>();
58
+ const maxDepth = complete ? Infinity : bound(props.maxDepth, DEFAULT_DEPTH);
59
+ const maxNodes = complete
60
+ ? Infinity
61
+ : bound(props.maxNodes, DEFAULT_MAX_NODES);
62
+ const maxHops = complete ? Infinity : maxNodes * MAX_HOPS_PER_NODE;
72
63
  const reverse = direction === "reverse" || direction === "impact";
73
64
  const includeExternal = props.includeExternal === true;
74
65
  // Only an impact trace tags reached nodes with their public-surface role; for
@@ -146,7 +137,9 @@ export function runTrace(
146
137
  ),
147
138
  };
148
139
  }
149
- const pathDepth = bound(props.maxDepth, MAX_PATH_DEPTH, 1, MAX_PATH_DEPTH);
140
+ const pathDepth = complete
141
+ ? Infinity
142
+ : bound(props.maxDepth, DEFAULT_PATH_DEPTH);
150
143
  const search = findPath(
151
144
  graph,
152
145
  start.node.id,
@@ -154,6 +147,8 @@ export function runTrace(
154
147
  pathDepth,
155
148
  focus,
156
149
  includeExternal,
150
+ complete,
151
+ dispatch,
157
152
  );
158
153
  const hasPath = search.found !== undefined;
159
154
  const path = search.found?.path ?? [];
@@ -163,7 +158,7 @@ export function runTrace(
163
158
  // presenting a shared symbol as "the seam" when a direct call path may run
164
159
  // past the bound sends the caller to a seam that is not there.
165
160
  const junctions =
166
- hasPath || search.bounded
161
+ hasPath || search.bounded || search.withheld
167
162
  ? []
168
163
  : junctionsBetween(graph, start.node.id, target.node.id, focus);
169
164
  return {
@@ -173,7 +168,8 @@ export function runTrace(
173
168
  target: summary(graph, target.node),
174
169
  hops,
175
170
  path: path.map((node, i) => summary(graph, node, i, false, true)),
176
- steps: traceSteps(graph, hops),
171
+ steps: traceSteps(graph, hops, complete),
172
+ truncated: !hasPath && (search.bounded || search.withheld),
177
173
  ...(junctions.length > 0 ? { junctions } : {}),
178
174
  },
179
175
  // A missing path is a fact, not an answer, and the old message called it
@@ -193,16 +189,18 @@ export function runTrace(
193
189
  ? pathNext
194
190
  : search.bounded
195
191
  ? resultNext("inspect", boundedPathReason(pathDepth), "trace")
196
- : junctions.length > 0
197
- ? resultNext(
198
- "inspect",
199
- "No call path runs between the two ends — a callback stands between them (an event emitter, a subscription, a lifecycle hook), and no call edge crosses one. `junctions` names the symbols both ends touch, which is the seam: trace the junction to see who registers on it and who fires it.",
200
- "trace",
201
- )
202
- : resultNext(
203
- "outside",
204
- "No call path runs from the start to the target and they touch nothing in common, so the graph holds no connection between them.",
205
- ),
192
+ : search.withheld
193
+ ? resultNext("inspect", withheldPathReason(), "details")
194
+ : junctions.length > 0
195
+ ? resultNext(
196
+ "inspect",
197
+ "No call path runs between the two ends — a callback stands between them (an event emitter, a subscription, a lifecycle hook), and no call edge crosses one. `junctions` names the symbols both ends touch, which is the seam: trace the junction to see who registers on it and who fires it.",
198
+ "trace",
199
+ )
200
+ : resultNext(
201
+ "outside",
202
+ "No call path runs from the start to the target and they touch nothing in common, so the graph holds no connection between them.",
203
+ ),
206
204
  };
207
205
  }
208
206
 
@@ -222,6 +220,8 @@ export function runTrace(
222
220
  id,
223
221
  reverse,
224
222
  focus,
223
+ complete,
224
+ dispatch,
225
225
  );
226
226
  // A hop the hub bound withheld is omitted whatever the depth budget does
227
227
  // next, so the flag is set before the boundary check consumes the step.
@@ -310,7 +310,7 @@ export function runTrace(
310
310
  direction,
311
311
  hops,
312
312
  reached: [...reached.values()],
313
- steps: traceSteps(graph, hops),
313
+ steps: traceSteps(graph, hops, complete),
314
314
  truncated,
315
315
  },
316
316
  next: resultNext(
@@ -339,11 +339,13 @@ function traceEdges(
339
339
  id: string,
340
340
  reverse: boolean,
341
341
  focus: ITtscGraphTrace.IRequest["focus"],
342
+ complete: boolean,
343
+ dispatch: Map<string, IDispatchSelection>,
342
344
  ): { edges: readonly ITtscGraphEdge[]; omitted: readonly ITtscGraphEdge[] } {
343
345
  const edges = reverse ? graph.incoming(id) : graph.outgoing(id);
344
346
  const dispatched = reverse
345
- ? reverseDispatchEdges(graph, id, focus)
346
- : dispatchEdges(graph, id, focus);
347
+ ? reverseDispatchEdges(graph, id, focus, complete, dispatch)
348
+ : dispatchEdges(graph, id, focus, complete, dispatch);
347
349
  // Nothing to add is the common case, and a walk visits the nodes with the
348
350
  // largest edge lists, so hand back the stored list rather than a copy of it.
349
351
  return {
@@ -359,6 +361,9 @@ function traceEdges(
359
361
  interface IDispatchSelection {
360
362
  readonly selected: readonly ITtscGraphEdge[];
361
363
  readonly omitted: readonly ITtscGraphEdge[];
364
+
365
+ /** Reuses each fanout edge for reverse arrivals without rescanning siblings. */
366
+ readonly byTarget?: ReadonlyMap<string, ITtscGraphEdge>;
362
367
  }
363
368
 
364
369
  // Returned by every no-dispatch path rather than allocated per call, which is
@@ -389,8 +394,9 @@ function eligibleTraceEndpoint(
389
394
  function traceSteps(
390
395
  graph: TtscGraphMemory,
391
396
  hops: ITtscGraphTrace.IHop[],
397
+ complete: boolean,
392
398
  ): string[] {
393
- return hops.slice(0, MAX_STEPS).map((hop) => {
399
+ return (complete ? hops : hops.slice(0, MAX_STEPS)).map((hop) => {
394
400
  const from = graph.node(hop.from);
395
401
  const to = graph.node(hop.to);
396
402
  const lhs = from?.qualifiedName ?? from?.name ?? hop.from;
@@ -476,7 +482,9 @@ function touchedBy(
476
482
  touched.set(edge.to, {
477
483
  kind: edge.kind,
478
484
  outgoing: true,
479
- ...(edge.evidence !== undefined ? { evidence: edge.evidence } : {}),
485
+ ...(edge.evidence !== undefined
486
+ ? { evidence: { ...edge.evidence } }
487
+ : {}),
480
488
  });
481
489
  }
482
490
  for (const edge of graph.incoming(id)) {
@@ -485,32 +493,37 @@ function touchedBy(
485
493
  touched.set(edge.from, {
486
494
  kind: edge.kind,
487
495
  outgoing: false,
488
- ...(edge.evidence !== undefined ? { evidence: edge.evidence } : {}),
496
+ ...(edge.evidence !== undefined
497
+ ? { evidence: { ...edge.evidence } }
498
+ : {}),
489
499
  });
490
500
  }
491
501
  return touched;
492
502
  }
493
503
 
494
- /**
495
- * What to say when the depth bound, not the graph, ended a path search.
496
- *
497
- * The caller asked a bounded question and the old answer returned a claim about
498
- * the whole graph — "they touch nothing in common, so the graph holds no
499
- * connection between them" — which is the worst thing an index can say wrongly:
500
- * the caller stops asking and either reads files or concludes the dependency is
501
- * not there. So report the boundary, and make the continuation one the caller
502
- * can actually take. At the 12-hop ceiling there is no larger `maxDepth` to
503
- * retry with, and a message that only invites one would be a dead end of its
504
- * own; two bounded walks from opposite ends cover twice the distance and are
505
- * requests the tool already answers.
506
- */
504
+ /** Explain an incomplete path search and its supported complete retry. */
507
505
  function boundedPathReason(depth: number): string {
508
506
  return (
509
507
  `No path was found within the requested depth of ${depth}, but the walk stopped on that bound with eligible graph still ahead of it. ` +
510
508
  `This is a boundary, not an absence: the two ends may be connected further out, and nothing here says they are not. ` +
511
- (depth < MAX_PATH_DEPTH
512
- ? `Re-run the same path request with a larger \`maxDepth\` (up to ${MAX_PATH_DEPTH}).`
513
- : `\`maxDepth\` is already at its ${MAX_PATH_DEPTH}-hop maximum, so close the gap from both ends: trace forward from the start, trace the target with \`direction: "reverse"\`, then request the path between a symbol both results name.`)
509
+ `Re-run with a larger \`maxDepth\` or \`complete: true\` to search the entire admitted graph.`
510
+ );
511
+ }
512
+
513
+ /**
514
+ * What to say when a dispatch fanout past the hub cut, not the graph, ended a
515
+ * path search.
516
+ *
517
+ * A declaration with this many implementations stays a leaf in a trace, so the
518
+ * walk never entered them. Telling the caller the ends touch nothing in common
519
+ * would turn that policy into a claim about the codebase, and the caller would
520
+ * stop asking while the connection ran through one of those implementations.
521
+ */
522
+ function withheldPathReason(): string {
523
+ return (
524
+ `No path was found, but the walk did not follow a dispatch fanout of ${DISPATCH_HUB} or more implementations, so it did not exhaust the graph. ` +
525
+ `This is a boundary of the walk, not an absence: the ends may connect through one of those implementations. ` +
526
+ `Re-run with \`complete: true\` to follow the entire dispatch fanout, or ask \`details\` for the implementation that the question means.`
514
527
  );
515
528
  }
516
529
 
@@ -529,6 +542,13 @@ interface IPathSearch {
529
542
  * still ahead of it, so nothing was proven about the two ends.
530
543
  */
531
544
  bounded: boolean;
545
+
546
+ /**
547
+ * True when no path was found and the walk left a dispatch fanout past the
548
+ * hub cut unfollowed, reaching an implementation no other route visited, so
549
+ * the walk exhausted only the graph it was willing to follow.
550
+ */
551
+ withheld: boolean;
532
552
  }
533
553
 
534
554
  /**
@@ -549,11 +569,17 @@ function findPath(
549
569
  maxDepth: number,
550
570
  focus: ITtscGraphTrace.IRequest["focus"],
551
571
  includeExternal: boolean,
572
+ complete: boolean,
573
+ dispatch: Map<string, IDispatchSelection>,
552
574
  ): IPathSearch {
553
575
  const startNode = graph.node(startId);
554
- if (startNode === undefined) return { bounded: false };
576
+ if (startNode === undefined) return { bounded: false, withheld: false };
555
577
  if (startId === targetId)
556
- return { found: { path: [startNode], hops: [] }, bounded: false };
578
+ return {
579
+ found: { path: [startNode], hops: [] },
580
+ bounded: false,
581
+ withheld: false,
582
+ };
557
583
  const parent = new Map<
558
584
  string,
559
585
  {
@@ -564,13 +590,35 @@ function findPath(
564
590
  >();
565
591
  const visited = new Set<string>([startId]);
566
592
  let bounded = false;
593
+ const withheld = new Set<string>();
567
594
  let queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
568
595
  while (queue.length > 0) {
569
596
  const next: Array<{ id: string; depth: number }> = [];
570
597
  for (const { id, depth } of queue) {
571
598
  // The forward step the open trace would take, built in one place so the
572
599
  // two walks cannot disagree about what a step follows.
573
- const { edges: candidates } = traceEdges(graph, id, false, focus);
600
+ const { edges: candidates, omitted } = traceEdges(
601
+ graph,
602
+ id,
603
+ false,
604
+ focus,
605
+ complete,
606
+ dispatch,
607
+ );
608
+ // A dispatch fanout past the hub cut is not walked, but each hop in it is
609
+ // a real, eligible continuation. Leaving it out of the search while still
610
+ // reporting "no path" would claim an absence the walk never established,
611
+ // unless the walk reaches that implementation some other way.
612
+ for (const edge of omitted) {
613
+ const endpoint = eligibleTraceEndpoint(
614
+ graph,
615
+ edge,
616
+ false,
617
+ focus,
618
+ includeExternal,
619
+ );
620
+ if (endpoint !== undefined) withheld.add(endpoint.otherId);
621
+ }
574
622
  if (depth >= maxDepth) {
575
623
  if (
576
624
  candidates.some(
@@ -605,9 +653,10 @@ function findPath(
605
653
  while (cur !== startId) {
606
654
  const p = parent.get(cur);
607
655
  if (p === undefined) break;
608
- ids.unshift(p.via);
656
+ ids.push(p.via);
609
657
  cur = p.via;
610
658
  }
659
+ ids.reverse();
611
660
  const path: ITtscGraphNode[] = [];
612
661
  for (const nid of ids) {
613
662
  const n = graph.node(nid);
@@ -624,17 +673,17 @@ function findPath(
624
673
  depth: i,
625
674
  };
626
675
  if (parentEdge?.evidence !== undefined)
627
- hop.evidence = parentEdge.evidence;
676
+ hop.evidence = { ...parentEdge.evidence };
628
677
  hops.push(hop);
629
678
  }
630
- return { found: { path, hops }, bounded };
679
+ return { found: { path, hops }, bounded, withheld: false };
631
680
  }
632
681
  next.push({ id: otherId, depth: depth + 1 });
633
682
  }
634
683
  }
635
684
  queue = next;
636
685
  }
637
- return { bounded };
686
+ return { bounded, withheld: [...withheld].some((id) => !visited.has(id)) };
638
687
  }
639
688
 
640
689
  /**
@@ -783,8 +832,12 @@ function dispatchEdges(
783
832
  graph: TtscGraphMemory,
784
833
  id: string,
785
834
  focus: ITtscGraphTrace.IRequest["focus"],
835
+ complete: boolean,
836
+ dispatch: Map<string, IDispatchSelection>,
786
837
  ): IDispatchSelection {
787
838
  if (focus === "types") return NO_DISPATCH;
839
+ const cached = dispatch.get(id);
840
+ if (cached !== undefined) return cached;
788
841
  // The checker relations first: almost no node has one, and reading a
789
842
  // declaration's own facts walks its ownership chain, which is work worth
790
843
  // doing only where there is something to dispatch to.
@@ -792,10 +845,15 @@ function dispatchEdges(
792
845
  for (const edge of graph.incoming(id)) {
793
846
  if (DISPATCH_KINDS.has(edge.kind)) (relations ??= []).push(edge);
794
847
  }
795
- if (relations === undefined) return NO_DISPATCH;
848
+ if (relations === undefined) {
849
+ dispatch.set(id, NO_DISPATCH);
850
+ return NO_DISPATCH;
851
+ }
796
852
  const declaration = graph.node(id);
797
- if (declaration === undefined || hasDeclarationBody(graph, declaration))
853
+ if (declaration === undefined || hasDeclarationBody(graph, declaration)) {
854
+ dispatch.set(id, NO_DISPATCH);
798
855
  return NO_DISPATCH;
856
+ }
799
857
  const out: ITtscGraphEdge[] = [];
800
858
  // Per implementation, not per relation. A class may name one base in two
801
859
  // heritage clauses — `class Impl extends Base implements Base` is legal — and
@@ -817,15 +875,20 @@ function dispatchEdges(
817
875
  from: id,
818
876
  to: edge.from,
819
877
  kind: "dispatches",
820
- ...(edge.evidence !== undefined ? { evidence: edge.evidence } : {}),
878
+ ...(edge.evidence !== undefined
879
+ ? { evidence: { ...edge.evidence } }
880
+ : {}),
821
881
  });
822
882
  }
823
- // Above the hub cut the fanout stops being a trace and starts being a
824
- // listing, so the walk does not follow it — but the hops are real and their
825
- // absence is an omission the caller has to be able to report.
826
- return out.length >= DISPATCH_HUB
827
- ? { selected: [], omitted: out }
828
- : { selected: out, omitted: [] };
883
+ // Bounded walks withhold broad fanouts, retaining omitted edges for truthful
884
+ // truncation. Complete walks follow them without the exploratory hub cut.
885
+ const byTarget = new Map(out.map((edge) => [edge.to, edge]));
886
+ const result =
887
+ !complete && out.length >= DISPATCH_HUB
888
+ ? { selected: [], omitted: out, byTarget }
889
+ : { selected: out, omitted: [], byTarget };
890
+ dispatch.set(id, result);
891
+ return result;
829
892
  }
830
893
 
831
894
  /**
@@ -848,6 +911,8 @@ function reverseDispatchEdges(
848
911
  graph: TtscGraphMemory,
849
912
  id: string,
850
913
  focus: ITtscGraphTrace.IRequest["focus"],
914
+ complete: boolean,
915
+ dispatch: Map<string, IDispatchSelection>,
851
916
  ): IDispatchSelection {
852
917
  if (focus === "types") return NO_DISPATCH;
853
918
  const selected: ITtscGraphEdge[] = [];
@@ -856,13 +921,13 @@ function reverseDispatchEdges(
856
921
  for (const edge of graph.outgoing(id)) {
857
922
  if (!DISPATCH_KINDS.has(edge.kind) || bases.has(edge.to)) continue;
858
923
  bases.add(edge.to);
859
- const fanout = dispatchEdges(graph, edge.to, focus);
860
- for (const dispatch of fanout.selected)
861
- if (dispatch.to === id) selected.push(dispatch);
924
+ const fanout = dispatchEdges(graph, edge.to, focus, complete, dispatch);
925
+ const match = fanout.byTarget?.get(id);
926
+ if (match === undefined) continue;
862
927
  // The base's fanout is bounded as a whole, so a reverse walk that lands on
863
928
  // a suppressed sibling has the same omission to report as the forward one.
864
- for (const dispatch of fanout.omitted)
865
- if (dispatch.to === id) omitted.push(dispatch);
929
+ if (fanout.omitted.length > 0) omitted.push(match);
930
+ else selected.push(match);
866
931
  }
867
932
  return { selected, omitted };
868
933
  }
@@ -886,6 +951,15 @@ function reverseDispatchEdges(
886
951
  * keyword is written; and an external leaf has a body the graph deliberately
887
952
  * does not hold. Everything else is a concrete declaration, which is a real
888
953
  * destination and is never promoted through an override, whatever it calls.
954
+ *
955
+ * @evidence contracts/common.md#principled-implementation Kind, modifiers, declaration-file semantics and containing type facts identify bodyless declarations before dispatch traversal.
956
+ * @evidence contracts/common.md#clear-and-simple-design One shared predicate supplies the body decision to forward and reverse dispatch helpers.
957
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Body presence is not guessed solely from a function-like name or a missing explicit declare keyword.
958
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains ambient and containing-type cases and distinguishes a possible body from execution proof.
959
+ * @evidence contracts/performance.md#efficient-algorithms Kind and modifier checks are constant or modifier-count work, followed by indexed containing-owner traversal rather than a graph-wide scan.
960
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work This shared declaration predicate coordinates no completed or in-flight work; trace and centrality own their producer reuse.
961
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The predicate borrows graph facts and acquires no handle, task or retained cache.
962
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation reads kind, modifier and ownership facts of in-memory nodes; no file, path or process.
889
963
  */
890
964
  export function hasDeclarationBody(
891
965
  graph: TtscGraphMemory,
@@ -906,7 +980,8 @@ function hasBodylessModifier(node: ITtscGraphNode): boolean {
906
980
  * Whether an owner up the `contains` tree makes this declaration bodyless: an
907
981
  * interface, or an ambient container. A member writes no keyword of its own —
908
982
  * `declare` on a class or namespace is not repeated on what it holds — so the
909
- * fact lives on the owner and the walk has to go get it.
983
+ * fact lives on the owner and the walk has to go get it. An abstract class can
984
+ * contain concrete bodies; its abstract modifier applies only to itself.
910
985
  */
911
986
  function inBodylessContainer(
912
987
  graph: TtscGraphMemory,
@@ -919,7 +994,10 @@ function inBodylessContainer(
919
994
  if (container === undefined || container.kind === "file") return false;
920
995
  if (seen.has(container.id)) return false;
921
996
  seen.add(container.id);
922
- if (container.kind === "interface" || hasBodylessModifier(container))
997
+ if (
998
+ container.kind === "interface" ||
999
+ container.modifiers?.includes("declare")
1000
+ )
923
1001
  return true;
924
1002
  current = container;
925
1003
  }
@@ -997,12 +1075,7 @@ function evidenceRank(edge: ITtscGraphEdge): number {
997
1075
  return line * 100 + col;
998
1076
  }
999
1077
 
1000
- function bound(
1001
- value: number | undefined,
1002
- fallback: number,
1003
- min: number,
1004
- max: number,
1005
- ): number {
1078
+ function bound(value: number | undefined, fallback: number): number {
1006
1079
  const n = value === undefined || !Number.isFinite(value) ? fallback : value;
1007
- return Math.max(min, Math.min(max, Math.floor(n)));
1080
+ return Math.max(1, Math.floor(n));
1008
1081
  }
@@ -2,16 +2,35 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2
2
 
3
3
  import { TtscGraphSession } from "../model/TtscGraphSession";
4
4
  import { createServer } from "./createServer";
5
+ import { formatGraphFailure } from "./formatGraphFailure";
5
6
 
6
7
  /**
7
8
  * Serve the graph tools over MCP on stdio. The server answers the MCP handshake
8
9
  * immediately and opens the resident incremental graph session on the first
9
10
  * real tool call, so a large project cannot make the client give up before
10
11
  * tools are advertised and an escape request still performs no graph work.
12
+ *
13
+ * The transport and stdin end events close any lazily created native session.
14
+ *
15
+ * @evidence contracts/common.md#principled-implementation A lazy provider opens the resident graph on demand while the stdio MCP transport advertises tools independently of index creation.
16
+ * @evidence contracts/common.md#clear-and-simple-design This boundary owns transport wiring and session lifetime; createServer owns reflected tools and the session owns native snapshots.
17
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts An escape request does not trigger a forced graph build or a fabricated empty graph response.
18
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains lazy indexing and shutdown ownership; option comments specify project coordinates and handshake version.
19
+ * @evidence contracts/portability.md#os-neutral-implementation The stdio transport and native session abstracts process handling across hosts; cwd and tsconfig are passed as project coordinates without shell interpolation.
20
+ * @evidence contracts/performance.md#efficient-algorithms Startup registers one tool and transport without scanning the project; index work occurs only when the provider is requested.
21
+ * @evidence contracts/performance.md#reuse-equivalent-work One resident session is shared by all tool calls for the selected project and refreshes its generation before use.
22
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The server owns at most one session; transport close or stdin end joins its child processes and pending requests; shutdown failure retains its nonzero exit and nested passive diagnostic.
11
23
  */
12
24
  export async function startServer(options: {
25
+ /** Project working directory, defaulting to the process directory. */
13
26
  cwd?: string;
27
+
28
+ /**
29
+ * Configuration path interpreted relative to cwd, defaulting to
30
+ * tsconfig.json.
31
+ */
14
32
  tsconfig?: string;
33
+
15
34
  /** Server version reported in the MCP handshake. */
16
35
  version: string;
17
36
  }): Promise<void> {
@@ -19,11 +38,20 @@ export async function startServer(options: {
19
38
  const tsconfig = options.tsconfig ?? "tsconfig.json";
20
39
  let session: TtscGraphSession | undefined;
21
40
  const server = createServer(async () => {
41
+ if (closed) throw new Error("@ttsc/graph: server is closed");
22
42
  session ??= new TtscGraphSession({ cwd, tsconfig });
23
43
  return session.graph();
24
44
  }, options.version);
25
45
  const transport = new StdioServerTransport();
26
- const closeSession = () => session?.close();
46
+ let closed = false;
47
+ const closeSession = () => {
48
+ if (closed) return;
49
+ closed = true;
50
+ void session?.close().catch((error: unknown) => {
51
+ process.stderr.write(`@ttsc/graph: ${formatGraphFailure(error)}\n`);
52
+ process.exitCode = 1;
53
+ });
54
+ };
27
55
  transport.onclose = closeSession;
28
56
  process.stdin.once("end", closeSession);
29
57
  await server.connect(transport);