Boss-signed via grounding-check PASS (fresh-context agent): all five load-bearing assumptions about current behaviour are ratified by named currently-green tests — the byte-canonical golden (signal_serializes_to_canonical_golden), the --content-id CLI contract (graph_introspect_content_id_is_deterministic_and_distinguishes and the bad-document refusal), the three topology_hash roles (introspect content id, blueprint-store round-trip key, reproduce fetch anchor — each with its own green test), the I64:4-vs-5 distinguishing property, and the bound-array openness encoding (bound slot textually present, open slot absent). Design carried from the #171 reconciliation (adversarially reviewed): the identity hash is an additive SIBLING of topology_hash — introspection-only this cycle, no manifest field, no store key; the byte-canonical form and every consumer of it stay byte-for-byte untouched. Reviewer note carried to planning: allowing --content-id and --identity-id together relaxes the current exactly-one flag dispatch in graph_construct.rs — the usage tests must be adjusted deliberately, not incidentally. refs #180, refs #171
8.2 KiB
The topology-identity hash — Design Spec
Date: 2026-07-02 Status: Draft — awaiting sign-off (boss run: grounding-check gate) Authors: orchestrator + Claude Cycle: 0104 — milestone "Project environment — the project-as-crate authoring loop" (#180); work item #171 (fork resolutions: the cycle-0104 reconciliation comment on #171)
Goal
Two blueprints expressing the same topology get the same identity hash
regardless of authoring path — an op-script-built graph and a Rust-builder
graph, or two builders differing only in debug names, become comparable by
id. The identity hash is additive: the byte-canonical topology_hash
keeps all three of its current roles (introspect content id, reproduction
store key, reproduce fetch anchor) byte-for-byte untouched.
Architecture
One new engine-side projection and one additive CLI flag:
blueprint_identity_json(aura-engine,blueprint_serde.rs) builds the same serialization document asblueprint_to_json, then normalizes every non-load-bearing debug symbol before serializing: the composite debug-name, primitive instance names, bound-param names, role names, and output re-export names are all replaced by"". Everything load-bearing survives untouched: node type ids, node order, edges (raw indices), role targets and order, output (node, field) pairs and order, and theboundarrays' positions, kinds, and values — so param openness stays identity-bearing (a bound slot carriespos+value, an open slot is textually absent) and an open blueprint never shares an id with any bound instance.aura graph introspect --identity-id(aura-cli) — a sibling of the existing--content-id: hashes the identity JSON through the same research-side SHA-256 helpercontent_idalready uses (sha2 stays out of the engine, invariant 8).--content-idoutput is byte-unchanged.
Decisions carried from the #171 reconciliation comment (adversarially
reviewed): the initial "redefine topology_hash" shape was refuted —
topology_hash keys the byte-exact reproduction store and reproduce
recovers bootstrap points by param-path name against the fetched
blueprint, so instance names are load-bearing on that path and a name-blind
store key would let cross-path name-twins clobber it. Hence: sibling id,
zero golden churn, reproduction provably unaffected. No manifest field and
no store keyed by the identity hash this cycle (no dedup consumer exists —
the deferred-until-a-consumer discipline).
Concrete code shapes
The user-facing comparison (the acceptance-criterion evidence)
# two authoring paths, one topology: the op-script (composite named "graph")
# and the Rust builder (named "sma_signal") — whatever input form
# `--content-id` consumes today, `--identity-id` consumes identically
$ aura graph introspect --content-id <op-script path's input>
9f3c…e1 # byte id, path A
$ aura graph introspect --identity-id <op-script path's input>
5b77…a2 # topology id, path A
$ aura graph introspect --identity-id <rust-builder path's input>
5b77…a2 # SAME topology id
$ aura graph introspect --content-id <rust-builder path's input>
41d0…07 # byte ids still differ
(The hex values are illustrative; the input form mirrors --content-id's
existing contract exactly.)
Engine: the identity projection (secondary, before → after shape)
blueprint_to_json today walks the composite into a BlueprintDoc DTO and
serializes it. The identity projection reuses that walk and normalizes the
DTO in place:
/// Identity-canonical form: the byte-canonical document with every
/// non-load-bearing debug symbol blanked (invariant 11 / C23). Feeds the
/// research-side identity hash; NOT a load path and NOT the reproduction
/// store's byte form.
pub fn blueprint_identity_json(bp: &Composite) -> String {
let mut doc = build_doc(bp); // the existing blueprint_to_json walk
strip_debug_symbols(&mut doc.blueprint);
serialize_doc(&doc) // the existing serializer
}
fn strip_debug_symbols(b: &mut BlueprintData) {
b.name = String::new();
for node in &mut b.nodes {
match node {
NodeData::Primitive(p) => {
p.name = None;
for bound in &mut p.bound {
bound.name = String::new(); // pos/kind/value survive
}
}
NodeData::Composite(c) => strip_debug_symbols(c),
}
}
for role in &mut b.input_roles {
role.name = String::new(); // targets + order survive
}
for out in &mut b.output {
out.name = String::new(); // (node, field) + order survive
}
}
(Field/type names above follow the existing blueprint_serde.rs DTOs; the
planner pins exact identifiers. If build_doc/serialize_doc are not yet
factored out of blueprint_to_json, factoring them is part of this cycle —
behaviour-preserving for the byte path, guarded by the existing canonical
golden.)
CLI: the sibling flag (secondary)
GraphIntrospectCmd gains one flag beside --content-id:
/// Print the topology-identity id (debug names stripped) and exit.
#[arg(long)]
identity_id: bool,
Its branch mirrors the --content-id branch byte-for-byte, substituting
blueprint_identity_json for the canonical bytes before the same
content_id SHA-256 helper.
Components
| Component | Crate | New/changed |
|---|---|---|
blueprint_identity_json + strip_debug_symbols (+ doc-walk factoring if needed) |
aura-engine | new fn(s) |
--identity-id flag + branch |
aura-cli (graph_construct.rs) |
additive |
| Property tests (engine) + e2e (CLI) | both | new tests |
Untouched by design: topology_hash stamping, put_blueprint/
get_blueprint, aura reproduce, blueprint_to_json bytes, every golden.
Data flow
Composite ──build_doc──► BlueprintDoc ──serialize──► byte-canonical JSON ──► content_id / topology_hash / store
└─clone──► strip_debug_symbols ──serialize──► identity JSON ──► identity id (introspect only)
Error handling
No new failure modes: --identity-id shares --content-id's input path
(stdin/file op-list or blueprint doc) and its existing refusal contract;
combining --content-id --identity-id prints both, one per line, in that
order (flags are not mutually exclusive).
Testing strategy
- Engine property tests (
blueprint_serde.rs):- renamed twins: two builders identical up to
.named(...)and composite name → equal identity JSON, unequal canonical JSON; - openness: a blueprint with
scalebound vs unbound → unequal identity JSON (boundness survives); - bound values: I64:4 vs I64:5 → unequal identity JSON (the existing distinguishing property, now on the identity form);
- wiring: swapping two edges' targets → unequal identity JSON (order and indices load-bearing);
- the existing byte-canonical golden stays byte-identical (the factoring is behaviour-preserving).
- renamed twins: two builders identical up to
- CLI e2e (
graph_construct.rstests): an op-script-built doc and the Rust-builtsma_signalfixture expressing the same topology → same--identity-idoutput, different--content-idoutput;--content-idalone byte-unchanged against its existing pin. - Regression: full workspace suite green unchanged (reproduce, goldens, store untouched).
Acceptance criteria
- The op-script and Rust-builder twins share an identity id while their content ids differ (the #171 headline, proven e2e).
- An open blueprint and any bound instance of it never share an identity id; bound-value twins differing in one value never share one.
--content-id,topology_hash, the blueprint store, andaura reproduceare byte-for-byte unaffected (existing tests + goldens green unchanged).- The identity projection is documented as introspection-only this cycle (no manifest field, no store key) — the deferral recorded on #171.