@lix-js/sdk 0.6.0-preview.1 → 0.6.0-preview.3

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 (205) hide show
  1. package/SKILL.md +304 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/generated/builtin-schemas.d.ts +87 -162
  7. package/dist/generated/builtin-schemas.js +139 -236
  8. package/dist/open-lix.d.ts +103 -14
  9. package/dist/open-lix.js +3 -0
  10. package/dist/sqlite/index.js +99 -22
  11. package/dist-engine-src/README.md +18 -0
  12. package/dist-engine-src/src/backend/kv.rs +358 -0
  13. package/dist-engine-src/src/backend/mod.rs +12 -0
  14. package/dist-engine-src/src/backend/testing.rs +658 -0
  15. package/dist-engine-src/src/backend/types.rs +96 -0
  16. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  17. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  18. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  19. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  20. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  21. package/dist-engine-src/src/binary_cas/types.rs +121 -0
  22. package/dist-engine-src/src/catalog/context.rs +412 -0
  23. package/dist-engine-src/src/catalog/mod.rs +10 -0
  24. package/dist-engine-src/src/catalog/schema.rs +4 -0
  25. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  26. package/dist-engine-src/src/cel/context.rs +86 -0
  27. package/dist-engine-src/src/cel/error.rs +19 -0
  28. package/dist-engine-src/src/cel/mod.rs +8 -0
  29. package/dist-engine-src/src/cel/provider.rs +9 -0
  30. package/dist-engine-src/src/cel/runtime.rs +167 -0
  31. package/dist-engine-src/src/cel/value.rs +50 -0
  32. package/dist-engine-src/src/commit_graph/context.rs +901 -0
  33. package/dist-engine-src/src/commit_graph/mod.rs +11 -0
  34. package/dist-engine-src/src/commit_graph/types.rs +109 -0
  35. package/dist-engine-src/src/commit_graph/walker.rs +756 -0
  36. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  37. package/dist-engine-src/src/commit_store/context.rs +944 -0
  38. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  39. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  40. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  41. package/dist-engine-src/src/commit_store/types.rs +215 -0
  42. package/dist-engine-src/src/common/error.rs +313 -0
  43. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  44. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  45. package/dist-engine-src/src/common/identity.rs +145 -0
  46. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  47. package/dist-engine-src/src/common/metadata.rs +40 -0
  48. package/dist-engine-src/src/common/mod.rs +23 -0
  49. package/dist-engine-src/src/common/types.rs +105 -0
  50. package/dist-engine-src/src/common/wire.rs +222 -0
  51. package/dist-engine-src/src/domain.rs +324 -0
  52. package/dist-engine-src/src/engine.rs +225 -0
  53. package/dist-engine-src/src/entity_identity.rs +405 -0
  54. package/dist-engine-src/src/functions/context.rs +292 -0
  55. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  56. package/dist-engine-src/src/functions/mod.rs +18 -0
  57. package/dist-engine-src/src/functions/provider.rs +130 -0
  58. package/dist-engine-src/src/functions/state.rs +336 -0
  59. package/dist-engine-src/src/functions/types.rs +37 -0
  60. package/dist-engine-src/src/init.rs +558 -0
  61. package/dist-engine-src/src/json_store/compression.rs +77 -0
  62. package/dist-engine-src/src/json_store/context.rs +423 -0
  63. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  64. package/dist-engine-src/src/json_store/mod.rs +12 -0
  65. package/dist-engine-src/src/json_store/store.rs +1109 -0
  66. package/dist-engine-src/src/json_store/types.rs +217 -0
  67. package/dist-engine-src/src/lib.rs +62 -0
  68. package/dist-engine-src/src/live_state/context.rs +2019 -0
  69. package/dist-engine-src/src/live_state/mod.rs +15 -0
  70. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  71. package/dist-engine-src/src/live_state/reader.rs +23 -0
  72. package/dist-engine-src/src/live_state/types.rs +222 -0
  73. package/dist-engine-src/src/live_state/visibility.rs +223 -0
  74. package/dist-engine-src/src/plugin/archive.rs +438 -0
  75. package/dist-engine-src/src/plugin/component.rs +183 -0
  76. package/dist-engine-src/src/plugin/install.rs +619 -0
  77. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  78. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  79. package/dist-engine-src/src/plugin/mod.rs +33 -0
  80. package/dist-engine-src/src/plugin/plugin_manifest.json +118 -0
  81. package/dist-engine-src/src/plugin/storage.rs +74 -0
  82. package/dist-engine-src/src/schema/annotations/defaults.rs +275 -0
  83. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  84. package/dist-engine-src/src/schema/builtin/lix_account.json +21 -0
  85. package/dist-engine-src/src/schema/builtin/lix_active_account.json +29 -0
  86. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +29 -0
  87. package/dist-engine-src/src/schema/builtin/lix_change.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_change_author.json +45 -0
  89. package/dist-engine-src/src/schema/builtin/lix_commit.json +24 -0
  90. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +53 -0
  91. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +52 -0
  92. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +52 -0
  93. package/dist-engine-src/src/schema/builtin/lix_key_value.json +40 -0
  94. package/dist-engine-src/src/schema/builtin/lix_label.json +29 -0
  95. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  96. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +25 -0
  97. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +34 -0
  98. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +48 -0
  99. package/dist-engine-src/src/schema/builtin/mod.rs +222 -0
  100. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  101. package/dist-engine-src/src/schema/definition.json +187 -0
  102. package/dist-engine-src/src/schema/definition.rs +742 -0
  103. package/dist-engine-src/src/schema/key.rs +138 -0
  104. package/dist-engine-src/src/schema/mod.rs +20 -0
  105. package/dist-engine-src/src/schema/seed.rs +14 -0
  106. package/dist-engine-src/src/schema/tests.rs +780 -0
  107. package/dist-engine-src/src/session/context.rs +364 -0
  108. package/dist-engine-src/src/session/create_version.rs +88 -0
  109. package/dist-engine-src/src/session/execute.rs +478 -0
  110. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  111. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  112. package/dist-engine-src/src/session/merge/conflicts.rs +63 -0
  113. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  114. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  115. package/dist-engine-src/src/session/merge/version.rs +427 -0
  116. package/dist-engine-src/src/session/mod.rs +27 -0
  117. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  118. package/dist-engine-src/src/session/switch_version.rs +109 -0
  119. package/dist-engine-src/src/sql2/change_provider.rs +331 -0
  120. package/dist-engine-src/src/sql2/classify.rs +182 -0
  121. package/dist-engine-src/src/sql2/context.rs +311 -0
  122. package/dist-engine-src/src/sql2/directory_history_provider.rs +631 -0
  123. package/dist-engine-src/src/sql2/directory_provider.rs +2453 -0
  124. package/dist-engine-src/src/sql2/dml.rs +148 -0
  125. package/dist-engine-src/src/sql2/entity_history_provider.rs +440 -0
  126. package/dist-engine-src/src/sql2/entity_provider.rs +3211 -0
  127. package/dist-engine-src/src/sql2/error.rs +216 -0
  128. package/dist-engine-src/src/sql2/execute.rs +3440 -0
  129. package/dist-engine-src/src/sql2/file_history_provider.rs +910 -0
  130. package/dist-engine-src/src/sql2/file_provider.rs +3679 -0
  131. package/dist-engine-src/src/sql2/filesystem_planner.rs +1490 -0
  132. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  133. package/dist-engine-src/src/sql2/filesystem_visibility.rs +383 -0
  134. package/dist-engine-src/src/sql2/history_projection.rs +56 -0
  135. package/dist-engine-src/src/sql2/history_provider.rs +412 -0
  136. package/dist-engine-src/src/sql2/history_route.rs +657 -0
  137. package/dist-engine-src/src/sql2/lix_state_provider.rs +2512 -0
  138. package/dist-engine-src/src/sql2/mod.rs +46 -0
  139. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  140. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  141. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  142. package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
  143. package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
  144. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  145. package/dist-engine-src/src/sql2/read_only.rs +63 -0
  146. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  147. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  148. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  149. package/dist-engine-src/src/sql2/session.rs +132 -0
  150. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  151. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  152. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  153. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  154. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  155. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  156. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  157. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  158. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  159. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  160. package/dist-engine-src/src/sql2/udfs/mod.rs +89 -0
  161. package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
  162. package/dist-engine-src/src/sql2/version_provider.rs +1202 -0
  163. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  164. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  165. package/dist-engine-src/src/storage/context.rs +356 -0
  166. package/dist-engine-src/src/storage/mod.rs +14 -0
  167. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  168. package/dist-engine-src/src/storage/types.rs +501 -0
  169. package/dist-engine-src/src/storage_bench.rs +4863 -0
  170. package/dist-engine-src/src/test_support.rs +228 -0
  171. package/dist-engine-src/src/tracked_state/by_file_index.rs +98 -0
  172. package/dist-engine-src/src/tracked_state/codec.rs +2085 -0
  173. package/dist-engine-src/src/tracked_state/context.rs +1867 -0
  174. package/dist-engine-src/src/tracked_state/diff.rs +686 -0
  175. package/dist-engine-src/src/tracked_state/materialization.rs +403 -0
  176. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  177. package/dist-engine-src/src/tracked_state/merge.rs +492 -0
  178. package/dist-engine-src/src/tracked_state/mod.rs +32 -0
  179. package/dist-engine-src/src/tracked_state/storage.rs +375 -0
  180. package/dist-engine-src/src/tracked_state/tree.rs +3187 -0
  181. package/dist-engine-src/src/tracked_state/types.rs +231 -0
  182. package/dist-engine-src/src/transaction/commit.rs +1484 -0
  183. package/dist-engine-src/src/transaction/context.rs +1548 -0
  184. package/dist-engine-src/src/transaction/live_state_overlay.rs +35 -0
  185. package/dist-engine-src/src/transaction/mod.rs +13 -0
  186. package/dist-engine-src/src/transaction/normalization.rs +890 -0
  187. package/dist-engine-src/src/transaction/prep.rs +37 -0
  188. package/dist-engine-src/src/transaction/schema_resolver.rs +149 -0
  189. package/dist-engine-src/src/transaction/staging.rs +1731 -0
  190. package/dist-engine-src/src/transaction/types.rs +460 -0
  191. package/dist-engine-src/src/transaction/validation.rs +5830 -0
  192. package/dist-engine-src/src/untracked_state/codec.rs +307 -0
  193. package/dist-engine-src/src/untracked_state/context.rs +98 -0
  194. package/dist-engine-src/src/untracked_state/materialization.rs +63 -0
  195. package/dist-engine-src/src/untracked_state/mod.rs +15 -0
  196. package/dist-engine-src/src/untracked_state/storage.rs +396 -0
  197. package/dist-engine-src/src/untracked_state/types.rs +146 -0
  198. package/dist-engine-src/src/version/context.rs +40 -0
  199. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  200. package/dist-engine-src/src/version/mod.rs +13 -0
  201. package/dist-engine-src/src/version/refs.rs +330 -0
  202. package/dist-engine-src/src/version/stage_rows.rs +67 -0
  203. package/dist-engine-src/src/version/types.rs +21 -0
  204. package/dist-engine-src/src/wasm/mod.rs +60 -0
  205. package/package.json +68 -64
