From 08dc9cf0ae13156b0e373d8721b5a5a562dbbc36 Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 11 Nov 2026 17:26:50 +0000 Subject: [PATCH 1/2] Walk rows lows: Rust @directory pins the I/O error kind and runs delete; drop closed known gaps Co-Authored-By: Claude Sonnet 5.5 --- docs/document-variants-cut.md | 4 ++-- packages/cultcache-rs/src/lib.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/document-variants-cut.md b/docs/document-variants-cut.md index 606e46b0..e9dc51ed 100644 --- a/docs/document-variants-cut.md +++ b/docs/document-variants-cut.md @@ -1609,7 +1609,7 @@ The one door per runtime, and who walks through it: - **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore needs a cultcache-ts release that exports it, which is the release that ships with this change; the manifests move to that version then (c0-ships-with-c2a), not before. - **Cut line.** `merge-r1` h3 (Rust) and h4 (TS, cultnet-ts); spec `cut-merge-r1.r3` authority map. - **Pinned by.** `tests/vectors/document-variants-c2a/readability/manifest.txt`, walked by `StoreReadabilityTests.OpenReadsExactlyTheFilesTheVectorsSayItReads` (C#), `CultMeshSingleFileReadabilityTests.AWriteReplacesAFileExactlyWhenItReads` (Mesh), `a_file_is_replaced_exactly_when_it_reads` (RS:5132), "SingleFileMessagePackBackingStore replaces a file exactly when it reads" (TS test :2321) and `test_single_file_replaces_a_file_exactly_when_it_reads` (Python). AUTH cases: `packages/cultnet-ts/test/idunn-runtime-authority.test.ts`. Declared divergences in the manifest: `legacy-envelopes.bin` (C# refuses; the others read and replace it) and `variant-slot-v3.bin` (C# reads; the others refuse). -- **Known gaps.** `variants:follow_up:c0-bad-canonical-in-manifest` (`current-catalog-bad-canonical.msgpack` is not in the manifest; TS reads it, Rust refuses). `variants:follow_up:legacy-vectors-all-runtimes` (the three `legacy-catalog-*` c0 vectors have a verdict only in C# tests; all four runtimes refuse them, probed). `variants:follow_up:range-check-in-ci` (`npm run test:pack:ts` builds every admitted tag and checks exports, but `.github/workflows/publish-packages.yml` does not run it). +- **Known gaps.** `variants:follow_up:range-check-in-ci` (`npm run test:pack:ts` builds every admitted tag and checks exports, but `.github/workflows/publish-packages.yml` does not run it). ### B. Existence and links (what "nothing at the path" means) @@ -1640,7 +1640,7 @@ The one door per runtime, and who walks through it: - **Shared paths.** Open, commit, Mesh read and write. - **Cut line.** `mesh-single-file-reader` h1 (the C# clause). Rust, TS and Python were correct by construction of their reads. - **Pinned by.** C#: `ADirectoryAtTheStorePathIsAnIoErrorOnOpenAndOnCommitNotAnEmptyStore` (`StoreReadabilityTests.cs:236`). Mesh: `ADirectoryAtThePathIsAnIoErrorOnReadAndWriteAndIsLeftAlone` (:149). Rust: `a_write_over_a_directory_stops_at_the_read_and_leaves_no_staging_file` RS:5726. -- **Known gaps.** TS and Python have no test for a directory at the store path. No follow-up is registered; the natural home is `write-set-vectors` with the other readability pins. +- **Known gaps.** None open. The `@directory` manifest row is walked by all four runtimes: every operation including delete is refused and leaves the directory untouched; Rust pins an I/O failure rather than a typed unreadable store. ### E. Lock diff --git a/packages/cultcache-rs/src/lib.rs b/packages/cultcache-rs/src/lib.rs index 0c872d91..f7b8cae0 100644 --- a/packages/cultcache-rs/src/lib.rs +++ b/packages/cultcache-rs/src/lib.rs @@ -6148,7 +6148,12 @@ mod tests { let bytes = if directory { Vec::new() } else { std::fs::read(root.join(vector))? }; // A v3 store keeps its marker through a rewrite; every other file that reads is written v1. let header = if vector.contains("v3") { "cultcache.store.v3" } else { STORE_FORMAT_V1 }; - for operation in ["open", "soft_push_all", "push_all", "push"] { + let operations: &[&str] = if directory { + &["open", "soft_push_all", "push_all", "push", "delete"] + } else { + &["open", "soft_push_all", "push_all", "push"] + }; + for &operation in operations { let temp = tempfile::tempdir()?; let path = temp.path().join("store.msgpack"); if directory { @@ -6161,11 +6166,15 @@ mod tests { "open" => store.pull_all().map(|_| ()), "soft_push_all" => store.push_all(&[snapshot_envelope("x", b"one")], PushAllOptions { soft: true }), "push_all" => store.push_all(&[snapshot_envelope("x", b"one")], PushAllOptions::default()), + "delete" => store.delete(&snapshot_envelope("x", b"one")), _ => store.push(&snapshot_envelope("x", b"one")), }; let what = format!("{vector} {operation}"); assert_eq!(outcome.is_ok(), reads, "{what}: {outcome:?}"); if directory { + // A directory is an I/O failure, never a typed unreadable store. + let error = outcome.unwrap_err(); + assert!(io_error_in(&error).is_some() && unreadable_kind(&error).is_none(), "{what}: {error:#}"); assert!(path.is_dir() && std::fs::read_dir(&path)?.next().is_none(), "{what} touched the directory"); continue; } -- 2.47.3 From 01bf40e942c0cdb62ee0ef12fb45cf98fd538e24 Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 11 Nov 2026 17:41:20 +0000 Subject: [PATCH 2/2] Walk rows lows: TS @directory pins an I/O failure; C# pins commit-remove over a directory Co-Authored-By: Claude Sonnet 5.5 --- packages/cultcache-ts/test/cult-cache.test.ts | 2 +- tests/GameCult.Caching.Tests/StoreReadabilityTests.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cultcache-ts/test/cult-cache.test.ts b/packages/cultcache-ts/test/cult-cache.test.ts index 2f40358f..76f3d084 100644 --- a/packages/cultcache-ts/test/cult-cache.test.ts +++ b/packages/cultcache-ts/test/cult-cache.test.ts @@ -2635,7 +2635,7 @@ test("SingleFileMessagePackBackingStore replaces a file exactly when it reads", assert.ok(!keys.includes(envelopes[0]!.key), `${vector} ${operation}`); } } else if (isDirectory) { - await assert.rejects(run, `${vector} ${operation}`); + await assert.rejects(run, (error) => !(error instanceof StoreUnreadableError), `${vector} ${operation}: an I/O failure, not a typed refusal`); assert.ok((await lstat(file)).isDirectory(), `${vector} ${operation} replaced the directory`); assert.deepEqual(await readdir(file), [], `${vector} ${operation} wrote into the directory`); } else { diff --git a/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs b/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs index 55406948..f923dbfb 100644 --- a/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs +++ b/tests/GameCult.Caching.Tests/StoreReadabilityTests.cs @@ -314,6 +314,9 @@ namespace GameCult.Caching.Tests Assert.That(() => cache.Commit(batch => batch.Upsert(typeof(IdDeck), new IdDeck { Name = "e" }, new CultRecordKey("e"))), Throws.TypeOf().With.Message.Contains("directory")); + Assert.That(() => cache.Commit(batch => batch.Remove(new CultRecordKey("e"))), + Throws.TypeOf().With.Message.Contains("directory")); + Assert.That(Directory.GetFileSystemEntries(path), Is.Empty); Assert.That(Directory.GetFileSystemEntries(later), Is.Empty); } -- 2.47.3