@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4

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 (311) hide show
  1. package/bin/cliOperations.ts +25 -3
  2. package/bin/run.ts +18 -0
  3. package/components/Application.ts +52 -78
  4. package/components/DESIGN.md +76 -3
  5. package/components/Scope.ts +10 -4
  6. package/components/anthropic/index.ts +10 -1
  7. package/components/bedrock/index.ts +10 -1
  8. package/components/componentLoader.ts +23 -14
  9. package/components/mcp/tools/operations.ts +3 -1
  10. package/components/ollama/index.ts +10 -1
  11. package/components/openai/index.ts +265 -18
  12. package/components/operations.js +40 -1
  13. package/config/configUtils.ts +3 -19
  14. package/dataLayer/DESIGN.md +29 -5
  15. package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
  16. package/dataLayer/restoreMarker.ts +250 -14
  17. package/dataLayer/schema.ts +2 -7
  18. package/dataLayer/schemaDescribe.ts +10 -0
  19. package/dist/bin/cliOperations.js +28 -3
  20. package/dist/bin/cliOperations.js.map +1 -1
  21. package/dist/bin/run.js +19 -0
  22. package/dist/bin/run.js.map +1 -1
  23. package/dist/components/Application.d.ts +8 -2
  24. package/dist/components/Application.js +43 -69
  25. package/dist/components/Application.js.map +1 -1
  26. package/dist/components/Scope.d.ts +1 -1
  27. package/dist/components/Scope.js +9 -4
  28. package/dist/components/Scope.js.map +1 -1
  29. package/dist/components/anthropic/index.d.ts +0 -10
  30. package/dist/components/anthropic/index.js +9 -1
  31. package/dist/components/anthropic/index.js.map +1 -1
  32. package/dist/components/bedrock/index.d.ts +0 -12
  33. package/dist/components/bedrock/index.js +9 -1
  34. package/dist/components/bedrock/index.js.map +1 -1
  35. package/dist/components/componentLoader.js +23 -14
  36. package/dist/components/componentLoader.js.map +1 -1
  37. package/dist/components/mcp/tools/operations.js +1 -1
  38. package/dist/components/mcp/tools/operations.js.map +1 -1
  39. package/dist/components/ollama/index.d.ts +0 -13
  40. package/dist/components/ollama/index.js +9 -1
  41. package/dist/components/ollama/index.js.map +1 -1
  42. package/dist/components/openai/index.d.ts +11 -15
  43. package/dist/components/openai/index.js +224 -12
  44. package/dist/components/openai/index.js.map +1 -1
  45. package/dist/components/operations.d.ts +9 -0
  46. package/dist/components/operations.js +32 -2
  47. package/dist/components/operations.js.map +1 -1
  48. package/dist/config/configUtils.d.ts +1 -2
  49. package/dist/config/configUtils.js +7 -22
  50. package/dist/config/configUtils.js.map +1 -1
  51. package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
  52. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  53. package/dist/dataLayer/restoreMarker.d.ts +25 -3
  54. package/dist/dataLayer/restoreMarker.js +213 -13
  55. package/dist/dataLayer/restoreMarker.js.map +1 -1
  56. package/dist/dataLayer/schema.js +1 -4
  57. package/dist/dataLayer/schema.js.map +1 -1
  58. package/dist/dataLayer/schemaDescribe.js +10 -0
  59. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/json/systemSchema.json +28 -0
  63. package/dist/resources/DatabaseTransaction.d.ts +21 -1
  64. package/dist/resources/DatabaseTransaction.js +147 -8
  65. package/dist/resources/DatabaseTransaction.js.map +1 -1
  66. package/dist/resources/PrimaryRocksDatabase.js +1 -1
  67. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  68. package/dist/resources/Table.d.ts +31 -3
  69. package/dist/resources/Table.js +397 -79
  70. package/dist/resources/Table.js.map +1 -1
  71. package/dist/resources/auditStore.js +16 -10
  72. package/dist/resources/auditStore.js.map +1 -1
  73. package/dist/resources/blob.d.ts +1 -0
  74. package/dist/resources/blob.js +14 -5
  75. package/dist/resources/blob.js.map +1 -1
  76. package/dist/resources/branchDatabase.js +3 -3
  77. package/dist/resources/branchDatabase.js.map +1 -1
  78. package/dist/resources/databaseDropPreparation.d.ts +20 -0
  79. package/dist/resources/databaseDropPreparation.js +106 -0
  80. package/dist/resources/databaseDropPreparation.js.map +1 -0
  81. package/dist/resources/databases.d.ts +26 -21
  82. package/dist/resources/databases.js +1040 -204
  83. package/dist/resources/databases.js.map +1 -1
  84. package/dist/resources/derivedIndexRegistry.d.ts +23 -0
  85. package/dist/resources/derivedIndexRegistry.js +92 -0
  86. package/dist/resources/derivedIndexRegistry.js.map +1 -1
  87. package/dist/resources/derivedIndexRuntime.d.ts +10 -0
  88. package/dist/resources/derivedIndexRuntime.js +136 -25
  89. package/dist/resources/derivedIndexRuntime.js.map +1 -1
  90. package/dist/resources/derivedIndexes.d.ts +39 -0
  91. package/dist/resources/derivedIndexes.js +733 -0
  92. package/dist/resources/derivedIndexes.js.map +1 -0
  93. package/dist/resources/fullTextSchema.d.ts +8 -0
  94. package/dist/resources/fullTextSchema.js +42 -0
  95. package/dist/resources/fullTextSchema.js.map +1 -1
  96. package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
  97. package/dist/resources/fullTextSchemaLifecycle.js +0 -9
  98. package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
  99. package/dist/resources/graphql.js +101 -9
  100. package/dist/resources/graphql.js.map +1 -1
  101. package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
  102. package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
  103. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
  104. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
  105. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
  106. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
  107. package/dist/resources/models/Models.d.ts +26 -3
  108. package/dist/resources/models/Models.js +256 -13
  109. package/dist/resources/models/Models.js.map +1 -1
  110. package/dist/resources/models/TestBackend.d.ts +3 -1
  111. package/dist/resources/models/TestBackend.js +35 -1
  112. package/dist/resources/models/TestBackend.js.map +1 -1
  113. package/dist/resources/models/analyticsTable.d.ts +2 -2
  114. package/dist/resources/models/analyticsTable.js +4 -2
  115. package/dist/resources/models/analyticsTable.js.map +1 -1
  116. package/dist/resources/models/backendHelpers.d.ts +24 -0
  117. package/dist/resources/models/backendHelpers.js +18 -1
  118. package/dist/resources/models/backendHelpers.js.map +1 -1
  119. package/dist/resources/models/backendRegistry.d.ts +10 -21
  120. package/dist/resources/models/backendRegistry.js +74 -18
  121. package/dist/resources/models/backendRegistry.js.map +1 -1
  122. package/dist/resources/models/bootstrap.d.ts +7 -0
  123. package/dist/resources/models/bootstrap.js +36 -4
  124. package/dist/resources/models/bootstrap.js.map +1 -1
  125. package/dist/resources/models/decideHook.d.ts +33 -0
  126. package/dist/resources/models/decideHook.js +105 -0
  127. package/dist/resources/models/decideHook.js.map +1 -0
  128. package/dist/resources/models/decision.d.ts +64 -0
  129. package/dist/resources/models/decision.js +507 -0
  130. package/dist/resources/models/decision.js.map +1 -0
  131. package/dist/resources/models/decisionStore.d.ts +134 -0
  132. package/dist/resources/models/decisionStore.js +350 -0
  133. package/dist/resources/models/decisionStore.js.map +1 -0
  134. package/dist/resources/models/embedHook.d.ts +66 -8
  135. package/dist/resources/models/embedHook.js +228 -61
  136. package/dist/resources/models/embedHook.js.map +1 -1
  137. package/dist/resources/models/generativeDecision.d.ts +58 -0
  138. package/dist/resources/models/generativeDecision.js +315 -0
  139. package/dist/resources/models/generativeDecision.js.map +1 -0
  140. package/dist/resources/models/types.d.ts +232 -4
  141. package/dist/security/authn/oidc/tokenExchange.js +3 -33
  142. package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
  143. package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
  144. package/dist/security/authn/oidc/tokenUseTable.js +28 -0
  145. package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
  146. package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
  147. package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
  148. package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
  149. package/dist/security/certificateVerification/crlVerification.js +77 -165
  150. package/dist/security/certificateVerification/crlVerification.js.map +1 -1
  151. package/dist/security/certificateVerification/types.d.ts +0 -3
  152. package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
  153. package/dist/security/certificateVerification/verificationTables.js +125 -0
  154. package/dist/security/certificateVerification/verificationTables.js.map +1 -0
  155. package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
  156. package/dist/security/certificateVerification/verificationUtils.js +6 -31
  157. package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
  158. package/dist/security/jsLoader.d.ts +5 -0
  159. package/dist/security/jsLoader.js +8 -0
  160. package/dist/security/jsLoader.js.map +1 -1
  161. package/dist/server/Server.d.ts +1 -0
  162. package/dist/server/Server.js.map +1 -1
  163. package/dist/server/http.js +2 -2
  164. package/dist/server/http.js.map +1 -1
  165. package/dist/server/itc/serverHandlers.js +21 -2
  166. package/dist/server/itc/serverHandlers.js.map +1 -1
  167. package/dist/server/jobs/jobOwnership.d.ts +25 -0
  168. package/dist/server/jobs/jobOwnership.js +127 -0
  169. package/dist/server/jobs/jobOwnership.js.map +1 -0
  170. package/dist/server/jobs/jobProcess.js +9 -2
  171. package/dist/server/jobs/jobProcess.js.map +1 -1
  172. package/dist/server/jobs/jobs.js +18 -5
  173. package/dist/server/jobs/jobs.js.map +1 -1
  174. package/dist/server/loadRootComponents.js +10 -0
  175. package/dist/server/loadRootComponents.js.map +1 -1
  176. package/dist/server/serverHelpers/contentTypes.js +11 -1
  177. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  178. package/dist/server/serverHelpers/serverHandlers.js +12 -2
  179. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  180. package/dist/server/threads/itc.d.ts +4 -1
  181. package/dist/server/threads/itc.js +29 -6
  182. package/dist/server/threads/itc.js.map +1 -1
  183. package/dist/server/threads/manageThreads.d.ts +12 -4
  184. package/dist/server/threads/manageThreads.js +241 -119
  185. package/dist/server/threads/manageThreads.js.map +1 -1
  186. package/dist/server/threads/threadServer.d.ts +1 -0
  187. package/dist/server/threads/threadServer.js +15 -1
  188. package/dist/server/threads/threadServer.js.map +1 -1
  189. package/dist/server/threads/windowsProcessTree.d.ts +91 -0
  190. package/dist/server/threads/windowsProcessTree.js +361 -0
  191. package/dist/server/threads/windowsProcessTree.js.map +1 -0
  192. package/dist/server/throttle.d.ts +1 -1
  193. package/dist/server/throttle.js +8 -1
  194. package/dist/server/throttle.js.map +1 -1
  195. package/dist/upgrade/directives/5-3-0.js +53 -46
  196. package/dist/upgrade/directives/5-3-0.js.map +1 -1
  197. package/dist/utility/errors/hdbError.d.ts +10 -1
  198. package/dist/utility/errors/hdbError.js +55 -11
  199. package/dist/utility/errors/hdbError.js.map +1 -1
  200. package/dist/utility/fsync.d.ts +2 -0
  201. package/dist/utility/fsync.js +20 -0
  202. package/dist/utility/fsync.js.map +1 -0
  203. package/dist/utility/globalSchema.d.ts +18 -0
  204. package/dist/utility/hdbTerms.d.ts +3 -0
  205. package/dist/utility/hdbTerms.js +3 -0
  206. package/dist/utility/hdbTerms.js.map +1 -1
  207. package/dist/utility/signalling.d.ts +7 -1
  208. package/dist/utility/signalling.js +27 -3
  209. package/dist/utility/signalling.js.map +1 -1
  210. package/dist/utility/when.d.ts +1 -0
  211. package/dist/utility/when.js +21 -0
  212. package/dist/utility/when.js.map +1 -1
  213. package/dist/validation/configValidator.js +30 -3
  214. package/dist/validation/configValidator.js.map +1 -1
  215. package/index.ts +13 -0
  216. package/json/systemSchema.json +28 -0
  217. package/npm-shrinkwrap.json +151 -55
  218. package/package.json +14 -13
  219. package/resources/DESIGN.md +80 -100
  220. package/resources/DatabaseTransaction.ts +157 -10
  221. package/resources/PrimaryRocksDatabase.ts +2 -2
  222. package/resources/Table.ts +494 -101
  223. package/resources/auditStore.ts +18 -10
  224. package/resources/blob.ts +13 -6
  225. package/resources/branchDatabase.ts +3 -3
  226. package/resources/databaseDropPreparation.ts +120 -0
  227. package/resources/databases.ts +1155 -205
  228. package/resources/derivedIndexRegistry.ts +122 -0
  229. package/resources/derivedIndexRuntime.ts +157 -30
  230. package/resources/derivedIndexes.ts +856 -0
  231. package/resources/fullTextSchema.ts +55 -0
  232. package/resources/fullTextSchemaLifecycle.ts +0 -15
  233. package/resources/graphql.ts +107 -12
  234. package/resources/indexes/DESIGN.md +7 -0
  235. package/resources/indexes/hnswDerivedIndex.ts +0 -260
  236. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
  237. package/resources/models/DESIGN.md +39 -0
  238. package/resources/models/Models.ts +361 -18
  239. package/resources/models/TestBackend.ts +56 -1
  240. package/resources/models/analyticsTable.ts +6 -4
  241. package/resources/models/backendHelpers.ts +17 -0
  242. package/resources/models/backendRegistry.ts +76 -21
  243. package/resources/models/bootstrap.ts +57 -7
  244. package/resources/models/decideHook.ts +135 -0
  245. package/resources/models/decision.ts +505 -0
  246. package/resources/models/decisionStore.ts +419 -0
  247. package/resources/models/embedHook.ts +272 -55
  248. package/resources/models/generativeDecision.ts +459 -0
  249. package/resources/models/types.ts +231 -4
  250. package/security/DESIGN.md +22 -2
  251. package/security/authn/oidc/tokenExchange.ts +6 -34
  252. package/security/authn/oidc/tokenUseTable.ts +27 -0
  253. package/security/certificateVerification/certificateVerificationSource.ts +2 -3
  254. package/security/certificateVerification/crlVerification.ts +96 -174
  255. package/security/certificateVerification/types.ts +0 -3
  256. package/security/certificateVerification/verificationTables.ts +124 -0
  257. package/security/certificateVerification/verificationUtils.ts +8 -32
  258. package/security/jsLoader.ts +9 -1
  259. package/server/DESIGN.md +27 -1
  260. package/server/Server.ts +2 -1
  261. package/server/http.ts +4 -2
  262. package/server/itc/serverHandlers.js +29 -1
  263. package/server/jobs/jobOwnership.ts +89 -0
  264. package/server/jobs/jobProcess.ts +8 -2
  265. package/server/jobs/jobs.ts +15 -4
  266. package/server/loadRootComponents.js +11 -0
  267. package/server/serverHelpers/contentTypes.ts +13 -1
  268. package/server/serverHelpers/serverHandlers.js +11 -2
  269. package/server/threads/itc.js +32 -6
  270. package/server/threads/manageThreads.js +268 -122
  271. package/server/threads/threadServer.js +13 -1
  272. package/server/threads/windowsProcessTree.ts +409 -0
  273. package/server/throttle.ts +11 -1
  274. package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
  275. package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
  276. package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
  277. package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
  278. package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
  279. package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
  280. package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
  281. package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
  282. package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
  283. package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
  284. package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
  285. package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
  286. package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
  287. package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
  288. package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
  289. package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
  290. package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
  291. package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
  292. package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
  293. package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
  294. package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
  295. package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
  296. package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
  297. package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
  298. package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
  299. package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
  300. package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
  301. package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
  302. package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
  303. package/studio/web/index.html +1 -1
  304. package/upgrade/directives/5-3-0.ts +61 -54
  305. package/utility/DESIGN.md +5 -1
  306. package/utility/errors/hdbError.ts +56 -8
  307. package/utility/fsync.ts +16 -0
  308. package/utility/hdbTerms.ts +3 -0
  309. package/utility/signalling.ts +33 -4
  310. package/utility/when.ts +22 -0
  311. package/validation/configValidator.ts +36 -7
