lodedb 1.3.1__tar.gz → 2.0.0__tar.gz

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 (236) hide show
  1. {lodedb-1.3.1 → lodedb-2.0.0}/Cargo.toml +4 -2
  2. {lodedb-1.3.1 → lodedb-2.0.0}/PKG-INFO +126 -2
  3. {lodedb-1.3.1 → lodedb-2.0.0}/README.md +120 -1
  4. lodedb-2.0.0/crates/lodedb-cloud-core/Cargo.toml +41 -0
  5. lodedb-2.0.0/crates/lodedb-cloud-core/src/artifact_store.rs +127 -0
  6. lodedb-2.0.0/crates/lodedb-cloud-core/src/blob_layout.rs +119 -0
  7. lodedb-2.0.0/crates/lodedb-cloud-core/src/client_ops.rs +621 -0
  8. lodedb-2.0.0/crates/lodedb-cloud-core/src/digest.rs +46 -0
  9. lodedb-2.0.0/crates/lodedb-cloud-core/src/error.rs +133 -0
  10. lodedb-2.0.0/crates/lodedb-cloud-core/src/generation_inventory.rs +502 -0
  11. lodedb-2.0.0/crates/lodedb-cloud-core/src/lib.rs +83 -0
  12. lodedb-2.0.0/crates/lodedb-cloud-core/src/local_artifact_store.rs +190 -0
  13. lodedb-2.0.0/crates/lodedb-cloud-core/src/managed.rs +440 -0
  14. lodedb-2.0.0/crates/lodedb-cloud-core/src/manifest_transfer.rs +245 -0
  15. lodedb-2.0.0/crates/lodedb-cloud-core/src/object_artifact_store.rs +495 -0
  16. lodedb-2.0.0/crates/lodedb-cloud-core/src/paths.rs +153 -0
  17. lodedb-2.0.0/crates/lodedb-cloud-core/src/snapshot_identity.rs +72 -0
  18. lodedb-2.0.0/crates/lodedb-cloud-core/src/status.rs +143 -0
  19. lodedb-2.0.0/crates/lodedb-cloud-core/src/store_target.rs +58 -0
  20. lodedb-2.0.0/crates/lodedb-cloud-core/src/sync_plan.rs +225 -0
  21. lodedb-2.0.0/crates/lodedb-cloud-core/src/sync_state.rs +212 -0
  22. lodedb-2.0.0/crates/lodedb-cloud-core/src/transfer_policy.rs +81 -0
  23. lodedb-2.0.0/crates/lodedb-cloud-core/src/verify.rs +160 -0
  24. lodedb-2.0.0/crates/lodedb-cloud-core/tests/artifact_store.rs +236 -0
  25. lodedb-2.0.0/crates/lodedb-cloud-core/tests/client_ops.rs +105 -0
  26. lodedb-2.0.0/crates/lodedb-cloud-core/tests/common/mod.rs +276 -0
  27. lodedb-2.0.0/crates/lodedb-cloud-core/tests/generation_inventory.rs +413 -0
  28. lodedb-2.0.0/crates/lodedb-cloud-core/tests/managed.rs +268 -0
  29. lodedb-2.0.0/crates/lodedb-cloud-core/tests/manifest_transfer.rs +317 -0
  30. lodedb-2.0.0/crates/lodedb-cloud-core/tests/object_artifact_store.rs +285 -0
  31. lodedb-2.0.0/crates/lodedb-cloud-core/tests/restore.rs +283 -0
  32. lodedb-2.0.0/crates/lodedb-cloud-core/tests/snapshot_identity.rs +67 -0
  33. lodedb-2.0.0/crates/lodedb-cloud-core/tests/store_target.rs +35 -0
  34. lodedb-2.0.0/crates/lodedb-cloud-core/tests/sync_ops.rs +663 -0
  35. lodedb-2.0.0/crates/lodedb-cloud-core/tests/sync_plan.rs +273 -0
  36. lodedb-2.0.0/crates/lodedb-cloud-core/tests/sync_state.rs +125 -0
  37. lodedb-2.0.0/crates/lodedb-cloud-core/tests/verify_status.rs +183 -0
  38. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/write_generation_fixture.rs +1 -0
  39. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/write_wal_fixture.rs +1 -0
  40. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/engine/mod.rs +1758 -291
  41. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lib.rs +1 -1
  42. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/commit_manifest.rs +53 -9
  43. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/mod.rs +60 -1
  44. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/multivec_store.rs +1 -20
  45. lodedb-2.0.0/crates/lodedb-core/src/storage/tvvf_store.rs +2088 -0
  46. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/util.rs +88 -5
  47. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/wal.rs +220 -10
  48. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/types.rs +32 -1
  49. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/turbovec.rs +111 -10
  50. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/version.rs +6 -2
  51. lodedb-2.0.0/crates/lodedb-core/tests/block_skip_layout.rs +55 -0
  52. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/core_engine.rs +2025 -18
  53. lodedb-2.0.0/crates/lodedb-core/tests/operator_observability.rs +216 -0
  54. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/storage_fixtures.rs +7 -0
  55. lodedb-2.0.0/crates/lodedb-core/tests/turbovec_adapter.rs +191 -0
  56. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-gpu/src/lib.rs +4 -4
  57. lodedb-2.0.0/crates/lodedb-graph/Cargo.toml +28 -0
  58. lodedb-2.0.0/crates/lodedb-graph/src/error.rs +61 -0
  59. lodedb-2.0.0/crates/lodedb-graph/src/graph.rs +809 -0
  60. lodedb-2.0.0/crates/lodedb-graph/src/index.rs +883 -0
  61. lodedb-2.0.0/crates/lodedb-graph/src/lib.rs +51 -0
  62. lodedb-2.0.0/crates/lodedb-graph/src/model.rs +231 -0
  63. lodedb-2.0.0/crates/lodedb-graph/src/search.rs +278 -0
  64. lodedb-2.0.0/crates/lodedb-graph/src/temporal.rs +126 -0
  65. lodedb-2.0.0/crates/lodedb-graph/src/topology.rs +1372 -0
  66. lodedb-2.0.0/crates/lodedb-graph/tests/temporal_graph.rs +608 -0
  67. {lodedb-1.3.1 → lodedb-2.0.0}/pyproject.toml +19 -2
  68. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/__init__.py +4 -2
  69. lodedb-2.0.0/src/lodedb/_native_core.py +77 -0
  70. lodedb-2.0.0/src/lodedb/cloud/__init__.py +113 -0
  71. lodedb-2.0.0/src/lodedb/cloud/_agents_scaffold.py +204 -0
  72. lodedb-2.0.0/src/lodedb/cloud/_config.py +159 -0
  73. lodedb-2.0.0/src/lodedb/cloud/_env_file.py +120 -0
  74. lodedb-2.0.0/src/lodedb/cloud/_sealing.py +60 -0
  75. lodedb-2.0.0/src/lodedb/cloud/cli.py +1845 -0
  76. lodedb-2.0.0/src/lodedb/cloud/client.py +447 -0
  77. lodedb-2.0.0/src/lodedb/cloud/serving.py +972 -0
  78. lodedb-2.0.0/src/lodedb/cloud/transfer.py +1086 -0
  79. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_atomic_io.py +2 -2
  80. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_commit_manifest.py +6 -6
  81. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_filelock.py +4 -4
  82. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_lexical.py +3 -3
  83. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/_predicate.py +4 -4
  84. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/embedding_backends.py +63 -34
  85. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/gpu_turbovec.py +3 -3
  86. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/native_adapter.py +42 -0
  87. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/route_registry.py +1 -1
  88. lodedb-2.0.0/src/lodedb/graph/__init__.py +27 -0
  89. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/graph/_store.py +2 -2
  90. lodedb-2.0.0/src/lodedb/graph/temporal.py +362 -0
  91. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/__init__.py +7 -5
  92. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/backends.py +1 -1
  93. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/benchmark.py +2 -2
  94. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/cli.py +46 -4
  95. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/db.py +518 -63
  96. lodedb-2.0.0/src/lodedb/local/doctor.py +729 -0
  97. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/__init__.py +14 -9
  98. lodedb-2.0.0/src/lodedb/local/integrations/kotaemon.py +604 -0
  99. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/langchain.py +2 -2
  100. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/llama_index.py +1 -1
  101. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/llama_index_graph.py +3 -3
  102. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/privategpt.py +5 -5
  103. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/mcp_install.py +5 -5
  104. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/mcp_server.py +2 -2
  105. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/detect.py +4 -4
  106. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/report.py +2 -2
  107. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/runner.py +1 -1
  108. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/__init__.py +4 -4
  109. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/base.py +2 -2
  110. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/onnx_artifacts.py +3 -3
  111. lodedb-2.0.0/src/lodedb/local/segments.py +187 -0
  112. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/server.py +52 -1
  113. lodedb-2.0.0/third_party/turbovec/Cargo.lock +3095 -0
  114. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/Cargo.toml +1 -1
  115. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/error.rs +31 -0
  116. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/id_map.rs +40 -1
  117. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/lib.rs +64 -1
  118. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/id_map.rs +133 -0
  119. lodedb-2.0.0/third_party/turbovec/turbovec-python/Cargo.toml +29 -0
  120. lodedb-2.0.0/third_party/turbovec/turbovec-python/src/cloud.rs +455 -0
  121. lodedb-2.0.0/third_party/turbovec/turbovec-python/src/graph.rs +510 -0
  122. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/src/lib.rs +342 -80
  123. lodedb-1.3.1/crates/lodedb-core/tests/turbovec_adapter.rs +0 -90
  124. lodedb-1.3.1/src/lodedb/_native_core.py +0 -35
  125. lodedb-1.3.1/src/lodedb/graph/__init__.py +0 -13
  126. lodedb-1.3.1/src/lodedb/local/doctor.py +0 -298
  127. lodedb-1.3.1/third_party/turbovec/Cargo.lock +0 -1455
  128. lodedb-1.3.1/third_party/turbovec/turbovec-python/Cargo.toml +0 -21
  129. {lodedb-1.3.1 → lodedb-2.0.0}/LICENSE +0 -0
  130. {lodedb-1.3.1 → lodedb-2.0.0}/NOTICE +0 -0
  131. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/Cargo.toml +0 -0
  132. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/filter_planner_bench.rs +0 -0
  133. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/examples/turbovec_adapter_smoke.rs +0 -0
  134. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/error.rs +0 -0
  135. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/ast.rs +0 -0
  136. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/doc_set.rs +0 -0
  137. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/field_index.rs +0 -0
  138. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/mod.rs +0 -0
  139. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/predicate.rs +0 -0
  140. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/resolve.rs +0 -0
  141. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/filter/validate.rs +0 -0
  142. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/bm25.rs +0 -0
  143. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/mod.rs +0 -0
  144. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/rrf.rs +0 -0
  145. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/lexical/tokenize.rs +0 -0
  146. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/legacy.rs +0 -0
  147. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/lexical_store.rs +0 -0
  148. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/lsn.rs +0 -0
  149. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/state_journal.rs +0 -0
  150. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/text_store.rs +0 -0
  151. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/tvann_store.rs +0 -0
  152. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/storage/tvim_delta.rs +0 -0
  153. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/text/chunk.rs +0 -0
  154. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/text/hash.rs +0 -0
  155. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/text/mod.rs +0 -0
  156. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/ann.rs +0 -0
  157. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/index.rs +0 -0
  158. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/math.rs +0 -0
  159. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/mod.rs +0 -0
  160. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/src/vector/stable_id.rs +0 -0
  161. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/ann_build_bench.rs +0 -0
  162. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/filter_plan_fixtures.rs +0 -0
  163. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/identity_fixtures.rs +0 -0
  164. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/lexical_fixtures.rs +0 -0
  165. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-core/tests/predicate_fixtures.rs +0 -0
  166. {lodedb-1.3.1 → lodedb-2.0.0}/crates/lodedb-gpu/Cargo.toml +0 -0
  167. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/__main__.py +0 -0
  168. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/config.py +0 -0
  169. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/__init__.py +0 -0
  170. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/core.py +0 -0
  171. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/route_profiles.py +0 -0
  172. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/runtime_policy.py +0 -0
  173. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/state_journal_store.py +0 -0
  174. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/turbovec_delta_store.py +0 -0
  175. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/engine/turbovec_index.py +0 -0
  176. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/graph/knowledge_graph.py +0 -0
  177. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/appender.py +0 -0
  178. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/checkpointer.py +0 -0
  179. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/collection.py +0 -0
  180. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/cognee.py +0 -0
  181. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/integrations/mem0.py +0 -0
  182. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/late_interaction.py +0 -0
  183. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/__init__.py +0 -0
  184. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/cli.py +0 -0
  185. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/plan.py +0 -0
  186. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/langchain_inmemory.py +0 -0
  187. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/llama_index_simple.py +0 -0
  188. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/mem0_qdrant.py +0 -0
  189. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/migrate/sources/pgvector.py +0 -0
  190. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/local/presets.py +0 -0
  191. {lodedb-1.3.1 → lodedb-2.0.0}/src/lodedb/py.typed +0 -0
  192. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/Cargo.toml +0 -0
  193. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/README.md +0 -0
  194. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/build.rs +0 -0
  195. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/examples/dump_state.rs +0 -0
  196. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/examples/kernel_xtest.rs +0 -0
  197. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/codebook.rs +0 -0
  198. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/encode.rs +0 -0
  199. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/io.rs +0 -0
  200. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/maxsim.rs +0 -0
  201. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/pack.rs +0 -0
  202. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/rotation.rs +0 -0
  203. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/src/search.rs +0 -0
  204. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/codebook.rs +0 -0
  205. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/concurrent_search.rs +0 -0
  206. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/distortion.rs +0 -0
  207. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/encode.rs +0 -0
  208. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/encoded_rows.rs +0 -0
  209. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/filtering.rs +0 -0
  210. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/input_validation.rs +0 -0
  211. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/io_versioning.rs +0 -0
  212. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/kernel_correctness.rs +0 -0
  213. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/lazy_init.rs +0 -0
  214. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/reconstruction.rs +0 -0
  215. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/rotation.rs +0 -0
  216. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/state_sequences.rs +0 -0
  217. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/swap_remove.rs +0 -0
  218. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec/tests/tqplus_calibration.rs +0 -0
  219. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/README.md +0 -0
  220. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/build.rs +0 -0
  221. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/__init__.py +0 -0
  222. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/_dedup.py +0 -0
  223. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/_persist.py +0 -0
  224. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/agno.py +0 -0
  225. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/haystack.py +0 -0
  226. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/langchain.py +0 -0
  227. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/python/turbovec/llama_index.py +0 -0
  228. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_agno.py +0 -0
  229. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_dedup.py +0 -0
  230. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_filtering.py +0 -0
  231. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_haystack.py +0 -0
  232. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_id_map.py +0 -0
  233. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_index.py +0 -0
  234. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_langchain.py +0 -0
  235. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_llama_index.py +0 -0
  236. {lodedb-1.3.1 → lodedb-2.0.0}/third_party/turbovec/turbovec-python/tests/test_security.py +0 -0
