@feltdb/core 0.8.3 → 0.8.5

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 (193) hide show
  1. package/dist/cli/commands.js +4 -1
  2. package/dist/cli/provisioning-neutrality.js +79 -0
  3. package/dist/collection.d.ts +43 -1
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +192 -22
  6. package/dist/create/create.js +25 -21
  7. package/dist/create/managed-account.js +11 -0
  8. package/dist/create/package-versions.js +1 -1
  9. package/dist/create/server-source/Cargo.lock +165 -0
  10. package/dist/create/server-source/Cargo.toml +9 -0
  11. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  28. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  29. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  30. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  31. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  32. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  33. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  34. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  35. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  36. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  37. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  38. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  39. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  40. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  41. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  42. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  43. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  44. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  45. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  46. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  47. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  48. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  49. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  50. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  51. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +595 -0
  52. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  53. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  54. package/dist/create/server-source/crates/feltdb/src/lib.rs +1050 -117
  55. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  56. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  57. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  58. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  59. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  60. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  61. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  62. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  63. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  64. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  65. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  66. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  67. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  68. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  69. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  70. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +90 -72
  71. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  72. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  73. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  74. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  75. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  76. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  77. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  78. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  79. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  80. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  81. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  82. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  83. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  84. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  85. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  86. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +126 -0
  87. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  88. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  89. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  90. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  91. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  92. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  93. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  94. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  95. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +305 -6
  96. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  97. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  98. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  99. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
  100. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  101. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  102. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  103. package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
  104. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  105. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  106. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  107. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  108. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  109. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  110. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  111. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  112. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +442 -0
  113. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  114. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
  115. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  116. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  117. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  118. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  119. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  120. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +234 -0
  121. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +947 -0
  122. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  123. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  124. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  125. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  126. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  127. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  128. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  129. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  130. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  131. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
  132. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  133. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  134. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  135. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  136. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  137. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  138. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  139. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  140. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  141. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  142. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  143. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  144. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  145. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  146. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  147. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
  149. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
  150. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  151. package/dist/create/server-source/crates/feltdb-server/src/main.rs +800 -34
  152. package/dist/db.d.ts +33 -34
  153. package/dist/db.d.ts.map +1 -1
  154. package/dist/db.js +74 -20
  155. package/dist/deployment.d.ts +30 -0
  156. package/dist/deployment.d.ts.map +1 -0
  157. package/dist/deployment.js +130 -0
  158. package/dist/embedded-transaction.d.ts +22 -4
  159. package/dist/embedded-transaction.d.ts.map +1 -1
  160. package/dist/embedded-transaction.js +51 -5
  161. package/dist/feltdb.d.ts +14 -2
  162. package/dist/feltdb.d.ts.map +1 -1
  163. package/dist/file-db.js +1 -1
  164. package/dist/http-client.d.ts +14 -0
  165. package/dist/http-client.d.ts.map +1 -1
  166. package/dist/http-client.js +23 -5
  167. package/dist/http-db.d.ts +119 -1
  168. package/dist/http-db.d.ts.map +1 -1
  169. package/dist/http-db.js +346 -31
  170. package/dist/index-core.d.ts +2 -0
  171. package/dist/index-core.d.ts.map +1 -1
  172. package/dist/index-core.js +2 -0
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +9 -0
  175. package/dist/indexeddb-db.d.ts.map +1 -1
  176. package/dist/indexeddb-db.js +35 -21
  177. package/dist/managed-recovery.d.ts +192 -0
  178. package/dist/managed-recovery.d.ts.map +1 -0
  179. package/dist/managed-recovery.js +242 -0
  180. package/dist/memory-db.js +1 -1
  181. package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.js → feltdb_wasm-DaNwCLRX.js} +1 -1
  182. package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
  183. package/dist/studio-app/assets/index-j8IlhNqJ.js +29 -0
  184. package/dist/studio-app/index.html +1 -1
  185. package/dist/transaction.d.ts +30 -0
  186. package/dist/transaction.d.ts.map +1 -1
  187. package/dist/transaction.js +41 -0
  188. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  189. package/dist/workload.d.ts +2 -1
  190. package/dist/workload.d.ts.map +1 -1
  191. package/package.json +1 -1
  192. package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
  193. package/dist/studio-app/assets/index-B0k4UAlI.js +0 -29
