@lzehrung/codegraph 2.1.2 → 2.2.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 (312) hide show
  1. package/README.md +2 -2
  2. package/codegraph-skill/codegraph/SKILL.md +21 -2
  3. package/dist/agent/explore.d.ts.map +1 -1
  4. package/dist/agent/explore.js +6 -1
  5. package/dist/agent/explore.js.map +1 -1
  6. package/dist/agent/handles.d.ts.map +1 -1
  7. package/dist/agent/handles.js +2 -8
  8. package/dist/agent/handles.js.map +1 -1
  9. package/dist/agent/orient.js +0 -16
  10. package/dist/agent/orient.js.map +1 -1
  11. package/dist/agent/query-index/store.d.ts +2 -0
  12. package/dist/agent/query-index/store.d.ts.map +1 -1
  13. package/dist/agent/query-index/store.js +4 -0
  14. package/dist/agent/query-index/store.js.map +1 -1
  15. package/dist/agent/session.js +3 -3
  16. package/dist/agent/session.js.map +1 -1
  17. package/dist/bin/cli.js +7303 -5659
  18. package/dist/bin/queryIndexWorker.js +357 -54
  19. package/dist/chunking/chunkSplit.js +0 -15
  20. package/dist/chunking/chunkSplit.js.map +1 -1
  21. package/dist/cli/commandCatalog.d.ts.map +1 -1
  22. package/dist/cli/commandCatalog.js +1 -0
  23. package/dist/cli/commandCatalog.js.map +1 -1
  24. package/dist/cli/commandTable.d.ts.map +1 -1
  25. package/dist/cli/commandTable.js +17 -0
  26. package/dist/cli/commandTable.js.map +1 -1
  27. package/dist/cli/compileCache.d.ts +5 -0
  28. package/dist/cli/compileCache.d.ts.map +1 -1
  29. package/dist/cli/compileCache.js +18 -0
  30. package/dist/cli/compileCache.js.map +1 -1
  31. package/dist/cli/context.d.ts.map +1 -1
  32. package/dist/cli/context.js +0 -1
  33. package/dist/cli/context.js.map +1 -1
  34. package/dist/cli/help.d.ts +2 -1
  35. package/dist/cli/help.d.ts.map +1 -1
  36. package/dist/cli/help.js +23 -2
  37. package/dist/cli/help.js.map +1 -1
  38. package/dist/cli/indexPolicy.d.ts.map +1 -1
  39. package/dist/cli/indexPolicy.js +1 -0
  40. package/dist/cli/indexPolicy.js.map +1 -1
  41. package/dist/cli/inspect.d.ts.map +1 -1
  42. package/dist/cli/inspect.js +5 -2
  43. package/dist/cli/inspect.js.map +1 -1
  44. package/dist/cli/options.d.ts +1 -1
  45. package/dist/cli/options.d.ts.map +1 -1
  46. package/dist/cli/options.js +31 -1
  47. package/dist/cli/options.js.map +1 -1
  48. package/dist/cli/server.d.ts +59 -0
  49. package/dist/cli/server.d.ts.map +1 -0
  50. package/dist/cli/server.js +906 -0
  51. package/dist/cli/server.js.map +1 -0
  52. package/dist/cli.d.ts +1 -1
  53. package/dist/cli.d.ts.map +1 -1
  54. package/dist/cli.js +1 -0
  55. package/dist/cli.js.map +1 -1
  56. package/dist/documentLinks.d.ts.map +1 -1
  57. package/dist/documentLinks.js +0 -1
  58. package/dist/documentLinks.js.map +1 -1
  59. package/dist/drift/snapshot.js +1 -1
  60. package/dist/drift/snapshot.js.map +1 -1
  61. package/dist/duplicates/scoring.d.ts.map +1 -1
  62. package/dist/duplicates/scoring.js.map +1 -1
  63. package/dist/duplicates/unitCache.d.ts.map +1 -1
  64. package/dist/duplicates/unitCache.js +1 -1
  65. package/dist/duplicates/unitCache.js.map +1 -1
  66. package/dist/duplicates/units.d.ts.map +1 -1
  67. package/dist/duplicates/units.js +2 -3
  68. package/dist/duplicates/units.js.map +1 -1
  69. package/dist/duplicates.d.ts.map +1 -1
  70. package/dist/duplicates.js +0 -6
  71. package/dist/duplicates.js.map +1 -1
  72. package/dist/graph-edge-collector.d.ts +0 -2
  73. package/dist/graph-edge-collector.d.ts.map +1 -1
  74. package/dist/graph-edge-collector.js +2 -4
  75. package/dist/graph-edge-collector.js.map +1 -1
  76. package/dist/graphs/edgeResolution.d.ts.map +1 -1
  77. package/dist/graphs/specifiers.d.ts +1 -1
  78. package/dist/graphs/specifiers.d.ts.map +1 -1
  79. package/dist/graphs/specifiers.js +1 -1
  80. package/dist/graphs/specifiers.js.map +1 -1
  81. package/dist/graphs/symbol-graph-detailed.d.ts.map +1 -1
  82. package/dist/graphs/symbol-graph-detailed.js +0 -1
  83. package/dist/graphs/symbol-graph-detailed.js.map +1 -1
  84. package/dist/index.d.ts +1 -1
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js.map +1 -1
  87. package/dist/indexer/build-cache/module-cache.d.ts.map +1 -1
  88. package/dist/indexer/build-cache/module-cache.js +1 -1
  89. package/dist/indexer/build-cache/module-cache.js.map +1 -1
  90. package/dist/indexer/build-cache/options.d.ts.map +1 -1
  91. package/dist/indexer/build-cache/options.js +0 -2
  92. package/dist/indexer/build-cache/options.js.map +1 -1
  93. package/dist/indexer/build-cache/project-snapshot.d.ts +9 -1
  94. package/dist/indexer/build-cache/project-snapshot.d.ts.map +1 -1
  95. package/dist/indexer/build-cache/project-snapshot.js +1 -5
  96. package/dist/indexer/build-cache/project-snapshot.js.map +1 -1
  97. package/dist/indexer/build-index.d.ts.map +1 -1
  98. package/dist/indexer/build-index.js +59 -44
  99. package/dist/indexer/build-index.js.map +1 -1
  100. package/dist/indexer/build-workers.d.ts +29 -2
  101. package/dist/indexer/build-workers.d.ts.map +1 -1
  102. package/dist/indexer/build-workers.js +54 -3
  103. package/dist/indexer/build-workers.js.map +1 -1
  104. package/dist/indexer/call-hierarchy.d.ts.map +1 -1
  105. package/dist/indexer/imports.d.ts +0 -3
  106. package/dist/indexer/imports.d.ts.map +1 -1
  107. package/dist/indexer/imports.js +1 -1
  108. package/dist/indexer/imports.js.map +1 -1
  109. package/dist/indexer/locals-and-exports.d.ts +2 -2
  110. package/dist/indexer/locals-and-exports.d.ts.map +1 -1
  111. package/dist/indexer/locals-and-exports.js +2 -3
  112. package/dist/indexer/locals-and-exports.js.map +1 -1
  113. package/dist/indexer/navigation-local.d.ts +2 -2
  114. package/dist/indexer/navigation-local.d.ts.map +1 -1
  115. package/dist/indexer/navigation-local.js +2 -2
  116. package/dist/indexer/navigation-local.js.map +1 -1
  117. package/dist/indexer/navigation-references.d.ts +1 -2
  118. package/dist/indexer/navigation-references.d.ts.map +1 -1
  119. package/dist/indexer/navigation-references.js +1 -1
  120. package/dist/indexer/navigation-references.js.map +1 -1
  121. package/dist/indexer/navigation.d.ts.map +1 -1
  122. package/dist/indexer/navigation.js +2 -4
  123. package/dist/indexer/navigation.js.map +1 -1
  124. package/dist/indexer/parse-context.d.ts +1 -4
  125. package/dist/indexer/parse-context.d.ts.map +1 -1
  126. package/dist/indexer/parse-context.js +11 -6
  127. package/dist/indexer/parse-context.js.map +1 -1
  128. package/dist/indexer/scope.d.ts +2 -2
  129. package/dist/indexer/scope.d.ts.map +1 -1
  130. package/dist/indexer/scope.js +1 -1
  131. package/dist/indexer/scope.js.map +1 -1
  132. package/dist/indexer.d.ts +3 -6
  133. package/dist/indexer.d.ts.map +1 -1
  134. package/dist/indexer.js +5 -8
  135. package/dist/indexer.js.map +1 -1
  136. package/dist/installer/registry.js +1 -27
  137. package/dist/installer/registry.js.map +1 -1
  138. package/dist/languages/definitions/c.d.ts.map +1 -1
  139. package/dist/languages/definitions/c.js +0 -1
  140. package/dist/languages/definitions/c.js.map +1 -1
  141. package/dist/languages/definitions/cFamily.d.ts +0 -1
  142. package/dist/languages/definitions/cFamily.d.ts.map +1 -1
  143. package/dist/languages/definitions/cFamily.js +0 -2
  144. package/dist/languages/definitions/cFamily.js.map +1 -1
  145. package/dist/languages/definitions/cpp.d.ts.map +1 -1
  146. package/dist/languages/definitions/cpp.js +0 -1
  147. package/dist/languages/definitions/cpp.js.map +1 -1
  148. package/dist/languages/definitions/csharp.d.ts.map +1 -1
  149. package/dist/languages/definitions/csharp.js +0 -2
  150. package/dist/languages/definitions/csharp.js.map +1 -1
  151. package/dist/languages/definitions/css.d.ts.map +1 -1
  152. package/dist/languages/definitions/css.js +0 -2
  153. package/dist/languages/definitions/css.js.map +1 -1
  154. package/dist/languages/definitions/go.d.ts.map +1 -1
  155. package/dist/languages/definitions/go.js +0 -2
  156. package/dist/languages/definitions/go.js.map +1 -1
  157. package/dist/languages/definitions/html.d.ts.map +1 -1
  158. package/dist/languages/definitions/html.js +0 -2
  159. package/dist/languages/definitions/html.js.map +1 -1
  160. package/dist/languages/definitions/htmlStub.d.ts.map +1 -1
  161. package/dist/languages/definitions/htmlStub.js +0 -2
  162. package/dist/languages/definitions/htmlStub.js.map +1 -1
  163. package/dist/languages/definitions/java.d.ts.map +1 -1
  164. package/dist/languages/definitions/java.js +0 -2
  165. package/dist/languages/definitions/java.js.map +1 -1
  166. package/dist/languages/definitions/javascript.d.ts.map +1 -1
  167. package/dist/languages/definitions/javascript.js +0 -2
  168. package/dist/languages/definitions/javascript.js.map +1 -1
  169. package/dist/languages/definitions/kotlin.d.ts.map +1 -1
  170. package/dist/languages/definitions/kotlin.js +0 -2
  171. package/dist/languages/definitions/kotlin.js.map +1 -1
  172. package/dist/languages/definitions/less.d.ts.map +1 -1
  173. package/dist/languages/definitions/less.js +0 -2
  174. package/dist/languages/definitions/less.js.map +1 -1
  175. package/dist/languages/definitions/php.d.ts.map +1 -1
  176. package/dist/languages/definitions/php.js +0 -2
  177. package/dist/languages/definitions/php.js.map +1 -1
  178. package/dist/languages/definitions/python.d.ts.map +1 -1
  179. package/dist/languages/definitions/python.js +0 -2
  180. package/dist/languages/definitions/python.js.map +1 -1
  181. package/dist/languages/definitions/ruby.d.ts.map +1 -1
  182. package/dist/languages/definitions/ruby.js +0 -2
  183. package/dist/languages/definitions/ruby.js.map +1 -1
  184. package/dist/languages/definitions/rust.d.ts.map +1 -1
  185. package/dist/languages/definitions/rust.js +0 -2
  186. package/dist/languages/definitions/rust.js.map +1 -1
  187. package/dist/languages/definitions/scss.d.ts.map +1 -1
  188. package/dist/languages/definitions/scss.js +0 -2
  189. package/dist/languages/definitions/scss.js.map +1 -1
  190. package/dist/languages/definitions/sql.d.ts.map +1 -1
  191. package/dist/languages/definitions/sql.js +0 -2
  192. package/dist/languages/definitions/sql.js.map +1 -1
  193. package/dist/languages/definitions/svelte.d.ts.map +1 -1
  194. package/dist/languages/definitions/svelte.js +0 -2
  195. package/dist/languages/definitions/svelte.js.map +1 -1
  196. package/dist/languages/definitions/swift.d.ts.map +1 -1
  197. package/dist/languages/definitions/swift.js +0 -2
  198. package/dist/languages/definitions/swift.js.map +1 -1
  199. package/dist/languages/definitions/typescript.d.ts.map +1 -1
  200. package/dist/languages/definitions/typescript.js +0 -3
  201. package/dist/languages/definitions/typescript.js.map +1 -1
  202. package/dist/languages/definitions/vue.d.ts.map +1 -1
  203. package/dist/languages/definitions/vue.js +0 -2
  204. package/dist/languages/definitions/vue.js.map +1 -1
  205. package/dist/languages/definitions/zig.d.ts.map +1 -1
  206. package/dist/languages/definitions/zig.js +0 -2
  207. package/dist/languages/definitions/zig.js.map +1 -1
  208. package/dist/languages/filePrep.d.ts +0 -2
  209. package/dist/languages/filePrep.d.ts.map +1 -1
  210. package/dist/languages/filePrep.js +0 -1
  211. package/dist/languages/filePrep.js.map +1 -1
  212. package/dist/languages/types.d.ts +19 -5
  213. package/dist/languages/types.d.ts.map +1 -1
  214. package/dist/languages.d.ts +1 -3
  215. package/dist/languages.d.ts.map +1 -1
  216. package/dist/languages.js +0 -7
  217. package/dist/languages.js.map +1 -1
  218. package/dist/lifecycle/manifest.d.ts.map +1 -1
  219. package/dist/lifecycle/manifest.js +21 -4
  220. package/dist/lifecycle/manifest.js.map +1 -1
  221. package/dist/mcp/handlers.d.ts +275 -0
  222. package/dist/mcp/handlers.d.ts.map +1 -0
  223. package/dist/mcp/handlers.js +657 -0
  224. package/dist/mcp/handlers.js.map +1 -0
  225. package/dist/mcp/httpTransport.d.ts +14 -0
  226. package/dist/mcp/httpTransport.d.ts.map +1 -0
  227. package/dist/mcp/httpTransport.js +185 -0
  228. package/dist/mcp/httpTransport.js.map +1 -0
  229. package/dist/mcp/legacySessions.d.ts +38 -0
  230. package/dist/mcp/legacySessions.d.ts.map +1 -0
  231. package/dist/mcp/legacySessions.js +264 -0
  232. package/dist/mcp/legacySessions.js.map +1 -0
  233. package/dist/mcp/lifecycleHealth.d.ts +12 -0
  234. package/dist/mcp/lifecycleHealth.d.ts.map +1 -0
  235. package/dist/mcp/lifecycleHealth.js +37 -0
  236. package/dist/mcp/lifecycleHealth.js.map +1 -0
  237. package/dist/mcp/protocol.d.ts +35 -0
  238. package/dist/mcp/protocol.d.ts.map +1 -0
  239. package/dist/mcp/protocol.js +577 -0
  240. package/dist/mcp/protocol.js.map +1 -0
  241. package/dist/mcp/server.d.ts +9 -306
  242. package/dist/mcp/server.d.ts.map +1 -1
  243. package/dist/mcp/server.js +13 -1609
  244. package/dist/mcp/server.js.map +1 -1
  245. package/dist/native/bindingLoader.d.ts +27 -0
  246. package/dist/native/bindingLoader.d.ts.map +1 -1
  247. package/dist/native/bindingLoader.js +18 -1
  248. package/dist/native/bindingLoader.js.map +1 -1
  249. package/dist/native/contracts.d.ts +59 -12
  250. package/dist/native/contracts.d.ts.map +1 -1
  251. package/dist/native/execution.d.ts +10 -1
  252. package/dist/native/execution.d.ts.map +1 -1
  253. package/dist/native/execution.js +59 -3
  254. package/dist/native/execution.js.map +1 -1
  255. package/dist/native/projectedTree.d.ts +20 -5
  256. package/dist/native/projectedTree.d.ts.map +1 -1
  257. package/dist/native/projectedTree.js +100 -48
  258. package/dist/native/projectedTree.js.map +1 -1
  259. package/dist/native/runtime.d.ts +47 -1
  260. package/dist/native/runtime.d.ts.map +1 -1
  261. package/dist/native/runtime.js +151 -8
  262. package/dist/native/runtime.js.map +1 -1
  263. package/dist/native/runtimeCache.d.ts +92 -0
  264. package/dist/native/runtimeCache.d.ts.map +1 -1
  265. package/dist/native/runtimeCache.js +443 -13
  266. package/dist/native/runtimeCache.js.map +1 -1
  267. package/dist/native/treeShape.d.ts +21 -0
  268. package/dist/native/treeShape.d.ts.map +1 -0
  269. package/dist/native/treeShape.js +28 -0
  270. package/dist/native/treeShape.js.map +1 -0
  271. package/dist/native/treeSitterNative.d.ts +3 -3
  272. package/dist/native/treeSitterNative.d.ts.map +1 -1
  273. package/dist/native/treeSitterNative.js +2 -2
  274. package/dist/native/treeSitterNative.js.map +1 -1
  275. package/dist/presets.d.ts +7 -1
  276. package/dist/presets.d.ts.map +1 -1
  277. package/dist/presets.js +4 -0
  278. package/dist/presets.js.map +1 -1
  279. package/dist/review/deleted.js +2 -2
  280. package/dist/review/deleted.js.map +1 -1
  281. package/dist/review.js.map +1 -1
  282. package/dist/sql/extractFacts.d.ts.map +1 -1
  283. package/dist/sql/extractFacts.js +1 -1
  284. package/dist/sql/extractFacts.js.map +1 -1
  285. package/dist/util/lazySymbols.js +1 -1
  286. package/dist/util/lazySymbols.js.map +1 -1
  287. package/dist/util/resolution/jvm.js +1 -1
  288. package/dist/util/resolution/jvm.js.map +1 -1
  289. package/dist/util/resolution/php.d.ts.map +1 -1
  290. package/dist/util/resolution/php.js +0 -1
  291. package/dist/util/resolution/php.js.map +1 -1
  292. package/dist/util/resolution.d.ts.map +1 -1
  293. package/dist/util/resolution.js +5 -5
  294. package/dist/util/resolution.js.map +1 -1
  295. package/dist/worker/nativeExtractWorker.d.ts +10 -4
  296. package/dist/worker/nativeExtractWorker.d.ts.map +1 -1
  297. package/dist/worker/nativeExtractWorker.js +55 -3
  298. package/dist/worker/nativeExtractWorker.js.map +1 -1
  299. package/dist/worker/nativeWorkerPool.d.ts +4 -0
  300. package/dist/worker/nativeWorkerPool.d.ts.map +1 -1
  301. package/dist/worker/nativeWorkerPool.js +6 -1
  302. package/dist/worker/nativeWorkerPool.js.map +1 -1
  303. package/docs/mcp.md +16 -2
  304. package/package.json +3 -3
  305. package/dist/languages/definitions/loadLanguage.d.ts +0 -2
  306. package/dist/languages/definitions/loadLanguage.d.ts.map +0 -1
  307. package/dist/languages/definitions/loadLanguage.js +0 -2
  308. package/dist/languages/definitions/loadLanguage.js.map +0 -1
  309. package/dist/parserBackend.d.ts +0 -57
  310. package/dist/parserBackend.d.ts.map +0 -1
  311. package/dist/parserBackend.js +0 -33
  312. package/dist/parserBackend.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import type { NativeBindingOrigin } from "./contracts.js";
