Merge commit '44718e8a' into feat/8jk6.3-date-hierarchy

# Conflicts:
#	crates/improvise-io/src/import/wizard.rs
#	tests/headless_cli.rs
This commit is contained in:
Claude (agent-hier)
2026-09-24 22:47:28 -07:00
committed by Edward Langley
13 changed files with 1523 additions and 166 deletions
+65
View File
@@ -0,0 +1,65 @@
//! A folder import's per-file views, saved and reloaded, as both front ends
//! project them (improvise-lpzu). Evidence: `context/test-evidence/cli.md`.
use improvise::import::csv_parser::merge_csvs_from;
use improvise::import::wizard::ImportPipeline;
use improvise::persistence::{format_md, parse_md};
use improvise::ui::app::App;
use improvise_command::ui::project::{Viewport, grid_in};
use improvise_io::storage::{MemoryFs, Storage};
fn reloaded_folder_import() -> App {
let fs = MemoryFs::new();
let files = [
(
"maat/age.csv",
"entity,age-months\nsrc/a.rs,2\nsrc/c.rs,7\n",
),
(
"maat/coupling.csv",
"entity,coupled,degree,average-revs\nsrc/a.rs,src/b.rs,80,5\nsrc/b.rs,src/c.rs,40,3\n",
),
("maat/soc.csv", "entity,soc\n"),
];
for (path, body) in files {
fs.write(std::path::Path::new(path), body.as_bytes())
.unwrap();
}
let merged = merge_csvs_from(&fs, &files.map(|(p, _)| p)).unwrap();
let (wb, _) = ImportPipeline::from_merge(merged)
.build_model_with_warnings()
.unwrap();
App::new(parse_md(&format_md(&wb)).unwrap(), None)
}
/// Evidence: `context/test-evidence/cli.md`.
#[test]
fn a_saved_per_file_view_projects_only_its_file() {
let app = reloaded_folder_import();
let whole = Viewport {
row_offset: 0,
col_offset: 0,
rows: 100,
cols: 100,
};
for (view, texts) in [("age", ["2.00", "7.00"]), ("coupling", ["5.00", "3.00"])] {
let grid = grid_in(&app, view, whole);
let file = grid.page_coords.iter().find(|p| p.cat == "File").unwrap();
assert_eq!((file.selected.as_str(), &file.items), (view, &None));
let drawn: Vec<&str> = grid
.cells
.iter()
.flatten()
.map(|c| c.text.as_str())
.filter(|t| !t.is_empty())
.collect();
assert_eq!(drawn, texts, "{view}");
}
// A header-only file: its item exists, and its view has nothing editable.
let soc = grid_in(&app, "soc", whole);
let file = soc.page_coords.iter().find(|p| p.cat == "File").unwrap();
assert_eq!(file.selected, "soc");
assert!(soc.records, "a file with no measure opens in records mode");
assert_eq!(soc.extent.rows, 0, "{soc:?}");
}
+127
View File
@@ -665,3 +665,130 @@ fn a_headless_date_import_splits_when_told_to() {
assert!(doc.contains("## Category: Date_Month"), "{doc}");
assert!(doc.contains("2024-01-15"), "the raw dates stay: {doc}");
}
/// code-maat's reports: three schemas, a stray note, and room for the output.
fn code_maat_folder(scratch: &Scratch) -> PathBuf {
let dir = scratch.path("maat");
std::fs::create_dir_all(&dir).unwrap();
for (name, body) in [
(
"coupling.csv",
"entity,coupled,degree,average-revs\nsrc/a.rs,src/b.rs,80,5\n",
),
("age.csv", "entity,age-months\nsrc/a.rs,2\nsrc/b.rs,9\n"),
(
"abs-churn.csv",
"date,added,deleted,commits\n2026-01-02,10,3,2\n",
),
("README.txt", "not a report\n"),
] {
std::fs::write(dir.join(name), body).unwrap();
}
dir
}
/// improvise-lpzu: a folder argument is its `*.csv` files, merged sparsely and
/// in name order. Evidence: `context/test-evidence/cli.md`.
#[test]
fn a_headless_import_of_a_folder_merges_its_csv_files_and_repeats_byte_for_byte() {
let scratch = Scratch::new("folder_import");
let dir = code_maat_folder(&scratch);
let out_path = dir.join("maat.improv");
let first = run_import(&dir, &out_path, &[]);
assert!(first.status.success(), "{}", stderr_of(&first));
let doc = std::fs::read_to_string(&out_path).unwrap();
for line in [
"File=coupling, _Measure=degree, coupled=|src/b.rs|, entity=|src/a.rs| = 80",
"File=age, _Measure=age-months, entity=|src/b.rs| = 9",
"File=abs-churn, _Measure=added, date=|2026-01-02| = 10",
] {
assert!(doc.lines().any(|l| l == line), "{line} missing from\n{doc}");
}
assert!(!doc.contains("README"), "{doc}");
let views: Vec<&str> = doc.lines().filter(|l| l.starts_with("## View: ")).collect();
assert_eq!(
views,
[
"## View: Default",
"## View: abs-churn",
"## View: age",
"## View: coupling"
]
);
assert!(doc.lines().any(|l| l == "File: filter, coupling"), "{doc}");
assert!(doc.contains("Initial View: Default"), "{doc}");
// The output now sits in the folder; a second run must not read it.
let second = run_import(&dir, &out_path, &[]);
assert!(second.status.success(), "{}", stderr_of(&second));
assert_eq!(std::fs::read_to_string(&out_path).unwrap(), doc);
}
/// improvise-lpzu: a file and a folder together are one merge, and a folder
/// with nothing to import is named. Evidence: `context/test-evidence/cli.md`.
#[test]
fn a_headless_import_takes_files_and_folders_together_and_refuses_an_empty_folder() {
let scratch = Scratch::new("folder_mixed");
let dir = code_maat_folder(&scratch);
let extra = scratch.path("extra.csv");
std::fs::write(&extra, "entity,age-months\nsrc/z.rs,4\n").unwrap();
let out_path = scratch.path("out.improv");
let out = Command::new(bin())
.args(["import", "--no-wizard", "-o"])
.arg(&out_path)
.arg(&extra)
.arg(&dir)
.output()
.unwrap();
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&out_path).unwrap();
assert!(
doc.contains("File=extra, _Measure=age-months, entity=|src/z.rs| = 4"),
"{doc}"
);
assert!(doc.contains("File=coupling"), "{doc}");
let empty = scratch.path("empty");
std::fs::create_dir_all(&empty).unwrap();
let refused = run_import(&empty, &scratch.path("never.improv"), &[]);
assert!(!refused.status.success());
let err = stderr_of(&refused);
assert!(
err.contains(&format!("no CSV files in folder {}", empty.display())),
"{err}"
);
assert!(!scratch.path("never.improv").exists());
}
/// improvise-lpzu, the user's ruling: a folder always merges, so even one CSV
/// keeps its file identity. Evidence: `context/test-evidence/cli.md`.
#[test]
fn a_folder_of_one_csv_still_gets_a_file_category_and_its_view_but_a_lone_file_does_not() {
let scratch = Scratch::new("folder_single");
let dir = scratch.path("one");
std::fs::create_dir_all(&dir).unwrap();
let csv = dir.join("age.csv");
std::fs::write(&csv, "entity,age-months\nsrc/a.rs,2\n").unwrap();
let from_folder = scratch.path("folder.improv");
let out = run_import(&dir, &from_folder, &[]);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&from_folder).unwrap();
assert!(doc.contains("## Category: File\n- age\n"), "{doc}");
assert!(doc.lines().any(|l| l == "File: filter, age"), "{doc}");
assert!(
doc.contains("File=age, _Measure=age-months, entity=|src/a.rs| = 2"),
"{doc}"
);
let from_file = scratch.path("file.improv");
let out = run_import(&csv, &from_file, &[]);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&from_file).unwrap();
assert!(
!doc.contains("File"),
"a lone file keeps today's shape: {doc}"
);
}