Cut core/view and shell/view comment bloat ~65% (comments only, zero code change)
This commit is contained in:
@@ -6,35 +6,16 @@
|
||||
#include <utility>
|
||||
|
||||
#include "core/json/json.h"
|
||||
#include "core/view/lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention
|
||||
|
||||
// view_mode_model implementation.
|
||||
//
|
||||
// JSON rides on the shared core/json lexical layer (Q-W1), mirroring bank_model.
|
||||
// A compact writer
|
||||
// plus a recursive-descent parser covers the field set: the mode registry, the
|
||||
// GUID-keyed membership map, per-track snapshots (with a variable-length per-FX
|
||||
// offline vector), and the active mode. Ints are emitted plainly; strings are
|
||||
// escaped identically to bank_model so control chars and unicode survive.
|
||||
#include "core/view/lane_keys.h" // laneNameForMode
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Q-W1 interim: laneNameForMode lives in reasampler::view now; this god module
|
||||
// re-namespaces in its own split wave.
|
||||
using view::laneNameForMode;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// equality
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool Mode::operator==(const Mode& o) const {
|
||||
return id == o.id && displayName == o.displayName && ordinal == o.ordinal;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ModeRegistry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ModeRegistry::ModeRegistry() {
|
||||
modes_.push_back(Mode{kArrangeModeId, "Arrange", 0});
|
||||
modes_.push_back(Mode{kDesignModeId, "Design", 1});
|
||||
@@ -44,8 +25,6 @@ bool ModeRegistry::add(const Mode& mode) {
|
||||
if (mode.id.empty()) return false;
|
||||
if (query(mode.id) != nullptr) return false; // ids are unique
|
||||
modes_.push_back(mode);
|
||||
// Keep ordinal order stable; std::stable_sort so equal ordinals keep insertion
|
||||
// order (the tie-break documented in the header).
|
||||
std::stable_sort(modes_.begin(), modes_.end(),
|
||||
[](const Mode& a, const Mode& b) { return a.ordinal < b.ordinal; });
|
||||
return true;
|
||||
@@ -57,10 +36,6 @@ const Mode* ModeRegistry::query(const std::string& id) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MembershipIndex
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool MembershipIndex::tag(const std::string& guid, const std::string& modeId) {
|
||||
if (guid.empty() || modeId.empty()) return false;
|
||||
Membership& m = entries_[guid];
|
||||
@@ -95,10 +70,6 @@ std::set<std::string> MembershipIndex::modesOf(const std::string& guid) const {
|
||||
return m ? m->modeIds : std::set<std::string>{};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LaneOwnershipIndex
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool LaneOwnershipIndex::setManaged(const std::string& trackGuid, const std::string& laneKey,
|
||||
const std::string& modeId) {
|
||||
if (trackGuid.empty() || laneKey.empty() || modeId.empty()) return false;
|
||||
@@ -123,24 +94,12 @@ const LaneOwnership* LaneOwnershipIndex::query(const std::string& trackGuid,
|
||||
}
|
||||
|
||||
int laneModeState(const std::string& managedMode, const std::string& activeMode) {
|
||||
// The active mode's lane plays exclusively; every other managed lane is silenced
|
||||
// and hidden (C_LANEPLAYS = 0). Exclusive membership: only one stance's lane at a
|
||||
// time. Show-both, which keeps a lane audible across modes, is a per-lane opt-out
|
||||
// the shell layers on; the default per-mode decision here is exclusive.
|
||||
//
|
||||
// EXCLUSIVITY ASSUMPTION (one managed lane per mode per track): the model assumes a
|
||||
// given (track, mode) owns AT MOST ONE managed lane. C_LANEPLAYS=1 means "this lane
|
||||
// plays EXCLUSIVELY" — two lanes on the same track both claiming mode M would both
|
||||
// be told to play exclusively on M's toggle, which REAPER cannot honor coherently
|
||||
// (the last write wins in the DAW). The Wave-3 lane-minting path is responsible for
|
||||
// upholding one-lane-per-(track,mode); planToggle asserts it in debug builds.
|
||||
// Assumes at most one managed lane per (track, mode) — planToggle asserts
|
||||
// this in debug builds; two lanes claiming the same mode would both be
|
||||
// told to play exclusively, which REAPER can't honor coherently.
|
||||
return managedMode == activeMode ? kLanePlaysExclusive : kLaneSilent;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// auto-tag decision
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
|
||||
const std::vector<NewItem>& newItems,
|
||||
const std::string& activeMode) {
|
||||
@@ -153,14 +112,9 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
}
|
||||
for (const auto& item : newItems) {
|
||||
if (item.guid.empty()) continue;
|
||||
if (item.onManualLane) continue; // manual-lane content is off-limits to auto-tag
|
||||
if (item.onManualLane) continue;
|
||||
|
||||
// ADOPTION (strand guard): a new item on a track whose PRE-EXISTING content
|
||||
// resolves to exactly one mode adopts THAT mode, so a drop onto a track already
|
||||
// showing content never pushes it multi-mode and never triggers a lane split that
|
||||
// would silence the pre-existing, previously-visible items. A track with no prior
|
||||
// content (empty trackModes) or one already carrying a deliberate multi-mode split
|
||||
// (>1) falls back to the active-mode rule.
|
||||
// Adopt the track's single pre-existing mode (strand guard — see header).
|
||||
const std::string& target =
|
||||
item.trackModes.size() == 1 ? *item.trackModes.begin() : activeMode;
|
||||
tags.push_back(AutoTag{item.guid, target});
|
||||
@@ -173,27 +127,19 @@ std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
||||
std::vector<ItemRetagOp> ops;
|
||||
const bool untag = targetMode.empty(); // empty target ⇒ untag (→ Arrange default)
|
||||
for (const RetagItem& item : selected) {
|
||||
if (item.guid.empty()) continue; // defensive; a real item always has a GUID
|
||||
if (item.onManualLane) continue; // manual-lane item is EXEMPT — never retagged
|
||||
if (item.guid.empty()) continue;
|
||||
if (item.onManualLane) continue;
|
||||
ops.push_back(ItemRetagOp{item.guid, untag, untag ? std::string{} : targetMode});
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// lane minting decision
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||
const std::vector<LaneTrack>& tracks) {
|
||||
LaneMintPlan plan;
|
||||
|
||||
// Precompute, per track GUID, the count of modes it is VISIBLE in and the set of
|
||||
// those mode ids — tree-aware, so a content-bearing folder's DERIVED visibility
|
||||
// (visibleTracks marks a parent visible in every mode a descendant is visible in)
|
||||
// is captured, not only the track's own item mode-span. This is the visibility
|
||||
// trigger source (b): a folder derived-visible in >= 2 modes must lane-separate its
|
||||
// own media even when that media is single-mode. Computed once for all tracks.
|
||||
// Per track GUID, the modes it's visible in (tree-aware) — captures the
|
||||
// folder-derived-visibility split trigger, not just own-item mode span.
|
||||
std::map<std::string, std::set<std::string>> visibleModesOf;
|
||||
for (const Mode& mode : model.modes().all()) {
|
||||
const std::set<std::string> vis = model.visibleTracks(tree, mode.id);
|
||||
@@ -204,58 +150,26 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||
for (const LaneTrack& track : tracks) {
|
||||
if (track.trackGuid.empty()) continue;
|
||||
|
||||
// SHOW-BOTH escape hatch: never force-split. A show-both track is visible in
|
||||
// every mode ON PURPOSE and its content is meant to play across all of them, so
|
||||
// neither the visibility trigger nor the own-item-span trigger confines it. Skip
|
||||
// it entirely (no split/mint/assign) so its items stay cross-mode-visible.
|
||||
if (model.membership().isShowBoth(track.trackGuid)) continue;
|
||||
if (model.membership().isShowBoth(track.trackGuid)) continue; // never force-split
|
||||
|
||||
// Collect the DISTINCT modes the track's managed-eligible OWN 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> ownItemModes;
|
||||
for (const LaneItem& item : track.items) {
|
||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||
if (item.onManualLane) continue; // exempt — user's hand-managed lane
|
||||
if (item.onManualLane) continue; // exempt
|
||||
ownItemModes.insert(item.modeId);
|
||||
}
|
||||
|
||||
// A track with NO managed-eligible own media never splits: there is nothing to
|
||||
// confine (lane separation projects OWN items across modes). A folder derived-
|
||||
// visible in many modes but carrying no own content stays whole-track visibility-
|
||||
// only (D1 parent handling) — this guards the "carries its own media" clause.
|
||||
if (ownItemModes.empty()) continue;
|
||||
if (ownItemModes.empty()) continue; // no own media, nothing to confine
|
||||
|
||||
// The two visibility sources, OR'd:
|
||||
// (a) own items span >= 2 modes (W3-A trigger), and
|
||||
// (b) the track is derived-visible in >= 2 modes (the folder-media case).
|
||||
// A track qualifies for a split if EITHER makes it multi-mode.
|
||||
const auto visIt = visibleModesOf.find(track.trackGuid);
|
||||
const std::size_t visibleModeCount =
|
||||
visIt == visibleModesOf.end() ? 0 : visIt->second.size();
|
||||
const bool multiMode = ownItemModes.size() >= 2 || visibleModeCount >= 2;
|
||||
|
||||
// Single-mode (visible in exactly one mode, own items single-mode): 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 (!multiMode) continue;
|
||||
if (!multiMode) continue; // single-mode: D1 whole-track parking still separates
|
||||
|
||||
// Lazy-mint: lanes to mint = ONLY the modes the track's OWN items actually occupy —
|
||||
// never an empty reserved lane for a mode the track is merely derived-visible in.
|
||||
// A folder whose own item is Design-only but which is derived-visible in Arrange too
|
||||
// mints a Design lane ONLY (holding the item); it mints NO Arrange lane. Confinement
|
||||
// still holds: with only a Design lane present, toggling to Arrange drives that lane's
|
||||
// C_LANEPLAYS to 0 (it hides+silences) and no lane plays, so the track reads as an
|
||||
// empty normal track — the Design item does not leak. The Arrange lane is minted on
|
||||
// demand the moment an Arrange item first lands (a later mint tick sees ownItemModes
|
||||
// gain Arrange). The visibility trigger above still decides WHETHER to split; it no
|
||||
// longer inflates WHICH lanes are minted.
|
||||
const std::set<std::string>& laneModes = ownItemModes;
|
||||
const std::set<std::string>& laneModes = ownItemModes; // lazy-mint: own modes only
|
||||
|
||||
// Transition to lane-split: one managed lane per own-content mode (durable key =
|
||||
// laneNameForMode(mode)), owned by that mode.
|
||||
plan.splits.push_back(LaneMintPlan::TrackSplit{
|
||||
track.trackGuid, static_cast<int>(laneModes.size())});
|
||||
for (const std::string& mode : laneModes) {
|
||||
@@ -263,13 +177,9 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||
LaneMint{track.trackGuid, laneNameForMode(mode), mode});
|
||||
}
|
||||
|
||||
// Assign EVERY managed-eligible OWN item onto its tagged mode's lane — including
|
||||
// the pre-existing single-mode items, so a folder carrying one own Design item
|
||||
// while derived-visible in Arrange still lanes that item to the Design lane (it
|
||||
// then hides+silences whenever Arrange is active — the exact failing-case fix).
|
||||
for (const LaneItem& item : track.items) {
|
||||
if (item.guid.empty() || item.modeId.empty()) continue;
|
||||
if (item.onManualLane) continue; // exempt — never reassigned
|
||||
if (item.onManualLane) continue;
|
||||
plan.assigns.push_back(LaneAssign{
|
||||
item.guid, track.trackGuid, laneNameForMode(item.modeId)});
|
||||
}
|
||||
@@ -278,13 +188,7 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
||||
return plan;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// planner helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
||||
// Parking contract: hide both panels, out of the mix, FX bypassed, every FX
|
||||
// offline. All fixed zeros — park never consults a snapshot.
|
||||
TrackPlan p;
|
||||
p.flags = {
|
||||
{guid, Flag::ShowInTcp, 0},
|
||||
@@ -298,8 +202,6 @@ TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
||||
}
|
||||
|
||||
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
|
||||
// Restore contract: every driven flag returns to its SNAPSHOTTED value — never
|
||||
// a hardcoded "on"/default. A flag captured at 0 restores to 0.
|
||||
TrackPlan p;
|
||||
p.flags = {
|
||||
{guid, Flag::ShowInTcp, snap.showInTcp},
|
||||
@@ -319,15 +221,9 @@ std::string nextModeId(const ModeRegistry& modes, const std::string& currentMode
|
||||
if (all[i].id == currentModeId)
|
||||
return all[(i + 1) % all.size()].id; // wrap past the last
|
||||
}
|
||||
// Active mode not in the registry (stale/unknown) — jump to the first mode as a
|
||||
// sane home rather than returning "".
|
||||
return all.front().id;
|
||||
return all.front().id; // stale/unknown current id -> jump to the first mode
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ViewModeModel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {}
|
||||
|
||||
bool ViewModeModel::setActiveMode(const std::string& modeId) {
|
||||
@@ -350,8 +246,7 @@ const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
|
||||
}
|
||||
|
||||
std::size_t ViewModeModel::reconcile(const std::set<std::string>& liveGuids) {
|
||||
// Prune snapshots for GUIDs the project no longer contains (see header for the
|
||||
// deliberate snapshot-yes / membership-no asymmetry and the undo-delete rationale).
|
||||
// See header: snapshots are pruned, membership is not (undo-delete rationale).
|
||||
std::size_t removed = 0;
|
||||
for (auto it = snapshots_.begin(); it != snapshots_.end();) {
|
||||
if (liveGuids.count(it->first) == 0) {
|
||||
@@ -368,7 +263,7 @@ bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string
|
||||
const Membership* m = membership_.query(guid);
|
||||
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
|
||||
if (m->showBoth) return true; // show-both ⇒ every mode
|
||||
if (m->modeIds.empty()) return modeId == kArrangeModeId; // show-both-cleared, no mode
|
||||
if (m->modeIds.empty()) return modeId == kArrangeModeId;
|
||||
return m->modeIds.count(modeId) > 0;
|
||||
}
|
||||
|
||||
@@ -376,28 +271,18 @@ std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
|
||||
const std::string& modeId) const {
|
||||
std::set<std::string> visible;
|
||||
|
||||
// Pass 1: every node — leaf OR parent — that belongs to the mode by its OWN
|
||||
// membership is visible. For a leaf this is the tagged/show-both/untagged-Arrange
|
||||
// rule; for a parent it means an untagged folder (which carries its own FX/media
|
||||
// and defaults to Arrange) shows in Arrange even when none of its children do.
|
||||
// Parents ALSO become visible in pass 2 by derivation from a visible descendant;
|
||||
// the two rules are OR'd, so an untagged folder of all-Design leaves shows in both
|
||||
// Arrange (own default) and Design (derived).
|
||||
// Pass 1: nodes visible by their own membership (leaf rule, or an
|
||||
// untagged/Arrange-default folder).
|
||||
for (const auto& node : tree.nodes) {
|
||||
if (leafBelongsToMode(node.guid, modeId))
|
||||
visible.insert(node.guid);
|
||||
}
|
||||
|
||||
// Pass 2: a parent is also visible if any descendant is visible. Walk each
|
||||
// currently-visible node up its parent chain and mark ancestors. Seeding from the
|
||||
// full pass-1 set means a parent made visible by its own membership propagates its
|
||||
// visibility up the remaining ancestors too. Parent chains are read from the
|
||||
// supplied tree only (no REAPER access). A cycle-guard bounds the walk in case a
|
||||
// malformed tree links a node to itself.
|
||||
// Pass 2: propagate up parent chains so a parent with any visible
|
||||
// descendant is visible too (OR'd with pass 1). Cycle-guarded.
|
||||
std::map<std::string, std::string> parentOf;
|
||||
for (const auto& node : tree.nodes) parentOf[node.guid] = node.parentGuid;
|
||||
|
||||
// Snapshot the pass-1 visible set so we don't re-walk parents we add mid-loop.
|
||||
const std::vector<std::string> seeds(visible.begin(), visible.end());
|
||||
for (const auto& node : seeds) {
|
||||
auto it = parentOf.find(node);
|
||||
@@ -415,52 +300,29 @@ std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
|
||||
TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
|
||||
TogglePlan plan;
|
||||
|
||||
// The mode system manages EVERY leaf, not just tagged ones. An untagged leaf is
|
||||
// an Arrange member (leafBelongsToMode resolves that), so it must park when the
|
||||
// target mode is not Arrange and restore when it is — the same full park/restore
|
||||
// a tagged leaf gets. Enumerating the FolderTree (not membership_.all()) is what
|
||||
// brings untagged leaves — which are absent from the membership index — under
|
||||
// management. Parents are visibility-only (handled by visibleTracks + the shell's
|
||||
// parent-visibility pass) and show-both leaves are the always-visible escape;
|
||||
// neither is ever parked.
|
||||
// Enumerate the tree (not membership_.all()) so untagged leaves — absent
|
||||
// from the membership index but still Arrange members — park/restore too.
|
||||
for (const auto& node : tree.nodes) {
|
||||
if (node.isParent) continue; // parents are derived, never parked
|
||||
if (node.isParent) continue;
|
||||
const std::string& guid = node.guid;
|
||||
if (membership_.isShowBoth(guid)) continue; // show-both leaves are never parked
|
||||
if (membership_.isShowBoth(guid)) continue;
|
||||
|
||||
const bool active = leafBelongsToMode(guid, targetMode);
|
||||
if (active) {
|
||||
// Returning to visibility: restore from snapshot if we have one. No
|
||||
// snapshot ⇒ the track was never parked, nothing to restore.
|
||||
if (const TrackSnapshot* snap = snapshot(guid))
|
||||
plan.restore.push_back(makeRestorePlan(guid, *snap));
|
||||
} else {
|
||||
// Inactive leaf (tagged into another mode, or untagged in a non-Arrange
|
||||
// mode) ⇒ park. fxOffline is intentionally empty here: the D2 shell
|
||||
// expands per-FX offline writes using TrackFX_GetCount. The pure model
|
||||
// has no access to REAPER FX counts at plan time; makeParkPlan(guid, 0)
|
||||
// emits only the scalar flags as a result.
|
||||
// fxOffline is empty here; the D2 shell expands it via TrackFX_GetCount.
|
||||
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
|
||||
}
|
||||
}
|
||||
|
||||
// D2 item-level projection: emit a C_LANEPLAYS op for every MANAGED lane. The
|
||||
// active mode's lane plays exclusively; every other managed lane is silenced+hidden
|
||||
// (laneModeState). MANUAL lanes are skipped entirely — the load-bearing invariant:
|
||||
// a toggle never drives a lane the tool did not mint (the fixed-lane analog of
|
||||
// "never touch mute/solo"). Lane ownership is not a tree property, so this walks the
|
||||
// ownership index directly, not the FolderTree; a project with no fixed lanes leaves
|
||||
// plan.lanes empty and the plan is byte-identical to a D1 plan.
|
||||
// One C_LANEPLAYS op per MANAGED lane; manual lanes are skipped entirely.
|
||||
#ifndef NDEBUG
|
||||
// Debug-time guard for the one-managed-lane-per-mode-per-track exclusivity
|
||||
// assumption (see laneModeState). Two managed lanes on the same track claiming the
|
||||
// same mode would both be told to play exclusively on that mode's toggle, which
|
||||
// REAPER cannot honor. Cheap set membership over the (usually tiny) managed-lane
|
||||
// set; compiled out of release builds.
|
||||
std::set<std::pair<std::string, std::string>> seenTrackMode; // (trackGuid, mode)
|
||||
#endif
|
||||
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||
if (!ownership.isManaged()) continue; // manual lanes are off-limits
|
||||
if (!ownership.isManaged()) continue;
|
||||
#ifndef NDEBUG
|
||||
assert(seenTrackMode.insert({ref.trackGuid, *ownership.managedMode}).second &&
|
||||
"two managed lanes on one track claim the same mode (exclusivity broken)");
|
||||
@@ -473,9 +335,6 @@ TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string&
|
||||
}
|
||||
|
||||
std::set<LaneRef> ViewModeModel::lanesTouchedByToggle() const {
|
||||
// Managed-only: exactly the lanes a toggle is permitted to drive. A manual lane —
|
||||
// absent OR recorded manual in the ownership index — is never returned, so the shell
|
||||
// can never write C_LANEPLAYS to a lane the user hand-manages.
|
||||
std::set<LaneRef> touched;
|
||||
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||
if (ownership.isManaged()) touched.insert(ref);
|
||||
@@ -488,14 +347,9 @@ bool ViewModeModel::operator==(const ViewModeModel& o) const {
|
||||
activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// JSON — writer
|
||||
// ===========================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
// Shared core/json emit helpers (Q-W1): same escape set + %d rendering as the
|
||||
// prior file-local writer, so the emitted blob is byte-identical.
|
||||
// Shared core/json emit helpers (Q-W1) — byte-identical escape/int rendering.
|
||||
using json::writeEscaped;
|
||||
using json::writeIntArray;
|
||||
std::string intToStr(int v) { return json::numToStr(v); }
|
||||
@@ -510,7 +364,6 @@ std::string ViewModeModel::serialize() const {
|
||||
root.keyRaw("version", intToStr(1));
|
||||
root.keyStr("activeMode", activeModeId_);
|
||||
|
||||
// modes
|
||||
root.keyBegin("modes");
|
||||
out += '[';
|
||||
{
|
||||
@@ -571,10 +424,7 @@ std::string ViewModeModel::serialize() const {
|
||||
}
|
||||
out += ']';
|
||||
|
||||
// lanes: array of { trackGuid, laneKey, managed(bool), mode(str, managed only) }.
|
||||
// A manual lane omits "mode"; managed carries the owning mode id. Emitting an
|
||||
// explicit "managed" bool keeps a manual lane distinguishable from a managed lane
|
||||
// whose mode string is (illegally) empty — the parser rejects the latter.
|
||||
// lanes: array of { trackGuid, laneKey, managed(bool), mode(str, managed only) }
|
||||
root.keyBegin("lanes");
|
||||
out += '[';
|
||||
{
|
||||
@@ -590,23 +440,12 @@ std::string ViewModeModel::serialize() const {
|
||||
}
|
||||
}
|
||||
out += ']';
|
||||
} // root closes here (see bank_model note on NRVO + deferred close)
|
||||
} // root closes here (NRVO + deferred close, mirrors bank_model)
|
||||
return out;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// JSON — parser (recursive descent; false on any malformed input, never UB)
|
||||
// ===========================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
// The model DOMAIN grammar over the shared core/json lexical layer (Q-W1).
|
||||
|
||||
// The registry starts seeded (Arrange + Design). Deserialization must reproduce the
|
||||
// serialized set exactly, so we replace the seeded contents with the parsed ones —
|
||||
// add() dedups by id, so a serialized Arrange/Design would otherwise be rejected as
|
||||
// duplicates and the ordinals/names would not round-trip. We therefore parse into a
|
||||
// fresh vector and swap. `reg` is passed empty (see parseModel).
|
||||
bool parseModes(json::Reader& r, ModeRegistry& reg) {
|
||||
if (!r.consume('[')) return false;
|
||||
r.skipWs();
|
||||
@@ -659,18 +498,9 @@ bool parseMembership(json::Reader& r, MembershipIndex& idx) {
|
||||
} while (r.consume(','));
|
||||
if (!r.consume('}')) return false;
|
||||
if (!haveGuid || guid.empty()) return false;
|
||||
// Install the entry verbatim (tag() would clear a multi-mode set and drop
|
||||
// show-both). A serialized entry is trusted to already satisfy the model's
|
||||
// invariants.
|
||||
//
|
||||
// Deliberate tolerance: we do NOT validate that membership modeIds reference
|
||||
// registered modes, and we do not validate snapshot GUIDs against the index.
|
||||
// Stale-GUID and stale-mode tolerance is a stated invariant of this model —
|
||||
// a deserialized entry is treated as trusted data, not as live cross-checked
|
||||
// state. Rejecting stale entries here would violate that invariant. The one
|
||||
// exception is activeMode (validated below in parseModel): a persisted active
|
||||
// mode that no longer exists has an immediate behavioral consequence, so it
|
||||
// is caught and the parse is rejected.
|
||||
// Install verbatim (tag() would clobber a multi-mode set / show-both).
|
||||
// Stale mode ids / stale GUIDs are tolerated by design — only
|
||||
// activeMode is validated (below).
|
||||
if (!idx.restore(guid, mem)) return false;
|
||||
} while (r.consume(','));
|
||||
return r.consume(']');
|
||||
@@ -721,10 +551,8 @@ bool parseLanes(json::Reader& r, LaneOwnershipIndex& idx) {
|
||||
else if (!r.skipValue()) return false;
|
||||
} while (r.consume(','));
|
||||
if (!r.consume('}')) return false;
|
||||
// Both keys mandatory and non-empty (they form the lane's identity). A managed
|
||||
// lane must carry a non-empty mode; a manual lane must not claim one. Enforcing
|
||||
// this on parse keeps a round-tripped index byte-for-byte identical to the
|
||||
// serialized one and rejects a malformed managed-without-mode entry.
|
||||
// Both keys mandatory/non-empty; managed must carry a mode, manual must not
|
||||
// — keeps a round-tripped index byte-for-byte identical to the source.
|
||||
if (!haveTrack || !haveLane || !haveManaged) return false;
|
||||
if (trackGuid.empty() || laneKey.empty()) return false;
|
||||
if (managed) {
|
||||
@@ -769,11 +597,7 @@ bool parseModel(json::Reader& r, ViewModeModel& out) {
|
||||
} else if (key == "lanes") {
|
||||
if (!parseLanes(r, lanes)) return false;
|
||||
} else {
|
||||
// Unknown keys and the "version" field are skipped here.
|
||||
// "version" is serialized as a forward-compat placeholder — there is no
|
||||
// active version gate yet; all persisted data is parsed the same way
|
||||
// regardless of the value. A future gate would add a version branch here.
|
||||
if (!r.skipValue()) return false;
|
||||
if (!r.skipValue()) return false; // unknown keys / "version" placeholder
|
||||
}
|
||||
} while (r.consume(','));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user