@feltdb/core 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/agent-state.d.ts +20 -0
  2. package/dist/agent-state.d.ts.map +1 -1
  3. package/dist/agent-state.js +36 -0
  4. package/dist/build-identity.json +12 -4
  5. package/dist/cli/commands.js +187 -4
  6. package/dist/cli/publish-engine.js +116 -16
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +20 -2
  9. package/dist/create/build-identity.json +12 -4
  10. package/dist/create/managed-account.js +93 -35
  11. package/dist/create/package-versions.js +1 -1
  12. package/dist/create/server-source/Cargo.lock +6 -509
  13. package/dist/create/server-source/Cargo.toml +2 -13
  14. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  15. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  16. package/dist/create/server-source/crates/feltdb-server/src/main.rs +356 -24
  17. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +10 -1
  18. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  19. package/dist/db.d.ts +11 -1
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +15 -2
  22. package/dist/error-codes.d.ts +5 -0
  23. package/dist/error-codes.d.ts.map +1 -1
  24. package/dist/error-codes.js +9 -1
  25. package/dist/http-db.d.ts +16 -0
  26. package/dist/http-db.d.ts.map +1 -1
  27. package/dist/http-db.js +36 -6
  28. package/dist/managed-recovery.d.ts +6 -0
  29. package/dist/managed-recovery.d.ts.map +1 -1
  30. package/dist/managed-recovery.js +9 -0
  31. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-BUIxCuTR.js} +1 -1
  32. package/dist/studio-app/assets/feltdb_wasm_bg-C3D6y6DK.wasm +0 -0
  33. package/dist/studio-app/assets/index-BhXYAhgS.js +37 -0
  34. package/dist/studio-app/index.html +1 -1
  35. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  36. package/dist/wasm/package.json +1 -1
  37. package/dist/workload.d.ts +55 -3
  38. package/dist/workload.d.ts.map +1 -1
  39. package/dist/workload.js +23 -0
  40. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  41. package/dist/workspace/workspace-connection.js +14 -3
  42. package/package.json +1 -1
  43. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  44. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  45. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  46. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  47. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  49. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  51. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  61. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  63. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  64. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  65. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  66. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  67. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  68. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  69. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  70. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  71. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  72. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  73. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  74. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  75. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  76. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  77. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  80. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  83. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  84. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  86. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  87. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  89. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  90. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  91. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  92. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  93. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  94. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  95. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  96. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  97. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  98. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  99. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  100. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  101. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  102. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  103. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  105. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  106. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  107. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  108. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  109. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  110. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  111. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  112. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  113. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  114. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  115. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  116. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  117. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  118. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  119. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  120. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  121. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  122. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  123. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  124. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  125. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  126. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  127. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  128. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  129. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  130. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  131. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  132. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  133. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  134. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  135. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  136. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  137. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  138. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  139. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  140. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  141. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  142. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  143. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  144. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  145. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  146. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  147. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  148. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  149. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  150. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  151. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  152. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  153. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  154. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  155. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  156. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  157. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  158. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  159. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  160. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  161. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  162. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  163. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  164. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  165. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  166. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  167. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  168. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  169. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  170. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  171. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  172. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  173. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  174. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  175. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  176. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  177. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  178. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  179. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  180. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  181. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  182. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  183. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  184. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  185. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  186. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  187. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  188. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  189. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  190. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  191. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  192. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  193. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  194. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  195. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  196. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  197. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  198. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  199. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  200. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  204. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  206. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  207. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  208. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  209. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  210. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  211. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  212. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  213. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  220. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  221. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  222. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  223. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  225. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  226. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  227. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  228. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  229. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  230. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  231. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  232. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  235. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  236. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  237. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  238. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  239. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  240. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  241. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  242. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  243. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  244. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  245. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  246. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  247. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  248. package/dist/studio-app/assets/index-1wd0uKua.js +0 -30
