@harperfast/harper 5.3.0-beta.4 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/bin/backup.ts +6 -1
  2. package/components/Application.ts +486 -115
  3. package/components/DESIGN.md +109 -21
  4. package/components/deploymentProvenance.ts +28 -0
  5. package/components/operations.js +14 -7
  6. package/components/packageComponent.ts +5 -1
  7. package/config-root.schema.json +1 -1
  8. package/dataLayer/DESIGN.md +35 -7
  9. package/dataLayer/blobBackup.ts +71 -30
  10. package/dataLayer/harperBridge/ResourceBridge.ts +16 -2
  11. package/dataLayer/rocksdbBackup.ts +88 -34
  12. package/dataLayer/schemaDescribe.ts +45 -1
  13. package/dist/bin/backup.js +1 -1
  14. package/dist/bin/backup.js.map +1 -1
  15. package/dist/components/Application.d.ts +11 -21
  16. package/dist/components/Application.js +436 -105
  17. package/dist/components/Application.js.map +1 -1
  18. package/dist/components/deploymentProvenance.d.ts +4 -0
  19. package/dist/components/deploymentProvenance.js +32 -0
  20. package/dist/components/deploymentProvenance.js.map +1 -0
  21. package/dist/components/operations.js +14 -7
  22. package/dist/components/operations.js.map +1 -1
  23. package/dist/components/packageComponent.js +6 -1
  24. package/dist/components/packageComponent.js.map +1 -1
  25. package/dist/dataLayer/blobBackup.d.ts +30 -3
  26. package/dist/dataLayer/blobBackup.js +73 -36
  27. package/dist/dataLayer/blobBackup.js.map +1 -1
  28. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +1 -0
  29. package/dist/dataLayer/harperBridge/ResourceBridge.js +15 -3
  30. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  31. package/dist/dataLayer/rocksdbBackup.d.ts +4 -1
  32. package/dist/dataLayer/rocksdbBackup.js +74 -36
  33. package/dist/dataLayer/rocksdbBackup.js.map +1 -1
  34. package/dist/dataLayer/schemaDescribe.js +45 -1
  35. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  36. package/dist/resources/DatabaseTransaction.d.ts +1 -0
  37. package/dist/resources/DatabaseTransaction.js +33 -8
  38. package/dist/resources/DatabaseTransaction.js.map +1 -1
  39. package/dist/resources/ResourceInterface.d.ts +7 -3
  40. package/dist/resources/ResourceInterface.js +2 -0
  41. package/dist/resources/ResourceInterface.js.map +1 -1
  42. package/dist/resources/Table.d.ts +151 -445
  43. package/dist/resources/Table.js +209 -8
  44. package/dist/resources/Table.js.map +1 -1
  45. package/dist/resources/analytics/write.d.ts +11 -0
  46. package/dist/resources/analytics/write.js +3 -0
  47. package/dist/resources/analytics/write.js.map +1 -1
  48. package/dist/resources/blob.d.ts +30 -0
  49. package/dist/resources/blob.js +189 -1
  50. package/dist/resources/blob.js.map +1 -1
  51. package/dist/resources/databases.d.ts +1 -0
  52. package/dist/resources/databases.js +80 -15
  53. package/dist/resources/databases.js.map +1 -1
  54. package/dist/resources/derivedIndexRuntime.d.ts +13 -0
  55. package/dist/resources/derivedIndexRuntime.js +57 -5
  56. package/dist/resources/derivedIndexRuntime.js.map +1 -1
  57. package/dist/resources/derivedIndexes.d.ts +1 -0
  58. package/dist/resources/derivedIndexes.js +116 -15
  59. package/dist/resources/derivedIndexes.js.map +1 -1
  60. package/dist/resources/fullTextSchema.d.ts +5 -3
  61. package/dist/resources/fullTextSchema.js +48 -5
  62. package/dist/resources/fullTextSchema.js.map +1 -1
  63. package/dist/resources/fullTextSchemaLifecycle.d.ts +2 -0
  64. package/dist/resources/fullTextSchemaLifecycle.js +33 -0
  65. package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
  66. package/dist/resources/graphql.js +30 -12
  67. package/dist/resources/graphql.js.map +1 -1
  68. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +21 -1
  69. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +114 -5
  70. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  71. package/dist/resources/indexes/fullTextDerivedIndex.d.ts +11 -2
  72. package/dist/resources/indexes/fullTextDerivedIndex.js +187 -24
  73. package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -1
  74. package/dist/resources/indexes/fullTextNativeBinding.d.ts +99 -1
  75. package/dist/resources/indexes/fullTextNativeBinding.js +35 -1
  76. package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
  77. package/dist/resources/indexes/fullTextQueryIndex.d.ts +58 -0
  78. package/dist/resources/indexes/fullTextQueryIndex.js +943 -0
  79. package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -0
  80. package/dist/resources/indexes/fullTextQueryProtocol.d.ts +6 -0
  81. package/dist/resources/indexes/fullTextQueryProtocol.js +32 -0
  82. package/dist/resources/indexes/fullTextQueryProtocol.js.map +1 -0
  83. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +4 -1
  84. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +30 -7
  85. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
  86. package/dist/resources/search.d.ts +41 -0
  87. package/dist/resources/search.js +468 -26
  88. package/dist/resources/search.js.map +1 -1
  89. package/dist/resources/transactionBroadcast.js +101 -50
  90. package/dist/resources/transactionBroadcast.js.map +1 -1
  91. package/dist/resources/typeParity.d.ts +9 -0
  92. package/dist/resources/typeParity.js +3 -0
  93. package/dist/resources/typeParity.js.map +1 -0
  94. package/dist/server/REST.js.map +1 -1
  95. package/dist/server/itc/serverHandlers.js +48 -8
  96. package/dist/server/itc/serverHandlers.js.map +1 -1
  97. package/dist/server/serverHelpers/contentTypes.d.ts +7 -0
  98. package/dist/server/serverHelpers/contentTypes.js +36 -23
  99. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  100. package/dist/server/serverHelpers/uwsServer.js +28 -5
  101. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  102. package/dist/server/threads/itc.js +19 -0
  103. package/dist/server/threads/itc.js.map +1 -1
  104. package/dist/server/threads/manageThreads.d.ts +3 -0
  105. package/dist/server/threads/manageThreads.js +90 -23
  106. package/dist/server/threads/manageThreads.js.map +1 -1
  107. package/dist/utility/operation_authorization.js +25 -7
  108. package/dist/utility/operation_authorization.js.map +1 -1
  109. package/dist/utility/signalling.js +3 -0
  110. package/dist/utility/signalling.js.map +1 -1
  111. package/dist/validation/searchValidator.js +22 -2
  112. package/dist/validation/searchValidator.js.map +1 -1
  113. package/npm-shrinkwrap.json +22 -22
  114. package/package.json +2 -4
  115. package/resources/DESIGN.md +19 -2
  116. package/resources/DatabaseTransaction.ts +40 -10
  117. package/resources/FULL_TEXT.md +97 -0
  118. package/resources/ResourceInterface.ts +8 -2
  119. package/resources/Table.ts +767 -17
  120. package/resources/analytics/write.ts +3 -3
  121. package/resources/blob.ts +183 -1
  122. package/resources/databases.ts +126 -14
  123. package/resources/derivedIndexRuntime.ts +79 -6
  124. package/resources/derivedIndexes.ts +142 -18
  125. package/resources/fullTextSchema.ts +55 -9
  126. package/resources/fullTextSchemaLifecycle.ts +45 -0
  127. package/resources/graphql.ts +33 -12
  128. package/resources/indexes/DESIGN.md +167 -15
  129. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +125 -5
  130. package/resources/indexes/fullTextDerivedIndex.ts +216 -32
  131. package/resources/indexes/fullTextNativeBinding.ts +133 -3
  132. package/resources/indexes/fullTextQueryIndex.ts +1120 -0
  133. package/resources/indexes/fullTextQueryProtocol.ts +35 -0
  134. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +28 -8
  135. package/resources/search.ts +513 -26
  136. package/resources/transactionBroadcast.ts +86 -47
  137. package/resources/typeParity.ts +20 -0
  138. package/schema.graphql +18 -5
  139. package/server/DESIGN.md +18 -0
  140. package/server/REST.ts +1 -1
  141. package/server/itc/serverHandlers.js +61 -8
  142. package/server/serverHelpers/contentTypes.ts +31 -26
  143. package/server/serverHelpers/uwsServer.ts +35 -7
  144. package/server/threads/itc.js +21 -0
  145. package/server/threads/manageThreads.js +95 -21
  146. package/studio/web/assets/{Chat-Bq60AO-g.js → Chat-CNEKMllJ.js} +13 -13
  147. package/studio/web/assets/FloatingChat-CEnWhHeR.js +23 -0
  148. package/studio/web/assets/SearchDialog-CwhJdEdY.js +1 -0
  149. package/studio/web/assets/{abnfDiagram-O67JEVCF-DpPZWMOf.js → abnfDiagram-O67JEVCF-raXE027J.js} +1 -1
  150. package/studio/web/assets/alertDialog-BbWD9Xxa.js +1 -0
  151. package/studio/web/assets/apiToken-DC5vsaaO.js +1 -0
  152. package/studio/web/assets/applications-DQZxDy0x.js +296 -0
  153. package/studio/web/assets/architecture-7GRP2DOG-DDmz3hpI.js +1 -0
  154. package/studio/web/assets/{architectureDiagram-NJMV4G6O-OmgLraID.js → architectureDiagram-NJMV4G6O-C8_Eet7w.js} +1 -1
  155. package/studio/web/assets/arrow-up-D2g_57vT.js +1 -0
  156. package/studio/web/assets/authStore-CJCaADw9.js +3 -0
  157. package/studio/web/assets/{blockDiagram-BEXU5L5S-Cv7CD3Ai.js → blockDiagram-BEXU5L5S-DR6S5-1D.js} +1 -1
  158. package/studio/web/assets/{button-MFP9n8Ck.js → button-DWwfgGWI.js} +2 -2
  159. package/studio/web/assets/{c4Diagram-YGBWAQC7-Ds1vKYnN.js → c4Diagram-YGBWAQC7-BAfZ1G01.js} +1 -1
  160. package/studio/web/assets/channel-D0AkPjJ8.js +1 -0
  161. package/studio/web/assets/{chevron-up-CFe8x5Bp.js → chevron-up-CFxbMGv2.js} +1 -1
  162. package/studio/web/assets/{chunk-3FUC2YCW-DpnBPH8B.js → chunk-3FUC2YCW-DuRp-j19.js} +1 -1
  163. package/studio/web/assets/{chunk-5DYCD2WN-LLLCQSzg.js → chunk-5DYCD2WN-BceBfIHT.js} +1 -1
  164. package/studio/web/assets/{chunk-742MDFTN-BL8J_pIO.js → chunk-742MDFTN-CIkt8ijt.js} +1 -1
  165. package/studio/web/assets/{chunk-7INBJB4K-C-hJHiiK.js → chunk-7INBJB4K-BpeAXyOi.js} +1 -1
  166. package/studio/web/assets/{chunk-7M6MHVWA-C6ZqkFbA.js → chunk-7M6MHVWA-h6WlaTzO.js} +1 -1
  167. package/studio/web/assets/{chunk-7PRAP22T-CUXM8BG6.js → chunk-7PRAP22T-KyqX6x3f.js} +1 -1
  168. package/studio/web/assets/{chunk-GTNCS2PH-DJ2BFvS1.js → chunk-GTNCS2PH-DGHLXyow.js} +1 -1
  169. package/studio/web/assets/{chunk-GWA4HPMP-C42bSqLL.js → chunk-GWA4HPMP-DsejB6gA.js} +1 -1
  170. package/studio/web/assets/{chunk-MBY4JIJT-C-5jz6Lo.js → chunk-MBY4JIJT-BwSGweQP.js} +1 -1
  171. package/studio/web/assets/{chunk-NETBCI7D-D-bzlN4r.js → chunk-NETBCI7D-C8UnOw_f.js} +1 -1
  172. package/studio/web/assets/{chunk-O7XYJQB3-ep14zA23.js → chunk-O7XYJQB3-CBS3nZYV.js} +1 -1
  173. package/studio/web/assets/{chunk-UA2S7LBM-BMRWRHED.js → chunk-UA2S7LBM-BHpEdH8D.js} +1 -1
  174. package/studio/web/assets/{chunk-WEXAMYUT-DdLYVdX1.js → chunk-WEXAMYUT-BAIhTwyh.js} +1 -1
  175. package/studio/web/assets/{chunk-Z7XXMR3K-BmKEDjcP.js → chunk-Z7XXMR3K-DSHbImdf.js} +1 -1
  176. package/studio/web/assets/{chunk-ZIGJFQKS-DLZgdNxA.js → chunk-ZIGJFQKS-Cs8fUn7y.js} +1 -1
  177. package/studio/web/assets/{classDiagram-v2-NBCMYWYE-DOI5vgKT.js → classDiagram-v2-NBCMYWYE-DmDPkt2u.js} +1 -1
  178. package/studio/web/assets/cn-6T8VranJ.js +1 -0
  179. package/studio/web/assets/{codicon-Brq4_Ui5.ttf → codicon-DF1abBS2.ttf} +0 -0
  180. package/studio/web/assets/{createLucideIcon-bziYxJli.js → createLucideIcon-fO5dL7D5.js} +1 -1
  181. package/studio/web/assets/{css.worker-l1psR_wG.js → css.worker-DoT-OOzx.js} +36 -36
  182. package/studio/web/assets/{cssMode-DJyc9mHj.js → cssMode-Dz2DxF4e.js} +1 -1
  183. package/studio/web/assets/{cynefin-OW5HDTMX-BSW-DB7S.js → cynefin-OW5HDTMX-DIvbXLSj.js} +1 -1
  184. package/studio/web/assets/{cynefinDiagram-VND7K2PF-CbAHEwxt.js → cynefinDiagram-VND7K2PF-BdtaXiCE.js} +1 -1
  185. package/studio/web/assets/{dagre-6A5THRUB-Csxewgs4.js → dagre-6A5THRUB-C7RUWHyz.js} +1 -1
  186. package/studio/web/assets/{diagram-22UHCM2B-a0rt8tY4.js → diagram-22UHCM2B-DJCvcaWP.js} +1 -1
  187. package/studio/web/assets/{diagram-3UASUU5V-lEj2SVNh.js → diagram-3UASUU5V-Du6VKWln.js} +1 -1
  188. package/studio/web/assets/{diagram-ATOU4E4O-HcrAeB-q.js → diagram-ATOU4E4O-dxRfeG4B.js} +1 -1
  189. package/studio/web/assets/{diagram-CDSNMT55-CYxiuyEx.js → diagram-CDSNMT55-l5Ga0u-W.js} +1 -1
  190. package/studio/web/assets/{diagram-MLGK6HIB-ChbCJTgJ.js → diagram-MLGK6HIB-CNQY47LA.js} +1 -1
  191. package/studio/web/assets/{diagram-MPIPVDR6-DaX9Y-N3.js → diagram-MPIPVDR6-7prveD1B.js} +1 -1
  192. package/studio/web/assets/{dist-Cz2Y6-6G.js → dist-BrRuMUzt.js} +1 -1
  193. package/studio/web/assets/{download-CtostjYn.js → download-Cn6tPG4L.js} +1 -1
  194. package/studio/web/assets/{ebnfDiagram-ZINNZB2B-CwAsENB1.js → ebnfDiagram-ZINNZB2B-5OnkTpfi.js} +1 -1
  195. package/studio/web/assets/{editor-DDIsQfj6.js → editor-B3mjd3tz.js} +1 -1
  196. package/studio/web/assets/editor.worker-Yd1NR0QT.js +33 -0
  197. package/studio/web/assets/{elk-276RUBZZ-Hexg9E4D.js → elk-276RUBZZ-DxDQPjQT.js} +1 -1
  198. package/studio/web/assets/{erDiagram-OPXOYQCR-CeC-wwO1.js → erDiagram-OPXOYQCR-CQlCX84D.js} +1 -1
  199. package/studio/web/assets/eventmodeling-NTZA5JFV-DONQf5aG.js +1 -0
  200. package/studio/web/assets/flowDiagram-KWPJA3E3-BoZ8_QR1.js +1 -0
  201. package/studio/web/assets/{ganttDiagram-FUAMR5RP-C2_Hhl1-.js → ganttDiagram-FUAMR5RP-bloX3Voy.js} +1 -1
  202. package/studio/web/assets/{getAnalytics-CjthMQqF.js → getAnalytics-C0FHjtdQ.js} +1 -1
  203. package/studio/web/assets/{gitGraph-4MIJSDKK-xIk5Vuew.js → gitGraph-4MIJSDKK-BrXWmJt2.js} +1 -1
  204. package/studio/web/assets/{gitGraphDiagram-X574FWY7-CMxJ34up.js → gitGraphDiagram-X574FWY7-C2KnnZeY.js} +1 -1
  205. package/studio/web/assets/{html-g1BJHxLy.js → html-BsRGM7tp.js} +1 -1
  206. package/studio/web/assets/{html.worker-DdwM4IrA.js → html.worker-B1dwY0Ay.js} +27 -27
  207. package/studio/web/assets/{htmlMode-Vh5IglCQ.js → htmlMode-tUKJaA3o.js} +1 -1
  208. package/studio/web/assets/index-72cEH474.css +1 -0
  209. package/studio/web/assets/index-BuqFjN5K.js +828 -0
  210. package/studio/web/assets/index.lazy-CcpfQFY_.js +2 -0
  211. package/studio/web/assets/{info-A6RAGUB7-o49TEbj-.js → info-A6RAGUB7-BhzBHe4U.js} +1 -1
  212. package/studio/web/assets/{infoDiagram-VRGFBTTK-Cjhh7yzZ.js → infoDiagram-VRGFBTTK-B0J1132p.js} +1 -1
  213. package/studio/web/assets/{ishikawaDiagram-OU5B5YK6-C63aXRmh.js → ishikawaDiagram-OU5B5YK6-BNk0tuQx.js} +1 -1
  214. package/studio/web/assets/{javascript-5VIolqf7.js → javascript-BZ_uRGHl.js} +1 -1
  215. package/studio/web/assets/{journeyDiagram-ZHPQQLJL-D_o0P5Mu.js → journeyDiagram-ZHPQQLJL-Be79RMQM.js} +1 -1
  216. package/studio/web/assets/json.worker-BQqXGI3X.js +65 -0
  217. package/studio/web/assets/{jsonMode-DG4zQraI.js → jsonMode-DgB-aFMj.js} +1 -1
  218. package/studio/web/assets/{kanban-definition-PNTS6WVX-CTI9SSxY.js → kanban-definition-PNTS6WVX-4uuoQP9W.js} +1 -1
  219. package/studio/web/assets/{languageServices-t5FBpX-i.js → languageServices-BleuBw17.js} +1 -1
  220. package/studio/web/assets/localStorageKeys-etzFY_xV.js +1 -0
  221. package/studio/web/assets/{lspLanguageFeatures-DgVnNPh-.js → lspLanguageFeatures-ByVYLSsQ.js} +1 -1
  222. package/studio/web/assets/{mermaid-parser.core-C2lcH_jP.js → mermaid-parser.core-V1UYMeXV.js} +3 -3
  223. package/studio/web/assets/{mermaid.core-BG5bjfIG.js → mermaid.core-1PX6R49U.js} +4 -4
  224. package/studio/web/assets/{mindmap-definition-NLK3R4M7-BK7ho6JD.js → mindmap-definition-NLK3R4M7-C70Ctm0J.js} +1 -1
  225. package/studio/web/assets/notifications-D1Nf-Edn.js +1 -0
  226. package/studio/web/assets/notifications-TuzN0IMi.js +1 -0
  227. package/studio/web/assets/{packet-AYTQ26CC-BHPy6KBV.js → packet-AYTQ26CC-DWbXHx5g.js} +1 -1
  228. package/studio/web/assets/{pegDiagram-GJSIUBJH-DzjwG3jO.js → pegDiagram-GJSIUBJH-d6z1cc7W.js} +1 -1
  229. package/studio/web/assets/{pie-WAS4IAKB-D0cP-DTU.js → pie-WAS4IAKB-DjoTm3yr.js} +1 -1
  230. package/studio/web/assets/{pieDiagram-5QR66LMP-C_hOP9Lv.js → pieDiagram-5QR66LMP-Dz3RHpDA.js} +1 -1
  231. package/studio/web/assets/profile-D7C7Lupz.js +1 -0
  232. package/studio/web/assets/{quadrantDiagram-O4NWA36T-CZW8PbV1.js → quadrantDiagram-O4NWA36T-CIF5Qkou.js} +1 -1
  233. package/studio/web/assets/{radar-RG4KPBEZ-C8fk0U81.js → radar-RG4KPBEZ-CVBgjCOj.js} +1 -1
  234. package/studio/web/assets/{railroad-74A4TZTK-B60FWm17.js → railroad-74A4TZTK-BdhXw10Q.js} +1 -1
  235. package/studio/web/assets/railroad-abnf-HS5TGJTU-BcoQPN39.js +1 -0
  236. package/studio/web/assets/railroad-ebnf-LZEXJU2U-B1Lke2JN.js +1 -0
  237. package/studio/web/assets/railroad-peg-WCYAUIDC-D-oJFkiV.js +1 -0
  238. package/studio/web/assets/{railroadDiagram-XR7U4H2S-DyyNmgbU.js → railroadDiagram-XR7U4H2S-y3Jv2U6N.js} +1 -1
  239. package/studio/web/assets/regions-nGQCm-v8.js +1 -0
  240. package/studio/web/assets/register-C3x9Ze6P.js +909 -0
  241. package/studio/web/assets/register-DjpsE8to.css +1 -0
  242. package/studio/web/assets/{requirementDiagram-PLB6GJNP-C4H-mLKu.js → requirementDiagram-PLB6GJNP-ChINZEmu.js} +1 -1
  243. package/studio/web/assets/restartGate-nHAFLDh-.js +1 -0
  244. package/studio/web/assets/{sankeyDiagram-IPEJSGJF-DHDyga4c.js → sankeyDiagram-IPEJSGJF-D9AyLce2.js} +1 -1
  245. package/studio/web/assets/{sequenceDiagram-PO4LG4MO-CdNcHXRI.js → sequenceDiagram-PO4LG4MO-hfk4aIEm.js} +1 -1
  246. package/studio/web/assets/{setComponentFile-z4-qJCbA.js → setComponentFile-DpjrRqeG.js} +1 -1
  247. package/studio/web/assets/{setup-CS50uJ4x.js → setup-CGiKLZ9e.js} +2 -2
  248. package/studio/web/assets/{stateDiagram-v2-GCMORJYK-CpKuXq15.js → stateDiagram-v2-GCMORJYK-Bs8RIb9K.js} +1 -1
  249. package/studio/web/assets/status-CGJwrNPH.js +61 -0
  250. package/studio/web/assets/{swimlanes-2SLR337P-NRHa2D8I.js → swimlanes-2SLR337P-B52xrf6t.js} +1 -1
  251. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-DfN7BAP5.js +8 -0
  252. package/studio/web/assets/tabs-BCTQzoQK.js +1 -0
  253. package/studio/web/assets/{timeline-definition-EJHVYXUP-D39Y-xpq.js → timeline-definition-EJHVYXUP-DJSITpQX.js} +1 -1
  254. package/studio/web/assets/toggleHighContrast-C-Ne6P7T.js +51 -0
  255. package/studio/web/assets/toggleHighContrast-n7clI0si.css +1 -0
  256. package/studio/web/assets/{treeView-Q6P3EWNA-CRWR9OqD.js → treeView-Q6P3EWNA-C-epdxRo.js} +1 -1
  257. package/studio/web/assets/{treemap-WGGIJYW6-ZfXyJgLO.js → treemap-WGGIJYW6-DOVyp_zw.js} +1 -1
  258. package/studio/web/assets/{ts.worker-DDMUrpqh.js → ts.worker-Clm12uY6.js} +131 -131
  259. package/studio/web/assets/{tsMode-D83Gg3cq.js → tsMode-CSreYTys.js} +1 -1
  260. package/studio/web/assets/{typescript-Dfp-GrNq.js → typescript-MnxBk89W.js} +1 -1
  261. package/studio/web/assets/{useEntityRestURL-BKetlINf.js → useEntityRestURL-TNrh77-s.js} +1 -1
  262. package/studio/web/assets/{usecaseDiagram-POWQR4AR-jf-zIDZ_.js → usecaseDiagram-POWQR4AR-B6252cbq.js} +1 -1
  263. package/studio/web/assets/{vendor-core-BdkdbUek.js → vendor-core-6LhfJlRC.js} +1 -1
  264. package/studio/web/assets/vendor-datadog-ozKf-pts.js +6 -0
  265. package/studio/web/assets/{vendor-react-izWchTRL.js → vendor-react-BOliS0BL.js} +1 -1
  266. package/studio/web/assets/vendor-tanstack-Bntfka-M.js +2 -0
  267. package/studio/web/assets/vendor-ui-9EYYDgoN.js +41 -0
  268. package/studio/web/assets/{vennDiagram-UO4OBE2U-BKYAZUh_.js → vennDiagram-UO4OBE2U-DZTsDA4k.js} +1 -1
  269. package/studio/web/assets/{wardley-WFR3VGLG-BO7f36ci.js → wardley-WFR3VGLG-Cfqoe6z4.js} +1 -1
  270. package/studio/web/assets/{wardleyDiagram-VNRHLVJA-Djb0JDQc.js → wardleyDiagram-VNRHLVJA-qu_f7Tvl.js} +1 -1
  271. package/studio/web/assets/{workers-CCvmDLH3.js → workers-BfZlUNVI.js} +3 -3
  272. package/studio/web/assets/{xml-06pZp00U.js → xml-BHd6qGH7.js} +1 -1
  273. package/studio/web/assets/{xychartDiagram-PMCCYNJV-BQh9ubvb.js → xychartDiagram-PMCCYNJV-UrOKRsmR.js} +1 -1
  274. package/studio/web/assets/{yaml-iSnWMVlV.js → yaml-C53R8OQq.js} +1 -1
  275. package/studio/web/assets/{yaml.worker-DAK2o_H1.js → yaml.worker-D9JekGdp.js} +183 -183
  276. package/studio/web/index.html +15 -15
  277. package/utility/operation_authorization.ts +27 -7
  278. package/utility/signalling.ts +3 -0
  279. package/validation/searchValidator.ts +22 -13
  280. package/studio/web/assets/FloatingChat-D4560T0p.js +0 -23
  281. package/studio/web/assets/alertDialog-UrbiMgQt.js +0 -1
  282. package/studio/web/assets/apiToken-BEvbFPMc.js +0 -1
  283. package/studio/web/assets/applications-CSUVTH4E.js +0 -296
  284. package/studio/web/assets/architecture-7GRP2DOG-11l4Ny1l.js +0 -1
  285. package/studio/web/assets/authStore-5jSS5pTW.js +0 -3
  286. package/studio/web/assets/channel-BDtouPd6.js +0 -1
  287. package/studio/web/assets/dialog-DTepachC.js +0 -1
  288. package/studio/web/assets/editor.worker-CkkvdXvO.js +0 -33
  289. package/studio/web/assets/eventmodeling-NTZA5JFV-CFb03e0o.js +0 -1
  290. package/studio/web/assets/flowDiagram-KWPJA3E3-Cidi00_Y.js +0 -1
  291. package/studio/web/assets/index-CPoDzQTe.css +0 -1
  292. package/studio/web/assets/index-DGRAzvO4.js +0 -824
  293. package/studio/web/assets/index.lazy-vgSic7et.js +0 -2
  294. package/studio/web/assets/json.worker-CSi2hF-a.js +0 -65
  295. package/studio/web/assets/notifications-BeWcX04m.js +0 -1
  296. package/studio/web/assets/notifications-DUGWrWoO.js +0 -1
  297. package/studio/web/assets/profile-BvUEDBzW.js +0 -1
  298. package/studio/web/assets/railroad-abnf-HS5TGJTU-CLMVZvH0.js +0 -1
  299. package/studio/web/assets/railroad-ebnf-LZEXJU2U--tlwFoVB.js +0 -1
  300. package/studio/web/assets/railroad-peg-WCYAUIDC-Dpuslblm.js +0 -1
  301. package/studio/web/assets/regions-CB-_2m7M.js +0 -1
  302. package/studio/web/assets/register-1w1Zr3b2.js +0 -848
  303. package/studio/web/assets/register-jjEx9u7D.css +0 -1
  304. package/studio/web/assets/setLocalStorage-D_kflv4U.js +0 -1
  305. package/studio/web/assets/status-CI0wOdOE.js +0 -61
  306. package/studio/web/assets/swimlanesDiagram-TC7HE7FX-BBk4t4hO.js +0 -8
  307. package/studio/web/assets/tabs-CnQ_zenf.js +0 -1
  308. package/studio/web/assets/toggleHighContrast--ixms01u.css +0 -1
  309. package/studio/web/assets/toggleHighContrast-BsAE4jPe.js +0 -49
  310. package/studio/web/assets/useLocalStorage-BYoZF60G.js +0 -1
  311. package/studio/web/assets/vendor-datadog-fCQhBHGk.js +0 -6
  312. package/studio/web/assets/vendor-tanstack-CQ1aRjVa.js +0 -2
  313. package/studio/web/assets/vendor-ui-aE1tHcW7.js +0 -41
  314. package/studio/web/assets/x-vYntCxm4.js +0 -1
