diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index 09e35d9..c9e585d 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -370,8 +370,9 @@ fn resolve_blueprint_text(target: &str, env: &crate::project::Env) -> Result Result { use std::fmt::Write as _; let text = resolve_blueprint_text(target, env)?; - let composite = - blueprint_from_json(&text, &|t| env.resolve(t)).map_err(|e| blueprint_load_prose(&e))?; + // Shape-discriminated like file-mode --content-id: an op-script (array) + // builds through the one-build tail, an envelope (object) loads (#202). + let composite = composite_from_any(&text, env)?; let mut out = String::new(); for p in composite.param_space() { let _ = writeln!(out, "{}:{:?}", p.name, p.kind); @@ -396,8 +397,9 @@ pub fn register_cmd(file: &Path, env: &crate::project::Env) { fn register_blueprint(file: &Path, env: &crate::project::Env) -> Result { let text = std::fs::read_to_string(file) .map_err(|e| format!("cannot read {}: {e}", file.display()))?; - let composite = - blueprint_from_json(&text, &|t| env.resolve(t)).map_err(|e| blueprint_load_prose(&e))?; + // Shape-discriminated like file-mode --content-id (#202): either shape + // canonicalizes to the envelope form, so the stored id is shape-invariant. + let composite = composite_from_any(&text, env)?; let canonical = blueprint_to_json(&composite).map_err(|e| format!("serialize error: {e:?}"))?; let id = crate::content_id(&canonical); let registry = env.registry(); diff --git a/crates/aura-cli/tests/graph_construct.rs b/crates/aura-cli/tests/graph_construct.rs index 4ecf189..001217a 100644 --- a/crates/aura-cli/tests/graph_construct.rs +++ b/crates/aura-cli/tests/graph_construct.rs @@ -605,3 +605,64 @@ fn graph_register_rejects_an_unknown_node_type_with_prose() { "names the unknown node type: {stderr}" ); } + +/// Property (#202 headline, the on-ramp is shape-invariant): `aura graph register` +/// accepts an op-script document (a JSON array) exactly as it accepts a #155 +/// blueprint envelope (a JSON object) — it shape-discriminates the array form, +/// builds it through the same one-build tail, and stores it under the SAME content +/// id it stores that op-script's `graph build` envelope under. The content id is +/// shape-invariant (an op-script and its built envelope hash identically), so the +/// two on-ramp shapes must agree on the store key; `register` must not reject the +/// op-script with the raw serde `invalid type: map, expected u32` message the +/// direct-`blueprint_from_json` path currently leaks. +#[test] +fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() { + let dir = temp_cwd("register-op-script"); + // The op-script form (a JSON array of ops), written to a file. + let op_script = dir.join("op-script.json"); + std::fs::write(&op_script, SIGNAL_DOC).expect("write op-script fixture"); + // Its `graph build` envelope (a JSON object), the shape `register` already accepts. + let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC); + assert!(built, "graph build produces the envelope"); + let envelope = dir.join("envelope.json"); + std::fs::write(&envelope, &envelope_bytes).expect("write envelope fixture"); + + // The envelope shape registers today (green sibling); the op-script must too. + let (env_out, env_err, env_code) = + run_in(&dir, &["graph", "register", envelope.to_str().unwrap()]); + assert_eq!(env_code, Some(0), "register envelope: {env_out} {env_err}"); + let (op_out, op_err, op_code) = + run_in(&dir, &["graph", "register", op_script.to_str().unwrap()]); + assert_eq!(op_code, Some(0), "register op-script exits 0: {op_out} {op_err}"); + + assert_eq!( + registered_id(&op_out), + registered_id(&env_out), + "op-script and its built envelope register to the same content id" + ); +} + +/// Property (#202, the same on-ramp seam at the sibling verb): `aura graph +/// introspect --params` accepts an op-script exactly as it accepts a blueprint +/// envelope — the raw axis namespace it prints for the op-script equals the one it +/// prints for that op-script's built envelope, both exit 0. Currently `--params` +/// rejects the op-script with the same raw serde `invalid type: map, expected u32` +/// message `register` does (both route through the direct `blueprint_from_json`). +#[test] +fn graph_params_accepts_an_op_script_matching_its_envelope() { + let dir = temp_cwd("params-op-script"); + let op_script = dir.join("op-script.json"); + std::fs::write(&op_script, SIGNAL_DOC).expect("write op-script fixture"); + let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC); + assert!(built, "graph build produces the envelope"); + let envelope = dir.join("envelope.json"); + std::fs::write(&envelope, &envelope_bytes).expect("write envelope fixture"); + + let (env_out, env_err, env_code) = + run_in(&dir, &["graph", "introspect", "--params", envelope.to_str().unwrap()]); + assert_eq!(env_code, Some(0), "params on envelope: {env_out} {env_err}"); + let (op_out, op_err, op_code) = + run_in(&dir, &["graph", "introspect", "--params", op_script.to_str().unwrap()]); + assert_eq!(op_code, Some(0), "params on op-script exits 0: {op_out} {op_err}"); + assert_eq!(op_out, env_out, "op-script and envelope agree on the raw axis namespace"); +}