@feltdb/core 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) 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 +3 -0
  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 +255 -12
  17. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  18. package/dist/db.d.ts +11 -1
  19. package/dist/db.d.ts.map +1 -1
  20. package/dist/db.js +15 -2
  21. package/dist/error-codes.d.ts +5 -0
  22. package/dist/error-codes.d.ts.map +1 -1
  23. package/dist/error-codes.js +9 -1
  24. package/dist/http-db.d.ts +16 -0
  25. package/dist/http-db.d.ts.map +1 -1
  26. package/dist/http-db.js +36 -6
  27. package/dist/managed-recovery.d.ts +6 -0
  28. package/dist/managed-recovery.d.ts.map +1 -1
  29. package/dist/managed-recovery.js +9 -0
  30. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-DtybUY0q.js} +1 -1
  31. package/dist/studio-app/assets/feltdb_wasm_bg-DsgIUwaB.wasm +0 -0
  32. package/dist/studio-app/assets/index-C4OffsZl.js +37 -0
  33. package/dist/studio-app/index.html +1 -1
  34. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  35. package/dist/wasm/package.json +1 -1
  36. package/dist/workload.d.ts +55 -3
  37. package/dist/workload.d.ts.map +1 -1
  38. package/dist/workload.js +23 -0
  39. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  40. package/dist/workspace/workspace-connection.js +14 -3
  41. package/package.json +1 -1
  42. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  43. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  44. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  45. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  46. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  47. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  49. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  51. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  61. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  63. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  64. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  65. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  66. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  67. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  68. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  69. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  70. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  71. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  72. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  73. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  74. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  75. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  76. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  77. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  80. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  83. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  84. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  86. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  87. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  89. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  90. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  91. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  92. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  93. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  94. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  95. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  96. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  97. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  98. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  99. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  100. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  101. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  102. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  103. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  105. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  106. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  107. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  108. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  109. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  110. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  111. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  112. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  113. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  114. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  115. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  116. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  117. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  118. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  119. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  120. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  121. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  122. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  123. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  124. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  125. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  126. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  127. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  128. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  129. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  130. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  131. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  132. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  133. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  134. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  135. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  136. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  137. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  138. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  139. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  140. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  141. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  142. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  143. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  144. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  145. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  146. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  147. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  148. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  149. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  150. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  151. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  152. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  153. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  154. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  155. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  156. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  157. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  158. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  159. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  160. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  161. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  162. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  163. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  164. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  165. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  166. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  167. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  168. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  169. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  170. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  171. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  172. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  173. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  174. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  175. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  176. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  177. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  178. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  179. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  180. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  181. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  182. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  183. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  184. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  185. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  186. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  187. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  188. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  189. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  190. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  191. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  192. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  193. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  194. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  195. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  196. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  197. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  198. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  199. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  200. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  204. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  206. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  207. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  208. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  209. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  210. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  211. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  212. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  213. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  220. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  221. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  222. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  223. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  225. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  226. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  227. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  228. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  229. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  230. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  231. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  232. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  235. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  236. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  237. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  238. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  239. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  240. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  241. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  242. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  243. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  244. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  245. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  246. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  247. 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
- }