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

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 (191) hide show
  1. package/SKILL.md +305 -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/open-lix.d.ts +103 -14
  7. package/dist/open-lix.js +3 -0
  8. package/dist/sqlite/index.js +99 -22
  9. package/dist-engine-src/README.md +18 -0
  10. package/dist-engine-src/src/backend/kv.rs +358 -0
  11. package/dist-engine-src/src/backend/mod.rs +12 -0
  12. package/dist-engine-src/src/backend/testing.rs +658 -0
  13. package/dist-engine-src/src/backend/types.rs +96 -0
  14. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  15. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  16. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  17. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  18. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  19. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  20. package/dist-engine-src/src/cel/context.rs +86 -0
  21. package/dist-engine-src/src/cel/error.rs +19 -0
  22. package/dist-engine-src/src/cel/mod.rs +8 -0
  23. package/dist-engine-src/src/cel/provider.rs +9 -0
  24. package/dist-engine-src/src/cel/runtime.rs +167 -0
  25. package/dist-engine-src/src/cel/value.rs +50 -0
  26. package/dist-engine-src/src/changelog/codec.rs +321 -0
  27. package/dist-engine-src/src/changelog/context.rs +92 -0
  28. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  29. package/dist-engine-src/src/changelog/mod.rs +13 -0
  30. package/dist-engine-src/src/changelog/reader.rs +20 -0
  31. package/dist-engine-src/src/changelog/storage.rs +220 -0
  32. package/dist-engine-src/src/changelog/types.rs +38 -0
  33. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  34. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  35. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  36. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  37. package/dist-engine-src/src/common/error.rs +313 -0
  38. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  39. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  40. package/dist-engine-src/src/common/identity.rs +135 -0
  41. package/dist-engine-src/src/common/metadata.rs +35 -0
  42. package/dist-engine-src/src/common/mod.rs +23 -0
  43. package/dist-engine-src/src/common/types.rs +105 -0
  44. package/dist-engine-src/src/common/wire.rs +222 -0
  45. package/dist-engine-src/src/engine.rs +239 -0
  46. package/dist-engine-src/src/entity_identity.rs +285 -0
  47. package/dist-engine-src/src/functions/context.rs +327 -0
  48. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  49. package/dist-engine-src/src/functions/mod.rs +18 -0
  50. package/dist-engine-src/src/functions/provider.rs +130 -0
  51. package/dist-engine-src/src/functions/state.rs +363 -0
  52. package/dist-engine-src/src/functions/types.rs +37 -0
  53. package/dist-engine-src/src/init.rs +505 -0
  54. package/dist-engine-src/src/json_store/compression.rs +77 -0
  55. package/dist-engine-src/src/json_store/context.rs +129 -0
  56. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  57. package/dist-engine-src/src/json_store/mod.rs +9 -0
  58. package/dist-engine-src/src/json_store/store.rs +236 -0
  59. package/dist-engine-src/src/json_store/types.rs +52 -0
  60. package/dist-engine-src/src/lib.rs +61 -0
  61. package/dist-engine-src/src/live_state/context.rs +2241 -0
  62. package/dist-engine-src/src/live_state/mod.rs +15 -0
  63. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  64. package/dist-engine-src/src/live_state/reader.rs +23 -0
  65. package/dist-engine-src/src/live_state/types.rs +239 -0
  66. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  67. package/dist-engine-src/src/plugin/archive.rs +441 -0
  68. package/dist-engine-src/src/plugin/component.rs +183 -0
  69. package/dist-engine-src/src/plugin/install.rs +637 -0
  70. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  71. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  72. package/dist-engine-src/src/plugin/mod.rs +33 -0
  73. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  74. package/dist-engine-src/src/plugin/storage.rs +74 -0
  75. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  76. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  77. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  78. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  79. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  80. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  81. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  82. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  85. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  86. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  87. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  89. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  90. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  91. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  92. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  93. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  94. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  95. package/dist-engine-src/src/schema/definition.json +157 -0
  96. package/dist-engine-src/src/schema/definition.rs +636 -0
  97. package/dist-engine-src/src/schema/key.rs +206 -0
  98. package/dist-engine-src/src/schema/mod.rs +20 -0
  99. package/dist-engine-src/src/schema/seed.rs +14 -0
  100. package/dist-engine-src/src/schema/tests.rs +739 -0
  101. package/dist-engine-src/src/schema_registry.rs +294 -0
  102. package/dist-engine-src/src/session/context.rs +366 -0
  103. package/dist-engine-src/src/session/create_version.rs +80 -0
  104. package/dist-engine-src/src/session/execute.rs +447 -0
  105. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  106. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  107. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  108. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  109. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  110. package/dist-engine-src/src/session/merge/version.rs +437 -0
  111. package/dist-engine-src/src/session/mod.rs +25 -0
  112. package/dist-engine-src/src/session/switch_version.rs +121 -0
  113. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  114. package/dist-engine-src/src/sql2/classify.rs +147 -0
  115. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  116. package/dist-engine-src/src/sql2/context.rs +307 -0
  117. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  118. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  119. package/dist-engine-src/src/sql2/dml.rs +148 -0
  120. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  121. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  122. package/dist-engine-src/src/sql2/error.rs +196 -0
  123. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  124. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  125. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  126. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  127. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  128. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  129. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  130. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  131. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  132. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  133. package/dist-engine-src/src/sql2/mod.rs +43 -0
  134. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  135. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  136. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  137. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  138. package/dist-engine-src/src/sql2/session.rs +135 -0
  139. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  140. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  141. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  142. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  143. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  144. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  148. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  149. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  150. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  151. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  152. package/dist-engine-src/src/storage/context.rs +356 -0
  153. package/dist-engine-src/src/storage/mod.rs +14 -0
  154. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  155. package/dist-engine-src/src/storage/types.rs +501 -0
  156. package/dist-engine-src/src/storage_bench.rs +3406 -0
  157. package/dist-engine-src/src/test_support.rs +81 -0
  158. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  159. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  160. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  161. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  162. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  163. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  164. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  165. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  166. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  167. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  168. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  169. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  170. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  171. package/dist-engine-src/src/transaction/context.rs +1307 -0
  172. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  173. package/dist-engine-src/src/transaction/mod.rs +11 -0
  174. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  175. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  176. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  177. package/dist-engine-src/src/transaction/types.rs +351 -0
  178. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  179. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  180. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  181. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  182. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  183. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  184. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  185. package/dist-engine-src/src/version/context.rs +52 -0
  186. package/dist-engine-src/src/version/mod.rs +12 -0
  187. package/dist-engine-src/src/version/refs.rs +421 -0
  188. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  189. package/dist-engine-src/src/version/types.rs +21 -0
  190. package/dist-engine-src/src/wasm/mod.rs +60 -0
  191. package/package.json +68 -64
