tensorfs 0.3.8__tar.gz → 0.3.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {tensorfs-0.3.8 → tensorfs-0.3.9}/Cargo.lock +2 -2
  2. {tensorfs-0.3.8 → tensorfs-0.3.9}/Cargo.toml +1 -1
  3. {tensorfs-0.3.8 → tensorfs-0.3.9}/PKG-INFO +1 -1
  4. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/derived.rs +16 -9
  5. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/durability.rs +54 -7
  6. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/source.rs +60 -15
  7. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-py/src/store.rs +18 -0
  8. {tensorfs-0.3.8 → tensorfs-0.3.9}/pyproject.toml +1 -1
  9. {tensorfs-0.3.8 → tensorfs-0.3.9}/python/tensorfs/_ext.pyi +4 -0
  10. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/Cargo.toml +0 -0
  11. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/b64.rs +0 -0
  12. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  13. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  14. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  15. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  16. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  17. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  18. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  19. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  20. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  21. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/canon.rs +0 -0
  22. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/capability.rs +0 -0
  23. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/catalog.rs +0 -0
  24. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/cbor.rs +0 -0
  25. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  26. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/corpus.rs +0 -0
  27. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  28. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/dtype.rs +0 -0
  29. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/err.rs +0 -0
  30. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/fetch.rs +0 -0
  31. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/fill.rs +0 -0
  32. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/fit.rs +0 -0
  33. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/gc.rs +0 -0
  34. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/header.rs +0 -0
  35. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/home.rs +0 -0
  36. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ids.rs +0 -0
  37. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  38. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  39. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  40. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  41. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  42. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  43. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  44. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  45. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  46. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/jcs.rs +0 -0
  47. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/lib.rs +0 -0
  48. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/limits.rs +0 -0
  49. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/machine.rs +0 -0
  50. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/manifest.rs +0 -0
  51. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/meta.rs +0 -0
  52. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/project.rs +0 -0
  53. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/providers.rs +0 -0
  54. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/read.rs +0 -0
  55. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/receipt.rs +0 -0
  56. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/reclaim.rs +0 -0
  57. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/registry.rs +0 -0
  58. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/relation.rs +0 -0
  59. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  60. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/repository.rs +0 -0
  61. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/sha256.rs +0 -0
  62. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/spec.rs +0 -0
  63. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/staging.rs +0 -0
  64. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/storage.rs +0 -0
  65. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/store.rs +0 -0
  66. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/api.rs +0 -0
  67. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/http.rs +0 -0
  68. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  69. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  70. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  71. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  72. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  73. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/push.rs +0 -0
  74. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  75. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  76. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/src/vectors.rs +0 -0
  77. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  78. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fit.rs +0 -0
  79. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  80. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  81. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  82. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  83. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  84. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  85. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  86. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/home.rs +0 -0
  87. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/liveness.rs +0 -0
  88. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  89. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  90. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  91. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  92. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  93. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  94. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  95. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  96. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/staging.rs +0 -0
  97. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  98. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  99. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/version.rs +0 -0
  100. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-core/tests/write_through.rs +0 -0
  101. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-py/Cargo.toml +0 -0
  102. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-py/src/docs.rs +0 -0
  103. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-py/src/errors.rs +0 -0
  104. {tensorfs-0.3.8 → tensorfs-0.3.9}/crates/tensorfs-py/src/lib.rs +0 -0
  105. {tensorfs-0.3.8 → tensorfs-0.3.9}/python/tensorfs/__init__.py +0 -0
  106. {tensorfs-0.3.8 → tensorfs-0.3.9}/python/tensorfs/errors.py +0 -0
  107. {tensorfs-0.3.8 → tensorfs-0.3.9}/python/tensorfs/py.typed +0 -0
@@ -392,7 +392,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
392
 
393
393
  [[package]]
394
394
  name = "tensorfs-core"
