From b8b83cf1c9af12d3e41678b7ac5644743ef12a34 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 30 Jun 2026 15:57:15 +0200 Subject: [PATCH] feat(0091): byte-canonical blueprint emit; record the equality-surface decision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #164 — tighten the C24 "canonical, versioned" blueprint contract on two corners the milestone fieldtest surfaced. Both forks were derivable; resolved with rationale (recorded on #164). Fork 1 — canonical artifact carries no trailing newline. `blueprint_to_json` returns the JSON value with no trailing newline (647 bytes for the SMA-cross fixture); `aura graph build` had framed it with a `println!` (648). The library serializer is the single canonical source — the form content-addressed topology identity (#158) hashes — so the CLI is a transport that must not mutate the bytes: `build_cmd` now `print!`s the canonical bytes verbatim, making the CLI and library emit paths byte-identical. RED-first: the new E2E `graph_build_emit_is_byte_canonical_no_trailing_newline` pinned the trailing newline (FAIL), green after the one-line change. The two cycle-0088 `.out.json` goldens are re-recorded at 647 bytes. The loader stays lenient (a trailing newline on input still parses, Tier-1 robustness; the milestone fieldtest's `mt_4` tolerance check is unaffected). Fork 2 — the canonical JSON is the blueprint equality/identity surface; no second in-memory PartialEq/Debug is added. `Composite`/`BlueprintNode` cannot derive them: `PrimitiveBuilder` (aura-core node.rs) holds a `build: Box Box>` closure, neither comparable nor printable. Equality could only be defined over the serialized data, of which `blueprint_to_json` is already the single source; a separate in-memory equality notion would be a redundant second source of truth and a drift hazard against the form #158 hashes. Recorded the decision (discharges acceptance box 2's "or JSON-string is the intended equality surface" arm) in the C24 ledger; no code added for it. Verified: cargo test --workspace green (51 suites, incl. the new RED->green E2E); cargo clippy --workspace --all-targets -D warnings clean. closes #164 --- crates/aura-cli/src/graph_construct.rs | 6 +++++- crates/aura-cli/tests/graph_construct.rs | 11 +++++++++++ docs/design/INDEX.md | 13 +++++++++++++ .../c0088_1_blueprint.out.json | 2 +- .../c0088_2_blueprint.out.json | 2 +- 5 files changed, 31 insertions(+), 3 deletions(-) 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