@feltdb/core 0.15.0 → 0.16.1

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 (248) hide show
  1. package/dist/agent-state.d.ts +20 -0
  2. package/dist/agent-state.d.ts.map +1 -1
  3. package/dist/agent-state.js +36 -0
  4. package/dist/build-identity.json +12 -4
  5. package/dist/cli/commands.js +187 -4
  6. package/dist/cli/publish-engine.js +116 -16
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +20 -2
  9. package/dist/create/build-identity.json +12 -4
  10. package/dist/create/managed-account.js +93 -35
  11. package/dist/create/package-versions.js +1 -1
  12. package/dist/create/server-source/Cargo.lock +6 -509
  13. package/dist/create/server-source/Cargo.toml +2 -13
  14. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  15. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  16. package/dist/create/server-source/crates/feltdb-server/src/main.rs +356 -24
  17. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +10 -1
  18. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  19. package/dist/db.d.ts +11 -1
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +15 -2
  22. package/dist/error-codes.d.ts +5 -0
  23. package/dist/error-codes.d.ts.map +1 -1
  24. package/dist/error-codes.js +9 -1
  25. package/dist/http-db.d.ts +16 -0
  26. package/dist/http-db.d.ts.map +1 -1
  27. package/dist/http-db.js +36 -6
  28. package/dist/managed-recovery.d.ts +6 -0
  29. package/dist/managed-recovery.d.ts.map +1 -1
  30. package/dist/managed-recovery.js +9 -0
  31. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-BUIxCuTR.js} +1 -1
  32. package/dist/studio-app/assets/feltdb_wasm_bg-C3D6y6DK.wasm +0 -0
  33. package/dist/studio-app/assets/index-BhXYAhgS.js +37 -0
  34. package/dist/studio-app/index.html +1 -1
  35. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  36. package/dist/wasm/package.json +1 -1
  37. package/dist/workload.d.ts +55 -3
  38. package/dist/workload.d.ts.map +1 -1
  39. package/dist/workload.js +23 -0
  40. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  41. package/dist/workspace/workspace-connection.js +14 -3
  42. package/package.json +1 -1
  43. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  44. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  45. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  46. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  47. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  49. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  51. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  61. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  63. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  64. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  65. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  66. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  67. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  68. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  69. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  70. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  71. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  72. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  73. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  74. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  75. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  76. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  77. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  80. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  83. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  84. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  86. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  87. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  89. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  90. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  91. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  92. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  93. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  94. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  95. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  96. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  97. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  98. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  99. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  100. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  101. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  102. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  103. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  105. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  106. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  107. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  108. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  109. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  110. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  111. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  112. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  113. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  114. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  115. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  116. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  117. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  118. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  119. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  120. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  121. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  122. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  123. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  124. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  125. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  126. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  127. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  128. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  129. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  130. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  131. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  132. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  133. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  134. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  135. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  136. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  137. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  138. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  139. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  140. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  141. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  142. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  143. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  144. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  145. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  146. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  147. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  148. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  149. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  150. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  151. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  152. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  153. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  154. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  155. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  156. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  157. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  158. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  159. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  160. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  161. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  162. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  163. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  164. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  165. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  166. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  167. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  168. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  169. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  170. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  171. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  172. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  173. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  174. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  175. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  176. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  177. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  178. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  179. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  180. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  181. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  182. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  183. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  184. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  185. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  186. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  187. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  188. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  189. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  190. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  191. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  192. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  193. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  194. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  195. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  196. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  197. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  198. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  199. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  200. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  204. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  206. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  207. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  208. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  209. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  210. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  211. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  212. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  213. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  220. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  221. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  222. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  223. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  225. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  226. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  227. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  228. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  229. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  230. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  231. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  232. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  235. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  236. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  237. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  238. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  239. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  240. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  241. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  242. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  243. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  244. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  245. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  246. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  247. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  248. package/dist/studio-app/assets/index-1wd0uKua.js +0 -30