@@ -1,15 +1,17 @@
1
1
  [workspace]
2
2
  members = [
3
+ "crates/lodedb-cloud-core",
3
4
  "crates/lodedb-core",
4
5
  "crates/lodedb-ffi",
5
6
  "crates/lodedb-gpu",
7
+ "crates/lodedb-graph",
6
8
  ]
7
9
  resolver = "2"
8
10
 
9
11
  [workspace.package]
10
- version = "1.3.0"
12
+ version = "2.0.0"
11
13
  edition = "2021"
12
- rust-version = "1.70"
14
+ rust-version = "1.88"
13
15
  license = "Apache-2.0"
14
16
  repository = "https://github.com/Egoist-Machines/LodeDB"
15
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lodedb
3
- Version: 1.3.1
3
+ Version: 2.0.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -10,6 +10,9 @@ Requires-Dist: numpy>=2.0.0,<3
10
10
  Requires-Dist: typer>=0.12.0
11
11
  Requires-Dist: pyyaml>=6.0.0
12
12
  Requires-Dist: lodedb[embeddings,torch,image,mcp,langchain,llama-index,mem0] ; extra == 'all'
13
+ Requires-Dist: httpx>=0.27 ; extra == 'cloud'
14
+ Requires-Dist: pynacl>=1.5 ; extra == 'cloud'
15
+ Requires-Dist: cryptography>=47 ; extra == 'cloud-sealed'
13
16
  Requires-Dist: cognee>=1.1.0,<2 ; python_full_version < '3.15' and extra == 'cognee'
