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
+52
View File
@@ -9,6 +9,8 @@
#include <set>
#include <utility>
#include "lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention
// view_mode_model implementation.
//
// JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief:
@@ -156,6 +158,56 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
return tags;
}
// ---------------------------------------------------------------------------
// lane minting decision
// ---------------------------------------------------------------------------
LaneMintPlan planLaneMinting(const std::vector<LaneTrack>& tracks) {
LaneMintPlan plan;
for (const LaneTrack& track : tracks) {
if (track.trackGuid.empty()) continue;
// Collect the DISTINCT modes the track's managed-eligible items belong to, in
// deterministic (sorted) order so the mint list and lane count are stable across
// runs (a set orders by mode id). Items on a manual lane are EXEMPT — never
// counted toward the multi-mode test and never reassigned (the managed-only
// invariant, upheld at the source of the decision).
std::set<std::string> involvedModes;
for (const LaneItem& item : track.items) {
if (item.guid.empty() || item.modeId.empty()) continue;
if (item.onManualLane) continue; // exempt — user's hand-managed lane
involvedModes.insert(item.modeId);
}
// Single-mode (or empty) track: whole-track parking (D1) still separates the
// stances. NO split, NO mint, NO assignment — this is the load-bearing
// "don't lane-split single-mode tracks" rule.
if (involvedModes.size() < 2) continue;
// Multi-mode track: transition to lane-split. One managed lane per involved
// mode (durable key = laneNameForMode(mode)), owned by that mode.
plan.splits.push_back(LaneMintPlan::TrackSplit{
track.trackGuid, static_cast<int>(involvedModes.size())});
for (const std::string& mode : involvedModes) {
plan.mints.push_back(
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
}
// Assign EVERY managed-eligible item onto its mode's lane — including the
// pre-existing single-mode items, so a track that just gained a second mode
// retroactively lanes all of its content, not only the newly-added item.
for (const LaneItem& item : track.items) {
if (item.guid.empty() || item.modeId.empty()) continue;
if (item.onManualLane) continue; // exempt — never reassigned
plan.assigns.push_back(LaneAssign{
item.guid, track.trackGuid, laneNameForMode(item.modeId)});
}
}
return plan;
}
// ---------------------------------------------------------------------------
// planner helpers
// ---------------------------------------------------------------------------