diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index 186c2e1..8487361 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -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 { + 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_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()), } } diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 68113eb..70021b1 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -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 diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index ef86ae0..e0ee704 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -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 diff --git a/crates/aura-cli/tests/project_load.rs b/crates/aura-cli/tests/project_load.rs index 308c95f..645365c 100644 --- a/crates/aura-cli/tests/project_load.rs +++ b/crates/aura-cli/tests/project_load.rs @@ -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