@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,488 @@
1
+ use crate::commit_store::{Change, ChangeLocator, Commit, CommitStoreContext};
2
+ use crate::storage::StorageReader;
3
+ use crate::tracked_state::context::{TrackedStateMaterializer, TrackedStateWriteReport};
4
+ use crate::tracked_state::types::TrackedStateKey;
5
+ use crate::tracked_state::TrackedStateDeltaRef;
6
+ use crate::LixError;
7
+ use std::collections::{BTreeMap, BTreeSet};
8
+
9
+ /// Owned materialization delta used only by explicit projection-root hydration.
10
+ ///
11
+ /// Normal transaction commits already have borrowed `ChangeRef` and
12
+ /// `ChangeLocatorRef` values available while staging commit_store.
13
+ /// Materialization loads those facts back from storage, so it owns the decoded
14
+ /// data internally and immediately passes a borrowed view into the same
15
+ /// tracked-state root writer.
16
+ #[derive(Debug, Clone, PartialEq, Eq)]
17
+ pub(crate) struct MaterializationDelta {
18
+ pub(crate) change: Change,
19
+ pub(crate) locator: ChangeLocator,
20
+ pub(crate) created_at: String,
21
+ pub(crate) updated_at: String,
22
+ }
23
+
24
+ impl MaterializationDelta {
25
+ pub(crate) fn as_ref(&self) -> TrackedStateDeltaRef<'_> {
26
+ TrackedStateDeltaRef {
27
+ change: self.change.as_ref(),
28
+ locator: self.locator.as_ref(),
29
+ created_at: &self.created_at,
30
+ updated_at: &self.updated_at,
31
+ }
32
+ }
33
+ }
34
+
35
+ #[derive(Debug, Clone, PartialEq, Eq)]
36
+ pub(crate) struct MaterializationInput {
37
+ pub(crate) commit_id: String,
38
+ pub(crate) parent_commit_id: Option<String>,
39
+ pub(crate) deltas: Vec<MaterializationDelta>,
40
+ }
41
+
42
+ struct LocatedChange {
43
+ locator: ChangeLocator,
44
+ change: Change,
45
+ }
46
+
47
+ /// Explicit projection-root materialization over commit_store.
48
+ ///
49
+ /// Normal transaction commits must use `TrackedStateWriter::stage_delta` with
50
+ /// already prepared commit_store refs. This path exists for deliberate
51
+ /// materialization only.
52
+ pub(crate) async fn materialize_root_at<S>(
53
+ materializer: &mut TrackedStateMaterializer<'_, S>,
54
+ commit_id: &str,
55
+ ) -> Result<TrackedStateWriteReport, LixError>
56
+ where
57
+ S: StorageReader + ?Sized,
58
+ {
59
+ let input =
60
+ build_materialization_input(materializer.store, materializer.commit_store, commit_id)
61
+ .await?;
62
+ let delta_refs = input
63
+ .deltas
64
+ .iter()
65
+ .map(MaterializationDelta::as_ref)
66
+ .collect::<Vec<_>>();
67
+ materializer
68
+ .tracked_state
69
+ .writer(materializer.store, materializer.writes)
70
+ .stage_projection_root(
71
+ &input.commit_id,
72
+ input.parent_commit_id.as_deref(),
73
+ delta_refs,
74
+ )
75
+ .await
76
+ }
77
+
78
+ async fn build_materialization_input<S>(
79
+ store: &mut S,
80
+ commit_store: &CommitStoreContext,
81
+ commit_id: &str,
82
+ ) -> Result<MaterializationInput, LixError>
83
+ where
84
+ S: StorageReader + ?Sized,
85
+ {
86
+ let lineage = load_first_parent_lineage(store, commit_store, commit_id).await?;
87
+ let mut located_changes = Vec::new();
88
+ for commit in lineage {
89
+ located_changes
90
+ .append(&mut load_commit_located_changes(store, commit_store, &commit).await?);
91
+ }
92
+ let deltas = project_materialization_deltas(located_changes);
93
+
94
+ Ok(MaterializationInput {
95
+ commit_id: commit_id.to_string(),
96
+ parent_commit_id: None,
97
+ deltas,
98
+ })
99
+ }
100
+
101
+ async fn load_first_parent_lineage<S>(
102
+ store: &mut S,
103
+ commit_store: &CommitStoreContext,
104
+ commit_id: &str,
105
+ ) -> Result<Vec<Commit>, LixError>
106
+ where
107
+ S: StorageReader + ?Sized,
108
+ {
109
+ let mut lineage = Vec::new();
110
+ let mut seen = BTreeSet::new();
111
+ let mut current = Some(commit_id.to_string());
112
+ while let Some(current_id) = current {
113
+ if !seen.insert(current_id.clone()) {
114
+ return Err(LixError::new(
115
+ LixError::CODE_INTERNAL_ERROR,
116
+ format!(
117
+ "tracked_state materialization found first-parent cycle at commit '{current_id}'"
118
+ ),
119
+ ));
120
+ }
121
+ let commit = commit_store
122
+ .load_commit_from(store, &current_id)
123
+ .await?
124
+ .ok_or_else(|| missing_commit_error(&current_id))?;
125
+ current = commit.parent_ids.first().cloned();
126
+ lineage.push(commit);
127
+ }
128
+ lineage.reverse();
129
+ Ok(lineage)
130
+ }
131
+
132
+ async fn load_commit_located_changes<S>(
133
+ store: &mut S,
134
+ commit_store: &CommitStoreContext,
135
+ commit: &Commit,
136
+ ) -> Result<Vec<LocatedChange>, LixError>
137
+ where
138
+ S: StorageReader + ?Sized,
139
+ {
140
+ let mut located_changes = Vec::new();
141
+ for pack_id in 0..commit.change_pack_count {
142
+ let changes = commit_store
143
+ .load_change_pack_from(store, &commit.id, pack_id)
144
+ .await?
145
+ .ok_or_else(|| missing_pack_error("change", &commit.id, pack_id))?;
146
+ for (source_ordinal, change) in changes.into_iter().enumerate() {
147
+ let locator = ChangeLocator {
148
+ source_commit_id: commit.id.clone(),
149
+ source_pack_id: pack_id,
150
+ source_ordinal: u32::try_from(source_ordinal).map_err(|_| {
151
+ LixError::new(
152
+ LixError::CODE_INTERNAL_ERROR,
153
+ "tracked_state materialization change pack ordinal exceeds u32",
154
+ )
155
+ })?,
156
+ change_id: change.id.clone(),
157
+ };
158
+ located_changes.push(LocatedChange { locator, change });
159
+ }
160
+ }
161
+
162
+ let mut adopted_locators = Vec::new();
163
+ for pack_id in 0..commit.membership_pack_count {
164
+ let mut locators = commit_store
165
+ .load_membership_pack_from(store, &commit.id, pack_id)
166
+ .await?
167
+ .ok_or_else(|| missing_pack_error("membership", &commit.id, pack_id))?;
168
+ adopted_locators.append(&mut locators);
169
+ }
170
+ let adopted_changes = load_changes_by_locators(store, commit_store, &adopted_locators).await?;
171
+ located_changes.extend(
172
+ adopted_locators
173
+ .into_iter()
174
+ .zip(adopted_changes)
175
+ .map(|(locator, change)| LocatedChange { locator, change }),
176
+ );
177
+ Ok(located_changes)
178
+ }
179
+
180
+ fn project_materialization_deltas(
181
+ changes: impl IntoIterator<Item = LocatedChange>,
182
+ ) -> Vec<MaterializationDelta> {
183
+ let mut projected = BTreeMap::<TrackedStateKey, MaterializationDelta>::new();
184
+ for LocatedChange { locator, change } in changes {
185
+ let key = TrackedStateKey {
186
+ schema_key: change.schema_key.clone(),
187
+ file_id: change.file_id.clone(),
188
+ entity_id: change.entity_id.clone(),
189
+ };
190
+ let created_at = projected
191
+ .get(&key)
192
+ .map(|delta| delta.created_at.clone())
193
+ .unwrap_or_else(|| change.created_at.clone());
194
+ let updated_at = change.created_at.clone();
195
+ projected.insert(
196
+ key,
197
+ MaterializationDelta {
198
+ change,
199
+ locator,
200
+ created_at,
201
+ updated_at,
202
+ },
203
+ );
204
+ }
205
+ projected.into_values().collect()
206
+ }
207
+
208
+ async fn load_changes_by_locators(
209
+ store: &mut (impl StorageReader + ?Sized),
210
+ commit_store: &CommitStoreContext,
211
+ locators: &[ChangeLocator],
212
+ ) -> Result<Vec<Change>, LixError> {
213
+ let mut packs = BTreeMap::<(String, u32), Vec<Change>>::new();
214
+ for locator in locators {
215
+ let key = (locator.source_commit_id.clone(), locator.source_pack_id);
216
+ if packs.contains_key(&key) {
217
+ continue;
218
+ }
219
+ let changes = commit_store
220
+ .load_change_pack_from(store, &locator.source_commit_id, locator.source_pack_id)
221
+ .await?
222
+ .ok_or_else(|| {
223
+ missing_pack_error("change", &locator.source_commit_id, locator.source_pack_id)
224
+ })?;
225
+ packs.insert(key, changes);
226
+ }
227
+
228
+ locators
229
+ .iter()
230
+ .map(|locator| change_from_loaded_packs(&packs, locator))
231
+ .collect()
232
+ }
233
+
234
+ fn change_from_loaded_packs(
235
+ packs: &BTreeMap<(String, u32), Vec<Change>>,
236
+ locator: &ChangeLocator,
237
+ ) -> Result<Change, LixError> {
238
+ let key = (locator.source_commit_id.clone(), locator.source_pack_id);
239
+ let changes = packs.get(&key).ok_or_else(|| {
240
+ LixError::new(
241
+ LixError::CODE_INTERNAL_ERROR,
242
+ format!(
243
+ "tracked_state materialization lost loaded change pack ({}, {})",
244
+ locator.source_commit_id, locator.source_pack_id
245
+ ),
246
+ )
247
+ })?;
248
+ let change = changes
249
+ .get(usize::try_from(locator.source_ordinal).map_err(|_| {
250
+ LixError::new(
251
+ LixError::CODE_INTERNAL_ERROR,
252
+ "tracked_state materialization locator ordinal does not fit usize",
253
+ )
254
+ })?)
255
+ .ok_or_else(|| {
256
+ LixError::new(
257
+ LixError::CODE_INTERNAL_ERROR,
258
+ format!(
259
+ "tracked_state materialization locator for '{}' points past pack ({}, {})",
260
+ locator.change_id, locator.source_commit_id, locator.source_pack_id
261
+ ),
262
+ )
263
+ })?;
264
+ if change.id != locator.change_id {
265
+ return Err(LixError::new(
266
+ LixError::CODE_INTERNAL_ERROR,
267
+ format!(
268
+ "tracked_state materialization locator expected '{}' but found '{}'",
269
+ locator.change_id, change.id
270
+ ),
271
+ ));
272
+ }
273
+ Ok(change.clone())
274
+ }
275
+
276
+ fn missing_pack_error(label: &str, commit_id: &str, pack_id: u32) -> LixError {
277
+ LixError::new(
278
+ LixError::CODE_INTERNAL_ERROR,
279
+ format!("tracked_state materialization missing {label} pack ({commit_id}, {pack_id})"),
280
+ )
281
+ }
282
+
283
+ fn missing_commit_error(commit_id: &str) -> LixError {
284
+ LixError::new(
285
+ LixError::CODE_INTERNAL_ERROR,
286
+ format!("tracked_state materialization missing commit '{commit_id}'"),
287
+ )
288
+ }
289
+
290
+ #[cfg(test)]
291
+ mod tests {
292
+ use super::*;
293
+ use crate::commit_store::ChangeLocator;
294
+ use crate::entity_identity::EntityIdentity;
295
+
296
+ #[test]
297
+ fn materialization_delta_ref_borrows_owned_facts() {
298
+ let delta = MaterializationDelta {
299
+ change: Change {
300
+ id: "change-1".to_string(),
301
+ entity_id: EntityIdentity::single("entity-1"),
302
+ schema_key: "schema".to_string(),
303
+ file_id: Some("file".to_string()),
304
+ snapshot_ref: None,
305
+ metadata_ref: None,
306
+ created_at: "2026-01-01T00:00:00Z".to_string(),
307
+ },
308
+ locator: ChangeLocator {
309
+ source_commit_id: "commit-1".to_string(),
310
+ source_pack_id: 7,
311
+ source_ordinal: 11,
312
+ change_id: "change-1".to_string(),
313
+ },
314
+ created_at: "2026-01-01T00:00:00Z".to_string(),
315
+ updated_at: "2026-02-01T00:00:00Z".to_string(),
316
+ };
317
+
318
+ let delta_ref = delta.as_ref();
319
+
320
+ assert_eq!(delta_ref.change.id, "change-1");
321
+ assert_eq!(delta_ref.change.schema_key, "schema");
322
+ assert_eq!(delta_ref.change.file_id, Some("file"));
323
+ assert_eq!(delta_ref.locator.source_commit_id, "commit-1");
324
+ assert_eq!(delta_ref.locator.source_pack_id, 7);
325
+ assert_eq!(delta_ref.locator.source_ordinal, 11);
326
+ assert_eq!(delta_ref.created_at, "2026-01-01T00:00:00Z");
327
+ assert_eq!(delta_ref.updated_at, "2026-02-01T00:00:00Z");
328
+ }
329
+
330
+ #[test]
331
+ fn change_from_loaded_packs_resolves_locator_by_pack_and_ordinal() {
332
+ let mut packs = BTreeMap::new();
333
+ packs.insert(
334
+ ("source-commit".to_string(), 3),
335
+ vec![change("change-0"), change("change-1"), change("change-2")],
336
+ );
337
+ let locator = ChangeLocator {
338
+ source_commit_id: "source-commit".to_string(),
339
+ source_pack_id: 3,
340
+ source_ordinal: 1,
341
+ change_id: "change-1".to_string(),
342
+ };
343
+
344
+ let resolved = change_from_loaded_packs(&packs, &locator).expect("locator should resolve");
345
+
346
+ assert_eq!(resolved.id, "change-1");
347
+ }
348
+
349
+ #[test]
350
+ fn change_from_loaded_packs_rejects_locator_change_id_mismatch() {
351
+ let mut packs = BTreeMap::new();
352
+ packs.insert(("source-commit".to_string(), 3), vec![change("actual")]);
353
+ let locator = ChangeLocator {
354
+ source_commit_id: "source-commit".to_string(),
355
+ source_pack_id: 3,
356
+ source_ordinal: 0,
357
+ change_id: "expected".to_string(),
358
+ };
359
+
360
+ let error =
361
+ change_from_loaded_packs(&packs, &locator).expect_err("mismatched locator should fail");
362
+
363
+ assert!(error.message.contains("expected"));
364
+ assert!(error.message.contains("actual"));
365
+ }
366
+
367
+ #[test]
368
+ fn project_materialization_deltas_keeps_first_seen_created_at_and_latest_updated_at() {
369
+ let deltas = project_materialization_deltas(vec![
370
+ located_change(
371
+ "commit-1",
372
+ 0,
373
+ "change-create",
374
+ "entity-1",
375
+ "2026-01-01T00:00:00Z",
376
+ ),
377
+ located_change(
378
+ "commit-2",
379
+ 0,
380
+ "change-update",
381
+ "entity-1",
382
+ "2026-02-01T00:00:00Z",
383
+ ),
384
+ ]);
385
+
386
+ assert_eq!(deltas.len(), 1);
387
+ let delta = &deltas[0];
388
+ assert_eq!(delta.change.id, "change-update");
389
+ assert_eq!(delta.locator.source_commit_id, "commit-2");
390
+ assert_eq!(delta.created_at, "2026-01-01T00:00:00Z");
391
+ assert_eq!(delta.updated_at, "2026-02-01T00:00:00Z");
392
+ }
393
+
394
+ #[test]
395
+ fn project_materialization_deltas_uses_adopted_change_time_not_target_commit_time() {
396
+ let deltas = project_materialization_deltas(vec![located_change(
397
+ "source-commit",
398
+ 0,
399
+ "adopted-change",
400
+ "entity-1",
401
+ "2026-01-01T00:00:00Z",
402
+ )]);
403
+
404
+ assert_eq!(deltas.len(), 1);
405
+ assert_eq!(deltas[0].created_at, "2026-01-01T00:00:00Z");
406
+ assert_eq!(deltas[0].updated_at, "2026-01-01T00:00:00Z");
407
+ }
408
+
409
+ #[test]
410
+ fn project_materialization_deltas_tracks_entities_independently() {
411
+ let deltas = project_materialization_deltas(vec![
412
+ located_change(
413
+ "commit-1",
414
+ 0,
415
+ "entity-a-create",
416
+ "entity-a",
417
+ "2026-01-01T00:00:00Z",
418
+ ),
419
+ located_change(
420
+ "commit-1",
421
+ 1,
422
+ "entity-b-create",
423
+ "entity-b",
424
+ "2026-01-02T00:00:00Z",
425
+ ),
426
+ located_change(
427
+ "commit-2",
428
+ 0,
429
+ "entity-a-update",
430
+ "entity-a",
431
+ "2026-02-01T00:00:00Z",
432
+ ),
433
+ ]);
434
+
435
+ let entity_a = deltas
436
+ .iter()
437
+ .find(|delta| delta.change.entity_id == EntityIdentity::single("entity-a"))
438
+ .expect("entity-a delta");
439
+ let entity_b = deltas
440
+ .iter()
441
+ .find(|delta| delta.change.entity_id == EntityIdentity::single("entity-b"))
442
+ .expect("entity-b delta");
443
+ assert_eq!(entity_a.change.id, "entity-a-update");
444
+ assert_eq!(entity_a.created_at, "2026-01-01T00:00:00Z");
445
+ assert_eq!(entity_a.updated_at, "2026-02-01T00:00:00Z");
446
+ assert_eq!(entity_b.change.id, "entity-b-create");
447
+ assert_eq!(entity_b.created_at, "2026-01-02T00:00:00Z");
448
+ assert_eq!(entity_b.updated_at, "2026-01-02T00:00:00Z");
449
+ }
450
+
451
+ fn change(id: &str) -> Change {
452
+ Change {
453
+ id: id.to_string(),
454
+ entity_id: EntityIdentity::single("entity-1"),
455
+ schema_key: "schema".to_string(),
456
+ file_id: Some("file".to_string()),
457
+ snapshot_ref: None,
458
+ metadata_ref: None,
459
+ created_at: "2026-01-01T00:00:00Z".to_string(),
460
+ }
461
+ }
462
+
463
+ fn located_change(
464
+ commit_id: &str,
465
+ source_ordinal: u32,
466
+ change_id: &str,
467
+ entity_id: &str,
468
+ created_at: &str,
469
+ ) -> LocatedChange {
470
+ LocatedChange {
471
+ locator: ChangeLocator {
472
+ source_commit_id: commit_id.to_string(),
473
+ source_pack_id: 0,
474
+ source_ordinal,
475
+ change_id: change_id.to_string(),
476
+ },
477
+ change: Change {
478
+ id: change_id.to_string(),
479
+ entity_id: EntityIdentity::single(entity_id),
480
+ schema_key: "schema".to_string(),
481
+ file_id: Some("file".to_string()),
482
+ snapshot_ref: None,
483
+ metadata_ref: None,
484
+ created_at: created_at.to_string(),
485
+ },
486
+ }
487
+ }
488
+ }