@feltdb/core 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/cli/commands.js +4 -1
  2. package/dist/cli/provisioning-neutrality.js +79 -0
  3. package/dist/collection.d.ts +43 -1
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +192 -22
  6. package/dist/create/create.js +25 -21
  7. package/dist/create/managed-account.js +11 -0
  8. package/dist/create/package-versions.js +1 -1
  9. package/dist/create/server-source/Cargo.lock +165 -0
  10. package/dist/create/server-source/Cargo.toml +9 -0
  11. package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
  28. package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
  29. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
  30. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
  31. package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
  32. package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
  33. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
  34. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
  35. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
  36. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
  37. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
  38. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
  39. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
  40. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
  41. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
  42. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
  43. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
  44. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
  45. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
  46. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
  47. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
  48. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
  49. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
  50. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
  51. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +595 -0
  52. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
  53. package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
  54. package/dist/create/server-source/crates/feltdb/src/lib.rs +1050 -117
  55. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
  56. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
  57. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
  58. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
  59. package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
  60. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
  61. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
  62. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
  63. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
  64. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
  65. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
  66. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
  67. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
  68. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
  69. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
  70. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +90 -72
  71. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
  72. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
  73. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
  74. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
  75. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
  76. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
  77. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
  78. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
  79. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
  80. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
  81. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
  82. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
  83. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
  84. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
  85. package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
  86. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +126 -0
  87. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
  88. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
  89. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
  90. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
  91. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
  92. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
  93. package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
  94. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
  95. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +305 -6
  96. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
  97. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
  98. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
  99. package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
  100. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
  101. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
  102. package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
  103. package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
  104. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
  105. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
  106. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
  107. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
  108. package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
  109. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
  110. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  111. package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
  112. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +442 -0
  113. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
  114. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
  115. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
  116. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
  117. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
  118. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
  119. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
  120. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +234 -0
  121. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +947 -0
  122. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
  123. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
  124. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
  125. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
  126. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
  127. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
  128. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
  129. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
  130. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
  131. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
  132. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
  133. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
  134. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
  135. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
  136. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
  137. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
  138. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
  139. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
  140. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
  141. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
  142. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
  143. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
  144. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
  145. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
  146. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
  147. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
  149. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
  150. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
  151. package/dist/create/server-source/crates/feltdb-server/src/main.rs +800 -34
  152. package/dist/db.d.ts +33 -34
  153. package/dist/db.d.ts.map +1 -1
  154. package/dist/db.js +74 -20
  155. package/dist/deployment.d.ts +30 -0
  156. package/dist/deployment.d.ts.map +1 -0
  157. package/dist/deployment.js +130 -0
  158. package/dist/embedded-transaction.d.ts +22 -4
  159. package/dist/embedded-transaction.d.ts.map +1 -1
  160. package/dist/embedded-transaction.js +51 -5
  161. package/dist/feltdb.d.ts +14 -2
  162. package/dist/feltdb.d.ts.map +1 -1
  163. package/dist/file-db.js +1 -1
  164. package/dist/http-client.d.ts +14 -0
  165. package/dist/http-client.d.ts.map +1 -1
  166. package/dist/http-client.js +23 -5
  167. package/dist/http-db.d.ts +119 -1
  168. package/dist/http-db.d.ts.map +1 -1
  169. package/dist/http-db.js +346 -31
  170. package/dist/index-core.d.ts +2 -0
  171. package/dist/index-core.d.ts.map +1 -1
  172. package/dist/index-core.js +2 -0
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +9 -0
  175. package/dist/indexeddb-db.d.ts.map +1 -1
  176. package/dist/indexeddb-db.js +35 -21
  177. package/dist/managed-recovery.d.ts +192 -0
  178. package/dist/managed-recovery.d.ts.map +1 -0
  179. package/dist/managed-recovery.js +242 -0
  180. package/dist/memory-db.js +1 -1
  181. package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.js → feltdb_wasm-DaNwCLRX.js} +1 -1
  182. package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
  183. package/dist/studio-app/assets/index-j8IlhNqJ.js +29 -0
  184. package/dist/studio-app/index.html +1 -1
  185. package/dist/transaction.d.ts +30 -0
  186. package/dist/transaction.d.ts.map +1 -1
  187. package/dist/transaction.js +41 -0
  188. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  189. package/dist/workload.d.ts +2 -1
  190. package/dist/workload.d.ts.map +1 -1
  191. package/package.json +1 -1
  192. package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
  193. package/dist/studio-app/assets/index-B0k4UAlI.js +0 -29
