@feltdb/core 0.8.4 → 0.8.6

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 (160) hide show
  1. package/dist/create/package-versions.js +1 -1
  2. package/dist/create/server-source/Cargo.lock +165 -0
  3. package/dist/create/server-source/Cargo.toml +9 -0
  4. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  5. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  6. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  7. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  8. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  9. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  10. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  11. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  12. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  13. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  14. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  15. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  19. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  20. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  21. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  22. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  23. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  24. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  25. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  26. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  27. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  28. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  29. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  30. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  31. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  32. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  33. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  34. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  35. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  36. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  37. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  38. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  39. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  40. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  41. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  42. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  43. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  44. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  45. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  46. package/dist/create/server-source/crates/feltdb/src/lib.rs +1648 -46
  47. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  48. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  49. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  50. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  51. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  52. package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
  53. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  54. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  55. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  56. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  57. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  58. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  59. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  60. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  61. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  62. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  63. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
  64. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  65. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  66. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  67. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  68. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  69. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  70. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  71. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  72. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  73. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  74. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  75. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  76. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  77. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  78. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  79. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  80. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  81. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  82. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  83. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  84. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  85. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  86. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  87. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
  88. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  89. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  90. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  91. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1565 -536
  92. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  93. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  94. package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
  95. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  96. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  97. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  98. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  99. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  100. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  101. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  102. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  103. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  104. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
  105. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
  106. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  107. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
  108. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
  109. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +309 -0
  110. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
  111. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
  112. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  113. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  114. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  115. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  116. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  117. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
  118. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
  119. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
  120. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  121. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  122. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  123. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  124. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  125. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  126. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  127. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  128. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  129. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1365 -0
  130. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  131. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  132. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
  133. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  134. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  135. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  136. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  137. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  138. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  139. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  140. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  141. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  142. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  143. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  144. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  145. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  146. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  147. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +16 -1
  149. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  150. package/dist/create/server-source/crates/feltdb-server/src/main.rs +695 -47
  151. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
  152. package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-C1VhI-U5.js} +1 -1
  153. package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
  154. package/dist/studio-app/assets/{index-DwgNAIIX.js → index-Bbos1m2U.js} +1 -1
  155. package/dist/studio-app/index.html +1 -1
  156. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  157. package/dist/workload.d.ts +2 -1
  158. package/dist/workload.d.ts.map +1 -1
  159. package/package.json +1 -1
  160. package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
@@ -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,928 @@ 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:
601
1048
  ///
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.
1049
+ /// > **Do I have revision X?**
605
1050
  ///
606
- /// For testing and validation of state semantics alone, StateStore::new_volatile()
607
- /// creates a non-persistent in-memory store.
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.
1086
+ ///
1087
+ /// # Non-claims
1088
+ ///
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}")
724
1242
  }
1243
+ RevisionError::ResourceAlreadyBegun { resource, head } => {
1244
+ write!(f, "resource {resource} already begins at revision {head}")
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
+ }
741
1285
 
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());
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
+ }
1292
+
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());
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
+ }
787
1402
 
788
- let mut current = self.current_id.lock().unwrap();
789
- *current = Some(id.clone());
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
+ }
1441
+ }
1442
+ Ok(revision)
1443
+ }
790
1444
 
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))?;
1445
+ // -- reading ------------------------------------------------------------
798
1446
 
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))?;
1447
+ /// Retrieve a revision by identity. This is the authoritative read path.
1448
+ pub fn get(&self, id: &StateId) -> Option<StateRevision> {
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(),
804
1455
  }
1456
+ }
805
1457
 
806
- Ok(revision)
1458
+ /// Whether a revision is present.
1459
+ pub fn exists(&self, id: &StateId) -> bool {
1460
+ self.get(id).is_some()
807
1461
  }
808
1462
 
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()
1463
+ /// Metadata for a revision.
1464
+ pub fn metadata(&self, id: &StateId) -> Option<BTreeMap<String, Value>> {
1465
+ self.get(id).map(|revision| revision.metadata)
1466
+ }
1467
+
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)
815
1480
  } else {
816
- None
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.
1490
+ pub fn parent(&self, id: &StateId) -> Option<StateRevision> {
1491
+ match self.parent_of(id)? {
1492
+ ParentLookup::Revision(parent) => Some(*parent),
1493
+ _ => None,
817
1494
  }
818
1495
  }
819
1496
 
820
- /// Get revision by id
821
- pub fn get(&self, id: &StateId) -> Option<StateRevision> {
822
- let revisions = self.revisions.lock().unwrap();
823
- revisions.get(id).cloned()
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
824
1506
  }
825
1507
 
826
- /// Check if revision exists
827
- pub fn exists(&self, id: &StateId) -> bool {
828
- let revisions = self.revisions.lock().unwrap();
829
- revisions.contains_key(id)
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
+ })
830
1521
  }
831
1522
 
832
- /// Get metadata for a revision
833
- 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())
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(),
1545
+ }
836
1546
  }
837
1547
 
838
- /// Get parent of a revision
839
- 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
- }
1548
+ /// A digest of this store's **history**: the revision graph and retention
1549
+ /// state, and nothing else.
1550
+ ///
1551
+ /// Deliberately independent of a digest over current records. Two databases
1552
+ /// can hold identical current state and completely different histories —
1553
+ /// one that recorded every step and one that only ever saw the last — and
1554
+ /// any equivalence check that cannot tell those apart is not checking
1555
+ /// history at all.
1556
+ ///
1557
+ /// Covers, for every revision: its resource, identity, parent, sequence,
1558
+ /// content identity and the authority that committed it; and for every
1559
+ /// resource: its retention policy and horizon. It excludes wall-clock
1560
+ /// timestamps, which two authorities holding the same history legitimately
1561
+ /// disagree about.
1562
+ pub fn history_digest(&self) -> String {
1563
+ let mut entries: Vec<String> = self
1564
+ .all_revisions()
1565
+ .into_iter()
1566
+ .map(|revision| {
1567
+ format!(
1568
+ "{}|{}|{}|{}|{}|{}",
1569
+ revision.resource,
1570
+ revision.id.as_hex(),
1571
+ revision
1572
+ .parent_id
1573
+ .as_ref()
1574
+ .map(StateId::as_hex)
1575
+ .unwrap_or("-"),
1576
+ revision.sequence,
1577
+ revision.content_id.as_hex(),
1578
+ revision.authority,
1579
+ )
1580
+ })
1581
+ .collect();
1582
+ for resource in self.resources() {
1583
+ let state = self.retention_state(&resource);
1584
+ entries.push(format!(
1585
+ "retention|{}|{:?}|{}",
1586
+ resource, state.policy.keep_last, state.horizon
1587
+ ));
845
1588
  }
846
- None
1589
+ entries.sort();
1590
+
1591
+ let mut hasher = Sha256::new();
1592
+ hasher.update(b"feltdb.history.v1");
1593
+ for entry in entries {
1594
+ hasher.update(entry.as_bytes());
1595
+ hasher.update([0u8]);
1596
+ }
1597
+ format!("{:x}", hasher.finalize())
847
1598
  }