395
- version = "0.3.8"
395
+ version = "0.3.9"
396
396
  dependencies = [
397
397
  "fs2",
398
398
  "minicbor",
@@ -404,7 +404,7 @@ dependencies = [
404
404
 
405
405
  [[package]]
406
406
  name = "tensorfs-py"
407
- version = "0.3.8"
407
+ version = "0.3.9"
408
408
  dependencies = [
409
409
  "pyo3",
410
410
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.8"
6
+ version = "0.3.9"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -143,6 +143,20 @@ pub struct Declaration {
143
143
  }
144
144
 
145
145
  impl Declaration {
146
+ /// The native declaration encoder shared by Runtime intents and writers.
147
+ pub fn canonical_bytes(&mut self) -> Result<Vec<u8>> {
148
+ self.normalize_and_validate()?;
149
+ self.require_order()?;
150
+ let bytes = crate::canon::write(&self.to_value());
151
+ if bytes.len() > limits::DOC_MAX_BYTES / 2 {
152
+ return refuse(
153
+ Code::SIZE_CAP,
154
+ "derived declaration is over its metadata budget",
155
+ );
156
+ }
157
+ Ok(bytes)
158
+ }
159
+
146
160
  fn require_order(&self) -> Result<()> {
147
161
  if self.order.is_empty() {
148
162
  return refuse(
@@ -1862,15 +1876,8 @@ pub fn begin(
1862
1876
  "WriterSessionId must be positive and inside the canonical integer range",
1863
1877
  );
1864
1878
  }
1865
- declaration.normalize_and_validate()?;
1866
- declaration.require_order()?;
1867
- let declaration_text = declaration.canonical_text();
1868
- if declaration_text.len() > limits::DOC_MAX_BYTES / 2 {
1869
- return refuse(
1870
- Code::SIZE_CAP,
1871
- "derived declaration is over its metadata budget",
1872
- );
1873
- }
1879
+ let declaration_text =
1880
+ String::from_utf8(declaration.canonical_bytes()?).expect("canonical JSON is UTF-8");
1874
1881
 
1875
1882
  // The writer hold is registered before source metadata is touched. It is the live
1876
1883
  // process proof that keeps exclusive GC out even for a source-free create.
@@ -185,21 +185,34 @@ pub fn canonical_refs(mut refs: Vec<ObjectRef>) -> Vec<ObjectRef> {
185
185
  /// Read an admitted link from the local Store. Unlike `chain`, this does not require an
186
186
  /// NFS binding: the ordinary transfer plane may have obtained it from the hub.
187
187
  pub fn local_link(store: &Store, object: &ObjectRef) -> Result<Link> {
188
- if object.length > Link::MAX_BYTES as u64 {
188
+ Link::parse(&read_document(store, object, Link::MAX_BYTES)?)
189
+ }
190
+
191
+ /// Bound the actual pinned file before allocation, not only the untrusted claimed length.
192
+ /// Source and derived progress use the same exact ObjectRef admission contract as links.
193
+ pub(crate) fn read_document(store: &Store, object: &ObjectRef, cap: usize) -> Result<Vec<u8>> {
194
+ if object.length > cap as u64 {
195
+ return refuse(Code::COUNT_CAP, "checkpoint document exceeds its byte cap");
196
+ }
197
+ let mut file = store.open_verified(&object.sha256)?;
198
+ if file.len() != object.length {
189
199
  return refuse(
190
- Code::COUNT_CAP,
191
- "checkpoint link exceeds the document byte cap",
200
+ Code::LENGTH_MISMATCH,
201
+ "checkpoint document differs from its ObjectRef length",
192
202
  );
193
203
  }
194
- let mut bytes = Vec::with_capacity(object.length as usize);
195
- store.read_into(&object.sha256, &mut bytes)?;
204
+ let mut bytes = vec![0; object.length as usize];
205
+ std::io::Read::read_exact(&mut file, &mut bytes).map_err(|error| crate::err::Refusal {
206
+ code: Code::IO_FAILED,
207
+ detail: format!("read checkpoint document: {error}"),
208
+ })?;
196
209
  if ObjectRef::of(&bytes) != *object {
197
210
  return refuse(
198
211
  Code::OBJECT_ID_MISMATCH,
199
- "checkpoint link differs from its ObjectRef",
212
+ "checkpoint document differs from its ObjectRef",
200
213
  );
201
214
  }
202
- Link::parse(&bytes)
215
+ Ok(bytes)
203
216
  }
204
217
 
205
218
  /// Each call reads one link and returns a bounded window, so a caller can fetch links and
@@ -1030,6 +1043,40 @@ pub fn restore_conversion(
1030
1043
  mod tests {
1031
1044
  use super::*;
1032
1045
 
1046
+ #[test]
1047
+ fn checkpoint_documents_refuse_wrong_lengths_before_reading_into_memory() {
1048
+ let root = std::env::temp_dir().join(format!(
1049
+ "tensorfs-document-bound-{}",
1050
+ crate::meta::now_nanos_unique()
1051
+ ));
1052
+ let store = Store::init(&root).unwrap();
1053
+ let bytes = b"a verified resident object";
1054
+ let object = store
1055
+ .put_stream(
1056
+ &mut bytes.as_slice(),
1057
+ Some(&ObjectRef::of(bytes)),
1058
+ &Default::default(),
1059
+ )
1060
+ .unwrap()
1061
+ .obj;
1062
+ let mut short = object.clone();
1063
+ short.length = 1;
1064
+ assert_eq!(
1065
+ local_link(&store, &short).unwrap_err().code,
1066
+ Code::LENGTH_MISMATCH
1067
+ );
1068
+ assert_eq!(
1069
+ read_document(&store, &short, 1).unwrap_err().code,
1070
+ Code::LENGTH_MISMATCH
1071
+ );
1072
+ assert_eq!(
1073
+ read_document(&store, &object, 1).unwrap_err().code,
1074
+ Code::COUNT_CAP
1075
+ );
1076
+ assert_eq!(read_document(&store, &object, bytes.len()).unwrap(), bytes);
1077
+ std::fs::remove_dir_all(root).unwrap();
1078
+ }
1079
+
1033
1080
  fn object(byte: &str, length: u64) -> ObjectRef {
1034
1081
  ObjectRef {
1035
1082
  sha256: byte.repeat(32),
@@ -1207,11 +1207,51 @@ fn prepare_model_source_with_registry(
1207
1207
  for source in &sources {
1208
1208
  validate_prepared_result(store, source)?;
1209
1209
  }
1210
+ // The final response can be lost after its catalog commit. Its existing local
1211
+ // cursors must remain discoverable without converting or reading source bodies.
1212
+ let mut checkpoints = Vec::new();
1213
+ let dir = transaction::candidate_dir(store.root(), &request.operation_id);
1214
+ for (requested, (plan_hex, _)) in request.profiles.iter().zip(&plans) {
1215
+ let plan_digest = format!("sha256:{plan_hex}");
1216
+ let chain = format!("{}/{}", request.operation_id, requested.slot);
1217
+ let journal = super::journal::Journal::open(&dir, plan_hex)?;
1218
+ let local = journal.local_head()?.ok_or_else(|| Refusal {
1219
+ code: Code::DURABILITY_UNPROVEN,
1220
+ detail: "completed source has no local checkpoint cursor".into(),
1221
+ })?;
1222
+ let supplied = request
1223
+ .checkpoints
1224
+ .iter()
1225
+ .find(|(slot, _)| slot == &requested.slot)
1226
+ .map(|(_, head)| head);
1227
+ let head = durability::checkpoint_predecessor(
1228
+ store,
1229
+ &durability::Policy {
1230
+ chain: &chain,
1231
+ operation: &request.operation_id,
1232
+ plan: &plan_digest,
1233
+ interval: durability::INTERVAL_BYTES,
1234
+ },
1235
+ Some(&local),
1236
+ supplied,
1237
+ )?
1238
+ .expect("a local cursor was supplied");
1239
+ let held = durability::local_chain(store, &head, &plan_digest)?;
1240
+ checkpoints.push(SourceCheckpoint {
1241
+ slot: requested.slot.clone(),
1242
+ plan_digest,
1243
+ head: durability::Head {
1244
+ head: Some(head),
1245
+ links: held.links,
1246
+ bytes: held.bytes,
1247
+ },
1248
+ });
1249
+ }
1210
1250
  return Ok(PreparedModelSources {
1211
1251
  replayed: true,
1212
1252
  complete: true,
1213
1253
  sources,
1214
- checkpoints: Vec::new(),
1254
+ checkpoints,
1215
1255
  spent: request
1216
1256
  .roster
1217
1257
  .iter()
@@ -1257,20 +1297,8 @@ fn prepare_model_source_with_registry(
1257
1297
  durability::retain_local_chain(store, head, &request.operation_id)?;
1258
1298
  if journal.is_empty() {
1259
1299
  if let Some(progress) = held.progress.last() {
1260
- if progress.length > crate::limits::DOC_MAX_BYTES as u64 {
1261
- return refuse(
1262
- Code::COUNT_CAP,
1263
- "checkpoint progress exceeds the document byte cap",
1264
- );
1265
- }
1266
- let mut bytes = Vec::with_capacity(progress.length as usize);
1267
- store.read_into(&progress.sha256, &mut bytes)?;
1268
- if ObjectRef::of(&bytes) != *progress {
1269
- return refuse(
1270
- Code::OBJECT_ID_MISMATCH,
1271
- "checkpoint progress differs from its ObjectRef",
1272
- );
1273
- }
1300
+ let bytes =
1301
+ durability::read_document(store, progress, crate::limits::DOC_MAX_BYTES)?;
1274
1302
  drop(journal);
1275
1303
  super::journal::install(&dir, &plan_hex, &bytes)?;
1276
1304
  journal = super::journal::Journal::open(&dir, &plan_hex)?;
@@ -1583,6 +1611,19 @@ mod tests {
1583
1611
  .unwrap();
1584
1612
  assert!(replay.replayed);
1585
1613
  assert_eq!(replay.sources, first.sources);
1614
+ assert_eq!(replay.checkpoints, first.checkpoints);
1615
+ let mut bodies_gone = request.clone();
1616
+ bodies_gone.landed.clear();
1617
+ for row in &bodies_gone.roster {
1618
+ fs::remove_file(&row.path).unwrap();
1619
+ }
1620
+ let recovered_heads =
1621
+ prepare_model_source_with_registry(&store, &bodies_gone, "fixture:registry", &registry)
1622
+ .unwrap();
1623
+ assert_eq!(recovered_heads.checkpoints, first.checkpoints);
1624
+ // Remaining arms need their original carriers again.
1625
+ carrier(&request.roster[0].path, "weight", [1, 2, 3, 4]);
1626
+ carrier(&request.roster[1].path, "weight", [5, 6, 7, 8]);
1586
1627
 
1587
1628
  let mut changed = request.clone();
1588
1629
  changed.roster[0].object_id = format!("sha256:{}", "33".repeat(32));
@@ -2239,6 +2280,10 @@ mod tests {
2239
2280
  )
2240
2281
  .unwrap();
2241
2282
  assert!(replay.replayed && replay.complete);
2283
+ assert_eq!(
2284
+ replay.checkpoints, resumed.checkpoints,
2285
+ "a lost final response replays even with an old ACK"
2286
+ );
2242
2287
  let mut unrelated = registry.clone();
2243
2288
  unrelated.entries[0]
2244
2289
  .provenance
@@ -1437,6 +1437,24 @@ impl PyStore {
1437
1437
  Ok(out)
1438
1438
  }
1439
1439
 
1440
+ /// Canonical TensorFS declaration bytes for the current producer intent.
1441
+ #[allow(clippy::too_many_arguments)]
1442
+ #[pyo3(signature = (sources, targets, configs, order, max_new_bytes))]
1443
+ fn derived_declaration<'p>(
1444
+ &self,
1445
+ py: Python<'p>,
1446
+ sources: &Bound<'_, PyDict>,
1447
+ targets: &Bound<'_, PyDict>,
1448
+ configs: &Bound<'_, PyDict>,
1449
+ order: Vec<(String, String)>,
1450
+ max_new_bytes: u64,
1451
+ ) -> PyResult<Bound<'p, PyBytes>> {
1452
+ let mut declaration =
1453
+ parse_declaration(py, sources, targets, configs, order, max_new_bytes)?;
1454
+ let bytes = declaration.canonical_bytes().or_refuse(py)?;
1455
+ Ok(PyBytes::new(py, &bytes))
1456
+ }
1457
+
1440
1458
  /// Open one exact tfs-019 transaction. Python supplies typed plain values; canonical
1441
1459
  /// declaration bytes are constructed and locked only by the compiled core.
1442
1460
  // PyO3 exposes these as six named boundary fields. Packing them into an opaque dict
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.8"
7
+ version = "0.3.9"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [
@@ -481,6 +481,10 @@ class Store:
481
481
  components: Sequence[str],
482
482
  configs: Sequence[str],
483
483
  ) -> dict[str, Any]: ...
484
+ def derived_declaration(
485
+ self, sources: dict[str, Any], targets: dict[str, Any], configs: dict[str, Any],
486
+ order: Sequence[tuple[str, str]], max_new_bytes: int,
487
+ ) -> bytes: ...
484
488
  def begin_derived(
485
489
  self,
486
490
  transaction_id: str,