tensorfs 0.3.8__tar.gz → 0.3.10__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.10}/Cargo.lock +2 -2
  2. {tensorfs-0.3.8 → tensorfs-0.3.10}/Cargo.toml +1 -1
  3. {tensorfs-0.3.8 → tensorfs-0.3.10}/PKG-INFO +1 -1
  4. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/derived.rs +289 -20
  5. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/durability.rs +54 -7
  6. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/gc.rs +26 -0
  7. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/source.rs +60 -15
  8. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/meta.rs +1 -14
  9. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/storage.rs +1 -1
  10. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-py/src/store.rs +24 -0
  11. {tensorfs-0.3.8 → tensorfs-0.3.10}/pyproject.toml +1 -1
  12. {tensorfs-0.3.8 → tensorfs-0.3.10}/python/tensorfs/_ext.pyi +4 -0
  13. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/Cargo.toml +0 -0
  14. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/b64.rs +0 -0
  15. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  16. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  17. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  18. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  19. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  20. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  21. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  22. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  23. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  24. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/canon.rs +0 -0
  25. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/capability.rs +0 -0
  26. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/catalog.rs +0 -0
  27. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/cbor.rs +0 -0
  28. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  29. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/corpus.rs +0 -0
  30. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  31. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/dtype.rs +0 -0
  32. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/err.rs +0 -0
  33. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/fetch.rs +0 -0
  34. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/fill.rs +0 -0
  35. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/fit.rs +0 -0
  36. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/header.rs +0 -0
  37. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/home.rs +0 -0
  38. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ids.rs +0 -0
  39. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  40. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  41. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  42. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  43. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  44. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  45. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  46. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  47. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  48. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/jcs.rs +0 -0
  49. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/lib.rs +0 -0
  50. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/limits.rs +0 -0
  51. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/machine.rs +0 -0
  52. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/manifest.rs +0 -0
  53. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/project.rs +0 -0
  54. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/providers.rs +0 -0
  55. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/read.rs +0 -0
  56. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/receipt.rs +0 -0
  57. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/reclaim.rs +0 -0
  58. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/registry.rs +0 -0
  59. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/relation.rs +0 -0
  60. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  61. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/repository.rs +0 -0
  62. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/sha256.rs +0 -0
  63. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/spec.rs +0 -0
  64. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/staging.rs +0 -0
  65. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/store.rs +0 -0
  66. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/api.rs +0 -0
  67. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/http.rs +0 -0
  68. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  69. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  70. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  71. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  72. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  73. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/push.rs +0 -0
  74. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  75. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  76. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/src/vectors.rs +0 -0
  77. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  78. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fit.rs +0 -0
  79. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  80. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  81. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  82. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  83. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  84. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  85. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  86. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/home.rs +0 -0
  87. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/liveness.rs +0 -0
  88. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  89. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  90. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  91. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  92. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  93. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  94. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  95. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  96. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/staging.rs +0 -0
  97. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  98. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  99. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/version.rs +0 -0
  100. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-core/tests/write_through.rs +0 -0
  101. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-py/Cargo.toml +0 -0
  102. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-py/src/docs.rs +0 -0
  103. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-py/src/errors.rs +0 -0
  104. {tensorfs-0.3.8 → tensorfs-0.3.10}/crates/tensorfs-py/src/lib.rs +0 -0
  105. {tensorfs-0.3.8 → tensorfs-0.3.10}/python/tensorfs/__init__.py +0 -0
  106. {tensorfs-0.3.8 → tensorfs-0.3.10}/python/tensorfs/errors.py +0 -0
  107. {tensorfs-0.3.8 → tensorfs-0.3.10}/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.10"
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.10"
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.10"
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.10
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(
@@ -1324,22 +1338,28 @@ fn require_payload_record(store: &Store, object: &ObjectRef, what: &str) -> Resu
1324
1338
  }
1325
1339
  }
1326
1340
 
