@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
@@ -0,0 +1,392 @@
1
+ //! Durable format versioning: FeltDB must fail closed when the durable meaning
2
+ //! of its own data is unknown.
3
+ //!
4
+ //! The production invariant this file exists to enforce:
5
+ //!
6
+ //! > **A durable database must never silently discard persisted information
7
+ //! > because the running FeltDB understands a different durable format.**
8
+ //!
9
+ //! Before this, a database written before revisions became resource-scoped
10
+ //! opened cleanly and reported no history at all. The records were still on
11
+ //! disk; every path that read them deserialized into the current shape and
12
+ //! discarded what failed. The database looked healthy and had lost its history
13
+ //! from the application's point of view. That is failing *open*.
14
+ //!
15
+ //! See `docs/architecture/durable-format-versioning.md`.
16
+
17
+ use feltdb::state_model::StateStore;
18
+ use feltdb::{
19
+ inspect_durable_format, DurableFormat, FeltDb, FlowError, FormatCompatibility,
20
+ DURABLE_FORMAT_VERSION,
21
+ };
22
+ use serde_json::{json, Value};
23
+ use std::io::Write;
24
+ use std::path::Path;
25
+ use std::sync::Arc;
26
+ use tempfile::TempDir;
27
+
28
+ /// Write a durable log by hand, one JSON record per line.
29
+ fn write_log(path: &Path, records: &[Value]) {
30
+ let mut file = std::fs::File::create(path).unwrap();
31
+ for record in records {
32
+ writeln!(file, "{}", serde_json::to_string(record).unwrap()).unwrap();
33
+ }
34
+ }
35
+
36
+ /// A data row as the log stores it.
37
+ fn data_row(key: &str, value: Value) -> Value {
38
+ let capability = key.split_once(':').map(|(c, _)| c).unwrap_or("default");
39
+ json!({
40
+ "capability": capability,
41
+ "key": key,
42
+ "rust_type": "serde_json::value::Value",
43
+ "value": value,
44
+ "unix_ms": 1,
45
+ "content_hash": null,
46
+ "flow_ref": null,
47
+ "deleted": false,
48
+ "operation": null
49
+ })
50
+ }
51
+
52
+ /// A revision record in the pre-resource-scoped shape: content-only identity,
53
+ /// no resource, no sequence.
54
+ fn legacy_revision(id: &str, content: &str, parent: Option<&str>) -> Value {
55
+ json!({
56
+ "capability": "state",
57
+ "key": format!("state:revision:{id}"),
58
+ "rust_type": "feltdb::state_model::StateRevision",
59
+ "value": {
60
+ "id": id,
61
+ "content": content,
62
+ "parent_id": parent,
63
+ "authority": "before-the-model",
64
+ "timestamp_ms": 0,
65
+ "metadata": {}
66
+ },
67
+ "unix_ms": 1,
68
+ "content_hash": null,
69
+ "flow_ref": null,
70
+ "deleted": false,
71
+ "operation": null
72
+ })
73
+ }
74
+
75
+ /// A real legacy database: application state **and** revision history.
76
+ fn legacy_database(path: &Path) {
77
+ write_log(
78
+ path,
79
+ &[
80
+ data_row("tasks:1", json!({"title": "B"})),
81
+ data_row("tasks:2", json!({"title": "C"})),
82
+ legacy_revision("aaa", r#"{"title":"A"}"#, None),
83
+ legacy_revision("bbb", r#"{"title":"B"}"#, Some("aaa")),
84
+ ],
85
+ );
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // The regression that matters
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /// **The #307 U2 failure, encoded.**
93
+ ///
94
+ /// A database written before revisions belonged to a resource must not open as
95
+ /// though it were healthy and empty of history.
96
+ ///
97
+ /// This is the most important test in the change; the implementation details
98
+ /// are secondary to it.
99
+ #[test]
100
+ fn pre_306_database_does_not_open_as_empty_history() {
101
+ let directory = TempDir::new().unwrap();
102
+ let path = directory.path().join("legacy.log");
103
+ legacy_database(&path);
104
+ let before = std::fs::read(&path).unwrap();
105
+
106
+ // 1. An explicit compatibility result, from a read-only examination.
107
+ let compatibility = inspect_durable_format(&path).unwrap();
108
+ let FormatCompatibility::Incompatible(incompatibility) = compatibility else {
109
+ panic!("the legacy format must be reported incompatible, got {compatibility:?}");
110
+ };
111
+ assert_eq!(incompatibility.found, DurableFormat::Unversioned);
112
+ assert_eq!(incompatibility.required, DURABLE_FORMAT_VERSION);
113
+ assert!(
114
+ incompatibility.reason.contains("resource"),
115
+ "the reason names the actual problem: {}",
116
+ incompatibility.reason
117
+ );
118
+ assert!(
119
+ !incompatibility.action.is_empty(),
120
+ "the operator is told what to do"
121
+ );
122
+
123
+ // 2. The open is refused, rather than succeeding with nothing to show.
124
+ match FeltDb::open(&path) {
125
+ Err(FlowError::IncompatibleFormat(reported)) => {
126
+ assert_eq!(*reported, incompatibility, "open agrees with the probe");
127
+ let rendered = FlowError::IncompatibleFormat(reported).to_string();
128
+ assert!(rendered.contains("INCOMPATIBLE_DURABLE_FORMAT"));
129
+ assert!(rendered.contains("No data was read or modified"));
130
+ }
131
+ Err(other) => panic!("wrong error: {other}"),
132
+ Ok(_) => panic!("the legacy database opened — this is the defect"),
133
+ }
134
+
135
+ // 3. Nothing was touched.
136
+ assert_eq!(
137
+ std::fs::read(&path).unwrap(),
138
+ before,
139
+ "a refused open must not modify a single byte"
140
+ );
141
+ }
142
+
143
+ /// Lossless migration is refused rather than attempted, and the reason is a
144
+ /// property of the data rather than a shortage of effort.
145
+ ///
146
+ /// A legacy revision recorded `id`, `content`, `parent_id`, `authority`,
147
+ /// `timestamp_ms` and `metadata`. It did **not** record which resource it was a
148
+ /// revision of — that absence is the finding the resource-scoped model was
149
+ /// adopted to fix. So which history each legacy revision belongs to cannot be
150
+ /// recovered from the data, and any migration would have to invent it.
151
+ ///
152
+ /// Inventing it would produce a database that looks migrated and is wrong,
153
+ /// which is worse than one that refuses.
154
+ #[test]
155
+ fn migration_is_refused_because_the_resource_was_never_recorded() {
156
+ let directory = TempDir::new().unwrap();
157
+ let path = directory.path().join("legacy.log");
158
+ legacy_database(&path);
159
+
160
+ let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path).unwrap()
161
+ else {
162
+ panic!("expected incompatible");
163
+ };
164
+ assert!(
165
+ incompatibility
166
+ .reason
167
+ .contains("cannot be migrated without inventing information"),
168
+ "the refusal explains why migration is not merely unimplemented: {}",
169
+ incompatibility.reason
170
+ );
171
+
172
+ // The legacy records are still readable as bytes by anything that
173
+ // understands them — nothing was deleted in the name of tidiness.
174
+ let text = std::fs::read_to_string(&path).unwrap();
175
+ assert!(text.contains("state:revision:aaa"));
176
+ assert!(text.contains("state:revision:bbb"));
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Symmetry: no build silently interprets a format it does not understand
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /// **Downgrade protection.** A database from a newer FeltDB is refused.
184
+ ///
185
+ /// The invariant is symmetric, and this is the half a rollback hits: an
186
+ /// operator reverting a deployment must not have the older build reinterpret
187
+ /// data written by the newer one.
188
+ #[test]
189
+ fn a_newer_durable_format_is_refused() {
190
+ let directory = TempDir::new().unwrap();
191
+ let path = directory.path().join("future.log");
192
+ write_log(
193
+ &path,
194
+ &[
195
+ json!({"record_type": "feltdb.format.v1", "format_version": DURABLE_FORMAT_VERSION + 1}),
196
+ data_row("tasks:1", json!({"title": "from the future"})),
197
+ ],
198
+ );
199
+ let before = std::fs::read(&path).unwrap();
200
+
201
+ let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path).unwrap()
202
+ else {
203
+ panic!("a newer format must be refused");
204
+ };
205
+ assert_eq!(
206
+ incompatibility.found,
207
+ DurableFormat::Versioned(DURABLE_FORMAT_VERSION + 1)
208
+ );
209
+ assert!(incompatibility.reason.contains("newer"));
210
+
211
+ assert!(matches!(
212
+ FeltDb::open(&path),
213
+ Err(FlowError::IncompatibleFormat(_))
214
+ ));
215
+ assert_eq!(std::fs::read(&path).unwrap(), before, "untouched");
216
+ }
217
+
218
+ /// An older declared format is refused too, so the check is a version
219
+ /// comparison rather than a special case for one legacy shape.
220
+ #[test]
221
+ fn an_older_declared_format_is_refused() {
222
+ let directory = TempDir::new().unwrap();
223
+ let path = directory.path().join("old.log");
224
+ write_log(
225
+ &path,
226
+ &[json!({"record_type": "feltdb.format.v1", "format_version": 1})],
227
+ );
228
+ let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path).unwrap()
229
+ else {
230
+ panic!("an older format must be refused");
231
+ };
232
+ assert_eq!(incompatibility.found, DurableFormat::Versioned(1));
233
+ assert!(matches!(
234
+ FeltDb::open(&path),
235
+ Err(FlowError::IncompatibleFormat(_))
236
+ ));
237
+ }
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // What must keep working
241
+ // ---------------------------------------------------------------------------
242
+
243
+ /// A new database states its format, and reopening it is compatible.
244
+ #[test]
245
+ fn a_new_database_declares_its_format() {
246
+ let directory = TempDir::new().unwrap();
247
+ let path = directory.path().join("new.log");
248
+ let db = FeltDb::open(&path).unwrap();
249
+ db.insert("tasks:1", json!({"n": 1})).unwrap();
250
+
251
+ let text = std::fs::read_to_string(&path).unwrap();
252
+ let first = text.lines().next().unwrap();
253
+ let record: Value = serde_json::from_str(first).unwrap();
254
+ assert_eq!(record["record_type"], json!("feltdb.format.v1"));
255
+ assert_eq!(record["format_version"], json!(DURABLE_FORMAT_VERSION));
256
+
257
+ drop(db);
258
+ assert_eq!(
259
+ inspect_durable_format(&path).unwrap(),
260
+ FormatCompatibility::Compatible(DurableFormat::Versioned(DURABLE_FORMAT_VERSION))
261
+ );
262
+ let reopened = FeltDb::open(&path).unwrap();
263
+ let value: Value = reopened.get("tasks:1").unwrap().unwrap();
264
+ assert_eq!(value["n"], json!(1));
265
+ }
266
+
267
+ /// A database that predates format versioning but holds nothing whose meaning
268
+ /// changed opens normally.
269
+ ///
270
+ /// The refusal is targeted at data whose interpretation actually differs, not
271
+ /// at every database that lacks a version record. Refusing those would make the
272
+ /// safety check an upgrade wall.
273
+ #[test]
274
+ fn an_unversioned_database_without_legacy_revisions_still_opens() {
275
+ let directory = TempDir::new().unwrap();
276
+ let path = directory.path().join("plain.log");
277
+ write_log(
278
+ &path,
279
+ &[
280
+ data_row("tasks:1", json!({"title": "A"})),
281
+ data_row("tasks:2", json!({"title": "B"})),
282
+ ],
283
+ );
284
+
285
+ assert_eq!(
286
+ inspect_durable_format(&path).unwrap(),
287
+ FormatCompatibility::Compatible(DurableFormat::Unversioned)
288
+ );
289
+ let db = FeltDb::open(&path).unwrap();
290
+ let value: Value = db.get("tasks:1").unwrap().unwrap();
291
+ assert_eq!(value["title"], json!("A"));
292
+ }
293
+
294
+ /// Compaction rewrites the log with the format stated in its header, so a
295
+ /// compacted database is self-describing too.
296
+ #[test]
297
+ fn a_compacted_log_declares_its_format() {
298
+ let directory = TempDir::new().unwrap();
299
+ let path = directory.path().join("compacted.log");
300
+ let db = Arc::new(FeltDb::open(&path).unwrap());
301
+ db.insert("tasks:1", json!({"n": 0})).unwrap();
302
+ db.update("tasks:1", json!({"n": 1})).unwrap();
303
+
304
+ let versions = db.operation_versions().unwrap();
305
+ db.acknowledge_peer_versions("peer-1".to_string(), versions)
306
+ .unwrap();
307
+ db.compact_operation_log(&["peer-1".to_string()]).unwrap();
308
+ drop(db);
309
+
310
+ assert_eq!(
311
+ inspect_durable_format(&path).unwrap(),
312
+ FormatCompatibility::Compatible(DurableFormat::Versioned(DURABLE_FORMAT_VERSION))
313
+ );
314
+ let reopened = Arc::new(FeltDb::open(&path).unwrap());
315
+ let store = StateStore::with_feltdb(reopened.clone()).unwrap();
316
+ assert_eq!(
317
+ store.history_of("tasks:1").len(),
318
+ 2,
319
+ "history survived compaction and reopen"
320
+ );
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Snapshots
325
+ // ---------------------------------------------------------------------------
326
+
327
+ /// A snapshot carries its own format, and one from another format is refused.
328
+ ///
329
+ /// Snapshots are durable interchange artifacts, not merely protocol values:
330
+ /// they carry whole rows, revisions included. Leaving that path unversioned
331
+ /// would have reopened the same hole the on-disk version closes.
332
+ #[test]
333
+ fn a_snapshot_from_another_format_is_refused() {
334
+ let directory = TempDir::new().unwrap();
335
+ let source = Arc::new(FeltDb::open(directory.path().join("source.log")).unwrap());
336
+ source.insert("tasks:1", json!({"n": 0})).unwrap();
337
+ source.update("tasks:1", json!({"n": 1})).unwrap();
338
+
339
+ let snapshot = source.export_snapshot().unwrap();
340
+ assert_eq!(snapshot.format_version, DURABLE_FORMAT_VERSION);
341
+
342
+ // A matching snapshot installs, history included.
343
+ let target = Arc::new(FeltDb::open(directory.path().join("target.log")).unwrap());
344
+ target.install_snapshot(snapshot.clone()).unwrap();
345
+ let store = StateStore::with_feltdb(target.clone()).unwrap();
346
+ assert_eq!(store.history_of("tasks:1").len(), 2);
347
+
348
+ // One claiming another format does not.
349
+ let mut foreign = snapshot.clone();
350
+ foreign.format_version = DURABLE_FORMAT_VERSION + 1;
351
+ let other = Arc::new(FeltDb::open(directory.path().join("other.log")).unwrap());
352
+ assert!(matches!(
353
+ other.install_snapshot(foreign),
354
+ Err(FlowError::IncompatibleFormat(_))
355
+ ));
356
+
357
+ // And neither does one serialized before snapshots carried a version.
358
+ let mut unversioned = snapshot;
359
+ unversioned.format_version = 0;
360
+ assert!(matches!(
361
+ other.install_snapshot(unversioned),
362
+ Err(FlowError::IncompatibleFormat(_))
363
+ ));
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // The server boundary
368
+ // ---------------------------------------------------------------------------
369
+
370
+ /// An incompatible database cannot reach a running server, so it cannot be
371
+ /// advertised as healthy.
372
+ ///
373
+ /// This is the one health interaction in scope: `/health` is not redesigned
374
+ /// here, and does not need to be, because the database never opens. The check
375
+ /// is that the failure is a returned error a caller must handle — not a panic,
376
+ /// and not a degraded-but-running database.
377
+ #[test]
378
+ fn an_incompatible_database_cannot_be_opened_by_a_service() {
379
+ let directory = TempDir::new().unwrap();
380
+ let path = directory.path().join("legacy.log");
381
+ legacy_database(&path);
382
+
383
+ let outcome = FeltDb::open(&path);
384
+ assert!(
385
+ outcome.is_err(),
386
+ "startup fails closed rather than serving an empty history"
387
+ );
388
+ // The error is actionable enough to put in a startup log.
389
+ let message = outcome.err().unwrap().to_string();
390
+ assert!(message.contains("INCOMPATIBLE_DURABLE_FORMAT"));
391
+ assert!(message.contains("format version"));
392
+ }