14
17
  Requires-Dist: pytest>=8.3.0 ; extra == 'dev'
15
18
  Requires-Dist: ruff==0.15.17 ; extra == 'dev'
@@ -25,6 +28,8 @@ Requires-Dist: mem0ai>=2.0.0 ; extra == 'mem0'
25
28
  Requires-Dist: optimum[exporters]>=1.23.0,<2.0.0 ; extra == 'onnx-export'
26
29
  Requires-Dist: sentence-transformers>=3.0.0,<5 ; extra == 'torch'
27
30
  Provides-Extra: all
31
+ Provides-Extra: cloud
32
+ Provides-Extra: cloud-sealed
28
33
  Provides-Extra: cognee
29
34
  Provides-Extra: dev
30
35
  Provides-Extra: embeddings
@@ -388,6 +393,42 @@ db.search("turbine fault", k=5) # nearest cluste
388
393
  `ann_clusters` (partitions) defaults to about `sqrt(n)` and `ann_nprobe` (clusters probed per
389
394
  query) to about `sqrt(clusters)`. ANN is a create-time choice persisted with the index and works
390
395
  for text and bring-your-own-vector indexes alike; keep the exact default for small-to-mid corpora.
396
+ On reopen, `ann_nprobe` is a session-only override, so benchmark handles can change probe breadth
397
+ without rebuilding; the algorithm and partition count remain the persisted configuration.
398
+
399
+ For large corpora, these are useful starting ranges to measure, not recall or latency promises:
400
+
401
+ | Knob | Useful range | Trade-off |
402
+ | --- | --- | --- |
403
+ | `ann_nprobe` | 64 to 256 clusters | More probes raise recall and scan work; probing every cluster is exact. |
404
+ | `rescore_oversample` | 2 to 8 times `k` | More candidates improve the chance that fp32 re-ranking repairs compact-code errors, at more sidecar reads. |
405
+ </details>
406
+
407
+ <details>
408
+ <summary><b>Two-stage rescore (`rescore="original"`)</b></summary>
409
+
410
+ TurboVec's compact 4-bit scan is fast, but its quantized first-stage ranking can put close
411
+ neighbors in the wrong order. Enable rescore at creation to retain each input vector in a separate
412
+ original-precision sidecar, then re-score the first-stage candidate pool with fp32 dot products:
413
+
414
+ ```python
415
+ db = LodeDB(
416
+ path="./data",
417
+ ann="cluster",
418
+ ann_clusters=1024,
419
+ rescore="original",
420
+ rescore_dtype="float16", # default; about 2 bytes per dimension per vector
421
+ rescore_oversample=4,
422
+ )
423
+ ```
424
+
425
+ `float16` sidecars add about 2 bytes per dimension per vector (`float32` adds 4; `int8` adds 1).
426
+ They let the final ordering rise above the practical 4-bit ranking ceiling while retaining the
427
+ compact index for candidate generation. Re-scored candidate scores are exact fp32 dots, but the
428
+ overall result can still be approximate when ANN or a too-small candidate pool excludes a true
429
+ neighbor. Rescore is a create-time capture choice: originals are not recoverable after a normal
430
+ store's first ingest, so enabling it later requires a rebuild. On a rescore-enabled reopen,
431
+ `rescore_oversample` is a session-only override; mode and dtype must match the persisted sidecar.
391
432
  </details>