@@ -45,6 +45,61 @@ export type FullTextDefinition = {
45
45
  highlighting?: FullTextHighlighting;
46
46
  };
47
47
 
48
+ export type FullTextStorageDefinition = Pick<
49
+ FullTextDefinition,
50
+ 'analyzer' | 'stopWords' | 'positions' | 'surfaceTerms'
51
+ > & {
52
+ fields: Array<Pick<FullTextSource, 'name' | 'weight'>>;
53
+ };
54
+
55
+ export type FullTextIndexGenerations = Record<string, string>;
56
+
57
+ /** Names are enough to locate native storage even when source attributes are missing or stale. */
58
+ export function persistedFullTextIndexNames(values: unknown): string[] {
59
+ if (!Array.isArray(values)) return [];
60
+ const names = new Set<string>();
61
+ for (const value of values) {
62
+ if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
63
+ const name = (value as { name?: unknown }).name;
64
+ if (typeof name === 'string' && name.length > 0) names.add(name);
65
+ }
66
+ return [...names].sort();
67
+ }
68
+
69
+ export function fullTextStorageDefinition(definition: FullTextDefinition): FullTextStorageDefinition {
70
+ return {
71
+ fields: definition.fields.map(({ name, weight }) => ({ name, weight })),
72
+ analyzer: definition.analyzer,
73
+ stopWords: definition.stopWords,
74
+ positions: definition.positions,
75
+ surfaceTerms: definition.surfaceTerms,
76
+ };
77
+ }
78
+
79
+ export function reconcileFullTextIndexGenerations(
80
+ previousDefinitions: readonly FullTextDefinition[],
81
+ previousGenerations: unknown,
82
+ nextDefinitions: readonly FullTextDefinition[],
83
+ createGeneration: () => string
84
+ ): FullTextIndexGenerations {
85
+ const previousByName = new Map(previousDefinitions.map((definition) => [definition.name, definition]));
86
+ const durable =
87
+ previousGenerations && typeof previousGenerations === 'object' && !Array.isArray(previousGenerations)
88
+ ? (previousGenerations as Record<string, unknown>)
89
+ : Object.create(null);
90
+ const generations: FullTextIndexGenerations = Object.create(null);
91
+ for (const definition of nextDefinitions) {
92
+ const previous = previousByName.get(definition.name);
93
+ const generation = durable[definition.name];
94
+ const storageUnchanged =
95
+ previous &&
96
+ JSON.stringify(fullTextStorageDefinition(previous)) === JSON.stringify(fullTextStorageDefinition(definition));
97
+ generations[definition.name] =
98
+ storageUnchanged && typeof generation === 'string' && generation.length > 0 ? generation : createGeneration();
99
+ }
100
+ return generations;
101
+ }
102
+
48
103
  export type FullTextSchemaAttribute = {
49
104
  name: string;
50
105
  type?: string;
@@ -34,21 +34,6 @@ export function readPersistedFullTextDefinitions(
34
34
  });
