@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,1799 @@
1
+ //! Conformance, determinism and capability proofs for the
2
+ //! `feltdb.state.conflict` deterministic contract.
3
+ //!
4
+ //! The third specimen of the pattern proved for `feltdb.trigger.match` and
5
+ //! `feltdb.state.diff`, and the first that reasons over competing changes:
6
+ //!
7
+ //! ```text
8
+ //! Contract semantics are authoritative.
9
+ //! Runtime implementation is replaceable.
10
+ //!
11
+ //! NativeClassify(base, left, right) == WasmClassify(base, left, right)
12
+ //! ```
13
+ //!
14
+ //! The corpus in `tests/fixtures/state_conflict_contract_corpus.json` is committed
15
+ //! rather than generated, and carries the canonical output each case must
16
+ //! produce. Three comparisons run over it:
17
+ //!
18
+ //! 1. native output == committed expectation (runs everywhere);
19
+ //! 2. WASM output == committed expectation (needs `wasm32-unknown-unknown`);
20
+ //! 3. native output == WASM output, byte for byte.
21
+ //!
22
+ //! The first makes the suite meaningful where the WASM toolchain is absent: a
23
+ //! change in the native classifier's semantics is caught by the same bytes the
24
+ //! WASM implementation was measured against.
25
+ //!
26
+ //! Beyond conformance, this suite asserts the *semantic invariants* the
27
+ //! classification must satisfy — see `classification_invariants_hold` — because
28
+ //! a contract over competing changes can be reproduced faithfully and still be
29
+ //! meaningless if the relation itself is not pinned.
30
+
31
+ use feltdb::state_conflict_contract::{
32
+ canonical_input_bytes, evaluate_canonical, StateConflictInput, STATE_CONFLICT_CONTRACT_ID,
33
+ STATE_CONFLICT_CONTRACT_VERSION,
34
+ };
35
+ use feltdb::state_model::{ConflictClass, ConflictClassification};
36
+ use serde::Deserialize;
37
+ use serde_json::Value;
38
+ use std::path::{Path, PathBuf};
39
+ use std::process::Command;
40
+ use std::sync::OnceLock;
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Corpus
44
+ // ---------------------------------------------------------------------------
45
+
46
+ #[derive(Debug, Deserialize)]
47
+ struct Corpus {
48
+ corpus_version: u32,
49
+ contract: String,
50
+ contract_version: u32,
51
+ #[allow(dead_code)]
52
+ about: String,
53
+ cases: Vec<Case>,
54
+ }
55
+
56
+ #[derive(Debug, Deserialize)]
57
+ struct Case {
58
+ name: String,
59
+ #[allow(dead_code)]
60
+ why: String,
61
+ /// A well-formed contract input, canonically encoded by the harness.
62
+ #[serde(default)]
63
+ input: Option<StateConflictInput>,
64
+ /// Bytes fed to the contract verbatim, for non-canonical and malformed
65
+ /// encodings that a typed value cannot express.
66
+ #[serde(default)]
67
+ raw_input: Option<String>,
68
+ /// The canonical encoding of `input`, recorded so that every consumer of
69
+ /// the corpus presents the same bytes.
70
+ ///
71
+ /// A consumer in another language cannot always reproduce it: JavaScript
72
+ /// has one number type, so it cannot encode the difference between `42` and
73
+ /// `42.0` that this contract's value equality depends on.
74
+ #[serde(default)]
75
+ canonical_input: Option<String>,
76
+ /// The canonical output both implementations must produce.
77
+ #[serde(default)]
78
+ expected_output: Option<String>,
79
+ }
80
+
81
+ impl Case {
82
+ /// The exact bytes this case presents to a contract implementation.
83
+ fn bytes(&self) -> Vec<u8> {
84
+ match (&self.input, &self.raw_input) {
85
+ (Some(input), None) => canonical_input_bytes(input),
86
+ (None, Some(raw)) => raw.as_bytes().to_vec(),
87
+ _ => panic!(
88
+ "case {} must have exactly one of input/raw_input",
89
+ self.name
90
+ ),
91
+ }
92
+ }
93
+
94
+ fn expected(&self) -> &str {
95
+ self.expected_output.as_deref().unwrap_or_else(|| {
96
+ panic!(
97
+ "case {} has no expected_output; regenerate the corpus with \
98
+ FELTDB_STATE_CONFLICT_CONTRACT_BLESS=1",
99
+ self.name
100
+ )
101
+ })
102
+ }
103
+ }
104
+
105
+ fn corpus_path() -> PathBuf {
106
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/state_conflict_contract_corpus.json")
107
+ }
108
+
109
+ fn corpus() -> &'static Corpus {
110
+ static CORPUS: OnceLock<Corpus> = OnceLock::new();
111
+ CORPUS.get_or_init(|| {
112
+ let bytes = std::fs::read(corpus_path()).expect("conformance corpus is committed");
113
+ serde_json::from_slice(&bytes).expect("conformance corpus is well formed")
114
+ })
115
+ }
116
+
117
+ #[test]
118
+ fn corpus_addresses_this_contract_at_this_version() {
119
+ let corpus = corpus();
120
+ assert_eq!(corpus.corpus_version, 1);
121
+ assert_eq!(corpus.contract, STATE_CONFLICT_CONTRACT_ID);
122
+ assert_eq!(corpus.contract_version, STATE_CONFLICT_CONTRACT_VERSION);
123
+ assert!(
124
+ corpus.cases.len() >= 150,
125
+ "the corpus is the proof; it should not shrink silently"
126
+ );
127
+ let mut names: Vec<&str> = corpus.cases.iter().map(|case| case.name.as_str()).collect();
128
+ names.sort_unstable();
129
+ let unique = names.len();
130
+ names.dedup();
131
+ assert_eq!(unique, names.len(), "case names identify cases");
132
+ }
133
+
134
+ /// Every required category of behaviour is represented by at least one case.
135
+ ///
136
+ /// A guard against the corpus quietly losing a dimension it was built to
137
+ /// cover, not a substitute for reading it.
138
+ #[test]
139
+ fn corpus_covers_every_required_category() {
140
+ let names: Vec<&str> = corpus().cases.iter().map(|c| c.name.as_str()).collect();
141
+ for required in [
142
+ // baseline
143
+ "all_identical",
144
+ "left_changed_right_unchanged",
145
+ "right_changed_left_unchanged",
146
+ "both_changed_identically",
147
+ "both_changed_differently",
148
+ // independent
149
+ "different_top_level_fields",
150
+ "different_nested_fields",
151
+ "different_sibling_subtrees",
152
+ "multiple_independent_changes",
153
+ "array_different_indexes",
154
+ // the path relation matrix
155
+ "relation_same_disagreeing",
156
+ "relation_same_agreeing",
157
+ "relation_ancestor_replaced_scalar",
158
+ "relation_ancestor_removed",
159
+ "relation_ancestor_two_levels",
160
+ "relation_descendant_replaced_scalar",
161
+ "relation_descendant_removed",
162
+ "relation_disjoint_siblings",
163
+ "relation_disjoint_string_prefix",
164
+ "relation_disjoint_key_against_index",
165
+ "relation_overlap_alongside_independent",
166
+ "relation_parent_object_to_array",
167
+ "relation_array_same_index",
168
+ "relation_array_different_indexes",
169
+ "relation_array_element_removed_vs_edited_inside",
170
+ "relation_array_prepend_vs_index_edit",
171
+ // overlapping
172
+ "same_path_different_values",
173
+ "parent_changed_vs_child_changed",
174
+ "child_changed_vs_parent_changed",
175
+ "addition_vs_addition_different",
176
+ "removal_vs_modification",
177
+ "object_to_scalar_vs_scalar_change",
178
+ "object_vs_array_at_same_path",
179
+ "scalar_to_object_vs_scalar_to_array",
180
+ // null and absence
181
+ "missing_vs_null",
182
+ "null_vs_value",
183
+ "value_vs_null",
184
+ "missing_vs_value_one_side",
185
+ "removal_vs_null_insertion",
186
+ // numbers
187
+ "integer_vs_float_same_magnitude",
188
+ "numeric_replacement_conflict",
189
+ "large_values",
190
+ "zero_vs_float_zero",
191
+ // arrays
192
+ "array_same_index_both_sides",
193
+ "array_append_both_sides_same",
194
+ "array_removal_one_side",
195
+ "array_removal_both_sides",
196
+ "array_prepend_one_side",
197
+ "array_reorder_vs_edit",
198
+ "array_nested_change_each_side",
199
+ // deep
200
+ "deeply_nested_independent_edits",
201
+ "many_simultaneous_conflicts",
202
+ "mixed_severities_in_one_result",
203
+ // empty
204
+ "empty_to_populated_both_sides",
205
+ "populated_to_empty_both_sides",
206
+ "empty_object_vs_empty_array",
207
+ "empty_object_vs_scalar",
208
+ "empty_array_vs_scalar",
209
+ // unparseable content
210
+ "unparseable_base",
211
+ "unparseable_all_three",
212
+ // invalid envelopes
213
+ "malformed_not_json",
214
+ "malformed_duplicate_envelope_member",
215
+ "malformed_state_is_not_a_string",
216
+ ] {
217
+ assert!(
218
+ names.contains(&required),
219
+ "corpus lost coverage: {required}"
220
+ );
221
+ }
222
+ }
223
+
224
+ /// The committed canonical bytes are what this crate's encoder produces.
225
+ ///
226
+ /// Consumers outside Rust read `canonical_input` rather than re-canonicalizing,
227
+ /// so this is the check that keeps those bytes honest: if canonicalization
228
+ /// changed, every other reader of the corpus would silently be testing an
229
+ /// encoding FeltDB no longer emits.
230
+ #[test]
231
+ fn committed_canonical_bytes_match_the_encoder() {
232
+ for case in &corpus().cases {
233
+ match (&case.input, &case.raw_input) {
234
+ (Some(input), None) => assert_eq!(
235
+ case.canonical_input.as_deref(),
236
+ Some(
237
+ String::from_utf8(canonical_input_bytes(input))
238
+ .unwrap()
239
+ .as_str()
240
+ ),
241
+ "{} has a stale canonical_input",
242
+ case.name
243
+ ),
244
+ (None, Some(_)) => assert!(
245
+ case.canonical_input.is_none(),
246
+ "{} is a raw case and must not also record canonical_input",
247
+ case.name
248
+ ),
249
+ _ => panic!(
250
+ "case {} must have exactly one of input/raw_input",
251
+ case.name
252
+ ),
253
+ }
254
+ }
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // WASM host
259
+ // ---------------------------------------------------------------------------
260
+
261
+ const WASM_CRATE: &str = "crates/feltdb-state-conflict-contract-wasm";
262
+ const WASM_ARTIFACT: &str =
263
+ "target/wasm32-unknown-unknown/release/feltdb_state_conflict_contract_wasm.wasm";
264
+
265
+ fn repository_root() -> &'static Path {
266
+ Path::new(env!("CARGO_MANIFEST_DIR"))
267
+ .parent()
268
+ .and_then(Path::parent)
269
+ .expect("the crate lives at <root>/crates/feltdb")
270
+ }
271
+
272
+ /// Whether this machine can compile the contract to WebAssembly at all.
273
+ fn wasm_target_installed() -> bool {
274
+ // `rustc --print target-libdir` succeeds for any target rustc *knows*, and
275
+ // prints where the standard library would live if it were installed. It
276
+ // does not report whether it actually is. Asking only about the exit status
277
+ // therefore reports every known target as installed, and the skip below
278
+ // never fires: a machine without `wasm32-unknown-unknown` fails the build
279
+ // and every WASM test with it, rather than skipping loudly as intended.
280
+ //
281
+ // The directory's existence is the fact worth having.
282
+ Command::new("rustc")
283
+ .args([
284
+ "--print",
285
+ "target-libdir",
286
+ "--target",
287
+ "wasm32-unknown-unknown",
288
+ ])
289
+ .output()
290
+ .ok()
291
+ .filter(|output| output.status.success())
292
+ .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
293
+ .is_some_and(|libdir| Path::new(&libdir).is_dir())
294
+ }
295
+
296
+ /// Compiles a WASM contract crate and returns the module bytes.
297
+ ///
298
+ /// A build failure is a defect and panics. A missing `wasm32-unknown-unknown`
299
+ /// target is an environment fact, reported by the caller as a skip.
300
+ fn build_module(manifest_dir: &Path) -> Vec<u8> {
301
+ let output = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
302
+ .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
303
+ .current_dir(manifest_dir)
304
+ // The contract crate is its own workspace; inheriting the outer
305
+ // build's target directory or flags would defeat that.
306
+ .env_remove("CARGO_TARGET_DIR")
307
+ .env_remove("RUSTFLAGS")
308
+ .output()
309
+ .expect("cargo is runnable");
310
+ assert!(
311
+ output.status.success(),
312
+ "building the WASM contract at {} failed:\n{}",
313
+ manifest_dir.display(),
314
+ String::from_utf8_lossy(&output.stderr)
315
+ );
316
+ std::fs::read(manifest_dir.join(WASM_ARTIFACT)).expect("the build emits one module")
317
+ }
318
+
319
+ /// The committed WASM contract, compiled once for the whole suite.
320
+ fn contract_module() -> Option<&'static [u8]> {
321
+ static MODULE: OnceLock<Option<Vec<u8>>> = OnceLock::new();
322
+ MODULE
323
+ .get_or_init(|| {
324
+ if let Ok(path) = std::env::var("FELTDB_STATE_CONFLICT_CONTRACT_WASM") {
325
+ return Some(
326
+ std::fs::read(path).expect("FELTDB_STATE_CONFLICT_CONTRACT_WASM is readable"),
327
+ );
328
+ }
329
+ if !wasm_target_installed() {
330
+ return None;
331
+ }
332
+ Some(build_module(&repository_root().join(WASM_CRATE)))
333
+ })
334
+ .as_deref()
335
+ }
336
+
337
+ /// Announces a skip loudly, so an absent toolchain is never a silent pass.
338
+ fn skipped(test: &str) {
339
+ eprintln!(
340
+ "SKIPPED {test}: the wasm32-unknown-unknown target is not installed, so the WASM \
341
+ contract implementation could not be built. Native conformance against the committed \
342
+ expectations still ran. Install the target, or set FELTDB_STATE_CONFLICT_CONTRACT_WASM to a \
343
+ prebuilt module, to execute the cross-runtime proof."
344
+ );
345
+ }
346
+
347
+ /// One instantiation of the WASM contract.
348
+ ///
349
+ /// The linker is empty and stays empty: the module declares no imports, so
350
+ /// there is nothing to satisfy and nothing to grant.
351
+ struct WasmContract {
352
+ store: wasmi::Store<()>,
353
+ memory: wasmi::Memory,
354
+ alloc: wasmi::TypedFunc<u32, u32>,
355
+ evaluate: wasmi::TypedFunc<(u32, u32), u64>,
356
+ free: wasmi::TypedFunc<(u32, u32), ()>,
357
+ }
358
+
359
+ impl WasmContract {
360
+ fn instantiate(bytes: &[u8]) -> Self {
361
+ let engine = wasmi::Engine::default();
362
+ let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
363
+ let mut store = wasmi::Store::new(&engine, ());
364
+ let instance = wasmi::Linker::<()>::new(&engine)
365
+ .instantiate_and_start(&mut store, &module)
366
+ .expect("an empty linker satisfies a module with no imports");
367
+ Self {
368
+ memory: instance
369
+ .get_memory(&store, "memory")
370
+ .expect("the module exports its memory"),
371
+ alloc: instance
372
+ .get_typed_func(&store, "contract_alloc")
373
+ .expect("contract_alloc"),
374
+ evaluate: instance
375
+ .get_typed_func(&store, "contract_evaluate")
376
+ .expect("contract_evaluate"),
377
+ free: instance
378
+ .get_typed_func(&store, "contract_free")
379
+ .expect("contract_free"),
380
+ store,
381
+ }
382
+ }
383
+
384
+ fn version(bytes: &[u8]) -> u32 {
385
+ let engine = wasmi::Engine::default();
386
+ let module = wasmi::Module::new(&engine, bytes).unwrap();
387
+ let mut store = wasmi::Store::new(&engine, ());
388
+ let instance = wasmi::Linker::<()>::new(&engine)
389
+ .instantiate_and_start(&mut store, &module)
390
+ .unwrap();
391
+ instance
392
+ .get_typed_func::<(), u32>(&store, "contract_version")
393
+ .expect("contract_version")
394
+ .call(&mut store, ())
395
+ .expect("contract_version is total")
396
+ }
397
+
398
+ fn evaluate(&mut self, input: &[u8]) -> Vec<u8> {
399
+ let len = input.len() as u32;
400
+ let ptr = self.alloc.call(&mut self.store, len).expect("allocation");
401
+ self.memory
402
+ .write(&mut self.store, ptr as usize, input)
403
+ .expect("input fits in linear memory");
404
+ let packed = self
405
+ .evaluate
406
+ .call(&mut self.store, (ptr, len))
407
+ .expect("the contract never traps on any input");
408
+ let out_ptr = (packed >> 32) as usize;
409
+ let out_len = (packed & 0xffff_ffff) as usize;
410
+ let mut output = vec![0u8; out_len];
411
+ self.memory
412
+ .read(&self.store, out_ptr, &mut output)
413
+ .expect("the output buffer is inside linear memory");
414
+ self.free.call(&mut self.store, (ptr, len)).expect("free");
415
+ self.free
416
+ .call(&mut self.store, (out_ptr as u32, out_len as u32))
417
+ .expect("free");
418
+ output
419
+ }
420
+ }
421
+
422
+ // ---------------------------------------------------------------------------
423
+ // Conformance
424
+ // ---------------------------------------------------------------------------
425
+
426
+ /// Native evaluation reproduces the committed canonical output for every case.
427
+ #[test]
428
+ fn native_matches_the_committed_expectations() {
429
+ let mut divergent = Vec::new();
430
+ for case in &corpus().cases {
431
+ let produced = String::from_utf8(evaluate_canonical(&case.bytes())).unwrap();
432
+ if produced != case.expected() {
433
+ divergent.push(format!(
434
+ "{}\n expected: {}\n native: {}",
435
+ case.name,
436
+ case.expected(),
437
+ produced
438
+ ));
439
+ }
440
+ }
441
+ assert!(
442
+ divergent.is_empty(),
443
+ "native contract diverged from the committed expectations:\n{}",
444
+ divergent.join("\n")
445
+ );
446
+ }
447
+
448
+ /// The proof: one corpus, two implementations, byte-identical canonical output.
449
+ #[test]
450
+ fn native_and_wasm_agree_on_every_fixture() {
451
+ let Some(bytes) = contract_module() else {
452
+ return skipped("native_and_wasm_agree_on_every_fixture");
453
+ };
454
+ let mut wasm = WasmContract::instantiate(bytes);
455
+
456
+ println!(
457
+ "{:<48} {:<9} {:<9} {}",
458
+ "fixture", "native", "wasm", "equal"
459
+ );
460
+ let mut divergent = Vec::new();
461
+ for case in &corpus().cases {
462
+ let input = case.bytes();
463
+ let native = evaluate_canonical(&input);
464
+ let produced = wasm.evaluate(&input);
465
+ let equal = native == produced;
466
+ println!(
467
+ "{:<44} {:<16} {:<16} {}",
468
+ case.name,
469
+ summarize(&native),
470
+ summarize(&produced),
471
+ if equal { "yes" } else { "NO" }
472
+ );
473
+ if !equal {
474
+ divergent.push(format!(
475
+ "{}\n input: {}\n native: {}\n wasm: {}",
476
+ case.name,
477
+ String::from_utf8_lossy(&input),
478
+ String::from_utf8_lossy(&native),
479
+ String::from_utf8_lossy(&produced)
480
+ ));
481
+ }
482
+ }
483
+ assert!(
484
+ divergent.is_empty(),
485
+ "native and WASM contract implementations diverged:\n{}",
486
+ divergent.join("\n")
487
+ );
488
+ }
489
+
490
+ /// The WASM implementation reproduces the committed canonical output too.
491
+ ///
492
+ /// Separate from the pairwise comparison on purpose: two implementations can
493
+ /// agree with each other while both having moved.
494
+ #[test]
495
+ fn wasm_matches_the_committed_expectations() {
496
+ let Some(bytes) = contract_module() else {
497
+ return skipped("wasm_matches_the_committed_expectations");
498
+ };
499
+ let mut wasm = WasmContract::instantiate(bytes);
500
+ for case in &corpus().cases {
501
+ assert_eq!(
502
+ String::from_utf8(wasm.evaluate(&case.bytes())).unwrap(),
503
+ case.expected(),
504
+ "WASM contract diverged from the committed expectation for {}",
505
+ case.name
506
+ );
507
+ }
508
+ }
509
+
510
+ /// Both implementations answer for the same contract at the same version.
511
+ #[test]
512
+ fn both_implementations_declare_the_same_contract_version() {
513
+ let Some(bytes) = contract_module() else {
514
+ return skipped("both_implementations_declare_the_same_contract_version");
515
+ };
516
+ assert_eq!(
517
+ WasmContract::version(bytes),
518
+ STATE_CONFLICT_CONTRACT_VERSION
519
+ );
520
+ for case in &corpus().cases {
521
+ let output: Value = serde_json::from_slice(&evaluate_canonical(&case.bytes())).unwrap();
522
+ assert_eq!(output["contract"], STATE_CONFLICT_CONTRACT_ID);
523
+ assert_eq!(output["version"], STATE_CONFLICT_CONTRACT_VERSION);
524
+ }
525
+ }
526
+
527
+ /// The native adapter adds nothing to the existing implementation.
528
+ ///
529
+ /// The contract must be the classification FeltDB already computes, not a
530
+ /// second one that happens to agree. This runs both entry points over the whole
531
+ /// corpus, constructing revisions the way the adapter does.
532
+ #[test]
533
+ fn the_contract_is_the_existing_classifier() {
534
+ use feltdb::state_conflict_contract::classify_states;
535
+ use feltdb::state_model::{ConflictClassification, StateId, StateRevision};
536
+
537
+ let revision = |content: &str| StateRevision {
538
+ id: StateId::compute(content),
539
+ resource: String::new(),
540
+ content: content.to_string(),
541
+ content_id: StateId::compute(content),
542
+ parent_id: None,
543
+ sequence: 0,
544
+ authority: String::new(),
545
+ timestamp_ms: 0,
546
+ metadata: Default::default(),
547
+ };
548
+
549
+ let mut checked = 0;
550
+ for case in &corpus().cases {
551
+ let Some(input) = case.input.as_ref() else {
552
+ continue;
553
+ };
554
+ assert_eq!(
555
+ serde_json::to_string(&classify_states(&input.base, &input.left, &input.right))
556
+ .unwrap(),
557
+ serde_json::to_string(&ConflictClassification::classify(
558
+ &revision(&input.base),
559
+ &revision(&input.left),
560
+ &revision(&input.right)
561
+ ))
562
+ .unwrap(),
563
+ "the contract adapter changed the result for {}",
564
+ case.name
565
+ );
566
+ checked += 1;
567
+ }
568
+ assert!(checked > 80, "most of the corpus is typed input");
569
+ }
570
+
571
+ /// A short rendering of an output, for the printed conformance table.
572
+ fn summarize(output: &[u8]) -> String {
573
+ let parsed: Value = match serde_json::from_slice(output) {
574
+ Ok(value) => value,
575
+ Err(_) => return "?".into(),
576
+ };
577
+ match parsed["classification"]["path_conflicts"].as_array() {
578
+ Some(paths) => format!(
579
+ "{}/{}",
580
+ parsed["classification"]["overall"].as_str().unwrap_or("?"),
581
+ paths.len()
582
+ ),
583
+ None => "error".into(),
584
+ }
585
+ }
586
+
587
+ // ---------------------------------------------------------------------------
588
+ // Float rendering
589
+ // ---------------------------------------------------------------------------
590
+
591
+ /// The WASM float writer reproduces `serde_json`'s, over far more values than
592
+ /// the corpus can hold.
593
+ ///
594
+ /// This is the riskiest part of the second implementation. The contract's
595
+ /// output embeds whole input values, so the WASM module has to render doubles
596
+ /// exactly as `ryu` does — and neither Rust's `Display` nor its `Debug` will
597
+ /// do it: they disagree with `ryu` on presentation (`1e16` against `1e+16`)
598
+ /// and, on a fraction of values, on which shortest representation to pick.
599
+ ///
600
+ /// The sample is restricted to doubles `serde_json` parses back from its own
601
+ /// output, for the reason
602
+ /// [`serde_json_does_not_round_trip_every_double`] records: outside that set
603
+ /// the two implementations disagree about which double the bytes denote,
604
+ /// before any rendering happens, and this test would be measuring that instead.
605
+ ///
606
+ /// One diff carries the whole array as a single changed value, so this drives
607
+ /// the real module over the whole sample in a handful of calls.
608
+ #[test]
609
+ fn wasm_renders_floats_exactly_as_serde_json_does() {
610
+ let Some(bytes) = contract_module() else {
611
+ return skipped("wasm_renders_floats_exactly_as_serde_json_does");
612
+ };
613
+ let mut wasm = WasmContract::instantiate(bytes);
614
+
615
+ let mut sample: Vec<f64> = vec![
616
+ 0.0,
617
+ -0.0,
618
+ 1.0,
619
+ -1.0,
620
+ 100.0,
621
+ 0.5,
622
+ 0.125,
623
+ 3.141592653589793,
624
+ 2.718281828459045,
625
+ 1e15,
626
+ 1e16,
627
+ 1e17,
628
+ 1e21,
629
+ 1e100,
630
+ 1e308,
631
+ 1e-1,
632
+ 1e-4,
633
+ 1e-5,
634
+ 1e-6,
635
+ 1e-7,
636
+ 1e-100,
637
+ 5e-324,
638
+ f64::MAX,
639
+ f64::MIN,
640
+ f64::MIN_POSITIVE,
641
+ 0.1,
642
+ 0.2,
643
+ 0.30000000000000004,
644
+ 9007199254740992.0,
645
+ ];
646
+ // A deterministic sweep over bit patterns, and over decimals of the kind
647
+ // records actually hold.
648
+ let mut seed: u64 = 0x243F6A8885A308D3;
649
+ for _ in 0..12_000 {
650
+ seed = seed
651
+ .wrapping_mul(6364136223846793005)
652
+ .wrapping_add(1442695040888963407);
653
+ let value = f64::from_bits(seed);
654
+ if value.is_finite() {
655
+ sample.push(value);
656
+ }
657
+ }
658
+ for step in -6_000i64..6_000 {
659
+ sample.push(step as f64 / 1000.0);
660
+ }
661
+ let offered = sample.len();
662
+ sample.retain(|value| serde_json_round_trips(*value));
663
+ let checked = sample.len();
664
+ assert!(
665
+ checked > 12_000,
666
+ "the sample should still be large after excluding what serde_json cannot round trip"
667
+ );
668
+
669
+ for chunk in sample.chunks(4_000) {
670
+ // The base holds the whole sample and both branches replace it with 0,
671
+ // so the array is emitted once as `base_value` and the sides converge.
672
+ let base = serde_json::to_string(&Value::Array(
673
+ chunk.iter().map(|value| Value::from(*value)).collect(),
674
+ ))
675
+ .unwrap();
676
+ let input = canonical_input_bytes(&StateConflictInput::new(
677
+ base,
678
+ "0".to_string(),
679
+ "0".to_string(),
680
+ ));
681
+ let native = evaluate_canonical(&input);
682
+ let produced = wasm.evaluate(&input);
683
+ if native != produced {
684
+ // Report the first differing value rather than a 100 KB diff.
685
+ let native_text = String::from_utf8_lossy(&native);
686
+ let wasm_text = String::from_utf8_lossy(&produced);
687
+ let at = native_text
688
+ .char_indices()
689
+ .zip(wasm_text.char_indices())
690
+ .find(|((_, a), (_, b))| a != b)
691
+ .map(|((index, _), _)| index)
692
+ .unwrap_or(0);
693
+ let from = at.saturating_sub(40);
694
+ panic!(
695
+ "the WASM float writer diverged from serde_json:\n native: …{}…\n wasm: …{}…",
696
+ &native_text[from..(at + 40).min(native_text.len())],
697
+ &wasm_text[from..(at + 40).min(wasm_text.len())]
698
+ );
699
+ }
700
+ }
701
+ println!("float rendering agrees on {checked} of {offered} sampled doubles");
702
+ }
703
+
704
+ /// Whether `serde_json` reads back the double it itself wrote.
705
+ fn serde_json_round_trips(value: f64) -> bool {
706
+ let text = serde_json::to_string(&Value::from(value)).expect("finite");
707
+ serde_json::from_str::<Value>(&text)
708
+ .ok()
709
+ .and_then(|parsed| parsed.as_f64())
710
+ .is_some_and(|parsed| parsed.to_bits() == value.to_bits())
711
+ }
712
+
713
+ /// `serde_json` does not parse back every double it writes, and that bounds
714
+ /// what this contract can claim.
715
+ ///
716
+ /// `serde_json`'s float parser is accurate to within one unit in the last
717
+ /// place but is not correctly rounded, which is its documented default: exact
718
+ /// round-tripping is the opt-in `float_roundtrip` feature, at roughly twice the
719
+ /// parsing cost. The WASM implementation uses `core`'s `dec2flt`, which *is*
720
+ /// correctly rounded, so for the affected literals the two implementations
721
+ /// disagree about which double the bytes denote — before either renders
722
+ /// anything.
723
+ ///
724
+ /// This test exists so that boundary is measured and visible rather than
725
+ /// assumed. It asserts only that short, ordinary decimals — the numbers real
726
+ /// records hold — are unaffected. It deliberately does not assert a rate: that
727
+ /// is a property of the dependency and may change when it is upgraded.
728
+ #[test]
729
+ fn serde_json_does_not_round_trip_every_double() {
730
+ let mut lost = 0usize;
731
+ let mut total = 0usize;
732
+ let mut example = None;
733
+ let mut seed: u64 = 0x243F6A8885A308D3;
734
+ for _ in 0..200_000 {
735
+ seed = seed
736
+ .wrapping_mul(6364136223846793005)
737
+ .wrapping_add(1442695040888963407);
738
+ let value = f64::from_bits(seed);
739
+ if !value.is_finite() {
740
+ continue;
741
+ }
742
+ total += 1;
743
+ if !serde_json_round_trips(value) {
744
+ lost += 1;
745
+ if example.is_none() {
746
+ example = Some(serde_json::to_string(&Value::from(value)).unwrap());
747
+ }
748
+ }
749
+ }
750
+ println!(
751
+ "serde_json {} of {total} random doubles do not survive its own \
752
+ serialize/parse round trip (example: {})",
753
+ lost,
754
+ example.as_deref().unwrap_or("none")
755
+ );
756
+ assert!(
757
+ lost > 0,
758
+ "if serde_json now round trips every double, the restriction on the float rendering \
759
+ test and the contract's stated input domain can both be removed"
760
+ );
761
+
762
+ // The values records actually hold are unaffected, which is what makes the
763
+ // contract useful despite the restriction.
764
+ for ordinary in [
765
+ 0.0, -0.0, 0.5, 0.125, 1.0, 2.5, 42.0, 99.99, 1e-5, 1e15, 1e16, 1e-6, 3.14, 2.718,
766
+ 1234.5678, -0.001, 100.0, 0.1, 0.2, 0.3,
767
+ ] {
768
+ assert!(
769
+ serde_json_round_trips(ordinary),
770
+ "an ordinary decimal must round trip: {ordinary}"
771
+ );
772
+ }
773
+ }
774
+
775
+ // ---------------------------------------------------------------------------
776
+ // Determinism
777
+ // ---------------------------------------------------------------------------
778
+
779
+ /// The same input, in the same instance, produces the same output every time.
780
+ #[test]
781
+ fn wasm_is_deterministic_across_repeated_calls() {
782
+ let Some(bytes) = contract_module() else {
783
+ return skipped("wasm_is_deterministic_across_repeated_calls");
784
+ };
785
+ let mut wasm = WasmContract::instantiate(bytes);
786
+ for case in &corpus().cases {
787
+ let input = case.bytes();
788
+ let first = wasm.evaluate(&input);
789
+ for round in 1..16 {
790
+ assert_eq!(
791
+ wasm.evaluate(&input),
792
+ first,
793
+ "{} changed answer on repeat {round} within one instance",
794
+ case.name
795
+ );
796
+ }
797
+ }
798
+ }
799
+
800
+ /// Repeated native evaluation is also stable.
801
+ ///
802
+ /// Cheap, and it forecloses the reading in which only the WASM side was ever
803
+ /// at risk of varying.
804
+ #[test]
805
+ fn native_is_deterministic_across_repeated_calls() {
806
+ for case in &corpus().cases {
807
+ let input = case.bytes();
808
+ let first = evaluate_canonical(&input);
809
+ for round in 1..16 {
810
+ assert_eq!(
811
+ evaluate_canonical(&input),
812
+ first,
813
+ "{} changed answer on native repeat {round}",
814
+ case.name
815
+ );
816
+ }
817
+ }
818
+ }
819
+
820
+ /// The same input produces the same output in a fresh instance.
821
+ ///
822
+ /// Each instance gets its own `Store`, so its linear memory, allocator state
823
+ /// and call history all start over. An implementation that carried state
824
+ /// between diffs would show it here.
825
+ #[test]
826
+ fn wasm_is_deterministic_across_fresh_instances() {
827
+ let Some(bytes) = contract_module() else {
828
+ return skipped("wasm_is_deterministic_across_fresh_instances");
829
+ };
830
+ let mut baseline = WasmContract::instantiate(bytes);
831
+ let expected: Vec<Vec<u8>> = corpus()
832
+ .cases
833
+ .iter()
834
+ .map(|case| baseline.evaluate(&case.bytes()))
835
+ .collect();
836
+
837
+ for round in 0..4 {
838
+ let mut fresh = WasmContract::instantiate(bytes);
839
+ for (case, want) in corpus().cases.iter().zip(&expected) {
840
+ assert_eq!(
841
+ &fresh.evaluate(&case.bytes()),
842
+ want,
843
+ "{} changed answer in fresh instance {round}",
844
+ case.name
845
+ );
846
+ }
847
+ }
848
+ }
849
+
850
+ /// Order of evaluation does not affect any answer.
851
+ #[test]
852
+ fn wasm_answers_do_not_depend_on_evaluation_order() {
853
+ let Some(bytes) = contract_module() else {
854
+ return skipped("wasm_answers_do_not_depend_on_evaluation_order");
855
+ };
856
+ let mut forwards = WasmContract::instantiate(bytes);
857
+ let ordered: Vec<Vec<u8>> = corpus()
858
+ .cases
859
+ .iter()
860
+ .map(|case| forwards.evaluate(&case.bytes()))
861
+ .collect();
862
+
863
+ let mut backwards = WasmContract::instantiate(bytes);
864
+ for (index, case) in corpus().cases.iter().enumerate().rev() {
865
+ assert_eq!(
866
+ backwards.evaluate(&case.bytes()),
867
+ ordered[index],
868
+ "{} answered differently when the corpus ran in reverse",
869
+ case.name
870
+ );
871
+ }
872
+ }
873
+
874
+ /// The same input produces the same output in a separate OS process.
875
+ #[test]
876
+ fn wasm_is_deterministic_across_process_boundaries() {
877
+ let Some(bytes) = contract_module() else {
878
+ return skipped("wasm_is_deterministic_across_process_boundaries");
879
+ };
880
+ let mut here = WasmContract::instantiate(bytes);
881
+ let mine: Vec<Vec<u8>> = corpus()
882
+ .cases
883
+ .iter()
884
+ .map(|case| here.evaluate(&case.bytes()))
885
+ .collect();
886
+
887
+ let executable = std::env::current_exe().expect("the test binary has a path");
888
+ let output = Command::new(executable)
889
+ .args([
890
+ "--exact",
891
+ "wasm_is_deterministic_across_fresh_instances",
892
+ "--nocapture",
893
+ ])
894
+ .output()
895
+ .expect("the test binary re-runs");
896
+ assert!(
897
+ output.status.success(),
898
+ "the corpus did not reproduce in a separate process:\n{}",
899
+ String::from_utf8_lossy(&output.stdout)
900
+ );
901
+
902
+ for (case, produced) in corpus().cases.iter().zip(&mine) {
903
+ assert_eq!(String::from_utf8_lossy(produced), case.expected());
904
+ }
905
+ }
906
+
907
+ // ---------------------------------------------------------------------------
908
+ // Runtime restrictions
909
+ // ---------------------------------------------------------------------------
910
+
911
+ /// The module imports nothing.
912
+ ///
913
+ /// This is the whole capability argument and it is deliberately narrow. A
914
+ /// WebAssembly module reaches the outside world only through its imports. With
915
+ /// an empty import section there is no clock, no filesystem handle, no socket,
916
+ /// no entropy source and no FeltDB authority state in scope — not because
917
+ /// access was denied at call time, but because no such function is linked. It
918
+ /// says nothing about hostile modules, which this suite does not test.
919
+ #[test]
920
+ fn wasm_module_declares_no_imports() {
921
+ let Some(bytes) = contract_module() else {
922
+ return skipped("wasm_module_declares_no_imports");
923
+ };
924
+ let engine = wasmi::Engine::default();
925
+ let module = wasmi::Module::new(&engine, bytes).expect("the module validates");
926
+ let imports: Vec<String> = module
927
+ .imports()
928
+ .map(|import: wasmi::ImportType| format!("{}::{}", import.module(), import.name()))
929
+ .collect();
930
+ assert!(
931
+ imports.is_empty(),
932
+ "the contract module must import nothing; it imports {imports:?}"
933
+ );
934
+ }
935
+
936
+ /// An empty linker instantiates the module.
937
+ #[test]
938
+ fn wasm_module_instantiates_with_no_host_functions() {
939
+ let Some(bytes) = contract_module() else {
940
+ return skipped("wasm_module_instantiates_with_no_host_functions");
941
+ };
942
+ let engine = wasmi::Engine::default();
943
+ let module = wasmi::Module::new(&engine, bytes).unwrap();
944
+ let mut store = wasmi::Store::new(&engine, ());
945
+ wasmi::Linker::<()>::new(&engine)
946
+ .instantiate_and_start(&mut store, &module)
947
+ .expect("no host function is required");
948
+ }
949
+
950
+ /// The module exports the contract ABI and nothing that could act on FeltDB.
951
+ #[test]
952
+ fn wasm_module_exports_only_the_contract_abi() {
953
+ let Some(bytes) = contract_module() else {
954
+ return skipped("wasm_module_exports_only_the_contract_abi");
955
+ };
956
+ let engine = wasmi::Engine::default();
957
+ let module = wasmi::Module::new(&engine, bytes).unwrap();
958
+ let mut functions: Vec<String> = module
959
+ .exports()
960
+ .filter(|export| export.ty().func().is_some())
961
+ .map(|export| export.name().to_string())
962
+ .collect();
963
+ functions.sort();
964
+ assert_eq!(
965
+ functions,
966
+ vec![
967
+ "contract_alloc".to_string(),
968
+ "contract_evaluate".to_string(),
969
+ "contract_free".to_string(),
970
+ "contract_version".to_string(),
971
+ ]
972
+ );
973
+ }
974
+
975
+ /// WASI is not present, under any of the names a runtime would look for.
976
+ #[test]
977
+ fn wasm_module_requires_no_wasi() {
978
+ let Some(bytes) = contract_module() else {
979
+ return skipped("wasm_module_requires_no_wasi");
980
+ };
981
+ let text = String::from_utf8_lossy(bytes);
982
+ for forbidden in [
983
+ "wasi_snapshot_preview1",
984
+ "wasi_unstable",
985
+ "clock_time_get",
986
+ "random_get",
987
+ "fd_read",
988
+ "fd_write",
989
+ "sock_recv",
990
+ "path_open",
991
+ ] {
992
+ assert!(
993
+ !text.contains(forbidden),
994
+ "the contract module names {forbidden}, which it has no reason to"
995
+ );
996
+ }
997
+ }
998
+
999
+ /// The contract never traps, on any corpus input.
1000
+ #[test]
1001
+ fn wasm_contract_returns_a_result_for_every_input_including_malformed() {
1002
+ let Some(bytes) = contract_module() else {
1003
+ return skipped("wasm_contract_returns_a_result_for_every_input_including_malformed");
1004
+ };
1005
+ let mut wasm = WasmContract::instantiate(bytes);
1006
+ for case in &corpus().cases {
1007
+ let output = wasm.evaluate(&case.bytes());
1008
+ let parsed: Value =
1009
+ serde_json::from_slice(&output).expect("every output is canonical JSON");
1010
+ assert_eq!(parsed["contract"], STATE_CONFLICT_CONTRACT_ID);
1011
+ assert!(
1012
+ parsed.get("classification").is_some() ^ parsed.get("error").is_some(),
1013
+ "{} produced neither or both of classification and error",
1014
+ case.name
1015
+ );
1016
+ }
1017
+ // Inputs no fixture would contain, to show trap-freedom is not a property
1018
+ // of the corpus. Nesting past the shared recursion bound is included.
1019
+ for hostile in [
1020
+ vec![0u8; 0],
1021
+ vec![0xff; 64],
1022
+ b"{".repeat(4096),
1023
+ b"[".repeat(4096),
1024
+ vec![b'"'; 1024],
1025
+ // Nesting past the shared recursion bound, inside state content.
1026
+ format!(
1027
+ r#"{{"base":{},"contract":"feltdb.state.conflict","left":"1","right":"1","version":1}}"#,
1028
+ serde_json::to_string(&format!("{}{}", "[".repeat(300), "]".repeat(300))).unwrap()
1029
+ )
1030
+ .into_bytes(),
1031
+ ] {
1032
+ let output = wasm.evaluate(&hostile);
1033
+ assert!(serde_json::from_slice::<Value>(&output).is_ok());
1034
+ }
1035
+ }
1036
+
1037
+ // ---------------------------------------------------------------------------
1038
+ // Mutation: does the suite actually detect divergence?
1039
+ // ---------------------------------------------------------------------------
1040
+
1041
+ /// A deliberate change to the WASM implementation's source.
1042
+ struct Mutation {
1043
+ name: &'static str,
1044
+ file: &'static str,
1045
+ from: &'static str,
1046
+ to: &'static str,
1047
+ /// Whether the corpus must catch it.
1048
+ ///
1049
+ /// Some mutations are expected *not* to be caught, and each such
1050
+ /// expectation is part of the contract rather than a gap. `why_invisible`
1051
+ /// says which reason applies.
1052
+ detected: bool,
1053
+ /// For a mutation expected to go uncaught, why it cannot be observed
1054
+ /// through this contract. Empty for mutations that must be caught.
1055
+ why_invisible: &'static str,
1056
+ }
1057
+
1058
+ const MUTATIONS: &[Mutation] = &[
1059
+ // --- the overlap relation, which this contract version establishes ---
1060
+ Mutation {
1061
+ name: "ancestor overlap is not detected",
1062
+ file: "src/conflict.rs",
1063
+ from: " for depth in 0..path.len() {",
1064
+ to: " for depth in 0..0 {",
1065
+ detected: true,
1066
+ why_invisible: "",
1067
+ },
1068
+ Mutation {
1069
+ name: "descendant overlap is not detected",
1070
+ file: "src/conflict.rs",
1071
+ from: " changes\n .get(after)\n .is_some_and(|change| path_starts_with(&change.path, path))",
1072
+ to: " changes\n .get(after)\n .is_some_and(|_| false)",
1073
+ detected: true,
1074
+ why_invisible: "",
1075
+ },
1076
+ Mutation {
1077
+ name: "overlap is checked against the branch's own changes",
1078
+ file: "src/conflict.rs",
1079
+ from: " if overlapping_change_exists(&right_changes, &path) {",
1080
+ to: " if overlapping_change_exists(&left_changes, &path) {",
1081
+ detected: true,
1082
+ why_invisible: "",
1083
+ },
1084
+ Mutation {
1085
+ name: "path components are compared by length alone",
1086
+ file: "src/conflict.rs",
1087
+ from: " candidate.len() >= prefix.len() && candidate[..prefix.len()] == *prefix",
1088
+ to: " candidate.len() >= prefix.len()",
1089
+ detected: true,
1090
+ why_invisible: "",
1091
+ },
1092
+ Mutation {
1093
+ name: "only the first path component is compared",
1094
+ file: "src/conflict.rs",
1095
+ from: " candidate.len() >= prefix.len() && candidate[..prefix.len()] == *prefix",
1096
+ to: " candidate.len() >= prefix.len()\n && (prefix.is_empty() || candidate[0] == prefix[0])",
1097
+ detected: true,
1098
+ why_invisible: "",
1099
+ },
1100
+ Mutation {
1101
+ name: "every overlap is reported as convergent rather than conflicting",
1102
+ file: "src/conflict.rs",
1103
+ from: " if overlapping_change_exists(&right_changes, &path) {\n has_conflict = true;\n ConflictClass::Conflict",
1104
+ to: " if overlapping_change_exists(&right_changes, &path) {\n has_convergence = true;\n ConflictClass::Convergent",
1105
+ detected: true,
1106
+ why_invisible: "",
1107
+ },
1108
+ // --- base_value, corrected in this version ---------------------------
1109
+ Mutation {
1110
+ name: "base_value is read from the left branch rather than the base",
1111
+ file: "src/conflict.rs",
1112
+ from: " base_value: resolve_path(base, &path).cloned(),",
1113
+ to: " base_value: resolve_path(left, &path).cloned(),",
1114
+ detected: true,
1115
+ why_invisible: "",
1116
+ },
1117
+ Mutation {
1118
+ name: "base_value is dropped",
1119
+ file: "src/conflict.rs",
1120
+ from: " base_value: resolve_path(base, &path).cloned(),",
1121
+ to: " base_value: None,",
1122
+ detected: true,
1123
+ why_invisible: "",
1124
+ },
1125
+ Mutation {
1126
+ name: "base_value resolves array elements as object members",
1127
+ file: "src/conflict.rs",
1128
+ from: " PathComponent::Index(index) => match current {\n Json::Array(items) => items.get(*index)?,\n _ => return None,\n },",
1129
+ to: " PathComponent::Index(_) => return None,",
1130
+ detected: true,
1131
+ why_invisible: "",
1132
+ },
1133
+ // --- the classification relation itself ------------------------------
1134
+ Mutation {
1135
+ name: "disagreeing edits are classified as independent",
1136
+ file: "src/conflict.rs",
1137
+ from: " } else {\n has_conflict = true;\n ConflictClass::Conflict\n }",
1138
+ to: " } else {\n ConflictClass::Independent\n }",
1139
+ detected: true,
1140
+ why_invisible: "",
1141
+ },
1142
+ Mutation {
1143
+ name: "agreeing edits are classified as conflicting",
1144
+ file: "src/conflict.rs",
1145
+ from: " if one.kind == two.kind && optional_equals(&one.new_value, &two.new_value) {",
1146
+ to: " if false {",
1147
+ detected: true,
1148
+ why_invisible: "",
1149
+ },
1150
+ Mutation {
1151
+ name: "convergence ignores the new value",
1152
+ file: "src/conflict.rs",
1153
+ from: " if one.kind == two.kind && optional_equals(&one.new_value, &two.new_value) {",
1154
+ to: " if one.kind == two.kind {",
1155
+ detected: true,
1156
+ why_invisible: "",
1157
+ },
1158
+ Mutation {
1159
+ name: "a disjoint one-sided change is classified as convergent",
1160
+ file: "src/conflict.rs",
1161
+ from: " } else {\n ConflictClass::Independent\n }\n }\n (None, Some(_)) => {",
1162
+ to: " } else {\n ConflictClass::Convergent\n }\n }\n (None, Some(_)) => {",
1163
+ detected: true,
1164
+ why_invisible: "",
1165
+ },
1166
+ Mutation {
1167
+ name: "the two branches are recorded on the wrong sides",
1168
+ file: "src/conflict.rs",
1169
+ from: " left_value: left_change.and_then(|change: &Change| change.new_value.clone()),\n right_value: right_change.and_then(|change: &Change| change.new_value.clone()),",
1170
+ to: " left_value: right_change.and_then(|change: &Change| change.new_value.clone()),\n right_value: left_change.and_then(|change: &Change| change.new_value.clone()),",
1171
+ detected: true,
1172
+ why_invisible: "",
1173
+ },
1174
+ Mutation {
1175
+ name: "overall severity prefers convergence over conflict",
1176
+ file: "src/conflict.rs",
1177
+ from: " let overall = if has_conflict {\n ConflictClass::Conflict\n } else if has_convergence {",
1178
+ to: " let overall = if has_convergence {\n ConflictClass::Convergent\n } else if has_conflict {",
1179
+ detected: true,
1180
+ why_invisible: "",
1181
+ },
1182
+ Mutation {
1183
+ name: "overall ignores convergence entirely",
1184
+ file: "src/conflict.rs",
1185
+ from: " } else if has_convergence {\n ConflictClass::Convergent\n } else {",
1186
+ to: " } else if false {\n ConflictClass::Convergent\n } else {",
1187
+ detected: true,
1188
+ why_invisible: "",
1189
+ },
1190
+ // --- the diff the classification is defined over ---------------------
1191
+ Mutation {
1192
+ name: "a null state and a missing member are diffed alike",
1193
+ file: "src/diff.rs",
1194
+ from: " (Json::Null, Json::Null) => {}",
1195
+ to: " (Json::Null, Json::Null) if false => {}",
1196
+ detected: true,
1197
+ why_invisible: "",
1198
+ },
1199
+ Mutation {
1200
+ name: "array indexes are off by one",
1201
+ file: "src/diff.rs",
1202
+ from: " for (index, (old_value, new_value)) in a.iter().zip(b.iter()).enumerate() {\n path.push(PathComponent::Index(index));",
1203
+ to: " for (index, (old_value, new_value)) in a.iter().zip(b.iter()).enumerate() {\n path.push(PathComponent::Index(index + 1));",
1204
+ detected: true,
1205
+ why_invisible: "",
1206
+ },
1207
+ Mutation {
1208
+ name: "integers and floats compare equal",
1209
+ file: "src/diff.rs",
1210
+ from: " (Json::Float(a), Json::Float(b)) if a == b => {}",
1211
+ to: " (Json::Float(a), Json::Float(b)) if a == b => {}\n (Json::PosInt(a), Json::Float(b)) if *a as f64 == *b => {}\n (Json::Float(a), Json::PosInt(b)) if *a == *b as f64 => {}",
1212
+ detected: true,
1213
+ why_invisible: "",
1214
+ },
1215
+ Mutation {
1216
+ name: "unparseable state content is an error rather than JSON null",
1217
+ file: "src/lib.rs",
1218
+ from: " .unwrap_or(json::Json::Null)",
1219
+ to: " .unwrap_or(json::Json::Bool(false))",
1220
+ detected: true,
1221
+ why_invisible: "",
1222
+ },
1223
+ // --- mutations that must NOT be caught -------------------------------
1224
+ Mutation {
1225
+ name: "object additions are diffed in reverse order before sorting",
1226
+ file: "src/diff.rs",
1227
+ from: " for (key, new_value) in b {\n if old.get(key).is_none() {",
1228
+ to: " for (key, new_value) in b.iter().rev() {\n if old.get(key).is_none() {",
1229
+ detected: false,
1230
+ why_invisible: "the diff sorts its own result, so the order additions were emitted in \
1231
+ cannot survive into either diff or into the classification",
1232
+ },
1233
+ Mutation {
1234
+ name: "ancestor paths are searched from the deepest prefix first",
1235
+ file: "src/conflict.rs",
1236
+ from: " for depth in 0..path.len() {",
1237
+ to: " for depth in (0..path.len()).rev() {",
1238
+ detected: false,
1239
+ why_invisible: "the ancestor search only asks whether any prefix is present, so the \
1240
+ order it visits them in cannot change the answer",
1241
+ },
1242
+ Mutation {
1243
+ name: "convergence ignores the change kind",
1244
+ file: "src/conflict.rs",
1245
+ from: " if one.kind == two.kind && optional_equals(&one.new_value, &two.new_value) {",
1246
+ to: " if optional_equals(&one.new_value, &two.new_value) {",
1247
+ detected: false,
1248
+ why_invisible: "the kind comparison is redundant: both diffs share a base, so at a path \
1249
+ both branches touched the kinds match or the new values already differ",
1250
+ },
1251
+ ];
1252
+
1253
+ /// Every mutation expected to go uncaught explains itself.
1254
+ #[test]
1255
+ fn uncaught_mutations_record_why_they_are_invisible() {
1256
+ for mutation in MUTATIONS {
1257
+ assert_eq!(
1258
+ mutation.detected,
1259
+ mutation.why_invisible.is_empty(),
1260
+ "mutation {:?}: a mutation is either expected to be caught, or must say why it \
1261
+ cannot be",
1262
+ mutation.name
1263
+ );
1264
+ }
1265
+ }
1266
+
1267
+ /// Every deliberate mutation of the WASM contract lands where it should.
1268
+ ///
1269
+ /// Without this the conformance suite proves only that two implementations
1270
+ /// agree, not that agreement was ever in question. Each mutant is built from a
1271
+ /// copy of the real source with one edit applied. Twelve must fail against the
1272
+ /// committed expectations; one must not, because it changes an order the
1273
+ /// contract deliberately does not make semantic.
1274
+ #[test]
1275
+ fn deliberate_wasm_mutations_land_where_they_should() {
1276
+ if contract_module().is_none() {
1277
+ return skipped("deliberate_wasm_mutations_land_where_they_should");
1278
+ }
1279
+ let source = repository_root().join(WASM_CRATE);
1280
+ let scratch = tempfile::tempdir().expect("a scratch directory");
1281
+
1282
+ let mut wrong = Vec::new();
1283
+ for mutation in MUTATIONS {
1284
+ let root = scratch.path().join(mutation.name.replace(' ', "-"));
1285
+ copy_crate(&source, &root);
1286
+ let target = root.join(mutation.file);
1287
+ let original = std::fs::read_to_string(&target).expect("the mutated file exists");
1288
+ assert!(
1289
+ original.matches(mutation.from).count() == 1,
1290
+ "mutation {:?} no longer applies: its anchor is not present exactly once in {}. \
1291
+ The mutation suite must be updated with the implementation it mutates.",
1292
+ mutation.name,
1293
+ mutation.file
1294
+ );
1295
+ std::fs::write(&target, original.replace(mutation.from, mutation.to)).unwrap();
1296
+
1297
+ let mut mutant = WasmContract::instantiate(&build_module(&root));
1298
+ let caught: Vec<&str> = corpus()
1299
+ .cases
1300
+ .iter()
1301
+ .filter(|case| {
1302
+ String::from_utf8_lossy(&mutant.evaluate(&case.bytes())) != case.expected()
1303
+ })
1304
+ .map(|case| case.name.as_str())
1305
+ .collect();
1306
+
1307
+ match (mutation.detected, caught.first()) {
1308
+ (true, Some(first)) => println!(
1309
+ "mutation {:?} caught by {} fixtures, first: {first}",
1310
+ mutation.name,
1311
+ caught.len()
1312
+ ),
1313
+ (false, None) => println!(
1314
+ "mutation {:?} correctly NOT caught: {}",
1315
+ mutation.name, mutation.why_invisible
1316
+ ),
1317
+ (true, None) => wrong.push(format!(
1318
+ "{:?} was not caught, but the corpus should distinguish it",
1319
+ mutation.name
1320
+ )),
1321
+ (false, Some(_)) => wrong.push(format!(
1322
+ "{:?} was caught by {} fixtures, but it was expected to be invisible because {}. \
1323
+ Either the implementation or that reasoning has changed, and both the mutation \
1324
+ expectation and the contract documentation need revisiting.",
1325
+ mutation.name,
1326
+ caught.len(),
1327
+ mutation.why_invisible
1328
+ )),
1329
+ }
1330
+ }
1331
+ assert!(
1332
+ wrong.is_empty(),
1333
+ "mutation outcomes were wrong:\n{}",
1334
+ wrong.join("\n")
1335
+ );
1336
+ }
1337
+
1338
+ /// Copies the contract crate's source, without its build directory.
1339
+ fn copy_crate(from: &Path, to: &Path) {
1340
+ std::fs::create_dir_all(to.join("src")).unwrap();
1341
+ std::fs::copy(from.join("Cargo.toml"), to.join("Cargo.toml")).unwrap();
1342
+ for entry in std::fs::read_dir(from.join("src")).unwrap() {
1343
+ let entry = entry.unwrap();
1344
+ std::fs::copy(entry.path(), to.join("src").join(entry.file_name())).unwrap();
1345
+ }
1346
+ }
1347
+
1348
+ /// Mutating the input encoding is detected where the encoding is semantically
1349
+ /// load-bearing, and not where it is not.
1350
+ ///
1351
+ /// - Reordering members changes nothing, because a JSON object denotes a map
1352
+ /// and both implementations decode it to one. The suite must *not* flag it.
1353
+ /// - Swapping the two states changes the diff's direction, which is entirely
1354
+ /// semantic. The suite must flag it.
1355
+ /// - Dropping the contract identity turns every case into an input error.
1356
+ #[test]
1357
+ fn input_encoding_mutations_are_detected_where_they_matter() {
1358
+ let typed: Vec<(&Case, Value)> = corpus()
1359
+ .cases
1360
+ .iter()
1361
+ .filter_map(|case| {
1362
+ let input = case.input.as_ref()?;
1363
+ Some((case, serde_json::to_value(input).unwrap()))
1364
+ })
1365
+ .collect();
1366
+ assert!(typed.len() > 70, "most of the corpus is typed input");
1367
+
1368
+ // 1. Member order, which carries no meaning.
1369
+ for (case, encoded) in &typed {
1370
+ let produced =
1371
+ String::from_utf8(evaluate_canonical(encode_reversed(encoded).as_bytes())).unwrap();
1372
+ assert_eq!(
1373
+ produced,
1374
+ case.expected(),
1375
+ "{} changed answer under a reordered encoding; object member order is not part of \
1376
+ this contract's semantics",
1377
+ case.name
1378
+ );
1379
+ }
1380
+
1381
+ // 2. Which state is the base, which carries all of it.
1382
+ let mut base_matters = 0;
1383
+ for (case, encoded) in &typed {
1384
+ let mut rotated = encoded.clone();
1385
+ let object = rotated.as_object_mut().unwrap();
1386
+ let base = object["base"].clone();
1387
+ let left = object["left"].clone();
1388
+ object["base"] = left;
1389
+ object["left"] = base;
1390
+ let produced = String::from_utf8(evaluate_canonical(
1391
+ serde_json::to_vec(&rotated).unwrap().as_slice(),
1392
+ ))
1393
+ .unwrap();
1394
+ if produced != case.expected() {
1395
+ base_matters += 1;
1396
+ }
1397
+ }
1398
+ assert!(
1399
+ base_matters > 40,
1400
+ "the ancestor is not interchangeable with a branch; exchanging them should change most \
1401
+ results, but it changed only {base_matters}"
1402
+ );
1403
+
1404
+ // 3. Contract identity dropped.
1405
+ for (case, encoded) in typed.iter().take(1) {
1406
+ let mut mutated = encoded.clone();
1407
+ mutated.as_object_mut().unwrap().remove("contract");
1408
+ let produced = String::from_utf8(evaluate_canonical(
1409
+ serde_json::to_vec(&mutated).unwrap().as_slice(),
1410
+ ))
1411
+ .unwrap();
1412
+ assert_ne!(produced, case.expected());
1413
+ assert!(produced.contains("CONTRACT_INPUT_INVALID"));
1414
+ }
1415
+ }
1416
+
1417
+ /// Encodes a value with every object's members in reverse sorted order.
1418
+ ///
1419
+ /// `serde_json::Map` re-sorts on insert, so a non-canonical member order has to
1420
+ /// be produced as text rather than as a value.
1421
+ fn encode_reversed(value: &Value) -> String {
1422
+ match value {
1423
+ Value::Object(members) => {
1424
+ let mut parts: Vec<String> = members
1425
+ .iter()
1426
+ .map(|(key, value)| {
1427
+ format!(
1428
+ "{}:{}",
1429
+ serde_json::to_string(key).unwrap(),
1430
+ encode_reversed(value)
1431
+ )
1432
+ })
1433
+ .collect();
1434
+ parts.reverse();
1435
+ format!("{{{}}}", parts.join(","))
1436
+ }
1437
+ Value::Array(items) => format!(
1438
+ "[{}]",
1439
+ items
1440
+ .iter()
1441
+ .map(encode_reversed)
1442
+ .collect::<Vec<_>>()
1443
+ .join(",")
1444
+ ),
1445
+ other => serde_json::to_string(other).unwrap(),
1446
+ }
1447
+ }
1448
+
1449
+ // ---------------------------------------------------------------------------
1450
+ // Semantic invariants
1451
+ // ---------------------------------------------------------------------------
1452
+
1453
+ /// The three states of every typed fixture, for invariant checks.
1454
+ fn triples() -> Vec<(&'static str, String, String, String)> {
1455
+ corpus()
1456
+ .cases
1457
+ .iter()
1458
+ .filter_map(|case| {
1459
+ let input = case.input.as_ref()?;
1460
+ Some((
1461
+ Box::leak(case.name.clone().into_boxed_str()) as &str,
1462
+ input.base.clone(),
1463
+ input.left.clone(),
1464
+ input.right.clone(),
1465
+ ))
1466
+ })
1467
+ .collect()
1468
+ }
1469
+
1470
+ fn classify(base: &str, left: &str, right: &str) -> ConflictClassification {
1471
+ feltdb::state_conflict_contract::classify_states(base, left, right)
1472
+ }
1473
+
1474
+ /// The relation's own laws, over every state in the corpus.
1475
+ ///
1476
+ /// Conformance proves two implementations agree. These prove the thing they
1477
+ /// agree on is the relation the contract describes, which is a different claim
1478
+ /// and the one that would survive replacing both implementations.
1479
+ #[test]
1480
+ fn classification_invariants_hold() {
1481
+ for (name, base, left, right) in triples() {
1482
+ // Nothing changed anywhere: no paths, and Independent overall. There is
1483
+ // no fourth "no change" class.
1484
+ for state in [&base, &left, &right] {
1485
+ let same = classify(state, state, state);
1486
+ assert!(
1487
+ same.path_conflicts.is_empty(),
1488
+ "{name}: identical states must produce no paths"
1489
+ );
1490
+ assert_eq!(
1491
+ same.overall,
1492
+ ConflictClass::Independent,
1493
+ "{name}: identical states are Independent"
1494
+ );
1495
+ }
1496
+
1497
+ // One side unchanged: every path is Independent, both ways round.
1498
+ for (one, two) in [(&left, &base), (&base, &right)] {
1499
+ let single = classify(&base, one, two);
1500
+ assert!(
1501
+ single
1502
+ .path_conflicts
1503
+ .iter()
1504
+ .all(|entry| entry.classification == ConflictClass::Independent),
1505
+ "{name}: a one-sided change cannot collide with anything"
1506
+ );
1507
+ assert_eq!(single.overall, ConflictClass::Independent, "{name}");
1508
+ }
1509
+
1510
+ // Both sides landing on the same state: every path converges, and the
1511
+ // overall class is Convergent unless nothing changed at all.
1512
+ for state in [&left, &right] {
1513
+ let agreeing = classify(&base, state, state);
1514
+ assert!(
1515
+ agreeing
1516
+ .path_conflicts
1517
+ .iter()
1518
+ .all(|entry| entry.classification == ConflictClass::Convergent),
1519
+ "{name}: agreeing branches converge at every path"
1520
+ );
1521
+ let expected = if agreeing.path_conflicts.is_empty() {
1522
+ ConflictClass::Independent
1523
+ } else {
1524
+ ConflictClass::Convergent
1525
+ };
1526
+ assert_eq!(agreeing.overall, expected, "{name}");
1527
+ }
1528
+
1529
+ // `overall` is the maximum severity present, never anything else.
1530
+ let full = classify(&base, &left, &right);
1531
+ let expected = if full
1532
+ .path_conflicts
1533
+ .iter()
1534
+ .any(|entry| entry.classification == ConflictClass::Conflict)
1535
+ {
1536
+ ConflictClass::Conflict
1537
+ } else if full
1538
+ .path_conflicts
1539
+ .iter()
1540
+ .any(|entry| entry.classification == ConflictClass::Convergent)
1541
+ {
1542
+ ConflictClass::Convergent
1543
+ } else {
1544
+ ConflictClass::Independent
1545
+ };
1546
+ assert_eq!(
1547
+ full.overall, expected,
1548
+ "{name}: overall must be the maximum severity present"
1549
+ );
1550
+
1551
+ // Paths are unique and sorted, which is what makes the ordering total.
1552
+ let paths: Vec<_> = full
1553
+ .path_conflicts
1554
+ .iter()
1555
+ .map(|entry| entry.path.clone())
1556
+ .collect();
1557
+ let mut sorted = paths.clone();
1558
+ sorted.sort();
1559
+ assert_eq!(paths, sorted, "{name}: paths are emitted in sorted order");
1560
+ sorted.dedup();
1561
+ assert_eq!(paths.len(), sorted.len(), "{name}: paths are unique");
1562
+ }
1563
+ }
1564
+
1565
+ /// At a path both branches touched, the change kinds match or the new values
1566
+ /// already differ.
1567
+ ///
1568
+ /// This is why `one.kind == two.kind` can never decide convergence, and so why
1569
+ /// relabelling a change kind is invisible to this contract. Both diffs are taken
1570
+ /// from the same base, so whether a path resolves in the base — which is what
1571
+ /// separates `Added` from `Changed` and `Removed` — is a property of the base
1572
+ /// alone and cannot differ between them. The remaining pair, `Removed` against
1573
+ /// `Changed`, always disagrees on the new value, because a removal has none.
1574
+ ///
1575
+ /// Asserted over the corpus rather than argued alone: if the diff ever produces
1576
+ /// a counterexample, the kind comparison becomes load-bearing and the mutation
1577
+ /// expectations recorded above must change with it.
1578
+ #[test]
1579
+ fn the_change_kind_comparison_is_never_decisive() {
1580
+ use feltdb::state_model::SemanticDiff;
1581
+
1582
+ let mut both_touched = 0;
1583
+ for (name, base, left, right) in triples() {
1584
+ let base_value: Value = serde_json::from_str(&base).unwrap_or(Value::Null);
1585
+ let left_value: Value = serde_json::from_str(&left).unwrap_or(Value::Null);
1586
+ let right_value: Value = serde_json::from_str(&right).unwrap_or(Value::Null);
1587
+ let left_diff = SemanticDiff::compute(&base_value, &left_value);
1588
+ let right_diff = SemanticDiff::compute(&base_value, &right_value);
1589
+
1590
+ for one in &left_diff.changes {
1591
+ let Some(two) = right_diff
1592
+ .changes
1593
+ .iter()
1594
+ .find(|other| other.path == one.path)
1595
+ else {
1596
+ continue;
1597
+ };
1598
+ both_touched += 1;
1599
+ assert!(
1600
+ one.kind == two.kind || one.new_value != two.new_value,
1601
+ "{name}: the kind comparison decided the outcome at {:?}, which the contract \
1602
+ documentation says cannot happen",
1603
+ one.path
1604
+ );
1605
+ }
1606
+ }
1607
+ assert!(
1608
+ both_touched > 30,
1609
+ "the corpus must actually exercise paths both branches touched"
1610
+ );
1611
+ println!("{both_touched} paths were touched by both branches");
1612
+ }
1613
+
1614
+ /// Swapping the branches has the documented relationship.
1615
+ ///
1616
+ /// `overall`, the path set and each path's `classification` are symmetric, and
1617
+ /// `left_value` and `right_value` exchange. `base_value` is now **also**
1618
+ /// symmetric, because it is read from the base state rather than from the left
1619
+ /// branch's change: version 1 reported it as `null` for a path only the right
1620
+ /// branch touched, which made the result side-dependent in a way nothing
1621
+ /// wanted. The only side-specific information left is the branch values, which
1622
+ /// is the information that ought to be side-specific.
1623
+ #[test]
1624
+ fn swapping_the_branches_exchanges_only_the_branch_values() {
1625
+ let mut exchanged = 0;
1626
+ for (name, base, left, right) in triples() {
1627
+ let forward = classify(&base, &left, &right);
1628
+ let reversed = classify(&base, &right, &left);
1629
+
1630
+ assert_eq!(
1631
+ forward.overall, reversed.overall,
1632
+ "{name}: the overall class is symmetric"
1633
+ );
1634
+ assert_eq!(
1635
+ forward.path_conflicts.len(),
1636
+ reversed.path_conflicts.len(),
1637
+ "{name}: the same paths are reported either way round"
1638
+ );
1639
+ for (one, two) in forward.path_conflicts.iter().zip(&reversed.path_conflicts) {
1640
+ assert_eq!(one.path, two.path, "{name}: paths are symmetric");
1641
+ assert_eq!(
1642
+ one.classification, two.classification,
1643
+ "{name}: per-path classification is symmetric"
1644
+ );
1645
+ assert_eq!(
1646
+ one.base_value, two.base_value,
1647
+ "{name}: base_value is the ancestor's value, so it does not depend on which \
1648
+ branch is called left"
1649
+ );
1650
+ assert_eq!(
1651
+ one.left_value, two.right_value,
1652
+ "{name}: the branches exchange sides"
1653
+ );
1654
+ assert_eq!(
1655
+ one.right_value, two.left_value,
1656
+ "{name}: the branches exchange sides"
1657
+ );
1658
+ if one.left_value != one.right_value {
1659
+ exchanged += 1;
1660
+ }
1661
+ }
1662
+ }
1663
+ assert!(
1664
+ exchanged > 40,
1665
+ "the corpus must exercise paths where the two branches hold different values, or the \
1666
+ exchange assertion above proves nothing"
1667
+ );
1668
+ println!("{exchanged} paths held different values on the two branches");
1669
+ }
1670
+
1671
+ /// An overlapping pair can never be convergent, and the corpus says so.
1672
+ ///
1673
+ /// For a descendant path to exist the base must hold a container there. A change
1674
+ /// reported *at* the ancestor path means that branch no longer holds a container
1675
+ /// of that kind, while the branch editing inside it still does — so the two
1676
+ /// branches' values at the ancestor path always differ. This asserts the
1677
+ /// premise directly rather than trusting the argument.
1678
+ #[test]
1679
+ fn an_overlapping_pair_never_agrees_at_the_ancestor_path() {
1680
+ use feltdb::state_model::{path_relation, resolve_path, PathRelation, SemanticDiff};
1681
+
1682
+ let mut overlaps = 0;
1683
+ for (name, base, left, right) in triples() {
1684
+ let base_value: Value = serde_json::from_str(&base).unwrap_or(Value::Null);
1685
+ let left_value: Value = serde_json::from_str(&left).unwrap_or(Value::Null);
1686
+ let right_value: Value = serde_json::from_str(&right).unwrap_or(Value::Null);
1687
+ let left_diff = SemanticDiff::compute(&base_value, &left_value);
1688
+ let right_diff = SemanticDiff::compute(&base_value, &right_value);
1689
+
1690
+ for one in &left_diff.changes {
1691
+ for two in &right_diff.changes {
1692
+ let ancestor = match path_relation(&one.path, &two.path) {
1693
+ PathRelation::Ancestor => &one.path,
1694
+ PathRelation::Descendant => &two.path,
1695
+ _ => continue,
1696
+ };
1697
+ overlaps += 1;
1698
+ assert_ne!(
1699
+ resolve_path(&left_value, ancestor),
1700
+ resolve_path(&right_value, ancestor),
1701
+ "{name}: the branches agree at an overlapping ancestor path {ancestor:?}, \
1702
+ which the contract documentation says cannot happen"
1703
+ );
1704
+ }
1705
+ }
1706
+ }
1707
+ assert!(
1708
+ overlaps > 20,
1709
+ "the corpus must actually exercise overlapping path pairs"
1710
+ );
1711
+ println!("{overlaps} overlapping path pairs were exercised");
1712
+ }
1713
+
1714
+ /// Every entry's `base_value` is the base state's value at that path.
1715
+ ///
1716
+ /// Version 1 read it from the left branch's change, so a path only the right
1717
+ /// branch touched reported `null` even where the base held a value. Downstream
1718
+ /// reconciliation needs the ancestor's value at a conflicting path to decide
1719
+ /// anything, so this is corrected rather than documented as a limitation.
1720
+ #[test]
1721
+ fn base_value_is_the_base_states_value_at_the_path() {
1722
+ use feltdb::state_model::resolve_path;
1723
+
1724
+ let mut right_only_with_a_base_value = 0;
1725
+ for (name, base, left, right) in triples() {
1726
+ let base_value: Value = serde_json::from_str(&base).unwrap_or(Value::Null);
1727
+ let output = classify(&base, &left, &right);
1728
+ for entry in &output.path_conflicts {
1729
+ assert_eq!(
1730
+ entry.base_value.as_ref(),
1731
+ resolve_path(&base_value, &entry.path),
1732
+ "{name}: base_value must be the base state's value at {:?}",
1733
+ entry.path
1734
+ );
1735
+ if entry.left_value.is_none()
1736
+ && entry.right_value.is_some()
1737
+ && entry.base_value.is_some()
1738
+ {
1739
+ right_only_with_a_base_value += 1;
1740
+ }
1741
+ }
1742
+ }
1743
+ assert!(
1744
+ right_only_with_a_base_value > 0,
1745
+ "the corpus must exercise a right-only change over a base value, which is exactly the \
1746
+ case version 1 reported as null"
1747
+ );
1748
+ println!("{right_only_with_a_base_value} right-only entries carry a base value version 1 lost");
1749
+ }
1750
+
1751
+ // ---------------------------------------------------------------------------
1752
+ // Corpus maintenance
1753
+ // ---------------------------------------------------------------------------
1754
+
1755
+ /// Records the canonical output of every case, after both implementations agree.
1756
+ ///
1757
+ /// Ignored by default: it writes the corpus. Run it deliberately with
1758
+ /// `FELTDB_STATE_CONFLICT_CONTRACT_BLESS=1 cargo test -p feltdb --test
1759
+ /// state_conflict_contract_conformance -- bless --ignored`. It refuses to record
1760
+ /// anything unless the WASM implementation is available and agrees, so an
1761
+ /// expectation can never be blessed from one implementation alone.
1762
+ #[test]
1763
+ #[ignore = "rewrites the committed corpus"]
1764
+ fn bless() {
1765
+ assert_eq!(
1766
+ std::env::var("FELTDB_STATE_CONFLICT_CONTRACT_BLESS").as_deref(),
1767
+ Ok("1"),
1768
+ "set FELTDB_STATE_CONFLICT_CONTRACT_BLESS=1 to rewrite the corpus"
1769
+ );
1770
+ let bytes = contract_module().expect(
1771
+ "blessing requires the WASM implementation: an expectation recorded from the native \
1772
+ implementation alone would prove nothing",
1773
+ );
1774
+ let mut wasm = WasmContract::instantiate(bytes);
1775
+
1776
+ let mut document: Value =
1777
+ serde_json::from_slice(&std::fs::read(corpus_path()).unwrap()).unwrap();
1778
+ for (index, case) in corpus().cases.iter().enumerate() {
1779
+ let input = case.bytes();
1780
+ let native = evaluate_canonical(&input);
1781
+ let produced = wasm.evaluate(&input);
1782
+ assert_eq!(
1783
+ String::from_utf8_lossy(&native),
1784
+ String::from_utf8_lossy(&produced),
1785
+ "refusing to record {}: the implementations disagree",
1786
+ case.name
1787
+ );
1788
+ if case.input.is_some() {
1789
+ document["cases"][index]["canonical_input"] =
1790
+ Value::String(String::from_utf8(input).unwrap());
1791
+ }
1792
+ document["cases"][index]["expected_output"] =
1793
+ Value::String(String::from_utf8(native).unwrap());
1794
+ }
1795
+ let mut text = serde_json::to_string_pretty(&document).unwrap();
1796
+ text.push('\n');
1797
+ std::fs::write(corpus_path(), text).unwrap();
1798
+ println!("recorded {} expectations", corpus().cases.len());
1799
+ }