392
433
 
393
434
  ## Multimodal & bring-your-own vectors
@@ -537,6 +578,9 @@ lodedb mcp # stdio MCP server for agent memory
537
578
  lodedb benchmark # local, metrics-only benchmark
538
579
  ```
539
580
 
581
+ Use `lodedb serve` as a local OpenAI-compatible embeddings provider; see [LodeDB as a local
582
+ embeddings provider](docs/integrations.md#lodedb-as-a-local-embeddings-provider).
583
+
540
584
  ## Use as an MCP server
541
585
 
542
586
  LodeDB ships a [Model Context Protocol](https://modelcontextprotocol.io) server, so an agent
@@ -617,6 +661,86 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
617
661
 
618
662
  </details>
619
663
 
664
+ ## Managed cloud (OreCloud)
665
+
666
+ LodeDB stores can live in [OreCloud](https://db.egoistmachines.com), the managed-cloud
667
+ companion: durable sync for local stores, hosted read serving, and a per-user memory
668
+ data plane for agent applications. The client ships as the `[cloud]` extra:
669
+
670
+ ```sh
671
+ pip install "lodedb[cloud]"
672
+
673
+ lodedb cloud login # one browser approval
674
+ lodedb cloud init ./my-store # link the directory to a managed remote
675
+ lodedb cloud keys ./my-store # list the directory's index keys
676
+ lodedb cloud sync ./my-store cloud <key> # mirror the local store to the cloud
677
+ ```
678
+
679
+ A cloud store opens through the same class as a local one, via the
680
+ `LodeDB.cloud` alternate constructor:
681
+
682
+ ```python
683
+ from lodedb import LodeDB
684
+
685
+ db = LodeDB("./notes") # local, as always
686
+ db = LodeDB.cloud("user-42") # managed cloud, same verbs
687
+ db.add("the quick brown fox") # embedded server-side
688
+ db.search("fox", k=5)
689
+ ```
690
+
691
+ A bare store id is enough. The org/environment half resolves from the
692
+ credential (`token=`, the `ORECLOUD_TOKEN` environment variable, or
693
+ `lodedb cloud login`); pass the full `"org/environment/store"` triple in
694
+ cross-environment scripts. For config-driven code, where one string field (an
695
+ env var, a YAML value) must express either a local path or a cloud store, the
696
+ plain constructor accepts the explicit URL form
697
+ `LodeDB("orecloud://org/environment/store")` and returns the same handle.
698
+ An application serving many end users should hold one `Client` and open
699
+ per-user handles from it (`Client().store(user_id)`); the credential resolves
700
+ once and the handles share one HTTP pool. `from lodedb.cloud import Client`
701
+ works as the import root.
702
+
703
+ Work locally with `lodedb` exactly as before. The client is first-party code
704
+ in this package (`lodedb.cloud` and the `lodedb cloud` CLI), and push/pull/
705
+ sync run on the same bundled native core as the engine. The extra installs
706
+ only its dependencies (`httpx`, `pynacl`). Everything cloud loads lazily, so
707
+ a plain `import lodedb` stays network-free.
708
+
709
+ ### Sealed stores
710
+
711
+ OreCloud can host a server-side encrypted store whose 32-byte wrapping key
712
+ remains with your application. Install the additional lazy crypto support,
713
+ keep that material outside source control, and keep a recovery copy: losing
714
+ it makes the store unreadable and the service never persists it.
715
+
716
+ ```sh
717
+ pip install "lodedb[cloud,cloud-sealed]"
718
+ ```
719
+
720
+ ```python
721
+ import secrets
722
+
723
+ from lodedb.cloud import Client
724
+
725
+ material = secrets.token_bytes(32)
726
+ client = Client()
727
+ client.create_store(
728
+ "user-42",
729
+ encrypted=True,
730
+ key_material=material,
731
+ mode="cloud_writer",
732
+ preset="minilm",
733
+ )
734
+ expires_at = client.unseal_store("user-42", material, ttl_seconds=900)
735
+ # Query or write while the grant is live, then remove it when finished.
736
+ client.reseal_store("user-42")
737
+ ```
738
+
739
+ `Client.rotate_store_key("user-42", fresh_material)` replaces the caller-held
740
+ material after the store is unsealed. A sealed data-plane read returns
741
+ `CloudError` with status 423 and a `store_sealed:` detail; applications decide
742
+ when to request an unseal grant.
743
+
620
744
  ## Concurrency & durability
621
745
 
622
746
  - **Single writer, many readers, per path.** One handle holds the path open for *writing* at
@@ -660,7 +784,7 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
660
784
  read-your-writes against an append's returned LSN). On Windows the shared lock degrades to an
661
785
  exclusive hold, so appenders serialize there rather than coexisting. A record is a precomputed
662
786
  vector plus metadata (with an optional caption, e.g. for an image, retained only when the appender
663
- opts into `store_text` -- off by default, so no raw text reaches the WAL); with an embedder
787
+ opts into `store_text`; it is off by default, so no raw text reaches the WAL); with an embedder
664
788
  configured, the appender also ingests full text (chunked by the core, embedded in the binding
665
789
  layer, then logged as a post-embedding record) so text writes are multi-producer too, without a
666
790
  captured base generation. It requires WAL commit mode. Exposed as the native `CoreAppender`, over
@@ -337,6 +337,42 @@ db.search("turbine fault", k=5) # nearest cluste
337
337
  `ann_clusters` (partitions) defaults to about `sqrt(n)` and `ann_nprobe` (clusters probed per
338
338
  query) to about `sqrt(clusters)`. ANN is a create-time choice persisted with the index and works
339
339
  for text and bring-your-own-vector indexes alike; keep the exact default for small-to-mid corpora.
340
+ On reopen, `ann_nprobe` is a session-only override, so benchmark handles can change probe breadth
341
+ without rebuilding; the algorithm and partition count remain the persisted configuration.
342
+
343
+ For large corpora, these are useful starting ranges to measure, not recall or latency promises:
344
+
345
+ | Knob | Useful range | Trade-off |
346
+ | --- | --- | --- |
347
+ | `ann_nprobe` | 64 to 256 clusters | More probes raise recall and scan work; probing every cluster is exact. |
348
+ | `rescore_oversample` | 2 to 8 times `k` | More candidates improve the chance that fp32 re-ranking repairs compact-code errors, at more sidecar reads. |
349
+ </details>
350
+
351
+ <details>
352
+ <summary><b>Two-stage rescore (`rescore="original"`)</b></summary>
353
+
354
+ TurboVec's compact 4-bit scan is fast, but its quantized first-stage ranking can put close
355
+ neighbors in the wrong order. Enable rescore at creation to retain each input vector in a separate
356
+ original-precision sidecar, then re-score the first-stage candidate pool with fp32 dot products:
357
+
358
+ ```python
359
+ db = LodeDB(
360
+ path="./data",
361
+ ann="cluster",
362
+ ann_clusters=1024,
363
+ rescore="original",
364
+ rescore_dtype="float16", # default; about 2 bytes per dimension per vector
365
+ rescore_oversample=4,
366
+ )
367
+ ```
368
+
369
+ `float16` sidecars add about 2 bytes per dimension per vector (`float32` adds 4; `int8` adds 1).
370
+ They let the final ordering rise above the practical 4-bit ranking ceiling while retaining the
371
+ compact index for candidate generation. Re-scored candidate scores are exact fp32 dots, but the
372
+ overall result can still be approximate when ANN or a too-small candidate pool excludes a true
373
+ neighbor. Rescore is a create-time capture choice: originals are not recoverable after a normal
374
+ store's first ingest, so enabling it later requires a rebuild. On a rescore-enabled reopen,
375
+ `rescore_oversample` is a session-only override; mode and dtype must match the persisted sidecar.
340
376
  </details>
341
377
 
342
378
  ## Multimodal & bring-your-own vectors
@@ -486,6 +522,9 @@ lodedb mcp # stdio MCP server for agent memory
486
522
  lodedb benchmark # local, metrics-only benchmark
487
523
  ```
