feat(0094b): content-id surface + Tier-1 stability — aura graph introspect --content-id (iter 2)

Completes #158 acceptance (acc 1 + acc 3) on top of iteration 1's reproduction:

- acc 1: `aura graph introspect --content-id` reads an op-script and prints the
  content id of its canonical blueprint — the SHA256 (hex) of the same
  blueprint_to_json bytes `graph build` emits. Extracted the single `content_id`
  primitive shared by this surface AND topology_hash, so the two command paths
  (this surface / hashing a `graph build` output) agree by construction. Verified
  live: both e2be81b2… for the SMA-cross op-script. Deterministic + distinguishes
  topologies; a malformed op-list fails cleanly (exit 2, no partial hash).
- acc 3: a Tier-1 optional field the blueprint does not use is tolerated by the
  loader (#156) and absent from the canonical omit-defaults form, so the content
  id is unchanged (content_id_is_stable_across_a_tolerated_tier1_field); plus
  content-id stability across the serialize -> reload -> re-serialize store
  round-trip (#164), the property reproduction rests on.

Finding (documented, filed forward): an op-script and the Rust `stage1_signal`
builder produce DIFFERENT canonical forms — the composite debug-name ("graph" vs
"stage1_signal"), a named vs unnamed Sub, and an unbound vs bound Bias.scale — so
they are different topologies by the byte definition and get different content
ids. Content-addressing keys on the canonical form, which currently includes the
composite debug-name (a non-load-bearing symbol, invariant 11). Whether the
content id should exclude the debug-name (a topology-canonical form distinct from
the byte-canonical form, which would also change the shipped cycle-0092
topology_hash values) is a forward design question, not this cycle.

Verified: full workspace suite green (51 suites); clippy -D warnings clean.

refs #158
This commit is contained in:
2026-07-01 03:05:37 +02:00
parent 008692c043
commit 717a0b70af
3 changed files with 111 additions and 7 deletions
+39
View File
@@ -212,3 +212,42 @@ fn graph_build_renders_role_kind_mismatch_at_finalize_by_identifier() {
assert!(!stderr.contains("RoleKindMismatch"),
"does not leak the Debug variant name: {stderr}");
}
/// Property (#158 acc 1, the content-id surface): `aura graph introspect --content-id`
/// prints the 64-hex SHA256 of the op-script's canonical blueprint — deterministic across
/// runs and distinct for a structurally different topology. It is the same hash `aura run`
/// stamps as `topology_hash` (both via the one `content_id` primitive over the canonical
/// `blueprint_to_json` bytes), so a data-authored topology has a stable, comparable content
/// id. (Note: an op-script and the Rust `stage1_signal` builder produce *different*
/// canonical forms — composite debug-name, an unbound `Bias.scale` — so they are different
/// topologies by the byte definition; content-addressing keys on the canonical form.)
#[test]
fn graph_introspect_content_id_is_deterministic_and_distinguishes() {
let (a, _e, ok) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
assert!(ok, "exit success");
let id = a.trim();
assert_eq!(id.len(), 64, "a 64-hex SHA256 content id: {id:?}");
assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "hex only: {id:?}");
// deterministic: the same op-script yields the same content id.
let (b, _e2, ok2) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
assert!(ok2, "exit success");
assert_eq!(id, b.trim(), "same op-script -> same content id");
// distinguishes: a structurally different topology (slow length 4 -> 5) differs.
let other = SIGNAL_DOC.replace("\"I64\":4", "\"I64\":5");
let (c, _e3, ok3) = run(&["graph", "introspect", "--content-id"], &other);
assert!(ok3, "exit success");
assert_ne!(id, c.trim(), "a different topology -> a different content id");
}
/// Property (negative): `aura graph introspect --content-id` on a malformed op-list exits
/// non-zero with the cause on stderr and prints no content id — it fails cleanly, never a
/// partial/garbage hash.
#[test]
fn graph_introspect_content_id_rejects_a_bad_document() {
let (stdout, stderr, ok) = run(&["graph", "introspect", "--content-id"], r#"[{"op":"add","type":"Nope"}]"#);
assert!(!ok, "non-zero exit on a bad op-list");
assert!(stdout.is_empty(), "no content id emitted on error: {stdout}");
assert!(stderr.contains("Nope"), "names the cause: {stderr}");
}