@@ -1,868 +0,0 @@
1
- //! Applying an explicit `ReconciliationPlan` to materialize a state.
2
- //!
3
- //! ```text
4
- //! Classification — which changes can coexist?
5
- //! Plan — what happens to the ones that cannot?
6
- //! Application — produce the resulting state from that decision.
7
- //! ```
8
- //!
9
- //! These tests establish the application semantics, that inputs are never
10
- //! mutated, that invalid plans are rejected rather than producing arbitrary
11
- //! state, and the whole causal path from three revisions to a materialized one.
12
- //!
13
- //! Nothing here advances a branch head, merges automatically, or reconciles
14
- //! anything FeltDB was not explicitly told to.
15
-
16
- use feltdb::state_model::{
17
- apply_reconciliation_plan, reconcile, ConflictClass, ConflictClassification, PathComponent,
18
- ReconciliationPlan, SemanticDiff, StateId, StateRevision, StateStore,
19
- RECONCILE_CONTENT_UNPARSEABLE, RECONCILE_OVERRIDE_PATHS_OVERLAP,
20
- RECONCILE_OVERRIDE_UNADDRESSABLE, RECONCILE_PLAN_INVALID, RECONCILE_REVISION_MISMATCH,
21
- };
22
- use serde_json::{json, Value};
23
-
24
- // ---------------------------------------------------------------------------
25
- // Helpers
26
- // ---------------------------------------------------------------------------
27
-
28
- /// A revision with fixed metadata, so tests compare states rather than clocks.
29
- fn revision(content: &str) -> StateRevision {
30
- StateRevision {
31
- id: StateId::compute(content),
32
- resource: "docs:1".into(),
33
- content: content.to_string(),
34
- content_id: StateId::compute(content),
35
- parent_id: None,
36
- sequence: 0,
37
- authority: "test".into(),
38
- timestamp_ms: 0,
39
- metadata: Default::default(),
40
- }
41
- }
42
-
43
- fn key(name: &str) -> PathComponent {
44
- PathComponent::Key(name.to_string())
45
- }
46
-
47
- const LEFT: bool = true;
48
- const RIGHT: bool = false;
49
-
50
- /// The three revisions a scenario compares, and a plan builder over them.
51
- struct Scenario {
52
- base: StateRevision,
53
- left: StateRevision,
54
- right: StateRevision,
55
- }
56
-
57
- impl Scenario {
58
- fn new(base: &str, left: &str, right: &str) -> Self {
59
- Self {
60
- base: revision(base),
61
- left: revision(left),
62
- right: revision(right),
63
- }
64
- }
65
-
66
- fn plan(&self, parent_choice: bool) -> ReconciliationPlan {
67
- ReconciliationPlan::new(
68
- self.left.id.clone(),
69
- self.right.id.clone(),
70
- self.base.id.clone(),
71
- parent_choice,
72
- )
73
- }
74
-
75
- fn apply(&self, plan: &ReconciliationPlan) -> Result<String, String> {
76
- apply_reconciliation_plan(&self.base, &self.left, &self.right, plan)
77
- }
78
-
79
- /// The applied result as a value, for comparing states rather than text.
80
- fn applied(&self, plan: &ReconciliationPlan) -> Value {
81
- serde_json::from_str(&self.apply(plan).expect("the plan applies")).unwrap()
82
- }
83
-
84
- fn classification(&self) -> ConflictClassification {
85
- ConflictClassification::classify(&self.base, &self.left, &self.right)
86
- }
87
- }
88
-
89
- /// The error code an application failed with.
90
- fn code(result: Result<String, String>) -> String {
91
- let message = result.expect_err("the plan must be rejected");
92
- message
93
- .split_once(':')
94
- .map(|(code, _)| code.to_string())
95
- .unwrap_or(message)
96
- }
97
-
98
- // ---------------------------------------------------------------------------
99
- // Parent choice
100
- // ---------------------------------------------------------------------------
101
-
102
- /// `parent_choice` selects the state to start from, and has exactly two values.
103
- ///
104
- /// The field is a `bool`, so there is no third choice and no "unknown parent
105
- /// choice" for application to reject — the type forecloses it. The ancestor is
106
- /// not selectable.
107
- #[test]
108
- fn parent_choice_selects_the_starting_state() {
109
- let scenario = Scenario::new(r#"{"a":1,"b":1}"#, r#"{"a":2,"b":1}"#, r#"{"a":1,"b":2}"#);
110
-
111
- assert_eq!(scenario.applied(&scenario.plan(LEFT)), json!({"a":2,"b":1}));
112
- assert_eq!(
113
- scenario.applied(&scenario.plan(RIGHT)),
114
- json!({"a":1,"b":2})
115
- );
116
-
117
- // With no overrides the result is exactly the selected branch's state, not
118
- // a merge of the two. Independent changes on the branch not selected are
119
- // not carried over: the plan did not ask for them.
120
- assert_ne!(
121
- scenario.applied(&scenario.plan(LEFT)),
122
- json!({"a":2,"b":2}),
123
- "application never merges on its own"
124
- );
125
- }
126
-
127
- // ---------------------------------------------------------------------------
128
- // Override semantics
129
- // ---------------------------------------------------------------------------
130
-
131
- #[test]
132
- fn an_override_replaces_a_value() {
133
- let scenario = Scenario::new(r#"{"a":1}"#, r#"{"a":2}"#, r#"{"a":3}"#);
134
- let plan = scenario.plan(LEFT).with_override(vec![key("a")], json!(9));
135
- assert_eq!(scenario.applied(&plan), json!({"a":9}));
136
- }
137
-
138
- #[test]
139
- fn an_override_may_add_a_member_to_an_existing_object() {
140
- // Assigning a new member creates no structure, so it is addressable. This
141
- // is how a plan takes a member the other branch added.
142
- let scenario = Scenario::new(r#"{"o":{}}"#, r#"{"o":{}}"#, r#"{"o":{"added":1}}"#);
143
- let plan = scenario
144
- .plan(LEFT)
145
- .with_override(vec![key("o"), key("added")], json!(1));
146
- assert_eq!(scenario.applied(&plan), json!({"o":{"added":1}}));
147
- }
148
-
149
- #[test]
150
- fn an_override_can_set_null_but_cannot_express_removal() {
151
- let scenario = Scenario::new(r#"{"a":1,"b":2}"#, r#"{"a":1,"b":2}"#, r#"{"a":1}"#);
152
-
153
- // Null is a value, and setting it is an ordinary override.
154
- let nulled = scenario
155
- .plan(LEFT)
156
- .with_override(vec![key("b")], Value::Null);
157
- assert_eq!(scenario.applied(&nulled), json!({"a":1,"b":null}));
158
-
159
- // `path_overrides` maps a path to a `Value`, and a `Value` cannot be
160
- // absent. Removing a member means overriding the container that holds it.
161
- let removed = scenario.plan(LEFT).with_override(vec![], json!({"a":1}));
162
- assert_eq!(scenario.applied(&removed), json!({"a":1}));
163
- }
164
-
165
- #[test]
166
- fn overrides_reach_nested_paths() {
167
- let scenario = Scenario::new(
168
- r#"{"o":{"m":{"deep":1}}}"#,
169
- r#"{"o":{"m":{"deep":2}}}"#,
170
- r#"{"o":{"m":{"deep":3}}}"#,
171
- );
172
- let plan = scenario
173
- .plan(LEFT)
174
- .with_override(vec![key("o"), key("m"), key("deep")], json!(9));
175
- assert_eq!(scenario.applied(&plan), json!({"o":{"m":{"deep":9}}}));
176
- }
177
-
178
- #[test]
179
- fn override_paths_are_structural_not_textual() {
180
- // `a.b` and `a.bc` share a string prefix and are different locations.
181
- let scenario = Scenario::new(
182
- r#"{"a":{"b":1,"bc":1}}"#,
183
- r#"{"a":{"b":2,"bc":1}}"#,
184
- r#"{"a":{"b":1,"bc":2}}"#,
185
- );
186
- let plan = scenario
187
- .plan(LEFT)
188
- .with_override(vec![key("a"), key("bc")], json!(9));
189
- assert_eq!(scenario.applied(&plan), json!({"a":{"b":2,"bc":9}}));
190
-
191
- // And a key named "0" is not the first element of an array.
192
- let mixed = Scenario::new(r#"{"x":{"0":1}}"#, r#"{"x":{"0":2}}"#, r#"{"x":{"0":3}}"#);
193
- let by_key = mixed
194
- .plan(LEFT)
195
- .with_override(vec![key("x"), key("0")], json!(9));
196
- assert_eq!(mixed.applied(&by_key), json!({"x":{"0":9}}));
197
- let by_index = mixed
198
- .plan(LEFT)
199
- .with_override(vec![key("x"), PathComponent::Index(0)], json!(9));
200
- assert_eq!(
201
- code(mixed.apply(&by_index)),
202
- RECONCILE_OVERRIDE_UNADDRESSABLE
203
- );
204
- }
205
-
206
- /// A repeated override path cannot exist: `path_overrides` is a map.
207
- #[test]
208
- fn a_repeated_override_path_is_impossible_by_construction() {
209
- let scenario = Scenario::new(r#"{"a":1}"#, r#"{"a":2}"#, r#"{"a":3}"#);
210
- let plan = scenario
211
- .plan(LEFT)
212
- .with_override(vec![key("a")], json!(8))
213
- .with_override(vec![key("a")], json!(9));
214
- assert_eq!(plan.path_overrides.len(), 1, "the map holds one entry");
215
- assert_eq!(scenario.applied(&plan), json!({"a":9}), "the later one");
216
- }
217
-
218
- // ---------------------------------------------------------------------------
219
- // Root path
220
- // ---------------------------------------------------------------------------
221
-
222
- #[test]
223
- fn the_root_path_replaces_the_whole_state() {
224
- let scenario = Scenario::new(r#"{"o":{"a":1}}"#, r#"{"o":7}"#, r#"{"o":{"a":2}}"#);
225
- for replacement in [
226
- json!({"whole":"object"}),
227
- json!([1, 2, 3]),
228
- json!("scalar"),
229
- json!(42),
230
- Value::Null,
231
- ] {
232
- let plan = scenario
233
- .plan(LEFT)
234
- .with_override(vec![], replacement.clone());
235
- assert_eq!(
236
- scenario.applied(&plan),
237
- replacement,
238
- "the empty path is the state itself"
239
- );
240
- }
241
- }
242
-
243
- /// A root override and any other override overlap, so the plan is ambiguous.
244
- #[test]
245
- fn a_root_override_excludes_every_other_override() {
246
- let scenario = Scenario::new(r#"{"a":1}"#, r#"{"a":2}"#, r#"{"a":3}"#);
247
- let plan = scenario
248
- .plan(LEFT)
249
- .with_override(vec![], json!({"a":8}))
250
- .with_override(vec![key("a")], json!(9));
251
- assert_eq!(
252
- code(scenario.apply(&plan)),
253
- RECONCILE_OVERRIDE_PATHS_OVERLAP
254
- );
255
- }
256
-
257
- // ---------------------------------------------------------------------------
258
- // Parent/child overrides
259
- // ---------------------------------------------------------------------------
260
-
261
- /// Overlapping override paths are rejected rather than given an invented
262
- /// precedence.
263
- ///
264
- /// The plan is a map from path to value with no stated ordering, so a plan
265
- /// holding both `o` and `o.a` does not say whether the inner override refines
266
- /// the outer value or the outer one was meant to stand. Rather than pick one,
267
- /// application refuses. Overlap is the same structural relation conflict
268
- /// classification uses.
269
- #[test]
270
- fn overlapping_override_paths_are_rejected() {
271
- let scenario = Scenario::new(r#"{"o":{"a":1}}"#, r#"{"o":{"a":2}}"#, r#"{"o":{"a":3}}"#);
272
- for (one, two) in [
273
- (vec![key("o")], vec![key("o"), key("a")]),
274
- (vec![key("o"), key("a")], vec![key("o")]),
275
- (vec![], vec![key("o")]),
276
- ] {
277
- let plan = scenario
278
- .plan(LEFT)
279
- .with_override(one, json!({"a": 8}))
280
- .with_override(two, json!(9));
281
- assert_eq!(
282
- code(scenario.apply(&plan)),
283
- RECONCILE_OVERRIDE_PATHS_OVERLAP
284
- );
285
- }
286
-
287
- // Disjoint paths under a shared, un-overridden parent are fine.
288
- let siblings = Scenario::new(
289
- r#"{"o":{"a":1,"b":1}}"#,
290
- r#"{"o":{"a":2,"b":1}}"#,
291
- r#"{"o":{"a":1,"b":2}}"#,
292
- );
293
- let plan = siblings
294
- .plan(LEFT)
295
- .with_override(vec![key("o"), key("a")], json!(8))
296
- .with_override(vec![key("o"), key("b")], json!(9));
297
- assert_eq!(siblings.applied(&plan), json!({"o":{"a":8,"b":9}}));
298
- }
299
-
300
- // ---------------------------------------------------------------------------
301
- // Arrays
302
- // ---------------------------------------------------------------------------
303
-
304
- #[test]
305
- fn array_overrides_are_positional() {
306
- let scenario = Scenario::new(
307
- r#"{"xs":[1,2,3]}"#,
308
- r#"{"xs":[9,2,3]}"#,
309
- r#"{"xs":[1,8,3]}"#,
310
- );
311
-
312
- // Replacing one element leaves the others alone.
313
- let replaced = scenario
314
- .plan(LEFT)
315
- .with_override(vec![key("xs"), PathComponent::Index(1)], json!(8));
316
- assert_eq!(scenario.applied(&replaced), json!({"xs":[9,8,3]}));
317
-
318
- // Two different indexes are disjoint paths, so both survive.
319
- let both = scenario
320
- .plan(LEFT)
321
- .with_override(vec![key("xs"), PathComponent::Index(0)], json!(7))
322
- .with_override(vec![key("xs"), PathComponent::Index(2)], json!(6));
323
- assert_eq!(scenario.applied(&both), json!({"xs":[7,2,6]}));
324
-
325
- // Length changes go through the array itself. There is no separate array
326
- // merge algorithm: the plan is authoritative.
327
- for (label, value) in [
328
- ("append", json!([1, 2, 3, 4])),
329
- ("remove", json!([1, 2])),
330
- ("prepend", json!([0, 1, 2, 3])),
331
- ("empty", json!([])),
332
- ("to an object", json!({"0":1})),
333
- ("to a scalar", json!("flat")),
334
- ] {
335
- let plan = scenario
336
- .plan(LEFT)
337
- .with_override(vec![key("xs")], value.clone());
338
- assert_eq!(
339
- scenario.applied(&plan),
340
- json!({ "xs": value }),
341
- "{label} is expressed by overriding the array"
342
- );
343
- }
344
- }
345
-
346
- #[test]
347
- fn an_index_past_the_end_is_rejected() {
348
- let scenario = Scenario::new(r#"{"xs":[1,2]}"#, r#"{"xs":[9,2]}"#, r#"{"xs":[1,8]}"#);
349
- for index in [2usize, 3, 99] {
350
- let plan = scenario
351
- .plan(LEFT)
352
- .with_override(vec![key("xs"), PathComponent::Index(index)], json!(0));
353
- assert_eq!(
354
- code(scenario.apply(&plan)),
355
- RECONCILE_OVERRIDE_UNADDRESSABLE,
356
- "index {index} would require inventing the elements before it"
357
- );
358
- }
359
- }
360
-
361
- #[test]
362
- fn overrides_reach_inside_array_elements() {
363
- let scenario = Scenario::new(
364
- r#"{"xs":[{"a":1},{"b":[1]}]}"#,
365
- r#"{"xs":[{"a":2},{"b":[1]}]}"#,
366
- r#"{"xs":[{"a":1},{"b":[2]}]}"#,
367
- );
368
- let plan = scenario.plan(LEFT).with_override(
369
- vec![
370
- key("xs"),
371
- PathComponent::Index(1),
372
- key("b"),
373
- PathComponent::Index(0),
374
- ],
375
- json!(9),
376
- );
377
- assert_eq!(scenario.applied(&plan), json!({"xs":[{"a":2},{"b":[9]}]}));
378
- }
379
-
380
- // ---------------------------------------------------------------------------
381
- // Canonical output
382
- // ---------------------------------------------------------------------------
383
-
384
- /// The result is canonical, whatever the selected parent's content looked like.
385
- ///
386
- /// Content identity is the hash of the content string, so two revisions holding
387
- /// the same state with differently ordered members are different revisions.
388
- /// Applying an equivalent plan to either produces the *same* canonical content,
389
- /// and therefore the same identity.
390
- #[test]
391
- fn the_result_is_canonical_regardless_of_the_parents_representation() {
392
- let sorted = Scenario::new(r#"{"a":1,"b":1}"#, r#"{"a":2,"b":1}"#, r#"{"a":1,"b":2}"#);
393
- let unsorted = Scenario::new(r#"{"b":1,"a":1}"#, r#"{"b":1,"a":2}"#, r#"{"a":1,"b":2}"#);
394
-
395
- assert_ne!(
396
- sorted.left.id, unsorted.left.id,
397
- "differently ordered content is a different revision"
398
- );
399
-
400
- let one = sorted.apply(&sorted.plan(LEFT)).unwrap();
401
- let two = unsorted.apply(&unsorted.plan(LEFT)).unwrap();
402
- assert_eq!(one, two, "both applications produce canonical content");
403
- assert_eq!(one, r#"{"a":2,"b":1}"#);
404
- assert_eq!(
405
- StateId::compute(&one),
406
- StateId::compute(&two),
407
- "so they have the same content identity"
408
- );
409
- }
410
-
411
- /// Repeated application produces the same bytes, and the result is a fixed
412
- /// point of the diff.
413
- #[test]
414
- fn application_is_repeatable_and_the_result_is_a_fixed_point() {
415
- let scenario = Scenario::new(
416
- r#"{"o":{"a":1},"xs":[1,2]}"#,
417
- r#"{"o":{"a":2},"xs":[9,2]}"#,
418
- r#"{"o":{"a":3},"xs":[1,8]}"#,
419
- );
420
- let plan = scenario
421
- .plan(LEFT)
422
- .with_override(vec![key("o"), key("a")], json!(5))
423
- .with_override(vec![key("xs"), PathComponent::Index(1)], json!(8));
424
-
425
- let first = scenario.apply(&plan).expect("applies");
426
- for _ in 0..8 {
427
- assert_eq!(scenario.apply(&plan).expect("applies"), first);
428
- }
429
-
430
- let result: Value = serde_json::from_str(&first).unwrap();
431
- assert!(
432
- SemanticDiff::compute(&result, &result).changes.is_empty(),
433
- "the result does not differ from itself"
434
- );
435
- }
436
-
437
- // ---------------------------------------------------------------------------
438
- // Immutability
439
- // ---------------------------------------------------------------------------
440
-
441
- /// Applying a plan mutates nothing it was given.
442
- #[test]
443
- fn application_mutates_none_of_its_inputs() {
444
- let scenario = Scenario::new(r#"{"o":{"a":1}}"#, r#"{"o":{"a":2}}"#, r#"{"o":{"a":3}}"#);
445
- let plan = scenario
446
- .plan(LEFT)
447
- .with_override(vec![key("o"), key("a")], json!(9));
448
-
449
- let before = (
450
- serde_json::to_string(&scenario.base).unwrap(),
451
- serde_json::to_string(&scenario.left).unwrap(),
452
- serde_json::to_string(&scenario.right).unwrap(),
453
- format!("{plan:?}"),
454
- );
455
-
456
- let result = reconcile(
457
- &scenario.base,
458
- &scenario.left,
459
- &scenario.right,
460
- &plan,
461
- "reconciler".into(),
462
- )
463
- .expect("applies");
464
-
465
- assert_eq!(serde_json::to_string(&scenario.base).unwrap(), before.0);
466
- assert_eq!(serde_json::to_string(&scenario.left).unwrap(), before.1);
467
- assert_eq!(serde_json::to_string(&scenario.right).unwrap(), before.2);
468
- assert_eq!(format!("{plan:?}"), before.3);
469
- assert_eq!(
470
- result.materialized_state.content, r#"{"o":{"a":9}}"#,
471
- "the result is newly materialized"
472
- );
473
- // And the result is a different revision from all three inputs.
474
- for existing in [&scenario.base, &scenario.left, &scenario.right] {
475
- assert_ne!(result.materialized_state.id, existing.id);
476
- }
477
- }
478
-
479
- // ---------------------------------------------------------------------------
480
- // Materialization
481
- // ---------------------------------------------------------------------------
482
-
483
- /// The materialized revision uses the existing identity and provenance model.
484
- #[test]
485
- fn the_materialized_revision_uses_the_existing_model() {
486
- let scenario = Scenario::new(r#"{"a":1}"#, r#"{"a":2}"#, r#"{"a":3}"#);
487
-
488
- for (choice, expected_parent) in [(LEFT, &scenario.left), (RIGHT, &scenario.right)] {
489
- let plan = scenario.plan(choice);
490
- let result = reconcile(
491
- &scenario.base,
492
- &scenario.left,
493
- &scenario.right,
494
- &plan,
495
- "reconciler".into(),
496
- )
497
- .expect("applies");
498
- let revision = &result.materialized_state;
499
-
500
- assert!(
501
- revision.verify_integrity(),
502
- "both identities match what they are computed from"
503
- );
504
- assert_eq!(
505
- revision.content_id,
506
- StateId::compute(&revision.content),
507
- "no new content-identity algorithm"
508
- );
509
- assert_ne!(
510
- revision.id, revision.content_id,
511
- "the revision identity names the occurrence, not the state"
512
- );
513
- assert_eq!(
514
- revision.parent_id.as_ref(),
515
- Some(&expected_parent.id),
516
- "the selected branch is the parent"
517
- );
518
- assert_eq!(revision.authority, "reconciler");
519
- // The plan travels with the result, so the decision stays attached to
520
- // the state it produced.
521
- assert_eq!(result.plan.parent_choice, choice);
522
- }
523
- }
524
-
525
- /// A materialized revision can be committed through the existing store, and
526
- /// nothing about branches moves.
527
- #[test]
528
- fn a_materialized_revision_commits_through_the_existing_store() {
529
- let store = StateStore::new_volatile();
530
- let base = store
531
- .create("docs:1", r#"{"a":1,"b":1}"#.to_string(), "auth".into())
532
- .expect("create");
533
- let left = store
534
- .commit(r#"{"a":2,"b":1}"#.to_string(), &base, "auth".into())
535
- .expect("left");
536
- let right = store
537
- .commit(r#"{"a":1,"b":2}"#.to_string(), &base, "auth".into())
538
- .expect("right");
539
-
540
- let plan = ReconciliationPlan::new(left.id.clone(), right.id.clone(), base.id.clone(), LEFT)
541
- .with_override(vec![key("b")], json!(2));
542
-
543
- let result = reconcile(&base, &left, &right, &plan, "reconciler".into()).expect("applies");
544
- let committed = store
545
- .commit(
546
- result.materialized_state.content.clone(),
547
- &left,
548
- "reconciler".into(),
549
- )
550
- .expect("commit");
551
-
552
- assert_eq!(committed.content, r#"{"a":2,"b":2}"#);
553
- assert_eq!(committed.id, result.materialized_state.id);
554
-
555
- // Applying a plan disturbed nothing it started from: both source revisions
556
- // are exactly as retrievable as before. There is no branch head to move,
557
- // because the store keeps none.
558
- assert_eq!(store.get(&left.id).unwrap().content, left.content);
559
- assert_eq!(store.get(&right.id).unwrap().content, right.content);
560
- }
561
-
562
- // ---------------------------------------------------------------------------
563
- // Rejection
564
- // ---------------------------------------------------------------------------
565
-
566
- #[test]
567
- fn an_invalid_plan_is_rejected_rather_than_producing_state() {
568
- let scenario = Scenario::new(r#"{"a":1}"#, r#"{"a":2}"#, r#"{"a":3}"#);
569
-
570
- // A plan whose ids are empty does not reference three revisions.
571
- let mut empty = scenario.plan(LEFT);
572
- empty.base_id = StateId::from_hex(String::new());
573
- assert_eq!(code(scenario.apply(&empty)), RECONCILE_PLAN_INVALID);
574
-
575
- // A plan that names revisions other than the ones supplied.
576
- let other = revision(r#"{"a":99}"#);
577
- for (base, left, right) in [
578
- (&other, &scenario.left, &scenario.right),
579
- (&scenario.base, &other, &scenario.right),
580
- (&scenario.base, &scenario.left, &other),
581
- ] {
582
- assert_eq!(
583
- code(apply_reconciliation_plan(
584
- base,
585
- left,
586
- right,
587
- &scenario.plan(LEFT)
588
- )),
589
- RECONCILE_REVISION_MISMATCH
590
- );
591
- }
592
-
593
- // Content that is not JSON. Classification reads such content as JSON null
594
- // because it only describes; materializing a state from it would be
595
- // producing arbitrary state, so this refuses.
596
- let broken = Scenario::new("{}", "not json", "{}");
597
- assert_eq!(
598
- code(broken.apply(&broken.plan(LEFT))),
599
- RECONCILE_CONTENT_UNPARSEABLE
600
- );
601
- // The branch that is not selected is not parsed, so it cannot fail the plan.
602
- assert_eq!(broken.applied(&broken.plan(RIGHT)), json!({}));
603
-
604
- // Override paths that name nothing assignable.
605
- let scenario = Scenario::new(r#"{"o":{"a":1}}"#, r#"{"o":7}"#, r#"{"o":{"a":2}}"#);
606
- for path in [
607
- vec![key("o"), key("a")], // `o` is a scalar in the left branch
608
- vec![key("missing"), key("deep")], // the prefix does not resolve
609
- vec![key("o"), PathComponent::Index(0)], // a scalar is not an array
610
- ] {
611
- let plan = scenario.plan(LEFT).with_override(path, json!(1));
612
- assert_eq!(
613
- code(scenario.apply(&plan)),
614
- RECONCILE_OVERRIDE_UNADDRESSABLE
615
- );
616
- }
617
- }
618
-
619
- /// Application does not require every conflict to have an override, and says so.
620
- ///
621
- /// `unresolved_conflicts` reports which conflicting paths a plan does not
622
- /// address, so a caller that wants each one decided individually can require it
623
- /// to be empty. Application itself does not: leaving a conflict unaddressed
624
- /// resolves it to the selected parent's value, and selecting that parent is
625
- /// itself an explicit decision.
626
- #[test]
627
- fn unresolved_conflicts_are_reported_but_not_required() {
628
- let scenario = Scenario::new(r#"{"a":1,"b":1}"#, r#"{"a":2,"b":2}"#, r#"{"a":3,"b":3}"#);
629
- let classification = scenario.classification();
630
- assert_eq!(classification.overall, ConflictClass::Conflict);
631
-
632
- let bare = scenario.plan(LEFT);
633
- let unresolved = bare.unresolved_conflicts(&classification);
634
- assert_eq!(unresolved.len(), 2, "neither conflict is addressed");
635
-
636
- // Applying anyway is allowed: the conflicts resolve to the chosen branch.
637
- assert_eq!(scenario.applied(&bare), json!({"a":2,"b":2}));
638
-
639
- let partly = scenario.plan(LEFT).with_override(vec![key("a")], json!(9));
640
- assert_eq!(partly.unresolved_conflicts(&classification).len(), 1);
641
-
642
- let fully = scenario
643
- .plan(LEFT)
644
- .with_override(vec![key("a")], json!(9))
645
- .with_override(vec![key("b")], json!(8));
646
- assert!(fully.unresolved_conflicts(&classification).is_empty());
647
- assert_eq!(scenario.applied(&fully), json!({"a":9,"b":8}));
648
- }
649
-
650
- // ---------------------------------------------------------------------------
651
- // The overlap regression from the corrected classifier
652
- // ---------------------------------------------------------------------------
653
-
654
- /// The ancestor/descendant case the corrected classifier now flags.
655
- ///
656
- /// ```text
657
- /// base: {"o":{"a":1}}
658
- /// left: {"o":7}
659
- /// right: {"o":{"a":2}}
660
- /// ```
661
- ///
662
- /// The two changes cannot both be applied. The classifier says so, and
663
- /// application does not quietly merge them into something that keeps both: the
664
- /// result is exactly the branch the plan chose, or exactly the override it
665
- /// supplied.
666
- #[test]
667
- fn an_overlapping_conflict_is_resolved_only_as_the_plan_says() {
668
- let scenario = Scenario::new(r#"{"o":{"a":1}}"#, r#"{"o":7}"#, r#"{"o":{"a":2}}"#);
669
-
670
- let classification = scenario.classification();
671
- assert_eq!(
672
- classification.overall,
673
- ConflictClass::Conflict,
674
- "the corrected classifier identifies the overlap"
675
- );
676
-
677
- // Choosing left takes the scalar, whole.
678
- assert_eq!(scenario.applied(&scenario.plan(LEFT)), json!({"o":7}));
679
- // Choosing right takes the object, whole.
680
- assert_eq!(
681
- scenario.applied(&scenario.plan(RIGHT)),
682
- json!({"o":{"a":2}})
683
- );
684
- // An explicit override takes exactly what it says.
685
- let explicit = scenario
686
- .plan(LEFT)
687
- .with_override(vec![key("o")], json!({"a":3}));
688
- assert_eq!(scenario.applied(&explicit), json!({"o":{"a":3}}));
689
-
690
- // Nothing produces a state carrying both the scalar and the member: these
691
- // structures cannot be combined, and application does not try.
692
- for plan in [scenario.plan(LEFT), scenario.plan(RIGHT), explicit] {
693
- let result = scenario.applied(&plan);
694
- let o = &result["o"];
695
- assert!(
696
- o.is_number() ^ o.is_object(),
697
- "the result is one shape or the other, never an invented combination"
698
- );
699
- }
700
- }
701
-
702
- // ---------------------------------------------------------------------------
703
- // End to end
704
- // ---------------------------------------------------------------------------
705
-
706
- /// The whole causal path, with independent changes and one real conflict.
707
- ///
708
- /// ```text
709
- /// base → left, right → SemanticDiff → ConflictClassification
710
- /// → explicit ReconciliationPlan → apply → result revision
711
- /// ```
712
- #[test]
713
- fn the_state_model_reconciles_an_explicit_decision_end_to_end() {
714
- let store = StateStore::new_volatile();
715
-
716
- // Two branches: each edits a different member independently, and both edit
717
- // `shared` differently, which is the conflict.
718
- let base = store
719
- .create(
720
- "docs:1",
721
- r#"{"only_left":1,"only_right":1,"shared":1,"untouched":"keep"}"#.to_string(),
722
- "auth".into(),
723
- )
724
- .expect("create");
725
- let left = store
726
- .commit(
727
- r#"{"only_left":2,"only_right":1,"shared":10,"untouched":"keep"}"#.to_string(),
728
- &base,
729
- "alice".into(),
730
- )
731
- .expect("left");
732
- let right = store
733
- .commit(
734
- r#"{"only_left":1,"only_right":2,"shared":20,"untouched":"keep"}"#.to_string(),
735
- &base,
736
- "bob".into(),
737
- )
738
- .expect("right");
739
-
740
- // The diffs are what classification is defined over.
741
- let base_value: Value = serde_json::from_str(&base.content).unwrap();
742
- let left_value: Value = serde_json::from_str(&left.content).unwrap();
743
- let right_value: Value = serde_json::from_str(&right.content).unwrap();
744
- assert_eq!(
745
- SemanticDiff::compute(&base_value, &left_value)
746
- .changes
747
- .len(),
748
- 2
749
- );
750
- assert_eq!(
751
- SemanticDiff::compute(&base_value, &right_value)
752
- .changes
753
- .len(),
754
- 2
755
- );
756
-
757
- // Classification separates what can coexist from what cannot.
758
- let classification = ConflictClassification::classify(&base, &left, &right);
759
- assert_eq!(classification.overall, ConflictClass::Conflict);
760
- let conflicting: Vec<_> = classification
761
- .path_conflicts
762
- .iter()
763
- .filter(|entry| entry.classification == ConflictClass::Conflict)
764
- .collect();
765
- assert_eq!(conflicting.len(), 1, "exactly one path conflicts");
766
- assert_eq!(conflicting[0].path, vec![key("shared")]);
767
- assert_eq!(conflicting[0].base_value, Some(json!(1)));
768
- assert_eq!(conflicting[0].left_value, Some(json!(10)));
769
- assert_eq!(conflicting[0].right_value, Some(json!(20)));
770
-
771
- // The caller decides: take the left branch, keep the right branch's
772
- // independent change, and resolve the conflict to a third value neither
773
- // branch proposed.
774
- let plan = ReconciliationPlan::new(left.id.clone(), right.id.clone(), base.id.clone(), LEFT)
775
- .with_override(vec![key("only_right")], json!(2))
776
- .with_override(vec![key("shared")], json!(99));
777
- assert!(
778
- plan.unresolved_conflicts(&classification).is_empty(),
779
- "every conflict is explicitly addressed"
780
- );
781
-
782
- let before = (
783
- base.content.clone(),
784
- left.content.clone(),
785
- right.content.clone(),
786
- );
787
- let result = reconcile(&base, &left, &right, &plan, "reconciler".into()).expect("applies");
788
- let reconciled: Value = serde_json::from_str(&result.materialized_state.content).unwrap();
789
-
790
- // 1. the independent change on the selected branch survives
791
- assert_eq!(reconciled["only_left"], json!(2));
792
- // 2. the independent change on the other branch survives, because the plan
793
- // asked for it
794
- assert_eq!(reconciled["only_right"], json!(2));
795
- // 3. the chosen resolution survives, and neither branch's value does
796
- assert_eq!(reconciled["shared"], json!(99));
797
- assert_ne!(reconciled["shared"], json!(10));
798
- assert_ne!(reconciled["shared"], json!(20));
799
- // 4. unrelated state is preserved
800
- assert_eq!(reconciled["untouched"], json!("keep"));
801
- // 5. the result is canonical
802
- assert_eq!(
803
- result.materialized_state.content,
804
- serde_json::to_string(&reconciled).unwrap()
805
- );
806
- assert!(result.materialized_state.verify_integrity());
807
- // 6. the inputs are unchanged
808
- assert_eq!(
809
- (
810
- base.content.clone(),
811
- left.content.clone(),
812
- right.content.clone()
813
- ),
814
- before
815
- );
816
-
817
- // The result is a revision like any other, committable through the store.
818
- let committed = store
819
- .commit(
820
- result.materialized_state.content.clone(),
821
- &left,
822
- "reconciler".into(),
823
- )
824
- .expect("commit");
825
- assert_eq!(committed.id, result.materialized_state.id);
826
- assert_eq!(committed.parent_id.as_ref(), Some(&left.id));
827
-
828
- // And the reconciled state has no remaining difference from itself.
829
- let final_value: Value = serde_json::from_str(&committed.content).unwrap();
830
- assert!(SemanticDiff::compute(&final_value, &reconciled)
831
- .changes
832
- .is_empty());
833
- }
834
-
835
- // ---------------------------------------------------------------------------
836
- // A recorded limitation of the plan type
837
- // ---------------------------------------------------------------------------
838
-
839
- /// A plan carrying overrides cannot be serialized to JSON.
840
- ///
841
- /// `path_overrides` is keyed by `Vec<PathComponent>`, and JSON object keys must
842
- /// be strings, so `serde_json` refuses the map the moment it holds an entry.
843
- /// The type derives `Serialize`, which makes this look supported; it is not.
844
- ///
845
- /// This is recorded, not fixed. A plan is presently constructed and applied
846
- /// in-process, which this PR is what establishes; nothing transmits one. Giving
847
- /// the path a string encoding would be inventing a second path syntax — exactly
848
- /// the flattened rendering the conflict work went out of its way to avoid —
849
- /// and it is not needed by any caller that exists. The test exists so that the
850
- /// day a caller does need to transmit a plan, the limitation is already stated
851
- /// rather than discovered.
852
- #[test]
853
- fn a_plan_with_overrides_is_not_json_serializable() {
854
- let scenario = Scenario::new(r#"{"a":1}"#, r#"{"a":2}"#, r#"{"a":3}"#);
855
-
856
- // Without overrides the plan serializes.
857
- let plain = scenario.plan(LEFT);
858
- assert!(serde_json::to_string(&plain).is_ok());
859
-
860
- // With one, it does not, and application is unaffected either way.
861
- let with_override = plain.clone().with_override(vec![key("a")], json!(9));
862
- let error = serde_json::to_string(&with_override).expect_err("non-string key");
863
- assert!(
864
- error.to_string().contains("key must be a string"),
865
- "unexpected error: {error}"
866
- );
867
- assert_eq!(scenario.apply(&with_override).unwrap(), r#"{"a":9}"#);
868
- }