35
35
  }
36
36
 
37
- /**
38
- * Produces the catalog state that is safe while source descriptors transition. The caller publishes
39
- * the requested definitions only after their source descriptors are durable.
40
- */
41
- export function safeInterimFullTextDefinitions(
42
- values: unknown,
43
- attributes: readonly FullTextSchemaAttribute[],
44
- requested: readonly FullTextDefinition[]
45
- ): FullTextDefinition[] {
46
- const requestedByName = new Map(requested.map((definition) => [definition.name, definition]));
47
- return compileValidFullTextDefinitions(values ?? [], attributes, () => {}).filter((definition) =>
48
- definitionsEqual(definition, requestedByName.get(definition.name))
49
- );
50
- }
51
-
52
37
  export function retainFullTextDefinitions(
53
38
  values: unknown,
54
39
  durableAttributes: readonly FullTextSchemaAttribute[],
@@ -4,11 +4,15 @@ import { scopedTableFactory, table } from './databases.ts';
4
4
  import { getWorkerIndex } from '../server/threads/manageThreads.js';
5
5
  import { thisThreadOwnsApplication } from '../server/threads/isolatedApplications.ts';
6
6
  import { Resources } from './Resources.ts';
7
- import type { NamedTypeNode, StringValueNode, ValueNode } from 'graphql';
7
+ import type { DirectiveNode, NamedTypeNode, StringValueNode, ValueNode } from 'graphql';
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
11
  import { compileFullTextDefinitions } from './fullTextSchema.ts';
12
+ import { validateDecisionSchema } from './models/decision.ts';
13
+ import { assertDerivedFieldOwnership } from './models/embedHook.ts';
14
+ import type { DecideConfig } from './models/decideHook.ts';
15
+ import type { DecisionLeaf } from './models/types.ts';
12
16
 
13
17
  const PRIMITIVE_TYPES = ['ID', 'Int', 'Float', 'Long', 'String', 'Boolean', 'Date', 'Bytes', 'Any', 'BigInt', 'Blob'];
14
18
 
@@ -41,6 +45,101 @@ function coerceDirectiveValue(node: ValueNode): any {
41
45
  }
42
46
  }
