feat(aura-cli): use op on the data plane — register --name, resolution echo, discovery
The op-script gains its tenth verb end-to-end: OpDoc::Use carries a tagged ref
({"content_id": id-or-unique-prefix} | {"name": label}); DTO conversion resolves
it (label sidecar / #302 prefix semantics / verbatim id), fetches, deserializes
eagerly (a vocab/parse failure fails fast naming the id prefix — review
finding, no swallowed .ok()), re-walks the C29 doc gate over the fetched
composite (a doc-less store entry cannot enter a NEW composition; existing
reads stay valid — no retroactive invalidation), echoes every resolution as
the existing benign marker (aura: note: use "<instance>": <ref> -> <id>,
stderr; stdout stays clean payload), and hands the session a pure cached
lookup closure.
graph register <file> --name <label> records the label and echoes repoints
(label "x" -> <new> (was <old>)); graph introspect --registered lists
label / 12-char id prefix / root doc line — the discovery surface for what
use can reference. Refusals are by-identifier, name their rule, and enumerate
the label set (empty store reads as prose); all op-list content faults are
runtime exit 1 per the pinned #175 attribution, argv misuse stays exit 2.
Ten new/extended e2e tests cover label round-trip + repoint, echo discipline,
unknown-label enumeration, the C29 refusal shape, end-to-end sweep axes
through a spliced instance (graph.<instance>.<node>.<param>), the open-pattern
build flow, and the standalone-run bootstrap refusal (asserts the existing
Debug-form fault; an index->name rendering at the run boundary is recorded
residue, not silently claimed). OP_REFERENCE and its help pin move 9 -> 10.
Empirical note for the record: aura run's bias arm re-roots the loaded signal
via wrap_r, so the standalone-run refusal for an open pattern is exercised on
the bare-tap measurement path; the bias path surfaces openness as a role-
binding refusal instead (pre-existing shape, untouched).
closes #317
This commit is contained in:
@@ -466,6 +466,24 @@ fn run_in(dir: &std::path::Path, args: &[&str]) -> (String, String, Option<i32>)
|
||||
)
|
||||
}
|
||||
|
||||
/// Run `aura <args>` in `dir` with `stdin_doc` piped in; return (stderr, exit
|
||||
/// code) — the `run_code`/`run_in` cross, for a `use`-seam fault that needs
|
||||
/// BOTH a project cwd (the label sidecar lives under `<cwd>/runs/`) and a
|
||||
/// piped op-list.
|
||||
fn run_code_in(dir: &std::path::Path, args: &[&str], stdin_doc: &str) -> (String, Option<i32>) {
|
||||
let mut child = Command::new(BIN)
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("spawn aura");
|
||||
child.stdin.take().unwrap().write_all(stdin_doc.as_bytes()).unwrap();
|
||||
let out = child.wait_with_output().expect("wait aura");
|
||||
(String::from_utf8_lossy(&out.stderr).into_owned(), out.status.code())
|
||||
}
|
||||
|
||||
/// The absolute path of a blueprint fixture shipped with this test crate.
|
||||
fn fixture(name: &str) -> String {
|
||||
format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"))
|
||||
@@ -1551,3 +1569,300 @@ fn tap_authored_via_op_script_runs_and_persists_the_series() {
|
||||
assert!((g - e).abs() < 1e-9, "row {i}: got {g}, expected {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- `use` / label sidecar / open patterns (#317) ------------------------
|
||||
|
||||
/// A small reusable pattern: an open `x` input role feeds an `SMA`, whose
|
||||
/// value re-exports as `out` — the fixture every `use`-seam e2e test below
|
||||
/// registers under a label. `sma.length` stays UNBOUND: the pattern's own
|
||||
/// open param, so a consumer's `--list-axes` sees the bare (unbound) form.
|
||||
const OPEN_PATTERN_DOC: &str = r#"[
|
||||
{"op":"doc","text":"a reusable smoothing pattern"},
|
||||
{"op":"input","role":"x"},
|
||||
{"op":"add","type":"SMA","name":"sma"},
|
||||
{"op":"feed","role":"x","into":["sma.series"]},
|
||||
{"op":"expose","from":"sma.value","as":"out"}
|
||||
]"#;
|
||||
|
||||
/// Build `doc` via `aura graph build` in `dir` and write the resulting
|
||||
/// envelope to `dir/<stem>.bp.json`, returning its path.
|
||||
fn build_envelope_in(dir: &std::path::Path, doc: &str, stem: &str) -> std::path::PathBuf {
|
||||
let (bytes, _e, ok) = run(&["graph", "build"], doc);
|
||||
assert!(ok, "the fixture document builds: {doc}");
|
||||
let path = dir.join(format!("{stem}.bp.json"));
|
||||
std::fs::write(&path, &bytes).expect("write built envelope");
|
||||
path
|
||||
}
|
||||
|
||||
/// `graph register --name` (#317): the label sidecar echo on first
|
||||
/// registration, and the repoint echo (`(was <old-id>)`) on a second
|
||||
/// registration of DIFFERENT bytes under the SAME label.
|
||||
#[test]
|
||||
fn graph_register_name_labels_and_repoints() {
|
||||
let dir = temp_cwd("register-name");
|
||||
let bp = build_envelope_in(&dir, OPEN_PATTERN_DOC, "pattern");
|
||||
let (out1, err1, code1) =
|
||||
run_in(&dir, &["graph", "register", bp.to_str().unwrap(), "--name", "smooth"]);
|
||||
assert_eq!(code1, Some(0), "first register --name: {out1} {err1}");
|
||||
let id1 = registered_id(&out1);
|
||||
assert!(
|
||||
out1.lines().any(|l| l == format!("label \"smooth\" -> {id1}")),
|
||||
"the plain label echo (no repoint on a fresh label): {out1}"
|
||||
);
|
||||
|
||||
// Different bytes (a bound sma.length), same label -> a repoint.
|
||||
let doc2 = OPEN_PATTERN_DOC.replace(
|
||||
r#"{"op":"add","type":"SMA","name":"sma"}"#,
|
||||
r#"{"op":"add","type":"SMA","name":"sma","bind":{"length":{"I64":7}}}"#,
|
||||
);
|
||||
let bp2 = build_envelope_in(&dir, &doc2, "pattern2");
|
||||
let (out2, err2, code2) =
|
||||
run_in(&dir, &["graph", "register", bp2.to_str().unwrap(), "--name", "smooth"]);
|
||||
assert_eq!(code2, Some(0), "second register --name: {out2} {err2}");
|
||||
let id2 = registered_id(&out2);
|
||||
assert_ne!(id1, id2, "the two registrations are distinct content");
|
||||
assert!(
|
||||
out2.lines().any(|l| l == format!("label \"smooth\" -> {id2} (was {id1})")),
|
||||
"the repoint echo names both ids: {out2}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `use` resolves a registry label (#317): the resolution echo lands on
|
||||
/// STDERR (`aura: note: use "<instance>": <label> -> <full id>`), stdout
|
||||
/// stays clean payload — the existing e2e convention this cycle extends.
|
||||
#[test]
|
||||
fn graph_build_use_resolves_a_label_and_echoes_the_id() {
|
||||
let dir = temp_cwd("use-resolves-label");
|
||||
let bp = build_envelope_in(&dir, OPEN_PATTERN_DOC, "pattern");
|
||||
let (reg_out, reg_err, reg_code) =
|
||||
run_in(&dir, &["graph", "register", bp.to_str().unwrap(), "--name", "smooth"]);
|
||||
assert_eq!(reg_code, Some(0), "register --name: {reg_out} {reg_err}");
|
||||
let id = registered_id(®_out);
|
||||
|
||||
let consumer = r#"[
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"use","ref":{"name":"smooth"},"name":"trend"},
|
||||
{"op":"feed","role":"price","into":["trend.x"]},
|
||||
{"op":"expose","from":"trend.out","as":"bias"}
|
||||
]"#;
|
||||
let mut child = Command::new(BIN)
|
||||
.args(["graph", "build"])
|
||||
.current_dir(&dir)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("spawn aura graph build");
|
||||
child.stdin.take().unwrap().write_all(consumer.as_bytes()).unwrap();
|
||||
let out = child.wait_with_output().expect("wait aura graph build");
|
||||
assert!(out.status.success(), "build succeeds: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert_eq!(
|
||||
stderr.trim(),
|
||||
format!("aura: note: use \"trend\": smooth -> {id}"),
|
||||
"the resolution echo names the instance, the label, and the full id: {stderr}"
|
||||
);
|
||||
assert!(stdout.starts_with('{'), "stdout stays clean blueprint payload: {stdout}");
|
||||
assert!(!stdout.contains("aura: note:"), "the note never leaks onto stdout: {stdout}");
|
||||
assert!(
|
||||
stdout.contains(&format!("\"doc\":\"{}\"", "a reusable smoothing pattern")),
|
||||
"the spliced instance's own doc survives inline: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
/// An unknown `use` label enumerates every registered label (C29's "name the
|
||||
/// closed set" idiom) and refuses at exit 1 (op-list content fault, #175).
|
||||
#[test]
|
||||
fn graph_build_use_unknown_label_enumerates_labels_exit_1() {
|
||||
let dir = temp_cwd("use-unknown-label");
|
||||
let bp = build_envelope_in(&dir, OPEN_PATTERN_DOC, "pattern");
|
||||
let (reg_out, reg_err, reg_code) =
|
||||
run_in(&dir, &["graph", "register", bp.to_str().unwrap(), "--name", "smooth"]);
|
||||
assert_eq!(reg_code, Some(0), "register --name: {reg_out} {reg_err}");
|
||||
|
||||
let consumer = r#"[
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"use","ref":{"name":"nope"},"name":"trend"},
|
||||
{"op":"feed","role":"price","into":["trend.x"]},
|
||||
{"op":"expose","from":"trend.out","as":"bias"}
|
||||
]"#;
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args(["graph", "build"])
|
||||
.current_dir(&dir)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
use std::io::Write;
|
||||
child.stdin.take().unwrap().write_all(consumer.as_bytes())?;
|
||||
child.wait_with_output()
|
||||
})
|
||||
.expect("run aura graph build");
|
||||
assert_eq!(out.status.code(), Some(1), "unknown label is a content fault -> exit 1");
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stdout.is_empty(), "no blueprint emitted on refusal: {stdout}");
|
||||
assert!(stderr.contains("op 1 (use \"trend\")"), "names the op by index and instance: {stderr}");
|
||||
assert!(stderr.contains("no registered blueprint labeled \"nope\""), "names the miss: {stderr}");
|
||||
assert!(stderr.contains("registered labels: smooth"), "enumerates the closed set: {stderr}");
|
||||
}
|
||||
|
||||
/// The empty-store form of the unknown-label refusal: no labels to
|
||||
/// enumerate reads as prose, not a bare empty list.
|
||||
#[test]
|
||||
fn graph_build_use_unknown_label_empty_store_reads_as_prose() {
|
||||
let dir = temp_cwd("use-unknown-label-empty-store");
|
||||
let consumer = r#"[
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"use","ref":{"name":"nope"},"name":"trend"},
|
||||
{"op":"feed","role":"price","into":["trend.x"]},
|
||||
{"op":"expose","from":"trend.out","as":"bias"}
|
||||
]"#;
|
||||
let (stderr, code) = run_code_in(&dir, &["graph", "build"], consumer);
|
||||
assert_eq!(code, Some(1), "still a content fault -> exit 1");
|
||||
assert!(
|
||||
stderr.contains("no registered blueprint labeled \"nope\" — no labels registered"),
|
||||
"the empty-store form reads as prose: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
/// C29 at the `use` seam (#317): a doc-less registered blueprint refuses
|
||||
/// entering a NEW composition, naming the ref's id-prefix, the failing
|
||||
/// (here root, "nested" from the consumer's view) composite, and the rule —
|
||||
/// exit 1. The doc-less entry reaches the store via the RAW file write
|
||||
/// existing pre-C29 fixtures use (`register_refuses_undescribed_composites_
|
||||
/// end_to_end`'s technique): `graph register` itself always C29-gates, so a
|
||||
/// doc-less entry can only reach the store by writing the file directly.
|
||||
#[test]
|
||||
fn graph_build_use_docless_source_refuses_with_the_c29_shape_exit_1() {
|
||||
let dir = temp_cwd("use-docless-source");
|
||||
// A doc-less pattern: same shape as OPEN_PATTERN_DOC minus the `doc` op.
|
||||
let docless = r#"[
|
||||
{"op":"input","role":"x"},
|
||||
{"op":"add","type":"SMA","name":"sma","bind":{"length":{"I64":3}}},
|
||||
{"op":"feed","role":"x","into":["sma.series"]},
|
||||
{"op":"expose","from":"sma.value","as":"out"}
|
||||
]"#;
|
||||
let (envelope, _e, built) = run(&["graph", "build"], docless);
|
||||
assert!(built, "the doc-less pattern still builds (C29 gates register/use, not build)");
|
||||
let (id_out, _e2, id_ok) = run(&["graph", "introspect", "--content-id"], docless);
|
||||
assert!(id_ok, "content-id computes without a doc");
|
||||
let id = id_out.trim().to_string();
|
||||
let store_dir = dir.join("runs").join("blueprints");
|
||||
std::fs::create_dir_all(&store_dir).expect("create store dir");
|
||||
std::fs::write(store_dir.join(format!("{id}.json")), &envelope).expect("raw store write");
|
||||
|
||||
let consumer = format!(
|
||||
r#"[
|
||||
{{"op":"source","role":"price","kind":"F64"}},
|
||||
{{"op":"use","ref":{{"content_id":"{id}"}},"name":"gate"}},
|
||||
{{"op":"feed","role":"price","into":["gate.x"]}},
|
||||
{{"op":"expose","from":"gate.out","as":"bias"}}
|
||||
]"#
|
||||
);
|
||||
let (stderr, code) = run_code_in(&dir, &["graph", "build"], &consumer);
|
||||
assert_eq!(code, Some(1), "a doc-less fetched composite refuses -> exit 1: {stderr}");
|
||||
assert!(stderr.contains("op 1 (use \"gate\")"), "names the op by index and instance: {stderr}");
|
||||
assert!(stderr.contains(&id[..12]), "names the ref's id-prefix: {stderr}");
|
||||
assert!(stderr.contains("fails the description gate"), "names the rule: {stderr}");
|
||||
assert!(
|
||||
stderr.contains("has no description") && stderr.contains("re-register it with a \"doc\" op"),
|
||||
"carries the re-register hint: {stderr}"
|
||||
);
|
||||
assert!(stderr.contains("C29"), "cites the contract: {stderr}");
|
||||
}
|
||||
|
||||
/// The worked acceptance flow's last leg (spec §Concrete code shapes): a
|
||||
/// pattern registered with an open param splices under an instance, and
|
||||
/// `aura sweep --list-axes` on the CONSUMER shows the path-qualified bare
|
||||
/// (unbound) axis `graph.<instance>.<node>.<param>:<kind>` — the existing
|
||||
/// nested-composite param-prefix discipline, reached through `use` this
|
||||
/// time, not a Rust-authored nested composite.
|
||||
#[test]
|
||||
fn graph_build_use_end_to_end_axes() {
|
||||
let dir = temp_cwd("use-end-to-end-axes");
|
||||
let bp = build_envelope_in(&dir, OPEN_PATTERN_DOC, "pattern");
|
||||
let (reg_out, reg_err, reg_code) =
|
||||
run_in(&dir, &["graph", "register", bp.to_str().unwrap(), "--name", "smooth"]);
|
||||
assert_eq!(reg_code, Some(0), "register --name: {reg_out} {reg_err}");
|
||||
|
||||
let consumer = r#"[
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"use","ref":{"name":"smooth"},"name":"trend"},
|
||||
{"op":"feed","role":"price","into":["trend.x"]},
|
||||
{"op":"expose","from":"trend.out","as":"bias"}
|
||||
]"#;
|
||||
let consumer_path = dir.join("consumer.ops.json");
|
||||
std::fs::write(&consumer_path, consumer).expect("write consumer fixture");
|
||||
let build = std::process::Command::new(BIN)
|
||||
.args(["graph", "build"])
|
||||
.current_dir(&dir)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.and_then(|mut child| {
|
||||
use std::io::Write;
|
||||
child.stdin.take().unwrap().write_all(consumer.as_bytes())?;
|
||||
child.wait_with_output()
|
||||
})
|
||||
.expect("run aura graph build");
|
||||
assert!(build.status.success(), "consumer builds: {}", String::from_utf8_lossy(&build.stderr));
|
||||
let consumer_bp = dir.join("consumer.bp.json");
|
||||
std::fs::write(&consumer_bp, &build.stdout).expect("write consumer envelope");
|
||||
|
||||
let (axes_out, axes_err, axes_code) =
|
||||
run_in(&dir, &["sweep", consumer_bp.to_str().unwrap(), "--list-axes"]);
|
||||
assert_eq!(axes_code, Some(0), "list-axes: {axes_out} {axes_err}");
|
||||
assert_eq!(
|
||||
axes_out, "graph.trend.sma.length:I64\n",
|
||||
"the spliced instance's open param surfaces path-qualified, bare (unbound) form"
|
||||
);
|
||||
}
|
||||
|
||||
/// Open patterns (#317 §"Open patterns", acceptance criterion 6): an
|
||||
/// `input`-role op-script — no bound root role at all — builds cleanly:
|
||||
/// `finish()` no longer gates root-role boundness, only `compile`/bootstrap
|
||||
/// do. The agree flow's first half (register is exercised by the other
|
||||
/// `use`-seam tests above; this pins the build step alone).
|
||||
#[test]
|
||||
fn graph_build_accepts_an_open_input_pattern() {
|
||||
let (stdout, stderr, ok) = run(&["graph", "build"], OPEN_PATTERN_DOC);
|
||||
assert!(ok, "an input-role pattern builds: {stderr}");
|
||||
assert!(stdout.contains("\"input_roles\""), "carries the open root role: {stdout}");
|
||||
assert!(!stdout.contains("\"source\""), "the role carries no bound kind (open, #317): {stdout}");
|
||||
}
|
||||
|
||||
/// Open patterns, the OTHER half: running an open blueprint standalone
|
||||
/// still refuses — the runnability gate moved nowhere, only `finish()`
|
||||
/// stopped pre-empting it (#317). `aura run`'s `bias`-output arm re-roots
|
||||
/// through `wrap_r` (an existing, unrelated nesting mechanism unaffected by
|
||||
/// this cycle), so a bare-tap (no-`bias`) pattern is used here: its
|
||||
/// `run_measurement` path compiles the signal directly, hitting
|
||||
/// `CompileError::UnboundRootRole` at bootstrap — via the EXISTING `{e:?}`
|
||||
/// Debug rendering (a raw index, not a role name); a name mapping there is
|
||||
/// left for a follow-up (out of this cycle's scope, per the plan's own
|
||||
/// escape hatch), so this pins the existing form.
|
||||
#[test]
|
||||
fn running_an_open_blueprint_refuses_at_bootstrap() {
|
||||
let doc = r#"[
|
||||
{"op":"input","role":"price"},
|
||||
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||
{"op":"feed","role":"price","into":["fast.series"]},
|
||||
{"op":"tap","from":"fast.value","as":"fast_ma"}
|
||||
]"#;
|
||||
let dir = temp_cwd("open-pattern-run-refuses");
|
||||
let (build_out, _e, built) = run(&["graph", "build"], doc);
|
||||
assert!(built, "an open (Input) root role finishes without root-role boundness (#317)");
|
||||
let bp = dir.join("open.bp.json");
|
||||
std::fs::write(&bp, &build_out).expect("write built blueprint");
|
||||
let (stdout, stderr, code) = run_in(&dir, &["run", bp.to_str().unwrap()]);
|
||||
assert_eq!(code, Some(1), "an open blueprint refuses standalone at bootstrap, not finish: {stdout} {stderr}");
|
||||
assert!(stdout.is_empty(), "no report emitted on a bootstrap refusal: {stdout}");
|
||||
assert!(
|
||||
stderr.contains("UnboundRootRole"),
|
||||
"the runnability gate (compile), unchanged, names the fault: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user