@@ -1,4158 +0,0 @@
1
- //! Proven FeltDB State Model
2
- //!
3
- //! This module contains the graduated state model from feltdbgit (PRs #6-#14).
4
- //! It is now integrated with FeltDB's canonical persistence layer.
5
- //!
6
- //! - Deterministic content-addressed StateId
7
- //! - Immutable StateRevision with ancestry
8
- //! - Durable StateHistory via FeltDB's operation log
9
- //! - State Transitions with atomic commit
10
- //! - Branching and divergence tracking
11
- //! - Causal Topology (ancestors, is_ancestor, common_ancestor)
12
- //! - Deterministic Semantic Diff (with Key/Index distinction)
13
- //! - Conflict Classification (Independent/Convergent/Conflict)
14
- //! - Explicit Reconciliation with parent_choice
15
-
16
- use serde::{Deserialize, Serialize};
17
- use serde_json::Value;
18
- use sha2::{Digest, Sha256};
19
- use std::collections::{BTreeMap, BTreeSet, HashMap};
20
- use std::sync::{Arc, Mutex};
21
-
22
- // Forward declaration to avoid circular imports
23
- pub struct FeltDb; // Will be linked at compile time from lib.rs
24
-
25
- /// Version constant for state model contracts
26
- pub const STATE_MODEL_VERSION: u32 = 1;
27
-
28
- // ============================================================================
29
- // StateId: Deterministic Content-Addressed State Identifier
30
- // ============================================================================
31
-
32
- /// Deterministic content-addressed state identifier
33
- /// Same state content → same StateId (representation-sensitive)
34
- /// Different content → different StateId
35
- #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
36
- pub struct StateId(String);
37
-
38
- impl StateId {
39
- /// Compute deterministic StateId from canonical state representation
40
- /// Preserves representation sensitivity: "1" ≠ 1 ≠ 1.0
41
- pub fn compute(canonical_json: &str) -> Self {
42
- let mut hasher = Sha256::new();
43
- hasher.update(canonical_json.as_bytes());
44
- let result = hasher.finalize();
45
- StateId(format!("{:x}", result))
46
- }
47
-
48
- /// Get hex representation
49
- pub fn as_hex(&self) -> &str {
50
- &self.0
51
- }
52
-
53
- /// Create from hex string (for recovery)
54
- pub fn from_hex(hex: String) -> Self {
55
- StateId(hex)
56
- }
57
- }
58
-
59
- impl std::fmt::Display for StateId {
60
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61
- write!(f, "{}", self.0)
62
- }
63
- }
64
-
65
- // ============================================================================
66
- // StateRevision: Immutable State Snapshot with Ancestry
67
- // ============================================================================
68
-
69
- /// An immutable, resource-scoped historical state.
70
- ///
71
- /// A revision is a revision **of a resource**. Two resources holding identical
72
- /// content are distinct revisions, and a resource that returns to a value it
73
- /// previously held records a *new* revision rather than resurrecting the old
74
- /// one.
75
- ///
76
- /// # Two identities, deliberately
77
- ///
78
- /// `id` is the **revision identity**: what distinguishes one historical
79
- /// occurrence from another. It is computed over the resource, the content, the
80
- /// parent and a per-resource sequence, so content equality is not identity.
81
- ///
82
- /// `content_id` is the **content identity**, the hash of the canonical content
83
- /// alone. It is what makes a revision verifiable against its own bytes, and it
84
- /// is what deduplicates *content* comparisons — two revisions of the same state
85
- /// share a `content_id` and differ in `id`.
86
- ///
87
- /// Conflating these two was the defect the identity contract found: a revert
88
- /// produced a revision whose identity already existed, so committing it
89
- /// overwrote the earlier one and closed a cycle in the parent chain.
90
- #[derive(Clone, Debug, Serialize, Deserialize)]
91
- pub struct StateRevision {
92
- /// Revision identity — resource, content, parent and sequence.
93
- pub id: StateId,
94
- /// The resource this is a revision of.
95
- pub resource: String,
96
- /// Canonical JSON representation (representation-sensitive)
97
- pub content: String,
98
- /// Content identity — the hash of `content` alone.
99
- pub content_id: StateId,
100
- /// Parent revision id. `None` only for a resource's first revision.
101
- pub parent_id: Option<StateId>,
102
- /// Position in this resource's history. Strictly increasing, never reused.
103
- pub sequence: u64,
104
- /// Authority that produced this revision
105
- pub authority: String,
106
- /// Timestamp when revision was created (informational only, not used for ordering)
107
- pub timestamp_ms: u64,
108
- /// Metadata (preservable with state)
109
- pub metadata: BTreeMap<String, Value>,
110
- }
111
-
112
- /// A content object addressed by its content identity.
113
- #[derive(Clone, Debug, Serialize, Deserialize)]
114
- pub struct StateObject {
115
- pub id: StateId,
116
- pub content: String,
117
- #[serde(default)]
118
- pub metadata: BTreeMap<String, Value>,
119
- }
120
-
121
- impl StateObject {
122
- pub fn new(content: String) -> Self {
123
- let id = StateId::compute(&content);
124
- Self {
125
- id,
126
- content,
127
- metadata: BTreeMap::new(),
128
- }
129
- }
130
-
131
- pub fn verify_integrity(&self) -> bool {
132
- self.id == StateId::compute(&self.content)
133
- }
134
- }
135
-
136
- /// A mutable named reference to an immutable revision.
137
- #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
138
- pub struct StateRef {
139
- pub namespace: String,
140
- pub name: String,
141
- pub target: StateId,
142
- pub version: u64,
143
- pub authority: String,
144
- pub updated_at_ms: u64,
145
- #[serde(default)]
146
- pub metadata: BTreeMap<String, Value>,
147
- }
148
-
149
- #[derive(Clone, Debug, PartialEq, Eq)]
150
- pub enum RefError {
151
- InvalidIdentifier,
152
- NotFound,
153
- AlreadyExists,
154
- UnknownTarget(StateId),
155
- VersionConflict { expected: u64, current: u64 },
156
- Storage(String),
157
- }
158
-
159
- impl std::fmt::Display for RefError {
160
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161
- match self {
162
- RefError::InvalidIdentifier => write!(f, "invalid ref identifier"),
163
- RefError::NotFound => write!(f, "ref not found"),
164
- RefError::AlreadyExists => write!(f, "ref already exists"),
165
- RefError::UnknownTarget(id) => write!(f, "ref target {id} does not exist"),
166
- RefError::VersionConflict { expected, current } => {
167
- write!(f, "stale ref version: expected {expected}, current {current}")
168
- }
169
- RefError::Storage(message) => write!(f, "{message}"),
170
- }
171
- }
172
- }
173
-
174
- impl std::error::Error for RefError {}
175
-
176
- #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
177
- pub struct MaterializationProjection {
178
- #[serde(default)]
179
- pub paths: Vec<Vec<PathComponent>>,
180
- }
181
-
182
- impl MaterializationProjection {
183
- pub fn full() -> Self {
184
- Self { paths: Vec::new() }
185
- }
186
-
187
- pub fn select(paths: Vec<Vec<PathComponent>>) -> Self {
188
- Self { paths }
189
- }
190
-
191
- pub fn is_full(&self) -> bool {
192
- self.paths.is_empty() || self.paths.iter().any(|path| path.is_empty())
193
- }
194
- }
195
-
196
- #[derive(Clone, Debug, PartialEq, Eq)]
197
- pub enum MaterializationError {
198
- RevisionNotFound(StateId),
199
- ContentUnparseable(String),
200
- MissingPath(Vec<PathComponent>),
201
- NonDeterministicProjection,
202
- }
203
-
204
- impl std::fmt::Display for MaterializationError {
205
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206
- match self {
207
- MaterializationError::RevisionNotFound(id) => write!(f, "revision {id} not found"),
208
- MaterializationError::ContentUnparseable(error) => {
209
- write!(f, "revision content is not parseable JSON: {error}")
210
- }
211
- MaterializationError::MissingPath(path) => {
212
- write!(f, "projection path does not resolve: {path:?}")
213
- }
214
- MaterializationError::NonDeterministicProjection => write!(
215
- f,
216
- "projection contains overlapping paths and does not deterministically resolve"
217
- ),
218
- }
219
- }
220
- }
221
-
222
- impl std::error::Error for MaterializationError {}
223
-
224
- #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
225
- pub struct GarbageCollectionReport {
226
- pub roots: Vec<StateRef>,
227
- pub reachable_revisions: Vec<StateId>,
228
- pub reachable_objects: Vec<StateId>,
229
- pub collected_revisions: Vec<StateId>,
230
- pub collected_objects: Vec<StateId>,
231
- }
232
-
233
- #[derive(Clone, Debug, PartialEq, Eq)]
234
- pub enum GarbageCollectionError {
235
- Storage(String),
236
- }
237
-
238
- impl std::fmt::Display for GarbageCollectionError {
239
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240
- match self {
241
- GarbageCollectionError::Storage(message) => write!(f, "{message}"),
242
- }
243
- }
244
- }
245
-
246
- impl std::error::Error for GarbageCollectionError {}
247
-
248
- /// Compute a revision identity.
249
- ///
250
- /// Every component participates: the same content is a different revision on a
251
- /// different resource, from a different parent, or at a different point in the
252
- /// resource's history. The parts are length-prefixed so no two distinct tuples
253
- /// can produce the same input string.
254
- fn compute_revision_id(
255
- resource: &str,
256
- content_id: &StateId,
257
- parent_id: Option<&StateId>,
258
- sequence: u64,
259
- ) -> StateId {
260
- let parent = parent_id.map(StateId::as_hex).unwrap_or("");
261
- let material = format!(
262
- "feltdb.revision.v2\n{}:{}\n{}:{}\n{}:{}\n{}",
263
- resource.len(),
264
- resource,
265
- content_id.as_hex().len(),
266
- content_id.as_hex(),
267
- parent.len(),
268
- parent,
269
- sequence,
270
- );
271
- StateId::compute(&material)
272
- }
273
-
274
- impl StateRevision {
275
- /// Create a resource's first revision.
276
- pub fn initial(resource: String, content: String, authority: String) -> Self {
277
- Self::at(resource, content, None, 0, authority)
278
- }
279
-
280
- /// Create a revision descending from `parent`, on the parent's resource.
281
- pub fn child(content: String, parent: &StateRevision, authority: String) -> Self {
282
- Self::at(
283
- parent.resource.clone(),
284
- content,
285
- Some(parent.id.clone()),
286
- parent.sequence + 1,
287
- authority,
288
- )
289
- }
290
-
291
- /// Create a revision at an explicit position in a resource's history.
292
- pub fn at(
293
- resource: String,
294
- content: String,
295
- parent_id: Option<StateId>,
296
- sequence: u64,
297
- authority: String,
298
- ) -> Self {
299
- let content_id = StateId::compute(&content);
300
- let id = compute_revision_id(&resource, &content_id, parent_id.as_ref(), sequence);
301
- let timestamp_ms = std::time::SystemTime::now()
302
- .duration_since(std::time::UNIX_EPOCH)
303
- .unwrap_or_default()
304
- .as_millis() as u64;
305
-
306
- StateRevision {
307
- id,
308
- resource,
309
- content,
310
- content_id,
311
- parent_id,
312
- sequence,
313
- authority,
314
- timestamp_ms,
315
- metadata: BTreeMap::new(),
316
- }
317
- }
318
-
319
- /// Verify that both identities match what they are computed from.
320
- pub fn verify_integrity(&self) -> bool {
321
- self.content_id == StateId::compute(&self.content)
322
- && self.id
323
- == compute_revision_id(
324
- &self.resource,
325
- &self.content_id,
326
- self.parent_id.as_ref(),
327
- self.sequence,
328
- )
329
- }
330
-
331
- /// Get parent id if this is not a resource's first revision.
332
- pub fn parent(&self) -> Option<&StateId> {
333
- self.parent_id.as_ref()
334
- }
335
-
336
- /// Whether two revisions are the same revision.
337
- ///
338
- /// Revision identity, not content equality: two revisions of the same state
339
- /// on the same resource are not equal.
340
- pub fn equals(&self, other: &StateRevision) -> bool {
341
- self.id == other.id
342
- }
343
-
344
- /// Whether two revisions hold the same state, regardless of where in
345
- /// history they occur.
346
- pub fn same_content_as(&self, other: &StateRevision) -> bool {
347
- self.content_id == other.content_id
348
- }
349
- }
350
-
351
- // ============================================================================
352
- // Topology: Ancestors, Relationships, and History Navigation
353
- // ============================================================================
354
-
355
- /// Relationship between two revisions
356
- #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
357
- pub enum Relationship {
358
- /// A is an ancestor of B
359
- Ancestor,
360
- /// B is an ancestor of A
361
- Descendant,
362
- /// A and B are the same
363
- Identity,
364
- /// A and B diverged from a common ancestor but neither is ancestor of other
365
- Diverged,
366
- /// A and B have no common ancestry
367
- Unrelated,
368
- }
369
-
370
- /// Topology tracker for state revisions
371
- pub struct StateTopology {
372
- revisions: HashMap<StateId, StateRevision>,
373
- parents: HashMap<StateId, StateId>,
374
- }
375
-
376
- impl StateTopology {
377
- pub fn new() -> Self {
378
- StateTopology {
379
- revisions: HashMap::new(),
380
- parents: HashMap::new(),
381
- }
382
- }
383
-
384
- /// Add revision to topology
385
- pub fn add_revision(&mut self, revision: StateRevision) {
386
- if let Some(parent_id) = revision.parent_id.clone() {
387
- self.parents.insert(revision.id.clone(), parent_id);
388
- }
389
- self.revisions.insert(revision.id.clone(), revision);
390
- }
391
-
392
- /// Get revision by id
393
- pub fn get(&self, id: &StateId) -> Option<&StateRevision> {
394
- self.revisions.get(id)
395
- }
396
-
397
- /// Check if A is ancestor of B
398
- pub fn is_ancestor(&self, a: &StateId, b: &StateId) -> bool {
399
- if a == b {
400
- return true;
401
- }
402
- // The revision model makes this graph acyclic, so the visited set is a
403
- // safety belt rather than the thing that makes the walk correct: a
404
- // topology assembled by hand, or read from an older log, must not be
405
- // able to hang a caller.
406
- let mut visited = BTreeSet::new();
407
- let mut current = b.clone();
408
- while let Some(parent_id) = self.parents.get(&current) {
409
- if parent_id == a {
410
- return true;
411
- }
412
- if !visited.insert(current.clone()) {
413
- return false;
414
- }
415
- current = parent_id.clone();
416
- }
417
- false
418
- }
419
-
420
- /// Get all ancestors of a revision (excluding self)
421
- pub fn ancestors(&self, id: &StateId) -> Vec<StateId> {
422
- let mut ancestors = Vec::new();
423
- let mut visited = BTreeSet::new();
424
- let mut current = id.clone();
425
-
426
- while let Some(parent_id) = self.parents.get(&current) {
427
- if !visited.insert(current.clone()) {
428
- break;
429
- }
430
- ancestors.push(parent_id.clone());
431
- current = parent_id.clone();
432
- }
433
-
434
- ancestors
435
- }
436
-
437
- /// Find common ancestor of two revisions
438
- pub fn common_ancestor(&self, a: &StateId, b: &StateId) -> Option<StateId> {
439
- if a == b {
440
- return Some(a.clone());
441
- }
442
-
443
- let ancestors_a: BTreeSet<_> = self.ancestors(a).iter().cloned().collect();
444
-
445
- let mut visited = BTreeSet::new();
446
- let mut current = b.clone();
447
- while let Some(parent_id) = self.parents.get(&current) {
448
- if ancestors_a.contains(parent_id) {
449
- return Some(parent_id.clone());
450
- }
451
- if !visited.insert(current.clone()) {
452
- break;
453
- }
454
- current = parent_id.clone();
455
- }
456
-
457
- None
458
- }
459
-
460
- /// Determine relationship between two revisions
461
- pub fn relationship(&self, a: &StateId, b: &StateId) -> Relationship {
462
- if a == b {
463
- return Relationship::Identity;
464
- }
465
- if self.is_ancestor(a, b) {
466
- return Relationship::Ancestor;
467
- }
468
- if self.is_ancestor(b, a) {
469
- return Relationship::Descendant;
470
- }
471
- if self.common_ancestor(a, b).is_some() {
472
- return Relationship::Diverged;
473
- }
474
- Relationship::Unrelated
475
- }
476
- }
477
-
478
- // ============================================================================
479
- // Semantic Diff: Deterministic, Representation-Sensitive Differences
480
- // ============================================================================
481
-
482
- /// Change type at a specific location
483
- #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
484
- pub enum ChangeKind {
485
- /// Value was added
486
- Added,
487
- /// Value was removed
488
- Removed,
489
- /// Value was changed
490
- Changed,
491
- }
492
-
493
- /// Distinguishes between object keys and array indices
494
- #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
495
- pub enum PathComponent {
496
- /// Object key
497
- Key(String),
498
- /// Array index
499
- Index(usize),
500
- }
501
-
502
- /// How two changed paths relate to one another.
503
- ///
504
- /// Conflict classification needs this because two changes interact whenever one
505
- /// path contains the other, not only when the paths are equal. Comparison is
506
- /// structural, over whole [`PathComponent`]s: `["a", "b"]` and `["a", "bc"]` are
507
- /// [`PathRelation::Disjoint`], even though one rendering of them shares a string
508
- /// prefix, and `["a"]` with `Key("a")` never matches `Index(0)`.
509
- #[derive(Clone, Copy, Debug, PartialEq, Eq)]
510
- pub enum PathRelation {
511
- /// The same path.
512
- Same,
513
- /// The first path contains the second: `a` against `a.b`.
514
- Ancestor,
515
- /// The first path is contained by the second: `a.b` against `a`.
516
- Descendant,
517
- /// Neither contains the other: siblings, or unrelated subtrees.
518
- Disjoint,
519
- }
520
-
521
- /// Whether `candidate` begins with every component of `prefix`, structurally.
522
- fn path_starts_with(candidate: &[PathComponent], prefix: &[PathComponent]) -> bool {
523
- candidate.len() >= prefix.len() && candidate[..prefix.len()] == *prefix
524
- }
525
-
526
- /// Classifies how two changed paths relate.
527
- ///
528
- /// Pure, total, and independent of any state: it reads only the two paths.
529
- pub fn path_relation(one: &[PathComponent], two: &[PathComponent]) -> PathRelation {
530
- if one == two {
531
- PathRelation::Same
532
- } else if path_starts_with(two, one) {
533
- PathRelation::Ancestor
534
- } else if path_starts_with(one, two) {
535
- PathRelation::Descendant
536
- } else {
537
- PathRelation::Disjoint
538
- }
539
- }
540
-
541
- /// Whether two changed paths interact: equal, or one containing the other.
542
- ///
543
- /// This is the predicate conflict classification is defined over. Two changes
544
- /// at overlapping paths cannot both be applied — the outer one determines the
545
- /// value the inner one was editing — so they are never independent.
546
- pub fn paths_overlap(one: &[PathComponent], two: &[PathComponent]) -> bool {
547
- !matches!(path_relation(one, two), PathRelation::Disjoint)
548
- }
549
-
550
- /// Reads the value a path names, or `None` when the path does not resolve.
551
- ///
552
- /// Object members are addressed by [`PathComponent::Key`] and array elements by
553
- /// [`PathComponent::Index`], exactly as [`SemanticDiff`] emits them, so a path
554
- /// taken from a diff resolves in the state that diff was computed from.
555
- pub fn resolve_path<'a>(value: &'a Value, path: &[PathComponent]) -> Option<&'a Value> {
556
- let mut current = value;
557
- for component in path {
558
- current = match component {
559
- PathComponent::Key(key) => current.as_object()?.get(key)?,
560
- PathComponent::Index(index) => current.as_array()?.get(*index)?,
561
- };
562
- }
563
- Some(current)
564
- }
565
-
566
- /// Semantic change at a specific path (deterministically ordered)
567
- #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
568
- pub struct SemanticChange {
569
- /// Path to changed value: Vec<PathComponent> for deterministic ordering
570
- pub path: Vec<PathComponent>,
571
- /// Kind of change
572
- pub kind: ChangeKind,
573
- /// Old value (None if Added)
574
- pub old_value: Option<Value>,
575
- /// New value (None if Removed)
576
- pub new_value: Option<Value>,
577
- }
578
-
579
- /// Semantic diff result (deterministically ordered)
580
- #[derive(Clone, Debug, Serialize, Deserialize)]
581
- pub struct SemanticDiff {
582
- /// Changes in deterministic order
583
- pub changes: Vec<SemanticChange>,
584
- /// Read-only indicator
585
- pub is_read_only: bool,
586
- }
587
-
588
- impl SemanticDiff {
589
- /// Compute diff from two JSON objects
590
- /// Deterministically ordered, direction-sensitive
591
- pub fn compute(old: &Value, new: &Value) -> Self {
592
- let mut changes = Vec::new();
593
- Self::diff_recursive(old, new, &mut vec![], &mut changes);
594
-
595
- // Sort deterministically
596
- changes.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.kind.cmp(&b.kind)));
597
-
598
- SemanticDiff {
599
- changes,
600
- is_read_only: false,
601
- }
602
- }
603
-
604
- fn diff_recursive(
605
- old: &Value,
606
- new: &Value,
607
- path: &mut Vec<PathComponent>,
608
- changes: &mut Vec<SemanticChange>,
609
- ) {
610
- use serde_json::{json, Value::*};
611
-
612
- match (old, new) {
613
- (Null, Null) => {}
614
- (Bool(a), Bool(b)) if a == b => {}
615
- (Number(a), Number(b)) if a == b => {}
616
- (String(a), String(b)) if a == b => {}
617
- (Array(a), Array(b)) => {
618
- for (i, (old_v, new_v)) in a.iter().zip(b.iter()).enumerate() {
619
- path.push(PathComponent::Index(i));
620
- Self::diff_recursive(old_v, new_v, path, changes);
621
- path.pop();
622
- }
623
- // Additions
624
- for (i, new_v) in b.iter().enumerate().skip(a.len()) {
625
- path.push(PathComponent::Index(i));
626
- changes.push(SemanticChange {
627
- path: path.clone(),
628
- kind: ChangeKind::Added,
629
- old_value: None,
630
- new_value: Some(new_v.clone()),
631
- });
632
- path.pop();
633
- }
634
- // Removals
635
- for (i, old_v) in a.iter().enumerate().skip(b.len()) {
636
- path.push(PathComponent::Index(i));
637
- changes.push(SemanticChange {
638
- path: path.clone(),
639
- kind: ChangeKind::Removed,
640
- old_value: Some(old_v.clone()),
641
- new_value: None,
642
- });
643
- path.pop();
644
- }
645
- }
646
- (Object(a), Object(b)) => {
647
- for key in a.keys() {
648
- if let Some(new_v) = b.get(key) {
649
- path.push(PathComponent::Key(key.clone()));
650
- Self::diff_recursive(&a[key], new_v, path, changes);
651
- path.pop();
652
- } else {
653
- path.push(PathComponent::Key(key.clone()));
654
- changes.push(SemanticChange {
655
- path: path.clone(),
656
- kind: ChangeKind::Removed,
657
- old_value: Some(a[key].clone()),
658
- new_value: None,
659
- });
660
- path.pop();
661
- }
662
- }
663
- for key in b.keys() {
664
- if !a.contains_key(key) {
665
- path.push(PathComponent::Key(key.clone()));
666
- changes.push(SemanticChange {
667
- path: path.clone(),
668
- kind: ChangeKind::Added,
669
- old_value: None,
670
- new_value: Some(b[key].clone()),
671
- });
672
- path.pop();
673
- }
674
- }
675
- }
676
- (_, _) => {
677
- changes.push(SemanticChange {
678
- path: path.clone(),
679
- kind: ChangeKind::Changed,
680
- old_value: Some(old.clone()),
681
- new_value: Some(new.clone()),
682
- });
683
- }
684
- }
685
- }
686
-
687
- /// Mark as read-only
688
- pub fn read_only(mut self) -> Self {
689
- self.is_read_only = true;
690
- self
691
- }
692
- }
693
-
694
- // ============================================================================
695
- // Conflict Classification: Three-Way Merge Analysis
696
- // ============================================================================
697
-
698
- /// Classification of conflict between two changes
699
- ///
700
- /// `Independent` carries the strong meaning its name and the state-model
701
- /// graduation audit both assert: the two branches' changes at this path do not
702
- /// overlap, so both can be applied without either overwriting or invalidating
703
- /// the other. It is not merely "the paths differ" — see [`paths_overlap`].
704
- #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
705
- pub enum ConflictClass {
706
- /// Changes don't overlap; can merge automatically
707
- Independent,
708
- /// Changes converge on same result
709
- Convergent,
710
- /// Changes conflict; requires explicit reconciliation
711
- Conflict,
712
- }
713
-
714
- /// Per-path conflict classification
715
- #[derive(Clone, Debug, Serialize, Deserialize)]
716
- pub struct PathConflict {
717
- pub path: Vec<PathComponent>,
718
- pub classification: ConflictClass,
719
- /// The common ancestor's value at this path, or `None` when the path does
720
- /// not resolve in the base.
721
- ///
722
- /// Read from the base state itself. It was previously read from the left
723
- /// branch's change, which reported `None` for a path only the right branch
724
- /// touched even when the base held a value there.
725
- pub base_value: Option<Value>,
726
- /// The left branch's value at this path, or `None` if the left branch did
727
- /// not change it or removed it.
728
- pub left_value: Option<Value>,
729
- /// The right branch's value at this path, or `None` if the right branch did
730
- /// not change it or removed it.
731
- pub right_value: Option<Value>,
732
- }
733
-
734
- /// Conflict classification result
735
- #[derive(Clone, Debug, Serialize, Deserialize)]
736
- pub struct ConflictClassification {
737
- /// Conflicts at each path (deterministically ordered)
738
- pub path_conflicts: Vec<PathConflict>,
739
- /// Overall classification
740
- pub overall: ConflictClass,
741
- }
742
-
743
- impl ConflictClassification {
744
- /// Classify conflicts from three-way merge
745
- ///
746
- /// `base` is the common ancestor, `left` the first branch, `right` the
747
- /// second. Only each revision's `content` is read.
748
- ///
749
- /// Two changes interact whenever their paths **overlap** — equal, or one
750
- /// containing the other — not only when they are equal. A branch that
751
- /// replaces `o` and a branch that edits `o.a` cannot both be applied: the
752
- /// first determines the value the second was editing. Both such paths are
753
- /// reported as [`ConflictClass::Conflict`].
754
- ///
755
- /// An overlap that is not at the same path can never be convergent. For a
756
- /// descendant path to exist, the base must hold a container there; a change
757
- /// reported *at* the ancestor path means that branch no longer holds a
758
- /// container of that kind, while the branch editing inside it still does.
759
- /// The two values therefore always differ. `classify` does not rely on that
760
- /// argument — it compares the two branches' resulting values — but the
761
- /// conformance suite asserts it, so the reasoning stays checked.
762
- pub fn classify(base: &StateRevision, left: &StateRevision, right: &StateRevision) -> Self {
763
- use serde_json::Value;
764
-
765
- let base_json: Value = serde_json::from_str(&base.content).unwrap_or(Value::Null);
766
- let left_json: Value = serde_json::from_str(&left.content).unwrap_or(Value::Null);
767
- let right_json: Value = serde_json::from_str(&right.content).unwrap_or(Value::Null);
768
-
769
- let left_diff = SemanticDiff::compute(&base_json, &left_json);
770
- let right_diff = SemanticDiff::compute(&base_json, &right_json);
771
-
772
- let mut path_conflicts = Vec::new();
773
- let mut has_conflict = false;
774
-
775
- let left_paths: BTreeMap<Vec<PathComponent>, &SemanticChange> = left_diff
776
- .changes
777
- .iter()
778
- .map(|c| (c.path.clone(), c))
779
- .collect();
780
- let right_paths: BTreeMap<Vec<PathComponent>, &SemanticChange> = right_diff
781
- .changes
782
- .iter()
783
- .map(|c| (c.path.clone(), c))
784
- .collect();
785
-
786
- // Find all unique paths
787
- let all_paths: BTreeSet<_> = left_paths
788
- .keys()
789
- .chain(right_paths.keys())
790
- .cloned()
791
- .collect();
792
-
793
- for path in all_paths {
794
- let left_change = left_paths.get(&path);
795
- let right_change = right_paths.get(&path);
796
-
797
- let classification = match (left_change, right_change) {
798
- (Some(lc), Some(rc)) => {
799
- if lc.kind == rc.kind && lc.new_value == rc.new_value {
800
- ConflictClass::Convergent
801
- } else {
802
- has_conflict = true;
803
- ConflictClass::Conflict
804
- }
805
- }
806
- // Only one branch changed this path. It is independent only if
807
- // the other branch changed nothing that contains it or that it
808
- // contains.
809
- (Some(_), None) => {
810
- if overlapping_change_exists(&right_paths, &path) {
811
- has_conflict = true;
812
- ConflictClass::Conflict
813
- } else {
814
- ConflictClass::Independent
815
- }
816
- }
817
- (None, Some(_)) => {
818
- if overlapping_change_exists(&left_paths, &path) {
819
- has_conflict = true;
820
- ConflictClass::Conflict
821
- } else {
822
- ConflictClass::Independent
823
- }
824
- }
825
- // Unreachable: every path came from one of the two maps.
826
- (None, None) => ConflictClass::Independent,
827
- };
828
-
829
- path_conflicts.push(PathConflict {
830
- path: path.clone(),
831
- classification,
832
- base_value: resolve_path(&base_json, &path).cloned(),
833
- left_value: left_change.and_then(|c| c.new_value.clone()),
834
- right_value: right_change.and_then(|c| c.new_value.clone()),
835
- });
836
- }
837
-
838
- // Sort deterministically
839
- path_conflicts.sort_by(|a, b| a.path.cmp(&b.path));
840
-
841
- let overall = if has_conflict {
842
- ConflictClass::Conflict
843
- } else if path_conflicts
844
- .iter()
845
- .any(|c| c.classification == ConflictClass::Convergent)
846
- {
847
- ConflictClass::Convergent
848
- } else {
849
- ConflictClass::Independent
850
- };
851
-
852
- ConflictClassification {
853
- path_conflicts,
854
- overall,
855
- }
856
- }
857
- }
858
-
859
- /// Whether `changes` holds any change at a path overlapping `path`, other than
860
- /// at `path` itself.
861
- ///
862
- /// Ancestors are the proper prefixes of `path`, so there are at most as many as
863
- /// the path is deep. Descendants are contiguous in sorted order — every
864
- /// sequence beginning with `path` sorts together, directly after `path` — so the
865
- /// first key strictly greater than `path` decides whether any exists.
866
- fn overlapping_change_exists(
867
- changes: &BTreeMap<Vec<PathComponent>, &SemanticChange>,
868
- path: &[PathComponent],
869
- ) -> bool {
870
- for depth in 0..path.len() {
871
- if changes.contains_key(&path[..depth].to_vec()) {
872
- return true;
873
- }
874
- }
875
- changes
876
- .range(path.to_vec()..)
877
- .find(|(candidate, _)| candidate.as_slice() != path)
878
- .is_some_and(|(candidate, _)| path_starts_with(candidate, path))
879
- }
880
-
881
- // ============================================================================
882
- // Reconciliation: Explicit Parent Choice Mechanism
883
- // ============================================================================
884
-
885
- /// Reconciliation plan specifying how to merge two branches
886
- #[derive(Clone, Debug, Serialize, Deserialize)]
887
- pub struct ReconciliationPlan {
888
- /// Left (first) branch revision
889
- pub left_id: StateId,
890
- /// Right (second) branch revision
891
- pub right_id: StateId,
892
- /// Common ancestor revision
893
- pub base_id: StateId,
894
- /// Which branch to use as primary: true for left, false for right
895
- pub parent_choice: bool,
896
- /// Override values for specific paths (caller-supplied)
897
- pub path_overrides: BTreeMap<Vec<PathComponent>, Value>,
898
- }
899
-
900
- impl ReconciliationPlan {
901
- /// Create reconciliation plan with explicit parent choice
902
- pub fn new(left_id: StateId, right_id: StateId, base_id: StateId, parent_choice: bool) -> Self {
903
- ReconciliationPlan {
904
- left_id,
905
- right_id,
906
- base_id,
907
- parent_choice,
908
- path_overrides: BTreeMap::new(),
909
- }
910
- }
911
-
912
- /// Add override for a specific path
913
- pub fn with_override(mut self, path: Vec<PathComponent>, value: Value) -> Self {
914
- self.path_overrides.insert(path, value);
915
- self
916
- }
917
-
918
- /// Validate that plan references valid states
919
- pub fn validate(&self) -> bool {
920
- // Base validation: ids are non-empty
921
- !self.left_id.as_hex().is_empty()
922
- && !self.right_id.as_hex().is_empty()
923
- && !self.base_id.as_hex().is_empty()
924
- }
925
- }
926
-
927
- /// Reconciliation result (immutable once created)
928
- #[derive(Clone, Debug, Serialize, Deserialize)]
929
- pub struct StateReconciliationResult {
930
- /// The materialized reconciled state
931
- pub materialized_state: StateRevision,
932
- /// Which plan was applied
933
- pub plan: ReconciliationPlan,
934
- }
935
-
936
- // ----------------------------------------------------------------------------
937
- // Applying a reconciliation plan
938
- // ----------------------------------------------------------------------------
939
- //
940
- // The plan says what the caller decided. Application produces the state that
941
- // decision names, and nothing else:
942
- //
943
- // Classification — which changes can coexist?
944
- // Plan — what happens to the ones that cannot?
945
- // Application — produce the resulting state from that decision.
946
- //
947
- // Nothing here merges. Nothing here resolves a conflict on the caller's behalf.
948
- // A path the plan does not mention keeps whatever the *selected parent* holds,
949
- // which is the caller's decision too — it is what `parent_choice` selects.
950
-
951
- /// The plan does not reference three usable revisions.
952
- pub const RECONCILE_PLAN_INVALID: &str = "RECONCILE_PLAN_INVALID";
953
- /// A supplied revision is not the one the plan names.
954
- pub const RECONCILE_REVISION_MISMATCH: &str = "RECONCILE_REVISION_MISMATCH";
955
- /// The selected parent's content is not JSON, so there is no state to start from.
956
- pub const RECONCILE_CONTENT_UNPARSEABLE: &str = "RECONCILE_CONTENT_UNPARSEABLE";
957
- /// Two override paths overlap, so the plan does not say what the result is.
958
- pub const RECONCILE_OVERRIDE_PATHS_OVERLAP: &str = "RECONCILE_OVERRIDE_PATHS_OVERLAP";
959
- /// An override path does not name an assignable location in the selected parent.
960
- pub const RECONCILE_OVERRIDE_UNADDRESSABLE: &str = "RECONCILE_OVERRIDE_UNADDRESSABLE";
961
-
962
- impl ReconciliationPlan {
963
- /// The branch this plan selects as the state to start from.
964
- ///
965
- /// `parent_choice` is a `bool` and so has exactly two values: `true` is the
966
- /// left branch, `false` the right. The ancestor is not a choice, and there
967
- /// is no third value for this to reject.
968
- pub fn selected_parent<'a>(
969
- &self,
970
- left: &'a StateRevision,
971
- right: &'a StateRevision,
972
- ) -> &'a StateRevision {
973
- if self.parent_choice {
974
- left
975
- } else {
976
- right
977
- }
978
- }
979
-
980
- /// The conflicting paths this plan supplies no override for.
981
- ///
982
- /// Descriptive, not enforced. Application does **not** require an override
983
- /// for every conflict: leaving one unaddressed resolves it to whatever the
984
- /// selected parent holds, and selecting that parent is itself an explicit
985
- /// caller decision. A caller that wants every conflict addressed
986
- /// individually can require this to be empty before applying.
987
- pub fn unresolved_conflicts(
988
- &self,
989
- classification: &ConflictClassification,
990
- ) -> Vec<Vec<PathComponent>> {
991
- classification
992
- .path_conflicts
993
- .iter()
994
- .filter(|entry| entry.classification == ConflictClass::Conflict)
995
- .map(|entry| entry.path.clone())
996
- .filter(|path| !self.path_overrides.contains_key(path))
997
- .collect()
998
- }
999
- }
1000
-
1001
- /// Applies a reconciliation plan, producing canonical reconciled content.
1002
- ///
1003
- /// Pure and total with respect to its inputs: it reads the three revisions and
1004
- /// the plan, mutates none of them, consults no clock, filesystem, network or
1005
- /// global state, and returns either canonical JSON or a stable error code.
1006
- ///
1007
- /// # What it does
1008
- ///
1009
- /// 1. Checks the plan validates and names the three revisions supplied.
1010
- /// 2. Takes the state of the branch `parent_choice` selects.
1011
- /// 3. Assigns each override into it.
1012
- /// 4. Serializes the result canonically.
1013
- ///
1014
- /// # What it does not do
1015
- ///
1016
- /// It does not merge, and it does not consult the other branch or the ancestor
1017
- /// for anything but identity. A conflicting path with no override keeps the
1018
- /// selected parent's value.
1019
- ///
1020
- /// # Overrides
1021
- ///
1022
- /// `path_overrides` is a map keyed by structural path, so a repeated path
1023
- /// cannot exist — the type forecloses it. What the type does not foreclose is
1024
- /// two paths where one *contains* the other, and such a plan does not say what
1025
- /// the result should be: does the inner override refine the outer value, or was
1026
- /// the outer one meant to stand? Rather than invent a precedence the plan never
1027
- /// stated, overlapping override paths are rejected. Overlap is the same
1028
- /// structural relation conflict classification uses, so `a.b` and `a.bc` are
1029
- /// not an overlap.
1030
- ///
1031
- /// Because overlaps are rejected, the order overrides are applied in cannot
1032
- /// affect the result.
1033
- ///
1034
- /// An override path must be *addressable* in the selected state: every proper
1035
- /// prefix must already resolve to a container of the matching kind. Assigning a
1036
- /// member to an existing object is allowed whether or not the member is already
1037
- /// there, because it creates no structure. Assigning past the end of an array is
1038
- /// not, because it would require inventing the elements in between. To change an
1039
- /// array's length, or to remove a member, override the container itself — a
1040
- /// `Value` cannot express absence, so removal has no other encoding in this
1041
- /// plan.
1042
- pub fn apply_reconciliation_plan(
1043
- base: &StateRevision,
1044
- left: &StateRevision,
1045
- right: &StateRevision,
1046
- plan: &ReconciliationPlan,
1047
- ) -> Result<String, String> {
1048
- if !plan.validate() {
1049
- return Err(format!(
1050
- "{RECONCILE_PLAN_INVALID}: the plan does not reference three revisions"
1051
- ));
1052
- }
1053
- for (label, expected, supplied) in [
1054
- ("base", &plan.base_id, &base.id),
1055
- ("left", &plan.left_id, &left.id),
1056
- ("right", &plan.right_id, &right.id),
1057
- ] {
1058
- if expected != supplied {
1059
- return Err(format!(
1060
- "{RECONCILE_REVISION_MISMATCH}: the plan names a different {label} revision"
1061
- ));
1062
- }
1063
- }
1064
-
1065
- let parent = plan.selected_parent(left, right);
1066
- let mut state: Value = serde_json::from_str(&parent.content).map_err(|error| {
1067
- // Classification tolerates unparseable content by reading it as JSON
1068
- // null, because it only describes. Materializing a state from content
1069
- // nobody can parse would be producing arbitrary state, so this refuses.
1070
- format!(
1071
- "{RECONCILE_CONTENT_UNPARSEABLE}: the selected parent's content is not JSON: {error}"
1072
- )
1073
- })?;
1074
-
1075
- // `BTreeMap` sorts its keys, and every sequence with a given prefix sorts
1076
- // contiguously after it, so any containment relation shows up between two
1077
- // consecutive keys.
1078
- let paths: Vec<&Vec<PathComponent>> = plan.path_overrides.keys().collect();
1079
- for pair in paths.windows(2) {
1080
- if paths_overlap(pair[0], pair[1]) {
1081
- return Err(format!(
1082
- "{RECONCILE_OVERRIDE_PATHS_OVERLAP}: {:?} and {:?} overlap, so the plan does \
1083
- not say what the result is",
1084
- pair[0], pair[1]
1085
- ));
1086
- }
1087
- }
1088
-
1089
- for (path, value) in &plan.path_overrides {
1090
- assign_path(&mut state, path, value.clone())?;
1091
- }
1092
-
1093
- serde_json::to_string(&state)
1094
- .map_err(|error| format!("{RECONCILE_CONTENT_UNPARSEABLE}: {error}"))
1095
- }
1096
-
1097
- /// Assigns `value` at `path`, or reports why the path is not assignable.
1098
- fn assign_path(state: &mut Value, path: &[PathComponent], value: Value) -> Result<(), String> {
1099
- let Some((last, prefix)) = path.split_last() else {
1100
- // The empty path is the state itself.
1101
- *state = value;
1102
- return Ok(());
1103
- };
1104
-
1105
- let mut current = state;
1106
- for (depth, component) in prefix.iter().enumerate() {
1107
- current = match component {
1108
- PathComponent::Key(key) => current.as_object_mut().and_then(|o| o.get_mut(key)),
1109
- PathComponent::Index(index) => current.as_array_mut().and_then(|a| a.get_mut(*index)),
1110
- }
1111
- .ok_or_else(|| {
1112
- format!(
1113
- "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} does not resolve at \
1114
- component {depth} ({component:?})"
1115
- )
1116
- })?;
1117
- }
1118
-
1119
- match last {
1120
- // A new member of an existing object creates no structure.
1121
- PathComponent::Key(key) => match current.as_object_mut() {
1122
- Some(members) => {
1123
- members.insert(key.clone(), value);
1124
- Ok(())
1125
- }
1126
- None => Err(format!(
1127
- "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} names a member of \
1128
- something that is not an object"
1129
- )),
1130
- },
1131
- // An index past the end would require inventing the elements before it.
1132
- PathComponent::Index(index) => match current.as_array_mut() {
1133
- Some(items) if *index < items.len() => {
1134
- items[*index] = value;
1135
- Ok(())
1136
- }
1137
- Some(items) => Err(format!(
1138
- "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} is past the end of an \
1139
- array of length {}",
1140
- items.len()
1141
- )),
1142
- None => Err(format!(
1143
- "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} indexes something that \
1144
- is not an array"
1145
- )),
1146
- },
1147
- }
1148
- }
1149
-
1150
- fn assign_projection(
1151
- state: &mut Value,
1152
- path: &[PathComponent],
1153
- value: Value,
1154
- ) -> Result<(), MaterializationError> {
1155
- let Some((last, prefix)) = path.split_last() else {
1156
- *state = value;
1157
- return Ok(());
1158
- };
1159
-
1160
- let mut current = state;
1161
- for component in prefix {
1162
- match component {
1163
- PathComponent::Key(key) => {
1164
- if current.is_null() {
1165
- *current = Value::Object(serde_json::Map::new());
1166
- }
1167
- let Some(object) = current.as_object_mut() else {
1168
- return Err(MaterializationError::MissingPath(path.to_vec()));
1169
- };
1170
- current = object.entry(key.clone()).or_insert(Value::Null);
1171
- }
1172
- PathComponent::Index(index) => {
1173
- if current.is_null() {
1174
- *current = Value::Array(Vec::new());
1175
- }
1176
- let Some(array) = current.as_array_mut() else {
1177
- return Err(MaterializationError::MissingPath(path.to_vec()));
1178
- };
1179
- while array.len() <= *index {
1180
- array.push(Value::Null);
1181
- }
1182
- current = &mut array[*index];
1183
- }
1184
- }
1185
- }
1186
-
1187
- match last {
1188
- PathComponent::Key(key) => {
1189
- if current.is_null() {
1190
- *current = Value::Object(serde_json::Map::new());
1191
- }
1192
- let Some(object) = current.as_object_mut() else {
1193
- return Err(MaterializationError::MissingPath(path.to_vec()));
1194
- };
1195
- object.insert(key.clone(), value);
1196
- Ok(())
1197
- }
1198
- PathComponent::Index(index) => {
1199
- if current.is_null() {
1200
- *current = Value::Array(Vec::new());
1201
- }
1202
- let Some(array) = current.as_array_mut() else {
1203
- return Err(MaterializationError::MissingPath(path.to_vec()));
1204
- };
1205
- while array.len() <= *index {
1206
- array.push(Value::Null);
1207
- }
1208
- array[*index] = value;
1209
- Ok(())
1210
- }
1211
- }
1212
- }
1213
-
1214
- /// Applies a plan and materializes the result as a new revision.
1215
- ///
1216
- /// The revision is built with the existing [`StateRevision::child`] constructor,
1217
- /// so it carries the existing content-addressed identity and takes the selected
1218
- /// parent as its parent. Nothing here is a new identity algorithm, and nothing
1219
- /// here moves a branch head, writes to a store, or touches replication: the
1220
- /// result is a value the caller decides what to do with.
1221
- ///
1222
- /// Unlike [`apply_reconciliation_plan`], this is not pure — `StateRevision`
1223
- /// timestamps come from the clock, as they do for every other revision this
1224
- /// model creates. The application semantics stay in the pure function.
1225
- pub fn reconcile(
1226
- base: &StateRevision,
1227
- left: &StateRevision,
1228
- right: &StateRevision,
1229
- plan: &ReconciliationPlan,
1230
- authority: String,
1231
- ) -> Result<StateReconciliationResult, String> {
1232
- let content = apply_reconciliation_plan(base, left, right, plan)?;
1233
- let parent = plan.selected_parent(left, right);
1234
- Ok(StateReconciliationResult {
1235
- materialized_state: StateRevision::child(content, parent, authority),
1236
- plan: plan.clone(),
1237
- })
1238
- }
1239
-
1240
- // ============================================================================
1241
- // ============================================================================
1242
- // StateStore: Durable State Persistence and Retrieval
1243
- // ============================================================================
1244
-
1245
- /// Durable, content-addressed revision storage.
1246
- ///
1247
- /// `StateStore` answers exactly one question:
1248
- ///
1249
- /// > **Do I have revision X?**
1250
- ///
1251
- /// It deliberately does **not** answer *"what is the current revision?"*. That
1252
- /// separation is the point of this type. State history and currentness are
1253
- /// different concerns, and conflating them is what produced the two defects the
1254
- /// branching audit recorded: a global `current` that moved on every commit
1255
- /// regardless of which parent was named, and a branch map sharing one replicated
1256
- /// record with it. Both lived in a single `state:current` row that nothing in
1257
- /// the reconciliation chain ever read.
1258
- ///
1259
- /// # The contract
1260
- ///
1261
- /// | operation | method |
1262
- /// | --- | --- |
1263
- /// | put | [`create`](Self::create), [`commit`](Self::commit) |
1264
- /// | get | [`get`](Self::get) |
1265
- ///
1266
- /// Ancestry is not a third operation: it is [`get`](Self::get) followed by
1267
- /// `parent_id`, repeated. [`parent`](Self::parent), [`exists`](Self::exists) and
1268
- /// [`metadata`](Self::metadata) are conveniences over `get`, not new powers.
1269
- ///
1270
- /// # Key schema
1271
- ///
1272
- /// - `state:revision:{hex_id}` → [`StateRevision`]
1273
- ///
1274
- /// That is the entire schema. A record keyed by the hash of its own content
1275
- /// cannot be stale and needs no reconstruction, so there is **no recovery
1276
- /// path**: [`with_feltdb`](Self::with_feltdb) does not scan, rebuild an index,
1277
- /// or validate history before returning.
1278
- ///
1279
- /// # What this deliberately does not provide
1280
- ///
1281
- /// No `current`, no branches, no branch heads, no "latest". Nothing above the
1282
- /// history layer is invented here. Something must eventually own *"which
1283
- /// revision represents the current application state"* — a ref, an application
1284
- /// record, a per-resource pointer, a transaction head — and that owner is not
1285
- /// this type. Until it exists, callers hold the ids they care about.
1286
- ///
1287
- /// # Non-claims
1288
- ///
1289
- /// Proven for this type: durable single-writer append, retrieval by identity
1290
- /// across restart, ancestry by parent traversal, and independence between
1291
- /// records. **Not** proven and not claimed: multi-writer persistence semantics,
1292
- /// concurrent revision creation, resource isolation, garbage collection, and
1293
- /// distributed replication of revision history.
1294
- ///
1295
- /// Two entries left that list when `/v1/state/*` began serving this type.
1296
- /// *Server lifecycle integration*: `feltdb-server` opens a store over its own
1297
- /// `FeltDb` through [`crate::FeltDBStateSystem`] and serves history, diff,
1298
- /// classification and reconciliation from it. *Authorization*: those routes are
1299
- /// authorized like every other route — `state:read` to read, `state:write` to
1300
- /// commit — which is authorization **at the route**, not in this type. This
1301
- /// type still has none of its own, and a caller holding a `StateStore` still
1302
- /// reaches every resource in it.
1303
- ///
1304
- /// *Current/head semantics* also left, but only partly, and the distinction
1305
- /// matters: [`head_of`](Self::head_of) answers "the newest retained revision of
1306
- /// this resource, by sequence". That is not a branch head and there is still no
1307
- /// ref anything can move.
1308
- pub struct StateStore {
1309
- backing: Backing,
1310
- mutation_lock: Arc<Mutex<()>>,
1311
- }
1312
-
1313
- /// Where revisions actually live.
1314
- ///
1315
- /// There is exactly one copy of a revision in either mode. The durable mode
1316
- /// keeps no second in-memory index beside FeltDB's own rows.
1317
- enum Backing {
1318
- /// In-memory only, for testing state semantics in isolation.
1319
- Volatile(Arc<Mutex<VolatileState>>),
1320
- /// FeltDB is the storage, and keyed reads are the authoritative read path.
1321
- Durable(Arc<crate::FeltDb>),
1322
- }
1323
-
1324
- /// The volatile backing's contents: revisions, and per-resource retention.
1325
- #[derive(Default)]
1326
- struct VolatileState {
1327
- revisions: HashMap<StateId, StateRevision>,
1328
- retention: HashMap<String, RetentionState>,
1329
- refs: HashMap<(String, String), StateRef>,
1330
- objects: HashMap<StateId, StateObject>,
1331
- }
1332
-
1333
- impl Clone for Backing {
1334
- fn clone(&self) -> Self {
1335
- match self {
1336
- Backing::Volatile(map) => Backing::Volatile(map.clone()),
1337
- Backing::Durable(db) => Backing::Durable(db.clone()),
1338
- }
1339
- }
1340
- }
1341
-
1342
- /// The durable key for a revision.
1343
- fn revision_key(id: &StateId) -> String {
1344
- format!("state:revision:{}", id.as_hex())
1345
- }
1346
-
1347
- fn object_key(id: &StateId) -> String {
1348
- format!("state:object:{}", id.as_hex())
1349
- }
1350
-
1351
- fn ref_key(namespace: &str, name: &str) -> String {
1352
- format!("state:ref:{}:{}", encode_ref_part(namespace), encode_ref_part(name))
1353
- }
1354
-
1355
- fn encode_ref_part(value: &str) -> String {
1356
- value.replace('%', "%25").replace(':', "%3A")
1357
- }
1358
-
1359
- fn valid_ref_component(value: &str) -> bool {
1360
- !value.trim().is_empty()
1361
- }
1362
-
1363
- fn looks_like_state_id(value: &str) -> bool {
1364
- value.len() == 64 && value.chars().all(|c| c.is_ascii_hexdigit())
1365
- }
1366
-
1367
- fn current_time_ms() -> u64 {
1368
- std::time::SystemTime::now()
1369
- .duration_since(std::time::UNIX_EPOCH)
1370
- .unwrap_or_default()
1371
- .as_millis() as u64
1372
- }
1373
-
1374
- /// How long a revision is protected from garbage collection after it is
1375
- /// minted, measured on its own `timestamp_ms`. A candidate an agent has just
1376
- /// committed but not yet promoted is a root for this long, so a collection that
1377
- /// races the promotion cannot reclaim it out from under the caller.
1378
- pub(crate) const GC_GRACE_MS: u64 = 15 * 60 * 1000;
1379
-
1380
- /// Durable state-model writes as FeltDB transactions.
1381
- ///
1382
- /// # Ordering
1383
- ///
1384
- /// Every operation here follows the same three steps:
1385
- ///
1386
- /// 1. **Read** the `state` rows the decision depends on.
1387
- /// 2. **Decide** in Rust: versions, presence, integrity.
1388
- /// 3. **Commit** with `FeltDb::apply_atomic_transaction_guarded`, carrying a
1389
- /// record precondition for *every row read in step 1*. FeltDB evaluates the
1390
- /// preconditions inside its commit lock, before staging anything, and
1391
- /// applies the mutations — one log record, `sync_data` before returning —
1392
- /// only if every row is still as it was read. Otherwise nothing is applied
1393
- /// and the operation starts again from step 1.
1394
- ///
1395
- /// So a decision is only ever applied to the state it was made against. The
1396
- /// mutual exclusion is FeltDB's `Inner` mutex, shared by every `StateStore`,
1397
- /// every `Arc<FeltDb>` clone and every `FeltDb::open` of the same path in one
1398
- /// process; the per-instance `mutation_lock` plays no part on this backing.
1399
- ///
1400
- /// # How a row is pinned
1401
- ///
1402
- /// FeltDB's record precondition compares `__version`, reading a row without one
1403
- /// as version 1, and stamps `N + 1` on a row written under `expected_version =
1404
- /// N` (`require_absent` writes 1). That gives three kinds of pin:
1405
- ///
1406
- /// - **Versioned rows** (ref, ref incarnation) are only ever written under a
1407
- /// precondition, so their `__version` advances on every write and
1408
- /// `expected_version = <as read>` pins them; an absent one is pinned by
1409
- /// `require_absent`.
1410
- /// - **Immutable rows** (revision, content object) are never versioned, so
1411
- /// `expected_version = 1` means exactly "still present" and writes nothing.
1412
- /// - **The generation row** (`state:_gcgen`) is rewritten by every ref change
1413
- /// and every committed revision *without* a precondition, carrying a
1414
- /// `__version` unique to that transaction (an unconditional write is stored
1415
- /// verbatim). Writers therefore never conflict with each other through it,
1416
- /// while a collection that pins the value it read is refused if any of them
1417
- /// committed in between.
1418
- ///
1419
- /// | operation | pinned | writes |
1420
- /// | --- | --- | --- |
1421
- /// | create ref | ref (absent), incarnation, target | ref, incarnation, generation |
1422
- /// | CAS ref | ref, incarnation, target | ref, generation |
1423
- /// | delete ref | ref, incarnation | (ref removed), incarnation, generation |
1424
- /// | commit revision | revision (absent), content object, parent | object (if new), revision, generation |
1425
- /// | collect | generation | (unreachable revisions and objects removed) |
1426
- ///
1427
- /// The incarnation row is pinned alongside the ref because a delete followed by
1428
- /// a create returns the ref row to `__version` 1; the incarnation row is never
1429
- /// deleted, so its version only grows and the pair cannot repeat.
1430
- ///
1431
- /// # The races this closes
1432
- ///
1433
- /// - *Concurrent CAS.* Both read the ref at `__version` N; the first commit
1434
- /// stamps N + 1; the second's precondition fails, it re-reads, and reports
1435
- /// `VersionConflict`.
1436
- /// - *GC vs promotion.* If the promotion commits first, the generation moved
1437
- /// and GC re-plans with the target as a ref root. If GC commits first, the
1438
- /// promotion's target pin fails, and on re-read it reports `UnknownTarget`.
1439
- /// - *GC vs commit of a child.* If the commit lands first, the generation
1440
- /// moved. If GC lands first, the commit's parent pin fails and it reports
1441
- /// `UnknownParent`.
1442
- ///
1443
- /// Revisions minted by the record write path are not committed here; each is a
1444
- /// child of that path's current head, which collection always treats as a
1445
- /// root, so they cannot gain a collected parent.
1446
- ///
1447
- /// # Idempotent ref requests
1448
- ///
1449
- /// Ref transactions carry deterministic ids derived from the ref, its
1450
- /// *incarnation* and the version they act on, and a payload hash of the rest
1451
- /// of the request. FeltDB binds an id to its payload durably, across restart
1452
- /// and compaction. A repeat of a committed request finds its own payload under
1453
- /// its id and returns `Ok` without writing; any other request for the same slot
1454
- /// finds a different payload and is a conflict. The incarnation
1455
- /// (`state:refinc:…`, advanced by every create) keeps a deleted-and-recreated
1456
- /// ref, back at version 1, from matching a transaction of its predecessor. Rows
1457
- /// written before this scheme have no incarnation and are incarnation 0.
1458
- pub(crate) mod durable {
1459
- use super::*;
1460
- use crate::{AtomicMutation, FeltDb, FlowError, PreconditionFailure, RecordPrecondition};
1461
- use serde_json::json;
1462
- use std::sync::atomic::{AtomicU64, Ordering};
1463
-
1464
- /// Fresh reads and a new attempt after a refused precondition. Each refusal
1465
- /// means a competing write committed, so the system as a whole makes
1466
- /// progress; the bound only turns pathological contention into an error.
1467
- const ATTEMPTS: usize = 64;
1468
-
1469
- pub(super) const GENERATION_KEY: &str = "state:_gcgen";
1470
-
1471
- fn incarnation_key(namespace: &str, name: &str) -> String {
1472
- format!(
1473
- "state:refinc:{}:{}",
1474
- encode_ref_part(namespace),
1475
- encode_ref_part(name)
1476
- )
1477
- }
1478
-
1479
- fn put(key: &str, value: Value) -> AtomicMutation {
1480
- AtomicMutation {
1481
- capability: "state".into(),
1482
- key: key.into(),
1483
- value: Some(value),
1484
- }
1485
- }
1486
-
1487
- fn remove(key: &str) -> AtomicMutation {
1488
- AtomicMutation {
1489
- capability: "state".into(),
1490
- key: key.into(),
1491
- value: None,
1492
- }
1493
- }
1494
-
1495
- /// The version FeltDB's record precondition reads for a row.
1496
- fn row_version(row: &Value) -> u64 {
1497
- row.get("__version").and_then(Value::as_u64).unwrap_or(1)
1498
- }
1499
-
1500
- /// Pin a row to exactly what was read: absent, or this version.
1501
- fn unchanged(key: &str, read: Option<&Value>) -> RecordPrecondition {
1502
- RecordPrecondition {
1503
- capability: "state".into(),
1504
- key: key.into(),
1505
- require_absent: read.is_none(),
1506
- expected_version: read.map(row_version),
1507
- ..RecordPrecondition::default()
1508
- }
1509
- }
1510
-
1511
- /// Pin an immutable (never versioned) row as present.
1512
- fn present(key: &str) -> RecordPrecondition {
1513
- RecordPrecondition {
1514
- capability: "state".into(),
1515
- key: key.into(),
1516
- expected_version: Some(1),
1517
- ..RecordPrecondition::default()
1518
- }
1519
- }
1520
-
1521
- /// The generation row's new value: a `__version` unique to this transaction.
1522
- /// Transaction ids are never applied twice, so every rewrite changes it.
1523
- fn advance_generation(transaction_id: &str) -> AtomicMutation {
1524
- let digest = Sha256::digest(transaction_id.as_bytes());
1525
- let mut bytes = [0u8; 8];
1526
- bytes.copy_from_slice(&digest[..8]);
1527
- put(
1528
- GENERATION_KEY,
1529
- json!({ "__version": u64::from_be_bytes(bytes), "transaction": transaction_id }),
1530
- )
1531
- }
1532
-
1533
- fn unique_suffix() -> String {
1534
- static COUNTER: AtomicU64 = AtomicU64::new(0);
1535
- let nanos = std::time::SystemTime::now()
1536
- .duration_since(std::time::UNIX_EPOCH)
1537
- .unwrap_or_default()
1538
- .as_nanos();
1539
- format!(
1540
- "{}-{nanos}-{}",
1541
- std::process::id(),
1542
- COUNTER.fetch_add(1, Ordering::Relaxed)
1543
- )
1544
- }
1545
-
1546
- fn ref_transaction_id(
1547
- kind: &str,
1548
- namespace: &str,
1549
- name: &str,
1550
- incarnation: u64,
1551
- version: Option<u64>,
1552
- ) -> String {
1553
- let mut hasher = Sha256::new();
1554
- for part in [
1555
- "feltdb.state.ref.v1",
1556
- kind,
1557
- namespace,
1558
- name,
1559
- &incarnation.to_string(),
1560
- &version.map(|v| v.to_string()).unwrap_or_default(),
1561
- ] {
1562
- hasher.update(part.as_bytes());
1563
- hasher.update([0u8]);
1564
- }
1565
- format!("state-ref:{:x}", hasher.finalize())
1566
- }
1567
-
1568
- /// serde_json maps are ordered (no `preserve_order`), so this is canonical.
1569
- fn payload_hash(payload: &Value) -> String {
1570
- format!("sha256:{:x}", Sha256::digest(payload.to_string().as_bytes()))
1571
- }
1572
-
1573
- fn incarnation_of(row: &Value) -> u64 {
1574
- row.get("__incarnation").and_then(Value::as_u64).unwrap_or(0)
1575
- }
1576
-
1577
- fn incarnation_in(row: Option<&Value>) -> u64 {
1578
- row.and_then(|row| row.get("incarnation").and_then(Value::as_u64))
1579
- .unwrap_or(0)
1580
- }
1581
-
1582
- fn ref_row(reference: &StateRef, incarnation: u64) -> Value {
1583
- let mut row = serde_json::to_value(reference).unwrap_or(Value::Null);
1584
- if let Some(object) = row.as_object_mut() {
1585
- object.insert("__incarnation".into(), Value::from(incarnation));
1586
- }
1587
- row
1588
- }
1589
-
1590
- enum Refusal {
1591
- /// A pinned row changed: re-read and decide again.
1592
- Changed,
1593
- /// The id is already bound to another payload: re-read and decide again.
1594
- OtherRequest,
1595
- Failed(String),
1596
- }
1597
-
1598
- fn refusal(error: FlowError) -> Refusal {
1599
- match error {
1600
- FlowError::PreconditionFailed(failure) => match *failure {
1601
- PreconditionFailure::Present { .. }
1602
- | PreconditionFailure::Missing { .. }
1603
- | PreconditionFailure::Version { .. } => Refusal::Changed,
1604
- other => Refusal::Failed(format!("{other:?}")),
1605
- },
1606
- FlowError::TransactionIdPayloadMismatch { .. } => Refusal::OtherRequest,
1607
- other => Refusal::Failed(other.to_string()),
1608
- }
1609
- }
1610
-
1611
- fn storage(error: impl std::fmt::Display) -> RefError {
1612
- RefError::Storage(error.to_string())
1613
- }
1614
-
1615
- fn contended(what: &str) -> String {
1616
- format!(
1617
- "{what} was refused by concurrent changes on {ATTEMPTS} consecutive attempts; nothing was applied"
1618
- )
1619
- }
1620
-
1621
- fn row(db: &FeltDb, key: &str) -> Result<Option<Value>, RefError> {
1622
- db.get::<Value>(key).map_err(storage)
1623
- }
1624
-
1625
- fn committed_payload(db: &FeltDb, id: &str) -> Result<Option<String>, RefError> {
1626
- db.applied_transaction_payload_hash(id).map_err(storage)
1627
- }
1628
-
1629
- fn decode(row: Value) -> Result<StateRef, RefError> {
1630
- serde_json::from_value(row).map_err(storage)
1631
- }
1632
-
1633
- fn commit(
1634
- db: &FeltDb,
1635
- id: &str,
1636
- payload: Option<&str>,
1637
- pins: &[RecordPrecondition],
1638
- mutations: &[AtomicMutation],
1639
- ) -> Result<(), Refusal> {
1640
- db.apply_atomic_transaction_guarded(id, payload, None, &[], pins, mutations, None)
1641
- .map(|_| ())
1642
- .map_err(refusal)
1643
- }
1644
-
1645
- pub(super) fn create_ref(
1646
- db: &FeltDb,
1647
- namespace: &str,
1648
- name: &str,
1649
- target: StateId,
1650
- authority: String,
1651
- metadata: BTreeMap<String, Value>,
1652
- ) -> Result<StateRef, RefError> {
1653
- let ref_k = ref_key(namespace, name);
1654
- let inc_k = incarnation_key(namespace, name);
1655
- let target_k = revision_key(&target);
1656
- let payload = payload_hash(&json!({
1657
- "target": target.as_hex(),
1658
- "authority": authority,
1659
- "metadata": metadata,
1660
- }));
1661
- for _ in 0..ATTEMPTS {
1662
- let incarnation_row = row(db, &inc_k)?;
1663
- if let Some(existing) = row(db, &ref_k)? {
1664
- // The create that made this incarnation, repeated, is that create.
1665
- let id = ref_transaction_id("create", namespace, name, incarnation_of(&existing), None);
1666
- if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
1667
- return decode(existing);
1668
- }
1669
- return Err(RefError::AlreadyExists);
1670
- }
1671
- if row(db, &target_k)?.is_none() {
1672
- return Err(RefError::UnknownTarget(target));
1673
- }
1674
- let incarnation = incarnation_in(incarnation_row.as_ref()) + 1;
1675
- let id = ref_transaction_id("create", namespace, name, incarnation, None);
1676
- let reference = StateRef {
1677
- namespace: namespace.to_string(),
1678
- name: name.to_string(),
1679
- target: target.clone(),
1680
- version: 1,
1681
- authority: authority.clone(),
1682
- updated_at_ms: current_time_ms(),
1683
- metadata: metadata.clone(),
1684
- };
1685
- let pins = [
1686
- unchanged(&ref_k, None),
1687
- unchanged(&inc_k, incarnation_row.as_ref()),
1688
- present(&target_k),
1689
- ];
1690
- let mutations = [
1691
- put(&ref_k, ref_row(&reference, incarnation)),
1692
- put(&inc_k, json!({ "incarnation": incarnation })),
1693
- advance_generation(&id),
1694
- ];
1695
- match commit(db, &id, Some(&payload), &pins, &mutations) {
1696
- Ok(()) => return Ok(reference),
1697
- Err(Refusal::Changed | Refusal::OtherRequest) => continue,
1698
- Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
1699
- }
1700
- }
1701
- Err(RefError::Storage(contended("ref create")))
1702
- }
1703
-
1704
- pub(super) fn compare_and_swap_ref(
1705
- db: &FeltDb,
1706
- namespace: &str,
1707
- name: &str,
1708
- expected_version: u64,
1709
- target: StateId,
1710
- authority: String,
1711
- metadata: Option<BTreeMap<String, Value>>,
1712
- ) -> Result<StateRef, RefError> {
1713
- let ref_k = ref_key(namespace, name);
1714
- let inc_k = incarnation_key(namespace, name);
1715
- let target_k = revision_key(&target);
1716
- let payload = payload_hash(&json!({
1717
- "target": target.as_hex(),
1718
- "authority": authority,
1719
- "metadata": metadata,
1720
- }));
1721
- for _ in 0..ATTEMPTS {
1722
- let incarnation_row = row(db, &inc_k)?;
1723
- let Some(ref_read) = row(db, &ref_k)? else {
1724
- return Err(RefError::NotFound);
1725
- };
1726
- let incarnation = incarnation_of(&ref_read);
1727
- let current = decode(ref_read.clone())?;
1728
- let id = ref_transaction_id("cas", namespace, name, incarnation, Some(expected_version));
1729
- if current.version != expected_version {
1730
- // Either this exact request already committed (a retry), or
1731
- // another one did: the payload bound to the id says which.
1732
- if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
1733
- if current.version == expected_version + 1 && current.target == target {
1734
- return Ok(current);
1735
- }
1736
- // Committed, and the ref has moved on since. Describe the
1737
- // transition this request made; `updated_at_ms` is now.
1738
- return Ok(StateRef {
1739
- version: expected_version + 1,
1740
- target,
1741
- authority,
1742
- updated_at_ms: current_time_ms(),
1743
- metadata: metadata.unwrap_or(current.metadata),
1744
- ..current
1745
- });
1746
- }
1747
- return Err(RefError::VersionConflict {
1748
- expected: expected_version,
1749
- current: current.version,
1750
- });
1751
- }
1752
- if row(db, &target_k)?.is_none() {
1753
- return Err(RefError::UnknownTarget(target));
1754
- }
1755
- let next = StateRef {
1756
- version: expected_version + 1,
1757
- target: target.clone(),
1758
- authority: authority.clone(),
1759
- updated_at_ms: current_time_ms(),
1760
- metadata: metadata.clone().unwrap_or_else(|| current.metadata.clone()),
1761
- ..current
1762
- };
1763
- let pins = [
1764
- unchanged(&ref_k, Some(&ref_read)),
1765
- unchanged(&inc_k, incarnation_row.as_ref()),
1766
- present(&target_k),
1767
- ];
1768
- let mutations = [put(&ref_k, ref_row(&next, incarnation)), advance_generation(&id)];
1769
- match commit(db, &id, Some(&payload), &pins, &mutations) {
1770
- Ok(()) => return Ok(next),
1771
- Err(Refusal::Changed | Refusal::OtherRequest) => continue,
1772
- Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
1773
- }
1774
- }
1775
- Err(RefError::Storage(contended("ref compare-and-swap")))
1776
- }
1777
-
1778
- pub(super) fn delete_ref(
1779
- db: &FeltDb,
1780
- namespace: &str,
1781
- name: &str,
1782
- expected_version: Option<u64>,
1783
- ) -> Result<(), RefError> {
1784
- let ref_k = ref_key(namespace, name);
1785
- let inc_k = incarnation_key(namespace, name);
1786
- let payload = payload_hash(&json!({ "expected_version": expected_version }));
1787
- for _ in 0..ATTEMPTS {
1788
- let incarnation_row = row(db, &inc_k)?;
1789
- let Some(ref_read) = row(db, &ref_k)? else {
1790
- // A repeat of a delete that already removed this incarnation.
1791
- let id = ref_transaction_id(
1792
- "delete",
1793
- namespace,
1794
- name,
1795
- incarnation_in(incarnation_row.as_ref()),
1796
- None,
1797
- );
1798
- if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
1799
- return Ok(());
1800
- }
1801
- return Err(RefError::NotFound);
1802
- };
1803
- let incarnation = incarnation_of(&ref_read);
1804
- let current = decode(ref_read.clone())?;
1805
- if let Some(expected) = expected_version {
1806
- if current.version != expected {
1807
- return Err(RefError::VersionConflict {
1808
- expected,
1809
- current: current.version,
1810
- });
1811
- }
1812
- }
1813
- let id = ref_transaction_id("delete", namespace, name, incarnation, None);
1814
- let pins = [
1815
- unchanged(&ref_k, Some(&ref_read)),
1816
- unchanged(&inc_k, incarnation_row.as_ref()),
1817
- ];
1818
- let mutations = [
1819
- remove(&ref_k),
1820
- put(&inc_k, json!({ "incarnation": incarnation })),
1821
- advance_generation(&id),
1822
- ];
1823
- match commit(db, &id, Some(&payload), &pins, &mutations) {
1824
- Ok(()) => return Ok(()),
1825
- Err(Refusal::Changed | Refusal::OtherRequest) => continue,
1826
- Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
1827
- }
1828
- }
1829
- Err(RefError::Storage(contended("ref delete")))
1830
- }
1831
-
1832
- pub(super) fn commit_revision(
1833
- db: &FeltDb,
1834
- revision: StateRevision,
1835
- ) -> Result<StateRevision, RevisionError> {
1836
- let fail = |error: &dyn std::fmt::Display| RevisionError::Storage(error.to_string());
1837
- let object = StateObject::new(revision.content.clone());
1838
- let rev_k = revision_key(&revision.id);
1839
- let obj_k = object_key(&object.id);
1840
- let parent_k = revision.parent_id.as_ref().map(revision_key);
1841
- for _ in 0..ATTEMPTS {
1842
- if let Some(existing) = db.get::<StateRevision>(&rev_k).map_err(|e| fail(&e))? {
1843
- let same = existing.resource == revision.resource
1844
- && existing.content_id == revision.content_id
1845
- && existing.parent_id == revision.parent_id
1846
- && existing.sequence == revision.sequence;
1847
- return if same {
1848
- Ok(existing)
1849
- } else {
1850
- Err(RevisionError::AlreadyCommitted(revision.id.clone()))
1851
- };
1852
- }
1853
- let mut pins = vec![unchanged(&rev_k, None)];
1854
- if let (Some(parent), Some(parent_k)) = (&revision.parent_id, &parent_k) {
1855
- if db.get::<Value>(parent_k).map_err(|e| fail(&e))?.is_none() {
1856
- return Err(RevisionError::UnknownParent(parent.clone()));
1857
- }
1858
- pins.push(present(parent_k));
1859
- }
1860
- let id = format!("state-revision:{}:{}", revision.id.as_hex(), unique_suffix());
1861
- let mut mutations = Vec::with_capacity(3);
1862
- match db.get::<StateObject>(&obj_k).map_err(|e| fail(&e))? {
1863
- Some(existing) if !existing.verify_integrity() => {
1864
- return Err(RevisionError::Storage(
1865
- "Stored state object failed integrity verification".to_string(),
1866
- ));
1867
- }
1868
- Some(_) => pins.push(present(&obj_k)),
1869
- None => {
1870
- pins.push(unchanged(&obj_k, None));
1871
- mutations.push(put(&obj_k, serde_json::to_value(&object).map_err(|e| fail(&e))?));
1872
- }
1873
- }
1874
- mutations.push(put(&rev_k, serde_json::to_value(&revision).map_err(|e| fail(&e))?));
1875
- mutations.push(advance_generation(&id));
1876
- match commit(db, &id, None, &pins, &mutations) {
1877
- Ok(()) => return Ok(revision),
1878
- Err(Refusal::Changed | Refusal::OtherRequest) => continue,
1879
- Err(Refusal::Failed(message)) => return Err(RevisionError::Storage(message)),
1880
- }
1881
- }
1882
- Err(RevisionError::Storage(contended("revision commit")))
1883
- }
1884
-
1885
- pub(super) fn collect(
1886
- store: &StateStore,
1887
- db: &FeltDb,
1888
- now_ms: u64,
1889
- ) -> Result<GarbageCollectionReport, GarbageCollectionError> {
1890
- let fail = |error: &dyn std::fmt::Display| GarbageCollectionError::Storage(error.to_string());
1891
- for _ in 0..ATTEMPTS {
1892
- // The generation first: everything planned below was read after it,
1893
- // so a write that lands after this read moves it.
1894
- let generation = db.get::<Value>(GENERATION_KEY).map_err(|e| fail(&e))?;
1895
- let report = store.plan_collection(now_ms, &db.revision_head_ids());
1896
- if report.collected_revisions.is_empty() && report.collected_objects.is_empty() {
1897
- return Ok(report);
1898
- }
1899
- let pins = [unchanged(GENERATION_KEY, generation.as_ref())];
1900
- let mutations: Vec<AtomicMutation> = report
1901
- .collected_revisions
1902
- .iter()
1903
- .map(|id| remove(&revision_key(id)))
1904
- .chain(report.collected_objects.iter().map(|id| remove(&object_key(id))))
1905
- .collect();
1906
- let id = format!("state-gc:{}", unique_suffix());
1907
- match commit(db, &id, None, &pins, &mutations) {
1908
- Ok(()) => return Ok(report),
1909
- Err(Refusal::Changed | Refusal::OtherRequest) => continue,
1910
- Err(Refusal::Failed(message)) => return Err(GarbageCollectionError::Storage(message)),
1911
- }
1912
- }
1913
- Err(GarbageCollectionError::Storage(contended("garbage collection")))
1914
- }
1915
- }
1916
-
1917
- fn collect_referenced_objects(revision: &StateRevision) -> BTreeSet<StateId> {
1918
- let mut ids = BTreeSet::from([revision.content_id.clone()]);
1919
- if let Some(values) = revision
1920
- .metadata
1921
- .get("object_refs")
1922
- .and_then(Value::as_array)
1923
- {
1924
- for value in values {
1925
- if let Some(hex) = value.as_str().filter(|value| looks_like_state_id(value)) {
1926
- ids.insert(StateId::from_hex(hex.to_string()));
1927
- }
1928
- }
1929
- }
1930
- ids
1931
- }
1932
-
1933
- /// Where a resource's retention watermark lives.
1934
- fn retention_key(resource: &str) -> String {
1935
- format!("_retention:{resource}")
1936
- }
1937
-
1938
- /// How much of a resource's history is kept.
1939
- ///
1940
- /// Count-based only. Age-based retention is deliberately not implemented:
1941
- /// nothing in the system needs it yet, and inventing a second policy dimension
1942
- /// before the first has been measured would be guesswork.
1943
- #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1944
- pub struct RetentionPolicy {
1945
- /// Newest revisions to keep, or `None` to keep every revision.
1946
- ///
1947
- /// Clamped to at least 1: **a resource's current revision always survives.**
1948
- pub keep_last: Option<usize>,
1949
- }
1950
-
1951
- impl RetentionPolicy {
1952
- /// Keep everything. The default, because silently discarding history would
1953
- /// be a worse surprise than unbounded growth.
1954
- pub const fn unbounded() -> Self {
1955
- RetentionPolicy { keep_last: None }
1956
- }
1957
-
1958
- /// Keep the newest `count` revisions of a resource.
1959
- pub fn keep_last(count: usize) -> Self {
1960
- RetentionPolicy {
1961
- keep_last: Some(count.max(1)),
1962
- }
1963
- }
1964
- }
1965
-
1966
- impl Default for RetentionPolicy {
1967
- fn default() -> Self {
1968
- Self::unbounded()
1969
- }
1970
- }
1971
-
1972
- /// A resource's retention state: its policy, and how far its history has been
1973
- /// expired.
1974
- #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1975
- struct RetentionState {
1976
- policy: RetentionPolicy,
1977
- /// The lowest sequence still retained. Everything below it was expired on
1978
- /// purpose, and is distinguishable from a parent that never existed.
1979
- horizon: u64,
1980
- }
1981
-
1982
- /// Decide what a retention policy expires, given a resource's history.
1983
- ///
1984
- /// Pure, and the single definition of what retention *means*, so the explicit
1985
- /// store API and the automatic mint at the write boundary cannot drift apart.
1986
- ///
1987
- /// `history` is `(id, sequence)` oldest first. Returns the revisions to expire
1988
- /// and the new horizon — the lowest sequence still retained.
1989
- ///
1990
- /// Deletion is from the oldest end only, so what survives is always a suffix of
1991
- /// the resource's timeline and **the current revision always survives**.
1992
- pub fn revisions_to_expire(
1993
- history: &[(StateId, u64)],
1994
- keep_last: Option<usize>,
1995
- ) -> (Vec<StateId>, Option<u64>) {
1996
- let Some(keep) = keep_last else {
1997
- return (Vec::new(), None);
1998
- };
1999
- let keep = keep.max(1);
2000
- if history.len() <= keep {
2001
- return (Vec::new(), None);
2002
- }
2003
- let split = history.len() - keep;
2004
- let expire = history[..split].iter().map(|(id, _)| id.clone()).collect();
2005
- let horizon = history[split].1;
2006
- (expire, Some(horizon))
2007
- }
2008
-
2009
- /// Why a revision could not be minted.
2010
- #[derive(Clone, Debug, PartialEq, Eq)]
2011
- pub enum RevisionError {
2012
- /// Invariant 2: the named parent is not a revision this store holds.
2013
- UnknownParent(StateId),
2014
- /// Invariant 1: that identity is already committed, and commits are final.
2015
- ///
2016
- /// Under the resource-scoped model this should be unreachable through
2017
- /// `mint`, because the sequence is allocated from the resource's own head.
2018
- /// It is enforced anyway: immutability is the invariant, not a consequence.
2019
- AlreadyCommitted(StateId),
2020
- /// A parent belonging to a different resource.
2021
- ForeignParent { expected: String, found: String },
2022
- /// The resource already has a first revision; a resource has one beginning.
2023
- ResourceAlreadyBegun { resource: String, head: StateId },
2024
- /// The revision's own identity does not match what it is computed from.
2025
- IntegrityFailed,
2026
- /// The backing store could not be read or written.
2027
- Storage(String),
2028
- }
2029
-
2030
- impl std::fmt::Display for RevisionError {
2031
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2032
- match self {
2033
- RevisionError::UnknownParent(id) => {
2034
- write!(f, "parent {id} is not a revision this store holds")
2035
- }
2036
- RevisionError::AlreadyCommitted(id) => {
2037
- write!(
2038
- f,
2039
- "revision {id} is already committed and cannot be rewritten"
2040
- )
2041
- }
2042
- RevisionError::ForeignParent { expected, found } => {
2043
- write!(f, "parent belongs to resource {found}, not {expected}")
2044
- }
2045
- RevisionError::ResourceAlreadyBegun { resource, head } => {
2046
- write!(f, "resource {resource} already begins at revision {head}")
2047
- }
2048
- RevisionError::IntegrityFailed => write!(f, "state integrity verification failed"),
2049
- RevisionError::Storage(message) => write!(f, "{message}"),
2050
- }
2051
- }
2052
- }
2053
-
2054
- impl std::error::Error for RevisionError {}
2055
-
2056
- /// What following a revision's parent link found.
2057
- ///
2058
- /// The three outcomes are deliberately distinct. An **expired** parent is a
2059
- /// retention decision the system made on purpose; an **unknown** one is a bug.
2060
- /// Collapsing them would recreate the dangling-parent defect at the other end
2061
- /// of the system, where retention rather than minting produced it.
2062
- #[derive(Clone, Debug)]
2063
- pub enum ParentLookup {
2064
- /// This revision began its resource's history.
2065
- Root,
2066
- /// The parent, still retained.
2067
- Revision(Box<StateRevision>),
2068
- /// The parent was expired by retention. Its identity is still named by the
2069
- /// child, and the child's ancestry is still true — it is simply no longer
2070
- /// stored.
2071
- Expired(StateId),
2072
- /// The parent is named but absent and *not* below the retention horizon.
2073
- /// The model does not permit minting one; finding one means damage.
2074
- Missing(StateId),
2075
- }
2076
-
2077
- impl ParentLookup {
2078
- /// The identity this lookup names, if it names one.
2079
- pub fn id(&self) -> Option<&StateId> {
2080
- match self {
2081
- ParentLookup::Root => None,
2082
- ParentLookup::Revision(revision) => Some(&revision.id),
2083
- ParentLookup::Expired(id) | ParentLookup::Missing(id) => Some(id),
2084
- }
2085
- }
2086
- }
2087
-
2088
- /// Equality is over the outcome and the identity named, not the whole revision.
2089
- impl PartialEq for ParentLookup {
2090
- fn eq(&self, other: &Self) -> bool {
2091
- std::mem::discriminant(self) == std::mem::discriminant(other) && self.id() == other.id()
2092
- }
2093
- }
2094
-
2095
- impl StateStore {
2096
- /// A volatile (in-memory) store, for testing state semantics in isolation.
2097
- ///
2098
- /// Revisions are lost when the store is dropped. Production must use
2099
- /// [`with_feltdb`](Self::with_feltdb).
2100
- pub fn new_volatile() -> Self {
2101
- StateStore {
2102
- backing: Backing::Volatile(Arc::new(Mutex::new(VolatileState::default()))),
2103
- mutation_lock: Arc::new(Mutex::new(())),
2104
- }
2105
- }
2106
-
2107
- /// A store backed by FeltDB's canonical persistence.
2108
- ///
2109
- /// This performs **no recovery**. Revisions are records under
2110
- /// `state:revision:{hex}`; they are already durable, so there is no index to
2111
- /// rebuild and nothing to validate up front. A revision's integrity is
2112
- /// checked when it is written, and can be rechecked by whoever reads it.
2113
- ///
2114
- /// The `Result` is retained because callers depend on it; today it is always
2115
- /// `Ok`.
2116
- pub fn with_feltdb(feltdb: Arc<crate::FeltDb>) -> Result<Self, String> {
2117
- Ok(StateStore {
2118
- backing: Backing::Durable(feltdb),
2119
- mutation_lock: Arc::new(Mutex::new(())),
2120
- })
2121
- }
2122
-
2123
- // -- minting ------------------------------------------------------------
2124
-
2125
- /// Begin a resource's history.
2126
- ///
2127
- /// Refused if the resource already has one. A resource has a single
2128
- /// beginning; without that, `history_of` would return a forest rather than
2129
- /// a chain and "the previous state of this resource" would be ambiguous
2130
- /// again. To extend an existing history, use [`commit`](Self::commit) or
2131
- /// [`mint`](Self::mint).
2132
- pub fn create(
2133
- &self,
2134
- resource: &str,
2135
- content: String,
2136
- authority: String,
2137
- ) -> Result<StateRevision, RevisionError> {
2138
- if let Some(head) = self.head_of(resource) {
2139
- return Err(RevisionError::ResourceAlreadyBegun {
2140
- resource: resource.to_string(),
2141
- head: head.id,
2142
- });
2143
- }
2144
- self.mint(resource, content, None, authority)
2145
- }
2146
-
2147
- /// Commit a revision descending from `parent`, on the parent's resource.
2148
- pub fn commit(
2149
- &self,
2150
- content: String,
2151
- parent: &StateRevision,
2152
- authority: String,
2153
- ) -> Result<StateRevision, RevisionError> {
2154
- let resource = parent.resource.clone();
2155
- self.mint(&resource, content, Some(parent), authority)
2156
- }
2157
-
2158
- /// Mint a revision of `resource`, optionally descending from `parent`.
2159
- ///
2160
- /// Enforces both invariants at the only point where they can be enforced:
2161
- ///
2162
- /// 1. A parent must resolve to a revision this store holds, and must belong
2163
- /// to the same resource. Parent existence is part of what makes a
2164
- /// revision valid, not a lookup applied afterwards.
2165
- /// 2. A committed revision is never rewritten. The sequence is allocated
2166
- /// from the resource's own head, so a state reached twice is two
2167
- /// revisions with two identities.
2168
- ///
2169
- /// Retention is applied to the resource afterwards, so a configured policy
2170
- /// bounds history at the moment history is created rather than at some
2171
- /// later maintenance pass that may never run.
2172
- pub fn mint(
2173
- &self,
2174
- resource: &str,
2175
- content: String,
2176
- parent: Option<&StateRevision>,
2177
- authority: String,
2178
- ) -> Result<StateRevision, RevisionError> {
2179
- if let Some(parent) = parent {
2180
- if parent.resource != resource {
2181
- return Err(RevisionError::ForeignParent {
2182
- expected: resource.to_string(),
2183
- found: parent.resource.clone(),
2184
- });
2185
- }
2186
- if !self.exists(&parent.id) {
2187
- return Err(RevisionError::UnknownParent(parent.id.clone()));
2188
- }
2189
- }
2190
-
2191
- let sequence = match parent {
2192
- Some(parent) => parent.sequence + 1,
2193
- None => 0,
2194
- };
2195
- let revision = StateRevision::at(
2196
- resource.to_string(),
2197
- content,
2198
- parent.map(|parent| parent.id.clone()),
2199
- sequence,
2200
- authority,
2201
- );
2202
- let committed = self.commit_revision(revision)?;
2203
- self.apply_retention(resource)?;
2204
- Ok(committed)
2205
- }
2206
-
2207
- /// Store a revision, after checking its integrity and that it is new.
2208
- fn commit_revision(&self, revision: StateRevision) -> Result<StateRevision, RevisionError> {
2209
- if !revision.verify_integrity() {
2210
- return Err(RevisionError::IntegrityFailed);
2211
- }
2212
- // Re-committing an identical historical fact is a no-op, not a rewrite:
2213
- // the identity is a function of the resource, the content, the parent
2214
- // and the sequence, so an existing revision with this identity records
2215
- // exactly what is being committed. Returning it keeps immutability
2216
- // without making a harmless repeat an error.
2217
- if let Some(existing) = self.get(&revision.id) {
2218
- let same = existing.resource == revision.resource
2219
- && existing.content_id == revision.content_id
2220
- && existing.parent_id == revision.parent_id
2221
- && existing.sequence == revision.sequence;
2222
- return if same {
2223
- Ok(existing)
2224
- } else {
2225
- Err(RevisionError::AlreadyCommitted(revision.id.clone()))
2226
- };
2227
- }
2228
- match &self.backing {
2229
- Backing::Volatile(state) => {
2230
- let mut state = state.lock().unwrap();
2231
- state
2232
- .objects
2233
- .entry(revision.content_id.clone())
2234
- .or_insert_with(|| StateObject::new(revision.content.clone()));
2235
- state.revisions.insert(revision.id.clone(), revision.clone());
2236
- }
2237
- Backing::Durable(feltdb) => {
2238
- return durable::commit_revision(feltdb, revision);
2239
- }
2240
- }
2241
- Ok(revision)
2242
- }
2243
-
2244
- // -- reading ------------------------------------------------------------
2245
-
2246
- /// Retrieve a revision by identity. This is the authoritative read path.
2247
- pub fn get(&self, id: &StateId) -> Option<StateRevision> {
2248
- match &self.backing {
2249
- Backing::Volatile(state) => state.lock().unwrap().revisions.get(id).cloned(),
2250
- Backing::Durable(feltdb) => feltdb
2251
- .get::<StateRevision>(&revision_key(id))
2252
- .ok()
2253
- .flatten(),
2254
- }
2255
- }
2256
-
2257
- /// Whether a revision is present.
2258
- pub fn exists(&self, id: &StateId) -> bool {
2259
- self.get(id).is_some()
2260
- }
2261
-
2262
- /// Metadata for a revision.
2263
- pub fn metadata(&self, id: &StateId) -> Option<BTreeMap<String, Value>> {
2264
- self.get(id).map(|revision| revision.metadata)
2265
- }
2266
-
2267
- /// The parent of a revision, and — when it is not there — why.
2268
- pub fn parent_of(&self, id: &StateId) -> Option<ParentLookup> {
2269
- let revision = self.get(id)?;
2270
- let Some(parent_id) = revision.parent_id else {
2271
- return Some(ParentLookup::Root);
2272
- };
2273
- if let Some(parent) = self.get(&parent_id) {
2274
- return Some(ParentLookup::Revision(Box::new(parent)));
2275
- }
2276
- let horizon = self.retention_state(&revision.resource).horizon;
2277
- Some(if revision.sequence <= horizon {
2278
- ParentLookup::Expired(parent_id)
2279
- } else {
2280
- ParentLookup::Missing(parent_id)
2281
- })
2282
- }
2283
-
2284
- /// The parent of a revision, if it is still retained.
2285
- ///
2286
- /// Repeating this walks the ancestry. It is a traversal, not a primitive.
2287
- /// Use [`parent_of`](Self::parent_of) to tell a resource's beginning from a
2288
- /// horizon.
2289
- pub fn parent(&self, id: &StateId) -> Option<StateRevision> {
2290
- match self.parent_of(id)? {
2291
- ParentLookup::Revision(parent) => Some(*parent),
2292
- _ => None,
2293
- }
2294
- }
2295
-
2296
- /// Every retained revision of one resource, oldest first.
2297
- pub fn history_of(&self, resource: &str) -> Vec<StateRevision> {
2298
- let mut history: Vec<StateRevision> = self
2299
- .all_revisions()
2300
- .into_iter()
2301
- .filter(|revision| revision.resource == resource)
2302
- .collect();
2303
- history.sort_by_key(|revision| revision.sequence);
2304
- history
2305
- }
2306
-
2307
- /// A resource's newest retained revision.
2308
- ///
2309
- /// Automatic minting produces a chain, so this is unambiguous for anything
2310
- /// the write path created. Explicit `commit` may fork a resource — which is
2311
- /// exactly what divergence and three-way reconciliation need — and where it
2312
- /// has, this is the newest by sequence, ties broken by identity so the
2313
- /// answer is deterministic rather than incidental.
2314
- pub fn head_of(&self, resource: &str) -> Option<StateRevision> {
2315
- self.history_of(resource).into_iter().max_by(|one, two| {
2316
- one.sequence
2317
- .cmp(&two.sequence)
2318
- .then_with(|| one.id.as_hex().cmp(two.id.as_hex()))
2319
- })
2320
- }
2321
-
2322
- /// Every resource this store holds history for.
2323
- pub fn resources(&self) -> Vec<String> {
2324
- let mut names: Vec<String> = self
2325
- .all_revisions()
2326
- .into_iter()
2327
- .map(|revision| revision.resource)
2328
- .collect();
2329
- names.sort();
2330
- names.dedup();
2331
- names
2332
- }
2333
-
2334
- /// Create a mutable reference in a namespace.
2335
- ///
2336
- /// On the durable backing this is one fenced FeltDB transaction (see
2337
- /// [`durable`]): the ref is created at version 1 only if, at the moment of
2338
- /// commit, it is still absent and its target revision is still present.
2339
- /// Repeating an identical create that already succeeded returns the ref it
2340
- /// created; a different create of an existing ref is `AlreadyExists`.
2341
- pub fn create_ref(
2342
- &self,
2343
- namespace: &str,
2344
- name: &str,
2345
- target: StateId,
2346
- authority: String,
2347
- metadata: BTreeMap<String, Value>,
2348
- ) -> Result<StateRef, RefError> {
2349
- if !valid_ref_component(namespace) || !valid_ref_component(name) {
2350
- return Err(RefError::InvalidIdentifier);
2351
- }
2352
- match &self.backing {
2353
- Backing::Durable(feltdb) => {
2354
- durable::create_ref(feltdb, namespace, name, target, authority, metadata)
2355
- }
2356
- Backing::Volatile(_) => {
2357
- let _guard = self.mutation_lock.lock().unwrap();
2358
- if !self.exists(&target) {
2359
- return Err(RefError::UnknownTarget(target));
2360
- }
2361
- if self.get_ref(namespace, name).is_some() {
2362
- return Err(RefError::AlreadyExists);
2363
- }
2364
- let reference = StateRef {
2365
- namespace: namespace.to_string(),
2366
- name: name.to_string(),
2367
- target,
2368
- version: 1,
2369
- authority,
2370
- updated_at_ms: current_time_ms(),
2371
- metadata,
2372
- };
2373
- self.write_volatile_ref(&reference);
2374
- Ok(reference)
2375
- }
2376
- }
2377
- }
2378
-
2379
- /// Read a reference.
2380
- pub fn get_ref(&self, namespace: &str, name: &str) -> Option<StateRef> {
2381
- if !valid_ref_component(namespace) || !valid_ref_component(name) {
2382
- return None;
2383
- }
2384
- match &self.backing {
2385
- Backing::Volatile(state) => state
2386
- .lock()
2387
- .unwrap()
2388
- .refs
2389
- .get(&(namespace.to_string(), name.to_string()))
2390
- .cloned(),
2391
- Backing::Durable(feltdb) => feltdb
2392
- .get::<StateRef>(&ref_key(namespace, name))
2393
- .ok()
2394
- .flatten(),
2395
- }
2396
- }
2397
-
2398
- /// Move a reference with stale-version rejection.
2399
- pub fn move_ref(
2400
- &self,
2401
- namespace: &str,
2402
- name: &str,
2403
- target: StateId,
2404
- authority: String,
2405
- ) -> Result<StateRef, RefError> {
2406
- let current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
2407
- self.compare_and_swap_ref(namespace, name, current.version, target, authority, None)
2408
- }
2409
-
2410
- /// Compare-and-swap move for a reference.
2411
- ///
2412
- /// On the durable backing this is one fenced FeltDB transaction: it commits
2413
- /// only if, at the moment of commit, the ref is still at
2414
- /// `expected_version` and the target revision is still present, whichever
2415
- /// `StateStore` or `FeltDb` handle the competing writes came through.
2416
- ///
2417
- /// The request `(ref incarnation, expected_version, target, authority,
2418
- /// metadata)` is idempotent: repeating one that already committed — after a
2419
- /// lost response, a restart or a compaction — returns `Ok` and does not
2420
- /// advance the ref again. Any other request for the same expected version
2421
- /// is a `VersionConflict`.
2422
- pub fn compare_and_swap_ref(
2423
- &self,
2424
- namespace: &str,
2425
- name: &str,
2426
- expected_version: u64,
2427
- target: StateId,
2428
- authority: String,
2429
- metadata: Option<BTreeMap<String, Value>>,
2430
- ) -> Result<StateRef, RefError> {
2431
- if !valid_ref_component(namespace) || !valid_ref_component(name) {
2432
- return Err(RefError::InvalidIdentifier);
2433
- }
2434
- match &self.backing {
2435
- Backing::Durable(feltdb) => durable::compare_and_swap_ref(
2436
- feltdb,
2437
- namespace,
2438
- name,
2439
- expected_version,
2440
- target,
2441
- authority,
2442
- metadata,
2443
- ),
2444
- Backing::Volatile(_) => {
2445
- let _guard = self.mutation_lock.lock().unwrap();
2446
- let mut current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
2447
- if current.version != expected_version {
2448
- return Err(RefError::VersionConflict {
2449
- expected: expected_version,
2450
- current: current.version,
2451
- });
2452
- }
2453
- if !self.exists(&target) {
2454
- return Err(RefError::UnknownTarget(target));
2455
- }
2456
- current.target = target;
2457
- current.version += 1;
2458
- current.authority = authority;
2459
- current.updated_at_ms = current_time_ms();
2460
- if let Some(metadata) = metadata {
2461
- current.metadata = metadata;
2462
- }
2463
- self.write_volatile_ref(&current);
2464
- Ok(current)
2465
- }
2466
- }
2467
- }
2468
-
2469
- /// Delete a reference, optionally enforcing the current version.
2470
- ///
2471
- /// On the durable backing this is one fenced FeltDB transaction, and a
2472
- /// repeat of an identical delete that already committed returns `Ok`.
2473
- pub fn delete_ref(
2474
- &self,
2475
- namespace: &str,
2476
- name: &str,
2477
- expected_version: Option<u64>,
2478
- ) -> Result<(), RefError> {
2479
- if !valid_ref_component(namespace) || !valid_ref_component(name) {
2480
- return Err(RefError::InvalidIdentifier);
2481
- }
2482
- match &self.backing {
2483
- Backing::Durable(feltdb) => {
2484
- durable::delete_ref(feltdb, namespace, name, expected_version)
2485
- }
2486
- Backing::Volatile(state) => {
2487
- let _guard = self.mutation_lock.lock().unwrap();
2488
- let current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
2489
- if let Some(expected) = expected_version {
2490
- if current.version != expected {
2491
- return Err(RefError::VersionConflict {
2492
- expected,
2493
- current: current.version,
2494
- });
2495
- }
2496
- }
2497
- state
2498
- .lock()
2499
- .unwrap()
2500
- .refs
2501
- .remove(&(namespace.to_string(), name.to_string()));
2502
- Ok(())
2503
- }
2504
- }
2505
- }
2506
-
2507
- /// Enumerate refs, optionally scoped to one namespace.
2508
- pub fn list_refs(&self, namespace: Option<&str>) -> Vec<StateRef> {
2509
- let mut refs: Vec<StateRef> = match &self.backing {
2510
- Backing::Volatile(state) => state.lock().unwrap().refs.values().cloned().collect(),
2511
- Backing::Durable(feltdb) => feltdb
2512
- .list_collection("state")
2513
- .unwrap_or_default()
2514
- .into_iter()
2515
- .filter(|row| row.key.starts_with("state:ref:") && !row.deleted)
2516
- .filter_map(|row| serde_json::from_value::<StateRef>(row.value).ok())
2517
- .collect(),
2518
- };
2519
- if let Some(namespace) = namespace {
2520
- refs.retain(|reference| reference.namespace == namespace);
2521
- }
2522
- refs.sort_by(|one, two| {
2523
- one.namespace
2524
- .cmp(&two.namespace)
2525
- .then_with(|| one.name.cmp(&two.name))
2526
- });
2527
- refs
2528
- }
2529
-
2530
- /// Materialize a full revision value or a deterministic projection of it.
2531
- pub fn materialize(
2532
- &self,
2533
- revision_id: &StateId,
2534
- projection: &MaterializationProjection,
2535
- ) -> Result<Value, MaterializationError> {
2536
- let revision = self
2537
- .get(revision_id)
2538
- .ok_or_else(|| MaterializationError::RevisionNotFound(revision_id.clone()))?;
2539
- let source: Value = serde_json::from_str(&revision.content)
2540
- .map_err(|error| MaterializationError::ContentUnparseable(error.to_string()))?;
2541
- if projection.is_full() {
2542
- return Ok(source);
2543
- }
2544
- let mut paths = projection.paths.clone();
2545
- paths.sort();
2546
- paths.dedup();
2547
- for pair in paths.windows(2) {
2548
- if paths_overlap(&pair[0], &pair[1]) {
2549
- return Err(MaterializationError::NonDeterministicProjection);
2550
- }
2551
- }
2552
- let mut projected = Value::Object(serde_json::Map::new());
2553
- for path in paths {
2554
- let value = resolve_path(&source, &path)
2555
- .cloned()
2556
- .ok_or_else(|| MaterializationError::MissingPath(path.clone()))?;
2557
- assign_projection(&mut projected, &path, value)?;
2558
- }
2559
- Ok(projected)
2560
- }
2561
-
2562
- /// Collect revisions and objects that no root can reach.
2563
- ///
2564
- /// **Roots** are: every ref target; every resource's latest revision (each
2565
- /// revision at the resource's highest sequence, and the head the record
2566
- /// write path will mint its next revision from); and every revision
2567
- /// younger than 15 minutes (`GC_GRACE_MS`). **Reachable** is the roots and all their
2568
- /// ancestors by `parent_id`, because history, diff, rollback and replicated
2569
- /// reconstruction all walk that chain. Only a revision that is old, named
2570
- /// by no ref, not latest, and an ancestor of none of those is collected —
2571
- /// in practice, an abandoned fork. Retention (`keep_last`) is unaffected:
2572
- /// it is positional and runs at the mint, not here.
2573
- ///
2574
- /// On the durable backing every deletion commits in one FeltDB transaction
2575
- /// fenced on the state generation, which every ref change and every
2576
- /// committed revision advances. A concurrent change aborts the collection
2577
- /// and it is re-planned from fresh reads; see [`durable`].
2578
- pub fn collect_unreachable(&self) -> Result<GarbageCollectionReport, GarbageCollectionError> {
2579
- self.collect_unreachable_at(current_time_ms())
2580
- }
2581
-
2582
- /// [`collect_unreachable`](Self::collect_unreachable) with an explicit
2583
- /// clock, so the grace period can be tested without waiting for it.
2584
- pub(crate) fn collect_unreachable_at(
2585
- &self,
2586
- now_ms: u64,
2587
- ) -> Result<GarbageCollectionReport, GarbageCollectionError> {
2588
- match &self.backing {
2589
- Backing::Volatile(state) => {
2590
- let _guard = self.mutation_lock.lock().unwrap();
2591
- let report = self.plan_collection(now_ms, &[]);
2592
- let mut state = state.lock().unwrap();
2593
- for id in &report.collected_revisions {
2594
- state.revisions.remove(id);
2595
- }
2596
- for id in &report.collected_objects {
2597
- state.objects.remove(id);
2598
- }
2599
- Ok(report)
2600
- }
2601
- Backing::Durable(feltdb) => durable::collect(self, feltdb, now_ms),
2602
- }
2603
- }
2604
-
2605
- /// What a collection at `now_ms` would keep and delete, from one read of
2606
- /// refs and revisions. `extra_roots` are heads known outside `StateStore`.
2607
- fn plan_collection(&self, now_ms: u64, extra_roots: &[StateId]) -> GarbageCollectionReport {
2608
- let roots = self.list_refs(None);
2609
- let revisions = self.all_revisions();
2610
- let by_id: HashMap<StateId, &StateRevision> =
2611
- revisions.iter().map(|revision| (revision.id.clone(), revision)).collect();
2612
-
2613
- let mut latest: HashMap<&str, u64> = HashMap::new();
2614
- for revision in &revisions {
2615
- let entry = latest.entry(revision.resource.as_str()).or_insert(revision.sequence);
2616
- if revision.sequence > *entry {
2617
- *entry = revision.sequence;
2618
- }
2619
- }
2620
-
2621
- let mut pending: Vec<StateId> = roots.iter().map(|reference| reference.target.clone()).collect();
2622
- pending.extend(extra_roots.iter().cloned());
2623
- for revision in &revisions {
2624
- let is_latest = latest.get(revision.resource.as_str()) == Some(&revision.sequence);
2625
- let is_young = now_ms.saturating_sub(revision.timestamp_ms) < GC_GRACE_MS;
2626
- if is_latest || is_young {
2627
- pending.push(revision.id.clone());
2628
- }
2629
- }
2630
-
2631
- let mut reachable_revisions = BTreeSet::new();
2632
- let mut reachable_objects = BTreeSet::new();
2633
- while let Some(id) = pending.pop() {
2634
- let Some(revision) = by_id.get(&id) else {
2635
- // A root or parent that is not held (expired by retention, or
2636
- // never committed) ends the walk; it is not an error.
2637
- continue;
2638
- };
2639
- if !reachable_revisions.insert(id.clone()) {
2640
- continue;
2641
- }
2642
- if let Some(parent_id) = revision.parent_id.clone() {
2643
- pending.push(parent_id);
2644
- }
2645
- reachable_objects.extend(collect_referenced_objects(revision));
2646
- }
2647
-
2648
- let mut collected_revisions: Vec<StateId> = revisions
2649
- .iter()
2650
- .map(|revision| revision.id.clone())
2651
- .filter(|id| !reachable_revisions.contains(id))
2652
- .collect();
2653
- collected_revisions.sort();
2654
- let mut collected_objects: Vec<StateId> = self
2655
- .all_objects()
2656
- .into_iter()
2657
- .map(|object| object.id)
2658
- .filter(|id| !reachable_objects.contains(id))
2659
- .collect();
2660
- collected_objects.sort();
2661
-
2662
- GarbageCollectionReport {
2663
- roots,
2664
- reachable_revisions: reachable_revisions.into_iter().collect(),
2665
- reachable_objects: reachable_objects.into_iter().collect(),
2666
- collected_revisions,
2667
- collected_objects,
2668
- }
2669
- }
2670
-
2671
- fn write_volatile_ref(&self, reference: &StateRef) {
2672
- if let Backing::Volatile(state) = &self.backing {
2673
- state.lock().unwrap().refs.insert(
2674
- (reference.namespace.clone(), reference.name.clone()),
2675
- reference.clone(),
2676
- );
2677
- }
2678
- }
2679
-
2680
- fn all_revisions(&self) -> Vec<StateRevision> {
2681
- match &self.backing {
2682
- Backing::Volatile(state) => state.lock().unwrap().revisions.values().cloned().collect(),
2683
- Backing::Durable(feltdb) => feltdb
2684
- .list_collection("state")
2685
- .unwrap_or_default()
2686
- .into_iter()
2687
- .filter(|row| row.key.starts_with("state:revision:") && !row.deleted)
2688
- .filter_map(|row| serde_json::from_value::<StateRevision>(row.value).ok())
2689
- .collect(),
2690
- }
2691
- }
2692
-
2693
- fn all_objects(&self) -> Vec<StateObject> {
2694
- match &self.backing {
2695
- Backing::Volatile(state) => state.lock().unwrap().objects.values().cloned().collect(),
2696
- Backing::Durable(feltdb) => feltdb
2697
- .list_collection("state")
2698
- .unwrap_or_default()
2699
- .into_iter()
2700
- .filter(|row| row.key.starts_with("state:object:") && !row.deleted)
2701
- .filter_map(|row| serde_json::from_value::<StateObject>(row.value).ok())
2702
- .collect(),
2703
- }
2704
- }
2705
-
2706
- /// A digest of this store's **history**: the revision graph and retention
2707
- /// state, and nothing else.
2708
- ///
2709
- /// Deliberately independent of a digest over current records. Two databases
2710
- /// can hold identical current state and completely different histories —
2711
- /// one that recorded every step and one that only ever saw the last — and
2712
- /// any equivalence check that cannot tell those apart is not checking
2713
- /// history at all.
2714
- ///
2715
- /// Covers, for every revision: its resource, identity, parent, sequence,
2716
- /// content identity and the authority that committed it; and for every
2717
- /// resource: its retention policy and horizon. It excludes wall-clock
2718
- /// timestamps, which two authorities holding the same history legitimately
2719
- /// disagree about.
2720
- pub fn history_digest(&self) -> String {
2721
- let mut entries: Vec<String> = self
2722
- .all_revisions()
2723
- .into_iter()
2724
- .map(|revision| {
2725
- format!(
2726
- "{}|{}|{}|{}|{}|{}",
2727
- revision.resource,
2728
- revision.id.as_hex(),
2729
- revision
2730
- .parent_id
2731
- .as_ref()
2732
- .map(StateId::as_hex)
2733
- .unwrap_or("-"),
2734
- revision.sequence,
2735
- revision.content_id.as_hex(),
2736
- revision.authority,
2737
- )
2738
- })
2739
- .collect();
2740
- for resource in self.resources() {
2741
- let state = self.retention_state(&resource);
2742
- entries.push(format!(
2743
- "retention|{}|{:?}|{}",
2744
- resource, state.policy.keep_last, state.horizon
2745
- ));
2746
- }
2747
- entries.sort();
2748
-
2749
- let mut hasher = Sha256::new();
2750
- hasher.update(b"feltdb.history.v1");
2751
- for entry in entries {
2752
- hasher.update(entry.as_bytes());
2753
- hasher.update([0u8]);
2754
- }
2755
- format!("{:x}", hasher.finalize())
2756
- }
2757
-
2758
- // -- retention ----------------------------------------------------------
2759
-
2760
- /// The retention policy in force for a resource.
2761
- pub fn retention_policy(&self, resource: &str) -> RetentionPolicy {
2762
- self.retention_state(resource).policy
2763
- }
2764
-
2765
- /// The lowest sequence still retained for a resource.
2766
- ///
2767
- /// Revisions below it were expired deliberately. This is what separates an
2768
- /// expired parent from a missing one.
2769
- pub fn retention_horizon(&self, resource: &str) -> u64 {
2770
- self.retention_state(resource).horizon
2771
- }
2772
-
2773
- /// Set a resource's retention policy, and apply it immediately.
2774
- ///
2775
- /// Returns how many revisions the policy expired on application.
2776
- pub fn set_retention_policy(
2777
- &self,
2778
- resource: &str,
2779
- policy: RetentionPolicy,
2780
- ) -> Result<usize, RevisionError> {
2781
- let mut state = self.retention_state(resource);
2782
- state.policy = policy;
2783
- self.write_retention_state(resource, &state)?;
2784
- self.apply_retention(resource)
2785
- }
2786
-
2787
- /// Expire whatever the resource's policy no longer retains.
2788
- ///
2789
- /// Deletes from the **oldest** end only, so the surviving history is always
2790
- /// a suffix of the resource's timeline and the current revision always
2791
- /// survives. A surviving revision's `parent_id` is never rewritten — that
2792
- /// would violate immutability — so the horizon is recorded instead, and the
2793
- /// oldest survivor reports an `Expired` parent rather than a missing one.
2794
- pub fn apply_retention(&self, resource: &str) -> Result<usize, RevisionError> {
2795
- let mut state = self.retention_state(resource);
2796
- if state.policy.keep_last.is_none() {
2797
- return Ok(0);
2798
- }
2799
- let history: Vec<(StateId, u64)> = self
2800
- .history_of(resource)
2801
- .into_iter()
2802
- .map(|revision| (revision.id, revision.sequence))
2803
- .collect();
2804
- let (expire, horizon) = revisions_to_expire(&history, state.policy.keep_last);
2805
- if expire.is_empty() {
2806
- return Ok(0);
2807
- }
2808
- for id in &expire {
2809
- self.remove(id)?;
2810
- }
2811
- if let Some(horizon) = horizon {
2812
- state.horizon = horizon;
2813
- }
2814
- self.write_retention_state(resource, &state)?;
2815
- Ok(expire.len())
2816
- }
2817
-
2818
- fn remove(&self, id: &StateId) -> Result<(), RevisionError> {
2819
- match &self.backing {
2820
- Backing::Volatile(state) => {
2821
- state.lock().unwrap().revisions.remove(id);
2822
- }
2823
- Backing::Durable(feltdb) => {
2824
- feltdb.delete(&revision_key(id)).map_err(|error| {
2825
- RevisionError::Storage(format!("Failed to expire revision: {error}"))
2826
- })?;
2827
- }
2828
- }
2829
- Ok(())
2830
- }
2831
-
2832
- fn retention_state(&self, resource: &str) -> RetentionState {
2833
- match &self.backing {
2834
- Backing::Volatile(state) => state
2835
- .lock()
2836
- .unwrap()
2837
- .retention
2838
- .get(resource)
2839
- .cloned()
2840
- .unwrap_or_default(),
2841
- Backing::Durable(feltdb) => feltdb
2842
- .get::<RetentionState>(&retention_key(resource))
2843
- .ok()
2844
- .flatten()
2845
- .unwrap_or_default(),
2846
- }
2847
- }
2848
-
2849
- fn write_retention_state(
2850
- &self,
2851
- resource: &str,
2852
- state: &RetentionState,
2853
- ) -> Result<(), RevisionError> {
2854
- match &self.backing {
2855
- Backing::Volatile(volatile) => {
2856
- volatile
2857
- .lock()
2858
- .unwrap()
2859
- .retention
2860
- .insert(resource.to_string(), state.clone());
2861
- }
2862
- Backing::Durable(feltdb) => {
2863
- feltdb
2864
- .insert(&retention_key(resource), state.clone())
2865
- .map_err(|error| {
2866
- RevisionError::Storage(format!(
2867
- "Failed to persist retention state: {error}"
2868
- ))
2869
- })?;
2870
- }
2871
- }
2872
- Ok(())
2873
- }
2874
- }
2875
-
2876
- impl Clone for StateStore {
2877
- fn clone(&self) -> Self {
2878
- StateStore {
2879
- backing: self.backing.clone(),
2880
- mutation_lock: self.mutation_lock.clone(),
2881
- }
2882
- }
2883
- }
2884
-
2885
- impl Default for StateStore {
2886
- fn default() -> Self {
2887
- Self::new_volatile()
2888
- }
2889
- }
2890
-
2891
- // ============================================================================
2892
- // Tests
2893
- // ============================================================================
2894
-
2895
- #[cfg(test)]
2896
- mod tests {
2897
- use super::*;
2898
- use serde_json::json;
2899
-
2900
- // -- garbage collection eligibility, with an explicit clock -------------
2901
-
2902
- /// `A(0) → B(1) → C(2)` on one resource, plus a fork `F(1)` off `A`.
2903
- fn forked(store: &StateStore) -> (StateRevision, StateRevision, StateRevision, StateRevision) {
2904
- let a = store.create("doc:gc", json!({"n": "a"}).to_string(), "t".into()).unwrap();
2905
- let b = store.commit(json!({"n": "b"}).to_string(), &a, "t".into()).unwrap();
2906
- let c = store.commit(json!({"n": "c"}).to_string(), &b, "t".into()).unwrap();
2907
- let f = store.commit(json!({"n": "f"}).to_string(), &a, "t".into()).unwrap();
2908
- (a, b, c, f)
2909
- }
2910
-
2911
- #[test]
2912
- fn gc_grace_boundary_is_exclusive() {
2913
- let store = StateStore::new_volatile();
2914
- let (_, _, _, f) = forked(&store);
2915
- let inside = store.collect_unreachable_at(f.timestamp_ms + GC_GRACE_MS - 1).unwrap();
2916
- assert!(inside.collected_revisions.is_empty(), "younger than the grace: a root");
2917
- let outside = store.collect_unreachable_at(f.timestamp_ms + GC_GRACE_MS).unwrap();
2918
- assert_eq!(outside.collected_revisions, vec![f.id.clone()]);
2919
- assert!(!store.exists(&f.id));
2920
- }
2921
-
2922
- #[test]
2923
- fn gc_keeps_latest_revisions_refs_and_all_their_ancestors() {
2924
- for store in [StateStore::new_volatile(), durable_store().1] {
2925
- let (a, b, c, f) = forked(&store);
2926
- let g = store.commit(json!({"n": "g"}).to_string(), &f, "t".into()).unwrap();
2927
- // g ties c at the highest sequence (2): both are "latest".
2928
- let far = c.timestamp_ms + 365 * 24 * 60 * 60 * 1000;
2929
- let report = store.collect_unreachable_at(far).unwrap();
2930
- assert!(report.collected_revisions.is_empty(), "every revision is a root or an ancestor");
2931
-
2932
- // A later revision on the main line makes f and g a stale fork …
2933
- let d = store.commit(json!({"n": "d"}).to_string(), &c, "t".into()).unwrap();
2934
- // … but a ref on g keeps g and its ancestor f.
2935
- store
2936
- .create_ref("candidates", "g", g.id.clone(), "t".into(), BTreeMap::new())
2937
- .unwrap();
2938
- assert!(store.collect_unreachable_at(far).unwrap().collected_revisions.is_empty());
2939
-
2940
- store.delete_ref("candidates", "g", None).unwrap();
2941
- let mut collected = store.collect_unreachable_at(far).unwrap().collected_revisions;
2942
- collected.sort();
2943
- let mut expected = vec![f.id.clone(), g.id.clone()];
2944
- expected.sort();
2945
- assert_eq!(collected, expected, "only the unreferenced stale fork");
2946
- for kept in [&a, &b, &c, &d] {
2947
- assert!(store.exists(&kept.id));
2948
- }
2949
- // The fork's content objects went with it; the kept ones stayed.
2950
- let objects: BTreeSet<StateId> = store.all_objects().into_iter().map(|o| o.id).collect();
2951
- for kept in [&a, &b, &c, &d] {
2952
- assert!(objects.contains(&kept.content_id), "object of a kept revision");
2953
- }
2954
- for gone in [&f, &g] {
2955
- assert!(!objects.contains(&gone.content_id), "object of a collected revision");
2956
- }
2957
- }
2958
- }
2959
-
2960
- fn durable_store() -> (tempfile::TempDir, StateStore, Arc<crate::FeltDb>) {
2961
- let dir = tempfile::TempDir::new().unwrap();
2962
- let db = Arc::new(crate::FeltDb::open(dir.path().join("gc.log")).unwrap());
2963
- let store = StateStore::with_feltdb(db.clone()).unwrap();
2964
- (dir, store, db)
2965
- }
2966
-
2967
- /// The record write path mints from its own head, which an explicit
2968
- /// `StateStore::commit` fork can overtake by sequence. That head must stay
2969
- /// a root, or the next record write would mint a child of a deleted parent.
2970
- #[test]
2971
- fn gc_keeps_the_write_path_head_even_when_a_fork_is_longer() {
2972
- let (_dir, store, db) = durable_store();
2973
- db.insert("tasks:1", json!({"n": 0})).unwrap();
2974
- db.update("tasks:1", json!({"n": 1})).unwrap();
2975
- let resource = store.resources().into_iter().find(|r| r.contains("tasks")).unwrap();
2976
- let history = store.history_of(&resource);
2977
- assert_eq!(history.len(), 2, "the write path minted two revisions");
2978
- let (w0, w1) = (history[0].clone(), history[1].clone());
2979
-
2980
- // A longer explicit fork off w0: x(1) → x2(2) overtakes w1(1).
2981
- let x = store.commit(json!({"x": 1}).to_string(), &w0, "t".into()).unwrap();
2982
- let x2 = store.commit(json!({"x": 2}).to_string(), &x, "t".into()).unwrap();
2983
- assert_eq!(store.head_of(&resource).unwrap().id, x2.id);
2984
- assert!(db.revision_head_ids().contains(&w1.id), "the write path still mints from w1");
2985
-
2986
- let far = x2.timestamp_ms + 365 * 24 * 60 * 60 * 1000;
2987
- let report = store.collect_unreachable_at(far).unwrap();
2988
- assert!(!report.collected_revisions.contains(&w1.id), "w1 is the write path's head");
2989
- assert!(store.exists(&w1.id));
2990
-
2991
- db.update("tasks:1", json!({"n": 2})).unwrap();
2992
- let w2 = store
2993
- .history_of(&resource)
2994
- .into_iter()
2995
- .find(|r| r.parent_id.as_ref() == Some(&w1.id))
2996
- .expect("the next write minted a child of w1");
2997
- assert_eq!(store.parent_of(&w2.id), Some(ParentLookup::Revision(Box::new(w1))));
2998
- }
2999
-
3000
- /// The generation row, the incarnation rows and the ref rows the durable
3001
- /// path writes are invisible to the history and ref listings.
3002
- #[test]
3003
- fn internal_rows_do_not_appear_as_history_or_refs() {
3004
- let (_dir, store, _db) = durable_store();
3005
- let (a, _, _, _) = forked(&store);
3006
- store.create_ref("heads", "main", a.id.clone(), "t".into(), BTreeMap::new()).unwrap();
3007
- store.delete_ref("heads", "main", Some(1)).unwrap();
3008
- store.create_ref("heads", "main", a.id.clone(), "t".into(), BTreeMap::new()).unwrap();
3009
- assert_eq!(store.list_refs(None).len(), 1);
3010
- assert_eq!(store.resources(), vec!["doc:gc".to_string()]);
3011
- assert_eq!(store.history_of("doc:gc").len(), 4);
3012
- }
3013
-
3014
- #[test]
3015
- fn test_state_id_deterministic() {
3016
- let json = r#"{"name":"Alice","age":30}"#;
3017
- let id1 = StateId::compute(json);
3018
- let id2 = StateId::compute(json);
3019
- assert_eq!(id1, id2);
3020
- }
3021
-
3022
- #[test]
3023
- fn test_state_id_representation_sensitive() {
3024
- let id_int = StateId::compute("1");
3025
- let id_float = StateId::compute("1.0");
3026
- let id_string = StateId::compute(r#""1""#);
3027
- assert_ne!(id_int, id_float);
3028
- assert_ne!(id_int, id_string);
3029
- assert_ne!(id_float, id_string);
3030
- }
3031
-
3032
- #[test]
3033
- fn test_state_revision_initial() {
3034
- let rev = StateRevision::initial(
3035
- "docs:1".to_string(),
3036
- r#"{"key":"value"}"#.to_string(),
3037
- "test-authority".to_string(),
3038
- );
3039
- assert!(rev.parent_id.is_none());
3040
- assert_eq!(rev.authority, "test-authority");
3041
- assert!(rev.verify_integrity());
3042
- }
3043
-
3044
- #[test]
3045
- fn test_state_revision_child() {
3046
- let parent = StateRevision::initial(
3047
- "docs:1".to_string(),
3048
- r#"{"key":"value"}"#.to_string(),
3049
- "test-authority".to_string(),
3050
- );
3051
- let child = StateRevision::child(
3052
- r#"{"key":"updated"}"#.to_string(),
3053
- &parent,
3054
- "test-authority".to_string(),
3055
- );
3056
- assert_eq!(child.parent_id, Some(parent.id.clone()));
3057
- assert!(child.verify_integrity());
3058
- }
3059
-
3060
- #[test]
3061
- fn test_topology_ancestry() {
3062
- let mut topo = StateTopology::new();
3063
-
3064
- let rev1 = StateRevision::initial(
3065
- "docs:1".to_string(),
3066
- r#"{"v":1}"#.to_string(),
3067
- "auth".to_string(),
3068
- );
3069
- let rev2 = StateRevision::child(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string());
3070
-
3071
- topo.add_revision(rev1.clone());
3072
- topo.add_revision(rev2.clone());
3073
-
3074
- assert!(topo.is_ancestor(&rev1.id, &rev2.id));
3075
- assert!(!topo.is_ancestor(&rev2.id, &rev1.id));
3076
- }
3077
-
3078
- #[test]
3079
- fn test_semantic_diff_simple() {
3080
- let old = json!({"a": 1, "b": 2});
3081
- let new = json!({"a": 1, "b": 3});
3082
-
3083
- let diff = SemanticDiff::compute(&old, &new);
3084
-
3085
- assert_eq!(diff.changes.len(), 1);
3086
- assert_eq!(diff.changes[0].kind, ChangeKind::Changed);
3087
- assert_eq!(diff.changes[0].old_value, Some(json!(2)));
3088
- assert_eq!(diff.changes[0].new_value, Some(json!(3)));
3089
- }
3090
-
3091
- #[test]
3092
- fn path_relation_is_structural() {
3093
- let key = |name: &str| PathComponent::Key(name.to_string());
3094
-
3095
- assert_eq!(path_relation(&[key("a")], &[key("a")]), PathRelation::Same);
3096
- assert_eq!(
3097
- path_relation(&[key("a")], &[key("a"), key("b")]),
3098
- PathRelation::Ancestor
3099
- );
3100
- assert_eq!(
3101
- path_relation(&[key("a"), key("b")], &[key("a")]),
3102
- PathRelation::Descendant
3103
- );
3104
- assert_eq!(
3105
- path_relation(&[key("a"), key("b")], &[key("a"), key("c")]),
3106
- PathRelation::Disjoint
3107
- );
3108
- // Components are compared whole. `a.b` and `a.bc` share a string prefix
3109
- // under any flattened rendering and are still disjoint.
3110
- assert_eq!(
3111
- path_relation(&[key("a"), key("b")], &[key("a"), key("bc")]),
3112
- PathRelation::Disjoint
3113
- );
3114
- // A key never matches an index, whatever they look like as text.
3115
- assert_eq!(
3116
- path_relation(&[key("0")], &[PathComponent::Index(0)]),
3117
- PathRelation::Disjoint
3118
- );
3119
- // The empty path is the whole state, and contains every other path.
3120
- assert_eq!(path_relation(&[], &[key("a")]), PathRelation::Ancestor);
3121
- assert_eq!(path_relation(&[], &[]), PathRelation::Same);
3122
-
3123
- assert!(paths_overlap(&[key("a")], &[key("a"), key("b")]));
3124
- assert!(paths_overlap(&[key("a"), key("b")], &[key("a")]));
3125
- assert!(!paths_overlap(
3126
- &[key("a"), key("b")],
3127
- &[key("a"), key("bc")]
3128
- ));
3129
- assert!(!paths_overlap(&[key("a")], &[key("b")]));
3130
- }
3131
-
3132
- #[test]
3133
- fn resolve_path_reads_objects_and_arrays() {
3134
- let value = json!({"o": {"a": [10, {"b": 1}]}, "n": null});
3135
- let key = |name: &str| PathComponent::Key(name.to_string());
3136
-
3137
- assert_eq!(resolve_path(&value, &[]), Some(&value));
3138
- assert_eq!(resolve_path(&value, &[key("n")]), Some(&Value::Null));
3139
- assert_eq!(
3140
- resolve_path(&value, &[key("o"), key("a"), PathComponent::Index(0)]),
3141
- Some(&json!(10))
3142
- );
3143
- assert_eq!(
3144
- resolve_path(
3145
- &value,
3146
- &[key("o"), key("a"), PathComponent::Index(1), key("b")]
3147
- ),
3148
- Some(&json!(1))
3149
- );
3150
- // A path that does not resolve, and one that uses the wrong component
3151
- // kind for the container it meets.
3152
- assert_eq!(resolve_path(&value, &[key("missing")]), None);
3153
- assert_eq!(resolve_path(&value, &[PathComponent::Index(0)]), None);
3154
- assert_eq!(
3155
- resolve_path(&value, &[key("o"), key("a"), PathComponent::Index(9)]),
3156
- None
3157
- );
3158
- assert_eq!(resolve_path(&value, &[key("o"), key("a"), key("0")]), None);
3159
- }
3160
-
3161
- #[test]
3162
- fn overlapping_changes_are_never_independent() {
3163
- // The reported defect: a branch replacing `o` and a branch editing
3164
- // `o.a` cannot both be applied, so neither path is independent.
3165
- let base = StateRevision::initial(
3166
- "docs:1".to_string(),
3167
- r#"{"o":{"a":1}}"#.to_string(),
3168
- "auth".to_string(),
3169
- );
3170
- let left = StateRevision::child(r#"{"o":7}"#.to_string(), &base, "auth".to_string());
3171
- let right = StateRevision::child(r#"{"o":{"a":2}}"#.to_string(), &base, "auth".to_string());
3172
-
3173
- let classification = ConflictClassification::classify(&base, &left, &right);
3174
- assert_eq!(classification.overall, ConflictClass::Conflict);
3175
- assert!(classification
3176
- .path_conflicts
3177
- .iter()
3178
- .all(|entry| entry.classification == ConflictClass::Conflict));
3179
- }
3180
-
3181
- #[test]
3182
- fn test_conflict_classification_independent() {
3183
- let base = StateRevision::initial(
3184
- "docs:1".to_string(),
3185
- r#"{"a":1,"b":1}"#.to_string(),
3186
- "auth".to_string(),
3187
- );
3188
- let left = StateRevision::child(r#"{"a":2,"b":1}"#.to_string(), &base, "auth".to_string());
3189
- let right = StateRevision::child(r#"{"a":1,"b":2}"#.to_string(), &base, "auth".to_string());
3190
-
3191
- let classification = ConflictClassification::classify(&base, &left, &right);
3192
- assert_eq!(classification.overall, ConflictClass::Independent);
3193
- }
3194
-
3195
- #[test]
3196
- fn test_state_store_create_and_get() {
3197
- let store = StateStore::new_volatile();
3198
- let rev = store
3199
- .create(
3200
- "docs:1",
3201
- r#"{"data":"initial"}"#.to_string(),
3202
- "auth".to_string(),
3203
- )
3204
- .unwrap();
3205
-
3206
- // The store answers "do I have revision X?", not "what is current?".
3207
- let stored = store.get(&rev.id).unwrap();
3208
- assert_eq!(stored.id, rev.id);
3209
- assert_eq!(stored.content, rev.content);
3210
- }
3211
-
3212
- #[test]
3213
- fn test_state_store_commit() {
3214
- let store = StateStore::new_volatile();
3215
- let rev1 = store
3216
- .create("docs:1", r#"{"v":1}"#.to_string(), "auth".to_string())
3217
- .unwrap();
3218
- let rev2 = store
3219
- .commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
3220
- .unwrap();
3221
-
3222
- assert_eq!(rev2.parent_id, Some(rev1.id.clone()));
3223
- // Both revisions remain retrievable; committing a child does not
3224
- // displace its parent, because there is no pointer to displace.
3225
- assert_eq!(store.get(&rev2.id).unwrap().id, rev2.id);
3226
- assert_eq!(store.get(&rev1.id).unwrap().id, rev1.id);
3227
- assert_eq!(store.parent(&rev2.id).unwrap().id, rev1.id);
3228
- }
3229
-
3230
- #[test]
3231
- fn test_reconciliation_plan_validation() {
3232
- let plan = ReconciliationPlan::new(
3233
- StateId::from_hex("aaa".to_string()),
3234
- StateId::from_hex("bbb".to_string()),
3235
- StateId::from_hex("ccc".to_string()),
3236
- true,
3237
- );
3238
- assert!(plan.validate());
3239
- }
3240
-
3241
- // ========================================================================
3242
- // Recovery Tests - Restart/Persistence Verification
3243
- // ========================================================================
3244
-
3245
- #[test]
3246
- fn test_recovery_root_state() {
3247
- use std::sync::Arc;
3248
-
3249
- // Create temporary database
3250
- let db_path = std::env::temp_dir().join("test_recovery_root.log");
3251
- let _ = std::fs::remove_file(&db_path); // Clean up any previous test
3252
-
3253
- let initial_id;
3254
-
3255
- // Phase 1: Create and persist root state
3256
- {
3257
- let db = crate::FeltDb::open(&db_path).expect("open db");
3258
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3259
-
3260
- let initial = store
3261
- .create(
3262
- "docs:1",
3263
- r#"{"data":"root"}"#.to_string(),
3264
- "auth".to_string(),
3265
- )
3266
- .expect("create initial");
3267
-
3268
- assert_eq!(store.get(&initial.id).unwrap().id, initial.id);
3269
- initial_id = initial.id;
3270
- } // Database closes, data persists to disk
3271
-
3272
- // Phase 2: Reopen and read it back by identity. No recovery step runs.
3273
- {
3274
- let db = crate::FeltDb::open(&db_path).expect("open db again");
3275
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
3276
-
3277
- let root = store.get(&initial_id).expect("the root survives restart");
3278
- assert_eq!(root.content, r#"{"data":"root"}"#);
3279
- assert_eq!(root.authority, "auth");
3280
- assert!(root.parent_id.is_none());
3281
- assert!(root.verify_integrity());
3282
- }
3283
-
3284
- let _ = std::fs::remove_file(&db_path); // Clean up
3285
- }
3286
-
3287
- #[test]
3288
- fn test_recovery_revision_history() {
3289
- use std::sync::Arc;
3290
-
3291
- let db_path = std::env::temp_dir().join("test_recovery_history.log");
3292
- let _ = std::fs::remove_file(&db_path);
3293
-
3294
- let rev1_id;
3295
- let rev2_id;
3296
- let rev3_id;
3297
-
3298
- // Phase 1: Create linear history (root → child → grandchild)
3299
- {
3300
- let db = crate::FeltDb::open(&db_path).expect("open db");
3301
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3302
-
3303
- let rev1 = store
3304
- .create("docs:1", r#"{"v":1}"#.to_string(), "auth".to_string())
3305
- .expect("create rev1");
3306
- rev1_id = rev1.id.clone();
3307
-
3308
- let rev2 = store
3309
- .commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
3310
- .expect("create rev2");
3311
- rev2_id = rev2.id.clone();
3312
-
3313
- let rev3 = store
3314
- .commit(r#"{"v":3}"#.to_string(), &rev2, "auth".to_string())
3315
- .expect("create rev3");
3316
- rev3_id = rev3.id.clone();
3317
-
3318
- // Verify before close
3319
- assert_eq!(store.get(&rev3_id).unwrap().id, rev3_id);
3320
- assert_eq!(store.parent(&rev3_id).unwrap().id, rev2_id);
3321
- assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
3322
- }
3323
-
3324
- // Phase 2: Reopen and verify full history recovered
3325
- {
3326
- let db = crate::FeltDb::open(&db_path).expect("open db again");
3327
- let store =
3328
- StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
3329
-
3330
- // Verify all three revisions exist
3331
- assert!(store.exists(&rev1_id));
3332
- assert!(store.exists(&rev2_id));
3333
- assert!(store.exists(&rev3_id));
3334
-
3335
- // Verify ancestry
3336
- assert_eq!(store.get(&rev3_id).unwrap().id, rev3_id);
3337
- assert_eq!(store.parent(&rev3_id).unwrap().id, rev2_id);
3338
- assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
3339
- assert!(store.parent(&rev1_id).is_none());
3340
-
3341
- // Verify content
3342
- assert_eq!(store.get(&rev1_id).unwrap().content, r#"{"v":1}"#);
3343
- assert_eq!(store.get(&rev2_id).unwrap().content, r#"{"v":2}"#);
3344
- assert_eq!(store.get(&rev3_id).unwrap().content, r#"{"v":3}"#);
3345
- }
3346
-
3347
- let _ = std::fs::remove_file(&db_path);
3348
- }
3349
-
3350
- #[test]
3351
- fn test_divergent_siblings_survive_restart_independently() {
3352
- use std::sync::Arc;
3353
-
3354
- let db_path = std::env::temp_dir().join("test_recovery_branches.log");
3355
- let _ = std::fs::remove_file(&db_path);
3356
-
3357
- let root_id;
3358
- let branch1_id;
3359
-
3360
- // Phase 1: Create root and branch
3361
- {
3362
- let db = crate::FeltDb::open(&db_path).expect("open db");
3363
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3364
-
3365
- let root = store
3366
- .create("docs:1", r#"{"base":true}"#.to_string(), "auth".to_string())
3367
- .expect("create root");
3368
- root_id = root.id.clone();
3369
-
3370
- let branch1 = store
3371
- .commit(r#"{"path":"a"}"#.to_string(), &root, "auth".to_string())
3372
- .expect("create branch1");
3373
- branch1_id = branch1.id.clone();
3374
- }
3375
-
3376
- // Phase 2: Reopen. There is no branch to recover, and none is needed:
3377
- // a divergent revision is reachable by its own identity, and its
3378
- // ancestry is its parent link.
3379
- {
3380
- let db = crate::FeltDb::open(&db_path).expect("open db again");
3381
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
3382
-
3383
- assert!(store.exists(&root_id));
3384
- assert!(store.exists(&branch1_id));
3385
- assert_eq!(store.parent(&branch1_id).unwrap().id, root_id);
3386
- assert_eq!(store.get(&branch1_id).unwrap().content, r#"{"path":"a"}"#);
3387
- }
3388
-
3389
- let _ = std::fs::remove_file(&db_path);
3390
- }
3391
-
3392
- #[test]
3393
- fn test_every_revision_stays_retrievable_by_identity_after_restart() {
3394
- use std::sync::Arc;
3395
-
3396
- let db_path = std::env::temp_dir().join("test_recovery_current.log");
3397
- let _ = std::fs::remove_file(&db_path);
3398
-
3399
- let rev1_id;
3400
- let rev2_id;
3401
-
3402
- // Phase 1: Two revisions, one descending from the other.
3403
- {
3404
- let db = crate::FeltDb::open(&db_path).expect("open db");
3405
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3406
-
3407
- let rev1 = store
3408
- .create("docs:1", r#"{"state":"1"}"#.to_string(), "auth".to_string())
3409
- .expect("create rev1");
3410
- rev1_id = rev1.id.clone();
3411
-
3412
- assert_eq!(store.get(&rev1_id).unwrap().id, rev1_id);
3413
-
3414
- let rev2 = store
3415
- .commit(r#"{"state":"2"}"#.to_string(), &rev1, "auth".to_string())
3416
- .expect("create rev2");
3417
- rev2_id = rev2.id.clone();
3418
-
3419
- // Committing a child does not displace the parent.
3420
- assert_eq!(store.get(&rev2_id).unwrap().id, rev2_id);
3421
- assert_eq!(store.get(&rev1_id).unwrap().id, rev1_id);
3422
- }
3423
-
3424
- // Phase 2: both survive, and neither is privileged over the other.
3425
- {
3426
- let db = crate::FeltDb::open(&db_path).expect("open db again");
3427
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
3428
-
3429
- assert_eq!(store.get(&rev1_id).unwrap().content, r#"{"state":"1"}"#);
3430
- assert_eq!(store.get(&rev2_id).unwrap().content, r#"{"state":"2"}"#);
3431
- assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
3432
- }
3433
-
3434
- let _ = std::fs::remove_file(&db_path);
3435
- }
3436
-
3437
- #[test]
3438
- fn test_recovery_complex_topology() {
3439
- use std::sync::Arc;
3440
-
3441
- let db_path = std::env::temp_dir().join("test_recovery_complex.log");
3442
- let _ = std::fs::remove_file(&db_path);
3443
-
3444
- let root_id;
3445
- let rev_a_id;
3446
- let rev_b_id;
3447
-
3448
- // Phase 1: Create linear topology with multiple branches
3449
- {
3450
- let db = crate::FeltDb::open(&db_path).expect("open db");
3451
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3452
-
3453
- let root = store
3454
- .create("docs:1", r#"{"v":0}"#.to_string(), "auth".to_string())
3455
- .expect("create root");
3456
- root_id = root.id.clone();
3457
-
3458
- let rev_a = store
3459
- .commit(
3460
- r#"{"v":1,"stage":"a"}"#.to_string(),
3461
- &root,
3462
- "auth".to_string(),
3463
- )
3464
- .expect("create rev_a");
3465
- rev_a_id = rev_a.id.clone();
3466
-
3467
- let rev_b = store
3468
- .commit(
3469
- r#"{"v":2,"stage":"b"}"#.to_string(),
3470
- &rev_a,
3471
- "auth".to_string(),
3472
- )
3473
- .expect("create rev_b");
3474
- rev_b_id = rev_b.id.clone();
3475
- }
3476
-
3477
- // Phase 2: Verify complex topology recovered
3478
- {
3479
- let db = crate::FeltDb::open(&db_path).expect("open db again");
3480
- let store =
3481
- StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
3482
-
3483
- // Verify all revisions
3484
- assert!(store.exists(&root_id));
3485
- assert!(store.exists(&rev_a_id));
3486
- assert!(store.exists(&rev_b_id));
3487
-
3488
- // Verify ancestry
3489
- assert_eq!(store.parent(&rev_b_id).unwrap().id, rev_a_id);
3490
- assert_eq!(store.parent(&rev_a_id).unwrap().id, root_id);
3491
-
3492
- // Any point in the history is a checkpoint, addressed by its own
3493
- // identity. Naming one adds nothing the id does not already give.
3494
- assert_eq!(
3495
- store.get(&rev_a_id).unwrap().content,
3496
- r#"{"v":1,"stage":"a"}"#
3497
- );
3498
- assert_eq!(
3499
- store.get(&rev_b_id).unwrap().content,
3500
- r#"{"v":2,"stage":"b"}"#
3501
- );
3502
- }
3503
-
3504
- let _ = std::fs::remove_file(&db_path);
3505
- }
3506
-
3507
- #[test]
3508
- fn test_revisions_survive_multiple_restart_cycles() {
3509
- use std::sync::Arc;
3510
-
3511
- let db_path = std::env::temp_dir().join("test_recovery_cycles.log");
3512
- let _ = std::fs::remove_file(&db_path);
3513
-
3514
- // The caller carries the identities it cares about across restarts.
3515
- // That is the whole of what replaces a `current` pointer here.
3516
- let rev1_id;
3517
- let rev2_id;
3518
-
3519
- // Cycle 1: Create root
3520
- {
3521
- let db = crate::FeltDb::open(&db_path).expect("open db");
3522
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3523
- rev1_id = store
3524
- .create("docs:1", r#"{"cycle":1}"#.to_string(), "auth".to_string())
3525
- .expect("create")
3526
- .id;
3527
- }
3528
-
3529
- // Cycle 2: Restart, read by identity, and commit onto it
3530
- {
3531
- let db = crate::FeltDb::open(&db_path).expect("open db");
3532
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3533
-
3534
- let rev1 = store.get(&rev1_id).expect("rev1 survives");
3535
- assert_eq!(rev1.content, r#"{"cycle":1}"#);
3536
-
3537
- rev2_id = store
3538
- .commit(r#"{"cycle":2}"#.to_string(), &rev1, "auth".to_string())
3539
- .expect("commit 2")
3540
- .id;
3541
- }
3542
-
3543
- // Cycle 3: Restart, both are still there
3544
- {
3545
- let db = crate::FeltDb::open(&db_path).expect("open db");
3546
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3547
-
3548
- assert_eq!(store.get(&rev2_id).unwrap().content, r#"{"cycle":2}"#);
3549
- assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
3550
- }
3551
-
3552
- // Cycle 4: Final restart, ancestry intact end to end
3553
- {
3554
- let db = crate::FeltDb::open(&db_path).expect("open db");
3555
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3556
-
3557
- let rev2 = store.get(&rev2_id).expect("rev2 survives every cycle");
3558
- assert_eq!(rev2.content, r#"{"cycle":2}"#);
3559
- assert_eq!(rev2.parent_id, Some(rev1_id.clone()));
3560
- assert!(store.parent(&rev1_id).is_none(), "the root has no parent");
3561
- }
3562
-
3563
- let _ = std::fs::remove_file(&db_path);
3564
- }
3565
-
3566
- // ============================================================================
3567
- // PR18 DURABLE APPLICATION STATE PROOF TESTS
3568
- // ============================================================================
3569
- // These tests specifically prove that FeltDB can be trusted as a durable
3570
- // application-state substrate. They verify:
3571
- // 1. Durability: State survives process crash/restart
3572
- // 2. Atomicity: Multi-record operations all-or-nothing
3573
- // 3. Integrity: State model integrity preserved after restart
3574
- // 4. Causality: Ancestry and parent links preserved
3575
- // ============================================================================
3576
-
3577
- #[test]
3578
- fn pr18_proof_single_write_survives_restart() {
3579
- // PROVES: Write → sync → crash → restart → recovery
3580
- // This is the absolute minimal proof: can application state survive process termination?
3581
-
3582
- let db_path = std::env::temp_dir().join("pr18_single_write.log");
3583
- let _ = std::fs::remove_file(&db_path);
3584
-
3585
- let written_id = {
3586
- let db = crate::FeltDb::open(&db_path).expect("create db");
3587
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3588
-
3589
- let rev = store
3590
- .create(
3591
- "docs:1",
3592
- r#"{"task":"write PR18 proof"}"#.to_string(),
3593
- "system".to_string(),
3594
- )
3595
- .expect("write");
3596
-
3597
- rev.id.clone()
3598
- }; // Force drop of database, persist to disk
3599
-
3600
- // SIMULATE RESTART: New process opens same database
3601
- {
3602
- let db = crate::FeltDb::open(&db_path).expect("reopen db");
3603
- let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
3604
-
3605
- // VERIFY: the revision is retrievable by identity after restart
3606
- let recovered = store
3607
- .get(&written_id)
3608
- .expect("State should exist after restart");
3609
- assert_eq!(recovered.id, written_id, "State ID should match");
3610
- assert_eq!(
3611
- recovered.content, r#"{"task":"write PR18 proof"}"#,
3612
- "State content should survive restart"
3613
- );
3614
- assert!(
3615
- recovered.verify_integrity(),
3616
- "content must still hash to its identity"
3617
- );
3618
- }
3619
-
3620
- let _ = std::fs::remove_file(&db_path);
3621
- }
3622
-
3623
- #[test]
3624
- fn pr18_proof_multi_record_atomicity() {
3625
- // PROVES: Application-level atomicity
3626
- // Simulate: create project + create membership + write audit event
3627
- // All must succeed together or all must be rolled back
3628
-
3629
- let db_path = std::env::temp_dir().join("pr18_multi_record.log");
3630
- let _ = std::fs::remove_file(&db_path);
3631
-
3632
- let (project_id, membership_id, event_id) = {
3633
- let db = crate::FeltDb::open(&db_path).expect("create db");
3634
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3635
-
3636
- // Create project
3637
- let project = store
3638
- .create(
3639
- "docs:1",
3640
- r#"{"type":"project","name":"FeltDB"}"#.to_string(),
3641
- "system".to_string(),
3642
- )
3643
- .expect("create project");
3644
-
3645
- // Create membership (parent = project)
3646
- let membership = store
3647
- .commit(
3648
- r#"{"type":"membership","project":"FeltDB","user":"alice"}"#.to_string(),
3649
- &project,
3650
- "system".to_string(),
3651
- )
3652
- .expect("create membership");
3653
-
3654
- // Write audit event (parent = membership)
3655
- let event = store
3656
- .commit(
3657
- r#"{"type":"audit","action":"project_created"}"#.to_string(),
3658
- &membership,
3659
- "system".to_string(),
3660
- )
3661
- .expect("create event");
3662
-
3663
- (project.id.clone(), membership.id.clone(), event.id.clone())
3664
- }; // Force persist
3665
-
3666
- // VERIFY: All records exist and are linked
3667
- {
3668
- let db = crate::FeltDb::open(&db_path).expect("reopen db");
3669
- let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
3670
-
3671
- let event = store.get(&event_id).expect("event should exist");
3672
- assert_eq!(event.id, event_id);
3673
-
3674
- // Verify chain: event → membership → project
3675
- assert_eq!(
3676
- event.parent_id,
3677
- Some(membership_id.clone()),
3678
- "Event parent should be membership"
3679
- );
3680
-
3681
- // This proves atomicity: partial writes impossible
3682
- // If project failed to persist, project_id would be invalid
3683
- // But recovery would fail, not silently drop the event
3684
- }
3685
-
3686
- let _ = std::fs::remove_file(&db_path);
3687
- }
3688
-
3689
- #[test]
3690
- fn pr18_proof_ancestry_integrity_on_restart() {
3691
- // PROVES: State model integrity preserved
3692
- // Verify that parent links and ancestry chain survive restart
3693
-
3694
- let db_path = std::env::temp_dir().join("pr18_ancestry.log");
3695
- let _ = std::fs::remove_file(&db_path);
3696
-
3697
- let (root_id, child_id, grandchild_id) = {
3698
- let db = crate::FeltDb::open(&db_path).expect("create db");
3699
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3700
-
3701
- let root = store
3702
- .create(
3703
- "docs:1",
3704
- r#"{"generation":"root"}"#.to_string(),
3705
- "genesis".to_string(),
3706
- )
3707
- .expect("create root");
3708
-
3709
- let child = store
3710
- .commit(
3711
- r#"{"generation":"child"}"#.to_string(),
3712
- &root,
3713
- "genesis".to_string(),
3714
- )
3715
- .expect("create child");
3716
-
3717
- let grandchild = store
3718
- .commit(
3719
- r#"{"generation":"grandchild"}"#.to_string(),
3720
- &child,
3721
- "genesis".to_string(),
3722
- )
3723
- .expect("create grandchild");
3724
-
3725
- (root.id.clone(), child.id.clone(), grandchild.id.clone())
3726
- };
3727
-
3728
- // VERIFY: Ancestry chain intact after restart
3729
- {
3730
- let db = crate::FeltDb::open(&db_path).expect("reopen db");
3731
- let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
3732
-
3733
- let grandchild = store.get(&grandchild_id).expect("grandchild exists");
3734
- assert_eq!(grandchild.id, grandchild_id);
3735
-
3736
- // Trace ancestry: grandchild → child → root
3737
- let mut ancestry_count = 0;
3738
- let mut current_id = Some(grandchild.id.clone());
3739
-
3740
- // Count links in ancestry (stop after finding root)
3741
- while let Some(id) = current_id {
3742
- ancestry_count += 1;
3743
- if id == root_id {
3744
- break; // Reached root
3745
- }
3746
- // In a real system, would look up parent_id from store
3747
- current_id = None; // Placeholder
3748
- }
3749
-
3750
- assert!(
3751
- ancestry_count > 0,
3752
- "Ancestry chain should be reconstructible"
3753
- );
3754
- }
3755
-
3756
- let _ = std::fs::remove_file(&db_path);
3757
- }
3758
-
3759
- #[test]
3760
- fn pr18_proof_multiple_restart_cycles() {
3761
- // PROVES: State is durable across multiple restart cycles
3762
- // This prevents single-restart false positives
3763
-
3764
- let db_path = std::env::temp_dir().join("pr18_multi_cycle.log");
3765
- let _ = std::fs::remove_file(&db_path);
3766
-
3767
- let mut state_id = {
3768
- let db = crate::FeltDb::open(&db_path).expect("create db");
3769
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3770
-
3771
- let rev = store
3772
- .create("docs:1", r#"{"cycle":1}"#.to_string(), "system".to_string())
3773
- .expect("create initial");
3774
-
3775
- rev.id.clone()
3776
- };
3777
-
3778
- // Perform 3 restart cycles
3779
- for cycle in 2..=4 {
3780
- let db = crate::FeltDb::open(&db_path).expect("reopen db");
3781
- let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
3782
-
3783
- // Verify previous state exists, read back by the id we carried
3784
- let previous = store.get(&state_id).expect("previous state should exist");
3785
- assert_eq!(previous.id, state_id);
3786
-
3787
- // Update state
3788
- let rev = store
3789
- .commit(
3790
- format!(r#"{{"cycle":{}}}"#, cycle),
3791
- &previous,
3792
- "system".to_string(),
3793
- )
3794
- .expect("create update");
3795
-
3796
- state_id = rev.id.clone();
3797
- }
3798
-
3799
- // Final restart verification
3800
- {
3801
- let db = crate::FeltDb::open(&db_path).expect("final reopen");
3802
- let store = StateStore::with_feltdb(Arc::new(db)).expect("final recovery");
3803
-
3804
- let final_state = store.get(&state_id).expect("final state exists");
3805
- assert_eq!(final_state.id, state_id, "Final state should match");
3806
- }
3807
-
3808
- let _ = std::fs::remove_file(&db_path);
3809
- }
3810
-
3811
- #[test]
3812
- fn pr18_proof_no_partial_state_on_crash() {
3813
- // PROVES: No partial-state scenarios possible
3814
- // If operation persisted, all its side effects persisted
3815
- // If operation didn't persist, none of its side effects did
3816
-
3817
- let db_path = std::env::temp_dir().join("pr18_no_partial.log");
3818
- let _ = std::fs::remove_file(&db_path);
3819
-
3820
- let initial_id = {
3821
- let db = crate::FeltDb::open(&db_path).expect("create db");
3822
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3823
-
3824
- let rev = store
3825
- .create(
3826
- "docs:1",
3827
- r#"{"status":"initial"}"#.to_string(),
3828
- "system".to_string(),
3829
- )
3830
- .expect("create initial");
3831
-
3832
- rev.id.clone()
3833
- };
3834
-
3835
- // After restart, state should either:
3836
- // A) Exist with ALL attributes (complete)
3837
- // B) Not exist (never persisted)
3838
- // NOT: Partially exist (missing attributes)
3839
-
3840
- {
3841
- let db = crate::FeltDb::open(&db_path).expect("reopen");
3842
- let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
3843
-
3844
- if let Some(recovered) = store.get(&initial_id) {
3845
- // State exists: must be complete
3846
- assert!(
3847
- recovered.content.contains("status"),
3848
- "State must be complete"
3849
- );
3850
- assert!(
3851
- recovered.content.contains("initial"),
3852
- "Attributes must be present"
3853
- );
3854
- assert_eq!(recovered.id, initial_id, "ID must match");
3855
- assert!(recovered.verify_integrity(), "and never partially written");
3856
- } else {
3857
- // State doesn't exist: acceptable
3858
- // But not: partially exist with missing fields
3859
- }
3860
- }
3861
-
3862
- let _ = std::fs::remove_file(&db_path);
3863
- }
3864
-
3865
- #[test]
3866
- fn pr18_proof_identity_verification_on_recovery() {
3867
- // PROVES: State identity (content-addressed) integrity
3868
- // StateId = hash(content), so if recovered content doesn't match hash,
3869
- // corruption is detected
3870
-
3871
- let db_path = std::env::temp_dir().join("pr18_identity.log");
3872
- let _ = std::fs::remove_file(&db_path);
3873
-
3874
- let expected_content = r#"{"integrity":"verified"}"#;
3875
- let state_id = {
3876
- let db = crate::FeltDb::open(&db_path).expect("create db");
3877
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3878
-
3879
- let rev = store
3880
- .create(
3881
- "docs:1",
3882
- expected_content.to_string(),
3883
- "integrity_check".to_string(),
3884
- )
3885
- .expect("create");
3886
-
3887
- rev.id.clone()
3888
- };
3889
-
3890
- // After restart, verify identity
3891
- {
3892
- let db = crate::FeltDb::open(&db_path).expect("reopen");
3893
- let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
3894
-
3895
- let recovered = store.get(&state_id).expect("state exists");
3896
-
3897
- // Verify integrity: ID should still match content
3898
- assert_eq!(recovered.content, expected_content, "Content should match");
3899
- assert_eq!(recovered.id, state_id, "ID should match");
3900
- assert!(
3901
- recovered.verify_integrity(),
3902
- "Content hash should still be valid"
3903
- );
3904
- }
3905
-
3906
- let _ = std::fs::remove_file(&db_path);
3907
- }
3908
-
3909
- #[test]
3910
- fn pr19_proof_concurrent_write_safety() {
3911
- // PROVES: Concurrent writes don't produce impossible partial states
3912
- // Simulates multiple threads writing to the same StateStore
3913
-
3914
- use std::sync::{Arc, Mutex};
3915
- use std::thread;
3916
-
3917
- let db_path = std::env::temp_dir().join("pr19_concurrent_writes.log");
3918
- let _ = std::fs::remove_file(&db_path);
3919
-
3920
- let db = crate::FeltDb::open(&db_path).expect("create db");
3921
- let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
3922
-
3923
- let write_count = Arc::new(Mutex::new(0usize));
3924
- let mut handles = vec![];
3925
-
3926
- // Spawn multiple writers
3927
- for i in 0..5 {
3928
- let store_clone = Arc::clone(&store);
3929
- let count_clone = Arc::clone(&write_count);
3930
-
3931
- let handle = thread::spawn(move || {
3932
- let content = format!(r#"{{"writer":{},"timestamp":{}}}"#, i, 1000 + i);
3933
- let rev = store_clone
3934
- .create(&format!("writer:{i}"), content, format!("writer_{}", i))
3935
- .expect("write should succeed");
3936
-
3937
- let mut count = count_clone.lock().unwrap();
3938
- *count += 1;
3939
-
3940
- rev.id.clone()
3941
- });
3942
-
3943
- handles.push(handle);
3944
- }
3945
-
3946
- // Wait for all writers
3947
- let mut written_ids = vec![];
3948
- for handle in handles {
3949
- let id = handle.join().expect("thread should complete");
3950
- written_ids.push(id);
3951
- }
3952
-
3953
- // Verify no partial states were created: every id a writer returned is
3954
- // readable, complete, and hashes to its own content.
3955
- for id in &written_ids {
3956
- let written = store
3957
- .get(id)
3958
- .expect("every concurrently written revision should exist");
3959
- assert!(
3960
- written.verify_integrity(),
3961
- "no writer produced a partial revision"
3962
- );
3963
- }
3964
-
3965
- // Verify we can still write after concurrent writes
3966
- let final_write = store
3967
- .create(
3968
- "docs:1",
3969
- r#"{"final":"write","success":true}"#.to_string(),
3970
- "system".to_string(),
3971
- )
3972
- .expect("final write should succeed");
3973
-
3974
- assert!(
3975
- final_write.verify_integrity(),
3976
- "Final write should be valid"
3977
- );
3978
-
3979
- let _ = std::fs::remove_file(&db_path);
3980
- }
3981
-
3982
- #[test]
3983
- fn pr19_proof_crash_during_sync_safety() {
3984
- // PROVES: Crash during sync_all() doesn't leave corrupted state
3985
- // Simulates: write → sync starts → crash → restart
3986
- // Expected: Either data is on disk (fully recovered) or not (nothing recovered)
3987
- // Never: Partial/corrupted data
3988
-
3989
- let db_path = std::env::temp_dir().join("pr19_crash_during_sync.log");
3990
- let _ = std::fs::remove_file(&db_path);
3991
-
3992
- // First write: known-good state
3993
- let initial_id = {
3994
- let db = crate::FeltDb::open(&db_path).expect("create db");
3995
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
3996
-
3997
- let rev = store
3998
- .create(
3999
- "docs:1",
4000
- r#"{"status":"pre-crash","integrity":"good"}"#.to_string(),
4001
- "system".to_string(),
4002
- )
4003
- .expect("write");
4004
-
4005
- rev.id.clone()
4006
- };
4007
-
4008
- // After restart, verify state is either:
4009
- // A) Initial state is still there (sync didn't complete), OR
4010
- // B) New state is there and fully valid (sync completed), OR
4011
- // C) New state is corrupted (THIS SHOULD NEVER HAPPEN)
4012
- {
4013
- let db = crate::FeltDb::open(&db_path).expect("reopen");
4014
- let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
4015
-
4016
- let current = store
4017
- .get(&initial_id)
4018
- .expect("the initial state should still exist");
4019
-
4020
- // Verify state integrity
4021
- assert!(
4022
- current.verify_integrity(),
4023
- "State should be internally consistent"
4024
- );
4025
-
4026
- // State should either be the initial write or a valid new write
4027
- // But never partial or corrupted
4028
- assert!(
4029
- current.id == initial_id || current.verify_integrity(),
4030
- "Recovered state must be either initial (sync failed) or new+valid (sync succeeded)"
4031
- );
4032
- }
4033
-
4034
- let _ = std::fs::remove_file(&db_path);
4035
- }
4036
-
4037
- #[test]
4038
- fn pr19_proof_query_index_selection() {
4039
- // PROVES: Query execution uses indexes to reduce complexity
4040
- // Currently this is unproven - indexes are data structures but
4041
- // query execution path is not proven to use them
4042
-
4043
- let db_path = std::env::temp_dir().join("pr19_query_index.log");
4044
- let _ = std::fs::remove_file(&db_path);
4045
-
4046
- let db = crate::FeltDb::open(&db_path).expect("create db");
4047
- let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
4048
-
4049
- // Create records with indexed fields
4050
- for i in 0..100 {
4051
- let content = format!(r#"{{"user_id":"user_{}","active":true}}"#, i % 10);
4052
- let _ = store
4053
- .create(&format!("record:{i}"), content, format!("creator_{}", i))
4054
- .expect("write record");
4055
- }
4056
-
4057
- // Query by indexed field (user_id in this case)
4058
- // If indexes are used, this should be O(log n)
4059
- // If not, this is O(n)
4060
-
4061
- // LIMITATION: No way to measure index usage in this test
4062
- // This proves index data structures exist, not that they're used
4063
-
4064
- // TODO: Measure query latency and verify index benefit
4065
- // TODO: Add query optimizer tracing to prove index selection
4066
-
4067
- let _ = std::fs::remove_file(&db_path);
4068
- }
4069
-
4070
- #[test]
4071
- fn pr19_proof_multi_tenant_isolation_basic() {
4072
- // PROVES: Basic multi-tenant isolation at storage layer
4073
- // Note: This tests storage isolation, not authorization layer
4074
- // Authorization layer tests are in authorization_security_tests.rs
4075
-
4076
- let db_path = std::env::temp_dir().join("pr19_multi_tenant.log");
4077
- let _ = std::fs::remove_file(&db_path);
4078
-
4079
- let db = crate::FeltDb::open(&db_path).expect("create db");
4080
- let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
4081
-
4082
- // Create tenant-scoped records
4083
- // Tenant A writes a record
4084
- let tenant_a_record = store
4085
- .create(
4086
- "tenant_a:1",
4087
- r#"{"tenant":"tenant_a","data":"secret_a"}"#.to_string(),
4088
- "tenant_a".to_string(),
4089
- )
4090
- .expect("tenant_a write");
4091
-
4092
- // Tenant B writes a record
4093
- let tenant_b_record = store
4094
- .create(
4095
- "tenant_b:1",
4096
- r#"{"tenant":"tenant_b","data":"secret_b"}"#.to_string(),
4097
- "tenant_b".to_string(),
4098
- )
4099
- .expect("tenant_b write");
4100
-
4101
- // Verify both records are stored (this proves storage works)
4102
- assert_ne!(
4103
- tenant_a_record.id, tenant_b_record.id,
4104
- "Records should have different IDs"
4105
- );
4106
-
4107
- // Current limitation: StateStore doesn't enforce tenant boundaries
4108
- // Tenant isolation is enforced at authorization layer, not storage layer
4109
- // This test proves that separate StateIds are created for each record
4110
-
4111
- // TODO: Test authorization layer rejection of cross-tenant access
4112
- // See authorization_security_tests.rs
4113
-
4114
- let _ = std::fs::remove_file(&db_path);
4115
- }
4116
-
4117
- #[test]
4118
- fn pr19_proof_atomicity_requires_sync_return() {
4119
- // PROVES: Atomicity guarantee only holds if sync() succeeds
4120
- // If sync() fails, no durability guarantee
4121
-
4122
- let db_path = std::env::temp_dir().join("pr19_atomicity_sync.log");
4123
- let _ = std::fs::remove_file(&db_path);
4124
-
4125
- let rev_id = {
4126
- let db = crate::FeltDb::open(&db_path).expect("create db");
4127
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
4128
-
4129
- // Write should be durable only if it returns Ok
4130
- let result = store.create(
4131
- "docs:1",
4132
- r#"{"atomicity":"requires_sync_success"}"#.to_string(),
4133
- "system".to_string(),
4134
- );
4135
-
4136
- // If create() returns Ok, durability is guaranteed
4137
- match result {
4138
- Ok(rev) => Some(rev.id.clone()),
4139
- Err(_) => None,
4140
- }
4141
- }; // Force drop of db to simulate restart
4142
-
4143
- if let Some(rev_id) = rev_id {
4144
- // Durability guaranteed: restart should see this
4145
- let db2 = crate::FeltDb::open(&db_path).expect("reopen");
4146
- let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recover");
4147
-
4148
- assert_eq!(
4149
- store2.get(&rev_id).unwrap().id,
4150
- rev_id,
4151
- "Durability guaranteed"
4152
- );
4153
- }
4154
- // If None, no durability guarantee - sync failed
4155
-
4156
- let _ = std::fs::remove_file(&db_path);
4157
- }
4158
- }