Files
Aura/docs/specs/0104-topology-identity-hash.md
T
Brummel b3a2e9cf8d spec: 0104 topology-identity hash (boss-signed)
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
2026-07-02 19:39:27 +02:00

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 as blueprint_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 the bound arrays' positions, kinds, and values — so param openness stays identity-bearing (a bound slot carries pos+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 helper content_id already uses (sha2 stays out of the engine, invariant 8). --content-id output is byte-unchanged.

Decisions carried from the #171 reconciliation comment (adversarially reviewed): the initial "redefine topology_hash" shape was refutedtopology_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

  1. 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 scale bound 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).
  2. CLI e2e (graph_construct.rs tests): an op-script-built doc and the Rust-built sma_signal fixture expressing the same topology → same --identity-id output, different --content-id output; --content-id alone byte-unchanged against its existing pin.
  3. Regression: full workspace suite green unchanged (reproduce, goldens, store untouched).

Acceptance criteria

  1. The op-script and Rust-builder twins share an identity id while their content ids differ (the #171 headline, proven e2e).
  2. An open blueprint and any bound instance of it never share an identity id; bound-value twins differing in one value never share one.
  3. --content-id, topology_hash, the blueprint store, and aura reproduce are byte-for-byte unaffected (existing tests + goldens green unchanged).
  4. The identity projection is documented as introspection-only this cycle (no manifest field, no store key) — the deferral recorded on #171.