@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,944 @@
1
+ use crate::commit_store::{
2
+ Change, ChangeIndexEntry, ChangeLocator, ChangeRef, ChangeScanRequest, Commit, CommitDraftRef,
3
+ LocatedChange, StagedCommitStoreCommit,
4
+ };
5
+ use crate::storage::{StorageReader, StorageWriteSet};
6
+ use crate::LixError;
7
+ use std::collections::{BTreeMap, BTreeSet};
8
+ use tokio::sync::Mutex;
9
+
10
+ /// Canonical physical storage boundary for commits and their changes.
11
+ #[derive(Clone, Copy, Debug, Default)]
12
+ pub(crate) struct CommitStoreContext;
13
+
14
+ impl CommitStoreContext {
15
+ pub(crate) fn new() -> Self {
16
+ Self
17
+ }
18
+
19
+ /// Creates a commit-store writer over read visibility and a pending write set.
20
+ pub(crate) fn writer<'a, S>(
21
+ &self,
22
+ store: &'a mut S,
23
+ writes: &'a mut StorageWriteSet,
24
+ ) -> CommitStoreWriter<'a, S>
25
+ where
26
+ S: StorageReader + ?Sized,
27
+ {
28
+ CommitStoreWriter { store, writes }
29
+ }
30
+
31
+ /// Creates a commit-store reader over a storage snapshot or transaction.
32
+ pub(crate) fn reader<S>(&self, store: S) -> CommitStoreReader<S>
33
+ where
34
+ S: StorageReader,
35
+ {
36
+ CommitStoreReader {
37
+ store: Mutex::new(store),
38
+ }
39
+ }
40
+
41
+ pub(crate) async fn load_commit_from(
42
+ &self,
43
+ store: &mut (impl StorageReader + ?Sized),
44
+ commit_id: &str,
45
+ ) -> Result<Option<Commit>, LixError> {
46
+ crate::commit_store::storage::load_commit(store, commit_id).await
47
+ }
48
+
49
+ pub(crate) async fn load_change_pack_from(
50
+ &self,
51
+ store: &mut (impl StorageReader + ?Sized),
52
+ commit_id: &str,
53
+ pack_id: u32,
54
+ ) -> Result<Option<Vec<Change>>, LixError> {
55
+ crate::commit_store::storage::load_change_pack(store, commit_id, pack_id).await
56
+ }
57
+
58
+ pub(crate) async fn load_membership_pack_from(
59
+ &self,
60
+ store: &mut (impl StorageReader + ?Sized),
61
+ commit_id: &str,
62
+ pack_id: u32,
63
+ ) -> Result<Option<Vec<ChangeLocator>>, LixError> {
64
+ crate::commit_store::storage::load_membership_pack(store, commit_id, pack_id).await
65
+ }
66
+ }
67
+
68
+ /// Commit-store reader over a storage snapshot or transaction.
69
+ pub(crate) struct CommitStoreReader<S> {
70
+ store: Mutex<S>,
71
+ }
72
+
73
+ impl<S> CommitStoreReader<S>
74
+ where
75
+ S: StorageReader,
76
+ {
77
+ pub(crate) async fn load_change_index_entries(
78
+ &self,
79
+ change_ids: &[String],
80
+ ) -> Result<Vec<Option<crate::commit_store::ChangeIndexEntry>>, LixError> {
81
+ crate::commit_store::storage::load_change_index_entries(
82
+ &mut *self.store.lock().await,
83
+ change_ids,
84
+ )
85
+ .await
86
+ }
87
+
88
+ pub(crate) async fn load_commit(
89
+ &self,
90
+ commit_id: &str,
91
+ ) -> Result<Option<crate::commit_store::Commit>, LixError> {
92
+ crate::commit_store::storage::load_commit(&mut *self.store.lock().await, commit_id).await
93
+ }
94
+
95
+ pub(crate) async fn scan_commits(&self) -> Result<Vec<crate::commit_store::Commit>, LixError> {
96
+ crate::commit_store::storage::scan_commits(&mut *self.store.lock().await).await
97
+ }
98
+
99
+ pub(crate) async fn load_change_pack(
100
+ &self,
101
+ commit_id: &str,
102
+ pack_id: u32,
103
+ ) -> Result<Option<Vec<crate::commit_store::Change>>, LixError> {
104
+ crate::commit_store::storage::load_change_pack(
105
+ &mut *self.store.lock().await,
106
+ commit_id,
107
+ pack_id,
108
+ )
109
+ .await
110
+ }
111
+
112
+ pub(crate) async fn load_membership_pack(
113
+ &self,
114
+ commit_id: &str,
115
+ pack_id: u32,
116
+ ) -> Result<Option<Vec<crate::commit_store::ChangeLocator>>, LixError> {
117
+ crate::commit_store::storage::load_membership_pack(
118
+ &mut *self.store.lock().await,
119
+ commit_id,
120
+ pack_id,
121
+ )
122
+ .await
123
+ }
124
+
125
+ pub(crate) async fn load_changes(
126
+ &self,
127
+ change_ids: &[String],
128
+ ) -> Result<Vec<Option<crate::commit_store::Change>>, LixError> {
129
+ if change_ids.is_empty() {
130
+ return Ok(Vec::new());
131
+ }
132
+
133
+ let mut store = self.store.lock().await;
134
+ let entries =
135
+ crate::commit_store::storage::load_change_index_entries(&mut *store, change_ids)
136
+ .await?;
137
+ let mut changes = Vec::with_capacity(entries.len());
138
+ let mut commits_by_id = BTreeMap::new();
139
+ let mut packs_by_locator = BTreeMap::new();
140
+ for (change_id, entry) in change_ids.iter().zip(entries) {
141
+ changes.push(match entry {
142
+ Some(ChangeIndexEntry::CommitHeader { commit_id, .. }) => {
143
+ if !commits_by_id.contains_key(&commit_id) {
144
+ let commit =
145
+ crate::commit_store::storage::load_commit(&mut *store, &commit_id)
146
+ .await?;
147
+ commits_by_id.insert(commit_id.clone(), commit);
148
+ }
149
+ commits_by_id
150
+ .get(&commit_id)
151
+ .cloned()
152
+ .flatten()
153
+ .map(commit_header_change)
154
+ }
155
+ Some(ChangeIndexEntry::PackedChange { locator }) => Some(
156
+ load_change_by_locator_cached(
157
+ &mut *store,
158
+ &mut packs_by_locator,
159
+ &locator,
160
+ change_id,
161
+ )
162
+ .await?,
163
+ ),
164
+ None => None,
165
+ });
166
+ }
167
+ Ok(changes)
168
+ }
169
+
170
+ pub(crate) async fn load_located_changes(
171
+ &self,
172
+ change_ids: &[String],
173
+ ) -> Result<Vec<Option<LocatedChange>>, LixError> {
174
+ if change_ids.is_empty() {
175
+ return Ok(Vec::new());
176
+ }
177
+
178
+ let mut store = self.store.lock().await;
179
+ let entries =
180
+ crate::commit_store::storage::load_change_index_entries(&mut *store, change_ids)
181
+ .await?;
182
+ let mut changes = Vec::with_capacity(entries.len());
183
+ let mut commits_by_id = BTreeMap::new();
184
+ let mut packs_by_locator = BTreeMap::new();
185
+ for (change_id, entry) in change_ids.iter().zip(entries) {
186
+ changes.push(match entry {
187
+ Some(ChangeIndexEntry::CommitHeader { commit_id, .. }) => {
188
+ if !commits_by_id.contains_key(&commit_id) {
189
+ let commit =
190
+ crate::commit_store::storage::load_commit(&mut *store, &commit_id)
191
+ .await?;
192
+ commits_by_id.insert(commit_id.clone(), commit);
193
+ }
194
+ commits_by_id
195
+ .get(&commit_id)
196
+ .cloned()
197
+ .flatten()
198
+ .map(|commit| located_commit_header_change(commit, 0))
199
+ }
200
+ Some(ChangeIndexEntry::PackedChange { locator }) => Some(LocatedChange {
201
+ record: load_change_by_locator_cached(
202
+ &mut *store,
203
+ &mut packs_by_locator,
204
+ &locator,
205
+ change_id,
206
+ )
207
+ .await?,
208
+ source_commit_id: locator.source_commit_id,
209
+ source_pack_id: locator.source_pack_id,
210
+ }),
211
+ None => None,
212
+ });
213
+ }
214
+ Ok(changes)
215
+ }
216
+
217
+ pub(crate) async fn load_commit_changes(
218
+ &self,
219
+ commit_id: &str,
220
+ ) -> Result<Vec<crate::commit_store::Change>, LixError> {
221
+ let mut store = self.store.lock().await;
222
+ let Some(commit) =
223
+ crate::commit_store::storage::load_commit(&mut *store, commit_id).await?
224
+ else {
225
+ return Ok(Vec::new());
226
+ };
227
+
228
+ let mut changes = Vec::new();
229
+ for pack_id in 0..commit.change_pack_count {
230
+ let Some(mut pack_changes) =
231
+ crate::commit_store::storage::load_change_pack(&mut *store, commit_id, pack_id)
232
+ .await?
233
+ else {
234
+ return Err(missing_pack_error("change", commit_id, pack_id));
235
+ };
236
+ changes.append(&mut pack_changes);
237
+ }
238
+
239
+ for pack_id in 0..commit.membership_pack_count {
240
+ let Some(locators) =
241
+ crate::commit_store::storage::load_membership_pack(&mut *store, commit_id, pack_id)
242
+ .await?
243
+ else {
244
+ return Err(missing_pack_error("membership", commit_id, pack_id));
245
+ };
246
+ for locator in locators {
247
+ let change =
248
+ load_change_by_locator(&mut *store, &locator, &locator.change_id).await?;
249
+ changes.push(change);
250
+ }
251
+ }
252
+
253
+ Ok(changes)
254
+ }
255
+
256
+ pub(crate) async fn scan_changes(
257
+ &self,
258
+ request: &ChangeScanRequest,
259
+ ) -> Result<Vec<LocatedChange>, LixError> {
260
+ scan_changes_from_commit_store(&mut *self.store.lock().await, request).await
261
+ }
262
+ }
263
+
264
+ /// Commit-store writer over read visibility and a transaction-local write set.
265
+ pub(crate) struct CommitStoreWriter<'a, S: ?Sized> {
266
+ store: &'a mut S,
267
+ writes: &'a mut StorageWriteSet,
268
+ }
269
+
270
+ struct PendingCommitDraft<'a> {
271
+ commit: CommitDraftRef<'a>,
272
+ authored_changes: Vec<ChangeRef<'a>>,
273
+ adopted_changes: Vec<ChangeRef<'a>>,
274
+ }
275
+
276
+ impl<S> CommitStoreWriter<'_, S>
277
+ where
278
+ S: StorageReader + ?Sized,
279
+ {
280
+ /// Validates and stages canonical commit-store writes for complete commits.
281
+ ///
282
+ /// Callers provide logical commit facts and borrowed change facts. The
283
+ /// commit store owns change-id uniqueness, adoption resolution, pack
284
+ /// locators, and physical namespace writes.
285
+ pub(crate) async fn stage_commit_draft<'a>(
286
+ &mut self,
287
+ commit: CommitDraftRef<'a>,
288
+ authored_changes: Vec<ChangeRef<'a>>,
289
+ adopted_changes: Vec<ChangeRef<'a>>,
290
+ ) -> Result<StagedCommitStoreCommit, LixError> {
291
+ let mut staged = self
292
+ .stage_commit_drafts([(commit, authored_changes, adopted_changes)])
293
+ .await?;
294
+ staged.pop().ok_or_else(|| {
295
+ LixError::new(
296
+ LixError::CODE_INTERNAL_ERROR,
297
+ "commit-store staged no result for one commit draft",
298
+ )
299
+ })
300
+ }
301
+
302
+ /// Validates and stages a tracked commit whose authored rows will be stored
303
+ /// in the tracked-state delta pack instead of a duplicate commit-store pack.
304
+ pub(crate) async fn stage_tracked_commit_draft<'a>(
305
+ &mut self,
306
+ commit: CommitDraftRef<'a>,
307
+ authored_changes: Vec<ChangeRef<'a>>,
308
+ adopted_changes: Vec<ChangeRef<'a>>,
309
+ ) -> Result<StagedCommitStoreCommit, LixError> {
310
+ let mut staged = self
311
+ .stage_tracked_commit_drafts([(commit, authored_changes, adopted_changes)])
312
+ .await?;
313
+ staged.pop().ok_or_else(|| {
314
+ LixError::new(
315
+ LixError::CODE_INTERNAL_ERROR,
316
+ "commit-store staged no result for one tracked commit draft",
317
+ )
318
+ })
319
+ }
320
+
321
+ /// Validates and stages multiple commit drafts as one commit-store batch.
322
+ pub(crate) async fn stage_commit_drafts<'a>(
323
+ &mut self,
324
+ commits: impl IntoIterator<Item = (CommitDraftRef<'a>, Vec<ChangeRef<'a>>, Vec<ChangeRef<'a>>)>,
325
+ ) -> Result<Vec<StagedCommitStoreCommit>, LixError> {
326
+ self.stage_commit_drafts_with_authored_pack(commits, true)
327
+ .await
328
+ }
329
+
330
+ /// Validates and stages multiple tracked commit drafts whose authored rows
331
+ /// will be stored in tracked-state delta packs.
332
+ pub(crate) async fn stage_tracked_commit_drafts<'a>(
333
+ &mut self,
334
+ commits: impl IntoIterator<Item = (CommitDraftRef<'a>, Vec<ChangeRef<'a>>, Vec<ChangeRef<'a>>)>,
335
+ ) -> Result<Vec<StagedCommitStoreCommit>, LixError> {
336
+ self.stage_commit_drafts_with_authored_pack(commits, false)
337
+ .await
338
+ }
339
+
340
+ async fn stage_commit_drafts_with_authored_pack<'a>(
341
+ &mut self,
342
+ commits: impl IntoIterator<Item = (CommitDraftRef<'a>, Vec<ChangeRef<'a>>, Vec<ChangeRef<'a>>)>,
343
+ write_authored_change_pack: bool,
344
+ ) -> Result<Vec<StagedCommitStoreCommit>, LixError> {
345
+ let commits = commits
346
+ .into_iter()
347
+ .map(
348
+ |(commit, authored_changes, adopted_changes)| PendingCommitDraft {
349
+ commit,
350
+ authored_changes,
351
+ adopted_changes,
352
+ },
353
+ )
354
+ .collect::<Vec<_>>();
355
+ let adopted_locators = validate_stage_commits(self.store, &commits).await?;
356
+ let mut staged = Vec::with_capacity(commits.len());
357
+ for commit in commits {
358
+ let mut adopted_changes = Vec::with_capacity(commit.adopted_changes.len());
359
+ for change in &commit.adopted_changes {
360
+ let Some(locator) = adopted_locators.get(change.id) else {
361
+ return Err(LixError::new(
362
+ LixError::CODE_INTERNAL_ERROR,
363
+ format!(
364
+ "validated adopted commit-store change id '{}' has no locator",
365
+ change.id
366
+ ),
367
+ ));
368
+ };
369
+ adopted_changes.push(locator.clone());
370
+ }
371
+ staged.push(if write_authored_change_pack {
372
+ crate::commit_store::storage::stage_commit(
373
+ self.writes,
374
+ commit.commit,
375
+ commit.authored_changes,
376
+ adopted_changes,
377
+ )?
378
+ } else {
379
+ crate::commit_store::storage::stage_commit_with_external_authored_pack(
380
+ self.writes,
381
+ commit.commit,
382
+ commit.authored_changes,
383
+ adopted_changes,
384
+ )?
385
+ });
386
+ }
387
+ Ok(staged)
388
+ }
389
+ }
390
+
391
+ async fn validate_stage_commits<'a>(
392
+ store: &mut (impl StorageReader + ?Sized),
393
+ commits: &[PendingCommitDraft<'a>],
394
+ ) -> Result<BTreeMap<&'a str, ChangeLocator>, LixError> {
395
+ validate_new_changes_absent(store, commits).await?;
396
+ validate_adopted_changes_present(store, commits).await
397
+ }
398
+
399
+ async fn scan_changes_from_commit_store(
400
+ store: &mut (impl StorageReader + ?Sized),
401
+ request: &ChangeScanRequest,
402
+ ) -> Result<Vec<LocatedChange>, LixError> {
403
+ let limit = request.limit.unwrap_or(usize::MAX);
404
+ let commits = crate::commit_store::storage::scan_commits(store).await?;
405
+ let mut changes = Vec::new();
406
+ for commit in commits {
407
+ if changes.len() >= limit {
408
+ break;
409
+ }
410
+ for pack_id in 0..commit.change_pack_count {
411
+ if changes.len() >= limit {
412
+ break;
413
+ }
414
+ let Some(mut pack_changes) =
415
+ crate::commit_store::storage::load_change_pack(store, &commit.id, pack_id).await?
416
+ else {
417
+ return Err(missing_pack_error("change", &commit.id, pack_id));
418
+ };
419
+ let remaining = limit - changes.len();
420
+ if pack_changes.len() > remaining {
421
+ pack_changes.truncate(remaining);
422
+ }
423
+ changes.extend(pack_changes.into_iter().map(|record| LocatedChange {
424
+ record,
425
+ source_commit_id: commit.id.clone(),
426
+ source_pack_id: pack_id,
427
+ }));
428
+ }
429
+ if changes.len() < limit {
430
+ changes.push(located_commit_header_change(commit, 0));
431
+ }
432
+ }
433
+ Ok(changes)
434
+ }
435
+
436
+ async fn load_change_by_locator(
437
+ store: &mut (impl StorageReader + ?Sized),
438
+ locator: &ChangeLocator,
439
+ expected_change_id: &str,
440
+ ) -> Result<Change, LixError> {
441
+ let Some(changes) = crate::commit_store::storage::load_change_pack(
442
+ store,
443
+ &locator.source_commit_id,
444
+ locator.source_pack_id,
445
+ )
446
+ .await?
447
+ else {
448
+ return Err(missing_pack_error(
449
+ "change",
450
+ &locator.source_commit_id,
451
+ locator.source_pack_id,
452
+ ));
453
+ };
454
+ let change = changes
455
+ .get(usize::try_from(locator.source_ordinal).map_err(|_| {
456
+ LixError::new(
457
+ LixError::CODE_INTERNAL_ERROR,
458
+ "commit-store change locator ordinal does not fit usize",
459
+ )
460
+ })?)
461
+ .ok_or_else(|| {
462
+ LixError::new(
463
+ LixError::CODE_INTERNAL_ERROR,
464
+ format!(
465
+ "commit-store change locator for '{}' points past pack '{}' in commit '{}'",
466
+ expected_change_id, locator.source_pack_id, locator.source_commit_id
467
+ ),
468
+ )
469
+ })?;
470
+ if change.id != expected_change_id || change.id != locator.change_id {
471
+ return Err(LixError::new(
472
+ LixError::CODE_INTERNAL_ERROR,
473
+ format!(
474
+ "commit-store change locator expected '{}' but found '{}'",
475
+ expected_change_id, change.id
476
+ ),
477
+ ));
478
+ }
479
+ Ok(change.clone())
480
+ }
481
+
482
+ async fn load_change_by_locator_cached(
483
+ store: &mut (impl StorageReader + ?Sized),
484
+ packs_by_locator: &mut BTreeMap<(String, u32), Vec<Change>>,
485
+ locator: &ChangeLocator,
486
+ expected_change_id: &str,
487
+ ) -> Result<Change, LixError> {
488
+ let key = (locator.source_commit_id.clone(), locator.source_pack_id);
489
+ if !packs_by_locator.contains_key(&key) {
490
+ let Some(changes) = crate::commit_store::storage::load_change_pack(
491
+ store,
492
+ &locator.source_commit_id,
493
+ locator.source_pack_id,
494
+ )
495
+ .await?
496
+ else {
497
+ return Err(missing_pack_error(
498
+ "change",
499
+ &locator.source_commit_id,
500
+ locator.source_pack_id,
501
+ ));
502
+ };
503
+ packs_by_locator.insert(key.clone(), changes);
504
+ }
505
+ let changes = packs_by_locator.get(&key).ok_or_else(|| {
506
+ LixError::new(
507
+ LixError::CODE_INTERNAL_ERROR,
508
+ "commit-store change pack cache lost a loaded pack",
509
+ )
510
+ })?;
511
+ let change = changes
512
+ .get(usize::try_from(locator.source_ordinal).map_err(|_| {
513
+ LixError::new(
514
+ LixError::CODE_INTERNAL_ERROR,
515
+ "commit-store change locator ordinal does not fit usize",
516
+ )
517
+ })?)
518
+ .ok_or_else(|| {
519
+ LixError::new(
520
+ LixError::CODE_INTERNAL_ERROR,
521
+ format!(
522
+ "commit-store change locator for '{}' points past pack '{}' in commit '{}'",
523
+ expected_change_id, locator.source_pack_id, locator.source_commit_id
524
+ ),
525
+ )
526
+ })?;
527
+ if change.id != expected_change_id || change.id != locator.change_id {
528
+ return Err(LixError::new(
529
+ LixError::CODE_INTERNAL_ERROR,
530
+ format!(
531
+ "commit-store change locator expected '{}' but found '{}'",
532
+ expected_change_id, change.id
533
+ ),
534
+ ));
535
+ }
536
+ Ok(change.clone())
537
+ }
538
+
539
+ fn commit_header_change(commit: Commit) -> Change {
540
+ Change {
541
+ id: commit.change_id,
542
+ entity_id: crate::entity_identity::EntityIdentity::single(commit.id),
543
+ schema_key: "lix_commit".to_string(),
544
+ file_id: None,
545
+ snapshot_ref: None,
546
+ metadata_ref: None,
547
+ created_at: commit.created_at,
548
+ }
549
+ }
550
+
551
+ fn located_commit_header_change(commit: Commit, source_pack_id: u32) -> LocatedChange {
552
+ let source_commit_id = commit.id.clone();
553
+ LocatedChange {
554
+ record: commit_header_change(commit),
555
+ source_commit_id,
556
+ source_pack_id,
557
+ }
558
+ }
559
+
560
+ fn missing_pack_error(label: &str, commit_id: &str, pack_id: u32) -> LixError {
561
+ LixError::new(
562
+ LixError::CODE_INTERNAL_ERROR,
563
+ format!("commit-store missing {label} pack ({commit_id}, {pack_id})"),
564
+ )
565
+ }
566
+
567
+ async fn validate_new_changes_absent<'a>(
568
+ store: &mut (impl StorageReader + ?Sized),
569
+ commits: &[PendingCommitDraft<'a>],
570
+ ) -> Result<(), LixError> {
571
+ let mut change_ids = Vec::new();
572
+ let mut seen_change_ids = BTreeSet::new();
573
+ for commit in commits {
574
+ if !seen_change_ids.insert(commit.commit.change_id) {
575
+ return Err(duplicate_change_id_error(commit.commit.change_id));
576
+ }
577
+ change_ids.push(commit.commit.change_id.to_string());
578
+ for change in &commit.authored_changes {
579
+ if !seen_change_ids.insert(change.id) {
580
+ return Err(duplicate_change_id_error(change.id));
581
+ }
582
+ change_ids.push(change.id.to_string());
583
+ }
584
+ }
585
+
586
+ let reader = CommitStoreContext::new().reader(&mut *store);
587
+ let existing_changes = reader.load_change_index_entries(&change_ids).await?;
588
+ for (change_id, existing) in change_ids.iter().zip(existing_changes) {
589
+ if existing.is_some() {
590
+ return Err(LixError::new(
591
+ LixError::CODE_UNIQUE,
592
+ format!("commit-store change id '{}' already exists", change_id),
593
+ ));
594
+ }
595
+ }
596
+ Ok(())
597
+ }
598
+
599
+ async fn validate_adopted_changes_present<'a>(
600
+ store: &mut (impl StorageReader + ?Sized),
601
+ commits: &[PendingCommitDraft<'a>],
602
+ ) -> Result<BTreeMap<&'a str, ChangeLocator>, LixError> {
603
+ let mut expected_changes = Vec::new();
604
+ let mut seen_change_ids = BTreeSet::new();
605
+ for commit in commits {
606
+ for change in &commit.adopted_changes {
607
+ if !seen_change_ids.insert(change.id) {
608
+ return Err(LixError::new(
609
+ LixError::CODE_UNIQUE,
610
+ format!(
611
+ "adopted commit-store change id '{}' appears more than once in the same transaction",
612
+ change.id
613
+ ),
614
+ ));
615
+ }
616
+ expected_changes.push(*change);
617
+ }
618
+ }
619
+ if expected_changes.is_empty() {
620
+ return Ok(BTreeMap::new());
621
+ }
622
+
623
+ let change_ids = expected_changes
624
+ .iter()
625
+ .map(|change| change.id.to_string())
626
+ .collect::<Vec<_>>();
627
+ let reader = CommitStoreContext::new().reader(&mut *store);
628
+ let existing_entries = reader.load_change_index_entries(&change_ids).await?;
629
+ let mut locators_by_change_id = BTreeMap::new();
630
+ for (expected, existing) in expected_changes.into_iter().zip(existing_entries) {
631
+ match existing {
632
+ Some(ChangeIndexEntry::PackedChange { locator }) => {
633
+ let existing_change = load_packed_change(&reader, &locator, expected.id).await?;
634
+ if !change_matches_ref(&existing_change, expected) {
635
+ let entity_id = existing_change
636
+ .entity_id
637
+ .as_json_array_text()
638
+ .unwrap_or_else(|_| "<invalid entity_id>".to_string());
639
+ return Err(LixError::new(
640
+ LixError::CODE_UNIQUE,
641
+ format!(
642
+ "adopted commit-store change id '{}' exists with different content for schema '{}' entity '{}'",
643
+ expected.id, existing_change.schema_key, entity_id
644
+ ),
645
+ ));
646
+ }
647
+ locators_by_change_id.insert(expected.id, locator);
648
+ }
649
+ Some(ChangeIndexEntry::CommitHeader { .. }) => {
650
+ return Err(LixError::new(
651
+ LixError::CODE_INTERNAL_ERROR,
652
+ format!(
653
+ "adopted commit-store change id '{}' resolves to a commit header, not a packed state change",
654
+ expected.id
655
+ ),
656
+ ));
657
+ }
658
+ None => {
659
+ return Err(LixError::new(
660
+ LixError::CODE_INTERNAL_ERROR,
661
+ format!(
662
+ "adopted commit-store change id '{}' does not exist",
663
+ expected.id
664
+ ),
665
+ ));
666
+ }
667
+ }
668
+ }
669
+ Ok(locators_by_change_id)
670
+ }
671
+
672
+ async fn load_packed_change<S>(
673
+ reader: &CommitStoreReader<S>,
674
+ locator: &ChangeLocator,
675
+ expected_change_id: &str,
676
+ ) -> Result<Change, LixError>
677
+ where
678
+ S: StorageReader,
679
+ {
680
+ let pack = reader
681
+ .load_change_pack(&locator.source_commit_id, locator.source_pack_id)
682
+ .await?
683
+ .ok_or_else(|| {
684
+ LixError::new(
685
+ LixError::CODE_INTERNAL_ERROR,
686
+ format!(
687
+ "commit-store change pack '{}:{}' for change '{}' is missing",
688
+ locator.source_commit_id, locator.source_pack_id, expected_change_id
689
+ ),
690
+ )
691
+ })?;
692
+ let change = pack
693
+ .get(usize::try_from(locator.source_ordinal).map_err(|_| {
694
+ LixError::new(
695
+ LixError::CODE_INTERNAL_ERROR,
696
+ "commit-store change locator ordinal exceeds usize",
697
+ )
698
+ })?)
699
+ .ok_or_else(|| {
700
+ LixError::new(
701
+ LixError::CODE_INTERNAL_ERROR,
702
+ format!(
703
+ "commit-store change locator '{}' points past pack length",
704
+ expected_change_id
705
+ ),
706
+ )
707
+ })?
708
+ .clone();
709
+ if change.id != expected_change_id {
710
+ return Err(LixError::new(
711
+ LixError::CODE_INTERNAL_ERROR,
712
+ format!(
713
+ "commit-store change locator expected '{}' but loaded '{}'",
714
+ expected_change_id, change.id
715
+ ),
716
+ ));
717
+ }
718
+ Ok(change)
719
+ }
720
+
721
+ fn change_matches_ref(change: &Change, expected: ChangeRef<'_>) -> bool {
722
+ change.id == expected.id
723
+ && &change.entity_id == expected.entity_id
724
+ && change.schema_key == expected.schema_key
725
+ && change.file_id.as_deref() == expected.file_id
726
+ && change.snapshot_ref.as_ref() == expected.snapshot_ref
727
+ && change.metadata_ref.as_ref() == expected.metadata_ref
728
+ && change.created_at == expected.created_at
729
+ }
730
+
731
+ fn duplicate_change_id_error(change_id: &str) -> LixError {
732
+ LixError::new(
733
+ LixError::CODE_UNIQUE,
734
+ format!(
735
+ "commit-store change id '{}' appears more than once in the same transaction",
736
+ change_id
737
+ ),
738
+ )
739
+ }
740
+
741
+ #[cfg(test)]
742
+ mod tests {
743
+ use std::sync::Arc;
744
+
745
+ use crate::backend::testing::UnitTestBackend;
746
+ use crate::commit_store::{
747
+ ChangeIndexEntry, ChangeLocator, CommitDraftRef, CommitStoreContext,
748
+ };
749
+ use crate::entity_identity::EntityIdentity;
750
+ use crate::json_store::JsonRef;
751
+ use crate::storage::{StorageContext, StorageWriteSet, StorageWriteTransaction};
752
+
753
+ use super::*;
754
+
755
+ #[tokio::test]
756
+ async fn load_changes_materializes_commit_header_and_packed_change() {
757
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
758
+ let mut transaction = storage
759
+ .begin_write_transaction()
760
+ .await
761
+ .expect("transaction should open");
762
+ let mut writes = StorageWriteSet::new();
763
+ let parent_ids = vec!["parent-1".to_string()];
764
+ let author_account_ids = vec!["author-1".to_string()];
765
+ let commit_id = "commit-1".to_string();
766
+ let commit_change_id = "commit-change-1".to_string();
767
+ let authored_change = test_change("change-1");
768
+
769
+ CommitStoreContext::new()
770
+ .writer(transaction.as_mut(), &mut writes)
771
+ .stage_commit_draft(
772
+ CommitDraftRef {
773
+ id: &commit_id,
774
+ change_id: &commit_change_id,
775
+ parent_ids: &parent_ids,
776
+ author_account_ids: &author_account_ids,
777
+ created_at: "2026-01-01T00:00:00Z",
778
+ },
779
+ vec![authored_change.as_ref()],
780
+ Vec::new(),
781
+ )
782
+ .await
783
+ .expect("commit should stage");
784
+ writes
785
+ .apply(&mut transaction.as_mut())
786
+ .await
787
+ .expect("writes should apply");
788
+ transaction.commit().await.expect("commit should persist");
789
+
790
+ let reader = CommitStoreContext::new().reader(storage.clone());
791
+ let index_entries = reader
792
+ .load_change_index_entries(&[
793
+ commit_change_id.clone(),
794
+ authored_change.id.clone(),
795
+ "missing-change".to_string(),
796
+ ])
797
+ .await
798
+ .expect("index entries should load");
799
+ assert_eq!(
800
+ index_entries,
801
+ vec![
802
+ Some(ChangeIndexEntry::CommitHeader {
803
+ commit_id: commit_id.clone(),
804
+ change_id: commit_change_id.clone(),
805
+ }),
806
+ Some(ChangeIndexEntry::PackedChange {
807
+ locator: ChangeLocator {
808
+ source_commit_id: commit_id.clone(),
809
+ source_pack_id: 0,
810
+ source_ordinal: 0,
811
+ change_id: authored_change.id.clone(),
812
+ },
813
+ }),
814
+ None,
815
+ ]
816
+ );
817
+
818
+ let changes = reader
819
+ .load_changes(&[
820
+ commit_change_id.clone(),
821
+ authored_change.id.clone(),
822
+ "missing-change".to_string(),
823
+ ])
824
+ .await
825
+ .expect("changes should load");
826
+ assert_eq!(changes.len(), 3);
827
+
828
+ let header_change = changes[0]
829
+ .as_ref()
830
+ .expect("commit-header change should materialize");
831
+ assert_eq!(header_change.id, commit_change_id);
832
+ assert_eq!(header_change.entity_id, EntityIdentity::single(&commit_id));
833
+ assert_eq!(header_change.schema_key, "lix_commit");
834
+ assert_eq!(header_change.file_id, None);
835
+ assert_eq!(header_change.snapshot_ref, None);
836
+ assert_eq!(header_change.metadata_ref, None);
837
+ assert_eq!(header_change.created_at, "2026-01-01T00:00:00Z");
838
+
839
+ assert_eq!(
840
+ changes[1]
841
+ .as_ref()
842
+ .expect("packed change should decode from change pack"),
843
+ &authored_change
844
+ );
845
+ assert_eq!(changes[2], None);
846
+ }
847
+
848
+ #[tokio::test]
849
+ async fn load_commit_changes_returns_equivalent_authored_and_adopted_changes() {
850
+ let storage = StorageContext::new(Arc::new(UnitTestBackend::new()));
851
+ let authored_change = test_change("shared-change-1");
852
+
853
+ stage_test_commit(
854
+ storage.clone(),
855
+ "source-commit",
856
+ "source-commit-change",
857
+ vec![authored_change.as_ref()],
858
+ Vec::new(),
859
+ )
860
+ .await;
861
+ stage_test_commit(
862
+ storage.clone(),
863
+ "adopting-commit",
864
+ "adopting-commit-change",
865
+ Vec::new(),
866
+ vec![authored_change.as_ref()],
867
+ )
868
+ .await;
869
+
870
+ let reader = CommitStoreContext::new().reader(storage.clone());
871
+ let source_changes = reader
872
+ .load_commit_changes("source-commit")
873
+ .await
874
+ .expect("source commit changes should load");
875
+ let adopting_changes = reader
876
+ .load_commit_changes("adopting-commit")
877
+ .await
878
+ .expect("adopting commit changes should load");
879
+
880
+ assert_eq!(source_changes, vec![authored_change.clone()]);
881
+ assert_eq!(adopting_changes, source_changes);
882
+ assert_eq!(
883
+ reader
884
+ .load_membership_pack("adopting-commit", 0)
885
+ .await
886
+ .expect("membership pack should load"),
887
+ Some(vec![ChangeLocator {
888
+ source_commit_id: "source-commit".to_string(),
889
+ source_pack_id: 0,
890
+ source_ordinal: 0,
891
+ change_id: authored_change.id.clone(),
892
+ }])
893
+ );
894
+ }
895
+
896
+ async fn stage_test_commit(
897
+ storage: StorageContext,
898
+ commit_id: &str,
899
+ commit_change_id: &str,
900
+ authored_changes: Vec<ChangeRef<'_>>,
901
+ adopted_changes: Vec<ChangeRef<'_>>,
902
+ ) {
903
+ let mut transaction = storage
904
+ .begin_write_transaction()
905
+ .await
906
+ .expect("transaction should open");
907
+ let mut writes = StorageWriteSet::new();
908
+ let parent_ids = Vec::new();
909
+ let author_account_ids = Vec::new();
910
+
911
+ CommitStoreContext::new()
912
+ .writer(transaction.as_mut(), &mut writes)
913
+ .stage_commit_draft(
914
+ CommitDraftRef {
915
+ id: commit_id,
916
+ change_id: commit_change_id,
917
+ parent_ids: &parent_ids,
918
+ author_account_ids: &author_account_ids,
919
+ created_at: "2026-01-01T00:00:00Z",
920
+ },
921
+ authored_changes,
922
+ adopted_changes,
923
+ )
924
+ .await
925
+ .expect("commit should stage");
926
+ writes
927
+ .apply(&mut transaction.as_mut())
928
+ .await
929
+ .expect("writes should apply");
930
+ transaction.commit().await.expect("commit should persist");
931
+ }
932
+
933
+ fn test_change(id: &str) -> Change {
934
+ Change {
935
+ id: id.to_string(),
936
+ entity_id: EntityIdentity::single("entity-1"),
937
+ schema_key: "test_schema".to_string(),
938
+ file_id: Some("file-1".to_string()),
939
+ snapshot_ref: Some(JsonRef::from_hash_bytes([1; 32])),
940
+ metadata_ref: Some(JsonRef::from_hash_bytes([2; 32])),
941
+ created_at: "2026-01-02T00:00:00Z".to_string(),
942
+ }
943
+ }
944
+ }