forked from 1b-agent/fieldcraft-upstream
Schema: one phrasing of a problem, by key or by label; Required by field kind
Problem::phrase serves both for_agents and for_form. A required name says "enter a name", a path "enter a path", anything else "required" (no more "Autosave: enter an autosave"). New kinds for what commands check (positive, range, list, name, path, invalid), and refine() to look a command's problem up in its form. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
70c5c9b26f
commit
de8b7b6321
2 files changed
+98
-20
No files matched your search
+97
-19
@@ -593,6 +593,31 @@ pub enum ProblemKind {
|
||||
TooManyRows {
|
||||
max: usize,
|
||||
},
|
||||
/// Not a number greater than 0.
|
||||
NotPositive,
|
||||
/// Not a number in `min..=max`.
|
||||
NotInRange {
|
||||
min: f64,
|
||||
max: f64,
|
||||
},
|
||||
/// Not a list of at most `max` of `what` ("[x, y, z]", "labels").
|
||||
NotAList {
|
||||
max: usize,
|
||||
what: &'static str,
|
||||
},
|
||||
NotAName,
|
||||
NotAPath,
|
||||
/// Wrong for a reason the schema does not describe: what it must be or do instead
|
||||
/// ("names the region of 2D elements; a solid block fills its body").
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl PartialEq for Problem {
|
||||
/// Problems are equal when they are about the same path and are the same problem (the field
|
||||
/// is where the path leads).
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path && self.kind == other.kind
|
||||
}
|
||||
}
|
||||
|
||||
impl Problem {
|
||||
@@ -600,6 +625,20 @@ impl Problem {
|
||||
Problem { path, field: Some(field), kind }
|
||||
}
|
||||
|
||||
/// A problem with the value at `path`, its field not looked up yet ([`Problem::in_form`]).
|
||||
pub fn at(path: impl Into<String>, kind: ProblemKind) -> Self {
|
||||
Problem { path: path.into(), field: None, kind }
|
||||
}
|
||||
|
||||
/// The problem with its field looked up in `fields` (when it has none yet and the path is
|
||||
/// there).
|
||||
pub fn in_form(mut self, fields: &'static [Field]) -> Self {
|
||||
if self.field.is_none() && !self.path.is_empty() {
|
||||
self.field = field_at(fields, &self.path);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// What the value must be, without the field's name ("required", "at least 0").
|
||||
pub fn expected(&self) -> String {
|
||||
let f = self.field;
|
||||
@@ -626,37 +665,76 @@ impl Problem {
|
||||
_ => "an object with a known type".into(),
|
||||
},
|
||||
ProblemKind::TooManyRows { max } => format!("a list of at most {max} rows"),
|
||||
ProblemKind::NotPositive => "a positive number".into(),
|
||||
ProblemKind::NotInRange { min, max } => format!("a number in {min}..={max}"),
|
||||
ProblemKind::NotAList { max, what } => format!("a list of at most {max} {what}"),
|
||||
ProblemKind::NotAName => "a name".into(),
|
||||
ProblemKind::NotAPath => "a file path".into(),
|
||||
ProblemKind::Invalid(why) => why.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The problem as agents read it, naming the field by its JSON path:
|
||||
/// "`elastic/young` is required", "`bands` must be a whole number in 2..=24".
|
||||
pub fn for_agents(&self) -> String {
|
||||
let what = if self.path.is_empty() { "the parameters".to_string() } else { format!("`{}`", self.path) };
|
||||
match self.kind {
|
||||
ProblemKind::Required => format!("{what} is required"),
|
||||
_ => format!("{what} must be {}", self.expected()),
|
||||
}
|
||||
self.phrase(Naming::Key)
|
||||
}
|
||||
|
||||
/// The problem as a form shows it, naming the field by its label and saying what to do:
|
||||
/// "Material: choose a material", "Young's modulus: must be at least 0".
|
||||
pub fn for_form(&self) -> String {
|
||||
let Some(f) = self.field else { return format!("The parameters must be {}", self.expected()) };
|
||||
let required = self.kind == ProblemKind::Required;
|
||||
let text = match f.kind {
|
||||
Kind::Ref(c) if required => format!("choose {}", article(c.label())),
|
||||
Kind::RefList(c) if required => format!("choose at least one {}", c.label()),
|
||||
Kind::Either(options) => format!("choose {}", options.iter().map(|o| article(&lower(o.label))).collect::<Vec<_>>().join(" or ")),
|
||||
Kind::Choice(options) => format!("choose one of {}", options.iter().map(|(_, l)| *l).collect::<Vec<_>>().join(", ")),
|
||||
Kind::Variants { .. } => format!("choose {}", article(&lower(f.label))),
|
||||
Kind::Path { .. } if required => "choose a file".into(),
|
||||
Kind::Text if required => format!("enter {}", article(&lower(f.label))),
|
||||
_ if required => "enter a value".into(),
|
||||
_ => format!("must be {}", self.expected()),
|
||||
};
|
||||
format!("{}: {text}", f.label)
|
||||
self.phrase(Naming::Label)
|
||||
}
|
||||
|
||||
/// The one phrasing of a problem, naming the field by its key or by its label.
|
||||
pub fn phrase(&self, naming: Naming) -> String {
|
||||
let by_label = naming == Naming::Label;
|
||||
let field = self.field.filter(|_| by_label);
|
||||
let subject = match field {
|
||||
Some(f) => format!("{}:", f.label),
|
||||
None if self.path.is_empty() => if by_label { "The parameters" } else { "the parameters" }.to_string(),
|
||||
None => format!("`{}`", self.path),
|
||||
};
|
||||
let required = self.kind == ProblemKind::Required;
|
||||
let what = match (field.map(|f| f.kind), &self.kind) {
|
||||
(_, ProblemKind::Invalid(why)) => why.clone(),
|
||||
(None, _) if required => "is required".into(),
|
||||
(None, _) => format!("must be {}", self.expected()),
|
||||
(Some(Kind::Ref(c)), _) if required => format!("choose {}", article(c.label())),
|
||||
(Some(Kind::RefList(c)), _) if required => format!("choose at least one {}", c.label()),
|
||||
(Some(Kind::Either(options)), _) => {
|
||||
format!("choose {}", options.iter().map(|o| article(&lower(o.label))).collect::<Vec<_>>().join(" or "))
|
||||
}
|
||||
(Some(Kind::Choice(options)), _) => format!("choose one of {}", options.iter().map(|(_, l)| *l).collect::<Vec<_>>().join(", ")),
|
||||
(Some(Kind::Variants { .. }), _) => format!("choose {}", article(&lower(field.map_or("", |f| f.label)))),
|
||||
(Some(Kind::Path { .. }), _) if required => "enter a path".into(),
|
||||
(Some(Kind::Text), _) if required && field.is_some_and(|f| f.key == "name") => "enter a name".into(),
|
||||
(Some(_), _) if required => "required".into(),
|
||||
(Some(_), _) => format!("must be {}", self.expected()),
|
||||
};
|
||||
format!("{subject} {what}")
|
||||
}
|
||||
}
|
||||
|
||||
/// How a problem names its field: by its JSON path (agents) or by its label (a form).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Naming {
|
||||
Key,
|
||||
Label,
|
||||
}
|
||||
|
||||
/// A command's problem with `params`, made as precise as its form `fields` can: when the
|
||||
/// command names a group (`plastic`) and the form finds a problem with a field inside it
|
||||
/// (`plastic/yield_stress` left out), that problem; otherwise the command's own, with its field
|
||||
/// looked up.
|
||||
pub fn refine(fields: &'static [Field], params: &Value, problem: Problem) -> Problem {
|
||||
let inside = |p: &Problem| p.path.strip_prefix(problem.path.as_str()).is_some_and(|r| r.starts_with('/'));
|
||||
if !problem.path.is_empty()
|
||||
&& let Some(precise) = problems(fields, params).into_iter().find(inside)
|
||||
{
|
||||
return precise;
|
||||
}
|
||||
problem.in_form(fields)
|
||||
}
|
||||
|
||||
/// "a material", "an element".
|
||||
|
||||
@@ -791,7 +791,7 @@ mod tests {
|
||||
assert_eq!(m.first(), Some(&("name".to_string(), "Material: choose a material".to_string())), "{m:?}");
|
||||
let load = fieldcraft_engine::find_command("model.load.create").and_then(|c| c.form).unwrap();
|
||||
let m: Vec<String> = messages(load, &json!({"type": "force"}).as_object().cloned().unwrap_or_default()).into_iter().map(|(_, m)| m).collect();
|
||||
assert_eq!(m, ["Name: enter a name", "Target: choose a set or a reference point", "Force: enter a value"]);
|
||||
assert_eq!(m, ["Name: enter a name", "Target: choose a set or a reference point", "Force: required"]);
|
||||
}
|
||||
|
||||
/// A problem waits until its field is edited or OK is tried; a refused parameter shows at once.
|
||||
|
||||
Reference in new issue
Block a user