@@ -0,0 +1,84 @@
1
+ use crate::commit_store::{LocatedChange, MaterializedChange};
2
+ use crate::json_store::{JsonLoadRequestRef, JsonReadScopeRef, JsonRef, JsonStoreReader};
3
+ use crate::storage::StorageReader;
4
+ use crate::{parse_row_metadata, LixError};
5
+
6
+ pub(crate) async fn materialize_change<S>(
7
+ json_reader: &mut JsonStoreReader<S>,
8
+ located: LocatedChange,
9
+ ) -> Result<MaterializedChange, LixError>
10
+ where
11
+ S: StorageReader,
12
+ {
13
+ let change = located.record;
14
+ let pack_ids = [located.source_pack_id];
15
+ let scope = JsonReadScopeRef::CommitPacks {
16
+ commit_id: &located.source_commit_id,
17
+ pack_ids: &pack_ids,
18
+ };
19
+ let snapshot_content = load_optional_json_text(
20
+ json_reader,
21
+ change.snapshot_ref.as_ref(),
22
+ scope,
23
+ "snapshot_ref",
24
+ )
25
+ .await?;
26
+ let metadata = match load_optional_json_text(
27
+ json_reader,
28
+ change.metadata_ref.as_ref(),
29
+ scope,
30
+ "metadata_ref",
31
+ )
32
+ .await?
33
+ {
34
+ Some(value) => Some(parse_row_metadata(
35
+ &value,
36
+ "commit_store change metadata_ref",
37
+ )?),
38
+ None => None,
39
+ };
40
+ Ok(MaterializedChange {
41
+ id: change.id,
42
+ entity_id: change.entity_id,
43
+ schema_key: change.schema_key,
44
+ file_id: change.file_id,
45
+ snapshot_content,
46
+ metadata,
47
+ created_at: change.created_at,
48
+ })
49
+ }
50
+
51
+ async fn load_optional_json_text<S>(
52
+ json_reader: &mut JsonStoreReader<S>,
53
+ json_ref: Option<&JsonRef>,
54
+ scope: JsonReadScopeRef<'_>,
55
+ field: &str,
56
+ ) -> Result<Option<String>, LixError>
57
+ where
58
+ S: StorageReader,
59
+ {
60
+ let Some(json_ref) = json_ref else {
61
+ return Ok(None);
62
+ };
63
+ let batch = json_reader
64
+ .load_bytes_many(JsonLoadRequestRef {
65
+ refs: std::slice::from_ref(json_ref),
66
+ scope,
67
+ })
68
+ .await?;
69
+ let Some(bytes) = batch.into_values().into_iter().next().flatten() else {
70
+ return Err(LixError::new(
71
+ LixError::CODE_INTERNAL_ERROR,
72
+ format!(
73
+ "commit_store change {field} '{}' is missing",
74
+ json_ref.to_hex()
75
+ ),
76
+ ));
77
+ };
78
+ String::from_utf8(bytes).map(Some).map_err(|error| {
79
+ LixError::new(
80
+ LixError::CODE_INTERNAL_ERROR,
81
+ format!("commit_store change {field} is not UTF-8 JSON: {error}"),
82
+ )
83
+ })
84
+ }
@@ -0,0 +1,16 @@
1
+ pub(crate) mod codec;
2
+ mod context;
3
+ mod materialization;
4
+ pub(crate) mod storage;
5
+ mod types;
6
+
7
+ #[allow(unused_imports)]
8
+ pub(crate) use context::{CommitStoreContext, CommitStoreReader, CommitStoreWriter};
9
+ #[allow(unused_imports)]
10
+ pub(crate) use materialization::materialize_change;
11
+ #[allow(unused_imports)]
12
+ pub(crate) use types::{
13
+ Change, ChangeIndexEntry, ChangeLocator, ChangeLocatorRef, ChangePack, ChangePackView,
14
+ ChangeRef, ChangeScanRequest, Commit, CommitDraftRef, LocatedChange, MaterializedChange,
15
+ MembershipPack, MembershipPackView, StagedCommitStoreCommit, StoredCommitRef,
16
+ };
@@ -0,0 +1,600 @@
1
+ use crate::commit_store::{
2
+ Change, ChangeIndexEntry, ChangeLocator, ChangeRef, Commit, CommitDraftRef,
3
+ StagedCommitStoreCommit, StoredCommitRef,
4
+ };
5
+ use crate::storage::{
6
+ KvGetGroup, KvGetRequest, KvScanRange, KvScanRequest, StorageReader, StorageWriteSet,
7
+ };
8
+ use crate::LixError;
9
+ use std::collections::{BTreeMap, BTreeSet};
10
+
11
+ pub(crate) const COMMIT_NAMESPACE: &str = "commit_store.commit";
12
+ pub(crate) const CHANGE_PACK_NAMESPACE: &str = "commit_store.change_pack";
13
+ pub(crate) const MEMBERSHIP_PACK_NAMESPACE: &str = "commit_store.membership_pack";
14
+
15
+ const SINGLE_PACK_ID: u32 = 0;
16
+
17
+ pub(crate) fn stage_commit(
18
+ writes: &mut StorageWriteSet,
19
+ commit: CommitDraftRef<'_>,
20
+ authored_changes: Vec<ChangeRef<'_>>,
21
+ adopted_changes: Vec<ChangeLocator>,
22
+ ) -> Result<StagedCommitStoreCommit, LixError> {
23
+ stage_commit_with_authored_pack(writes, commit, authored_changes, adopted_changes, true)
24
+ }
25
+
26
+ pub(crate) fn stage_commit_with_external_authored_pack(
27
+ writes: &mut StorageWriteSet,
28
+ commit: CommitDraftRef<'_>,
29
+ authored_changes: Vec<ChangeRef<'_>>,
30
+ adopted_changes: Vec<ChangeLocator>,
31
+ ) -> Result<StagedCommitStoreCommit, LixError> {
32
+ stage_commit_with_authored_pack(writes, commit, authored_changes, adopted_changes, false)
33
+ }
34
+
35
+ fn stage_commit_with_authored_pack(
36
+ writes: &mut StorageWriteSet,
37
+ commit: CommitDraftRef<'_>,
38
+ authored_changes: Vec<ChangeRef<'_>>,
39
+ adopted_changes: Vec<ChangeLocator>,
40
+ write_authored_change_pack: bool,
41
+ ) -> Result<StagedCommitStoreCommit, LixError> {
42
+ let stored_commit = StoredCommitRef {
43
+ id: commit.id,
44
+ change_id: commit.change_id,
45
+ parent_ids: commit.parent_ids,
46
+ author_account_ids: commit.author_account_ids,
47
+ created_at: commit.created_at,
48
+ change_pack_count: if authored_changes.is_empty() { 0 } else { 1 },
49
+ membership_pack_count: if adopted_changes.is_empty() { 0 } else { 1 },
50
+ };
51
+
52
+ writes.put(
53
+ COMMIT_NAMESPACE,
54
+ commit_key(commit.id),
55
+ crate::commit_store::codec::encode_commit_ref(stored_commit)?,
56
+ );
57
+
58
+ let mut authored_locators = Vec::with_capacity(authored_changes.len());
59
+ if !authored_changes.is_empty() {
60
+ if write_authored_change_pack {
61
+ writes.put(
62
+ CHANGE_PACK_NAMESPACE,
63
+ pack_key(commit.id, SINGLE_PACK_ID)?,
64
+ crate::commit_store::codec::encode_change_pack(
65
+ commit.id,
66
+ SINGLE_PACK_ID,
67
+ &authored_changes,
68
+ )?,
69
+ );
70
+ }
71
+ for (source_ordinal, change) in authored_changes.iter().enumerate() {
72
+ authored_locators.push(ChangeLocator {
73
+ source_commit_id: commit.id.to_string(),
74
+ source_pack_id: SINGLE_PACK_ID,
75
+ source_ordinal: u32::try_from(source_ordinal).map_err(|_| {
76
+ LixError::new(
77
+ LixError::CODE_INTERNAL_ERROR,
78
+ "commit-store change pack ordinal exceeds u32",
79
+ )
80
+ })?,
81
+ change_id: change.id.to_string(),
82
+ });
83
+ }
84
+ }
85
+
86
+ if !adopted_changes.is_empty() {
87
+ writes.put(
88
+ MEMBERSHIP_PACK_NAMESPACE,
89
+ pack_key(commit.id, SINGLE_PACK_ID)?,
90
+ crate::commit_store::codec::encode_membership_pack(
91
+ commit.id,
92
+ SINGLE_PACK_ID,
93
+ adopted_changes.iter().map(ChangeLocator::as_ref),
94
+ )?,
95
+ );
96
+ }
97
+
98
+ Ok(StagedCommitStoreCommit {
99
+ authored_locators,
100
+ adopted_locators: adopted_changes,
101
+ })
102
+ }
103
+
104
+ pub(crate) async fn load_commit(
105
+ store: &mut (impl StorageReader + ?Sized),
106
+ commit_id: &str,
107
+ ) -> Result<Option<Commit>, LixError> {
108
+ let Some(bytes) = get_one(store, COMMIT_NAMESPACE, commit_key(commit_id)).await? else {
109
+ return Ok(None);
110
+ };
111
+ crate::commit_store::codec::decode_commit(&bytes).map(Some)
112
+ }
113
+
114
+ pub(crate) async fn scan_commits(
115
+ store: &mut (impl StorageReader + ?Sized),
116
+ ) -> Result<Vec<Commit>, LixError> {
117
+ let page = store
118
+ .scan_values(KvScanRequest {
119
+ namespace: COMMIT_NAMESPACE.to_string(),
120
+ range: KvScanRange::prefix(Vec::new()),
121
+ after: None,
122
+ limit: usize::MAX,
123
+ })
124
+ .await?;
125
+ page.values
126
+ .iter()
127
+ .map(|bytes| crate::commit_store::codec::decode_commit(bytes))
128
+ .collect()
129
+ }
130
+
131
+ pub(crate) async fn load_change_pack(
132
+ store: &mut (impl StorageReader + ?Sized),
133
+ commit_id: &str,
134
+ pack_id: u32,
135
+ ) -> Result<Option<Vec<Change>>, LixError> {
136
+ let Some(bytes) = get_one(store, CHANGE_PACK_NAMESPACE, pack_key(commit_id, pack_id)?).await?
137
+ else {
138
+ return load_tracked_authored_change_pack(store, commit_id, pack_id).await;
139
+ };
140
+ let (stored_commit_id, stored_pack_id, changes) =
141
+ crate::commit_store::codec::decode_change_pack(&bytes)?;
142
+ ensure_pack_identity(
143
+ "change pack",
144
+ commit_id,
145
+ pack_id,
146
+ &stored_commit_id,
147
+ stored_pack_id,
148
+ )?;
149
+ Ok(Some(changes))
150
+ }
151
+
152
+ pub(crate) async fn load_tracked_authored_change_pack(
153
+ store: &mut (impl StorageReader + ?Sized),
154
+ commit_id: &str,
155
+ pack_id: u32,
156
+ ) -> Result<Option<Vec<Change>>, LixError> {
157
+ let Some(delta_entries) = crate::tracked_state::load_delta_pack(store, commit_id).await? else {
158
+ return Ok(None);
159
+ };
160
+ let mut changes_by_ordinal = BTreeMap::<u32, Change>::new();
161
+ for delta in delta_entries {
162
+ let locator = &delta.value.change_locator;
163
+ if locator.source_commit_id != commit_id || locator.source_pack_id != pack_id {
164
+ continue;
165
+ }
166
+ let ordinal = locator.source_ordinal;
167
+ let change = Change {
168
+ id: locator.change_id.clone(),
169
+ entity_id: delta.key.entity_id,
170
+ schema_key: delta.key.schema_key,
171
+ file_id: delta.key.file_id,
172
+ snapshot_ref: delta.value.snapshot_ref,
173
+ metadata_ref: delta.value.metadata_ref,
174
+ created_at: delta.value.updated_at,
175
+ };
176
+ if changes_by_ordinal.insert(ordinal, change).is_some() {
177
+ return Err(LixError::new(
178
+ LixError::CODE_INTERNAL_ERROR,
179
+ format!(
180
+ "tracked authored change pack ({commit_id}, {pack_id}) has duplicate ordinal {ordinal}"
181
+ ),
182
+ ));
183
+ }
184
+ }
185
+ if changes_by_ordinal.is_empty() {
186
+ return Ok(None);
187
+ }
188
+ let mut changes = Vec::with_capacity(changes_by_ordinal.len());
189
+ for (expected_ordinal, (ordinal, change)) in (0u32..).zip(changes_by_ordinal) {
190
+ if ordinal != expected_ordinal {
191
+ return Err(LixError::new(
192
+ LixError::CODE_INTERNAL_ERROR,
193
+ format!(
194
+ "tracked authored change pack ({commit_id}, {pack_id}) is missing ordinal {expected_ordinal}"
195
+ ),
196
+ ));
197
+ }
198
+ changes.push(change);
199
+ }
200
+ Ok(Some(changes))
201
+ }
202
+
203
+ pub(crate) async fn load_membership_pack(
204
+ store: &mut (impl StorageReader + ?Sized),
205
+ commit_id: &str,
206
+ pack_id: u32,
207
+ ) -> Result<Option<Vec<ChangeLocator>>, LixError> {
208
+ let Some(bytes) = get_one(
209
+ store,
210
+ MEMBERSHIP_PACK_NAMESPACE,
211
+ pack_key(commit_id, pack_id)?,
212
+ )
213
+ .await?
214
+ else {
215
+ return Ok(None);
216
+ };
217
+ let (stored_commit_id, stored_pack_id, members) =
218
+ crate::commit_store::codec::decode_membership_pack(&bytes)?;
219
+ ensure_pack_identity(
220
+ "membership pack",
221
+ commit_id,
222
+ pack_id,
223
+ &stored_commit_id,
224
+ stored_pack_id,
225
+ )?;
226
+ Ok(Some(members))
227
+ }
228
+
229
+ pub(crate) async fn load_change_index_entries(
230
+ store: &mut (impl StorageReader + ?Sized),
231
+ change_ids: &[String],
232
+ ) -> Result<Vec<Option<ChangeIndexEntry>>, LixError> {
233
+ if change_ids.is_empty() {
234
+ return Ok(Vec::new());
235
+ }
236
+
237
+ let mut unresolved = change_ids.iter().cloned().collect::<BTreeSet<_>>();
238
+ let mut entries_by_change_id = BTreeMap::new();
239
+ let commits = scan_commits(store).await?;
240
+ for commit in commits {
241
+ if unresolved.remove(&commit.change_id) {
242
+ entries_by_change_id.insert(
243
+ commit.change_id.clone(),
244
+ ChangeIndexEntry::CommitHeader {
245
+ commit_id: commit.id.clone(),
246
+ change_id: commit.change_id.clone(),
247
+ },
248
+ );
249
+ }
250
+ if unresolved.is_empty() {
251
+ break;
252
+ }
253
+
254
+ for pack_id in 0..commit.change_pack_count {
255
+ let Some(changes) = load_change_pack(store, &commit.id, pack_id).await? else {
256
+ return Err(LixError::new(
257
+ LixError::CODE_INTERNAL_ERROR,
258
+ format!(
259
+ "commit-store missing change pack ({}, {pack_id})",
260
+ commit.id
261
+ ),
262
+ ));
263
+ };
264
+ for (source_ordinal, change) in changes.iter().enumerate() {
265
+ if !unresolved.remove(&change.id) {
266
+ continue;
267
+ }
268
+ entries_by_change_id.insert(
269
+ change.id.clone(),
270
+ ChangeIndexEntry::PackedChange {
271
+ locator: ChangeLocator {
272
+ source_commit_id: commit.id.clone(),
273
+ source_pack_id: pack_id,
274
+ source_ordinal: u32::try_from(source_ordinal).map_err(|_| {
275
+ LixError::new(
276
+ LixError::CODE_INTERNAL_ERROR,
277
+ "commit-store change pack ordinal exceeds u32",
278
+ )
279
+ })?,
280
+ change_id: change.id.clone(),
281
+ },
282
+ },
283
+ );
284
+ if unresolved.is_empty() {
285
+ break;
286
+ }
287
+ }
288
+ if unresolved.is_empty() {
289
+ break;
290
+ }
291
+ }
292
+ if unresolved.is_empty() {
293
+ break;
294
+ }
295
+ }
296
+
297
+ Ok(change_ids
298
+ .iter()
299
+ .map(|change_id| entries_by_change_id.get(change_id).cloned())
300
+ .collect())
301
+ }
302
+
303
+ async fn get_one(
304
+ store: &mut (impl StorageReader + ?Sized),
305
+ namespace: &str,
306
+ key: Vec<u8>,
307
+ ) -> Result<Option<Vec<u8>>, LixError> {
308
+ Ok(store
309
+ .get_values(KvGetRequest {
310
+ groups: vec![KvGetGroup {
311
+ namespace: namespace.to_string(),
312
+ keys: vec![key],
313
+ }],
314
+ })
315
+ .await?
316
+ .groups
317
+ .into_iter()
318
+ .next()
319
+ .and_then(|group| group.single_value_owned()))
320
+ }
321
+
322
+ fn ensure_pack_identity(
323
+ label: &str,
324
+ expected_commit_id: &str,
325
+ expected_pack_id: u32,
326
+ actual_commit_id: &str,
327
+ actual_pack_id: u32,
328
+ ) -> Result<(), LixError> {
329
+ if actual_commit_id != expected_commit_id || actual_pack_id != expected_pack_id {
330
+ return Err(LixError::new(
331
+ LixError::CODE_INTERNAL_ERROR,
332
+ format!(
333
+ "commit-store {label} identity mismatch: expected ({expected_commit_id}, {expected_pack_id}), got ({actual_commit_id}, {actual_pack_id})"
334
+ ),
335
+ ));
336
+ }
337
+ Ok(())
338
+ }
339
+
340
+ fn commit_key(commit_id: &str) -> Vec<u8> {
341
+ commit_id.as_bytes().to_vec()
342
+ }
343
+
344
+ fn pack_key(commit_id: &str, pack_id: u32) -> Result<Vec<u8>, LixError> {
345
+ let commit_id_len = u32::try_from(commit_id.len()).map_err(|_| {
346
+ LixError::new(
347
+ LixError::CODE_INTERNAL_ERROR,
348
+ "commit-store pack key commit id exceeds u32 length",
349
+ )
350
+ })?;
351
+ let mut key = Vec::with_capacity(8 + commit_id.len());
352
+ key.extend_from_slice(&commit_id_len.to_be_bytes());
353
+ key.extend_from_slice(commit_id.as_bytes());
354
+ key.extend_from_slice(&pack_id.to_be_bytes());
355
+ Ok(key)
356
+ }
357
+
358
+ #[cfg(test)]
359
+ mod tests {
360
+ use std::sync::Arc;
361
+
362
+ use crate::backend::testing::UnitTestBackend;
363
+ use crate::commit_store::CommitDraftRef;
364
+ use crate::entity_identity::EntityIdentity;
365
+ use crate::json_store::JsonRef;
366
+ use crate::storage::{StorageContext, StorageWriteTransaction};
367
+ use crate::tracked_state::{TrackedStateContext, TrackedStateDeltaRef};
368
+
369
+ use super::*;
370
+
371
+ #[tokio::test]
372
+ async fn stage_commit_writes_all_commit_store_namespaces() {
373
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
374
+ let mut tx = storage
375
+ .begin_write_transaction()
376
+ .await
377
+ .expect("transaction should open");
378
+ let mut writes = StorageWriteSet::new();
379
+ let commit = test_commit();
380
+ let change = test_change("change-1");
381
+ let adopted = ChangeLocator {
382
+ source_commit_id: "source-commit".to_string(),
383
+ source_pack_id: 3,
384
+ source_ordinal: 7,
385
+ change_id: "adopted-change".to_string(),
386
+ };
387
+
388
+ let staged = stage_commit(
389
+ &mut writes,
390
+ CommitDraftRef {
391
+ id: &commit.id,
392
+ change_id: &commit.change_id,
393
+ parent_ids: &commit.parent_ids,
394
+ author_account_ids: &commit.author_account_ids,
395
+ created_at: &commit.created_at,
396
+ },
397
+ vec![change.as_ref()],
398
+ vec![adopted.clone()],
399
+ )
400
+ .expect("commit should stage");
401
+ writes
402
+ .apply(&mut tx.as_mut())
403
+ .await
404
+ .expect("writes should apply");
405
+ tx.commit().await.expect("commit should succeed");
406
+
407
+ assert_eq!(
408
+ staged.authored_locators,
409
+ vec![ChangeLocator {
410
+ source_commit_id: "commit-1".to_string(),
411
+ source_pack_id: 0,
412
+ source_ordinal: 0,
413
+ change_id: "change-1".to_string(),
414
+ }]
415
+ );
416
+ assert_eq!(staged.adopted_locators, vec![adopted.clone()]);
417
+
418
+ let mut reader = storage.clone();
419
+ assert_eq!(
420
+ load_commit(&mut reader, "commit-1")
421
+ .await
422
+ .expect("commit should load"),
423
+ Some(commit)
424
+ );
425
+ assert_eq!(
426
+ load_change_pack(&mut reader, "commit-1", 0)
427
+ .await
428
+ .expect("change pack should load"),
429
+ Some(vec![change])
430
+ );
431
+ assert_eq!(
432
+ load_membership_pack(&mut reader, "commit-1", 0)
433
+ .await
434
+ .expect("membership pack should load"),
435
+ Some(vec![adopted])
436
+ );
437
+
438
+ let index_entries = load_change_index_entries(
439
+ &mut reader,
440
+ &["commit-change-1".to_string(), "change-1".to_string()],
441
+ )
442
+ .await
443
+ .expect("index entries should load");
444
+ assert_eq!(
445
+ index_entries,
446
+ vec![
447
+ Some(ChangeIndexEntry::CommitHeader {
448
+ commit_id: "commit-1".to_string(),
449
+ change_id: "commit-change-1".to_string(),
450
+ }),
451
+ Some(ChangeIndexEntry::PackedChange {
452
+ locator: ChangeLocator {
453
+ source_commit_id: "commit-1".to_string(),
454
+ source_pack_id: 0,
455
+ source_ordinal: 0,
456
+ change_id: "change-1".to_string(),
457
+ },
458
+ }),
459
+ ]
460
+ );
461
+ }
462
+
463
+ #[tokio::test]
464
+ async fn tracked_commit_change_pack_loads_from_delta_pack() {
465
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
466
+ let mut tx = storage
467
+ .begin_write_transaction()
468
+ .await
469
+ .expect("transaction should open");
470
+ let mut writes = StorageWriteSet::new();
471
+ let commit = test_commit();
472
+ let change = test_change("change-1");
473
+
474
+ let staged = stage_commit_with_external_authored_pack(
475
+ &mut writes,
476
+ CommitDraftRef {
477
+ id: &commit.id,
478
+ change_id: &commit.change_id,
479
+ parent_ids: &commit.parent_ids,
480
+ author_account_ids: &commit.author_account_ids,
481
+ created_at: &commit.created_at,
482
+ },
483
+ vec![change.as_ref()],
484
+ Vec::new(),
485
+ )
486
+ .expect("tracked commit should stage");
487
+ let deltas = [TrackedStateDeltaRef {
488
+ change: change.as_ref(),
489
+ locator: staged.authored_locators[0].as_ref(),
490
+ created_at: "2026-01-01T00:00:00Z",
491
+ updated_at: "2026-01-02T00:00:00Z",
492
+ }];
493
+ TrackedStateContext::new()
494
+ .writer(&mut tx.as_mut(), &mut writes)
495
+ .stage_delta(&commit.id, None, &deltas)
496
+ .await
497
+ .expect("tracked delta should stage");
498
+ writes
499
+ .apply(&mut tx.as_mut())
500
+ .await
501
+ .expect("writes should apply");
502
+ tx.commit().await.expect("commit should succeed");
503
+
504
+ let mut reader = storage.clone();
505
+ assert_eq!(
506
+ get_one(
507
+ &mut reader,
508
+ CHANGE_PACK_NAMESPACE,
509
+ pack_key("commit-1", 0).unwrap()
510
+ )
511
+ .await
512
+ .expect("direct change pack lookup should succeed"),
513
+ None
514
+ );
515
+ assert_eq!(
516
+ load_change_pack(&mut reader, "commit-1", 0)
517
+ .await
518
+ .expect("tracked change pack should load"),
519
+ Some(vec![Change {
520
+ created_at: "2026-01-02T00:00:00Z".to_string(),
521
+ ..change.clone()
522
+ }])
523
+ );
524
+ assert_eq!(
525
+ load_change_index_entries(&mut reader, &["change-1".to_string()])
526
+ .await
527
+ .expect("index entries should load"),
528
+ vec![Some(ChangeIndexEntry::PackedChange {
529
+ locator: staged.authored_locators[0].clone(),
530
+ })]
531
+ );
532
+ }
533
+
534
+ #[tokio::test]
535
+ async fn tracked_commit_change_pack_rejects_sparse_delta_ordinals() {
536
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
537
+ let mut tx = storage
538
+ .begin_write_transaction()
539
+ .await
540
+ .expect("transaction should open");
541
+ let mut writes = StorageWriteSet::new();
542
+ let commit = test_commit();
543
+ let change = test_change("change-1");
544
+ let sparse_locator = ChangeLocator {
545
+ source_commit_id: commit.id.clone(),
546
+ source_pack_id: 0,
547
+ source_ordinal: 1,
548
+ change_id: change.id.clone(),
549
+ };
550
+ let deltas = [TrackedStateDeltaRef {
551
+ change: change.as_ref(),
552
+ locator: sparse_locator.as_ref(),
553
+ created_at: "2026-01-01T00:00:00Z",
554
+ updated_at: "2026-01-02T00:00:00Z",
555
+ }];
556
+ TrackedStateContext::new()
557
+ .writer(&mut tx.as_mut(), &mut writes)
558
+ .stage_delta(&commit.id, None, &deltas)
559
+ .await
560
+ .expect("tracked delta should stage");
561
+ writes
562
+ .apply(&mut tx.as_mut())
563
+ .await
564
+ .expect("writes should apply");
565
+ tx.commit().await.expect("commit should succeed");
566
+
567
+ let mut reader = storage.clone();
568
+ let error = load_change_pack(&mut reader, "commit-1", 0)
569
+ .await
570
+ .expect_err("sparse tracked authored ordinals should reject");
571
+ assert!(
572
+ error.to_string().contains("missing ordinal 0"),
573
+ "error should mention missing ordinal: {error}"
574
+ );
575
+ }
576
+
577
+ fn test_commit() -> Commit {
578
+ Commit {
579
+ id: "commit-1".to_string(),
580
+ change_id: "commit-change-1".to_string(),
581
+ parent_ids: vec!["parent-1".to_string()],
582
+ author_account_ids: Vec::new(),
583
+ created_at: "2026-01-01T00:00:00Z".to_string(),
584
+ change_pack_count: 1,
585
+ membership_pack_count: 1,
586
+ }
587
+ }
588
+
589
+ fn test_change(id: &str) -> Change {
590
+ Change {
591
+ id: id.to_string(),
592
+ entity_id: EntityIdentity::single("entity-1"),
593
+ schema_key: "test_schema".to_string(),
594
+ file_id: None,
595
+ snapshot_ref: Some(JsonRef::from_hash_bytes([1; 32])),
596
+ metadata_ref: None,
597
+ created_at: "2026-01-01T00:00:00Z".to_string(),
598
+ }
599
+ }
600
+ }