Import asks what to do with records that share every coordinate (improvise-cgqt)

When source records collide on every coordinate, the import wizard now asks:
sum them, keep them separate with a generated occurrence category, or go
back. Collisions are found before the build by the same placement pass the
build uses, and are carried to the TUI as structured data (WizardCache) and
listed in full, scrollable. At the 12-category cap, "keep separate" is shown
disabled with its reason and remedy.

Every wizard warning and message now wraps to the panel instead of being
truncated, and long lists scroll.

Headless paths no longer sum silently: `import --no-wizard` takes
`--on-duplicate refuse|sum|separate` (default refuse), and `:import-json`
refuses and points at `:import`.

Reasoning: context/code/formula-and-io.md, "Colliding records are a
question, not a default"; context/code/tui.md; context/code/cli.md §11.

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-24 18:43:19 -07:00
parent ae5cf40824
commit 1a2952058d
17 changed files with 1682 additions and 124 deletions
+3 -3
View File
@@ -86,9 +86,9 @@ fn repeated_import_coordinates_not_silently_overwritten() {
use improvise_io::import::wizard::ImportPipeline;
let (records, _) =
parse_csv_bytes_with_warnings(b"Region,Revenue\nEast,10\nEast,20\n").unwrap();
let (wb, warnings) = ImportPipeline::new(serde_json::Value::Array(records))
.build_model_with_warnings()
.unwrap();
let mut pipeline = ImportPipeline::new(serde_json::Value::Array(records));
pipeline.on_duplicate = improvise_io::import::wizard::DuplicatePolicy::Sum;
let (wb, warnings) = pipeline.build_model_with_warnings().unwrap();
let values: Vec<f64> = wb
.model
.data
+64
View File
@@ -548,3 +548,67 @@ fn a_refusal_that_only_writes_a_status_exits_non_zero() {
stderr_of(&out)
);
}
/// Two regions, East repeated: records #1 and #3 share every coordinate.
fn colliding_csv(scratch: &Scratch) -> PathBuf {
let csv = scratch.path("dups.csv");
std::fs::write(&csv, "Region,Revenue\nEast,10\nWest,5\nEast,20\n").unwrap();
csv
}
fn run_import(csv: &Path, out: &Path, extra: &[&str]) -> Output {
Command::new(bin())
.arg("import")
.arg(csv)
.arg("--no-wizard")
.args(extra)
.arg("-o")
.arg(out)
.output()
.expect("the improvise binary runs")
}
/// improvise-cgqt: nobody is there to ask, so an unanswered collision stops
/// the import and names the records. `context/test-evidence/cli.md`.
#[test]
fn a_headless_import_with_duplicates_refuses_by_default_and_names_them() {
let scratch = Scratch::new("dup_refuse");
let out_path = scratch.path("out.improv");
let out = run_import(&colliding_csv(&scratch), &out_path, &[]);
let err = stderr_of(&out);
assert!(!out.status.success(), "{err}");
assert!(err.contains("2 records (#1, #3): Region=East"), "{err}");
assert!(err.contains("--on-duplicate"), "names the way out: {err}");
assert!(!out_path.exists(), "nothing half-imported is written");
}
#[test]
fn a_headless_import_sums_duplicates_when_told_to() {
let scratch = Scratch::new("dup_sum");
let out_path = scratch.path("out.improv");
let out = run_import(
&colliding_csv(&scratch),
&out_path,
&["--on-duplicate", "sum"],
);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&out_path).unwrap();
assert!(doc.contains("= 30"), "{doc}");
assert!(!doc.contains("Occurrence"), "{doc}");
}
#[test]
fn a_headless_import_keeps_duplicates_separate_when_told_to() {
let scratch = Scratch::new("dup_separate");
let out_path = scratch.path("out.improv");
let out = run_import(
&colliding_csv(&scratch),
&out_path,
&["--on-duplicate", "separate"],
);
assert!(out.status.success(), "{}", stderr_of(&out));
let doc = std::fs::read_to_string(&out_path).unwrap();
assert!(doc.contains("## Category: Occurrence"), "{doc}");
assert!(doc.contains("= 10") && doc.contains("= 20"), "{doc}");
assert!(!doc.contains("= 30"), "{doc}");
}