feat(actions): item-level Design/Arrange move + untag actions; W3-A polish

Add three MIDI-bindable item actions mirroring the track tag family, routed
through the shared hookcommand: retag selected items' membership then re-drive
mint/apply so each lands on its mode's managed lane (manual-lane items exempt),
all in one undo block. Extract shared item_read seam, simplify applyMintPlan's
lane-count pass, and guard reconcile against unregistered-mode lanes.
This commit is contained in:
2026-07-25 13:30:07 -04:00
parent 22012861c5
commit 261efe5452
9 changed files with 372 additions and 54 deletions
+44
View File
@@ -519,6 +519,50 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
const std::vector<NewItem>& newItems,
const std::string& activeMode);
// -- Item-level mode-move decision (Phase D2 / Wave 3-B) ---------------------
//
// The bindable item actions (Move selected items -> Design / -> Arrange / Untag)
// retag the CURRENT item selection's membership, then re-drive the minting/apply
// path so each moved item lands on its target mode's managed lane. The DECISION —
// which selected items to retag, and to what — is pure and unit-tested here; the
// shell only reads the item selection (GUID + manual-lane disposition) and applies
// the resulting membership writes + re-lane pass.
//
// MANAGED-LANES-ONLY INVARIANT (upheld at the source, exactly as auto-tag does): an
// item the shell reports as already on a MANUAL lane is EXEMPT — it is never retagged,
// never untagged, never re-laned. The tool drives only what it minted, even under an
// explicit user action. The shell reports `onManualLane` per item and this decision
// emits NO op for such items; the shell then skips them entirely.
// One selected item the shell reports for the retag decision: its GUID and whether it
// currently sits on a MANUAL lane (⇒ EXEMPT: no membership change, no re-lane).
struct RetagItem {
std::string guid;
bool onManualLane = false; // true ⇒ EXEMPT from the item mode-move actions
};
// One membership op the item mode-move decision produced for one selected item. `untag`
// true ⇒ remove the item from the index (return it to the Arrange default); otherwise
// tag it into `modeId`. The shell applies each verbatim to the MembershipIndex.
struct ItemRetagOp {
std::string guid;
bool untag = false; // true ⇒ untag; false ⇒ tag into modeId
std::string modeId; // the target mode when !untag (empty when untag)
bool operator==(const ItemRetagOp& o) const {
return guid == o.guid && untag == o.untag && modeId == o.modeId;
}
};
// The pure item mode-move decision: given the selected items and a target mode, produce
// the membership ops. An EMPTY `targetMode` means UNTAG (the "Untag selected items" and
// "Move -> Arrange" actions collapse to the same act — Arrange is the absence of a tag,
// mirroring the track-level doUntag). A non-empty `targetMode` tags each eligible item
// into it. Manual-lane items are skipped (no op emitted); items with an empty GUID are
// skipped (defensive). The function mutates nothing — it returns a plan the shell applies.
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
const std::string& targetMode);
// -- Lane minting decision (Phase D2 / Wave 3) -------------------------------
//
// D1 parks a whole track when it holds content of only ONE mode. The moment a track