@@ -0,0 +1,747 @@
1
+ use xxhash_rust::xxh3::xxh3_64_with_seed;
2
+
3
+ use crate::entity_identity::{EntityIdentity, EntityIdentityPart};
4
+ use crate::json_store::JsonRef;
5
+ use crate::tracked_state::tree_types::{
6
+ TrackedStateKey, TrackedStateValue, TRACKED_STATE_HASH_BYTES,
7
+ };
8
+ use crate::LixError;
9
+
10
+ const NODE_VERSION: u8 = 1;
11
+ const VALUE_VERSION: u8 = 2;
12
+ const NODE_KIND_LEAF: u8 = 1;
13
+ const NODE_KIND_INTERNAL: u8 = 2;
14
+ const WEIBULL_K: i32 = 4;
15
+ const ENTITY_IDENTITY_END: u8 = 0;
16
+ const ENTITY_IDENTITY_STRING: u8 = 1;
17
+ const ENTITY_IDENTITY_BOOL: u8 = 2;
18
+ const ENTITY_IDENTITY_NUMBER: u8 = 3;
19
+
20
+ #[derive(Debug, Clone, PartialEq, Eq)]
21
+ pub(crate) struct EncodedLeafEntry {
22
+ pub(crate) key: Vec<u8>,
23
+ pub(crate) value: Vec<u8>,
24
+ }
25
+
26
+ #[derive(Debug, Clone, PartialEq, Eq)]
27
+ pub(crate) struct PendingChunkWrite {
28
+ pub(crate) hash: [u8; TRACKED_STATE_HASH_BYTES],
29
+ pub(crate) data: Vec<u8>,
30
+ }
31
+
32
+ #[derive(Debug, Clone, PartialEq, Eq)]
33
+ pub(crate) struct ChildSummary {
34
+ pub(crate) first_key: Vec<u8>,
35
+ pub(crate) last_key: Vec<u8>,
36
+ pub(crate) child_hash: [u8; TRACKED_STATE_HASH_BYTES],
37
+ pub(crate) subtree_count: u64,
38
+ }
39
+
40
+ #[derive(Debug, Clone)]
41
+ pub(crate) enum DecodedNode {
42
+ Leaf(DecodedLeafNode),
43
+ Internal(DecodedInternalNode),
44
+ }
45
+
46
+ #[derive(Debug, Clone)]
47
+ pub(crate) struct DecodedLeafNode {
48
+ entries: Vec<EncodedLeafEntry>,
49
+ }
50
+
51
+ impl DecodedLeafNode {
52
+ pub(crate) fn entries(&self) -> &[EncodedLeafEntry] {
53
+ &self.entries
54
+ }
55
+ }
56
+
57
+ #[derive(Debug, Clone)]
58
+ pub(crate) struct DecodedInternalNode {
59
+ children: Vec<ChildSummary>,
60
+ }
61
+
62
+ impl DecodedInternalNode {
63
+ pub(crate) fn children(&self) -> &[ChildSummary] {
64
+ &self.children
65
+ }
66
+ }
67
+
68
+ pub(crate) fn hash_bytes(bytes: &[u8]) -> [u8; TRACKED_STATE_HASH_BYTES] {
69
+ *blake3::hash(bytes).as_bytes()
70
+ }
71
+
72
+ pub(crate) fn encode_key(key: &TrackedStateKey) -> Vec<u8> {
73
+ let mut out = Vec::new();
74
+ push_sized_bytes(&mut out, key.schema_key.as_bytes());
75
+ match &key.file_id {
76
+ Some(file_id) => {
77
+ out.push(1);
78
+ push_sized_bytes(&mut out, file_id.as_bytes());
79
+ }
80
+ None => out.push(0),
81
+ }
82
+ push_entity_identity(&mut out, &key.entity_id);
83
+ out
84
+ }
85
+
86
+ pub(crate) fn encode_schema_key_prefix(schema_key: &str) -> Vec<u8> {
87
+ let mut out = Vec::new();
88
+ push_sized_bytes(&mut out, schema_key.as_bytes());
89
+ out
90
+ }
91
+
92
+ pub(crate) fn encode_schema_file_prefix(schema_key: &str, file_id: Option<&str>) -> Vec<u8> {
93
+ let mut out = encode_schema_key_prefix(schema_key);
94
+ match file_id {
95
+ Some(file_id) => {
96
+ out.push(1);
97
+ push_sized_bytes(&mut out, file_id.as_bytes());
98
+ }
99
+ None => out.push(0),
100
+ }
101
+ out
102
+ }
103
+
104
+ pub(crate) fn decode_key(bytes: &[u8]) -> Result<TrackedStateKey, LixError> {
105
+ let mut cursor = 0usize;
106
+ let schema_key = read_sized_string(bytes, &mut cursor, "schema_key")?;
107
+ let file_id = match read_u8(bytes, &mut cursor, "file_id presence")? {
108
+ 0 => None,
109
+ 1 => Some(read_sized_string(bytes, &mut cursor, "file_id")?),
110
+ other => {
111
+ return Err(LixError::new(
112
+ "LIX_ERROR_UNKNOWN",
113
+ format!("tracked-state tree key has invalid file_id presence byte {other}"),
114
+ ))
115
+ }
116
+ };
117
+ let entity_id = read_entity_identity(bytes, &mut cursor)?;
118
+ if cursor != bytes.len() {
119
+ return Err(LixError::new(
120
+ "LIX_ERROR_UNKNOWN",
121
+ "tracked-state tree key decode found trailing bytes",
122
+ ));
123
+ }
124
+ Ok(TrackedStateKey {
125
+ schema_key,
126
+ file_id,
127
+ entity_id,
128
+ })
129
+ }
130
+
131
+ pub(crate) fn encode_value(value: &TrackedStateValue) -> Vec<u8> {
132
+ let mut out = Vec::new();
133
+ out.push(VALUE_VERSION);
134
+ push_optional_json_ref(&mut out, value.snapshot_ref.as_ref());
135
+ push_optional_json_ref(&mut out, value.metadata_ref.as_ref());
136
+ push_sized_bytes(&mut out, value.schema_version.as_bytes());
137
+ push_sized_bytes(&mut out, value.created_at.as_bytes());
138
+ push_sized_bytes(&mut out, value.updated_at.as_bytes());
139
+ push_sized_bytes(&mut out, value.change_id.as_bytes());
140
+ push_sized_bytes(&mut out, value.commit_id.as_bytes());
141
+ out.push(u8::from(value.deleted));
142
+ out
143
+ }
144
+
145
+ #[cfg(test)]
146
+ pub(crate) fn encoded_value_len(value: &TrackedStateValue) -> usize {
147
+ 1 + optional_json_ref_len(value.snapshot_ref.as_ref())
148
+ + optional_json_ref_len(value.metadata_ref.as_ref())
149
+ + sized_bytes_len(value.schema_version.as_bytes())
150
+ + sized_bytes_len(value.created_at.as_bytes())
151
+ + sized_bytes_len(value.updated_at.as_bytes())
152
+ + sized_bytes_len(value.change_id.as_bytes())
153
+ + sized_bytes_len(value.commit_id.as_bytes())
154
+ + 1
155
+ }
156
+
157
+ pub(crate) fn decode_value(bytes: &[u8]) -> Result<TrackedStateValue, LixError> {
158
+ let mut cursor = 0usize;
159
+ let version = read_u8(bytes, &mut cursor, "value version")?;
160
+ if version != VALUE_VERSION {
161
+ return Err(LixError::new(
162
+ "LIX_ERROR_UNKNOWN",
163
+ format!("unsupported tracked-state tree value version {version}"),
164
+ ));
165
+ }
166
+ let snapshot_ref = read_optional_json_ref(bytes, &mut cursor, "snapshot_ref")?;
167
+ let metadata_ref = read_optional_json_ref(bytes, &mut cursor, "metadata_ref")?;
168
+ let schema_version = read_sized_string(bytes, &mut cursor, "schema_version")?;
169
+ let created_at = read_sized_string(bytes, &mut cursor, "created_at")?;
170
+ let updated_at = read_sized_string(bytes, &mut cursor, "updated_at")?;
171
+ let change_id = read_sized_string(bytes, &mut cursor, "change_id")?;
172
+ let commit_id = read_sized_string(bytes, &mut cursor, "commit_id")?;
173
+ let deleted = match read_u8(bytes, &mut cursor, "deleted")? {
174
+ 0 => false,
175
+ 1 => true,
176
+ other => {
177
+ return Err(LixError::new(
178
+ "LIX_ERROR_UNKNOWN",
179
+ format!("tracked-state tree value has invalid deleted byte {other}"),
180
+ ))
181
+ }
182
+ };
183
+ if cursor != bytes.len() {
184
+ return Err(LixError::new(
185
+ "LIX_ERROR_UNKNOWN",
186
+ "tracked-state tree value decode found trailing bytes",
187
+ ));
188
+ }
189
+ Ok(TrackedStateValue {
190
+ snapshot_ref,
191
+ metadata_ref,
192
+ schema_version,
193
+ created_at,
194
+ updated_at,
195
+ change_id,
196
+ commit_id,
197
+ deleted,
198
+ })
199
+ }
200
+
201
+ #[cfg(test)]
202
+ fn sized_bytes_len(bytes: &[u8]) -> usize {
203
+ 4 + bytes.len()
204
+ }
205
+
206
+ fn push_optional_json_ref(out: &mut Vec<u8>, value: Option<&JsonRef>) {
207
+ match value {
208
+ Some(value) => {
209
+ out.push(1);
210
+ out.extend_from_slice(value.as_hash_bytes());
211
+ }
212
+ None => out.push(0),
213
+ }
214
+ }
215
+
216
+ #[cfg(test)]
217
+ fn optional_json_ref_len(value: Option<&JsonRef>) -> usize {
218
+ match value {
219
+ Some(_) => 1 + TRACKED_STATE_HASH_BYTES,
220
+ None => 1,
221
+ }
222
+ }
223
+
224
+ fn read_optional_json_ref(
225
+ bytes: &[u8],
226
+ cursor: &mut usize,
227
+ field: &str,
228
+ ) -> Result<Option<JsonRef>, LixError> {
229
+ match read_u8(bytes, cursor, field)? {
230
+ 0 => Ok(None),
231
+ 1 => {
232
+ let hash = read_fixed_hash(bytes, cursor, field)?;
233
+ Ok(Some(JsonRef::from_hash_bytes(hash)))
234
+ }
235
+ other => Err(LixError::new(
236
+ "LIX_ERROR_UNKNOWN",
237
+ format!("tracked-state tree value has invalid {field} presence byte {other}"),
238
+ )),
239
+ }
240
+ }
241
+
242
+ pub(crate) fn encode_leaf_node(entries: &[EncodedLeafEntry]) -> Vec<u8> {
243
+ let mut out = Vec::new();
244
+ out.push(NODE_KIND_LEAF);
245
+ out.push(NODE_VERSION);
246
+ push_u32(&mut out, entries.len());
247
+ for entry in entries {
248
+ push_sized_bytes(&mut out, &entry.key);
249
+ push_sized_bytes(&mut out, &entry.value);
250
+ }
251
+ out
252
+ }
253
+
254
+ pub(crate) fn encode_internal_node(children: &[ChildSummary]) -> Vec<u8> {
255
+ let mut out = Vec::new();
256
+ out.push(NODE_KIND_INTERNAL);
257
+ out.push(NODE_VERSION);
258
+ push_u32(&mut out, children.len());
259
+ for child in children {
260
+ push_sized_bytes(&mut out, &child.first_key);
261
+ push_sized_bytes(&mut out, &child.last_key);
262
+ out.extend_from_slice(&child.child_hash);
263
+ out.extend_from_slice(&child.subtree_count.to_be_bytes());
264
+ }
265
+ out
266
+ }
267
+
268
+ pub(crate) fn decode_node(bytes: &[u8]) -> Result<DecodedNode, LixError> {
269
+ let mut cursor = 0usize;
270
+ let kind = read_u8(bytes, &mut cursor, "node kind")?;
271
+ let version = read_u8(bytes, &mut cursor, "node version")?;
272
+ if version != NODE_VERSION {
273
+ return Err(LixError::new(
274
+ "LIX_ERROR_UNKNOWN",
275
+ format!("unsupported tracked-state tree node version {version}"),
276
+ ));
277
+ }
278
+ let count = read_u32(bytes, &mut cursor, "entry count")?;
279
+ let node = match kind {
280
+ NODE_KIND_LEAF => {
281
+ let mut entries = Vec::with_capacity(count);
282
+ for _ in 0..count {
283
+ entries.push(EncodedLeafEntry {
284
+ key: read_sized_bytes(bytes, &mut cursor, "leaf key")?,
285
+ value: read_sized_bytes(bytes, &mut cursor, "leaf value")?,
286
+ });
287
+ }
288
+ DecodedNode::Leaf(DecodedLeafNode { entries })
289
+ }
290
+ NODE_KIND_INTERNAL => {
291
+ let mut children = Vec::with_capacity(count);
292
+ for _ in 0..count {
293
+ let first_key = read_sized_bytes(bytes, &mut cursor, "internal first_key")?;
294
+ let last_key = read_sized_bytes(bytes, &mut cursor, "internal last_key")?;
295
+ let child_hash = read_fixed_hash(bytes, &mut cursor, "internal child_hash")?;
296
+ let subtree_count = read_u64(bytes, &mut cursor, "internal subtree_count")?;
297
+ children.push(ChildSummary {
298
+ first_key,
299
+ last_key,
300
+ child_hash,
301
+ subtree_count,
302
+ });
303
+ }
304
+ DecodedNode::Internal(DecodedInternalNode { children })
305
+ }
306
+ other => {
307
+ return Err(LixError::new(
308
+ "LIX_ERROR_UNKNOWN",
309
+ format!("unknown tracked-state tree node kind {other}"),
310
+ ))
311
+ }
312
+ };
313
+ if cursor != bytes.len() {
314
+ return Err(LixError::new(
315
+ "LIX_ERROR_UNKNOWN",
316
+ "tracked-state tree node decode found trailing bytes",
317
+ ));
318
+ }
319
+ Ok(node)
320
+ }
321
+
322
+ pub(crate) fn child_summary_from_node(
323
+ node_bytes: Vec<u8>,
324
+ first_key: Vec<u8>,
325
+ last_key: Vec<u8>,
326
+ subtree_count: u64,
327
+ ) -> (PendingChunkWrite, ChildSummary) {
328
+ let hash = hash_bytes(&node_bytes);
329
+ (
330
+ PendingChunkWrite {
331
+ hash,
332
+ data: node_bytes,
333
+ },
334
+ ChildSummary {
335
+ first_key,
336
+ last_key,
337
+ child_hash: hash,
338
+ subtree_count,
339
+ },
340
+ )
341
+ }
342
+
343
+ pub(crate) fn boundary_trigger(
344
+ encoded_key: &[u8],
345
+ level: usize,
346
+ chunk_size: usize,
347
+ item_size: usize,
348
+ target_chunk_bytes: usize,
349
+ ) -> bool {
350
+ if item_size == 0 || target_chunk_bytes == 0 {
351
+ return false;
352
+ }
353
+
354
+ let start =
355
+ weibull_cdf(chunk_size.saturating_sub(item_size) as f64 / target_chunk_bytes as f64);
356
+ let end = weibull_cdf(chunk_size as f64 / target_chunk_bytes as f64);
357
+ let remaining = 1.0 - start;
358
+ if remaining <= 0.0 {
359
+ return true;
360
+ }
361
+
362
+ let split_probability = ((end - start) / remaining).clamp(0.0, 1.0);
363
+ let hash = xxh3_64_with_seed(encoded_key, level_salt(level));
364
+ (hash as f64) < split_probability * (u64::MAX as f64)
365
+ }
366
+
367
+ fn weibull_cdf(normalized_size: f64) -> f64 {
368
+ if normalized_size <= 0.0 {
369
+ return 0.0;
370
+ }
371
+ -f64::exp_m1(-normalized_size.powi(WEIBULL_K))
372
+ }
373
+
374
+ fn level_salt(level: usize) -> u64 {
375
+ let mut value = (level as u64).wrapping_add(0x9e37_79b9_7f4a_7c15);
376
+ value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
377
+ value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
378
+ value ^ (value >> 31)
379
+ }
380
+
381
+ fn push_entity_identity(out: &mut Vec<u8>, identity: &EntityIdentity) {
382
+ assert!(
383
+ !identity.parts.is_empty(),
384
+ "tracked-state key entity identity must contain at least one part"
385
+ );
386
+ for part in &identity.parts {
387
+ match part {
388
+ EntityIdentityPart::String(value) => {
389
+ out.push(ENTITY_IDENTITY_STRING);
390
+ push_sized_bytes(out, value.as_bytes());
391
+ }
392
+ EntityIdentityPart::Bool(value) => {
393
+ out.push(ENTITY_IDENTITY_BOOL);
394
+ out.push(u8::from(*value));
395
+ }
396
+ EntityIdentityPart::Number(value) => {
397
+ out.push(ENTITY_IDENTITY_NUMBER);
398
+ push_sized_bytes(out, value.as_bytes());
399
+ }
400
+ }
401
+ }
402
+ out.push(ENTITY_IDENTITY_END);
403
+ }
404
+
405
+ fn read_entity_identity(bytes: &[u8], cursor: &mut usize) -> Result<EntityIdentity, LixError> {
406
+ let mut parts = Vec::new();
407
+ loop {
408
+ let tag = read_u8(bytes, cursor, "entity identity part tag")?;
409
+ match tag {
410
+ ENTITY_IDENTITY_END => break,
411
+ ENTITY_IDENTITY_STRING => {
412
+ parts.push(EntityIdentityPart::String(read_sized_string(
413
+ bytes,
414
+ cursor,
415
+ "entity identity string part",
416
+ )?));
417
+ }
418
+ ENTITY_IDENTITY_BOOL => {
419
+ let value = match read_u8(bytes, cursor, "entity identity bool part")? {
420
+ 0 => false,
421
+ 1 => true,
422
+ other => {
423
+ return Err(LixError::new(
424
+ "LIX_ERROR_UNKNOWN",
425
+ format!(
426
+ "tracked-state tree key has invalid entity identity bool byte {other}"
427
+ ),
428
+ ))
429
+ }
430
+ };
431
+ parts.push(EntityIdentityPart::Bool(value));
432
+ }
433
+ ENTITY_IDENTITY_NUMBER => {
434
+ parts.push(EntityIdentityPart::Number(read_sized_string(
435
+ bytes,
436
+ cursor,
437
+ "entity identity number part",
438
+ )?));
439
+ }
440
+ other => {
441
+ return Err(LixError::new(
442
+ "LIX_ERROR_UNKNOWN",
443
+ format!("tracked-state tree key has invalid entity identity part tag {other}"),
444
+ ))
445
+ }
446
+ }
447
+ }
448
+ if parts.is_empty() {
449
+ return Err(LixError::new(
450
+ "LIX_ERROR_UNKNOWN",
451
+ "tracked-state tree key entity identity must contain at least one part",
452
+ ));
453
+ }
454
+ Ok(EntityIdentity { parts })
455
+ }
456
+
457
+ fn push_sized_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
458
+ push_u32(out, bytes.len());
459
+ out.extend_from_slice(bytes);
460
+ }
461
+
462
+ fn push_u32(out: &mut Vec<u8>, value: usize) {
463
+ out.extend_from_slice(&(value as u32).to_be_bytes());
464
+ }
465
+
466
+ fn read_sized_string(
467
+ bytes: &[u8],
468
+ cursor: &mut usize,
469
+ field_name: &str,
470
+ ) -> Result<String, LixError> {
471
+ String::from_utf8(read_sized_bytes(bytes, cursor, field_name)?).map_err(|error| {
472
+ LixError::new(
473
+ "LIX_ERROR_UNKNOWN",
474
+ format!("tracked-state tree field '{field_name}' is invalid UTF-8: {error}"),
475
+ )
476
+ })
477
+ }
478
+
479
+ fn read_sized_bytes(
480
+ bytes: &[u8],
481
+ cursor: &mut usize,
482
+ field_name: &str,
483
+ ) -> Result<Vec<u8>, LixError> {
484
+ let len = read_u32(bytes, cursor, field_name)?;
485
+ let end = cursor.checked_add(len).ok_or_else(|| {
486
+ LixError::new(
487
+ "LIX_ERROR_UNKNOWN",
488
+ format!("tracked-state tree field '{field_name}' length overflow"),
489
+ )
490
+ })?;
491
+ let slice = bytes.get(*cursor..end).ok_or_else(|| {
492
+ LixError::new(
493
+ "LIX_ERROR_UNKNOWN",
494
+ format!("tracked-state tree field '{field_name}' is truncated"),
495
+ )
496
+ })?;
497
+ *cursor = end;
498
+ Ok(slice.to_vec())
499
+ }
500
+
501
+ fn read_fixed_hash(
502
+ bytes: &[u8],
503
+ cursor: &mut usize,
504
+ field_name: &str,
505
+ ) -> Result<[u8; TRACKED_STATE_HASH_BYTES], LixError> {
506
+ let end = *cursor + TRACKED_STATE_HASH_BYTES;
507
+ let slice = bytes.get(*cursor..end).ok_or_else(|| {
508
+ LixError::new(
509
+ "LIX_ERROR_UNKNOWN",
510
+ format!("tracked-state tree field '{field_name}' is truncated"),
511
+ )
512
+ })?;
513
+ let mut out = [0_u8; TRACKED_STATE_HASH_BYTES];
514
+ out.copy_from_slice(slice);
515
+ *cursor = end;
516
+ Ok(out)
517
+ }
518
+
519
+ fn read_u8(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u8, LixError> {
520
+ let value = *bytes.get(*cursor).ok_or_else(|| {
521
+ LixError::new(
522
+ "LIX_ERROR_UNKNOWN",
523
+ format!("tracked-state tree field '{field_name}' is truncated"),
524
+ )
525
+ })?;
526
+ *cursor += 1;
527
+ Ok(value)
528
+ }
529
+
530
+ fn read_u32(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<usize, LixError> {
531
+ let end = *cursor + 4;
532
+ let slice = bytes.get(*cursor..end).ok_or_else(|| {
533
+ LixError::new(
534
+ "LIX_ERROR_UNKNOWN",
535
+ format!("tracked-state tree field '{field_name}' is truncated"),
536
+ )
537
+ })?;
538
+ let mut out = [0_u8; 4];
539
+ out.copy_from_slice(slice);
540
+ *cursor = end;
541
+ Ok(u32::from_be_bytes(out) as usize)
542
+ }
543
+
544
+ fn read_u64(bytes: &[u8], cursor: &mut usize, field_name: &str) -> Result<u64, LixError> {
545
+ let end = *cursor + 8;
546
+ let slice = bytes.get(*cursor..end).ok_or_else(|| {
547
+ LixError::new(
548
+ "LIX_ERROR_UNKNOWN",
549
+ format!("tracked-state tree field '{field_name}' is truncated"),
550
+ )
551
+ })?;
552
+ let mut out = [0_u8; 8];
553
+ out.copy_from_slice(slice);
554
+ *cursor = end;
555
+ Ok(u64::from_be_bytes(out))
556
+ }
557
+
558
+ #[cfg(test)]
559
+ mod tests {
560
+ use super::*;
561
+
562
+ #[test]
563
+ fn key_codec_distinguishes_null_and_value_file_id() {
564
+ let null_key = encode_key(&TrackedStateKey {
565
+ schema_key: "schema".to_string(),
566
+ file_id: None,
567
+ entity_id: EntityIdentity::single("entity"),
568
+ });
569
+ let file_key = encode_key(&TrackedStateKey {
570
+ schema_key: "schema".to_string(),
571
+ file_id: Some("file".to_string()),
572
+ entity_id: EntityIdentity::single("entity"),
573
+ });
574
+
575
+ assert_ne!(null_key, file_key);
576
+ assert_eq!(
577
+ decode_key(&null_key).expect("null key"),
578
+ TrackedStateKey {
579
+ schema_key: "schema".to_string(),
580
+ file_id: None,
581
+ entity_id: EntityIdentity::single("entity"),
582
+ }
583
+ );
584
+ assert_eq!(
585
+ decode_key(&file_key).expect("file key"),
586
+ TrackedStateKey {
587
+ schema_key: "schema".to_string(),
588
+ file_id: Some("file".to_string()),
589
+ entity_id: EntityIdentity::single("entity"),
590
+ }
591
+ );
592
+ }
593
+
594
+ #[test]
595
+ fn key_codec_encodes_composite_identity_as_typed_tuple_parts() {
596
+ let key = TrackedStateKey {
597
+ schema_key: "schema".to_string(),
598
+ file_id: None,
599
+ entity_id: EntityIdentity {
600
+ parts: vec![
601
+ EntityIdentityPart::String("namespace".to_string()),
602
+ EntityIdentityPart::Bool(true),
603
+ EntityIdentityPart::Number("42".to_string()),
604
+ ],
605
+ },
606
+ };
607
+
608
+ let encoded = encode_key(&key);
609
+
610
+ assert_eq!(decode_key(&encoded).expect("key should decode"), key);
611
+ assert!(
612
+ !encoded
613
+ .windows(b"pk:v1:".len())
614
+ .any(|window| window == b"pk:v1:"),
615
+ "tracked-state keys should not store the SQL entity_id projection"
616
+ );
617
+ }
618
+
619
+ #[test]
620
+ fn key_codec_distinguishes_typed_identity_parts() {
621
+ let string_true = encode_key(&TrackedStateKey {
622
+ schema_key: "schema".to_string(),
623
+ file_id: None,
624
+ entity_id: EntityIdentity {
625
+ parts: vec![EntityIdentityPart::String("true".to_string())],
626
+ },
627
+ });
628
+ let bool_true = encode_key(&TrackedStateKey {
629
+ schema_key: "schema".to_string(),
630
+ file_id: None,
631
+ entity_id: EntityIdentity {
632
+ parts: vec![EntityIdentityPart::Bool(true)],
633
+ },
634
+ });
635
+
636
+ assert_ne!(string_true, bool_true);
637
+ }
638
+
639
+ #[test]
640
+ fn key_codec_preserves_tuple_prefix_ordering() {
641
+ let prefix = encode_key(&TrackedStateKey {
642
+ schema_key: "schema".to_string(),
643
+ file_id: None,
644
+ entity_id: EntityIdentity {
645
+ parts: vec![EntityIdentityPart::String("a".to_string())],
646
+ },
647
+ });
648
+ let extended = encode_key(&TrackedStateKey {
649
+ schema_key: "schema".to_string(),
650
+ file_id: None,
651
+ entity_id: EntityIdentity {
652
+ parts: vec![
653
+ EntityIdentityPart::String("a".to_string()),
654
+ EntityIdentityPart::String("b".to_string()),
655
+ ],
656
+ },
657
+ });
658
+
659
+ assert!(prefix < extended);
660
+ }
661
+
662
+ #[test]
663
+ fn value_codec_roundtrips_tombstone_value() {
664
+ let value = TrackedStateValue {
665
+ snapshot_ref: None,
666
+ metadata_ref: Some(JsonRef::from_hash_bytes([1; 32])),
667
+ schema_version: "1".to_string(),
668
+ created_at: "2026-01-01T00:00:00Z".to_string(),
669
+ updated_at: "2026-01-02T00:00:00Z".to_string(),
670
+ change_id: "change".to_string(),
671
+ commit_id: "commit".to_string(),
672
+ deleted: true,
673
+ };
674
+
675
+ let encoded = encode_value(&value);
676
+ assert_eq!(decode_value(&encoded).expect("value"), value);
677
+ }
678
+
679
+ #[test]
680
+ fn value_codec_roundtrips_snapshot_ref() {
681
+ let value = TrackedStateValue {
682
+ snapshot_ref: Some(JsonRef::from_hash_bytes([2; 32])),
683
+ metadata_ref: None,
684
+ schema_version: "1".to_string(),
685
+ created_at: "2026-01-01T00:00:00Z".to_string(),
686
+ updated_at: "2026-01-02T00:00:00Z".to_string(),
687
+ change_id: "change".to_string(),
688
+ commit_id: "commit".to_string(),
689
+ deleted: false,
690
+ };
691
+
692
+ let encoded = encode_value(&value);
693
+ assert_eq!(decode_value(&encoded).expect("value"), value);
694
+ }
695
+
696
+ #[test]
697
+ fn encoded_value_len_matches_encoded_value_bytes() {
698
+ let values = [
699
+ TrackedStateValue {
700
+ snapshot_ref: None,
701
+ metadata_ref: None,
702
+ schema_version: "1".to_string(),
703
+ created_at: "2026-01-01T00:00:00Z".to_string(),
704
+ updated_at: "2026-01-02T00:00:00Z".to_string(),
705
+ change_id: "change".to_string(),
706
+ commit_id: "commit".to_string(),
707
+ deleted: true,
708
+ },
709
+ TrackedStateValue {
710
+ snapshot_ref: Some(JsonRef::from_hash_bytes([3; 32])),
711
+ metadata_ref: Some(JsonRef::from_hash_bytes([4; 32])),
712
+ schema_version: "1".to_string(),
713
+ created_at: "2026-01-01T00:00:00Z".to_string(),
714
+ updated_at: "2026-01-02T00:00:00Z".to_string(),
715
+ change_id: "change".to_string(),
716
+ commit_id: "commit".to_string(),
717
+ deleted: false,
718
+ },
719
+ TrackedStateValue {
720
+ snapshot_ref: Some(JsonRef::from_hash_bytes([5; 32])),
721
+ metadata_ref: None,
722
+ schema_version: "1".to_string(),
723
+ created_at: "2026-01-01T00:00:00Z".to_string(),
724
+ updated_at: "2026-01-02T00:00:00Z".to_string(),
725
+ change_id: "change".to_string(),
726
+ commit_id: "commit".to_string(),
727
+ deleted: false,
728
+ },
729
+ ];
730
+
731
+ for value in values {
732
+ assert_eq!(encoded_value_len(&value), encode_value(&value).len());
733
+ }
734
+ }
735
+
736
+ #[test]
737
+ fn content_hash_is_blake3() {
738
+ assert_eq!(hash_bytes(b"abc"), *blake3::hash(b"abc").as_bytes());
739
+ }
740
+
741
+ #[test]
742
+ fn boundary_decisions_are_xxh3_based_and_deterministic() {
743
+ let left = boundary_trigger(b"key", 0, 4096, 128, 4096);
744
+ let right = boundary_trigger(b"key", 0, 4096, 128, 4096);
745
+ assert_eq!(left, right);
746
+ }
747
+ }