@ttsc/graph 0.30.3 → 0.31.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 (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,106 +1,46 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { createHash } from "node:crypto";
3
2
  import fs from "node:fs";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
- import { type ITtscCapabilityPlugin, resolveCapabilityPlugins } from "ttsc";
5
+ import {
6
+ type CapabilityPluginResolver,
7
+ type ITtscCapabilityPlugin,
8
+ type ITtscCapabilityPluginResolution,
9
+ resolveCapabilityPluginResolution,
10
+ } from "ttsc";
7
11
 
12
+ import { GraphProcessTrace } from "../internal/GraphProcessTrace";
13
+ import type { IArtifactDirectory } from "./IArtifactDirectory";
14
+ import type { IArtifactInputs } from "./IArtifactInputs";
15
+ import type { IPublishedArtifacts } from "./IPublishedArtifacts";
16
+ import type { IPublishedArtifactsResident } from "./IPublishedArtifactsResident";
8
17
  import { TtscLintDaemon } from "./TtscLintDaemon";
9
18
 
10
19
  /**
11
- * Ask the project's `@ttsc/lint` for the artifacts a citation can name, and
12
- * write them where `ttscgraph dump --artifacts` reads them.
20
+ * Discover configured publishers and synchronously publish their artifact set.
21
+ * Input identity is captured before the graph-nodes verbs to avoid accepting a
22
+ * concurrent document edit as already represented by an older answer.
13
23
  *
14
- * A project that configures no such plugin gets `file: null`, which is the
15
- * common case and not an error: the graph it produces is the graph it produced
16
- * before this existed, and the dump says so by not claiming the capability.
17
- *
18
- * ## Why this runs here and not in the compiler host
19
- *
20
- * The addresses a citation names — a Markdown anchor, `prisma:Sale.price`,
21
- * `POST:/orders` — are produced by parsers that live in the rule that owns
22
- * them, and re-deriving any of them in the graph producer would be a second
23
- * implementation of a published contract. So the units have to arrive from the
24
- * rule.
25
- *
26
- * They cannot arrive in-process. `ttscgraph` is the shipped per-platform
27
- * binary, never a per-project native host, so it can never have a linked
28
- * plugin; and `packages/lint` is its own Go module that deliberately carries no
29
- * requirement on the compiler host. What is left is the channel the host
30
- * already has: a plugin declares a capability and its sidecar answers a verb,
31
- * exactly as `lsp-hints` does. `resolveCapabilityPlugins` is what builds and
32
- * locates those sidecars, and it is the seam `ttscserver` already uses for
33
- * `capabilities.lsp`, published so a consumer outside the compiler can ask
34
- * too.
35
- *
36
- * Nothing here knows what `@ttsc/evidence` is. It asks a lint install for
37
- * whatever its configured rules published, and a project that configured none
38
- * gets an empty answer.
24
+ * @evidence contracts/common.md#principled-implementation Capability discovery and each sidecar's project-inputs/graph-nodes verbs remain the authorities for plugin selection and artifact meaning.
25
+ * @evidence contracts/common.md#clear-and-simple-design The one-shot path composes discovery, input capture and publication through helpers also used by resident requests.
26
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Unavailable sidecars do not produce fabricated artifacts; no evidence-rule-specific parser or private compiler mutation substitutes for the publisher.
27
+ * @evidence contracts/common.md#meaningful-documentation Native prose documents synchronous ownership and the freshness snapshot's required ordering.
28
+ * @evidence contracts/performance.md#efficient-algorithms Discovery runs once per publication, distinct input paths are merged and fingerprinted once, and publisher JSON is concatenated in one pass.
29
+ * @evidence contracts/performance.md#reuse-equivalent-work This one-shot caller has no resident sidecar owner; resident consumers use publishArtifactsResident and session freshness reuse instead of repeatedly spawning this path.
30
+ * @evidence contracts/performance.md#bound-retention-and-release-resources SpawnSync owns each child through completion; the exchange file is overwritten per process/project, but historical project files have no explicit reclamation yet.
31
+ * @evidence contracts/portability.md#os-neutral-implementation Native paths and temporary storage use Node path/os APIs; sidecars receive argument arrays and hidden Windows child windows without shell composition.
39
32
  */
40
- export interface IPublishedArtifacts {
41
- /**
42
- * Path to the JSON the native producer reads, or `null` when no configured
43
- * plugin publishes one.
44
- *
45
- * `null` is a state rather than an absence, which is why it still carries
46
- * {@link inputs}. A project that adds an evidence plugin while a session is
47
- * running would otherwise never be reconsidered: nothing would be watched, so
48
- * nothing could report that the answer had changed from "none" to "some".
49
- */
50
- file: string | null;
51
- /**
52
- * Everything the answer was derived from, as paths this process can state for
53
- * itself.
54
- *
55
- * The artifacts describe documents the compiler's Program never read, so a
56
- * source edit does not move them and a document edit does not move the code
57
- * graph. Refreshing them is therefore a second invalidation with its own
58
- * inputs, and these are those inputs.
59
- */
60
- inputs: IArtifactInputs;
61
- /**
62
- * The state of {@link inputs} when the answer was produced.
63
- *
64
- * Compared against a freshly taken one to decide whether the answer is stale.
65
- * When it moved, nothing else in the session can tell: the compiler's own
66
- * invalidation watches the build universe, and none of this is in it.
67
- */
68
- fingerprint: string;
69
- }
70
-
71
- /** Paths an answer was derived from, split by how they are watched. */
72
- export interface IArtifactInputs {
73
- /** Files stated one by one. */
74
- files: string[];
75
- /** Directories walked, which is what notices an added or deleted file. */
76
- directories: IArtifactDirectory[];
77
- }
78
-
79
- /** A directory watched on behalf of the pattern that named it. */
80
- export interface IArtifactDirectory {
81
- /** Absolute path of the directory to walk. */
82
- path: string;
83
- /**
84
- * Whether the walk descends.
85
- *
86
- * Taken from the pattern rather than assumed, because assuming it is
87
- * expensive in exactly the case that looks harmless: a rule declaring `*.md`
88
- * has the project root for its fixed prefix, and treating that as recursive
89
- * would state every file in the repository before every graph request.
90
- */
91
- recursive: boolean;
92
- }
93
-
94
33
  export function publishArtifacts(options: {
95
34
  cwd: string;
96
35
  tsconfig: string;
97
36
  }): IPublishedArtifacts {
98
- const plugins = resolveCapabilityPlugins({
37
+ const discovery = resolveCapabilityPluginResolution({
99
38
  capability: "graphNodes",
100
39
  cwd: options.cwd,
101
40
  tsconfig: options.tsconfig,
102
41
  });
103
- if (plugins.length === 0) return unpublished(options);
42
+ const plugins = discovery.plugins;
43
+ if (plugins.length === 0) return unpublished(options, discovery);
104
44
  // The inputs are stated before the set is asked for, never after. A document
105
45
  // edited between the two calls has to read as a change next time, and only
106
46
  // this order gives that: a fingerprint taken first describes a state at least
@@ -112,10 +52,13 @@ export function publishArtifacts(options: {
112
52
  plugins.map((plugin) => runVerb(plugin, "project-inputs", options)),
113
53
  options,
114
54
  );
55
+ const fingerprint = fingerprintInputs(inputs);
115
56
  return assemble(
116
57
  options,
117
58
  inputs,
59
+ fingerprint,
118
60
  plugins.map((plugin) => runVerb(plugin, "graph-nodes", options)),
61
+ discovery,
119
62
  );
120
63
  }
121
64
 
@@ -132,41 +75,101 @@ export function publishArtifacts(options: {
132
75
  * so the answer is the same one either way and only its cost differs. The
133
76
  * fallback matters more here than most: a daemon that quietly answered nothing
134
77
  * would be indistinguishable from a project that publishes nothing.
78
+ *
79
+ * @evidence contracts/common.md#principled-implementation Both paths ask the same configured sidecar verbs; input identity is captured before publication and invalidation clears the daemon's stale Program first.
80
+ * @evidence contracts/common.md#clear-and-simple-design The caller supplies daemon and asynchronous resolver ownership while this function owns ordered input and artifact phases and the shared result assembly.
81
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts A failed daemon falls back to the public direct verb, not an invented empty-success result or a reimplementation of its parser.
82
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains resident reuse, direct-command fallback and the reason to invalidate the Program before input collection.
83
+ * @evidence contracts/performance.md#efficient-algorithms Independent publishers run concurrently within each of two ordered phases; deduplication avoids repeatedly hashing the same declared file path.
84
+ * @evidence contracts/performance.md#reuse-equivalent-work Session-owned daemons reuse process and plugin loading only while binary, manifest and project context match; changed inputs rebuild the publication.
85
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The session owns the resolver and daemons. Failed publication joins every selected verb and releases its proof; successful publication transfers proof release to the session. Cancellation closes and joins an active daemon or supervised direct command. One exchange file per process/project still has no historical-file reclamation.
86
+ * @evidence contracts/portability.md#os-neutral-implementation The fallback preserves native argument-vector invocation and Node path resolution; daemon ownership uses the same cross-platform process boundary.
135
87
  */
136
88
  export async function publishArtifactsResident(
137
- options: { cwd: string; tsconfig: string },
138
- daemon: (plugin: ITtscCapabilityPlugin) => TtscLintDaemon | undefined,
139
- ): Promise<IPublishedArtifacts> {
140
- const plugins = resolveCapabilityPlugins({
141
- capability: "graphNodes",
142
- cwd: options.cwd,
143
- tsconfig: options.tsconfig,
144
- });
145
- if (plugins.length === 0) return unpublished(options);
146
- // The first request of a republish drops the daemon's warm Program. The
147
- // artifacts depend on which sources exist and what they declare — that is
148
- // what activates a claim — and between two republishes the developer has
149
- // been editing code as well as documents. Reusing a Program from before
150
- // those edits would deactivate a claim whose files now exist, which is a
151
- // stale answer of exactly the kind this whole mechanism removes. What the
152
- // daemon still saves is the process, the plugin load, and the configuration
153
- // evaluation, which is most of the cost.
154
- const inputs = readInputs(
155
- await Promise.all(
156
- plugins.map((plugin) =>
157
- askVerb(plugin, "project-inputs", options, daemon(plugin), true),
158
- ),
159
- ),
160
- options,
89
+ options: { cwd: string; tsconfig: string; signal?: AbortSignal },
90
+ daemon: (
91
+ plugin: ITtscCapabilityPlugin,
92
+ ) =>
93
+ | Pick<TtscLintDaemon, "ask" | "close">
94
+ | undefined
95
+ | Promise<Pick<TtscLintDaemon, "ask" | "close"> | undefined>,
96
+ resolver: Pick<CapabilityPluginResolver, "resolve" | "runCommand">,
97
+ ): Promise<IPublishedArtifactsResident> {
98
+ options.signal?.throwIfAborted();
99
+ const discovery = await resolver.resolve(
100
+ { capability: "graphNodes", cwd: options.cwd, tsconfig: options.tsconfig },
101
+ { signal: options.signal },
161
102
  );
162
- return assemble(
163
- options,
164
- inputs,
165
- await Promise.all(
166
- plugins.map((plugin) =>
167
- askVerb(plugin, "graph-nodes", options, daemon(plugin), false),
103
+ try {
104
+ options.signal?.throwIfAborted();
105
+ const plugins = discovery.plugins;
106
+ if (plugins.length === 0) return unpublished(options, discovery);
107
+ // The first request of a republish drops the daemon's warm Program. The
108
+ // artifacts depend on which sources exist and what they declare — that is
109
+ // what activates a claim — and between two republishes the developer has
110
+ // been editing code as well as documents. Reusing a Program from before
111
+ // those edits would deactivate a claim whose files now exist, which is a
112
+ // stale answer of exactly the kind this whole mechanism removes. What the
113
+ // daemon still saves is the process, the plugin load, and the configuration
114
+ // evaluation, which is most of the cost.
115
+ const inputs = readInputs(
116
+ await joinedPublications(
117
+ plugins.map(async (plugin) =>
118
+ askVerb(
119
+ plugin,
120
+ "project-inputs",
121
+ options,
122
+ await daemon(plugin),
123
+ true,
124
+ resolver,
125
+ ),
126
+ ),
127
+ ),
128
+ options,
129
+ );
130
+ const fingerprint = fingerprintInputs(inputs);
131
+ const outputs = await joinedPublications(
132
+ plugins.map(async (plugin) =>
133
+ askVerb(
134
+ plugin,
135
+ "graph-nodes",
136
+ options,
137
+ await daemon(plugin),
138
+ false,
139
+ resolver,
140
+ ),
168
141
  ),
169
- ),
142
+ );
143
+ options.signal?.throwIfAborted();
144
+ return assemble(options, inputs, fingerprint, outputs, discovery);
145
+ } catch (error) {
146
+ try {
147
+ await discovery.release();
148
+ } catch (cleanup) {
149
+ throw new AggregateError(
150
+ [error, cleanup],
151
+ "@ttsc/graph: discovery release failed",
152
+ );
153
+ }
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ /** Join every publisher before releasing a failed publication's proof. */
159
+ async function joinedPublications(
160
+ outputs: Promise<string | null>[],
161
+ ): Promise<(string | null)[]> {
162
+ const results = await Promise.allSettled(outputs);
163
+ const failures = results.filter(
164
+ (result): result is PromiseRejectedResult => result.status === "rejected",
165
+ );
166
+ if (failures.length > 0)
167
+ throw new AggregateError(
168
+ failures.map((result) => result.reason),
169
+ "@ttsc/graph: artifact publication failed",
170
+ );
171
+ return results.map(
172
+ (result) => (result as PromiseFulfilledResult<string | null>).value,
170
173
  );
171
174
  }
172
175
 
@@ -176,7 +179,7 @@ function runVerb(
176
179
  verb: string,
177
180
  options: { cwd: string; tsconfig: string },
178
181
  ): string | null {
179
- const result = spawnSync(
182
+ const result = GraphProcessTrace.spawnSync(
180
183
  plugin.binary,
181
184
  [
182
185
  verb,
@@ -214,32 +217,90 @@ function runVerb(
214
217
  async function askVerb(
215
218
  plugin: ITtscCapabilityPlugin,
216
219
  verb: string,
217
- options: { cwd: string; tsconfig: string },
218
- daemon: TtscLintDaemon | undefined,
220
+ options: { cwd: string; tsconfig: string; signal?: AbortSignal },
221
+ daemon: Pick<TtscLintDaemon, "ask" | "close"> | undefined,
219
222
  invalidate: boolean,
223
+ resolver: Pick<CapabilityPluginResolver, "runCommand">,
220
224
  ): Promise<string | null> {
221
- const served = await daemon?.ask(verb, invalidate);
222
- return served ?? runVerb(plugin, verb, options);
225
+ options.signal?.throwIfAborted();
226
+ let closing: Promise<void> | undefined;
227
+ const abort = () => {
228
+ closing = daemon?.close();
229
+ void closing?.catch(() => undefined);
230
+ };
231
+ options.signal?.addEventListener("abort", abort, { once: true });
232
+ if (options.signal?.aborted) abort();
233
+ let served: string | null | undefined;
234
+ let failure: unknown;
235
+ try {
236
+ served = await daemon?.ask(verb, invalidate);
237
+ } catch (error) {
238
+ failure = error;
239
+ throw error;
240
+ } finally {
241
+ options.signal?.removeEventListener("abort", abort);
242
+ try {
243
+ await closing;
244
+ } catch (cleanup) {
245
+ throw new AggregateError(
246
+ [
247
+ ...(failure === undefined ? [] : [failure]),
248
+ ...(options.signal?.aborted ? [options.signal.reason] : []),
249
+ cleanup,
250
+ ],
251
+ "@ttsc/graph: cancelled daemon cleanup failed",
252
+ );
253
+ }
254
+ }
255
+ options.signal?.throwIfAborted();
256
+ if (served !== undefined && served !== null) return served;
257
+ const result = await resolver.runCommand(
258
+ plugin.binary,
259
+ [
260
+ verb,
261
+ "--cwd",
262
+ options.cwd,
263
+ "--tsconfig",
264
+ options.tsconfig,
265
+ `--plugins-json=${plugin.manifest}`,
266
+ ...projectContextArgs(plugin),
267
+ ],
268
+ { maxBuffer: 256 * 1024 * 1024, encoding: "utf8", windowsHide: true },
269
+ { signal: options.signal },
270
+ );
271
+ options.signal?.throwIfAborted();
272
+ return result.error ||
273
+ result.status !== 0 ||
274
+ typeof result.stdout !== "string"
275
+ ? null
276
+ : result.stdout;
223
277
  }
224
278
 
225
279
  /** The artifact answer, from each sidecar's `graph-nodes` output. */
226
- function assemble(
280
+ function assemble<
281
+ Discovery extends
282
+ | ITtscCapabilityPluginResolution
283
+ | CapabilityPluginResolver.Resolution,
284
+ >(
227
285
  options: { cwd: string; tsconfig: string },
228
286
  inputs: IArtifactInputs,
287
+ fingerprint: string,
229
288
  outputs: readonly (string | null)[],
230
- ): IPublishedArtifacts {
231
- const fingerprint = fingerprintInputs(inputs);
289
+ discovery: Discovery,
290
+ ): Omit<IPublishedArtifacts, "discovery"> & { discovery: Discovery } {
232
291
  const published: unknown[] = [];
233
292
  for (const output of outputs) {
234
293
  if (output === null) continue;
235
294
  try {
236
295
  const parsed: unknown = JSON.parse(output);
237
- if (Array.isArray(parsed)) published.push(...parsed);
296
+ if (Array.isArray(parsed))
297
+ for (const entryToAppend of parsed) published.push(entryToAppend);
238
298
  } catch {
239
299
  continue;
240
300
  }
241
301
  }
242
- if (published.length === 0) return { file: null, fingerprint, inputs };
302
+ if (published.length === 0)
303
+ return { file: null, fingerprint, inputs, discovery };
243
304
 
244
305
  // One file per process and project, overwritten, rather than a fresh temp
245
306
  // directory per call. A directory per call is a leak nothing here is
@@ -259,24 +320,65 @@ function assemble(
259
320
  `ttsc-graph-artifacts-${String(process.pid)}-${projectKey(options)}.json`,
260
321
  );
261
322
  fs.writeFileSync(file, JSON.stringify(published));
262
- return { file, fingerprint, inputs };
323
+ return { file, fingerprint, inputs, discovery };
263
324
  }
264
325
 
265
326
  /**
266
327
  * Whether the inputs an answer was derived from have moved since.
267
328
  *
268
- * Answered by stating paths this process already knows, not by asking the
269
- * plugin again. The question is asked before every graph request in a resident
270
- * session, and re-running plugin discovery per request would cost more than the
271
- * refresh it guards — while a `stat` per document costs less than reading one
272
- * of them.
329
+ * Published documents are compared by content identity. The owning resolver
330
+ * separately qualifies discovery, including successful absence. Unavailable,
331
+ * incomplete or changed discovery proof requires another lookup.
273
332
  *
274
- * The cost of being wrong in the cheap direction is what makes this worth
275
- * paying at all: a developer who edited only a spec section, and nothing the
276
- * compiler reads, otherwise saw the graph keep answering with the headings that
277
- * section used to have.
333
+ * @evidence contracts/common.md#principled-implementation The owning discovery query must still qualify this exact lookup, and exchange-file existence plus declared input identity must also hold before any publication or successful absence is reused.
334
+ * @evidence contracts/common.md#clear-and-simple-design Discovery freshness stays with the resolver and downstream document identity stays with fingerprintInputs; this predicate combines their independent authority.
335
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Equal size and modification time cannot substitute for file-content equivalence, and a partial no-plugin watch list cannot prove discovery stability.
336
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain content-based publication freshness, successful absence and conservative relookup for incomplete discovery proof.
337
+ * @evidence contracts/performance.md#efficient-algorithms Freshness hashes the distinct declared inputs and traverses watch roots once per inventory entry; its cost grows with watched bytes and entries rather than only stat count.
338
+ * @evidence contracts/performance.md#reuse-equivalent-work The resolver's original complete discovery proof authorizes sharing populated or empty results; changed or unproved selection requires relookup, and document fingerprints separately qualify overlay reuse.
339
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources This predicate borrows an inventory and returns a boolean; the publication and session own their storage and sidecars.
340
+ * @evidence contracts/portability.md#os-neutral-implementation Existence and fingerprints use Node filesystem APIs and native absolute paths without assumptions about platform shell commands.
278
341
  */
279
342
  export function artifactsAreStale(published: IPublishedArtifacts): boolean {
343
+ // Discovery remains the loader's authority, including successful absence.
344
+ // A downstream input list cannot replace an unavailable or incomplete proof.
345
+ if (
346
+ published.discovery?.status !== "resolved" ||
347
+ !published.discovery.isCurrent()
348
+ )
349
+ return true;
350
+ return artifactInputsAreStale(published);
351
+ }
352
+
353
+ /**
354
+ * Check the exact resident discovery proof before reusing its overlay.
355
+ * Discovery runs off the request thread; declared artifact bytes remain an
356
+ * independent freshness premise after that authority has answered.
357
+ *
358
+ * @evidence contracts/common.md#principled-implementation The original asynchronous proof must authorize reuse before exchange existence and declared input fingerprints are compared.
359
+ * @evidence contracts/common.md#clear-and-simple-design Resolver proof and artifact-input validation retain their separate owners in one ordered predicate.
360
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Unavailable or invalid proof requires relookup; no partial inventory substitutes for SDK authority.
361
+ * @evidence contracts/common.md#meaningful-documentation Native prose distinguishes off-thread discovery from this consumer's independent artifact inputs.
362
+ * @evidence contracts/performance.md#efficient-algorithms The owner validates discovery once and fingerprintInputs hashes each declared file and directory inventory, scaling with declared entries and bytes.
363
+ * @evidence contracts/performance.md#reuse-equivalent-work Only this exact publication's current proof plus unchanged artifact inputs authorizes reuse, including resolved absence.
364
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources This predicate borrows the session-owned publication and acquires no lasting proof or process.
365
+ * @evidence contracts/portability.md#os-neutral-implementation SDK process observation remains with its owner and artifact checks use Node native filesystem APIs.
366
+ */
367
+ export async function artifactsAreStaleResident(
368
+ published: IPublishedArtifactsResident,
369
+ options: { signal?: AbortSignal } = {},
370
+ ): Promise<boolean> {
371
+ options.signal?.throwIfAborted();
372
+ if (published.discovery.status !== "resolved") return true;
373
+ const current = await published.discovery.isCurrent(options);
374
+ options.signal?.throwIfAborted();
375
+ if (!current) return true;
376
+ return artifactInputsAreStale(published);
377
+ }
378
+
379
+ function artifactInputsAreStale(
380
+ published: Omit<IPublishedArtifacts, "discovery">,
381
+ ): boolean {
280
382
  // The written set is one of its own inputs, by existence alone. It lives in
281
383
  // the system temp directory, which is swept on a schedule this session has no
282
384
  // say in, and the server is handed the path on every request — so once it is
@@ -295,21 +397,19 @@ export function artifactsAreStale(published: IPublishedArtifacts): boolean {
295
397
  * The answer for a project that publishes nothing, and what to watch so that
296
398
  * answer can change.
297
399
  *
298
- * Configuring a plugin means editing the project's tsconfig or installing a
299
- * package that declares one, so those two files are what could turn this answer
300
- * into a different one. They are stated rather than the whole discovery being
301
- * re-run, because re-running it walks the dependency closure — the cost
302
- * samchon/ttsc#1276 is about — and paying that per request to learn nothing
303
- * would be worse than the staleness it prevents.
304
- *
305
- * Bounded, and deliberately: a tsconfig that inherits its plugins from an
306
- * extended config is not tracked here, because naming the whole extends chain
307
- * means asking the loader that is itself the expense.
400
+ * The direct config and package manifest are useful provenance, but do not
401
+ * cover inherited configuration or dependency installation. The attached owning
402
+ * discovery proof supplies that independent authority; direct paths do not
403
+ * stand in for it.
308
404
  */
309
- function unpublished(options: {
310
- cwd: string;
311
- tsconfig: string;
312
- }): IPublishedArtifacts {
405
+ function unpublished<
406
+ Discovery extends
407
+ | ITtscCapabilityPluginResolution
408
+ | CapabilityPluginResolver.Resolution,
409
+ >(
410
+ options: { cwd: string; tsconfig: string },
411
+ discovery: Discovery,
412
+ ): Omit<IPublishedArtifacts, "discovery"> & { discovery: Discovery } {
313
413
  const inputs: IArtifactInputs = {
314
414
  directories: [],
315
415
  files: [
@@ -317,7 +417,12 @@ function unpublished(options: {
317
417
  path.resolve(options.cwd, "package.json"),
318
418
  ],
319
419
  };
320
- return { file: null, fingerprint: fingerprintInputs(inputs), inputs };
420
+ return {
421
+ file: null,
422
+ fingerprint: fingerprintInputs(inputs),
423
+ inputs,
424
+ discovery,
425
+ };
321
426
  }
322
427
 
323
428
  /**
@@ -399,16 +504,28 @@ function readInputs(
399
504
  * Two readings have to be right here, and both are cheap to get wrong. A
400
505
  * pattern carrying no wildcard is a path, not a tree: watching its parent
401
506
  * instead would state every sibling on every request to learn about the one
402
- * file that was declared. And a pattern that does not say `**` does not descend
403
- * — treating it as though it did is unbounded in exactly the case that looks
404
- * harmless, because a bare `*.md` has the project root for its prefix.
507
+ * file that was declared. A bare filename wildcard stays shallow; globstar or a
508
+ * wildcard directory component requires descent from the fixed prefix.
509
+ *
510
+ * @evidence contracts/common.md#principled-implementation Literal paths remain individual inputs; wildcard paths use their fixed directory prefix and descend when remaining path components can contain matched files.
511
+ * @evidence contracts/common.md#clear-and-simple-design A single fixed-prefix calculation and descent decision produce the directory descriptor consumed by fingerprinting.
512
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Descent is not inferred solely from globstar, which would miss wildcard directory components followed by a filename.
513
+ * @evidence contracts/common.md#meaningful-documentation Native prose documents literal paths, bare wildcards and wildcard-directory descent.
514
+ * @evidence contracts/performance.md#efficient-algorithms Pattern scanning is linear in pattern length and avoids directory traversal at selection time; a bare filename wildcard keeps the walk shallow.
515
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work This pure descriptor calculation has no cross-request producer or validation coordinator; publication owns reuse.
516
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The returned descriptor transfers to its caller and owns no handle or retained task.
517
+ * @evidence contracts/portability.md#os-neutral-implementation Both slash forms are recognized at the native-path boundary and relative roots resolve through Node path APIs.
405
518
  */
406
519
  export function watchedBy(
407
520
  pattern: string,
408
521
  cwd: string,
409
522
  ): IArtifactDirectory | null {
410
523
  if (pattern.search(GLOB_MAGIC) < 0) return null;
411
- return { path: globRoot(pattern, cwd), recursive: pattern.includes("**") };
524
+ const magic = pattern.search(GLOB_MAGIC);
525
+ return {
526
+ path: globRoot(pattern, cwd),
527
+ recursive: pattern.includes("**") || /[/\\]/u.test(pattern.slice(magic)),
528
+ };
412
529
  }
413
530
 
414
531
  /**
@@ -458,37 +575,72 @@ function absolute(target: string, cwd: string): string {
458
575
  /**
459
576
  * The state of every input, as one comparable string.
460
577
  *
461
- * Files are stated by size and modification time rather than content: this runs
462
- * before every graph request, and hashing a documentation corpus per request
463
- * would cost more than the refresh it guards. Directories are walked for the
464
- * same pair, which is what notices a section added or a document deleted rather
465
- * than edited.
578
+ * Files are hashed by content so same-size edits with restored modification
579
+ * times remain observable. Directory membership is walked conservatively from
580
+ * declared roots; matching remains the sidecar's responsibility.
581
+ *
582
+ * @evidence contracts/common.md#principled-implementation The fingerprint includes input paths and regular-file bytes; sorted records make enumeration order irrelevant while missing paths retain an explicit marker.
583
+ * @evidence contracts/common.md#clear-and-simple-design File state and conservative directory traversal share one record representation before the final digest.
584
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Metadata equality, hidden-directory skipping and arbitrary depth limits do not stand in for declared input identity.
585
+ * @evidence contracts/common.md#meaningful-documentation Native prose states content hashing, conservative membership and sidecar ownership of actual matching.
586
+ * @evidence contracts/performance.md#efficient-algorithms Cost is linear in watched regular-file bytes plus O(N log N) record sorting; broad recursive globs conservatively over-read and may invalidate unrelated edits.
587
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work This computes one validation identity; the session compares it with the publication snapshot and decides reuse.
588
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Synchronous reads close their descriptors internally, and the record array exists only for this computation; no historical fingerprint state is retained here.
589
+ * @evidence contracts/portability.md#os-neutral-implementation Native stat, read and directory APIs handle platform paths; NUL-separated fields prevent filename ambiguity without shell serialization.
466
590
  */
467
591
  export function fingerprintInputs(inputs: IArtifactInputs): string {
468
592
  const parts: string[] = [];
469
- for (const file of inputs.files) parts.push(stateOf(file));
593
+ const states = new Map<string, string>();
594
+ for (const file of inputs.files) parts.push(stateOf(file, states));
470
595
  for (const directory of inputs.directories)
471
- parts.push(...walkState(directory.path, directory.recursive));
596
+ for (const entryToAppend of walkState(
597
+ directory.path,
598
+ directory.recursive,
599
+ states,
600
+ ))
601
+ parts.push(entryToAppend);
472
602
  parts.sort();
473
603
  return createHash("sha256").update(parts.join("\n")).digest("hex");
474
604
  }
475
605
 
476
606
  /**
477
- * A path's size and modification time, or a marker when it is absent.
607
+ * A regular file's content identity, physical directory identity, or absence.
478
608
  *
479
609
  * The fields are joined on a character a path cannot contain. Separated by a
480
610
  * space, a file literally named `a 1 2` states the same string as a one-byte
481
611
  * file named `a`, and an edit to either would then read as no edit at all.
482
612
  */
483
- function stateOf(file: string): string {
613
+ function stateOf(file: string, states: Map<string, string>): string {
614
+ const existing = states.get(file);
615
+ if (existing !== undefined) return existing;
484
616
  try {
485
617
  const stat = fs.statSync(file);
486
- return [file, String(stat.size), String(stat.mtimeMs)].join(SEPARATOR);
487
- } catch {
488
- return [file, "absent"].join(SEPARATOR);
618
+ const identity = stat.isFile()
619
+ ? createHash("sha256").update(fs.readFileSync(file)).digest("hex")
620
+ : stat.isDirectory()
621
+ ? ["directory", fs.realpathSync.native(file)].join(SEPARATOR)
622
+ : ["other", String(stat.size), String(stat.mtimeMs)].join(SEPARATOR);
623
+ const state = [file, identity].join(SEPARATOR);
624
+ states.set(file, state);
625
+ return state;
626
+ } catch (error) {
627
+ if (!missingPath(error)) throw error;
628
+ const state = [file, "absent"].join(SEPARATOR);
629
+ states.set(file, state);
630
+ return state;
489
631
  }
490
632
  }
491
633
 
634
+ /** Missing paths are inputs; unreadable existing inputs are failures. */
635
+ function missingPath(error: unknown): boolean {
636
+ return (
637
+ typeof error === "object" &&
638
+ error !== null &&
639
+ "code" in error &&
640
+ (error.code === "ENOENT" || error.code === "ENOTDIR")
641
+ );
642
+ }
643
+
492
644
  /**
493
645
  * The character that joins fields a path could otherwise forge.
494
646
  *
@@ -509,40 +661,67 @@ function globRoot(pattern: string, cwd: string): string {
509
661
  const magic = pattern.search(GLOB_MAGIC);
510
662
  const head = magic < 0 ? pattern : pattern.slice(0, magic);
511
663
  const slash = Math.max(head.lastIndexOf("/"), head.lastIndexOf("\\"));
512
- const root = slash < 0 ? "" : head.slice(0, slash);
664
+ const root =
665
+ slash < 0
666
+ ? ""
667
+ : head.slice(0, Math.max(slash, path.parse(head).root.length));
513
668
  return root === "" ? cwd : absolute(root, cwd);
514
669
  }
515
670
 
516
671
  /**
517
672
  * Every entry below `directory`, stated.
518
673
  *
519
- * Bounded three ways, because this runs before every graph request: it descends
520
- * only when the pattern that named the directory descends, it never enters
521
- * `node_modules`, and it stops at a depth no documentation tree reaches. Dotted
522
- * directories are skipped below the declared root, which is what the glob that
523
- * named it would have matched anyway.
674
+ * Descent follows the declared pattern rather than a guessed maximum depth or
675
+ * excluded directory name. Symlink directories are followed with a physical
676
+ * ancestor set so their declared contents are watched without creating cycles.
524
677
  *
525
678
  * A directory that does not exist states itself absent, which is what notices
526
679
  * one being created.
527
680
  */
528
- function walkState(directory: string, recursive: boolean, depth = 0): string[] {
529
- if (depth > 12) return [];
681
+ function walkState(
682
+ directory: string,
683
+ recursive: boolean,
684
+ inputStates: Map<string, string>,
685
+ ancestors: ReadonlySet<string> = new Set(),
686
+ ): string[] {
530
687
  let entries: fs.Dirent[];
688
+ let physical: string;
531
689
  try {
690
+ physical = fs.realpathSync.native(directory);
691
+ if (ancestors.has(physical)) return [stateOf(directory, inputStates)];
532
692
  entries = fs.readdirSync(directory, { withFileTypes: true });
533
- } catch {
534
- return [stateOf(directory)];
693
+ } catch (error) {
694
+ if (!missingPath(error)) throw error;
695
+ return [stateOf(directory, inputStates)];
535
696
  }
697
+ const nextAncestors = new Set(ancestors);
698
+ nextAncestors.add(physical);
536
699
  const states: string[] = [];
537
700
  for (const entry of entries) {
538
701
  const child = path.join(directory, entry.name);
539
- if (entry.isDirectory()) {
540
- if (!recursive) continue;
541
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
542
- states.push(...walkState(child, recursive, depth + 1));
702
+ let directoryEntry = entry.isDirectory();
703
+ if (entry.isSymbolicLink()) {
704
+ try {
705
+ directoryEntry = fs.statSync(child).isDirectory();
706
+ } catch (error) {
707
+ if (!missingPath(error)) throw error;
708
+ }
709
+ }
710
+ if (directoryEntry) {
711
+ if (!recursive) {
712
+ states.push(stateOf(child, inputStates));
713
+ continue;
714
+ }
715
+ for (const entryToAppend of walkState(
716
+ child,
717
+ recursive,
718
+ inputStates,
719
+ nextAncestors,
720
+ ))
721
+ states.push(entryToAppend);
543
722
  continue;
544
723
  }
545
- states.push(stateOf(child));
724
+ states.push(stateOf(child, inputStates));
546
725
  }
547
726
  return states;
548
727
  }