feat(cli): house-style loader-error prose + missing-project hint on the run path
aura run leaked the raw LoadError Debug form (UnknownNodeType("..."))
while graph build already phrased the same fault house-style (#162
convention: engine error types are Display-free, the CLI presents them).
Route the run path through the shared blueprint_load_prose presenter (now
pub(crate), env-agnostic) so both surfaces phrase a LoadError identically
and neither leaks the Debug token (#184). Add unresolved_namespace_hint:
when the unresolved id looks project-namespaced (::) and no project was
loaded (env.provenance() is None), append a hint pointing at the missing
Aura.toml — the newcomer running the host from the wrong directory now
gets an orienting diagnosis instead of a bare type error (#185). The hint
is applied on both the run and graph-build paths. RED-first: the two
existing run-path pins were flipped to assert the prose + absence of the
Debug token before implementing; suite green (1043/0).
closes #184
closes #185
This commit is contained in:
@@ -314,8 +314,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &crate::project::Env)
|
||||
|
||||
/// Phrase a blueprint-document `LoadError` as prose (Debug-leak-free; the
|
||||
/// engine error types are `Display`-free by convention — this is the CLI's
|
||||
/// presentation layer, like `format_op_error`).
|
||||
fn blueprint_load_prose(e: &LoadError) -> String {
|
||||
/// presentation layer, like `format_op_error`). `pub(crate)`: shared by the
|
||||
/// `graph build` path (below) and `aura run`'s loaded-blueprint branch
|
||||
/// (main.rs) — env-agnostic on purpose, so both callers get identical wording
|
||||
/// (#184).
|
||||
pub(crate) fn blueprint_load_prose(e: &LoadError) -> String {
|
||||
match e {
|
||||
LoadError::Json(err) => format!("blueprint document is not valid JSON: {err}"),
|
||||
LoadError::UnsupportedVersion { found, supported } => {
|
||||
@@ -325,6 +328,22 @@ fn blueprint_load_prose(e: &LoadError) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// (#185) When an unresolved type id LOOKS project-namespaced (`::`) but no
|
||||
/// project was loaded for this invocation (`env.provenance()` is `None`),
|
||||
/// append a hint pointing at the missing `Aura.toml` — the likeliest cause is
|
||||
/// "ran outside the project directory", not "genuinely unknown std id". Bare
|
||||
/// std-vocabulary ids (no `::`) and invocations that DO have a project loaded
|
||||
/// get no hint.
|
||||
pub(crate) fn unresolved_namespace_hint(e: &LoadError, env: &crate::project::Env) -> Option<String> {
|
||||
match e {
|
||||
LoadError::UnknownNodeType(t) if t.contains("::") && env.provenance().is_none() => Some(
|
||||
"type id looks project-namespaced but no Aura.toml was found — run inside a project directory"
|
||||
.to_string(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// #196: build a `Composite` from a document that is EITHER a #155 blueprint
|
||||
/// envelope (a JSON object: `format_version` + `blueprint`) OR a construction
|
||||
/// op-list (a JSON array) — shape-discriminated on the top-level JSON type,
|
||||
@@ -334,9 +353,14 @@ fn composite_from_any(text: &str, env: &crate::project::Env) -> Result<Composite
|
||||
serde_json::from_str(text).map_err(|e| format!("invalid document: {e}"))?;
|
||||
match value {
|
||||
serde_json::Value::Array(_) => composite_from_str(text, env),
|
||||
serde_json::Value::Object(_) => {
|
||||
blueprint_from_json(text, &|t| env.resolve(t)).map_err(|e| blueprint_load_prose(&e))
|
||||
}
|
||||
serde_json::Value::Object(_) => blueprint_from_json(text, &|t| env.resolve(t)).map_err(|e| {
|
||||
let mut msg = blueprint_load_prose(&e);
|
||||
if let Some(hint) = unresolved_namespace_hint(&e, env) {
|
||||
msg.push_str(" — ");
|
||||
msg.push_str(&hint);
|
||||
}
|
||||
msg
|
||||
}),
|
||||
_ => Err("document is neither a blueprint envelope (object) nor an op-list (array)".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4292,7 +4292,12 @@ fn dispatch_run(a: RunCmd, env: &project::Env) {
|
||||
std::process::exit(2);
|
||||
});
|
||||
let signal = blueprint_from_json(&doc, &|t| env.resolve(t)).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {path}: {e:?}");
|
||||
let mut msg = graph_construct::blueprint_load_prose(&e);
|
||||
if let Some(hint) = graph_construct::unresolved_namespace_hint(&e, env) {
|
||||
msg.push_str(" — ");
|
||||
msg.push_str(&hint);
|
||||
}
|
||||
eprintln!("aura: {path}: {msg}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
// Refuse an open (free-knob) blueprint at the dispatch boundary, mirroring
|
||||
|
||||
@@ -337,7 +337,9 @@ fn aura_run_loads_and_runs_a_blueprint_file() {
|
||||
}
|
||||
|
||||
/// A blueprint referencing a node type outside the closed vocabulary fails clean
|
||||
/// at load (the #160 closed-set guard at the data-plane face; invariant 9).
|
||||
/// at load (the #160 closed-set guard at the data-plane face; invariant 9), and
|
||||
/// (#184) the failure is phrased as house-style prose — matching the `graph
|
||||
/// build` presenter's wording — never the raw `UnknownNodeType` Debug leak.
|
||||
#[test]
|
||||
fn aura_run_rejects_an_unknown_node_blueprint() {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
@@ -346,7 +348,14 @@ fn aura_run_rejects_an_unknown_node_blueprint() {
|
||||
.expect("spawn aura run unknown");
|
||||
assert_ne!(out.status.code(), Some(0), "an unknown node type must fail the run");
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8");
|
||||
assert!(stderr.contains("UnknownNodeType"), "names the cause: {stderr}");
|
||||
assert!(
|
||||
stderr.contains(r#"unknown node type "Nope""#),
|
||||
"house-style prose names the type: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("UnknownNodeType"),
|
||||
"does not leak the Debug variant name: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#175, refuse-don't-guess): built-in-only flags on `aura run
|
||||
|
||||
@@ -79,6 +79,12 @@ fn introspect_vocabulary_lists_project_types() {
|
||||
assert!(text.contains("SMA"), "std ids still listed");
|
||||
}
|
||||
|
||||
/// (#184/#185) Outside any project, `demo::Identity` resolves nowhere: the base
|
||||
/// failure is house-style prose (never the raw `UnknownNodeType` Debug leak),
|
||||
/// and — because the unresolved id LOOKS project-namespaced (`::`) while no
|
||||
/// `Aura.toml` was found up from cwd — the message carries a hint pointing at
|
||||
/// the missing project, distinguishing "no project" from "genuinely unknown
|
||||
/// std id" (the bare-id case in `cli_run.rs` gets no such hint).
|
||||
#[test]
|
||||
fn outside_a_project_the_demo_blueprint_is_unknown() {
|
||||
// cwd = the aura-cli crate dir: no Aura.toml anywhere up the tree.
|
||||
@@ -87,7 +93,15 @@ fn outside_a_project_the_demo_blueprint_is_unknown() {
|
||||
let out = aura(&["run", bp.to_str().unwrap()], cwd);
|
||||
assert!(!out.status.success());
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("UnknownNodeType"), "stderr: {err}");
|
||||
assert!(
|
||||
err.contains(r#"unknown node type "demo::Identity""#),
|
||||
"house-style prose names the type: {err}"
|
||||
);
|
||||
assert!(!err.contains("UnknownNodeType"), "does not leak the Debug variant name: {err}");
|
||||
assert!(
|
||||
err.contains("Aura.toml"),
|
||||
"hints that no project was found for the namespaced id: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `Env::runs_root()` resolves `Aura.toml`'s `[paths] runs = "runs"` against
|
||||
|
||||
Reference in New Issue
Block a user