diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index fc65fdf..2b3907c 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -48,7 +48,7 @@ Node types and their ports: aura graph introspect --vocabulary | --node "#; /// values are the typed `Scalar` form (`{"I64":2}`); `kind` the capitalized /// `ScalarKind` form (`"F64"`) — both the #155 representations. #[derive(Debug, Deserialize)] -#[serde(tag = "op", rename_all = "lowercase")] +#[serde(tag = "op", rename_all = "lowercase", deny_unknown_fields)] enum OpDoc { Source { role: String, kind: ScalarKind }, Input { role: String }, @@ -285,9 +285,14 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project } } else if cmd.folds { // The fold vocabulary binds at graph-declared taps (C27), so the - // graph introspect namespace is its discovery surface (#315). - for f in aura_std::fold_vocabulary() { - println!("{:<7} binds {:<9} out {:<9} — {}", f.id, f.binds, f.out, f.doc); + // graph introspect namespace is its discovery surface (#315). #332: + // this renders the fold-REGISTRY roster — the same roster `aura run + // --tap TAP=FOLD` resolves labels against (aura-runner's layered + // `FoldRegistry`) — not the aura-std `FoldKind` table: labels here + // must be exactly what `--tap` accepts (lowercase), including the + // `record` entry that has no `FoldKind` counterpart. + for (label, doc) in aura_runner::FoldRegistry::core().roster() { + println!("{label:<7} — {doc}"); } } else if let Some(type_id) = cmd.node.as_deref() { match introspect_node(type_id, env) { diff --git a/crates/aura-cli/tests/graph_construct.rs b/crates/aura-cli/tests/graph_construct.rs index 86e34a6..994511f 100644 --- a/crates/aura-cli/tests/graph_construct.rs +++ b/crates/aura-cli/tests/graph_construct.rs @@ -354,6 +354,21 @@ fn graph_build_bad_stdin_content_is_runtime_exit_1() { assert!(stderr.contains("Nope"), "still names the cause: {stderr}"); } +/// Property (#326): an op-list element carrying an unknown/typo'd key (e.g. +/// `params` where the schema expects `bind`) is refused at parse — not +/// silently dropped. Previously the unrecognized key vanished, the field it +/// meant to set kept its default, and the wrong graph built with zero +/// signal. Same content-fault family as `graph_build_bad_stdin_content_is_ +/// runtime_exit_1` (both fire from the same top-level deserialize), so the +/// exit class matches: exit 1, stderr names the offending key. +#[test] +fn graph_build_rejects_an_unknown_op_field() { + let doc = r#"[{"op":"add","type":"Const","params":{"value":{"F64":1.0}}}]"#; + let (stderr, code) = run_code(&["graph", "build"], doc); + assert_eq!(code, Some(1), "unknown op field is a content fault -> exit 1; stderr: {stderr}"); + assert!(stderr.contains("params"), "names the unknown key: {stderr}"); +} + /// Property (exit-code partition, #175 iteration 2): within the SAME `graph /// introspect` subcommand, an invalid `--node ` flag VALUE is a USAGE error (a /// command-line fault the user must fix in argv) — exit 2 — distinct from bad stdin diff --git a/crates/aura-cli/tests/help_self_description.rs b/crates/aura-cli/tests/help_self_description.rs index 5b2ece9..818144f 100644 --- a/crates/aura-cli/tests/help_self_description.rs +++ b/crates/aura-cli/tests/help_self_description.rs @@ -103,18 +103,28 @@ fn graph_introspect_node_head_line_carries_the_meaning() { assert!(head.contains("moving average"), "head line carries the meaning: {out}"); } -/// #315: the fold vocabulary is introspectable from the binary — name, bind -/// rule, output kind, and meaning per fold (it lived only in source comments). +/// #332: `--folds` renders the fold-REGISTRY roster (the same roster +/// `aura run --tap TAP=FOLD` resolves labels against), not the aura-std +/// `FoldKind` table — lowercase, parseable labels, including the `record` +/// entry that has no `FoldKind` counterpart. Previously this surface printed +/// capitalized `FoldKind` ids (`Mean`) and omitted `record`, so the help +/// text's own discovery surface described input `--tap` refused. #[test] -fn graph_introspect_folds_lists_the_fold_vocabulary() { +fn graph_introspect_folds_lists_the_fold_registry_roster() { let (out, err, ok) = run(&["graph", "introspect", "--folds"]); assert!(ok, "folds listing failed: {err}"); let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect(); - assert_eq!(lines.len(), 7, "one line per fold kind: {out}"); - let mean = lines.iter().find(|l| l.starts_with("Mean")).expect("Mean row"); - assert!(mean.contains("f64") && mean.contains("mean"), "Mean row rules + meaning: {mean}"); - let count = lines.iter().find(|l| l.starts_with("Count")).expect("Count row"); - assert!(count.contains("i64") && count.contains("any"), "Count row rules: {count}"); + assert_eq!(lines.len(), 8, "one line per registry entry (record + 7 folds): {out}"); + assert!( + !out.split_whitespace().any(|w| w == "Mean"), + "no capitalized FoldKind id leaks through: {out}" + ); + let record = lines.iter().find(|l| l.starts_with("record ")).expect("record row"); + assert!(record.contains("series"), "record row meaning: {record}"); + let mean = lines.iter().find(|l| l.starts_with("mean ")).expect("mean row"); + assert!(mean.contains("f64") && mean.contains("mean"), "mean row rules + meaning: {mean}"); + let count = lines.iter().find(|l| l.starts_with("count ")).expect("count row"); + assert!(count.contains("i64") && count.contains("any"), "count row rules: {count}"); } /// #315: the metric roster carries each metric's one-line meaning beside its