@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
@@ -1,9 +1,5 @@
1
- /**
2
- * `@embed` directive write-time hook. `createDefaultEmbedder` builds the embedder
3
- * a table registers for an `@embed` attribute; `buildEmbedBefore` produces the
4
- * pre-commit callback that runs registered embedders and writes their vectors onto
5
- * the record before it commits.
6
- */
1
+ import { ClientError } from '../../utility/errors/hdbError.ts';
2
+ import { validateDecisionSchema } from './decision.ts';
7
3
 
8
4
  // Lazily resolved to avoid a require cycle on the unit-test load path; only needed on failure.
9
5
  function getLogger(): { error?: (...args: any[]) => void } {
@@ -25,12 +21,17 @@ export type EmbedAttribute = {
25
21
  embed: EmbedConfig;
26
22
  };
27
23
 
28
- export type Embedder = (record: any) => Promise<number[] | Float32Array | null | undefined>;
24
+ /** The signal aborts when a sibling hook of the same write fails; a hook that honors it lets the write fail promptly. */
25
+ export type WriteHookContext = { signal: AbortSignal };
26
+
27
+ export type Embedder = (record: any, hook?: WriteHookContext) => Promise<number[] | Float32Array | null | undefined>;
28
+
29
+ /** A pre-commit callback of one write; `signal` is the write's, aborted when another hook of the write fails. */
30
+ export type WriteHook = (signal?: AbortSignal) => Promise<void>;
29
31
 
30
- // Matches the public `Models.embed` signature; a named type so tests can inject a fake.
31
32
  type EmbedFn = (
32
33
  input: string | string[],
33
- opts: { model?: string; inputType?: 'document' | 'query' }
34
+ opts: { model?: string; inputType?: 'document' | 'query'; signal?: AbortSignal }
34
35
  ) => Promise<Float32Array[]>;
35
36
 
36
37
  // Lazy-imported so this module can be unit-tested without loading the transaction
@@ -49,14 +50,259 @@ export function __setEmbedFnForTest(fn: EmbedFn | undefined): void {
49
50
  _embedFn = fn;
50
51
  }
51
52
 
53
+ /**
54
+ * Whether a write-time model hook runs for this write at all. Skip when the write already
55
+ * carries the derived attributes: a cluster-replication receiver (isNotification), REST
56
+ * x-replicate-from:none, or audit-log replay. Proactive source-subscribe pushes also set
57
+ * isNotification and so skip. One predicate for `@embed` and `@decide` so their trigger rules
58
+ * cannot drift.
59
+ */
60
+ export function writeHookApplies(record: any, context: any, options: any): boolean {
61
+ if (options?.isNotification === true || context?.replicateFrom === false || context?.alreadyLogged === true)
62
+ return false;
63
+ return Boolean(record) && typeof record === 'object';
64
+ }
65
+
66
+ /**
67
+ * How a hook treats one source field of the write payload: absent leaves the derived
68
+ * attributes alone (a PATCH that omits the source), null clears them, a CRDT op payload
69
+ * (`{__op__, value}`) is not a meaningful input and is skipped, and a value is derived from.
70
+ */
71
+ export type SourceState = 'absent' | 'null' | 'op' | 'value';
72
+ export function sourceState(record: any, sourceKey: string | undefined): SourceState {
73
+ if (!sourceKey || !(sourceKey in record)) return 'absent';
74
+ const value = record[sourceKey];
75
+ if (value == null) return 'null';
76
+ if (typeof value === 'object' && (value as any).__op__) return 'op';
77
+ return 'value';
78
+ }
79
+
80
+ /**
81
+ * Run the jobs of one write. The first failure aborts the others, and every job settles before
82
+ * that failure is reported: a rejected write neither waits on a sibling that honors the signal
83
+ * nor keeps being mutated by one that has not finished. A job that ignores the signal is
84
+ * awaited regardless, so the model calls behind the built-in hooks forward it.
85
+ */
86
+ export async function runWriteJobs(
87
+ jobs: Array<(signal: AbortSignal) => Promise<void>>,
88
+ signal?: AbortSignal
89
+ ): Promise<void> {
90
+ const controller = new AbortController();
91
+ const onAbort = () => controller.abort(signal?.reason);
92
+ if (signal?.aborted) onAbort();
93
+ else signal?.addEventListener('abort', onAbort, { once: true });
94
+ let failed = false;
95
+ let failure: unknown;
96
+ try {
97
+ await Promise.all(
98
+ jobs.map((job) =>
99
+ job(controller.signal).catch((err) => {
100
+ if (!failed) {
101
+ failed = true;
102
+ failure = err;
103
+ }
104
+ controller.abort(err);
105
+ })
106
+ )
107
+ );
108
+ } finally {
109
+ signal?.removeEventListener('abort', onAbort);
110
+ }
111
+ if (failed) throw failure;
112
+ }
113
+
114
+ export function combineWriteHooks(a: WriteHook | undefined, b: WriteHook | undefined): WriteHook | undefined {
115
+ if (!a) return b;
116
+ if (!b) return a;
117
+ return (signal) => runWriteJobs([a, b], signal);
118
+ }
119
+
120
+ const SAFE_ERROR_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
121
+
122
+ /**
123
+ * Backend errors can carry URLs / key tails; log raw, return a sanitized error to throw. Only
124
+ * safe identifiers are included (an identifier-shaped error class name, a finite upstream HTTP
125
+ * status) so the failure is diagnosable without hunting through server logs (#1593).
126
+ * `upstreamStatus` is the provider's status — NOT ServerError's statusCode, which is Harper's
127
+ * own response status and would misleadingly read 500 here.
128
+ */
129
+ export function sanitizedHookError(
130
+ actor: string,
131
+ product: string,
132
+ attributeName: string,
133
+ err: unknown,
134
+ log = true
135
+ ): Error {
136
+ if (log) getLogger().error?.(`${actor} for attribute "${attributeName}" failed:`, err);
137
+ const status = (err as any)?.upstreamStatus;
138
+ const errName = (err as any)?.name;
139
+ const detail =
140
+ (typeof errName === 'string' && errName !== 'Error' && SAFE_ERROR_NAME.test(errName) ? ` [${errName}]` : '') +
141
+ (Number.isInteger(status) && status >= 100 && status <= 599 ? ` (backend HTTP ${status})` : '');
142
+ const error = new Error(
143
+ `Failed to compute ${product} for attribute "${attributeName}"${detail} — see server log for details`
144
+ );
145
+ if (errName === 'AbortError') error.name = 'AbortError';
146
+ return error;
147
+ }
148
+
149
+ type DerivedAttribute = {
150
+ name: string;
151
+ type?: string;
152
+ nullable?: boolean;
153
+ isPrimaryKey?: boolean;
154
+ computed?: unknown;
155
+ embed?: { source: string };
156
+ decide?: { source: string; confidence?: string; decision?: string; schema?: unknown };
157
+ };
158
+
159
+ const INT32_MIN = -2147483648;
160
+ const INT32_MAX = 2147483647;
161
+
162
+ // Table validation holds a String to strings and an Int to 32 bits, so a leaf outside either would
163
+ // load and then fail every write.
164
+ const LEAF_KIND_BY_TYPE: Record<string, (schema: any) => boolean> = {
165
+ String: (schema) => Array.isArray(schema?.enum) && schema.enum.every((value: unknown) => typeof value === 'string'),
166
+ Boolean: (schema) => schema?.type === 'boolean' && !Array.isArray(schema.enum),
167
+ Int: (schema) =>
168
+ schema?.type === 'integer' &&
169
+ !Array.isArray(schema.enum) &&
170
+ schema.minimum >= INT32_MIN &&
171
+ schema.maximum <= INT32_MAX,
172
+ };
173
+
174
+ /**
175
+ * Every derived field (an `@embed` target, a `@decide` target or confidence) has exactly one
176
+ * writer, and no directive derives from another directive's output: the hooks run concurrently
177
+ * within one write, so the stored pair would otherwise depend on scheduling. A source and a
178
+ * confidence field must be declared; a decided field and its confidence field are nullable,
179
+ * neither the primary key nor computed, the decided field's type matches its leaf and the leaf
180
+ * is a valid decision schema; and none of those fields may be named after an `Object.prototype`
181
+ * key, because `sourceState` tests presence with `in`. Run on the complete attribute list.
182
+ */
183
+ export function assertDerivedFieldOwnership(attributes: DerivedAttribute[]): void {
184
+ const declared = new Map<string, DerivedAttribute>();
185
+ for (const attribute of attributes) if (attribute) declared.set(attribute.name, attribute);
186
+ const writers = new Map<string, string>();
187
+ const claim = (field: string, writer: string) => {
188
+ const prior = writers.get(field);
189
+ if (prior) throw new ClientError(`${writer} and ${prior} both write "${field}"`, 400);
190
+ writers.set(field, writer);
191
+ };
192
+ const directives = (
193
+ attribute: DerivedAttribute
194
+ ): Array<[string, { source: string; confidence?: string; decision?: string; schema?: unknown }]> => {
195
+ const out: Array<[string, { source: string; confidence?: string; decision?: string; schema?: unknown }]> = [];
196
+ if (attribute.embed) out.push(['@embed', attribute.embed]);
197
+ if (attribute.decide) out.push(['@decide', attribute.decide]);
198
+ return out;
199
+ };
200
+ for (const attribute of attributes) {
201
+ if (!attribute) continue;
202
+ for (const [directive, config] of directives(attribute)) {
203
+ const writer = `${directive} on "${attribute.name}"`;
204
+ for (const [name, field] of [
205
+ ['confidence', config.confidence],
206
+ ['decision', config.decision],
207
+ ] as const)
208
+ if (field !== undefined && (typeof field !== 'string' || field === ''))
209
+ throw new ClientError(`${writer}: "${name}" must name a field`, 400);
210
+ for (const field of [attribute.name, config.source, config.confidence, config.decision])
211
+ if (field && Object.hasOwn(Object.prototype, field))
212
+ throw new ClientError(
213
+ `${writer}: "${field}" is an Object.prototype key and cannot be a derived, source, confidence or decision field`,
214
+ 400
215
+ );
216
+ if (!declared.has(config.source))
217
+ throw new ClientError(`${writer} references unknown source field "${config.source}"`, 400);
218
+ if (directive === '@decide') {
219
+ if (attribute.isPrimaryKey || attribute.computed)
220
+ throw new ClientError(`${writer} cannot combine with @primaryKey or @computed`, 400);
221
+ if (attribute.nullable === false)
222
+ throw new ClientError(`${writer} cannot be declared non-null: a null source clears the attribute`, 400);
223
+ try {
224
+ validateDecisionSchema(config.schema);
225
+ } catch (err) {
226
+ throw new ClientError(`${writer}: ${(err as Error).message}`, 400);
227
+ }
228
+ if (attribute.type !== undefined) {
229
+ const matches = LEAF_KIND_BY_TYPE[attribute.type];
230
+ if (!matches)
231
+ throw new ClientError(
232
+ `${writer} requires a String, Boolean or Int attribute type; got "${attribute.type === 'array' ? '[...]' : attribute.type}"`,
233
+ 400
234
+ );
235
+ if (!matches(config.schema))
236
+ throw new ClientError(`${writer}: the decision schema does not fit a ${attribute.type} attribute`, 400);
237
+ }
238
+ }
239
+ claim(attribute.name, writer);
240
+ if (config.confidence) {
241
+ const confidence = declared.get(config.confidence);
242
+ if (!confidence)
243
+ throw new ClientError(`${writer} references unknown confidence field "${config.confidence}"`, 400);
244
+ if (confidence.type !== undefined && confidence.type !== 'Float')
245
+ throw new ClientError(
246
+ `${writer} requires a Float confidence attribute; "${config.confidence}" is ${confidence.type === 'array' ? '[...]' : confidence.type}`,
247
+ 400
248
+ );
249
+ if (confidence.nullable === false)
250
+ throw new ClientError(
251
+ `${writer}: confidence attribute "${config.confidence}" cannot be declared non-null: a null source clears it`,
252
+ 400
253
+ );
254
+ if (confidence.isPrimaryKey || confidence.computed)
255
+ throw new ClientError(
256
+ `${writer}: confidence attribute "${config.confidence}" cannot be @primaryKey or @computed`,
257
+ 400
258
+ );
259
+ claim(config.confidence, writer);
260
+ }
261
+ if (config.decision) {
262
+ const decisionField = declared.get(config.decision);
263
+ if (!decisionField)
264
+ throw new ClientError(`${writer} references unknown decision field "${config.decision}"`, 400);
265
+ if (decisionField.type !== undefined && decisionField.type !== 'String')
266
+ throw new ClientError(
267
+ `${writer} requires a String decision attribute; "${config.decision}" is ${decisionField.type === 'array' ? '[...]' : decisionField.type}`,
268
+ 400
269
+ );
270
+ if (decisionField.nullable === false)
271
+ throw new ClientError(
272
+ `${writer}: decision attribute "${config.decision}" cannot be declared non-null: a null source clears it`,
273
+ 400
274
+ );
275
+ if (decisionField.isPrimaryKey || decisionField.computed)
276
+ throw new ClientError(
277
+ `${writer}: decision attribute "${config.decision}" cannot be @primaryKey or @computed`,
278
+ 400
279
+ );
280
+ claim(config.decision, writer);
281
+ }
282
+ }
283
+ }
284
+ for (const attribute of attributes) {
285
+ if (!attribute) continue;
286
+ for (const [directive, config] of directives(attribute)) {
287
+ const sourceWriter = writers.get(config.source);
288
+ if (sourceWriter)
289
+ throw new ClientError(
290
+ `${directive} on "${attribute.name}" derives from "${config.source}", which ${sourceWriter} writes`,
291
+ 400
292
+ );
293
+ }
294
+ }
295
+ }
296
+
52
297
  export function createDefaultEmbedder(embedConfig: EmbedConfig): Embedder {
53
298
  const { source, model } = embedConfig;
54
- return async (record: any): Promise<number[] | null | undefined> => {
299
+ return async (record: any, hook?: WriteHookContext): Promise<number[] | null | undefined> => {
55
300
  const sourceValue = record?.[source];
56
301
  if (sourceValue == null) return null;
57
302
  const vectors = await resolveEmbedFn()(String(sourceValue), {
58
303
  model,
59
304
  inputType: 'document',
305
+ signal: hook?.signal,
60
306
  });
61
307
  const v = vectors?.[0];
62
308
  if (v == null) return undefined;
@@ -80,63 +326,34 @@ export function buildEmbedBefore(
80
326
  options: any,
81
327
  embedAttributes: EmbedAttribute[] | undefined,
82
328
  userEmbedders: Record<string, Embedder>
83
- ): (() => Promise<void>) | undefined {
329
+ ): WriteHook | undefined {
84
330
  if (!embedAttributes || embedAttributes.length === 0) return undefined;
85
- // Skip when the write already carries the vector: cluster-replication receiver
86
- // (isNotification), REST x-replicate-from:none, or audit-log replay. Note that
87
- // proactive source-subscribe pushes also set isNotification and so skip embedding.
88
- if (options?.isNotification === true || context?.replicateFrom === false || context?.alreadyLogged === true) {
89
- return undefined;
90
- }
91
- if (!record || typeof record !== 'object') return undefined;
92
- let anySourcePresent = false;
93
- for (const attr of embedAttributes) {
94
- const sourceKey = attr.embed?.source;
95
- if (sourceKey && sourceKey in record) {
96
- anySourcePresent = true;
97
- break;
98
- }
99
- }
100
- if (!anySourcePresent) return undefined;
101
- return async (): Promise<void> => {
102
- // Parallel: each embedder mutates a distinct attribute, so there's no ordering hazard.
103
- await Promise.all(
104
- embedAttributes.map(async (attr) => {
105
- const sourceKey = attr.embed?.source;
106
- if (!sourceKey) return;
107
- if (!(sourceKey in record)) return;
108
- const sourceValue = record[sourceKey];
109
- if (sourceValue == null) {
331
+ if (!writeHookApplies(record, context, options)) return undefined;
332
+ let present = false;
333
+ for (const attr of embedAttributes) if (sourceState(record, attr.embed?.source) !== 'absent') present = true;
334
+ if (!present) return undefined;
335
+ return (signal) =>
336
+ runWriteJobs(
337
+ embedAttributes.map((attr) => async (jobSignal) => {
338
+ const state = sourceState(record, attr.embed?.source);
339
+ if (state === 'absent' || state === 'op') return;
340
+ if (state === 'null') {
110
341
  record[attr.name] = null;
111
342
  return;
112
343
  }
113
- // CRDT op payloads (`{__op__, value}`) aren't a meaningful embed source; skip.
114
- if (sourceValue && typeof sourceValue === 'object' && (sourceValue as any).__op__) return;
115
344
  const embedder = userEmbedders[attr.name];
116
345
  if (!embedder) return;
117
346
  let vector;
118
347
  try {
119
- vector = await embedder(record);
348
+ vector = await embedder(record, { signal: jobSignal });
120
349
  } catch (err) {
121
- // Backend errors can carry URLs / key tails; log raw, rethrow sanitized. Include
122
- // only safe identifiers (error class name, upstream HTTP status) so the failure is
123
- // diagnosable without hunting through server logs (#1593). upstreamStatus is the
124
- // provider's status — NOT ServerError's statusCode, which is Harper's own response
125
- // status and would misleadingly read 500 here.
126
- getLogger().error?.(`Embedder for attribute "${attr.name}" failed:`, err);
127
- const status = (err as any)?.upstreamStatus;
128
- const errName = (err as any)?.name;
129
- const detail =
130
- (errName && errName !== 'Error' ? ` [${errName}]` : '') +
131
- (typeof status === 'number' ? ` (backend HTTP ${status})` : '');
132
- throw new Error(
133
- `Failed to compute embedding for attribute "${attr.name}"${detail} — see server log for details`
134
- );
350
+ // An aborted job failed because a sibling did; only that failure is worth a log line.
351
+ throw sanitizedHookError('Embedder', 'embedding', attr.name, err, !jobSignal.aborted);
135
352
  }
136
353
  record[attr.name] = normalizeVector(vector);
137
- })
354
+ }),
355
+ signal
138
356
  );
139
- };
140
357
  }
141
358
 
142
359
  // Custom embedders may return any typed array; flatten to a plain array so it round-trips