feat(cli): the tier-aware namespace hint fires on the graph-build path too

The op-script error seam (OpError::UnknownNodeType) now consults the
same tier-hint logic as the blueprint load path: outside a project the
hint names the missing Aura.toml, inside a data-only project it names
the missing node crate and the attach verb. Hint texts shared, load-path
behaviour and the op-error prose byte-stable, exit codes unchanged.

closes #244
This commit is contained in:
2026-07-12 20:12:55 +02:00
parent 72bfdf79d1
commit 0ad8fc6ad2
+41 -18
View File
@@ -133,7 +133,18 @@ fn composite_from_str(doc: &str, env: &crate::project::Env) -> Result<Composite,
let labels: Vec<&'static str> = docs.iter().map(OpDoc::kind_label).collect();
let ops: Vec<Op> = docs.into_iter().map(Op::from).collect();
replay("graph", ops, &|t| env.resolve(t)).map_err(|(idx, err)| {
let cause = format_op_error(&err);
let mut cause = format_op_error(&err);
// #244: the op-script path reports an unresolvable namespaced type as
// `OpError::UnknownNodeType`, a different error family than the
// blueprint LOAD path's `LoadError::UnknownNodeType` — so without this,
// `unresolved_namespace_hint` is never consulted here. Same tier hint,
// appended the same way.
if let OpError::UnknownNodeType(t) = &err
&& let Some(hint) = tier_hint_for_type_id(t, env)
{
cause.push_str("");
cause.push_str(&hint);
}
match labels.get(idx) {
Some(kind) => format!("op {idx} ({kind}): {cause}"),
None => format!("finalize: {cause}"),
@@ -376,25 +387,37 @@ fn gang_fault_prose(e: &CompileError) -> String {
}
}
/// (#185/#241) When an unresolved type id LOOKS project-namespaced (`::`),
/// the likeliest causes are environmental, tiered: outside any project the
/// hint points at the missing `Aura.toml`; inside a data-only project it
/// points at the missing node crate. Bare std ids (no `::`) and invocations
/// with a loaded node crate get no hint (the vocabulary error carries the
/// message).
/// (#185/#241/#244) When an unresolved type id LOOKS project-namespaced
/// (`::`), the likeliest causes are environmental, tiered: outside any
/// project the hint points at the missing `Aura.toml`; inside a data-only
/// project it points at the missing node crate. Bare std ids (no `::`) and
/// invocations with a loaded node crate get no hint (the vocabulary error
/// carries the message). Shared core: both the blueprint LOAD path
/// (`unresolved_namespace_hint`, over `LoadError`) and the op-script `graph
/// build` path (`composite_from_str`, over `OpError`) call this same helper,
/// so the tier texts cannot drift between the two error families.
fn tier_hint_for_type_id(type_id: &str, env: &crate::project::Env) -> Option<String> {
if !type_id.contains("::") {
return None;
}
match env.provenance() {
None => Some(
"type id looks project-namespaced but no Aura.toml was found — run inside a project directory"
.to_string(),
),
Some(p) if p.namespace.is_none() => Some(
"type id looks project-namespaced but this project binds no node crate — attach one with `aura nodes new <name>`"
.to_string(),
),
Some(_) => None,
}
}
/// (#185/#241) See [`tier_hint_for_type_id`] for the tier rules; this is the
/// blueprint LOAD path's entry point over `LoadError`.
pub(crate) fn unresolved_namespace_hint(e: &LoadError, env: &crate::project::Env) -> Option<String> {
match e {
LoadError::UnknownNodeType(t) if t.contains("::") => match env.provenance() {
None => Some(
"type id looks project-namespaced but no Aura.toml was found — run inside a project directory"
.to_string(),
),
Some(p) if p.namespace.is_none() => Some(
"type id looks project-namespaced but this project binds no node crate — attach one with `aura nodes new <name>`"
.to_string(),
),
Some(_) => None,
},
LoadError::UnknownNodeType(t) => tier_hint_for_type_id(t, env),
_ => None,
}
}