848
1599
 
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());
1600
+ // -- retention ----------------------------------------------------------
853
1601
 
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))?;
1602
+ /// The retention policy in force for a resource.
1603
+ pub fn retention_policy(&self, resource: &str) -> RetentionPolicy {
1604
+ self.retention_state(resource).policy
1605
+ }
1606
+
1607
+ /// The lowest sequence still retained for a resource.
1608
+ ///
1609
+ /// Revisions below it were expired deliberately. This is what separates an
1610
+ /// expired parent from a missing one.
1611
+ pub fn retention_horizon(&self, resource: &str) -> u64 {
1612
+ self.retention_state(resource).horizon
1613
+ }
1614
+
1615
+ /// Set a resource's retention policy, and apply it immediately.
1616
+ ///
1617
+ /// Returns how many revisions the policy expired on application.
1618
+ pub fn set_retention_policy(
1619
+ &self,
1620
+ resource: &str,
1621
+ policy: RetentionPolicy,
1622
+ ) -> Result<usize, RevisionError> {
1623
+ let mut state = self.retention_state(resource);
1624
+ state.policy = policy;
1625
+ self.write_retention_state(resource, &state)?;
1626
+ self.apply_retention(resource)
1627
+ }
1628
+
1629
+ /// Expire whatever the resource's policy no longer retains.
1630
+ ///
1631
+ /// Deletes from the **oldest** end only, so the surviving history is always
1632
+ /// a suffix of the resource's timeline and the current revision always
1633
+ /// survives. A surviving revision's `parent_id` is never rewritten — that
1634
+ /// would violate immutability — so the horizon is recorded instead, and the
1635
+ /// oldest survivor reports an `Expired` parent rather than a missing one.
1636
+ pub fn apply_retention(&self, resource: &str) -> Result<usize, RevisionError> {
1637
+ let mut state = self.retention_state(resource);
1638
+ if state.policy.keep_last.is_none() {
1639
+ return Ok(0);
1640
+ }
1641
+ let history: Vec<(StateId, u64)> = self
1642
+ .history_of(resource)
1643
+ .into_iter()
1644
+ .map(|revision| (revision.id, revision.sequence))
1645
+ .collect();
1646
+ let (expire, horizon) = revisions_to_expire(&history, state.policy.keep_last);
1647
+ if expire.is_empty() {
1648
+ return Ok(0);
1649
+ }
1650
+ for id in &expire {
1651
+ self.remove(id)?;
865
1652
  }
1653
+ if let Some(horizon) = horizon {
1654
+ state.horizon = horizon;
1655
+ }
1656
+ self.write_retention_state(resource, &state)?;
1657
+ Ok(expire.len())
1658
+ }
866
1659
 
1660
+ fn remove(&self, id: &StateId) -> Result<(), RevisionError> {
1661
+ match &self.backing {
1662
+ Backing::Volatile(state) => {
1663
+ state.lock().unwrap().revisions.remove(id);
1664
+ }
1665
+ Backing::Durable(feltdb) => {
1666
+ feltdb.delete(&revision_key(id)).map_err(|error| {
1667
+ RevisionError::Storage(format!("Failed to expire revision: {error}"))
1668
+ })?;
1669
+ }
1670
+ }
867
1671
  Ok(())
868
1672
  }
869
1673
 
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()
1674
+ fn retention_state(&self, resource: &str) -> RetentionState {
1675
+ match &self.backing {
1676
+ Backing::Volatile(state) => state
1677
+ .lock()
1678
+ .unwrap()
1679
+ .retention
1680
+ .get(resource)
1681
+ .cloned()
1682
+ .unwrap_or_default(),
1683
+ Backing::Durable(feltdb) => feltdb
1684
+ .get::<RetentionState>(&retention_key(resource))
1685
+ .ok()
1686
+ .flatten()
1687
+ .unwrap_or_default(),
1688
+ }
1689
+ }
1690
+
1691
+ fn write_retention_state(
1692
+ &self,
1693
+ resource: &str,
1694
+ state: &RetentionState,
1695
+ ) -> Result<(), RevisionError> {
1696
+ match &self.backing {
1697
+ Backing::Volatile(volatile) => {
1698
+ volatile
1699
+ .lock()
1700
+ .unwrap()
1701
+ .retention
1702
+ .insert(resource.to_string(), state.clone());
1703
+ }
1704
+ Backing::Durable(feltdb) => {
1705
+ feltdb
1706
+ .insert(&retention_key(resource), state.clone())
1707
+ .map_err(|error| {
1708
+ RevisionError::Storage(format!(
1709
+ "Failed to persist retention state: {error}"
1710
+ ))
1711
+ })?;
1712
+ }
1713
+ }
1714
+ Ok(())
874
1715
  }
875
1716
  }
876
1717
 
877
1718
  impl Clone for StateStore {
878
1719
  fn clone(&self) -> Self {
879
1720
  StateStore {
880
- revisions: self.revisions.clone(),
881
- current_id: self.current_id.clone(),
882
- branches: self.branches.clone(),
883
- feltdb: self.feltdb.clone(),
1721
+ backing: self.backing.clone(),
884
1722
  }
885
1723
  }
886
1724
  }
