feat(view): mint managed lanes to separate cross-mode content per stance (D2 W3-A)

Pure planLaneMinting decides which tracks hold >1 mode's content and which managed
lane each item lands on; view.cpp applies it (I_FREEMODE/I_NUMFIXEDLANES/P_LANENAME/
I_FIXEDLANE) under one undo block, driven off the auto-tag detection tick. Manual lanes
and their items are never touched. Load-time reconcile rebuilds ownership from durable
lane names before reapplying active-mode visibility.
This commit is contained in:
2026-07-23 20:08:38 -04:00
parent fed70c0a80
commit b182146f9a
11 changed files with 673 additions and 4 deletions
+20
View File
@@ -82,11 +82,31 @@ static void testRoundTrip() {
}
}
static void testModeIdFromLaneName() {
// The exact inverse of laneNameForMode: recover the owning mode from a managed name.
// Used by the Wave-3 load-time reconcile to rebuild ownership from durable names.
for (const std::string mode : {std::string("arrange"), std::string("design"),
std::string("mixdown"), std::string("mode:with:colons")}) {
auto recovered = modeIdFromLaneName(laneNameForMode(mode));
CHECK(recovered.has_value() && *recovered == mode); // modeIdFromLaneName∘laneNameForMode == id
}
// Manual / unnamed lanes carry no mode (⇒ left off the ownership index on reconcile).
CHECK(!modeIdFromLaneName("").has_value());
CHECK(!modeIdFromLaneName("Comp 1").has_value());
CHECK(!modeIdFromLaneName("Reasampler:design").has_value()); // wrong case ⇒ manual
// Prefix-only with no mode suffix is illegal for a managed lane ⇒ no mode recovered
// (defensive: reconcile skips it rather than recording an empty-mode ownership).
CHECK(!modeIdFromLaneName("reasampler:").has_value());
}
int main() {
testIsManagedLaneName();
testManagedLaneKey();
testIsOnManualLane();
testRoundTrip();
testModeIdFromLaneName();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
+152
View File
@@ -16,6 +16,7 @@
// guards the in-DAW "all leaves hidden after toggling twice" regression.
#include "../src/view_mode_model.h"
#include "../src/lane_keys.h" // laneNameForMode — assert the minting plan's durable keys
#include <algorithm>
#include <cstdio>
@@ -1073,6 +1074,153 @@ static void testAutoTagDecision() {
}
}
// -- D2.7 Lane minting decision (Wave 3) -------------------------------------
//
// planLaneMinting: a track with content of only ONE mode is NOT split (D1 unchanged);
// a track that holds >1 mode's content mints one managed lane per mode and assigns EVERY
// managed-eligible item (incl. pre-existing) to its mode's lane; manual-lane items are
// exempt (never counted, never reassigned, their lane never minted-over).
static bool hasMint(const LaneMintPlan& p, const std::string& track,
const std::string& mode) {
for (const auto& m : p.mints)
if (m.trackGuid == track && m.modeId == mode &&
m.laneKey == laneNameForMode(mode))
return true;
return false;
}
static bool hasAssign(const LaneMintPlan& p, const std::string& item,
const std::string& track, const std::string& mode) {
for (const auto& a : p.assigns)
if (a.itemGuid == item && a.trackGuid == track &&
a.laneKey == laneNameForMode(mode))
return true;
return false;
}
static int splitLaneCount(const LaneMintPlan& p, const std::string& track) {
for (const auto& s : p.splits)
if (s.trackGuid == track) return s.laneCount;
return -1; // no split for this track
}
static void testLaneMintingSingleModeNoSplit() {
// A track whose items all belong to ONE mode is NOT lane-split — D1 whole-track
// parking still separates the stances. No split, no mint, no assignment.
std::vector<LaneTrack> tracks{
LaneTrack{"{T}", {
LaneItem{"{i1}", kArrangeModeId, false},
LaneItem{"{i2}", kArrangeModeId, false},
}},
};
const LaneMintPlan plan = planLaneMinting(tracks);
CHECK(plan.empty());
CHECK(splitLaneCount(plan, "{T}") == -1);
// An empty track (no items) is likewise never split.
CHECK(planLaneMinting({LaneTrack{"{E}", {}}}).empty());
}
static void testLaneMintingMultiModeMintsAndAssignsAll() {
// A track that gained a second mode's item: it now holds Arrange + Design content.
// Both modes get a managed lane; ALL managed-eligible items are assigned — including
// the pre-existing Arrange item (retroactive lane assignment), not only the new one.
std::vector<LaneTrack> tracks{
LaneTrack{"{T}", {
LaneItem{"{arr1}", kArrangeModeId, false}, // pre-existing single-mode item
LaneItem{"{arr2}", kArrangeModeId, false}, // pre-existing single-mode item
LaneItem{"{des1}", kDesignModeId, false}, // the newly-added 2nd-mode item
}},
};
const LaneMintPlan plan = planLaneMinting(tracks);
CHECK(!plan.empty());
// One split with two managed lanes (one per involved mode).
CHECK(splitLaneCount(plan, "{T}") == 2);
CHECK(plan.mints.size() == 2);
CHECK(hasMint(plan, "{T}", kArrangeModeId));
CHECK(hasMint(plan, "{T}", kDesignModeId));
// EVERY managed-eligible item assigned to its mode's lane — pre-existing included.
CHECK(plan.assigns.size() == 3);
CHECK(hasAssign(plan, "{arr1}", "{T}", kArrangeModeId)); // retroactive
CHECK(hasAssign(plan, "{arr2}", "{T}", kArrangeModeId)); // retroactive
CHECK(hasAssign(plan, "{des1}", "{T}", kDesignModeId)); // the new item
}
static void testLaneMintingManualLaneExempt() {
// A track with Arrange + Design managed-eligible content AND an item the user placed
// on a manual lane: the manual item is EXEMPT — it is not counted, not assigned, and
// its lane is never minted-over. The managed split proceeds around it.
std::vector<LaneTrack> tracks{
LaneTrack{"{T}", {
LaneItem{"{arr}", kArrangeModeId, false},
LaneItem{"{des}", kDesignModeId, false},
LaneItem{"{comp}", kDesignModeId, /*onManualLane=*/true}, // user's comp take
}},
};
const LaneMintPlan plan = planLaneMinting(tracks);
// Split for the two managed modes; the manual item never appears in assigns.
CHECK(splitLaneCount(plan, "{T}") == 2);
CHECK(plan.assigns.size() == 2);
CHECK(hasAssign(plan, "{arr}", "{T}", kArrangeModeId));
CHECK(hasAssign(plan, "{des}", "{T}", kDesignModeId));
for (const auto& a : plan.assigns)
CHECK(a.itemGuid != "{comp}"); // manual-lane item NEVER reassigned
// Manual-lane exemption can also SUPPRESS a split: if the ONLY second mode is
// supplied by a manual-lane item, the managed-eligible items are single-mode ⇒ NO
// split (the user's manual lane is not a mode the tool separates).
std::vector<LaneTrack> t2{
LaneTrack{"{U}", {
LaneItem{"{a}", kArrangeModeId, false},
LaneItem{"{d}", kDesignModeId, /*onManualLane=*/true}, // only 2nd mode, exempt
}},
};
CHECK(planLaneMinting(t2).empty()); // managed-eligible content is single-mode ⇒ no split
}
static void testLaneMintingThreeModesAndOwnershipKeys() {
// N-mode proof + the ownership writes the shell will apply: three modes on one track
// mint three managed lanes, each keyed by its durable name (== laneNameForMode), each
// owning the right mode. Applying the mints to a real ownership index reproduces the
// managed classification the toggle planner then gates on.
ViewModeModel vm;
CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
std::vector<LaneTrack> tracks{
LaneTrack{"{T}", {
LaneItem{"{a}", kArrangeModeId, false},
LaneItem{"{d}", kDesignModeId, false},
LaneItem{"{m}", "mixdown", false},
}},
};
const LaneMintPlan plan = planLaneMinting(tracks);
CHECK(splitLaneCount(plan, "{T}") == 3);
CHECK(plan.mints.size() == 3);
// Apply the mints exactly as the shell does — record managed ownership — then assert
// the ownership index classifies each lane managed-for-its-mode and the toggle
// planner would drive exactly these three lanes (managed-only invariant intact).
for (const auto& m : plan.mints)
CHECK(vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId));
CHECK(vm.lanes().size() == 3);
CHECK(vm.lanes().isManaged("{T}", laneNameForMode(kArrangeModeId)));
CHECK(vm.lanes().isManaged("{T}", laneNameForMode(kDesignModeId)));
CHECK(vm.lanes().isManaged("{T}", laneNameForMode("mixdown")));
CHECK(vm.lanesTouchedByToggle().size() == 3);
// Persist round-trip of the just-minted lane-split project: the ownership index (and
// the whole model) survives serialize/deserialize unchanged, so a saved lane-split
// project restores its managed classification without re-minting.
auto back = ViewModeModel::deserialize(vm.serialize());
CHECK(back.has_value());
CHECK(back && *back == vm);
if (back) CHECK(back->lanes().size() == 3);
}
// -- D2.6 JSON round-trip with lane index + membership -----------------------
static void testLaneJsonRoundTrip() {
@@ -1161,6 +1309,10 @@ int main() {
testLaneOwnershipLastWriterWins();
testManagedOnlyPlannerAndQuery();
testAutoTagDecision();
testLaneMintingSingleModeNoSplit();
testLaneMintingMultiModeMintsAndAssignsAll();
testLaneMintingManualLaneExempt();
testLaneMintingThreeModesAndOwnershipKeys();
testLaneJsonRoundTrip();
testLaneMalformedJson();