M10: provenance populate + re-capture from source (bank-only)

Pure provenance core (recipe fingerprint, FX-chain identity, parent
detection) + shell reads; capture stamps provenance on resample-from-sample;
re-capture regenerates a provenanced sample from its source, never touching
the timeline. Adds BankIndex/BankBook in-place update. CTest-covered.
This commit is contained in:
2026-07-26 17:33:22 -04:00
parent 399dafa859
commit 20018c1df2
13 changed files with 1368 additions and 11 deletions
+41
View File
@@ -164,6 +164,46 @@ static void testRelativePathInvariant() {
CHECK(idx.add(noId) == AddResult::RejectedEmptyId);
}
// M10: updateInPlace refreshes an entry while preserving its slot + identity, and
// bypasses dedup (an in-place refresh is not a new insert). The relative-paths-only
// invariant still guards the replacement.
static void testUpdateInPlace() {
BankIndex idx;
CHECK(idx.add(minimalSample("a")) == AddResult::Added); // id "min-a"
CHECK(idx.add(minimalSample("b")) == AddResult::Added); // id "min-b"
CHECK(idx.add(minimalSample("c")) == AddResult::Added); // id "min-c"
// Refresh the MIDDLE entry: new file/hash/name, same id — position must be kept.
Sample updated = minimalSample("b");
updated.relativePath = "reasampler_bank/regenerated.wav";
updated.contentHash = "new-hash-b";
updated.displayName = "regenerated";
CHECK(idx.updateInPlace("min-b", updated));
CHECK(idx.size() == 3); // no new entry, no removal
CHECK(idx.all()[1].id == "min-b"); // slot preserved (still middle)
CHECK(idx.all()[1].relativePath == "reasampler_bank/regenerated.wav");
CHECK(idx.all()[1].contentHash == "new-hash-b");
CHECK(idx.query("min-b")->displayName == "regenerated");
// An updated hash colliding with ANOTHER entry does NOT collapse (updateInPlace
// is not an insert): the refreshed entry keeps its slot even sharing a hash.
Sample collide = minimalSample("b");
collide.contentHash = idx.query("min-a")->contentHash; // same as entry "min-a"
CHECK(idx.updateInPlace("min-b", collide));
CHECK(idx.size() == 3); // still three; no collapse
// Updating an absent id fails without mutation.
CHECK(!idx.updateInPlace("nope", minimalSample("x")));
CHECK(idx.size() == 3);
// An absolute replacement path is rejected (invariant preserved).
Sample bad = minimalSample("b");
bad.relativePath = "C:/evil.wav";
CHECK(!idx.updateInPlace("min-b", bad));
CHECK(idx.query("min-b")->relativePath != "C:/evil.wav");
}
static void testEmptyIndexRoundTrip() {
BankIndex idx;
CHECK(idx.empty());
@@ -375,6 +415,7 @@ int main() {
testDedupByHash();
testTierFilterAndMove();
testRelativePathInvariant();
testUpdateInPlace();
testEmptyIndexRoundTrip();
testMalformedJson();
testRemoveAndQuery();