tensorfs 0.3.36__tar.gz → 0.3.38__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 (127) hide show
  1. {tensorfs-0.3.36 → tensorfs-0.3.38}/Cargo.lock +32 -2
  2. {tensorfs-0.3.36 → tensorfs-0.3.38}/Cargo.toml +1 -1
  3. {tensorfs-0.3.36 → tensorfs-0.3.38}/PKG-INFO +1 -1
  4. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/Cargo.toml +1 -0
  5. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/adoption.rs +140 -0
  6. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/reclaim.rs +34 -106
  7. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/repo_cache.rs +340 -104
  8. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/tests.rs +64 -194
  9. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/reclaim_run.rs +65 -28
  10. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/repo_cache.rs +155 -4
  11. {tensorfs-0.3.36 → tensorfs-0.3.38}/pyproject.toml +1 -1
  12. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/build.rs +0 -0
  13. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  14. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  15. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  16. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  17. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/b64.rs +0 -0
  18. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  19. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  20. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  21. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  22. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  23. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  24. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  25. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  26. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  27. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  28. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/canon.rs +0 -0
  29. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/capability.rs +0 -0
  30. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/catalog.rs +0 -0
  31. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/cbor.rs +0 -0
  32. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  33. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/corpus.rs +0 -0
  34. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  35. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  36. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  37. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  38. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  39. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/derived.rs +0 -0
  40. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/dtype.rs +0 -0
  41. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/durability.rs +0 -0
  42. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/err.rs +0 -0
  43. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/fetch.rs +0 -0
  44. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/fill.rs +0 -0
  45. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/fit.rs +0 -0
  46. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/gc.rs +0 -0
  47. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/header.rs +0 -0
  48. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/home.rs +0 -0
  49. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ids.rs +0 -0
  50. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  51. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  52. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  53. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  54. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  55. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  56. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  57. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  58. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  59. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  60. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/jcs.rs +0 -0
  61. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/lib.rs +0 -0
  62. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/limits.rs +0 -0
  63. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/machine.rs +0 -0
  64. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/manifest.rs +0 -0
  65. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/meta.rs +0 -0
  66. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/project.rs +0 -0
  67. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/providers.rs +0 -0
  68. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/read.rs +0 -0
  69. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/receipt.rs +0 -0
  70. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/registry.rs +0 -0
  71. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/relation.rs +0 -0
  72. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/repository.rs +0 -0
  73. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/sha256.rs +0 -0
  74. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  75. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/spec.rs +0 -0
  76. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/staging.rs +0 -0
  77. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/storage.rs +0 -0
  78. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/store.rs +0 -0
  79. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/api.rs +0 -0
  80. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/http.rs +0 -0
  81. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  82. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/ledger.rs +0 -0
  83. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  84. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  85. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  86. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/pull.rs +0 -0
  87. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/push.rs +0 -0
  88. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  89. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/src/vectors.rs +0 -0
  90. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  91. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fit.rs +0 -0
  92. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  93. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  94. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  95. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  96. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  97. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  98. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  99. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  100. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/home.rs +0 -0
  101. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  102. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  103. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/liveness.rs +0 -0
  104. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  105. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  106. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  107. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  108. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  109. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  110. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  111. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  112. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  113. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  114. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/staging.rs +0 -0
  115. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  116. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  117. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/version.rs +0 -0
  118. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-core/tests/write_through.rs +0 -0
  119. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-py/Cargo.toml +0 -0
  120. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-py/src/docs.rs +0 -0
  121. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-py/src/errors.rs +0 -0
  122. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-py/src/lib.rs +0 -0
  123. {tensorfs-0.3.36 → tensorfs-0.3.38}/crates/tensorfs-py/src/store.rs +0 -0
  124. {tensorfs-0.3.36 → tensorfs-0.3.38}/python/tensorfs/__init__.py +0 -0
  125. {tensorfs-0.3.36 → tensorfs-0.3.38}/python/tensorfs/_ext.pyi +0 -0
  126. {tensorfs-0.3.36 → tensorfs-0.3.38}/python/tensorfs/errors.py +0 -0
  127. {tensorfs-0.3.36 → tensorfs-0.3.38}/python/tensorfs/py.typed +0 -0
@@ -48,6 +48,16 @@ version = "0.5.8"
48
48
  source = "registry+https://github.com/rust-lang/crates.io-index"
49
49
  checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
50
50
 
