From beebf220322df1fed6eff0c4a3bb4abf050e72af Mon Sep 17 00:00:00 2001
From: jyviko <2501648+jyviko@users.noreply.github.com>
Date: Thu, 22 Oct 2026 07:21:21 -0400
Subject: [PATCH] kernel-cases: --dumps=DIR leaves each failing SMLib call's
inputs for replay
A rig case whose SMLib call fails left nothing to replay it from: nothing called
solidcraft_smlib::set_dump_dir. `solidcraft-cli kernel-cases --dumps=DIR` now points SMLib's
dump folder at DIR/- before each case runs, so a failing call leaves its
arguments and B-reps there (smlib_brep_read reads them back into a scratch C program), and
turns it off after the run. Off without the flag. The kernel exposes it as
smlib_dump_dir, beside smlib_built (the CLI reaches SMLib only through the kernel).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
---
apps/solidcraft-cli/src/kernel_cases.rs | 24 ++++++++++++++++++++++--
apps/solidcraft-cli/src/main.rs | 4 ++--
crates/kernel/src/lib.rs | 2 +-
crates/kernel/src/validity.rs | 14 ++++++++++++++
tests/kernel-cases/README.md | 1 +
5 files changed, 40 insertions(+), 5 deletions(-)
diff --git a/apps/solidcraft-cli/src/kernel_cases.rs b/apps/solidcraft-cli/src/kernel_cases.rs
index eeddf32e07..549ad11cd2 100644
--- a/apps/solidcraft-cli/src/kernel_cases.rs
+++ b/apps/solidcraft-cli/src/kernel_cases.rs
@@ -129,7 +129,7 @@ fn case_name(dir: &str) -> (String, String) {
(name(p.parent()), name(Some(p)))
}
-fn check_case(dir: &str) -> Value {
+fn check_case(dir: &str, dumps: Option<&std::path::Path>) -> Value {
let (area, name) = case_name(dir);
let mut out = json!({"case": name, "area": area, "kernel": kernel()});
let want = match read_json(&format!("{dir}/expected.json")) {
@@ -167,6 +167,18 @@ fn check_case(dir: &str) -> Value {
let outcome = want["outcome"].as_str().unwrap_or("solid");
let refused = outcome == "refused";
let may_refuse = refused || outcome == "solid_or_refused";
+ // The SMLib calls that fail in this case leave their inputs in its own folder.
+ if let Some(root) = dumps {
+ let folder = root.join(format!("{area}-{name}"));
+ if let Err(e) = std::fs::create_dir_all(&folder)
+ .map_err(|e| e.to_string())
+ .and_then(|()| solidcraft_engine::kernel::smlib_dump_dir(Some(&folder)).map_err(|e| e.to_string()))
+ {
+ out["status"] = json!("fail");
+ out["error"] = json!(format!("--dumps: {}: {e}", folder.display()));
+ return out;
+ }
+ }
let mut s = solidcraft_engine::Session::default();
let clock = std::time::Instant::now();
let ran = s.run_script(&commands);
@@ -331,7 +343,15 @@ pub fn run(args: &[String]) -> Result<(), String> {
if dirs.is_empty() {
return Err("kernel-cases: give one or more case directories (each with recipe.json and expected.json)".into());
}
- let results: Vec = dirs.iter().map(|d| check_case(d)).collect();
+ // `--dumps=DIR`: each case's failing SMLib calls leave their inputs in DIR/-.
+ let dumps = args.iter().find_map(|a| a.strip_prefix("--dumps=")).map(std::path::PathBuf::from);
+ if dumps.as_ref().is_some_and(|d| d.as_os_str().is_empty()) {
+ return Err("kernel-cases: --dumps= needs a folder".into());
+ }
+ let results: Vec = dirs.iter().map(|d| check_case(d, dumps.as_deref())).collect();
+ if dumps.is_some() {
+ solidcraft_engine::kernel::smlib_dump_dir(None).map_err(|e| format!("kernel-cases: --dumps: {e}"))?;
+ }
report(args, &results);
let bad = results.iter().filter(|r| matches!(r["status"].as_str(), Some("fail" | "crash"))).count();
if bad > 0 { Err(format!("{bad} kernel case(s) failed")) } else { Ok(()) }
diff --git a/apps/solidcraft-cli/src/main.rs b/apps/solidcraft-cli/src/main.rs
index 4fffaff44d..1bd2627a12 100644
--- a/apps/solidcraft-cli/src/main.rs
+++ b/apps/solidcraft-cli/src/main.rs
@@ -15,7 +15,7 @@
//! solidcraft-cli commands the command registry as JSON
//! solidcraft-cli recipe translate an oracle recipe to a script
//! solidcraft-cli oracle ... replay recipes and compare with expected.json
-//! solidcraft-cli kernel-cases [--json] ... the kernel regression rig (tests/kernel-cases)
+//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] ... the kernel regression rig (tests/kernel-cases)
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! [--param-check off|warn|strict]
//! MCP server on stdio (docs/mcp.md)
@@ -82,7 +82,7 @@ const USAGE_RECIPE: &str = " solidcraft-cli recipe \n";
/// `oracle`'s lines in the usage.
const USAGE_ORACLE: &str = " solidcraft-cli oracle ... [--json]\n";
/// `kernel-cases`'s lines in the usage.
-const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases ... [--json]\n (the kernel regression rig, tests/kernel-cases; SOLIDCRAFT_KERNEL picks the kernel)\n";
+const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases ... [--json] [--dumps=DIR]\n (the kernel regression rig, tests/kernel-cases; SOLIDCRAFT_KERNEL picks the kernel; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/-)\n";
/// `fuzz`'s lines in the usage.
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs
index 0c53bbb1c3..b6c1e40fd7 100644
--- a/crates/kernel/src/lib.rs
+++ b/crates/kernel/src/lib.rs
@@ -217,7 +217,7 @@ pub use sweep_path::{RailFit, SweepControl, SweepOrientation, sweep_path, sweep_
pub use topo::{CylinderFace, cylinder_end_at, cylinder_face, cylinder_face_at};
pub use topo::{TopoCounts, merged_topology, seam_flags};
pub use tspline::tspline_sheet;
-pub use validity::{SmlibCheck, Validity, smlib_built, validity};
+pub use validity::{SmlibCheck, Validity, smlib_built, smlib_dump_dir, validity};
pub use whole_face::{WholeFace, cut_takes_only_the_face, cut_tool, face_prism, matching_face};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
diff --git a/crates/kernel/src/validity.rs b/crates/kernel/src/validity.rs
index 52cb851295..e5dd336ee2 100644
--- a/crates/kernel/src/validity.rs
+++ b/crates/kernel/src/validity.rs
@@ -59,3 +59,17 @@ pub fn smlib_built() -> bool {
cfg!(feature = "smlib")
}
+/// Where every SMLib call that fails from now on leaves its inputs for replay (the C API's
+/// `smlib_set_dump_dir`: the call's arguments and B-reps, which `smlib_brep_read` reads back);
+/// `None` stops it. A build without SMLib makes no calls to dump.
+pub fn smlib_dump_dir(dir: Option<&std::path::Path>) -> crate::Result<()> {
+ #[cfg(feature = "smlib")]
+ {
+ solidcraft_smlib::set_dump_dir(dir).map_err(|e| crate::KernelError::Failed(format!("SMLib dump folder: {e}")))
+ }
+ #[cfg(not(feature = "smlib"))]
+ {
+ let _ = dir;
+ Ok(())
+ }
+}
diff --git a/tests/kernel-cases/README.md b/tests/kernel-cases/README.md
index 67733cf247..119e22bd40 100644
--- a/tests/kernel-cases/README.md
+++ b/tests/kernel-cases/README.md
@@ -8,6 +8,7 @@ them. `cargo xtask kernel-cases` runs them and writes [`docs/kernel-cases.md`](.
cargo xtask kernel-cases # the default kernel
cargo xtask kernel-cases --kernel both --cli target/release/solidcraft-cli # and SMLib
cargo run -p solidcraft-cli -- kernel-cases tests/kernel-cases/booleans/ # one case
+cargo run -p solidcraft-cli --features smlib -- kernel-cases --dumps=/tmp/dumps # failing SMLib calls leave replayable inputs
```
For the `smlib` column, build `solidcraft-cli` with `--features smlib` (after