From 35b2a3a15195fddd5dd914a6bc8746cddbce1b37 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 2 Aug 2026 07:17:01 -0400 Subject: [PATCH 1/2] tracking: append OriginKind::PackageImport as value 5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An appended field-vocabulary value, so kLedgerVersion stays 2 — pinned by a test. Unknown kinds still degrade to Unknown with the ledger Loaded. --- src/core/tracking/origin_ledger.cpp | 1 + src/core/tracking/origin_ledger.h | 3 ++ tests/test_origin_ledger.cpp | 84 +++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/core/tracking/origin_ledger.cpp b/src/core/tracking/origin_ledger.cpp index dbcafd4..82568b7 100644 --- a/src/core/tracking/origin_ledger.cpp +++ b/src/core/tracking/origin_ledger.cpp @@ -38,6 +38,7 @@ OriginKind kindFromInt(int v) { case 2: return OriginKind::Ingest; case 3: return OriginKind::Recapture; case 4: return OriginKind::Resample; + case 5: return OriginKind::PackageImport; default: return OriginKind::Unknown; } } diff --git a/src/core/tracking/origin_ledger.h b/src/core/tracking/origin_ledger.h index f04264e..01b99d1 100644 --- a/src/core/tracking/origin_ledger.h +++ b/src/core/tracking/origin_ledger.h @@ -20,6 +20,9 @@ enum class OriginKind { Ingest = 2, Recapture = 3, // regenerated in place from its recorded source recipe Resample = 4, // baked from an instrument's own processing chain + // Kept distinct from Ingest — both bring in a foreign file, but only this one + // can answer "which package did this bank come from" later. + PackageImport = 5, }; // One system-created file's birth record. `relativePath` is the key and is ALWAYS diff --git a/tests/test_origin_ledger.cpp b/tests/test_origin_ledger.cpp index ff2fb8f..f5363fc 100644 --- a/tests/test_origin_ledger.cpp +++ b/tests/test_origin_ledger.cpp @@ -2,9 +2,10 @@ // // The record family behind file tracking. Covers: the relative-paths-only invariant, // exact-string ownership, dedup, insertion order, the JSON round-trip (incl. golden -// byte literals over every persisted enum value), the no-backfill rule, the legacy -// path-only lift, and the Fresh / Loaded / Unreadable / FutureVersion classification -// that keeps never-recorded apart from the two degraded states. +// byte literals over every persisted enum value), the append-a-kind-without-moving-"v" +// rule and its degrade-don't-block twin, the no-backfill rule, the legacy path-only +// lift, and the Fresh / Loaded / Unreadable / FutureVersion classification that keeps +// never-recorded apart from the two degraded states. #include "../src/core/tracking/origin_ledger.h" @@ -162,13 +163,15 @@ static void testSerializeGoldenLiteralPinsEveryPersistedKind() { l.record(rec("bank/ingest.wav", OriginKind::Ingest, "S-b")); l.record(rec("bank/recapture.wav", OriginKind::Recapture, "S-c", "S-a")); l.record(rec("bank/resample.wav", OriginKind::Resample, "S-d", "S-a")); + l.record(rec("bank/import.wav", OriginKind::PackageImport, "S-e")); const std::string expected = "{\"v\":2,\"records\":[" "{\"path\":\"bank/unknown.wav\",\"kind\":0,\"sample\":\"\",\"parent\":\"\"}," "{\"path\":\"bank/capture.wav\",\"kind\":1,\"sample\":\"S-a\",\"parent\":\"\"}," "{\"path\":\"bank/ingest.wav\",\"kind\":2,\"sample\":\"S-b\",\"parent\":\"\"}," "{\"path\":\"bank/recapture.wav\",\"kind\":3,\"sample\":\"S-c\",\"parent\":\"S-a\"}," - "{\"path\":\"bank/resample.wav\",\"kind\":4,\"sample\":\"S-d\",\"parent\":\"S-a\"}" + "{\"path\":\"bank/resample.wav\",\"kind\":4,\"sample\":\"S-d\",\"parent\":\"S-a\"}," + "{\"path\":\"bank/import.wav\",\"kind\":5,\"sample\":\"S-e\",\"parent\":\"\"}" "]}"; CHECK(l.serialize() == expected); @@ -180,6 +183,76 @@ static void testSerializeGoldenLiteralPinsEveryPersistedKind() { CHECK(back->find("bank/ingest.wav")->kind == OriginKind::Ingest); CHECK(back->find("bank/recapture.wav")->kind == OriginKind::Recapture); CHECK(back->find("bank/resample.wav")->kind == OriginKind::Resample); + CHECK(back->find("bank/import.wav")->kind == OriginKind::PackageImport); + CHECK(*back == l); // every field, not just the kind, survives the trip +} + +// Appending a value to the kind vocabulary must NOT move the document version: the +// two rules sit side by side and pull in opposite directions — an unknown kind +// degrades, an unknown "v" blocks. Pinned on the emitted bytes rather than on the +// internal constant, because the byte is what an older build actually reads. +static void testAppendingAKindDoesNotMoveTheDocumentVersion() { + OriginLedger l; + l.record(rec("bank/import.wav", OriginKind::PackageImport, "S-e")); + const std::string json = l.serialize(); + CHECK(json.rfind("{\"v\":2,", 0) == 0); + CHECK(loadLedger(json).status == LedgerStatus::Loaded); + + // The ceiling did not move with it: v3 is still a future document shape. + CHECK(loadLedger("{\"v\":3,\"records\":[]}").status == LedgerStatus::FutureVersion); +} + +// The other half of the append rule: a kind this build does NOT know degrades to +// Unknown while the ledger still loads and the path stays owned. Losing the kind +// detail costs nothing today — no consumer reads it — but an Unreadable here would +// block prune entirely on nothing worse than a vocabulary gap. +static void testUnknownKindDegradesWithoutBlockingTheLedger() { + const std::string blob = + "{\"v\":2,\"records\":[" + "{\"path\":\"bank/next.wav\",\"kind\":6,\"sample\":\"S-1\",\"parent\":\"\"}," + "{\"path\":\"bank/far.wav\",\"kind\":99,\"sample\":\"S-2\",\"parent\":\"\"}," + "{\"path\":\"bank/bogus.wav\",\"kind\":-1,\"sample\":\"S-3\",\"parent\":\"\"}]}"; + + const LedgerLoad load = loadLedger(blob); + CHECK(load.status == LedgerStatus::Loaded); + CHECK(!ledgerDegraded(load.status)); + CHECK(load.ledger.size() == 3); + CHECK(load.ledger.find("bank/next.wav")->kind == OriginKind::Unknown); + CHECK(load.ledger.find("bank/far.wav")->kind == OriginKind::Unknown); + CHECK(load.ledger.find("bank/bogus.wav")->kind == OriginKind::Unknown); + + // Every path still owned, in order — the protection prune reads is untouched. + const std::vector paths = load.ledger.ownedPaths(); + CHECK(paths.size() == 3); + CHECK(paths[0] == "bank/next.wav"); + CHECK(paths[1] == "bank/far.wav"); + CHECK(paths[2] == "bank/bogus.wav"); + + // The rest of the record survives the degrade; only the kind is lost. + CHECK(load.ledger.find("bank/next.wav")->sampleId == "S-1"); +} + +// ownedPaths() is the ONLY thing pruneProtection reads out of a ledger, and it is +// kind-blind: two ledgers agreeing on paths and differing on every kind, new value +// included, yield identical protection input. Adding a kind therefore cannot change +// a prune decision for any existing kind. +static void testOwnedPathsAreKindIndependent() { + const OriginKind kinds[] = {OriginKind::Unknown, OriginKind::Capture, + OriginKind::Ingest, OriginKind::Recapture, + OriginKind::Resample, OriginKind::PackageImport}; + const std::size_t n = sizeof(kinds) / sizeof(kinds[0]); + + OriginLedger baseline; + OriginLedger varied; + for (std::size_t i = 0; i < n; ++i) { + const std::string path = "bank/f" + std::to_string(i) + ".wav"; + baseline.record(rec(path, OriginKind::Unknown, "S")); + varied.record(rec(path, kinds[i], "S")); + } + + CHECK(baseline.ownedPaths() == varied.ownedPaths()); + CHECK(baseline.ownedPaths().size() == n); + CHECK(!(baseline == varied)); // the ledgers really do differ, kind by kind } // contains() is an EXACT-string predicate, never a prefix or substring match — the @@ -383,6 +456,9 @@ int main() { testRoundTripWithLineage(); testRoundTripWithJsonMetacharacters(); testSerializeGoldenLiteralPinsEveryPersistedKind(); + testAppendingAKindDoesNotMoveTheDocumentVersion(); + testUnknownKindDegradesWithoutBlockingTheLedger(); + testOwnedPathsAreKindIndependent(); testMalformedParsesToNullopt(); testTrailingGarbageIsRejected(); testLegacyShapeTypeErrorsAreRejectedButEmptyIsValid(); From a197ff7d68231ebbba6475637a8929b6411bd791 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 2 Aug 2026 07:24:30 -0400 Subject: [PATCH 2/2] docs: fix overclaiming OriginKind comment and trim restated test comments Enum comment claimed package-id lookup that no persisted field supports; reworded to the real distinction. Trimmed CLAUDE.md-duplicated test comments and the header. --- src/core/tracking/origin_ledger.h | 14 ++++++-------- tests/test_origin_ledger.cpp | 21 +++++++-------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/core/tracking/origin_ledger.h b/src/core/tracking/origin_ledger.h index 01b99d1..da8d0e3 100644 --- a/src/core/tracking/origin_ledger.h +++ b/src/core/tracking/origin_ledger.h @@ -15,14 +15,12 @@ namespace reasampler::tracking { // lifted from a legacy path-only manifest, or one whose creator did not know. // PERSISTED AS INTEGERS: never renumber an existing value, only append. enum class OriginKind { - Unknown = 0, - Capture = 1, - Ingest = 2, - Recapture = 3, // regenerated in place from its recorded source recipe - Resample = 4, // baked from an instrument's own processing chain - // Kept distinct from Ingest — both bring in a foreign file, but only this one - // can answer "which package did this bank come from" later. - PackageImport = 5, + Unknown = 0, + Capture = 1, + Ingest = 2, + Recapture = 3, // regenerated in place from its recorded source recipe + Resample = 4, // baked from an instrument's own processing chain + PackageImport = 5, // package-sourced vs Ingest's user-picked; unrecoverable once merged }; // One system-created file's birth record. `relativePath` is the key and is ALWAYS diff --git a/tests/test_origin_ledger.cpp b/tests/test_origin_ledger.cpp index f5363fc..aa1eecd 100644 --- a/tests/test_origin_ledger.cpp +++ b/tests/test_origin_ledger.cpp @@ -1,11 +1,9 @@ // Standalone tests for reasampler::tracking::OriginLedger — no REAPER, no framework. // -// The record family behind file tracking. Covers: the relative-paths-only invariant, -// exact-string ownership, dedup, insertion order, the JSON round-trip (incl. golden -// byte literals over every persisted enum value), the append-a-kind-without-moving-"v" -// rule and its degrade-don't-block twin, the no-backfill rule, the legacy path-only -// lift, and the Fresh / Loaded / Unreadable / FutureVersion classification that keeps -// never-recorded apart from the two degraded states. +// Covers: relative-paths-only, exact-string ownership, dedup, insertion order, the +// JSON round-trip (incl. golden bytes over every persisted enum value), the append-a- +// kind rules, the no-backfill rule, the legacy path-only lift, and the Fresh / Loaded +// / Unreadable / FutureVersion classification. #include "../src/core/tracking/origin_ledger.h" @@ -187,10 +185,7 @@ static void testSerializeGoldenLiteralPinsEveryPersistedKind() { CHECK(*back == l); // every field, not just the kind, survives the trip } -// Appending a value to the kind vocabulary must NOT move the document version: the -// two rules sit side by side and pull in opposite directions — an unknown kind -// degrades, an unknown "v" blocks. Pinned on the emitted bytes rather than on the -// internal constant, because the byte is what an older build actually reads. +// Pins the emitted "v" byte, not the internal constant — an older build reads bytes. static void testAppendingAKindDoesNotMoveTheDocumentVersion() { OriginLedger l; l.record(rec("bank/import.wav", OriginKind::PackageImport, "S-e")); @@ -202,10 +197,8 @@ static void testAppendingAKindDoesNotMoveTheDocumentVersion() { CHECK(loadLedger("{\"v\":3,\"records\":[]}").status == LedgerStatus::FutureVersion); } -// The other half of the append rule: a kind this build does NOT know degrades to -// Unknown while the ledger still loads and the path stays owned. Losing the kind -// detail costs nothing today — no consumer reads it — but an Unreadable here would -// block prune entirely on nothing worse than a vocabulary gap. +// Pins three unrecognized kind values (future, far-future, negative) all landing on +// Unknown with the ledger still Loaded and the path still owned. static void testUnknownKindDegradesWithoutBlockingTheLedger() { const std::string blob = "{\"v\":2,\"records\":["