1
2
  export type NativeCacheManifestV1 = {
2
3
  schemaVersion: 1;
3
4
  packageName: string;
@@ -8,12 +9,57 @@ export type NativeCacheManifestV1 = {
8
9
  sourceSha256: string;
9
10
  cachedAt: string;
10
11
  };
12
+ /**
13
+ * Written as a path-keyed record beside `manifest.json` after a process has successfully
14
+ * loaded the cached binary. `manifest.json` is content-addressed and published once,
15
+ * immutably; these records are mutable (the TTL refreshes them) and carry facts that only exist
16
+ * after the addon loads. One record per source realpath lets projects sharing the binary retain
17
+ * their own origin without invalidating each other's fast path.
18
+ */
19
+ export type NativeCacheIdentityV1 = {
20
+ version: 1;
21
+ /** Size and mtime of the installed source binary at the last full verification. */
22
+ sourceSize: number;
23
+ sourceMtimeMs: number;
24
+ /** Size and mtime of the cached copy that was verified and then loaded. */
25
+ cachedSize: number;
26
+ cachedMtimeMs: number;
27
+ sha256: string;
28
+ cacheKey: string;
29
+ sourcePath: string;
30
+ loadedPath: string;
31
+ verifiedAt: string;
32
+ /**
33
+ * The runtime that proved this file loadable. The stat fields say the bytes are unchanged;
34
+ * they say nothing about whether this Node can still load them. A major Node upgrade changes
35
+ * the ABI, so the same bytes stop loading while every other check still matches, and the fast
36
+ * path would report an addon as available that the build then falls back away from.
37
+ */
38
+ runtime: {
39
+ abi: string;
40
+ platform: string;
41
+ arch: string;
42
+ };
43
+ /**
44
+ * Recorded so a cache-hit command can answer "which languages does the addon support"
45
+ * without loading it. Present only once a load has succeeded for this exact file.
46
+ */
47
+ supportedLanguageIds: string[];
48
+ /**
49
+ * The origin the loading process reported, stored whole rather than rebuilt field by field.
50
+ * The runtime fingerprint embeds this object verbatim, so replaying it is what guarantees a
51
+ * fast-path fingerprint is byte-identical to the one a full load produces.
52
+ */
53
+ origin: NativeBindingOrigin;
54
+ };
11
55
  export type PrepareNativeRuntimeCacheRequest = {
12
56
  sourcePath: string;
13
57
  packageName: string;
14
58
  packageVersion: string;
15
59
  target: string;
16
60
  cacheRoot?: string | undefined;
61
+ /** Injected in tests so the identity TTL branch is reachable without waiting a day. */
62
+ now?: number | undefined;
17
63
  };
18
64
  export type PrepareNativeRuntimeCacheResult = {
19
65
  status: "cached" | "reused";
@@ -21,6 +67,17 @@ export type PrepareNativeRuntimeCacheResult = {
21
67
  loadedPath: string;
22
68
  cacheKey: string;
23
69
  sha256: string;
70
+ /** Verified stat snapshots used to reject a source/cache replacement before identity recording. */
71
+ sourceSize: number;
72
+ sourceMtimeMs: number;
73
+ cachedSize: number;
74
+ cachedMtimeMs: number;
75
+ /**
76
+ * True when this call hashed the files rather than trusting a recorded identity. Only a
77
+ * verified result may refresh the identity record, otherwise each fast-path hit would
78
+ * push the re-verification TTL out and the entry would never be hashed again.
79
+ */
80
+ verified: boolean;
24
81
  } | {
25
82
  status: "unavailable";
26
83
  sourcePath: string;
@@ -29,8 +86,43 @@ export type PrepareNativeRuntimeCacheResult = {
29
86
  type FileHash = {
30
87
  sha256: string;
31
88
  size: number;
89
+ mtimeMs: number;
32
90
  };
33
91
  export declare function hashFileStreaming(filePath: string, onChunk?: (bytesRead: number) => void): FileHash;
92
+ export type NativeCacheLookupRequest = {
93
+ sourcePath: string;
94
+ packageName: string;
95
+ packageVersion: string;
96
+ target: string;
97
+ cacheRoot?: string | undefined;
98
+ /** Injected in tests so the TTL branch is reachable without waiting a day. */
99
+ now?: number | undefined;
100
+ };
101
+ export type NativeCacheLookupHit = {
102
+ entryPath: string;
103
+ identity: NativeCacheIdentityV1;
104
+ revalidateAt: number;
105
+ };
106
+ /**
107
+ * Find a cache entry for this (package version, target) whose recorded identity still matches
108
+ * the files on disk, without hashing either of them.
109
+ *
110
+ * The entry directory is named `<packageVersion>-<sha256>`, so locating it the normal way means
111
+ * hashing the source first. Scanning the target directory for the version prefix and comparing
112
+ * the stat fields recorded at the last full verification reaches the same entry with bounded
113
+ * metadata reads. A miss - no entry, no identity file, a stat mismatch, or an expired
114
+ * TTL - returns null, and every caller then does the full hash-and-verify pass.
115
+ */
116
+ export declare function lookupNativeRuntimeCacheEntry(request: NativeCacheLookupRequest): NativeCacheLookupHit | null;
117
+ /**
118
+ * Record what a successful load proved about this entry, so the next process can skip both
119
+ * SHA-256 passes and the addon load itself. Called only after the binary at `loadedPath` has
120
+ * been fully verified and then loaded, so every field describes a state known to work.
121
+ *
122
+ * Replaces any previous record atomically: the TTL refresh rewrites it, unlike the immutable
123
+ * `manifest.json` beside it.
124
+ */
125
+ export declare function recordNativeRuntimeCacheIdentity(identity: Omit<NativeCacheIdentityV1, "version" | "runtime">): void;
34
126
  export declare function prepareNativeRuntimeCache(request: PrepareNativeRuntimeCacheRequest): PrepareNativeRuntimeCacheResult;
35
127
  export {};
36
128
  //# sourceMappingURL=runtimeCache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtimeCache.d.ts","sourceRoot":"","sources":["../../src/native/runtimeCache.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,qBAAqB,GAAG;IAClC,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,+BAA+B,GACvC;IACE,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEN,KAAK,QAAQ,GAAG;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AA8DF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,QAAQ,CA0BnG;AAgGD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,+BAA+B,CAoDpH"}
1
+ {"version":3,"file":"runtimeCache.d.ts","sourceRoot":"","sources":["../../src/native/runtimeCache.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,32 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  const CACHE_SCHEMA_VERSION = 1;
5
5
  const HASH_BUFFER_SIZE = 1024 * 1024;
6
+ const IDENTITY_FILE_PREFIX = "identity-";
7
+ const IDENTITY_FILE_SUFFIX = ".json";
8
+ const IDENTITY_VERSION = 1;
9
+ /**
10
+ * How long a stat-only match is trusted before the entry is hashed again.
11
+ *
12
+ * The fast path below skips two 29 MB SHA-256 passes when the source and cached binaries
13
+ * both match the size and mtime recorded at the last full verification. That is weaker than
14
+ * hashing: an attacker who can write the cache directory can also preserve both fields. The
15
+ * TTL bounds how long such a substitution goes unnoticed, so verification still happens -
16
+ * once a day per entry instead of twice per process.
17
+ */
18
+ const IDENTITY_REVERIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
19
+ /**
20
+ * How long an entry for another package version is kept before it is treated as abandoned.
21
+ *
22
+ * The cache root is per-user and shared by every project on the machine, so "not the version I
23
+ * am installing" does not mean "nobody's". Deleting on that basis alone makes two projects
24
+ * pinned to different native versions delete each other's entry on every run, each re-copying
25
+ * 29 MB and never reaching the fast path - worse than the unbounded growth it set out to fix.
26
+ *
27
+ * An entry in use is re-verified at least every IDENTITY_REVERIFY_INTERVAL_MS of use, and that
28
+ * refreshes its timestamp, so an entry older than this window is one no project has run in a
29
+ * month. That is the signal, rather than which version happens to be installing right now.
30
+ */
31
+ const ABANDONED_ENTRY_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
6
32
  function asError(error) {
7
33
  return error instanceof Error ? error : new Error(String(error));
8
34
  }
@@ -24,7 +50,7 @@ function assertSafePathComponent(componentPath) {
24
50
  throw new Error(`unsafe native cache path component: ${componentPath}`);
25
51
  }
26
52
  }
27
- function prepareSafeDirectory(directoryPath) {
53
+ function prepareSafeDirectory(directoryPath, create = true) {
28
54
  const absolute = path.resolve(directoryPath);
29
55
  const parsed = path.parse(absolute);
30
56
  let current = parsed.root;
@@ -36,7 +62,7 @@ function prepareSafeDirectory(directoryPath) {
36
62
  assertSafePathComponent(current);
37
63
  }
38
64
  catch (error) {
39
- if (error.code !== "ENOENT") {
65
+ if (error.code !== "ENOENT" || !create) {
40
66
  throw error;
41
67
  }
42
68
  try {
@@ -51,12 +77,18 @@ function prepareSafeDirectory(directoryPath) {
51
77
  }
52
78
  return fs.realpathSync.native(absolute);
53
79
  }
54
- function resolveProductionCacheRoot() {
80
+ function findProductionCacheRoot() {
55
81
  const localAppData = process.env.LOCALAPPDATA;
56
- if (!localAppData) {
82
+ if (!localAppData)
83
+ return null;
84
+ return path.join(localAppData, "codegraph", "native-cache", `v${CACHE_SCHEMA_VERSION}`);
85
+ }
86
+ function resolveProductionCacheRoot() {
87
+ const cacheRoot = findProductionCacheRoot();
88
+ if (!cacheRoot) {
57
89
  throw new Error("LOCALAPPDATA is unavailable; cannot prepare the Windows native runtime cache");
58
90
  }
59
- return path.join(localAppData, "codegraph", "native-cache", `v${CACHE_SCHEMA_VERSION}`);
91
+ return cacheRoot;
60
92
  }
61
93
  export function hashFileStreaming(filePath, onChunk) {
62
94
  const before = fs.statSync(filePath);
@@ -80,10 +112,11 @@ export function hashFileStreaming(filePath, onChunk) {
80
112
  finally {
81
113
  fs.closeSync(descriptor);
82
114
  }
83
- if (size !== before.size) {
115
+ const after = fs.statSync(filePath);
116
+ if (!after.isFile() || size !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
84
117
  throw new Error(`native binary changed while hashing: ${filePath}`);
85
118
  }
86
- return { sha256: hash.digest("hex"), size };
119
+ return { sha256: hash.digest("hex"), size, mtimeMs: before.mtimeMs };
87
120
  }
88
121
  function randomSuffix() {
89
122
  return `${process.pid}.${randomBytes(8).toString("hex")}`;
@@ -124,10 +157,10 @@ function removeRegularTemporaryFile(filePath) {
124
157
  function verifyFile(filePath, expected) {
125
158
  try {
126
159
  const actual = hashFileStreaming(filePath);
127
- return actual.size === expected.size && actual.sha256 === expected.sha256;
160
+ return actual.size === expected.size && actual.sha256 === expected.sha256 ? actual : null;
128
161
  }
129
162
  catch {
130
- return false;
163
+ return null;
131
164
  }
132
165
  }
133
166
  function publishExclusive(temporaryPath, finalPath) {
@@ -183,11 +216,399 @@ function writeManifest(entryPath, manifest) {
183
216
  throw error;
184
217
  }
185
218
  }
219
+ function currentRuntimeStamp() {
220
+ return { abi: process.versions.modules, platform: process.platform, arch: process.arch };
221
+ }
222
+ function identityFilePath(entryPath, sourcePath) {
223
+ const sourceKey = createHash("sha256").update(sourcePath).digest("hex");
224
+ return path.join(entryPath, `${IDENTITY_FILE_PREFIX}${sourceKey}${IDENTITY_FILE_SUFFIX}`);
225
+ }
226
+ function isIdentityFileName(fileName) {
227
+ return /^identity-[a-f0-9]{64}\.json$/.test(fileName);
228
+ }
229
+ function readCacheIdentityFile(filePath) {
230
+ let parsed;
231
+ try {
232
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
233
+ }
234
+ catch {
235
+ // Absent (an entry written before this file existed), unreadable, or malformed. All three
236
+ // mean the same thing to the caller: no fast path, fall back to hashing.
237
+ return null;
238
+ }
239
+ if (!parsed || typeof parsed !== "object")
240
+ return null;
241
+ const candidate = parsed;
242
+ if (candidate.version !== IDENTITY_VERSION)
243
+ return null;
244
+ const numbers = [candidate.sourceSize, candidate.sourceMtimeMs, candidate.cachedSize, candidate.cachedMtimeMs];
245
+ if (numbers.some((value) => typeof value !== "number" || !Number.isFinite(value)))
246
+ return null;
247
+ const strings = [
248
+ candidate.sha256,
249
+ candidate.cacheKey,
250
+ candidate.sourcePath,
251
+ candidate.loadedPath,
252
+ candidate.verifiedAt,
253
+ ];
254
+ if (strings.some((value) => typeof value !== "string" || !value))
255
+ return null;
256
+ if (!Array.isArray(candidate.supportedLanguageIds))
257
+ return null;
258
+ if (candidate.supportedLanguageIds.some((value) => typeof value !== "string"))
259
+ return null;
260
+ const origin = candidate.origin;
261
+ if (!origin || typeof origin !== "object" || origin.mode !== "cache" || typeof origin.packageName !== "string") {
262
+ return null;
263
+ }
264
+ const runtime = candidate.runtime;
265
+ if (!runtime || typeof runtime !== "object")
266
+ return null;
267
+ if (typeof runtime.abi !== "string" || typeof runtime.platform !== "string" || typeof runtime.arch !== "string") {
268
+ return null;
269
+ }
270
+ return candidate;
271
+ }
272
+ function readCacheIdentity(entryPath, sourcePath) {
273
+ return readCacheIdentityFile(identityFilePath(entryPath, sourcePath));
274
+ }
275
+ function latestCacheIdentityVerifiedAt(entryPath, now) {
276
+ let fileNames;
277
+ try {
278
+ fileNames = fs.readdirSync(entryPath);
279
+ }
280
+ catch {
281
+ return Number.NaN;
282
+ }
283
+ let latest = Number.NaN;
284
+ for (const fileName of fileNames) {
285
+ if (!isIdentityFileName(fileName))
286
+ continue;
287
+ const identity = readCacheIdentityFile(path.join(entryPath, fileName));
288
+ const verifiedAt = identity ? Date.parse(identity.verifiedAt) : Number.NaN;
289
+ if (!Number.isFinite(verifiedAt) || verifiedAt > now)
290
+ continue;
291
+ if (!Number.isFinite(latest) || verifiedAt > latest)
292
+ latest = verifiedAt;
293
+ }
294
+ return latest;
295
+ }
296
+ function statOrNull(filePath) {
297
+ try {
298
+ const stats = fs.statSync(filePath);
299
+ return stats.isFile() ? stats : null;
300
+ }
301
+ catch {
302
+ return null;
303
+ }
304
+ }
305
+ function lstatRegularFileOrNull(filePath) {
306
+ try {
307
+ const stats = fs.lstatSync(filePath);
308
+ return stats.isFile() && !stats.isSymbolicLink() ? stats : null;
309
+ }
310
+ catch {
311
+ return null;
312
+ }
313
+ }
314
+ function isSafeExistingDirectory(directoryPath) {
315
+ try {
316
+ prepareSafeDirectory(directoryPath, false);
317
+ return true;
318
+ }
319
+ catch {
320
+ return false;
321
+ }
322
+ }
323
+ function pathsResolveEqual(left, right) {
324
+ return path.resolve(left) === path.resolve(right);
325
+ }
326
+ /**
327
+ * Find a cache entry for this (package version, target) whose recorded identity still matches
328
+ * the files on disk, without hashing either of them.
329
+ *
330
+ * The entry directory is named `<packageVersion>-<sha256>`, so locating it the normal way means
331
+ * hashing the source first. Scanning the target directory for the version prefix and comparing
332
+ * the stat fields recorded at the last full verification reaches the same entry with bounded
333
+ * metadata reads. A miss - no entry, no identity file, a stat mismatch, or an expired
334
+ * TTL - returns null, and every caller then does the full hash-and-verify pass.
335
+ */
336
+ export function lookupNativeRuntimeCacheEntry(request) {
337
+ try {
338
+ assertSafeCacheSegment(request.target, "target");
339
+ assertSafeCacheSegment(request.packageVersion, "package version");
340
+ const cacheRoot = request.cacheRoot ?? findProductionCacheRoot();
341
+ if (!cacheRoot)
342
+ return null;
343
+ const sourceRealPath = fs.realpathSync.native(request.sourcePath);
344
+ const sourceStats = statOrNull(sourceRealPath);
345
+ if (!sourceStats)
346
+ return null;
347
+ const runtime = currentRuntimeStamp();
348
+ const resolvedCacheRoot = path.resolve(cacheRoot);
349
+ const targetPath = path.join(resolvedCacheRoot, request.target);
350
+ if (!isSafeExistingDirectory(resolvedCacheRoot) || !isSafeExistingDirectory(targetPath))
351
+ return null;
352
+ const prefix = `${request.packageVersion}-`;
353
+ let entryNames;
354
+ try {
355
+ entryNames = fs.readdirSync(targetPath);
356
+ }
357
+ catch {
358
+ return null;
359
+ }
360
+ const now = request.now ?? Date.now();
361
+ for (const entryName of entryNames) {
362
+ if (!entryName.startsWith(prefix))
363
+ continue;
364
+ const entryPath = path.join(targetPath, entryName);
365
+ if (!isSafeExistingDirectory(entryPath))
366
+ continue;
367
+ const identity = readCacheIdentity(entryPath, sourceRealPath);
368
+ if (!identity)
369
+ continue;
370
+ const expectedEntryName = `${request.packageVersion}-${identity.sha256}`;
371
+ const expectedCacheKey = `${request.packageName}@${request.packageVersion}:${request.target}:${identity.sha256}`;
372
+ const loadedPath = path.resolve(identity.loadedPath);
373
+ if (entryName !== expectedEntryName ||
374
+ identity.cacheKey !== expectedCacheKey ||
375
+ identity.loadedPath !== loadedPath ||
376
+ path.dirname(loadedPath) !== entryPath ||
377
+ path.basename(loadedPath) !== path.basename(sourceRealPath)) {
378
+ continue;
379
+ }
380
+ // Same version, same bytes, different install. The cache root is shared by every project
381
+ // on the machine, and npm can produce byte-identical files with matching mtimes, so size
382
+ // and mtime alone can match another project's record - whose sourcePath would then be
383
+ // replayed into this build's binding origin and change its runtime fingerprint.
384
+ if (identity.sourcePath !== sourceRealPath)
385
+ continue;
386
+ if (identity.sourceSize !== sourceStats.size || identity.sourceMtimeMs !== sourceStats.mtimeMs)
387
+ continue;
388
+ if (identity.origin.packageName !== request.packageName ||
389
+ identity.origin.packageVersion !== request.packageVersion ||
390
+ identity.origin.target !== request.target ||
391
+ identity.origin.sourcePath === undefined ||
392
+ identity.origin.loadedPath === undefined ||
393
+ !pathsResolveEqual(identity.origin.sourcePath, sourceRealPath) ||
394
+ !pathsResolveEqual(identity.origin.loadedPath, identity.loadedPath) ||
395
+ identity.origin.cacheKey !== identity.cacheKey ||
396
+ identity.origin.sha256 !== identity.sha256) {
397
+ continue;
398
+ }
399
+ // Recorded by a runtime that could load the file. A different ABI cannot, and claiming
400
+ // otherwise would report native as available for a build that falls back away from it.
401
+ if (identity.runtime.abi !== runtime.abi ||
402
+ identity.runtime.platform !== runtime.platform ||
403
+ identity.runtime.arch !== runtime.arch) {
404
+ continue;
405
+ }
406
+ const cachedStats = lstatRegularFileOrNull(identity.loadedPath);
407
+ if (!cachedStats)
408
+ continue;
409
+ if (identity.cachedSize !== cachedStats.size || identity.cachedMtimeMs !== cachedStats.mtimeMs)
410
+ continue;
411
+ const verifiedAt = Date.parse(identity.verifiedAt);
412
+ if (!Number.isFinite(verifiedAt) || verifiedAt > now || now - verifiedAt >= IDENTITY_REVERIFY_INTERVAL_MS) {
413
+ continue;
414
+ }
415
+ if (!isWithinDirectory(identity.loadedPath, resolvedCacheRoot))
416
+ continue;
417
+ return { entryPath, identity, revalidateAt: verifiedAt + IDENTITY_REVERIFY_INTERVAL_MS };
418
+ }
419
+ return null;
420
+ }
421
+ catch {
422
+ return null;
423
+ }
424
+ }
425
+ /**
426
+ * Record what a successful load proved about this entry, so the next process can skip both
427
+ * SHA-256 passes and the addon load itself. Called only after the binary at `loadedPath` has
428
+ * been fully verified and then loaded, so every field describes a state known to work.
429
+ *
430
+ * Replaces any previous record atomically: the TTL refresh rewrites it, unlike the immutable
431
+ * `manifest.json` beside it.
432
+ */
433
+ export function recordNativeRuntimeCacheIdentity(identity) {
434
+ try {
435
+ const sourceStats = statOrNull(identity.sourcePath);
436
+ const cachedStats = lstatRegularFileOrNull(identity.loadedPath);
437
+ if (!sourceStats ||
438
+ !cachedStats ||
439
+ sourceStats.size !== identity.sourceSize ||
440
+ sourceStats.mtimeMs !== identity.sourceMtimeMs ||
441
+ cachedStats.size !== identity.cachedSize ||
442
+ cachedStats.mtimeMs !== identity.cachedMtimeMs) {
443
+ return;
444
+ }
445
+ const entryPath = path.dirname(identity.loadedPath);
446
+ const record = {
447
+ version: IDENTITY_VERSION,
448
+ runtime: currentRuntimeStamp(),
449
+ ...identity,
450
+ };
451
+ const finalPath = identityFilePath(entryPath, identity.sourcePath);
452
+ const temporaryPath = `${finalPath}.${randomSuffix()}.tmp`;
453
+ try {
454
+ const descriptor = fs.openSync(temporaryPath, "wx");
455
+ try {
456
+ fs.writeFileSync(descriptor, `${JSON.stringify(record, null, 2)}\n`, "utf8");
457
+ fs.fsyncSync(descriptor);
458
+ }
459
+ finally {
460
+ fs.closeSync(descriptor);
461
+ }
462
+ fs.renameSync(temporaryPath, finalPath);
463
+ }
464
+ catch {
465
+ removeRegularTemporaryFile(temporaryPath);
466
+ }
467
+ }
468
+ catch {
469
+ // Best effort. Failing to record identity costs the next process the slow path, nothing more.
470
+ }
471
+ }
472
+ /**
473
+ * When an entry was last known to be in use, and which version it holds.
474
+ *
475
+ * Path-keyed identity records are refreshed whenever a process re-verifies an entry, so their
476
+ * latest non-future timestamp is the freshest signal. `manifest.json`'s `cachedAt` covers an
477
+ * entry that has never been loaded through the fast path. An entry that yields neither is left
478
+ * alone rather than guessed about.
479
+ */
480
+ function readEntrySummary(entryPath, now) {
481
+ let packageVersion = null;
482
+ let sourceFileName = null;
483
+ let cachedAtMs = null;
484
+ try {
485
+ const parsed = JSON.parse(fs.readFileSync(path.join(entryPath, "manifest.json"), "utf8"));
486
+ if (parsed && typeof parsed === "object") {
487
+ const manifest = parsed;
488
+ if (typeof manifest.packageVersion === "string" && manifest.packageVersion) {
489
+ packageVersion = manifest.packageVersion;
490
+ }
491
+ if (typeof manifest.sourceFileName === "string" &&
492
+ manifest.sourceFileName &&
493
+ path.basename(manifest.sourceFileName) === manifest.sourceFileName) {
494
+ sourceFileName = manifest.sourceFileName;
495
+ }
496
+ const cachedAt = typeof manifest.cachedAt === "string" ? Date.parse(manifest.cachedAt) : Number.NaN;
497
+ if (Number.isFinite(cachedAt))
498
+ cachedAtMs = cachedAt;
499
+ }
500
+ }
501
+ catch {
502
+ // Missing or malformed: indistinguishable from an entry another process is still writing.
503
+ return null;
504
+ }
505
+ if (!packageVersion || !sourceFileName)
506
+ return null;
507
+ const verifiedAt = latestCacheIdentityVerifiedAt(entryPath, now);
508
+ const lastUsedMs = Number.isFinite(verifiedAt) ? verifiedAt : cachedAtMs;
509
+ if (lastUsedMs === null)
510
+ return null;
511
+ return { packageVersion, lastUsedMs, sourceFileName };
512
+ }
513
+ /**
514
+ * Delete the mapped binary before its metadata. Windows rejects an unlink of a loaded DLL,
515
+ * leaving the whole entry unchanged instead of partially deleting it through recursive removal.
516
+ */
517
+ function removeAbandonedEntry(entryPath, sourceFileName) {
518
+ const cachedBinaryPath = path.join(entryPath, sourceFileName);
519
+ if (!isWithinDirectory(cachedBinaryPath, entryPath) || path.dirname(cachedBinaryPath) !== entryPath) {
520
+ throw new Error("invalid native cache binary path");
521
+ }
522
+ const stats = fs.lstatSync(cachedBinaryPath);
523
+ if (!stats.isFile() || stats.isSymbolicLink())
524
+ throw new Error("unsafe native cache binary");
525
+ fs.unlinkSync(cachedBinaryPath);
526
+ fs.rmSync(entryPath, { recursive: true, force: true });
527
+ }
528
+ /**
529
+ * Delete entries that no project has used in a long time.
530
+ *
531
+ * The cache only ever grew: every upgrade left roughly 29 MB behind, and a machine that had
532
+ * tracked a few releases carried well over a hundred megabytes of addons nothing would load
533
+ * again. Keeping the directory small also keeps it warm, which is what the stat-only fast path
534
+ * above depends on.
535
+ *
536
+ * Retention is by age rather than by version, because the cache root is per-user and shared by
537
+ * every project on the machine. Removing "every version that is not the one installing" would
538
+ * make two projects pinned to different native versions delete each other's entry on every run.
539
+ *
540
+ * Best effort throughout, and deliberately conservative:
541
+ * - the entry just produced is never a candidate, nor is any entry for its version;
542
+ * - an entry whose manifest is missing, unreadable, or undated is left alone, because that is
543
+ * what a concurrent population in progress looks like;
544
+ * - Windows holds a lock on a loaded DLL, so unlinking its binary fails with EBUSY or EPERM
545
+ * before recursive metadata removal begins. Those failures preserve the whole entry and are
546
+ * expected rather than exceptional, so they are simply skipped.
547
+ */
548
+ function pruneAbandonedEntries(targetPath, cacheRoot, keepEntryPath, keepVersion, now) {
549
+ let entryNames;
550
+ try {
551
+ entryNames = fs.readdirSync(targetPath);
552
+ }
553
+ catch {
554
+ return;
555
+ }
556
+ for (const entryName of entryNames) {
557
+ const entryPath = path.join(targetPath, entryName);
558
+ if (entryPath === keepEntryPath)
559
+ continue;
560
+ if (!isWithinDirectory(entryPath, cacheRoot))
561
+ continue;
562
+ try {
563
+ if (!fs.lstatSync(entryPath).isDirectory())
564
+ continue;
565
+ }
566
+ catch {
567
+ continue;
568
+ }
569
+ const summary = readEntrySummary(entryPath, now);
570
+ if (!summary || summary.packageVersion === keepVersion)
571
+ continue;
572
+ if (now - summary.lastUsedMs < ABANDONED_ENTRY_RETENTION_MS)
573
+ continue;
574
+ try {
575
+ removeAbandonedEntry(entryPath, summary.sourceFileName);
576
+ }
577
+ catch {
578
+ // In use, incomplete, or not ours to remove. The next install tries again.
579
+ }
580
+ }
581
+ }
186
582
  export function prepareNativeRuntimeCache(request) {
187
583
  const sourcePath = path.resolve(request.sourcePath);
188
584
  try {
189
585
  assertSafeCacheSegment(request.target, "target");
190
586
  assertSafeCacheSegment(request.packageVersion, "package version");
587
+ // Cheap-identity fast path: a previous process verified this exact pair of files and
588
+ // recorded their stat fields, so nothing here needs to be hashed. Population and every
589
+ // mismatch still take the full path below.
590
+ const hit = lookupNativeRuntimeCacheEntry({
591
+ sourcePath,
592
+ packageName: request.packageName,
593
+ packageVersion: request.packageVersion,
594
+ target: request.target,
595
+ ...(request.cacheRoot !== undefined ? { cacheRoot: request.cacheRoot } : {}),
596
+ ...(request.now !== undefined ? { now: request.now } : {}),
597
+ });
598
+ if (hit) {
599
+ return {
600
+ status: "reused",
601
+ sourcePath: hit.identity.sourcePath,
602
+ loadedPath: hit.identity.loadedPath,
603
+ cacheKey: hit.identity.cacheKey,
604
+ sha256: hit.identity.sha256,
605
+ sourceSize: hit.identity.sourceSize,
606
+ sourceMtimeMs: hit.identity.sourceMtimeMs,
607
+ cachedSize: hit.identity.cachedSize,
608
+ cachedMtimeMs: hit.identity.cachedMtimeMs,
609
+ verified: false,
610
+ };
611
+ }
191
612
  const sourceRealPath = fs.realpathSync.native(sourcePath);
192
613
  const source = hashFileStreaming(sourceRealPath);
193
614
  const cacheRoot = prepareSafeDirectory(request.cacheRoot ?? resolveProductionCacheRoot());
@@ -203,12 +624,13 @@ export function prepareNativeRuntimeCache(request) {
203
624
  const sourceFileName = path.basename(sourceRealPath);
204
625
  const finalPath = path.join(entryPath, sourceFileName);
205
626
  let status = "reused";
206
- const finalExists = fs.existsSync(finalPath);
207
- if (!finalExists || !verifyFile(finalPath, source)) {
208
- if (finalExists)
627
+ let cached = fs.existsSync(finalPath) ? verifyFile(finalPath, source) : null;
628
+ if (!cached) {
629
+ if (fs.existsSync(finalPath))
209
630
  recoverCorruptFinal(finalPath);
210
631
  status = populateBinary(sourceRealPath, finalPath, source) ? "cached" : "reused";
211
- if (!verifyFile(finalPath, source)) {
632
+ cached = verifyFile(finalPath, source);
633
+ if (!cached) {
212
634
  throw new Error("native cache winner failed integrity verification");
213
635
  }
214
636
  }
@@ -222,12 +644,20 @@ export function prepareNativeRuntimeCache(request) {
222
644
  sourceSha256: source.sha256,
223
645
  cachedAt: new Date().toISOString(),
224
646
  });
647
+ // Only on the verified path. A fast-path hit means nothing new arrived, so there is nothing
648
+ // to clean up, and the warm run stays free of extra directory work.
649
+ pruneAbandonedEntries(targetPath, cacheRoot, entryPath, request.packageVersion, request.now ?? Date.now());
225
650
  return {
226
651
  status,
227
652
  sourcePath: sourceRealPath,
228
653
  loadedPath: finalPath,
229
654
  cacheKey,
230
655
  sha256: source.sha256,
656
+ sourceSize: source.size,
657
+ sourceMtimeMs: source.mtimeMs,
658
+ cachedSize: cached.size,
659
+ cachedMtimeMs: cached.mtimeMs,
660
+ verified: true,
231
661
  };
232
662
  }
233
663
  catch (error) {