@@ -95,6 +95,7 @@ const manageThreads_js_1 = require("../server/threads/manageThreads.js");
95
95
  const gitCredentialServer_ts_1 = require("./gitCredentialServer.js");
96
96
  const secretDecryptor_ts_1 = require("../resources/secretDecryptor.js");
97
97
  const envFile_ts_1 = require("../utility/envFile.js");
98
+ const deploymentProvenance_ts_1 = require("./deploymentProvenance.js");
98
99
  const node_path_1 = require("node:path");
99
100
  const promises_1 = require("node:fs/promises");
100
101
  const node_child_process_1 = require("node:child_process");
@@ -951,6 +952,11 @@ const ACTIVATION_JOURNAL_VERSION = 2;
951
952
  const LEGACY_ACTIVATION_JOURNAL_VERSION = 1;
952
953
  const ARTIFACT_DESCRIPTOR_VERSION = 1;
953
954
  const DEFAULT_STAGING_RETENTION_MAX_COUNT = 5;
955
+ // A digest of the component, not its name, follows the uuid in a claim's name: a long name would pass the filename
956
+ // limit.
957
+ const CLAIMING_PREFIX = '.claiming-';
958
+ const CLAIMING_UUID_LENGTH = 36;
959
+ const CLAIM_OWNER_DIGEST_LENGTH = 16;
954
960
  /** `deployment_stagingRetention_maxCount`; 0 keeps none. Only a number or numeric string counts, so `true`/`[]`/blank cannot become "keep nothing". */
