@feltdb/core 0.15.0 → 0.16.0

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 (247) 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 +3 -0
  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 +255 -12
  17. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  18. package/dist/db.d.ts +11 -1
  19. package/dist/db.d.ts.map +1 -1
  20. package/dist/db.js +15 -2
  21. package/dist/error-codes.d.ts +5 -0
  22. package/dist/error-codes.d.ts.map +1 -1
  23. package/dist/error-codes.js +9 -1
  24. package/dist/http-db.d.ts +16 -0
  25. package/dist/http-db.d.ts.map +1 -1
  26. package/dist/http-db.js +36 -6
  27. package/dist/managed-recovery.d.ts +6 -0
  28. package/dist/managed-recovery.d.ts.map +1 -1
  29. package/dist/managed-recovery.js +9 -0
  30. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-DtybUY0q.js} +1 -1
  31. package/dist/studio-app/assets/feltdb_wasm_bg-DsgIUwaB.wasm +0 -0
  32. package/dist/studio-app/assets/index-C4OffsZl.js +37 -0
  33. package/dist/studio-app/index.html +1 -1
  34. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  35. package/dist/wasm/package.json +1 -1
  36. package/dist/workload.d.ts +55 -3
  37. package/dist/workload.d.ts.map +1 -1
  38. package/dist/workload.js +23 -0
  39. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  40. package/dist/workspace/workspace-connection.js +14 -3
  41. package/package.json +1 -1
  42. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  43. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  44. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  45. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  46. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  47. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  49. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  51. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  61. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  63. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  64. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  65. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  66. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  67. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  68. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  69. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  70. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  71. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  72. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  73. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  74. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  75. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  76. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  77. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  80. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  83. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  84. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  86. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  87. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  89. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  90. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  91. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  92. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  93. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  94. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  95. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  96. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  97. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  98. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  99. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  100. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  101. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  102. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  103. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  105. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  106. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  107. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  108. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  109. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  110. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  111. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  112. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  113. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  114. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  115. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  116. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  117. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  118. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  119. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  120. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  121. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  122. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  123. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  124. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  125. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  126. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  127. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  128. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  129. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  130. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  131. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  132. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  133. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  134. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  135. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  136. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  137. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  138. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  139. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  140. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  141. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  142. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  143. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  144. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  145. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  146. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  147. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  148. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  149. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  150. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  151. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  152. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  153. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  154. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  155. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  156. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  157. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  158. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  159. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  160. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  161. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  162. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  163. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  164. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  165. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  166. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  167. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  168. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  169. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  170. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  171. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  172. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  173. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  174. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  175. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  176. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  177. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  178. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  179. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  180. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  181. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  182. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  183. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  184. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  185. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  186. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  187. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  188. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  189. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  190. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  191. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  192. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  193. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  194. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  195. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  196. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  197. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  198. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  199. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  200. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  204. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  206. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  207. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  208. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  209. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  210. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  211. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  212. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  213. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  220. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  221. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  222. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  223. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  225. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  226. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  227. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  228. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  229. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  230. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  231. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  232. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  235. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  236. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  237. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  238. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  239. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  240. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  241. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  242. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  243. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  244. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  245. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  246. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  247. 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
- }