@feltdb/core 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/cli/commands.js +4 -1
  2. package/dist/cli/provisioning-neutrality.js +79 -0
  3. package/dist/collection.d.ts +43 -1
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +192 -22
  6. package/dist/create/create.js +25 -21
  7. package/dist/create/managed-account.js +11 -0
  8. package/dist/create/package-versions.js +1 -1
  9. package/dist/create/server-source/Cargo.lock +165 -0
  10. package/dist/create/server-source/Cargo.toml +9 -0
  11. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  28. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  29. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  30. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  31. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  32. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  33. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  34. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  35. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  36. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  37. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  38. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  39. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  40. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  41. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  42. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  43. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  44. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  45. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  46. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  47. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  48. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  49. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  50. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  51. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +595 -0
  52. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  53. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  54. package/dist/create/server-source/crates/feltdb/src/lib.rs +1050 -117
  55. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  56. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  57. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  58. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  59. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  60. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  61. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  62. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  63. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  64. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  65. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  66. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  67. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  68. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  69. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  70. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +90 -72
  71. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  72. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  73. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  74. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  75. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  76. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  77. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  78. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  79. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  80. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  81. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  82. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  83. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  84. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  85. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  86. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +126 -0
  87. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  88. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  89. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  90. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  91. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  92. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  93. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  94. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  95. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +305 -6
  96. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  97. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  98. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  99. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
  100. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  101. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  102. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  103. package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
  104. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  105. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  106. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  107. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  108. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  109. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  110. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  111. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  112. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +442 -0
  113. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  114. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
  115. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  116. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  117. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  118. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  119. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  120. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +234 -0
  121. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +947 -0
  122. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  123. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  124. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  125. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  126. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  127. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  128. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  129. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  130. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  131. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
  132. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  133. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  134. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  135. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  136. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  137. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  138. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  139. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  140. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  141. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  142. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  143. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  144. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  145. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  146. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  147. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
  149. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
  150. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  151. package/dist/create/server-source/crates/feltdb-server/src/main.rs +800 -34
  152. package/dist/db.d.ts +33 -34
  153. package/dist/db.d.ts.map +1 -1
  154. package/dist/db.js +74 -20
  155. package/dist/deployment.d.ts +30 -0
  156. package/dist/deployment.d.ts.map +1 -0
  157. package/dist/deployment.js +130 -0
  158. package/dist/embedded-transaction.d.ts +22 -4
  159. package/dist/embedded-transaction.d.ts.map +1 -1
  160. package/dist/embedded-transaction.js +51 -5
  161. package/dist/feltdb.d.ts +14 -2
  162. package/dist/feltdb.d.ts.map +1 -1
  163. package/dist/file-db.js +1 -1
  164. package/dist/http-client.d.ts +14 -0
  165. package/dist/http-client.d.ts.map +1 -1
  166. package/dist/http-client.js +23 -5
  167. package/dist/http-db.d.ts +119 -1
  168. package/dist/http-db.d.ts.map +1 -1
  169. package/dist/http-db.js +346 -31
  170. package/dist/index-core.d.ts +2 -0
  171. package/dist/index-core.d.ts.map +1 -1
  172. package/dist/index-core.js +2 -0
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +9 -0
  175. package/dist/indexeddb-db.d.ts.map +1 -1
  176. package/dist/indexeddb-db.js +35 -21
  177. package/dist/managed-recovery.d.ts +192 -0
  178. package/dist/managed-recovery.d.ts.map +1 -0
  179. package/dist/managed-recovery.js +242 -0
  180. package/dist/memory-db.js +1 -1
  181. package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.js → feltdb_wasm-DaNwCLRX.js} +1 -1
  182. package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
  183. package/dist/studio-app/assets/index-j8IlhNqJ.js +29 -0
  184. package/dist/studio-app/index.html +1 -1
  185. package/dist/transaction.d.ts +30 -0
  186. package/dist/transaction.d.ts.map +1 -1
  187. package/dist/transaction.js +41 -0
  188. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  189. package/dist/workload.d.ts +2 -1
  190. package/dist/workload.d.ts.map +1 -1
  191. package/package.json +1 -1
  192. package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
  193. package/dist/studio-app/assets/index-B0k4UAlI.js +0 -29
@@ -20,7 +20,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
20
20
  use std::sync::{Arc, Mutex};
21
21
 
22
22
  // Forward declaration to avoid circular imports
23
- pub struct FeltDb; // Will be linked at compile time from lib.rs
23
+ pub struct FeltDb; // Will be linked at compile time from lib.rs
24
24
 
25
25
  /// Version constant for state model contracts
26
26
  pub const STATE_MODEL_VERSION: u32 = 1;
