@feltdb/core 0.15.0 → 0.16.1

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 (248) hide show
  1. package/dist/agent-state.d.ts +20 -0
  2. package/dist/agent-state.d.ts.map +1 -1
  3. package/dist/agent-state.js +36 -0
  4. package/dist/build-identity.json +12 -4
  5. package/dist/cli/commands.js +187 -4
  6. package/dist/cli/publish-engine.js +116 -16
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +20 -2
  9. package/dist/create/build-identity.json +12 -4
  10. package/dist/create/managed-account.js +93 -35
  11. package/dist/create/package-versions.js +1 -1
  12. package/dist/create/server-source/Cargo.lock +6 -509
  13. package/dist/create/server-source/Cargo.toml +2 -13
  14. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  15. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  16. package/dist/create/server-source/crates/feltdb-server/src/main.rs +356 -24
  17. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +10 -1
  18. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  19. package/dist/db.d.ts +11 -1
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +15 -2
  22. package/dist/error-codes.d.ts +5 -0
  23. package/dist/error-codes.d.ts.map +1 -1
  24. package/dist/error-codes.js +9 -1
  25. package/dist/http-db.d.ts +16 -0
  26. package/dist/http-db.d.ts.map +1 -1
  27. package/dist/http-db.js +36 -6
  28. package/dist/managed-recovery.d.ts +6 -0
  29. package/dist/managed-recovery.d.ts.map +1 -1
  30. package/dist/managed-recovery.js +9 -0
  31. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-BUIxCuTR.js} +1 -1
  32. package/dist/studio-app/assets/feltdb_wasm_bg-C3D6y6DK.wasm +0 -0
  33. package/dist/studio-app/assets/index-BhXYAhgS.js +37 -0
  34. package/dist/studio-app/index.html +1 -1
  35. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  36. package/dist/wasm/package.json +1 -1
  37. package/dist/workload.d.ts +55 -3
  38. package/dist/workload.d.ts.map +1 -1
  39. package/dist/workload.js +23 -0
  40. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  41. package/dist/workspace/workspace-connection.js +14 -3
  42. package/package.json +1 -1
  43. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  44. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  45. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  46. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  47. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  49. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  51. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  61. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  63. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  64. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  65. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  66. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  67. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  68. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  69. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  70. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  71. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  72. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  73. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  74. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  75. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  76. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  77. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  80. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  83. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  84. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  86. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  87. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  89. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  90. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  91. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  92. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  93. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  94. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  95. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  96. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  97. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  98. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  99. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  100. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  101. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  102. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  103. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  105. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  106. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  107. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  108. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  109. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  110. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  111. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  112. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  113. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  114. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  115. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  116. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  117. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  118. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  119. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  120. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  121. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  122. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  123. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  124. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  125. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  126. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  127. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  128. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  129. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  130. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  131. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  132. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  133. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  134. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  135. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  136. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  137. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  138. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  139. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  140. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  141. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  142. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  143. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  144. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  145. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  146. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  147. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  148. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  149. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  150. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  151. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  152. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  153. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  154. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  155. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  156. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  157. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  158. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  159. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  160. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  161. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  162. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  163. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  164. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  165. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  166. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  167. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  168. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  169. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  170. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  171. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  172. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  173. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  174. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  175. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  176. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  177. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  178. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  179. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  180. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  181. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  182. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  183. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  184. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  185. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  186. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  187. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  188. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  189. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  190. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  191. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  192. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  193. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  194. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  195. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  196. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  197. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  198. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  199. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  200. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  204. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  206. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  207. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  208. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  209. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  210. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  211. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  212. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  213. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  220. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  221. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  222. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  223. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  225. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  226. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  227. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  228. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  229. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  230. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  231. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  232. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  235. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  236. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  237. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  238. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  239. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  240. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  241. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  242. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  243. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  244. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  245. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  246. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  247. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  248. package/dist/studio-app/assets/index-1wd0uKua.js +0 -30