43
47
 
48
+ const DECIDE_STRING_ARGS = new Set(['source', 'model', 'confidence', 'decision', 'instructions']);
49
+ const DECIDE_INT_ARGS = new Set(['minimum', 'maximum']);
50
+
51
+ /**
52
+ * `@decide(source, values | minimum/maximum, model?, confidence?, decision?, instructions?)`: the attribute
53
+ * type selects the decision leaf (String → the `values` enum, Boolean, Int → a bounded range),
54
+ * and the leaf is validated here so a bad closed set fails the schema load, not the first write.
55
+ * Field references (source, confidence, decision) are checked once every field of the type is known.
56
+ */
57
+ function parseDecideDirective(directive: DirectiveNode, property: any): DecideConfig {
58
+ const target = `@decide on "${property.name}"`;
59
+ const args: {
60
+ source?: string;
61
+ model?: string;
62
+ confidence?: string;
63
+ decision?: string;
64
+ instructions?: string;
65
+ values?: string[];
66
+ minimum?: number;
67
+ maximum?: number;
68
+ } = {};
69
+ for (const arg of directive.arguments || []) {
70
+ const name = arg.name.value;
71
+ if (Object.hasOwn(args, name)) throw new ClientError(`${target} declares "${name}" more than once`, 400);
72
+ const value = arg.value;
73
+ if (DECIDE_STRING_ARGS.has(name)) {
74
+ if (value.kind !== 'StringValue')
75
+ throw new ClientError(`@decide(${name}: ...) on "${property.name}" expects a string literal`, 400);
76
+ args[name] = value.value;
77
+ } else if (DECIDE_INT_ARGS.has(name)) {
78
+ if (value.kind !== 'IntValue')
79
+ throw new ClientError(`@decide(${name}: ...) on "${property.name}" expects an integer literal`, 400);
80
+ args[name] = Number(value.value);
81
+ } else if (name === 'values') {
82
+ if (value.kind !== 'ListValue' || value.values.some((item) => item.kind !== 'StringValue'))
83
+ throw new ClientError(`@decide(values: ...) on "${property.name}" expects a list of string literals`, 400);
84
+ args.values = value.values.map((item) => (item as StringValueNode).value);
85
+ } else throw new ClientError(`${target} has an unknown argument "${name}"`, 400);
86
+ }
87
+ if (!args.source) throw new ClientError(`${target} requires a "source" argument`, 400);
88
+ for (const name of ['confidence', 'decision'] as const)
89
+ if (args[name] === '') throw new ClientError(`${target}: "${name}" must name a field`, 400);
90
+ const hasRange = args.minimum !== undefined || args.maximum !== undefined;
91
+ let schema: DecisionLeaf;
92
+ switch (property.type) {
93
+ case 'String':
94
+ if (!args.values)
95
+ throw new ClientError(`${target} requires "values" (the allowed strings) on a String attribute`, 400);
96
+ if (hasRange) throw new ClientError(`${target} takes "minimum" and "maximum" only on an Int attribute`, 400);
97
+ schema = { enum: args.values };
98
+ break;
99
+ case 'Boolean':
100
+ if (args.values || hasRange)
101
+ throw new ClientError(`${target} takes no "values", "minimum" or "maximum" on a Boolean attribute`, 400);
102
+ schema = { type: 'boolean' };
103
+ break;
104
+ case 'Int':
105
+ if (args.values)
106
+ throw new ClientError(`${target} takes "minimum" and "maximum", not "values", on an Int attribute`, 400);
107
+ if (args.minimum === undefined || args.maximum === undefined)
108
+ throw new ClientError(`${target} requires both "minimum" and "maximum" on an Int attribute`, 400);
109
+ // Table validation holds an Int to 32 bits; a wider bound would load and then fail every write.
110
+ for (const [name, bound] of Object.entries({ minimum: args.minimum, maximum: args.maximum }))
111
+ if (bound >> 0 !== bound)
112
+ throw new ClientError(`${target}: "${name}" must be an integer from -2147483648 to 2147483647`, 400);
113
+ schema = { type: 'integer', minimum: args.minimum, maximum: args.maximum };
114
+ break;
115
+ default:
116
+ throw new ClientError(
117
+ `${target} requires a String, Boolean or Int attribute type; got "${property.type === 'array' ? '[...]' : property.type}"`,
118
+ 400
119
+ );
120
+ }
121
+ try {
122
+ validateDecisionSchema(schema);
123
+ } catch (err) {
124
+ throw new ClientError(`${target}: ${(err as Error).message}`, 400);
125
+ }
126
+ if (property.nullable === false)
127
+ throw new ClientError(`${target} cannot be declared non-null: a null source clears the attribute`, 400);
128
+ if (property.isPrimaryKey || property.computed)
129
+ throw new ClientError(`${target} cannot combine with @primaryKey or @computed`, 400);
130
+ const fields = new Set([property.name, args.source, args.confidence, args.decision].filter(Boolean));
131
+ if (fields.size !== 2 + (args.confidence ? 1 : 0) + (args.decision ? 1 : 0))
132
+ throw new ClientError(
133
+ `${target}: the attribute, "source", "confidence" and "decision" must be different fields`,
134
+ 400
135
+ );
136
+ const config: DecideConfig = { source: args.source, model: args.model ?? 'default', schema };
137
+ if (args.confidence) config.confidence = args.confidence;
138
+ if (args.decision) config.decision = args.decision;
139
+ if (args.instructions) config.instructions = args.instructions;
140
+ return config;
141
+ }
142
+
44
143
  if (!server.knownGraphQLDirectives) {
45
144
  server.knownGraphQLDirectives = [];
46
145
  }