51
+ [[package]]
52
+ name = "errno"
53
+ version = "0.3.14"
54
+ source = "registry+https://github.com/rust-lang/crates.io-index"
55
+ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
56
+ dependencies = [
57
+ "libc",
58
+ "windows-sys",
59
+ ]
60
+
51
61
  [[package]]
52
62
  name = "fallible-iterator"
53
63
  version = "0.3.0"
@@ -128,6 +138,12 @@ dependencies = [
128
138
  "vcpkg",
129
139
  ]
130
140
 
141
+ [[package]]
142
+ name = "linux-raw-sys"
143
+ version = "0.12.1"
144
+ source = "registry+https://github.com/rust-lang/crates.io-index"
145
+ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
146
+
131
147
  [[package]]
132
148
  name = "minicbor"
133
149
  version = "2.3.0"
@@ -290,6 +306,19 @@ dependencies = [
290
306
  "smallvec",
291
307
  ]
292
308
 
309
+ [[package]]
310
+ name = "rustix"
311
+ version = "1.1.4"
312
+ source = "registry+https://github.com/rust-lang/crates.io-index"
313
+ checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
314
+ dependencies = [
315
+ "bitflags",
316
+ "errno",
317
+ "libc",
318
+ "linux-raw-sys",
319
+ "windows-sys",
320
+ ]
321
+
293
322
  [[package]]
294
323
  name = "rustls"
295
324
  version = "0.23.43"
@@ -392,19 +421,20 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
392
421
 
393
422
  [[package]]
394
423
  name = "tensorfs-core"
395
- version = "0.3.36"
424
+ version = "0.3.38"
396
425
  dependencies = [
397
426
  "fs2",
398
427
  "minicbor",
399
428
  "rcgen",
400
429
  "rusqlite",
430
+ "rustix",
401
431
  "rustls",
402
432
  "webpki-roots 0.26.11",
403
433
  ]
404
434
 
405
435
  [[package]]
406
436
  name = "tensorfs-py"
