@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
@@ -0,0 +1,419 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { getDatabases, isReadOnlyMode, table } from '../databases.ts';
3
+ import { contextStorage, transaction } from '../transaction.ts';
4
+ import type { Context } from '../ResourceInterface.ts';
5
+ import { ClientError, ServerError } from '../../utility/errors/hdbError.ts';
6
+ import harperLogger from '../../utility/logging/harper_logger.ts';
7
+ import { canonicalJson, isAllowedValue, isObjectSchema } from './decision.ts';
8
+ import type {
9
+ DecisionLeaf,
10
+ DecisionRecord,
11
+ DecisionSchema,
12
+ LeafOutcomeReport,
13
+ OutcomeAction,
14
+ OutcomeReport,
15
+ OutcomeTruth,
16
+ RecordedLeafOutcome,
17
+ RecordedOutcome,
18
+ } from './types.ts';
19
+
20
+ const log = harperLogger.forComponent('models').conditional;
21
+
22
+ export const DECISIONS_TABLE = 'hdb_model_decisions';
23
+ export const OUTCOMES_TABLE = 'hdb_model_outcomes';
24
+ /** Facts copy the decision's instant, so a report never extends a decision's life. */
25
+ export const DECISION_RETENTION_MS = 365 * 24 * 60 * 60 * 1000;
26
+
27
+ /** A `hdb_model_decisions` row: written once by `models.decide`, never rewritten. */
28
+ export interface DecisionRow {
29
+ id: string;
30
+ callId: number;
31
+ at: number;
32
+ expiresAt: number;
33
+ tenant?: string;
34
+ app?: string;
35
+ backend: string;
36
+ model: string;
37
+ signature?: string;
38
+ configHash?: string;
39
+ instructionsHash?: string;
40
+ schema: DecisionSchema;
41
+ schemaHash: string;
42
+ value: unknown;
43
+ probability?: number;
44
+ distribution?: DecisionRecord['distribution'];
45
+ fields?: DecisionRecord['fields'];
46
+ noMatch?: number;
47
+ calibrated: boolean;
48
+ }
49
+
50
+ type Fact = 'truth' | 'action';
51
+
52
+ interface OutcomeRow {
53
+ id: string;
54
+ decisionId: string;
55
+ fact: Fact;
56
+ field?: string;
57
+ state: OutcomeTruth | OutcomeAction;
58
+ at: number;
59
+ expiresAt: number;
60
+ }
61
+
62
+ /**
63
+ * The catalog stub (`systemSchema.json` and the upgrade directive) declares only the primary key;
64
+ * these declarations add the attributes and the indexed `expiresAt` TTL. Writes go through the
65
+ * Resource API, which maintains the index, so `indexed` holds here, unlike in `hdb_model_calls`.
66
+ */
67
+ export const DECISION_ATTRIBUTES = [
68
+ { name: 'id', isPrimaryKey: true },
69
+ { name: 'callId', type: 'number' },
70
+ { name: 'at', type: 'number' },
71
+ { name: 'expiresAt', expiresAt: true, indexed: true },
72
+ { name: 'tenant', type: 'string' },
73
+ { name: 'app', type: 'string' },
74
+ { name: 'backend', type: 'string' },
75
+ { name: 'model', type: 'string' },
76
+ { name: 'signature', type: 'string' },
77
+ { name: 'configHash', type: 'string' },
78
+ { name: 'instructionsHash', type: 'string' },
79
+ { name: 'schema' },
80
+ { name: 'schemaHash', type: 'string' },
81
+ { name: 'value' },
82
+ { name: 'probability', type: 'number' },
83
+ { name: 'distribution' },
84
+ { name: 'fields' },
85
+ { name: 'noMatch', type: 'number' },
86
+ { name: 'calibrated', type: 'boolean' },
87
+ ];
88
+
89
+ export const OUTCOME_ATTRIBUTES = [
90
+ { name: 'id', isPrimaryKey: true },
91
+ { name: 'decisionId', type: 'string' },
92
+ { name: 'fact', type: 'string' },
93
+ { name: 'field', type: 'string' },
94
+ { name: 'state' },
95
+ { name: 'at', type: 'number' },
96
+ { name: 'expiresAt', expiresAt: true, indexed: true },
97
+ ];
98
+
99
+ export interface DecisionTables {
100
+ decisions: any;
101
+ outcomes: any;
102
+ }
103
+
104
+ let tables: DecisionTables | undefined;
105
+
106
+ /**
107
+ * Declares both tables and memoizes the handles. A read-only node cannot write the catalog, so it
108
+ * takes whatever the catalog already holds and declares nothing; a table not there yet reads as empty.
109
+ */
110
+ export function getDecisionTables(readOnly = isReadOnlyMode()): DecisionTables {
111
+ if (tables) return tables;
112
+ const resolve = (name: string, attributes: object[]) => {
113
+ const handle: any = readOnly
114
+ ? getDatabases().system?.[name]
115
+ : table({
116
+ table: name,
117
+ database: 'system',
118
+ // Declared positively, as the scheduler does: a scoped `replication.databases` must not
119
+ // exclude these, or an outcome recorded on one node never reaches the others.
120
+ replicate: true,
121
+ // auditing is the replication feed; must match systemSchema.json
122
+ audit: true,
123
+ attributes,
124
+ });
125
+ if (handle) handle.loadAsInstance = false;
126
+ return handle;
127
+ };
128
+ const resolved = {
129
+ decisions: resolve(DECISIONS_TABLE, DECISION_ATTRIBUTES),
130
+ outcomes: resolve(OUTCOMES_TABLE, OUTCOME_ATTRIBUTES),
131
+ };
132
+ if (resolved.decisions && resolved.outcomes) tables = resolved;
133
+ return resolved;
134
+ }
135
+
136
+ export function resetDecisionTables(): void {
137
+ tables = undefined;
138
+ }
139
+
140
+ /**
141
+ * Declared at boot on every writable node so the TTL index exists where decisions are never made.
142
+ * A failure is logged, not thrown: the first `decide` declares again and reports its own error.
143
+ */
144
+ export function declareDecisionTablesAtBoot(): void {
145
+ if (isReadOnlyMode()) return;
146
+ try {
147
+ getDecisionTables();
148
+ } catch (err) {
149
+ log.warn?.(`Decision tables could not be declared at boot: ${(err as Error)?.message ?? err}`);
150
+ }
151
+ }
152
+
153
+ export class DecisionNotFoundError extends ClientError {
154
+ constructor(id: string) {
155
+ super(
156
+ `No decision '${id}' on this node: it may not exist, may have expired, or may not have replicated here yet`,
157
+ 404
158
+ );
159
+ this.name = 'DecisionNotFoundError';
160
+ }
161
+ }
162
+
163
+ export class OutcomeReportError extends ClientError {
164
+ constructor(message: string) {
165
+ super(`Invalid outcome report: ${message}`, 400);
166
+ this.name = 'OutcomeReportError';
167
+ }
168
+ }
169
+
170
+ export class DecisionPersistenceError extends ServerError {
171
+ constructor(message: string, statusCode = 500) {
172
+ super(message, statusCode);
173
+ this.name = 'DecisionPersistenceError';
174
+ }
175
+ }
176
+
177
+ export interface DecisionStoreOpts {
178
+ getTables?: () => DecisionTables;
179
+ isReadOnly?: () => boolean;
180
+ }
181
+
182
+ interface Found {
183
+ row: DecisionRow;
184
+ facts: Array<OutcomeRow | undefined>;
185
+ }
186
+
187
+ /**
188
+ * Durable decisions and their recorded facts. Every operation runs in a transaction of its own
189
+ * (`transaction` with a context object that is not the ALS store), so a decision made inside an
190
+ * application transaction survives that transaction's abort, and the commit is awaited before the
191
+ * id is returned to the caller.
192
+ */
193
+ export class DecisionStore {
194
+ #getTables: () => DecisionTables;
195
+ #isReadOnly: () => boolean;
196
+
197
+ constructor(opts: DecisionStoreOpts = {}) {
198
+ this.#getTables = opts.getTables ?? getDecisionTables;
199
+ this.#isReadOnly = opts.isReadOnly ?? isReadOnlyMode;
200
+ }
201
+
202
+ assertWritable(what: 'Decisions' | 'Outcomes'): void {
203
+ if (this.#isReadOnly()) throw new DecisionPersistenceError(`${what} cannot be recorded on a read-only node`, 503);
204
+ }
205
+
206
+ async persist(row: DecisionRow): Promise<void> {
207
+ const { decisions } = this.#getTables();
208
+ await transaction(freshContext(), () => decisions.put(row));
209
+ }
210
+
211
+ async get(id: string, tenant?: string): Promise<DecisionRecord | undefined> {
212
+ return storageFaultsAs('Decision could not be read', () =>
213
+ transaction(freshContext(), async () => {
214
+ const found = await this.#read(id, tenant);
215
+ return found && { ...found.row, outcome: assembleOutcome(found.row.schema, found.facts) };
216
+ })
217
+ );
218
+ }
219
+
220
+ async recordOutcome(id: string, report: OutcomeReport, tenant?: string): Promise<DecisionRecord> {
221
+ this.assertWritable('Outcomes');
222
+ return storageFaultsAs('Outcome could not be recorded', () =>
223
+ transaction(freshContext(), async () => {
224
+ const { outcomes } = this.#getTables();
225
+ const found = await this.#read(id, tenant);
226
+ if (!found) throw new DecisionNotFoundError(id);
227
+ const { row, facts } = found;
228
+ const at = Date.now();
229
+ for (const reported of validateReport(row.schema, report)) {
230
+ const index = factIndex(row.schema, reported.fact, reported.field);
231
+ const existing = facts[index];
232
+ if (existing && canonicalJson(existing.state) === canonicalJson(reported.state)) continue;
233
+ const written: OutcomeRow = {
234
+ id: factKey(id, reported.fact, reported.field),
235
+ decisionId: id,
236
+ fact: reported.fact,
237
+ field: reported.field,
238
+ state: reported.state,
239
+ at,
240
+ expiresAt: row.expiresAt,
241
+ };
242
+ await outcomes.put(written);
243
+ facts[index] = written;
244
+ }
245
+ return { ...row, outcome: assembleOutcome(row.schema, facts) };
246
+ })
247
+ );
248
+ }
249
+
250
+ async #read(id: string, tenant: string | undefined): Promise<Found | undefined> {
251
+ const { decisions, outcomes } = this.#getTables();
252
+ if (!decisions || !outcomes) return undefined;
253
+ const row: DecisionRow | undefined = await decisions.get(id);
254
+ if (!row || !visibleTo(row, tenant)) return undefined;
255
+ const facts: Array<OutcomeRow | undefined> = await Promise.all(
256
+ factKeys(row.id, row.schema).map((key) => outcomes.get(key))
257
+ );
258
+ return { row, facts };
259
+ }
260
+ }
261
+
262
+ let store: DecisionStore | undefined;
263
+ export function getDecisionStore(): DecisionStore {
264
+ if (!store) store = new DecisionStore();
265
+ return store;
266
+ }
267
+
268
+ export function newDecisionId(): string {
269
+ return randomUUID();
270
+ }
271
+
272
+ /** A context of its own, so the transaction is separate, carrying the caller's user for the audit entry. */
273
+ function freshContext(): Context {
274
+ const user = contextStorage.getStore()?.user;
275
+ return (user ? { user } : {}) as Context;
276
+ }
277
+
278
+ function visibleTo(row: DecisionRow, tenant: string | undefined): boolean {
279
+ return tenant == null || row.tenant == null || row.tenant === tenant;
280
+ }
281
+
282
+ function factKey(id: string, fact: Fact, field?: string): string {
283
+ return field == null ? `${id}/${fact}` : `${id}/${fact}/${field}`;
284
+ }
285
+
286
+ /** Truth then action, per property in schema order for object schemas; `factIndex` follows the same order. */
287
+ function factKeys(id: string, schema: DecisionSchema): string[] {
288
+ if (!isObjectSchema(schema)) return [factKey(id, 'truth'), factKey(id, 'action')];
289
+ return Object.keys(schema.properties).flatMap((field) => [factKey(id, 'truth', field), factKey(id, 'action', field)]);
290
+ }
291
+
292
+ function factIndex(schema: DecisionSchema, fact: Fact, field: string | undefined): number {
293
+ const offset = fact === 'truth' ? 0 : 1;
294
+ if (!isObjectSchema(schema)) return offset;
295
+ return Object.keys(schema.properties).indexOf(field as string) * 2 + offset;
296
+ }
297
+
298
+ function assembleOutcome(schema: DecisionSchema, facts: Array<OutcomeRow | undefined>): RecordedOutcome {
299
+ if (!isObjectSchema(schema)) return leafOutcome(facts);
300
+ const fields: Record<string, RecordedLeafOutcome> = {};
301
+ const names = Object.keys(schema.properties);
302
+ for (let i = 0; i < names.length; i++) fields[names[i]] = leafOutcome(facts.slice(i * 2, i * 2 + 2));
303
+ return { fields };
304
+ }
305
+
306
+ function leafOutcome(facts: Array<OutcomeRow | undefined>): RecordedLeafOutcome {
307
+ const outcome: RecordedLeafOutcome = {};
308
+ for (const fact of facts) {
309
+ if (!fact) continue;
310
+ if (fact.fact === 'truth') {
311
+ outcome.truth = fact.state as OutcomeTruth;
312
+ outcome.truthAt = fact.at;
313
+ } else {
314
+ outcome.action = fact.state as OutcomeAction;
315
+ outcome.actionAt = fact.at;
316
+ }
317
+ }
318
+ return outcome;
319
+ }
320
+
321
+ const TRUTH_KINDS = ['value', 'noMatch', 'unknown'];
322
+ const ACTION_KINDS = ['value', 'noMatch', 'abstained', 'unknown'];
323
+
324
+ interface ReportedFact {
325
+ fact: Fact;
326
+ field?: string;
327
+ state: OutcomeTruth | OutcomeAction;
328
+ }
329
+
330
+ /** Checks a report against the stored schema. Messages name fields, never reported values. */
331
+ function validateReport(schema: DecisionSchema, report: OutcomeReport): ReportedFact[] {
332
+ if (!report || typeof report !== 'object' || Array.isArray(report))
333
+ throw new OutcomeReportError('the report must be an object');
334
+ const facts: ReportedFact[] = [];
335
+ if (isObjectSchema(schema)) {
336
+ if ('truth' in report || 'action' in report)
337
+ throw new OutcomeReportError('an object schema takes { fields: { <name>: { truth?, action? } } }');
338
+ const fields = (report as { fields?: unknown }).fields;
339
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields))
340
+ throw new OutcomeReportError('an object schema needs a fields map');
341
+ for (const [name, leafReport] of Object.entries(fields)) {
342
+ if (!Object.hasOwn(schema.properties, name)) throw new OutcomeReportError(`unknown field '${name}'`);
343
+ facts.push(...leafFacts(schema.properties[name], leafReport as LeafOutcomeReport, name));
344
+ }
345
+ } else {
346
+ if ('fields' in report) throw new OutcomeReportError('a leaf schema takes { truth?, action? }');
347
+ facts.push(...leafFacts(schema, report as LeafOutcomeReport, undefined));
348
+ }
349
+ if (facts.length === 0) throw new OutcomeReportError('the report carries no fact');
350
+ return facts;
351
+ }
352
+
353
+ function leafFacts(leaf: DecisionLeaf, report: LeafOutcomeReport, field: string | undefined): ReportedFact[] {
354
+ const label = field === undefined ? 'the report' : `field '${field}'`;
355
+ if (!report || typeof report !== 'object' || Array.isArray(report))
356
+ throw new OutcomeReportError(`${label} must be an object`);
357
+ const facts: ReportedFact[] = [];
358
+ if (report.truth !== undefined)
359
+ facts.push({ fact: 'truth', field, state: checkState(leaf, report.truth, TRUTH_KINDS, `${label}: truth`) });
360
+ if (report.action !== undefined)
361
+ facts.push({ fact: 'action', field, state: checkState(leaf, report.action, ACTION_KINDS, `${label}: action`) });
362
+ return facts;
363
+ }
364
+
365
+ function checkState(leaf: DecisionLeaf, state: unknown, kinds: string[], label: string): OutcomeTruth | OutcomeAction {
366
+ if (!state || typeof state !== 'object' || Array.isArray(state))
367
+ throw new OutcomeReportError(`${label} must be a tagged state`);
368
+ const { kind, value } = state as { kind?: unknown; value?: unknown };
369
+ if (typeof kind !== 'string' || !kinds.includes(kind))
370
+ throw new OutcomeReportError(`${label} has an unknown kind; expected one of ${kinds.join(', ')}`);
371
+ if (kind === 'value') {
372
+ if (!isAllowedValue(leaf, value)) throw new OutcomeReportError(`${label} is not an allowed value`);
373
+ return { kind, value };
374
+ }
375
+ return { kind } as OutcomeTruth | OutcomeAction;
376
+ }
377
+
378
+ let modelsConfigHash: string | undefined;
379
+
380
+ /**
381
+ * Identity of the model configuration actually installed, recorded on each decision so a reload
382
+ * that changes the model behind an unchanged logical name still separates the decisions it produced.
383
+ * Credential-looking keys are dropped before hashing.
384
+ */
385
+ export function setModelsConfigHash(installed: unknown): void {
386
+ modelsConfigHash = installed
387
+ ? createHash('sha256')
388
+ .update(canonicalJson(withoutCredentials(installed)))
389
+ .digest('hex')
390
+ : undefined;
391
+ }
392
+
393
+ export function getModelsConfigHash(): string | undefined {
394
+ return modelsConfigHash;
395
+ }
396
+
397
+ const CREDENTIAL_KEY = /^(apiKey|accessKeyId|authorization|.*secret.*|.*password.*|.*credential.*|.*key|.*token)$/i;
398
+
399
+ function withoutCredentials(value: unknown): unknown {
400
+ if (Array.isArray(value)) return value.map(withoutCredentials);
401
+ if (!value || typeof value !== 'object') return value;
402
+ const kept: Record<string, unknown> = {};
403
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
404
+ if (!CREDENTIAL_KEY.test(key)) kept[key] = withoutCredentials(entry);
405
+ }
406
+ return kept;
407
+ }
408
+
409
+ /** A fault from the storage layer can name a data path; only its class and a fixed message may reach a response body. */
410
+ async function storageFaultsAs<T>(message: string, run: () => Promise<T>): Promise<T> {
411
+ try {
412
+ return await run();
413
+ } catch (err) {
414
+ if (err instanceof ClientError || err instanceof DecisionPersistenceError) throw err;
415
+ const error = new DecisionPersistenceError(message);
416
+ (error as Error & { cause?: unknown }).cause = err;
417
+ throw error;
418
+ }
419
+ }