Make Model's Debug exhaustive and the Debug fingerprint canonical across instances (improvise-djcn)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-23 08:45:14 -07:00
parent aa96cd6404
commit 6096c03caa
5 changed files with 130 additions and 14 deletions
+37 -3
View File
@@ -920,13 +920,47 @@ pub(crate) mod tests {
fn a_model_command_lands_where_its_keypress_lands() {
/// Two categories on different axes: a one-category fixture cannot tell
/// a transpose from a cycle (`doctrine(check_the_checker)`).
/// Cells and multi-entry view maps too: two `App`s are compared, so every
/// `HashMap` the fingerprint walks must hold 2+ entries (improvise-djcn).
fn pivoted() -> App {
use crate::model::cell::{CellKey, CellValue};
let mut app = app();
app.model_state.workbook.add_category("Month").unwrap();
app.model_state.workbook.add_item("Month", "Jan").unwrap();
let view = app.model_state.workbook.active_view_mut();
let wb = &mut app.model_state.workbook;
let items = [
("Region", ["South", "East", "West"]),
("Month", ["Jan", "Feb", "Mar"]),
("Channel", ["Web", "Store", "Phone"]),
("Year", ["2024", "2025", "2026"]),
];
for (category, names) in items {
wb.add_category(category).ok();
names.iter().for_each(|n| drop(wb.add_item(category, *n)));
}
for (n, (region, month)) in [("North", "Jan"), ("South", "Feb"), ("East", "Jan")]
.into_iter()
.enumerate()
{
let key = CellKey::new(vec![
("Region".into(), region.into()),
("Month".into(), month.into()),
("Channel".into(), "Web".into()),
("Year".into(), "2024".into()),
]);
wb.model.set_cell(key, CellValue::Number(n as f64 + 1.0));
}
let view = wb.active_view_mut();
view.set_axis("Region", Axis::Row);
view.set_axis("Month", Axis::Column);
view.set_axis("Channel", Axis::Page);
view.set_axis("Year", Axis::Page);
view.set_page_selection("Channel", "Web");
view.set_page_selection("Year", "2024");
view.hide_item("Region", "West");
view.hide_item("Region", "South");
view.hide_item("Month", "Mar");
view.toggle_group_collapse("Region", "Coastal");
view.toggle_group_collapse("Region", "Inland");
view.toggle_group_collapse("Month", "Q1");
app.rebuild_layout();
app
}
+32 -1
View File
@@ -194,7 +194,7 @@ impl RecordEntry {
}
}
#[derive(Debug, Clone, Default)]
#[derive(Clone, Default)]
pub struct DataStore {
/// Primary storage: interned keys, insertion-ordered so records mode can
/// show rows in entry order. That order is correctness — core.md §8.
@@ -208,6 +208,37 @@ pub struct DataStore {
records: HashMap<RecordHash, RecordEntry>,
}
/// Resolved through the symbol table and sorted wherever the storage is a
/// `HashMap`, so two equal stores print alike (improvise-djcn).
impl std::fmt::Debug for DataStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let DataStore {
cells,
symbols,
index,
records,
} = self;
let cells: Vec<_> = cells
.iter()
.map(|(key, value)| (self.to_cell_key(key), value))
.collect();
let index: std::collections::BTreeMap<_, std::collections::BTreeSet<_>> = index
.iter()
.map(|((cat, item), keys)| {
let pair = (symbols.resolve(*cat), symbols.resolve(*item));
(pair, keys.iter().map(|k| self.to_cell_key(k).0).collect())
})
.collect();
let records: std::collections::BTreeMap<_, _> = records.iter().collect();
f.debug_struct("DataStore")
.field("cells", &cells)
.field("symbols", symbols)
.field("index", &index)
.field("records", &records)
.finish()
}
}
impl DataStore {
pub fn new() -> Self {
Self::default()
+11 -1
View File
@@ -5,12 +5,22 @@ use std::collections::HashMap;
pub struct Symbol(u64);
/// Bidirectional string ↔ Symbol mapping.
#[derive(Debug, Clone, Default)]
#[derive(Clone, Default)]
pub struct SymbolTable {
to_id: HashMap<String, Symbol>,
to_str: Vec<String>,
}
/// The interned strings, sorted: which `Symbol` each got is interning order,
/// not content, so two equal stores print alike (improvise-djcn).
impl std::fmt::Debug for SymbolTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let SymbolTable { to_id, to_str: _ } = self;
let interned: std::collections::BTreeSet<&String> = to_id.keys().collect();
f.debug_set().entries(interned).finish()
}
}
impl SymbolTable {
#[allow(dead_code)]
pub fn new() -> Self {
+17 -8
View File
@@ -317,16 +317,25 @@ pub struct Model {
/// cache in hash order, which is not a change (improvise-pc3b, gotcha 18).
impl std::fmt::Debug for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut aggs: Vec<_> = self.measure_agg.iter().collect();
aggs.sort_by(|a, b| a.0.cmp(b.0));
let mut cache: Vec<_> = self.formula_cache.iter().collect();
// Destructured, so a new field is a compile error here (improvise-djcn).
let Model {
name,
categories,
data,
formulas,
next_category_id,
measure_agg,
formula_cache,
} = self;
let aggs: std::collections::BTreeMap<_, _> = measure_agg.iter().collect();
let mut cache: Vec<_> = formula_cache.iter().collect();
cache.sort_by(|a, b| a.0.0.cmp(&b.0.0));
f.debug_struct("Model")
.field("name", &self.name)
.field("categories", &self.categories)
.field("data", &self.data)
.field("formulas", &self.formulas)
.field("next_category_id", &self.next_category_id)
.field("name", name)
.field("categories", categories)
.field("data", data)
.field("formulas", formulas)
.field("next_category_id", next_category_id)
.field("measure_agg", &aggs)
.field("formula_cache", &cache)
.finish()
+33 -1
View File
@@ -27,7 +27,7 @@ pub fn unused_view_name<'a>(existing: impl IntoIterator<Item = &'a str>) -> Stri
.expect("a name is free below usize::MAX, there being fewer views")
}
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct View {
pub name: String,
/// Axis assignment for each category
@@ -44,6 +44,38 @@ pub struct View {
pub prune_empty: bool,
}
/// Sorted wherever the storage is a `HashMap`, so two equal views print alike;
/// destructured, so a new field is a compile error here (improvise-djcn).
impl std::fmt::Debug for View {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::collections::{BTreeMap, BTreeSet};
let View {
name,
category_axes,
page_selections,
hidden_items,
collapsed_groups,
number_format,
prune_empty,
} = self;
let sorted = |m: &HashMap<String, HashSet<String>>| -> BTreeMap<String, BTreeSet<String>> {
m.iter()
.map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
.collect()
};
let pages: BTreeMap<_, _> = page_selections.iter().collect();
f.debug_struct("View")
.field("name", name)
.field("category_axes", category_axes)
.field("page_selections", &pages)
.field("hidden_items", &sorted(hidden_items))
.field("collapsed_groups", &sorted(collapsed_groups))
.field("number_format", number_format)
.field("prune_empty", prune_empty)
.finish()
}
}
impl View {
/// True for the on-demand scratch views, rebuilt rather than persisted.
pub fn is_virtual(&self) -> bool {