gitnexus 1.6.10-rc.98 → 1.6.10

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 (777) hide show
  1. package/README.md +177 -18
  2. package/dist/_shared/graph/types.d.ts +21 -2
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +3 -3
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/index.js +1 -1
  7. package/dist/_shared/index.js.map +1 -1
  8. package/dist/_shared/integrations/circuit-breaker.d.ts.map +1 -1
  9. package/dist/_shared/integrations/circuit-breaker.js.map +1 -1
  10. package/dist/_shared/integrations/resilient-fetch.d.ts +16 -0
  11. package/dist/_shared/integrations/resilient-fetch.d.ts.map +1 -1
  12. package/dist/_shared/integrations/resilient-fetch.js +19 -7
  13. package/dist/_shared/integrations/resilient-fetch.js.map +1 -1
  14. package/dist/_shared/language-detection.d.ts.map +1 -1
  15. package/dist/_shared/lbug/schema-constants.d.ts +2 -2
  16. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  17. package/dist/_shared/lbug/schema-constants.js +3 -0
  18. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  19. package/dist/_shared/scope-resolution/evidence-weights.d.ts.map +1 -1
  20. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +5 -0
  21. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -1
  22. package/dist/_shared/scope-resolution/finalize-algorithm.js +315 -36
  23. package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -1
  24. package/dist/_shared/scope-resolution/reference-site.d.ts +115 -0
  25. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -1
  26. package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -1
  27. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -1
  28. package/dist/_shared/scope-resolution/registries/lookup-core.js +58 -2
  29. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
  30. package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -1
  31. package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
  32. package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
  33. package/dist/_shared/scope-resolution/symbol-definition.d.ts +47 -0
  34. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  35. package/dist/_shared/scope-resolution/types.d.ts +278 -3
  36. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  37. package/dist/cli/ai-context.d.ts +3 -2
  38. package/dist/cli/ai-context.js +71 -16
  39. package/dist/cli/analyze-config.d.ts +1 -1
  40. package/dist/cli/analyze-options.d.ts +131 -0
  41. package/dist/cli/analyze-options.js +1 -0
  42. package/dist/cli/analyze.d.ts +21 -113
  43. package/dist/cli/analyze.js +172 -29
  44. package/dist/cli/cli-message.d.ts +1 -1
  45. package/dist/cli/detect-changes-format.js +19 -2
  46. package/dist/cli/doctor.d.ts +12 -0
  47. package/dist/cli/doctor.js +32 -5
  48. package/dist/cli/editor-targets.js +6 -0
  49. package/dist/cli/eval-server.js +2 -1
  50. package/dist/cli/format-symbol.d.ts +15 -0
  51. package/dist/cli/format-symbol.js +17 -0
  52. package/dist/cli/generated-skill.d.ts +17 -0
  53. package/dist/cli/generated-skill.js +1 -0
  54. package/dist/cli/group.js +229 -21
  55. package/dist/cli/help-i18n.js +1 -2
  56. package/dist/cli/i18n/en.d.ts +11 -9
  57. package/dist/cli/i18n/en.js +13 -9
  58. package/dist/cli/i18n/resources.d.ts +15 -11
  59. package/dist/cli/i18n/zh-CN.d.ts +4 -2
  60. package/dist/cli/i18n/zh-CN.js +11 -9
  61. package/dist/cli/index.js +8 -4
  62. package/dist/cli/resolve-invocation.d.ts +7 -3
  63. package/dist/cli/resolve-invocation.js +7 -3
  64. package/dist/cli/setup.js +4 -3
  65. package/dist/cli/skill-gen.d.ts +1 -6
  66. package/dist/cli/tool.d.ts +1 -0
  67. package/dist/cli/tool.js +74 -17
  68. package/dist/cli/wiki.js +53 -10
  69. package/dist/config/ignore-service.d.ts +2 -0
  70. package/dist/config/ignore-service.js +54 -13
  71. package/dist/core/analyzer-identity.d.ts +80 -0
  72. package/dist/core/analyzer-identity.js +364 -26
  73. package/dist/core/augmentation/engine.js +86 -71
  74. package/dist/core/embedding-checkpoint.d.ts +143 -0
  75. package/dist/core/embedding-checkpoint.js +221 -0
  76. package/dist/core/embedding-count.d.ts +61 -0
  77. package/dist/core/embedding-count.js +69 -0
  78. package/dist/core/embeddings/ast-utils.js +9 -1
  79. package/dist/core/embeddings/embedding-pipeline.d.ts +8 -0
  80. package/dist/core/embeddings/embedding-pipeline.js +253 -14
  81. package/dist/core/embeddings/http-client.d.ts +5 -0
  82. package/dist/core/embeddings/http-client.js +142 -27
  83. package/dist/core/graph/graph.js +3 -0
  84. package/dist/core/graph/import-cycles.d.ts +194 -3
  85. package/dist/core/graph/import-cycles.js +522 -73
  86. package/dist/core/graph/types.d.ts +18 -0
  87. package/dist/core/group/bridge-db.d.ts +204 -1
  88. package/dist/core/group/bridge-db.js +401 -28
  89. package/dist/core/group/completeness.d.ts +130 -0
  90. package/dist/core/group/completeness.js +83 -0
  91. package/dist/core/group/config-parser.js +0 -5
  92. package/dist/core/group/cross-impact.d.ts +46 -1
  93. package/dist/core/group/cross-impact.js +220 -19
  94. package/dist/core/group/cross-trace.d.ts +32 -11
  95. package/dist/core/group/cross-trace.js +114 -20
  96. package/dist/core/group/extractors/http-patterns/java.js +278 -22
  97. package/dist/core/group/extractors/http-patterns/node.js +332 -216
  98. package/dist/core/group/extractors/http-patterns/types.d.ts +8 -0
  99. package/dist/core/group/extractors/http-route-extractor.d.ts +1 -0
  100. package/dist/core/group/extractors/http-route-extractor.js +122 -48
  101. package/dist/core/group/extractors/include-extractor.js +20 -2
  102. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  103. package/dist/core/group/extractors/manifest-extractor.js +7 -7
  104. package/dist/core/group/extractors/python-workspace-extractor.js +5 -2
  105. package/dist/core/group/group-lock.d.ts +31 -0
  106. package/dist/core/group/group-lock.js +166 -0
  107. package/dist/core/group/service.js +297 -16
  108. package/dist/core/group/storage.d.ts +3 -0
  109. package/dist/core/group/storage.js +7 -39
  110. package/dist/core/group/sync.d.ts +49 -3
  111. package/dist/core/group/sync.js +0 -0
  112. package/dist/core/group/types.d.ts +183 -8
  113. package/dist/core/group/types.js +28 -1
  114. package/dist/core/incremental/subgraph-extract.d.ts +9 -6
  115. package/dist/core/incremental/subgraph-extract.js +33 -10
  116. package/dist/core/index-freshness.d.ts +77 -2
  117. package/dist/core/index-freshness.js +109 -2
  118. package/dist/core/ingestion/call-processor.d.ts +13 -1
  119. package/dist/core/ingestion/call-processor.js +110 -3
  120. package/dist/core/ingestion/cfg/callee-cell-format.d.ts +42 -0
  121. package/dist/core/ingestion/cfg/callee-cell-format.js +42 -0
  122. package/dist/core/ingestion/cfg/emit.d.ts +8 -23
  123. package/dist/core/ingestion/cfg/emit.js +9 -23
  124. package/dist/core/ingestion/class-extractors/configs/go.js +10 -5
  125. package/dist/core/ingestion/class-extractors/configs/jvm.js +6 -6
  126. package/dist/core/ingestion/cluster-enricher.js +8 -4
  127. package/dist/core/ingestion/community-processor.d.ts +21 -3
  128. package/dist/core/ingestion/community-processor.js +81 -82
  129. package/dist/core/ingestion/di-extractors/index.d.ts +9 -33
  130. package/dist/core/ingestion/di-extractors/spring.d.ts +1 -1
  131. package/dist/core/ingestion/di-extractors/spring.js +8 -1
  132. package/dist/core/ingestion/di-extractors/types.d.ts +60 -0
  133. package/dist/core/ingestion/di-extractors/types.js +15 -0
  134. package/dist/core/ingestion/filesystem-walker.js +30 -14
  135. package/dist/core/ingestion/finalize-orchestrator.js +1 -0
  136. package/dist/core/ingestion/frameworks/spring/analysis-features.d.ts +6 -0
  137. package/dist/core/ingestion/frameworks/spring/analysis-features.js +35 -1
  138. package/dist/core/ingestion/frameworks/spring/annotation-arguments.d.ts +13 -0
  139. package/dist/core/ingestion/frameworks/spring/annotation-arguments.js +227 -0
  140. package/dist/core/ingestion/frameworks/spring/aop-candidates.d.ts +15 -0
  141. package/dist/core/ingestion/frameworks/spring/aop-candidates.js +85 -0
  142. package/dist/core/ingestion/frameworks/spring/aop.d.ts +118 -0
  143. package/dist/core/ingestion/frameworks/spring/aop.js +476 -0
  144. package/dist/core/ingestion/frameworks/spring/auto-configuration.d.ts +8 -0
  145. package/dist/core/ingestion/frameworks/spring/auto-configuration.js +16 -0
  146. package/dist/core/ingestion/frameworks/spring/bean-catalog.js +1 -0
  147. package/dist/core/ingestion/frameworks/spring/bean-factories.d.ts +33 -0
  148. package/dist/core/ingestion/frameworks/spring/bean-factories.js +65 -0
  149. package/dist/core/ingestion/frameworks/spring/conditionals.d.ts +25 -0
  150. package/dist/core/ingestion/frameworks/spring/conditionals.js +319 -0
  151. package/dist/core/ingestion/frameworks/spring/di-metadata.d.ts +3 -0
  152. package/dist/core/ingestion/frameworks/spring/di-metadata.js +162 -13
  153. package/dist/core/ingestion/frameworks/spring/non-http-handlers.d.ts +28 -0
  154. package/dist/core/ingestion/frameworks/spring/non-http-handlers.js +152 -0
  155. package/dist/core/ingestion/frameworks/spring/resource-injection.d.ts +5 -0
  156. package/dist/core/ingestion/frameworks/spring/resource-injection.js +91 -0
  157. package/dist/core/ingestion/import-resolvers/configs/csharp.js +5 -1
  158. package/dist/core/ingestion/import-resolvers/configs/swift.js +13 -0
  159. package/dist/core/ingestion/import-resolvers/csharp.d.ts +9 -1
  160. package/dist/core/ingestion/import-resolvers/csharp.js +234 -27
  161. package/dist/core/ingestion/import-resolvers/go.d.ts +11 -1
  162. package/dist/core/ingestion/import-resolvers/go.js +44 -17
  163. package/dist/core/ingestion/import-resolvers/jvm.d.ts +2 -2
  164. package/dist/core/ingestion/import-resolvers/node-workspace-packages.d.ts +90 -0
  165. package/dist/core/ingestion/import-resolvers/node-workspace-packages.js +429 -0
  166. package/dist/core/ingestion/import-resolvers/package-dir-index.d.ts +124 -0
  167. package/dist/core/ingestion/import-resolvers/package-dir-index.js +223 -0
  168. package/dist/core/ingestion/import-resolvers/pass-cache.d.ts +50 -0
  169. package/dist/core/ingestion/import-resolvers/pass-cache.js +60 -0
  170. package/dist/core/ingestion/import-resolvers/per-file-set.d.ts +71 -0
  171. package/dist/core/ingestion/import-resolvers/per-file-set.js +11 -0
  172. package/dist/core/ingestion/import-resolvers/php.d.ts +1 -1
  173. package/dist/core/ingestion/import-resolvers/php.js +65 -19
  174. package/dist/core/ingestion/import-resolvers/python-file-index.d.ts +201 -0
  175. package/dist/core/ingestion/import-resolvers/python-file-index.js +302 -0
  176. package/dist/core/ingestion/import-resolvers/python.js +25 -6
  177. package/dist/core/ingestion/import-resolvers/ruby.d.ts +1 -1
  178. package/dist/core/ingestion/import-resolvers/rust.d.ts +2 -2
  179. package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
  180. package/dist/core/ingestion/import-resolvers/types.d.ts +1 -12
  181. package/dist/core/ingestion/import-resolvers/utils.d.ts +65 -6
  182. package/dist/core/ingestion/import-resolvers/utils.js +215 -36
  183. package/dist/core/ingestion/import-resolvers/workspace-file-index.d.ts +72 -0
  184. package/dist/core/ingestion/import-resolvers/workspace-file-index.js +58 -0
  185. package/dist/core/ingestion/language-config.d.ts +29 -1
  186. package/dist/core/ingestion/language-config.js +110 -35
  187. package/dist/core/ingestion/language-provider.d.ts +209 -7
  188. package/dist/core/ingestion/language-provider.js +37 -0
  189. package/dist/core/ingestion/languages/c/captures.js +7 -0
  190. package/dist/core/ingestion/languages/c/import-target.js +12 -16
  191. package/dist/core/ingestion/languages/c/scope-resolver.js +20 -19
  192. package/dist/core/ingestion/languages/c/static-linkage.js +12 -16
  193. package/dist/core/ingestion/languages/c-cpp.js +13 -0
  194. package/dist/core/ingestion/languages/cobol/scope-resolver.js +27 -20
  195. package/dist/core/ingestion/languages/cobol.js +13 -0
  196. package/dist/core/ingestion/languages/cpp/adl.d.ts +2 -2
  197. package/dist/core/ingestion/languages/cpp/adl.js +67 -44
  198. package/dist/core/ingestion/languages/cpp/captures.js +14 -2
  199. package/dist/core/ingestion/languages/cpp/file-local-linkage.js +11 -15
  200. package/dist/core/ingestion/languages/cpp/inline-namespaces.d.ts +28 -8
  201. package/dist/core/ingestion/languages/cpp/inline-namespaces.js +242 -68
  202. package/dist/core/ingestion/languages/cpp/interpret.d.ts +2 -0
  203. package/dist/core/ingestion/languages/cpp/interpret.js +126 -6
  204. package/dist/core/ingestion/languages/cpp/query.js +106 -0
  205. package/dist/core/ingestion/languages/cpp/scope-resolver.js +48 -20
  206. package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +2 -15
  207. package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +18 -10
  208. package/dist/core/ingestion/languages/csharp/captures.js +19 -0
  209. package/dist/core/ingestion/languages/csharp/import-target.js +46 -72
  210. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +5 -1
  211. package/dist/core/ingestion/languages/csharp/query.js +25 -5
  212. package/dist/core/ingestion/languages/csharp/scope-resolver.js +75 -2
  213. package/dist/core/ingestion/languages/dart/captures.d.ts +10 -0
  214. package/dist/core/ingestion/languages/dart/captures.js +598 -16
  215. package/dist/core/ingestion/languages/dart/import-target.js +32 -9
  216. package/dist/core/ingestion/languages/dart/index.d.ts +1 -1
  217. package/dist/core/ingestion/languages/dart/index.js +1 -1
  218. package/dist/core/ingestion/languages/dart/query.js +98 -1
  219. package/dist/core/ingestion/languages/dart/scope-resolver.js +14 -3
  220. package/dist/core/ingestion/languages/dart/simple-hooks.js +9 -0
  221. package/dist/core/ingestion/languages/dart.js +6 -1
  222. package/dist/core/ingestion/languages/go/captures.js +108 -3
  223. package/dist/core/ingestion/languages/go/generic-type-parameters.d.ts +31 -0
  224. package/dist/core/ingestion/languages/go/generic-type-parameters.js +140 -0
  225. package/dist/core/ingestion/languages/go/import-target.d.ts +3 -10
  226. package/dist/core/ingestion/languages/go/import-target.js +34 -30
  227. package/dist/core/ingestion/languages/go/index.d.ts +1 -1
  228. package/dist/core/ingestion/languages/go/interface-impls.d.ts +9 -1
  229. package/dist/core/ingestion/languages/go/interface-impls.js +693 -87
  230. package/dist/core/ingestion/languages/go/interpret.d.ts +2 -0
  231. package/dist/core/ingestion/languages/go/interpret.js +22 -8
  232. package/dist/core/ingestion/languages/go/method-owners.js +31 -12
  233. package/dist/core/ingestion/languages/go/package-clause.d.ts +46 -0
  234. package/dist/core/ingestion/languages/go/package-clause.js +75 -0
  235. package/dist/core/ingestion/languages/go/package-siblings.js +7 -11
  236. package/dist/core/ingestion/languages/go/query.js +56 -3
  237. package/dist/core/ingestion/languages/go/scope-resolver.js +39 -0
  238. package/dist/core/ingestion/languages/go.js +9 -0
  239. package/dist/core/ingestion/languages/java/analysis-features.d.ts +4 -0
  240. package/dist/core/ingestion/languages/java/analysis-features.js +12 -0
  241. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +12 -0
  242. package/dist/core/ingestion/languages/java/capture-side-channel.js +48 -0
  243. package/dist/core/ingestion/languages/java/captures.js +129 -44
  244. package/dist/core/ingestion/languages/java/import-target.d.ts +49 -9
  245. package/dist/core/ingestion/languages/java/import-target.js +68 -88
  246. package/dist/core/ingestion/languages/java/interpret.js +3 -2
  247. package/dist/core/ingestion/languages/java/module-resolution.d.ts +62 -0
  248. package/dist/core/ingestion/languages/java/module-resolution.js +142 -0
  249. package/dist/core/ingestion/languages/java/query.js +16 -3
  250. package/dist/core/ingestion/languages/java/record-components.d.ts +14 -0
  251. package/dist/core/ingestion/languages/java/record-components.js +185 -0
  252. package/dist/core/ingestion/languages/java/scope-resolver.js +10 -2
  253. package/dist/core/ingestion/languages/java/spring-aop.d.ts +12 -0
  254. package/dist/core/ingestion/languages/java/spring-aop.js +49 -0
  255. package/dist/core/ingestion/languages/java/spring-conditionals.d.ts +11 -0
  256. package/dist/core/ingestion/languages/java/spring-conditionals.js +48 -0
  257. package/dist/core/ingestion/languages/java/spring-di.d.ts +4 -1
  258. package/dist/core/ingestion/languages/java/spring-di.js +41 -9
  259. package/dist/core/ingestion/languages/java/spring-non-http-handlers.d.ts +7 -0
  260. package/dist/core/ingestion/languages/java/spring-non-http-handlers.js +32 -0
  261. package/dist/core/ingestion/languages/java.js +94 -3
  262. package/dist/core/ingestion/languages/javascript/captures.d.ts +6 -1
  263. package/dist/core/ingestion/languages/javascript/captures.js +262 -4
  264. package/dist/core/ingestion/languages/javascript/import-target.d.ts +17 -21
  265. package/dist/core/ingestion/languages/javascript/import-target.js +24 -41
  266. package/dist/core/ingestion/languages/javascript/index.d.ts +54 -5
  267. package/dist/core/ingestion/languages/javascript/index.js +54 -5
  268. package/dist/core/ingestion/languages/javascript/query.d.ts +1 -0
  269. package/dist/core/ingestion/languages/javascript/query.js +211 -5
  270. package/dist/core/ingestion/languages/javascript/scope-resolver.js +12 -0
  271. package/dist/core/ingestion/languages/javascript/simple-hooks.js +2 -1
  272. package/dist/core/ingestion/languages/jvm/package-siblings.js +77 -9
  273. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +15 -0
  274. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +48 -0
  275. package/dist/core/ingestion/languages/kotlin/captures.js +43 -1
  276. package/dist/core/ingestion/languages/kotlin/import-target.d.ts +8 -1
  277. package/dist/core/ingestion/languages/kotlin/import-target.js +20 -123
  278. package/dist/core/ingestion/languages/kotlin/index.d.ts +1 -0
  279. package/dist/core/ingestion/languages/kotlin/index.js +1 -0
  280. package/dist/core/ingestion/languages/kotlin/module-resolution.d.ts +22 -0
  281. package/dist/core/ingestion/languages/kotlin/module-resolution.js +108 -0
  282. package/dist/core/ingestion/languages/kotlin/query.js +28 -2
  283. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +23 -2
  284. package/dist/core/ingestion/languages/kotlin/spring-aop.d.ts +12 -0
  285. package/dist/core/ingestion/languages/kotlin/spring-aop.js +64 -0
  286. package/dist/core/ingestion/languages/kotlin/spring-conditionals.d.ts +12 -0
  287. package/dist/core/ingestion/languages/kotlin/spring-conditionals.js +49 -0
  288. package/dist/core/ingestion/languages/kotlin/spring-di.d.ts +2 -0
  289. package/dist/core/ingestion/languages/kotlin/spring-di.js +60 -11
  290. package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.d.ts +10 -0
  291. package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.js +41 -0
  292. package/dist/core/ingestion/languages/php/captures.d.ts +5 -0
  293. package/dist/core/ingestion/languages/php/captures.js +319 -51
  294. package/dist/core/ingestion/languages/php/import-target.d.ts +3 -4
  295. package/dist/core/ingestion/languages/php/import-target.js +192 -62
  296. package/dist/core/ingestion/languages/php/query.js +31 -0
  297. package/dist/core/ingestion/languages/python/arity-metadata.d.ts +1 -0
  298. package/dist/core/ingestion/languages/python/arity-metadata.js +1 -0
  299. package/dist/core/ingestion/languages/python/captures.js +29 -0
  300. package/dist/core/ingestion/languages/python/import-decomposer.js +22 -1
  301. package/dist/core/ingestion/languages/python/import-target.d.ts +42 -1
  302. package/dist/core/ingestion/languages/python/import-target.js +277 -91
  303. package/dist/core/ingestion/languages/python/index.d.ts +1 -1
  304. package/dist/core/ingestion/languages/python/index.js +1 -1
  305. package/dist/core/ingestion/languages/python/interpret.d.ts +52 -0
  306. package/dist/core/ingestion/languages/python/interpret.js +249 -7
  307. package/dist/core/ingestion/languages/python/query.js +9 -0
  308. package/dist/core/ingestion/languages/python/receiver-binding.d.ts +8 -6
  309. package/dist/core/ingestion/languages/python/receiver-binding.js +108 -12
  310. package/dist/core/ingestion/languages/python/scope-resolver.js +27 -4
  311. package/dist/core/ingestion/languages/python/simple-hooks.js +2 -8
  312. package/dist/core/ingestion/languages/python.js +13 -0
  313. package/dist/core/ingestion/languages/ruby/captures.js +158 -0
  314. package/dist/core/ingestion/languages/ruby/import-target.js +7 -5
  315. package/dist/core/ingestion/languages/ruby/query.js +93 -0
  316. package/dist/core/ingestion/languages/ruby/scope-resolver.js +2 -0
  317. package/dist/core/ingestion/languages/ruby/simple-hooks.d.ts +1 -1
  318. package/dist/core/ingestion/languages/ruby/simple-hooks.js +22 -1
  319. package/dist/core/ingestion/languages/rust/captures.js +39 -1
  320. package/dist/core/ingestion/languages/rust/method-owners.js +11 -1
  321. package/dist/core/ingestion/languages/rust/module-path.d.ts +126 -0
  322. package/dist/core/ingestion/languages/rust/module-path.js +214 -0
  323. package/dist/core/ingestion/languages/rust/qualified-call.d.ts +41 -0
  324. package/dist/core/ingestion/languages/rust/qualified-call.js +481 -0
  325. package/dist/core/ingestion/languages/rust/query.js +39 -6
  326. package/dist/core/ingestion/languages/rust/scope-resolver.js +21 -2
  327. package/dist/core/ingestion/languages/rust.js +17 -0
  328. package/dist/core/ingestion/languages/swift/captures.js +87 -3
  329. package/dist/core/ingestion/languages/swift/conditional-directive-preprocess.d.ts +48 -0
  330. package/dist/core/ingestion/languages/swift/conditional-directive-preprocess.js +253 -0
  331. package/dist/core/ingestion/languages/swift/import-target.js +4 -9
  332. package/dist/core/ingestion/languages/swift/query.js +17 -0
  333. package/dist/core/ingestion/languages/swift/receiver-binding.d.ts +8 -0
  334. package/dist/core/ingestion/languages/swift/receiver-binding.js +6 -2
  335. package/dist/core/ingestion/languages/swift.js +2 -0
  336. package/dist/core/ingestion/languages/typescript/captures.d.ts +90 -1
  337. package/dist/core/ingestion/languages/typescript/captures.js +267 -5
  338. package/dist/core/ingestion/languages/typescript/cjs-export-assignment.d.ts +134 -0
  339. package/dist/core/ingestion/languages/typescript/cjs-export-assignment.js +523 -0
  340. package/dist/core/ingestion/languages/typescript/cjs-module-exports.d.ts +54 -0
  341. package/dist/core/ingestion/languages/typescript/cjs-module-exports.js +191 -0
  342. package/dist/core/ingestion/languages/typescript/convex-endpoint-metadata.d.ts +7 -0
  343. package/dist/core/ingestion/languages/typescript/convex-endpoint-metadata.js +92 -0
  344. package/dist/core/ingestion/languages/typescript/file-candidates.d.ts +29 -0
  345. package/dist/core/ingestion/languages/typescript/file-candidates.js +70 -0
  346. package/dist/core/ingestion/languages/typescript/import-decomposer.d.ts +28 -1
  347. package/dist/core/ingestion/languages/typescript/import-decomposer.js +125 -10
  348. package/dist/core/ingestion/languages/typescript/import-target.d.ts +25 -37
  349. package/dist/core/ingestion/languages/typescript/import-target.js +24 -26
  350. package/dist/core/ingestion/languages/typescript/interpret.d.ts +21 -1
  351. package/dist/core/ingestion/languages/typescript/interpret.js +45 -5
  352. package/dist/core/ingestion/languages/typescript/module-resolution.d.ts +48 -0
  353. package/dist/core/ingestion/languages/typescript/module-resolution.js +171 -0
  354. package/dist/core/ingestion/languages/typescript/query.d.ts +1 -0
  355. package/dist/core/ingestion/languages/typescript/query.js +303 -14
  356. package/dist/core/ingestion/languages/typescript/receiver-binding.d.ts +17 -0
  357. package/dist/core/ingestion/languages/typescript/receiver-binding.js +27 -0
  358. package/dist/core/ingestion/languages/typescript/scope-resolver.js +27 -40
  359. package/dist/core/ingestion/languages/typescript/simple-hooks.d.ts +0 -10
  360. package/dist/core/ingestion/languages/typescript/simple-hooks.js +22 -22
  361. package/dist/core/ingestion/languages/typescript/tsconfig.d.ts +59 -0
  362. package/dist/core/ingestion/languages/typescript/tsconfig.js +292 -0
  363. package/dist/core/ingestion/languages/typescript.js +93 -2
  364. package/dist/core/ingestion/languages/vue/import-target.d.ts +6 -22
  365. package/dist/core/ingestion/languages/vue/import-target.js +11 -47
  366. package/dist/core/ingestion/languages/vue/scope-resolver.js +4 -2
  367. package/dist/core/ingestion/local-symbol-pruner.js +11 -11
  368. package/dist/core/ingestion/method-extractors/configs/csharp.js +1 -0
  369. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  370. package/dist/core/ingestion/method-types.d.ts +16 -0
  371. package/dist/core/ingestion/pipeline-phases/di.d.ts +1 -1
  372. package/dist/core/ingestion/pipeline-phases/di.js +136 -21
  373. package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
  374. package/dist/core/ingestion/pipeline-phases/index.js +2 -0
  375. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +15 -0
  376. package/dist/core/ingestion/pipeline-phases/parse-impl.js +154 -4
  377. package/dist/core/ingestion/pipeline-phases/parse.js +5 -0
  378. package/dist/core/ingestion/pipeline-phases/processes.d.ts +3 -2
  379. package/dist/core/ingestion/pipeline-phases/processes.js +91 -6
  380. package/dist/core/ingestion/pipeline-phases/routes.js +116 -25
  381. package/dist/core/ingestion/pipeline-phases/spring-aop.d.ts +34 -0
  382. package/dist/core/ingestion/pipeline-phases/spring-aop.js +439 -0
  383. package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.d.ts +34 -0
  384. package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.js +255 -0
  385. package/dist/core/ingestion/pipeline-phases/types.d.ts +7 -0
  386. package/dist/core/ingestion/pipeline.d.ts +26 -2
  387. package/dist/core/ingestion/pipeline.js +60 -11
  388. package/dist/core/ingestion/process-processor.d.ts +109 -2
  389. package/dist/core/ingestion/process-processor.js +464 -52
  390. package/dist/core/ingestion/resolve-references.d.ts +6 -1
  391. package/dist/core/ingestion/resolve-references.js +66 -6
  392. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +8 -2
  393. package/dist/core/ingestion/route-extractors/constant-resolver.js +11 -7
  394. package/dist/core/ingestion/route-extractors/data-route-table.d.ts +40 -0
  395. package/dist/core/ingestion/route-extractors/data-route-table.js +876 -0
  396. package/dist/core/ingestion/route-extractors/dispatch-guard.d.ts +96 -0
  397. package/dist/core/ingestion/route-extractors/dispatch-guard.js +1184 -0
  398. package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +114 -0
  399. package/dist/core/ingestion/route-extractors/java-const-resolver.js +569 -0
  400. package/dist/core/ingestion/route-extractors/js-const-resolver.d.ts +192 -0
  401. package/dist/core/ingestion/route-extractors/js-const-resolver.js +1033 -0
  402. package/dist/core/ingestion/route-extractors/nest.d.ts +53 -0
  403. package/dist/core/ingestion/route-extractors/nest.js +523 -0
  404. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +11 -0
  405. package/dist/core/ingestion/route-extractors/spring-shared.js +89 -0
  406. package/dist/core/ingestion/route-extractors/spring.js +214 -29
  407. package/dist/core/ingestion/scope-extractor-bridge.js +9 -1
  408. package/dist/core/ingestion/scope-extractor.d.ts +22 -7
  409. package/dist/core/ingestion/scope-extractor.js +311 -16
  410. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +311 -21
  411. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +16 -5
  412. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -0
  413. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +47 -0
  414. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +20 -15
  415. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +280 -42
  416. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +123 -0
  417. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +171 -17
  418. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +58 -1
  419. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +137 -34
  420. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +30 -1
  421. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +39 -1
  422. package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.d.ts +18 -1
  423. package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.js +113 -18
  424. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +107 -5
  425. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +590 -33
  426. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +3 -0
  427. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +101 -41
  428. package/dist/core/ingestion/scope-resolution/passes/imported-value-refs.d.ts +34 -0
  429. package/dist/core/ingestion/scope-resolution/passes/imported-value-refs.js +62 -0
  430. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +5 -1
  431. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +8 -1
  432. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +105 -5
  433. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +931 -92
  434. package/dist/core/ingestion/scope-resolution/passes/return-shape-members.d.ts +58 -0
  435. package/dist/core/ingestion/scope-resolution/passes/return-shape-members.js +458 -0
  436. package/dist/core/ingestion/scope-resolution/passes/unique-name-properties.d.ts +180 -0
  437. package/dist/core/ingestion/scope-resolution/passes/unique-name-properties.js +418 -0
  438. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +29 -0
  439. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +57 -0
  440. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +55 -1
  441. package/dist/core/ingestion/scope-resolution/pipeline/run.js +241 -19
  442. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +112 -2
  443. package/dist/core/ingestion/scope-resolution/resolution-outcome.js +38 -1
  444. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +25 -3
  445. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +36 -16
  446. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +210 -9
  447. package/dist/core/ingestion/scope-resolution/scope/walkers.js +905 -22
  448. package/dist/core/ingestion/scope-resolution/summary-maps.d.ts +28 -0
  449. package/dist/core/ingestion/scope-resolution/summary-maps.js +50 -0
  450. package/dist/core/ingestion/scope-resolution/undecided-satisfaction.d.ts +55 -0
  451. package/dist/core/ingestion/scope-resolution/undecided-satisfaction.js +37 -0
  452. package/dist/core/ingestion/scope-resolution/unresolved-receivers.d.ts +109 -0
  453. package/dist/core/ingestion/scope-resolution/unresolved-receivers.js +204 -0
  454. package/dist/core/ingestion/scope-resolution/utils/definition-id.d.ts +13 -0
  455. package/dist/core/ingestion/scope-resolution/utils/definition-id.js +19 -0
  456. package/dist/core/ingestion/scope-resolution/utils/generic-instantiation.d.ts +171 -0
  457. package/dist/core/ingestion/scope-resolution/utils/generic-instantiation.js +218 -0
  458. package/dist/core/ingestion/scope-resolution/workspace-index-types.d.ts +34 -0
  459. package/dist/core/ingestion/scope-resolution/workspace-index-types.js +15 -0
  460. package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +7 -20
  461. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
  462. package/dist/core/ingestion/tree-sitter-queries.js +678 -4
  463. package/dist/core/ingestion/type-env.js +11 -7
  464. package/dist/core/ingestion/type-extractors/jvm.js +2 -2
  465. package/dist/core/ingestion/type-extractors/shared.d.ts +17 -0
  466. package/dist/core/ingestion/type-extractors/shared.js +16 -0
  467. package/dist/core/ingestion/type-extractors/types.d.ts +12 -0
  468. package/dist/core/ingestion/type-extractors/typescript.d.ts +20 -0
  469. package/dist/core/ingestion/type-extractors/typescript.js +26 -0
  470. package/dist/core/ingestion/utils/ast-helpers.d.ts +322 -16
  471. package/dist/core/ingestion/utils/ast-helpers.js +727 -150
  472. package/dist/core/ingestion/utils/call-analysis.d.ts +27 -22
  473. package/dist/core/ingestion/utils/call-analysis.js +202 -7
  474. package/dist/core/ingestion/utils/callable-flow-captures.d.ts +59 -0
  475. package/dist/core/ingestion/utils/callable-flow-captures.js +51 -1
  476. package/dist/core/ingestion/utils/callable-labels.d.ts +25 -0
  477. package/dist/core/ingestion/utils/callable-labels.js +31 -0
  478. package/dist/core/ingestion/utils/effective-ram.d.ts +40 -0
  479. package/dist/core/ingestion/utils/effective-ram.js +58 -0
  480. package/dist/core/ingestion/utils/line-base.d.ts +14 -0
  481. package/dist/core/ingestion/utils/line-base.js +14 -0
  482. package/dist/core/ingestion/utils/method-props.d.ts +27 -4
  483. package/dist/core/ingestion/utils/method-props.js +29 -4
  484. package/dist/core/ingestion/utils/receiver-chain-captures.d.ts +31 -0
  485. package/dist/core/ingestion/utils/receiver-chain-captures.js +57 -0
  486. package/dist/core/ingestion/utils/receiver-chain-codec.d.ts +89 -0
  487. package/dist/core/ingestion/utils/receiver-chain-codec.js +182 -0
  488. package/dist/core/ingestion/utils/scope-tree-walk.d.ts +22 -0
  489. package/dist/core/ingestion/utils/scope-tree-walk.js +33 -0
  490. package/dist/core/ingestion/utils/template-arguments.d.ts +96 -0
  491. package/dist/core/ingestion/utils/template-arguments.js +196 -0
  492. package/dist/core/ingestion/utils/type-parameters.d.ts +49 -0
  493. package/dist/core/ingestion/utils/type-parameters.js +206 -0
  494. package/dist/core/ingestion/workers/callable-id.d.ts +80 -0
  495. package/dist/core/ingestion/workers/callable-id.js +125 -0
  496. package/dist/core/ingestion/workers/parse-worker.d.ts +14 -1
  497. package/dist/core/ingestion/workers/parse-worker.js +493 -54
  498. package/dist/core/ingestion/workers/worker-pool.d.ts +20 -27
  499. package/dist/core/ingestion/workers/worker-pool.js +91 -3
  500. package/dist/core/lbug/csv-generator.d.ts +27 -0
  501. package/dist/core/lbug/csv-generator.js +72 -17
  502. package/dist/core/lbug/extension-load-error.js +13 -2
  503. package/dist/core/lbug/extension-loader.d.ts +29 -0
  504. package/dist/core/lbug/extension-loader.js +31 -6
  505. package/dist/core/lbug/graph-emit-sink.d.ts +264 -0
  506. package/dist/core/lbug/graph-emit-sink.js +581 -0
  507. package/dist/core/lbug/lbug-adapter.d.ts +290 -11
  508. package/dist/core/lbug/lbug-adapter.js +652 -69
  509. package/dist/core/lbug/native-check.d.ts +21 -0
  510. package/dist/core/lbug/native-check.js +248 -32
  511. package/dist/core/lbug/pdg-emit-sink.d.ts +13 -4
  512. package/dist/core/lbug/pdg-emit-sink.js +21 -111
  513. package/dist/core/lbug/pool-adapter.d.ts +9 -0
  514. package/dist/core/lbug/pool-adapter.js +164 -14
  515. package/dist/core/lbug/query-batch.d.ts +75 -0
  516. package/dist/core/lbug/query-batch.js +102 -0
  517. package/dist/core/lbug/rel-pair-routing.d.ts +160 -2
  518. package/dist/core/lbug/rel-pair-routing.js +228 -10
  519. package/dist/core/lbug/schema.d.ts +163 -4
  520. package/dist/core/lbug/schema.js +404 -159
  521. package/dist/core/lbug/sync-csv-writer.d.ts +47 -0
  522. package/dist/core/lbug/sync-csv-writer.js +108 -0
  523. package/dist/core/run-analyze.d.ts +147 -0
  524. package/dist/core/run-analyze.js +1215 -179
  525. package/dist/core/search/bm25-index.d.ts +11 -0
  526. package/dist/core/search/bm25-index.js +60 -26
  527. package/dist/core/search/fts-indexes.d.ts +150 -9
  528. package/dist/core/search/fts-indexes.js +241 -23
  529. package/dist/core/wiki/graph-queries.js +145 -112
  530. package/dist/core/wiki/llm-client.d.ts +29 -3
  531. package/dist/core/wiki/llm-client.js +47 -12
  532. package/dist/core/wiki/prompts.d.ts +19 -0
  533. package/dist/core/wiki/prompts.js +20 -1
  534. package/dist/lib/utils.d.ts +113 -0
  535. package/dist/lib/utils.js +185 -0
  536. package/dist/mcp/local/aop-metadata.d.ts +77 -0
  537. package/dist/mcp/local/aop-metadata.js +227 -0
  538. package/dist/mcp/local/bean-metadata.d.ts +2 -1
  539. package/dist/mcp/local/bean-metadata.js +22 -1
  540. package/dist/mcp/local/convex-metadata.d.ts +8 -0
  541. package/dist/mcp/local/convex-metadata.js +45 -0
  542. package/dist/mcp/local/local-backend.d.ts +174 -11
  543. package/dist/mcp/local/local-backend.js +1678 -331
  544. package/dist/mcp/local/path-predicate.d.ts +19 -0
  545. package/dist/mcp/local/path-predicate.js +21 -0
  546. package/dist/mcp/local/pdg-impact.d.ts +132 -10
  547. package/dist/mcp/local/pdg-impact.js +391 -98
  548. package/dist/mcp/repository-policy.d.ts +1 -0
  549. package/dist/mcp/repository-policy.js +5 -0
  550. package/dist/mcp/resources.js +26 -4
  551. package/dist/mcp/server.js +23 -13
  552. package/dist/mcp/tools.d.ts +8 -0
  553. package/dist/mcp/tools.js +76 -23
  554. package/dist/server/analyze-job.d.ts +37 -2
  555. package/dist/server/analyze-job.js +13 -2
  556. package/dist/server/analyze-launch.d.ts +1 -1
  557. package/dist/server/analyze-launch.js +79 -10
  558. package/dist/server/analyze-worker-core.d.ts +1 -1
  559. package/dist/server/analyze-worker-core.js +10 -1
  560. package/dist/server/analyze-worker-ipc.d.ts +1 -1
  561. package/dist/server/analyze-worker-ipc.js +4 -0
  562. package/dist/server/analyze-worker-protocol.d.ts +59 -0
  563. package/dist/server/analyze-worker-protocol.js +1 -0
  564. package/dist/server/analyze-worker.d.ts +1 -17
  565. package/dist/server/api.d.ts +2 -11
  566. package/dist/server/api.js +164 -163
  567. package/dist/server/embed-run-outcome.d.ts +89 -0
  568. package/dist/server/embed-run-outcome.js +88 -0
  569. package/dist/server/git-clone.js +6 -2
  570. package/dist/server/middleware.d.ts +116 -9
  571. package/dist/server/middleware.js +291 -22
  572. package/dist/server/sse-progress.d.ts +41 -0
  573. package/dist/server/sse-progress.js +125 -0
  574. package/dist/storage/branch-index.d.ts +11 -6
  575. package/dist/storage/branch-index.js +15 -7
  576. package/dist/storage/file-hash.js +2 -2
  577. package/dist/storage/fs-atomic.d.ts +25 -0
  578. package/dist/storage/fs-atomic.js +42 -0
  579. package/dist/storage/git.d.ts +131 -0
  580. package/dist/storage/git.js +236 -2
  581. package/dist/storage/index-lock.d.ts +102 -0
  582. package/dist/storage/index-lock.js +660 -0
  583. package/dist/storage/parse-cache.js +588 -3
  584. package/dist/storage/parsedfile-store.js +76 -6
  585. package/dist/storage/repo-manager.d.ts +54 -435
  586. package/dist/storage/repo-manager.js +312 -215
  587. package/dist/storage/repo-meta.d.ts +530 -0
  588. package/dist/storage/repo-meta.js +100 -0
  589. package/dist/types/pipeline.d.ts +37 -0
  590. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +1 -1
  591. package/hooks/claude/gitnexus-hook.cjs +1 -1
  592. package/hooks/claude/resolve-analyze-cmd.cjs +80 -13
  593. package/package.json +11 -2
  594. package/scripts/cross-platform-shard.ts +153 -0
  595. package/scripts/cross-platform-tests.ts +122 -0
  596. package/scripts/run-cross-platform.ts +16 -5
  597. package/skills/gitnexus-cli.md +3 -3
  598. package/skills/gitnexus-debugging.md +37 -7
  599. package/skills/gitnexus-exploring.md +26 -4
  600. package/skills/gitnexus-impact-analysis.md +71 -10
  601. package/skills/gitnexus-plan/README.md +11 -6
  602. package/skills/gitnexus-plan/references/evidence-provenance.md +49 -12
  603. package/skills/gitnexus-plan/scripts/evidence-provenance.mjs +654 -372
  604. package/skills/gitnexus-refactoring.md +48 -5
  605. package/skills/gitnexus-review/SKILL.md +12 -6
  606. package/skills/gitnexus-review/ci-personas/ci-adversarial-lens.md +1 -1
  607. package/skills/gitnexus-review/ci-personas/ci-blast-radius-lens.md +1 -1
  608. package/skills/gitnexus-review/ci-personas/ci-correctness-lens.md +1 -1
  609. package/skills/gitnexus-review/ci-personas/ci-coverage-lens.md +1 -1
  610. package/skills/gitnexus-review/ci-personas/ci-critic-lens.md +1 -1
  611. package/skills/gitnexus-review/ci-personas/ci-security-lens.md +1 -1
  612. package/skills/gitnexus-taint-analysis.md +13 -7
  613. package/skills/gitnexus-work/SKILL.md +4 -1
  614. package/skills/gitnexus-work/references/evidence-provenance.md +49 -12
  615. package/skills/gitnexus-work/scripts/evidence-provenance.mjs +654 -372
  616. package/vendor/leiden/utils.cjs +28 -1
  617. package/web/assets/ProcessFlowModal-Badh2Pbg.js +24 -0
  618. package/web/assets/{__vite-browser-external-bMKyYQkO.js → __vite-browser-external-COxE8F0G.js} +1 -1
  619. package/web/assets/abnfDiagram-QNN2D4NM-Cq2djWWe.js +1 -0
  620. package/web/assets/agent-Dr4l5EOp.js +662 -0
  621. package/web/assets/architecture-TIHT7OUA-V6N4J3SY-fTePvLoe.js +1 -0
  622. package/web/assets/architectureDiagram-OT7VKSJ7-mKEXxkeF.js +36 -0
  623. package/web/assets/{blockDiagram-7IZFK4PR-DBefcjof.js → blockDiagram-54WLDVWM-Dg_hrRWE.js} +4 -4
  624. package/web/assets/{c4Diagram-Y2BXMSZH-EjWnjEJl.js → c4Diagram-X2LASM7V-CnhoT4aV.js} +2 -2
  625. package/web/assets/chunk-224SPVON-B8gxtXpy.js +1 -0
  626. package/web/assets/chunk-2AEHWXPW-CV4r6-1I.js +127 -0
  627. package/web/assets/chunk-2SREHG4O-Dip42xOq.js +1 -0
  628. package/web/assets/chunk-4RFN2BYJ-DHHWuC-n.js +1 -0
  629. package/web/assets/chunk-66DQ2XMT-CM0xjFAN.js +1 -0
  630. package/web/assets/chunk-6BELYETK-ChsYtfHW.js +1 -0
  631. package/web/assets/{chunk-LII3EMHJ-BL6lKJ_P.js → chunk-6L755F7B-CMhLBxi2.js} +1 -1
  632. package/web/assets/{chunk-QA3QBVWF-DVXPOIxe.js → chunk-7CWYLC5S-DRgGkkpF.js} +1 -1
  633. package/web/assets/{chunk-ZXARS5L4-C3v-4Fsw.js → chunk-A7VWPJGB-B4O3oQdQ.js} +1 -1
  634. package/web/assets/chunk-BI6VK774-D0oaWk8V.js +1 -0
  635. package/web/assets/chunk-D6VWDJW2-BxcBdZ9c.js +2 -0
  636. package/web/assets/chunk-DMV4VAQV-Bn6WXDSX.js +4 -0
  637. package/web/assets/chunk-E5QJAATJ-dh-5CtIV.js +2 -0
  638. package/web/assets/chunk-EQFTRU2I-1ix7TzW9.js +2 -0
  639. package/web/assets/chunk-FWYVLQTC-BgDItRzJ.js +1 -0
  640. package/web/assets/chunk-HNC4WDU7-DoU3KnA7.js +1 -0
  641. package/web/assets/chunk-L5GCOVLC-CE2w8Nm_.js +161 -0
  642. package/web/assets/chunk-LAHSG3N2-Bz4hcn0N.js +321 -0
  643. package/web/assets/chunk-LORZLZWG-B57uGw2N.js +88 -0
  644. package/web/assets/chunk-NLANEA3F-XpH_NPA5.js +62 -0
  645. package/web/assets/chunk-NV3KIAZN-5yvcpMiu.js +1 -0
  646. package/web/assets/chunk-OIYT25JQ-CQ6KOZGX.js +1 -0
  647. package/web/assets/chunk-PE7DX7ZZ-CCvRVj43.js +10 -0
  648. package/web/assets/chunk-Q67WD55A-BWCH2SVz.js +156 -0
  649. package/web/assets/chunk-RRFMTAIC-CMN0wcYA.js +1 -0
  650. package/web/assets/{chunk-RLI5ZMPA-CkXZ0y0U.js → chunk-SATU7PGQ-B1zhgOvc.js} +1 -1
  651. package/web/assets/chunk-STOV2HOB-GnExfzIX.js +1 -0
  652. package/web/assets/{chunk-INKRHTLW-Ckl3_KkO.js → chunk-SZD42YQK-ijyKKVr1.js} +21 -21
  653. package/web/assets/{chunk-LCXTWHL2-C162wE5z.js → chunk-TFPGFQPA-Dj6a-0pC.js} +3 -3
  654. package/web/assets/chunk-TGVD4F4B-BMc31Gj2.js +2 -0
  655. package/web/assets/chunk-TZUXEDM2-B7p9AcTw.js +1 -0
  656. package/web/assets/chunk-UAT7B5JY-DW-A_fpI.js +1 -0
  657. package/web/assets/{chunk-6764PJDD-C7xxk063.js → chunk-VE5CLXGZ-C1vB7wpY.js} +1 -1
  658. package/web/assets/{chunk-RG4AUYOV-ZNgyLFq6.js → chunk-VJFVEZTB-CX66F2ep.js} +3 -3
  659. package/web/assets/chunk-VY5UBI4V-yxPQAWxw.js +1 -0
  660. package/web/assets/chunk-WJBAP47W-BckNkuEN.js +1 -0
  661. package/web/assets/chunk-XNMVGMAZ-Cg_LnPtj.js +1 -0
  662. package/web/assets/classDiagram-VPOSVPXH-BjOiXA_C.js +1 -0
  663. package/web/assets/classDiagram-v2-5VFNUXQA-BjOiXA_C.js +1 -0
  664. package/web/assets/{cose-bilkent-UX7MHV2Q-BmnZ7Mvj.js → cose-bilkent-RZA4XIPR-BHpK4Crp.js} +1 -1
  665. package/web/assets/cynefin-VYW2F7L2-2VAHYKR6-DAsF6GZv.js +1 -0
  666. package/web/assets/cynefinDiagram-TR7LZUCG-C3Iar9lZ.js +62 -0
  667. package/web/assets/dagre-K64A6Z3X-CUwi79mA.js +4 -0
  668. package/web/assets/diagram-2TLGJV2N-C2F5Nfym.js +30 -0
  669. package/web/assets/diagram-JCD7PQ7C-DC5-9FRr.js +24 -0
  670. package/web/assets/{diagram-HNR7UZ2L-DVRltWbL.js → diagram-KFBZAVDW-Dfmf_4ti.js} +3 -3
  671. package/web/assets/diagram-L7DVE3PN-GIRyVUtt.js +41 -0
  672. package/web/assets/diagram-YP3FGXHI-CyNFYtkd.js +24 -0
  673. package/web/assets/ebnfDiagram-DRHRQJWO-D8_CI4ZF.js +1 -0
  674. package/web/assets/erDiagram-SZLJLQMB-CohQjznC.js +85 -0
  675. package/web/assets/error-D9MtuG9s.js +1 -0
  676. package/web/assets/eventmodeling-45OFAUF4-AOUJWSLX-BvTc6iQi.js +1 -0
  677. package/web/assets/flowDiagram-BWE6NHOH-B6RnEvD9.js +1 -0
  678. package/web/assets/ganttDiagram-2QTCJSNI-Cks_mEcF.js +292 -0
  679. package/web/assets/gitGraph-TEB2WS4Q-U5VWYIBC-Dy7CCSZ5.js +1 -0
  680. package/web/assets/gitGraphDiagram-VVZAVV5G-CG41cIVc.js +106 -0
  681. package/web/assets/{index-D7mTFcvQ.js → index-2zdvEdzg.js} +51 -51
  682. package/web/assets/index-CJDpyH-n.css +2 -0
  683. package/web/assets/info-DKCQHKI2-DX6KST54-BLJg1ikX.js +1 -0
  684. package/web/assets/infoDiagram-FY4PZKRE-BJ3vPVwW.js +2 -0
  685. package/web/assets/{ishikawaDiagram-BNXS4ZKH-XpjJRU9h.js → ishikawaDiagram-4JDSWWDM-DmqWtjkl.js} +2 -2
  686. package/web/assets/{journeyDiagram-M6C3CM3L-C6wjooas.js → journeyDiagram-QUS3FAKT-XAzVzrQW.js} +3 -3
  687. package/web/assets/{kanban-definition-75IXJCU3-5_UVY79g.js → kanban-definition-D4L7SQKK-YurRnmYn.js} +6 -6
  688. package/web/assets/katex-C5OPUE3Q-Bnbavtmu.js +257 -0
  689. package/web/assets/{mindmap-definition-2TDM6QVE-8xDLzk2e.js → mindmap-definition-66QP5TD6-ChrWWnil.js} +5 -5
  690. package/web/assets/node.browser-U-KyT5AW.js +1 -0
  691. package/web/assets/packet-7NZHBO7P-5WG45QPS-KUxcuvaA.js +1 -0
  692. package/web/assets/pegDiagram-YKIO5C6U-QGeQLEq2.js +1 -0
  693. package/web/assets/pie-RZYD4A2V-2OCZ2VTD-DPs08kJY.js +1 -0
  694. package/web/assets/pieDiagram-YPY3TKG5-BuT3MYzm.js +39 -0
  695. package/web/assets/{quadrantDiagram-VICAPDV7-DR54jOeR.js → quadrantDiagram-VO5QLCXA-B7098wSc.js} +3 -3
  696. package/web/assets/radar-I7S5WNFK-EPLSA35B-w5F0K3a7.js +1 -0
  697. package/web/assets/railroad-3IZDKUUU-PWFQV6Y6-Bu668yqY.js +1 -0
  698. package/web/assets/railroad-abnf-AHOZXSZD-JYYJX3BS-B6ghpETS.js +1 -0
  699. package/web/assets/railroad-ebnf-EBAXGLYW-RBZ4ONAP-D-XmHh3M.js +1 -0
  700. package/web/assets/railroad-peg-LSFZ7HO6-KHNPXGPG-mhkrhzEn.js +1 -0
  701. package/web/assets/railroadDiagram-M77LTMSB-AXu4vxk_.js +1 -0
  702. package/web/assets/{requirementDiagram-JXO7QTGE-PEkg7sKC.js → requirementDiagram-RASO5HTO-B-l8InpK.js} +4 -4
  703. package/web/assets/{sankeyDiagram-URQDO5SZ-DDWEtjcs.js → sankeyDiagram-F32LOJ6L-Kd7zjJgX.js} +3 -3
  704. package/web/assets/{sequenceDiagram-VS2MUI6T-Bv-uwANx.js → sequenceDiagram-URATNSBD-BlcdY9DS.js} +7 -7
  705. package/web/assets/sizeCapture-5GORAGP4-BEqQzsxs.js +1 -0
  706. package/web/assets/src-C5sx_gMp.js +1 -0
  707. package/web/assets/stateDiagram-DODFXNG5-DTdHc-fF.js +1 -0
  708. package/web/assets/stateDiagram-v2-JNMM5JRA-DioNLDDo.js +1 -0
  709. package/web/assets/swimlanes-OSWOYLSK-CA0ofdwY.js +2 -0
  710. package/web/assets/swimlanesDiagram-RHGX2RDE-PkwUdckC.js +8 -0
  711. package/web/assets/{timeline-definition-O6YCAMPW-Qmsuo7qS.js → timeline-definition-WJXWRH4I-DUfp1R3E.js} +3 -3
  712. package/web/assets/treeView-QDETBFTQ-EHYTSQNO-C5lpBgPm.js +1 -0
  713. package/web/assets/treemap-6X3UGDF4-CCWCF7EK-BB0ZyO-B.js +1 -0
  714. package/web/assets/vennDiagram-K3VCMQLI-Bj-FN-XS.js +34 -0
  715. package/web/assets/wardley-OPB4EBWU-6OWERDVT-CEA3Nq0j.js +1 -0
  716. package/web/assets/wardleyDiagram-XK6FUAWV-CVV0dHFr.js +78 -0
  717. package/web/assets/xychartDiagram-5VRUVLCW-CK_rbSWE.js +7 -0
  718. package/web/index.html +16 -15
  719. package/dist/core/ingestion/languages/python/index-stats.d.ts +0 -18
  720. package/dist/core/ingestion/languages/python/index-stats.js +0 -25
  721. package/web/assets/ProcessFlowModal-CwkpVkv-.js +0 -22
  722. package/web/assets/agent-Ds-lkZjX.js +0 -626
  723. package/web/assets/architecture-7EHR7CIX-6QZW5X65-BVWFvYz2.js +0 -1
  724. package/web/assets/architectureDiagram-UL44E2DR-CmG3uPWf.js +0 -36
  725. package/web/assets/chunk-2T2R6R2M-Bb6Sy3_j.js +0 -4
  726. package/web/assets/chunk-2UTLFMKG-CXlhF3Pt.js +0 -1
  727. package/web/assets/chunk-3SSMPTDK-rPsRM1x0.js +0 -321
  728. package/web/assets/chunk-4R4BOZG6-Be_F0JoX.js +0 -159
  729. package/web/assets/chunk-67TQ5CYL-CA7OhIEf.js +0 -124
  730. package/web/assets/chunk-7J6CGLKN-BlKwgXKs.js +0 -10
  731. package/web/assets/chunk-C62D2QBJ-3gdL8EKc.js +0 -1
  732. package/web/assets/chunk-CEXFNPSA-CjwtMDkc.js +0 -1
  733. package/web/assets/chunk-J5EP6P6S-L19oz55R.js +0 -1
  734. package/web/assets/chunk-KGFNY3KK-BZeP-55u.js +0 -62
  735. package/web/assets/chunk-KGYTTC2M-zm6n242I.js +0 -1
  736. package/web/assets/chunk-KNLZD3CH-BGlP6X9R.js +0 -1
  737. package/web/assets/chunk-LRIF4GLE-n59lhprL.js +0 -1
  738. package/web/assets/chunk-RERM46MO-B5dAdWpP.js +0 -1
  739. package/web/assets/chunk-RKZBBQEN-MyIQbGgD.js +0 -1
  740. package/web/assets/chunk-T2UQINTJ-BEyXhZWe.js +0 -1
  741. package/web/assets/chunk-UP6H54XL-BG-n7qDp.js +0 -1
  742. package/web/assets/chunk-UXSXWOXI-CW1_r5cS.js +0 -1
  743. package/web/assets/chunk-UY5QBCOK-ByqTVLvh.js +0 -1
  744. package/web/assets/chunk-VU6ZFW4Y-BLeasdkU.js +0 -2
  745. package/web/assets/classDiagram-KGZ6W3CR-olpO3fMe.js +0 -1
  746. package/web/assets/classDiagram-v2-72OJOZXJ-olpO3fMe.js +0 -1
  747. package/web/assets/dagre-ND4H6XIP-CY4iIF7v.js +0 -4
  748. package/web/assets/diagram-3NCE3AQN-CO6QpzEE.js +0 -43
  749. package/web/assets/diagram-GF46GFSD-B2atbzmU.js +0 -24
  750. package/web/assets/diagram-QXG6HAR7-DR6AHD2h.js +0 -24
  751. package/web/assets/diagram-WEQXMOUZ-DkpoMwTL.js +0 -10
  752. package/web/assets/erDiagram-L5TCEMPS-n8g_90bc.js +0 -85
  753. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-CgFBM7e-.js +0 -1
  754. package/web/assets/flowDiagram-H6V6AXG4-SGMDiCkB.js +0 -162
  755. package/web/assets/ganttDiagram-JCBTUEKG-Cb3UBqRS.js +0 -292
  756. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DCJv7SII.js +0 -1
  757. package/web/assets/gitGraphDiagram-S2ZK5IYY-Bs9CR7tq.js +0 -106
  758. package/web/assets/index-CX_fADmQ.css +0 -2
  759. package/web/assets/info-J43DQDTF-KCYPFFUO-BfNy-9Nd.js +0 -1
  760. package/web/assets/infoDiagram-3YFTVSEB-T368on4D.js +0 -2
  761. package/web/assets/katex-K3KEBU37-qRQBy-EZ.js +0 -261
  762. package/web/assets/node-Ct_zna8U.js +0 -8
  763. package/web/assets/packet-YPE3B663-LP52Z2RK-D4qkeG9k.js +0 -1
  764. package/web/assets/pie-LRSECV5Y-TCRJHUBD-B_yMKyQa.js +0 -1
  765. package/web/assets/pieDiagram-CU6KROY3-Bv4LpAbr.js +0 -30
  766. package/web/assets/radar-GUYGQ44K-RDLRG3WG-DeD6yN_0.js +0 -1
  767. package/web/assets/sdk-CzHHH8tD.js +0 -37
  768. package/web/assets/src-Cv1eWg0o.js +0 -1
  769. package/web/assets/stateDiagram-7D4R322I-DVNjmkEq.js +0 -1
  770. package/web/assets/stateDiagram-v2-36443NZ5-CyN4Fh7_.js +0 -1
  771. package/web/assets/treeView-BLDUP644-QA4HXRO3-CDuSYnd4.js +0 -1
  772. package/web/assets/treemap-LRROVOQU-LLAWBHMP-imazucyJ.js +0 -1
  773. package/web/assets/vennDiagram-MWXL3ELB-BD1iK1SX.js +0 -34
  774. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-DqrT9tB8.js +0 -1
  775. package/web/assets/wardleyDiagram-CUQ6CDDI-D6TzLVob.js +0 -78
  776. package/web/assets/xychartDiagram-N2JHSOCM-CoZ_Tits.js +0 -7
  777. /package/web/assets/{chunk-7W6UQGC5-pyKGssV3.js → chunk-LIEV3EAG-pyKGssV3.js} +0 -0
