Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+145
View File
@@ -0,0 +1,145 @@
#include "core/model/slot_map.h"
#include <algorithm>
#include "core/json/json.h"
// slot_map implementation (extracted from bank_book, Q-W1 T4-05).
//
// The invariant: entries_ is kept sorted ascending by slot, one id per slot, one
// slot per id. Every mutator restores it; queries assume it. serialize rides the
// shared core/json emit helpers — the emitted fragment is byte-identical to the
// pre-extraction bank_book writer.
namespace reasampler::model {
void SlotMap::sortBySlot() {
std::stable_sort(entries_.begin(), entries_.end(),
[](const Entry& a, const Entry& b) { return a.slot < b.slot; });
}
int SlotMap::slotOf(const std::string& id) const {
for (const auto& e : entries_)
if (e.id == id) return e.slot;
return -1;
}
std::string SlotMap::idAt(int slot) const {
for (const auto& e : entries_)
if (e.slot == slot) return e.id;
return {};
}
int SlotMap::maxSlot() const {
int m = -1;
for (const auto& e : entries_)
if (e.slot > m) m = e.slot;
return m;
}
std::vector<std::string> SlotMap::orderedIds() const {
// entries_ is sorted ascending by slot, so a straight walk is display order.
std::vector<std::string> out;
out.reserve(entries_.size());
for (const auto& e : entries_) out.push_back(e.id);
return out;
}
void SlotMap::append(const std::string& id) {
if (id.empty()) return;
remove(id); // an existing id is re-appended, not left in place
entries_.push_back(Entry{id, maxSlot() + 1}); // next free slot after the last occupied
sortBySlot();
}
bool SlotMap::remove(const std::string& id) {
for (auto it = entries_.begin(); it != entries_.end(); ++it) {
if (it->id == id) {
entries_.erase(it); // leaves the slot empty — no re-pack
return true;
}
}
return false;
}
bool SlotMap::reorder(const std::string& id, int targetSlot) {
if (slotOf(id) < 0) return false; // not mapped -> no mutation
if (targetSlot < 0) targetSlot = 0;
if (slotOf(id) == targetSlot) return false; // already there — true no-op
// Detach the moving id first so the occupancy test below sees the post-move world.
remove(id);
const bool occupied = !idAt(targetSlot).empty();
if (occupied) {
// Insert-before-and-shift: every occupant at slot >= targetSlot shifts up by one,
// preserving relative order and interior gaps above the target. The moving id then
// takes targetSlot cleanly.
for (auto& e : entries_)
if (e.slot >= targetSlot) ++e.slot;
}
entries_.push_back(Entry{id, targetSlot});
sortBySlot();
return true;
}
void SlotMap::resetDense(const std::vector<std::string>& ids) {
entries_.clear();
int slot = 0;
for (const auto& id : ids) {
if (id.empty()) continue;
if (slotOf(id) >= 0) continue; // skip a duplicate id (one slot per id)
entries_.push_back(Entry{id, slot++});
}
// Already ascending by construction; no sort needed.
}
void SlotMap::reconcile(const std::vector<std::string>& liveIds) {
// Drop markers whose sample left the index.
entries_.erase(
std::remove_if(entries_.begin(), entries_.end(),
[&](const Entry& e) {
return std::find(liveIds.begin(), liveIds.end(), e.id) ==
liveIds.end();
}),
entries_.end());
// Append live ids that have no mapping yet (out-of-band index growth), in liveIds
// order, each to the next free slot after the current frontier.
for (const auto& id : liveIds)
if (slotOf(id) < 0) append(id);
sortBySlot();
}
bool SlotMap::operator==(const SlotMap& o) const {
return entries_ == o.entries_;
}
SlotMap SlotMap::fromEntries(const std::vector<std::pair<std::string, int>>& pairs) {
SlotMap m;
for (const auto& [id, slot] : pairs) {
if (id.empty() || slot < 0) continue; // drop malformed pair
if (m.slotOf(id) >= 0) continue; // duplicate id: first wins
if (!m.idAt(slot).empty()) continue; // slot taken: never double-occupy
m.entries_.push_back(Entry{id, slot});
}
m.sortBySlot();
return m;
}
std::string SlotMap::serialize() const {
// Array of {id, slot} objects in ascending slot order (entries_ is kept sorted).
// json::Writer + numToStr are the same emit path the pre-extraction writer used,
// so the fragment is byte-identical.
std::string out;
out += '[';
for (std::size_t i = 0; i < entries_.size(); ++i) {
if (i) out += ',';
json::Writer e(out);
e.keyStr("id", entries_[i].id);
e.keyRaw("slot", json::numToStr(entries_[i].slot));
}
out += ']';
return out;
}
} // namespace reasampler::model