488
524
 
525
+ Use `lodedb serve` as a local OpenAI-compatible embeddings provider; see [LodeDB as a local
526
+ embeddings provider](docs/integrations.md#lodedb-as-a-local-embeddings-provider).
527
+
489
528
  ## Use as an MCP server
490
529
 
491
530
  LodeDB ships a [Model Context Protocol](https://modelcontextprotocol.io) server, so an agent
@@ -566,6 +605,86 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
566
605
 
567
606
  </details>
568
607
 
608
+ ## Managed cloud (OreCloud)
609
+
610
+ LodeDB stores can live in [OreCloud](https://db.egoistmachines.com), the managed-cloud
611
+ companion: durable sync for local stores, hosted read serving, and a per-user memory
612
+ data plane for agent applications. The client ships as the `[cloud]` extra:
613
+
614
+ ```sh
615
+ pip install "lodedb[cloud]"
616
+
617
+ lodedb cloud login # one browser approval
618
+ lodedb cloud init ./my-store # link the directory to a managed remote
619
+ lodedb cloud keys ./my-store # list the directory's index keys
620
+ lodedb cloud sync ./my-store cloud <key> # mirror the local store to the cloud
621
+ ```
622
+
623
+ A cloud store opens through the same class as a local one, via the
624
+ `LodeDB.cloud` alternate constructor:
625
+
626
+ ```python
627
+ from lodedb import LodeDB
628
+
629
+ db = LodeDB("./notes") # local, as always
630
+ db = LodeDB.cloud("user-42") # managed cloud, same verbs
631
+ db.add("the quick brown fox") # embedded server-side
632
+ db.search("fox", k=5)
633
+ ```
634
+
635
+ A bare store id is enough. The org/environment half resolves from the
636
+ credential (`token=`, the `ORECLOUD_TOKEN` environment variable, or
637
+ `lodedb cloud login`); pass the full `"org/environment/store"` triple in
638
+ cross-environment scripts. For config-driven code, where one string field (an
639
+ env var, a YAML value) must express either a local path or a cloud store, the
640
+ plain constructor accepts the explicit URL form
641
+ `LodeDB("orecloud://org/environment/store")` and returns the same handle.
642
+ An application serving many end users should hold one `Client` and open
643
+ per-user handles from it (`Client().store(user_id)`); the credential resolves
644
+ once and the handles share one HTTP pool. `from lodedb.cloud import Client`
645
+ works as the import root.
646
+
647
+ Work locally with `lodedb` exactly as before. The client is first-party code
648
+ in this package (`lodedb.cloud` and the `lodedb cloud` CLI), and push/pull/
649
+ sync run on the same bundled native core as the engine. The extra installs
650
+ only its dependencies (`httpx`, `pynacl`). Everything cloud loads lazily, so
651
+ a plain `import lodedb` stays network-free.
652
+
653
+ ### Sealed stores
654
+
655
+ OreCloud can host a server-side encrypted store whose 32-byte wrapping key
656
+ remains with your application. Install the additional lazy crypto support,
657
+ keep that material outside source control, and keep a recovery copy: losing
658
+ it makes the store unreadable and the service never persists it.
659
+
660
+ ```sh
661
+ pip install "lodedb[cloud,cloud-sealed]"
662
+ ```
663
+
664
+ ```python
665
+ import secrets
666
+
667
+ from lodedb.cloud import Client
668
+
669
+ material = secrets.token_bytes(32)
670
+ client = Client()
671
+ client.create_store(
672
+ "user-42",
673
+ encrypted=True,
674
+ key_material=material,
675
+ mode="cloud_writer",
676
+ preset="minilm",
677
+ )
678
+ expires_at = client.unseal_store("user-42", material, ttl_seconds=900)
679
+ # Query or write while the grant is live, then remove it when finished.
680
+ client.reseal_store("user-42")
681
+ ```
682
+
683
+ `Client.rotate_store_key("user-42", fresh_material)` replaces the caller-held
684
+ material after the store is unsealed. A sealed data-plane read returns
685
+ `CloudError` with status 423 and a `store_sealed:` detail; applications decide
686
+ when to request an unseal grant.
687
+
569
688
  ## Concurrency & durability
570
689
 
571
690
  - **Single writer, many readers, per path.** One handle holds the path open for *writing* at
@@ -609,7 +728,7 @@ See [`examples/mcp_config.json`](examples/mcp_config.json) for a copy-paste star
609
728
  read-your-writes against an append's returned LSN). On Windows the shared lock degrades to an
610
729
  exclusive hold, so appenders serialize there rather than coexisting. A record is a precomputed
611
730
  vector plus metadata (with an optional caption, e.g. for an image, retained only when the appender
612
- opts into `store_text` -- off by default, so no raw text reaches the WAL); with an embedder
731
+ opts into `store_text`; it is off by default, so no raw text reaches the WAL); with an embedder
613
732
  configured, the appender also ingests full text (chunked by the core, embedded in the binding
614
733
  layer, then logged as a post-embedding record) so text writes are multi-producer too, without a
615
734
  captured base generation. It requires WAL commit mode. Exposed as the native `CoreAppender`, over
@@ -0,0 +1,41 @@
1
+ [package]
2
+ name = "lodedb-cloud-core"
3
+ description = "Native transfer core for the LodeDB managed cloud — durable push/pull/sync over the open commit format."
4
+ version.workspace = true
5
+ edition.workspace = true
6
+ rust-version.workspace = true
7
+ license.workspace = true
8
+ repository.workspace = true
9
+ publish = false
10
+
11
+ [dependencies]
12
+ # The engine core. The cloud transfer plane links its `storage` module directly
13
+ # so the managed cloud and the embedded product share ONE commit-format
14
+ # implementation (schema, layout, checksum, sub-manifests incl. `tvmv`). A path
15
+ # dep now that both live in this workspace — engine changes and the transfer
16
+ # plane move together under one suite.
17
+ lodedb-core = { path = "../lodedb-core" }
18
+ # Object-storage backend (S3/GCS/Azure) behind the same ArtifactStore trait. The
19
+ # `aws` feature adds the S3 builder; the crate's conditional-put API (Create /
20
+ # Update{e_tag}) is what makes the root-pointer compare-and-swap strongly
21
+ # consistent on object storage. Its InMemory backend also drives the tests.
22
+ object_store = { version = "0.12.5", features = ["aws"] }
23
+ serde_json = "1.0"
24
+ sha2 = "0.10"
25
+ # Streaming transfer plumbing: `bytes` for the object-store chunk type and
26
+ # `futures` for driving its download streams chunk-by-chunk from the sync
27
+ # ArtifactStore surface. Both are already in the tree via object_store.
28
+ bytes = "1"
29
+ futures = "0.3"
30
+ # The object_store backends are async; the runtime is enabled with net + time so a
31
+ # real S3 client works (not just the in-memory test backend). ObjectArtifactStore
32
+ # keeps the sync ArtifactStore trait by owning a current-thread runtime.
33
+ tokio = { version = "1.47.5", default-features = false, features = ["rt", "net", "time"] }
34
+ # The pointer serialize/validate round-trip reuses lodedb-core's own
35
+ # write/read_commit_manifest (which operate on file paths) via a scratch file, so
36
+ # object-store pointer documents are byte-identical to on-disk ones. Also used by
37
+ # the test fixtures to build committed generations in throwaway directories.
38
+ tempfile = "3"
39
+
40
+ [lints]
41
+ workspace = true
@@ -0,0 +1,127 @@
1
+ //! Storage abstraction for committed, generation-addressed artifacts.
2
+ //!
3
+ //! The substrate is the engine's existing on-disk layout: a committed generation
4
+ //! is a set of immutable, sha256-addressed `g<epoch>.*` artifacts under
5
+ //! `<key>.gen/` pinned by an atomic `<key>.commit.json` root pointer. An
6
+ //! [`ArtifactStore`] exposes exactly the operations that layout needs:
7
+ //!
8
+ //! - **names** are store-relative paths mirroring the on-disk layout, e.g.
9
+ //! `idx.gen/g7.json` or `idx.gen/g7.json.json-delta/delta-00000000.jsd`;
10
+ //! - the **pointer key** is the index key (`idx` -> `idx.commit.json`).
11
+ //!
12
+ //! Implementations treat content artifacts as immutable and content-addressed
13
+ //! (written once, never overwritten), and commit a generation *only* by swapping
14
+ //! the root pointer. [`LocalArtifactStore`](crate::LocalArtifactStore) is the
15
+ //! default; object-storage backends (S3/GCS/Azure) land in later milestones with
16
+ //! the artifact names becoming object keys under a per-tenant prefix and the
17
+ //! pointer swap becoming a conditional write.
18
+
19
+ use crate::error::Result;
20
+ use serde_json::Value;
21
+ use std::io::Read;
22
+
23
+ /// Reads/writes immutable artifacts and swaps a root pointer atomically.
24
+ ///
25
+ /// The interface is deliberately small: streaming artifact I/O plus a pointer
26
+ /// compare-and-swap. That is everything the generation-addressed commit format
27
+ /// needs as a cloud substrate. Streaming is the primitive. Vector bases run
28
+ /// to gigabytes, so a transfer's peak memory must be a fixed buffer, never a
29
+ /// function of artifact size; the buffered methods are conveniences for the
30
+ /// small payloads (pointer documents) built on top.
31
+ pub trait ArtifactStore {
32
+ /// Opens one artifact for streaming reads;
33
+ /// [`ArtifactStoreError::NotFound`] if the name is absent.
34
+ ///
35
+ /// [`ArtifactStoreError::NotFound`]: crate::ArtifactStoreError::NotFound
36
+ fn open_read<'a>(&'a self, name: &str) -> Result<Box<dyn Read + 'a>>;
37
+
38
+ /// Returns one artifact's bytes, fully buffered, for small payloads
39
+ /// only; transfers and verification stream via [`open_read`](Self::open_read).
40
+ fn read_bytes(&self, name: &str) -> Result<Vec<u8>> {
41
+ let mut data = Vec::new();
42
+ self.open_read(name)?.read_to_end(&mut data)?;
43
+ Ok(data)
44
+ }
45
+
46
+ /// Streams one immutable artifact into the store unless it already
47
+ /// exists, hashing incrementally as it copies.
48
+ ///
49
+ /// `sha256` is the expected lowercase-hex digest of the streamed bytes; a
50
+ /// mismatch is an integrity error and nothing is stored, so corruption can
51
+ /// never land, even when the source is another store's live stream. A
52
+ /// name already present with identical bytes is a no-op (idempotent
53
+ /// re-push); present with *different* bytes is a conflict. Artifacts are
54
+ /// immutable and are never overwritten in place. On the already-present
55
+ /// no-op path the incoming stream may be left partially (or wholly)
56
+ /// unread and unvalidated: success then attests that the STORED bytes
57
+ /// match `sha256`, not that the stream did.
58
+ ///
59
+ /// `size_hint` is the expected byte count (0 = unknown), advisory only:
60
+ /// backends use it to pick an upload strategy (the object store's
61
+ /// conditional-claim path has a provider copy-size ceiling), never to
62
+ /// trust the stream's length. Transfers pass the manifest-recorded size.
63
+ fn write_stream_if_absent(
64
+ &self,
65
+ name: &str,
66
+ data: &mut dyn Read,
67
+ sha256: &str,
68
+ size_hint: u64,
69
+ ) -> Result<()>;
70
+
71
+ /// Buffered convenience over
72
+ /// [`write_stream_if_absent`](Self::write_stream_if_absent), for small
73
+ /// payloads a caller already holds in memory.
74
+ fn write_bytes_if_absent(&self, name: &str, data: &[u8], sha256: &str) -> Result<()> {
75
+ let mut cursor = data;
76
+ self.write_stream_if_absent(name, &mut cursor, sha256, data.len() as u64)
77
+ }
78
+
79
+ /// Whether an artifact named `name` is present, without asserting anything
80
+ /// about its content.
81
+ ///
82
+ /// The default opens the artifact and maps
83
+ /// [`NotFound`](crate::ArtifactStoreError::NotFound) to `false`; backends
84
+ /// with a cheap existence primitive (a filesystem stat, an object-store
85
+ /// HEAD) should override it to avoid opening a stream.
86
+ fn contains(&self, name: &str) -> Result<bool> {
87
+ match self.open_read(name) {
88
+ Ok(_) => Ok(true),
89
+ Err(crate::ArtifactStoreError::NotFound(_)) => Ok(false),
90
+ Err(error) => Err(error),
91
+ }
92
+ }
93
+
94
+ /// Returns the committed root-manifest body for `key`, or `None`.
95
+ ///
96
+ /// The body carries the generation number, the base epoch, counts, and the
97
+ /// per-store sub-manifests that name every artifact in the generation. It is
98
+ /// the read side that [`compare_and_swap_pointer`](Self::compare_and_swap_pointer)
99
+ /// and the inventory helpers need to learn the currently-committed generation.
100
+ fn read_pointer(&self, key: &str) -> Result<Option<Value>>;
101
+
102
+ /// Publishes `new_body` as the root pointer iff the currently committed body is
103
+ /// exactly `old_body` (`None` means the pointer must not yet exist); otherwise
104
+ /// [`ArtifactStoreError::PointerConflict`]. This swap is the only commit point:
105
+ /// artifacts are published first, then the pointer flips all-or-nothing.
106
+ ///
107
+ /// The precondition is the full committed body, not just its generation number.
108
+ /// A generation number is not a unique version token (two independent lineages
109
+ /// can share one with different content), so comparing the whole body is what
110
+ /// makes this a sound compare-and-swap rather than an ABA-prone check.
111
+ ///
112
+ /// [`ArtifactStoreError::PointerConflict`]: crate::ArtifactStoreError::PointerConflict
113
+ fn compare_and_swap_pointer(
114
+ &self,
115
+ key: &str,
116
+ old_body: Option<&Value>,
117
+ new_body: &Value,
118
+ ) -> Result<()>;
119
+ }
120
+
121
+ /// The generation number recorded in a committed body, if present.
122
+ ///
123
+ /// Used only to annotate a [`PointerConflict`](crate::ArtifactStoreError::PointerConflict)
124
+ /// for readability; the swap precondition is the full body, not this number.
125
+ pub(crate) fn body_generation(body: &Value) -> Option<u64> {
126
+ body.get("generation").and_then(Value::as_u64)
127
+ }
@@ -0,0 +1,119 @@
1
+ //! Content-addressed blob naming for the managed remote layout.
2
+ //!
3
+ //! A managed remote can store artifacts content-addressed
4
+ //! (`blobs/sha256/aa/<sha256>` under a per-tenant prefix) instead of under
5
+ //! their engine path names. Content addressing is what absorbs the
6
+ //! fork-collision case (two branches committing different artifacts under the
7
+ //! same engine name, e.g. `idx.gen/g7.json`, coexist as two blobs), and the
8
+ //! two-level `aa/` fan-out keeps listings and prefix operations tractable.
9
+ //!
10
+ //! These helpers are pure and tested; they pin the naming contract the
11
+ //! transfer plane builds on. The digest is always the lowercase-hex
12
+ //! SHA-256 of the blob's bytes, the same digest the engine records per
13
+ //! artifact and [`ArtifactStore::write_bytes_if_absent`] verifies.
14
+ //!
15
+ //! [`ArtifactStore::write_bytes_if_absent`]: crate::ArtifactStore::write_bytes_if_absent
16
+
17
+ use crate::error::{ArtifactStoreError, Result};
18
+
19
+ /// The fixed prefix every blob name lives under.
20
+ const BLOB_PREFIX: &str = "blobs/sha256";
21
+
22
+ /// Returns the store-relative blob name for a content digest:
23
+ /// `blobs/sha256/aa/<sha256>`, where `aa` is the digest's first two hex chars.
24
+ ///
25
+ /// Rejects anything that is not exactly 64 lowercase hex characters. Blob
26
+ /// names participate in authorization paths, so a malformed digest must fail
27
+ /// closed rather than mint a name outside the contract.
28
+ pub fn blob_name(sha256: &str) -> Result<String> {
29
+ validate_sha256(sha256)?;
30
+ Ok(format!("{BLOB_PREFIX}/{}/{sha256}", &sha256[..2]))
31
+ }
32
+
33
+ /// Parses a blob name back to its content digest, the exact inverse of
34
+ /// [`blob_name`]. Rejects any name that deviates from the contract (wrong
35
+ /// prefix, fan-out directory disagreeing with the digest, malformed digest).
36
+ pub fn parse_blob_name(name: &str) -> Result<String> {
37
+ let malformed = || {
38
+ ArtifactStoreError::Integrity(format!(
39
+ "blob name {name:?} does not match {BLOB_PREFIX}/aa/<sha256>"
40
+ ))
41
+ };
42
+ let rest = name.strip_prefix(BLOB_PREFIX).ok_or_else(malformed)?;
43
+ let rest = rest.strip_prefix('/').ok_or_else(malformed)?;
44
+ let (fan_out, sha256) = rest.split_once('/').ok_or_else(malformed)?;
45
+ validate_sha256(sha256).map_err(|_| malformed())?;
46
+ if fan_out != &sha256[..2] {
47
+ return Err(malformed());
48
+ }
49
+ Ok(sha256.to_string())
50
+ }
51
+
52
+ /// Requires exactly 64 lowercase hex characters. Also used by the generation
53
+ /// inventory to validate every manifest digest at the trust boundary: digests
54
+ /// become staging file names and object keys, so a non-hex "digest" must fail
55
+ /// closed before it can name a path.
56
+ pub(crate) fn validate_sha256(sha256: &str) -> Result<()> {
57
+ let valid = sha256.len() == 64
58
+ && sha256
59
+ .bytes()
60
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte));
61
+ if valid {
62
+ Ok(())
63
+ } else {
64
+ Err(ArtifactStoreError::Integrity(format!(
65
+ "{sha256:?} is not a lowercase-hex sha256 digest"
66
+ )))
67
+ }
68
+ }
69
+
70
+ #[cfg(test)]
71
+ mod tests {
72
+ use super::{blob_name, parse_blob_name};
73
+ use crate::error::ArtifactStoreError;
74
+
75
+ const SHA: &str = "aa30b1cc05c10ac8a1f4e6d46f7d4b1a9c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f";
76
+
77
+ #[test]
78
+ fn names_a_blob_under_its_two_char_fan_out() {
79
+ assert_eq!(blob_name(SHA).unwrap(), format!("blobs/sha256/aa/{SHA}"));
80
+ }
81
+
82
+ #[test]
83
+ fn parse_inverts_blob_name() {
84
+ assert_eq!(parse_blob_name(&blob_name(SHA).unwrap()).unwrap(), SHA);
85
+ }
86
+
87
+ #[test]
88
+ fn rejects_malformed_digests() {
89
+ for bad in [
90
+ "",
91
+ "abc",
92
+ &SHA[..63], // too short
93
+ &format!("{SHA}0"), // too long
94
+ &SHA.to_uppercase(), // uppercase
95
+ &format!("g{}", &SHA[1..]), // non-hex
96
+ ] {
97
+ assert!(matches!(
98
+ blob_name(bad).unwrap_err(),
99
+ ArtifactStoreError::Integrity(_)
100
+ ));
101
+ }
102
+ }
103
+
104
+ #[test]
105
+ fn rejects_names_off_contract() {
106
+ for bad in [
107
+ format!("blobs/sha256/{SHA}"), // no fan-out level
108
+ format!("blobs/sha256/bb/{SHA}"), // fan-out disagrees
109
+ format!("blobs/md5/aa/{SHA}"), // wrong algorithm segment
110
+ format!("sha256/aa/{SHA}"), // wrong prefix
111
+ format!("blobs/sha256/aa/{}", &SHA[..63]), // malformed digest
112
+ ] {
113
+ assert!(matches!(
114
+ parse_blob_name(&bad).unwrap_err(),
115
+ ArtifactStoreError::Integrity(_)
116
+ ));
117
+ }
118
+ }
119
+ }