tensorfs 0.3.9__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.

Potentially problematic release.


This version of tensorfs might be problematic. Click here for more details.

Files changed (107) hide show
  1. {tensorfs-0.3.9 → tensorfs-0.3.10}/Cargo.lock +2 -2
  2. {tensorfs-0.3.9 → tensorfs-0.3.10}/Cargo.toml +1 -1
  3. {tensorfs-0.3.9 → tensorfs-0.3.10}/PKG-INFO +1 -1
  4. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/derived.rs +273 -11
  5. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/gc.rs +26 -0
  6. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/meta.rs +1 -14
  7. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/storage.rs +1 -1
  8. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-py/src/store.rs +6 -0
  9. {tensorfs-0.3.9 → tensorfs-0.3.10}/pyproject.toml +1 -1
  10. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/Cargo.toml +0 -0
  11. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/b64.rs +0 -0
  12. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  13. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  14. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  15. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  16. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  17. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  18. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  19. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  20. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  21. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/canon.rs +0 -0
  22. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/capability.rs +0 -0
  23. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/catalog.rs +0 -0
  24. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/cbor.rs +0 -0
  25. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  26. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/corpus.rs +0 -0
  27. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  28. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/dtype.rs +0 -0
  29. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/durability.rs +0 -0
  30. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/err.rs +0 -0
  31. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/fetch.rs +0 -0
  32. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/fill.rs +0 -0
  33. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/fit.rs +0 -0
  34. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/header.rs +0 -0
  35. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/home.rs +0 -0
  36. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ids.rs +0 -0
  37. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  38. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  39. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  40. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  41. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  42. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  43. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  44. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  45. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  46. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  47. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/jcs.rs +0 -0
  48. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/lib.rs +0 -0
  49. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/limits.rs +0 -0
  50. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/machine.rs +0 -0
  51. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/manifest.rs +0 -0
  52. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/project.rs +0 -0
  53. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/providers.rs +0 -0
  54. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/read.rs +0 -0
  55. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/receipt.rs +0 -0
  56. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/reclaim.rs +0 -0
  57. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/registry.rs +0 -0
  58. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/relation.rs +0 -0
  59. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  60. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/repository.rs +0 -0
  61. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/sha256.rs +0 -0
  62. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/spec.rs +0 -0
  63. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/staging.rs +0 -0
  64. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/store.rs +0 -0
  65. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/api.rs +0 -0
  66. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/http.rs +0 -0
  67. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  68. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  69. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  70. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  71. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  72. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/push.rs +0 -0
  73. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  74. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/transport/tests.rs +0 -0
  75. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/src/vectors.rs +0 -0
  76. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  77. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fit.rs +0 -0
  78. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  79. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  80. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  81. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  82. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  83. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  84. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  85. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/home.rs +0 -0
  86. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/liveness.rs +0 -0
  87. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  88. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  89. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  90. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  91. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  92. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  93. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  94. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  95. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/staging.rs +0 -0
  96. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  97. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  98. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/version.rs +0 -0
  99. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-core/tests/write_through.rs +0 -0
  100. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-py/Cargo.toml +0 -0
  101. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-py/src/docs.rs +0 -0
  102. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-py/src/errors.rs +0 -0
  103. {tensorfs-0.3.9 → tensorfs-0.3.10}/crates/tensorfs-py/src/lib.rs +0 -0
  104. {tensorfs-0.3.9 → tensorfs-0.3.10}/python/tensorfs/__init__.py +0 -0
  105. {tensorfs-0.3.9 → tensorfs-0.3.10}/python/tensorfs/_ext.pyi +0 -0
  106. {tensorfs-0.3.9 → tensorfs-0.3.10}/python/tensorfs/errors.py +0 -0
  107. {tensorfs-0.3.9 → 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.9"
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.9"
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.9"
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.9
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
@@ -1338,22 +1338,28 @@ fn require_payload_record(store: &Store, object: &ObjectRef, what: &str) -> Resu
1338
1338
  }
1339
1339
  }
1340
1340
 