955
961
  function getStagingRetentionMaxCount() {
956
962
  const configured = (0, configUtils_ts_1.getConfigValue)(hdbTerms_ts_1.CONFIG_PARAMS.DEPLOYMENT_STAGINGRETENTION_MAXCOUNT);
@@ -961,6 +967,9 @@ function getStagingRetentionMaxCount() {
961
967
  const parsed = Number(configured);
962
968
  return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : DEFAULT_STAGING_RETENTION_MAX_COUNT;
963
969
  }
970
+ function isDeploymentDirectoryEntry(entry) {
971
+ return entry.isDirectory() && !entry.name.startsWith('.');
972
+ }
964
973
  async function presentOrAbsent(path) {
965
974
  return (0, promises_1.lstat)(path).catch((error) => {
966
975
  if (error?.code === 'ENOENT')
@@ -991,15 +1000,20 @@ async function dormantBuildAt(deploymentDirPath, owner) {
991
1000
  * cannot hold or miss a slot. Never throws: a failure must neither fail a component closed nor replace a
992
1001
  * deploy's own error.
993
1002
  *
994
- * `pinnedDeploymentId` is never evicted. A delayed activation runs this preamble under the same lock it is
995
- * about to activate under, so without the pin retention would delete the artifact the request named —
996
- * immediately, when the knob is `0`. The pin is applied after the kept set is chosen, so a pinned build in
997
- * the eviction tail leaves `maxCount + 1` on disk for the life of the request; the next preamble that does
998
- * not pin it brings the count back down.
1003
+ * A `pinned` id is never evicted. A delayed activation runs this preamble under the same lock it is about to
1004
+ * activate under, so without the pin retention would delete the artifact the request named — immediately, when the
1005
+ * knob is `0` — and a settlement's pins keep the releases it just put back. Pins are applied after the kept set is
1006
+ * chosen, so each pinned build in the eviction tail leaves one more than `maxCount` on disk until a pass that does
1007
+ * not pin it.
999
1008
  */
1000
- async function pruneDormantBuilds(componentName, builds, maxCount, pinnedDeploymentId) {
1009
+ async function pruneDormantBuilds(componentName, builds, maxCount, pinned) {
1010
+ const pins = new Set(typeof pinned === 'string' ? [pinned] : (pinned ?? []));
1001
1011
  const current = [];
1012
+ const seen = new Set();
1002
1013
  for (const build of builds) {
1014
+ if (seen.has(build.deploymentDirPath))
1015
+ continue;
1016
+ seen.add(build.deploymentDirPath);
1003
1017
  try {
1004
1018
  const fresh = await dormantBuildAt(build.deploymentDirPath, componentName);
1005
1019
  if (fresh && !(await presentOrAbsent((0, node_path_1.join)(build.deploymentDirPath, ACTIVATION_JOURNAL))))
@@ -1013,7 +1027,7 @@ async function pruneDormantBuilds(componentName, builds, maxCount, pinnedDeploym
1013
1027
  .sort((left, right) => right.completedAt - left.completedAt ||
1014
1028
  (left.deploymentId < right.deploymentId ? -1 : left.deploymentId > right.deploymentId ? 1 : 0))
1015
1029
  .slice(Math.max(0, maxCount))
1016
- .filter((build) => build.deploymentId !== pinnedDeploymentId);
1030
+ .filter((build) => !pins.has(build.deploymentId));
1017
1031
  for (const build of evictions) {
1018
1032
  try {
1019
1033
  await (0, promises_1.rm)(build.deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
@@ -1039,7 +1053,7 @@ async function dormantBuildsOf(componentsRootDirPath, componentName) {
1039
1053
  }
1040
1054
  const builds = [];
1041
1055
  for (const deployment of deployments) {
1042
- if (!deployment.isDirectory())
1056
+ if (!isDeploymentDirectoryEntry(deployment))
1043
1057
  continue;
1044
1058
  const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
1045
1059
  try {
@@ -1327,6 +1341,142 @@ async function writeArtifactDescriptor(componentDirPath, deploymentId, descripto
1327
1341
  throw error;
1328
1342
  }
1329
1343
  }
1344
+ async function writeDeploymentProvenance(candidateDirPath, componentName, deploymentId) {
1345
+ const markerPath = (0, node_path_1.join)(candidateDirPath, deploymentProvenance_ts_1.DEPLOYMENT_PROVENANCE_FILE);
1346
+ await (0, promises_1.rm)(markerPath, { recursive: true, force: true });
1347
+ await writeControlFileDurably(markerPath, (0, deploymentProvenance_ts_1.formatDeploymentProvenance)(componentName, deploymentId));
1348
+ }
1349
+ /**
1350
+ * A link carries no provenance, whatever its target holds. A read that fails propagates rather than reading as
1351
+ * absent.
1352
+ */
1353
+ async function readDeploymentProvenance(treePath, componentName) {
1354
+ const tree = await presentOrAbsent(treePath);
1355
+ if (!tree?.isDirectory())
1356
+ return undefined;
1357
+ const raw = await (0, promises_1.readFile)((0, node_path_1.join)(treePath, deploymentProvenance_ts_1.DEPLOYMENT_PROVENANCE_FILE), 'utf8').catch((error) => {
1358
+ if (error?.code === 'ENOENT' || error?.code === 'EISDIR')
1359
+ return undefined;
1360
+ throw error;
1361
+ });
1362
+ return raw === undefined ? undefined : (0, deploymentProvenance_ts_1.parseDeploymentProvenance)(raw, componentName);
1363
+ }
1364
+ async function isDeploymentRecord(deploymentDirPath, componentName) {
1365
+ if ((await candidateComponentName(deploymentDirPath)) !== componentName)
1366
+ return false;
1367
+ if (!(await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER))))
1368
+ return false;
1369
+ for (const unexpected of [ACTIVATION_JOURNAL, UNSETTLED_MARKER, componentName]) {
1370
+ if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, unexpected)))
1371
+ return false;
1372
+ }
1373
+ try {
1374
+ return (await readArtifactDescriptor(deploymentDirPath, componentName)) !== undefined;
1375
+ }
1376
+ catch (error) {
1377
+ if (error instanceof hdbError_ts_1.ClientError)
1378
+ return false;
1379
+ throw error;
1380
+ }
1381
+ }
1382
+ /**
1383
+ * "Not live" does not make a record stale: a crash between the commit and the re-home leaves the displaced tree in
1384
+ * `.deploy-aside`, retired or not, and a scan can reach the record before the journal that re-homes into it.
1385
+ */
1386
+ async function deploymentRecordIsReferenced(componentsRootDirPath, componentName, deploymentId) {
1387
+ const liveDirPath = (0, node_path_1.join)(componentsRootDirPath, componentName);
1388
+ if ((await readDeploymentProvenance(liveDirPath, componentName)) === deploymentId)
1389
+ return true;
1390
+ const asideStagingDir = extractionStagingDirectory(liveDirPath);
1391
+ const entries = await (0, promises_1.readdir)(asideStagingDir, { withFileTypes: true }).catch((error) => {
1392
+ if (error?.code === 'ENOENT')
1393
+ return [];
1394
+ throw error;
1395
+ });
1396
+ for (const entry of entries) {
1397
+ if (!entry.isDirectory() || !entry.name.startsWith(IN_PROGRESS_ASIDE_PREFIX))
1398
+ continue;
1399
+ if ((await readDeploymentProvenance((0, node_path_1.join)(asideStagingDir, entry.name), componentName)) === deploymentId) {
1400
+ return true;
1401
+ }
1402
+ }
1403
+ return false;
1404
+ }
1405
+ async function isReferencedDeploymentRecord(componentsRootDirPath, deploymentDirPath, owner) {
1406
+ for (const expected of [CANDIDATE_COMPLETE_MARKER, CANDIDATE_ARTIFACT_FILE]) {
1407
+ if (!(await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, expected))))
1408
+ return false;
1409
+ }
1410
+ if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, owner)))
1411
+ return false;
1412
+ return deploymentRecordIsReferenced(componentsRootDirPath, owner, (0, node_path_1.basename)(deploymentDirPath));
1413
+ }
1414
+ /** A failed read keeps the directory: a leftover record costs a few bytes, a lost one costs the release. */
1415
+ async function keepsDeploymentRecord(deploymentDirPath) {
1416
+ return presentOrAbsent((0, node_path_1.join)(deploymentDirPath, CANDIDATE_ARTIFACT_FILE)).then(Boolean, () => true);
1417
+ }
1418
+ /** Retention orders by `.complete`'s mtime, and a kept release has to outrank stages nobody activated. */
1419
+ async function refreshCompletedAt(deploymentDirPath, componentName, deploymentId) {
1420
+ for (let attempt = 1;; attempt++) {
1421
+ const now = new Date();
1422
+ try {
1423
+ await (0, promises_1.utimes)((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER), now, now);
1424
+ return;
1425
+ }
1426
+ catch (error) {
1427
+ if (attempt >= 3) {
1428
+ harper_logger_ts_1.default.warn(`Kept deployment ${deploymentId} of ${componentName}, but retention may evict it ahead of older ` +
1429
+ `builds: ${errorMessage(error)}`);
1430
+ return;
1431
+ }
1432
+ await (0, promises_3.setTimeout)(100 * attempt);
1433
+ }
1434
+ }
1435
+ }
1436
+ /**
1437
+ * Only an explicit retention decision deletes a displaced tree: `failed` leaves it for the next preparation to retry.
1438
+ * A failure after the rename propagates, as the aside syncs around it do.
1439
+ */
1440
+ async function retainDisplacedRelease(componentsRootDirPath, componentName, displacedTreePath) {
1441
+ const keepNone = getStagingRetentionMaxCount() === 0;
1442
+ let record;
1443
+ try {
1444
+ const deploymentId = await readDeploymentProvenance(displacedTreePath, componentName);
1445
+ if (deploymentId !== undefined) {
1446
+ const recordDirPath = (0, node_path_1.join)(componentsRootDirPath, exports.DEPLOY_STAGING_DIR, deploymentId);
1447
+ if (await isDeploymentRecord(recordDirPath, componentName))
1448
+ record = { deploymentId, recordDirPath };
1449
+ }
1450
+ }
1451
+ catch (error) {
1452
+ if (keepNone)
1453
+ return { verdict: 'ineligible' };
1454
+ harper_logger_ts_1.default.warn(`Could not tell whether the previous release of ${componentName} at ${displacedTreePath} can be kept; ` +
1455
+ `leaving it for its next deploy:`, (0, harper_logger_ts_1.errorForLog)(error));
1456
+ return { verdict: 'failed' };
1457
+ }
1458
+ if (!record)
1459
+ return { verdict: 'ineligible' };
1460
+ const { deploymentId, recordDirPath } = record;
1461
+ if (keepNone) {
1462
+ // Nothing will ever put the tree back into its record, so the record goes with it.
1463
+ await (0, promises_1.rm)(recordDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((error) => harper_logger_ts_1.default.warn(`Could not remove the record of deployment ${deploymentId} of ${componentName}:`, (0, harper_logger_ts_1.errorForLog)(error)));
1464
+ return { verdict: 'ineligible' };
1465
+ }
1466
+ const keptTreePath = (0, node_path_1.join)(recordDirPath, componentName);
1467
+ try {
1468
+ await renameThroughTransientHolder(displacedTreePath, keptTreePath);
1469
+ }
1470
+ catch (error) {
1471
+ harper_logger_ts_1.default.warn(`Could not keep the previous release of ${componentName} as deployment ${deploymentId}; leaving it for ` +
1472
+ `its next deploy:`, (0, harper_logger_ts_1.errorForLog)(error));
1473
+ return { verdict: 'failed' };
1474
+ }
1475
+ await syncRenameParents(displacedTreePath, keptTreePath);
1476
+ await refreshCompletedAt(recordDirPath, componentName, deploymentId);
1477
+ harper_logger_ts_1.default.trace?.(`Kept the previous release of ${componentName} as dormant deployment ${deploymentId}`);
1478
+ return { verdict: 'kept', deploymentId };
1479
+ }
1330
1480
  /**
1331
1481
  * A component name safe to join onto the components root: no separator, no traversal, not dot-prefixed.
1332
1482
  * Applied to EVERY source of the name — the journal and the sidecar — because validating one and trusting
@@ -1491,88 +1641,107 @@ async function ensureSecureStagingDirectory(stagingDir) {
1491
1641
  }
1492
1642
  }
1493
1643
  /**
1494
- * Claim a deployment directory for this build, EXCLUSIVELY. The id is the public deployment id, which an
1495
- * operator can repeat and a redelivered replication can repeat for them, so tolerating an existing
1496
- * directory would let a replayed stage rewrite the bytes under an existing `.complete` and descriptor —
1497
- * and a crash mid-rebuild would leave a partial tree that still reads as certified.
1498
- *
1499
- * The caller holds the component's preparation lock, which is what makes the EEXIST verdicts sound: no
1500
- * other preparation of THIS component is running, and a directory belonging to another component is not
1501
- * this lock's to touch.
1644
+ * Exclusive, because an operator can repeat a public id; built aside and renamed onto it, so it never appears
1645
+ * unattributed. The caller's preparation lock is what makes the verdicts on an existing directory sound.
1502
1646
  */
1503
1647
  async function claimDeploymentDirectory(deploymentDirPath, componentName) {
1504
- // Every refusal below is a conflict over an id that already exists, which is the caller's to resolve by
1505
- // naming a different one — not a server fault, and not the 500 a bare Error reaches the caller as.
1506
- const taken = (message) => new hdbError_ts_1.ClientError(message, 409);
1648
+ const claimDirPath = (0, node_path_1.join)((0, node_path_1.dirname)(deploymentDirPath), `${CLAIMING_PREFIX}${(0, node_crypto_1.randomUUID)()}-${claimOwnerDigest(componentName)}`);
1649
+ await (0, promises_1.mkdir)(claimDirPath, { mode: 0o700 });
1507
1650
  try {
1508
- await (0, promises_1.mkdir)(deploymentDirPath, { mode: 0o700 });
1509
- }
1510
- catch (error) {
1511
- if (error.code !== 'EEXIST')
1512
- throw error;
1513
- const owner = await candidateComponentName(deploymentDirPath);
1514
- if (owner !== undefined && owner !== componentName) {
1515
- throw taken(`Deployment id ${(0, node_path_1.basename)(deploymentDirPath)} already holds a build of '${owner}'; a deployment id ` +
1516
- `names one artifact for its lifetime`);
1517
- }
1518
- // Ownership has to be POSITIVE to reclaim, and EMPTINESS IS NOT A VERDICT. The sidecar below is
1519
- // written as part of the claim, so a directory naming nobody is another component between its own
1520
- // mkdir and that write, and a deploy can hold a nearly empty one for minutes while it resolves and
1521
- // packs. Nothing on disk separates that from a claim that got no further, and the lock that
1522
- // serializes it is not this one, so only the exclusive create may conclude the id is free: the
1523
- // directory can also have been discarded by a failed build between the first mkdir and this read,
1524
- // and another component can claim it in that same gap.
1525
- if (owner === undefined) {
1526
- await (0, promises_1.mkdir)(deploymentDirPath, { mode: 0o700 }).catch((retry) => {
1527
- if (retry?.code !== 'EEXIST')
1528
- throw retry;
1529
- throw taken(`Deployment id ${(0, node_path_1.basename)(deploymentDirPath)} is already claimed by a build that has not named its ` +
1530
- `component yet. If no deploy of any component is in flight, that directory is abandoned and has ` +
1531
- `to be removed by hand; deploying again without a deployment_id mints a fresh id`);
1532
- });
1651
+ const claimed = await (0, promises_1.lstat)(claimDirPath);
1652
+ if (!claimed.isDirectory() || claimed.isSymbolicLink()) {
1653
+ throw new Error(`Component deploy staging path is not a directory: ${claimDirPath}`);
1533
1654
  }
1534
- else {
1535
- if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER))) {
1536
- throw taken(`Deployment id ${(0, node_path_1.basename)(deploymentDirPath)} already holds a completed build of '${componentName}'; ` +
1537
- `deploy it with deployment_id, or deploy again to build a new one`);
1655
+ if (process.platform !== 'win32' && (claimed.mode & 0o777) !== 0o700) {
1656
+ await (0, promises_1.chmod)(claimDirPath, 0o700).catch((error) => harper_logger_ts_1.default.warn(`Could not restrict component deploy staging permissions for ${claimDirPath}:`, (0, harper_logger_ts_1.errorForLog)(error)));
1657
+ }
1658
+ await publishClaimOwnership(claimDirPath, componentName);
1659
+ for (let attempt = 1;; attempt++) {
1660
+ // Checked first: Windows refuses a rename onto an existing directory with a transient holder's EPERM.
1661
+ if (!(await presentOrAbsent(deploymentDirPath))) {
1662
+ try {
1663
+ await renameThroughTransientHolder(claimDirPath, deploymentDirPath);
1664
+ return;
1665
+ }
1666
+ catch (error) {
1667
+ // Only this component's lock can remove the claim, so a vanished source means the rename landed.
1668
+ if (!(await presentOrAbsent(claimDirPath)))
1669
+ return;
1670
+ if (!(await presentOrAbsent(deploymentDirPath)))
1671
+ throw error;
1672
+ }
1538
1673
  }
1539
- // This component's own preparation lock serializes the claim and nothing certified it, so nothing
1540
- // is lost by rebuilding over it.
1541
- await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
1542
- await (0, promises_1.mkdir)(deploymentDirPath, { mode: 0o700 });
1674
+ if (attempt >= MAX_CLAIM_ATTEMPTS) {
1675
+ throw new hdbError_ts_1.ClientError(`Deployment id ${(0, node_path_1.basename)(deploymentDirPath)} was claimed again each time it was freed; deploying ` +
1676
+ `again without a deployment_id mints a fresh id`, 409);
1677
+ }
1678
+ await makeRoomForClaim(deploymentDirPath, componentName);
1543
1679
  }
1544
1680
  }
1545
- const claimed = await (0, promises_1.lstat)(deploymentDirPath);
1546
- if (!claimed.isDirectory() || claimed.isSymbolicLink()) {
1547
- throw new Error(`Component deploy staging path is not a directory: ${deploymentDirPath}`);
1681
+ finally {
1682
+ // A claim that did not land names no id, so it is nobody else's.
1683
+ await (0, promises_1.rm)(claimDirPath, { recursive: true, force: true }).catch(() => { });
1548
1684
  }
1549
- if (process.platform !== 'win32' && (claimed.mode & 0o777) !== 0o700) {
1550
- await (0, promises_1.chmod)(deploymentDirPath, 0o700).catch((error) => harper_logger_ts_1.default.warn(`Could not restrict component deploy staging permissions for ${deploymentDirPath}:`, (0, harper_logger_ts_1.errorForLog)(error)));
1685
+ }
1686
+ const MAX_CLAIM_ATTEMPTS = 3;
1687
+ /** Every refusal is a conflict the caller resolves by naming another id: a 409, not a server fault. */
1688
+ async function makeRoomForClaim(deploymentDirPath, componentName) {
1689
+ const taken = (message) => new hdbError_ts_1.ClientError(message, 409);
1690
+ const deploymentId = (0, node_path_1.basename)(deploymentDirPath);
1691
+ const owner = await candidateComponentName(deploymentDirPath);
1692
+ if (owner !== undefined && owner !== componentName) {
1693
+ throw taken(`Deployment id ${deploymentId} already holds a build of '${owner}'; a deployment id names one artifact for ` +
1694
+ `its lifetime`);
1695
+ }
1696
+ if (owner === undefined) {
1697
+ // Claims appear already attributed, so an EMPTY directory is only ever one a build created before naming it,
1698
+ // abandoned; `rmdir` removes nothing else.
1699
+ try {
1700
+ await (0, promises_1.rmdir)(deploymentDirPath);
1701
+ return;
1702
+ }
1703
+ catch (error) {
1704
+ const code = error.code;
1705
+ if (code === 'ENOENT')
1706
+ return;
1707
+ if (code !== 'ENOTEMPTY' && code !== 'EEXIST')
1708
+ throw error;
1709
+ }
1710
+ throw taken(`Deployment id ${deploymentId} is held by a directory that names no component. Remove it once you have ` +
1711
+ `determined it holds nothing you need; deploying again without a deployment_id mints a fresh id`);
1712
+ }
1713
+ if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER))) {
1714
+ throw taken(`Deployment id ${deploymentId} already holds a completed build of '${componentName}'; deploy it with ` +
1715
+ `deployment_id, or deploy again to build a new one`);
1716
+ }
1717
+ // Nothing certified it, and this component's lock serializes its claims.
1718
+ await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
1719
+ }
1720
+ function claimOwnerDigest(componentName) {
1721
+ return (0, node_crypto_1.createHash)('sha256').update(componentName).digest('hex').slice(0, CLAIM_OWNER_DIGEST_LENGTH);
1722
+ }
1723
+ function isClaimOf(entryName, componentName) {
1724
+ return (entryName.startsWith(CLAIMING_PREFIX) &&
1725
+ entryName.slice(CLAIMING_PREFIX.length + CLAIMING_UUID_LENGTH + 1) === claimOwnerDigest(componentName));
1726
+ }
1727
+ /** The caller holds the component's preparation lock, so none of its claims is in flight. */
1728
+ async function sweepAbandonedClaims(stagingRoot, entries, componentName) {
1729
+ for (const entry of entries) {
1730
+ if (!entry.isDirectory() || !isClaimOf(entry.name, componentName))
1731
+ continue;
1732
+ await (0, promises_1.rm)((0, node_path_1.join)(stagingRoot, entry.name), { recursive: true, force: true }).catch((error) => harper_logger_ts_1.default.warn(`Could not remove the abandoned deploy claim ${entry.name}:`, (0, harper_logger_ts_1.errorForLog)(error)));
1551
1733
  }
1552
- await publishClaimOwnership(deploymentDirPath, componentName);
1553
1734
  }
1554
1735
  /**
1555
- * Name the component that owns a deployment directory THIS CALL created, and take the directory back if that
1556
- * cannot be recorded.
1557
- *
1558
- * Ownership is published as part of the claim rather than at certification, because resolving and packing can
1559
- * take minutes and until a tree exists to infer an owner from, a directory answering to nobody is one another
1560
- * component will take for abandoned. Unattributed is a permanent refusal, though, so a failure here — a full
1561
- * disk, an EIO on the temp write or its sync — would burn this deployment id for good, every retry refused by
1562
- * its own wreckage. The removal is scoped to the directory this invocation made, and deliberately not
1563
- * broadened to one found by EEXIST: that directory may be another claimant's, and removing it is the race the
1564
- * refusal exists to prevent. Best-effort, because the claim failure is what the caller needs to see.
1565
- *
1566
- * `write` is a parameter so the failure is testable without a filesystem that can be made to fail on exactly
1567
- * this write and nothing else.
1736
+ * The claim names no id yet, so a failure burns nothing. `write` is a parameter so the failure is testable without a
1737
+ * filesystem that can be made to fail on exactly this write.
1568
1738
  */
1569
- async function publishClaimOwnership(deploymentDirPath, componentName, write = writeControlFileDurably) {
1739
+ async function publishClaimOwnership(claimDirPath, componentName, write = writeControlFileDurably) {
1570
1740
  try {
1571
- await write((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPONENT_FILE), componentName);
1741
+ await write((0, node_path_1.join)(claimDirPath, CANDIDATE_COMPONENT_FILE), componentName);
1572
1742
  }
1573
1743
  catch (error) {
1574
- await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true }).catch((cleanupError) => harper_logger_ts_1.default.warn(`Could not remove the deployment directory ${deploymentDirPath} after failing to publish its ` +
1575
- `ownership; the id stays unusable until it is removed:`, (0, harper_logger_ts_1.errorForLog)(cleanupError)));
1744
+ await (0, promises_1.rm)(claimDirPath, { recursive: true, force: true }).catch((cleanupError) => harper_logger_ts_1.default.warn(`Could not remove the unfinished deploy claim ${claimDirPath}:`, (0, harper_logger_ts_1.errorForLog)(cleanupError)));
1576
1745
  throw error;
1577
1746
  }
1578
1747
  }
@@ -1632,7 +1801,7 @@ async function journaledDeploymentForComponent(componentsRootDirPath, componentN
1632
1801
  throw error;
1633
1802
  }
1634
1803
  for (const deployment of deployments) {
1635
- if (!deployment.isDirectory())
1804
+ if (!isDeploymentDirectoryEntry(deployment))
1636
1805
  continue;
1637
1806
  const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
1638
1807
  const journalPath = (0, node_path_1.join)(deploymentDirPath, ACTIVATION_JOURNAL);
@@ -1741,9 +1910,11 @@ async function settleStagingForComponent(componentsRootDirPath, componentName, p
1741
1910
  return;
1742
1911
  throw error;
1743
1912
  }
1913
+ await sweepAbandonedClaims(stagingRoot, deployments, componentName);
1744
1914
  const dormant = [];
1915
+ const kept = [];
1745
1916
  for (const deployment of deployments) {
1746
- if (!deployment.isDirectory())
1917
+ if (!isDeploymentDirectoryEntry(deployment))
1747
1918
  continue;
1748
1919
  const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
1749
1920
  // Ownership BEFORE parsing. Reading every journal first meant a truncated journal belonging to another
@@ -1790,11 +1961,27 @@ async function settleStagingForComponent(componentsRootDirPath, componentName, p
1790
1961
  // activation interrupted before B1 has no rollback record for the restore gate to catch.
1791
1962
  if (journal.component !== componentName)
1792
1963
  continue;
1793
- await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, journal);
1964
+ kept.push(...(await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, journal)));
1794
1965
  }
1966
+ await catalogueKeptReleases(stagingRoot, componentName, kept, dormant);
1795
1967
  const maxCount = getStagingRetentionMaxCount();
1796
- if (dormant.length > maxCount)
1797
- await pruneDormantBuilds(componentName, dormant, maxCount, pinnedDeploymentId);
1968
+ if (dormant.length > maxCount) {
1969
+ await pruneDormantBuilds(componentName, dormant, maxCount, [
1970
+ ...kept,
1971
+ ...(pinnedDeploymentId ? [pinnedDeploymentId] : []),
1972
+ ]);
1973
+ }
1974
+ }
1975
+ /** Counted and pinned, or the pass a settlement ran inside can miss what it kept, or evict it after a failed refresh. */
1976
+ async function catalogueKeptReleases(stagingRoot, componentName, kept, into) {
1977
+ const present = new Set(into.map((build) => build.deploymentDirPath));
1978
+ for (const deploymentId of kept) {
1979
+ const build = await dormantBuildAt((0, node_path_1.join)(stagingRoot, deploymentId), componentName).catch(() => undefined);
1980
+ if (build && !present.has(build.deploymentDirPath)) {
1981
+ present.add(build.deploymentDirPath);
1982
+ into.push(build);
1983
+ }
1984
+ }
1798
1985
  }
1799
1986
  /**
1800
1987
  * Components that on-disk evidence says were left in a state nobody settled — determined READ-ONLY, so any
@@ -1822,7 +2009,7 @@ async function unsettleableComponentsFromDisk(componentsRootDirPath) {
1822
2009
  throw error;
1823
2010
  }
1824
2011
  for (const deployment of deployments) {
1825
- if (!deployment.isDirectory())
2012
+ if (!isDeploymentDirectoryEntry(deployment))
1826
2013
  continue;
1827
2014
  const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
1828
2015
  // Per deployment. `candidateComponentName` propagates every non-ENOENT error now, and this pass runs
@@ -1906,13 +2093,23 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
1906
2093
  const dormant = new Map();
1907
2094
  const catalogue = (owner, build) => {
1908
2095
  const builds = dormant.get(owner);
1909
- if (builds)
1910
- builds.push(build);
1911
- else
2096
+ if (!builds)
1912
2097
  dormant.set(owner, [build]);
2098
+ else if (!builds.some((existing) => existing.deploymentDirPath === build.deploymentDirPath))
2099
+ builds.push(build);
2100
+ };
2101
+ const keptBy = new Map();
2102
+ const keptDuring = async (owner, kept) => {
2103
+ if (kept.length === 0)
2104
+ return;
2105
+ keptBy.set(owner, [...(keptBy.get(owner) ?? []), ...kept]);
2106
+ const builds = [];
2107
+ await catalogueKeptReleases(stagingRoot, owner, kept, builds);
2108
+ for (const build of builds)
2109
+ catalogue(owner, build);
1913
2110
  };
1914
2111
  for (const deployment of deployments) {
1915
- if (!deployment.isDirectory())
2112
+ if (!isDeploymentDirectoryEntry(deployment))
1916
2113
  continue;
1917
2114
  const deploymentDirPath = (0, node_path_1.join)(stagingRoot, deployment.name);
1918
2115
  const journalPath = (0, node_path_1.join)(deploymentDirPath, ACTIVATION_JOURNAL);
@@ -1971,7 +2168,7 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
1971
2168
  throw splitAttributionError(deploymentDirPath, appeared.component, splitNames[0]);
1972
2169
  }
1973
2170
  activationToFail = appeared.component;
1974
- await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, appeared);
2171
+ await keptDuring(appeared.component, await settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, appeared));
1975
2172
  // Settled. Anything that fails after this — releasing the lock, say — is not this
1976
2173
  // activation's, and attributing it here would fail a correctly settled component closed.
1977
2174
  activationToFail = undefined;
@@ -2014,6 +2211,9 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
2014
2211
  // Cleared, and still not a retainable build — an incomplete or treeless staged directory. That
2015
2212
  // is residue like any other, so it falls through to the removal below.
2016
2213
  }
2214
+ // Re-derived under the lock too: the release may have been re-homed, or displaced, since the scan.
2215
+ if (await isReferencedDeploymentRecord(componentsRootDirPath, deploymentDirPath, owner))
2216
+ return;
2017
2217
  // Cleanup, not settlement. There was no activation here — this is most often the residue a
2018
2218
  // SUCCESSFUL settlement leaves when its own sweep failed — so a sweep that fails again cannot
2019
2219
  // make anything unsettled, and recording it would refuse a live component on every worker
@@ -2050,6 +2250,18 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
2050
2250
  catalogue(owner, build);
2051
2251
  continue;
2052
2252
  }
2253
+ // The record of a release that is live, or displaced and not yet put back, is left alone without the
2254
+ // lock for the same reason. A read that fails leaves it in place: deleting it is the irreversible side.
2255
+ try {
2256
+ if (!(await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, UNSETTLED_MARKER))) &&
2257
+ (await isReferencedDeploymentRecord(componentsRootDirPath, deploymentDirPath, owner))) {
2258
+ continue;
2259
+ }
2260
+ }
2261
+ catch (error) {
2262
+ harper_logger_ts_1.default.warn(`Leaving deploy staging ${deploymentDirPath} in place; it could not be read:`, (0, harper_logger_ts_1.errorForLog)(error));
2263
+ continue;
2264
+ }
2053
2265
  }
2054
2266
  // Scoped to THIS deployment, like the journaled branch below: a lock timeout or an EIO here used to
2055
2267
  // abort the entire scan, leaving every later deployment unsettled and unmarked.
@@ -2107,11 +2319,12 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
2107
2319
  continue;
2108
2320
  }
2109
2321
  const settling = journal;
2110
- await (0, componentPreparationLock_ts_1.withComponentPreparationLock)((0, node_path_1.join)(componentsRootDirPath, settling.component), () => settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, settling), {
2322
+ const kept = await (0, componentPreparationLock_ts_1.withComponentPreparationLock)((0, node_path_1.join)(componentsRootDirPath, settling.component), () => settleInterruptedActivation(componentsRootDirPath, deploymentDirPath, settling), {
2111
2323
  purpose: 'activation-recovery',
2112
2324
  ...RECOVERY_LOCK_WAIT,
2113
2325
  isOwnerAlive: (lockOwner) => lockOwner.pid !== process.pid || (0, manageThreads_js_1.isThreadRunning)(lockOwner.threadId),
2114
2326
  });
2327
+ await keptDuring(settling.component, kept);
2115
2328
  }
2116
2329
  catch (error) {
2117
2330
  // The journal named its component, so attribution is exact however the settle failed.
@@ -2120,7 +2333,8 @@ async function recoverInterruptedActivations(componentsRootDirPath) {
2120
2333
  }
2121
2334
  const maxCount = getStagingRetentionMaxCount();
2122
2335
  for (const [owner, builds] of dormant) {
2123
- for (const [component, error] of await reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxCount)) {
2336
+ const pinned = keptBy.get(owner) ?? [];
2337
+ for (const [component, error] of await reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxCount, pinned)) {
2124
2338
  if (!failures.has(component))
2125
2339
  failures.set(component, error);
2126
2340
  }
@@ -2150,7 +2364,7 @@ async function recordUnsettled(failures, component, error, deploymentDirPath) {
2150
2364
  * dormant. The lock is taken only when there is something to do; a lock a live deploy holds is the same
2151
2365
  * deferral the residue branch records.
2152
2366
  */
2153
- async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxCount) {
2367
+ async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxCount, pinned = []) {
2154
2368
  const failures = new Map();
2155
2369
  let journaled;
2156
2370
  for (const build of builds) {
@@ -2166,6 +2380,7 @@ async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxC
2166
2380
  try {
2167
2381
  await (0, componentPreparationLock_ts_1.withComponentPreparationLock)((0, node_path_1.join)(componentsRootDirPath, owner), async () => {
2168
2382
  const stillDormant = [];
2383
+ const keptHere = [];
2169
2384
  for (const build of builds) {
2170
2385
  let journal;
2171
2386
  try {
@@ -2189,14 +2404,17 @@ async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxC
2189
2404
  continue;
2190
2405
  }
2191
2406
  try {
2192
- await settleInterruptedActivation(componentsRootDirPath, build.deploymentDirPath, journal);
2407
+ const kept = await settleInterruptedActivation(componentsRootDirPath, build.deploymentDirPath, journal);
2408
+ keptHere.push(...kept);
2409
+ await catalogueKeptReleases((0, node_path_1.join)(componentsRootDirPath, exports.DEPLOY_STAGING_DIR), owner, kept, stillDormant);
2193
2410
  }
2194
2411
  catch (error) {
2195
2412
  await recordUnsettled(failures, journal.component, error, build.deploymentDirPath);
2196
2413
  }
2197
2414
  }
2198
- if (stillDormant.length > maxCount)
2199
- await pruneDormantBuilds(owner, stillDormant, maxCount);
2415
+ if (stillDormant.length > maxCount) {
2416
+ await pruneDormantBuilds(owner, stillDormant, maxCount, [...pinned, ...keptHere]);
2417
+ }
2200
2418
  }, {
2201
2419
  purpose: 'activation-recovery',
2202
2420
  ...RECOVERY_LOCK_WAIT,
@@ -2229,6 +2447,7 @@ async function reconcileDormantBuilds(componentsRootDirPath, owner, builds, maxC
2229
2447
  * journal prevents. Sweeping the displaced tree only costs disk, so it is logged.
2230
2448
  */
2231
2449
  async function sweepAsideRecords(records, componentName, liveDirPath, asideStagingDir) {
2450
+ const kept = [];
2232
2451
  for (const record of records) {
2233
2452
  // RETIRING IS CORRECTNESS, not hygiene: the retired marker is what stops the legacy pass treating this
2234
2453
  // record as authoritative and restoring the displaced tree over the candidate that was just rolled
@@ -2236,10 +2455,18 @@ async function sweepAsideRecords(records, componentName, liveDirPath, asideStagi
2236
2455
  // the inversion this protocol exists to prevent, so a failure here PROPAGATES — the caller keeps the
2237
2456
  // journal and the next start retries.
2238
2457
  const retiredMarkerPath = await retireExtractionAside(record);
2458
+ // No prune here: settlement can run inside another request's preamble, whose pin it cannot see. A tree that
2459
+ // could not be kept stays retired in the aside for the legacy pass to retry.
2460
+ const retention = await retainDisplacedRelease((0, node_path_1.dirname)(liveDirPath), componentName, record);
2461
+ if (retention.verdict === 'kept')
2462
+ kept.push(retention.deploymentId);
2463
+ if (retention.verdict === 'failed')
2464
+ continue;
2239
2465
  // Sweeping the displaced tree is hygiene: it bounds disk, and a failure costs space rather than
2240
2466
  // correctness, so it is logged. The retired marker above already makes the record non-authoritative.
2241
2467
  await cleanupExtractionPaths({ name: componentName, dirPath: liveDirPath, logger: harper_logger_ts_1.default }, asideStagingDir, new Set([record, retiredMarkerPath])).catch((error) => harper_logger_ts_1.default.warn(`Settled ${componentName} but could not sweep ${record}:`, (0, harper_logger_ts_1.errorForLog)(error)));
2242
2468
  }
2469
+ return kept;
2243
2470
  }
2244
2471
  /**
2245
2472
  * Clear an earlier failed recovery's verdict once this settlement has decided. Treated as CORRECTNESS, not
@@ -2280,7 +2507,10 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
2280
2507
  const candidateExists = await exists(candidateDirPath);
2281
2508
  const candidateComplete = await exists((0, node_path_1.join)(deploymentDirPath, CANDIDATE_COMPLETE_MARKER));
2282
2509
  const asideRecords = await inProgressAsideRecords(asideStagingDir);
2510
+ let rolledForward = false;
2511
+ let kept = [];
2283
2512
  const rollForward = async () => {
2513
+ rolledForward = true;
2284
2514
  if (!liveExists)
2285
2515
  await renameThroughTransientHolder(candidateDirPath, liveDirPath);
2286
2516
  // Unconditional, not only when THIS pass performed the rename: a crash after normal activation
@@ -2296,7 +2526,7 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
2296
2526
  // this roll-forward just displaced. Failing the component closed and retrying at the next start is
2297
2527
  // the cheaper mistake — the journal survives, so the verdict is re-derivable. Only the disk sweep
2298
2528
  // inside is best-effort.
2299
- await sweepAsideRecords(asideRecords, journal.component, liveDirPath, asideStagingDir);
2529
+ kept = await sweepAsideRecords(asideRecords, journal.component, liveDirPath, asideStagingDir);
2300
2530
  };
2301
2531
  const rollBack = async (restoreFrom) => {
2302
2532
  if (restoreFrom) {
@@ -2376,7 +2606,7 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
2376
2606
  `could not flush that to storage; a power loss could resurrect its activation journal:`, (0, harper_logger_ts_1.errorForLog)(error)));
2377
2607
  harper_logger_ts_1.default.info?.(`Returned the staged build ${(0, node_path_1.basename)(deploymentDirPath)} of ${journal.component} to dormant after an ` +
2378
2608
  `activation that never moved its live tree aside`);
2379
- return;
2609
+ return kept;
2380
2610
  }
2381
2611
  await rollBack();
2382
2612
  }
@@ -2397,7 +2627,7 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
2397
2627
  catch (error) {
2398
2628
  harper_logger_ts_1.default.warn(`Settled the interrupted activation of ${journal.component} but could not flush its rollback record; ` +
2399
2629
  `leaving the journal for the next start: ${errorMessage(error)}`);
2400
- return;
2630
+ return kept;
2401
2631
  }
2402
2632
  // Best-effort, matching the activation path: the activation is settled by this point, so a transient
2403
2633
  // EBUSY removing staging must not throw out of the recovery pass and take the other components with it.
@@ -2406,8 +2636,11 @@ async function settleInterruptedActivation(componentsRootDirPath, deploymentDirP
2406
2636
  // the stale marker and failed it closed.
2407
2637
  await clearUnsettledVerdict(deploymentDirPath, journal.component);
2408
2638
  await (0, promises_1.rm)(journalPath, { force: true }).catch((error) => harper_logger_ts_1.default.warn(`Settled ${journal.component} but could not remove its activation journal:`, (0, harper_logger_ts_1.errorForLog)(error)));
2639
+ if (rolledForward && (await keepsDeploymentRecord(deploymentDirPath)))
2640
+ return kept;
2409
2641
  await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((error) => harper_logger_ts_1.default.warn(`Settled ${journal.component} but could not clean up its staging directory:`, (0, harper_logger_ts_1.errorForLog)(error)));
2410
2642
  await (0, promises_1.rmdir)((0, node_path_1.dirname)(deploymentDirPath)).catch(() => { });
2643
+ return kept;
2411
2644
  }
2412
2645
  /** Mark a candidate build+validation complete. Idempotent, so a retried activation is not a failure. */
2413
2646
  /**
@@ -2725,14 +2958,21 @@ async function activateCandidateApplication(application, deploymentId, options =
2725
2958
  }
2726
2959
  const settledRecord = asidePath ?? priorAbsentRecordPath;
2727
2960
  let retired = false;
2961
+ let keptDeploymentId;
2728
2962
  // Skipped entirely when the swap is not known to be on storage, so the journal below survives.
2729
2963
  if (swapDurable) {
2730
2964
  try {
2731
2965
  const retiredMarkerPath = await retireExtractionAside(settledRecord);
2732
- // Retiring only MARKS the displaced tree disposable. Without this sweep the tree every deploy
2733
- // displaces stays under `.deploy-aside/<component>` forever, so the components root grows by a
2734
- // whole component version per deploy.
2735
- await cleanupExtractionPaths(application, asideStagingDir, new Set([settledRecord, retiredMarkerPath]));
2966
+ // A tree nothing can keep is swept, or the components root grows by a component version per deploy; one
2967
+ // whose keep FAILED stays retired here for a retry.
2968
+ const retention = asidePath
2969
+ ? await retainDisplacedRelease((0, node_path_1.dirname)(liveDirPath), application.name, asidePath)
2970
+ : { verdict: 'ineligible' };
2971
+ if (retention.verdict === 'kept')
2972
+ keptDeploymentId = retention.deploymentId;
2973
+ if (retention.verdict !== 'failed') {
2974
+ await cleanupExtractionPaths(application, asideStagingDir, new Set([settledRecord, retiredMarkerPath]));
2975
+ }
2736
2976
  // Before the journal goes: if the journal's removal persists but the record's does not, startup sees
2737
2977
  // an in-progress aside with no journal and the legacy pass restores the old tree over the new one.
2738
2978
  await syncDirectory(asideStagingDir);
@@ -2747,8 +2987,23 @@ async function activateCandidateApplication(application, deploymentId, options =
2747
2987
  // `.in-progress-*` record still names the displaced tree lets the legacy pass restore the old tree.
2748
2988
  if (retired) {
2749
2989
  await (0, promises_1.rm)(journalPath, { force: true }).catch((error) => application.logger.warn(`Deployed ${application.name} but could not remove its activation journal:`, error));
2750
- await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((error) => application.logger.warn(`Deployed ${application.name} but could not clean up its staging directory:`, error));
2751
- await (0, promises_1.rmdir)((0, node_path_1.dirname)(deploymentDirPath)).catch(() => { });
2990
+ if (!(await keepsDeploymentRecord(deploymentDirPath))) {
2991
+ await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((error) => application.logger.warn(`Deployed ${application.name} but could not clean up its staging directory:`, error));
2992
+ await (0, promises_1.rmdir)((0, node_path_1.dirname)(deploymentDirPath)).catch(() => { });
2993
+ }
2994
+ }
2995
+ // Only here, where the request's own artifact is already live: a prune inside settlement could evict the
2996
+ // artifact a request's preamble pinned. Best-effort, like every prune.
2997
+ if (keptDeploymentId !== undefined) {
2998
+ const maxCount = getStagingRetentionMaxCount();
2999
+ try {
3000
+ const builds = await dormantBuildsOf((0, node_path_1.dirname)(liveDirPath), application.name);
3001
+ if (builds.length > maxCount)
3002
+ await pruneDormantBuilds(application.name, builds, maxCount, keptDeploymentId);
3003
+ }
3004
+ catch (error) {
3005
+ application.logger.warn(`Deployed ${application.name} but could not bound its kept releases:`, error);
3006
+ }
2752
3007
  }
2753
3008
  }
2754
3009
  /**
@@ -2944,6 +3199,10 @@ async function buildCandidateApplication(application, deploymentId, options = {}
2944
3199
  // deployer's git token. `prepareApplication`'s finally still calls this; it is idempotent.
2945
3200
  await application.cleanupGitCredentialSession();
2946
3201
  await installApplication(application, candidateDirPath);
3202
+ // After the install, which can rewrite the tree; never through a link, whose target is not the deploy's to write.
3203
+ if (resolved.kind !== 'link') {
3204
+ await writeDeploymentProvenance(candidateDirPath, application.name, deploymentId);
3205
+ }
2947
3206
  return candidateDirPath;
2948
3207
  }
2949
3208
  catch (error) {
@@ -2996,6 +3255,21 @@ async function recoverOrCleanupStaleExtractionPaths(application, asideStagingDir
2996
3255
  (recoveryRecords.length > 1 ? `; discarded ${recoveryRecords.length - 1} older recovery candidates` : ''));
2997
3256
  return;
2998
3257
  }
3258
+ // Each retired tree left here is a displaced release: kept if it can be — the retry of a keep that failed — and
3259
+ // swept only when it cannot.
3260
+ for (const entry of entries) {
3261
+ if (!entry.isDirectory() || !entry.name.startsWith(IN_PROGRESS_ASIDE_PREFIX))
3262
+ continue;
3263
+ const treePath = (0, node_path_1.join)(asideStagingDir, entry.name);
3264
+ const retention = await retainDisplacedRelease((0, node_path_1.dirname)(application.dirPath), application.name, treePath).catch((error) => {
3265
+ application.logger.warn(`Could not keep the previous release at ${treePath}; leaving it in place:`, error);
3266
+ return { verdict: 'failed' };
3267
+ });
3268
+ if (retention.verdict === 'failed') {
3269
+ paths.delete(treePath);
3270
+ paths.delete(retiredMarkerForAside(treePath));
3271
+ }
3272
+ }
2999
3273
  await cleanupExtractionPaths(application, asideStagingDir, paths);
3000
3274
  }
3001
3275
  function isPriorAbsentRecoveryRecord(entry) {
@@ -3163,9 +3437,11 @@ async function retireComponentDirectory(componentDirPath, componentName = (0, no
3163
3437
  },
3164
3438
  async discard() {
3165
3439
  await cleanupExtractionPaths({ name: componentName, dirPath: componentDirPath, logger: componentLogger }, asideStagingDir, new Set([droppedPath]));
3166
- // A dropped component has no next deploy to bound its dormant builds.
3440
+ // A dropped component has no next deploy to bound its dormant builds, and nothing left for its records to
3441
+ // describe.
3167
3442
  try {
3168
3443
  await pruneDormantBuilds(componentName, await dormantBuildsOf((0, node_path_1.dirname)(componentDirPath), componentName), 0);
3444
+ await reclaimDeploymentRecordsOf((0, node_path_1.dirname)(componentDirPath), componentName);
3169
3445
  }
3170
3446
  catch (error) {
3171
3447
  componentLogger.warn(`Dropped ${componentName} but could not reclaim its dormant staged builds:`, (0, harper_logger_ts_1.errorForLog)(error));
@@ -3173,6 +3449,33 @@ async function retireComponentDirectory(componentDirPath, componentName = (0, no
3173
3449
  },
3174
3450
  };
3175
3451
  }
3452
+ /** Journaled directories stay: an activation nobody settled is evidence, not litter. */
3453
+ async function reclaimDeploymentRecordsOf(componentsRootDirPath, componentName) {
3454
+ const stagingRoot = (0, node_path_1.join)(componentsRootDirPath, exports.DEPLOY_STAGING_DIR);
3455
+ const entries = await (0, promises_1.readdir)(stagingRoot, { withFileTypes: true }).catch((error) => {
3456
+ if (error?.code === 'ENOENT')
3457
+ return [];
3458
+ throw error;
3459
+ });
3460
+ await sweepAbandonedClaims(stagingRoot, entries, componentName);
3461
+ for (const entry of entries) {
3462
+ if (!isDeploymentDirectoryEntry(entry))
3463
+ continue;
3464
+ const deploymentDirPath = (0, node_path_1.join)(stagingRoot, entry.name);
3465
+ try {
3466
+ if ((await candidateComponentName(deploymentDirPath)) !== componentName)
3467
+ continue;
3468
+ if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, ACTIVATION_JOURNAL)))
3469
+ continue;
3470
+ if (await presentOrAbsent((0, node_path_1.join)(deploymentDirPath, componentName)))
3471
+ continue;
3472
+ await (0, promises_1.rm)(deploymentDirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
3473
+ }
3474
+ catch (error) {
3475
+ harper_logger_ts_1.default.warn(`Could not reclaim the deployment record ${deploymentDirPath} of ${componentName}:`, (0, harper_logger_ts_1.errorForLog)(error));
3476
+ }
3477
+ }
3478
+ }
3176
3479
  async function cleanupExtractionPaths(application, asideStagingDir, paths) {
3177
3480
  const retiredMarkers = [];
3178
3481
  for (const path of paths) {
@@ -3672,6 +3975,7 @@ class Application {
3672
3975
  isNewComponent = true;
3673
3976
  packageMetadataChanged = false;
3674
3977
  installationIsOpaque = false;
3978
+ alreadyActive = false;
3675
3979
  constructor({ name, payload, packageIdentifier, install, onInstallLine, credentials }) {
3676
3980
  this.name = name;
3677
3981
  this.payload = payload;
@@ -3852,6 +4156,15 @@ async function prepareApplication(application, options = {}) {
3852
4156
  throw error;
3853
4157
  }));
3854
4158
  if (mode === 'activate') {
4159
+ // A retry of an activation that swapped here and failed later or elsewhere: answered without a swap so
4160
+ // it can go on to the nodes still holding the artifact. Nothing can tell whether the running workers
4161
+ // loaded it, hence the restart request.
4162
+ if ((await readDeploymentProvenance(application.dirPath, application.name)) === artifactId) {
4163
+ application.alreadyActive = true;
4164
+ application.packageMetadataChanged = true;
4165
+ application.logger.debug?.(`Deployment ${artifactId} is already live; nothing to swap`);
4166
+ return;
4167
+ }
3855
4168
  await activateStagedArtifact(application, artifactId, previousPackageMetadata, options);
3856
4169
  return;
3857
4170
  }
@@ -3896,8 +4209,23 @@ async function prepareApplication(application, options = {}) {
3896
4209
  await syncArtifactAncestors(candidateDeploymentDirPath(application.dirPath, artifactId));
3897
4210
  return;
3898
4211
  }
3899
- await markCandidateComplete(application.dirPath, artifactId, application.name);
3900
4212
  const declared = options.describeArtifact?.();
4213
+ // The directory outlives the swap as the record a later displacement puts this release back into;
4214
+ // a link has no bytes of its own to put back.
4215
+ const described = declared && !(await (0, promises_1.lstat)(candidateDirPath)).isSymbolicLink() ? declared : undefined;
4216
+ if (described) {
4217
+ await writeArtifactDescriptor(application.dirPath, artifactId, {
4218
+ v: ARTIFACT_DESCRIPTOR_VERSION,
4219
+ component: application.name,
4220
+ rootConfig: described.rootConfig,
4221
+ installationIsOpaque: application.installationIsOpaque,
4222
+ isolated: described.isolated,
4223
+ });
4224
+ }
4225
+ await markCandidateComplete(application.dirPath, artifactId, application.name);
4226
+ // The record is meant to outlive the swap, so its own entry has to be on storage, as a stage's is.
4227
+ if (described)
4228
+ await syncArtifactAncestors(candidateDeploymentDirPath(application.dirPath, artifactId));
3901
4229
  await activateCandidateApplication(application, artifactId, {
3902
4230
  rootConfig: declared ? (0, rootConfigPublication_ts_1.rootConfigEffectFromDeclaration)(declared.rootConfig) : { kind: 'keep' },
3903
4231
  });
@@ -3969,11 +4297,14 @@ async function activateStagedArtifact(application, artifactId, previousPackageMe
3969
4297
  throw unusable('an activation of it is unsettled');
3970
4298
  }
3971
4299
  const candidateStat = await presentOrAbsent(candidateDirPath);
3972
- if (!candidateStat || !candidateStat.isDirectory()) {
4300
+ // The record of a release that is neither live nor kept here: nothing on this node answers to the id.
4301
+ if (!candidateStat)
4302
+ throw missing('that release is no longer on this node');
4303
+ if (!candidateStat.isDirectory()) {
3973
4304
  // Unlike an immediate deploy, a symlink is refused: a `file:` directory is linked rather than
3974
4305
  // copied, so what it points at now is not what was certified. Staging rejects the source for the
3975
4306
  // same reason; this is the other end of the same rule, for an artifact staged by an older build.
3976
- throw unusable('its build tree is missing or is a link rather than a copy');
4307
+ throw unusable('its build tree is a link rather than a copy');
3977
4308
  }
3978
4309
  const descriptor = await readArtifactDescriptor(deploymentDirPath, application.name);
3979
4310
  if (!descriptor)