CLI: check subcommand - cached-value calc oracle and XLSX round-trip fidelity on any corpus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
neilcawse
2026-10-11 16:39:30 -04:00
co-authored by Claude Opus 5.5
parent 4dca02c89b
commit f7bc97b378
6 changed files with 1521 additions and 0 deletions
+3
View File
@@ -18,3 +18,6 @@ gridcraft-engine = { workspace = true }
gridcraft-mcp = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
# `check`: validating the packages GridCraft writes (content types, relationships).
zip = { workspace = true }
quick-xml = { workspace = true }
File diff suppressed because it is too large Load Diff
+6
View File
@@ -5,6 +5,7 @@
//! gridcraft-cli convert <in> <out> [--sheet NAME]
//! gridcraft-cli eval <formula> [--in FILE] [--sheet S] [--cell A1] [--json]
//! gridcraft-cli cat <file> [--range A1:D20] [--sheet S] [--formulas] [--csv]
//! gridcraft-cli check <file-or-dir>... [--json] [--tolerance 1e-9] [--max-report N]
//! gridcraft-cli run [--in FILE | --sample NAME] [--cmd 'id={json}']... [--script FILE.jsonl] [--out FILE] [--print RANGE] [--quiet]
//! gridcraft-cli commands [--json] [--search X]
//! gridcraft-cli functions [--json] [--search X] [--category C]
@@ -28,6 +29,8 @@ macro_rules! outln {
}};
}
mod check;
use gridcraft_engine::Session;
use serde_json::{Value, json};
@@ -36,6 +39,8 @@ const USAGE: &str = "usage:
gridcraft-cli convert <in> <out> [--sheet NAME] xlsx/csv/tsv/json/html by extension
gridcraft-cli eval <formula> [--in FILE] [--sheet S] [--cell A1] [--json]
gridcraft-cli cat <file> [--range A1:D20] [--sheet S] [--formulas] [--csv]
gridcraft-cli check <file-or-dir>... [--json] [--tolerance 1e-9] [--max-report N]
cached-value calc oracle + XLSX round-trip fidelity (exit 1 on any difference)
gridcraft-cli run [--in FILE | --sample NAME] [--cmd 'id={json}']... [--script FILE.jsonl]
[--out FILE] [--print RANGE] [--quiet]
gridcraft-cli commands [--json] [--search X]
@@ -71,6 +76,7 @@ fn main() -> ExitCode {
Some("eval") => eval(rest),
Some("cat") => cat(rest),
Some("run") => run(rest),
Some("check") => return check::main(rest),
Some("commands") => commands(rest),
Some("functions") => functions(rest),
Some("mcp") => mcp(rest),
+24
View File
@@ -150,3 +150,27 @@ fn mcp_over_stdio() {
assert_eq!(lines[0]["result"]["serverInfo"]["name"], "gridcraft");
assert!(lines[1]["result"]["content"][0]["text"].as_str().unwrap().contains("42"));
}
#[test]
fn check_exit_codes_and_json() {
let dir = tmpdir("check");
let book = dir.join("calc.xlsx");
let book_s = book.to_str().unwrap();
ok(&["run", "--cmd", r#"cell.set={"cell":"A1","input":"2"}"#, "--cmd", r#"cell.set={"cell":"B1","input":"=A1*3"}"#, "--out", book_s, "--quiet"]);
// GridCraft's own file: its cached values are its own results, and it round-trips.
let o = run(&["check", dir.to_str().unwrap()]);
let out = String::from_utf8_lossy(&o.stdout);
assert_eq!(o.status.code(), Some(0), "{out}");
assert!(out.contains("1 checked: 1 matched, 0 mismatched"), "{out}");
let v: serde_json::Value = serde_json::from_str(&ok(&["check", book_s, "--json"])).unwrap();
assert_eq!(v["schema"], "gridcraft-check/1");
assert_eq!(v["totals"]["matched"], 1);
// A file that can't be opened is reported, not fatal, and fails the check.
let o = run(&["check", book_s, "/no/such/file.xlsx"]);
assert_eq!(o.status.code(), Some(1));
assert!(String::from_utf8_lossy(&o.stdout).contains("1 failed to open"));
// Usage errors exit 2.
assert_eq!(run(&["check"]).status.code(), Some(2));
assert_eq!(run(&["check", book_s, "--tolerance", "x"]).status.code(), Some(2));
assert_eq!(run(&["check", book_s, "--bogus"]).status.code(), Some(2));
}