diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index 1495277..a043a55 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -132,7 +132,11 @@ pub fn build_cmd() { std::process::exit(2); } match build_from_str(&doc) { - Ok(json) => println!("{json}"), + // `print!`, not `println!`: the canonical artifact is exactly the library + // `blueprint_to_json` bytes (the form #158 content-addresses) — a JSON value + // with no trailing newline. The CLI is a transport; it must not frame the + // canonical bytes (#164). + Ok(json) => print!("{json}"), Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); diff --git a/crates/aura-cli/tests/graph_construct.rs b/crates/aura-cli/tests/graph_construct.rs index 935d0f8..278b72e 100644 --- a/crates/aura-cli/tests/graph_construct.rs +++ b/crates/aura-cli/tests/graph_construct.rs @@ -58,6 +58,17 @@ fn graph_build_emits_blueprint_for_a_valid_document() { assert!(stdout.contains("\"SMA\""), "carries the nodes: {stdout}"); } +/// The canonical blueprint artifact carries no trailing newline: `aura graph build` +/// emits exactly the library `blueprint_to_json` bytes (the form #158 content-addresses), +/// not a display-framed line. Byte-canonical across the CLI and library surfaces (#164). +#[test] +fn graph_build_emit_is_byte_canonical_no_trailing_newline() { + let (stdout, _stderr, ok) = run(&["graph", "build"], SIGNAL_DOC); + assert!(ok, "exit success"); + assert!(!stdout.ends_with('\n'), "no trailing newline on the canonical emit: {stdout:?}"); + assert!(stdout.contains("\"format_version\":1"), "still the #155 envelope: {stdout}"); +} + #[test] fn graph_build_fails_fast_at_the_offending_op() { let doc = r#"[{"op":"add","type":"SMA","name":"fast"},{"op":"add","type":"Nope"}]"#; diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index 5048587..47ddd1d 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -1870,6 +1870,19 @@ the capitalized `ScalarKind` (`"F64"`). Worked example: `aura graph introspect --vocabulary | --node | --unwired`. (Surfacing this same grammar in `aura graph build --help` rides with the CLI-discoverability work, #159.) +**Canonical form (#164).** The canonical blueprint artifact is exactly the bytes +`blueprint_to_json` returns — a JSON value with **no trailing newline**. +`aura graph build` emits those bytes verbatim (it does not frame them with a display +newline via `println!`), so the CLI and library emit paths are **byte-identical** — a +prerequisite for content-addressed topology identity (#158, a hash over the canonical +form). The **canonical JSON is also the blueprint's equality / identity surface**: +`Composite` / `PrimitiveBuilder` deliberately carry **no in-memory `PartialEq` / +`Debug`** (the recipe holds a `build: Box` closure — non-comparable, +non-printable; equality could only be defined over the serialized *data*, of which +`blueprint_to_json` is already the single source — a second in-memory notion would be +a drift hazard against the very form #158 content-addresses). The loader stays lenient +(a trailing newline on input still parses, Tier-1 robustness). + --- ## Open architectural threads not yet resolved diff --git a/fieldtests/cycle-0088-construction-op-script/c0088_1_blueprint.out.json b/fieldtests/cycle-0088-construction-op-script/c0088_1_blueprint.out.json index a8c9d74..45b7b58 100644 --- a/fieldtests/cycle-0088-construction-op-script/c0088_1_blueprint.out.json +++ b/fieldtests/cycle-0088-construction-op-script/c0088_1_blueprint.out.json @@ -1 +1 @@ -{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}} +{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}} \ No newline at end of file diff --git a/fieldtests/cycle-0088-construction-op-script/c0088_2_blueprint.out.json b/fieldtests/cycle-0088-construction-op-script/c0088_2_blueprint.out.json index 9b65d65..55be04e 100644 --- a/fieldtests/cycle-0088-construction-op-script/c0088_2_blueprint.out.json +++ b/fieldtests/cycle-0088-construction-op-script/c0088_2_blueprint.out.json @@ -1 +1 @@ -{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}} +{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}} \ No newline at end of file