1341
- fn resolve_sources(
1342
- store: &Store,
1343
- meta: &Meta,
1344
- declaration: &Declaration,
1345
- ) -> Result<Vec<SourceData>> {
1346
- 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)?;
1347
1346
  for declared in &declaration.sources {
1348
- if !meta.manifest_is_released(&declared.manifest.sha256)? {
1347
+ if !retained.contains(&declared.manifest) {
1349
1348
  return refuse(
1350
1349
  Code::ROOT_ABSENT,
1351
1350
  format!(
1352
- "source {} is not referenced by a repository release",
1351
+ "source {} has no exact retained filesystem root",
1353
1352
  declared.manifest.id()
1354
1353
  ),
1355
1354
  );
1356
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 {
1357
1363
  let manifest = checkpoint::load_manifest(store, &declared.manifest)?;
1358
1364
  if manifest.manifest_id() != declared.manifest.id() {
1359
1365
  return refuse(
@@ -1791,6 +1797,18 @@ pub fn inspect_source(
1791
1797
  manifest: ObjectRef,
1792
1798
  component_names: Vec<String>,
1793
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>,
1794
1812
  ) -> Result<SourceInspection> {
1795
1813
  let alias = "source".to_string();
1796
1814
  let mut declaration = Declaration {
@@ -1820,7 +1838,8 @@ pub fn inspect_source(
1820
1838
  max_new_bytes: 0,
1821
1839
  };
1822
1840
  declaration.normalize_and_validate()?;
1823
- let mut sources = resolve_sources(store, meta, &declaration)?;
1841
+ require_source_roots(store, &declaration)?;
1842
+ let mut sources = load_sources(store, &declaration)?;
1824
1843
  let source = sources.pop().ok_or_else(|| Refusal {
1825
1844
  code: Code::ROOT_ABSENT,
1826
1845
  detail: "source inspection resolved no source".into(),
@@ -1882,7 +1901,9 @@ pub fn begin(
1882
1901
  // The writer hold is registered before source metadata is touched. It is the live
1883
1902
  // process proof that keeps exclusive GC out even for a source-free create.
1884
1903
  let writer_hold = meta.acquire_hold("derived-writer")?;
1885
- 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
+ {
1886
1907
  Ok(sources) => sources,
1887
1908
  Err(error) => {
1888
1909
  release_guards(meta, Some(writer_hold), Vec::new());
@@ -2330,6 +2351,9 @@ fn receipt_from(_store: &Store, row: &TransactionRow) -> Result<ReceiptFacts> {
2330
2351
  })
2331
2352
  }
2332
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.
2333
2357
  pub fn commit(
2334
2358
  store: &Store,
2335
2359
  meta: &Meta,
@@ -2342,7 +2366,7 @@ pub fn commit(
2342
2366
  open_row(&mut txn.rows, &transaction, writer_session)?.clone()
2343
2367
  };
2344
2368
  let declaration = declaration(&row_before)?;
2345
- let sources = resolve_sources(store, meta, &declaration)?;
2369
+ let sources = load_sources(store, &declaration)?;
2346
2370
  let (header, closure, registry_adoptions) = build_header(
2347
2371
  &declaration,
2348
2372
  &sources,
@@ -2700,6 +2724,235 @@ mod tests {
2700
2724
  }
2701
2725
  }
2702
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
+
2703
2956
  #[test]
2704
2957
  fn private_adoption_retains_real_payload_until_dispose_without_a_repository() {
2705
2958
  let root = std::env::temp_dir().join(format!(
@@ -2946,6 +3199,15 @@ mod tests {
2946
3199
  )
2947
3200
  .unwrap()
2948
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();
2949
3211
  store
2950
3212
  .apply_repository(
2951
3213
  Some(&checkpointed.canonical_bytes()),
@@ -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)?;
@@ -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() {
@@ -1892,6 +1892,12 @@ impl PyDerivedWriter {
1892
1892
  fn commit<'p>(&self, py: Python<'p>) -> PyResult<Bound<'p, PyAny>> {
1893
1893
  let value = py
1894
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)?;
1895
1901
  derived::commit(
1896
1902
  &self.store,
1897
1903
  &self.meta,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "tensorfs"
7
- version = "0.3.9"
7
+ version = "0.3.10"
8
8
  description = "TensorFS — canonical tensor artifact substrate"
9
9
  requires-python = ">=3.12"
10
10
  classifiers = [