@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
@@ -2,7 +2,9 @@ import { ClientError } from '../utility/errors/hdbError.ts';
2
2
  import {
3
3
  compileFullTextDefinition,
4
4
  compileFullTextDefinitions,
5
+ compileFullTextFields,
5
6
  compileValidFullTextDefinitions,
7
+ persistedFullTextIndexNames,
6
8
  sortFullTextDefinitions,
7
9
  type FullTextDefinition,
8
10
  type FullTextSchemaAttribute,
@@ -22,6 +24,49 @@ export function definitionsEqual(left: unknown, right: unknown): boolean {
22
24
  return serializeFullTextState(left ?? []) === serializeFullTextState(right ?? []);
23
25
  }
24
26
 
27
+ export function readPersistedFullTextFields(
28
+ values: unknown,
29
+ definitions: unknown,
30
+ attributes: readonly FullTextSchemaAttribute[],
31
+ warn: FullTextWarning
32
+ ): string[] {
33
+ const persistedNames = persistedFullTextIndexNames(definitions);
34
+ if (values === undefined) return persistedNames;
35
+ const names = persistedNames.map((name) => ({ name }));
36
+ try {
37
+ return compileFullTextFields(values, names, attributes, true);
38
+ } catch (error) {
39
+ if (!(error instanceof ClientError)) throw error;
40
+ warn(`Invalid persisted full-text fields; reserving every persisted index name: ${error.message}`);
41
+ return persistedNames;
42
+ }
43
+ }
44
+
45
+ export function mergePeerFullTextFields(
46
+ persistedValues: unknown,
47
+ incomingValues: unknown,
48
+ persistedDefinitions: unknown,
49
+ finalDefinitions: unknown,
50
+ attributes: readonly FullTextSchemaAttribute[],
51
+ warn: FullTextWarning
52
+ ): string[] {
53
+ const retained = readPersistedFullTextFields(persistedValues, persistedDefinitions, attributes, warn);
54
+ const finalNames = persistedFullTextIndexNames(finalDefinitions);
55
+ if (incomingValues === undefined) incomingValues = finalNames;
56
+ try {
57
+ compileFullTextFields(
58
+ incomingValues,
59
+ finalNames.map((name) => ({ name })),
60
+ attributes
61
+ );
62
+ } catch (error) {
63
+ if (!(error instanceof ClientError)) throw error;
64
+ warn(`Ignoring invalid peer full-text fields: ${error.message}`);
65
+ }
66
+ const existingNames = new Set(persistedFullTextIndexNames(persistedDefinitions));
67
+ return [...new Set([...retained, ...finalNames.filter((name) => !existingNames.has(name))])].sort();
68
+ }
69
+
25
70
  export function readPersistedFullTextDefinitions(
26
71
  values: unknown,
27
72
  attributes: readonly FullTextSchemaAttribute[],
@@ -8,7 +8,7 @@ import type { DirectiveNode, NamedTypeNode, StringValueNode, ValueNode } from 'g
8
8
  import { ClientError } from '../utility/errors/hdbError.ts';
9
9
  import { attributeToFragment, type JsonSchemaFragment } from './jsonSchemaTypes.ts';
10
10
  import harperLogger from '../utility/logging/harper_logger.ts';
11
- import { compileFullTextDefinitions } from './fullTextSchema.ts';
11
+ import { compileFullTextDefinitions, compileFullTextFields } from './fullTextSchema.ts';
12
12
  import { validateDecisionSchema } from './models/decision.ts';
13
13
  import { assertDerivedFieldOwnership } from './models/embedHook.ts';
14
14
  import type { DecideConfig } from './models/decideHook.ts';
@@ -253,6 +253,7 @@ async function processGraphQLSchema(
253
253
  attributes,
254
254
  properties: typeProperties,
255
255
  fullTextIndexes: [],
256
+ fullTextFields: [],
256
257
  };
257
258
  if (definition.description?.value) typeDef.description = definition.description.value;
258
259
  types.set(typeName, typeDef);
@@ -287,15 +288,8 @@ async function processGraphQLSchema(
287
288
  if (directive.name.value === 'splitSegments') typeDef.splitSegments = true;
288
289
  if (directive.name.value === 'replicate') typeDef.replicate = true;
289
290
  if (directive.name.value === 'hidden') typeDef.hidden = true;
290
- if (directive.name.value === 'fullText') {
291
- const definition: Record<string, unknown> = Object.create(null);
292
- for (const arg of directive.arguments || []) {
293
- if (Object.hasOwn(definition, arg.name.value))
294
- throw new ClientError(`@fullText declares "${arg.name.value}" more than once`, 400);
295
- definition[arg.name.value] = coerceDirectiveValue(arg.value);
296
- }
297
- typeDef.fullTextIndexes.push(definition);
298
- }
291
+ if (directive.name.value === 'fullText')
292
+ throw new ClientError('@fullText must be declared on a FullText field', 400);
299
293
  if (directive.name.value === 'export') {
300
294
  typeDef.export = true;
301
295
  for (const arg of directive.arguments) {
@@ -318,12 +312,40 @@ async function processGraphQLSchema(
318
312
  };
319
313
  }
320
314
  const typeName = (type as NamedTypeNode).name?.value;
315
+ if (typeName === 'FullText') throw new ClientError('FullText fields require @fullText', 400);
321
316
  const property = { type: typeName };
322
317
  Object.defineProperty(property, 'location', { value: type.loc.startToken });
323
318
  return property;
324
319
  }
325
320
  const attributesObject = {};
326
321
  for (const field of definition.fields) {
322
+ const name = field.name.value;
323
+ const fullTextDirectives = field.directives.filter(({ name }) => name.value === 'fullText');
324
+ if (fullTextDirectives.length > 0) {
325
+ if (fullTextDirectives.length !== 1)
326
+ throw new ClientError(`Full-text field "${name}" requires exactly one @fullText directive`, 400);
327
+ if (field.type.kind !== 'NamedType' || field.type.name.value !== 'FullText')
328
+ throw new ClientError(`@fullText field "${name}" requires the nullable FullText type`, 400);
329
+ for (const directive of field.directives) {
330
+ if (directive.name.value !== 'fullText')
331
+ throw new ClientError(
332
+ `Full-text field "${name}" cannot use @${directive.name.value}; configure permissions and record values on its source fields`,
333
+ 400
334
+ );
335
+ }
336
+ const index: Record<string, unknown> = Object.create(null);
337
+ index.name = name;
338
+ for (const arg of fullTextDirectives[0].arguments || []) {
339
+ if (arg.name.value === 'name')
340
+ throw new ClientError(`@fullText uses the field name "${name}"; remove its name argument`, 400);
341
+ if (Object.hasOwn(index, arg.name.value))
342
+ throw new ClientError(`@fullText declares "${arg.name.value}" more than once`, 400);
343
+ index[arg.name.value] = coerceDirectiveValue(arg.value);
344
+ }
345
+ typeDef.fullTextIndexes.push(index);
346
+ typeDef.fullTextFields.push(name);
347
+ continue;
348
+ }
327
349
  const property = getProperty(field.type);
328
350
  property.name = field.name.value;
329
351
  if (field.description?.value) property.description = field.description.value;
@@ -392,8 +414,6 @@ async function processGraphQLSchema(
392
414
  // Resolved after the field's other directives so @primaryKey / @computed in any
393
415
  // order are seen; the ownership checks below need every field of the type.
394
416
  property.decide = directive;
395
- } else if (directiveName === 'fullText') {
396
- throw new ClientError('@fullText must be declared on a @table type, not on a field', 400);
397
417
  } else if (directiveName === 'relationship') {
398
418
  const relationshipDefinition = {};
399
419
  for (const arg of directive.arguments) {
@@ -447,6 +467,7 @@ async function processGraphQLSchema(
447
467
  if (typeDef.fullTextIndexes.length > 0 && !typeDef.table)
448
468
  throw new ClientError('@fullText is only supported on a @table type', 400);
449
469
  typeDef.fullTextIndexes = compileFullTextDefinitions(typeDef.fullTextIndexes, attributes);
470
+ typeDef.fullTextFields = compileFullTextFields(typeDef.fullTextFields, typeDef.fullTextIndexes, attributes);
450
471
  // Project the array form into the canonical `properties` Record (JSON-Schema-shaped,
451
472
  // keyed by attribute name). Both shapes are co-populated in this single pass;
452
473
  // downstream consumers (MCP, OpenAPI) read whichever form they prefer.
@@ -178,11 +178,38 @@ unit switch does not shift the estimate on its own. The ef term remains the conf
178
178
  not the runtime auto-scaled value, so the planner increasingly underestimates vector traversal cost
179
179
  as an automatically scaled graph grows.
180
180
 
181
+ ## A filtered vector search may admit from an index key set only if the set holds every predicate match
182
+
183
+ `search.ts → planCandidateKeys` turns companion AND conditions a secondary index answers (equality,
184
+ `gt`/`ge`/`lt`/`le`, the `between` family, nested AND/OR over those) into key-only range scans, and the
185
+ HNSW index admits from the keys instead of loading a record per visited node (#2688). A set covering
186
+ every pushed-down condition, with no `rowFilter`/`vectorFilter`, replaces the predicate; otherwise it
187
+ gates the residual predicate. The post-filter re-checks every row, so the set may over-admit but must
188
+ never omit: an omission is unrecoverable and makes results depend on whether the cost model built it.
189
+
190
+ `planCandidateKeyScan` enforces that by running the condition's own `filterByType` predicate on the
191
+ records an index cannot supply: one lacking the attribute, a null one when the index holds no nulls,
192
+ and a `0`, because a number written as `-0` is indexed under an encoding that sorts past every number
193
+ and decodes to a wrong primary key (ordered-binary; index-led queries miss that row too). A condition
194
+ admitting any of them stays on the predicate path. `compareKeys` ranks undefined below every scalar, so
195
+ this declines `lt`/`le` over a number, string or boolean bound, `ge null` and any range covering 0; the
196
+ `lt`/`le` that remain (a BigInt bound) start at `null`, not `searchByIndex`'s `true`, so indexed nulls
197
+ are read. Normalizing `-0` in the encoder or index writer would let zero-covering ranges plan again.
198
+
199
+ Building the set is a cost decision: one index-entry read per match against a record load per visited
200
+ node, where filling `ef` matches at selectivity `s` takes about `ef / s` visits. `KEYS_PER_PREDICATE_VISIT`
201
+ is that ratio at the warm, regression-safe end of the measured range; `MAX_CANDIDATE_KEYS` and half the
202
+ node count cap the synchronous scan. A complete set derives the visit budget from its selectivity,
203
+ widening the `filterExpansion` budget by at most `ALLOW_SET_BUDGET_MAX_WIDENING` and never narrowing it;
204
+ a configured `filterExpansion` stays authoritative. `CandidateKeySet` is the one identity rule for both
205
+ the scan's intersection and admission. A node-id bitset for the native plane is not built: the
206
+ pk → node-id lookup costs more than the record load it saves.
207
+
181
208
  ## Derived-index runtime: committed-log delivery to native index backends (`resources/derivedIndexRuntime.ts`)
182
209
 
183
- A derived index (the native HNSW plane, a future Tantivy full-text index) is a materialized view
184
- that lives outside the record transaction: its apply is native, costs 0.2–1.4 ms per mutation, and
185
- its durability barrier is an `msync` or a segment publish, none of which belong on the commit path.
210
+ A derived index (the native HNSW or Tantivy full-text plane) is a materialized view
211
+ that lives outside the record transaction: its apply is native and its durability barrier is an
212
+ `msync` or a segment publish, neither of which belongs on the commit path.
186
213
  The runtime is the one Harper-side implementation of the delivery protocol in harper#2489: **the
187
214
  transaction log is the durable fact, a commit only wakes a runner, and every backend resumes from an
188
215
  exact cursor into that log.** There is deliberately no second delivery fact — no transactional
@@ -327,16 +354,29 @@ cursor is backend-owned and validation is Harper's.
327
354
  `@harperfast/fulltext/native`; it does not implement another replay or ownership protocol. Harper
328
355
  turns each resolved mutation into one stable document id from `tableId` and the record id's
329
356
  ordered-binary storage-key bytes, and
330
- passes only the schema-selected string and array fields to the wrapper. Harper does not rescan array
331
- contents; the wrapper owns value validation, Tantivy schema, exact frame partitioning, its exclusive writer, segment publication, and file
332
- lifecycle. Harper keeps accepted runtime batches in a 64 MiB bounded queue and submits at most 256
357
+ passes only the schema-selected string and array fields to the wrapper. Harper validates each selected
358
+ value, including every array element, and resolves text Blobs before submission. The wrapper owns
359
+ native document validation, Tantivy schema, exact frame partitioning, its exclusive writer, segment
360
+ publication, and file lifecycle. Harper keeps accepted runtime batches in a 64 MiB bounded queue and submits at most 256
333
361
  records or 5 ms of conversion work per turn, so the runtime's 4096-record chunk cannot become one
334
- long event-loop task. A rebuild chunk without a source-size estimate consumes the adapter's entire
362
+ long event-loop task. Awaited Blob reads yield the event loop and therefore do not consume that
363
+ conversion budget; otherwise ordinary storage latency would fragment a rebuild into small native
364
+ mutation submissions. Blob-bearing slices also have a wall-time ceiling derived from the writer shutdown
365
+ budget, so a run of slow reads cannot keep one drain turn alive through hundreds of per-record read
366
+ timeouts. A rebuild chunk without a source-size estimate consumes the adapter's entire
335
367
  queue-byte allowance, ensuring that only one unknown-size chunk is retained at a time. Wrapper
336
368
  rejections remove the previous document and count it as unindexable; they do not leave stale search
337
369
  content. A projector returning null or no string-valued fields deletes the prior document rather
338
370
  than indexing an empty replacement.
339
371
 
372
+ The elected runner opens one writer per index with two Tantivy indexing threads. The wrapper passes
373
+ the 64 MiB total writer budget to Tantivy's multi-threaded writer, which divides it into a 32 MiB
374
+ arena per indexing thread. Harper does not search through that writer handle, so it keeps the one
375
+ search worker required by the wrapper. Query readers can open on every Harper worker, so each uses
376
+ only the wrapper's minimum search pool for ordinary-versus-expensive query isolation rather than
377
+ scaling with host CPU. Read-only handles reserve search threads and queue bytes, but the wrapper
378
+ does not reserve their validation-only indexing-thread or writer-memory values.
379
+
340
380
  Every runtime flush is a native publication barrier. Full-text activation chooses and benchmarks the
341
381
  runtime flush thresholds; the adapter does not reinterpret a flush because the runtime uses durable
342
382
  cursor progress to bound replay work and transaction-log retention.
@@ -356,7 +396,7 @@ not prove the index files should be reset. Persistent writer unavailability emit
356
396
  or record content. Ownership handoff does not finish until drain and close prove quiescence. A
357
397
  writer-open failure that lands after shutdown begins discards queued work before finalization so it
358
398
  cannot enter another retry drain. Harper
359
- gives native close its own 35-second bound and bounds the complete handoff at 70 seconds. If that proof fails, shutdown rejects and the runtime keeps its
399
+ gives native close its own 35-second bound and bounds the complete handoff at 105 seconds. If that proof fails, shutdown rejects and the runtime keeps its
360
400
  runner lock, preventing a second writer. The underlying native operation continues and a later operator
361
401
  retry attaches to the same shutdown rather than starting a competing close. A cursor that cannot fit
362
402
  the native commit-payload limit is terminal for that backend instance: accepted work is rollback-closed
@@ -373,6 +413,8 @@ Inspection is synchronous and writer-free. The first durable-cursor read in each
373
413
  refreshes native state, while later reads in the same acquisition use the cache; every shutdown path
374
414
  invalidates it, including an owner that received no batch. A refresh failure never falls back to a
375
415
  cached checkpoint because the runtime can publish `ready` before lazy writer-open reconciliation.
416
+ Query readers never use inspection: their committed payload belongs to the native snapshot they
417
+ actually search, while inspection is an ownership-acquisition and recovery primitive.
376
418
  The synchronous backend contract has no retryable acquisition result, so an inspection exception
377
419
  deliberately fails closed: the runtime condemns the generation and rebuilds from authoritative
378
420
  records rather than trusting an unverified cursor. A transient filesystem error can therefore cost
@@ -381,7 +423,10 @@ Missing, cursorless, incompatible, or malformed native state has no usable curso
381
423
  the runtime's ordinary local rebuild from records.
382
424
  Reset first asks the wrapper to retire the live generation atomically, then reclaims only wrapper-
383
425
  validated retired paths. The reset operation is tracked and bounded; shutdown attaches to the same
384
- operation and keeps the runner lock if it cannot prove settlement. Retired-path reclamation is
426
+ operation and keeps the runner lock if it cannot prove settlement. If that operation outlives its
427
+ owner's bound, a successor waits for the same operation to settle and then issues its own epoch's
428
+ reset; it does not spend the remaining rebuild attempts on immediate owner-epoch mismatches.
429
+ Retired-path reclamation is
385
430
  best-effort, serialized per lifecycle, and never extends that reset handoff. The native directory is node-local derived state: restarts reuse it and
386
431
  replay after its payload; replicas independently derive it from their own applied transaction log;
387
432
  backup and restore need only authoritative records and schema. A new or unusable directory serves no
@@ -396,16 +441,120 @@ the catalog first would turn a crash in that interval into an untraceable native
396
441
 
397
442
  Tantivy files are not an opaque encrypted cache. They contain the document-id term dictionary,
398
443
  analyzed term dictionaries, postings, frequencies and optionally positions; `surfaceTerms: true`
399
- also stores the original projected strings needed for surface-term features. Operators must protect
444
+ adds an unstemmed companion term field for surface-term features, but does not store the original
445
+ source values. Operators must protect
400
446
  the full-text directory with the same filesystem controls as Harper data. Removing source records
401
447
  does not erase old segment bytes immediately; normal Tantivy merge/reclamation governs physical
402
448
  removal, and destroying an index uses the wrapper's retirement protocol.
403
449
 
404
- The binding remains unloaded until a full-text declaration is activated. Before activation can
405
- construct this backend, Harper must exact-pin the Fulltext package, document the dependency in
406
- `dependencies.md`, and prove that its native prebuild loads on Linux, macOS, and Windows CI. A
407
- missing or incompatible binding is an activation error; Harper must not silently omit the declared
408
- index.
450
+ The binding remains unloaded until a full-text declaration is activated. Harper exact-pins the
451
+ Fulltext package, documents it in `dependencies.md`, and loads its native prebuild in dedicated
452
+ Linux, macOS, and Windows CI jobs. A missing or incompatible binding is an activation error; Harper
453
+ does not silently omit the declared index.
454
+
455
+ ### Full-text query plane
456
+
457
+ Each declared full-text index can lazily open a read-only native handle beside its derived-index writer. Readers
458
+ are keyed by native path, readiness id and owner epoch. A process-wide atomic publication generation
459
+ invalidates stale handles and notifies peer workers. A warm reader reloads in the background after a
460
+ notification, while every query still samples the generation and reloads on mismatch; notification
461
+ delivery is an optimization, not a correctness dependency. Workers that never query an index open no
462
+ reader or publication subscription. The shared derived-index runtime allocates this publication state
463
+ only for backends with external reader snapshots; HNSW does not pay for it. A waiting query samples the
464
+ generation after its coverage wait, so the data-publication bump that precedes the covering watermark
465
+ is included in reader acquisition. The wrapper refreshes the reader's committed payload with the
466
+ searcher, and Harper keeps that decoded checkpoint on the reader slot so coverage always describes the
467
+ snapshot being searched.
468
+ Coverage-only publications do not change the document set: the runtime permits them only after every
469
+ accepted index mutation through that capture is durable. The shared live watermark may therefore wake a
470
+ waiting query before Tantivy commits the newer restart checkpoint; the existing reader is data-current,
471
+ and the later publication notification reloads its checkpoint. Catalog-scale qualification measures this
472
+ metadata-only reload cadence so unrelated write traffic cannot become a hidden query-plane cost.
473
+ Missing, cursorless or malformed payloads fail closed and request one rebuild. A strict, non-waiting query on a
474
+ cold worker can return retryable lag while it opens the reader; retrying, or using
475
+ `waitForIndexMilliseconds`, allows the aligned reader to finish opening without synchronous filesystem
476
+ inspection. An epoch or query-configuration change requires a replacement handle. A publication reload
477
+ uses the existing handle only while it has no active lease; otherwise Harper opens a replacement and
478
+ retires the old handle after its search finishes. One logical query therefore uses one native snapshot
479
+ across all result pages and highlight tracing. The wrapper's reported query budget bounds that whole
480
+ query, including page fetches, source Blob reads and tracing, rather than restarting for each native
481
+ call. Highlight source materialization is bounded by both the wrapper's trace-record count and source-byte
482
+ limit before Blob reads start. Query leaves with the same field set reuse that materialization; distinct
483
+ field sets remain independently bounded. Requesting highlights from an index that did not enable them is rejected.
484
+ A retired handle accepts no new leases but remains open until its active searches finish.
485
+ Rejected closes remain tracked and are retried during the next pause or close; a fulfilled native
486
+ cleanup warning is logged. Neither replaces an otherwise successful query response.
487
+ One failed reload leaves the last aligned snapshot installed and returns retryable lag. Three
488
+ consecutive reload failures retire that handle so the next query reopens from native storage.
489
+ Table close, drop and schema replacement never force-close an active native handle. They stop waiting
490
+ after the wrapper's query budget plus a small settlement grace and report a retryable teardown failure;
491
+ the retired handle remains registered for reset fencing and closes when the query eventually releases
492
+ its lease. It unregisters only after that drain settles.
493
+
494
+ Reset is destructive, so the elected writer first pauses readers in every worker and waits for their
495
+ active leases to drain. Harper does not force-close a handle that native code may still be using; a
496
+ stuck native request therefore delays reset rather than risking use-after-close. The reset handoff
497
+ allows two reader-coordination bounds plus the native reset bound. Resume carries the
498
+ exact readiness-id and owner-epoch token that paused the path, preventing an old reset from reopening
499
+ readers after a newer owner has paused them. Epochs from different readiness generations are never
500
+ compared because each generation owns an independent counter.
501
+ The runtime publishes `rebuilding` before invoking reset, so a worker created after the pause
502
+ broadcast cannot admit a new reader. A worker that misses resume clears a token after that
503
+ token's own shared readiness reaches ready at the same or a newer owner epoch. A predecessor generation
504
+ that crashes during reset may never publish ready again. A successor that observed the foreign token
505
+ before reaching ready can clear it when that same successor becomes ready: reaching ready requires the
506
+ stable backend runner lock, so the predecessor's reset can no longer be active. A generation that was
507
+ already ready when it observed the token must acquire the runner lock before clearing it. Recovery waits
508
+ if a writer still owns that lock.
509
+ The pause covers every reader on the physical path, including a superseded generation. Each active reset
510
+ retains its own token; stale resumes cannot clear newer tokens. A worker with no query index on the path
511
+ retains no token because it has no reader to fence, and any later attachment remains gated by its
512
+ generation's shared readiness.
513
+ After the last local attachment for a superseded readiness id drains and unregisters, Harper clears
514
+ that id's exact-epoch token from surviving readers on the path and remembers that the id retired. A
515
+ delayed pause for that retired id cannot reinstall its fence on a successor. A readiness id never seen
516
+ on this worker is still accepted, so a successor reset fences a predecessor reader on a worker whose
517
+ schema refresh is late. Closing attachments remain registered until their readers drain, and a duplicate
518
+ attachment for the same readiness id keeps the fence. This closes both handoff directions without
519
+ assuming an ordering between generation-local epochs. Retirement uses an explicit synthetic token that
520
+ still pauses every reader because it is not tied to a generation attachment. If the native
521
+ reader violates its contract by rejecting close, Harper logs the failure and proceeds with reset
522
+ rather than wedging the path indefinitely; the reset therefore assumes that rejected handle is dead.
523
+
524
+ Weights and highlighting are query configuration. Changing either refreshes readers without rotating
525
+ the persisted generation. Analyzer behavior, stop words, positions, surface terms, synonyms, source
526
+ media types, source-field membership and source order are storage identity and require a new generation. The
527
+ wrapper validates that identity on inspection and open, so an older analyzer generation fails closed
528
+ and rebuilds instead of serving mixed tokenization semantics.
529
+
530
+ Highlighting is declaration-controlled and off unless configured. Snippet fragments contain source
531
+ plaintext, so Harper authorizes every highlighted source field before search; selecting
532
+ `$highlights` is also selecting that configured plaintext egress. Harper bounds each native trace call
533
+ by both record count and encoded source bytes and sends only the fields used by that query leaf. A
534
+ single record larger than the native trace limit is rejected before crossing the binding.
535
+
536
+ Search hits carry the source record version. Harper loads the authoritative record through its read
537
+ transaction and omits a hit when that version no longer matches; it never attaches an old score or
538
+ highlight to new content. Bounded searches over-fetch at least 32 and at most 256 native hits per
539
+ page, growing through the native result window only when version checks or structured filters reject
540
+ candidates. Exhausting that window because native versions are stale is retryable index lag; filter-
541
+ only exhaustion remains a client error asking for a narrower query. During bounded lag a smaller
542
+ result can still temporarily omit a recently changed record when the native result set ends before
543
+ the search window is exhausted.
544
+ REST exposes the index coverage header, and callers that require current coverage use
545
+ `maxIndexLagMilliseconds: 0` or `waitForIndexMilliseconds`.
546
+ Full-text score descending is the only supported ordering in this release. Count requests return
547
+ `recordCount: null` and `recordCountExact: false`; the native candidate total cannot become an exact
548
+ Harper count after authorization, structured filtering and source-version checks.
549
+
550
+ A declared Blob source is part of one index document. An oversized, invalid UTF-8 or otherwise
551
+ permanently unusable Blob makes that whole document unindexable rather than publishing a partial
552
+ document with different match semantics. A transient Blob read failure rolls back the accepted
553
+ native batch and replays it. Each source read is time-bounded and replay uses the writer's bounded
554
+ backoff, but an unreadable authoritative record never becomes an index deletion. Publication remains
555
+ behind that record until its source is readable, preserving cursor and publication atomicity at the
556
+ cost of delaying unrelated records in the batch. The normal derived-index lag policy makes prolonged
557
+ failure visible and eventually applies write backpressure rather than silently diverging replicas.
409
558
 
410
559
  ### Bounded delivery
411
560
 
@@ -570,6 +719,9 @@ health transitions. An owner refreshes idle coverage at its flush cadence withou
570
719
  release deadline. A query within the certified age bound reads only shared memory and the monotonic
571
720
  clock; strict or older queries compare the persisted vector with current physical positions. This also
572
721
  certifies an unchanged index after owner release or process restart, when no usable time proof remains.
722
+ An old owner can race with a non-ready transition and restore an earlier coverage time, but consumers
723
+ gate the time on stable ready readiness; after rebuild, the rebuilt anchor still covers that earlier true
724
+ prefix.
573
725
  Concurrent waiters on one worker/index share a single 25 ms poll timer, removed when all resolve,
574
726
  time out, or abort. A first waiter nudges its local runner without changing writer-lag accounting or
575
727
  retrying a deferred batch; an active peer owner already refreshes at flush cadence. No cross-worker
@@ -4,6 +4,7 @@ import { FLOAT32_OPTIONS, pack, unpack } from 'msgpackr';
4
4
  import { loggerWithTag } from '../../utility/logging/logger.ts';
5
5
  import { ClientError, ServerError, DerivedIndexLagError } from '../../utility/errors/hdbError.ts';
6
6
  import type { Id } from '../../resources/ResourceInterface.ts';
7
+ import type { CandidateKeyPlan, CandidateKeySet } from '../search.ts';
7
8
  import {
8
9
  DEFAULT_MAX_INDEX_LAG_MILLISECONDS,
9
10
  MAX_WAIT_FOR_INDEX_MILLISECONDS,
@@ -269,6 +270,20 @@ const MAX_LEVEL = 10; // should give good high-level skip list performance up to
269
270
  // islands are small (bounded by the deleted node's neighborhood), so a probe that visits this many
270
271
  // nodes without reaching the entry point is treated as connected-but-far and repaired conservatively.
271
272
  const PROBE_VISIT_LIMIT = 256;
273
+ // Candidate keys one predicate visit pays for: a key-only index-entry read against a record load
274
+ // and decode. Measured warm at 0.37 us against 2.1 us, rounded up from that 5.7; the same load is
275
+ // ~130 us cold, so the warm end is the regression-safe calibration — over-stating the ratio would
276
+ // build allow-sets for broad filters the predicate already answers cheaply. See DESIGN.md.
277
+ const KEYS_PER_PREDICATE_VISIT = 8;
278
+ // Absolute ceiling on index entries one query may read building an allow-set, whatever the cost
279
+ // model allows. The scan is synchronous, so this bounds both the event-loop stall and the retained
280
+ // key set; past it the predicate path — which is incremental and budget-bounded — is the safer cost.
281
+ const MAX_CANDIDATE_KEYS = 50_000;
282
+ // Filling ef matches at selectivity s costs ~ef/s visits; this is the slack over that.
283
+ const ALLOW_SET_BUDGET_SLACK = 2;
284
+ // How far an allow-set may widen the filterExpansion visit budget. A visit got cheaper, not free:
285
+ // the JS graph still loads a node and computes a distance for each one, synchronously.
286
+ const ALLOW_SET_BUDGET_MAX_WIDENING = 4;
272
287
  type Connection = {
273
288
  id: number;
274
289
  distance: number;
@@ -437,6 +452,10 @@ export class HierarchicalNavigableSmallWorld {
437
452
  // during traversal (predicate-aware / ACORN-style filtering), so companion conditions and RBAC can
438
453
  // be pushed down instead of post-filtering an under-filled candidate set (#1241).
439
454
  filteredSearch = true;
455
+ // Signals that search() also accepts a CandidateKeyPlan and admits from it directly, so an indexed
456
+ // companion condition costs a key-only index scan per matching record instead of a record load and
457
+ // decode per visited node. See DESIGN.md.
458
+ candidateKeyFilter = true;
440
459
  indexStore: any;
441
460
  M: number = 16; // max number of connections per layer
442
461
  efConstruction: number = 100; // size of dynamic candidate list
@@ -457,6 +476,10 @@ export class HierarchicalNavigableSmallWorld {
457
476
  // latency for recall on selective function predicates; 24 fills to roughly 4% selectivity before
458
477
  // the budget binds. Per-query override via the search options.
459
478
  filterExpansion = 24;
479
+ // Whether the schema pinned filterExpansion. An explicit pin is an authoritative cost ceiling and
480
+ // keeps its exact meaning on every path; only an index that never set it gets the
481
+ // selectivity-derived budget an allow-set makes measurable.
482
+ filterExpansionConfigured = false;
460
483
 
461
484
  idIncrementer: BigInt64Array | undefined;
462
485
  distance: (a: number[], b: number[]) => number;
@@ -534,7 +557,10 @@ export class HierarchicalNavigableSmallWorld {
534
557
  if (configuredEfConstructionSearch !== undefined) this.efConstructionSearch = configuredEfConstructionSearch;
535
558
  if (configuredML !== undefined) this.mL = configuredML;
536
559
  if (configuredOptimizeRouting !== undefined) this.optimizeRouting = configuredOptimizeRouting;
537
- if (configuredFilterExpansion !== undefined) this.filterExpansion = configuredFilterExpansion;
560
+ if (configuredFilterExpansion !== undefined) {
561
+ this.filterExpansion = configuredFilterExpansion;
562
+ this.filterExpansionConfigured = true;
563
+ }
538
564
  }
539
565
  if (options?.nativePlane) {
540
566
  const { nativeM, nativeEfConstruction, nativeML, nativeOptimizeRouting, maxNodes, keyCap, layer0Cap } =
@@ -847,6 +873,52 @@ export class HierarchicalNavigableSmallWorld {
847
873
  });
848
874
  }
849
875
 
876
+ /**
877
+ * Run a candidate-key plan when its scans are cheaper than the predicate visits they replace, and
878
+ * return the matching primary keys with whether they alone decide admission. Undefined means
879
+ * "take the predicate path": the plan was too broad, its narrowest term overran the scan budget,
880
+ * or the scan failed — none of which is a fault of the graph.
881
+ */
882
+ private collectCandidateKeys(
883
+ plan: CandidateKeyPlan,
884
+ ef: number,
885
+ predicateMaxVisits: number
886
+ ): { keys: CandidateKeySet; complete: boolean } | undefined {
887
+ const nodeCount = this.approximateNodeCount();
888
+ const estimated = Math.max(1, plan.estimatedCount);
889
+ // Filling ef matches at selectivity s costs ~ef/s visits, and s is estimated/nodeCount, so a
890
+ // broad filter needs few predicate visits however cheap the test that would replace them.
891
+ const predicateVisits = Math.min(predicateMaxVisits, (ef * nodeCount) / estimated);
892
+ let maxKeys = Math.min(Math.ceil(predicateVisits * KEYS_PER_PREDICATE_VISIT), MAX_CANDIDATE_KEYS);
893
+ // An allow-set over a large fraction of the graph is not a filter worth building.
894
+ if (nodeCount > 0) maxKeys = Math.min(maxKeys, Math.max(1, nodeCount >> 1));
895
+ if (estimated > maxKeys) return undefined;
896
+ try {
897
+ return plan.collect(maxKeys) ?? undefined;
898
+ } catch (error) {
899
+ // A sibling index that started rebuilding or closed between planning and the scan is a
900
+ // reason to filter the other way, not to fail — and not to reach the caller's handler,
901
+ // which reads an unclassified search error as plane corruption and requests a rebuild.
902
+ logger.warn?.('could not build the HNSW candidate-key allow-set; using the predicate path', error);
903
+ return undefined;
904
+ }
905
+ }
906
+
907
+ /**
908
+ * Visit budget for a traversal an allow-set decides, derived from the selectivity the set makes
909
+ * measurable rather than from filterExpansion — which exists to bound an admission test expensive
910
+ * enough to be worth bounding, and this one is a set lookup. It only ever widens the
911
+ * filterExpansion budget, never narrows it: a cheaper visit makes a tighter budget buy nothing
912
+ * and cost recall. An empty set keeps that budget, so a zero-match filter returns empty after a
913
+ * bounded descent rather than crawling the graph.
914
+ */
915
+ private allowSetVisitBudget(allowed: number, predicateMaxVisits: number, budgetEf: number): number {
916
+ if (allowed <= 0) return predicateMaxVisits;
917
+ const selectivity = Math.min(1, allowed / Math.max(1, this.approximateNodeCount()));
918
+ const derived = budgetEf * Math.ceil(ALLOW_SET_BUDGET_SLACK / selectivity);
919
+ return Math.min(Math.max(predicateMaxVisits, derived), predicateMaxVisits * ALLOW_SET_BUDGET_MAX_WIDENING);
920
+ }
921
+
850
922
  attachDerivedHost(host: DerivedNativeIndexHost): void {
851
923
  this.derivedHost = host;
852
924
  }
@@ -1796,7 +1868,9 @@ export class HierarchicalNavigableSmallWorld {
1796
1868
  * cheap even when the predicate loads a record.
1797
1869
  */
1798
1870
  private admit(filter: (primaryKey: Id) => boolean, filterState: FilterState | undefined, primaryKey: Id): boolean {
1799
- if (filterState) filterState.filterEvaluations++;
1871
+ // A filter fronted by an allow-set counts its own evaluations: the counter reports how much of
1872
+ // the graph a filtered query had to LOAD, and a membership test loads nothing.
1873
+ if (filterState && !filterState.countsOwnEvaluations) filterState.filterEvaluations++;
1800
1874
  return filter(primaryKey);
1801
1875
  }
1802
1876
 
@@ -1839,6 +1913,7 @@ export class HierarchicalNavigableSmallWorld {
1839
1913
  {
1840
1914
  filter,
1841
1915
  minResults,
1916
+ candidateKeys,
1842
1917
  }: {
1843
1918
  // Predicate-aware traversal (#1241). When provided, only nodes for which `filter(primaryKey)`
1844
1919
  // returns true are admitted to the result list at layer 0; routing is unaffected. Composed by
@@ -1850,6 +1925,8 @@ export class HierarchicalNavigableSmallWorld {
1850
1925
  // (AUTO_EF_MAX) however large the limit was. Raising ef to cover the request keeps `limit`
1851
1926
  // meaningful; the caller pays for what it asked for.
1852
1927
  minResults?: number;
1928
+ // Always optional: the predicate path is the fallback for every plan this declines.
1929
+ candidateKeys?: CandidateKeyPlan;
1853
1930
  } = {}
1854
1931
  ) {
1855
1932
  const waiting = this.filePrimary && waitForIndexMilliseconds > 0;
@@ -1930,15 +2007,53 @@ export class HierarchicalNavigableSmallWorld {
1930
2007
  // second-regime search ef (up to AUTO_EF_CEILING) must not silently quadruple the filtered
1931
2008
  // worst case — the recall decision and the filtered-scan budget are separate decisions. Callers
1932
2009
  // wanting a deeper filtered search set an explicit ef or filterExpansion, and own the cost.
2010
+ const budgetEf = explicitEf || this.efSearchConfigured ? resolvedEf : Math.min(resolvedEf, AUTO_EF_MAX);
2011
+ const predicateMaxVisits =
2012
+ budgetEf * (filterExpansion && filterExpansion > 0 ? filterExpansion : this.filterExpansion);
2013
+ const allowedKeys =
2014
+ filter && candidateKeys ? this.collectCandidateKeys(candidateKeys, resolvedEf, predicateMaxVisits) : undefined;
2015
+ const allowDecides = Boolean(allowedKeys?.complete);
1933
2016
  const filterState: FilterState | undefined = filter
1934
2017
  ? {
1935
- maxVisits:
1936
- (explicitEf || this.efSearchConfigured ? resolvedEf : Math.min(resolvedEf, AUTO_EF_MAX)) *
1937
- (filterExpansion && filterExpansion > 0 ? filterExpansion : this.filterExpansion),
2018
+ maxVisits: predicateMaxVisits,
1938
2019
  nodesVisited: 0,
1939
2020
  filterEvaluations: 0,
2021
+ candidateKeys: allowedKeys?.keys.size,
1940
2022
  }
1941
2023
  : undefined;
2024
+ if (allowedKeys && filter && filterState) {
2025
+ // Both traversals admit by primary key — the plane hands its predicate the key of every
2026
+ // candidate — so the set is a membership test in front of the record load, and a complete
2027
+ // plan removes the load entirely.
2028
+ const allowed = allowedKeys.keys;
2029
+ const admits = (primaryKey: Id) => allowed.has(primaryKey);
2030
+ const residual = filter;
2031
+ const state = filterState;
2032
+ state.countsOwnEvaluations = true;
2033
+ filter = allowDecides
2034
+ ? admits
2035
+ : (primaryKey: Id) => {
2036
+ if (!admits(primaryKey)) return false;
2037
+ state.filterEvaluations++;
2038
+ return residual(primaryKey);
2039
+ };
2040
+ // Only a filter the set DECIDES gets the derived budget: an admitted visit on the residual
2041
+ // path still costs a record decode, so that path keeps today's ceiling. A positive per-query
2042
+ // or any schema-configured filterExpansion is an authoritative cost ceiling and is used
2043
+ // exactly as given; a per-query 0 still means unset.
2044
+ if (allowDecides) {
2045
+ const configured =
2046
+ filterExpansion && filterExpansion > 0
2047
+ ? filterExpansion
2048
+ : this.filterExpansionConfigured
2049
+ ? this.filterExpansion
2050
+ : undefined;
2051
+ state.maxVisits =
2052
+ configured === undefined
2053
+ ? this.allowSetVisitBudget(allowedKeys.keys.size, predicateMaxVisits, budgetEf)
2054
+ : budgetEf * configured;
2055
+ }
2056
+ }
1942
2057
  const rerankK =
1943
2058
  comparator === 'sort' && minResults !== undefined
1944
2059
  ? Math.min(effectiveEf, Math.max(RERANK_MIN, minResults * RERANK_FACTOR))
@@ -2328,6 +2443,9 @@ type FilterState = {
2328
2443
  maxVisits: number;
2329
2444
  nodesVisited: number;
2330
2445
  filterEvaluations: number;
2446
+ candidateKeys?: number;
2447
+ // The filter counts its own record-loading evaluations, so admit() must not count again.
2448
+ countsOwnEvaluations?: boolean;
2331
2449
  };
2332
2450
  /**
2333
2451
  * Attach filtered-traversal counters to the returned entries array so callers (search.ts / explain) can
@@ -2338,6 +2456,8 @@ function withStats<T extends any[]>(entries: T, filterState: FilterState | undef
2338
2456
  if (filterState) {
2339
2457
  Object.defineProperty(entries, 'nodesVisited', { value: filterState.nodesVisited, enumerable: false });
2340
2458
  Object.defineProperty(entries, 'filterEvaluations', { value: filterState.filterEvaluations, enumerable: false });
2459
+ if (filterState.candidateKeys !== undefined)
2460
+ Object.defineProperty(entries, 'candidateKeys', { value: filterState.candidateKeys, enumerable: false });
2341
2461
  }
2342
2462
  return entries;
2343
2463
  }