@@ -52,6 +151,7 @@ server.knownGraphQLDirectives.push(
52
151
  'indexed',
53
152
  'computed',
54
153
  'embed',
154
+ 'decide',
55
155
  'fullText',
56
156
  'relationship',
57
157
  'createdTime',
@@ -288,6 +388,10 @@ async function processGraphQLSchema(
288
388
  property.version = `embed:${embedDefinition.model}`;
289
389
  }
290
390
  }
391
+ } else if (directiveName === 'decide') {
392
+ // Resolved after the field's other directives so @primaryKey / @computed in any
393
+ // order are seen; the ownership checks below need every field of the type.
394
+ property.decide = directive;
291
395
  } else if (directiveName === 'fullText') {
292
396
  throw new ClientError('@fullText must be declared on a @table type, not on a field', 400);
293
397
  } else if (directiveName === 'relationship') {
@@ -337,18 +441,9 @@ async function processGraphQLSchema(
337
441
  400
338
442
  );
339
443
  }
444
+ if (property.decide) property.decide = parseDecideDirective(property.decide, property);
340
445
  }
341
- // @embed source must reference a declared field; a typo would silently leave
342
- // the vector column unpopulated (the source key never appears in write payloads).
343
- for (const prop of attributes as any[]) {
344
- // Object.hasOwn (not `in`): `attributesObject` is a plain object, so `in` would
345
- // match inherited prototype keys (toString, constructor) and pass a bad source.
346
- if (prop.embed && !Object.hasOwn(attributesObject, prop.embed.source))
347
- throw new ClientError(
348
- `@embed on "${prop.name}" references unknown source field "${prop.embed.source}"`,
349
- 400
350
- );
351
- }
446
+ assertDerivedFieldOwnership(attributes as any[]);
352
447
  if (typeDef.fullTextIndexes.length > 0 && !typeDef.table)
353
448
  throw new ClientError('@fullText is only supported on a @table type', 400);
354
449
  typeDef.fullTextIndexes = compileFullTextDefinitions(typeDef.fullTextIndexes, attributes);
@@ -387,6 +387,13 @@ replay after its payload; replicas independently derive it from their own applie
387
387
  backup and restore need only authoritative records and schema. A new or unusable directory serves no
388
388
  full-text queries until rebuild and catch-up publish `ready`.
389
389
 
390
+ A table drop keeps the tombstoned catalog descriptor, including its native index names, until
391
+ retirement completes. The drop broadcast first unloads peer attachments and awaits their writer
392
+ shutdown; only then may the wrapper retire native storage, followed by RocksDB column families and
393
+ catalog rows. If native retirement cannot prove success, the table remains logically dropped and
394
+ unloaded but its tombstone stays durable so restart or same-name creation retries cleanup. Removing
395
+ the catalog first would turn a crash in that interval into an untraceable native-directory leak.
396
+
390
397
  Tantivy files are not an opaque encrypted cache. They contain the document-id term dictionary,
391
398
  analyzed term dictionaries, postings, frequencies and optionally positions; `surfaceTerms: true`
392
399
  also stores the original projected strings needed for surface-term features. Operators must protect