@@ -25,7 +25,8 @@ use crate::{
25
25
  policy_evaluation::{
26
26
  Actor, AuthorizationState, PolicyEvaluator, PolicySubject, RecordAuthorizationContext,
27
27
  },
28
- AtomicMutation, AtomicPrecondition, FeltDb, StoredRow,
28
+ AtomicMutation, AtomicPrecondition, FeltDb, FlowError, PreconditionFailure, RecordPrecondition,
29
+ StoredRow,
29
30
  };
30
31
  use serde::{Deserialize, Serialize};
31
32
  use serde_json::{Map, Value};
@@ -853,6 +854,12 @@ pub struct StateFailure {
853
854
  pub transaction: Option<String>,
854
855
  #[serde(default)]
855
856
  pub causal_position: Option<u64>,
857
+ /// The structured predicate failure, when the refusal was a record
858
+ /// precondition. Named `failure` because that is what the direct
859
+ /// authority's refused transaction already calls it: one refusal, one
860
+ /// shape, whichever endpoint answered.
861
+ #[serde(default, skip_serializing_if = "Option::is_none")]
862
+ pub failure: Option<Value>,
856
863
  }
857
864
  impl StateFailure {
858
865
  fn new(code: &str, message: &str) -> Self {
@@ -864,6 +871,7 @@ impl StateFailure {
864
871
  resource: None,
865
872
  transaction: None,
866
873
  causal_position: None,
874
+ failure: None,
867
875
  }
868
876
  }
869
877
  fn storage(error: impl std::fmt::Display) -> Self {
@@ -1611,10 +1619,47 @@ pub struct TransactionOperation {
1611
1619
  #[derive(Debug, Clone, Serialize, Deserialize)]
1612
1620
  #[serde(tag = "kind", rename_all = "snake_case")]
1613
1621
  pub enum TransactionPrecondition {
1614
- CollectionVersion { collection: String, version: u64 },
1615
- StateVersion { version: u64 },
1616
- ApplicationRevision { revision_id: String },
1617
- CausalCursor { cursor: BTreeMap<String, u64> },
1622
+ CollectionVersion {
1623
+ collection: String,
1624
+ version: u64,
1625
+ },
1626
+ StateVersion {
1627
+ version: u64,
1628
+ },
1629
+ ApplicationRevision {
1630
+ revision_id: String,
1631
+ },
1632
+ CausalCursor {
1633
+ cursor: BTreeMap<String, u64>,
1634
+ },
1635
+ /// What must be true of one record for the transaction to commit.
1636
+ ///
1637
+ /// Distinct from every variant above, and evaluated in a different place
1638
+ /// for a reason: those compare counters that can be read before the commit
1639
+ /// lock is taken, while this one compares the fields a caller reads back —
1640
+ /// the document's `__version`, its authority epoch, its lease — and must
1641
+ /// therefore be evaluated *inside* the same lock as the writes. Checking it
1642
+ /// against the pre-commit snapshot would be a client-side check wearing the
1643
+ /// authority's name: between the read and the write, another writer
1644
+ /// commits.
1645
+ ///
1646
+ /// It is also what makes `putIfAbsent` and `updateIfVersion` mean the same
1647
+ /// thing against a managed authority as against the file runtime. Mapping
1648
+ /// a caller's `expectedVersion` onto `if_version` would not: that fences
1649
+ /// the row's internal storage sequence, which is a different number that
1650
+ /// happens to share a name.
1651
+ Record {
1652
+ collection: String,
1653
+ id: String,
1654
+ #[serde(default)]
1655
+ require_absent: bool,
1656
+ #[serde(default)]
1657
+ expected_version: Option<u64>,
1658
+ #[serde(default)]
1659
+ expected_epoch: Option<u64>,
1660
+ #[serde(default)]
1661
+ expected_lease_id: Option<String>,
1662
+ },
1618
1663
  }
1619
1664
  #[derive(Debug, Clone, Serialize, Deserialize)]
1620
1665
  pub struct TransactionRequest {
@@ -2056,8 +2101,47 @@ fn execute_transaction_with_policies(
2056
2101
  });
2057
2102
  }
2058
2103
  let snapshot = db.state_rows().map_err(StateFailure::storage)?;
2104
+ let namespace = request
2105
+ .state_namespace
2106
+ .as_deref()
2107
+ .unwrap_or(&request.application_id)
2108
+ .to_string();
2109
+ // Record preconditions are collected here and evaluated by the store,
2110
+ // inside the same lock as the writes. Evaluating them against `snapshot`
2111
+ // alongside the counters below would reintroduce exactly the gap they
2112
+ // exist to close.
2113
+ let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
2059
2114
  for condition in &request.preconditions {
2060
2115
  let (resource, expected, actual, passed) = match condition {
2116
+ TransactionPrecondition::Record {
2117
+ collection,
2118
+ id,
2119
+ require_absent,
2120
+ expected_version,
2121
+ expected_epoch,
2122
+ expected_lease_id,
2123
+ } => {
2124
+ let precondition = RecordPrecondition {
2125
+ capability: format!("{namespace}:{collection}"),
2126
+ key: id.clone(),
2127
+ require_absent: *require_absent,
2128
+ expected_version: *expected_version,
2129
+ expected_epoch: *expected_epoch,
2130
+ expected_lease_id: expected_lease_id.clone(),
2131
+ };
2132
+ if precondition.is_empty() {
2133
+ // Refused rather than ignored: a precondition that
2134
+ // constrains nothing reads as protection and provides none.
2135
+ let mut failure = StateFailure::new(
2136
+ "VALIDATION_FAILED",
2137
+ &format!("precondition on {collection}:{id} constrains nothing"),
2138
+ );
2139
+ failure.resource = Some(format!("{collection}:{id}"));
2140
+ return Err(failure);
2141
+ }
2142
+ record_preconditions.push(precondition);
2143
+ continue;
2144
+ }
2061
2145
  TransactionPrecondition::CollectionVersion {
2062
2146
  collection,
2063
2147
  version,
@@ -2350,15 +2434,37 @@ fn execute_transaction_with_policies(
2350
2434
  }
2351
2435
  }
2352
2436
  let commit = db
2353
- .apply_atomic_transaction_content_addressed(
2437
+ .apply_atomic_transaction_guarded(
2354
2438
  &storage_transaction_id,
2355
2439
  Some(&payload_hash),
2356
2440
  request.causal_parent,
2357
2441
  &preconditions,
2442
+ &record_preconditions,
2358
2443
  &mutations,
2359
2444
  Some(serde_json::json!({"transaction_id":transaction_id.clone(),"subject":request.authorization.subject.clone(),"tenant":request.tenant_id.clone(),"application":request.application_id.clone(),"revision":request.revision_id.clone(),"schema_version":request.schema_version,"operations":request.operations.len(),"authorization_decisions":["state:write allowed"],"causal_parent":request.causal_parent,"result":"committed"})),
2360
2445
  )
2361
2446
  .map_err(|error| {
2447
+ // A record precondition that did not hold is a lost race, and it
2448
+ // says which predicate failed and what the authority holds. That
2449
+ // detail is the difference between "re-read and decide again" and
2450
+ // "this deployment is broken", so it is carried through as
2451
+ // structure rather than flattened into the message.
2452
+ if let FlowError::PreconditionFailed(refusal) = &error {
2453
+ let mut failure =
2454
+ StateFailure::new("PRECONDITION_FAILED", &error.to_string());
2455
+ failure.transaction = Some(transaction_id.clone());
2456
+ failure.causal_position = db.sequence().ok();
2457
+ failure.resource = Some(refusal.key().to_string());
2458
+ failure.failure = serde_json::to_value(refusal.as_ref()).ok();
2459
+ if let PreconditionFailure::Version {
2460
+ expected, actual, ..
2461
+ } = refusal.as_ref()
2462
+ {
2463
+ failure.expected = Some(Value::from(*expected));
2464
+ failure.actual = Some(Value::from(*actual));
2465
+ }
2466
+ return failure;
2467
+ }
2362
2468
  let text = error.to_string();
2363
2469
  if text.contains("PRECONDITION_FAILED") {
2364
2470
  let mut failure = StateFailure::new("PRECONDITION_FAILED", &text);
@@ -2665,6 +2771,199 @@ mod tests {
2665
2771
  "CONFLICT"
2666
2772
  );
2667
2773
  }
2774
+ /// A conditional write through the canonical contract has to mean the same
2775
+ /// thing it means everywhere else in FeltDB, or `putIfAbsent` and
2776
+ /// `updateIfVersion` become file-runtime privileges.
2777
+ ///
2778
+ /// The predicate under test is the document's `__version` -- the number a
2779
+ /// caller reads back -- not the row's internal storage sequence. Those are
2780
+ /// two different numbers that happen to share a name, and a fence on the
2781
+ /// wrong one is worse than no fence, because it reads as protection.
2782
+ #[test]
2783
+ fn a_record_precondition_fences_the_document_version() {
2784
+ let db = db("record-precondition");
2785
+ let schema = schema();
2786
+ let write = |id: &str,
2787
+ transaction: &str,
2788
+ title: &str,
2789
+ preconditions: Vec<TransactionPrecondition>,
2790
+ kind: TransactionOperationKind| {
2791
+ execute_transaction(
2792
+ &db,
2793
+ &schema,
2794
+ &TransactionRequest {
2795
+ transaction_id: Some(transaction.into()),
2796
+ tenant_id: "tenant".into(),
2797
+ application_id: "app".into(),
2798
+ revision_id: "rev".into(),
2799
+ schema_version: 1,
2800
+ state_namespace: None,
2801
+ causal_parent: None,
2802
+ authorization: auth(),
2803
+ preconditions,
2804
+ operations: vec![TransactionOperation {
2805
+ kind,
2806
+ collection: "incidents".into(),
2807
+ id: id.into(),
2808
+ value: serde_json::json!({ "title": title }),
2809
+ if_version: None,
2810
+ }],
2811
+ },
2812
+ None,
2813
+ )
2814
+ };
2815
+ let fence = |version: Option<u64>, absent: bool| {
2816
+ vec![TransactionPrecondition::Record {
2817
+ collection: "incidents".into(),
2818
+ id: "i1".into(),
2819
+ require_absent: absent,
2820
+ expected_version: version,
2821
+ expected_epoch: None,
2822
+ expected_lease_id: None,
2823
+ }]
2824
+ };
2825
+ let version = || {
2826
+ db.state_rows()
2827
+ .unwrap()
2828
+ .into_iter()
2829
+ .find(|row| row.capability == "app:incidents" && row.key == "i1" && !row.deleted)
2830
+ .and_then(|row| row.value.get("__version").and_then(Value::as_u64))
2831
+ };
2832
+
2833
+ // A conditional create is a create at version 1.
2834
+ write(
2835
+ "i1",
2836
+ "create",
2837
+ "First",
2838
+ fence(None, true),
2839
+ TransactionOperationKind::Insert,
2840
+ )
2841
+ .expect("the record is absent, so the create commits");
2842
+ assert_eq!(version(), Some(1));
2843
+
2844
+ // The same create again is a lost race. Whichever check catches it,
2845
+ // the answer is a conflict and nothing was written.
2846
+ let refused = write(
2847
+ "i1",
2848
+ "create-again",
2849
+ "Second",
2850
+ fence(None, true),
2851
+ TransactionOperationKind::Insert,
2852
+ )
2853
+ .expect_err("the record now exists");
2854
+ assert!(
2855
+ refused.code == "CONFLICT" || refused.code == "PRECONDITION_FAILED",
2856
+ "a lost creation race is a conflict, got {}",
2857
+ refused.code,
2858
+ );
2859
+ assert_eq!(version(), Some(1), "a refused transaction writes nothing");
2860
+
2861
+ // A guard on a record the transaction does not write is evaluated
2862
+ // inside the commit lock, and says which predicate failed.
2863
+ let guarded = write(
2864
+ "i2",
2865
+ "guarded",
2866
+ "Other",
2867
+ fence(None, true),
2868
+ TransactionOperationKind::Insert,
2869
+ )
2870
+ .expect_err("i1 exists, so the guard fails and i2 is never written");
2871
+ assert_eq!(guarded.code, "PRECONDITION_FAILED");
2872
+ assert_eq!(
2873
+ guarded
2874
+ .failure
2875
+ .as_ref()
2876
+ .and_then(|value| value.get("predicate").and_then(Value::as_str)),
2877
+ Some("present"),
2878
+ );
2879
+ assert!(
2880
+ db.state_rows()
2881
+ .unwrap()
2882
+ .iter()
2883
+ .all(|row| row.key != "i2" || row.deleted),
2884
+ "a refused guard leaves the staged write unwritten",
2885
+ );
2886
+
2887
+ // A stale fence conflicts and names what the authority actually holds.
2888
+ let stale = write(
2889
+ "i1",
2890
+ "stale",
2891
+ "Third",
2892
+ fence(Some(7), false),
2893
+ TransactionOperationKind::Update,
2894
+ )
2895
+ .expect_err("version 7 is not the version on record");
2896
+ assert_eq!(stale.code, "PRECONDITION_FAILED");
2897
+ assert_eq!(stale.expected, Some(Value::from(7)));
2898
+ assert_eq!(stale.actual, Some(Value::from(1)));
2899
+ assert_eq!(version(), Some(1));
2900
+
2901
+ // The fenced write commits and the authority owns the next version.
2902
+ write(
2903
+ "i1",
2904
+ "fenced",
2905
+ "Fourth",
2906
+ fence(Some(1), false),
2907
+ TransactionOperationKind::Update,
2908
+ )
2909
+ .expect("version 1 is the version on record");
2910
+ assert_eq!(version(), Some(2));
2911
+
2912
+ // Replaying a committed transaction id applies nothing, so it cannot
2913
+ // advance the version a second time.
2914
+ write(
2915
+ "i1",
2916
+ "fenced",
2917
+ "Fourth",
2918
+ fence(Some(1), false),
2919
+ TransactionOperationKind::Update,
2920
+ )
2921
+ .expect("a duplicate transaction id is not a failure");
2922
+ assert_eq!(version(), Some(2));
2923
+ }
2924
+
2925
+ /// A precondition that constrains nothing is refused rather than accepted,
2926
+ /// because one that is silently ignored reads as protection and provides
2927
+ /// none.
2928
+ #[test]
2929
+ fn an_empty_record_precondition_is_refused() {
2930
+ let db = db("empty-precondition");
2931
+ let schema = schema();
2932
+ let failure = execute_transaction(
2933
+ &db,
2934
+ &schema,
2935
+ &TransactionRequest {
2936
+ transaction_id: Some("empty".into()),
2937
+ tenant_id: "tenant".into(),
2938
+ application_id: "app".into(),
2939
+ revision_id: "rev".into(),
2940
+ schema_version: 1,
2941
+ state_namespace: None,
2942
+ causal_parent: None,
2943
+ authorization: auth(),
2944
+ preconditions: vec![TransactionPrecondition::Record {
2945
+ collection: "incidents".into(),
2946
+ id: "i1".into(),
2947
+ require_absent: false,
2948
+ expected_version: None,
2949
+ expected_epoch: None,
2950
+ expected_lease_id: None,
2951
+ }],
2952
+ operations: vec![TransactionOperation {
2953
+ kind: TransactionOperationKind::Insert,
2954
+ collection: "incidents".into(),
2955
+ id: "i1".into(),
2956
+ value: serde_json::json!({ "title": "First" }),
2957
+ if_version: None,
2958
+ }],
2959
+ },
2960
+ None,
2961
+ )
2962
+ .expect_err("a precondition that constrains nothing is a caller error");
2963
+ assert_eq!(failure.code, "VALIDATION_FAILED");
2964
+ assert!(db.state_rows().unwrap().iter().all(|row| row.deleted));
2965
+ }
2966
+
2668
2967
  #[test]
2669
2968
  fn validation_failure_rolls_back_every_operation() {
2670
2969
  let db = db("rollback");
@@ -0,0 +1,222 @@
1
+ //! The deterministic contract behind FeltDB's semantic state diff.
2
+ //!
3
+ //! This is the second specimen of the pattern established by
4
+ //! [`crate::trigger_contract`]: a decision FeltDB already makes, drawn behind a
5
+ //! runtime-independent boundary so that more than one implementation can be
6
+ //! held to it.
7
+ //!
8
+ //! ```text
9
+ //! canonical state A + canonical state B
10
+ //! │
11
+ //! ┌──────────────┴──────────────┐
12
+ //! │ │
13
+ //! native evaluation WASM evaluation
14
+ //! │ │
15
+ //! └──────────────┬──────────────┘
16
+ //! │
17
+ //! canonical diff bytes
18
+ //! ```
19
+ //!
20
+ //! # This module adds no diff semantics
21
+ //!
22
+ //! [`SemanticDiff::compute`] is untouched and remains authoritative. Everything
23
+ //! here is envelope: identity, version, canonical encoding, and the decision to
24
+ //! report an unusable input rather than fail. If this module and
25
+ //! `SemanticDiff::compute` ever disagree, `SemanticDiff::compute` is right.
26
+ //!
27
+ //! # Determinism budget
28
+ //!
29
+ //! Evaluation reads nothing but its two states. No clock, filesystem, network,
30
+ //! random source, process identity, authentication context, authority metadata
31
+ //! or process-global mutable state participates, and the audit that produced
32
+ //! this contract found none in the implementation either.
33
+ //!
34
+ //! # What the audit established about the semantics
35
+ //!
36
+ //! These are properties of the existing implementation, recorded here because
37
+ //! the contract is defined against them rather than against an idea of what a
38
+ //! diff should do:
39
+ //!
40
+ //! - **Arrays are positional.** A reorder is not detected; `[1,2] -> [2,1]`
41
+ //! reports `Changed` at index 0 and at index 1. Elements past the shorter
42
+ //! length become `Added` or `Removed` at their index.
43
+ //! - **Absence and null are different, in both directions.** `missing -> value`
44
+ //! is `Added`, `null -> value` is `Changed`, `value -> missing` is `Removed`,
45
+ //! `value -> null` is `Changed`.
46
+ //! - **`old_value` and `new_value` are `Option`, encoded as `null` when absent.**
47
+ //! That is ambiguous with a JSON null on its own, and `kind` is what
48
+ //! disambiguates: `missing -> null` is `Added` with `new_value: null`.
49
+ //! - **Numbers compare by `serde_json` variant.** `42` and `42.0` differ, so
50
+ //! they report `Changed`; `-0` is a float and so differs from the integer
51
+ //! `0`; `-0.0` and `0.0` are equal under IEEE equality and report nothing.
52
+ //! - **A type transition is one `Changed`,** carrying both whole values, at the
53
+ //! path where it happens — including at the empty path, for the states
54
+ //! themselves.
55
+ //! - **Ordering is total.** Changes sort by path, then by kind. Paths are
56
+ //! unique within one diff, so the ordering does not depend on sort stability.
57
+
58
+ use crate::state_model::SemanticDiff;
59
+ use serde::{Deserialize, Serialize};
60
+ use serde_json::Value;
61
+ use sha2::{Digest, Sha256};
62
+
63
+ /// Stable identity of this contract.
64
+ pub const STATE_DIFF_CONTRACT_ID: &str = "feltdb.state.diff";
65
+
66
+ /// Version of this contract's input shape, output shape and semantics.
67
+ ///
68
+ /// The output shape is [`SemanticDiff`]'s own serialization, so a change to
69
+ /// that type is a change to this contract and must bump this.
70
+ pub const STATE_DIFF_CONTRACT_VERSION: u32 = 1;
71
+
72
+ /// The complete deterministic input: two states, and the contract's identity.
73
+ ///
74
+ /// Members are declared alphabetically so the canonical struct encoding matches
75
+ /// the sorted-key encoding `serde_json` gives every JSON object.
76
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77
+ #[serde(deny_unknown_fields)]
78
+ pub struct StateDiffInput {
79
+ pub contract: String,
80
+ pub state_a: Value,
81
+ pub state_b: Value,
82
+ pub version: u32,
83
+ }
84
+
85
+ impl StateDiffInput {
86
+ /// Builds an input stamped with this contract's identity and version.
87
+ pub fn new(state_a: Value, state_b: Value) -> Self {
88
+ Self {
89
+ contract: STATE_DIFF_CONTRACT_ID.to_string(),
90
+ state_a,
91
+ state_b,
92
+ version: STATE_DIFF_CONTRACT_VERSION,
93
+ }
94
+ }
95
+ }
96
+
97
+ /// Why an input could not be evaluated.
98
+ ///
99
+ /// Three codes, matching the vocabulary [`crate::trigger_contract`] uses. As
100
+ /// there, an error carries a code and nothing else: a free-form message would
101
+ /// be a divergence surface between implementations that says nothing about the
102
+ /// semantics under test.
103
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
105
+ pub enum ContractErrorCode {
106
+ /// The bytes are not a well-formed instance of this contract's input.
107
+ ContractInputInvalid,
108
+ /// The input declares a different contract.
109
+ ContractIdMismatch,
110
+ /// The input declares a version this implementation does not implement.
111
+ ContractVersionUnsupported,
112
+ }
113
+
114
+ /// The error body of a contract output.
115
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116
+ #[serde(deny_unknown_fields)]
117
+ pub struct ContractErrorBody {
118
+ pub code: ContractErrorCode,
119
+ }
120
+
121
+ /// The complete deterministic output.
122
+ ///
123
+ /// Exactly one of `diff` and `error` is present. `diff` is [`SemanticDiff`]
124
+ /// serialized exactly as it already serializes — this contract does not
125
+ /// reshape it, because the existing semantic result *is* the contract.
126
+ #[derive(Debug, Clone, Serialize, Deserialize)]
127
+ #[serde(deny_unknown_fields)]
128
+ pub struct StateDiffOutput {
129
+ pub contract: String,
130
+ #[serde(default, skip_serializing_if = "Option::is_none")]
131
+ pub diff: Option<SemanticDiff>,
132
+ #[serde(default, skip_serializing_if = "Option::is_none")]
133
+ pub error: Option<ContractErrorBody>,
134
+ pub version: u32,
135
+ }
136
+
137
+ impl StateDiffOutput {
138
+ fn computed(diff: SemanticDiff) -> Self {
139
+ Self {
140
+ contract: STATE_DIFF_CONTRACT_ID.to_string(),
141
+ diff: Some(diff),
142
+ error: None,
143
+ version: STATE_DIFF_CONTRACT_VERSION,
144
+ }
145
+ }
146
+
147
+ fn failure(code: ContractErrorCode) -> Self {
148
+ Self {
149
+ contract: STATE_DIFF_CONTRACT_ID.to_string(),
150
+ diff: None,
151
+ error: Some(ContractErrorBody { code }),
152
+ version: STATE_DIFF_CONTRACT_VERSION,
153
+ }
154
+ }
155
+ }
156
+
157
+ /// The runtime that executed a contract, recorded in evidence.
158
+ ///
159
+ /// Only [`ContractRuntime::Native`] is wired anywhere. As with the trigger
160
+ /// contract, the WASM variant exists so a recorded runtime can be named
161
+ /// honestly; it is not a selection switch.
162
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163
+ #[serde(rename_all = "snake_case")]
164
+ pub enum ContractRuntime {
165
+ Native,
166
+ Wasm,
167
+ }
168
+
169
+ impl ContractRuntime {
170
+ pub fn as_str(self) -> &'static str {
171
+ match self {
172
+ Self::Native => "native",
173
+ Self::Wasm => "wasm",
174
+ }
175
+ }
176
+ }
177
+
178
+ /// The deterministic contract, natively implemented.
179
+ ///
180
+ /// A thin adapter over the existing implementation, deliberately holding no
181
+ /// diff logic of its own.
182
+ pub fn diff_states(state_a: &Value, state_b: &Value) -> SemanticDiff {
183
+ SemanticDiff::compute(state_a, state_b)
184
+ }
185
+
186
+ /// The canonical byte encoding of a contract input.
187
+ pub fn canonical_input_bytes(input: &StateDiffInput) -> Vec<u8> {
188
+ serde_json::to_vec(input).expect("contract input is always serializable")
189
+ }
190
+
191
+ /// The canonical byte encoding of a contract output.
192
+ pub fn canonical_output_bytes(output: &StateDiffOutput) -> Vec<u8> {
193
+ serde_json::to_vec(output).expect("contract output is always serializable")
194
+ }
195
+
196
+ /// Content identity of a contract input, for provenance.
197
+ pub fn input_digest(input: &StateDiffInput) -> String {
198
+ format!("sha256:{:x}", Sha256::digest(canonical_input_bytes(input)))
199
+ }
200
+
201
+ /// The runtime-independent entry point: canonical bytes in, canonical bytes out.
202
+ ///
203
+ /// This is the exact shape a foreign runtime implements. It never panics and
204
+ /// never returns anything but a canonical output encoding, so an unusable input
205
+ /// is a contract decision rather than a host-specific failure mode.
206
+ pub fn evaluate_canonical(input: &[u8]) -> Vec<u8> {
207
+ canonical_output_bytes(&evaluate_canonical_output(input))
208
+ }
209
+
210
+ /// [`evaluate_canonical`] before encoding, for callers that want the value.
211
+ pub fn evaluate_canonical_output(input: &[u8]) -> StateDiffOutput {
212
+ let Ok(parsed) = serde_json::from_slice::<StateDiffInput>(input) else {
213
+ return StateDiffOutput::failure(ContractErrorCode::ContractInputInvalid);
214
+ };
215
+ if parsed.contract != STATE_DIFF_CONTRACT_ID {
216
+ return StateDiffOutput::failure(ContractErrorCode::ContractIdMismatch);
217
+ }
218
+ if parsed.version != STATE_DIFF_CONTRACT_VERSION {
219
+ return StateDiffOutput::failure(ContractErrorCode::ContractVersionUnsupported);
220
+ }
221
+ StateDiffOutput::computed(diff_states(&parsed.state_a, &parsed.state_b))
222
+ }