feat(aura-cli, aura-engine): graph introspect --taps — positive declared-tap discovery

Harvest sweep, batch 3 of 6 (first half).

Composite::declared_taps() walks the blueprint depth-first collecting
(tap name, source wire, column kind), names bare at every depth —
mirroring inline_composite's hoist, where a tap's name IS the flat
graph's load-bearing identifier — and bounds-total over invalid wires
(compile's validate_wiring stays the real gate). The CLI view loads
like --params (FILE gates the authored root name, a content id does
not), prints one 'name  node.field  Kind' row per tap, and renders the
tap-less case as a stderr note with empty stdout, exit 0.

Closes the recovery-only discovery loop: since #333 the only way to
learn a blueprint's declared tap names was provoking the undeclared-tap
refusal roster.

closes #337
This commit is contained in:
2026-07-26 12:10:43 +02:00
parent 521459dd50
commit c39f5e4762
4 changed files with 197 additions and 5 deletions
+48
View File
@@ -832,6 +832,54 @@ fn graph_params_tolerates_content_prefix_on_target() {
);
}
/// Property (#337): `--taps <FILE>` is the positive declared-tap discovery
/// view — before this, the only way to learn a blueprint's declared tap names
/// was provoking #333's undeclared-tap refusal roster. One row per declared
/// tap: name, source wire (`node.field`), column kind, in declaration order.
#[test]
fn graph_introspect_taps_lists_declared_taps_wire_and_kind() {
let dir = temp_cwd("taps-listed");
let ops = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"},
{"op":"connect","from":"slow.value","to":"sub.rhs"},
{"op":"tap","from":"fast.value","as":"fast_ma"},
{"op":"tap","from":"sub.value","as":"spread"},
{"op":"expose","from":"sub.value","as":"bias"}
]"#;
let (build_out, build_err, build_code) = run_in_stdin(&dir, &["graph", "build"], ops);
assert_eq!(build_code, Some(0), "graph build: {build_out} {build_err}");
let bp = dir.join("tapped.json");
std::fs::write(&bp, &build_out).expect("write built blueprint");
let (stdout, stderr, code) =
run_in(&dir, &["graph", "introspect", "--taps", bp.to_str().unwrap()]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(
stdout, "fast_ma fast.value F64\nspread sub.value F64\n",
"one row per declared tap: name, source wire, column kind"
);
}
/// Property (#337): a blueprint with no declared taps prints nothing to
/// stdout (nothing was declared to list) plus a one-line stderr note — a
/// listing, not a fault, exit 0.
#[test]
fn graph_introspect_taps_reports_no_taps_on_stderr_when_none_declared() {
let dir = temp_cwd("taps-empty");
let bp = fixture("r_sma_open.json");
let (stdout, stderr, code) = run_in(&dir, &["graph", "introspect", "--taps", &bp]);
assert_eq!(code, Some(0), "a tap-less blueprint is a listing, not a fault: {stderr}");
assert_eq!(stdout, "", "no declared taps to list: {stdout}");
assert!(
stderr.contains("declares no taps"),
"stderr carries the one-line note: {stderr}"
);
}
/// Property (#196, negative): `--params <ID>` with a well-shaped 64-hex id
/// that was never registered refuses with a store-miss message, not a panic
/// or a silent empty namespace — the registry lookup branch of