407
- version = "0.3.36"
437
+ version = "0.3.38"
408
438
  dependencies = [
409
439
  "pyo3",
410
440
  "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.36"
6
+ version = "0.3.38"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.36
3
+ Version: 0.3.38
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -11,6 +11,7 @@ path = "src/bin/tfs/main.rs"
11
11
  fs2 = "0.4"
12
12
  minicbor = { version = "2.3.0", default-features = false, features = ["alloc"] }
13
13
  rusqlite = { version = "0.32", features = ["bundled"] }
14
+ rustix = { version = "1.1.4", default-features = false, features = ["fs", "std"] }
14
15
  # tfs-049: the ONE transport (src/transport/). rustls is the one TLS stack, used only
15
16
  # there; scripts/fence.py refuses it anywhere else so a second mover cannot grow.
16
17
  rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
@@ -250,6 +250,146 @@ fn export(
250
250
  #[cfg(test)]
251
251
  mod tests {
252
252
  use super::*;
253
+ use std::collections::BTreeSet;
254
+ use std::sync::atomic::{AtomicU64, Ordering};
255
+ use std::sync::Arc;
256
+
257
+ #[derive(Default)]
258
+ struct MeasuredIO {
259
+ moved: AtomicU64,
260
+ admitted: AtomicU64,
261
+ }
262
+
263
+ impl crate::store::Progress for MeasuredIO {
264
+ fn moved(&self, bytes: u64) {
265
+ self.moved.fetch_add(bytes, Ordering::Relaxed);
266
+ }
267
+ fn admitted(&self) {
268
+ self.admitted.fetch_add(1, Ordering::Relaxed);
269
+ }
270
+ }
271
+
272
+ impl MeasuredIO {
273
+ fn take(&self) -> (u64, u64) {
274
+ (
275
+ self.moved.swap(0, Ordering::Relaxed),
276
+ self.admitted.swap(0, Ordering::Relaxed),
277
+ )
278
+ }
279
+ }
280
+
281
+ #[test]
282
+ fn multiple_checkpoint_groups_separate_rehash_from_adoption_metadata_io() {
283
+ let (store, meta, mut declaration, source, target) = fixture();
284
+ let mut third = declaration.components[0].add[0].clone();
285
+ third.key = "third".into();
286
+ declaration.components[0].add.push(third);
287
+ declaration.order.push(("model".into(), "third".into()));
288
+ declaration.max_new_bytes += 2048;
289
+ let raw = declaration.canonical_work_bytes().unwrap();
290
+ let writer = begin(&store, &meta, &source, 1, declaration.clone(), None).unwrap();
291
+ let first = write(&store, &meta, &source, "weight", 3);
292
+ checkpoint_progress(&store, &meta, &source, 1, "source", "model", None).unwrap();
293
+ let second = write(&store, &meta, &source, "next", 4);
294
+ let (_, checkpoint) =
295
+ checkpoint_progress(&store, &meta, &source, 1, "source", "model", None).unwrap();
296
+ fence(&meta, &source, 1).unwrap();
297
+ release_guards(&meta, Some(writer.writer_hold), writer.source_leases);
298
+
299
+ let progress = Arc::new(MeasuredIO::default());
300
+ let store = store.observed(progress.clone());
301
+ eprintln!("CR108_IO_ROOT {}", store.root().display());
302
+ eprintln!("CR108_IO_PAYLOAD {} {}", first.sha256, second.sha256);
303
+ // Lose only the verification receipts in this fixture, not the actual
304
+ // bytes. The real verifier must rehash them before the warm adoption arm.
305
+ let catalog = crate::catalog::Catalog::open(store.root()).unwrap();
306
+ catalog.drop_verification(&first.sha256);
307
+ catalog.drop_verification(&second.sha256);
308
+ drop(catalog);
309
+ eprintln!("CR108_IO_PHASE verification");
310
+ assert!(matches!(
311
+ store.verify(&first.sha256).unwrap(),
312
+ crate::store::Verdict::Invalidated { .. }
313
+ ));
314
+ assert!(matches!(
315
+ store.verify(&second.sha256).unwrap(),
316
+ crate::store::Verdict::Invalidated { .. }
317
+ ));
318
+ let verification = progress.take();
319
+ assert_eq!(verification, (4096, 0));
320
+
321
+ eprintln!("CR108_IO_PHASE inspect_before");
322
+ let before: BTreeSet<_> = store.objects().unwrap().into_iter().collect();
323
+ let before_payloads: Vec<_> = [&first, &second]
324
+ .iter()
325
+ .map(|object| {
326
+ let path = store.blob_path(&object.sha256);
327
+ (
328
+ std::fs::read(&path).unwrap(),
329
+ std::fs::metadata(path).unwrap().modified().unwrap(),
330
+ )
331
+ })
332
+ .collect();
333
+ eprintln!("CR108_IO_PHASE adoption");
334
+ let (_, adopted) = adopt_checkpoint(
335
+ &store,
336
+ &meta,
337
+ &target,
338
+ &source,
339
+ &raw,
340
+ checkpoint.head.as_ref().unwrap(),
341
+ "target",
342
+ "model",
343
+ )
344
+ .unwrap();
345
+ let adoption = progress.take();
346
+ eprintln!("CR108_IO_PHASE inspect_after");
347
+ let fresh: Vec<_> = store
348
+ .objects()
349
+ .unwrap()
350
+ .into_iter()
351
+ .filter(|id| !before.contains(id))
352
+ .collect();
353
+ let metadata_bytes: u64 = fresh
354
+ .iter()
355
+ .map(|id| std::fs::metadata(store.blob_path(id)).unwrap().len())
356
+ .sum();
357
+ assert!(metadata_bytes > 0);
358
+ assert_eq!(adoption, (metadata_bytes, fresh.len() as u64));
359
+ for (object, prior) in [&first, &second].iter().zip(before_payloads) {
360
+ let path = store.blob_path(&object.sha256);
361
+ assert_eq!(std::fs::read(&path).unwrap(), prior.0);
362
+ assert_eq!(
363
+ std::fs::metadata(path).unwrap().modified().unwrap(),
364
+ prior.1
365
+ );
366
+ }
367
+ eprintln!("CR108_IO_PHASE restore");
368
+ let next = begin(
369
+ &store,
370
+ &meta,
371
+ &target,
372
+ 1,
373
+ declaration,
374
+ adopted.head.as_ref(),
375
+ )
376
+ .unwrap();
377
+ assert_eq!(
378
+ completed(&meta, &target, 1).unwrap().0,
379
+ vec![
380
+ ("model".into(), "next".into(), "value".into()),
381
+ ("model".into(), "weight".into(), "value".into())
382
+ ]
383
+ );
384
+ let restore = progress.take();
385
+ assert_eq!(restore, (0, 0));
386
+ eprintln!("CR108_IO_PHASE finish");
387
+ write(&store, &meta, &target, "third", 5);
388
+ commit(&store, &meta, &target, 1).unwrap();
389
+ release_guards(&meta, Some(next.writer_hold), next.source_leases);
390
+ eprintln!("CR108_IO_PHASE done");
391
+ eprintln!("CR108_IO_COUNTS verification={verification:?} adoption={adoption:?} restore={restore:?} adoption_metadata_bytes={metadata_bytes}");
392
+ }
253
393
 
254
394
  fn fixture() -> (Store, Meta, Declaration, String, String) {
255
395
  let path = std::env::temp_dir().join(format!(
@@ -498,22 +498,11 @@ pub(crate) fn plan_managed(
498
498
 
499
499
  // ------------------------------------------------------------------ the act (tfs-066)
500
500
 
501
- /// How a plan's bytes are proven to be gettable again before they are let go.
502
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
503
- pub enum Recovery {
504
- /// The object is already on the bound repo cache at its exact length.
505
- Cache,
506
- /// This run put it there.
507
- Demoted,
508
- /// The caller declared the roots recoverable from the hub. Their word, like free space.
509
- Declared,
510
- }
511
-
512
501
  /// What `run` was asked to do beyond the plan itself.
513
502
  #[derive(Debug, Clone, Copy, Default)]
514
503
  pub struct RunOptions {
515
- /// Publish each object to the bound repo cache before dropping it, so the refetch is a
516
- /// volume read rather than an R2 pull.
504
+ /// Best-effort cache warming before reclaim. This never authorizes deletion;
505
+ /// assume_upstream is still required because the cache is disposable.
517
506
  pub demote: bool,
518
507
  /// The caller declares these roots recoverable from the hub. Same class of caller-input
519
508
  /// as free space (decisions.md 241): TensorFS cannot see the hub, the caller can.
@@ -573,87 +562,23 @@ impl RunReport {
573
562
  }
574
563
  }
575
564
 
576
- /// Is this object gettable again without the pod's own history?
577
- ///
578
- /// decisions.md 842 set the DURABLE-UPSTREAM INVARIANT — *"pressure eviction may target
579
- /// only objects with a recorded durable upstream; local-only artifacts are never
580
- /// pressure-evicted"* — with the falsifier *"a pressure sweep evicting a local-only
581
- /// artifact"*. tfs-020 then deleted the `hub_published` boolean along with the pressure
582
- /// plane it gated, so the invariant has had no mechanism since.
583
- ///
584
- /// It comes back CONSTRUCTIVELY rather than as a flag someone must remember to set: the
585
- /// cache is checked, or this run puts the bytes there, or the caller says so. Demotion is
586
- /// the interesting one, because it does not FIND the invariant true — it MAKES it true, and
587
- /// it makes it true against the `/workspace` volume, which measurably survives the
588
- /// container-disk reset that destroys the Store (th-152, 2026-09-04).
589
- fn recovery(
590
- store: &Store,
591
- row: &crate::storage::GcDelete,
592
- options: RunOptions,
593
- report: &mut RunReport,
594
- ) -> Option<Recovery> {
595
- if options.assume_upstream {
596
- return Some(Recovery::Declared);
597
- }
598
- let cache = store.repo_cache()?;
599
- let kind = match row.kind {
600
- "manifest" => CacheKind::Manifest,
601
- _ => CacheKind::Blob,
602
- };
603
- let want = ObjectRef {
604
- sha256: row.sha256.clone(),
605
- length: row.length,
606
- };
607
- // Present at the exact length is the cheap check and the honest one: the cache's own
608
- // admission path re-verifies the digest on the way back in, so a byte-level check here
609
- // would pay for a promise something else already keeps.
610
- if let Ok(path) = cache.path(kind, &row.sha256) {
611
- if std::fs::metadata(&path).is_ok_and(|meta| meta.is_file() && meta.len() == row.length) {
612
- report.already_cached_objects += 1;
613
- return Some(Recovery::Cache);
614
- }
615
- }
616
- if !options.demote {
617
- return None;
618
- }
619
- match cache.backfill_store(store, kind, &want) {
620
- Ok(CacheWrite::Stored) => {
621
- report.demoted_objects += 1;
622
- report.demoted_bytes = report.demoted_bytes.saturating_add(row.length);
623
- Some(Recovery::Demoted)
624
- }
625
- Ok(CacheWrite::Present) => {
626
- report.already_cached_objects += 1;
627
- Some(Recovery::Cache)
628
- }
629
- // A cache that cannot take the bytes is weather everywhere else in this crate and
630
- // stays weather here — but for an EVICTION the consequence is different. On a fetch
631
- // the authority is still R2; on an eviction this pod may hold the last copy. So a
632
- // failed demote does not become a silent delete, it becomes a refusal.
633
- Ok(CacheWrite::Unavailable) | Err(_) => None,
634
- }
635
- }
636
-
637
- /// Demote, tombstone, collect — in that order, and the order is the argument.
638
- ///
639
- /// 1. **Demote** while the objects are still live and still reachable. Nothing is deleted.
640
- /// 2. **Tombstone** — `DeleteRepository` per victim, through `apply_repository`'s
641
- /// compare-and-set. This is the *explicit act* decisions.md 1135 requires: bytes become
642
- /// collectable because somebody removed the thing that named them, never because a sweep
643
- /// inferred they were garbage.
644
- /// 3. **Collect** — the existing `gc::collect`, unchanged. It recomputes reachability from
645
- /// `repos/`, `manifests/` and `blobs/` alone.
646
- ///
647
- /// **Step 3 is why a wrong plan cannot corrupt the Store.** The plan chooses which ROOTS
648
- /// stop being named; the census decides which OBJECTS that actually frees. A caller that
649
- /// names every root still cannot delete an object a hold protects or a retained manifest
650
- /// needs.
565
+ /// Reclaim named roots only after the caller confirms durable upstream custody.
566
+ /// Optional cache warming precedes repository tombstones and ordinary GC; cache
567
+ /// presence never authorizes either step. Retained roots and active holds still
568
+ /// protect their objects through the existing reachability census.
651
569
  pub fn run(store: &Store, drop: &[RepositoryName], options: RunOptions) -> Result<RunReport> {
652
570
  permitted(store)?;
653
571
  let mut report = RunReport::default();
654
572
  if drop.is_empty() {
655
573
  return Ok(report);
656
574
  }
575
+ if !options.assume_upstream {
576
+ return refuse(
577
+ Code::DURABILITY_UNPROVEN,
578
+ "named local roots require --assume-upstream after confirming durable upstream \
579
+ custody; an optional cache or --demote cannot preserve the only copy",
580
+ );
581
+ }
657
582
 
658
583
  // EXCLUSIVITY IS PROVED BEFORE ANYTHING IS DONE, not discovered at the end.
659
584
  //
@@ -698,26 +623,29 @@ pub fn run(store: &Store, drop: &[RepositoryName], options: RunOptions) -> Resul
698
623
  .filter(|row| !already.contains(&row.key))
699
624
  .collect();
700
625
 
701
- let mut unrecoverable = Vec::new();
702
- for row in &condemned {
703
- if recovery(store, row, options, &mut report).is_none() {
704
- unrecoverable.push(format!("{}:{}", row.kind, row.sha256));
626
+ if options.demote {
627
+ if let Some(cache) = store.repo_cache() {
628
+ for row in &condemned {
629
+ let kind = if row.kind == "manifest" {
630
+ CacheKind::Manifest
631
+ } else {
632
+ CacheKind::Blob
633
+ };
634
+ let object = ObjectRef {
635
+ sha256: row.sha256.clone(),
636
+ length: row.length,
637
+ };
638
+ match cache.backfill_store(store, kind, &object) {
639
+ Ok(CacheWrite::Stored) => {
640
+ report.demoted_objects += 1;
641
+ report.demoted_bytes = report.demoted_bytes.saturating_add(row.length);
642
+ }
643
+ Ok(CacheWrite::Present) => report.already_cached_objects += 1,
644
+ Ok(CacheWrite::Unavailable) | Err(_) => {}
645
+ }
646
+ }
705
647
  }
706
648
  }
707
- if !unrecoverable.is_empty() {
708
- let shown: Vec<&str> = unrecoverable.iter().take(3).map(String::as_str).collect();
709
- return refuse(
710
- Code::DURABILITY_UNPROVEN,
711
- format!(
712
- "{} of {} objects cannot be got back if they go — {} — so nothing was \
713
- dropped; bind a repo cache and pass --demote, or declare the roots \
714
- recoverable with --assume-upstream",
715
- unrecoverable.len(),
716
- condemned.len(),
717
- shown.join(", "),
718
- ),
719
- );
720
- }
721
649
 
722
650
  let fault = crate::store::Fault::default();
723
651
  for repo in drop {