@@ -1,2029 +0,0 @@
1
- use crate::authorization::{AuthorizationRequest, Subject};
2
- use crate::{FeltDb, FlowError};
3
- use serde::{Deserialize, Serialize};
4
- use serde_json::{json, Map, Value};
5
- use sha2::{Digest, Sha256};
6
- use std::collections::{BTreeMap, BTreeSet};
7
- use std::sync::{Arc, Mutex};
8
-
9
- pub const DEFAULT_DECISION_COLLECTION: &str = "semantic_decisions";
10
-
11
- #[cfg(target_arch = "wasm32")]
12
- fn semantic_now_ms() -> u128 {
13
- 0
14
- }
15
-
16
- #[cfg(not(target_arch = "wasm32"))]
17
- fn semantic_now_ms() -> u128 {
18
- crate::now_ms()
19
- }
20
-
21
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22
- #[serde(rename_all = "snake_case")]
23
- pub enum DecisionStatus {
24
- Active,
25
- Superseded,
26
- }
27
-
28
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29
- #[serde(rename_all = "snake_case")]
30
- pub enum DecisionKind {
31
- Binary,
32
- Choice,
33
- Score,
34
- }
35
-
36
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37
- pub struct BinaryDecisionResult {
38
- pub kind: DecisionKind,
39
- pub decision: bool,
40
- pub option_mass: f64,
41
- #[serde(default)]
42
- pub supporting_fields: Vec<String>,
43
- }
44
-
45
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46
- pub struct WeightedOption {
47
- pub value: String,
48
- pub probability: f64,
49
- }
50
-
51
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52
- pub struct ChoiceDecisionResult {
53
- pub kind: DecisionKind,
54
- pub selected: String,
55
- pub options: Vec<WeightedOption>,
56
- pub option_mass: f64,
57
- #[serde(default)]
58
- pub supporting_fields: Vec<String>,
59
- }
60
-
61
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62
- pub struct ScoreDecisionResult {
63
- pub kind: DecisionKind,
64
- pub selected: String,
65
- pub levels: Vec<WeightedOption>,
66
- pub option_mass: f64,
67
- #[serde(default)]
68
- pub supporting_fields: Vec<String>,
69
- }
70
-
71
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72
- #[serde(tag = "kind", rename_all = "snake_case")]
73
- pub enum DecisionResult {
74
- Binary {
75
- decision: bool,
76
- option_mass: f64,
77
- #[serde(default)]
78
- supporting_fields: Vec<String>,
79
- },
80
- Choice {
81
- selected: String,
82
- options: Vec<WeightedOption>,
83
- option_mass: f64,
84
- #[serde(default)]
85
- supporting_fields: Vec<String>,
86
- },
87
- Score {
88
- selected: String,
89
- levels: Vec<WeightedOption>,
90
- option_mass: f64,
91
- #[serde(default)]
92
- supporting_fields: Vec<String>,
93
- },
94
- }
95
-
96
- impl DecisionResult {
97
- pub fn kind(&self) -> DecisionKind {
98
- match self {
99
- Self::Binary { .. } => DecisionKind::Binary,
100
- Self::Choice { .. } => DecisionKind::Choice,
101
- Self::Score { .. } => DecisionKind::Score,
102
- }
103
- }
104
-
105
- pub fn option_mass(&self) -> f64 {
106
- match self {
107
- Self::Binary { option_mass, .. }
108
- | Self::Choice { option_mass, .. }
109
- | Self::Score { option_mass, .. } => *option_mass,
110
- }
111
- }
112
-
113
- pub fn supporting_fields(&self) -> &[String] {
114
- match self {
115
- Self::Binary {
116
- supporting_fields, ..
117
- }
118
- | Self::Choice {
119
- supporting_fields, ..
120
- }
121
- | Self::Score {
122
- supporting_fields, ..
123
- } => supporting_fields.as_slice(),
124
- }
125
- }
126
- }
127
-
128
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129
- #[serde(tag = "kind", rename_all = "snake_case")]
130
- pub enum DecisionDefinition {
131
- Binary {
132
- predicate: String,
133
- },
134
- Choice {
135
- question: String,
136
- options: Vec<String>,
137
- },
138
- Score {
139
- question: String,
140
- levels: Vec<String>,
141
- },
142
- }
143
-
144
- impl DecisionDefinition {
145
- pub fn kind(&self) -> DecisionKind {
146
- match self {
147
- Self::Binary { .. } => DecisionKind::Binary,
148
- Self::Choice { .. } => DecisionKind::Choice,
149
- Self::Score { .. } => DecisionKind::Score,
150
- }
151
- }
152
-
153
- pub fn prompt(&self) -> &str {
154
- match self {
155
- Self::Binary { predicate } => predicate,
156
- Self::Choice { question, .. } | Self::Score { question, .. } => question,
157
- }
158
- }
159
-
160
- pub fn schema_values(&self) -> &[String] {
161
- match self {
162
- Self::Binary { .. } => &[],
163
- Self::Choice { options, .. } => options,
164
- Self::Score { levels, .. } => levels,
165
- }
166
- }
167
- }
168
-
169
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170
- #[serde(tag = "kind", rename_all = "snake_case")]
171
- pub enum DecisionRequest {
172
- Binary {
173
- predicate: String,
174
- context: Value,
175
- },
176
- Choice {
177
- question: String,
178
- options: Vec<String>,
179
- context: Value,
180
- },
181
- Score {
182
- question: String,
183
- levels: Vec<String>,
184
- context: Value,
185
- },
186
- }
187
-
188
- impl DecisionRequest {
189
- pub fn definition(&self) -> DecisionDefinition {
190
- match self {
191
- Self::Binary { predicate, .. } => DecisionDefinition::Binary {
192
- predicate: predicate.clone(),
193
- },
194
- Self::Choice {
195
- question, options, ..
196
- } => DecisionDefinition::Choice {
197
- question: question.clone(),
198
- options: options.clone(),
199
- },
200
- Self::Score {
201
- question, levels, ..
202
- } => DecisionDefinition::Score {
203
- question: question.clone(),
204
- levels: levels.clone(),
205
- },
206
- }
207
- }
208
-
209
- pub fn context(&self) -> &Value {
210
- match self {
211
- Self::Binary { context, .. }
212
- | Self::Choice { context, .. }
213
- | Self::Score { context, .. } => context,
214
- }
215
- }
216
- }
217
-
218
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
219
- pub struct DecisionRecordRef {
220
- pub collection: String,
221
- pub record_id: String,
222
- }
223
-
224
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225
- pub struct DecisionPrincipal {
226
- pub subject: Subject,
227
- pub tenant_id: String,
228
- pub application_id: String,
229
- #[serde(default = "default_read_capability")]
230
- pub capability: String,
231
- }
232
-
233
- fn default_read_capability() -> String {
234
- "state:read".into()
235
- }
236
-
237
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
238
- pub struct DecisionAuthorizationDecision {
239
- pub allowed: bool,
240
- pub decision_id: Option<String>,
241
- }
242
-
243
- pub trait DecisionAuthorizationGate {
244
- fn check(
245
- &self,
246
- request: &AuthorizationRequest,
247
- ) -> Result<DecisionAuthorizationDecision, DecisionError>;
248
- }
249
-
250
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
251
- pub struct DecisionAuthorizationConfig {
252
- pub principal: Option<DecisionPrincipal>,
253
- }
254
-
255
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
256
- pub struct DecisionRuntimeMetadata {
257
- pub runtime: String,
258
- #[serde(default, skip_serializing_if = "Option::is_none")]
259
- pub model: Option<String>,
260
- #[serde(default, skip_serializing_if = "Option::is_none")]
261
- pub model_revision: Option<String>,
262
- #[serde(default, skip_serializing_if = "Option::is_none")]
263
- pub prompt_revision: Option<String>,
264
- #[serde(default, skip_serializing_if = "Option::is_none")]
265
- pub execution_method: Option<DecisionExecutionMethod>,
266
- #[serde(default, skip_serializing_if = "Option::is_none")]
267
- pub decision_schema_revision: Option<String>,
268
- }
269
-
270
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
271
- #[serde(rename_all = "snake_case")]
272
- pub enum DecisionExecutionMethod {
273
- Logit,
274
- Structured,
275
- Generated,
276
- Head,
277
- }
278
-
279
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
280
- pub struct DecisionRuntimeCapabilities {
281
- pub binary: bool,
282
- pub choice: bool,
283
- pub score: bool,
284
- pub batch: bool,
285
- pub probability_distribution: bool,
286
- pub execution_methods: BTreeSet<DecisionExecutionMethod>,
287
- }
288
-
289
- impl Default for DecisionRuntimeCapabilities {
290
- fn default() -> Self {
291
- Self {
292
- binary: true,
293
- choice: true,
294
- score: true,
295
- batch: true,
296
- probability_distribution: true,
297
- execution_methods: BTreeSet::new(),
298
- }
299
- }
300
- }
301
-
302
- pub trait DecisionRuntime {
303
- fn metadata(&self) -> DecisionRuntimeMetadata;
304
- fn decide(&self, request: &DecisionRequest) -> Result<DecisionResult, DecisionError>;
305
-
306
- fn capabilities(&self) -> DecisionRuntimeCapabilities {
307
- DecisionRuntimeCapabilities::default()
308
- }
309
-
310
- fn execute(&self, request: &DecisionRequest) -> Result<DecisionResult, DecisionError> {
311
- self.decide(request)
312
- }
313
-
314
- fn execute_batch(
315
- &self,
316
- requests: &[DecisionRequest],
317
- ) -> Result<Vec<DecisionResult>, DecisionError> {
318
- requests
319
- .iter()
320
- .map(|request| self.execute(request))
321
- .collect()
322
- }
323
- }
324
-
325
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
326
- pub struct DecisionProvenance {
327
- pub runtime: String,
328
- #[serde(default, skip_serializing_if = "Option::is_none")]
329
- pub model: Option<String>,
330
- #[serde(default, skip_serializing_if = "Option::is_none")]
331
- pub model_revision: Option<String>,
332
- #[serde(default, skip_serializing_if = "Option::is_none")]
333
- pub prompt_revision: Option<String>,
334
- #[serde(default, skip_serializing_if = "Option::is_none")]
335
- pub execution_method: Option<DecisionExecutionMethod>,
336
- #[serde(default, skip_serializing_if = "Option::is_none")]
337
- pub decision_schema_revision: Option<String>,
338
- pub context_hash: String,
339
- pub relevant_state_version: String,
340
- pub cache_key: String,
341
- pub source: String,
342
- pub evaluated_at: u128,
343
- pub served_at: u128,
344
- }
345
-
346
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
347
- pub struct DecisionEvidenceField {
348
- pub field: String,
349
- pub value: Value,
350
- }
351
-
352
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
353
- pub struct DecisionEvidence {
354
- pub decision_id: String,
355
- pub kind: DecisionKind,
356
- pub input: DecisionDefinition,
357
- pub result: DecisionResult,
358
- pub supporting_context: Vec<DecisionEvidenceField>,
359
- pub runtime: DecisionRuntimeMetadata,
360
- pub state: DecisionStateReference,
361
- }
362
-
363
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
364
- pub struct DecisionStateReference {
365
- pub collection: String,
366
- pub record_id: String,
367
- pub record_version: Option<u64>,
368
- pub reference: String,
369
- pub relevant_state_version: String,
370
- pub withheld_fields: Vec<String>,
371
- }
372
-
373
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
374
- pub struct DecisionEvaluation {
375
- pub result: DecisionResult,
376
- pub cached: bool,
377
- pub evidence: Vec<DecisionEvidence>,
378
- pub provenance: DecisionProvenance,
379
- }
380
-
381
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
382
- pub struct DecisionJudgmentRecord {
383
- pub id: String,
384
- pub lineage_key: String,
385
- pub kind: DecisionKind,
386
- pub collection: String,
387
- pub record_id: String,
388
- pub input: DecisionDefinition,
389
- pub schema_version: String,
390
- pub relevant_state_version: String,
391
- pub runtime: String,
392
- #[serde(default, skip_serializing_if = "Option::is_none")]
393
- pub model: Option<String>,
394
- #[serde(default, skip_serializing_if = "Option::is_none")]
395
- pub model_revision: Option<String>,
396
- #[serde(default, skip_serializing_if = "Option::is_none")]
397
- pub prompt_revision: Option<String>,
398
- #[serde(default, skip_serializing_if = "Option::is_none")]
399
- pub execution_method: Option<DecisionExecutionMethod>,
400
- #[serde(default, skip_serializing_if = "Option::is_none")]
401
- pub decision_schema_revision: Option<String>,
402
- pub result: DecisionResult,
403
- pub option_mass: f64,
404
- pub context_fields: Vec<String>,
405
- pub supporting_fields: Vec<String>,
406
- pub status: DecisionStatus,
407
- pub state: DecisionStateReference,
408
- pub evidence: DecisionEvidence,
409
- pub evaluated_at: u128,
410
- #[serde(default, skip_serializing_if = "Option::is_none")]
411
- pub superseded_at: Option<u128>,
412
- }
413
-
414
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
415
- pub struct DecisionOptions {
416
- #[serde(default)]
417
- pub fields: Option<Vec<String>>,
418
- #[serde(default)]
419
- pub application_id: Option<String>,
420
- #[serde(default)]
421
- pub decision_collection: Option<String>,
422
- #[serde(default)]
423
- pub schema_version: Option<String>,
424
- }
425
-
426
- impl Default for DecisionOptions {
427
- fn default() -> Self {
428
- Self {
429
- fields: None,
430
- application_id: None,
431
- decision_collection: None,
432
- schema_version: Some("semantic-decision-v1".into()),
433
- }
434
- }
435
- }
436
-
437
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
438
- pub struct DecisionTransportAuthorization {
439
- #[serde(default)]
440
- pub allowed_fields: Vec<String>,
441
- #[serde(default)]
442
- pub principal: Option<DecisionPrincipal>,
443
- }
444
-
445
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
446
- #[serde(tag = "kind", rename_all = "snake_case")]
447
- pub enum DecisionTransportRuntime {
448
- Deterministic {
449
- metadata: DecisionRuntimeMetadata,
450
- responses: BTreeMap<String, DecisionResult>,
451
- },
452
- Recording {
453
- metadata: DecisionRuntimeMetadata,
454
- result: DecisionResult,
455
- },
456
- }
457
-
458
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
459
- pub struct DecisionTransportExecutionRequest {
460
- #[serde(default)]
461
- pub target: Option<DecisionRecordRef>,
462
- #[serde(default, skip_serializing_if = "Option::is_none")]
463
- pub context: Option<Value>,
464
- pub definition: DecisionDefinition,
465
- #[serde(default)]
466
- pub options: DecisionOptions,
467
- #[serde(default)]
468
- pub authorization: Option<DecisionTransportAuthorization>,
469
- pub runtime: DecisionTransportRuntime,
470
- }
471
-
472
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
473
- pub struct DecisionTransportExecutionResponse {
474
- pub evaluation: DecisionEvaluation,
475
- #[serde(default)]
476
- pub runtime_requests: Vec<DecisionRequest>,
477
- }
478
-
479
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
480
- pub struct DecisionBatchDecision {
481
- pub id: String,
482
- pub definition: DecisionDefinition,
483
- #[serde(default = "empty_object")]
484
- pub context: Value,
485
- }
486
-
487
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
488
- pub struct DecisionBatch {
489
- #[serde(default = "empty_object")]
490
- pub context: Value,
491
- pub decisions: Vec<DecisionBatchDecision>,
492
- }
493
-
494
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
495
- pub struct DecisionBatchEvaluation {
496
- pub id: String,
497
- pub result: DecisionResult,
498
- }
499
-
500
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
501
- pub struct ChoicePermutationObservation {
502
- pub order: Vec<String>,
503
- pub selected: String,
504
- pub distribution: Vec<WeightedOption>,
505
- pub rank: usize,
506
- pub probability: f64,
507
- }
508
-
509
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
510
- pub struct ChoiceExpansionObservation {
511
- pub baseline: ChoicePermutationObservation,
512
- pub expanded: ChoicePermutationObservation,
513
- }
514
-
515
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
516
- pub struct ChoiceDiagnosticsReport {
517
- pub permutations: Vec<ChoicePermutationObservation>,
518
- #[serde(default, skip_serializing_if = "Option::is_none")]
519
- pub expansion: Option<ChoiceExpansionObservation>,
520
- }
521
-
522
- fn empty_object() -> Value {
523
- Value::Object(Map::new())
524
- }
525
-
526
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
527
- pub struct DecisionContextPacket {
528
- pub collection: String,
529
- pub record_id: String,
530
- pub record_version: Option<u64>,
531
- pub reference: String,
532
- pub fields: Vec<String>,
533
- pub values: BTreeMap<String, Value>,
534
- pub withheld_fields: Vec<String>,
535
- pub relevant_state_version: String,
536
- pub context_hash: String,
537
- pub authorization_decisions: Vec<String>,
538
- }
539
-
540
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
541
- pub struct DecisionBenchmarkCase {
542
- pub collection: String,
543
- pub record_id: String,
544
- pub input: DecisionDefinition,
545
- pub mutate: Option<Map<String, Value>>,
546
- }
547
-
548
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
549
- pub struct DecisionBenchmarkReport {
550
- pub binary: usize,
551
- pub choice: usize,
552
- pub score: usize,
553
- pub runtime_invocations_first_pass: usize,
554
- pub runtime_invocations_second_pass: usize,
555
- pub cache_hits_second_pass: usize,
556
- }
557
-
558
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
559
- pub struct DecisionError {
560
- pub code: String,
561
- pub message: String,
562
- }
563
-
564
- impl DecisionError {
565
- pub fn new(code: &str, message: impl Into<String>) -> Self {
566
- Self {
567
- code: code.into(),
568
- message: message.into(),
569
- }
570
- }
571
- }
572
-
573
- impl std::fmt::Display for DecisionError {
574
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575
- write!(f, "{}: {}", self.code, self.message)
576
- }
577
- }
578
-
579
- impl std::error::Error for DecisionError {}
580
-
581
- pub(crate) struct TransportAuthorizationGate {
582
- allowed_fields: BTreeSet<String>,
583
- }
584
-
585
- impl TransportAuthorizationGate {
586
- pub(crate) fn new(allowed_fields: &[String]) -> Self {
587
- Self {
588
- allowed_fields: allowed_fields.iter().cloned().collect(),
589
- }
590
- }
591
- }
592
-
593
- impl DecisionAuthorizationGate for TransportAuthorizationGate {
594
- fn check(
595
- &self,
596
- request: &AuthorizationRequest,
597
- ) -> Result<DecisionAuthorizationDecision, DecisionError> {
598
- let field = request
599
- .resource
600
- .uri
601
- .split('/')
602
- .next_back()
603
- .unwrap_or_default()
604
- .to_string();
605
- Ok(DecisionAuthorizationDecision {
606
- allowed: self.allowed_fields.contains(&field),
607
- decision_id: Some(digest(&[
608
- "semantic-decision-authorization-v1".into(),
609
- request.application_id.clone(),
610
- request.resource.uri.clone(),
611
- field,
612
- ])),
613
- })
614
- }
615
- }
616
-
617
- struct RecordingDecisionRuntime {
618
- metadata: DecisionRuntimeMetadata,
619
- result: DecisionResult,
620
- requests: Mutex<Vec<DecisionRequest>>,
621
- }
622
-
623
- impl RecordingDecisionRuntime {
624
- fn new(metadata: DecisionRuntimeMetadata, result: DecisionResult) -> Self {
625
- Self {
626
- metadata,
627
- result,
628
- requests: Mutex::new(Vec::new()),
629
- }
630
- }
631
-
632
- fn recorded_requests(&self) -> Vec<DecisionRequest> {
633
- self.requests
634
- .lock()
635
- .expect("recording runtime mutex")
636
- .clone()
637
- }
638
- }
639
-
640
- impl DecisionRuntime for RecordingDecisionRuntime {
641
- fn metadata(&self) -> DecisionRuntimeMetadata {
642
- self.metadata.clone()
643
- }
644
-
645
- fn decide(&self, request: &DecisionRequest) -> Result<DecisionResult, DecisionError> {
646
- self.requests
647
- .lock()
648
- .expect("recording runtime mutex")
649
- .push(request.clone());
650
- Ok(self.result.clone())
651
- }
652
- }
653
-
654
- enum TransportRuntimeHandle {
655
- Deterministic(DeterministicDecisionRuntime),
656
- Recording(RecordingDecisionRuntime),
657
- }
658
-
659
- impl TransportRuntimeHandle {
660
- fn new(runtime: &DecisionTransportRuntime) -> Self {
661
- match runtime {
662
- DecisionTransportRuntime::Deterministic {
663
- metadata,
664
- responses,
665
- } => Self::Deterministic(DeterministicDecisionRuntime::new(
666
- metadata.clone(),
667
- responses.clone(),
668
- )),
669
- DecisionTransportRuntime::Recording { metadata, result } => Self::Recording(
670
- RecordingDecisionRuntime::new(metadata.clone(), result.clone()),
671
- ),
672
- }
673
- }
674
-
675
- fn recorded_requests(&self) -> Vec<DecisionRequest> {
676
- match self {
677
- Self::Deterministic(_) => Vec::new(),
678
- Self::Recording(runtime) => runtime.recorded_requests(),
679
- }
680
- }
681
- }
682
-
683
- impl DecisionRuntime for TransportRuntimeHandle {
684
- fn metadata(&self) -> DecisionRuntimeMetadata {
685
- match self {
686
- Self::Deterministic(runtime) => runtime.metadata(),
687
- Self::Recording(runtime) => runtime.metadata(),
688
- }
689
- }
690
-
691
- fn decide(&self, request: &DecisionRequest) -> Result<DecisionResult, DecisionError> {
692
- match self {
693
- Self::Deterministic(runtime) => runtime.execute(request),
694
- Self::Recording(runtime) => runtime.execute(request),
695
- }
696
- }
697
- }
698
-
699
- fn digest(parts: &[String]) -> String {
700
- let mut hasher = Sha256::new();
701
- for part in parts {
702
- hasher.update(part.as_bytes());
703
- hasher.update([0x1f]);
704
- }
705
- format!("sha256:{:x}", hasher.finalize())
706
- }
707
-
708
- fn canonical_text(text: &str) -> String {
709
- text.split_whitespace().collect::<Vec<_>>().join(" ")
710
- }
711
-
712
- fn require_object(value: &Value, message: &str) -> Result<Map<String, Value>, DecisionError> {
713
- value
714
- .as_object()
715
- .cloned()
716
- .ok_or_else(|| DecisionError::new("DECISION_INVALID_RESULT", message))
717
- }
718
-
719
- fn canonical_values(values: &[String]) -> Result<Vec<String>, DecisionError> {
720
- if values.is_empty() {
721
- return Err(DecisionError::new(
722
- "DECISION_INVALID_OPTION",
723
- "closed-set decisions must declare at least one value",
724
- ));
725
- }
726
- let mut seen = BTreeSet::new();
727
- let mut normalized = Vec::with_capacity(values.len());
728
- for value in values {
729
- let canonical = canonical_text(value);
730
- if canonical.is_empty() {
731
- return Err(DecisionError::new(
732
- "DECISION_INVALID_OPTION",
733
- "closed-set decisions cannot declare an empty value",
734
- ));
735
- }
736
- if !seen.insert(canonical.clone()) {
737
- return Err(DecisionError::new(
738
- "DECISION_INVALID_OPTION",
739
- format!("closed-set decision declared \"{canonical}\" more than once"),
740
- ));
741
- }
742
- normalized.push(canonical);
743
- }
744
- Ok(normalized)
745
- }
746
-
747
- pub fn validate_decision_definition(
748
- input: &DecisionDefinition,
749
- ) -> Result<DecisionDefinition, DecisionError> {
750
- Ok(match input {
751
- DecisionDefinition::Binary { predicate } => {
752
- let predicate = canonical_text(predicate);
753
- if predicate.is_empty() {
754
- return Err(DecisionError::new(
755
- "DECISION_INVALID_RESULT",
756
- "binary decisions require a non-empty predicate",
757
- ));
758
- }
759
- DecisionDefinition::Binary { predicate }
760
- }
761
- DecisionDefinition::Choice { question, options } => DecisionDefinition::Choice {
762
- question: {
763
- let question = canonical_text(question);
764
- if question.is_empty() {
765
- return Err(DecisionError::new(
766
- "DECISION_INVALID_RESULT",
767
- "choice decisions require a non-empty question",
768
- ));
769
- }
770
- question
771
- },
772
- options: canonical_values(options)?,
773
- },
774
- DecisionDefinition::Score { question, levels } => DecisionDefinition::Score {
775
- question: {
776
- let question = canonical_text(question);
777
- if question.is_empty() {
778
- return Err(DecisionError::new(
779
- "DECISION_INVALID_RESULT",
780
- "score decisions require a non-empty question",
781
- ));
782
- }
783
- question
784
- },
785
- levels: canonical_values(levels)?,
786
- },
787
- })
788
- }
789
-
790
- fn distribution_by_values(
791
- values: &[String],
792
- observed: &[WeightedOption],
793
- ) -> Result<Vec<WeightedOption>, DecisionError> {
794
- let mut probabilities = BTreeMap::new();
795
- let mut total = 0.0;
796
- for entry in observed {
797
- if !values.contains(&entry.value) {
798
- return Err(DecisionError::new(
799
- "DECISION_INVALID_OPTION",
800
- format!("runtime returned undeclared option \"{}\"", entry.value),
801
- ));
802
- }
803
- if entry.probability.is_nan()
804
- || !entry.probability.is_finite()
805
- || entry.probability < 0.0
806
- || entry.probability > 1.0
807
- {
808
- return Err(DecisionError::new(
809
- "DECISION_INVALID_DISTRIBUTION",
810
- format!(
811
- "runtime returned an invalid probability for \"{}\"",
812
- entry.value
813
- ),
814
- ));
815
- }
816
- if probabilities
817
- .insert(entry.value.clone(), entry.probability)
818
- .is_some()
819
- {
820
- return Err(DecisionError::new(
821
- "DECISION_INVALID_DISTRIBUTION",
822
- format!(
823
- "runtime returned duplicate probability for \"{}\"",
824
- entry.value
825
- ),
826
- ));
827
- }
828
- total += entry.probability;
829
- }
830
- if probabilities.len() != values.len() {
831
- return Err(DecisionError::new(
832
- "DECISION_INVALID_DISTRIBUTION",
833
- "runtime did not return probabilities for every declared value",
834
- ));
835
- }
836
- if (total - 1.0).abs() > 1e-6 {
837
- return Err(DecisionError::new(
838
- "DECISION_INVALID_DISTRIBUTION",
839
- "runtime returned a distribution that does not sum to 1",
840
- ));
841
- }
842
- Ok(values
843
- .iter()
844
- .map(|value| WeightedOption {
845
- value: value.clone(),
846
- probability: *probabilities.get(value).unwrap_or(&0.0),
847
- })
848
- .collect())
849
- }
850
-
851
- pub fn validate_decision_result(
852
- request: &DecisionRequest,
853
- result: &DecisionResult,
854
- ) -> Result<DecisionResult, DecisionError> {
855
- let supporting_fields = result.supporting_fields().to_vec();
856
- let option_mass = result.option_mass();
857
- if option_mass.is_nan() || !option_mass.is_finite() || !(0.0..=1.0).contains(&option_mass) {
858
- return Err(DecisionError::new(
859
- "DECISION_INVALID_RESULT",
860
- "runtime returned an optionMass outside [0, 1]",
861
- ));
862
- }
863
-
864
- Ok(match (request, result) {
865
- (DecisionRequest::Binary { .. }, DecisionResult::Binary { decision, .. }) => {
866
- DecisionResult::Binary {
867
- decision: *decision,
868
- option_mass,
869
- supporting_fields,
870
- }
871
- }
872
- (
873
- DecisionRequest::Choice { options, .. },
874
- DecisionResult::Choice {
875
- selected,
876
- options: returned,
877
- ..
878
- },
879
- ) => {
880
- if !options.contains(selected) {
881
- return Err(DecisionError::new(
882
- "DECISION_INVALID_OPTION",
883
- format!("runtime selected undeclared option \"{selected}\""),
884
- ));
885
- }
886
- DecisionResult::Choice {
887
- selected: selected.clone(),
888
- options: distribution_by_values(options, returned)?,
889
- option_mass,
890
- supporting_fields,
891
- }
892
- }
893
- (
894
- DecisionRequest::Score { levels, .. },
895
- DecisionResult::Score {
896
- selected,
897
- levels: returned,
898
- ..
899
- },
900
- ) => {
901
- if !levels.contains(selected) {
902
- return Err(DecisionError::new(
903
- "DECISION_INVALID_OPTION",
904
- format!("runtime selected undeclared level \"{selected}\""),
905
- ));
906
- }
907
- DecisionResult::Score {
908
- selected: selected.clone(),
909
- levels: distribution_by_values(levels, returned)?,
910
- option_mass,
911
- supporting_fields,
912
- }
913
- }
914
- _ => {
915
- return Err(DecisionError::new(
916
- "DECISION_INVALID_RESULT",
917
- "runtime returned a result for the wrong decision kind",
918
- ))
919
- }
920
- })
921
- }
922
-
923
- fn selected_fields(record: &Map<String, Value>, fields: &Option<Vec<String>>) -> Vec<String> {
924
- let mut readable: Vec<String> = record
925
- .keys()
926
- .filter(|key| !matches!(key.as_str(), "id" | "__version"))
927
- .cloned()
928
- .collect();
929
- readable.sort();
930
- match fields {
931
- None => readable,
932
- Some(fields) => {
933
- let selected: BTreeSet<_> = fields.iter().cloned().collect();
934
- readable
935
- .into_iter()
936
- .filter(|field| selected.contains(field))
937
- .collect()
938
- }
939
- }
940
- }
941
-
942
- fn state_reference(application_id: Option<&str>, collection: &str, record_id: &str) -> String {
943
- match application_id {
944
- Some(application_id) if !application_id.is_empty() => {
945
- format!("flow://{application_id}/{collection}/{record_id}")
946
- }
947
- _ => format!("{collection}:{record_id}"),
948
- }
949
- }
950
-
951
- pub fn authorize_context(
952
- collection: &str,
953
- record_id: &str,
954
- record: &Value,
955
- options: &DecisionOptions,
956
- authorization: Option<(&dyn DecisionAuthorizationGate, &DecisionAuthorizationConfig)>,
957
- ) -> Result<DecisionContextPacket, DecisionError> {
958
- let object = record.as_object().ok_or_else(|| {
959
- DecisionError::new(
960
- "DECISION_INVALID_RESULT",
961
- "semantic decisions require object-shaped records",
962
- )
963
- })?;
964
- let record_version = object.get("__version").and_then(Value::as_u64);
965
- let selected = selected_fields(object, &options.fields);
966
- let deselected: BTreeSet<_> = object
967
- .keys()
968
- .filter(|field| !matches!(field.as_str(), "id" | "__version"))
969
- .filter(|field| !selected.contains(field))
970
- .cloned()
971
- .collect();
972
- let application_id = options.application_id.as_deref().or_else(|| {
973
- authorization
974
- .and_then(|(_, config)| config.principal.as_ref())
975
- .map(|principal| principal.application_id.as_str())
976
- });
977
- let mut authorized = Vec::new();
978
- let mut withheld: BTreeSet<String> = deselected;
979
- let mut decisions = Vec::new();
980
-
981
- for field in &selected {
982
- let allowed = if let Some((gate, config)) = authorization {
983
- let principal = config.principal.as_ref().ok_or_else(|| {
984
- DecisionError::new(
985
- "DECISION_INVALID_RESULT",
986
- "authorization config requires a principal",
987
- )
988
- })?;
989
- let uri = format!(
990
- "flow://{}/{}/{}/{}",
991
- principal.application_id, collection, record_id, field
992
- );
993
- let decision = gate.check(&AuthorizationRequest {
994
- subject: principal.subject.clone(),
995
- tenant_id: principal.tenant_id.clone(),
996
- application_id: principal.application_id.clone(),
997
- resource: crate::authorization::Resource {
998
- uri,
999
- attributes: BTreeMap::new(),
1000
- },
1001
- capability: principal.capability.clone(),
1002
- context: crate::authorization::AuthorizationContext::default(),
1003
- grant_id: None,
1004
- subject_attributes: BTreeMap::new(),
1005
- })?;
1006
- if let Some(decision_id) = decision.decision_id {
1007
- decisions.push(decision_id);
1008
- }
1009
- decision.allowed
1010
- } else {
1011
- true
1012
- };
1013
-
1014
- if allowed {
1015
- authorized.push(field.clone());
1016
- } else {
1017
- withheld.insert(field.clone());
1018
- }
1019
- }
1020
-
1021
- let mut values = BTreeMap::new();
1022
- for field in &authorized {
1023
- if let Some(value) = object.get(field) {
1024
- values.insert(field.clone(), value.clone());
1025
- }
1026
- }
1027
- let projection = json!({ "fields": authorized, "values": values });
1028
- let projection_json = serde_json::to_string(&projection)
1029
- .map_err(|error| DecisionError::new("DECISION_INVALID_RESULT", error.to_string()))?;
1030
- Ok(DecisionContextPacket {
1031
- collection: collection.into(),
1032
- record_id: record_id.into(),
1033
- record_version,
1034
- reference: state_reference(application_id, collection, record_id),
1035
- fields: projection["fields"]
1036
- .as_array()
1037
- .into_iter()
1038
- .flatten()
1039
- .filter_map(|value| value.as_str().map(str::to_string))
1040
- .collect(),
1041
- values,
1042
- withheld_fields: withheld.into_iter().collect(),
1043
- relevant_state_version: digest(&[
1044
- "semantic-decision-state-v1".into(),
1045
- collection.into(),
1046
- record_id.into(),
1047
- projection_json.clone(),
1048
- ]),
1049
- context_hash: digest(&["semantic-decision-context-v1".into(), projection_json]),
1050
- authorization_decisions: decisions,
1051
- })
1052
- }
1053
-
1054
- fn request_with_context(
1055
- definition: &DecisionDefinition,
1056
- context: &DecisionContextPacket,
1057
- ) -> DecisionRequest {
1058
- request_with_values(definition, &context.values)
1059
- }
1060
-
1061
- fn request_with_values(
1062
- definition: &DecisionDefinition,
1063
- values: &BTreeMap<String, Value>,
1064
- ) -> DecisionRequest {
1065
- let context_value = Value::Object(
1066
- values
1067
- .iter()
1068
- .map(|(field, value)| (field.clone(), value.clone()))
1069
- .collect(),
1070
- );
1071
- match definition {
1072
- DecisionDefinition::Binary { predicate } => DecisionRequest::Binary {
1073
- predicate: predicate.clone(),
1074
- context: context_value,
1075
- },
1076
- DecisionDefinition::Choice { question, options } => DecisionRequest::Choice {
1077
- question: question.clone(),
1078
- options: options.clone(),
1079
- context: context_value,
1080
- },
1081
- DecisionDefinition::Score { question, levels } => DecisionRequest::Score {
1082
- question: question.clone(),
1083
- levels: levels.clone(),
1084
- context: context_value,
1085
- },
1086
- }
1087
- }
1088
-
1089
- fn require_runtime_capability(
1090
- runtime: &dyn DecisionRuntime,
1091
- kind: &DecisionKind,
1092
- ) -> Result<(), DecisionError> {
1093
- let capabilities = runtime.capabilities();
1094
- let supported = match kind {
1095
- DecisionKind::Binary => capabilities.binary,
1096
- DecisionKind::Choice => capabilities.choice,
1097
- DecisionKind::Score => capabilities.score,
1098
- };
1099
- if supported {
1100
- Ok(())
1101
- } else {
1102
- Err(DecisionError::new(
1103
- "DECISION_UNSUPPORTED",
1104
- format!("runtime does not support {:?} decisions", kind),
1105
- ))
1106
- }
1107
- }
1108
-
1109
- fn decision_collection<'a>(options: &'a DecisionOptions) -> &'a str {
1110
- options
1111
- .decision_collection
1112
- .as_deref()
1113
- .unwrap_or(DEFAULT_DECISION_COLLECTION)
1114
- }
1115
-
1116
- fn schema_version(options: &DecisionOptions) -> &str {
1117
- options
1118
- .schema_version
1119
- .as_deref()
1120
- .unwrap_or("semantic-decision-v1")
1121
- }
1122
-
1123
- fn lineage_key(
1124
- collection: &str,
1125
- record_id: &str,
1126
- definition: &DecisionDefinition,
1127
- runtime: &DecisionRuntimeMetadata,
1128
- schema_version: &str,
1129
- ) -> String {
1130
- digest(&[
1131
- "semantic-decision-lineage-v1".into(),
1132
- format!("{:?}", definition.kind()),
1133
- collection.into(),
1134
- record_id.into(),
1135
- canonical_text(definition.prompt()),
1136
- serde_json::to_string(definition.schema_values()).unwrap_or_default(),
1137
- runtime.runtime.clone(),
1138
- runtime
1139
- .model_revision
1140
- .clone()
1141
- .or(runtime.model.clone())
1142
- .unwrap_or_default(),
1143
- runtime.prompt_revision.clone().unwrap_or_default(),
1144
- serde_json::to_string(&runtime.execution_method).unwrap_or_default(),
1145
- runtime.decision_schema_revision.clone().unwrap_or_default(),
1146
- schema_version.into(),
1147
- ])
1148
- }
1149
-
1150
- pub fn decision_cache_key(
1151
- collection: &str,
1152
- record_id: &str,
1153
- definition: &DecisionDefinition,
1154
- context: &DecisionContextPacket,
1155
- runtime: &DecisionRuntimeMetadata,
1156
- schema_version: &str,
1157
- ) -> String {
1158
- digest(&[
1159
- "semantic-decision-cache-v1".into(),
1160
- format!("{:?}", definition.kind()),
1161
- collection.into(),
1162
- record_id.into(),
1163
- canonical_text(definition.prompt()),
1164
- serde_json::to_string(definition.schema_values()).unwrap_or_default(),
1165
- context.relevant_state_version.clone(),
1166
- runtime.runtime.clone(),
1167
- runtime
1168
- .model_revision
1169
- .clone()
1170
- .or(runtime.model.clone())
1171
- .unwrap_or_default(),
1172
- runtime.prompt_revision.clone().unwrap_or_default(),
1173
- serde_json::to_string(&runtime.execution_method).unwrap_or_default(),
1174
- runtime.decision_schema_revision.clone().unwrap_or_default(),
1175
- schema_version.into(),
1176
- ])
1177
- }
1178
-
1179
- fn valid_supporting_fields(
1180
- context: &DecisionContextPacket,
1181
- result: &DecisionResult,
1182
- ) -> Vec<String> {
1183
- result
1184
- .supporting_fields()
1185
- .iter()
1186
- .filter(|field| context.fields.contains(field))
1187
- .cloned()
1188
- .collect()
1189
- }
1190
-
1191
- fn supporting_context(
1192
- context: &DecisionContextPacket,
1193
- supporting_fields: &[String],
1194
- ) -> Vec<DecisionEvidenceField> {
1195
- let fields = if supporting_fields.is_empty() {
1196
- &context.fields
1197
- } else {
1198
- supporting_fields
1199
- };
1200
- fields
1201
- .iter()
1202
- .filter_map(|field| {
1203
- context
1204
- .values
1205
- .get(field)
1206
- .map(|value| DecisionEvidenceField {
1207
- field: field.clone(),
1208
- value: value.clone(),
1209
- })
1210
- })
1211
- .collect()
1212
- }
1213
-
1214
- fn load_judgment(
1215
- db: &FeltDb,
1216
- collection: &str,
1217
- key: &str,
1218
- ) -> Result<Option<DecisionJudgmentRecord>, DecisionError> {
1219
- let storage_key = format!("{collection}:{key}");
1220
- db.capability(collection)
1221
- .get(&storage_key)
1222
- .map_err(|error| DecisionError::new("DECISION_STORAGE_ERROR", error.to_string()))
1223
- }
1224
-
1225
- fn update_judgment(
1226
- db: &FeltDb,
1227
- collection: &str,
1228
- key: &str,
1229
- record: &DecisionJudgmentRecord,
1230
- ) -> Result<(), DecisionError> {
1231
- let storage_key = format!("{collection}:{key}");
1232
- db.capability(collection)
1233
- .update(&storage_key, record.clone())
1234
- .map_err(|error| DecisionError::new("DECISION_STORAGE_ERROR", error.to_string()))
1235
- }
1236
-
1237
- fn insert_judgment(
1238
- db: &FeltDb,
1239
- collection: &str,
1240
- key: &str,
1241
- record: &DecisionJudgmentRecord,
1242
- ) -> Result<(), DecisionError> {
1243
- let storage_key = format!("{collection}:{key}");
1244
- db.capability(collection)
1245
- .insert_with_key(&storage_key, record.clone())
1246
- .map_err(|error| DecisionError::new("DECISION_STORAGE_ERROR", error.to_string()))
1247
- }
1248
-
1249
- fn supersede_lineage(
1250
- db: &FeltDb,
1251
- decision_collection: &str,
1252
- lineage_key: &str,
1253
- keep: &str,
1254
- ) -> Result<(), DecisionError> {
1255
- let stale: Vec<DecisionJudgmentRecord> = db
1256
- .capability(decision_collection)
1257
- .find(|row: &DecisionJudgmentRecord| {
1258
- row.lineage_key == lineage_key && row.status == DecisionStatus::Active && row.id != keep
1259
- })
1260
- .map_err(|error| DecisionError::new("DECISION_STORAGE_ERROR", error.to_string()))?;
1261
- let superseded_at = semantic_now_ms();
1262
- for mut row in stale {
1263
- row.status = DecisionStatus::Superseded;
1264
- row.superseded_at = Some(superseded_at);
1265
- update_judgment(db, decision_collection, &row.id, &row)?;
1266
- }
1267
- Ok(())
1268
- }
1269
-
1270
- fn stored_to_evaluation(
1271
- stored: DecisionJudgmentRecord,
1272
- context: &DecisionContextPacket,
1273
- source: &str,
1274
- ) -> DecisionEvaluation {
1275
- DecisionEvaluation {
1276
- result: stored.result.clone(),
1277
- cached: source == "cache",
1278
- evidence: vec![DecisionEvidence {
1279
- decision_id: stored.id.clone(),
1280
- kind: stored.kind.clone(),
1281
- input: stored.input.clone(),
1282
- result: stored.result.clone(),
1283
- supporting_context: stored
1284
- .supporting_fields
1285
- .iter()
1286
- .filter_map(|field| {
1287
- context
1288
- .values
1289
- .get(field)
1290
- .map(|value| DecisionEvidenceField {
1291
- field: field.clone(),
1292
- value: value.clone(),
1293
- })
1294
- })
1295
- .collect(),
1296
- runtime: DecisionRuntimeMetadata {
1297
- runtime: stored.runtime.clone(),
1298
- model: stored.model.clone(),
1299
- model_revision: stored.model_revision.clone(),
1300
- prompt_revision: stored.prompt_revision.clone(),
1301
- execution_method: stored.execution_method.clone(),
1302
- decision_schema_revision: stored.decision_schema_revision.clone(),
1303
- },
1304
- state: stored.state.clone(),
1305
- }],
1306
- provenance: DecisionProvenance {
1307
- runtime: stored.runtime,
1308
- model: stored.model,
1309
- model_revision: stored.model_revision,
1310
- prompt_revision: stored.prompt_revision,
1311
- execution_method: stored.execution_method,
1312
- decision_schema_revision: stored.decision_schema_revision,
1313
- context_hash: context.context_hash.clone(),
1314
- relevant_state_version: context.relevant_state_version.clone(),
1315
- cache_key: stored.id,
1316
- source: source.into(),
1317
- evaluated_at: stored.evaluated_at,
1318
- served_at: semantic_now_ms(),
1319
- },
1320
- }
1321
- }
1322
-
1323
- /// The stored judgment whose identity matches `cache_key`, if one exists.
1324
- ///
1325
- /// A judgment superseded by an intermediate state change that has since been
1326
- /// reversed carries the same identity as the state it was derived from, so it
1327
- /// is reactivated rather than re-derived: the lineage's other judgments are
1328
- /// superseded and this one becomes active again.
1329
- fn reusable_judgment(
1330
- db: &FeltDb,
1331
- storage: &str,
1332
- cache_key: &str,
1333
- lineage: &str,
1334
- ) -> Result<Option<DecisionJudgmentRecord>, DecisionError> {
1335
- let Some(mut stored) = load_judgment(db, storage, cache_key)? else {
1336
- return Ok(None);
1337
- };
1338
- if stored.status != DecisionStatus::Active {
1339
- supersede_lineage(db, storage, lineage, &stored.id)?;
1340
- stored.status = DecisionStatus::Active;
1341
- stored.superseded_at = None;
1342
- update_judgment(db, storage, &stored.id, &stored)?;
1343
- }
1344
- Ok(Some(stored))
1345
- }
1346
-
1347
- /// Reuse a durable judgment for a record without invoking the runtime.
1348
- ///
1349
- /// This is the `reuse-only` path of semantic query composition: the record is
1350
- /// read and authorized exactly as `decide_record` reads and authorizes it, the
1351
- /// judgment identity is derived from that authorized state and the runtime's
1352
- /// metadata, and the stored judgment is returned when one with that identity
1353
- /// exists. `Ok(None)` means no reusable judgment exists; nothing is executed
1354
- /// and nothing is written except the reactivation described on
1355
- /// `reusable_judgment`.
1356
- pub fn reuse_record_judgment(
1357
- db: &FeltDb,
1358
- target: &DecisionRecordRef,
1359
- definition: &DecisionDefinition,
1360
- runtime_metadata: &DecisionRuntimeMetadata,
1361
- options: &DecisionOptions,
1362
- authorization: Option<(&dyn DecisionAuthorizationGate, &DecisionAuthorizationConfig)>,
1363
- ) -> Result<Option<DecisionEvaluation>, DecisionError> {
1364
- let definition = validate_decision_definition(definition)?;
1365
- let record: Value = db
1366
- .capability(&target.collection)
1367
- .get(&target.record_id)
1368
- .map_err(|error| DecisionError::new("DECISION_STORAGE_ERROR", error.to_string()))?
1369
- .ok_or_else(|| {
1370
- DecisionError::new(
1371
- "DECISION_RECORD_NOT_FOUND",
1372
- "decision target record not found",
1373
- )
1374
- })?;
1375
- let context = authorize_context(
1376
- &target.collection,
1377
- &target.record_id,
1378
- &record,
1379
- options,
1380
- authorization,
1381
- )?;
1382
- if context.fields.is_empty() {
1383
- return Err(DecisionError::new(
1384
- "DECISION_CONTEXT_EMPTY",
1385
- "decision refuses to evaluate an empty authorized context",
1386
- ));
1387
- }
1388
- let cache_key = decision_cache_key(
1389
- &target.collection,
1390
- &target.record_id,
1391
- &definition,
1392
- &context,
1393
- runtime_metadata,
1394
- schema_version(options),
1395
- );
1396
- let lineage = lineage_key(
1397
- &target.collection,
1398
- &target.record_id,
1399
- &definition,
1400
- runtime_metadata,
1401
- schema_version(options),
1402
- );
1403
- Ok(
1404
- reusable_judgment(db, decision_collection(options), &cache_key, &lineage)?
1405
- .map(|stored| stored_to_evaluation(stored, &context, "cache")),
1406
- )
1407
- }
1408
-
1409
- /// Build the runtime binding a FeltDB deployment configures for semantic
1410
- /// query composition.
1411
- ///
1412
- /// A query never names its runtime; the deployment does, and the binding's
1413
- /// identity becomes part of every judgment the query stage produces. Only the
1414
- /// deterministic runtime exists in Rust today, so this is the only binding a
1415
- /// deployment can configure; the recording runtime is a transport test double
1416
- /// that answers every request identically and is refused here.
1417
- pub fn configured_decision_runtime(
1418
- runtime: &DecisionTransportRuntime,
1419
- ) -> Result<Arc<dyn DecisionRuntime + Send + Sync>, DecisionError> {
1420
- match runtime {
1421
- DecisionTransportRuntime::Deterministic {
1422
- metadata,
1423
- responses,
1424
- } => Ok(Arc::new(DeterministicDecisionRuntime::new(
1425
- metadata.clone(),
1426
- responses.clone(),
1427
- ))),
1428
- DecisionTransportRuntime::Recording { .. } => Err(DecisionError::new(
1429
- "DECISION_UNSUPPORTED",
1430
- "the recording runtime is a transport test double, not a configurable runtime binding",
1431
- )),
1432
- }
1433
- }
1434
-
1435
- pub fn decide_record(
1436
- db: &FeltDb,
1437
- target: &DecisionRecordRef,
1438
- definition: &DecisionDefinition,
1439
- runtime: &dyn DecisionRuntime,
1440
- options: &DecisionOptions,
1441
- authorization: Option<(&dyn DecisionAuthorizationGate, &DecisionAuthorizationConfig)>,
1442
- ) -> Result<DecisionEvaluation, DecisionError> {
1443
- let definition = validate_decision_definition(definition)?;
1444
- require_runtime_capability(runtime, &definition.kind())?;
1445
- let record: Value = db
1446
- .capability(&target.collection)
1447
- .get(&target.record_id)
1448
- .map_err(|error| DecisionError::new("DECISION_STORAGE_ERROR", error.to_string()))?
1449
- .ok_or_else(|| {
1450
- DecisionError::new(
1451
- "DECISION_RECORD_NOT_FOUND",
1452
- "decision target record not found",
1453
- )
1454
- })?;
1455
- let context = authorize_context(
1456
- &target.collection,
1457
- &target.record_id,
1458
- &record,
1459
- options,
1460
- authorization,
1461
- )?;
1462
- if context.fields.is_empty() {
1463
- return Err(DecisionError::new(
1464
- "DECISION_CONTEXT_EMPTY",
1465
- "decision refuses to evaluate an empty authorized context",
1466
- ));
1467
- }
1468
- let runtime_metadata = runtime.metadata();
1469
- let cache_key = decision_cache_key(
1470
- &target.collection,
1471
- &target.record_id,
1472
- &definition,
1473
- &context,
1474
- &runtime_metadata,
1475
- schema_version(options),
1476
- );
1477
- let lineage = lineage_key(
1478
- &target.collection,
1479
- &target.record_id,
1480
- &definition,
1481
- &runtime_metadata,
1482
- schema_version(options),
1483
- );
1484
- let storage = decision_collection(options);
1485
- if let Some(stored) = reusable_judgment(db, storage, &cache_key, &lineage)? {
1486
- return Ok(stored_to_evaluation(stored, &context, "cache"));
1487
- }
1488
-
1489
- let request = request_with_context(&definition, &context);
1490
- let result = validate_decision_result(&request, &runtime.execute(&request)?)?;
1491
- let evaluated_at = semantic_now_ms();
1492
- let supporting_fields = valid_supporting_fields(&context, &result);
1493
- let supporting_context = supporting_context(&context, &supporting_fields);
1494
- let state = DecisionStateReference {
1495
- collection: target.collection.clone(),
1496
- record_id: target.record_id.clone(),
1497
- record_version: context.record_version,
1498
- reference: context.reference.clone(),
1499
- relevant_state_version: context.relevant_state_version.clone(),
1500
- withheld_fields: context.withheld_fields.clone(),
1501
- };
1502
- let evidence = DecisionEvidence {
1503
- decision_id: cache_key.clone(),
1504
- kind: definition.kind(),
1505
- input: definition.clone(),
1506
- result: result.clone(),
1507
- supporting_context,
1508
- runtime: runtime_metadata.clone(),
1509
- state: state.clone(),
1510
- };
1511
- let record = DecisionJudgmentRecord {
1512
- id: cache_key.clone(),
1513
- lineage_key: lineage.clone(),
1514
- kind: definition.kind(),
1515
- collection: target.collection.clone(),
1516
- record_id: target.record_id.clone(),
1517
- input: definition.clone(),
1518
- schema_version: schema_version(options).into(),
1519
- relevant_state_version: context.relevant_state_version.clone(),
1520
- runtime: runtime_metadata.runtime.clone(),
1521
- model: runtime_metadata.model.clone(),
1522
- model_revision: runtime_metadata.model_revision.clone(),
1523
- prompt_revision: runtime_metadata.prompt_revision.clone(),
1524
- execution_method: runtime_metadata.execution_method.clone(),
1525
- decision_schema_revision: runtime_metadata.decision_schema_revision.clone(),
1526
- option_mass: result.option_mass(),
1527
- context_fields: context.fields.clone(),
1528
- supporting_fields: if result.supporting_fields().is_empty() {
1529
- context.fields.clone()
1530
- } else {
1531
- supporting_fields.clone()
1532
- },
1533
- status: DecisionStatus::Active,
1534
- state,
1535
- evidence: evidence.clone(),
1536
- result: result.clone(),
1537
- evaluated_at,
1538
- superseded_at: None,
1539
- };
1540
- supersede_lineage(db, storage, &lineage, &cache_key)?;
1541
- insert_judgment(db, storage, &cache_key, &record)?;
1542
- Ok(DecisionEvaluation {
1543
- result,
1544
- cached: false,
1545
- evidence: vec![evidence],
1546
- provenance: DecisionProvenance {
1547
- runtime: runtime_metadata.runtime,
1548
- model: runtime_metadata.model,
1549
- model_revision: runtime_metadata.model_revision,
1550
- prompt_revision: runtime_metadata.prompt_revision,
1551
- execution_method: runtime_metadata.execution_method,
1552
- decision_schema_revision: runtime_metadata.decision_schema_revision,
1553
- context_hash: context.context_hash,
1554
- relevant_state_version: context.relevant_state_version,
1555
- cache_key,
1556
- source: "runtime".into(),
1557
- evaluated_at,
1558
- served_at: evaluated_at,
1559
- },
1560
- })
1561
- }
1562
-
1563
- fn evaluate_context_packet(
1564
- target: DecisionRecordRef,
1565
- definition: &DecisionDefinition,
1566
- context: &DecisionContextPacket,
1567
- runtime: &dyn DecisionRuntime,
1568
- options: &DecisionOptions,
1569
- ) -> Result<DecisionEvaluation, DecisionError> {
1570
- let definition = validate_decision_definition(definition)?;
1571
- require_runtime_capability(runtime, &definition.kind())?;
1572
- if context.fields.is_empty() {
1573
- return Err(DecisionError::new(
1574
- "DECISION_CONTEXT_EMPTY",
1575
- "decision refuses to evaluate an empty authorized context",
1576
- ));
1577
- }
1578
- let runtime_metadata = runtime.metadata();
1579
- let cache_key = decision_cache_key(
1580
- &target.collection,
1581
- &target.record_id,
1582
- &definition,
1583
- context,
1584
- &runtime_metadata,
1585
- schema_version(options),
1586
- );
1587
- let request = request_with_context(&definition, context);
1588
- let result = validate_decision_result(&request, &runtime.execute(&request)?)?;
1589
- let evaluated_at = semantic_now_ms();
1590
- let supporting_fields = valid_supporting_fields(context, &result);
1591
- let state = DecisionStateReference {
1592
- collection: target.collection,
1593
- record_id: target.record_id,
1594
- record_version: context.record_version,
1595
- reference: context.reference.clone(),
1596
- relevant_state_version: context.relevant_state_version.clone(),
1597
- withheld_fields: context.withheld_fields.clone(),
1598
- };
1599
- let evidence = DecisionEvidence {
1600
- decision_id: cache_key.clone(),
1601
- kind: definition.kind(),
1602
- input: definition,
1603
- result: result.clone(),
1604
- supporting_context: supporting_context(context, &supporting_fields),
1605
- runtime: runtime_metadata.clone(),
1606
- state,
1607
- };
1608
- Ok(DecisionEvaluation {
1609
- result,
1610
- cached: false,
1611
- evidence: vec![evidence],
1612
- provenance: DecisionProvenance {
1613
- runtime: runtime_metadata.runtime,
1614
- model: runtime_metadata.model,
1615
- model_revision: runtime_metadata.model_revision,
1616
- prompt_revision: runtime_metadata.prompt_revision,
1617
- execution_method: runtime_metadata.execution_method,
1618
- decision_schema_revision: runtime_metadata.decision_schema_revision,
1619
- context_hash: context.context_hash.clone(),
1620
- relevant_state_version: context.relevant_state_version.clone(),
1621
- cache_key,
1622
- source: "runtime".into(),
1623
- evaluated_at,
1624
- served_at: evaluated_at,
1625
- },
1626
- })
1627
- }
1628
-
1629
- fn inline_context_packet(
1630
- request: &DecisionTransportExecutionRequest,
1631
- authorization: Option<(&dyn DecisionAuthorizationGate, &DecisionAuthorizationConfig)>,
1632
- ) -> Result<DecisionContextPacket, DecisionError> {
1633
- let context = request.context.as_ref().ok_or_else(|| {
1634
- DecisionError::new(
1635
- "DECISION_INVALID_RESULT",
1636
- "inline decision transport requires a context object",
1637
- )
1638
- })?;
1639
- authorize_context(
1640
- "$context",
1641
- "inline",
1642
- context,
1643
- &request.options,
1644
- authorization,
1645
- )
1646
- }
1647
-
1648
- pub fn execute_inline_decision_transport(
1649
- request: &DecisionTransportExecutionRequest,
1650
- ) -> Result<DecisionTransportExecutionResponse, DecisionError> {
1651
- let runtime = TransportRuntimeHandle::new(&request.runtime);
1652
- let authorization_gate = request.authorization.as_ref().map(|authorization| {
1653
- (
1654
- TransportAuthorizationGate::new(&authorization.allowed_fields),
1655
- DecisionAuthorizationConfig {
1656
- principal: authorization.principal.clone(),
1657
- },
1658
- )
1659
- });
1660
- let authorization = authorization_gate
1661
- .as_ref()
1662
- .map(|(gate, config)| (gate as &dyn DecisionAuthorizationGate, config));
1663
- if request.target.is_some() {
1664
- return Err(DecisionError::new(
1665
- "DECISION_INVALID_RESULT",
1666
- "inline decision transport does not support durable target records",
1667
- ));
1668
- }
1669
- let evaluation = evaluate_context_packet(
1670
- DecisionRecordRef {
1671
- collection: "$context".into(),
1672
- record_id: "inline".into(),
1673
- },
1674
- &request.definition,
1675
- &inline_context_packet(request, authorization)?,
1676
- &runtime,
1677
- &request.options,
1678
- )?;
1679
- Ok(DecisionTransportExecutionResponse {
1680
- evaluation,
1681
- runtime_requests: runtime.recorded_requests(),
1682
- })
1683
- }
1684
-
1685
- pub fn execute_decision_transport(
1686
- db: &FeltDb,
1687
- request: &DecisionTransportExecutionRequest,
1688
- ) -> Result<DecisionTransportExecutionResponse, DecisionError> {
1689
- let runtime = TransportRuntimeHandle::new(&request.runtime);
1690
- let authorization_gate = request.authorization.as_ref().map(|authorization| {
1691
- (
1692
- TransportAuthorizationGate {
1693
- allowed_fields: authorization.allowed_fields.iter().cloned().collect(),
1694
- },
1695
- DecisionAuthorizationConfig {
1696
- principal: authorization.principal.clone(),
1697
- },
1698
- )
1699
- });
1700
- let authorization = authorization_gate
1701
- .as_ref()
1702
- .map(|(gate, config)| (gate as &dyn DecisionAuthorizationGate, config));
1703
- let evaluation = match (&request.target, &request.context) {
1704
- (Some(_), Some(_)) => return Err(DecisionError::new(
1705
- "DECISION_INVALID_RESULT",
1706
- "decision transport accepts either a target record or an inline context, but not both",
1707
- )),
1708
- (Some(target), None) => decide_record(
1709
- db,
1710
- target,
1711
- &request.definition,
1712
- &runtime,
1713
- &request.options,
1714
- authorization,
1715
- )?,
1716
- (None, Some(context)) => evaluate_context_packet(
1717
- DecisionRecordRef {
1718
- collection: "$context".into(),
1719
- record_id: "inline".into(),
1720
- },
1721
- &request.definition,
1722
- &authorize_context(
1723
- "$context",
1724
- "inline",
1725
- context,
1726
- &request.options,
1727
- authorization,
1728
- )?,
1729
- &runtime,
1730
- &request.options,
1731
- )?,
1732
- (None, None) => {
1733
- return Err(DecisionError::new(
1734
- "DECISION_INVALID_RESULT",
1735
- "decision transport requires either a target record or an inline context",
1736
- ))
1737
- }
1738
- };
1739
- Ok(DecisionTransportExecutionResponse {
1740
- evaluation,
1741
- runtime_requests: runtime.recorded_requests(),
1742
- })
1743
- }
1744
-
1745
- pub fn execute_decision_batch(
1746
- context: &DecisionContextPacket,
1747
- batch: &DecisionBatch,
1748
- runtime: &dyn DecisionRuntime,
1749
- ) -> Result<Vec<DecisionBatchEvaluation>, DecisionError> {
1750
- if batch.decisions.is_empty() {
1751
- return Err(DecisionError::new(
1752
- "DECISION_INVALID_RESULT",
1753
- "decision batches require at least one decision",
1754
- ));
1755
- }
1756
- if !runtime.capabilities().batch {
1757
- return Err(DecisionError::new(
1758
- "DECISION_UNSUPPORTED",
1759
- "runtime does not support decision batches",
1760
- ));
1761
- }
1762
- let mut ids = BTreeSet::new();
1763
- let shared = require_object(
1764
- &batch.context,
1765
- "decision batch shared context must be object-shaped",
1766
- )?;
1767
- let mut base = context.values.clone();
1768
- for (field, value) in shared {
1769
- base.insert(field, value);
1770
- }
1771
- let mut requests = Vec::with_capacity(batch.decisions.len());
1772
- let mut ids_in_order = Vec::with_capacity(batch.decisions.len());
1773
- for decision in &batch.decisions {
1774
- if canonical_text(&decision.id).is_empty() || !ids.insert(decision.id.clone()) {
1775
- return Err(DecisionError::new(
1776
- "DECISION_INVALID_RESULT",
1777
- "decision batches require unique non-empty decision ids",
1778
- ));
1779
- }
1780
- let local = require_object(
1781
- &decision.context,
1782
- "decision batch decision context must be object-shaped",
1783
- )?;
1784
- let definition = validate_decision_definition(&decision.definition)?;
1785
- let mut merged = base.clone();
1786
- for (field, value) in local {
1787
- merged.insert(field, value);
1788
- }
1789
- let request = request_with_values(&definition, &merged);
1790
- ids_in_order.push(decision.id.clone());
1791
- requests.push(request);
1792
- }
1793
- let results = runtime.execute_batch(&requests)?;
1794
- if results.len() != requests.len() {
1795
- return Err(DecisionError::new(
1796
- "DECISION_INVALID_RESULT",
1797
- "runtime returned a batch result count different from the request count",
1798
- ));
1799
- }
1800
- let mut evaluations = Vec::with_capacity(results.len());
1801
- for ((request, result), id) in requests.into_iter().zip(results).zip(ids_in_order) {
1802
- let result = validate_decision_result(&request, &result)?;
1803
- evaluations.push(DecisionBatchEvaluation { id, result });
1804
- }
1805
- Ok(evaluations)
1806
- }
1807
-
1808
- fn permutations(values: &[String]) -> Vec<Vec<String>> {
1809
- if values.len() <= 1 {
1810
- return vec![values.to_vec()];
1811
- }
1812
- let mut result = Vec::new();
1813
- for index in 0..values.len() {
1814
- let mut remaining = values.to_vec();
1815
- let head = remaining.remove(index);
1816
- for mut tail in permutations(&remaining) {
1817
- let mut permutation = vec![head.clone()];
1818
- permutation.append(&mut tail);
1819
- result.push(permutation);
1820
- }
1821
- }
1822
- result
1823
- }
1824
-
1825
- fn observe_choice(
1826
- context: &DecisionContextPacket,
1827
- question: &str,
1828
- options: Vec<String>,
1829
- runtime: &dyn DecisionRuntime,
1830
- ) -> Result<ChoicePermutationObservation, DecisionError> {
1831
- let definition = validate_decision_definition(&DecisionDefinition::Choice {
1832
- question: question.into(),
1833
- options: options.clone(),
1834
- })?;
1835
- let request = request_with_context(&definition, context);
1836
- let result = validate_decision_result(&request, &runtime.execute(&request)?)?;
1837
- match result {
1838
- DecisionResult::Choice {
1839
- selected,
1840
- options: distribution,
1841
- ..
1842
- } => {
1843
- let rank = distribution
1844
- .iter()
1845
- .enumerate()
1846
- .find_map(|(index, option)| (option.value == selected).then_some(index + 1))
1847
- .unwrap_or(distribution.len());
1848
- let probability = distribution
1849
- .iter()
1850
- .find_map(|option| (option.value == selected).then_some(option.probability))
1851
- .unwrap_or(0.0);
1852
- Ok(ChoicePermutationObservation {
1853
- order: options,
1854
- selected,
1855
- distribution,
1856
- rank,
1857
- probability,
1858
- })
1859
- }
1860
- _ => Err(DecisionError::new(
1861
- "DECISION_INVALID_RESULT",
1862
- "choice diagnostics require a choice decision",
1863
- )),
1864
- }
1865
- }
1866
-
1867
- pub fn run_choice_diagnostics(
1868
- context: &DecisionContextPacket,
1869
- question: &str,
1870
- options: &[String],
1871
- expansion_option: Option<&str>,
1872
- runtime: &dyn DecisionRuntime,
1873
- ) -> Result<ChoiceDiagnosticsReport, DecisionError> {
1874
- let canonical = validate_decision_definition(&DecisionDefinition::Choice {
1875
- question: question.into(),
1876
- options: options.to_vec(),
1877
- })?;
1878
- let base_options = canonical.schema_values().to_vec();
1879
- let mut permutations_observed = Vec::new();
1880
- for order in permutations(&base_options) {
1881
- permutations_observed.push(observe_choice(context, question, order, runtime)?);
1882
- }
1883
- let expansion = match expansion_option {
1884
- Some(option) => {
1885
- let baseline = observe_choice(context, question, base_options.clone(), runtime)?;
1886
- let mut expanded = base_options.clone();
1887
- expanded.push(option.into());
1888
- Some(ChoiceExpansionObservation {
1889
- baseline,
1890
- expanded: observe_choice(context, question, expanded, runtime)?,
1891
- })
1892
- }
1893
- None => None,
1894
- };
1895
- Ok(ChoiceDiagnosticsReport {
1896
- permutations: permutations_observed,
1897
- expansion,
1898
- })
1899
- }
1900
-
1901
- pub fn jev(
1902
- db: &FeltDb,
1903
- target: &DecisionRecordRef,
1904
- predicate: &str,
1905
- runtime: &dyn DecisionRuntime,
1906
- options: &DecisionOptions,
1907
- authorization: Option<(&dyn DecisionAuthorizationGate, &DecisionAuthorizationConfig)>,
1908
- ) -> Result<DecisionEvaluation, DecisionError> {
1909
- decide_record(
1910
- db,
1911
- target,
1912
- &DecisionDefinition::Binary {
1913
- predicate: predicate.into(),
1914
- },
1915
- runtime,
1916
- options,
1917
- authorization,
1918
- )
1919
- }
1920
-
1921
- #[derive(Debug, Clone)]
1922
- pub struct DeterministicDecisionRuntime {
1923
- metadata: DecisionRuntimeMetadata,
1924
- responses: BTreeMap<String, DecisionResult>,
1925
- }
1926
-
1927
- impl DeterministicDecisionRuntime {
1928
- pub fn new(
1929
- metadata: DecisionRuntimeMetadata,
1930
- responses: BTreeMap<String, DecisionResult>,
1931
- ) -> Self {
1932
- Self {
1933
- metadata,
1934
- responses,
1935
- }
1936
- }
1937
- }
1938
-
1939
- impl DecisionRuntime for DeterministicDecisionRuntime {
1940
- fn metadata(&self) -> DecisionRuntimeMetadata {
1941
- self.metadata.clone()
1942
- }
1943
-
1944
- fn decide(&self, request: &DecisionRequest) -> Result<DecisionResult, DecisionError> {
1945
- let key = serde_json::to_string(request)
1946
- .map_err(|error| DecisionError::new("DECISION_INVALID_RESULT", error.to_string()))?;
1947
- self.responses.get(&key).cloned().ok_or_else(|| {
1948
- DecisionError::new(
1949
- "DECISION_INVALID_RESULT",
1950
- "deterministic runtime has no configured response for this request",
1951
- )
1952
- })
1953
- }
1954
- }
1955
-
1956
- pub fn deterministic_request_key(request: &DecisionRequest) -> Result<String, DecisionError> {
1957
- serde_json::to_string(request)
1958
- .map_err(|error| DecisionError::new("DECISION_INVALID_RESULT", error.to_string()))
1959
- }
1960
-
1961
- pub fn run_decision_benchmark(
1962
- db: &FeltDb,
1963
- runtime: &dyn DecisionRuntime,
1964
- options: &DecisionOptions,
1965
- ) -> Result<DecisionBenchmarkReport, DecisionError> {
1966
- let collection = db.capability("benchmark_people");
1967
- for index in 0..20 {
1968
- collection
1969
- .insert_with_key(
1970
- &format!("b-{index}"),
1971
- json!({
1972
- "id": format!("b-{index}"),
1973
- "__version": 1,
1974
- "employmentType": if index % 2 == 0 { "remote" } else { "office" },
1975
- "publicRole": if index % 3 == 0 { "Staff Backend Engineer" } else { "Lab Technician" },
1976
- "urgency": if index % 3 == 0 { "high" } else if index % 3 == 1 { "medium" } else { "low" },
1977
- "notes": if index % 2 == 0 { "Remote-first and distributed systems ownership." } else { "On-site inventory and lab work." }
1978
- }),
1979
- )
1980
- .map_err(|error| DecisionError::new("DECISION_STORAGE_ERROR", error.to_string()))?;
1981
- }
1982
- let mut first = 0usize;
1983
- let mut second_cache_hits = 0usize;
1984
- let mut second_runtime = 0usize;
1985
- for index in 0..20 {
1986
- let target = DecisionRecordRef {
1987
- collection: "benchmark_people".into(),
1988
- record_id: format!("b-{index}"),
1989
- };
1990
- for definition in [
1991
- DecisionDefinition::Binary {
1992
- predicate: "could work from home".into(),
1993
- },
1994
- DecisionDefinition::Choice {
1995
- question: "Which reviewer should handle this?".into(),
1996
- options: vec!["approve".into(), "reject".into(), "review".into()],
1997
- },
1998
- DecisionDefinition::Score {
1999
- question: "What is the urgency?".into(),
2000
- levels: vec!["low".into(), "medium".into(), "high".into()],
2001
- },
2002
- ] {
2003
- let first_eval = decide_record(db, &target, &definition, runtime, options, None)?;
2004
- if !first_eval.cached {
2005
- first += 1;
2006
- }
2007
- let second_eval = decide_record(db, &target, &definition, runtime, options, None)?;
2008
- if second_eval.cached {
2009
- second_cache_hits += 1;
2010
- } else {
2011
- second_runtime += 1;
2012
- }
2013
- }
2014
- }
2015
- Ok(DecisionBenchmarkReport {
2016
- binary: 20,
2017
- choice: 20,
2018
- score: 20,
2019
- runtime_invocations_first_pass: first,
2020
- runtime_invocations_second_pass: second_runtime,
2021
- cache_hits_second_pass: second_cache_hits,
2022
- })
2023
- }
2024
-
2025
- impl From<DecisionError> for FlowError {
2026
- fn from(error: DecisionError) -> Self {
2027
- FlowError::CapabilityError(format!("{}: {}", error.code, error.message))
2028
- }
2029
- }