@@ -479,11 +479,11 @@ function resolveOwnGitTopLevel(absolute) {
479
479
  if (result.status !== 0) return null;
480
480
  let topLevel;
481
481
  try {
482
- topLevel = fs.realpathSync(decodeUtf8(result.stdout, 'nested repository root').trim());
482
+ topLevel = fs.realpathSync.native(decodeUtf8(result.stdout, 'nested repository root').trim());
483
483
  } catch {
484
484
  return null;
485
485
  }
486
- return topLevel === fs.realpathSync(absolute) ? topLevel : null;
486
+ return topLevel === fs.realpathSync.native(absolute) ? topLevel : null;
487
487
  }
488
488
 
489
489
  function readOwnGitlinkHead(absolute) {
@@ -616,17 +616,30 @@ function filesystemObject(absolute, expectedKind, mutationGuards, testHooks) {
616
616
  throw new Error(`Unsupported filesystem object at ${absolute}`);
617
617
  }
618
618
 
619
- function guardPathParents(repo, repoPath, mutationGuards) {
619
+ // Every dirty path re-walks its own parents, and dirty paths overwhelmingly
620
+ // share them — the repository root is re-stat'ed once per path. `guarded` is
621
+ // per-snapshot and remembers which absolute directories already carry a guard,
622
+ // so each distinct directory is stat'ed and guarded exactly once.
623
+ //
624
+ // Keeping the first-seen identity is the conservative choice: verifyGuards
625
+ // re-checks every guard against the filesystem at the end, so a directory that
626
+ // changes after it was guarded still fails there. Skipping a re-stat cannot hide
627
+ // a change; it only avoids recording the same directory twice.
628
+ function guardPathParents(repo, repoPath, mutationGuards, guarded) {
620
629
  const components = repoPath.split('/');
621
630
  let current = repo;
622
- const rootStat = fs.lstatSync(repo, { bigint: true });
623
- mutationGuards.push({
624
- type: 'directory',
625
- absolute: repo,
626
- identity: stableDirectoryIdentity(rootStat),
627
- });
631
+ if (!guarded.has(repo)) {
632
+ guarded.add(repo);
633
+ mutationGuards.push({
634
+ type: 'directory',
635
+ absolute: repo,
636
+ identity: stableDirectoryIdentity(fs.lstatSync(repo, { bigint: true })),
637
+ });
638
+ }
628
639
  for (const component of components.slice(0, -1)) {
629
640
  current = path.join(current, component);
641
+ // Already proved a real directory and already guarded on an earlier path.
642
+ if (guarded.has(current)) continue;
630
643
  let stat;
631
644
  try {
632
645
  stat = fs.lstatSync(current, { bigint: true });
@@ -638,6 +651,7 @@ function guardPathParents(repo, repoPath, mutationGuards) {
638
651
  throw new Error(`Refusing to traverse symlink parent for ${repoPath}`);
639
652
  }
640
653
  if (!stat.isDirectory()) return;
654
+ guarded.add(current);
641
655
  mutationGuards.push({
642
656
  type: 'directory',
643
657
  absolute: current,
@@ -646,81 +660,153 @@ function guardPathParents(repo, repoPath, mutationGuards) {
646
660
  }
647
661
  }
648
662
 
649
- function recordAnchoredAbsence(repo, repoPath, mutationGuards) {
663
+ // A bound, not a bug: the absence cache deduplicates correctly and leaks nothing,
664
+ // but citedPaths is caller-supplied and unbounded, so a pathological snapshot
665
+ // could hold more descriptors than the process is allowed (macOS
666
+ // kern.maxfilesperproc is 24576). The peak precedes a `git` spawn, so exhaustion
667
+ // would surface as a git failure misreported as evidence instability.
668
+ //
669
+ // Refuse rather than evict: closing a cached descriptor would silently break the
670
+ // pinned chain of an absence guard that was already recorded against it, which is
671
+ // exactly the inode-recycling hole the pins exist to close.
672
+ const ABSENCE_ANCHOR_LIMITS = Object.freeze({ maxPinnedDirectories: 4096 });
673
+
674
+ // Every no-follow read and every exclusive create in this file uses one of these
675
+ // two, so a change lands in one place rather than in seven.
676
+ const VERIFIED_READ_FLAGS =
677
+ fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0);
678
+ const VERIFIED_CREATE_FLAGS =
679
+ fs.constants.O_RDWR |
680
+ fs.constants.O_CREAT |
681
+ fs.constants.O_EXCL |
682
+ fs.constants.O_NOFOLLOW |
683
+ (fs.constants.O_CLOEXEC ?? 0);
684
+
685
+ function requireAbsenceAnchorCapacity(cache) {
686
+ if (cache.size >= ABSENCE_ANCHOR_LIMITS.maxPinnedDirectories) {
687
+ throw new Error(
688
+ `Absence anchoring exceeds ${ABSENCE_ANCHOR_LIMITS.maxPinnedDirectories} pinned directories`,
689
+ );
690
+ }
691
+ }
692
+
693
+ const ANCHORED_DIRECTORY_FLAGS =
694
+ fs.constants.O_RDONLY |
695
+ fs.constants.O_DIRECTORY |
696
+ fs.constants.O_NOFOLLOW |
697
+ (fs.constants.O_CLOEXEC ?? 0);
698
+
699
+ // Every absence receipt is verified long after its walk returns, so the chain
700
+ // that produced it has to stay pinned until the snapshot ends — an unpinned inode
701
+ // number can be recycled by a replacement directory that then reproduces the
702
+ // recorded identity exactly. Absent cited paths overwhelmingly share prefixes, so
703
+ // the walked directories are cached per snapshot and keyed by repo-relative
704
+ // prefix: one open descriptor and one anchored walk per distinct directory rather
705
+ // than per path. snapshotEvidence owns every descriptor in this cache and closes
706
+ // each exactly once; guards only borrow them for verification.
707
+ function anchoredAbsenceRoot(repo, cache) {
708
+ const cached = cache.get('');
709
+ if (cached) return cached;
710
+ requireAbsenceAnchorCapacity(cache);
711
+ const fd = openVerifiedDirectory(repo, ANCHORED_DIRECTORY_FLAGS);
712
+ const handle = {
713
+ fd,
714
+ expectedPath: repo,
715
+ chain: [
716
+ { expectedPath: repo, identity: stableDirectoryIdentity(fs.fstatSync(fd, { bigint: true })) },
717
+ ],
718
+ descriptors: [fd],
719
+ };
720
+ cache.set('', handle);
721
+ return handle;
722
+ }
723
+
724
+ function recordAnchoredAbsence(repo, repoPath, mutationGuards, cache) {
650
725
  requireDescriptorAnchoring();
651
- const flags =
652
- fs.constants.O_RDONLY |
653
- fs.constants.O_DIRECTORY |
654
- fs.constants.O_NOFOLLOW |
655
- (fs.constants.O_CLOEXEC ?? 0);
656
- const descriptors = [];
657
- let retainedFd;
658
- try {
659
- let currentFd = fs.openSync(repo, flags);
660
- descriptors.push(currentFd);
661
- const components = repoPath.split('/');
662
- for (let index = 0; index < components.length; index += 1) {
663
- const component = components[index];
664
- const child = descriptorPath(currentFd, component);
665
- let childStat;
666
- try {
667
- childStat = fs.lstatSync(child, { bigint: true });
668
- } catch (error) {
669
- if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
670
- const parentStat = fs.fstatSync(currentFd, { bigint: true });
671
- if (!parentStat.isDirectory()) {
672
- throw new Error(`Absence parent is no longer a directory for ${repoPath}`);
673
- }
674
- retainedFd = currentFd;
675
- mutationGuards.push({
676
- type: 'absence',
677
- fd: retainedFd,
678
- childName: component,
679
- repoPath,
680
- parentIdentity: stableDirectoryIdentity(parentStat),
681
- parentMutationIdentity: statIdentity(parentStat),
682
- });
683
- for (const fd of descriptors) {
684
- if (fd !== retainedFd) fs.closeSync(fd);
685
- }
686
- return;
687
- }
688
- if (index === components.length - 1) {
689
- throw new Error(`${repoPath} appeared while its absence was being anchored`);
690
- }
691
- if (childStat.isSymbolicLink() || !childStat.isDirectory()) {
692
- throw new Error(`Refusing a non-directory parent while anchoring absence for ${repoPath}`);
726
+ const components = repoPath.split('/');
727
+ let handle = anchoredAbsenceRoot(repo, cache);
728
+ let prefix = '';
729
+ for (let index = 0; index < components.length; index += 1) {
730
+ const component = components[index];
731
+ const isFinal = index === components.length - 1;
732
+ prefix = prefix === '' ? component : `${prefix}/${component}`;
733
+ // The final component is always re-checked against the filesystem: it is the
734
+ // one whose absence is being recorded, and a cached answer would be a stale
735
+ // one. Only the prefix directories are reused.
736
+ const cached = isFinal ? undefined : cache.get(prefix);
737
+ if (cached) {
738
+ handle = cached;
739
+ continue;
740
+ }
741
+ const child = anchoredChild(handle, component);
742
+ let childStat;
743
+ try {
744
+ childStat = lstatChild(child);
745
+ } catch (error) {
746
+ if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
747
+ const parentStat = fs.fstatSync(handle.fd, { bigint: true });
748
+ if (!parentStat.isDirectory()) {
749
+ throw new Error(`Absence parent is no longer a directory for ${repoPath}`);
693
750
  }
694
- const nextFd = fs.openSync(child, flags);
695
- descriptors.push(nextFd);
696
- currentFd = nextFd;
751
+ mutationGuards.push({
752
+ type: 'absence',
753
+ // The handle is the holder the guard verifies against, and `ref` is the
754
+ // child path already built through the anchoredChild chokepoint — the
755
+ // guard must never re-derive that name itself.
756
+ handle,
757
+ ref: child,
758
+ fd: handle.fd,
759
+ repoPath,
760
+ parentMutationIdentity: statIdentity(parentStat),
761
+ });
762
+ return;
697
763
  }
698
- throw new Error(`Could not anchor absence for ${repoPath}`);
699
- } catch (error) {
700
- for (const fd of descriptors) {
701
- if (fd === retainedFd) continue;
702
- try {
703
- fs.closeSync(fd);
704
- } catch {
705
- // Preserve the primary absence-anchoring error.
764
+ if (isFinal) {
765
+ throw new Error(`${repoPath} appeared while its absence was being anchored`);
766
+ }
767
+ if (childStat.isSymbolicLink() || !childStat.isDirectory()) {
768
+ throw new Error(`Refusing a non-directory parent while anchoring absence for ${repoPath}`);
769
+ }
770
+ requireAbsenceAnchorCapacity(cache);
771
+ const childFd = openVerifiedDirectory(child.path, ANCHORED_DIRECTORY_FLAGS);
772
+ const expectedPath = path.join(handle.expectedPath, component);
773
+ let next;
774
+ try {
775
+ if (!anchoringBackend().descriptorMatchesChild(childFd, expectedPath, childStat)) {
776
+ throw new Error(
777
+ `Absence parent descriptor does not match its verified inode for ${repoPath}`,
778
+ );
706
779
  }
780
+ next = {
781
+ fd: childFd,
782
+ expectedPath,
783
+ chain: [...handle.chain, { expectedPath, identity: stableDirectoryIdentity(childStat) }],
784
+ descriptors: [...handle.descriptors, childFd],
785
+ };
786
+ } catch (error) {
787
+ fs.closeSync(childFd);
788
+ throw error;
707
789
  }
708
- throw error;
790
+ cache.set(prefix, next);
791
+ handle = next;
709
792
  }
793
+ throw new Error(`Could not anchor absence for ${repoPath}`);
710
794
  }
711
795
 
712
- function materializeRecord(repo, statusRecord, layers, mutationGuards, testHooks) {
796
+ function materializeRecord(repo, statusRecord, layers, mutationGuards, testHooks, walkState) {
713
797
  const head = layers.head(statusRecord.path);
714
798
  const index = layers.index(statusRecord.path);
715
799
  const expectedKind = index.kind === 'gitlink' || head.kind === 'gitlink' ? 'gitlink' : null;
716
- guardPathParents(repo, statusRecord.path, mutationGuards);
800
+ guardPathParents(repo, statusRecord.path, mutationGuards, walkState.guardedDirectories);
717
801
  const filesystem = filesystemObject(
718
802
  path.join(repo, ...statusRecord.path.split('/')),
719
803
  expectedKind,
720
804
  mutationGuards,
721
805
  testHooks,
722
806
  );
723
- if (filesystem.kind === ABSENT) recordAnchoredAbsence(repo, statusRecord.path, mutationGuards);
807
+ if (filesystem.kind === ABSENT) {
808
+ recordAnchoredAbsence(repo, statusRecord.path, mutationGuards, walkState.absenceCache);
809
+ }
724
810
  if (statusRecord.directory_hint && filesystem.kind !== 'directory') {
725
811
  throw new Error(
726
812
  `Git reported an embedded directory but found ${filesystem.kind}: ${statusRecord.path}`,
@@ -789,9 +875,15 @@ export function serializeDirtyRecords(entries) {
789
875
  }
790
876
 
791
877
  function assertRepository(repoInput) {
792
- const repo = fs.realpathSync(requireString(repoInput, 'repo'));
878
+ // realpathSync.native, not realpathSync: the JS resolver preserves a Windows
879
+ // 8.3 short component (C:\Users\RUNNER~1\...) while git always reports the long
880
+ // form, so the two would never compare equal and every caller would be told the
881
+ // worktree root is not the worktree root it just named.
882
+ const repo = fs.realpathSync.native(requireString(repoInput, 'repo'));
793
883
  const topLevelResult = git(repo, ['rev-parse', '--show-toplevel']);
794
- const topLevel = fs.realpathSync(decodeUtf8(topLevelResult.stdout, 'repository root').trim());
884
+ const topLevel = fs.realpathSync.native(
885
+ decodeUtf8(topLevelResult.stdout, 'repository root').trim(),
886
+ );
795
887
  if (topLevel !== repo) throw new Error(`--repo must be the Git worktree root (${topLevel})`);
796
888
  return repo;
797
889
  }
@@ -882,17 +974,48 @@ function stableFileIdentity(stat) {
882
974
  return [stat.dev, stat.ino, stat.mode, stat.size].map(String).join(':');
883
975
  }
884
976
 
977
+ // The two backends below differ in one decisive way, and it is worth stating
978
+ // plainly because the security properties are not the same.
979
+ //
980
+ // Linux ANCHORS. A name is resolved through /proc/self/fd/<fd>/<child>, which
981
+ // starts the walk at the inode the descriptor holds, so a parent that is renamed
982
+ // away cannot be traversed at all: the descriptor keeps pointing at the original
983
+ // directory and the impostor planted at the same name is simply never reached.
984
+ //
985
+ // macOS VERIFIES. Node cannot resolve a name relative to a descriptor there —
986
+ // /dev/fd/<fd> is not a magic link (it stats as the directory but every attempt
987
+ // to traverse a child through it returns ENOENT), and fcntl F_GETPATH is a
988
+ // name-cache snapshot rather than a live anchor. So the Darwin backend resolves
989
+ // lexically, holds an open descriptor on every element of the chain, and proves
990
+ // before and after each operation that the path chain still names exactly the
991
+ // inodes it is holding. That DETECTS a swapped parent and aborts the write; it
992
+ // does not make the swap impossible the way the Linux path does. A swap landing
993
+ // inside the window between a check and the call it guards is caught by the
994
+ // following check, after the fact, rather than being unreachable.
995
+ //
996
+ // Every other platform gets neither and is refused outright.
885
997
  function requireDescriptorAnchoring() {
886
- if (
887
- process.platform !== 'linux' ||
888
- fs.constants.O_DIRECTORY === undefined ||
889
- fs.constants.O_NOFOLLOW === undefined ||
890
- !fs.existsSync('/proc/self/fd')
891
- ) {
892
- throw new Error(
893
- 'Safe generated-plan writes require Linux /proc/self/fd and O_DIRECTORY/O_NOFOLLOW; refusing an unanchored write',
894
- );
998
+ const directoryFlagsAvailable =
999
+ fs.constants.O_DIRECTORY !== undefined && fs.constants.O_NOFOLLOW !== undefined;
1000
+ if (process.platform === 'linux') {
1001
+ if (!directoryFlagsAvailable || !fs.existsSync('/proc/self/fd')) {
1002
+ throw new Error(
1003
+ 'Safe generated-plan writes require Linux /proc/self/fd and O_DIRECTORY/O_NOFOLLOW; refusing an unanchored write',
1004
+ );
1005
+ }
1006
+ return;
895
1007
  }
1008
+ if (process.platform === 'darwin') {
1009
+ if (!directoryFlagsAvailable) {
1010
+ throw new Error(
1011
+ 'Safe generated-plan writes require macOS O_DIRECTORY/O_NOFOLLOW; refusing an unverified write',
1012
+ );
1013
+ }
1014
+ return;
1015
+ }
1016
+ throw new Error(
1017
+ `Safe generated-plan writes require Linux /proc/self/fd or macOS O_DIRECTORY/O_NOFOLLOW; ${process.platform} offers neither, so refusing an unanchored write`,
1018
+ );
896
1019
  }
897
1020
 
898
1021
  function descriptorPath(fd, childName) {
@@ -900,157 +1023,352 @@ function descriptorPath(fd, childName) {
900
1023
  return childName === undefined ? base : path.join(base, childName);
901
1024
  }
902
1025
 
903
- function externalDescriptorPath(fd, childName) {
904
- const base = `/proc/${process.pid}/fd/${fd}`;
905
- return childName === undefined ? base : path.join(base, childName);
906
- }
907
-
908
- const RENAME_NOREPLACE_SCRIPT = String.raw`
909
- import ctypes
910
- import errno
911
- import os
912
- import sys
913
-
914
- libc = ctypes.CDLL(None, use_errno=True)
915
- try:
916
- renameat2 = libc.renameat2
917
- except AttributeError:
918
- print("libc does not expose renameat2", file=sys.stderr)
919
- raise SystemExit(125)
920
-
921
- renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
922
- renameat2.restype = ctypes.c_int
923
- result = renameat2(-100, os.fsencode(sys.argv[1]), -100, os.fsencode(sys.argv[2]), 1)
924
- if result != 0:
925
- error_number = ctypes.get_errno()
926
- error_name = errno.errorcode.get(error_number, "UNKNOWN")
927
- print(f"renameat2 RENAME_NOREPLACE failed: {error_name}: {os.strerror(error_number)}", file=sys.stderr)
928
- raise SystemExit(17 if error_number == errno.EEXIST else 126)
929
- `;
930
-
931
- let atomicMoverPath;
932
-
933
- function spawnHeldExecutable(executable, args, options) {
934
- const before = fs.fstatSync(executable.fd, { bigint: true });
935
- if (!before.isFile() || statIdentity(before) !== executable.identity) {
936
- throw new Error('Validated Python executable changed before invocation');
937
- }
938
- const result = spawnSync('/proc/self/fd/3', args, {
939
- ...options,
940
- stdio: ['ignore', 'pipe', 'pipe', executable.fd],
941
- });
942
- const after = fs.fstatSync(executable.fd, { bigint: true });
943
- assertStableIdentity(before, after, 'validated Python executable');
944
- return result;
945
- }
946
-
947
- function validatedPathExecutable(candidate) {
948
- if (!path.isAbsolute(candidate)) return null;
949
- const candidateDirectory = path.dirname(candidate);
950
- let resolvedDirectory;
951
- let resolved;
952
- let directoryStats;
953
- let executableStat;
1026
+ // Directory opens are plain O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC on both
1027
+ // platforms, and deliberately nothing else.
1028
+ //
1029
+ // O_NOFOLLOW_ANY (macOS 11+) used to be ORed in here on the theory that XNU
1030
+ // ignores unrecognized open flag bits, so it would be inert where unsupported.
1031
+ // That was wrong: combined with O_DIRECTORY macOS rejects it outright with
1032
+ // EINVAL, and every directory open on Darwin failed. It is gone and is not
1033
+ // coming back behind a probe or a degrade-on-EINVAL path — the per-component
1034
+ // O_NOFOLLOW walk is what delivers the guarantee. Rust's cap-std, the closest
1035
+ // reference implementation of this problem, has not adopted O_NOFOLLOW_ANY
1036
+ // either (their issue #179 is still open).
1037
+ function openVerifiedDirectory(absolute, flags) {
1038
+ return fs.openSync(absolute, flags);
1039
+ }
1040
+
1041
+ // File opens additionally get O_NONBLOCK, which directory opens do not need:
1042
+ // it stops a FIFO swapped in at the target name from wedging the process on
1043
+ // open. The identity comparison that follows rejects the FIFO anyway, but only
1044
+ // if we ever get as far as running it.
1045
+ function openVerifiedFile(absolute, flags, mode) {
1046
+ const nonBlocking = flags | (fs.constants.O_NONBLOCK ?? 0);
1047
+ return mode === undefined
1048
+ ? fs.openSync(absolute, nonBlocking)
1049
+ : fs.openSync(absolute, nonBlocking, mode);
1050
+ }
1051
+
1052
+ // The publish primitive, identical on both platforms.
1053
+ //
1054
+ // link() is the portable no-replace publish: it fails with EEXIST if the
1055
+ // destination name is taken — by a regular file, by a directory, or by a symlink,
1056
+ // live or dangling — and it never follows that symlink to clobber its target.
1057
+ // It also works where renameat2(RENAME_NOREPLACE) does not, notably v9fs, which
1058
+ // is why the WSL2 9p case that used to fail every time now works.
1059
+ //
1060
+ // The published file is the same inode as the temporary, so every identity
1061
+ // comparison the callers already make still holds, and validateCommittedPlan
1062
+ // becomes strictly stronger: it compares the destination against the exact inode
1063
+ // whose bytes were fsynced.
1064
+ //
1065
+ // On Linux both paths are /proc/self/fd/<fd>/<name>, so the publish is anchored
1066
+ // to the held parent descriptors exactly like every other operation.
1067
+ // link(2) BUGS: "On NFS filesystems, the return code may be wrong in case the NFS
1068
+ // server performs the link creation and dies before it can say so. Use stat(2) to
1069
+ // find out if the link got created." open(2) NOTES gives the remedy this
1070
+ // implements: on a reported failure, stat the source and see whether its link
1071
+ // count reached 2. A false positive would need someone to have hardlinked a
1072
+ // 16-random-byte name inside a directory we hold open — and validateCommittedPlan
1073
+ // still proves the destination is the exact temporary inode afterwards.
1074
+ function linkCreatedDespiteError(sourcePath) {
954
1075
  try {
955
- resolvedDirectory = fs.realpathSync(candidateDirectory);
956
- resolved = fs.realpathSync(candidate);
957
- const resolvedExecutableDirectory = fs.realpathSync(path.dirname(resolved));
958
- directoryStats = [...new Set([resolvedDirectory, resolvedExecutableDirectory])].map(
959
- (directory) => fs.statSync(directory),
960
- );
961
- executableStat = fs.lstatSync(resolved);
962
- fs.accessSync(resolved, fs.constants.X_OK);
1076
+ return fs.statSync(sourcePath, { bigint: true }).nlink === 2n;
963
1077
  } catch {
964
- return null;
1078
+ return false;
965
1079
  }
966
- if (
967
- directoryStats.some((stat) => !stat.isDirectory()) ||
968
- !executableStat.isFile() ||
969
- executableStat.isSymbolicLink()
970
- ) {
971
- return null;
1080
+ }
1081
+
1082
+ function linkNoReplace(sourcePath, destinationPath) {
1083
+ try {
1084
+ fs.linkSync(sourcePath, destinationPath);
1085
+ } catch (error) {
1086
+ // Callers treat "destination taken" as a distinct outcome, not a failure.
1087
+ if (error?.code === 'EEXIST') return false;
1088
+ if (!linkCreatedDespiteError(sourcePath)) {
1089
+ // FAT, Coda, and some SMB/FUSE/virtiofs mounts have no hardlinks at all.
1090
+ // Git falls back to rename here, but git can afford to lose collision
1091
+ // detection because its objects are content-addressed; a plan destination
1092
+ // is a plain name, so a replacing rename would silently clobber whatever
1093
+ // is already there. Refuse loudly instead.
1094
+ if (error?.code === 'EPERM' || error?.code === 'ENOTSUP' || error?.code === 'EMLINK') {
1095
+ throw new Error(
1096
+ `Generated-plan publication requires hard links, which this filesystem refused (${error.code}); refusing to fall back to a replacing rename`,
1097
+ );
1098
+ }
1099
+ throw error;
1100
+ }
972
1101
  }
973
- const uid = typeof process.getuid === 'function' ? process.getuid() : null;
974
- const trustedOwner = (stat) => uid === null || stat.uid === 0 || stat.uid === uid;
975
- if (
976
- directoryStats.some((stat) => !trustedOwner(stat) || (stat.mode & 0o022) !== 0) ||
977
- !trustedOwner(executableStat) ||
978
- (executableStat.mode & 0o022) !== 0
979
- ) {
980
- return null;
1102
+ try {
1103
+ fs.unlinkSync(sourcePath);
1104
+ } catch {
1105
+ // The link succeeded, so the plan IS published. A temporary name left behind
1106
+ // is a stray file, not an unpublished plan: reporting it as a failure would
1107
+ // be a lie, and rolling back would unpublish a plan that is already live.
981
1108
  }
982
- return resolved;
1109
+ return true;
983
1110
  }
984
1111
 
985
- function resolveAtomicMover() {
986
- if (atomicMoverPath) return atomicMoverPath;
987
- const candidates = new Set();
988
- for (const entry of (process.env.PATH ?? '').split(path.delimiter)) {
989
- if (entry && path.isAbsolute(entry)) candidates.add(path.join(entry, 'python3'));
990
- }
991
- for (const entry of ['/usr/local/bin/python3', '/usr/bin/python3', '/bin/python3']) {
992
- candidates.add(entry);
1112
+ // A directory holder is anything that owns a verified chain: a plan-parent
1113
+ // handle, a ref's parent directory, or an absence guard. Two arrays describe it,
1114
+ // both root-first and the same length — `chain` records each element's expected
1115
+ // path and dev/ino/mode, and `descriptors` holds an open descriptor on each.
1116
+ //
1117
+ // Holding those descriptors is load-bearing rather than decorative. dev/ino/mode
1118
+ // is unique only among *live* inodes: an inode number freed by an rmdir is handed
1119
+ // straight back to the next mkdir, so a replacement directory can reproduce a
1120
+ // recorded identity exactly. An open descriptor pins the inode, so the number
1121
+ // cannot be recycled for as long as the holder exists.
1122
+ function verifyPinnedDescriptors(holder) {
1123
+ const { chain, descriptors } = holder;
1124
+ if (!Array.isArray(descriptors) || descriptors.length !== chain.length) {
1125
+ throw new Error('Generated-plan parent chain is missing the descriptors that pin it');
1126
+ }
1127
+ chain.forEach((item, index) => {
1128
+ const pinned = fs.fstatSync(descriptors[index], { bigint: true });
1129
+ if (!pinned.isDirectory() || stableDirectoryIdentity(pinned) !== item.identity) {
1130
+ throw new Error('Generated-plan parent descriptor changed during the write');
1131
+ }
1132
+ });
1133
+ }
1134
+
1135
+ function verifyLexicalChain(holder) {
1136
+ for (const item of holder.chain) {
1137
+ let lexical;
1138
+ try {
1139
+ lexical = fs.lstatSync(item.expectedPath, { bigint: true });
1140
+ } catch (error) {
1141
+ if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
1142
+ // A parent renamed out from under us is a mismatch, not a missing file:
1143
+ // reporting the raw ENOENT would leak an unrelated-looking error out of a
1144
+ // check whose whole job is to say the chain no longer holds.
1145
+ throw new Error('Generated-plan lexical parent no longer matches its directory descriptor');
1146
+ }
1147
+ if (
1148
+ lexical.isSymbolicLink() ||
1149
+ !lexical.isDirectory() ||
1150
+ stableDirectoryIdentity(lexical) !== item.identity
1151
+ ) {
1152
+ throw new Error('Generated-plan lexical parent no longer matches its directory descriptor');
1153
+ }
993
1154
  }
994
- for (const candidate of candidates) {
995
- const resolved = validatedPathExecutable(candidate);
996
- if (!resolved) continue;
997
- let fd;
1155
+ }
1156
+
1157
+ // The whole platform seam, in five methods. Everything else an operation does is
1158
+ // identical on both platforms and lives in the shared functions below.
1159
+ //
1160
+ // Only two things actually differ: how a name becomes a path, and what guard
1161
+ // wraps the operation that uses it.
1162
+ //
1163
+ // Linux ANCHORS. /proc/self/fd/<fd>/<name> starts the walk at the inode the
1164
+ // descriptor holds, so a parent renamed away cannot be traversed at all and the
1165
+ // guard is a no-op — there is nothing left to verify.
1166
+ //
1167
+ // macOS VERIFIES. It resolves lexically, so before and after every operation it
1168
+ // proves that each element of the path chain still names the exact inode being
1169
+ // held for it. That DETECTS a swapped parent and aborts; it does not make the
1170
+ // swap impossible. A swap landing inside the window is caught by the trailing
1171
+ // check, after the fact, rather than being unreachable. The check runs after a
1172
+ // failure too, because a verdict observed through a chain that has since changed
1173
+ // is not a verdict.
1174
+ const LINUX_ANCHORING = {
1175
+ childPath(dirHandle, childName) {
1176
+ return descriptorPath(dirHandle.fd, childName);
1177
+ },
1178
+ verified(holders, run) {
1179
+ return run();
1180
+ },
1181
+ descriptorMatchesChild(fd, expectedPath) {
1182
+ return fs.realpathSync.native(descriptorPath(fd)) === expectedPath;
1183
+ },
1184
+ parentStillResolves(parentHandle) {
1185
+ return fs.realpathSync.native(descriptorPath(parentHandle.fd)) === parentHandle.expectedPath;
1186
+ },
1187
+ verifyAbsentChild(guard) {
1188
+ if (absentChildIsPresent(guard.ref)) {
1189
+ throw new Error(`${guard.repoPath} appeared before evidence materialization completed`);
1190
+ }
1191
+ },
1192
+ };
1193
+
1194
+ const DARWIN_ANCHORING = {
1195
+ childPath(dirHandle, childName) {
1196
+ return path.join(dirHandle.expectedPath, childName);
1197
+ },
1198
+ verified(holders, run) {
1199
+ const list = Array.isArray(holders) ? holders : [holders];
1200
+ const proveChain = () => {
1201
+ for (const holder of list) {
1202
+ verifyPinnedDescriptors(holder);
1203
+ verifyLexicalChain(holder);
1204
+ }
1205
+ };
1206
+ proveChain();
1207
+ let value;
998
1208
  try {
999
- fd = fs.openSync(
1000
- resolved,
1001
- fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
1002
- );
1003
- } catch {
1004
- continue;
1209
+ value = run();
1210
+ } catch (error) {
1211
+ proveChain();
1212
+ throw error;
1005
1213
  }
1214
+ proveChain();
1215
+ return value;
1216
+ },
1217
+ descriptorMatchesChild(fd, _expectedPath, childStat) {
1218
+ // There is no live fd-to-path oracle on macOS (F_GETPATH is a name-cache
1219
+ // snapshot, not an anchor), so escape is decided the other way round: the
1220
+ // name was just resolved under a verified chain, and the descriptor opened
1221
+ // from it counts only if it is that same inode.
1006
1222
  const opened = fs.fstatSync(fd, { bigint: true });
1007
- const executable = { fd, identity: statIdentity(opened), resolved };
1008
- const version = spawnHeldExecutable(
1009
- executable,
1010
- ['-I', '-S', '-c', 'import sys; print(sys.version_info[0])'],
1011
- {
1012
- encoding: 'utf8',
1013
- env: { ...process.env, LANG: 'C', LC_ALL: 'C' },
1014
- timeout: 10_000,
1015
- windowsHide: true,
1016
- },
1223
+ return (
1224
+ opened.isDirectory() && stableDirectoryIdentity(opened) === stableDirectoryIdentity(childStat)
1017
1225
  );
1018
- if (version.status === 0 && version.stdout.trim() === '3') {
1019
- atomicMoverPath = executable;
1020
- return executable;
1226
+ },
1227
+ parentStillResolves(parentHandle) {
1228
+ // Both halves are needed: a directory renamed away keeps its inode, so the
1229
+ // descriptors alone still match and only the lexical half notices it moved.
1230
+ try {
1231
+ verifyPinnedDescriptors(parentHandle);
1232
+ verifyLexicalChain(parentHandle);
1233
+ } catch {
1234
+ return false;
1235
+ }
1236
+ return true;
1237
+ },
1238
+ verifyAbsentChild(guard) {
1239
+ let present;
1240
+ try {
1241
+ present = DARWIN_ANCHORING.verified(guard.handle, () => absentChildIsPresent(guard.ref));
1242
+ } catch (error) {
1243
+ // A chain that no longer holds makes the absence verdict meaningless, and
1244
+ // the caller reports that as the anchor changing rather than as a stray
1245
+ // parent-descriptor error. Linux cannot reach this: its guard is a no-op.
1246
+ throw new Error(
1247
+ `Absence anchor changed for ${guard.repoPath}: ${
1248
+ error instanceof Error ? error.message : String(error)
1249
+ }`,
1250
+ );
1251
+ }
1252
+ if (present) {
1253
+ throw new Error(`${guard.repoPath} appeared before evidence materialization completed`);
1021
1254
  }
1255
+ },
1256
+ };
1257
+
1258
+ const ANCHORING_BACKENDS = new Map([
1259
+ ['linux', LINUX_ANCHORING],
1260
+ ['darwin', DARWIN_ANCHORING],
1261
+ ]);
1262
+
1263
+ function anchoringBackend() {
1264
+ const backend = ANCHORING_BACKENDS.get(process.platform);
1265
+ if (!backend) {
1266
+ // requireDescriptorAnchoring normally refuses first; this is the same answer
1267
+ // from the other side, so an unsupported platform can never fall through to
1268
+ // whichever backend happened to be the ternary's default.
1269
+ throw new Error(
1270
+ `No generated-plan anchoring backend for ${process.platform}; refusing an unanchored write`,
1271
+ );
1272
+ }
1273
+ return backend;
1274
+ }
1275
+
1276
+ // Open, fstat, compare, close on mismatch. The descriptor never escapes this
1277
+ // function unless it refers to the inode the caller already verified by name, so
1278
+ // a lexical open that landed anywhere else cannot be used by accident. On Linux
1279
+ // the comparison passes trivially — the /proc walk already resolved from the
1280
+ // held parent — and costs one fstat to keep the guarantee structural rather than
1281
+ // dependent on which backend is in play.
1282
+ function adoptVerifiedFile(ref, expectedStat, flags) {
1283
+ const fd = openVerifiedFile(ref.path, flags);
1284
+ let opened;
1285
+ try {
1286
+ opened = fs.fstatSync(fd, { bigint: true });
1287
+ } catch (error) {
1022
1288
  fs.closeSync(fd);
1289
+ throw error;
1023
1290
  }
1024
- throw new Error(
1025
- 'Safe generated-plan publication requires a trusted absolute Python 3 PATH candidate with libc renameat2 support',
1026
- );
1291
+ if (stableFileIdentity(opened) !== stableFileIdentity(expectedStat)) {
1292
+ fs.closeSync(fd);
1293
+ return null;
1294
+ }
1295
+ return fd;
1027
1296
  }
1028
1297
 
1029
- function atomicMoveNoReplace(source, destination) {
1030
- const mover = resolveAtomicMover();
1031
- const result = spawnHeldExecutable(
1032
- mover,
1033
- ['-I', '-S', '-c', RENAME_NOREPLACE_SCRIPT, source, destination],
1034
- {
1035
- encoding: 'utf8',
1036
- env: { ...process.env, LANG: 'C', LC_ALL: 'C' },
1037
- timeout: 10_000,
1038
- windowsHide: true,
1039
- },
1040
- );
1041
- if (result.error) throw result.error;
1042
- if (result.status === 17) return false;
1043
- if (result.status !== 0) {
1044
- throw new Error(
1045
- `Atomic no-replace move failed (${result.status}): ${(result.stderr ?? '').trim()}`,
1046
- );
1298
+ function absentChildIsPresent(ref) {
1299
+ try {
1300
+ fs.lstatSync(ref.path, { bigint: true });
1301
+ } catch (error) {
1302
+ if (error?.code === 'ENOENT') return false;
1303
+ throw error;
1047
1304
  }
1048
1305
  return true;
1049
1306
  }
1050
1307
 
1051
- function lstatOptional(absolute) {
1308
+ // The operations. Each is the same on both platforms; only the guard differs.
1309
+ function lstatChild(ref) {
1310
+ return anchoringBackend().verified(ref.dir, () => fs.lstatSync(ref.path, { bigint: true }));
1311
+ }
1312
+
1313
+ function openChildRead(ref, flags, expectedStat) {
1314
+ return anchoringBackend().verified(ref.dir, () => {
1315
+ const fd = adoptVerifiedFile(ref, expectedStat, flags);
1316
+ if (fd === null) {
1317
+ throw new Error(`${ref.name} was replaced between its verified stat and its no-follow open`);
1318
+ }
1319
+ return fd;
1320
+ });
1321
+ }
1322
+
1323
+ function createChild(ref, flags, mode) {
1324
+ // O_CREAT|O_EXCL|O_NOFOLLOW is atomic at the leaf, so the only thing the guard
1325
+ // has to cover is which directory the leaf landed in.
1326
+ return anchoringBackend().verified(ref.dir, () => openVerifiedFile(ref.path, flags, mode));
1327
+ }
1328
+
1329
+ function mkdirChild(ref, mode) {
1330
+ anchoringBackend().verified(ref.dir, () => fs.mkdirSync(ref.path, { mode }));
1331
+ }
1332
+
1333
+ function publishNoReplace(sourceRef, destinationRef) {
1334
+ return anchoringBackend().verified([sourceRef.dir, destinationRef.dir], () =>
1335
+ linkNoReplace(sourceRef.path, destinationRef.path),
1336
+ );
1337
+ }
1338
+
1339
+ // The single place a name becomes a path, and therefore the right place to
1340
+ // enforce that a name is one ordinary component.
1341
+ //
1342
+ // A trailing separator is the sharp edge here, not a tidiness concern:
1343
+ // open(path, O_NOFOLLOW) FOLLOWS a symlink when path ends in "/" — the trap
1344
+ // behind CVE-2026-39822 / golang/go#79005, which let os.Root escape its own
1345
+ // root. path.join preserves that trailing slash, so a component carrying one
1346
+ // would turn every no-follow open in this file into a following one.
1347
+ // normalizeRepoPath already rejects such components upstream; this is the
1348
+ // chokepoint that makes it true for every caller, including the generated
1349
+ // temporary and vault names that never pass through it.
1350
+ function anchoredChild(dirHandle, childName) {
1351
+ if (
1352
+ typeof childName !== 'string' ||
1353
+ childName === '' ||
1354
+ childName === '.' ||
1355
+ childName === '..' ||
1356
+ childName.includes('/') ||
1357
+ childName.includes('\\') ||
1358
+ childName.includes('\0')
1359
+ ) {
1360
+ throw new Error(`Refusing to resolve ${JSON.stringify(childName)} as a single path component`);
1361
+ }
1362
+ return {
1363
+ dir: dirHandle,
1364
+ name: childName,
1365
+ path: anchoringBackend().childPath(dirHandle, childName),
1366
+ };
1367
+ }
1368
+
1369
+ function lstatAnchoredOptional(ref) {
1052
1370
  try {
1053
- return fs.lstatSync(absolute, { bigint: true });
1371
+ return lstatChild(ref);
1054
1372
  } catch (error) {
1055
1373
  if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return null;
1056
1374
  throw error;
@@ -1063,39 +1381,37 @@ function openPlanParent(
1063
1381
  { createMissing = true, purpose = 'Generated-plan' } = {},
1064
1382
  ) {
1065
1383
  requireDescriptorAnchoring();
1066
- const flags =
1067
- fs.constants.O_RDONLY |
1068
- fs.constants.O_DIRECTORY |
1069
- fs.constants.O_NOFOLLOW |
1070
- (fs.constants.O_CLOEXEC ?? 0);
1384
+ // Root-first and index-aligned with `chain`: verifyPinnedDescriptors relies on
1385
+ // that, and the descriptors are what pin each recorded inode against reuse.
1071
1386
  const descriptors = [];
1072
1387
  try {
1073
- let currentFd = fs.openSync(repo, flags);
1388
+ let currentFd = openVerifiedDirectory(repo, ANCHORED_DIRECTORY_FLAGS);
1074
1389
  descriptors.push(currentFd);
1075
1390
  const rootStat = fs.fstatSync(currentFd, { bigint: true });
1076
1391
  const chain = [{ expectedPath: repo, identity: stableDirectoryIdentity(rootStat) }];
1392
+ let currentHandle = { fd: currentFd, expectedPath: repo, chain, descriptors };
1077
1393
  const traversed = [];
1078
1394
  for (const component of parentComponents) {
1079
1395
  traversed.push(component);
1080
- const anchoredChild = descriptorPath(currentFd, component);
1396
+ const child = anchoredChild(currentHandle, component);
1081
1397
  let childStat;
1082
1398
  let created = false;
1083
1399
  try {
1084
- childStat = fs.lstatSync(anchoredChild, { bigint: true });
1400
+ childStat = lstatChild(child);
1085
1401
  } catch (error) {
1086
1402
  if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
1087
1403
  if (!createMissing) {
1088
1404
  throw new Error(`${purpose} parent does not exist: ${traversed.join('/')}`);
1089
1405
  }
1090
- fs.mkdirSync(anchoredChild, { mode: 0o755 });
1091
- childStat = fs.lstatSync(anchoredChild, { bigint: true });
1406
+ mkdirChild(child, 0o755);
1407
+ childStat = lstatChild(child);
1092
1408
  created = true;
1093
1409
  }
1094
1410
  if (childStat.isSymbolicLink() || !childStat.isDirectory()) {
1095
1411
  throw new Error(`${purpose} parent is not a real directory: ${traversed.join('/')}`);
1096
1412
  }
1097
1413
  const parentFd = currentFd;
1098
- const childFd = fs.openSync(anchoredChild, flags);
1414
+ const childFd = openVerifiedDirectory(child.path, ANCHORED_DIRECTORY_FLAGS);
1099
1415
  descriptors.push(childFd);
1100
1416
  currentFd = childFd;
1101
1417
  if (created) {
@@ -1103,18 +1419,16 @@ function openPlanParent(
1103
1419
  fs.fsyncSync(parentFd);
1104
1420
  }
1105
1421
  const expected = path.join(repo, ...traversed);
1106
- const actual = fs.realpathSync(descriptorPath(currentFd));
1107
- if (actual !== expected) {
1422
+ if (!anchoringBackend().descriptorMatchesChild(currentFd, expected, childStat)) {
1108
1423
  throw new Error(`${purpose} parent escaped the repository: ${traversed.join('/')}`);
1109
1424
  }
1110
1425
  const openedStat = fs.fstatSync(currentFd, { bigint: true });
1111
1426
  chain.push({ expectedPath: expected, identity: stableDirectoryIdentity(openedStat) });
1427
+ currentHandle = { fd: currentFd, expectedPath: expected, chain, descriptors };
1112
1428
  }
1113
- const stat = fs.fstatSync(currentFd, { bigint: true });
1114
1429
  return {
1115
1430
  descriptors,
1116
1431
  fd: currentFd,
1117
- identity: stableDirectoryIdentity(stat),
1118
1432
  expectedPath: path.join(repo, ...parentComponents),
1119
1433
  chain,
1120
1434
  };
@@ -1134,9 +1448,16 @@ function closeDescriptors(descriptors) {
1134
1448
  }
1135
1449
  }
1136
1450
 
1451
+ // A handle's identity IS its chain leaf's identity. Storing it twice meant two
1452
+ // fstats a line apart and a re-stamp helper to keep them agreeing; deriving it
1453
+ // removes both.
1454
+ function handleIdentity(handle) {
1455
+ return handle.chain[handle.chain.length - 1].identity;
1456
+ }
1457
+
1137
1458
  function resolveGitDirectory(repo) {
1138
1459
  const result = git(repo, ['rev-parse', '--absolute-git-dir']);
1139
- return fs.realpathSync(decodeUtf8(result.stdout, 'Git administrative directory').trim());
1460
+ return fs.realpathSync.native(decodeUtf8(result.stdout, 'Git administrative directory').trim());
1140
1461
  }
1141
1462
 
1142
1463
  function openBackupVault(repo, { createMissing = true } = {}) {
@@ -1147,9 +1468,12 @@ function openBackupVault(repo, { createMissing = true } = {}) {
1147
1468
  });
1148
1469
  fs.fchmodSync(handle.fd, 0o700);
1149
1470
  fs.fsyncSync(handle.fd);
1150
- const stat = fs.fstatSync(handle.fd, { bigint: true });
1151
- handle.identity = stableDirectoryIdentity(stat);
1152
- handle.chain[handle.chain.length - 1].identity = handle.identity;
1471
+ // mode is part of every directory identity, so hardening the vault changes the
1472
+ // identity the chain recorded for it; without this the next verification would
1473
+ // reject the directory it just hardened.
1474
+ handle.chain[handle.chain.length - 1].identity = stableDirectoryIdentity(
1475
+ fs.fstatSync(handle.fd, { bigint: true }),
1476
+ );
1153
1477
  return { ...handle, gitDirectory };
1154
1478
  }
1155
1479
 
@@ -1157,33 +1481,28 @@ function validatePlanParent(parentHandle) {
1157
1481
  const descriptorStat = fs.fstatSync(parentHandle.fd, { bigint: true });
1158
1482
  if (
1159
1483
  !descriptorStat.isDirectory() ||
1160
- stableDirectoryIdentity(descriptorStat) !== parentHandle.identity
1484
+ stableDirectoryIdentity(descriptorStat) !== handleIdentity(parentHandle)
1161
1485
  ) {
1162
1486
  throw new Error('Generated-plan parent descriptor changed during the write');
1163
1487
  }
1164
- const descriptorRealPath = fs.realpathSync(descriptorPath(parentHandle.fd));
1165
- if (descriptorRealPath !== parentHandle.expectedPath) {
1488
+ if (!anchoringBackend().parentStillResolves(parentHandle)) {
1166
1489
  throw new Error('Generated-plan parent moved or was replaced during the write');
1167
1490
  }
1168
- for (const item of parentHandle.chain) {
1169
- const lexicalStat = fs.lstatSync(item.expectedPath, { bigint: true });
1170
- if (
1171
- lexicalStat.isSymbolicLink() ||
1172
- !lexicalStat.isDirectory() ||
1173
- stableDirectoryIdentity(lexicalStat) !== item.identity
1174
- ) {
1175
- throw new Error('Generated-plan lexical parent no longer matches its directory descriptor');
1176
- }
1177
- }
1491
+ // Both halves come from the shared helpers rather than being restated here: an
1492
+ // earlier hand-copy of the lexical loop lost verifyLexicalChain's ENOENT/ENOTDIR
1493
+ // translation, so a renamed parent could surface a raw errno from a function
1494
+ // with a dozen call sites.
1495
+ verifyPinnedDescriptors(parentHandle);
1496
+ verifyLexicalChain(parentHandle);
1178
1497
  }
1179
1498
 
1180
1499
  function inspectPlanDestination(
1181
- finalPath,
1500
+ finalRef,
1182
1501
  { replace, expectedIdentity, mustBeAbsent = false } = {},
1183
1502
  ) {
1184
1503
  let stat;
1185
1504
  try {
1186
- stat = fs.lstatSync(finalPath, { bigint: true });
1505
+ stat = lstatChild(finalRef);
1187
1506
  } catch (error) {
1188
1507
  if (error?.code === 'ENOENT') {
1189
1508
  if (expectedIdentity) throw new Error('Generated plan disappeared during the write');
@@ -1201,19 +1520,17 @@ function inspectPlanDestination(
1201
1520
  if (expectedIdentity && identity !== expectedIdentity) {
1202
1521
  throw new Error('Generated plan changed during the write');
1203
1522
  }
1204
- return identity;
1523
+ return stat;
1205
1524
  }
1206
1525
 
1207
- function openExistingPlanDestination(finalPath, replace) {
1208
- const identity = inspectPlanDestination(finalPath, { replace });
1209
- if (identity === null) {
1526
+ function openExistingPlanDestination(finalRef, replace) {
1527
+ const stat = inspectPlanDestination(finalRef, { replace });
1528
+ if (stat === null) {
1210
1529
  if (replace) throw new Error('Deepen mode requires an existing generated plan to replace');
1211
1530
  return { fd: undefined, identity: null, stableIdentity: null };
1212
1531
  }
1213
- const fd = fs.openSync(
1214
- finalPath,
1215
- fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
1216
- );
1532
+ const identity = statIdentity(stat);
1533
+ const fd = openChildRead(finalRef, VERIFIED_READ_FLAGS, stat);
1217
1534
  try {
1218
1535
  const opened = fs.fstatSync(fd, { bigint: true });
1219
1536
  if (!opened.isFile() || statIdentity(opened) !== identity) {
@@ -1264,8 +1581,8 @@ function hashOpenFile(fd, label) {
1264
1581
  };
1265
1582
  }
1266
1583
 
1267
- function validateCommittedPlan(finalPath, tempFd, expectedTemp, testHooks) {
1268
- const before = fs.lstatSync(finalPath, { bigint: true });
1584
+ function validateCommittedPlan(finalRef, tempFd, expectedTemp, testHooks) {
1585
+ const before = lstatChild(finalRef);
1269
1586
  if (
1270
1587
  before.isSymbolicLink() ||
1271
1588
  !before.isFile() ||
@@ -1273,19 +1590,16 @@ function validateCommittedPlan(finalPath, tempFd, expectedTemp, testHooks) {
1273
1590
  ) {
1274
1591
  throw new Error('Generated-plan destination failed its first post-write identity check');
1275
1592
  }
1276
- const finalFd = fs.openSync(
1277
- finalPath,
1278
- fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
1279
- );
1593
+ const finalFd = openChildRead(finalRef, VERIFIED_READ_FLAGS, before);
1280
1594
  try {
1281
1595
  const opened = fs.fstatSync(finalFd, { bigint: true });
1282
1596
  if (!opened.isFile() || stableFileIdentity(opened) !== expectedTemp.identity) {
1283
1597
  throw new Error('Generated-plan destination changed while its no-follow descriptor opened');
1284
1598
  }
1285
- testHooks?.afterFinalOpen?.({ fd: finalFd, finalPath });
1599
+ testHooks?.afterFinalOpen?.({ fd: finalFd, finalPath: finalRef.path });
1286
1600
  const committedViaTemp = hashOpenFile(tempFd, 'generated-plan committed file');
1287
1601
  const committedViaPath = hashOpenFile(finalFd, 'generated-plan destination descriptor');
1288
- const after = fs.lstatSync(finalPath, { bigint: true });
1602
+ const after = lstatChild(finalRef);
1289
1603
  const openedAfter = fs.fstatSync(finalFd, { bigint: true });
1290
1604
  if (
1291
1605
  after.isSymbolicLink() ||
@@ -1320,22 +1634,19 @@ function copyOpenFile(sourceFd, destinationFd, label) {
1320
1634
  return after;
1321
1635
  }
1322
1636
 
1323
- function openVerifiedPathFile(absolute, label) {
1324
- const before = fs.lstatSync(absolute, { bigint: true });
1637
+ function openVerifiedAnchoredFile(ref, label, knownStat) {
1638
+ const before = knownStat ?? lstatChild(ref);
1325
1639
  if (before.isSymbolicLink() || !before.isFile()) {
1326
1640
  throw new Error(`${label} is not a regular no-follow file`);
1327
1641
  }
1328
- const fd = fs.openSync(
1329
- absolute,
1330
- fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
1331
- );
1642
+ const fd = openChildRead(ref, VERIFIED_READ_FLAGS, before);
1332
1643
  try {
1333
1644
  const opened = fs.fstatSync(fd, { bigint: true });
1334
1645
  if (!opened.isFile() || stableFileIdentity(opened) !== stableFileIdentity(before)) {
1335
1646
  throw new Error(`${label} changed while its descriptor opened`);
1336
1647
  }
1337
1648
  const layer = hashOpenFile(fd, label);
1338
- const after = fs.lstatSync(absolute, { bigint: true });
1649
+ const after = lstatChild(ref);
1339
1650
  if (after.isSymbolicLink() || !after.isFile() || stableFileIdentity(after) !== layer.identity) {
1340
1651
  throw new Error(`${label} changed after verification`);
1341
1652
  }
@@ -1358,10 +1669,10 @@ export function readPlanSafely({ repo: repoInput, generatedPlanPath, testHooks }
1358
1669
  let fd;
1359
1670
  try {
1360
1671
  validatePlanParent(parentHandle);
1361
- const finalPath = descriptorPath(parentHandle.fd, finalName);
1672
+ const finalRef = anchoredChild(parentHandle, finalName);
1362
1673
  let before;
1363
1674
  try {
1364
- before = fs.lstatSync(finalPath, { bigint: true });
1675
+ before = lstatChild(finalRef);
1365
1676
  } catch (error) {
1366
1677
  if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
1367
1678
  throw new Error(`Loaded plan does not exist: ${generatedPlan}`);
@@ -1371,15 +1682,12 @@ export function readPlanSafely({ repo: repoInput, generatedPlanPath, testHooks }
1371
1682
  if (before.isSymbolicLink() || !before.isFile()) {
1372
1683
  throw new Error('Loaded plan must be a regular file, never a symlink');
1373
1684
  }
1374
- fd = fs.openSync(
1375
- finalPath,
1376
- fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
1377
- );
1685
+ fd = openChildRead(finalRef, VERIFIED_READ_FLAGS, before);
1378
1686
  const opened = fs.fstatSync(fd, { bigint: true });
1379
1687
  if (!opened.isFile() || statIdentity(opened) !== statIdentity(before)) {
1380
1688
  throw new Error('Loaded plan changed while its no-follow descriptor opened');
1381
1689
  }
1382
- testHooks?.afterPlanOpen?.({ fd, finalPath });
1690
+ testHooks?.afterPlanOpen?.({ fd, finalPath: finalRef.path });
1383
1691
  const chunks = [];
1384
1692
  let total = 0;
1385
1693
  const buffer = Buffer.allocUnsafe(64 * 1024);
@@ -1394,7 +1702,7 @@ export function readPlanSafely({ repo: repoInput, generatedPlanPath, testHooks }
1394
1702
  decodeUtf8(contents, 'loaded plan');
1395
1703
  const after = fs.fstatSync(fd, { bigint: true });
1396
1704
  assertStableIdentity(opened, after, 'loaded plan');
1397
- const pathAfter = fs.lstatSync(finalPath, { bigint: true });
1705
+ const pathAfter = lstatChild(finalRef);
1398
1706
  if (
1399
1707
  pathAfter.isSymbolicLink() ||
1400
1708
  !pathAfter.isFile() ||
@@ -1419,24 +1727,22 @@ function artifactGitPath(name) {
1419
1727
  return `gitnexus-plan-backups/${name}`;
1420
1728
  }
1421
1729
 
1422
- function verifyVaultArtifactFromFreshRoot(repo, gitPath, expectedLayer) {
1423
- const components = gitPath.split('/');
1424
- if (components.length !== 2 || components[0] !== 'gitnexus-plan-backups') {
1425
- throw new Error(`Invalid Git-admin artifact path: ${gitPath}`);
1426
- }
1730
+ function verifyVaultArtifactFromFreshRoot(repo, name, expectedLayer) {
1427
1731
  const freshVault = openBackupVault(repo, { createMissing: false });
1428
1732
  try {
1429
1733
  validatePlanParent(freshVault);
1430
- const opened = openVerifiedPathFile(
1431
- descriptorPath(freshVault.fd, components[1]),
1432
- `Git-admin artifact ${gitPath}`,
1734
+ const opened = openVerifiedAnchoredFile(
1735
+ anchoredChild(freshVault, name),
1736
+ `Git-admin artifact ${artifactGitPath(name)}`,
1433
1737
  );
1434
1738
  try {
1435
1739
  if (
1436
1740
  opened.layer.identity !== expectedLayer.identity ||
1437
1741
  opened.layer.digest !== expectedLayer.digest
1438
1742
  ) {
1439
- throw new Error(`Git-admin artifact changed before fresh-root verification: ${gitPath}`);
1743
+ throw new Error(
1744
+ `Git-admin artifact changed before fresh-root verification: ${artifactGitPath(name)}`,
1745
+ );
1440
1746
  }
1441
1747
  } finally {
1442
1748
  fs.closeSync(opened.fd);
@@ -1449,16 +1755,8 @@ function verifyVaultArtifactFromFreshRoot(repo, gitPath, expectedLayer) {
1449
1755
  function createVaultCopyFromFd(repo, vault, sourceFd, role) {
1450
1756
  validatePlanParent(vault);
1451
1757
  const name = `.gitnexus-plan-${role}-${process.pid}-${randomBytes(16).toString('hex')}.bak`;
1452
- const absolute = descriptorPath(vault.fd, name);
1453
- const destinationFd = fs.openSync(
1454
- absolute,
1455
- fs.constants.O_RDWR |
1456
- fs.constants.O_CREAT |
1457
- fs.constants.O_EXCL |
1458
- fs.constants.O_NOFOLLOW |
1459
- (fs.constants.O_CLOEXEC ?? 0),
1460
- 0o600,
1461
- );
1758
+ const artifact = anchoredChild(vault, name);
1759
+ const destinationFd = createChild(artifact, VERIFIED_CREATE_FLAGS, 0o600);
1462
1760
  let destination;
1463
1761
  try {
1464
1762
  const sourceStat = copyOpenFile(sourceFd, destinationFd, role);
@@ -1469,7 +1767,7 @@ function createVaultCopyFromFd(repo, vault, sourceFd, role) {
1469
1767
  if (source.size !== destination.size || source.digest !== destination.digest) {
1470
1768
  throw new Error(`${role} vault copy does not match its held source descriptor`);
1471
1769
  }
1472
- const pathStat = fs.lstatSync(absolute, { bigint: true });
1770
+ const pathStat = lstatChild(artifact);
1473
1771
  if (
1474
1772
  pathStat.isSymbolicLink() ||
1475
1773
  !pathStat.isFile() ||
@@ -1481,24 +1779,15 @@ function createVaultCopyFromFd(repo, vault, sourceFd, role) {
1481
1779
  } finally {
1482
1780
  fs.closeSync(destinationFd);
1483
1781
  }
1484
- const gitPath = artifactGitPath(name);
1485
- verifyVaultArtifactFromFreshRoot(repo, gitPath, destination);
1486
- return { role, gitPath, layer: destination };
1782
+ verifyVaultArtifactFromFreshRoot(repo, name, destination);
1783
+ return { role, gitPath: artifactGitPath(name), layer: destination };
1487
1784
  }
1488
1785
 
1489
1786
  function createVaultCopyFromBytes(repo, vault, contents, role) {
1490
1787
  validatePlanParent(vault);
1491
1788
  const name = `.gitnexus-plan-${role}-${process.pid}-${randomBytes(16).toString('hex')}.bak`;
1492
- const absolute = descriptorPath(vault.fd, name);
1493
- const fd = fs.openSync(
1494
- absolute,
1495
- fs.constants.O_RDWR |
1496
- fs.constants.O_CREAT |
1497
- fs.constants.O_EXCL |
1498
- fs.constants.O_NOFOLLOW |
1499
- (fs.constants.O_CLOEXEC ?? 0),
1500
- 0o600,
1501
- );
1789
+ const artifact = anchoredChild(vault, name);
1790
+ const fd = createChild(artifact, VERIFIED_CREATE_FLAGS, 0o600);
1502
1791
  let layer;
1503
1792
  try {
1504
1793
  writeAll(fd, contents);
@@ -1508,7 +1797,7 @@ function createVaultCopyFromBytes(repo, vault, contents, role) {
1508
1797
  if (layer.size !== BigInt(contents.length) || layer.digest !== sha256(contents)) {
1509
1798
  throw new Error(`${role} vault copy does not match the intended plan bytes`);
1510
1799
  }
1511
- const pathStat = fs.lstatSync(absolute, { bigint: true });
1800
+ const pathStat = lstatChild(artifact);
1512
1801
  if (
1513
1802
  pathStat.isSymbolicLink() ||
1514
1803
  !pathStat.isFile() ||
@@ -1520,32 +1809,31 @@ function createVaultCopyFromBytes(repo, vault, contents, role) {
1520
1809
  } finally {
1521
1810
  fs.closeSync(fd);
1522
1811
  }
1523
- const gitPath = artifactGitPath(name);
1524
- verifyVaultArtifactFromFreshRoot(repo, gitPath, layer);
1525
- return { role, gitPath, layer };
1812
+ verifyVaultArtifactFromFreshRoot(repo, name, layer);
1813
+ return { role, gitPath: artifactGitPath(name), layer };
1526
1814
  }
1527
1815
 
1528
1816
  function movePathToVault(repo, sourceHandle, sourceName, vault, role) {
1529
- const source = descriptorPath(sourceHandle.fd, sourceName);
1530
- if (!lstatOptional(source)) return null;
1817
+ const source = anchoredChild(sourceHandle, sourceName);
1818
+ if (!lstatAnchoredOptional(source)) return null;
1531
1819
  const name = `.gitnexus-plan-${role}-${process.pid}-${randomBytes(16).toString('hex')}.bak`;
1532
- const destination = descriptorPath(vault.fd, name);
1533
- const moved = atomicMoveNoReplace(
1534
- externalDescriptorPath(sourceHandle.fd, sourceName),
1535
- externalDescriptorPath(vault.fd, name),
1536
- );
1820
+ const destination = anchoredChild(vault, name);
1821
+ const moved = publishNoReplace(source, destination);
1537
1822
  if (!moved) throw new Error(`${role} preservation destination unexpectedly exists`);
1538
1823
  fs.fsyncSync(sourceHandle.fd);
1539
1824
  if (vault.fd !== sourceHandle.fd) fs.fsyncSync(vault.fd);
1540
- const sourceAfter = lstatOptional(source);
1541
- const destinationAfter = lstatOptional(destination);
1825
+ const sourceAfter = lstatAnchoredOptional(source);
1826
+ const destinationAfter = lstatAnchoredOptional(destination);
1542
1827
  if (sourceAfter || !destinationAfter) {
1543
1828
  throw new Error(`${role} could not be atomically moved into the Git-admin vault`);
1544
1829
  }
1545
- const opened = openVerifiedPathFile(destination, `${role} Git-admin artifact`);
1546
- const gitPath = artifactGitPath(name);
1547
- verifyVaultArtifactFromFreshRoot(repo, gitPath, opened.layer);
1548
- return { role, gitPath, layer: opened.layer, fd: opened.fd };
1830
+ const opened = openVerifiedAnchoredFile(
1831
+ destination,
1832
+ `${role} Git-admin artifact`,
1833
+ destinationAfter,
1834
+ );
1835
+ verifyVaultArtifactFromFreshRoot(repo, name, opened.layer);
1836
+ return { role, gitPath: artifactGitPath(name), layer: opened.layer, fd: opened.fd };
1549
1837
  }
1550
1838
 
1551
1839
  function formatPreservedArtifacts(artifacts) {
@@ -1600,10 +1888,10 @@ export function writePlanSafely({
1600
1888
  const finalName = components.pop();
1601
1889
  let parentHandle;
1602
1890
  let vaultHandle;
1603
- let tempPath;
1891
+ let tempRef;
1604
1892
  let tempName;
1605
1893
  let tempFd;
1606
- let finalPath;
1894
+ let finalRef;
1607
1895
  let expectedTemp;
1608
1896
  let originalDestination;
1609
1897
  let priorBackup;
@@ -1611,7 +1899,6 @@ export function writePlanSafely({
1611
1899
  try {
1612
1900
  parentHandle = openPlanParent(repo, components);
1613
1901
  vaultHandle = openBackupVault(repo);
1614
- resolveAtomicMover();
1615
1902
  const parentDevice = fs.fstatSync(parentHandle.fd, { bigint: true }).dev;
1616
1903
  const vaultDevice = fs.fstatSync(vaultHandle.fd, { bigint: true }).dev;
1617
1904
  if (parentDevice !== vaultDevice) {
@@ -1622,19 +1909,11 @@ export function writePlanSafely({
1622
1909
  testHooks?.afterParentOpen?.({ fd: parentHandle.fd, path: parentHandle.expectedPath });
1623
1910
  validatePlanParent(parentHandle);
1624
1911
  validatePlanParent(vaultHandle);
1625
- finalPath = descriptorPath(parentHandle.fd, finalName);
1626
- originalDestination = openExistingPlanDestination(finalPath, shouldReplace);
1912
+ finalRef = anchoredChild(parentHandle, finalName);
1913
+ originalDestination = openExistingPlanDestination(finalRef, shouldReplace);
1627
1914
  tempName = `.gitnexus-plan-${process.pid}-${randomBytes(16).toString('hex')}.tmp`;
1628
- tempPath = descriptorPath(parentHandle.fd, tempName);
1629
- tempFd = fs.openSync(
1630
- tempPath,
1631
- fs.constants.O_RDWR |
1632
- fs.constants.O_CREAT |
1633
- fs.constants.O_EXCL |
1634
- fs.constants.O_NOFOLLOW |
1635
- (fs.constants.O_CLOEXEC ?? 0),
1636
- 0o600,
1637
- );
1915
+ tempRef = anchoredChild(parentHandle, tempName);
1916
+ tempFd = createChild(tempRef, VERIFIED_CREATE_FLAGS, 0o600);
1638
1917
  writeAll(tempFd, contents);
1639
1918
  fs.fchmodSync(tempFd, 0o644);
1640
1919
  fs.fsyncSync(tempFd);
@@ -1646,12 +1925,12 @@ export function writePlanSafely({
1646
1925
  testHooks?.beforeRename?.({
1647
1926
  fd: parentHandle.fd,
1648
1927
  path: parentHandle.expectedPath,
1649
- tempPath,
1928
+ tempPath: tempRef.path,
1650
1929
  });
1651
1930
  validatePlanParent(parentHandle);
1652
1931
  validatePlanParent(vaultHandle);
1653
1932
  validateOpenPlanDestination(originalDestination);
1654
- const tempPathStat = fs.lstatSync(tempPath, { bigint: true });
1933
+ const tempPathStat = lstatChild(tempRef);
1655
1934
  const currentTemp = hashOpenFile(tempFd, 'generated-plan temporary file');
1656
1935
  if (
1657
1936
  tempPathStat.isSymbolicLink() ||
@@ -1664,7 +1943,7 @@ export function writePlanSafely({
1664
1943
  }
1665
1944
 
1666
1945
  if (shouldReplace) {
1667
- testHooks?.beforeBackupMove?.({ fd: parentHandle.fd, finalPath });
1946
+ testHooks?.beforeBackupMove?.({ fd: parentHandle.fd, finalPath: finalRef.path });
1668
1947
  const originalLayer = hashOpenFile(originalDestination.fd, 'prior generated plan');
1669
1948
  if (originalLayer.digest !== expectedDigest) {
1670
1949
  throw new Error(
@@ -1673,7 +1952,7 @@ export function writePlanSafely({
1673
1952
  }
1674
1953
  validatePlanParent(parentHandle);
1675
1954
  validateOpenPlanDestination(originalDestination);
1676
- inspectPlanDestination(finalPath, {
1955
+ inspectPlanDestination(finalRef, {
1677
1956
  replace: true,
1678
1957
  expectedIdentity: originalDestination.identity,
1679
1958
  });
@@ -1691,20 +1970,20 @@ export function writePlanSafely({
1691
1970
  );
1692
1971
  throw new Error('Destination raced while the prior plan was moved into preservation');
1693
1972
  }
1694
- if (lstatOptional(finalPath)) {
1973
+ if (lstatAnchoredOptional(finalRef)) {
1695
1974
  throw new Error('Destination reappeared after the prior plan was preserved');
1696
1975
  }
1697
1976
  }
1698
1977
 
1699
1978
  testHooks?.beforePublication?.({
1700
1979
  fd: parentHandle.fd,
1701
- finalPath,
1702
- tempPath,
1980
+ finalPath: finalRef.path,
1981
+ tempPath: tempRef.path,
1703
1982
  replace: shouldReplace,
1704
1983
  });
1705
1984
  validatePlanParent(parentHandle);
1706
1985
  validatePlanParent(vaultHandle);
1707
- const finalTempPathStat = fs.lstatSync(tempPath, { bigint: true });
1986
+ const finalTempPathStat = lstatChild(tempRef);
1708
1987
  const finalTemp = hashOpenFile(tempFd, 'generated-plan temporary file');
1709
1988
  if (
1710
1989
  finalTempPathStat.isSymbolicLink() ||
@@ -1715,19 +1994,25 @@ export function writePlanSafely({
1715
1994
  ) {
1716
1995
  throw new Error('Generated-plan temporary path or content changed at publication');
1717
1996
  }
1718
- atomicMoveNoReplace(
1719
- externalDescriptorPath(parentHandle.fd, tempName),
1720
- externalDescriptorPath(parentHandle.fd, finalName),
1721
- );
1722
- if (lstatOptional(tempPath) || !lstatOptional(finalPath)) {
1997
+ // link() reports the race itself; re-deriving that verdict from a later pair
1998
+ // of stats would be both slower and weaker.
1999
+ if (!publishNoReplace(tempRef, finalRef)) {
1723
2000
  throw new Error('Generated-plan publication was refused because the destination raced');
1724
2001
  }
2002
+ // link() creates a directory entry, so it needs the parent fsync that rename
2003
+ // needed: the file's own bytes were fsynced through tempFd before this point,
2004
+ // and this makes the name that now reaches them durable too. Skipping it is
2005
+ // the step write-file-atomic omits and maildir, git and atomicwrites all
2006
+ // mandate.
2007
+ //
2008
+ // Honest limitation: on macOS fsync is not a write barrier — the durable
2009
+ // primitive there is fcntl(F_FULLFSYNC), which Node does not expose. A
2010
+ // macOS plan write is therefore as durable as fsync makes it and no more.
1725
2011
  fs.fsyncSync(parentHandle.fd);
1726
- testHooks?.afterPublication?.({ fd: parentHandle.fd, finalPath });
1727
- testHooks?.afterRename?.({ fd: parentHandle.fd, finalPath });
2012
+ testHooks?.afterPublication?.({ fd: parentHandle.fd, finalPath: finalRef.path });
1728
2013
  validatePlanParent(parentHandle);
1729
2014
  validatePlanParent(vaultHandle);
1730
- validateCommittedPlan(finalPath, tempFd, expectedTemp, testHooks);
2015
+ validateCommittedPlan(finalRef, tempFd, expectedTemp, testHooks);
1731
2016
  const receipt = { generated_plan_path: generatedPlan, bytes_written: contents.length };
1732
2017
  if (priorBackup) receipt.prior_plan_backup_git_path = priorBackup.gitPath;
1733
2018
  return receipt;
@@ -1848,6 +2133,11 @@ export function snapshotEvidence({
1848
2133
  const headGuards = captureHeadGuards(repo);
1849
2134
  const dirty = initialDirty.records;
1850
2135
  const mutationGuards = [];
2136
+ // Per-snapshot walk state: `absenceCache` owns every descriptor an absence
2137
+ // anchor holds, deduplicated by repo-relative prefix and closed exactly once
2138
+ // below; `guardedDirectories` keeps parent guarding to one stat per directory.
2139
+ const absenceCache = new Map();
2140
+ const walkState = { absenceCache, guardedDirectories: new Set() };
1851
2141
 
1852
2142
  try {
1853
2143
  testHooks?.afterAnchorCapture?.({ headCommit: head });
@@ -1862,7 +2152,9 @@ export function snapshotEvidence({
1862
2152
  testHooks?.afterGitLayerLoad?.({ headCommit: head });
1863
2153
  const globalEntries = [...dirty.values()]
1864
2154
  .filter((record) => record.path !== generatedPlan)
1865
- .map((record) => materializeRecord(repo, record, layers, mutationGuards, testHooks));
2155
+ .map((record) =>
2156
+ materializeRecord(repo, record, layers, mutationGuards, testHooks, walkState),
2157
+ );
1866
2158
  const citedEntries = [...normalizedCitations].sort(compareUtf8).map((repoPath) => {
1867
2159
  const status = dirty.get(repoPath) ?? {
1868
2160
  path: repoPath,
@@ -1871,7 +2163,7 @@ export function snapshotEvidence({
1871
2163
  rename_to: null,
1872
2164
  has_untracked: false,
1873
2165
  };
1874
- const entry = materializeRecord(repo, status, layers, mutationGuards, testHooks);
2166
+ const entry = materializeRecord(repo, status, layers, mutationGuards, testHooks, walkState);
1875
2167
  const present = Object.values(entry.object_kind).some((kind) => kind !== ABSENT);
1876
2168
  if (!present) entry.state = ABSENT;
1877
2169
  else if (entry.state === 'clean' && entry.object_kind.untracked !== ABSENT) {
@@ -1906,21 +2198,13 @@ export function snapshotEvidence({
1906
2198
  throw new Error(`${guard.absolute} changed before evidence materialization completed`);
1907
2199
  }
1908
2200
  } else if (guard.type === 'absence') {
2201
+ // statIdentity is a strict superset of stableDirectoryIdentity on the
2202
+ // same stat, so comparing both could only ever fire together.
1909
2203
  const parent = fs.fstatSync(guard.fd, { bigint: true });
1910
- if (
1911
- !parent.isDirectory() ||
1912
- stableDirectoryIdentity(parent) !== guard.parentIdentity ||
1913
- statIdentity(parent) !== guard.parentMutationIdentity
1914
- ) {
2204
+ if (!parent.isDirectory() || statIdentity(parent) !== guard.parentMutationIdentity) {
1915
2205
  throw new Error(`Absence anchor changed for ${guard.repoPath}`);
1916
2206
  }
1917
- try {
1918
- fs.lstatSync(descriptorPath(guard.fd, guard.childName), { bigint: true });
1919
- } catch (error) {
1920
- if (error?.code === 'ENOENT') continue;
1921
- throw error;
1922
- }
1923
- throw new Error(`${guard.repoPath} appeared before evidence materialization completed`);
2207
+ anchoringBackend().verifyAbsentChild(guard);
1924
2208
  }
1925
2209
  }
1926
2210
  for (const guard of headGuards) verifyControlFile(guard);
@@ -1955,12 +2239,10 @@ export function snapshotEvidence({
1955
2239
  cited_path_manifest: citedEntries,
1956
2240
  };
1957
2241
  } finally {
1958
- const closed = new Set();
1959
- for (const guard of mutationGuards) {
1960
- if (guard.type !== 'absence' || closed.has(guard.fd)) continue;
1961
- closed.add(guard.fd);
2242
+ // One entry per distinct anchored directory, so one close per descriptor.
2243
+ for (const handle of absenceCache.values()) {
1962
2244
  try {
1963
- fs.closeSync(guard.fd);
2245
+ fs.closeSync(handle.fd);
1964
2246
  } catch {
1965
2247
  // Preserve the primary snapshot result/error.
1966
2248
  }