Engine: EngineError::Param carries the schema Problem at its field's path

The ~40 parameter checks give a ProblemKind instead of text; Session::execute looks the problem
up in the command's form (a group's missing field becomes that field's own path). Replies still
name keys; forms phrase the same problem by label. A test refuses every field of every form and
checks each refusal names a field of that form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 22:42:19 -04:00
1 parent de8b7b6321
commit 65fb1112ab
16 files changed
+172 -87

No files matched your search

+5 -4
View File
@@ -2,6 +2,7 @@
//! animation) and reaction sums. The display is session state ([`ResultsView`]): these
//! commands are not edits, so they are not journalled or undone.
use crate::schema::ProblemKind;
use serde_json::{Value, json};
use super::{opt, xyz};
@@ -173,7 +174,7 @@ fn deformed(s: &mut Session, p: &Value) -> Result<Value> {
fn contour(s: &mut Session, p: &Value) -> Result<Value> {
let step = step_for(s, p, Want::Any)?;
let name = opt::<String>(p, "field")?.ok_or_else(|| EngineError::param("field", "is required (a field id, or \"none\")"))?;
let name = opt::<String>(p, "field")?.ok_or_else(|| EngineError::param("field", ProblemKind::Required))?;
let field = match name.as_str() {
"none" => None,
id => Some(Field::parse(id).ok_or_else(|| {
@@ -255,7 +256,7 @@ fn frame(s: &mut Session, p: &Value) -> Result<Value> {
(None, Some(t)) if t.is_finite() => {
times.iter().enumerate().min_by(|a, b| (a.1 - t).abs().total_cmp(&(b.1 - t).abs())).map_or(last, |(i, _)| i)
}
(None, Some(_)) => return Err(EngineError::param("time", "must be a number")),
(None, Some(_)) => return Err(EngineError::param("time", ProblemKind::NotANumber)),
(None, None) => last,
};
let v = view_of(s, &step)?;
@@ -272,7 +273,7 @@ fn reactions(s: &mut Session, p: &Value) -> Result<Value> {
let step = step_for(s, p, Want::Any)?;
let about = if p.get("about").is_some() { xyz(p, "about")? } else { [0.0; 3] };
if !about.iter().all(|c| c.is_finite()) {
return Err(EngineError::param("about", "must be a finite [x, y, z]"));
return Err(EngineError::param("about", ProblemKind::NotAVector { len: 3 }));
}
let Some((result, current)) = s.latest_result(&step) else { return Err(EngineError::Params(solved_list(s))) };
let Kind::Static(r) = result.kind() else {
@@ -287,7 +288,7 @@ fn reactions(s: &mut Session, p: &Value) -> Result<Value> {
.filter(|a| a.len() <= MAX_SETS)
.ok_or_else(|| EngineError::Params(format!("`sets` must be a list of at most {MAX_SETS} set names")))?;
list.iter()
.map(|n| n.as_str().map(str::to_string).ok_or_else(|| EngineError::param("sets", "must be set names")))
.map(|n| n.as_str().map(str::to_string).ok_or_else(|| EngineError::param("sets", ProblemKind::NotNames)))
.collect::<Result<_>>()?
}
None => {