From b3a2e9cf8d14e094610f62ee4d09a1e33f8b9d90 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 2 Jul 2026 19:39:27 +0200 Subject: [PATCH] spec: 0104 topology-identity hash (boss-signed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/specs/0104-topology-identity-hash.md | 184 ++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/specs/0104-topology-identity-hash.md diff --git a/docs/specs/0104-topology-identity-hash.md b/docs/specs/0104-topology-identity-hash.md new file mode 100644 index 0000000..55d036e --- /dev/null +++ b/docs/specs/0104-topology-identity-hash.md @@ -0,0 +1,184 @@ +# 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 **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) + +```console +# 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 +9f3c…e1 # byte id, path A +$ aura graph introspect --identity-id +5b77…a2 # topology id, path A +$ aura graph introspect --identity-id +5b77…a2 # SAME topology id +$ aura graph introspect --content-id +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: + +```rust +/// 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`: + +```rust + /// 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.