@@ -5,9 +5,7 @@ import type { RocksTransactionLogStore } from '../RocksTransactionLogStore.ts';
5
5
  import {
6
6
  DERIVED_INDEX_ACCEPTED,
7
7
  DERIVED_INDEX_DEFERRED,
8
- DerivedIndexRuntime,
9
8
  readDerivedIndexReadiness,
10
- readDerivedIndexCoverage,
11
9
  sameDerivedIndexPositions,
12
10
  type DerivedIndexBackend,
13
11
  type DerivedIndexBackendHost,
@@ -42,9 +40,6 @@ const QUEUE_CAPACITY_BYTES = 64 * 1024 * 1024;
42
40
  const APPLY_SLICE_MILLIS = 5;
43
41
  const BARRIER_IDLE_MULTIPLE = 3;
44
42
  const BARRIER_IDLE_CEILING_MILLISECONDS = 7_500;
45
- // Writes to an index this far behind fail with a retryable 503 (see the runtime's lag policy).
46
- const DEFAULT_MAX_LAG_MILLISECONDS = 30_000;
47
-
48
43
  /**
49
44
  * The native index methods the backend drives. `applyDerivedValue` inserts, replaces or removes
50
45
  * one primary key's vector and stages its mapping as pending; `flushDerived` is the durability
@@ -277,261 +272,6 @@ export class HnswDerivedIndexBackend implements DerivedIndexBackend {
277
272
  }
278
273
  }
279
274
 
280
- // One generation owns each physical backend; destructive cleanup also awaits superseded generations.
281
- type RegisteredTable = { current: { Table: any }; owners: Set<{ Table: any }> };
282
- type RegisteredBackend = {
283
- tableId: number;
284
- settle: () => Promise<void>;
285
- };
286
- type Registered = {
287
- runtime: DerivedIndexRuntime;
288
- tables: Map<number, RegisteredTable>;
289
- backends: Map<string, RegisteredBackend>;
290
- tableBackends: Map<number, Set<RegisteredBackend>>;
291
- droppingTables: Set<number>;
292
- retryUnavailable: Set<string>;
293
- };
294
- const runtimes = new WeakMap<object, Registered>();
295
- const retryUnavailableByStore = new Set<string>();
296
-
297
- function retryUnavailableKey(auditStore: RocksTransactionLogStore, backendId: string): string {
298
- return `${auditStore.rootStore.path}\0${backendId}`;
299
- }
300
-
301
- function markUnavailableRetry(registered: Registered, auditStore: RocksTransactionLogStore, backendId: string): void {
302
- registered.retryUnavailable.add(backendId);
303
- retryUnavailableByStore.add(retryUnavailableKey(auditStore, backendId));
304
- }
305
-
306
- function consumeUnavailableRetry(
307
- registered: Registered,
308
- auditStore: RocksTransactionLogStore,
309
- backendId: string
310
- ): boolean {
311
- const local = registered.retryUnavailable.delete(backendId);
312
- const storeScoped = retryUnavailableByStore.delete(retryUnavailableKey(auditStore, backendId));
313
- return local || storeScoped;
314
- }
315
-
316
- function runtimeFor(auditStore: RocksTransactionLogStore): Registered {
317
- let registered = runtimes.get(auditStore);
318
- if (registered) return registered;
319
- const tables = new Map<number, RegisteredTable>();
320
- const runtime = new DerivedIndexRuntime(
321
- auditStore,
322
- (tableId, recordId) => {
323
- const entry = tables.get(tableId)?.current.Table.primaryStore.getEntry(recordId);
324
- return entry?.value == null ? undefined : { version: entry.version, value: entry.value };
325
- },
326
- {
327
- scanRecords: (tableId) =>
328
- tables
329
- .get(tableId)!
330
- .current.Table.primaryStore.getRange({ versions: true, snapshot: false })
331
- .map(({ key, value, version }) => ({ recordId: key, version, value })),
332
- }
333
- );
334
- registered = {
335
- runtime,
336
- tables,
337
- backends: new Map(),
338
- tableBackends: new Map(),
339
- droppingTables: new Set(),
340
- retryUnavailable: new Set(),
341
- };
342
- runtimes.set(auditStore, registered);
343
- return registered;
344
- }
345
-
346
- const warnedAuditIndexes = new Set<string>();
347
-
348
- /**
349
- * Register every post-commit custom index of a table with the shared derived-index runtime of its
350
- * database. Returns the release for the table's registrations, or undefined when it has none. Runs
351
- * on every worker; the runtime elects one owner per index.
352
- */
353
- export function attachDerivedIndexes(Table: any):
354
- | {
355
- close(dropping?: boolean): Promise<void>;
356
- restoreAfterFailedDrop(): ReturnType<typeof attachDerivedIndexes>;
357
- completeDrop(dropped?: boolean): void;
358
- }
359
- | undefined {
360
- const hnswAttributes = Table.attributes.filter(
361
- (attribute: any) => attribute.indexed?.type === 'HNSW' && Table.indices[attribute.name]?.customIndex
362
- );
363
- const attributes = hnswAttributes.filter((attribute: any) => Table.indices[attribute.name]?.customIndex?.postCommit);
364
- if (Table.audit !== true && attributes.length > 0) {
365
- throw new ClientError(
366
- `Table '${Table.databaseName}.${Table.tableName}' must enable audit logging before using a post-commit derived index`
367
- );
368
- }
369
- if (!Table.auditStore) return;
370
- const auditStore = Table.auditStore as RocksTransactionLogStore;
371
- const registered = runtimeFor(auditStore);
372
- if (registered.droppingTables.has(Table.tableId)) return;
373
- for (const attribute of hnswAttributes) {
374
- const indexStore = Table.indices[attribute.name];
375
- const id = `hnsw:${indexStore.name}`;
376
- // A transient JS-backed declaration can precede the first native registration while a
377
- // schema is being installed. Only an explicit transition away from an already-registered
378
- // native backend should make a later native registration eligible to rearm an exhausted
379
- // rebuild budget.
380
- if (attribute.indexed.nativePlane === false && !indexStore.customIndex.postCommit && registered.backends.has(id))
381
- markUnavailableRetry(registered, auditStore, id);
382
- }
383
- if (attributes.length === 0) return;
384
- const installed = { Table };
385
- let registeredTable = registered.tables.get(Table.tableId);
386
- if (registeredTable) {
387
- registeredTable.owners.add(installed);
388
- registeredTable.current = installed;
389
- } else {
390
- registeredTable = { current: installed, owners: new Set([installed]) };
391
- registered.tables.set(Table.tableId, registeredTable);
392
- }
393
- const releases: Array<() => Promise<void>> = [];
394
- const backendIds: string[] = [];
395
- const registeredBackends = new Map<string, RegisteredBackend>();
396
- for (const attribute of attributes) {
397
- const indexStore = Table.indices[attribute.name];
398
- const index = indexStore.customIndex as DerivedNativeIndex & { postCommit: true };
399
- const id = `hnsw:${indexStore.name}`;
400
- const warningKey = `${Table.databaseName}.${Table.tableName}.${indexStore.name}`;
401
- if (!warnedAuditIndexes.has(warningKey)) {
402
- warnedAuditIndexes.add(warningKey);
403
- logger.warn?.(
404
- `Derived index ${indexStore.name} requires auditing; the audit API retains full record history for the configured retention window`
405
- );
406
- }
407
- const resolver = Table.propertyResolvers?.[attribute.name];
408
- const label = `Vector for attribute "${attribute.name}"`;
409
- index.attachDerivedHost({
410
- readiness: () => registered.runtime.getReadiness(id),
411
- coverage: (maxLagMilliseconds) =>
412
- readDerivedIndexCoverage(
413
- auditStore,
414
- id,
415
- () => indexStore.getSync(DERIVED_INDEX_CURSOR_KEY),
416
- maxLagMilliseconds
417
- ),
418
- requestRebuild: () => registered.runtime.requestRebuild(id),
419
- waitForCoverage: (since, timeout, signal) => registered.runtime.waitForCoverage(id, since, timeout, signal),
420
- });
421
- let predecessor = registered.backends.get(id);
422
- const inherited = predecessor;
423
- const settlePredecessor = () => {
424
- const current = predecessor;
425
- if (!current) return Promise.resolve();
426
- const settled = current.settle();
427
- settled.then(
428
- () => {
429
- if (predecessor === current) predecessor = undefined;
430
- },
431
- () => {}
432
- );
433
- return settled;
434
- };
435
- const predecessorSettled = settlePredecessor();
436
- predecessorSettled.catch(() => {});
437
- if (registered.backends.get(id) === inherited) registered.backends.delete(id);
438
- const release = registered.runtime.register({
439
- backend: new HnswDerivedIndexBackend(id, index),
440
- projections: new Map([
441
- [
442
- Table.tableId,
443
- (record: any) => {
444
- const vector = resolver ? resolver(record) : record[attribute.name];
445
- if (vector == null) return undefined;
446
- index.assertDerivedValue(vector, label);
447
- return vector;
448
- },
449
- ],
450
- ]),
451
- options: { maxLagMilliseconds: attribute.indexed?.maxLagMilliseconds ?? DEFAULT_MAX_LAG_MILLISECONDS },
452
- });
453
- let settling: Promise<void> | undefined;
454
- const registeredBackend: RegisteredBackend = {
455
- tableId: Table.tableId,
456
- settle: () => {
457
- if (settling) return settling;
458
- const attempt = Promise.allSettled([settlePredecessor(), release()]).then((results) => {
459
- if (results[1].status === 'fulfilled') {
460
- if (registered.backends.get(id) === registeredBackend) registered.backends.delete(id);
461
- const tableBackends = registered.tableBackends.get(Table.tableId);
462
- tableBackends?.delete(registeredBackend);
463
- if (tableBackends?.size === 0) registered.tableBackends.delete(Table.tableId);
464
- }
465
- const failures = results
466
- .filter((result): result is PromiseRejectedResult => result.status === 'rejected')
467
- .map((result) => result.reason);
468
- if (failures.length === 1) throw failures[0];
469
- if (failures.length) throw new AggregateError(failures, `Could not settle HNSW backend '${id}'`);
470
- });
471
- settling = attempt;
472
- attempt.catch(() => {
473
- if (settling === attempt) settling = undefined;
474
- });
475
- return attempt;
476
- },
477
- };
478
- let tableBackends = registered.tableBackends.get(Table.tableId);
479
- if (!tableBackends) registered.tableBackends.set(Table.tableId, (tableBackends = new Set()));
480
- tableBackends.add(registeredBackend);
481
- registered.backends.set(id, registeredBackend);
482
- registeredBackends.set(id, registeredBackend);
483
- const retryUnavailable = consumeUnavailableRetry(registered, auditStore, id);
484
- if (retryUnavailable && registered.runtime.getReadiness(id).state === 'unavailable')
485
- registered.runtime.requestRebuild(id);
486
- backendIds.push(id);
487
- releases.push(() => registeredBackend.settle());
488
- }
489
- return {
490
- async close(dropping = false) {
491
- if (dropping) registered.droppingTables.add(Table.tableId);
492
- const settlements = dropping
493
- ? [...(registered.tableBackends.get(Table.tableId) ?? [])].map((backend) => backend.settle())
494
- : releases.map((release) => release());
495
- const tableRegistration = registered.tables.get(Table.tableId);
496
- if (tableRegistration) {
497
- tableRegistration.owners.delete(installed);
498
- if (tableRegistration.owners.size === 0) {
499
- registered.tables.delete(Table.tableId);
500
- } else if (tableRegistration.current === installed) {
501
- tableRegistration.current = tableRegistration.owners.values().next().value;
502
- }
503
- }
504
- if (dropping) {
505
- await Promise.all(settlements);
506
- } else {
507
- for (const result of await Promise.allSettled(settlements)) {
508
- if (result.status === 'rejected')
509
- logger.error(`Could not settle a superseded HNSW backend for table ${Table.tableId}`, result.reason);
510
- }
511
- }
512
- },
513
- restoreAfterFailedDrop() {
514
- registered.droppingTables.delete(Table.tableId);
515
- const currentTable = registered.tables.get(Table.tableId)?.current.Table;
516
- if (currentTable && currentTable !== Table) return;
517
- for (const registration of registered.tables.values()) {
518
- const current = registration.current.Table;
519
- if (current !== Table && current.databasePath === Table.databasePath && current.tableName === Table.tableName)
520
- return;
521
- }
522
- for (const [id, registeredBackend] of registeredBackends) {
523
- const current = registered.backends.get(id);
524
- if (current && current !== registeredBackend) return;
525
- }
526
- return attachDerivedIndexes(Table);
527
- },
528
- completeDrop(dropped = true) {
529
- registered.droppingTables.delete(Table.tableId);
530
- if (dropped) for (const id of backendIds) markUnavailableRetry(registered, auditStore, id);
531
- },
532
- };
533
- }
534
-
535
275
  /** Shared readiness of an index on any worker, registered or not. */
536
276
  export function derivedIndexReadiness(auditStore: RocksTransactionLogStore, indexStoreName: string) {
537
277
  return readDerivedIndexReadiness(auditStore, `hnsw:${indexStoreName}`);
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { isAbsolute, join, resolve } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
3
4
  import { loggerWithTag } from '../../utility/logging/logger.ts';
4
5
  import {
5
6
  FullTextDerivedIndexBackend,
@@ -29,6 +30,11 @@ export type NativeFullTextDerivedIndexLifecycleOptions = NativeFullTextIndexConf
29
30
  export type NativeFullTextDerivedIndexBackendOptions = Omit<FullTextDerivedIndexBackendOptions, 'lifecycle'> &
30
31
  Omit<NativeFullTextDerivedIndexLifecycleOptions, 'indexId'>;
31
32
 
33
+ export type RetireNativeFullTextDerivedIndexStorageOptions = Pick<
34
+ NativeFullTextDerivedIndexLifecycleOptions,
35
+ 'storePath' | 'storeName' | 'indexId' | 'binding'
36
+ >;
37
+
32
38
  export class NativeFullTextDerivedIndexLifecycle {
33
39
  readonly #options: NativeFullTextDerivedIndexLifecycleOptions;
34
40
  readonly #path: string;
@@ -48,7 +54,7 @@ export class NativeFullTextDerivedIndexLifecycle {
48
54
  limits: { ...options.limits },
49
55
  };
50
56
  // Hashing bounds filenames for arbitrary store names; the index id remains in native metadata and logs.
51
- this.#path = join(resolve(options.storePath), `${digest(options.storeName)}.fulltext`);
57
+ this.#path = nativeFullTextIndexPath(options.storePath, options.storeName);
52
58
  this.#generation = digest(options.sourceGeneration);
53
59
  }
54
60
 
@@ -135,10 +141,7 @@ export class NativeFullTextDerivedIndexLifecycle {
135
141
 
136
142
  async #getBinding(): Promise<NativeFullTextModule> {
137
143
  if (this.#binding) return this.#binding;
138
- const configured = this.#options.binding;
139
- this.#binding = configured
140
- ? await validateFullTextNativeBinding(typeof configured === 'function' ? await configured() : configured)
141
- : await loadFullTextNativeBinding();
144
+ this.#binding = await resolveNativeFullTextBinding(this.#options.binding);
142
145
  return this.#binding;
143
146
  }
144
147
 
@@ -148,16 +151,64 @@ export class NativeFullTextDerivedIndexLifecycle {
148
151
  }
149
152
  }
150
153
 
154
+ /** Retire one quiesced native directory after its Harper declaration or table is removed. */
155
+ export async function retireNativeFullTextDerivedIndexStorage(
156
+ options: RetireNativeFullTextDerivedIndexStorageOptions
157
+ ): Promise<void> {
158
+ if (!isAbsolute(options.storePath)) throw new TypeError('Full-text storePath must be absolute');
159
+ if (!options.storeName) throw new TypeError('Full-text storeName is required');
160
+ if (!options.indexId) throw new TypeError('Full-text indexId is required');
161
+ const path = nativeFullTextIndexPath(options.storePath, options.storeName);
162
+ if (!existsSync(path)) {
163
+ if (existsSync(join(dirname(path), '.fulltext-retired'))) {
164
+ try {
165
+ const binding = await resolveNativeFullTextBinding(options.binding);
166
+ const reclaimed = await binding.reclaimRetiredNativeFullTextIndexes({ path });
167
+ if (reclaimed.failed > 0)
168
+ logWarning(`Could not remove ${reclaimed.failed} retired paths for full-text index '${options.indexId}'`);
169
+ } catch (error) {
170
+ logWarning(`Could not reclaim retired storage for full-text index '${options.indexId}'`, error);
171
+ }
172
+ }
173
+ return;
174
+ }
175
+ const binding = await resolveNativeFullTextBinding(options.binding);
176
+ // The wrapper owns the physical lifecycle fence: reset refuses an active/unproven writer and
177
+ // takes Tantivy's writer lock before renaming. Harper treats that refusal as best-effort cleanup,
178
+ // never as permission to remove a live directory itself.
179
+ const result = await binding.resetNativeFullTextIndex({ path, indexId: options.indexId });
180
+ void binding
181
+ .reclaimRetiredNativeFullTextIndexes({
182
+ path,
183
+ retiredPath: result.state === 'reset' ? result.retiredPath : undefined,
184
+ })
185
+ .then((reclaimed) => {
186
+ if (reclaimed.failed > 0)
187
+ logWarning(`Could not remove ${reclaimed.failed} retired paths for full-text index '${options.indexId}'`);
188
+ })
189
+ .catch((error) => logWarning(`Could not reclaim retired storage for full-text index '${options.indexId}'`, error));
190
+ }
191
+
151
192
  export async function createNativeFullTextDerivedIndexBackend(
152
193
  options: NativeFullTextDerivedIndexBackendOptions
153
194
  ): Promise<FullTextDerivedIndexBackend> {
154
- const maxCursorPayloadBytes = options.maxCursorPayloadBytes ?? HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES;
155
- if (maxCursorPayloadBytes > HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES)
195
+ const configuredMaxCursorPayloadBytes = options.maxCursorPayloadBytes;
196
+ if (
197
+ configuredMaxCursorPayloadBytes !== undefined &&
198
+ configuredMaxCursorPayloadBytes > HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES
199
+ )
156
200
  throw new RangeError(`Full-text maxCursorPayloadBytes must not exceed ${HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES}`);
157
201
  const lifecycle = new NativeFullTextDerivedIndexLifecycle({ ...options, indexId: options.id });
158
202
  await lifecycle.initialize({ reclaimRetired: false });
159
- if (maxCursorPayloadBytes > lifecycle.maxCommitPayloadBytes)
203
+ if (
204
+ configuredMaxCursorPayloadBytes !== undefined &&
205
+ configuredMaxCursorPayloadBytes > lifecycle.maxCommitPayloadBytes
206
+ )
160
207
  throw new RangeError('Full-text maxCursorPayloadBytes exceeds the native commit payload capacity');
208
+ const maxCursorPayloadBytes = Math.min(
209
+ configuredMaxCursorPayloadBytes ?? HARPER_FULLTEXT_MAX_CURSOR_PAYLOAD_BYTES,
210
+ lifecycle.maxCommitPayloadBytes
211
+ );
161
212
  const backend = new FullTextDerivedIndexBackend({
162
213
  ...options,
163
214
  id: options.id,
@@ -168,6 +219,18 @@ export async function createNativeFullTextDerivedIndexBackend(
168
219
  return backend;
169
220
  }
170
221
 
222
+ function nativeFullTextIndexPath(storePath: string, storeName: string): string {
223
+ return join(resolve(storePath), `${digest(storeName)}.fulltext`);
224
+ }
225
+
226
+ async function resolveNativeFullTextBinding(
227
+ configured?: NativeFullTextModule | (() => Promise<NativeFullTextModule>)
228
+ ): Promise<NativeFullTextModule> {
229
+ return configured
230
+ ? validateFullTextNativeBinding(typeof configured === 'function' ? await configured() : configured)
231
+ : loadFullTextNativeBinding();
232
+ }
233
+
171
234
  function digest(value: string): string {
172
235
  return createHash('sha256').update(value).digest('hex');
173
236
  }