1327
- fn resolve_sources(
1328
- store: &Store,
1329
- meta: &Meta,
1330
- declaration: &Declaration,
1331
- ) -> Result<Vec<SourceData>> {
1332
- let mut out = Vec::new();
1341
+ fn require_source_roots(store: &Store, declaration: &Declaration) -> Result<()> {
1342
+ if declaration.sources.is_empty() {
1343
+ return Ok(());
1344
+ }
1345
+ let retained = crate::gc::retained_manifests(store)?;
1333
1346
  for declared in &declaration.sources {
1334
- if !meta.manifest_is_released(&declared.manifest.sha256)? {
1347
+ if !retained.contains(&declared.manifest) {
1335
1348
  return refuse(
1336
1349
  Code::ROOT_ABSENT,
1337
1350
  format!(
1338
- "source {} is not referenced by a repository release",
1351
+ "source {} has no exact retained filesystem root",
1339
1352
  declared.manifest.id()
1340
1353
  ),
1341
1354
  );
1342
1355
  }
1356
+ }
1357
+ Ok(())
1358
+ }
1359
+
1360
+ fn load_sources(store: &Store, declaration: &Declaration) -> Result<Vec<SourceData>> {
1361
+ let mut out = Vec::new();
1362
+ for declared in &declaration.sources {
1343
1363
  let manifest = checkpoint::load_manifest(store, &declared.manifest)?;
1344
1364
  if manifest.manifest_id() != declared.manifest.id() {
1345
1365
  return refuse(
@@ -1777,6 +1797,18 @@ pub fn inspect_source(
1777
1797
  manifest: ObjectRef,
1778
1798
  component_names: Vec<String>,
1779
1799
  config_names: Vec<String>,
1800
+ ) -> Result<SourceInspection> {
1801
+ let hold = meta.acquire_hold("derived-source-inspect")?;
1802
+ let result = inspect_retained_source(store, manifest, component_names, config_names);
1803
+ let released = hold.release(meta);
1804
+ result.and_then(|inspection| released.map(|()| inspection))
1805
+ }
1806
+
1807
+ fn inspect_retained_source(
1808
+ store: &Store,
1809
+ manifest: ObjectRef,
1810
+ component_names: Vec<String>,
1811
+ config_names: Vec<String>,
1780
1812
  ) -> Result<SourceInspection> {
1781
1813
  let alias = "source".to_string();
1782
1814
  let mut declaration = Declaration {
@@ -1806,7 +1838,8 @@ pub fn inspect_source(
1806
1838
  max_new_bytes: 0,
1807
1839
  };
1808
1840
  declaration.normalize_and_validate()?;
1809
- let mut sources = resolve_sources(store, meta, &declaration)?;
1841
+ require_source_roots(store, &declaration)?;
1842
+ let mut sources = load_sources(store, &declaration)?;
1810
1843
  let source = sources.pop().ok_or_else(|| Refusal {
1811
1844
  code: Code::ROOT_ABSENT,
1812
1845
  detail: "source inspection resolved no source".into(),
@@ -1862,20 +1895,15 @@ pub fn begin(
1862
1895
  "WriterSessionId must be positive and inside the canonical integer range",
1863
1896
  );
1864
1897
  }
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
- }
1898
+ let declaration_text =
1899
+ String::from_utf8(declaration.canonical_bytes()?).expect("canonical JSON is UTF-8");
1874
1900
 
1875
1901
  // The writer hold is registered before source metadata is touched. It is the live
1876
1902
  // process proof that keeps exclusive GC out even for a source-free create.
1877
1903
  let writer_hold = meta.acquire_hold("derived-writer")?;
1878
- let sources = match resolve_sources(store, meta, &declaration) {
1904
+ let sources = match require_source_roots(store, &declaration)
1905
+ .and_then(|()| load_sources(store, &declaration))
1906
+ {
1879
1907
  Ok(sources) => sources,
1880
1908
  Err(error) => {
1881
1909
  release_guards(meta, Some(writer_hold), Vec::new());
@@ -2323,6 +2351,9 @@ fn receipt_from(_store: &Store, row: &TransactionRow) -> Result<ReceiptFacts> {
2323
2351
  })
2324
2352
  }
2325
2353
 
2354
+ /// The caller retains Begin's writer hold and source leases through this call.
2355
+ /// Root membership was admitted at begin; the live hold now protects those exact
2356
+ /// bytes even if their original repository/session/private root is released.
2326
2357
  pub fn commit(
2327
2358
  store: &Store,
2328
2359
  meta: &Meta,
@@ -2335,7 +2366,7 @@ pub fn commit(
2335
2366
  open_row(&mut txn.rows, &transaction, writer_session)?.clone()
2336
2367
  };
2337
2368
  let declaration = declaration(&row_before)?;
2338
- let sources = resolve_sources(store, meta, &declaration)?;
2369
+ let sources = load_sources(store, &declaration)?;
2339
2370
  let (header, closure, registry_adoptions) = build_header(
2340
2371
  &declaration,
2341
2372
  &sources,
@@ -2693,6 +2724,235 @@ mod tests {
2693
2724
  }
2694
2725
  }
2695
2726
 
2727
+ fn inherited_declaration(manifest: ObjectRef) -> Declaration {
2728
+ Declaration {
2729
+ sources: vec![Source {
2730
+ alias: "source".into(),
2731
+ manifest,
2732
+ }],
2733
+ components: vec![ComponentDeclaration {
2734
+ target: "inherited".into(),
2735
+ source: Some("source".into()),
2736
+ source_component: Some("model".into()),
2737
+ drop: Vec::new(),
2738
+ add: Vec::new(),
2739
+ }],
2740
+ configs: Vec::new(),
2741
+ order: vec![("inherited".into(), "weight".into())],
2742
+ max_new_bytes: 0,
2743
+ }
2744
+ }
2745
+
2746
+ #[test]
2747
+ fn retained_source_admission_and_writer_hold_share_the_gc_roots() {
2748
+ let root = std::env::temp_dir().join(format!(
2749
+ "tensorfs-retained-source-{}",
2750
+ crate::meta::now_nanos_unique()
2751
+ ));
2752
+ let store = Store::init(&root).unwrap();
2753
+ let meta = Meta::open(&store).unwrap();
2754
+ let source_id = format!("sha256:{}", "81".repeat(32));
2755
+ let source_writer = begin(&store, &meta, &source_id, 1, created_declaration()).unwrap();
2756
+ let bytes = vec![0x21; 2048];
2757
+ add_part(
2758
+ &store,
2759
+ &meta,
2760
+ &source_id,
2761
+ 1,
2762
+ ("model", "weight", "value"),
2763
+ &mut bytes.as_slice(),
2764
+ )
2765
+ .unwrap();
2766
+ let source = commit(&store, &meta, &source_id, 1).unwrap();
2767
+ release_guards(
2768
+ &meta,
2769
+ Some(source_writer.writer_hold),
2770
+ source_writer.source_leases,
2771
+ );
2772
+ let preview = || {
2773
+ inspect_source(
2774
+ &store,
2775
+ &meta,
2776
+ source.manifest.clone(),
2777
+ vec!["model".into()],
2778
+ Vec::new(),
2779
+ )
2780
+ };
2781
+
2782
+ // Committed pending output and owner-adopted output have real filesystem roots.
2783
+ preview().unwrap();
2784
+ adopt(&store, &meta, &source_id, "source-private").unwrap();
2785
+ preview().unwrap();
2786
+ dispose(&store, &meta, &source_id).unwrap();
2787
+ assert_eq!(preview().unwrap_err().code, Code::ROOT_ABSENT);
2788
+ let target_id = format!("sha256:{}", "82".repeat(32));
2789
+ assert_eq!(
2790
+ begin(
2791
+ &store,
2792
+ &meta,
2793
+ &target_id,
2794
+ 1,
2795
+ inherited_declaration(source.manifest.clone())
2796
+ )
2797
+ .err()
2798
+ .unwrap()
2799
+ .code,
2800
+ Code::ROOT_ABSENT
2801
+ );
2802
+ assert!(matches!(
2803
+ lookup(&store, &meta, &target_id).unwrap(),
2804
+ Lookup::Absent
2805
+ ));
2806
+
2807
+ // The coordinator's actual prepared-source lifecycle names a private candidate.
2808
+ let operation = "prepared-source";
2809
+ let request_digest = format!("sha256:{}", "83".repeat(32));
2810
+ let catalog = crate::catalog::Catalog::open(&root).unwrap();
2811
+ let crate::catalog::SourcePreparation::Open(source_guard) = catalog
2812
+ .begin_source_preparation(operation, &request_digest)
2813
+ .unwrap()
2814
+ else {
2815
+ panic!("source operation already prepared")
2816
+ };
2817
+ crate::ingest::transaction::ensure_session_root(&root, operation, "_tensorfs").unwrap();
2818
+ crate::ingest::transaction::name_candidates(
2819
+ &root,
2820
+ operation,
2821
+ std::slice::from_ref(&source.manifest),
2822
+ )
2823
+ .unwrap();
2824
+ // An actual candidate is retained even before the rebuildable result row is written.
2825
+ preview().unwrap();
2826
+ catalog
2827
+ .commit_source_preparation(
2828
+ operation,
2829
+ &request_digest,
2830
+ &[crate::catalog::SourcePreparationResult {
2831
+ slot: "source".into(),
2832
+ profile: "proof".into(),
2833
+ manifest_sha256: source.manifest.sha256.clone(),
2834
+ manifest_length: source.manifest.length,
2835
+ }],
2836
+ )
2837
+ .unwrap();
2838
+ drop(source_guard);
2839
+ crate::gc::collect(&root, false).unwrap();
2840
+ preview().unwrap();
2841
+ let mut wrong = inherited_declaration(source.manifest.clone());
2842
+ wrong.sources[0].manifest.length += 1;
2843
+ assert_eq!(
2844
+ begin(&store, &meta, &target_id, 1, wrong)
2845
+ .err()
2846
+ .unwrap()
2847
+ .code,
2848
+ Code::ROOT_ABSENT
2849
+ );
2850
+
2851
+ let writer = begin(
2852
+ &store,
2853
+ &meta,
2854
+ &target_id,
2855
+ 1,
2856
+ inherited_declaration(source.manifest.clone()),
2857
+ )
2858
+ .unwrap();
2859
+ crate::ingest::source::release_model_source(&store, operation).unwrap();
2860
+ assert_eq!(preview().unwrap_err().code, Code::ROOT_ABSENT);
2861
+ writer.writer_hold.still_registered(&meta).unwrap();
2862
+ assert_eq!(
2863
+ crate::gc::collect(&root, false).unwrap_err().code,
2864
+ Code::STORE_BUSY
2865
+ );
2866
+ let inherited = commit(&store, &meta, &target_id, 1).unwrap();
2867
+ assert_eq!(inherited.inherited_payload_bytes, 2048);
2868
+ assert!(inherited.added_objects.is_empty());
2869
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
2870
+ crate::gc::collect(&root, false).unwrap();
2871
+ store.read_manifest(&inherited.manifest).unwrap();
2872
+ inspect_source(
2873
+ &store,
2874
+ &meta,
2875
+ inherited.manifest.clone(),
2876
+ vec!["inherited".into()],
2877
+ Vec::new(),
2878
+ )
2879
+ .unwrap();
2880
+ dispose(&store, &meta, &target_id).unwrap();
2881
+ assert_eq!(
2882
+ inspect_source(
2883
+ &store,
2884
+ &meta,
2885
+ inherited.manifest,
2886
+ vec!["inherited".into()],
2887
+ Vec::new()
2888
+ )
2889
+ .unwrap_err()
2890
+ .code,
2891
+ Code::ROOT_ABSENT
2892
+ );
2893
+ let _ = std::fs::remove_dir_all(root);
2894
+ }
2895
+
2896
+ #[test]
2897
+ fn retained_source_still_refuses_corrupt_payload_and_abandoned_writer() {
2898
+ let root = std::env::temp_dir().join(format!(
2899
+ "tensorfs-retained-source-refusal-{}",
2900
+ crate::meta::now_nanos_unique()
2901
+ ));
2902
+ let store = Store::init(&root).unwrap();
2903
+ let meta = Meta::open(&store).unwrap();
2904
+ let source_id = format!("sha256:{}", "84".repeat(32));
2905
+ let source_writer = begin(&store, &meta, &source_id, 1, created_declaration()).unwrap();
2906
+ let bytes = vec![0x31; 2048];
2907
+ let part = add_part(
2908
+ &store,
2909
+ &meta,
2910
+ &source_id,
2911
+ 1,
2912
+ ("model", "weight", "value"),
2913
+ &mut bytes.as_slice(),
2914
+ )
2915
+ .unwrap();
2916
+ let source = commit(&store, &meta, &source_id, 1).unwrap();
2917
+ release_guards(
2918
+ &meta,
2919
+ Some(source_writer.writer_hold),
2920
+ source_writer.source_leases,
2921
+ );
2922
+ let target_id = format!("sha256:{}", "85".repeat(32));
2923
+ let writer = begin(
2924
+ &store,
2925
+ &meta,
2926
+ &target_id,
2927
+ 1,
2928
+ inherited_declaration(source.manifest.clone()),
2929
+ )
2930
+ .unwrap();
2931
+ assert!(fence(&meta, &target_id, 1).unwrap());
2932
+ abandon(&store, &meta, &target_id).unwrap();
2933
+ assert_eq!(
2934
+ commit(&store, &meta, &target_id, 1).unwrap_err().code,
2935
+ Code::TRANSACTION_CLOSED
2936
+ );
2937
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
2938
+ let path = store.blob_path(&part.part.segments()[0].sha256);
2939
+ std::fs::remove_file(&path).unwrap();
2940
+ std::fs::write(path, vec![0x32; 2048]).unwrap();
2941
+ assert_eq!(
2942
+ inspect_source(
2943
+ &store,
2944
+ &meta,
2945
+ source.manifest,
2946
+ vec!["model".into()],
2947
+ Vec::new()
2948
+ )
2949
+ .unwrap_err()
2950
+ .code,
2951
+ Code::OBJECT_CORRUPT
2952
+ );
2953
+ let _ = std::fs::remove_dir_all(root);
2954
+ }
2955
+
2696
2956
  #[test]
2697
2957
  fn private_adoption_retains_real_payload_until_dispose_without_a_repository() {
2698
2958
  let root = std::env::temp_dir().join(format!(
@@ -2939,6 +3199,15 @@ mod tests {
2939
3199
  )
2940
3200
  .unwrap()
2941
3201
  .unwrap();
3202
+ // An owner-retained checkpoint is a GC root before any public release exists.
3203
+ inspect_source(
3204
+ &store,
3205
+ &meta,
3206
+ manifest_ref.clone(),
3207
+ vec!["model".into()],
3208
+ Vec::new(),
3209
+ )
3210
+ .unwrap();
2942
3211
  store
2943
3212
  .apply_repository(
2944
3213
  Some(&checkpointed.canonical_bytes()),
@@ -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),
@@ -154,6 +154,32 @@ pub fn session_holds(store: &Store) -> Result<(Vec<HeldKey>, Vec<String>)> {
154
154
  Ok((holds, sessions))
155
155
  }
156
156
 
157
+ /// The same exact manifest roots used by reclamation, without a payload census.
158
+ /// Membership is retention, not caller authorization. A consumer must acquire its
159
+ /// live hold before checking roots so GC cannot remove bytes during admission.
160
+ pub(crate) fn retained_manifests(store: &Store) -> Result<Vec<crate::ids::ObjectRef>> {
161
+ let mut roots = Vec::new();
162
+ for (repository, _) in storage::read_repositories(store.root())? {
163
+ roots.extend(
164
+ repository
165
+ .checkpoints
166
+ .into_iter()
167
+ .map(|checkpoint| checkpoint.manifest),
168
+ );
169
+ }
170
+ let (holds, _) = session_holds(store)?;
171
+ roots.extend(
172
+ holds
173
+ .into_iter()
174
+ .filter(|hold| hold.kind == "manifest")
175
+ .map(|hold| crate::ids::ObjectRef {
176
+ sha256: hold.sha256,
177
+ length: hold.length,
178
+ }),
179
+ );
180
+ Ok(roots)
181
+ }
182
+
157
183
  /// One complete pass: exclusivity, orphan temps, census, plan, delete, catalog repair.
158
184
  pub fn collect(root: &Path, dry_run: bool) -> Result<Report> {
159
185
  let store = Store::open(root)?;
@@ -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
@@ -11,7 +11,7 @@ use fs2::FileExt;
11
11
  use rusqlite::{params, Connection};
12
12
 
13
13
  use crate::err::{refuse, Code, Refusal, Result};
14
- use crate::ids::{ascii_name, hex64};
14
+ use crate::ids::ascii_name;
15
15
  use crate::limits;
16
16
  use crate::store::Store;
17
17
 
@@ -82,19 +82,6 @@ impl Meta {
82
82
  self.lease_dir.join(format!("{id}.lease"))
83
83
  }
84
84
 
85
- pub fn manifest_is_released(&self, digest: &str) -> Result<bool> {
86
- hex64("manifest", digest)?;
87
- let count: u64 = self
88
- .connection()?
89
- .query_row(
90
- "SELECT count(*) FROM tensorfs_released_manifests WHERE manifest_sha256=?1",
91
- [digest],
92
- |row| row.get(0),
93
- )
94
- .map_err(|error| sql("query released manifest", error))?;
95
- Ok(count != 0)
96
- }
97
-
98
85
  pub(crate) fn txn(&self) -> Result<Txn> {
99
86
  let connection = self.connection()?;
100
87
  connection
@@ -1223,7 +1223,7 @@ impl GcDelete {
1223
1223
  }
1224
1224
  }
1225
1225
 
1226
- fn read_repositories(root: &Path) -> Result<Vec<(Repository, Vec<u8>)>> {
1226
+ pub(crate) fn read_repositories(root: &Path) -> Result<Vec<(Repository, Vec<u8>)>> {
1227
1227
  let mut result = Vec::new();
1228
1228
  for org_path in read_dir(&root.join("repos"))? {
1229
1229
  if !org_path.is_dir() {
@@ -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
@@ -1874,6 +1892,12 @@ impl PyDerivedWriter {
1874
1892
  fn commit<'p>(&self, py: Python<'p>) -> PyResult<Bound<'p, PyAny>> {
1875
1893
  let value = py
1876
1894
  .detach(|| {
1895
+ let guards = self.guards.lock().expect("writer hold mutex poisoned");
1896
+ let guards = guards.as_ref().ok_or_else(|| Refusal {
1897
+ code: Code::LEASE_REVOKED,
1898
+ detail: "derived writer holds were released".into(),
1899
+ })?;
1900
+ guards.writer_hold.still_registered(&self.meta)?;
1877
1901
  derived::commit(
1878
1902
  &self.store,
1879
1903
  &self.meta,
@@ -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.10"
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,