@@ -921,6 +1759,7 @@ mod tests {
921
1759
  #[test]
922
1760
  fn test_state_revision_initial() {
923
1761
  let rev = StateRevision::initial(
1762
+ "docs:1".to_string(),
924
1763
  r#"{"key":"value"}"#.to_string(),
925
1764
  "test-authority".to_string(),
926
1765
  );
@@ -932,6 +1771,7 @@ mod tests {
932
1771
  #[test]
933
1772
  fn test_state_revision_child() {
934
1773
  let parent = StateRevision::initial(
1774
+ "docs:1".to_string(),
935
1775
  r#"{"key":"value"}"#.to_string(),
936
1776
  "test-authority".to_string(),
937
1777
  );
@@ -949,14 +1789,11 @@ mod tests {
949
1789
  let mut topo = StateTopology::new();
950
1790
 
951
1791
  let rev1 = StateRevision::initial(
1792
+ "docs:1".to_string(),
952
1793
  r#"{"v":1}"#.to_string(),
953
1794
  "auth".to_string(),
954
1795
  );
955
- let rev2 = StateRevision::child(
956
- r#"{"v":2}"#.to_string(),
957
- &rev1,
958
- "auth".to_string(),
959
- );
1796
+ let rev2 = StateRevision::child(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string());
960
1797
 
961
1798
  topo.add_revision(rev1.clone());
962
1799
  topo.add_revision(rev2.clone());
@@ -979,50 +1816,142 @@ mod tests {
979
1816
  }
980
1817
 
981
1818
  #[test]
982
- fn test_conflict_classification_independent() {
983
- let base = StateRevision::initial(
984
- r#"{"a":1,"b":1}"#.to_string(),
985
- "auth".to_string(),
1819
+ fn path_relation_is_structural() {
1820
+ let key = |name: &str| PathComponent::Key(name.to_string());
1821
+
1822
+ assert_eq!(path_relation(&[key("a")], &[key("a")]), PathRelation::Same);
1823
+ assert_eq!(
1824
+ path_relation(&[key("a")], &[key("a"), key("b")]),
1825
+ PathRelation::Ancestor
1826
+ );
1827
+ assert_eq!(
1828
+ path_relation(&[key("a"), key("b")], &[key("a")]),
1829
+ PathRelation::Descendant
1830
+ );
1831
+ assert_eq!(
1832
+ path_relation(&[key("a"), key("b")], &[key("a"), key("c")]),
1833
+ PathRelation::Disjoint
1834
+ );
1835
+ // Components are compared whole. `a.b` and `a.bc` share a string prefix
1836
+ // under any flattened rendering and are still disjoint.
1837
+ assert_eq!(
1838
+ path_relation(&[key("a"), key("b")], &[key("a"), key("bc")]),
1839
+ PathRelation::Disjoint
1840
+ );
1841
+ // A key never matches an index, whatever they look like as text.
1842
+ assert_eq!(
1843
+ path_relation(&[key("0")], &[PathComponent::Index(0)]),
1844
+ PathRelation::Disjoint
1845
+ );
1846
+ // The empty path is the whole state, and contains every other path.
1847
+ assert_eq!(path_relation(&[], &[key("a")]), PathRelation::Ancestor);
1848
+ assert_eq!(path_relation(&[], &[]), PathRelation::Same);
1849
+
1850
+ assert!(paths_overlap(&[key("a")], &[key("a"), key("b")]));
1851
+ assert!(paths_overlap(&[key("a"), key("b")], &[key("a")]));
1852
+ assert!(!paths_overlap(
1853
+ &[key("a"), key("b")],
1854
+ &[key("a"), key("bc")]
1855
+ ));
1856
+ assert!(!paths_overlap(&[key("a")], &[key("b")]));
1857
+ }
1858
+
1859
+ #[test]
1860
+ fn resolve_path_reads_objects_and_arrays() {
1861
+ let value = json!({"o": {"a": [10, {"b": 1}]}, "n": null});
1862
+ let key = |name: &str| PathComponent::Key(name.to_string());
1863
+
1864
+ assert_eq!(resolve_path(&value, &[]), Some(&value));
1865
+ assert_eq!(resolve_path(&value, &[key("n")]), Some(&Value::Null));
1866
+ assert_eq!(
1867
+ resolve_path(&value, &[key("o"), key("a"), PathComponent::Index(0)]),
1868
+ Some(&json!(10))
1869
+ );
1870
+ assert_eq!(
1871
+ resolve_path(
1872
+ &value,
1873
+ &[key("o"), key("a"), PathComponent::Index(1), key("b")]
1874
+ ),
1875
+ Some(&json!(1))
1876
+ );
1877
+ // A path that does not resolve, and one that uses the wrong component
1878
+ // kind for the container it meets.
1879
+ assert_eq!(resolve_path(&value, &[key("missing")]), None);
1880
+ assert_eq!(resolve_path(&value, &[PathComponent::Index(0)]), None);
1881
+ assert_eq!(
1882
+ resolve_path(&value, &[key("o"), key("a"), PathComponent::Index(9)]),
1883
+ None
986
1884
  );
987
- let left = StateRevision::child(
988
- r#"{"a":2,"b":1}"#.to_string(),
989
- &base,
1885
+ assert_eq!(resolve_path(&value, &[key("o"), key("a"), key("0")]), None);
1886
+ }
1887
+
1888
+ #[test]
1889
+ fn overlapping_changes_are_never_independent() {
1890
+ // The reported defect: a branch replacing `o` and a branch editing
1891
+ // `o.a` cannot both be applied, so neither path is independent.
1892
+ let base = StateRevision::initial(
1893
+ "docs:1".to_string(),
1894
+ r#"{"o":{"a":1}}"#.to_string(),
990
1895
  "auth".to_string(),
991
1896
  );
992
- let right = StateRevision::child(
993
- r#"{"a":1,"b":2}"#.to_string(),
994
- &base,
1897
+ let left = StateRevision::child(r#"{"o":7}"#.to_string(), &base, "auth".to_string());
1898
+ let right = StateRevision::child(r#"{"o":{"a":2}}"#.to_string(), &base, "auth".to_string());
1899
+
1900
+ let classification = ConflictClassification::classify(&base, &left, &right);
1901
+ assert_eq!(classification.overall, ConflictClass::Conflict);
1902
+ assert!(classification
1903
+ .path_conflicts
1904
+ .iter()
1905
+ .all(|entry| entry.classification == ConflictClass::Conflict));
1906
+ }
1907
+
1908
+ #[test]
1909
+ fn test_conflict_classification_independent() {
1910
+ let base = StateRevision::initial(
1911
+ "docs:1".to_string(),
1912
+ r#"{"a":1,"b":1}"#.to_string(),
995
1913
  "auth".to_string(),
996
1914
  );
1915
+ let left = StateRevision::child(r#"{"a":2,"b":1}"#.to_string(), &base, "auth".to_string());
1916
+ let right = StateRevision::child(r#"{"a":1,"b":2}"#.to_string(), &base, "auth".to_string());
997
1917
 
998
1918
  let classification = ConflictClassification::classify(&base, &left, &right);
999
1919
  assert_eq!(classification.overall, ConflictClass::Independent);
1000
1920
  }
1001
1921
 
1002
1922
  #[test]
1003
- fn test_state_store_create_and_current() {
1923
+ fn test_state_store_create_and_get() {
1004
1924
  let store = StateStore::new_volatile();
1005
1925
  let rev = store
1006
- .create(r#"{"data":"initial"}"#.to_string(), "auth".to_string())
1926
+ .create(
1927
+ "docs:1",
1928
+ r#"{"data":"initial"}"#.to_string(),
1929
+ "auth".to_string(),
1930
+ )
1007
1931
  .unwrap();
1008
1932
 
1009
- let current = store.current().unwrap();
1010
- assert_eq!(current.id, rev.id);
1933
+ // The store answers "do I have revision X?", not "what is current?".
1934
+ let stored = store.get(&rev.id).unwrap();
1935
+ assert_eq!(stored.id, rev.id);
1936
+ assert_eq!(stored.content, rev.content);
1011
1937
  }
1012
1938
 
1013
1939
  #[test]
1014
1940
  fn test_state_store_commit() {
1015
1941
  let store = StateStore::new_volatile();
1016
1942
  let rev1 = store
1017
- .create(r#"{"v":1}"#.to_string(), "auth".to_string())
1943
+ .create("docs:1", r#"{"v":1}"#.to_string(), "auth".to_string())
1018
1944
  .unwrap();
1019
1945
  let rev2 = store
1020
1946
  .commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
1021
1947
  .unwrap();
1022
1948
 
1023
1949
  assert_eq!(rev2.parent_id, Some(rev1.id.clone()));
1024
- let current = store.current().unwrap();
1025
- assert_eq!(current.id, rev2.id);
1950
+ // Both revisions remain retrievable; committing a child does not
1951
+ // displace its parent, because there is no pointer to displace.
1952
+ assert_eq!(store.get(&rev2.id).unwrap().id, rev2.id);
1953
+ assert_eq!(store.get(&rev1.id).unwrap().id, rev1.id);
1954
+ assert_eq!(store.parent(&rev2.id).unwrap().id, rev1.id);
1026
1955
  }
1027
1956
 
1028
1957
  #[test]
@@ -1048,29 +1977,35 @@ mod tests {
1048
1977
  let db_path = std::env::temp_dir().join("test_recovery_root.log");
1049
1978
  let _ = std::fs::remove_file(&db_path); // Clean up any previous test
1050
1979
 
1980
+ let initial_id;
1981
+
1051
1982
  // Phase 1: Create and persist root state
1052
1983
  {
1053
1984
  let db = crate::FeltDb::open(&db_path).expect("open db");
1054
1985
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1055
1986
 
1056
- let initial = store.create(r#"{"data":"root"}"#.to_string(), "auth".to_string())
1987
+ let initial = store
1988
+ .create(
1989
+ "docs:1",
1990
+ r#"{"data":"root"}"#.to_string(),
1991
+ "auth".to_string(),
1992
+ )
1057
1993
  .expect("create initial");
1058
1994
 
1059
- assert!(store.current().is_some());
1060
- assert_eq!(store.current().unwrap().id, initial.id);
1995
+ assert_eq!(store.get(&initial.id).unwrap().id, initial.id);
1996
+ initial_id = initial.id;
1061
1997
  } // Database closes, data persists to disk
1062
1998
 
1063
- // Phase 2: Reopen and verify recovery
1999
+ // Phase 2: Reopen and read it back by identity. No recovery step runs.
1064
2000
  {
1065
2001
  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");
2002
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
1067
2003
 
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());
2004
+ let root = store.get(&initial_id).expect("the root survives restart");
2005
+ assert_eq!(root.content, r#"{"data":"root"}"#);
2006
+ assert_eq!(root.authority, "auth");
2007
+ assert!(root.parent_id.is_none());
2008
+ assert!(root.verify_integrity());
1074
2009
  }
1075
2010
 
1076
2011
  let _ = std::fs::remove_file(&db_path); // Clean up
@@ -1092,20 +2027,23 @@ mod tests {
1092
2027
  let db = crate::FeltDb::open(&db_path).expect("open db");
1093
2028
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1094
2029
 
1095
- let rev1 = store.create(r#"{"v":1}"#.to_string(), "auth".to_string())
2030
+ let rev1 = store
2031
+ .create("docs:1", r#"{"v":1}"#.to_string(), "auth".to_string())
1096
2032
  .expect("create rev1");
1097
2033
  rev1_id = rev1.id.clone();
1098
2034
 
1099
- let rev2 = store.commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
2035
+ let rev2 = store
2036
+ .commit(r#"{"v":2}"#.to_string(), &rev1, "auth".to_string())
1100
2037
  .expect("create rev2");
1101
2038
  rev2_id = rev2.id.clone();
1102
2039
 
1103
- let rev3 = store.commit(r#"{"v":3}"#.to_string(), &rev2, "auth".to_string())
2040
+ let rev3 = store
2041
+ .commit(r#"{"v":3}"#.to_string(), &rev2, "auth".to_string())
1104
2042
  .expect("create rev3");
1105
2043
  rev3_id = rev3.id.clone();
1106
2044
 
1107
2045
  // Verify before close
1108
- assert_eq!(store.current().unwrap().id, rev3_id);
2046
+ assert_eq!(store.get(&rev3_id).unwrap().id, rev3_id);
1109
2047
  assert_eq!(store.parent(&rev3_id).unwrap().id, rev2_id);
1110
2048
  assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1111
2049
  }
@@ -1113,7 +2051,8 @@ mod tests {
1113
2051
  // Phase 2: Reopen and verify full history recovered
1114
2052
  {
1115
2053
  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");
2054
+ let store =
2055
+ StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
1117
2056
 
1118
2057
  // Verify all three revisions exist
1119
2058
  assert!(store.exists(&rev1_id));
@@ -1121,7 +2060,7 @@ mod tests {
1121
2060
  assert!(store.exists(&rev3_id));
1122
2061
 
1123
2062
  // Verify ancestry
1124
- assert_eq!(store.current().unwrap().id, rev3_id);
2063
+ assert_eq!(store.get(&rev3_id).unwrap().id, rev3_id);
1125
2064
  assert_eq!(store.parent(&rev3_id).unwrap().id, rev2_id);
1126
2065
  assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1127
2066
  assert!(store.parent(&rev1_id).is_none());
@@ -1136,7 +2075,7 @@ mod tests {
1136
2075
  }
1137
2076
 
1138
2077
  #[test]
1139
- fn test_recovery_branches() {
2078
+ fn test_divergent_siblings_survive_restart_independently() {
1140
2079
  use std::sync::Arc;
1141
2080
 
1142
2081
  let db_path = std::env::temp_dir().join("test_recovery_branches.log");
@@ -1150,36 +2089,35 @@ mod tests {
1150
2089
  let db = crate::FeltDb::open(&db_path).expect("open db");
1151
2090
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1152
2091
 
1153
- let root = store.create(r#"{"base":true}"#.to_string(), "auth".to_string())
2092
+ let root = store
2093
+ .create("docs:1", r#"{"base":true}"#.to_string(), "auth".to_string())
1154
2094
  .expect("create root");
1155
2095
  root_id = root.id.clone();
1156
2096
 
1157
- let branch1 = store.commit(r#"{"path":"a"}"#.to_string(), &root, "auth".to_string())
2097
+ let branch1 = store
2098
+ .commit(r#"{"path":"a"}"#.to_string(), &root, "auth".to_string())
1158
2099
  .expect("create branch1");
1159
2100
  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
2101
  }
1164
2102
 
1165
- // Phase 2: Reopen and verify branch recovered
2103
+ // Phase 2: Reopen. There is no branch to recover, and none is needed:
2104
+ // a divergent revision is reachable by its own identity, and its
2105
+ // ancestry is its parent link.
1166
2106
  {
1167
2107
  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");
2108
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
1169
2109
 
1170
- // Verify branch exists and points correctly
1171
- assert_eq!(store.branch_head("feature-a"), Some(branch1_id.clone()));
1172
-
1173
- // Verify both revisions exist
1174
2110
  assert!(store.exists(&root_id));
1175
2111
  assert!(store.exists(&branch1_id));
2112
+ assert_eq!(store.parent(&branch1_id).unwrap().id, root_id);
2113
+ assert_eq!(store.get(&branch1_id).unwrap().content, r#"{"path":"a"}"#);
1176
2114
  }
1177
2115
 
1178
2116
  let _ = std::fs::remove_file(&db_path);
1179
2117
  }
1180
2118
 
1181
2119
  #[test]
1182
- fn test_recovery_current_pointer() {
2120
+ fn test_every_revision_stays_retrievable_by_identity_after_restart() {
1183
2121
  use std::sync::Arc;
1184
2122
 
1185
2123
  let db_path = std::env::temp_dir().join("test_recovery_current.log");
@@ -1188,34 +2126,36 @@ mod tests {
1188
2126
  let rev1_id;
1189
2127
  let rev2_id;
1190
2128
 
1191
- // Phase 1: Create and move current pointer
2129
+ // Phase 1: Two revisions, one descending from the other.
1192
2130
  {
1193
2131
  let db = crate::FeltDb::open(&db_path).expect("open db");
1194
2132
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1195
2133
 
1196
- let rev1 = store.create(r#"{"state":"1"}"#.to_string(), "auth".to_string())
2134
+ let rev1 = store
2135
+ .create("docs:1", r#"{"state":"1"}"#.to_string(), "auth".to_string())
1197
2136
  .expect("create rev1");
1198
2137
  rev1_id = rev1.id.clone();
1199
2138
 
1200
- // Current should be rev1
1201
- assert_eq!(store.current().unwrap().id, rev1_id);
2139
+ assert_eq!(store.get(&rev1_id).unwrap().id, rev1_id);
1202
2140
 
1203
- let rev2 = store.commit(r#"{"state":"2"}"#.to_string(), &rev1, "auth".to_string())
2141
+ let rev2 = store
2142
+ .commit(r#"{"state":"2"}"#.to_string(), &rev1, "auth".to_string())
1204
2143
  .expect("create rev2");
1205
2144
  rev2_id = rev2.id.clone();
1206
2145
 
1207
- // Current should move to rev2
1208
- assert_eq!(store.current().unwrap().id, rev2_id);
2146
+ // Committing a child does not displace the parent.
2147
+ assert_eq!(store.get(&rev2_id).unwrap().id, rev2_id);
2148
+ assert_eq!(store.get(&rev1_id).unwrap().id, rev1_id);
1209
2149
  }
1210
2150
 
1211
- // Phase 2: Verify current pointer survived restart
2151
+ // Phase 2: both survive, and neither is privileged over the other.
1212
2152
  {
1213
2153
  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");
2154
+ let store = StateStore::with_feltdb(Arc::new(db)).expect("create store over same log");
1215
2155
 
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"}"#);
2156
+ assert_eq!(store.get(&rev1_id).unwrap().content, r#"{"state":"1"}"#);
2157
+ assert_eq!(store.get(&rev2_id).unwrap().content, r#"{"state":"2"}"#);
2158
+ assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1219
2159
  }
1220
2160
 
1221
2161
  let _ = std::fs::remove_file(&db_path);
@@ -1237,27 +2177,35 @@ mod tests {
1237
2177
  let db = crate::FeltDb::open(&db_path).expect("open db");
1238
2178
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1239
2179
 
1240
- let root = store.create(r#"{"v":0}"#.to_string(), "auth".to_string())
2180
+ let root = store
2181
+ .create("docs:1", r#"{"v":0}"#.to_string(), "auth".to_string())
1241
2182
  .expect("create root");
1242
2183
  root_id = root.id.clone();
1243
2184
 
1244
- let rev_a = store.commit(r#"{"v":1,"stage":"a"}"#.to_string(), &root, "auth".to_string())
2185
+ let rev_a = store
2186
+ .commit(
2187
+ r#"{"v":1,"stage":"a"}"#.to_string(),
2188
+ &root,
2189
+ "auth".to_string(),
2190
+ )
1245
2191
  .expect("create rev_a");
1246
2192
  rev_a_id = rev_a.id.clone();
1247
2193
 
1248
- let rev_b = store.commit(r#"{"v":2,"stage":"b"}"#.to_string(), &rev_a, "auth".to_string())
2194
+ let rev_b = store
2195
+ .commit(
2196
+ r#"{"v":2,"stage":"b"}"#.to_string(),
2197
+ &rev_a,
2198
+ "auth".to_string(),
2199
+ )
1249
2200
  .expect("create rev_b");
1250
2201
  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
2202
  }
1256
2203
 
1257
2204
  // Phase 2: Verify complex topology recovered
1258
2205
  {
1259
2206
  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");
2207
+ let store =
2208
+ StateStore::with_feltdb(Arc::new(db)).expect("create store from recovered db");
1261
2209
 
1262
2210
  // Verify all revisions
1263
2211
  assert!(store.exists(&root_id));
@@ -1268,67 +2216,75 @@ mod tests {
1268
2216
  assert_eq!(store.parent(&rev_b_id).unwrap().id, rev_a_id);
1269
2217
  assert_eq!(store.parent(&rev_a_id).unwrap().id, root_id);
1270
2218
 
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);
2219
+ // Any point in the history is a checkpoint, addressed by its own
2220
+ // identity. Naming one adds nothing the id does not already give.
2221
+ assert_eq!(
2222
+ store.get(&rev_a_id).unwrap().content,
2223
+ r#"{"v":1,"stage":"a"}"#
2224
+ );
2225
+ assert_eq!(
2226
+ store.get(&rev_b_id).unwrap().content,
2227
+ r#"{"v":2,"stage":"b"}"#
2228
+ );
1277
2229
  }
1278
2230
 
1279
2231
  let _ = std::fs::remove_file(&db_path);
1280
2232
  }
1281
2233
 
1282
2234
  #[test]
1283
- fn test_recovery_multiple_restart_cycles() {
2235
+ fn test_revisions_survive_multiple_restart_cycles() {
1284
2236
  use std::sync::Arc;
1285
2237
 
1286
2238
  let db_path = std::env::temp_dir().join("test_recovery_cycles.log");
1287
2239
  let _ = std::fs::remove_file(&db_path);
1288
2240
 
2241
+ // The caller carries the identities it cares about across restarts.
2242
+ // That is the whole of what replaces a `current` pointer here.
2243
+ let rev1_id;
2244
+ let rev2_id;
2245
+
1289
2246
  // Cycle 1: Create root
1290
2247
  {
1291
2248
  let db = crate::FeltDb::open(&db_path).expect("open db");
1292
2249
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1293
- store.create(r#"{"cycle":1}"#.to_string(), "auth".to_string()).expect("create");
2250
+ rev1_id = store
2251
+ .create("docs:1", r#"{"cycle":1}"#.to_string(), "auth".to_string())
2252
+ .expect("create")
2253
+ .id;
1294
2254
  }
1295
2255
 
1296
- // Cycle 2: Restart, verify, and commit
2256
+ // Cycle 2: Restart, read by identity, and commit onto it
1297
2257
  {
1298
2258
  let db = crate::FeltDb::open(&db_path).expect("open db");
1299
2259
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1300
- assert!(store.current().is_some());
1301
2260
 
1302
- let rev1 = store.current().unwrap();
2261
+ let rev1 = store.get(&rev1_id).expect("rev1 survives");
1303
2262
  assert_eq!(rev1.content, r#"{"cycle":1}"#);
1304
-
1305
- store.commit(r#"{"cycle":2}"#.to_string(), &rev1, "auth".to_string()).expect("commit 2");
2263
+
2264
+ rev2_id = store
2265
+ .commit(r#"{"cycle":2}"#.to_string(), &rev1, "auth".to_string())
2266
+ .expect("commit 2")
2267
+ .id;
1306
2268
  }
1307
2269
 
1308
- // Cycle 3: Restart, create branch
2270
+ // Cycle 3: Restart, both are still there
1309
2271
  {
1310
2272
  let db = crate::FeltDb::open(&db_path).expect("open db");
1311
2273
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1312
- assert!(store.current().is_some());
1313
2274
 
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");
2275
+ assert_eq!(store.get(&rev2_id).unwrap().content, r#"{"cycle":2}"#);
2276
+ assert_eq!(store.parent(&rev2_id).unwrap().id, rev1_id);
1318
2277
  }
1319
2278
 
1320
- // Cycle 4: Final restart and verify everything survived
2279
+ // Cycle 4: Final restart, ancestry intact end to end
1321
2280
  {
1322
2281
  let db = crate::FeltDb::open(&db_path).expect("open db");
1323
2282
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1324
2283
 
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()));
2284
+ let rev2 = store.get(&rev2_id).expect("rev2 survives every cycle");
2285
+ assert_eq!(rev2.content, r#"{"cycle":2}"#);
2286
+ assert_eq!(rev2.parent_id, Some(rev1_id.clone()));
2287
+ assert!(store.parent(&rev1_id).is_none(), "the root has no parent");
1332
2288
  }
1333
2289
 
1334
2290
  let _ = std::fs::remove_file(&db_path);
@@ -1357,10 +2313,13 @@ mod tests {
1357
2313
  let db = crate::FeltDb::open(&db_path).expect("create db");
1358
2314
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1359
2315
 
1360
- let rev = store.create(
1361
- r#"{"task":"write PR18 proof"}"#.to_string(),
1362
- "system".to_string(),
1363
- ).expect("write");
2316
+ let rev = store
2317
+ .create(
2318
+ "docs:1",
2319
+ r#"{"task":"write PR18 proof"}"#.to_string(),
2320
+ "system".to_string(),
2321
+ )
2322
+ .expect("write");
1364
2323
 
1365
2324
  rev.id.clone()
1366
2325
  }; // Force drop of database, persist to disk
@@ -1370,15 +2329,19 @@ mod tests {
1370
2329
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1371
2330
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1372
2331
 
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");
2332
+ // VERIFY: the revision is retrievable by identity after restart
2333
+ let recovered = store
2334
+ .get(&written_id)
2335
+ .expect("State should exist after restart");
2336
+ assert_eq!(recovered.id, written_id, "State ID should match");
1377
2337
  assert_eq!(
1378
- current.content,
1379
- r#"{"task":"write PR18 proof"}"#,
2338
+ recovered.content, r#"{"task":"write PR18 proof"}"#,
1380
2339
  "State content should survive restart"
1381
2340
  );
2341
+ assert!(
2342
+ recovered.verify_integrity(),
2343
+ "content must still hash to its identity"
2344
+ );
1382
2345
  }
1383
2346
 
1384
2347
  let _ = std::fs::remove_file(&db_path);
@@ -1398,24 +2361,31 @@ mod tests {
1398
2361
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1399
2362
 
1400
2363
  // Create project
1401
- let project = store.create(
1402
- r#"{"type":"project","name":"FeltDB"}"#.to_string(),
1403
- "system".to_string(),
1404
- ).expect("create project");
2364
+ let project = store
2365
+ .create(
2366
+ "docs:1",
2367
+ r#"{"type":"project","name":"FeltDB"}"#.to_string(),
2368
+ "system".to_string(),
2369
+ )
2370
+ .expect("create project");
1405
2371
 
1406
2372
  // 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");
2373
+ let membership = store
2374
+ .commit(
2375
+ r#"{"type":"membership","project":"FeltDB","user":"alice"}"#.to_string(),
2376
+ &project,
2377
+ "system".to_string(),
2378
+ )
2379
+ .expect("create membership");
1412
2380
 
1413
2381
  // 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");
2382
+ let event = store
2383
+ .commit(
2384
+ r#"{"type":"audit","action":"project_created"}"#.to_string(),
2385
+ &membership,
2386
+ "system".to_string(),
2387
+ )
2388
+ .expect("create event");
1419
2389
 
1420
2390
  (project.id.clone(), membership.id.clone(), event.id.clone())
1421
2391
  }; // Force persist
@@ -1425,12 +2395,13 @@ mod tests {
1425
2395
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1426
2396
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1427
2397
 
1428
- let current = store.current().expect("current should exist");
1429
- assert_eq!(current.id, event_id, "Event should be current");
2398
+ let event = store.get(&event_id).expect("event should exist");
2399
+ assert_eq!(event.id, event_id);
1430
2400
 
1431
2401
  // Verify chain: event → membership → project
1432
2402
  assert_eq!(
1433
- current.parent_id, Some(membership_id.clone()),
2403
+ event.parent_id,
2404
+ Some(membership_id.clone()),
1434
2405
  "Event parent should be membership"
1435
2406
  );
1436
2407
 
@@ -1454,22 +2425,29 @@ mod tests {
1454
2425
  let db = crate::FeltDb::open(&db_path).expect("create db");
1455
2426
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1456
2427
 
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");
2428
+ let root = store
2429
+ .create(
2430
+ "docs:1",
2431
+ r#"{"generation":"root"}"#.to_string(),
2432
+ "genesis".to_string(),
2433
+ )
2434
+ .expect("create root");
1467
2435
 
1468
- let grandchild = store.commit(
1469
- r#"{"generation":"grandchild"}"#.to_string(),
1470
- &child,
1471
- "genesis".to_string(),
1472
- ).expect("create grandchild");
2436
+ let child = store
2437
+ .commit(
2438
+ r#"{"generation":"child"}"#.to_string(),
2439
+ &root,
2440
+ "genesis".to_string(),
2441
+ )
2442
+ .expect("create child");
2443
+
2444
+ let grandchild = store
2445
+ .commit(
2446
+ r#"{"generation":"grandchild"}"#.to_string(),
2447
+ &child,
2448
+ "genesis".to_string(),
2449
+ )
2450
+ .expect("create grandchild");
1473
2451
 
1474
2452
  (root.id.clone(), child.id.clone(), grandchild.id.clone())
1475
2453
  };
@@ -1479,12 +2457,12 @@ mod tests {
1479
2457
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1480
2458
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1481
2459
 
1482
- let current = store.current().expect("current exists");
1483
- assert_eq!(current.id, grandchild_id, "Grandchild is current");
2460
+ let grandchild = store.get(&grandchild_id).expect("grandchild exists");
2461
+ assert_eq!(grandchild.id, grandchild_id);
1484
2462
 
1485
2463
  // Trace ancestry: grandchild → child → root
1486
2464
  let mut ancestry_count = 0;
1487
- let mut current_id = Some(current.id.clone());
2465
+ let mut current_id = Some(grandchild.id.clone());
1488
2466
 
1489
2467
  // Count links in ancestry (stop after finding root)
1490
2468
  while let Some(id) = current_id {
@@ -1517,10 +2495,9 @@ mod tests {
1517
2495
  let db = crate::FeltDb::open(&db_path).expect("create db");
1518
2496
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1519
2497
 
1520
- let rev = store.create(
1521
- r#"{"cycle":1}"#.to_string(),
1522
- "system".to_string(),
1523
- ).expect("create initial");
2498
+ let rev = store
2499
+ .create("docs:1", r#"{"cycle":1}"#.to_string(), "system".to_string())
2500
+ .expect("create initial");
1524
2501
 
1525
2502
  rev.id.clone()
1526
2503
  };
@@ -1530,16 +2507,18 @@ mod tests {
1530
2507
  let db = crate::FeltDb::open(&db_path).expect("reopen db");
1531
2508
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover store");
1532
2509
 
1533
- // Verify previous state exists
1534
- let current = store.current().expect("current exists");
1535
- assert_eq!(current.id, state_id, "Previous state should exist");
2510
+ // Verify previous state exists, read back by the id we carried
2511
+ let previous = store.get(&state_id).expect("previous state should exist");
2512
+ assert_eq!(previous.id, state_id);
1536
2513
 
1537
2514
  // Update state
1538
- let rev = store.commit(
1539
- format!(r#"{{"cycle":{}}}"#, cycle),
1540
- &current,
1541
- "system".to_string(),
1542
- ).expect("create update");
2515
+ let rev = store
2516
+ .commit(
2517
+ format!(r#"{{"cycle":{}}}"#, cycle),
2518
+ &previous,
2519
+ "system".to_string(),
2520
+ )
2521
+ .expect("create update");
1543
2522
 
1544
2523
  state_id = rev.id.clone();
1545
2524
  }
@@ -1549,7 +2528,7 @@ mod tests {
1549
2528
  let db = crate::FeltDb::open(&db_path).expect("final reopen");
1550
2529
  let store = StateStore::with_feltdb(Arc::new(db)).expect("final recovery");
1551
2530
 
1552
- let final_state = store.current().expect("final state exists");
2531
+ let final_state = store.get(&state_id).expect("final state exists");
1553
2532
  assert_eq!(final_state.id, state_id, "Final state should match");
1554
2533
  }
1555
2534
 
@@ -1569,10 +2548,13 @@ mod tests {
1569
2548
  let db = crate::FeltDb::open(&db_path).expect("create db");
1570
2549
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1571
2550
 
1572
- let rev = store.create(
1573
- r#"{"status":"initial"}"#.to_string(),
1574
- "system".to_string(),
1575
- ).expect("create initial");
2551
+ let rev = store
2552
+ .create(
2553
+ "docs:1",
2554
+ r#"{"status":"initial"}"#.to_string(),
2555
+ "system".to_string(),
2556
+ )
2557
+ .expect("create initial");
1576
2558
 
1577
2559
  rev.id.clone()
1578
2560
  };
@@ -1586,11 +2568,18 @@ mod tests {
1586
2568
  let db = crate::FeltDb::open(&db_path).expect("reopen");
1587
2569
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
1588
2570
 
1589
- if let Some(current) = store.current() {
2571
+ if let Some(recovered) = store.get(&initial_id) {
1590
2572
  // 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");
2573
+ assert!(
2574
+ recovered.content.contains("status"),
2575
+ "State must be complete"
2576
+ );
2577
+ assert!(
2578
+ recovered.content.contains("initial"),
2579
+ "Attributes must be present"
2580
+ );
2581
+ assert_eq!(recovered.id, initial_id, "ID must match");
2582
+ assert!(recovered.verify_integrity(), "and never partially written");
1594
2583
  } else {
1595
2584
  // State doesn't exist: acceptable
1596
2585
  // But not: partially exist with missing fields
@@ -1614,10 +2603,13 @@ mod tests {
1614
2603
  let db = crate::FeltDb::open(&db_path).expect("create db");
1615
2604
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1616
2605
 
1617
- let rev = store.create(
1618
- expected_content.to_string(),
1619
- "integrity_check".to_string(),
1620
- ).expect("create");
2606
+ let rev = store
2607
+ .create(
2608
+ "docs:1",
2609
+ expected_content.to_string(),
2610
+ "integrity_check".to_string(),
2611
+ )
2612
+ .expect("create");
1621
2613
 
1622
2614
  rev.id.clone()
1623
2615
  };
@@ -1627,14 +2619,15 @@ mod tests {
1627
2619
  let db = crate::FeltDb::open(&db_path).expect("reopen");
1628
2620
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
1629
2621
 
1630
- let current = store.current().expect("state exists");
2622
+ let recovered = store.get(&state_id).expect("state exists");
1631
2623
 
1632
2624
  // 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");
2625
+ assert_eq!(recovered.content, expected_content, "Content should match");
2626
+ assert_eq!(recovered.id, state_id, "ID should match");
2627
+ assert!(
2628
+ recovered.verify_integrity(),
2629
+ "Content hash should still be valid"
2630
+ );
1638
2631
  }
1639
2632
 
1640
2633
  let _ = std::fs::remove_file(&db_path);
@@ -1644,59 +2637,72 @@ mod tests {
1644
2637
  fn pr19_proof_concurrent_write_safety() {
1645
2638
  // PROVES: Concurrent writes don't produce impossible partial states
1646
2639
  // Simulates multiple threads writing to the same StateStore
1647
-
2640
+
1648
2641
  use std::sync::{Arc, Mutex};
1649
2642
  use std::thread;
1650
-
2643
+
1651
2644
  let db_path = std::env::temp_dir().join("pr19_concurrent_writes.log");
1652
2645
  let _ = std::fs::remove_file(&db_path);
1653
-
2646
+
1654
2647
  let db = crate::FeltDb::open(&db_path).expect("create db");
1655
2648
  let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
1656
-
2649
+
1657
2650
  let write_count = Arc::new(Mutex::new(0usize));
1658
2651
  let mut handles = vec![];
1659
-
2652
+
1660
2653
  // Spawn multiple writers
1661
2654
  for i in 0..5 {
1662
2655
  let store_clone = Arc::clone(&store);
1663
2656
  let count_clone = Arc::clone(&write_count);
1664
-
2657
+
1665
2658
  let handle = thread::spawn(move || {
1666
2659
  let content = format!(r#"{{"writer":{},"timestamp":{}}}"#, i, 1000 + i);
1667
- let rev = store_clone.create(content, format!("writer_{}", i))
2660
+ let rev = store_clone
2661
+ .create(&format!("writer:{i}"), content, format!("writer_{}", i))
1668
2662
  .expect("write should succeed");
1669
-
2663
+
1670
2664
  let mut count = count_clone.lock().unwrap();
1671
2665
  *count += 1;
1672
-
2666
+
1673
2667
  rev.id.clone()
1674
2668
  });
1675
-
2669
+
1676
2670
  handles.push(handle);
1677
2671
  }
1678
-
2672
+
1679
2673
  // Wait for all writers
1680
2674
  let mut written_ids = vec![];
1681
2675
  for handle in handles {
1682
2676
  let id = handle.join().expect("thread should complete");
1683
2677
  written_ids.push(id);
1684
2678
  }
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
-
2679
+
2680
+ // Verify no partial states were created: every id a writer returned is
2681
+ // readable, complete, and hashes to its own content.
2682
+ for id in &written_ids {
2683
+ let written = store
2684
+ .get(id)
2685
+ .expect("every concurrently written revision should exist");
2686
+ assert!(
2687
+ written.verify_integrity(),
2688
+ "no writer produced a partial revision"
2689
+ );
2690
+ }
2691
+
1692
2692
  // 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
-
2693
+ let final_write = store
2694
+ .create(
2695
+ "docs:1",
2696
+ r#"{"final":"write","success":true}"#.to_string(),
2697
+ "system".to_string(),
2698
+ )
2699
+ .expect("final write should succeed");
2700
+
2701
+ assert!(
2702
+ final_write.verify_integrity(),
2703
+ "Final write should be valid"
2704
+ );
2705
+
1700
2706
  let _ = std::fs::remove_file(&db_path);
1701
2707
  }
1702
2708
 
@@ -1706,23 +2712,26 @@ mod tests {
1706
2712
  // Simulates: write → sync starts → crash → restart
1707
2713
  // Expected: Either data is on disk (fully recovered) or not (nothing recovered)
1708
2714
  // Never: Partial/corrupted data
1709
-
2715
+
1710
2716
  let db_path = std::env::temp_dir().join("pr19_crash_during_sync.log");
1711
2717
  let _ = std::fs::remove_file(&db_path);
1712
-
2718
+
1713
2719
  // First write: known-good state
1714
2720
  let initial_id = {
1715
2721
  let db = crate::FeltDb::open(&db_path).expect("create db");
1716
2722
  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
-
2723
+
2724
+ let rev = store
2725
+ .create(
2726
+ "docs:1",
2727
+ r#"{"status":"pre-crash","integrity":"good"}"#.to_string(),
2728
+ "system".to_string(),
2729
+ )
2730
+ .expect("write");
2731
+
1723
2732
  rev.id.clone()
1724
2733
  };
1725
-
2734
+
1726
2735
  // After restart, verify state is either:
1727
2736
  // A) Initial state is still there (sync didn't complete), OR
1728
2737
  // B) New state is there and fully valid (sync completed), OR
@@ -1730,12 +2739,17 @@ mod tests {
1730
2739
  {
1731
2740
  let db = crate::FeltDb::open(&db_path).expect("reopen");
1732
2741
  let store = StateStore::with_feltdb(Arc::new(db)).expect("recover");
1733
-
1734
- let current = store.current().expect("state should exist");
1735
-
2742
+
2743
+ let current = store
2744
+ .get(&initial_id)
2745
+ .expect("the initial state should still exist");
2746
+
1736
2747
  // Verify state integrity
1737
- assert!(current.verify_integrity(), "State should be internally consistent");
1738
-
2748
+ assert!(
2749
+ current.verify_integrity(),
2750
+ "State should be internally consistent"
2751
+ );
2752
+
1739
2753
  // State should either be the initial write or a valid new write
1740
2754
  // But never partial or corrupted
1741
2755
  assert!(
@@ -1743,7 +2757,7 @@ mod tests {
1743
2757
  "Recovered state must be either initial (sync failed) or new+valid (sync succeeded)"
1744
2758
  );
1745
2759
  }
1746
-
2760
+
1747
2761
  let _ = std::fs::remove_file(&db_path);
1748
2762
  }
1749
2763
 
@@ -1752,30 +2766,31 @@ mod tests {
1752
2766
  // PROVES: Query execution uses indexes to reduce complexity
1753
2767
  // Currently this is unproven - indexes are data structures but
1754
2768
  // query execution path is not proven to use them
1755
-
2769
+
1756
2770
  let db_path = std::env::temp_dir().join("pr19_query_index.log");
1757
2771
  let _ = std::fs::remove_file(&db_path);
1758
-
2772
+
1759
2773
  let db = crate::FeltDb::open(&db_path).expect("create db");
1760
2774
  let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
1761
-
2775
+
1762
2776
  // Create records with indexed fields
1763
2777
  for i in 0..100 {
1764
2778
  let content = format!(r#"{{"user_id":"user_{}","active":true}}"#, i % 10);
1765
- let _ = store.create(content, format!("creator_{}", i))
2779
+ let _ = store
2780
+ .create(&format!("record:{i}"), content, format!("creator_{}", i))
1766
2781
  .expect("write record");
1767
2782
  }
1768
-
2783
+
1769
2784
  // Query by indexed field (user_id in this case)
1770
2785
  // If indexes are used, this should be O(log n)
1771
2786
  // If not, this is O(n)
1772
-
2787
+
1773
2788
  // LIMITATION: No way to measure index usage in this test
1774
2789
  // This proves index data structures exist, not that they're used
1775
-
2790
+
1776
2791
  // TODO: Measure query latency and verify index benefit
1777
2792
  // TODO: Add query optimizer tracing to prove index selection
1778
-
2793
+
1779
2794
  let _ = std::fs::remove_file(&db_path);
1780
2795
  }
1781
2796
 
@@ -1784,36 +2799,45 @@ mod tests {
1784
2799
  // PROVES: Basic multi-tenant isolation at storage layer
1785
2800
  // Note: This tests storage isolation, not authorization layer
1786
2801
  // Authorization layer tests are in authorization_security_tests.rs
1787
-
2802
+
1788
2803
  let db_path = std::env::temp_dir().join("pr19_multi_tenant.log");
1789
2804
  let _ = std::fs::remove_file(&db_path);
1790
-
2805
+
1791
2806
  let db = crate::FeltDb::open(&db_path).expect("create db");
1792
2807
  let store = Arc::new(StateStore::with_feltdb(Arc::new(db)).expect("create store"));
1793
-
2808
+
1794
2809
  // Create tenant-scoped records
1795
2810
  // 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
-
2811
+ let tenant_a_record = store
2812
+ .create(
2813
+ "tenant_a:1",
2814
+ r#"{"tenant":"tenant_a","data":"secret_a"}"#.to_string(),
2815
+ "tenant_a".to_string(),
2816
+ )
2817
+ .expect("tenant_a write");
2818
+
1801
2819
  // 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
-
2820
+ let tenant_b_record = store
2821
+ .create(
2822
+ "tenant_b:1",
2823
+ r#"{"tenant":"tenant_b","data":"secret_b"}"#.to_string(),
2824
+ "tenant_b".to_string(),
2825
+ )
2826
+ .expect("tenant_b write");
2827
+
1807
2828
  // 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
-
2829
+ assert_ne!(
2830
+ tenant_a_record.id, tenant_b_record.id,
2831
+ "Records should have different IDs"
2832
+ );
2833
+
1810
2834
  // Current limitation: StateStore doesn't enforce tenant boundaries
1811
2835
  // Tenant isolation is enforced at authorization layer, not storage layer
1812
2836
  // This test proves that separate StateIds are created for each record
1813
-
2837
+
1814
2838
  // TODO: Test authorization layer rejection of cross-tenant access
1815
2839
  // See authorization_security_tests.rs
1816
-
2840
+
1817
2841
  let _ = std::fs::remove_file(&db_path);
1818
2842
  }
1819
2843
 
@@ -1821,36 +2845,41 @@ mod tests {
1821
2845
  fn pr19_proof_atomicity_requires_sync_return() {
1822
2846
  // PROVES: Atomicity guarantee only holds if sync() succeeds
1823
2847
  // If sync() fails, no durability guarantee
1824
-
2848
+
1825
2849
  let db_path = std::env::temp_dir().join("pr19_atomicity_sync.log");
1826
2850
  let _ = std::fs::remove_file(&db_path);
1827
-
2851
+
1828
2852
  let rev_id = {
1829
2853
  let db = crate::FeltDb::open(&db_path).expect("create db");
1830
2854
  let store = StateStore::with_feltdb(Arc::new(db)).expect("create store");
1831
-
2855
+
1832
2856
  // Write should be durable only if it returns Ok
1833
2857
  let result = store.create(
2858
+ "docs:1",
1834
2859
  r#"{"atomicity":"requires_sync_success"}"#.to_string(),
1835
2860
  "system".to_string(),
1836
2861
  );
1837
-
2862
+
1838
2863
  // If create() returns Ok, durability is guaranteed
1839
2864
  match result {
1840
2865
  Ok(rev) => Some(rev.id.clone()),
1841
2866
  Err(_) => None,
1842
2867
  }
1843
2868
  }; // Force drop of db to simulate restart
1844
-
2869
+
1845
2870
  if let Some(rev_id) = rev_id {
1846
2871
  // Durability guaranteed: restart should see this
1847
2872
  let db2 = crate::FeltDb::open(&db_path).expect("reopen");
1848
2873
  let store2 = StateStore::with_feltdb(Arc::new(db2)).expect("recover");
1849
-
1850
- assert_eq!(store2.current().unwrap().id, rev_id, "Durability guaranteed");
2874
+
2875
+ assert_eq!(
2876
+ store2.get(&rev_id).unwrap().id,
2877
+ rev_id,
2878
+ "Durability guaranteed"
2879
+ );
1851
2880
  }
1852
2881
  // If None, no durability guarantee - sync failed
1853
-
2882
+
1854
2883
  let _ = std::fs::remove_file(&db_path);
1855
2884
  }
1856
2885
  }