@@ -0,0 +1,739 @@
1
+ //! The revision model decision experiment.
2
+ //!
3
+ //! This file **implements nothing in the product**. It builds three tiny probes
4
+ //! of what a `StateRevision` could *be*, runs the same seven scenarios against
5
+ //! each, and fills in a decision matrix by execution rather than by preference.
6
+ //!
7
+ //! It is the executable half of `docs/architecture/revision-model-decision.md`.
8
+ //!
9
+ //! # The question
10
+ //!
11
+ //! The [identity and ancestry contract](../../../docs/architecture/revision-identity-and-ancestry.md)
12
+ //! established that content-only identity cannot survive automatic minting, and
13
+ //! that `base` as "the previous state of resource X" is unanswerable because a
14
+ //! `StateRevision` has no resource. So the decision is not which fix to apply.
15
+ //! It is:
16
+ //!
17
+ //! > **What is a `StateRevision` a revision *of*?**
18
+ //!
19
+ //! Three answers are consistent with the evidence:
20
+ //!
21
+ //! | model | a revision is | identity |
22
+ //! | --- | --- | --- |
23
+ //! | **A — State** | a durable content | content alone; ancestry lives outside the revision |
24
+ //! | **B — Historical transition** | a particular occurrence in history | content + parent + a discriminator |
25
+ //! | **C — Resource transition** | a transition belonging to a resource | resource + content + parent + a discriminator |
26
+ //!
27
+ //! # Two invariants, asserted against every model
28
+ //!
29
+ //! These are not model choices. They are the boundary the T5 and T6 findings
30
+ //! draw, and a model that cannot hold them is not a revision model:
31
+ //!
32
+ //! 1. **Once committed, a revision's identity and ancestry are immutable.** A
33
+ //! record whose historical parent can change after commit is not a durable
34
+ //! historical object; it is a mutable record that happens to contain
35
+ //! historical fields.
36
+ //! 2. **A `parent_id` is either a reference to an authoritative revision, or it
37
+ //! is not a parent.** Parent existence is part of what makes a revision
38
+ //! valid, not a lookup applied afterwards.
39
+ //!
40
+ //! The probes are deliberately small and their identities are readable strings
41
+ //! rather than hashes: what is under test is the *structure* of identity, not
42
+ //! its encoding.
43
+ //!
44
+ //! Storage cost, compaction economics and copy-on-write are **deliberately
45
+ //! absent**. Measuring them before the object is defined would optimise
46
+ //! something that does not yet exist.
47
+
48
+ use feltdb::state_model::{StateRevision, StateStore};
49
+ use feltdb::FeltDb;
50
+ use std::collections::BTreeMap;
51
+ use std::sync::Arc;
52
+ use tempfile::TempDir;
53
+
54
+ type Rid = String;
55
+
56
+ #[derive(Debug, PartialEq, Eq)]
57
+ enum MintError {
58
+ /// Invariant 2: the named parent is not an authoritative revision.
59
+ UnknownParent,
60
+ }
61
+
62
+ /// The model has no way to express the question, as opposed to answering "no".
63
+ #[derive(Debug, PartialEq, Eq)]
64
+ struct Unsupported(&'static str);
65
+
66
+ /// What every candidate model must be able to do.
67
+ trait Model {
68
+ fn name(&self) -> &'static str;
69
+
70
+ /// Mint a revision of `content` for `resource`, descending from `parent`.
71
+ fn mint(
72
+ &mut self,
73
+ resource: &str,
74
+ content: &str,
75
+ parent: Option<&Rid>,
76
+ ) -> Result<Rid, MintError>;
77
+
78
+ /// The parent of `id` as seen from `resource`.
79
+ fn parent_of(&self, resource: &str, id: &Rid) -> Option<Rid>;
80
+
81
+ /// The revisions of one resource, oldest first.
82
+ fn history_of(&self, resource: &str) -> Result<Vec<Rid>, Unsupported>;
83
+
84
+ /// Retain only the newest `keep` revisions of one resource.
85
+ fn retain_last(&mut self, resource: &str, keep: usize) -> Result<usize, Unsupported>;
86
+
87
+ /// Every durable record, for immutability checking.
88
+ fn records(&self) -> BTreeMap<Rid, String>;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Model A — a revision is a durable state
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /// Identity is content. Ancestry is **not a field of the revision** — it is a
96
+ /// separate edge set, so the same state recurring in history is one object
97
+ /// referenced twice rather than one object rewritten twice.
98
+ #[derive(Default)]
99
+ struct StateModel {
100
+ contents: BTreeMap<Rid, String>,
101
+ edges: Vec<Edge>,
102
+ }
103
+
104
+ struct Edge {
105
+ resource: String,
106
+ from: Option<Rid>,
107
+ to: Rid,
108
+ }
109
+
110
+ impl Model for StateModel {
111
+ fn name(&self) -> &'static str {
112
+ "A state"
113
+ }
114
+
115
+ fn mint(
116
+ &mut self,
117
+ resource: &str,
118
+ content: &str,
119
+ parent: Option<&Rid>,
120
+ ) -> Result<Rid, MintError> {
121
+ if let Some(parent) = parent {
122
+ if !self.contents.contains_key(parent) {
123
+ return Err(MintError::UnknownParent);
124
+ }
125
+ }
126
+ let id = format!("state:{content}");
127
+ // Idempotent by construction: the same content is the same bytes.
128
+ self.contents.insert(id.clone(), content.to_string());
129
+ self.edges.push(Edge {
130
+ resource: resource.to_string(),
131
+ from: parent.cloned(),
132
+ to: id.clone(),
133
+ });
134
+ Ok(id)
135
+ }
136
+
137
+ fn parent_of(&self, resource: &str, id: &Rid) -> Option<Rid> {
138
+ // The last time this state was reached on this resource.
139
+ self.edges
140
+ .iter()
141
+ .rev()
142
+ .find(|edge| edge.resource == resource && &edge.to == id)
143
+ .and_then(|edge| edge.from.clone())
144
+ }
145
+
146
+ fn history_of(&self, resource: &str) -> Result<Vec<Rid>, Unsupported> {
147
+ Ok(self
148
+ .edges
149
+ .iter()
150
+ .filter(|edge| edge.resource == resource)
151
+ .map(|edge| edge.to.clone())
152
+ .collect())
153
+ }
154
+
155
+ fn retain_last(&mut self, resource: &str, keep: usize) -> Result<usize, Unsupported> {
156
+ let mine: Vec<usize> = self
157
+ .edges
158
+ .iter()
159
+ .enumerate()
160
+ .filter(|(_, edge)| edge.resource == resource)
161
+ .map(|(index, _)| index)
162
+ .collect();
163
+ let drop_count = mine.len().saturating_sub(keep);
164
+ let dropped: Vec<usize> = mine.into_iter().take(drop_count).collect();
165
+ self.edges = std::mem::take(&mut self.edges)
166
+ .into_iter()
167
+ .enumerate()
168
+ .filter(|(index, _)| !dropped.contains(index))
169
+ .map(|(_, edge)| edge)
170
+ .collect();
171
+ // Contents stay: they are shared, and another resource may reference them.
172
+ Ok(drop_count)
173
+ }
174
+
175
+ fn records(&self) -> BTreeMap<Rid, String> {
176
+ self.contents.clone()
177
+ }
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Model B — a revision is a historical transition
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /// Identity is content plus parent plus a monotonic discriminator, so the same
185
+ /// content occurring twice is two revisions. There is **no resource**: a
186
+ /// revision is an occurrence in history, and history is not divided up.
187
+ #[derive(Default)]
188
+ struct TransitionModel {
189
+ revisions: BTreeMap<Rid, (String, Option<Rid>)>,
190
+ sequence: u64,
191
+ }
192
+
193
+ impl Model for TransitionModel {
194
+ fn name(&self) -> &'static str {
195
+ "B transition"
196
+ }
197
+
198
+ fn mint(
199
+ &mut self,
200
+ _resource: &str,
201
+ content: &str,
202
+ parent: Option<&Rid>,
203
+ ) -> Result<Rid, MintError> {
204
+ if let Some(parent) = parent {
205
+ if !self.revisions.contains_key(parent) {
206
+ return Err(MintError::UnknownParent);
207
+ }
208
+ }
209
+ self.sequence += 1;
210
+ let id = format!(
211
+ "occurrence:{}:{}:{content}",
212
+ self.sequence,
213
+ parent.map(String::as_str).unwrap_or("root")
214
+ );
215
+ self.revisions
216
+ .insert(id.clone(), (content.to_string(), parent.cloned()));
217
+ Ok(id)
218
+ }
219
+
220
+ fn parent_of(&self, _resource: &str, id: &Rid) -> Option<Rid> {
221
+ self.revisions
222
+ .get(id)
223
+ .and_then(|(_, parent)| parent.clone())
224
+ }
225
+
226
+ fn history_of(&self, _resource: &str) -> Result<Vec<Rid>, Unsupported> {
227
+ Err(Unsupported(
228
+ "a transition belongs to no resource, so a resource has no history",
229
+ ))
230
+ }
231
+
232
+ fn retain_last(&mut self, _resource: &str, _keep: usize) -> Result<usize, Unsupported> {
233
+ Err(Unsupported(
234
+ "retention cannot be scoped to a resource the model cannot name",
235
+ ))
236
+ }
237
+
238
+ fn records(&self) -> BTreeMap<Rid, String> {
239
+ self.revisions
240
+ .iter()
241
+ .map(|(id, (content, parent))| (id.clone(), format!("{content}|{parent:?}")))
242
+ .collect()
243
+ }
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Model C — a revision is a transition of a resource
248
+ // ---------------------------------------------------------------------------
249
+
250
+ /// Identity is resource plus content plus parent plus a discriminator. A
251
+ /// revision knows what it is a revision *of*.
252
+ #[derive(Default)]
253
+ struct ResourceTransitionModel {
254
+ revisions: BTreeMap<Rid, (String, String, Option<Rid>)>,
255
+ sequence: u64,
256
+ }
257
+
258
+ impl Model for ResourceTransitionModel {
259
+ fn name(&self) -> &'static str {
260
+ "C resource transition"
261
+ }
262
+
263
+ fn mint(
264
+ &mut self,
265
+ resource: &str,
266
+ content: &str,
267
+ parent: Option<&Rid>,
268
+ ) -> Result<Rid, MintError> {
269
+ if let Some(parent) = parent {
270
+ if !self.revisions.contains_key(parent) {
271
+ return Err(MintError::UnknownParent);
272
+ }
273
+ }
274
+ self.sequence += 1;
275
+ let id = format!(
276
+ "{resource}@{}:{}:{content}",
277
+ self.sequence,
278
+ parent.map(String::as_str).unwrap_or("root")
279
+ );
280
+ self.revisions.insert(
281
+ id.clone(),
282
+ (resource.to_string(), content.to_string(), parent.cloned()),
283
+ );
284
+ Ok(id)
285
+ }
286
+
287
+ fn parent_of(&self, _resource: &str, id: &Rid) -> Option<Rid> {
288
+ self.revisions
289
+ .get(id)
290
+ .and_then(|(_, _, parent)| parent.clone())
291
+ }
292
+
293
+ fn history_of(&self, resource: &str) -> Result<Vec<Rid>, Unsupported> {
294
+ Ok(self
295
+ .revisions
296
+ .iter()
297
+ .filter(|(_, (owner, _, _))| owner == resource)
298
+ .map(|(id, _)| id.clone())
299
+ .collect())
300
+ }
301
+
302
+ fn retain_last(&mut self, resource: &str, keep: usize) -> Result<usize, Unsupported> {
303
+ let mine = self.history_of(resource)?;
304
+ let drop_count = mine.len().saturating_sub(keep);
305
+ for id in mine.into_iter().take(drop_count) {
306
+ self.revisions.remove(&id);
307
+ }
308
+ Ok(drop_count)
309
+ }
310
+
311
+ fn records(&self) -> BTreeMap<Rid, String> {
312
+ self.revisions
313
+ .iter()
314
+ .map(|(id, (owner, content, parent))| {
315
+ (id.clone(), format!("{owner}|{content}|{parent:?}"))
316
+ })
317
+ .collect()
318
+ }
319
+ }
320
+
321
+ fn models() -> Vec<Box<dyn Model>> {
322
+ vec![
323
+ Box::<StateModel>::default(),
324
+ Box::<TransitionModel>::default(),
325
+ Box::<ResourceTransitionModel>::default(),
326
+ ]
327
+ }
328
+
329
+ const X: &str = r#"{"title":"X"}"#;
330
+ const Y: &str = r#"{"title":"Y"}"#;
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // The two invariants, asserted against every model
334
+ // ---------------------------------------------------------------------------
335
+
336
+ /// **Invariant 1 — a committed revision's identity and ancestry are immutable.**
337
+ ///
338
+ /// Run every scenario that rewrote history under today's implementation, and
339
+ /// assert that no already-stored record changed. All three models hold, by
340
+ /// different means: A because a revision has no ancestry to rewrite, B and C
341
+ /// because the rewrite would have a different identity.
342
+ #[test]
343
+ fn every_model_holds_committed_revisions_immutable() {
344
+ for mut model in models() {
345
+ let name = model.name();
346
+ let first = model.mint("tasks:1", X, None).unwrap();
347
+ let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
348
+ let before = model.records();
349
+
350
+ // The three sequences that corrupt history today.
351
+ model.mint("tasks:1", X, Some(&second)).unwrap(); // revert
352
+ model.mint("tasks:1", Y, None).unwrap(); // re-mint a child as a root
353
+ model.mint("tasks:2", X, None).unwrap(); // same content, other resource
354
+
355
+ let after = model.records();
356
+ for (id, record) in &before {
357
+ assert_eq!(
358
+ after.get(id),
359
+ Some(record),
360
+ "{name}: the record for {id} changed after commit"
361
+ );
362
+ }
363
+ }
364
+ }
365
+
366
+ /// **Invariant 2 — a `parent_id` references an authoritative revision, or it is
367
+ /// not a parent.**
368
+ ///
369
+ /// Model-independent: all three can enforce it at the mint, and it costs a
370
+ /// containment check. This is the clause T5 should be discharged by — a rule in
371
+ /// the definition of a valid revision, not a lookup bolted on afterwards.
372
+ #[test]
373
+ fn every_model_rejects_a_parent_it_does_not_hold() {
374
+ for mut model in models() {
375
+ let name = model.name();
376
+ let foreign = "state:{\"never\":\"stored\"}".to_string();
377
+ assert_eq!(
378
+ model.mint("tasks:1", X, Some(&foreign)),
379
+ Err(MintError::UnknownParent),
380
+ "{name}: accepted a parent it does not hold"
381
+ );
382
+ }
383
+ }
384
+
385
+ /// **The real store now holds both — this is what the experiment decided.**
386
+ ///
387
+ /// This test asserted the opposite when the experiment was run: `StateStore`
388
+ /// accepted an unheld parent, and re-minting a child's content as a root erased
389
+ /// the parent that child had. Model C was adopted and implemented, and the same
390
+ /// two sequences are now refused at the mint.
391
+ ///
392
+ /// It is kept in the experiment file rather than moved because the experiment's
393
+ /// claim was that both invariants are *model-independent and cheap*. This is
394
+ /// that claim, checked against the real store instead of a probe.
395
+ #[test]
396
+ fn the_real_store_now_holds_both_invariants() {
397
+ let directory = TempDir::new().unwrap();
398
+ let db = Arc::new(FeltDb::open(directory.path().join("today.log")).unwrap());
399
+ let store = StateStore::with_feltdb(db).unwrap();
400
+
401
+ let first = store
402
+ .create("tasks:1", X.into(), "decision".into())
403
+ .unwrap();
404
+ let second = store.commit(Y.into(), &first, "decision".into()).unwrap();
405
+ assert_eq!(
406
+ store.get(&second.id).unwrap().parent_id,
407
+ Some(first.id.clone())
408
+ );
409
+
410
+ // Invariant 1: beginning the resource again is refused, and a further
411
+ // commit of the same content is a new revision rather than a rewrite.
412
+ assert!(
413
+ store
414
+ .create("tasks:1", Y.into(), "decision".into())
415
+ .is_err(),
416
+ "a resource has one beginning"
417
+ );
418
+ let third = store.commit(Y.into(), &second, "decision".into()).unwrap();
419
+ assert_ne!(
420
+ third.id, second.id,
421
+ "the same state again is a new revision"
422
+ );
423
+ assert_eq!(
424
+ store.get(&second.id).unwrap().parent_id,
425
+ Some(first.id),
426
+ "the committed revision kept the parent it was committed with"
427
+ );
428
+
429
+ // Invariant 2: a parent the store does not hold is refused.
430
+ let unheld = StateRevision::at(
431
+ "tasks:1".into(),
432
+ r#"{"never":"stored"}"#.into(),
433
+ None,
434
+ 0,
435
+ "decision".into(),
436
+ );
437
+ assert!(
438
+ store.commit(X.into(), &unheld, "decision".into()).is_err(),
439
+ "a parent must resolve to a held revision"
440
+ );
441
+ }
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // The seven scenarios
445
+ // ---------------------------------------------------------------------------
446
+
447
+ /// Whether the two mints produce one identity or two.
448
+ fn same_content_twice(model: &mut dyn Model) -> bool {
449
+ let first = model.mint("tasks:1", X, None).unwrap();
450
+ let second = model.mint("tasks:1", X, None).unwrap();
451
+ first == second
452
+ }
453
+
454
+ /// Whether a revert lands back on the identity it started from.
455
+ fn revert_reuses_the_original_identity(model: &mut dyn Model) -> bool {
456
+ let first = model.mint("tasks:1", X, None).unwrap();
457
+ let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
458
+ let third = model.mint("tasks:1", X, Some(&second)).unwrap();
459
+ first == third
460
+ }
461
+
462
+ /// Whether two resources holding the same content share one identity.
463
+ fn two_resources_share_an_identity(model: &mut dyn Model) -> bool {
464
+ let one = model.mint("tasks:1", X, None).unwrap();
465
+ let two = model.mint("tasks:2", X, None).unwrap();
466
+ one == two
467
+ }
468
+
469
+ /// Whether re-minting a child's content as a root can disturb the child.
470
+ fn re_minting_a_child_as_a_root_disturbs_it(model: &mut dyn Model) -> bool {
471
+ let first = model.mint("tasks:1", X, None).unwrap();
472
+ let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
473
+ let before = model.parent_of("tasks:1", &second);
474
+ model.mint("tasks:1", Y, None).unwrap();
475
+ model.parent_of("tasks:1", &second) != before
476
+ }
477
+
478
+ /// Whether a foreign parent is refused.
479
+ fn foreign_parent_refused(model: &mut dyn Model) -> bool {
480
+ model.mint("tasks:1", X, Some(&"nothing:here".to_string())) == Err(MintError::UnknownParent)
481
+ }
482
+
483
+ /// Whether the history of one resource can be listed.
484
+ fn resource_history_addressable(model: &mut dyn Model) -> bool {
485
+ model.mint("tasks:1", X, None).unwrap();
486
+ model.mint("tasks:2", Y, None).unwrap();
487
+ matches!(model.history_of("tasks:1"), Ok(history) if history.len() == 1)
488
+ }
489
+
490
+ /// Whether a retention policy can be *stated* — "keep the last k of this
491
+ /// resource" — as opposed to being applied to everything at once.
492
+ fn retention_expressible(model: &mut dyn Model) -> bool {
493
+ let mut previous = model.mint("tasks:1", X, None).unwrap();
494
+ for n in 0..4 {
495
+ previous = model
496
+ .mint("tasks:1", &format!(r#"{{"n":{n}}}"#), Some(&previous))
497
+ .unwrap();
498
+ }
499
+ model.retain_last("tasks:1", 2).is_ok()
500
+ }
501
+
502
+ /// Whether following parents from the newest revision terminates.
503
+ fn topology_terminates(model: &mut dyn Model) -> bool {
504
+ let first = model.mint("tasks:1", X, None).unwrap();
505
+ let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
506
+ let third = model.mint("tasks:1", X, Some(&second)).unwrap();
507
+
508
+ let mut seen: Vec<Rid> = Vec::new();
509
+ let mut current = third;
510
+ for _ in 0..32 {
511
+ if seen.contains(&current) {
512
+ return false;
513
+ }
514
+ seen.push(current.clone());
515
+ match model.parent_of("tasks:1", &current) {
516
+ Some(parent) => current = parent,
517
+ None => return true,
518
+ }
519
+ }
520
+ false
521
+ }
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // The decision matrix
525
+ // ---------------------------------------------------------------------------
526
+
527
+ /// One row of the matrix: a requirement, and what each model does about it.
528
+ struct Row {
529
+ requirement: &'static str,
530
+ /// Evaluated per model, in the order A, B, C.
531
+ evaluate: fn(&mut dyn Model) -> bool,
532
+ expected: [bool; 3],
533
+ }
534
+
535
+ /// The matrix, filled by execution.
536
+ ///
537
+ /// `expected` is what the probes actually do. Changing a probe without changing
538
+ /// this table fails the suite.
539
+ const MATRIX: &[Row] = &[
540
+ Row {
541
+ requirement: "the same content can recur without reusing an identity",
542
+ evaluate: |m| !same_content_twice(m),
543
+ expected: [false, true, true],
544
+ },
545
+ Row {
546
+ requirement: "a revert is distinguishable from the state it returns to",
547
+ evaluate: |m| !revert_reuses_the_original_identity(m),
548
+ expected: [false, true, true],
549
+ },
550
+ Row {
551
+ requirement: "two resources at the same value are distinguishable",
552
+ evaluate: |m| !two_resources_share_an_identity(m),
553
+ expected: [false, true, true],
554
+ },
555
+ Row {
556
+ requirement: "a committed revision's parent is undisturbed by later mints",
557
+ evaluate: |m| !re_minting_a_child_as_a_root_disturbs_it(m),
558
+ // A fails this for the same reason it deduplicates: parentage is not a
559
+ // property of a revision there, so "the parent of R" has no stable
560
+ // answer. See `model_a_is_sound_only_when_history_is_walked_over_edges`.
561
+ expected: [false, true, true],
562
+ },
563
+ Row {
564
+ requirement: "a parent must resolve to a held revision",
565
+ evaluate: foreign_parent_refused,
566
+ expected: [true, true, true],
567
+ },
568
+ Row {
569
+ requirement: "the history of one resource is addressable",
570
+ evaluate: resource_history_addressable,
571
+ expected: [true, false, true],
572
+ },
573
+ Row {
574
+ requirement: "a per-resource retention policy is expressible",
575
+ evaluate: retention_expressible,
576
+ expected: [true, false, true],
577
+ },
578
+ Row {
579
+ requirement: "following parents terminates",
580
+ evaluate: topology_terminates,
581
+ // A cycles exactly as today's implementation does, and for the same
582
+ // reason: resolving a parent by *identity* collapses the two occasions
583
+ // one state was reached.
584
+ expected: [false, true, true],
585
+ },
586
+ ];
587
+
588
+ #[test]
589
+ fn the_decision_matrix_is_exactly_this() {
590
+ let mut drifted = Vec::new();
591
+ for row in MATRIX {
592
+ for (index, mut model) in models().into_iter().enumerate() {
593
+ let actual = (row.evaluate)(model.as_mut());
594
+ if actual != row.expected[index] {
595
+ drifted.push(format!(
596
+ " {} / {}: expected {}, observed {}",
597
+ row.requirement,
598
+ model.name(),
599
+ row.expected[index],
600
+ actual
601
+ ));
602
+ }
603
+ }
604
+ }
605
+ assert!(
606
+ drifted.is_empty(),
607
+ "the decision matrix has drifted from the probes:\n{}\n\n\
608
+ Update docs/architecture/revision-model-decision.md and this table together.",
609
+ drifted.join("\n")
610
+ );
611
+ }
612
+
613
+ /// **What the matrix forces.**
614
+ ///
615
+ /// A passes 3 of 8, B passes 6 of 8, C passes 8 of 8.
616
+ ///
617
+ /// B fails exactly the two rows that need a resource, and no discriminator added
618
+ /// to its identity can fix that: those rows are unanswerable because the model
619
+ /// has no resource to name, not because its identity is too coarse. Giving B a
620
+ /// resource dimension **is** model C.
621
+ ///
622
+ /// A fails the three identity rows by design — a recurring state is one object
623
+ /// referenced twice — and then fails two more as a consequence, because once
624
+ /// identity is shared, "the parent of this revision" stops having one answer.
625
+ /// That is not a defect in the probe; it is what moving ancestry out of the
626
+ /// revision costs, and it is only recoverable under the discipline the next
627
+ /// test describes.
628
+ #[test]
629
+ fn the_matrix_scores_a_three_b_six_c_eight() {
630
+ let mut scores = [0usize; 3];
631
+ for row in MATRIX {
632
+ for (index, mut model) in models().into_iter().enumerate() {
633
+ if (row.evaluate)(model.as_mut()) {
634
+ scores[index] += 1;
635
+ }
636
+ }
637
+ }
638
+ assert_eq!(scores, [3, 6, 8], "A, B, C");
639
+ }
640
+
641
+ /// **B's failures are the two rows that need a resource, and only those.**
642
+ #[test]
643
+ fn model_b_fails_exactly_the_resource_rows() {
644
+ let failed: Vec<&str> = MATRIX
645
+ .iter()
646
+ .filter(|row| !row.expected[1])
647
+ .map(|row| row.requirement)
648
+ .collect();
649
+ assert_eq!(
650
+ failed,
651
+ vec![
652
+ "the history of one resource is addressable",
653
+ "a per-resource retention policy is expressible",
654
+ ]
655
+ );
656
+ }
657
+
658
+ /// **Model A is sound — but only if the addressable unit of history is the
659
+ /// edge, not the revision.**
660
+ ///
661
+ /// The matrix shows A cycling and losing parentage when ancestry is resolved by
662
+ /// revision identity. That is the tempting reading of "ancestry lives outside
663
+ /// the revision", and it does not work: two occasions of one state are one
664
+ /// identity, so the question has two answers and the walk closes a loop.
665
+ ///
666
+ /// Walked over the *edge list* instead — where each edge is a distinct,
667
+ /// ordered occurrence — A is well behaved: the walk strictly decreases in index
668
+ /// and therefore terminates, and no edge is ever rewritten.
669
+ ///
670
+ /// So A is not disqualified. It is conditional, and the condition is a real
671
+ /// constraint on every consumer: `base` in model A is an edge lookup, and
672
+ /// `revision.parent` does not exist.
673
+ #[test]
674
+ fn model_a_is_sound_only_when_history_is_walked_over_edges() {
675
+ let mut model = StateModel::default();
676
+ let first = model.mint("tasks:1", X, None).unwrap();
677
+ let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
678
+ let third = model.mint("tasks:1", X, Some(&second)).unwrap();
679
+ assert_eq!(first, third, "one object, reached twice");
680
+
681
+ // Walk backwards over the edges of this resource, by position.
682
+ let positions: Vec<usize> = model
683
+ .edges
684
+ .iter()
685
+ .enumerate()
686
+ .filter(|(_, edge)| edge.resource == "tasks:1")
687
+ .map(|(index, _)| index)
688
+ .collect();
689
+
690
+ let mut walked = Vec::new();
691
+ let mut cursor = Some(*positions.last().unwrap());
692
+ while let Some(position) = cursor {
693
+ walked.push(model.edges[position].to.clone());
694
+ let from = model.edges[position].from.clone();
695
+ cursor = match from {
696
+ None => None,
697
+ Some(parent) => positions
698
+ .iter()
699
+ .rev()
700
+ .find(|candidate| **candidate < position && model.edges[**candidate].to == parent)
701
+ .copied(),
702
+ };
703
+ assert!(walked.len() <= 8, "the edge walk must terminate");
704
+ }
705
+
706
+ assert_eq!(
707
+ walked,
708
+ vec![third, second, first],
709
+ "three occasions, in order, from one deduplicated pair of states"
710
+ );
711
+ }
712
+
713
+ /// **Why A behaves that way**, stated where it cannot be missed.
714
+ ///
715
+ /// A keeps deduplication — the only thing currently bounding storage growth —
716
+ /// but it pays for it by moving ancestry out of the revision. A revision can no
717
+ /// longer be asked for its parent: the question only has an answer relative to a
718
+ /// resource, and if a state recurs on one resource it has more than one answer.
719
+ #[test]
720
+ fn model_a_has_no_well_formed_parent_for_a_revision() {
721
+ let mut model = StateModel::default();
722
+ let first = model.mint("tasks:1", X, None).unwrap();
723
+ let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
724
+ let third = model.mint("tasks:1", X, Some(&second)).unwrap();
725
+ assert_eq!(first, third, "one object, reached twice");
726
+
727
+ // The same identity has two parents on one resource, depending on when.
728
+ let parents: Vec<Option<Rid>> = model
729
+ .edges
730
+ .iter()
731
+ .filter(|edge| edge.to == first)
732
+ .map(|edge| edge.from.clone())
733
+ .collect();
734
+ assert_eq!(
735
+ parents,
736
+ vec![None, Some(second)],
737
+ "'the parent of this revision' is not a well-formed question in model A"
738
+ );
739
+ }