@@ -66,15 +66,41 @@ impl std::fmt::Display for StateId {
66
66
  // StateRevision: Immutable State Snapshot with Ancestry
67
67
  // ============================================================================
68
68
 
69
- /// Immutable state revision with explicit ancestry
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.
70
90
  #[derive(Clone, Debug, Serialize, Deserialize)]
71
91
  pub struct StateRevision {
72
- /// Deterministic content-addressed identifier
92
+ /// Revision identity — resource, content, parent and sequence.
73
93
  pub id: StateId,
94
+ /// The resource this is a revision of.
95
+ pub resource: String,
74
96
  /// Canonical JSON representation (representation-sensitive)
75
97
  pub content: String,
76
- /// Parent revision id (if any). Empty for initial state.
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.
77
101
  pub parent_id: Option<StateId>,
102
+ /// Position in this resource's history. Strictly increasing, never reused.
103
+ pub sequence: u64,
78
104
  /// Authority that produced this revision
79
105
  pub authority: String,
80
106
  /// Timestamp when revision was created (informational only, not used for ordering)
@@ -83,60 +109,106 @@ pub struct StateRevision {
83
109
  pub metadata: BTreeMap<String, Value>,
84
110
  }
85
111
 
112
+ /// Compute a revision identity.
113
+ ///
114
+ /// Every component participates: the same content is a different revision on a
115
+ /// different resource, from a different parent, or at a different point in the
116
+ /// resource's history. The parts are length-prefixed so no two distinct tuples
117
+ /// can produce the same input string.
118
+ fn compute_revision_id(
119
+ resource: &str,
120
+ content_id: &StateId,
121
+ parent_id: Option<&StateId>,
122
+ sequence: u64,
123
+ ) -> StateId {
124
+ let parent = parent_id.map(StateId::as_hex).unwrap_or("");
125
+ let material = format!(
126
+ "feltdb.revision.v2\n{}:{}\n{}:{}\n{}:{}\n{}",
127
+ resource.len(),
128
+ resource,
129
+ content_id.as_hex().len(),
130
+ content_id.as_hex(),
131
+ parent.len(),
132
+ parent,
133
+ sequence,
134
+ );
135
+ StateId::compute(&material)
136
+ }
137
+
86
138
  impl StateRevision {
87
- /// Create initial state revision
88
- pub fn initial(content: String, authority: String) -> Self {
89
- let id = StateId::compute(&content);
90
- let timestamp_ms = std::time::SystemTime::now()
91
- .duration_since(std::time::UNIX_EPOCH)
92
- .unwrap()
93
- .as_millis() as u64;
139
+ /// Create a resource's first revision.
140
+ pub fn initial(resource: String, content: String, authority: String) -> Self {
141
+ Self::at(resource, content, None, 0, authority)
142
+ }
94
143
 
95
- StateRevision {
96
- id,
144
+ /// Create a revision descending from `parent`, on the parent's resource.
145
+ pub fn child(content: String, parent: &StateRevision, authority: String) -> Self {
146
+ Self::at(
147
+ parent.resource.clone(),
97
148
  content,
98
- parent_id: None,
149
+ Some(parent.id.clone()),
150
+ parent.sequence + 1,
99
151
  authority,
100
- timestamp_ms,
101
- metadata: BTreeMap::new(),
102
- }
152
+ )
103
153
  }
104
154
 
105
- /// Create child revision from parent
106
- pub fn child(
155
+ /// Create a revision at an explicit position in a resource's history.
156
+ pub fn at(
157
+ resource: String,
107
158
  content: String,
108
- parent: &StateRevision,
159
+ parent_id: Option<StateId>,
160
+ sequence: u64,
109
161
  authority: String,
110
162
  ) -> Self {
111
- let id = StateId::compute(&content);
163
+ let content_id = StateId::compute(&content);
164
+ let id = compute_revision_id(&resource, &content_id, parent_id.as_ref(), sequence);
112
165
  let timestamp_ms = std::time::SystemTime::now()
113
166
  .duration_since(std::time::UNIX_EPOCH)
114
- .unwrap()
167
+ .unwrap_or_default()
115
168
  .as_millis() as u64;
116
169
 
117
170
  StateRevision {
118
171
  id,
172
+ resource,
119
173
  content,
120
- parent_id: Some(parent.id.clone()),
174
+ content_id,
175
+ parent_id,
176
+ sequence,
121
177
  authority,
122
178
  timestamp_ms,
123
179
  metadata: BTreeMap::new(),
124
180
  }
125
181
  }
126
182
 
127
- /// Verify content matches id
183
+ /// Verify that both identities match what they are computed from.
128
184
  pub fn verify_integrity(&self) -> bool {
129
- StateId::compute(&self.content) == self.id
185
+ self.content_id == StateId::compute(&self.content)
186
+ && self.id
187
+ == compute_revision_id(
188
+ &self.resource,
189
+ &self.content_id,
190
+ self.parent_id.as_ref(),
191
+ self.sequence,
192
+ )
130
193
  }
131
194
 
132
- /// Get parent id if this is not initial state
195
+ /// Get parent id if this is not a resource's first revision.
133
196
  pub fn parent(&self) -> Option<&StateId> {
134
197
  self.parent_id.as_ref()
135
198
  }
136
199
 
137
- /// Check if two revisions are the same
200
+ /// Whether two revisions are the same revision.
201
+ ///
202
+ /// Revision identity, not content equality: two revisions of the same state
203
+ /// on the same resource are not equal.
138
204
  pub fn equals(&self, other: &StateRevision) -> bool {
139
- self.id == other.id && self.content == other.content
205
+ self.id == other.id
206
+ }
207
+
208
+ /// Whether two revisions hold the same state, regardless of where in
209
+ /// history they occur.
210
+ pub fn same_content_as(&self, other: &StateRevision) -> bool {
211
+ self.content_id == other.content_id
140
212
  }
141
213
  }
142
214
 
@@ -191,11 +263,19 @@ impl StateTopology {
191
263
  if a == b {
192
264
  return true;
193
265
  }
266
+ // The revision model makes this graph acyclic, so the visited set is a
267
+ // safety belt rather than the thing that makes the walk correct: a
268
+ // topology assembled by hand, or read from an older log, must not be
269
+ // able to hang a caller.
270
+ let mut visited = BTreeSet::new();
194
271
  let mut current = b.clone();
195
272
  while let Some(parent_id) = self.parents.get(&current) {
196
273
  if parent_id == a {
197
274
  return true;
198
275
  }
276
+ if !visited.insert(current.clone()) {
277
+ return false;
278
+ }
199
279
  current = parent_id.clone();
200
280
  }
201
281
  false
@@ -204,9 +284,13 @@ impl StateTopology {
204
284
  /// Get all ancestors of a revision (excluding self)
205
285
  pub fn ancestors(&self, id: &StateId) -> Vec<StateId> {
206
286
  let mut ancestors = Vec::new();
287
+ let mut visited = BTreeSet::new();
207
288
  let mut current = id.clone();
208
289
 
209
290
  while let Some(parent_id) = self.parents.get(&current) {
291
+ if !visited.insert(current.clone()) {
292
+ break;
293
+ }
210
294
  ancestors.push(parent_id.clone());
211
295
  current = parent_id.clone();
212
296
  }
@@ -222,11 +306,15 @@ impl StateTopology {
222
306
 
223
307
  let ancestors_a: BTreeSet<_> = self.ancestors(a).iter().cloned().collect();
224
308
 
309
+ let mut visited = BTreeSet::new();
225
310
  let mut current = b.clone();
226
311
  while let Some(parent_id) = self.parents.get(&current) {
227
312
  if ancestors_a.contains(parent_id) {
228
313
  return Some(parent_id.clone());
229
314
  }
315
+ if !visited.insert(current.clone()) {
316
+ break;
317
+ }
230
318
  current = parent_id.clone();
231
319
  }
232
320
 
@@ -275,6 +363,70 @@ pub enum PathComponent {
275
363
  Index(usize),
276
364
  }
277
365
 
366
+ /// How two changed paths relate to one another.
367
+ ///
368
+ /// Conflict classification needs this because two changes interact whenever one
369
+ /// path contains the other, not only when the paths are equal. Comparison is
370
+ /// structural, over whole [`PathComponent`]s: `["a", "b"]` and `["a", "bc"]` are
371
+ /// [`PathRelation::Disjoint`], even though one rendering of them shares a string
372
+ /// prefix, and `["a"]` with `Key("a")` never matches `Index(0)`.
373
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
374
+ pub enum PathRelation {
375
+ /// The same path.
376
+ Same,
377
+ /// The first path contains the second: `a` against `a.b`.
378
+ Ancestor,
379
+ /// The first path is contained by the second: `a.b` against `a`.
380
+ Descendant,
381
+ /// Neither contains the other: siblings, or unrelated subtrees.
382
+ Disjoint,
383
+ }
384
+
385
+ /// Whether `candidate` begins with every component of `prefix`, structurally.
386
+ fn path_starts_with(candidate: &[PathComponent], prefix: &[PathComponent]) -> bool {
387
+ candidate.len() >= prefix.len() && candidate[..prefix.len()] == *prefix
388
+ }
389
+
390
+ /// Classifies how two changed paths relate.
391
+ ///
392
+ /// Pure, total, and independent of any state: it reads only the two paths.
393
+ pub fn path_relation(one: &[PathComponent], two: &[PathComponent]) -> PathRelation {
394
+ if one == two {
395
+ PathRelation::Same
396
+ } else if path_starts_with(two, one) {
397
+ PathRelation::Ancestor
398
+ } else if path_starts_with(one, two) {
399
+ PathRelation::Descendant
400
+ } else {
401
+ PathRelation::Disjoint
402
+ }
403
+ }
404
+
405
+ /// Whether two changed paths interact: equal, or one containing the other.
406
+ ///
407
+ /// This is the predicate conflict classification is defined over. Two changes
408
+ /// at overlapping paths cannot both be applied — the outer one determines the
409
+ /// value the inner one was editing — so they are never independent.
410
+ pub fn paths_overlap(one: &[PathComponent], two: &[PathComponent]) -> bool {
411
+ !matches!(path_relation(one, two), PathRelation::Disjoint)
412
+ }
413
+
414
+ /// Reads the value a path names, or `None` when the path does not resolve.
415
+ ///
416
+ /// Object members are addressed by [`PathComponent::Key`] and array elements by
417
+ /// [`PathComponent::Index`], exactly as [`SemanticDiff`] emits them, so a path
418
+ /// taken from a diff resolves in the state that diff was computed from.
419
+ pub fn resolve_path<'a>(value: &'a Value, path: &[PathComponent]) -> Option<&'a Value> {
420
+ let mut current = value;
421
+ for component in path {
422
+ current = match component {
423
+ PathComponent::Key(key) => current.as_object()?.get(key)?,
424
+ PathComponent::Index(index) => current.as_array()?.get(*index)?,
425
+ };
426
+ }
427
+ Some(current)
428
+ }
429
+
278
430
  /// Semantic change at a specific path (deterministically ordered)
279
431
  #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
280
432
  pub struct SemanticChange {
@@ -305,10 +457,7 @@ impl SemanticDiff {
305
457
  Self::diff_recursive(old, new, &mut vec![], &mut changes);
306
458
 
307
459
  // Sort deterministically
308
- changes.sort_by(|a, b| {
309
- a.path.cmp(&b.path)
310
- .then_with(|| a.kind.cmp(&b.kind))
311
- });
460
+ changes.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.kind.cmp(&b.kind)));
312
461
 
313
462
  SemanticDiff {
314
463
  changes,
@@ -411,6 +560,11 @@ impl SemanticDiff {
411
560
  // ============================================================================
412
561
 
413
562
  /// Classification of conflict between two changes
563
+ ///
564
+ /// `Independent` carries the strong meaning its name and the state-model
565
+ /// graduation audit both assert: the two branches' changes at this path do not
566
+ /// overlap, so both can be applied without either overwriting or invalidating
567
+ /// the other. It is not merely "the paths differ" — see [`paths_overlap`].
414
568
  #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
415
569
  pub enum ConflictClass {
416
570
  /// Changes don't overlap; can merge automatically
@@ -426,8 +580,18 @@ pub enum ConflictClass {
426
580
  pub struct PathConflict {
427
581
  pub path: Vec<PathComponent>,
428
582
  pub classification: ConflictClass,
583
+ /// The common ancestor's value at this path, or `None` when the path does
584
+ /// not resolve in the base.
585
+ ///
586
+ /// Read from the base state itself. It was previously read from the left
587
+ /// branch's change, which reported `None` for a path only the right branch
588
+ /// touched even when the base held a value there.
429
589
  pub base_value: Option<Value>,
590
+ /// The left branch's value at this path, or `None` if the left branch did
591
+ /// not change it or removed it.
430
592
  pub left_value: Option<Value>,
593
+ /// The right branch's value at this path, or `None` if the right branch did
594
+ /// not change it or removed it.
431
595
  pub right_value: Option<Value>,
432
596
  }
433
597
 
@@ -442,69 +606,96 @@ pub struct ConflictClassification {
442
606
 
443
607
  impl ConflictClassification {
444
608
  /// Classify conflicts from three-way merge
445
- /// base: common ancestor, left: first branch, right: second branch
446
- pub fn classify(
447
- base: &StateRevision,
448
- left: &StateRevision,
449
- right: &StateRevision,
450
- ) -> Self {
609
+ ///
610
+ /// `base` is the common ancestor, `left` the first branch, `right` the
611
+ /// second. Only each revision's `content` is read.
612
+ ///
613
+ /// Two changes interact whenever their paths **overlap** — equal, or one
614
+ /// containing the other — not only when they are equal. A branch that
615
+ /// replaces `o` and a branch that edits `o.a` cannot both be applied: the
616
+ /// first determines the value the second was editing. Both such paths are
617
+ /// reported as [`ConflictClass::Conflict`].
618
+ ///
619
+ /// An overlap that is not at the same path can never be convergent. For a
620
+ /// descendant path to exist, the base must hold a container there; a change
621
+ /// reported *at* the ancestor path means that branch no longer holds a
622
+ /// container of that kind, while the branch editing inside it still does.
623
+ /// The two values therefore always differ. `classify` does not rely on that
624
+ /// argument — it compares the two branches' resulting values — but the
625
+ /// conformance suite asserts it, so the reasoning stays checked.
626
+ pub fn classify(base: &StateRevision, left: &StateRevision, right: &StateRevision) -> Self {
451
627
  use serde_json::Value;
452
628
 
453
- let base_json: Value = serde_json::from_str(&base.content)
454
- .unwrap_or(Value::Null);
455
- let left_json: Value = serde_json::from_str(&left.content)
456
- .unwrap_or(Value::Null);
457
- let right_json: Value = serde_json::from_str(&right.content)
458
- .unwrap_or(Value::Null);
629
+ let base_json: Value = serde_json::from_str(&base.content).unwrap_or(Value::Null);
630
+ let left_json: Value = serde_json::from_str(&left.content).unwrap_or(Value::Null);
631
+ let right_json: Value = serde_json::from_str(&right.content).unwrap_or(Value::Null);
459
632
 
460
- let base_diff = SemanticDiff::compute(&base_json, &left_json);
633
+ let left_diff = SemanticDiff::compute(&base_json, &left_json);
461
634
  let right_diff = SemanticDiff::compute(&base_json, &right_json);
462
635
 
463
636
  let mut path_conflicts = Vec::new();
464
637
  let mut has_conflict = false;
465
638
 
466
- // Convert changes to map for easier lookup
467
- let base_paths: BTreeMap<Vec<PathComponent>, &SemanticChange> =
468
- base_diff.changes.iter()
469
- .map(|c| (c.path.clone(), c))
470
- .collect();
471
- let right_paths: BTreeMap<Vec<PathComponent>, &SemanticChange> =
472
- right_diff.changes.iter()
473
- .map(|c| (c.path.clone(), c))
474
- .collect();
639
+ let left_paths: BTreeMap<Vec<PathComponent>, &SemanticChange> = left_diff
640
+ .changes
641
+ .iter()
642
+ .map(|c| (c.path.clone(), c))
643
+ .collect();
644
+ let right_paths: BTreeMap<Vec<PathComponent>, &SemanticChange> = right_diff
645
+ .changes
646
+ .iter()
647
+ .map(|c| (c.path.clone(), c))
648
+ .collect();
475
649
 
476
650
  // Find all unique paths
477
- let all_paths: BTreeSet<_> = base_paths.keys()
651
+ let all_paths: BTreeSet<_> = left_paths
652
+ .keys()
478
653
  .chain(right_paths.keys())
479
654
  .cloned()
480
655
  .collect();
481
656
 
482
657
  for path in all_paths {
483
- let left_change = base_paths.get(&path);
658
+ let left_change = left_paths.get(&path);
484
659
  let right_change = right_paths.get(&path);
485
660
 
486
661
  let classification = match (left_change, right_change) {
487
- (None, None) => ConflictClass::Independent,
488
- (Some(lc), None) => ConflictClass::Independent,
489
- (None, Some(rc)) => ConflictClass::Independent,
490
662
  (Some(lc), Some(rc)) => {
491
- if lc.kind == rc.kind
492
- && lc.new_value == rc.new_value
493
- {
663
+ if lc.kind == rc.kind && lc.new_value == rc.new_value {
494
664
  ConflictClass::Convergent
495
665
  } else {
496
666
  has_conflict = true;
497
667
  ConflictClass::Conflict
498
668
  }
499
669
  }
670
+ // Only one branch changed this path. It is independent only if
671
+ // the other branch changed nothing that contains it or that it
672
+ // contains.
673
+ (Some(_), None) => {
674
+ if overlapping_change_exists(&right_paths, &path) {
675
+ has_conflict = true;
676
+ ConflictClass::Conflict
677
+ } else {
678
+ ConflictClass::Independent
679
+ }
680
+ }
681
+ (None, Some(_)) => {
682
+ if overlapping_change_exists(&left_paths, &path) {
683
+ has_conflict = true;
684
+ ConflictClass::Conflict
685
+ } else {
686
+ ConflictClass::Independent
687
+ }
688
+ }
689
+ // Unreachable: every path came from one of the two maps.
690
+ (None, None) => ConflictClass::Independent,
500
691
  };
501
692
 
502
693
  path_conflicts.push(PathConflict {
503
- path,
694
+ path: path.clone(),
504
695
  classification,
505
- base_value: left_change.map(|c| c.old_value.clone()).flatten(),
506
- left_value: left_change.map(|c| c.new_value.clone()).flatten(),
507
- right_value: right_change.map(|c| c.new_value.clone()).flatten(),
696
+ base_value: resolve_path(&base_json, &path).cloned(),
697
+ left_value: left_change.and_then(|c| c.new_value.clone()),
698
+ right_value: right_change.and_then(|c| c.new_value.clone()),
508
699
  });
509
700
  }
510
701
 
@@ -513,7 +704,10 @@ impl ConflictClassification {
513
704
 
514
705
  let overall = if has_conflict {
515
706
  ConflictClass::Conflict
516
- } else if path_conflicts.iter().any(|c| c.classification == ConflictClass::Convergent) {
707
+ } else if path_conflicts
708
+ .iter()
709
+ .any(|c| c.classification == ConflictClass::Convergent)
710
+ {
517
711
  ConflictClass::Convergent
518
712
  } else {
519
713
  ConflictClass::Independent
@@ -526,6 +720,28 @@ impl ConflictClassification {
526
720
  }
527
721
  }
528
722
 
723
+ /// Whether `changes` holds any change at a path overlapping `path`, other than
724
+ /// at `path` itself.
725
+ ///
726
+ /// Ancestors are the proper prefixes of `path`, so there are at most as many as
727
+ /// the path is deep. Descendants are contiguous in sorted order — every
728
+ /// sequence beginning with `path` sorts together, directly after `path` — so the
729
+ /// first key strictly greater than `path` decides whether any exists.
730
+ fn overlapping_change_exists(
731
+ changes: &BTreeMap<Vec<PathComponent>, &SemanticChange>,
732
+ path: &[PathComponent],
733
+ ) -> bool {
734
+ for depth in 0..path.len() {
735
+ if changes.contains_key(&path[..depth].to_vec()) {
736
+ return true;
737
+ }
738
+ }
739
+ changes
740
+ .range(path.to_vec()..)
741
+ .find(|(candidate, _)| candidate.as_slice() != path)
742
+ .is_some_and(|(candidate, _)| path_starts_with(candidate, path))
743
+ }
744
+
529
745
  // ============================================================================
530
746
  // Reconciliation: Explicit Parent Choice Mechanism
531
747
  // ============================================================================
@@ -547,12 +763,7 @@ pub struct ReconciliationPlan {
547
763
 
548
764
  impl ReconciliationPlan {
549
765
  /// Create reconciliation plan with explicit parent choice
550
- pub fn new(
551
- left_id: StateId,
552
- right_id: StateId,
553
- base_id: StateId,
554
- parent_choice: bool,
555
- ) -> Self {
766
+ pub fn new(left_id: StateId, right_id: StateId, base_id: StateId, parent_choice: bool) -> Self {
556
767
  ReconciliationPlan {
557
768
  left_id,
558
769
  right_id,
@@ -586,301 +797,876 @@ pub struct StateReconciliationResult {
586
797
  pub plan: ReconciliationPlan,
587
798
  }
588
799
 
800
+ // ----------------------------------------------------------------------------
801
+ // Applying a reconciliation plan
802
+ // ----------------------------------------------------------------------------
803
+ //
804
+ // The plan says what the caller decided. Application produces the state that
805
+ // decision names, and nothing else:
806
+ //
807
+ // Classification — which changes can coexist?
808
+ // Plan — what happens to the ones that cannot?
809
+ // Application — produce the resulting state from that decision.
810
+ //
811
+ // Nothing here merges. Nothing here resolves a conflict on the caller's behalf.
812
+ // A path the plan does not mention keeps whatever the *selected parent* holds,
813
+ // which is the caller's decision too — it is what `parent_choice` selects.
814
+
815
+ /// The plan does not reference three usable revisions.
816
+ pub const RECONCILE_PLAN_INVALID: &str = "RECONCILE_PLAN_INVALID";
817
+ /// A supplied revision is not the one the plan names.
818
+ pub const RECONCILE_REVISION_MISMATCH: &str = "RECONCILE_REVISION_MISMATCH";
819
+ /// The selected parent's content is not JSON, so there is no state to start from.
820
+ pub const RECONCILE_CONTENT_UNPARSEABLE: &str = "RECONCILE_CONTENT_UNPARSEABLE";
821
+ /// Two override paths overlap, so the plan does not say what the result is.
822
+ pub const RECONCILE_OVERRIDE_PATHS_OVERLAP: &str = "RECONCILE_OVERRIDE_PATHS_OVERLAP";
823
+ /// An override path does not name an assignable location in the selected parent.
824
+ pub const RECONCILE_OVERRIDE_UNADDRESSABLE: &str = "RECONCILE_OVERRIDE_UNADDRESSABLE";
825
+
826
+ impl ReconciliationPlan {
827
+ /// The branch this plan selects as the state to start from.
828
+ ///
829
+ /// `parent_choice` is a `bool` and so has exactly two values: `true` is the
830
+ /// left branch, `false` the right. The ancestor is not a choice, and there
831
+ /// is no third value for this to reject.
832
+ pub fn selected_parent<'a>(
833
+ &self,
834
+ left: &'a StateRevision,
835
+ right: &'a StateRevision,
836
+ ) -> &'a StateRevision {
837
+ if self.parent_choice {
838
+ left
839
+ } else {
840
+ right
841
+ }
842
+ }
843
+
844
+ /// The conflicting paths this plan supplies no override for.
845
+ ///
846
+ /// Descriptive, not enforced. Application does **not** require an override
847
+ /// for every conflict: leaving one unaddressed resolves it to whatever the
848
+ /// selected parent holds, and selecting that parent is itself an explicit
849
+ /// caller decision. A caller that wants every conflict addressed
850
+ /// individually can require this to be empty before applying.
851
+ pub fn unresolved_conflicts(
852
+ &self,
853
+ classification: &ConflictClassification,
854
+ ) -> Vec<Vec<PathComponent>> {
855
+ classification
856
+ .path_conflicts
857
+ .iter()
858
+ .filter(|entry| entry.classification == ConflictClass::Conflict)
859
+ .map(|entry| entry.path.clone())
860
+ .filter(|path| !self.path_overrides.contains_key(path))
861
+ .collect()
862
+ }
863
+ }
864
+
865
+ /// Applies a reconciliation plan, producing canonical reconciled content.
866
+ ///
867
+ /// Pure and total with respect to its inputs: it reads the three revisions and
868
+ /// the plan, mutates none of them, consults no clock, filesystem, network or
869
+ /// global state, and returns either canonical JSON or a stable error code.
870
+ ///
871
+ /// # What it does
872
+ ///
873
+ /// 1. Checks the plan validates and names the three revisions supplied.
874
+ /// 2. Takes the state of the branch `parent_choice` selects.
875
+ /// 3. Assigns each override into it.
876
+ /// 4. Serializes the result canonically.
877
+ ///
878
+ /// # What it does not do
879
+ ///
880
+ /// It does not merge, and it does not consult the other branch or the ancestor
881
+ /// for anything but identity. A conflicting path with no override keeps the
882
+ /// selected parent's value.
883
+ ///
884
+ /// # Overrides
885
+ ///
886
+ /// `path_overrides` is a map keyed by structural path, so a repeated path
887
+ /// cannot exist — the type forecloses it. What the type does not foreclose is
888
+ /// two paths where one *contains* the other, and such a plan does not say what
889
+ /// the result should be: does the inner override refine the outer value, or was
890
+ /// the outer one meant to stand? Rather than invent a precedence the plan never
891
+ /// stated, overlapping override paths are rejected. Overlap is the same
892
+ /// structural relation conflict classification uses, so `a.b` and `a.bc` are
893
+ /// not an overlap.
894
+ ///
895
+ /// Because overlaps are rejected, the order overrides are applied in cannot
896
+ /// affect the result.
897
+ ///
898
+ /// An override path must be *addressable* in the selected state: every proper
899
+ /// prefix must already resolve to a container of the matching kind. Assigning a
900
+ /// member to an existing object is allowed whether or not the member is already
901
+ /// there, because it creates no structure. Assigning past the end of an array is
902
+ /// not, because it would require inventing the elements in between. To change an
903
+ /// array's length, or to remove a member, override the container itself — a
904
+ /// `Value` cannot express absence, so removal has no other encoding in this
905
+ /// plan.
906
+ pub fn apply_reconciliation_plan(
907
+ base: &StateRevision,
908
+ left: &StateRevision,
909
+ right: &StateRevision,
910
+ plan: &ReconciliationPlan,
911
+ ) -> Result<String, String> {
912
+ if !plan.validate() {
913
+ return Err(format!(
914
+ "{RECONCILE_PLAN_INVALID}: the plan does not reference three revisions"
915
+ ));
916
+ }
917
+ for (label, expected, supplied) in [
918
+ ("base", &plan.base_id, &base.id),
919
+ ("left", &plan.left_id, &left.id),
920
+ ("right", &plan.right_id, &right.id),
921
+ ] {
922
+ if expected != supplied {
923
+ return Err(format!(
924
+ "{RECONCILE_REVISION_MISMATCH}: the plan names a different {label} revision"
925
+ ));
926
+ }
927
+ }
928
+
929
+ let parent = plan.selected_parent(left, right);
930
+ let mut state: Value = serde_json::from_str(&parent.content).map_err(|error| {
931
+ // Classification tolerates unparseable content by reading it as JSON
932
+ // null, because it only describes. Materializing a state from content
933
+ // nobody can parse would be producing arbitrary state, so this refuses.
934
+ format!(
935
+ "{RECONCILE_CONTENT_UNPARSEABLE}: the selected parent's content is not JSON: {error}"
936
+ )
937
+ })?;
938
+
939
+ // `BTreeMap` sorts its keys, and every sequence with a given prefix sorts
940
+ // contiguously after it, so any containment relation shows up between two
941
+ // consecutive keys.
942
+ let paths: Vec<&Vec<PathComponent>> = plan.path_overrides.keys().collect();
943
+ for pair in paths.windows(2) {
944
+ if paths_overlap(pair[0], pair[1]) {
945
+ return Err(format!(
946
+ "{RECONCILE_OVERRIDE_PATHS_OVERLAP}: {:?} and {:?} overlap, so the plan does \
947
+ not say what the result is",
948
+ pair[0], pair[1]
949
+ ));
950
+ }
951
+ }
952
+
953
+ for (path, value) in &plan.path_overrides {
954
+ assign_path(&mut state, path, value.clone())?;
955
+ }
956
+
957
+ serde_json::to_string(&state)
958
+ .map_err(|error| format!("{RECONCILE_CONTENT_UNPARSEABLE}: {error}"))
959
+ }
960
+
961
+ /// Assigns `value` at `path`, or reports why the path is not assignable.
962
+ fn assign_path(state: &mut Value, path: &[PathComponent], value: Value) -> Result<(), String> {
963
+ let Some((last, prefix)) = path.split_last() else {
964
+ // The empty path is the state itself.
965
+ *state = value;
966
+ return Ok(());
967
+ };
968
+
969
+ let mut current = state;
970
+ for (depth, component) in prefix.iter().enumerate() {
971
+ current = match component {
972
+ PathComponent::Key(key) => current.as_object_mut().and_then(|o| o.get_mut(key)),
973
+ PathComponent::Index(index) => current.as_array_mut().and_then(|a| a.get_mut(*index)),
974
+ }
975
+ .ok_or_else(|| {
976
+ format!(
977
+ "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} does not resolve at \
978
+ component {depth} ({component:?})"
979
+ )
980
+ })?;
981
+ }
982
+
983
+ match last {
984
+ // A new member of an existing object creates no structure.
985
+ PathComponent::Key(key) => match current.as_object_mut() {
986
+ Some(members) => {
987
+ members.insert(key.clone(), value);
988
+ Ok(())
989
+ }
990
+ None => Err(format!(
991
+ "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} names a member of \
992
+ something that is not an object"
993
+ )),
994
+ },
995
+ // An index past the end would require inventing the elements before it.
996
+ PathComponent::Index(index) => match current.as_array_mut() {
997
+ Some(items) if *index < items.len() => {
998
+ items[*index] = value;
999
+ Ok(())
1000
+ }
1001
+ Some(items) => Err(format!(
1002
+ "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} is past the end of an \
1003
+ array of length {}",
1004
+ items.len()
1005
+ )),
1006
+ None => Err(format!(
1007
+ "{RECONCILE_OVERRIDE_UNADDRESSABLE}: {path:?} indexes something that \
1008
+ is not an array"
1009
+ )),
1010
+ },
1011
+ }
1012
+ }
1013
+
1014
+ /// Applies a plan and materializes the result as a new revision.
1015
+ ///
1016
+ /// The revision is built with the existing [`StateRevision::child`] constructor,
1017
+ /// so it carries the existing content-addressed identity and takes the selected
1018
+ /// parent as its parent. Nothing here is a new identity algorithm, and nothing
1019
+ /// here moves a branch head, writes to a store, or touches replication: the
1020
+ /// result is a value the caller decides what to do with.
1021
+ ///
1022
+ /// Unlike [`apply_reconciliation_plan`], this is not pure — `StateRevision`
1023
+ /// timestamps come from the clock, as they do for every other revision this
1024
+ /// model creates. The application semantics stay in the pure function.
1025
+ pub fn reconcile(
1026
+ base: &StateRevision,
1027
+ left: &StateRevision,
1028
+ right: &StateRevision,
1029
+ plan: &ReconciliationPlan,
1030
+ authority: String,
1031
+ ) -> Result<StateReconciliationResult, String> {
1032
+ let content = apply_reconciliation_plan(base, left, right, plan)?;
1033
+ let parent = plan.selected_parent(left, right);
1034
+ Ok(StateReconciliationResult {
1035
+ materialized_state: StateRevision::child(content, parent, authority),
1036
+ plan: plan.clone(),
1037
+ })
1038
+ }
1039
+
589
1040
  // ============================================================================
590
1041
  // ============================================================================
591
1042
  // StateStore: Durable State Persistence and Retrieval
592
1043
  // ============================================================================
593
1044
 
594
- /// State storage and retrieval operations
595
- ///
596
- /// StateStore integrates with FeltDB's canonical persistence layer for durability.
597
- /// Revisions are persisted through FeltDB's operation log with key schema:
598
- /// - "state:revision:{hex_id}" → StateRevision
599
- /// - "state:current" → current pointer
600
- /// - "state:branch:{name}" branch pointers
1045
+ /// Durable, content-addressed revision storage.
1046
+ ///
1047
+ /// `StateStore` answers exactly one question:
1048
+ ///
1049
+ /// > **Do I have revision X?**
1050
+ ///
1051
+ /// It deliberately does **not** answer *"what is the current revision?"*. That
1052
+ /// separation is the point of this type. State history and currentness are
1053
+ /// different concerns, and conflating them is what produced the two defects the
1054
+ /// branching audit recorded: a global `current` that moved on every commit
1055
+ /// regardless of which parent was named, and a branch map sharing one replicated
1056
+ /// record with it. Both lived in a single `state:current` row that nothing in
1057
+ /// the reconciliation chain ever read.
1058
+ ///
1059
+ /// # The contract
1060
+ ///
1061
+ /// | operation | method |
1062
+ /// | --- | --- |
1063
+ /// | put | [`create`](Self::create), [`commit`](Self::commit) |
1064
+ /// | get | [`get`](Self::get) |
1065
+ ///
1066
+ /// Ancestry is not a third operation: it is [`get`](Self::get) followed by
1067
+ /// `parent_id`, repeated. [`parent`](Self::parent), [`exists`](Self::exists) and
1068
+ /// [`metadata`](Self::metadata) are conveniences over `get`, not new powers.
1069
+ ///
1070
+ /// # Key schema
1071
+ ///
1072
+ /// - `state:revision:{hex_id}` → [`StateRevision`]
1073
+ ///
1074
+ /// That is the entire schema. A record keyed by the hash of its own content
1075
+ /// cannot be stale and needs no reconstruction, so there is **no recovery
1076
+ /// path**: [`with_feltdb`](Self::with_feltdb) does not scan, rebuild an index,
1077
+ /// or validate history before returning.
1078
+ ///
1079
+ /// # What this deliberately does not provide
1080
+ ///
1081
+ /// No `current`, no branches, no branch heads, no "latest". Nothing above the
1082
+ /// history layer is invented here. Something must eventually own *"which
1083
+ /// revision represents the current application state"* — a ref, an application
1084
+ /// record, a per-resource pointer, a transaction head — and that owner is not
1085
+ /// this type. Until it exists, callers hold the ids they care about.
601
1086
  ///
602
- /// For production use, StateStore MUST be initialized with FeltDB via
603
- /// `StateStore::with_feltdb()`. This ensures all mutations are persisted
604
- /// through FeltDB's canonical operation log.
1087
+ /// # Non-claims
605
1088
  ///
606
- /// For testing and validation of state semantics alone, StateStore::new_volatile()
607
- /// creates a non-persistent in-memory store.
1089
+ /// Proven for this type: durable single-writer append, retrieval by identity
1090
+ /// across restart, ancestry by parent traversal, and independence between
1091
+ /// records. **Not** proven and not claimed: multi-writer persistence semantics,
1092
+ /// concurrent revision creation, authorization, resource isolation, garbage
1093
+ /// collection, server lifecycle integration, current/head semantics, and
1094
+ /// distributed replication of revision history.
608
1095
  pub struct StateStore {
609
- revisions: Arc<Mutex<HashMap<StateId, StateRevision>>>,
610
- current_id: Arc<Mutex<Option<StateId>>>,
611
- branches: Arc<Mutex<HashMap<String, StateId>>>,
612
- /// FeltDB reference for durable persistence
613
- /// For production: REQUIRED (Some)
614
- /// For testing: Optional (None)
615
- /// Mutations silently succeed with None, losing persistence.
616
- /// Production code must verify Some before use.
617
- feltdb: Option<Arc<crate::FeltDb>>,
1096
+ backing: Backing,
618
1097
  }
619
1098
 
620
- impl StateStore {
621
- /// Create a new volatile (in-memory) state store
622
- ///
623
- /// This is for testing state semantics in isolation. Mutations
624
- /// are stored in memory only and lost when the store is dropped.
625
- ///
626
- /// Do NOT use in production. Production must use `with_feltdb()`.
627
- pub fn new_volatile() -> Self {
628
- StateStore {
629
- revisions: Arc::new(Mutex::new(HashMap::new())),
630
- current_id: Arc::new(Mutex::new(None)),
631
- branches: Arc::new(Mutex::new(HashMap::new())),
632
- feltdb: None,
1099
+ /// Where revisions actually live.
1100
+ ///
1101
+ /// There is exactly one copy of a revision in either mode. The durable mode
1102
+ /// keeps no second in-memory index beside FeltDB's own rows.
1103
+ enum Backing {
1104
+ /// In-memory only, for testing state semantics in isolation.
1105
+ Volatile(Arc<Mutex<VolatileState>>),
1106
+ /// FeltDB is the storage, and keyed reads are the authoritative read path.
1107
+ Durable(Arc<crate::FeltDb>),
1108
+ }
1109
+
1110
+ /// The volatile backing's contents: revisions, and per-resource retention.
1111
+ #[derive(Default)]
1112
+ struct VolatileState {
1113
+ revisions: HashMap<StateId, StateRevision>,
1114
+ retention: HashMap<String, RetentionState>,
1115
+ }
1116
+
1117
+ impl Clone for Backing {
1118
+ fn clone(&self) -> Self {
1119
+ match self {
1120
+ Backing::Volatile(map) => Backing::Volatile(map.clone()),
1121
+ Backing::Durable(db) => Backing::Durable(db.clone()),
633
1122
  }
634
1123
  }
1124
+ }
635
1125
 
636
- /// Create a new state store backed by FeltDB's canonical persistence
637
- ///
638
- /// This will:
639
- /// - Recover all previously persisted state revisions from FeltDB
640
- /// - Restore the current pointer
641
- /// - Restore all branch references
642
- /// - Validate recovered state integrity
643
- /// - Persist all subsequent mutations through FeltDB's operation log
644
- ///
645
- /// # Arguments
646
- /// * `feltdb` - Arc<FeltDb> instance for durable storage
647
- ///
648
- /// # Returns
649
- /// * `Ok(StateStore)` - Successfully initialized store with FeltDB backing and recovered state
650
- /// * `Err(String)` - If initialization or recovery fails
651
- ///
652
- /// # Recovery Process
653
- ///
654
- /// Recovery reconstructs the full state model from FeltDB records:
655
- /// 1. Queries all StateRevision objects and deserializes them
656
- /// 2. Retrieves "state:current" to restore the current pointer and branches
657
- /// 3. Validates all parent references point to existing revisions
658
- /// 4. Validates each StateRevision's content matches its StateId
659
- /// 5. Validates topology consistency
1126
+ /// The durable key for a revision.
1127
+ fn revision_key(id: &StateId) -> String {
1128
+ format!("state:revision:{}", id.as_hex())
1129
+ }
1130
+
1131
+ /// Where a resource's retention watermark lives.
1132
+ fn retention_key(resource: &str) -> String {
1133
+ format!("_retention:{resource}")
1134
+ }
1135
+
1136
+ /// How much of a resource's history is kept.
1137
+ ///
1138
+ /// Count-based only. Age-based retention is deliberately not implemented:
1139
+ /// nothing in the system needs it yet, and inventing a second policy dimension
1140
+ /// before the first has been measured would be guesswork.
1141
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1142
+ pub struct RetentionPolicy {
1143
+ /// Newest revisions to keep, or `None` to keep every revision.
660
1144
  ///
661
- /// If recovery encounters any malformed records or missing references,
662
- /// it returns an error and does not create a partially-recovered store.
663
- pub fn with_feltdb(feltdb: Arc<crate::FeltDb>) -> Result<Self, String> {
664
- // Step 1: Recover all state revisions from FeltDB
665
- // Query returns all StateRevision objects stored in FeltDB
666
- let revisions: Vec<StateRevision> = feltdb
667
- .query(|_rev: &StateRevision| true)
668
- .map_err(|e| format!("Failed to query revisions from FeltDB: {}", e))?;
669
-
670
- let mut revisions_map = HashMap::new();
671
- for revision in revisions {
672
- // Validate content matches id (this is the integrity check)
673
- if !revision.verify_integrity() {
674
- return Err(format!(
675
- "StateId mismatch during recovery: stored={}, content_hash={}",
676
- revision.id.as_hex(),
677
- StateId::compute(&revision.content).as_hex()
678
- ));
679
- }
1145
+ /// Clamped to at least 1: **a resource's current revision always survives.**
1146
+ pub keep_last: Option<usize>,
1147
+ }
680
1148
 
681
- // Validate parent references exist (if parent is specified)
682
- if let Some(_parent_id) = &revision.parent_id {
683
- // Parent will be validated once all revisions are loaded
684
- // (parent may be recovered after child in query result)
685
- }
1149
+ impl RetentionPolicy {
1150
+ /// Keep everything. The default, because silently discarding history would
1151
+ /// be a worse surprise than unbounded growth.
1152
+ pub const fn unbounded() -> Self {
1153
+ RetentionPolicy { keep_last: None }
1154
+ }
686
1155
 
687
- revisions_map.insert(revision.id.clone(), revision);
1156
+ /// Keep the newest `count` revisions of a resource.
1157
+ pub fn keep_last(count: usize) -> Self {
1158
+ RetentionPolicy {
1159
+ keep_last: Some(count.max(1)),
688
1160
  }
1161
+ }
1162
+ }
689
1163
 
690
- // Step 2: Validate parent references after all revisions are loaded
691
- for revision in revisions_map.values() {
692
- if let Some(parent_id) = &revision.parent_id {
693
- if !revisions_map.contains_key(parent_id) {
694
- return Err(format!(
695
- "Parent reference missing during recovery: revision={}, parent={}",
696
- revision.id.as_hex(),
697
- parent_id.as_hex()
698
- ));
699
- }
700
- }
701
- }
1164
+ impl Default for RetentionPolicy {
1165
+ fn default() -> Self {
1166
+ Self::unbounded()
1167
+ }
1168
+ }
702
1169
 
703
- // Step 3: Recover current pointer and branches from FeltDB
704
- // Current pointer is stored as: ("state:current", (current_id_hex, branches_map))
705
- let mut current_id = None;
706
- let mut branches_map = HashMap::new();
1170
+ /// A resource's retention state: its policy, and how far its history has been
1171
+ /// expired.
1172
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1173
+ struct RetentionState {
1174
+ policy: RetentionPolicy,
1175
+ /// The lowest sequence still retained. Everything below it was expired on
1176
+ /// purpose, and is distinguishable from a parent that never existed.
1177
+ horizon: u64,
1178
+ }
707
1179
 
708
- if let Ok(Some((current_id_hex, recovered_branches))) =
709
- feltdb.get::<(String, HashMap<String, String>)>("state:current")
710
- {
711
- // Parse current ID
712
- if !current_id_hex.is_empty() {
713
- let parsed_id = StateId::from_hex(current_id_hex.clone());
714
-
715
- // Validate current pointer exists in revisions
716
- if !revisions_map.contains_key(&parsed_id) {
717
- return Err(format!(
718
- "Current pointer references non-existent revision during recovery: {}",
719
- current_id_hex
720
- ));
721
- }
1180
+ /// Decide what a retention policy expires, given a resource's history.
1181
+ ///
1182
+ /// Pure, and the single definition of what retention *means*, so the explicit
1183
+ /// store API and the automatic mint at the write boundary cannot drift apart.
1184
+ ///
1185
+ /// `history` is `(id, sequence)` oldest first. Returns the revisions to expire
1186
+ /// and the new horizon — the lowest sequence still retained.
1187
+ ///
1188
+ /// Deletion is from the oldest end only, so what survives is always a suffix of
1189
+ /// the resource's timeline and **the current revision always survives**.
1190
+ pub fn revisions_to_expire(
1191
+ history: &[(StateId, u64)],
1192
+ keep_last: Option<usize>,
1193
+ ) -> (Vec<StateId>, Option<u64>) {
1194
+ let Some(keep) = keep_last else {
1195
+ return (Vec::new(), None);
1196
+ };
1197
+ let keep = keep.max(1);
1198
+ if history.len() <= keep {
1199
+ return (Vec::new(), None);
1200
+ }
1201
+ let split = history.len() - keep;
1202
+ let expire = history[..split].iter().map(|(id, _)| id.clone()).collect();
1203
+ let horizon = history[split].1;
1204
+ (expire, Some(horizon))
1205
+ }
1206
+
1207
+ /// Why a revision could not be minted.
1208
+ #[derive(Clone, Debug, PartialEq, Eq)]
1209
+ pub enum RevisionError {
1210
+ /// Invariant 2: the named parent is not a revision this store holds.
1211
+ UnknownParent(StateId),
1212
+ /// Invariant 1: that identity is already committed, and commits are final.
1213
+ ///
1214
+ /// Under the resource-scoped model this should be unreachable through
1215
+ /// `mint`, because the sequence is allocated from the resource's own head.
1216
+ /// It is enforced anyway: immutability is the invariant, not a consequence.
1217
+ AlreadyCommitted(StateId),
1218
+ /// A parent belonging to a different resource.
1219
+ ForeignParent { expected: String, found: String },
1220
+ /// The resource already has a first revision; a resource has one beginning.
1221
+ ResourceAlreadyBegun { resource: String, head: StateId },
1222
+ /// The revision's own identity does not match what it is computed from.
1223
+ IntegrityFailed,
1224
+ /// The backing store could not be read or written.
1225
+ Storage(String),
1226
+ }
722
1227
 
723
- current_id = Some(parsed_id);
1228
+ impl std::fmt::Display for RevisionError {
1229
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1230
+ match self {
1231
+ RevisionError::UnknownParent(id) => {
1232
+ write!(f, "parent {id} is not a revision this store holds")
1233
+ }
1234
+ RevisionError::AlreadyCommitted(id) => {
1235
+ write!(
1236
+ f,
1237
+ "revision {id} is already committed and cannot be rewritten"
1238
+ )
1239
+ }
1240
+ RevisionError::ForeignParent { expected, found } => {
1241
+ write!(f, "parent belongs to resource {found}, not {expected}")
1242
+ }
1243
+ RevisionError::ResourceAlreadyBegun { resource, head } => {
1244
+ write!(f, "resource {resource} already begins at revision {head}")
724
1245
  }
1246
+ RevisionError::IntegrityFailed => write!(f, "state integrity verification failed"),
1247
+ RevisionError::Storage(message) => write!(f, "{message}"),
1248
+ }
1249
+ }
1250
+ }
725
1251
 
726
- // Parse branch references
727
- for (branch_name, branch_id_hex) in recovered_branches {
728
- let parsed_id = StateId::from_hex(branch_id_hex.clone());
1252
+ impl std::error::Error for RevisionError {}
729
1253
 
730
- // Validate branch target exists in revisions
731
- if !revisions_map.contains_key(&parsed_id) {
732
- return Err(format!(
733
- "Branch references non-existent revision during recovery: branch={}, target={}",
734
- branch_name, branch_id_hex
735
- ));
736
- }
1254
+ /// What following a revision's parent link found.
1255
+ ///
1256
+ /// The three outcomes are deliberately distinct. An **expired** parent is a
1257
+ /// retention decision the system made on purpose; an **unknown** one is a bug.
1258
+ /// Collapsing them would recreate the dangling-parent defect at the other end
1259
+ /// of the system, where retention rather than minting produced it.
1260
+ #[derive(Clone, Debug)]
1261
+ pub enum ParentLookup {
1262
+ /// This revision began its resource's history.
1263
+ Root,
1264
+ /// The parent, still retained.
1265
+ Revision(Box<StateRevision>),
1266
+ /// The parent was expired by retention. Its identity is still named by the
1267
+ /// child, and the child's ancestry is still true — it is simply no longer
1268
+ /// stored.
1269
+ Expired(StateId),
1270
+ /// The parent is named but absent and *not* below the retention horizon.
1271
+ /// The model does not permit minting one; finding one means damage.
1272
+ Missing(StateId),
1273
+ }
737
1274
 
738
- branches_map.insert(branch_name, parsed_id);
739
- }
1275
+ impl ParentLookup {
1276
+ /// The identity this lookup names, if it names one.
1277
+ pub fn id(&self) -> Option<&StateId> {
1278
+ match self {
1279
+ ParentLookup::Root => None,
1280
+ ParentLookup::Revision(revision) => Some(&revision.id),
1281
+ ParentLookup::Expired(id) | ParentLookup::Missing(id) => Some(id),
740
1282
  }
1283
+ }
1284
+ }
1285
+
1286
+ /// Equality is over the outcome and the identity named, not the whole revision.
1287
+ impl PartialEq for ParentLookup {
1288
+ fn eq(&self, other: &Self) -> bool {
1289
+ std::mem::discriminant(self) == std::mem::discriminant(other) && self.id() == other.id()
1290
+ }
1291
+ }
741
1292
 
742
- // Step 4: Validate topology consistency
743
- // If there are revisions but no current pointer, that's an error
744
- if !revisions_map.is_empty() && current_id.is_none() {
745
- return Err("Recovered revisions but current pointer is missing".to_string());
1293
+ impl StateStore {
1294
+ /// A volatile (in-memory) store, for testing state semantics in isolation.
1295
+ ///
1296
+ /// Revisions are lost when the store is dropped. Production must use
1297
+ /// [`with_feltdb`](Self::with_feltdb).
1298
+ pub fn new_volatile() -> Self {
1299
+ StateStore {
1300
+ backing: Backing::Volatile(Arc::new(Mutex::new(VolatileState::default()))),
746
1301
  }
1302
+ }
747
1303
 
748
- // Step 5: Return recovered StateStore
1304
+ /// A store backed by FeltDB's canonical persistence.
1305
+ ///
1306
+ /// This performs **no recovery**. Revisions are records under
1307
+ /// `state:revision:{hex}`; they are already durable, so there is no index to
1308
+ /// rebuild and nothing to validate up front. A revision's integrity is
1309
+ /// checked when it is written, and can be rechecked by whoever reads it.
1310
+ ///
1311
+ /// The `Result` is retained because callers depend on it; today it is always
1312
+ /// `Ok`.
1313
+ pub fn with_feltdb(feltdb: Arc<crate::FeltDb>) -> Result<Self, String> {
749
1314
  Ok(StateStore {
750
- revisions: Arc::new(Mutex::new(revisions_map)),
751
- current_id: Arc::new(Mutex::new(current_id)),
752
- branches: Arc::new(Mutex::new(branches_map)),
753
- feltdb: Some(feltdb),
1315
+ backing: Backing::Durable(feltdb),
754
1316
  })
755
1317
  }
756
1318
 
757
- /// Create and store initial state
1319
+ // -- minting ------------------------------------------------------------
1320
+
1321
+ /// Begin a resource's history.
1322
+ ///
1323
+ /// Refused if the resource already has one. A resource has a single
1324
+ /// beginning; without that, `history_of` would return a forest rather than
1325
+ /// a chain and "the previous state of this resource" would be ambiguous
1326
+ /// again. To extend an existing history, use [`commit`](Self::commit) or
1327
+ /// [`mint`](Self::mint).
758
1328
  pub fn create(
759
1329
  &self,
1330
+ resource: &str,
760
1331
  content: String,
761
1332
  authority: String,
762
- ) -> Result<StateRevision, String> {
763
- let revision = StateRevision::initial(content, authority);
764
- self.commit_revision(revision)
1333
+ ) -> Result<StateRevision, RevisionError> {
1334
+ if let Some(head) = self.head_of(resource) {
1335
+ return Err(RevisionError::ResourceAlreadyBegun {
1336
+ resource: resource.to_string(),
1337
+ head: head.id,
1338
+ });
1339
+ }
1340
+ self.mint(resource, content, None, authority)
765
1341
  }
766
1342
 
767
- /// Commit a new revision
1343
+ /// Commit a revision descending from `parent`, on the parent's resource.
768
1344
  pub fn commit(
769
1345
  &self,
770
1346
  content: String,
771
1347
  parent: &StateRevision,
772
1348
  authority: String,
773
- ) -> Result<StateRevision, String> {
774
- let revision = StateRevision::child(content, parent, authority);
775
- self.commit_revision(revision)
1349
+ ) -> Result<StateRevision, RevisionError> {
1350
+ let resource = parent.resource.clone();
1351
+ self.mint(&resource, content, Some(parent), authority)
776
1352
  }
777
1353
 
778
- /// Internal: commit revision and update current pointer
779
- fn commit_revision(&self, revision: StateRevision) -> Result<StateRevision, String> {
780
- if !revision.verify_integrity() {
781
- return Err("State integrity verification failed".to_string());
1354
+ /// Mint a revision of `resource`, optionally descending from `parent`.
1355
+ ///
1356
+ /// Enforces both invariants at the only point where they can be enforced:
1357
+ ///
1358
+ /// 1. A parent must resolve to a revision this store holds, and must belong
1359
+ /// to the same resource. Parent existence is part of what makes a
1360
+ /// revision valid, not a lookup applied afterwards.
1361
+ /// 2. A committed revision is never rewritten. The sequence is allocated
1362
+ /// from the resource's own head, so a state reached twice is two
1363
+ /// revisions with two identities.
1364
+ ///
1365
+ /// Retention is applied to the resource afterwards, so a configured policy
1366
+ /// bounds history at the moment history is created rather than at some
1367
+ /// later maintenance pass that may never run.
1368
+ pub fn mint(
1369
+ &self,
1370
+ resource: &str,
1371
+ content: String,
1372
+ parent: Option<&StateRevision>,
1373
+ authority: String,
1374
+ ) -> Result<StateRevision, RevisionError> {
1375
+ if let Some(parent) = parent {
1376
+ if parent.resource != resource {
1377
+ return Err(RevisionError::ForeignParent {
1378
+ expected: resource.to_string(),
1379
+ found: parent.resource.clone(),
1380
+ });
1381
+ }
1382
+ if !self.exists(&parent.id) {
1383
+ return Err(RevisionError::UnknownParent(parent.id.clone()));
1384
+ }
782
1385
  }
783
1386
 
784
- let mut revisions = self.revisions.lock().unwrap();
785
- let id = revision.id.clone();
786
- revisions.insert(id.clone(), revision.clone());
787
-
788
- let mut current = self.current_id.lock().unwrap();
789
- *current = Some(id.clone());
790
-
791
- // Persist through FeltDB if available (production must provide FeltDB)
792
- if let Some(feltdb) = &self.feltdb {
793
- let revision_key = format!("state:revision:{}", id.as_hex());
794
-
795
- // Persist the revision through FeltDB's operation log
796
- feltdb.insert(&revision_key, revision.clone())
797
- .map_err(|e| format!("Failed to persist revision to FeltDB: {}", e))?;
1387
+ let sequence = match parent {
1388
+ Some(parent) => parent.sequence + 1,
1389
+ None => 0,
1390
+ };
1391
+ let revision = StateRevision::at(
1392
+ resource.to_string(),
1393
+ content,
1394
+ parent.map(|parent| parent.id.clone()),
1395
+ sequence,
1396
+ authority,
1397
+ );
1398
+ let committed = self.commit_revision(revision)?;
1399
+ self.apply_retention(resource)?;
1400
+ Ok(committed)
1401
+ }
798
1402
 
799
- // Persist current pointer through FeltDB
800
- let branches = self.branches.lock().unwrap();
801
- let current_pointer = (id.as_hex().to_string(), branches.clone());
802
- feltdb.insert("state:current", current_pointer)
803
- .map_err(|e| format!("Failed to persist current pointer to FeltDB: {}", e))?;
1403
+ /// Store a revision, after checking its integrity and that it is new.
1404
+ fn commit_revision(&self, revision: StateRevision) -> Result<StateRevision, RevisionError> {
1405
+ if !revision.verify_integrity() {
1406
+ return Err(RevisionError::IntegrityFailed);
1407
+ }
1408
+ // Re-committing an identical historical fact is a no-op, not a rewrite:
1409
+ // the identity is a function of the resource, the content, the parent
1410
+ // and the sequence, so an existing revision with this identity records
1411
+ // exactly what is being committed. Returning it keeps immutability
1412
+ // without making a harmless repeat an error.
1413
+ if let Some(existing) = self.get(&revision.id) {
1414
+ let same = existing.resource == revision.resource
1415
+ && existing.content_id == revision.content_id
1416
+ && existing.parent_id == revision.parent_id
1417
+ && existing.sequence == revision.sequence;
1418
+ return if same {
1419
+ Ok(existing)
1420
+ } else {
1421
+ Err(RevisionError::AlreadyCommitted(revision.id.clone()))
1422
+ };
1423
+ }
1424
+ match &self.backing {
1425
+ Backing::Volatile(state) => {
1426
+ state
1427
+ .lock()
1428
+ .unwrap()
1429
+ .revisions
1430
+ .insert(revision.id.clone(), revision.clone());
1431
+ }
1432
+ Backing::Durable(feltdb) => {
1433
+ feltdb
1434
+ .insert(&revision_key(&revision.id), revision.clone())
1435
+ .map_err(|error| {
1436
+ RevisionError::Storage(format!(
1437
+ "Failed to persist revision to FeltDB: {error}"
1438
+ ))
1439
+ })?;
1440
+ }
804
1441
  }
805
-
806
1442
  Ok(revision)
807
1443
  }
808
1444
 
809
- /// Get current state
810
- pub fn current(&self) -> Option<StateRevision> {
811
- let current_id = self.current_id.lock().unwrap();
812
- if let Some(id) = &*current_id {
813
- let revisions = self.revisions.lock().unwrap();
814
- revisions.get(id).cloned()
815
- } else {
816
- None
817
- }
818
- }
1445
+ // -- reading ------------------------------------------------------------
819
1446
 
820
- /// Get revision by id
1447
+ /// Retrieve a revision by identity. This is the authoritative read path.
821
1448
  pub fn get(&self, id: &StateId) -> Option<StateRevision> {
822
- let revisions = self.revisions.lock().unwrap();
823
- revisions.get(id).cloned()
1449
+ match &self.backing {
1450
+ Backing::Volatile(state) => state.lock().unwrap().revisions.get(id).cloned(),
1451
+ Backing::Durable(feltdb) => feltdb
1452
+ .get::<StateRevision>(&revision_key(id))
1453
+ .ok()
1454
+ .flatten(),
1455
+ }
824
1456
  }
825
1457
 
826
- /// Check if revision exists
1458
+ /// Whether a revision is present.
827
1459
  pub fn exists(&self, id: &StateId) -> bool {
828
- let revisions = self.revisions.lock().unwrap();
829
- revisions.contains_key(id)
1460
+ self.get(id).is_some()
830
1461
  }
831
1462
 
832
- /// Get metadata for a revision
1463
+ /// Metadata for a revision.
833
1464
  pub fn metadata(&self, id: &StateId) -> Option<BTreeMap<String, Value>> {
834
- let revisions = self.revisions.lock().unwrap();
835
- revisions.get(id).map(|r| r.metadata.clone())
1465
+ self.get(id).map(|revision| revision.metadata)
836
1466
  }
837
1467
 
838
- /// Get parent of a revision
1468
+ /// The parent of a revision, and — when it is not there — why.
1469
+ pub fn parent_of(&self, id: &StateId) -> Option<ParentLookup> {
1470
+ let revision = self.get(id)?;
1471
+ let Some(parent_id) = revision.parent_id else {
1472
+ return Some(ParentLookup::Root);
1473
+ };
1474
+ if let Some(parent) = self.get(&parent_id) {
1475
+ return Some(ParentLookup::Revision(Box::new(parent)));
1476
+ }
1477
+ let horizon = self.retention_state(&revision.resource).horizon;
1478
+ Some(if revision.sequence <= horizon {
1479
+ ParentLookup::Expired(parent_id)
1480
+ } else {
1481
+ ParentLookup::Missing(parent_id)
1482
+ })
1483
+ }
1484
+
1485
+ /// The parent of a revision, if it is still retained.
1486
+ ///
1487
+ /// Repeating this walks the ancestry. It is a traversal, not a primitive.
1488
+ /// Use [`parent_of`](Self::parent_of) to tell a resource's beginning from a
1489
+ /// horizon.
839
1490
  pub fn parent(&self, id: &StateId) -> Option<StateRevision> {
840
- let revisions = self.revisions.lock().unwrap();
841
- if let Some(revision) = revisions.get(id) {
842
- if let Some(parent_id) = &revision.parent_id {
843
- return revisions.get(parent_id).cloned();
844
- }
1491
+ match self.parent_of(id)? {
1492
+ ParentLookup::Revision(parent) => Some(*parent),
1493
+ _ => None,
845
1494
  }
846
- None
847
1495
  }
848
1496
 
849
- /// Create a named branch
850
- pub fn create_branch(&self, name: String, revision_id: StateId) -> Result<(), String> {
851
- let mut branches = self.branches.lock().unwrap();
852
- branches.insert(name.clone(), revision_id.clone());
1497
+ /// Every retained revision of one resource, oldest first.
1498
+ pub fn history_of(&self, resource: &str) -> Vec<StateRevision> {
1499
+ let mut history: Vec<StateRevision> = self
1500
+ .all_revisions()
1501
+ .into_iter()
1502
+ .filter(|revision| revision.resource == resource)
1503
+ .collect();
1504
+ history.sort_by_key(|revision| revision.sequence);
1505
+ history
1506
+ }
853
1507
 
854
- // Persist branches through FeltDB if available
855
- if let Some(feltdb) = &self.feltdb {
856
- let current_id = self.current_id.lock().unwrap();
857
-
858
- // Update state:current with new branches map to ensure recovery captures all branches
859
- let current_pointer = (
860
- current_id.as_ref().map(|id| id.as_hex().to_string()).unwrap_or_default(),
861
- branches.clone()
862
- );
863
- feltdb.insert("state:current", current_pointer)
864
- .map_err(|e| format!("Failed to persist branch to state:current: {}", e))?;
1508
+ /// A resource's newest retained revision.
1509
+ ///
1510
+ /// Automatic minting produces a chain, so this is unambiguous for anything
1511
+ /// the write path created. Explicit `commit` may fork a resource — which is
1512
+ /// exactly what divergence and three-way reconciliation need and where it
1513
+ /// has, this is the newest by sequence, ties broken by identity so the
1514
+ /// answer is deterministic rather than incidental.
1515
+ pub fn head_of(&self, resource: &str) -> Option<StateRevision> {
1516
+ self.history_of(resource).into_iter().max_by(|one, two| {
1517
+ one.sequence
1518
+ .cmp(&two.sequence)
1519
+ .then_with(|| one.id.as_hex().cmp(two.id.as_hex()))
1520
+ })
1521
+ }
1522
+
1523
+ /// Every resource this store holds history for.
1524
+ pub fn resources(&self) -> Vec<String> {
1525
+ let mut names: Vec<String> = self
1526
+ .all_revisions()
1527
+ .into_iter()
1528
+ .map(|revision| revision.resource)
1529
+ .collect();
1530
+ names.sort();
1531
+ names.dedup();
1532
+ names
1533
+ }
1534
+
1535
+ fn all_revisions(&self) -> Vec<StateRevision> {
1536
+ match &self.backing {
1537
+ Backing::Volatile(state) => state.lock().unwrap().revisions.values().cloned().collect(),
1538
+ Backing::Durable(feltdb) => feltdb
1539
+ .list_collection("state")
1540
+ .unwrap_or_default()
1541
+ .into_iter()
1542
+ .filter(|row| row.key.starts_with("state:revision:") && !row.deleted)
1543
+ .filter_map(|row| serde_json::from_value::<StateRevision>(row.value).ok())
1544
+ .collect(),
865
1545
  }
1546
+ }
1547
+
1548
+ // -- retention ----------------------------------------------------------
866
1549
 
1550
+ /// The retention policy in force for a resource.
1551
+ pub fn retention_policy(&self, resource: &str) -> RetentionPolicy {
1552
+ self.retention_state(resource).policy
1553
+ }
1554
+
1555
+ /// The lowest sequence still retained for a resource.
1556
+ ///
1557
+ /// Revisions below it were expired deliberately. This is what separates an
1558
+ /// expired parent from a missing one.
1559
+ pub fn retention_horizon(&self, resource: &str) -> u64 {
1560
+ self.retention_state(resource).horizon
1561
+ }
1562
+
1563
+ /// Set a resource's retention policy, and apply it immediately.
1564
+ ///
1565
+ /// Returns how many revisions the policy expired on application.
1566
+ pub fn set_retention_policy(
1567
+ &self,
1568
+ resource: &str,
1569
+ policy: RetentionPolicy,
1570
+ ) -> Result<usize, RevisionError> {
1571
+ let mut state = self.retention_state(resource);
1572
+ state.policy = policy;
1573
+ self.write_retention_state(resource, &state)?;
1574
+ self.apply_retention(resource)
1575
+ }
1576
+
1577
+ /// Expire whatever the resource's policy no longer retains.
1578
+ ///
1579
+ /// Deletes from the **oldest** end only, so the surviving history is always
1580
+ /// a suffix of the resource's timeline and the current revision always
1581
+ /// survives. A surviving revision's `parent_id` is never rewritten — that
1582
+ /// would violate immutability — so the horizon is recorded instead, and the
1583
+ /// oldest survivor reports an `Expired` parent rather than a missing one.
1584
+ pub fn apply_retention(&self, resource: &str) -> Result<usize, RevisionError> {
1585
+ let mut state = self.retention_state(resource);
1586
+ if state.policy.keep_last.is_none() {
1587
+ return Ok(0);
1588
+ }
1589
+ let history: Vec<(StateId, u64)> = self
1590
+ .history_of(resource)
1591
+ .into_iter()
1592
+ .map(|revision| (revision.id, revision.sequence))
1593
+ .collect();
1594
+ let (expire, horizon) = revisions_to_expire(&history, state.policy.keep_last);
1595
+ if expire.is_empty() {
1596
+ return Ok(0);
1597
+ }
1598
+ for id in &expire {
1599
+ self.remove(id)?;
1600
+ }
1601
+ if let Some(horizon) = horizon {
1602
+ state.horizon = horizon;
1603
+ }
1604
+ self.write_retention_state(resource, &state)?;
1605
+ Ok(expire.len())
1606
+ }
1607
+
1608
+ fn remove(&self, id: &StateId) -> Result<(), RevisionError> {
1609
+ match &self.backing {
1610
+ Backing::Volatile(state) => {
1611
+ state.lock().unwrap().revisions.remove(id);
1612
+ }
1613
+ Backing::Durable(feltdb) => {
1614
+ feltdb.delete(&revision_key(id)).map_err(|error| {
1615
+ RevisionError::Storage(format!("Failed to expire revision: {error}"))
1616
+ })?;
1617
+ }
1618
+ }
867
1619
  Ok(())
868
1620
  }
869
1621
 
870
- /// Get branch head
871
- pub fn branch_head(&self, name: &str) -> Option<StateId> {
872
- let branches = self.branches.lock().unwrap();
873
- branches.get(name).cloned()
1622
+ fn retention_state(&self, resource: &str) -> RetentionState {
1623
+ match &self.backing {
1624
+ Backing::Volatile(state) => state
1625
+ .lock()
1626
+ .unwrap()
1627
+ .retention
1628
+ .get(resource)
1629
+ .cloned()
1630
+ .unwrap_or_default(),
1631
+ Backing::Durable(feltdb) => feltdb
1632
+ .get::<RetentionState>(&retention_key(resource))
1633
+ .ok()
1634
+ .flatten()
1635
+ .unwrap_or_default(),
1636
+ }
1637
+ }
1638
+
1639
+ fn write_retention_state(
1640
+ &self,
1641
+ resource: &str,
1642
+ state: &RetentionState,
1643
+ ) -> Result<(), RevisionError> {
1644
+ match &self.backing {
1645
+ Backing::Volatile(volatile) => {
1646
+ volatile
1647
+ .lock()
1648
+ .unwrap()
1649
+ .retention
1650
+ .insert(resource.to_string(), state.clone());
1651
+ }
1652
+ Backing::Durable(feltdb) => {
1653
+ feltdb
1654
+ .insert(&retention_key(resource), state.clone())
1655
+ .map_err(|error| {
1656
+ RevisionError::Storage(format!(
1657
+ "Failed to persist retention state: {error}"
1658
+ ))
1659
+ })?;
1660
+ }
1661
+ }
1662
+ Ok(())
874
1663
  }
875
1664
  }
876
1665
 
877
1666
  impl Clone for StateStore {
878
1667
  fn clone(&self) -> Self {
879
1668
  StateStore {
880
- revisions: self.revisions.clone(),
881
- current_id: self.current_id.clone(),
882
- branches: self.branches.clone(),
883
- feltdb: self.feltdb.clone(),
1669
+ backing: self.backing.clone(),
884
1670
  }
885
1671
  }
886
1672
  }
@@ -921,6 +1707,7 @@ mod tests {
921
1707
  #[test]
922
1708
  fn test_state_revision_initial() {
923
1709
  let rev = StateRevision::initial(
1710
+ "docs:1".to_string(),
924
1711
  r#"{"key":"value"}"#.to_string(),
925
1712
  "test-authority".to_string(),
926
1713
  );
@@ -932,6 +1719,7 @@ mod tests {
932
1719
  #[test]
933
1720
  fn test_state_revision_child() {
934
1721
  let parent = StateRevision::initial(
1722
+ "docs:1".to_string(),
935
1723
  r#"{"key":"value"}"#.to_string(),
936
1724
  "test-authority".to_string(),
937
1725
  );
@@ -949,14 +1737,11 @@ mod tests {
949
1737
  let mut topo = StateTopology::new();
950
1738
 
951
1739
  let rev1 = StateRevision::initial(
1740
+ "docs:1".to_string(),
952
1741
  r#"{"v":1}"#.to_string(),
953
1742
  "auth".to_string(),
954
1743
  );
955
- let rev2 = StateRevision::child(
956
- r#"{"v":2}"#.to_string(),
957
- &rev1,
958
- "auth".to_string(),
959
- );
1744
+ let rev2 = StateRevision::child(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string());
960
1745
 
961
1746
  topo.add_revision(rev1.clone());
962
1747
  topo.add_revision(rev2.clone());
@@ -979,50 +1764,142 @@ mod tests {
979
1764
  }
980
1765
 
981
1766
  #[test]
982
- fn test_conflict_classification_independent() {
983
- let base = StateRevision::initial(
984
- r#"{"a":1,"b":1}"#.to_string(),
985
- "auth".to_string(),
1767
+ fn path_relation_is_structural() {
1768
+ let key = |name: &str| PathComponent::Key(name.to_string());
1769
+
1770
+ assert_eq!(path_relation(&[key("a")], &[key("a")]), PathRelation::Same);
1771
+ assert_eq!(
1772
+ path_relation(&[key("a")], &[key("a"), key("b")]),
1773
+ PathRelation::Ancestor
1774
+ );
1775
+ assert_eq!(
1776
+ path_relation(&[key("a"), key("b")], &[key("a")]),
1777
+ PathRelation::Descendant
1778
+ );
1779
+ assert_eq!(
1780
+ path_relation(&[key("a"), key("b")], &[key("a"), key("c")]),
1781
+ PathRelation::Disjoint
986
1782
  );
987
- let left = StateRevision::child(
988
- r#"{"a":2,"b":1}"#.to_string(),
989
- &base,
1783
+ // Components are compared whole. `a.b` and `a.bc` share a string prefix
1784
+ // under any flattened rendering and are still disjoint.
1785
+ assert_eq!(
1786
+ path_relation(&[key("a"), key("b")], &[key("a"), key("bc")]),
1787
+ PathRelation::Disjoint
1788
+ );
1789
+ // A key never matches an index, whatever they look like as text.
1790
+ assert_eq!(
1791
+ path_relation(&[key("0")], &[PathComponent::Index(0)]),
1792
+ PathRelation::Disjoint
1793
+ );
1794
+ // The empty path is the whole state, and contains every other path.
1795
+ assert_eq!(path_relation(&[], &[key("a")]), PathRelation::Ancestor);
1796
+ assert_eq!(path_relation(&[], &[]), PathRelation::Same);
1797
+
1798
+ assert!(paths_overlap(&[key("a")], &[key("a"), key("b")]));
1799
+ assert!(paths_overlap(&[key("a"), key("b")], &[key("a")]));
1800
+ assert!(!paths_overlap(
1801
+ &[key("a"), key("b")],
1802
+ &[key("a"), key("bc")]
1803
+ ));
1804
+ assert!(!paths_overlap(&[key("a")], &[key("b")]));
1805
+ }
1806
+
1807
+ #[test]
1808
+ fn resolve_path_reads_objects_and_arrays() {
1809
+ let value = json!({"o": {"a": [10, {"b": 1}]}, "n": null});
1810
+ let key = |name: &str| PathComponent::Key(name.to_string());
1811
+
1812
+ assert_eq!(resolve_path(&value, &[]), Some(&value));
1813
+ assert_eq!(resolve_path(&value, &[key("n")]), Some(&Value::Null));
1814
+ assert_eq!(
1815
+ resolve_path(&value, &[key("o"), key("a"), PathComponent::Index(0)]),
1816
+ Some(&json!(10))
1817
+ );
1818
+ assert_eq!(
1819
+ resolve_path(
1820
+ &value,
1821
+ &[key("o"), key("a"), PathComponent::Index(1), key("b")]
1822
+ ),
1823
+ Some(&json!(1))
1824
+ );
1825
+ // A path that does not resolve, and one that uses the wrong component
1826
+ // kind for the container it meets.
1827
+ assert_eq!(resolve_path(&value, &[key("missing")]), None);
1828
+ assert_eq!(resolve_path(&value, &[PathComponent::Index(0)]), None);
1829
+ assert_eq!(
1830
+ resolve_path(&value, &[key("o"), key("a"), PathComponent::Index(9)]),
1831
+ None
1832
+ );
1833
+ assert_eq!(resolve_path(&value, &[key("o"), key("a"), key("0")]), None);
1834
+ }
1835
+
1836
+ #[test]
1837
+ fn overlapping_changes_are_never_independent() {
1838
+ // The reported defect: a branch replacing `o` and a branch editing
1839
+ // `o.a` cannot both be applied, so neither path is independent.
1840
+ let base = StateRevision::initial(
1841
+ "docs:1".to_string(),
1842
+ r#"{"o":{"a":1}}"#.to_string(),
990
1843
  "auth".to_string(),
991
1844
  );
992
- let right = StateRevision::child(
993
- r#"{"a":1,"b":2}"#.to_string(),
994
- &base,
1845
+ let left = StateRevision::child(r#"{"o":7}"#.to_string(), &base, "auth".to_string());
1846
+ let right = StateRevision::child(r#"{"o":{"a":2}}"#.to_string(), &base, "auth".to_string());
1847
+
1848
+ let classification = ConflictClassification::classify(&base, &left, &right);
1849
+ assert_eq!(classification.overall, ConflictClass::Conflict);
1850
+ assert!(classification
1851
+ .path_conflicts
1852
+ .iter()
1853
+ .all(|entry| entry.classification == ConflictClass::Conflict));
1854
+ }
1855
+
1856
+ #[test]
1857
+ fn test_conflict_classification_independent() {
1858
+ let base = StateRevision::initial(
1859
+ "docs:1".to_string(),
1860
+ r#"{"a":1,"b":1}"#.to_string(),
995
1861
  "auth".to_string(),
996
1862
  );
1863
+ let left = StateRevision::child(r#"{"a":2,"b":1}"#.to_string(), &base, "auth".to_string());
1864
+ let right = StateRevision::child(r#"{"a":1,"b":2}"#.to_string(), &base, "auth".to_string());
997
1865
 
998
1866
  let classification = ConflictClassification::classify(&base, &left, &right);
999
1867
  assert_eq!(classification.overall, ConflictClass::Independent);
1000
1868
  }
1001
1869
 
1002
1870
  #[test]
1003
- fn test_state_store_create_and_current() {
1871
+ fn test_state_store_create_and_get() {
1004
1872
  let store = StateStore::new_volatile();
1005
1873
  let rev = store
1006
- .create(r#"{"data":"initial"}"#.to_string(), "auth".to_string())
1874
+ .create(
1875
+ "docs:1",
1876
+ r#"{"data":"initial"}"#.to_string(),
1877
+ "auth".to_string(),
1878
+ )
1007
1879
  .unwrap();
1008
1880
 
1009
- let current = store.current().unwrap();
1010
- assert_eq!(current.id, rev.id);
1881
+ // The store answers "do I have revision X?", not "what is current?".
1882
+ let stored = store.get(&rev.id).unwrap();
1883
+ assert_eq!(stored.id, rev.id);
1884
+ assert_eq!(stored.content, rev.content);
1011
1885
  }
1012
1886
 
1013
1887
  #[test]
1014
1888
  fn test_state_store_commit() {
1015
1889
  let store = StateStore::new_volatile();
1016
1890
  let rev1 = store
1017
- .create(r#"{"v":1}"#.to_string(), "auth".to_string())
1891
+ .create("docs:1", r#"{"v":1}"#.to_string(), "auth".to_string())
1018
1892
  .unwrap();
1019
1893
  let rev2 = store
1020
1894
  .commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
1021
1895
  .unwrap();
1022
1896
 
1023
1897
  assert_eq!(rev2.parent_id, Some(rev1.id.clone()));
1024
- let current = store.current().unwrap();
1025
- assert_eq!(current.id, rev2.id);
1898
+ // Both revisions remain retrievable; committing a child does not
1899
+ // displace its parent, because there is no pointer to displace.
1900
+ assert_eq!(store.get(&rev2.id).unwrap().id, rev2.id);
1901
+ assert_eq!(store.get(&rev1.id).unwrap().id, rev1.id);
1902
+ assert_eq!(store.parent(&rev2.id).unwrap().id, rev1.id);
1026
1903
  }
1027
1904
 
1028
1905
  #[test]
@@ -1048,29 +1925,35 @@ mod tests {
1048
1925
  let db_path = std::env::temp_dir().join("test_recovery_root.log");
1049
1926
  let _ = std::fs::remove_file(&db_path); // Clean up any previous test
1050
1927
 
1928
+ let initial_id;
1929
+
1051
1930
  // Phase 1: Create and persist root state
1052
1931
  {
1053
1932
  let db = crate::FeltDb::open(&db_path).expect("open db");
1054
1933
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1055
1934
 
1056
- let initial = store.create(r#"{"data":"root"}"#.to_string(), "auth".to_string())
1935
+ let initial = store
1936
+ .create(
1937
+ "docs:1",
1938
+ r#"{"data":"root"}"#.to_string(),
1939
+ "auth".to_string(),
1940
+ )
1057
1941
  .expect("create initial");
1058
1942
 
1059
- assert!(store.current().is_some());
1060
- assert_eq!(store.current().unwrap().id, initial.id);
1943
+ assert_eq!(store.get(&initial.id).unwrap().id, initial.id);
1944
+ initial_id = initial.id;
1061
1945
  } // Database closes, data persists to disk
1062
1946
 
1063
- // Phase 2: Reopen and verify recovery
1947
+ // Phase 2: Reopen and read it back by identity. No recovery step runs.
1064
1948
  {
1065
1949
  let db = crate::FeltDb::open(&db_path).expect("open db again");
1066
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
1950
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
1067
1951
 
1068
- // Verify root exists and is current
1069
- assert!(store.current().is_some());
1070
- let current = store.current().unwrap();
1071
- assert_eq!(current.content, r#"{"data":"root"}"#);
1072
- assert_eq!(current.authority, "auth");
1073
- assert!(current.parent_id.is_none());
1952
+ let root = store.get(&initial_id).expect("the root survives restart");
1953
+ assert_eq!(root.content, r#"{"data":"root"}"#);
1954
+ assert_eq!(root.authority, "auth");
1955
+ assert!(root.parent_id.is_none());
1956
+ assert!(root.verify_integrity());
1074
1957
  }
1075
1958
 
1076
1959
  let _ = std::fs::remove_file(&db_path); // Clean up
@@ -1092,20 +1975,23 @@ mod tests {
1092
1975
  let db = crate::FeltDb::open(&db_path).expect("open db");
1093
1976
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1094
1977
 
1095
- let rev1 = store.create(r#"{"v":1}"#.to_string(), "auth".to_string())
1978
+ let rev1 = store
1979
+ .create("docs:1", r#"{"v":1}"#.to_string(), "auth".to_string())
1096
1980
  .expect("create rev1");
1097
1981
  rev1_id = rev1.id.clone();
1098
1982
 
1099
- let rev2 = store.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
1983
+ let rev2 = store
1984
+ .commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
1100
1985
  .expect("create rev2");
1101
1986
  rev2_id = rev2.id.clone();
1102
1987
 
1103
- let rev3 = store.commit(r#"{"v":3}"#.to_string(), &rev2, "auth".to_string())
1988
+ let rev3 = store
1989
+ .commit(r#"{"v":3}"#.to_string(), &rev2, "auth".to_string())
1104
1990
  .expect("create rev3");
1105
1991
  rev3_id = rev3.id.clone();
1106
1992
 
1107
1993
  // Verify before close
1108
- assert_eq!(store.current().unwrap().id, rev3_id);
1994
+ assert_eq!(store.get(&rev3_id).unwrap().id, rev3_id);
1109
1995
  assert_eq!(store.parent(&rev3_id).unwrap().id, rev2_id);
1110
1996
  assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1111
1997
  }
@@ -1113,7 +1999,8 @@ mod tests {
1113
1999
  // Phase 2: Reopen and verify full history recovered
1114
2000
  {
1115
2001
  let db = crate::FeltDb::open(&db_path).expect("open db again");
1116
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
2002
+ let store =
2003
+ StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
1117
2004
 
1118
2005
  // Verify all three revisions exist
1119
2006
  assert!(store.exists(&rev1_id));
@@ -1121,7 +2008,7 @@ mod tests {
1121
2008
  assert!(store.exists(&rev3_id));
1122
2009
 
1123
2010
  // Verify ancestry
1124
- assert_eq!(store.current().unwrap().id, rev3_id);
2011
+ assert_eq!(store.get(&rev3_id).unwrap().id, rev3_id);
1125
2012
  assert_eq!(store.parent(&rev3_id).unwrap().id, rev2_id);
1126
2013
  assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1127
2014
  assert!(store.parent(&rev1_id).is_none());
@@ -1136,7 +2023,7 @@ mod tests {
1136
2023
  }
1137
2024
 
1138
2025
  #[test]
1139
- fn test_recovery_branches() {
2026
+ fn test_divergent_siblings_survive_restart_independently() {
1140
2027
  use std::sync::Arc;
1141
2028
 
1142
2029
  let db_path = std::env::temp_dir().join("test_recovery_branches.log");
@@ -1150,36 +2037,35 @@ mod tests {
1150
2037
  let db = crate::FeltDb::open(&db_path).expect("open db");
1151
2038
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1152
2039
 
1153
- let root = store.create(r#"{"base":true}"#.to_string(), "auth".to_string())
2040
+ let root = store
2041
+ .create("docs:1", r#"{"base":true}"#.to_string(), "auth".to_string())
1154
2042
  .expect("create root");
1155
2043
  root_id = root.id.clone();
1156
2044
 
1157
- let branch1 = store.commit(r#"{"path":"a"}"#.to_string(), &root, "auth".to_string())
2045
+ let branch1 = store
2046
+ .commit(r#"{"path":"a"}"#.to_string(), &root, "auth".to_string())
1158
2047
  .expect("create branch1");
1159
2048
  branch1_id = branch1.id.clone();
1160
-
1161
- // Create named branch reference
1162
- store.create_branch("feature-a".to_string(), branch1_id.clone()).expect("create feature-a");
1163
2049
  }
1164
2050
 
1165
- // Phase 2: Reopen and verify branch recovered
2051
+ // Phase 2: Reopen. There is no branch to recover, and none is needed:
2052
+ // a divergent revision is reachable by its own identity, and its
2053
+ // ancestry is its parent link.
1166
2054
  {
1167
2055
  let db = crate::FeltDb::open(&db_path).expect("open db again");
1168
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
1169
-
1170
- // Verify branch exists and points correctly
1171
- assert_eq!(store.branch_head("feature-a"), Some(branch1_id.clone()));
2056
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
1172
2057
 
1173
- // Verify both revisions exist
1174
2058
  assert!(store.exists(&root_id));
1175
2059
  assert!(store.exists(&branch1_id));
2060
+ assert_eq!(store.parent(&branch1_id).unwrap().id, root_id);
2061
+ assert_eq!(store.get(&branch1_id).unwrap().content, r#"{"path":"a"}"#);
1176
2062
  }
1177
2063
 
1178
2064
  let _ = std::fs::remove_file(&db_path);
1179
2065
  }
1180
2066
 
1181
2067
  #[test]
1182
- fn test_recovery_current_pointer() {
2068
+ fn test_every_revision_stays_retrievable_by_identity_after_restart() {
1183
2069
  use std::sync::Arc;
1184
2070
 
1185
2071
  let db_path = std::env::temp_dir().join("test_recovery_current.log");
@@ -1188,34 +2074,36 @@ mod tests {
1188
2074
  let rev1_id;
1189
2075
  let rev2_id;
1190
2076
 
1191
- // Phase 1: Create and move current pointer
2077
+ // Phase 1: Two revisions, one descending from the other.
1192
2078
  {
1193
2079
  let db = crate::FeltDb::open(&db_path).expect("open db");
1194
2080
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1195
2081
 
1196
- let rev1 = store.create(r#"{"state":"1"}"#.to_string(), "auth".to_string())
2082
+ let rev1 = store
2083
+ .create("docs:1", r#"{"state":"1"}"#.to_string(), "auth".to_string())
1197
2084
  .expect("create rev1");
1198
2085
  rev1_id = rev1.id.clone();
1199
2086
 
1200
- // Current should be rev1
1201
- assert_eq!(store.current().unwrap().id, rev1_id);
2087
+ assert_eq!(store.get(&rev1_id).unwrap().id, rev1_id);
1202
2088
 
1203
- let rev2 = store.commit(r#"{"state":"2"}"#.to_string(), &rev1, "auth".to_string())
2089
+ let rev2 = store
2090
+ .commit(r#"{"state":"2"}"#.to_string(), &rev1, "auth".to_string())
1204
2091
  .expect("create rev2");
1205
2092
  rev2_id = rev2.id.clone();
1206
2093
 
1207
- // Current should move to rev2
1208
- assert_eq!(store.current().unwrap().id, rev2_id);
2094
+ // Committing a child does not displace the parent.
2095
+ assert_eq!(store.get(&rev2_id).unwrap().id, rev2_id);
2096
+ assert_eq!(store.get(&rev1_id).unwrap().id, rev1_id);
1209
2097
  }
1210
2098
 
1211
- // Phase 2: Verify current pointer survived restart
2099
+ // Phase 2: both survive, and neither is privileged over the other.
1212
2100
  {
1213
2101
  let db = crate::FeltDb::open(&db_path).expect("open db again");
1214
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
2102
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
1215
2103
 
1216
- // Current should still be rev2
1217
- assert_eq!(store.current().unwrap().id, rev2_id);
1218
- assert_eq!(store.current().unwrap().content, r#"{"state":"2"}"#);
2104
+ assert_eq!(store.get(&rev1_id).unwrap().content, r#"{"state":"1"}"#);
2105
+ assert_eq!(store.get(&rev2_id).unwrap().content, r#"{"state":"2"}"#);
2106
+ assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1219
2107
  }
1220
2108
 
1221
2109
  let _ = std::fs::remove_file(&db_path);
@@ -1237,27 +2125,35 @@ mod tests {
1237
2125
  let db = crate::FeltDb::open(&db_path).expect("open db");
1238
2126
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1239
2127
 
1240
- let root = store.create(r#"{"v":0}"#.to_string(), "auth".to_string())
2128
+ let root = store
2129
+ .create("docs:1", r#"{"v":0}"#.to_string(), "auth".to_string())
1241
2130
  .expect("create root");
1242
2131
  root_id = root.id.clone();
1243
2132
 
1244
- let rev_a = store.commit(r#"{"v":1,"stage":"a"}"#.to_string(), &root, "auth".to_string())
2133
+ let rev_a = store
2134
+ .commit(
2135
+ r#"{"v":1,"stage":"a"}"#.to_string(),
2136
+ &root,
2137
+ "auth".to_string(),
2138
+ )
1245
2139
  .expect("create rev_a");
1246
2140
  rev_a_id = rev_a.id.clone();
1247
2141
 
1248
- let rev_b = store.commit(r#"{"v":2,"stage":"b"}"#.to_string(), &rev_a, "auth".to_string())
2142
+ let rev_b = store
2143
+ .commit(
2144
+ r#"{"v":2,"stage":"b"}"#.to_string(),
2145
+ &rev_a,
2146
+ "auth".to_string(),
2147
+ )
1249
2148
  .expect("create rev_b");
1250
2149
  rev_b_id = rev_b.id.clone();
1251
-
1252
- // Create named branches at different points in history
1253
- store.create_branch("checkpoint-a".to_string(), rev_a_id.clone()).expect("create checkpoint-a");
1254
- store.create_branch("checkpoint-b".to_string(), rev_b_id.clone()).expect("create checkpoint-b");
1255
2150
  }
1256
2151
 
1257
2152
  // Phase 2: Verify complex topology recovered
1258
2153
  {
1259
2154
  let db = crate::FeltDb::open(&db_path).expect("open db again");
1260
- let store = StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
2155
+ let store =
2156
+ StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
1261
2157
 
1262
2158
  // Verify all revisions
1263
2159
  assert!(store.exists(&root_id));
@@ -1268,67 +2164,75 @@ mod tests {
1268
2164
  assert_eq!(store.parent(&rev_b_id).unwrap().id, rev_a_id);
1269
2165
  assert_eq!(store.parent(&rev_a_id).unwrap().id, root_id);
1270
2166
 
1271
- // Verify branches
1272
- assert_eq!(store.branch_head("checkpoint-a"), Some(rev_a_id.clone()));
1273
- assert_eq!(store.branch_head("checkpoint-b"), Some(rev_b_id.clone()));
1274
-
1275
- // Verify current is at rev_b
1276
- assert_eq!(store.current().unwrap().id, rev_b_id);
2167
+ // Any point in the history is a checkpoint, addressed by its own
2168
+ // identity. Naming one adds nothing the id does not already give.
2169
+ assert_eq!(
2170
+ store.get(&rev_a_id).unwrap().content,
2171
+ r#"{"v":1,"stage":"a"}"#
2172
+ );
2173
+ assert_eq!(
2174
+ store.get(&rev_b_id).unwrap().content,
2175
+ r#"{"v":2,"stage":"b"}"#
2176
+ );
1277
2177
  }
1278
2178
 
1279
2179
  let _ = std::fs::remove_file(&db_path);
1280
2180
  }
1281
2181
 
1282
2182
  #[test]
1283
- fn test_recovery_multiple_restart_cycles() {
2183
+ fn test_revisions_survive_multiple_restart_cycles() {
1284
2184
  use std::sync::Arc;
1285
2185
 
1286
2186
  let db_path = std::env::temp_dir().join("test_recovery_cycles.log");
1287
2187
  let _ = std::fs::remove_file(&db_path);
1288
2188
 
2189
+ // The caller carries the identities it cares about across restarts.
2190
+ // That is the whole of what replaces a `current` pointer here.
2191
+ let rev1_id;
2192
+ let rev2_id;
2193
+
1289
2194
  // Cycle 1: Create root
1290
2195
  {
1291
2196
  let db = crate::FeltDb::open(&db_path).expect("open db");
1292
2197
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1293
- store.create(r#"{"cycle":1}"#.to_string(), "auth".to_string()).expect("create");
2198
+ rev1_id = store
2199
+ .create("docs:1", r#"{"cycle":1}"#.to_string(), "auth".to_string())
2200
+ .expect("create")
2201
+ .id;
1294
2202
  }
1295
2203
 
1296
- // Cycle 2: Restart, verify, and commit
2204
+ // Cycle 2: Restart, read by identity, and commit onto it
1297
2205
  {
1298
2206
  let db = crate::FeltDb::open(&db_path).expect("open db");
1299
2207
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1300
- assert!(store.current().is_some());
1301
2208
 
1302
- let rev1 = store.current().unwrap();
2209
+ let rev1 = store.get(&rev1_id).expect("rev1 survives");
1303
2210
  assert_eq!(rev1.content, r#"{"cycle":1}"#);
1304
-
1305
- store.commit(r#"{"cycle":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit 2");
2211
+
2212
+ rev2_id = store
2213
+ .commit(r#"{"cycle":2}"#.to_string(), &rev1, "auth".to_string())
2214
+ .expect("commit 2")
2215
+ .id;
1306
2216
  }
1307
2217
 
1308
- // Cycle 3: Restart, create branch
2218
+ // Cycle 3: Restart, both are still there
1309
2219
  {
1310
2220
  let db = crate::FeltDb::open(&db_path).expect("open db");
1311
2221
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1312
- assert!(store.current().is_some());
1313
2222
 
1314
- let current = store.current().unwrap();
1315
- assert_eq!(current.content, r#"{"cycle":2}"#);
1316
-
1317
- store.create_branch("stable".to_string(), current.id.clone()).expect("create branch");
2223
+ assert_eq!(store.get(&rev2_id).unwrap().content, r#"{"cycle":2}"#);
2224
+ assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1318
2225
  }
1319
2226
 
1320
- // Cycle 4: Final restart and verify everything survived
2227
+ // Cycle 4: Final restart, ancestry intact end to end
1321
2228
  {
1322
2229
  let db = crate::FeltDb::open(&db_path).expect("open db");
1323
2230
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1324
2231
 
1325
- // Should have current with parent
1326
- let current = store.current().expect("get current");
1327
- assert_eq!(current.content, r#"{"cycle":2}"#);
1328
- assert!(current.parent_id.is_some());
1329
-
1330
- // Should have branch
1331
- assert_eq!(store.branch_head("stable"), Some(current.id.clone()));
2232
+ let rev2 = store.get(&rev2_id).expect("rev2 survives every cycle");
2233
+ assert_eq!(rev2.content, r#"{"cycle":2}"#);
2234
+ assert_eq!(rev2.parent_id, Some(rev1_id.clone()));
2235
+ assert!(store.parent(&rev1_id).is_none(), "the root has no parent");
1332
2236
  }
1333
2237
 
1334
2238
  let _ = std::fs::remove_file(&db_path);
@@ -1357,10 +2261,13 @@ mod tests {
1357
2261
  let db = crate::FeltDb::open(&db_path).expect("create db");
1358
2262
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1359
2263
 
1360
- let rev = store.create(
1361
- r#"{"task":"write PR18 proof"}"#.to_string(),
1362
- "system".to_string(),
1363
- ).expect("write");
2264
+ let rev = store
2265
+ .create(
2266
+ "docs:1",
2267
+ r#"{"task":"write PR18 proof"}"#.to_string(),
2268
+ "system".to_string(),
2269
+ )
2270
+ .expect("write");
1364
2271
 
1365
2272
  rev.id.clone()
1366
2273
  }; // Force drop of database, persist to disk
@@ -1370,15 +2277,19 @@ mod tests {
1370
2277
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1371
2278
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1372
2279
 
1373
- // VERIFY: Data exists after restart
1374
- assert!(store.current().is_some(), "State should exist after restart");
1375
- let current = store.current().unwrap();
1376
- assert_eq!(current.id, written_id, "State ID should match");
2280
+ // VERIFY: the revision is retrievable by identity after restart
2281
+ let recovered = store
2282
+ .get(&written_id)
2283
+ .expect("State should exist after restart");
2284
+ assert_eq!(recovered.id, written_id, "State ID should match");
1377
2285
  assert_eq!(
1378
- current.content,
1379
- r#"{"task":"write PR18 proof"}"#,
2286
+ recovered.content, r#"{"task":"write PR18 proof"}"#,
1380
2287
  "State content should survive restart"
1381
2288
  );
2289
+ assert!(
2290
+ recovered.verify_integrity(),
2291
+ "content must still hash to its identity"
2292
+ );
1382
2293
  }
1383
2294
 
1384
2295
  let _ = std::fs::remove_file(&db_path);
@@ -1398,24 +2309,31 @@ mod tests {
1398
2309
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1399
2310
 
1400
2311
  // Create project
1401
- let project = store.create(
1402
- r#"{"type":"project","name":"FeltDB"}"#.to_string(),
1403
- "system".to_string(),
1404
- ).expect("create project");
2312
+ let project = store
2313
+ .create(
2314
+ "docs:1",
2315
+ r#"{"type":"project","name":"FeltDB"}"#.to_string(),
2316
+ "system".to_string(),
2317
+ )
2318
+ .expect("create project");
1405
2319
 
1406
2320
  // Create membership (parent = project)
1407
- let membership = store.commit(
1408
- r#"{"type":"membership","project":"FeltDB","user":"alice"}"#.to_string(),
1409
- &project,
1410
- "system".to_string(),
1411
- ).expect("create membership");
2321
+ let membership = store
2322
+ .commit(
2323
+ r#"{"type":"membership","project":"FeltDB","user":"alice"}"#.to_string(),
2324
+ &project,
2325
+ "system".to_string(),
2326
+ )
2327
+ .expect("create membership");
1412
2328
 
1413
2329
  // Write audit event (parent = membership)
1414
- let event = store.commit(
1415
- r#"{"type":"audit","action":"project_created"}"#.to_string(),
1416
- &membership,
1417
- "system".to_string(),
1418
- ).expect("create event");
2330
+ let event = store
2331
+ .commit(
2332
+ r#"{"type":"audit","action":"project_created"}"#.to_string(),
2333
+ &membership,
2334
+ "system".to_string(),
2335
+ )
2336
+ .expect("create event");
1419
2337
 
1420
2338
  (project.id.clone(), membership.id.clone(), event.id.clone())
1421
2339
  }; // Force persist
@@ -1425,12 +2343,13 @@ mod tests {
1425
2343
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1426
2344
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1427
2345
 
1428
- let current = store.current().expect("current should exist");
1429
- assert_eq!(current.id, event_id, "Event should be current");
2346
+ let event = store.get(&event_id).expect("event should exist");
2347
+ assert_eq!(event.id, event_id);
1430
2348
 
1431
2349
  // Verify chain: event → membership → project
1432
2350
  assert_eq!(
1433
- current.parent_id, Some(membership_id.clone()),
2351
+ event.parent_id,
2352
+ Some(membership_id.clone()),
1434
2353
  "Event parent should be membership"
1435
2354
  );
1436
2355
 
@@ -1454,22 +2373,29 @@ mod tests {
1454
2373
  let db = crate::FeltDb::open(&db_path).expect("create db");
1455
2374
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1456
2375
 
1457
- let root = store.create(
1458
- r#"{"generation":"root"}"#.to_string(),
1459
- "genesis".to_string(),
1460
- ).expect("create root");
1461
-
1462
- let child = store.commit(
1463
- r#"{"generation":"child"}"#.to_string(),
1464
- &root,
1465
- "genesis".to_string(),
1466
- ).expect("create child");
2376
+ let root = store
2377
+ .create(
2378
+ "docs:1",
2379
+ r#"{"generation":"root"}"#.to_string(),
2380
+ "genesis".to_string(),
2381
+ )
2382
+ .expect("create root");
1467
2383
 
1468
- let grandchild = store.commit(
1469
- r#"{"generation":"grandchild"}"#.to_string(),
1470
- &child,
1471
- "genesis".to_string(),
1472
- ).expect("create grandchild");
2384
+ let child = store
2385
+ .commit(
2386
+ r#"{"generation":"child"}"#.to_string(),
2387
+ &root,
2388
+ "genesis".to_string(),
2389
+ )
2390
+ .expect("create child");
2391
+
2392
+ let grandchild = store
2393
+ .commit(
2394
+ r#"{"generation":"grandchild"}"#.to_string(),
2395
+ &child,
2396
+ "genesis".to_string(),
2397
+ )
2398
+ .expect("create grandchild");
1473
2399
 
1474
2400
  (root.id.clone(), child.id.clone(), grandchild.id.clone())
1475
2401
  };
@@ -1479,12 +2405,12 @@ mod tests {
1479
2405
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1480
2406
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1481
2407
 
1482
- let current = store.current().expect("current exists");
1483
- assert_eq!(current.id, grandchild_id, "Grandchild is current");
2408
+ let grandchild = store.get(&grandchild_id).expect("grandchild exists");
2409
+ assert_eq!(grandchild.id, grandchild_id);
1484
2410
 
1485
2411
  // Trace ancestry: grandchild → child → root
1486
2412
  let mut ancestry_count = 0;
1487
- let mut current_id = Some(current.id.clone());
2413
+ let mut current_id = Some(grandchild.id.clone());
1488
2414
 
1489
2415
  // Count links in ancestry (stop after finding root)
1490
2416
  while let Some(id) = current_id {
@@ -1517,10 +2443,9 @@ mod tests {
1517
2443
  let db = crate::FeltDb::open(&db_path).expect("create db");
1518
2444
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1519
2445
 
1520
- let rev = store.create(
1521
- r#"{"cycle":1}"#.to_string(),
1522
- "system".to_string(),
1523
- ).expect("create initial");
2446
+ let rev = store
2447
+ .create("docs:1", r#"{"cycle":1}"#.to_string(), "system".to_string())
2448
+ .expect("create initial");
1524
2449
 
1525
2450
  rev.id.clone()
1526
2451
  };
@@ -1530,16 +2455,18 @@ mod tests {
1530
2455
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1531
2456
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1532
2457
 
1533
- // Verify previous state exists
1534
- let current = store.current().expect("current exists");
1535
- assert_eq!(current.id, state_id, "Previous state should exist");
2458
+ // Verify previous state exists, read back by the id we carried
2459
+ let previous = store.get(&state_id).expect("previous state should exist");
2460
+ assert_eq!(previous.id, state_id);
1536
2461
 
1537
2462
  // Update state
1538
- let rev = store.commit(
1539
- format!(r#"{{"cycle":{}}}"#, cycle),
1540
- &current,
1541
- "system".to_string(),
1542
- ).expect("create update");
2463
+ let rev = store
2464
+ .commit(
2465
+ format!(r#"{{"cycle":{}}}"#, cycle),
2466
+ &previous,
2467
+ "system".to_string(),
2468
+ )
2469
+ .expect("create update");
1543
2470
 
1544
2471
  state_id = rev.id.clone();
1545
2472
  }
@@ -1549,7 +2476,7 @@ mod tests {
1549
2476
  let db = crate::FeltDb::open(&db_path).expect("final reopen");
1550
2477
  let store = StateStore::with_feltdb(Arc::new(db)).expect("final recovery");
1551
2478
 
1552
- let final_state = store.current().expect("final state exists");
2479
+ let final_state = store.get(&state_id).expect("final state exists");
1553
2480
  assert_eq!(final_state.id, state_id, "Final state should match");
1554
2481
  }
1555
2482
 
@@ -1569,10 +2496,13 @@ mod tests {
1569
2496
  let db = crate::FeltDb::open(&db_path).expect("create db");
1570
2497
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1571
2498
 
1572
- let rev = store.create(
1573
- r#"{"status":"initial"}"#.to_string(),
1574
- "system".to_string(),
1575
- ).expect("create initial");
2499
+ let rev = store
2500
+ .create(
2501
+ "docs:1",
2502
+ r#"{"status":"initial"}"#.to_string(),
2503
+ "system".to_string(),
2504
+ )
2505
+ .expect("create initial");
1576
2506
 
1577
2507
  rev.id.clone()
1578
2508
  };
@@ -1586,11 +2516,18 @@ mod tests {
1586
2516
  let db = crate::FeltDb::open(&db_path).expect("reopen");
1587
2517
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
1588
2518
 
1589
- if let Some(current) = store.current() {
2519
+ if let Some(recovered) = store.get(&initial_id) {
1590
2520
  // State exists: must be complete
1591
- assert!(current.content.contains("status"), "State must be complete");
1592
- assert!(current.content.contains("initial"), "Attributes must be present");
1593
- assert_eq!(current.id, initial_id, "ID must match");
2521
+ assert!(
2522
+ recovered.content.contains("status"),
2523
+ "State must be complete"
2524
+ );
2525
+ assert!(
2526
+ recovered.content.contains("initial"),
2527
+ "Attributes must be present"
2528
+ );
2529
+ assert_eq!(recovered.id, initial_id, "ID must match");
2530
+ assert!(recovered.verify_integrity(), "and never partially written");
1594
2531
  } else {
1595
2532
  // State doesn't exist: acceptable
1596
2533
  // But not: partially exist with missing fields
@@ -1614,10 +2551,13 @@ mod tests {
1614
2551
  let db = crate::FeltDb::open(&db_path).expect("create db");
1615
2552
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1616
2553
 
1617
- let rev = store.create(
1618
- expected_content.to_string(),
1619
- "integrity_check".to_string(),
1620
- ).expect("create");
2554
+ let rev = store
2555
+ .create(
2556
+ "docs:1",
2557
+ expected_content.to_string(),
2558
+ "integrity_check".to_string(),
2559
+ )
2560
+ .expect("create");
1621
2561
 
1622
2562
  rev.id.clone()
1623
2563
  };
@@ -1627,14 +2567,15 @@ mod tests {
1627
2567
  let db = crate::FeltDb::open(&db_path).expect("reopen");
1628
2568
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
1629
2569
 
1630
- let current = store.current().expect("state exists");
2570
+ let recovered = store.get(&state_id).expect("state exists");
1631
2571
 
1632
2572
  // Verify integrity: ID should still match content
1633
- assert_eq!(current.content, expected_content, "Content should match");
1634
- assert_eq!(current.id, state_id, "ID should match");
1635
-
1636
- // In real implementation: verify_integrity() would recompute hash
1637
- // assert!(current.verify_integrity(), "Content hash should be valid");
2573
+ assert_eq!(recovered.content, expected_content, "Content should match");
2574
+ assert_eq!(recovered.id, state_id, "ID should match");
2575
+ assert!(
2576
+ recovered.verify_integrity(),
2577
+ "Content hash should still be valid"
2578
+ );
1638
2579
  }
1639
2580
 
1640
2581
  let _ = std::fs::remove_file(&db_path);
@@ -1644,59 +2585,72 @@ mod tests {
1644
2585
  fn pr19_proof_concurrent_write_safety() {
1645
2586
  // PROVES: Concurrent writes don't produce impossible partial states
1646
2587
  // Simulates multiple threads writing to the same StateStore
1647
-
2588
+
1648
2589
  use std::sync::{Arc, Mutex};
1649
2590
  use std::thread;
1650
-
2591
+
1651
2592
  let db_path = std::env::temp_dir().join("pr19_concurrent_writes.log");
1652
2593
  let _ = std::fs::remove_file(&db_path);
1653
-
2594
+
1654
2595
  let db = crate::FeltDb::open(&db_path).expect("create db");
1655
2596
  let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
1656
-
2597
+
1657
2598
  let write_count = Arc::new(Mutex::new(0usize));
1658
2599
  let mut handles = vec![];
1659
-
2600
+
1660
2601
  // Spawn multiple writers
1661
2602
  for i in 0..5 {
1662
2603
  let store_clone = Arc::clone(&store);
1663
2604
  let count_clone = Arc::clone(&write_count);
1664
-
2605
+
1665
2606
  let handle = thread::spawn(move || {
1666
2607
  let content = format!(r#"{{"writer":{},"timestamp":{}}}"#, i, 1000 + i);
1667
- let rev = store_clone.create(content, format!("writer_{}", i))
2608
+ let rev = store_clone
2609
+ .create(&format!("writer:{i}"), content, format!("writer_{}", i))
1668
2610
  .expect("write should succeed");
1669
-
2611
+
1670
2612
  let mut count = count_clone.lock().unwrap();
1671
2613
  *count += 1;
1672
-
2614
+
1673
2615
  rev.id.clone()
1674
2616
  });
1675
-
2617
+
1676
2618
  handles.push(handle);
1677
2619
  }
1678
-
2620
+
1679
2621
  // Wait for all writers
1680
2622
  let mut written_ids = vec![];
1681
2623
  for handle in handles {
1682
2624
  let id = handle.join().expect("thread should complete");
1683
2625
  written_ids.push(id);
1684
2626
  }
1685
-
1686
- // Verify no partial states were created
1687
- let current = store.current().expect("current state should exist");
1688
-
1689
- // The current state should be one of the written states, not partial
1690
- assert!(written_ids.contains(&current.id), "Current state should match one of the written states");
1691
-
2627
+
2628
+ // Verify no partial states were created: every id a writer returned is
2629
+ // readable, complete, and hashes to its own content.
2630
+ for id in &written_ids {
2631
+ let written = store
2632
+ .get(id)
2633
+ .expect("every concurrently written revision should exist");
2634
+ assert!(
2635
+ written.verify_integrity(),
2636
+ "no writer produced a partial revision"
2637
+ );
2638
+ }
2639
+
1692
2640
  // Verify we can still write after concurrent writes
1693
- let final_write = store.create(
1694
- r#"{"final":"write","success":true}"#.to_string(),
1695
- "system".to_string(),
1696
- ).expect("final write should succeed");
1697
-
1698
- assert!(final_write.verify_integrity(), "Final write should be valid");
1699
-
2641
+ let final_write = store
2642
+ .create(
2643
+ "docs:1",
2644
+ r#"{"final":"write","success":true}"#.to_string(),
2645
+ "system".to_string(),
2646
+ )
2647
+ .expect("final write should succeed");
2648
+
2649
+ assert!(
2650
+ final_write.verify_integrity(),
2651
+ "Final write should be valid"
2652
+ );
2653
+
1700
2654
  let _ = std::fs::remove_file(&db_path);
1701
2655
  }
1702
2656
 
@@ -1706,23 +2660,26 @@ mod tests {
1706
2660
  // Simulates: write → sync starts → crash → restart
1707
2661
  // Expected: Either data is on disk (fully recovered) or not (nothing recovered)
1708
2662
  // Never: Partial/corrupted data
1709
-
2663
+
1710
2664
  let db_path = std::env::temp_dir().join("pr19_crash_during_sync.log");
1711
2665
  let _ = std::fs::remove_file(&db_path);
1712
-
2666
+
1713
2667
  // First write: known-good state
1714
2668
  let initial_id = {
1715
2669
  let db = crate::FeltDb::open(&db_path).expect("create db");
1716
2670
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1717
-
1718
- let rev = store.create(
1719
- r#"{"status":"pre-crash","integrity":"good"}"#.to_string(),
1720
- "system".to_string(),
1721
- ).expect("write");
1722
-
2671
+
2672
+ let rev = store
2673
+ .create(
2674
+ "docs:1",
2675
+ r#"{"status":"pre-crash","integrity":"good"}"#.to_string(),
2676
+ "system".to_string(),
2677
+ )
2678
+ .expect("write");
2679
+
1723
2680
  rev.id.clone()
1724
2681
  };
1725
-
2682
+
1726
2683
  // After restart, verify state is either:
1727
2684
  // A) Initial state is still there (sync didn't complete), OR
1728
2685
  // B) New state is there and fully valid (sync completed), OR
@@ -1730,12 +2687,17 @@ mod tests {
1730
2687
  {
1731
2688
  let db = crate::FeltDb::open(&db_path).expect("reopen");
1732
2689
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
1733
-
1734
- let current = store.current().expect("state should exist");
1735
-
2690
+
2691
+ let current = store
2692
+ .get(&initial_id)
2693
+ .expect("the initial state should still exist");
2694
+
1736
2695
  // Verify state integrity
1737
- assert!(current.verify_integrity(), "State should be internally consistent");
1738
-
2696
+ assert!(
2697
+ current.verify_integrity(),
2698
+ "State should be internally consistent"
2699
+ );
2700
+
1739
2701
  // State should either be the initial write or a valid new write
1740
2702
  // But never partial or corrupted
1741
2703
  assert!(
@@ -1743,7 +2705,7 @@ mod tests {
1743
2705
  "Recovered state must be either initial (sync failed) or new+valid (sync succeeded)"
1744
2706
  );
1745
2707
  }
1746
-
2708
+
1747
2709
  let _ = std::fs::remove_file(&db_path);
1748
2710
  }
1749
2711
 
@@ -1752,30 +2714,31 @@ mod tests {
1752
2714
  // PROVES: Query execution uses indexes to reduce complexity
1753
2715
  // Currently this is unproven - indexes are data structures but
1754
2716
  // query execution path is not proven to use them
1755
-
2717
+
1756
2718
  let db_path = std::env::temp_dir().join("pr19_query_index.log");
1757
2719
  let _ = std::fs::remove_file(&db_path);
1758
-
2720
+
1759
2721
  let db = crate::FeltDb::open(&db_path).expect("create db");
1760
2722
  let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
1761
-
2723
+
1762
2724
  // Create records with indexed fields
1763
2725
  for i in 0..100 {
1764
2726
  let content = format!(r#"{{"user_id":"user_{}","active":true}}"#, i % 10);
1765
- let _ = store.create(content, format!("creator_{}", i))
2727
+ let _ = store
2728
+ .create(&format!("record:{i}"), content, format!("creator_{}", i))
1766
2729
  .expect("write record");
1767
2730
  }
1768
-
2731
+
1769
2732
  // Query by indexed field (user_id in this case)
1770
2733
  // If indexes are used, this should be O(log n)
1771
2734
  // If not, this is O(n)
1772
-
2735
+
1773
2736
  // LIMITATION: No way to measure index usage in this test
1774
2737
  // This proves index data structures exist, not that they're used
1775
-
2738
+
1776
2739
  // TODO: Measure query latency and verify index benefit
1777
2740
  // TODO: Add query optimizer tracing to prove index selection
1778
-
2741
+
1779
2742
  let _ = std::fs::remove_file(&db_path);
1780
2743
  }
1781
2744
 
@@ -1784,36 +2747,45 @@ mod tests {
1784
2747
  // PROVES: Basic multi-tenant isolation at storage layer
1785
2748
  // Note: This tests storage isolation, not authorization layer
1786
2749
  // Authorization layer tests are in authorization_security_tests.rs
1787
-
2750
+
1788
2751
  let db_path = std::env::temp_dir().join("pr19_multi_tenant.log");
1789
2752
  let _ = std::fs::remove_file(&db_path);
1790
-
2753
+
1791
2754
  let db = crate::FeltDb::open(&db_path).expect("create db");
1792
2755
  let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
1793
-
2756
+
1794
2757
  // Create tenant-scoped records
1795
2758
  // Tenant A writes a record
1796
- let tenant_a_record = store.create(
1797
- r#"{"tenant":"tenant_a","data":"secret_a"}"#.to_string(),
1798
- "tenant_a".to_string(),
1799
- ).expect("tenant_a write");
1800
-
2759
+ let tenant_a_record = store
2760
+ .create(
2761
+ "tenant_a:1",
2762
+ r#"{"tenant":"tenant_a","data":"secret_a"}"#.to_string(),
2763
+ "tenant_a".to_string(),
2764
+ )
2765
+ .expect("tenant_a write");
2766
+
1801
2767
  // Tenant B writes a record
1802
- let tenant_b_record = store.create(
1803
- r#"{"tenant":"tenant_b","data":"secret_b"}"#.to_string(),
1804
- "tenant_b".to_string(),
1805
- ).expect("tenant_b write");
1806
-
2768
+ let tenant_b_record = store
2769
+ .create(
2770
+ "tenant_b:1",
2771
+ r#"{"tenant":"tenant_b","data":"secret_b"}"#.to_string(),
2772
+ "tenant_b".to_string(),
2773
+ )
2774
+ .expect("tenant_b write");
2775
+
1807
2776
  // Verify both records are stored (this proves storage works)
1808
- assert_ne!(tenant_a_record.id, tenant_b_record.id, "Records should have different IDs");
1809
-
2777
+ assert_ne!(
2778
+ tenant_a_record.id, tenant_b_record.id,
2779
+ "Records should have different IDs"
2780
+ );
2781
+
1810
2782
  // Current limitation: StateStore doesn't enforce tenant boundaries
1811
2783
  // Tenant isolation is enforced at authorization layer, not storage layer
1812
2784
  // This test proves that separate StateIds are created for each record
1813
-
2785
+
1814
2786
  // TODO: Test authorization layer rejection of cross-tenant access
1815
2787
  // See authorization_security_tests.rs
1816
-
2788
+
1817
2789
  let _ = std::fs::remove_file(&db_path);
1818
2790
  }
1819
2791
 
@@ -1821,36 +2793,41 @@ mod tests {
1821
2793
  fn pr19_proof_atomicity_requires_sync_return() {
1822
2794
  // PROVES: Atomicity guarantee only holds if sync() succeeds
1823
2795
  // If sync() fails, no durability guarantee
1824
-
2796
+
1825
2797
  let db_path = std::env::temp_dir().join("pr19_atomicity_sync.log");
1826
2798
  let _ = std::fs::remove_file(&db_path);
1827
-
2799
+
1828
2800
  let rev_id = {
1829
2801
  let db = crate::FeltDb::open(&db_path).expect("create db");
1830
2802
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1831
-
2803
+
1832
2804
  // Write should be durable only if it returns Ok
1833
2805
  let result = store.create(
2806
+ "docs:1",
1834
2807
  r#"{"atomicity":"requires_sync_success"}"#.to_string(),
1835
2808
  "system".to_string(),
1836
2809
  );
1837
-
2810
+
1838
2811
  // If create() returns Ok, durability is guaranteed
1839
2812
  match result {
1840
2813
  Ok(rev) => Some(rev.id.clone()),
1841
2814
  Err(_) => None,
1842
2815
  }
1843
2816
  }; // Force drop of db to simulate restart
1844
-
2817
+
1845
2818
  if let Some(rev_id) = rev_id {
1846
2819
  // Durability guaranteed: restart should see this
1847
2820
  let db2 = crate::FeltDb::open(&db_path).expect("reopen");
1848
2821
  let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recover");
1849
-
1850
- assert_eq!(store2.current().unwrap().id, rev_id, "Durability guaranteed");
2822
+
2823
+ assert_eq!(
2824
+ store2.get(&rev_id).unwrap().id,
2825
+ rev_id,
2826
+ "Durability guaranteed"
2827
+ );
1851
2828
  }
1852
2829
  // If None, no durability guarantee - sync failed
1853
-
2830
+
1854
2831
  let _ = std::fs::remove_file(&db_path);
1855
2832
  }
1856
2833
  }